diff --git a/.ha_run.lock b/.ha_run.lock index 143fa81f..bac30485 100644 --- a/.ha_run.lock +++ b/.ha_run.lock @@ -1 +1 @@ -{"pid": 72, "version": 1, "ha_version": "2026.10.0", "start_ts": 1791476271.5855424} \ No newline at end of file +{"pid": 71, "version": 1, "ha_version": "2026.10.0", "start_ts": 1791546769.0837624} \ No newline at end of file diff --git a/.storage/lovelace_resources b/.storage/lovelace_resources index af253360..339dd7c8 100644 --- a/.storage/lovelace_resources +++ b/.storage/lovelace_resources @@ -41,7 +41,7 @@ }, { "id": "0b28e2d01d0d4033b2f0a339cc7d86c1", - "url": "/ha_washdata/ha-washdata-card.js?v=cad53dea8b", + "url": "/ha_washdata/ha-washdata-card.js?v=5d52b92f80", "type": "module" }, { @@ -81,117 +81,117 @@ }, { "id": "88b90f2b1d6b45f0b44bee786ee1a525", - "url": "/taskmate/taskmate-attr-resolver.js?v=6.1.0", + "url": "/taskmate/taskmate-attr-resolver.js?v=6.2.0", "type": "module" }, { "id": "0796c92931dd458e81dc9c2677248274", - "url": "/taskmate/taskmate-localize.js?v=6.1.0", + "url": "/taskmate/taskmate-localize.js?v=6.2.0", "type": "module" }, { "id": "d2e281920ac7409583eafff1f1db69bd", - "url": "/taskmate/taskmate-design.js?v=6.1.0", + "url": "/taskmate/taskmate-design.js?v=6.2.0", "type": "module" }, { "id": "9eae9632f0734480be5edd784b030673", - "url": "/taskmate/taskmate-badges-card.js?v=6.1.0", + "url": "/taskmate/taskmate-badges-card.js?v=6.2.0", "type": "module" }, { "id": "5df49cd8a4eb420497b5df22cb31362c", - "url": "/taskmate/taskmate-child-card.js?v=6.1.0", + "url": "/taskmate/taskmate-child-card.js?v=6.2.0", "type": "module" }, { "id": "b44c87d0d74f468cb96c5eabc340f9ee", - "url": "/taskmate/taskmate-rewards-card.js?v=6.1.0", + "url": "/taskmate/taskmate-rewards-card.js?v=6.2.0", "type": "module" }, { "id": "787a96f9c30548708165a48457c99feb", - "url": "/taskmate/taskmate-approvals-card.js?v=6.1.0", + "url": "/taskmate/taskmate-approvals-card.js?v=6.2.0", "type": "module" }, { "id": "84031597de424ad68e7c2bc66d06b72e", - "url": "/taskmate/taskmate-points-card.js?v=6.1.0", + "url": "/taskmate/taskmate-points-card.js?v=6.2.0", "type": "module" }, { "id": "02613225844746b699bfbfac9642eca2", - "url": "/taskmate/taskmate-reorder-card.js?v=6.1.0", + "url": "/taskmate/taskmate-reorder-card.js?v=6.2.0", "type": "module" }, { "id": "7a06a07e17ba4b31b712bc91b77360b4", - "url": "/taskmate/taskmate-overview-card.js?v=6.1.0", + "url": "/taskmate/taskmate-overview-card.js?v=6.2.0", "type": "module" }, { "id": "08b42b53b33b4fa88db932455cf8d5aa", - "url": "/taskmate/taskmate-activity-card.js?v=6.1.0", + "url": "/taskmate/taskmate-activity-card.js?v=6.2.0", "type": "module" }, { "id": "15907eff51784acdba88ff694b574509", - "url": "/taskmate/taskmate-streak-card.js?v=6.1.0", + "url": "/taskmate/taskmate-streak-card.js?v=6.2.0", "type": "module" }, { "id": "1bd6bb5c0a6d4f0d916e3d82cb7fac39", - "url": "/taskmate/taskmate-weekly-card.js?v=6.1.0", + "url": "/taskmate/taskmate-weekly-card.js?v=6.2.0", "type": "module" }, { "id": "289ce5943f5a4972a593697db4c96ece", - "url": "/taskmate/taskmate-graph-card.js?v=6.1.0", + "url": "/taskmate/taskmate-graph-card.js?v=6.2.0", "type": "module" }, { "id": "d2904acee4c74a398dcc7b996c23e7e7", - "url": "/taskmate/taskmate-reward-progress-card.js?v=6.1.0", + "url": "/taskmate/taskmate-reward-progress-card.js?v=6.2.0", "type": "module" }, { "id": "46d378736fd54a26be253f2dfb27202e", - "url": "/taskmate/taskmate-leaderboard-card.js?v=6.1.0", + "url": "/taskmate/taskmate-leaderboard-card.js?v=6.2.0", "type": "module" }, { "id": "db11ae36457842fa826c7991e1d9c6f4", - "url": "/taskmate/taskmate-parent-dashboard-card.js?v=6.1.0", + "url": "/taskmate/taskmate-parent-dashboard-card.js?v=6.2.0", "type": "module" }, { "id": "ce85f301b59a412e9abfe5ffd51cb9a2", - "url": "/taskmate/taskmate-penalties-card.js?v=6.1.0", + "url": "/taskmate/taskmate-penalties-card.js?v=6.2.0", "type": "module" }, { "id": "1959e1629de2422daa8adaf87069c288", - "url": "/taskmate/taskmate-bonuses-card.js?v=6.1.0", + "url": "/taskmate/taskmate-bonuses-card.js?v=6.2.0", "type": "module" }, { "id": "2548524b269c4959bc7e0e967df943af", - "url": "/taskmate/taskmate-points-display-card.js?v=6.1.0", + "url": "/taskmate/taskmate-points-display-card.js?v=6.2.0", "type": "module" }, { "id": "36ada49368db4187a283fbbbef45454f", - "url": "/taskmate/taskmate-calendar-card.js?v=6.1.0", + "url": "/taskmate/taskmate-calendar-card.js?v=6.2.0", "type": "module" }, { "id": "67c32687acd2497b90030ef141f4b6db", - "url": "/taskmate/taskmate-photo-gallery-card.js?v=6.1.0", + "url": "/taskmate/taskmate-photo-gallery-card.js?v=6.2.0", "type": "module" }, { "id": "6f0c8348a91f4b74a9b846ab3f62621f", - "url": "/taskmate/taskmate-family-goal-card.js?v=6.1.0", + "url": "/taskmate/taskmate-family-goal-card.js?v=6.2.0", "type": "module" }, { @@ -276,7 +276,7 @@ }, { "id": "73dbd2de03de4530b451a55bfe6c46cf", - "url": "/taskmate/taskmate-routine-card.js?v=6.1.0", + "url": "/taskmate/taskmate-routine-card.js?v=6.2.0", "type": "module" }, { @@ -291,47 +291,47 @@ }, { "id": "1a4939ce1d2b47f89502176d3c5ac6e7", - "url": "/taskmate/taskmate-sounds.js?v=6.1.0", + "url": "/taskmate/taskmate-sounds.js?v=6.2.0", "type": "module" }, { "id": "f4b0f22e5ce74727afbde9d7168a141e", - "url": "/ha_creality_ws/k_printer_card.js?v=efead36a13", + "url": "/ha_creality_ws/k_printer_card.js?v=fd0786eb1e", "type": "module" }, { "id": "5e4ca4c959f04b4c9b1dd8da0c4cb3b7", - "url": "/ha_creality_ws/k_cfs_card.js?v=71e833984f", + "url": "/ha_creality_ws/k_cfs_card.js?v=04dd7dff75", "type": "module" }, { "id": "2cc757922cd142b69ce8844afc0e8410", - "url": "/taskmate/taskmate-wishlist-card.js?v=6.1.0", + "url": "/taskmate/taskmate-wishlist-card.js?v=6.2.0", "type": "module" }, { "id": "107319877a72413fbbea40b7f8adb06c", - "url": "/taskmate/taskmate-kiosk-card.js?v=6.1.0", + "url": "/taskmate/taskmate-kiosk-card.js?v=6.2.0", "type": "module" }, { "id": "5a2899c29e9046c78f060580ba113d9f", - "url": "/taskmate/taskmate-bounty-card.js?v=6.1.0", + "url": "/taskmate/taskmate-bounty-card.js?v=6.2.0", "type": "module" }, { "id": "a1aae7e334ba4c2d8dd3849178f780f6", - "url": "/taskmate/taskmate-auction-card.js?v=6.1.0", + "url": "/taskmate/taskmate-auction-card.js?v=6.2.0", "type": "module" }, { "id": "464b73198b7d4fe480c09bbf386a4e5d", - "url": "/taskmate/taskmate-recap-card.js?v=6.1.0", + "url": "/taskmate/taskmate-recap-card.js?v=6.2.0", "type": "module" }, { "id": "35f1f324116d4b589f3087065519360b", - "url": "/taskmate/taskmate-chore-board-card.js?v=6.1.0", + "url": "/taskmate/taskmate-chore-board-card.js?v=6.2.0", "type": "module" }, { @@ -340,18 +340,18 @@ "type": "module" }, { - "id": "0fe51f5857344078bf96e51e13015ab3", - "url": "/album_slideshow_static/album-slideshow-card.js?v=1.13.0", + "id": "14283fbeb8494a2d970378245aaa012f", + "url": "/hacsfiles/Filament-Card/filament-card.js?hacstag=1290882685050", "type": "module" }, { - "id": "d6cb6831dea04e7d9e127fde97503e21", + "id": "2f9c3d3bdf2e46c2b9607a28d505d4a4", "url": "/climate_scheduler/static/climate-scheduler-card.js?v=1.15.1", "type": "module" }, { - "id": "14283fbeb8494a2d970378245aaa012f", - "url": "/hacsfiles/Filament-Card/filament-card.js?hacstag=1290882685050", + "id": "147a07760eec4fe69bd509064f81e3cd", + "url": "/album_slideshow_static/album-slideshow-card.js?v=1.14.0", "type": "module" } ] diff --git a/custom_components/album_slideshow/camera.py b/custom_components/album_slideshow/camera.py index c1bb6f36..c58d16a8 100644 --- a/custom_components/album_slideshow/camera.py +++ b/custom_components/album_slideshow/camera.py @@ -11,6 +11,7 @@ from pathlib import Path from typing import Any import async_timeout +from aiohttp import ClientResponseError from PIL import Image from homeassistant.components.camera import Camera @@ -1581,12 +1582,26 @@ class AlbumSlideshowCamera(Camera): return dict(headers) return None + def _image_request_params(self, url: str) -> dict[str, str] | None: + """Session query params added at fetch time only (UGREEN ``ugk``).""" + params = getattr(self.coordinator, "image_request_params", None) + if params and isinstance(url, str) and url.startswith("http"): + return dict(params) + return None + async def _http_get(self, url: str) -> bytes | None: """Fetch one remote image with validation and a hard timeout.""" - session = async_get_clientsession(self.hass) + session = async_get_clientsession( + self.hass, + verify_ssl=getattr(self.coordinator, "image_request_verify_ssl", True), + ) try: async with async_timeout.timeout(30): - async with session.get(url, headers=self._image_request_headers(url)) as resp: + async with session.get( + url, + headers=self._image_request_headers(url), + params=self._image_request_params(url), + ) as resp: resp.raise_for_status() content_type = resp.headers.get("Content-Type", "") @@ -1636,6 +1651,12 @@ class AlbumSlideshowCamera(Camera): return None chunks.append(chunk) return b"".join(chunks) + except ClientResponseError as err: + # str(err) includes the full request URL, fetch-time params and all. + _LOGGER.warning( + "Album Slideshow: failed to fetch image %s: HTTP %s", url, err.status + ) + return None except Exception as err: _LOGGER.warning("Album Slideshow: failed to fetch image: %s", err) return None diff --git a/custom_components/album_slideshow/config_flow.py b/custom_components/album_slideshow/config_flow.py index 636a3ba8..bef1eec3 100644 --- a/custom_components/album_slideshow/config_flow.py +++ b/custom_components/album_slideshow/config_flow.py @@ -99,6 +99,13 @@ from .const import ( DEFAULT_ENTE_IMAGE_SIZE, ENTE_IMAGE_FULL, ENTE_IMAGE_PREVIEW, + CONF_UGREEN_URL, + CONF_UGREEN_USERNAME, + CONF_UGREEN_PASSWORD, + CONF_UGREEN_ALBUM_UUID, + CONF_UGREEN_ALBUM_TYPE, + CONF_UGREEN_ALBUM_NAME, + CONF_UGREEN_VERIFY_SSL, DEFAULT_REVERSE_GEOCODE, PROVIDER_GOOGLE_SHARED, PROVIDER_LOCAL_FOLDER, @@ -109,6 +116,7 @@ from .const import ( PROVIDER_SYNOLOGY, PROVIDER_NEXTCLOUD, PROVIDER_ENTE, + PROVIDER_UGREEN, DEFAULT_RECURSIVE, ) @@ -374,6 +382,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._syn_places: dict[str, str] = {} self._syn_tags: dict[str, str] = {} self._syn_subjects: dict[str, str] = {} + # UGREEN flow state carried between steps. + self._ugr_url: str | None = None + self._ugr_username: str | None = None + self._ugr_password: str | None = None + self._ugr_verify_ssl: bool = True + self._ugr_albums: list[dict[str, Any]] = [] @staticmethod @callback @@ -382,8 +396,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> config_entries.OptionsFlow: """Return the options flow handler. - Local-folder, Nextcloud and Ente entries expose the reverse-geocode - toggle; Immich entries reopen the albums/people/favorites picker. + Local-folder, Nextcloud, Ente and UGREEN entries expose the + reverse-geocode toggle; Immich entries reopen the + albums/people/favorites picker. Other providers get a no-op handler so that the "Configure" button doesn't appear empty in the UI. @@ -397,6 +412,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): PROVIDER_LOCAL_FOLDER, PROVIDER_NEXTCLOUD, PROVIDER_ENTE, + PROVIDER_UGREEN, ): return LocalFolderOptionsFlow() if config_entry.data.get(CONF_PROVIDER) == PROVIDER_IMMICH: @@ -424,6 +440,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_nextcloud() if self._provider == PROVIDER_ENTE: return await self.async_step_ente() + if self._provider == PROVIDER_UGREEN: + return await self.async_step_ugreen() return await self.async_step_google_shared() schema = vol.Schema( @@ -437,6 +455,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): PROVIDER_SYNOLOGY: "Synology Photos (direct API, full metadata)", PROVIDER_NEXTCLOUD: "Nextcloud (WebDAV folder or public album link)", PROVIDER_ENTE: "Ente Photos (public album link)", + PROVIDER_UGREEN: "UGREEN NAS (UGOS Photos, experimental)", PROVIDER_MEDIA_SOURCE: "Media Source (any source, no metadata)", }) } @@ -1116,6 +1135,123 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="synology_select", data_schema=vol.Schema(fields), errors=errors ) + async def async_step_ugreen( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Collect the UGREEN NAS URL + account and log in. + + The album is picked from a live dropdown in the next step rather + than typed in, so logging in here (and listing albums) doubles as + credential validation. + """ + errors: dict[str, str] = {} + + if user_input is not None: + url = user_input[CONF_UGREEN_URL].strip() + username = user_input[CONF_UGREEN_USERNAME].strip() + password = user_input.get(CONF_UGREEN_PASSWORD) or "" + verify_ssl = bool(user_input.get(CONF_UGREEN_VERIFY_SSL, True)) + + from . import ugreen as ugr_api + + client = ugr_api.UGreenClient( + self.hass, url, username, password, verify_ssl=verify_ssl + ) + albums: list[dict[str, Any]] = [] + try: + await client.async_login() + albums = await client.async_list_albums() + except ugr_api.UGreenAuthError as err: + _LOGGER.warning( + "UGREEN login failed for %s: %s", url, _describe_error(err) + ) + errors["base"] = "ugreen_cannot_connect" + except ugr_api.UGreenApiError as err: + _LOGGER.warning( + "UGREEN album listing failed for %s: %s", url, _describe_error(err) + ) + errors["base"] = "ugreen_cannot_connect" + + named_albums = [ + a for a in albums if isinstance(a, dict) and a.get("album_name") + ] + if not errors and not named_albums: + errors["base"] = "ugreen_no_albums" + + if not errors: + self._ugr_url = client.base_url + self._ugr_username = username + self._ugr_password = password + self._ugr_verify_ssl = verify_ssl + self._ugr_albums = named_albums + return await self.async_step_ugreen_select() + + schema = vol.Schema( + { + vol.Required(CONF_UGREEN_URL): str, + vol.Required(CONF_UGREEN_USERNAME): str, + vol.Required(CONF_UGREEN_PASSWORD): selector.TextSelector( + selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD) + ), + vol.Optional(CONF_UGREEN_VERIFY_SSL, default=True): bool, + } + ) + return self.async_show_form(step_id="ugreen", data_schema=schema, errors=errors) + + async def async_step_ugreen_select( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Pick the UGOS Photos album from a live, searchable dropdown.""" + from . import ugreen as ugr_api + + # Keyed by uuid since album names aren't guaranteed unique. + albums_by_uuid = {a["album_uuid"]: a for a in self._ugr_albums} + + if user_input is not None: + album_uuid = user_input[CONF_UGREEN_ALBUM_UUID] + album = albums_by_uuid[album_uuid] + unique = ( + f"{DOMAIN}:{PROVIDER_UGREEN}:{self._ugr_url}:" + f"{self._ugr_username}:{album_uuid}" + ) + await self.async_set_unique_id(unique) + self._abort_if_unique_id_configured() + data = { + CONF_PROVIDER: PROVIDER_UGREEN, + CONF_UGREEN_URL: self._ugr_url, + CONF_UGREEN_USERNAME: self._ugr_username, + CONF_UGREEN_PASSWORD: self._ugr_password, + CONF_UGREEN_VERIFY_SSL: self._ugr_verify_ssl, + CONF_UGREEN_ALBUM_UUID: album_uuid, + CONF_UGREEN_ALBUM_TYPE: album.get( + "album_type", ugr_api.ALBUM_TYPE_REGULAR + ), + CONF_UGREEN_ALBUM_NAME: album["album_name"], + } + return self.async_create_entry(title=album["album_name"], data=data) + + # A searchable dropdown (rather than a plain vol.In) keeps this + # usable for accounts with a large number of albums. + options = [ + selector.SelectOptionDict( + value=a["album_uuid"], + label=f"{a['album_name']} ({ugr_api.describe_album_type(a.get('album_type'))})", + ) + for a in self._ugr_albums + ] + schema = vol.Schema( + { + vol.Required(CONF_UGREEN_ALBUM_UUID): selector.SelectSelector( + selector.SelectSelectorConfig( + options=options, + mode=selector.SelectSelectorMode.DROPDOWN, + custom_value=False, + ) + ) + } + ) + return self.async_show_form(step_id="ugreen_select", data_schema=schema) + async def async_step_nextcloud( self, user_input: dict[str, Any] | None = None ) -> FlowResult: diff --git a/custom_components/album_slideshow/const.py b/custom_components/album_slideshow/const.py index 2cf3b306..c910d394 100644 --- a/custom_components/album_slideshow/const.py +++ b/custom_components/album_slideshow/const.py @@ -75,6 +75,7 @@ PROVIDER_ICLOUD = "icloud" PROVIDER_SYNOLOGY = "synology" PROVIDER_NEXTCLOUD = "nextcloud" PROVIDER_ENTE = "ente" +PROVIDER_UGREEN = "ugreen" # Providers whose coordinator runs a background enrichment pass: per-photo # metadata reads, reverse-geocoding, or both. These are the entries that get @@ -85,6 +86,7 @@ ENRICHING_PROVIDERS = ( PROVIDER_IMMICH, PROVIDER_NEXTCLOUD, PROVIDER_ENTE, + PROVIDER_UGREEN, ) # Nextcloud provider - two auth modes against the same PROVIDER_NEXTCLOUD id: @@ -239,6 +241,23 @@ DEFAULT_PHOTOPRISM_IMAGE_SIZE = PHOTOPRISM_IMAGE_PREVIEW PHOTOPRISM_SELECTION_COMPOSITE = "composite" +# UGREEN NAS (UGOS Photos) provider. Talks to the undocumented UGOS Photos +# web API (see ``ugreen.py``); login is username + password, with the +# password RSA-encrypted before it ever leaves HA, same as the UGOS web app. +# TLS certificates are verified unless the user turns that off for a NAS with +# a self-signed certificate. +CONF_UGREEN_URL = "ugreen_url" +CONF_UGREEN_USERNAME = "ugreen_username" +CONF_UGREEN_PASSWORD = "ugreen_password" +CONF_UGREEN_VERIFY_SSL = "ugreen_verify_ssl" +# The album is stored by ``album_uuid`` + ``album_type`` (what the API needs), +# so renaming it in UGOS Photos does not break the slideshow. The name is kept +# for display only. +CONF_UGREEN_ALBUM_UUID = "ugreen_album_uuid" +CONF_UGREEN_ALBUM_TYPE = "ugreen_album_type" +CONF_UGREEN_ALBUM_NAME = "ugreen_album_name" + + FILL_COVER = "cover" FILL_CONTAIN = "contain" FILL_BLUR = "blur" diff --git a/custom_components/album_slideshow/coordinator.py b/custom_components/album_slideshow/coordinator.py index edddff6e..f659bff5 100644 --- a/custom_components/album_slideshow/coordinator.py +++ b/custom_components/album_slideshow/coordinator.py @@ -89,6 +89,12 @@ from .const import ( CONF_ENTE_IMAGE_SIZE, DEFAULT_ENTE_IMAGE_SIZE, ENTE_IMAGE_PREVIEW, + CONF_UGREEN_URL, + CONF_UGREEN_USERNAME, + CONF_UGREEN_PASSWORD, + CONF_UGREEN_ALBUM_UUID, + CONF_UGREEN_ALBUM_TYPE, + CONF_UGREEN_VERIFY_SSL, DEFAULT_REVERSE_GEOCODE, DOMAIN, ENRICHING_PROVIDERS, @@ -101,6 +107,7 @@ from .const import ( PROVIDER_SYNOLOGY, PROVIDER_NEXTCLOUD, PROVIDER_ENTE, + PROVIDER_UGREEN, ) from .store import SlideshowStore @@ -1094,9 +1101,20 @@ class AlbumCoordinator(DataUpdateCoordinator): # Extra headers the camera must send when fetching image bytes # (Immich API key). Empty for providers that need no auth. self.image_request_headers: dict[str, str] = {} + # Query params added only at fetch time (UGREEN ``ugk``), so stored + # URLs, attributes and logs never carry them. + self.image_request_params: dict[str, str] = {} + # False when the entry opted out of TLS certificate checks (UGREEN NAS + # with a self-signed certificate). + self.image_request_verify_ssl: bool = True # Ente only: file id -> {key, header, thumbnail}, the material needed # to decrypt each image. Rebuilt on every album refresh. self._ente_fetch_meta: dict[str, dict[str, Any]] = {} + # UGREEN only: logged-in client + selected album, reused by the + # background enrichment pass instead of logging in again per photo. + self._ugreen_client: Any = None + self._ugreen_album_uuid: str | None = None + self._ugreen_album_type: int = 1 # Persist the most recent successful album fetch so that a transient # network/Google failure doesn't blank the slideshow on restart. @@ -1172,6 +1190,8 @@ class AlbumCoordinator(DataUpdateCoordinator): data = await self._update_nextcloud() elif self.provider == PROVIDER_ENTE: data = await self._update_ente() + elif self.provider == PROVIDER_UGREEN: + data = await self._update_ugreen() else: raise UpdateFailed(f"Unsupported provider: {self.provider}") except UpdateFailed: @@ -1993,6 +2013,102 @@ class AlbumCoordinator(DataUpdateCoordinator): "items": items, } + async def _update_ugreen(self) -> dict[str, Any]: + """Fetch photos from a UGREEN NAS UGOS Photos album. + + Logs in fresh on every refresh, like the Synology provider. The + album is addressed by its stored uuid, so renaming it in UGOS Photos + needs no reconfiguration. + """ + from . import ugreen as ugr_api + + url = self.entry.data.get(CONF_UGREEN_URL) + username = self.entry.data.get(CONF_UGREEN_USERNAME) + password = self.entry.data.get(CONF_UGREEN_PASSWORD) + album_uuid = self.entry.data.get(CONF_UGREEN_ALBUM_UUID) + album_type = self.entry.data.get( + CONF_UGREEN_ALBUM_TYPE, ugr_api.ALBUM_TYPE_REGULAR + ) + verify_ssl = bool(self.entry.data.get(CONF_UGREEN_VERIFY_SSL, True)) + if not url or not username or not password or not album_uuid: + raise UpdateFailed("UGREEN provider is missing URL, credentials or album") + + client = ugr_api.UGreenClient( + self.hass, url, username, password, verify_ssl=verify_ssl + ) + try: + await client.async_login() + photos = await client.async_list_album_pictures(album_uuid, album_type) + except ugr_api.UGreenAuthError as err: + raise UpdateFailed(f"UGREEN login failed: {err}") from err + except ugr_api.UGreenApiError as err: + raise UpdateFailed(f"Error listing UGREEN album photos: {err}") from err + + if not photos: + raise UpdateFailed(f"No images found in UGREEN album '{self.entry.title}'") + + # Stored so the camera can fetch image bytes server-side (session + # cookie plus ``ugk``, added per request so stored URLs never carry it) + # with the same TLS setting as the login. + self.image_request_headers = dict(client.image_headers) + self.image_request_params = dict(client.image_params) + self.image_request_verify_ssl = verify_ssl + # Kept for the background enrichment pass (GPS/address lookups), + # which needs a logged-in client and the album context per photo. + self._ugreen_client = client + self._ugreen_album_uuid = album_uuid + self._ugreen_album_type = album_type + + items: list[MediaItem] = [] + for p in photos: + picture_id = p.get("picture_id") + if picture_id is None: + continue + # Backstop for type_option; a video would only show its poster frame. + ext = p.get("real_ext_name") or Path(str(p.get("file_name") or "")).suffix + if f".{str(ext).lstrip('.').lower()}" in _VIDEO_EXTS: + continue + meta = ugr_api.parse_photo_meta(p) + items.append( + MediaItem( + url=ugr_api.build_image_url( + client.base_url, + picture_id, + album_uuid, + source_album_type=album_type, + upload_time=p.get("upload_time", 0), + ), + width=meta.get("width"), + height=meta.get("height"), + mime_type=None, + filename=p.get("file_name"), + captured_at=meta.get("captured_at"), + byte_size=meta.get("byte_size"), + latitude=None, + longitude=None, + location=None, + description=None, + source_id=str(picture_id), + # GPS/address isn't in this listing - the background + # enrichment pass fills it in via picture/info. + exif_scanned=False, + ) + ) + + if not items: + raise UpdateFailed("Could not resolve any UGREEN images") + + try: + album_name = await client.async_get_album_name(album_uuid) + except Exception as err: # noqa: BLE001 - the title is cosmetic + _LOGGER.debug("UGREEN: could not read the album name: %s", err) + album_name = None + + return { + "title": album_name or self.entry.title, + "items": items, + } + async def _update_nextcloud(self) -> dict[str, Any]: """List photos from Nextcloud, dispatching on the configured auth mode.""" mode = self.entry.data.get( @@ -2502,6 +2618,40 @@ class AlbumCoordinator(DataUpdateCoordinator): if completed: self.async_set_updated_data(data) + async def _enrich_ugreen_item(self, item: MediaItem) -> None: + """Fetch GPS coordinates and a location label for one UGREEN photo. + + Reuses the client stashed by ``_update_ugreen`` instead of logging in + again per photo. Image URLs are stable across refreshes, so + ``_merge_prior_enrichment`` carries results forward and only new + photos are looked up. + """ + from . import ugreen as ugr_api + + client = self._ugreen_client + if client is None or not item.source_id or not self._ugreen_album_uuid: + item.exif_scanned = True + return + try: + info = await client.async_get_picture_info( + item.source_id, self._ugreen_album_uuid, self._ugreen_album_type + ) + except asyncio.CancelledError: + raise + except ugr_api.UGreenApiError as err: + _LOGGER.debug( + "UGREEN: picture/info failed for %s: %s", item.source_id, err + ) + item.exif_scanned = True + return + meta = ugr_api.parse_picture_location(info) + if "latitude" in meta and "longitude" in meta: + item.latitude = meta["latitude"] + item.longitude = meta["longitude"] + if "location" in meta: + item.location = meta["location"] + item.exif_scanned = True + async def _enrich_items_background(self, data: dict[str, Any]) -> None: """Read EXIF for unscanned local files, then reverse-geocode. @@ -2564,6 +2714,24 @@ class AlbumCoordinator(DataUpdateCoordinator): self.async_set_updated_data(data) continue + if self.provider == PROVIDER_UGREEN: + try: + await self._enrich_ugreen_item(item) + except asyncio.CancelledError: + raise + except Exception as err: # noqa: BLE001 + _LOGGER.debug("UGREEN enrich error: %s", err) + item.exif_scanned = True + scanned_since_save += 1 + self._enrich_progress["exif_done"] = ( + self._enrich_progress.get("exif_done", 0) + 1 + ) + if scanned_since_save >= _EXIF_BATCH_SAVE: + scanned_since_save = 0 + await self._save_cached_items(data) + self.async_set_updated_data(data) + continue + url = item.url if not url.startswith("file://"): item.exif_scanned = True diff --git a/custom_components/album_slideshow/manifest.json b/custom_components/album_slideshow/manifest.json index b6f78d50..3e3a3ab2 100644 --- a/custom_components/album_slideshow/manifest.json +++ b/custom_components/album_slideshow/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "issue_tracker": "https://github.com/eyalgal/album_slideshow/issues", "requirements": ["PyNaCl>=1.5.0"], - "version": "1.13.0" + "version": "1.14.0" } diff --git a/custom_components/album_slideshow/sensor.py b/custom_components/album_slideshow/sensor.py index 97fc217b..66f8f249 100644 --- a/custom_components/album_slideshow/sensor.py +++ b/custom_components/album_slideshow/sensor.py @@ -41,7 +41,13 @@ class _BaseAlbumSensor(SensorEntity): def __init__(self, entry: ConfigEntry, coordinator: AlbumCoordinator) -> None: self.entry = entry self.coordinator = coordinator - coordinator.async_add_listener(self.async_write_ha_state) + + async def async_added_to_hass(self) -> None: + await super().async_added_to_hass() + # Disabled entities are constructed but never added, so subscribe here. + self.async_on_remove( + self.coordinator.async_add_listener(self.async_write_ha_state) + ) @property def device_info(self): @@ -80,7 +86,10 @@ class HiddenPhotoCountSensor(_BaseAlbumSensor): super().__init__(entry, coordinator) self._attr_unique_id = f"{entry.entry_id}_hidden_photo_count" self._attr_name = "Hidden photos" - coordinator.store.add_listener(self.async_write_ha_state) + + async def async_added_to_hass(self) -> None: + await super().async_added_to_hass() + self.coordinator.store.add_listener(self.async_write_ha_state) @property def native_value(self): diff --git a/custom_components/album_slideshow/strings.json b/custom_components/album_slideshow/strings.json index 1a2ff808..97b319dc 100644 --- a/custom_components/album_slideshow/strings.json +++ b/custom_components/album_slideshow/strings.json @@ -110,6 +110,23 @@ "synology_image_size": "Image quality" } }, + "ugreen": { + "title": "UGREEN NAS (UGOS Photos)", + "description": "Experimental: connect to the Photos app on a UGREEN NAS (UGOS Pro). Enter the NAS address including port (e.g. https://192.168.1.10:9443) and an account. This provider talks to an undocumented, reverse-engineered API and may break with future UGOS updates.", + "data": { + "ugreen_url": "NAS URL", + "ugreen_username": "Username", + "ugreen_password": "Password", + "ugreen_verify_ssl": "Verify SSL certificate" + } + }, + "ugreen_select": { + "title": "UGREEN album", + "description": "Pick the UGOS Photos album to show.", + "data": { + "ugreen_album_uuid": "Album" + } + }, "nextcloud": { "title": "Nextcloud", "description": "Choose how to connect: an authenticated WebDAV folder in your Nextcloud files, or a public Nextcloud Photos album share link.", @@ -173,7 +190,9 @@ "invalid_nextcloud_url": "That does not look like a Nextcloud Photos public album link.", "nextcloud_public_cannot_connect": "Could not connect to that Nextcloud album. Check the link is still valid.", "invalid_ente_url": "That does not look like an Ente public album link. It should look like {example_url}, including the part after the #.", - "ente_cannot_connect": "Could not open that Ente album. The link may be expired, revoked, or password-protected." + "ente_cannot_connect": "Could not open that Ente album. The link may be expired, revoked, or password-protected.", + "ugreen_cannot_connect": "Could not connect to the UGREEN NAS. Check the URL, username and password. If the NAS uses a self-signed certificate, turn off Verify SSL certificate.", + "ugreen_no_albums": "No albums were found on this UGREEN NAS." } }, "options": { diff --git a/custom_components/album_slideshow/text.py b/custom_components/album_slideshow/text.py index cb26e2ae..7ab827bc 100644 --- a/custom_components/album_slideshow/text.py +++ b/custom_components/album_slideshow/text.py @@ -29,11 +29,6 @@ class PairDividerColorText(TextEntity, RestoreEntity): self._attr_unique_id = f"{entry.entry_id}_pair_divider_color" self._attr_name = "Pair divider color" - def _on_store_change() -> None: - self.async_write_ha_state() - - store.add_listener(_on_store_change) - @property def native_value(self) -> str: return self.store.pair_divider_color diff --git a/custom_components/album_slideshow/translations/en.json b/custom_components/album_slideshow/translations/en.json index 1a2ff808..97b319dc 100644 --- a/custom_components/album_slideshow/translations/en.json +++ b/custom_components/album_slideshow/translations/en.json @@ -110,6 +110,23 @@ "synology_image_size": "Image quality" } }, + "ugreen": { + "title": "UGREEN NAS (UGOS Photos)", + "description": "Experimental: connect to the Photos app on a UGREEN NAS (UGOS Pro). Enter the NAS address including port (e.g. https://192.168.1.10:9443) and an account. This provider talks to an undocumented, reverse-engineered API and may break with future UGOS updates.", + "data": { + "ugreen_url": "NAS URL", + "ugreen_username": "Username", + "ugreen_password": "Password", + "ugreen_verify_ssl": "Verify SSL certificate" + } + }, + "ugreen_select": { + "title": "UGREEN album", + "description": "Pick the UGOS Photos album to show.", + "data": { + "ugreen_album_uuid": "Album" + } + }, "nextcloud": { "title": "Nextcloud", "description": "Choose how to connect: an authenticated WebDAV folder in your Nextcloud files, or a public Nextcloud Photos album share link.", @@ -173,7 +190,9 @@ "invalid_nextcloud_url": "That does not look like a Nextcloud Photos public album link.", "nextcloud_public_cannot_connect": "Could not connect to that Nextcloud album. Check the link is still valid.", "invalid_ente_url": "That does not look like an Ente public album link. It should look like {example_url}, including the part after the #.", - "ente_cannot_connect": "Could not open that Ente album. The link may be expired, revoked, or password-protected." + "ente_cannot_connect": "Could not open that Ente album. The link may be expired, revoked, or password-protected.", + "ugreen_cannot_connect": "Could not connect to the UGREEN NAS. Check the URL, username and password. If the NAS uses a self-signed certificate, turn off Verify SSL certificate.", + "ugreen_no_albums": "No albums were found on this UGREEN NAS." } }, "options": { diff --git a/custom_components/album_slideshow/www/album-slideshow-card.js b/custom_components/album_slideshow/www/album-slideshow-card.js index 11b2d74a..e94b4169 100644 --- a/custom_components/album_slideshow/www/album-slideshow-card.js +++ b/custom_components/album_slideshow/www/album-slideshow-card.js @@ -26,7 +26,7 @@ * tap_action: none # none | more-info */ -const VERSION = "1.13.0"; +const VERSION = "1.14.0"; const ANIMATED_TRANSITIONS = [ "fade", diff --git a/custom_components/ha_creality_ws/manifest.json b/custom_components/ha_creality_ws/manifest.json index 632a18d9..11523df5 100644 --- a/custom_components/ha_creality_ws/manifest.json +++ b/custom_components/ha_creality_ws/manifest.json @@ -6,14 +6,15 @@ "config_flow": true, "dependencies": ["lovelace", "http", "frontend", "persistent_notification"], "documentation": "https://github.com/3dg1luk43/ha_creality_ws", + "integration_type": "device", "iot_class": "local_push", "issue_tracker": "https://github.com/3dg1luk43/ha_creality_ws/issues", - "loggers": ["custom_components.ha_creality_ws"], + "loggers": ["websockets", "go2rtc_client"], "requirements": [ "websockets>=10.4", "go2rtc-client>=0.1.0" ], - "version": "0.9.8", + "version": "0.9.9", "zeroconf": [ { "type": "_http._tcp.local.", "name": "*creality*" }, { "type": "_workstation._tcp.local.", "name": "*creality*" }, diff --git a/custom_components/ha_creality_ws/strings.json b/custom_components/ha_creality_ws/strings.json index 1ed7dc1b..2e2b428e 100644 --- a/custom_components/ha_creality_ws/strings.json +++ b/custom_components/ha_creality_ws/strings.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{name}", "step": { "user": { "title": "Connect to Creality Printer", @@ -8,12 +9,14 @@ "host": "IP Address", "name": "Friendly Name" } + }, + "zeroconf_confirm": { + "title": "Add discovered printer", + "description": "Add the Creality printer {name} at {host}?" } }, "error": { - "cannot_connect": "Failed to connect to printer. Ensure IP is correct and printer is ON.", - "not_K": "Connection refused or not a supported Creality K-series printer.", - "already_configured": "This printer is already configured." + "cannot_connect": "Failed to connect to printer. Ensure IP is correct and printer is ON." }, "abort": { "already_configured": "Device is already configured", @@ -34,6 +37,7 @@ "status_starting": "Starting", "status_paused": "Paused", "status_finishing": "Finishing", + "status_printing": "Printing", "body_progress": "{progress}%", "body_paused": "Paused at {progress}%", "body_paused_unknown": "Paused", @@ -48,12 +52,60 @@ "stopped": "{device} stopped at {progress}%", "completed_detailed": "{device} finished. {filament} consumed.", "error": "{device} error - {key} [{code}]", - "filament_runout": "{device} filament ran out. Print is {state}. Change the filament.", - "finishing_soon": "{device} will finish in {minutes} minutes." + "filament_runout": "{device} filament ran out. Print status: {state}. Change the filament.", + "finishing_soon": "{device} will finish in {minutes} min.", + "cfs_info_title": "CFS information request", + "cfs_info_failed": "Could not reach {printers}.", + "cfs_material_failed_title": "CFS material not changed", + "cfs_material_failed": "{printer} is not connected, so box {box} slot {slot} was not changed.", + "diagnostic_title": "Creality diagnostic data", + "diagnostic_collected": "Diagnostic data was collected (printers: {printers}). It is in this action's response and in the Home Assistant log." }, "exceptions": { "unsupported_ha_version": { "message": "This version of the Creality WebSocket integration needs Home Assistant {minimum} or newer, but this system is running {running}. Update Home Assistant, or install integration version 0.9.7 instead." + }, + "cfs_material_write_failed": { + "message": "The CFS material was not changed on {printers}: the printer is not connected." + }, + "cfs_material_needs_device": { + "message": "Choose the printer to change. This action only writes to the printers you select, never to all of them." + }, + "no_printer_matched": { + "message": "None of the selected devices is a Creality printer set up in this integration." + }, + "cfs_material_printer_busy": { + "message": "{printer} is in the middle of a print, so its CFS material was not changed. Try again once the print is over." + }, + "material_type_empty": { + "message": "The material type must not be empty." + }, + "material_not_a_number": { + "message": "{field} must be a number, not {value}." + }, + "material_temp_order": { + "message": "The maximum temperature ({high}) must not be below the minimum temperature ({low})." + }, + "material_pressure_range": { + "message": "The pressure advance must be between 0 and 1, not {value}." + }, + "material_colour_list": { + "message": "The colour must be a hex value such as #ff8800, not a list of RGB values." + }, + "material_colour_multi": { + "message": "{value} holds several colours, and a slot can only store one." + }, + "material_colour_invalid": { + "message": "The colour must be six hex digits such as #ff8800, not {value}." + }, + "printer_not_connected": { + "message": "The printer is not connected, so the command was not sent. Check that it is switched on and reachable, then try again." + } + }, + "issues": { + "missing_power_switch": { + "title": "Power switch for {printer} not found", + "description": "The power switch set for {printer}, `{entity_id}`, does not exist, so it was renamed or removed. The printer is connected to as if no power switch were set. To use one again, choose it under Settings > Devices & services > Creality WebSocket Integration > Configure > Power switch." } }, "selector": { @@ -173,7 +225,9 @@ }, "error": { "invalid_camera_url": "Enter a full URL including the scheme, e.g. http://192.168.1.50:8080/?action=stream or rtsp://192.168.1.50:554/stream.", - "unknown_placeholder": "That is not a placeholder this integration can fill. Check the list in the description above, and mind the spelling." + "unknown_placeholder": "That is not a placeholder this integration can fill. Check the list in the description above, and mind the spelling.", + "cannot_connect": "Failed to connect to the printer at this address. Check the IP address and that the printer is switched on.", + "host_in_use": "Another printer in this integration is already set up at this address." } }, "entity": { @@ -189,6 +243,7 @@ "position_z": { "name": "Position Z" }, "feedrate_pct": { "name": "Print Speed" }, "flowrate_pct": { "name": "Flow Rate" }, + "real_time_speed": { "name": "Real-Time Speed" }, "model_info": { "name": "Model" }, "filament_status": { "name": "Filament Status", @@ -220,7 +275,10 @@ "print_job_time": { "name": "Print Job Time" }, "print_left_time": { "name": "Print Time Left" }, "real_time_flow": { "name": "Real-Time Flow" }, - "current_object": { "name": "Current Object" }, + "current_object": { + "name": "Current Object", + "state": { "not_printing": "Not printing" } + }, "object_count": { "name": "Object Count" }, "print_control": { "name": "Print Control", @@ -233,7 +291,28 @@ "max_nozzle_temp": { "name": "Max Nozzle Temperature" }, "max_bed_temp": { "name": "Max Bed Temperature" }, "max_chamber_temp": { "name": "Max Chamber Temperature" }, - "active_filament_slot": { "name": "Active Filament Slot" }, + "active_filament_slot": { + "name": "Active Filament Slot", + "state": { + "external": "External", + "box_1_slot_1": "Box 1 Slot 1", + "box_1_slot_2": "Box 1 Slot 2", + "box_1_slot_3": "Box 1 Slot 3", + "box_1_slot_4": "Box 1 Slot 4", + "box_2_slot_1": "Box 2 Slot 1", + "box_2_slot_2": "Box 2 Slot 2", + "box_2_slot_3": "Box 2 Slot 3", + "box_2_slot_4": "Box 2 Slot 4", + "box_3_slot_1": "Box 3 Slot 1", + "box_3_slot_2": "Box 3 Slot 2", + "box_3_slot_3": "Box 3 Slot 3", + "box_3_slot_4": "Box 3 Slot 4", + "box_4_slot_1": "Box 4 Slot 1", + "box_4_slot_2": "Box 4 Slot 2", + "box_4_slot_3": "Box 4 Slot 3", + "box_4_slot_4": "Box 4 Slot 4" + } + }, "cfs_box_temp": { "name": "CFS Box {box_id} Temperature" }, "cfs_box_humidity": { "name": "CFS Box {box_id} Humidity" }, "cfs_slot_filament": { "name": "CFS Box {box_id} Slot {slot} Filament" }, @@ -251,7 +330,8 @@ "reconnect": { "name": "Reconnect" } }, "number": { - "print_tuning_pct": { "name": "Print Tuning" }, + "print_tuning_pct": { "name": "Speed Factor" }, + "flow_rate_pct": { "name": "Flow Factor" }, "nozzle_target": { "name": "Nozzle Target" }, "bed_target": { "name": "Bed Target" }, "chamber_target": { "name": "Chamber Target" } @@ -274,11 +354,11 @@ "services": { "diagnostic_dump": { "name": "Diagnostic Dump", - "description": "Dump all WebSocket telemetry data from connected Creality printers to JSON format for troubleshooting and analysis.", + "description": "Collect diagnostics for every Creality printer and return them as the response, for troubleshooting. Also written to the log.", "fields": { "include_sensitive_data": { "name": "Include Sensitive Data", - "description": "Whether to include potentially sensitive data in the diagnostic output (currently unused, reserved for future use)" + "description": "Show printer addresses and network names, notify targets and access tokens instead of hiding them. Leave this off for anything you share publicly." } } }, diff --git a/custom_components/ha_creality_ws/translations/en.json b/custom_components/ha_creality_ws/translations/en.json index 1ed7dc1b..2e2b428e 100644 --- a/custom_components/ha_creality_ws/translations/en.json +++ b/custom_components/ha_creality_ws/translations/en.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{name}", "step": { "user": { "title": "Connect to Creality Printer", @@ -8,12 +9,14 @@ "host": "IP Address", "name": "Friendly Name" } + }, + "zeroconf_confirm": { + "title": "Add discovered printer", + "description": "Add the Creality printer {name} at {host}?" } }, "error": { - "cannot_connect": "Failed to connect to printer. Ensure IP is correct and printer is ON.", - "not_K": "Connection refused or not a supported Creality K-series printer.", - "already_configured": "This printer is already configured." + "cannot_connect": "Failed to connect to printer. Ensure IP is correct and printer is ON." }, "abort": { "already_configured": "Device is already configured", @@ -34,6 +37,7 @@ "status_starting": "Starting", "status_paused": "Paused", "status_finishing": "Finishing", + "status_printing": "Printing", "body_progress": "{progress}%", "body_paused": "Paused at {progress}%", "body_paused_unknown": "Paused", @@ -48,12 +52,60 @@ "stopped": "{device} stopped at {progress}%", "completed_detailed": "{device} finished. {filament} consumed.", "error": "{device} error - {key} [{code}]", - "filament_runout": "{device} filament ran out. Print is {state}. Change the filament.", - "finishing_soon": "{device} will finish in {minutes} minutes." + "filament_runout": "{device} filament ran out. Print status: {state}. Change the filament.", + "finishing_soon": "{device} will finish in {minutes} min.", + "cfs_info_title": "CFS information request", + "cfs_info_failed": "Could not reach {printers}.", + "cfs_material_failed_title": "CFS material not changed", + "cfs_material_failed": "{printer} is not connected, so box {box} slot {slot} was not changed.", + "diagnostic_title": "Creality diagnostic data", + "diagnostic_collected": "Diagnostic data was collected (printers: {printers}). It is in this action's response and in the Home Assistant log." }, "exceptions": { "unsupported_ha_version": { "message": "This version of the Creality WebSocket integration needs Home Assistant {minimum} or newer, but this system is running {running}. Update Home Assistant, or install integration version 0.9.7 instead." + }, + "cfs_material_write_failed": { + "message": "The CFS material was not changed on {printers}: the printer is not connected." + }, + "cfs_material_needs_device": { + "message": "Choose the printer to change. This action only writes to the printers you select, never to all of them." + }, + "no_printer_matched": { + "message": "None of the selected devices is a Creality printer set up in this integration." + }, + "cfs_material_printer_busy": { + "message": "{printer} is in the middle of a print, so its CFS material was not changed. Try again once the print is over." + }, + "material_type_empty": { + "message": "The material type must not be empty." + }, + "material_not_a_number": { + "message": "{field} must be a number, not {value}." + }, + "material_temp_order": { + "message": "The maximum temperature ({high}) must not be below the minimum temperature ({low})." + }, + "material_pressure_range": { + "message": "The pressure advance must be between 0 and 1, not {value}." + }, + "material_colour_list": { + "message": "The colour must be a hex value such as #ff8800, not a list of RGB values." + }, + "material_colour_multi": { + "message": "{value} holds several colours, and a slot can only store one." + }, + "material_colour_invalid": { + "message": "The colour must be six hex digits such as #ff8800, not {value}." + }, + "printer_not_connected": { + "message": "The printer is not connected, so the command was not sent. Check that it is switched on and reachable, then try again." + } + }, + "issues": { + "missing_power_switch": { + "title": "Power switch for {printer} not found", + "description": "The power switch set for {printer}, `{entity_id}`, does not exist, so it was renamed or removed. The printer is connected to as if no power switch were set. To use one again, choose it under Settings > Devices & services > Creality WebSocket Integration > Configure > Power switch." } }, "selector": { @@ -173,7 +225,9 @@ }, "error": { "invalid_camera_url": "Enter a full URL including the scheme, e.g. http://192.168.1.50:8080/?action=stream or rtsp://192.168.1.50:554/stream.", - "unknown_placeholder": "That is not a placeholder this integration can fill. Check the list in the description above, and mind the spelling." + "unknown_placeholder": "That is not a placeholder this integration can fill. Check the list in the description above, and mind the spelling.", + "cannot_connect": "Failed to connect to the printer at this address. Check the IP address and that the printer is switched on.", + "host_in_use": "Another printer in this integration is already set up at this address." } }, "entity": { @@ -189,6 +243,7 @@ "position_z": { "name": "Position Z" }, "feedrate_pct": { "name": "Print Speed" }, "flowrate_pct": { "name": "Flow Rate" }, + "real_time_speed": { "name": "Real-Time Speed" }, "model_info": { "name": "Model" }, "filament_status": { "name": "Filament Status", @@ -220,7 +275,10 @@ "print_job_time": { "name": "Print Job Time" }, "print_left_time": { "name": "Print Time Left" }, "real_time_flow": { "name": "Real-Time Flow" }, - "current_object": { "name": "Current Object" }, + "current_object": { + "name": "Current Object", + "state": { "not_printing": "Not printing" } + }, "object_count": { "name": "Object Count" }, "print_control": { "name": "Print Control", @@ -233,7 +291,28 @@ "max_nozzle_temp": { "name": "Max Nozzle Temperature" }, "max_bed_temp": { "name": "Max Bed Temperature" }, "max_chamber_temp": { "name": "Max Chamber Temperature" }, - "active_filament_slot": { "name": "Active Filament Slot" }, + "active_filament_slot": { + "name": "Active Filament Slot", + "state": { + "external": "External", + "box_1_slot_1": "Box 1 Slot 1", + "box_1_slot_2": "Box 1 Slot 2", + "box_1_slot_3": "Box 1 Slot 3", + "box_1_slot_4": "Box 1 Slot 4", + "box_2_slot_1": "Box 2 Slot 1", + "box_2_slot_2": "Box 2 Slot 2", + "box_2_slot_3": "Box 2 Slot 3", + "box_2_slot_4": "Box 2 Slot 4", + "box_3_slot_1": "Box 3 Slot 1", + "box_3_slot_2": "Box 3 Slot 2", + "box_3_slot_3": "Box 3 Slot 3", + "box_3_slot_4": "Box 3 Slot 4", + "box_4_slot_1": "Box 4 Slot 1", + "box_4_slot_2": "Box 4 Slot 2", + "box_4_slot_3": "Box 4 Slot 3", + "box_4_slot_4": "Box 4 Slot 4" + } + }, "cfs_box_temp": { "name": "CFS Box {box_id} Temperature" }, "cfs_box_humidity": { "name": "CFS Box {box_id} Humidity" }, "cfs_slot_filament": { "name": "CFS Box {box_id} Slot {slot} Filament" }, @@ -251,7 +330,8 @@ "reconnect": { "name": "Reconnect" } }, "number": { - "print_tuning_pct": { "name": "Print Tuning" }, + "print_tuning_pct": { "name": "Speed Factor" }, + "flow_rate_pct": { "name": "Flow Factor" }, "nozzle_target": { "name": "Nozzle Target" }, "bed_target": { "name": "Bed Target" }, "chamber_target": { "name": "Chamber Target" } @@ -274,11 +354,11 @@ "services": { "diagnostic_dump": { "name": "Diagnostic Dump", - "description": "Dump all WebSocket telemetry data from connected Creality printers to JSON format for troubleshooting and analysis.", + "description": "Collect diagnostics for every Creality printer and return them as the response, for troubleshooting. Also written to the log.", "fields": { "include_sensitive_data": { "name": "Include Sensitive Data", - "description": "Whether to include potentially sensitive data in the diagnostic output (currently unused, reserved for future use)" + "description": "Show printer addresses and network names, notify targets and access tokens instead of hiding them. Leave this off for anything you share publicly." } } }, diff --git a/custom_components/ha_creality_ws/translations/es.json b/custom_components/ha_creality_ws/translations/es.json index aa4780d2..5e789e55 100644 --- a/custom_components/ha_creality_ws/translations/es.json +++ b/custom_components/ha_creality_ws/translations/es.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{name}", "step": { "user": { "title": "Conectar a la impresora Creality", @@ -8,12 +9,14 @@ "host": "Dirección IP", "name": "Nombre descriptivo" } + }, + "zeroconf_confirm": { + "title": "Añadir impresora descubierta", + "description": "¿Quieres añadir la impresora Creality {name} en {host}?" } }, "error": { - "cannot_connect": "Error al conectar con la impresora. Asegúrate de que la dirección IP sea correcta y de que la impresora esté encendida.", - "not_K": "Conexión rechazada o no es una impresora Creality de la serie K compatible.", - "already_configured": "Esta impresora ya está configurada." + "cannot_connect": "Error al conectar con la impresora. Asegúrate de que la dirección IP sea correcta y de que la impresora esté encendida." }, "abort": { "already_configured": "El dispositivo ya está configurado", @@ -34,6 +37,7 @@ "status_starting": "Iniciando", "status_paused": "En pausa", "status_finishing": "Finalizando", + "status_printing": "Imprimiendo", "body_progress": "{progress}%", "body_paused": "En pausa al {progress}%", "body_paused_unknown": "En pausa", @@ -49,11 +53,59 @@ "completed_detailed": "{device} ha terminado. Filamento consumido: {filament}.", "error": "Error de {device} - {key} [{code}]", "filament_runout": "{device} se ha quedado sin filamento. Estado de la impresión: {state}. Cambia el filamento.", - "finishing_soon": "{device} terminará en {minutes} minutos." + "finishing_soon": "{device} terminará en {minutes} min.", + "cfs_info_title": "Solicitud de información del CFS", + "cfs_info_failed": "No se ha podido contactar con {printers}.", + "cfs_material_failed_title": "Material del CFS no cambiado", + "cfs_material_failed": "{printer} no está conectada, así que no se ha cambiado la ranura {slot} de la caja {box}.", + "diagnostic_title": "Datos de diagnóstico de Creality", + "diagnostic_collected": "Se han recopilado los datos de diagnóstico (impresoras: {printers}). Están en la respuesta de esta acción y en el registro de Home Assistant." }, "exceptions": { "unsupported_ha_version": { "message": "Esta versión de la integración Creality WebSocket necesita Home Assistant {minimum} o posterior, pero este sistema ejecuta la versión {running}. Actualiza Home Assistant o instala en su lugar la versión 0.9.7 de la integración." + }, + "cfs_material_write_failed": { + "message": "No se ha cambiado el material del CFS en {printers}: la impresora no está conectada." + }, + "cfs_material_needs_device": { + "message": "Elige la impresora que quieres cambiar. Esta acción solo escribe en las impresoras que selecciones, nunca en todas." + }, + "no_printer_matched": { + "message": "Ninguno de los dispositivos seleccionados es una impresora Creality configurada en esta integración." + }, + "cfs_material_printer_busy": { + "message": "{printer} está en mitad de una impresión, así que no se ha cambiado su material del CFS. Vuelve a intentarlo cuando termine la impresión." + }, + "material_type_empty": { + "message": "El tipo de material no puede estar vacío." + }, + "material_not_a_number": { + "message": "{field} debe ser un número, no {value}." + }, + "material_temp_order": { + "message": "La temperatura máxima ({high}) no puede ser inferior a la temperatura mínima ({low})." + }, + "material_pressure_range": { + "message": "El avance de presión debe estar entre 0 y 1, no {value}." + }, + "material_colour_list": { + "message": "El color debe ser un valor hexadecimal como #ff8800, no una lista de valores RGB." + }, + "material_colour_multi": { + "message": "{value} contiene varios colores, y una ranura solo puede guardar uno." + }, + "material_colour_invalid": { + "message": "El color debe tener seis dígitos hexadecimales, como #ff8800, no {value}." + }, + "printer_not_connected": { + "message": "La impresora no está conectada, así que no se ha enviado el comando. Comprueba que esté encendida y que se pueda acceder a ella, y vuelve a intentarlo." + } + }, + "issues": { + "missing_power_switch": { + "title": "No se encuentra el interruptor de encendido de {printer}", + "description": "El interruptor de encendido configurado para {printer}, `{entity_id}`, no existe, así que se ha cambiado de nombre o se ha eliminado. La integración se conecta a la impresora como si no se hubiera configurado ninguno. Para volver a usar uno, elígelo en Ajustes > Dispositivos y servicios > Creality WebSocket Integration > Configurar > Interruptor de encendido." } }, "selector": { @@ -173,7 +225,9 @@ }, "error": { "invalid_camera_url": "Introduce una URL completa incluyendo el esquema, p. ej. http://192.168.1.50:8080/?action=stream o rtsp://192.168.1.50:554/stream.", - "unknown_placeholder": "Ese no es un marcador de posición que esta integración pueda rellenar. Consulta la lista que aparece en la descripción de arriba y comprueba que esté bien escrito." + "unknown_placeholder": "Ese no es un marcador de posición que esta integración pueda rellenar. Consulta la lista que aparece en la descripción de arriba y comprueba que esté bien escrito.", + "cannot_connect": "No se ha podido conectar con la impresora en esta dirección. Comprueba la dirección IP y que la impresora esté encendida.", + "host_in_use": "Otra impresora de esta integración ya está configurada en esta dirección." } }, "entity": { @@ -189,6 +243,7 @@ "position_z": { "name": "Posición Z" }, "feedrate_pct": { "name": "Velocidad de impresión" }, "flowrate_pct": { "name": "Tasa de flujo" }, + "real_time_speed": { "name": "Velocidad en tiempo real" }, "model_info": { "name": "Modelo" }, "filament_status": { "name": "Estado del filamento", @@ -220,7 +275,10 @@ "print_job_time": { "name": "Tiempo de trabajo de impresión" }, "print_left_time": { "name": "Tiempo restante de impresión" }, "real_time_flow": { "name": "Flujo en tiempo real" }, - "current_object": { "name": "Objeto actual" }, + "current_object": { + "name": "Objeto actual", + "state": { "not_printing": "Sin impresión" } + }, "object_count": { "name": "Cantidad de objetos" }, "print_control": { "name": "Control de impresión", @@ -233,7 +291,28 @@ "max_nozzle_temp": { "name": "Temperatura máxima de la boquilla" }, "max_bed_temp": { "name": "Temperatura máxima de la cama" }, "max_chamber_temp": { "name": "Temperatura máxima de la cámara" }, - "active_filament_slot": { "name": "Ranura de filamento activa" }, + "active_filament_slot": { + "name": "Ranura de filamento activa", + "state": { + "external": "Externa", + "box_1_slot_1": "Caja 1, ranura 1", + "box_1_slot_2": "Caja 1, ranura 2", + "box_1_slot_3": "Caja 1, ranura 3", + "box_1_slot_4": "Caja 1, ranura 4", + "box_2_slot_1": "Caja 2, ranura 1", + "box_2_slot_2": "Caja 2, ranura 2", + "box_2_slot_3": "Caja 2, ranura 3", + "box_2_slot_4": "Caja 2, ranura 4", + "box_3_slot_1": "Caja 3, ranura 1", + "box_3_slot_2": "Caja 3, ranura 2", + "box_3_slot_3": "Caja 3, ranura 3", + "box_3_slot_4": "Caja 3, ranura 4", + "box_4_slot_1": "Caja 4, ranura 1", + "box_4_slot_2": "Caja 4, ranura 2", + "box_4_slot_3": "Caja 4, ranura 3", + "box_4_slot_4": "Caja 4, ranura 4" + } + }, "cfs_box_temp": { "name": "Temperatura de la caja CFS {box_id}" }, "cfs_box_humidity": { "name": "Humedad de la caja CFS {box_id}" }, "cfs_slot_filament": { "name": "Filamento (caja CFS {box_id}, ranura {slot})" }, @@ -251,7 +330,8 @@ "reconnect": { "name": "Reconectar" } }, "number": { - "print_tuning_pct": { "name": "Ajuste de impresión" }, + "print_tuning_pct": { "name": "Factor de velocidad" }, + "flow_rate_pct": { "name": "Factor de flujo" }, "nozzle_target": { "name": "Objetivo de la boquilla" }, "bed_target": { "name": "Objetivo de la cama" }, "chamber_target": { "name": "Objetivo de la cámara" } @@ -274,11 +354,11 @@ "services": { "diagnostic_dump": { "name": "Volcado de diagnóstico", - "description": "Vuelca todos los datos de telemetría WebSocket de las impresoras Creality conectadas en formato JSON para resolución de problemas y análisis.", + "description": "Recopila los diagnósticos de todas las impresoras Creality y los devuelve como respuesta, para resolver problemas. También se escriben en el registro.", "fields": { "include_sensitive_data": { "name": "Incluir datos sensibles", - "description": "Si se deben incluir datos potencialmente sensibles en la salida de diagnóstico (actualmente sin uso, reservado para uso futuro)" + "description": "Muestra las direcciones y los nombres de red de las impresoras, los destinos de notificación y los tokens de acceso en lugar de ocultarlos. Déjalo desactivado para todo lo que compartas públicamente." } } }, diff --git a/custom_components/ha_creality_ws/www/i18n/en.json b/custom_components/ha_creality_ws/www/i18n/en.json index 6a98a5e4..a63d772e 100644 --- a/custom_components/ha_creality_ws/www/i18n/en.json +++ b/custom_components/ha_creality_ws/www/i18n/en.json @@ -1,5 +1,8 @@ { "printer_card": { + "default_name": "3D Printer", + "picker_name": "Creality Printer Card", + "picker_description": "Standalone card for Creality K-Series printers", "status_unknown": "Unknown", "confirm_stop": "Are you sure you want to stop the print?", "confirm_power_off": "Are you sure you want to power off the printer?", @@ -77,6 +80,7 @@ "label_custom_btn": "Custom Action Entity", "label_custom_btn_icon": "Custom Button Icon", "label_custom_btn_hidden": "Hide Custom Button", + "label_hidden_buttons": "Hidden Buttons", "label_button_order": "Button Order (list)", "label_hide_box_temp": "Hide Chamber Temperature", "label_pause_btn_icon": "Pause Icon Override", @@ -104,6 +108,7 @@ "helper_custom_btn": "Any entity to trigger (Button, Script, Switch, etc.)", "helper_custom_btn_icon": "Icon for the custom button", "helper_custom_btn_hidden": "Hide the custom button", + "helper_hidden_buttons": "Never shown on the card, whatever the printer is doing", "helper_button_order": "List of buttons to show in order (pause, resume, stop, light, power, custom)", "helper_hide_box_temp": "Hide the chamber temperature pill even when a sensor is configured", "editor_error_title": "Editor Error", @@ -111,9 +116,14 @@ "editor_error_prefix": "Error:" }, "cfs_card": { + "picker_name": "Creality CFS Card", + "label_device": "Printer", + "btn_fill_from_device": "Fill all fields from the printer", + "status_device_filled": "Filled {filled} of {total} fields.", + "status_device_empty": "This printer has no CFS sensors yet.", + "picker_description": "A card to control the Creality Filament System (CFS)", "no_data": "No CFS data available", "ext_label": "EXT", - "cfs_label": "CFS", "cfs_number_label": "CFS {number}", "label_card_title": "Card Title", "label_external_filament": "External Filament", @@ -124,7 +134,6 @@ "label_slot_filament": "Box {box} Slot {slot} Filament", "label_slot_color": "Box {box} Slot {slot} Color", "label_slot_percent": "Box {box} Slot {slot} Remaining Percent", - "schema_compact_view": "Compact View (Mini Mode)", "schema_show_type_in_mini": "Show Filament Type in Mini Mode", "tab_entities": "Entities", "tab_theme": "Theme", diff --git a/custom_components/ha_creality_ws/www/i18n/es.json b/custom_components/ha_creality_ws/www/i18n/es.json index 2df40271..2d724a4b 100644 --- a/custom_components/ha_creality_ws/www/i18n/es.json +++ b/custom_components/ha_creality_ws/www/i18n/es.json @@ -1,5 +1,8 @@ { "printer_card": { + "default_name": "Impresora 3D", + "picker_name": "Tarjeta de impresora Creality", + "picker_description": "Tarjeta independiente para impresoras Creality de la serie K", "status_unknown": "Desconocido", "confirm_stop": "¿Seguro que deseas detener la impresión?", "confirm_power_off": "¿Seguro que deseas apagar la impresora?", @@ -77,6 +80,7 @@ "label_custom_btn": "Entidad de acción personalizada", "label_custom_btn_icon": "Icono del botón personalizado", "label_custom_btn_hidden": "Ocultar el botón personalizado", + "label_hidden_buttons": "Botones ocultos", "label_button_order": "Orden de los botones (lista)", "label_hide_box_temp": "Ocultar la temperatura de la cámara", "label_pause_btn_icon": "Icono de pausa (sustitución)", @@ -104,6 +108,7 @@ "helper_custom_btn": "Cualquier entidad a activar (botón, script, interruptor, etc.)", "helper_custom_btn_icon": "Icono del botón personalizado", "helper_custom_btn_hidden": "Ocultar el botón personalizado", + "helper_hidden_buttons": "Nunca se muestran en la tarjeta, sea cual sea el estado de la impresora", "helper_button_order": "Lista de botones en orden (pause, resume, stop, light, power, custom)", "helper_hide_box_temp": "Ocultar el indicador de temperatura de la cámara aunque haya un sensor configurado", "editor_error_title": "Error del editor", @@ -111,9 +116,14 @@ "editor_error_prefix": "Error:" }, "cfs_card": { + "picker_name": "Tarjeta CFS de Creality", + "picker_description": "Tarjeta para controlar el Creality Filament System (CFS)", + "label_device": "Impresora", + "btn_fill_from_device": "Rellenar todos los campos desde la impresora", + "status_device_filled": "Se han rellenado {filled} de {total} campos.", + "status_device_empty": "Esta impresora aún no tiene sensores del CFS.", "no_data": "No hay datos del CFS disponibles", "ext_label": "EXT", - "cfs_label": "CFS", "cfs_number_label": "CFS {number}", "label_card_title": "Título de la tarjeta", "label_external_filament": "Filamento externo", @@ -124,7 +134,6 @@ "label_slot_filament": "Filamento (caja {box}, ranura {slot})", "label_slot_color": "Color (caja {box}, ranura {slot})", "label_slot_percent": "Restante (caja {box}, ranura {slot})", - "schema_compact_view": "Vista compacta (modo mini)", "schema_show_type_in_mini": "Mostrar el tipo de filamento en modo mini", "tab_entities": "Entidades", "tab_theme": "Tema", diff --git a/custom_components/ha_creality_ws/www/k_cfs_card.js b/custom_components/ha_creality_ws/www/k_cfs_card.js index 37badb54..a523e574 100644 --- a/custom_components/ha_creality_ws/www/k_cfs_card.js +++ b/custom_components/ha_creality_ws/www/k_cfs_card.js @@ -6,20 +6,28 @@ const mdi = (name) => `mdi:${name}`; const ASSET_URL_BASE = "/ha_creality_ws/"; const I18N_URL_BASE = `${ASSET_URL_BASE}i18n/`; -const _i18nData = {}; -const _i18nPromises = {}; +// One cache for both cards, on the page: each module used to fetch en.json for +// itself, and a language with no file (a 404) was asked for again by every new +// card. A 404 is now remembered; only a failed request is retried. `no-cache` +// revalidates, so an updated translation is not served stale (R45). +const _i18nShared = (globalThis.__haCrealityWsI18n = globalThis.__haCrealityWsI18n || { data: {}, promises: {} }); +const _i18nData = _i18nShared.data; function _loadI18n(lang) { - if (_i18nData[lang]) return Promise.resolve(_i18nData[lang]); - if (_i18nPromises[lang]) return _i18nPromises[lang]; - _i18nPromises[lang] = fetch(`${I18N_URL_BASE}${lang}.json`) - .then((res) => (res.ok ? res.json() : null)) + if (lang in _i18nData) return Promise.resolve(_i18nData[lang]); + const promises = _i18nShared.promises; + if (promises[lang]) return promises[lang]; + promises[lang] = fetch(`${I18N_URL_BASE}${lang}.json`, { cache: "no-cache" }) + .then((res) => { + if (res.ok) return res.json(); + if (res.status === 404) return null; + throw new Error(`HTTP ${res.status}`); + }) .then((data) => { - if (data) _i18nData[lang] = data; - else _i18nPromises[lang] = null; + _i18nData[lang] = data; return data; }) - .catch(() => { _i18nPromises[lang] = null; return null; }); - return _i18nPromises[lang]; + .catch(() => { promises[lang] = null; return null; }); + return promises[lang]; } function _resolveLang(hass) { return hass?.locale?.language || hass?.language || "en"; @@ -33,9 +41,12 @@ function _translate(hass, section, fallbackDict, key, vars) { : (remoteEn && key in remoteEn) ? remoteEn[key] : (fallbackDict[lang]?.[key] ?? fallbackDict[short]?.[key] ?? fallbackDict["en"]?.[key] ?? key); if (vars) { - Object.entries(vars).forEach(([k, v]) => { - text = text.replace(new RegExp(`\\{${k}\\}`, "g"), v); - }); + // One pass with a function: a replacement *string* expands `$&` and + // friends, so a preset named "Teal $& Co" toasted as "Teal {name} Co", and + // a value containing "{other}" was substituted again by a later key. + text = text.replace(/\{(\w+)\}/g, (match, name) => ( + Object.prototype.hasOwnProperty.call(vars, name) ? String(vars[name]) : match + )); } return text; } @@ -153,15 +164,6 @@ class ColourPresetsManager { return true; } - rename(from, to) { - const target = String(to || "").trim(); - if (!target || !(from in this.presets) || target === from) return false; - this.presets[target] = this.presets[from]; - delete this.presets[from]; - this._persist(); - return true; - } - remove(name) { if (!(name in this.presets)) return false; delete this.presets[name]; @@ -219,9 +221,14 @@ const BUSY_PRINT_STATES = new Set([ const CFS_TRANSLATIONS = { en: { + picker_name: "Creality CFS Card", + label_device: "Printer", + btn_fill_from_device: "Fill all fields from the printer", + status_device_filled: "Filled {filled} of {total} fields.", + status_device_empty: "This printer has no CFS sensors yet.", + picker_description: "A card to control the Creality Filament System (CFS)", no_data: "No CFS data available", ext_label: "EXT", - cfs_label: "CFS", cfs_number_label: "CFS {number}", // Editor label_card_title: "Card Title", @@ -290,6 +297,19 @@ class KCFSCard extends HTMLElement { * used to prefill it like a real value -- so changing only the material type * and saving wrote `#cccccc` to the spool as though the printer had said so. */ + /** + * The external spool's label: its name, plus the material type only when + * the name does not already say it. The filament sensor's state is already + * "Generic PLA", so appending the type read "Generic PLA PLA" (#115). + */ + static _nameWithType(name, type) { + const n = String(name ?? "").trim(); + const t = String(type ?? "").trim(); + if (!t || t === "-") return n; + const words = n.toLowerCase().split(/\s+/); + return words.includes(t.toLowerCase()) ? n : `${n} ${t}`; + } + static _parseColor(value) { if (isSentinel(value)) return null; const raw = String(value).trim(); @@ -411,7 +431,25 @@ class KCFSCard extends HTMLElement { return '#f44336'; // Red (60-100%) - Critical } - static getStubConfig() { + /** + * What a new card starts with: the first printer that has CFS sensors, else + * nothing. Only what differs from the defaults goes into the dashboard + * (R44); every default used to be written there. + */ + static getStubConfig(hass) { + const registry = hass?.entities || {}; + const devices = [...new Set(Object.values(registry) + .filter((e) => e?.platform === "ha_creality_ws" && e.device_id) + .map((e) => e.device_id))]; + for (const deviceId of devices) { + const found = cfsEntitiesForDevice(hass, deviceId); + if (Object.keys(found).length) return { device: deviceId, ...found }; + } + return {}; + } + + /** Every option with its default value. */ + static defaultConfig() { const cfg = { name: "CFS", view_mode: "full", @@ -439,7 +477,7 @@ class KCFSCard extends HTMLElement { } setConfig(config) { - this._cfg = { ...KCFSCard.getStubConfig(), ...KCFSCard._migrateConfig(config) }; + this._cfg = { ...KCFSCard.defaultConfig(), ...KCFSCard._migrateConfig(config) }; if (!this._root) { this._root = this.attachShadow({ mode: "open" }); } @@ -460,9 +498,6 @@ class KCFSCard extends HTMLElement { } // i18n helpers ------------------------------------------------------- - _resolveLanguage() { - return _resolveLang(this._hass); - } _t(key, vars) { return _translate(this._hass, "cfs_card", CFS_TRANSLATIONS, key, vars); } @@ -741,6 +776,10 @@ class KCFSCard extends HTMLElement { 50% { opacity: 0.7; transform: scale(1.1); } } + @media (prefers-reduced-motion: reduce) { + .status-badge { animation: none; } + } + /* === COMPACT MODE === */ .compact-mode { padding: 14px; @@ -858,14 +897,16 @@ class KCFSCard extends HTMLElement { gap: 2px; } - .env-mini .temp { - color: #ffb74d; + /* The hue says warm, dry or damp; the theme's text colour carries the + contrast. The bare #ffb74d was 1.73:1 on a light theme (R42). */ + .env-mini .temp, .env-temp { + color: color-mix(in srgb, #ffb74d 45%, var(--primary-text-color)); font-weight: 600; } - .env-mini .hum { + .env-mini .hum, .env-hum { + color: color-mix(in srgb, var(--hum-color, #64b5f6) 45%, var(--primary-text-color)); font-weight: 600; - /* Cor aplicada dinamicamente via inline style */ } /* === EXTERNAL SECTION === */ @@ -1079,7 +1120,17 @@ class KCFSCard extends HTMLElement { place-items: center; padding: 16px; background: rgba(0, 0, 0, 0.55); + /* Undo the defaults: the element is the full-screen backdrop. */ + border: none; + margin: 0; + width: 100%; + height: 100%; + max-width: none; + max-height: none; + box-sizing: border-box; + color: inherit; } + .edit-overlay::backdrop { background: transparent; } .edit-dialog { width: min(420px, 100%); max-height: 85vh; @@ -1293,8 +1344,14 @@ class KCFSCard extends HTMLElement { const name = filamentObj?.state; const type = filamentObj?.attributes?.type; const selected = filamentObj?.attributes?.selected; - const rawColor = colorObj?.state || filamentObj?.attributes?.color_hex; - const parsedColor = KCFSCard._parseColor(rawColor); + // Parsed in turn, not `||`: "unknown"/"unavailable" are truthy, so a + // colour sensor reading either hid the slot's color_hex attribute and + // the spool went grey (R28). + const rawColor = isSentinel(colorObj?.state) + ? filamentObj?.attributes?.color_hex + : colorObj.state; + const parsedColor = KCFSCard._parseColor(rawColor) + ?? KCFSCard._parseColor(filamentObj?.attributes?.color_hex); const color = parsedColor ?? "#cccccc"; const percent = KCFSCard._parsePercent(percentObj); const percentText = fmtState(percentObj); @@ -1361,8 +1418,11 @@ class KCFSCard extends HTMLElement { const name = filamentObj?.state; const type = filamentObj?.attributes?.type; const selected = filamentObj?.attributes?.selected; - const rawColor = colorObj?.state || filamentObj?.attributes?.color_hex; - const parsedColor = KCFSCard._parseColor(rawColor); + const rawColor = isSentinel(colorObj?.state) + ? filamentObj?.attributes?.color_hex + : colorObj.state; + const parsedColor = KCFSCard._parseColor(rawColor) + ?? KCFSCard._parseColor(filamentObj?.attributes?.color_hex); const color = parsedColor ?? "#cccccc"; const percent = KCFSCard._parsePercent(percentObj); const percentText = fmtState(percentObj); @@ -1523,7 +1583,7 @@ class KCFSCard extends HTMLElement { const env = []; if (box?.temp && box.temp !== "-") env.push(`${esc(box.temp)}`); if (box?.humidity && box.humidity !== "-") { - env.push(`${esc(box.humidity)}`); + env.push(`${esc(box.humidity)}`); } return ` @@ -1571,7 +1631,7 @@ class KCFSCard extends HTMLElement { if (tempStr || humStr) { const tempHtml = tempStr ? `${esc(tempStr)}` : ''; - const humHtml = humStr ? `${esc(humStr)}` : ''; + const humHtml = humStr ? `${esc(humStr)}` : ''; const separator = tempStr && humStr ? ' • ' : ''; envInfo = `
${tempHtml}${separator}${humHtml}
`; } @@ -1604,7 +1664,7 @@ class KCFSCard extends HTMLElement { const hasFilament = safeType !== "-" && safeName !== "-"; const pct = hasFilament && external.percent !== null ? external.percent : 0; const percentTextDisplay = hasFilament ? (external.percentText || '-') : '-'; - const displayName = hasFilament ? `${safeName} ${safeType}` : '-'; + const displayName = hasFilament ? KCFSCard._nameWithType(safeName, safeType) : '-'; externalSection = `
@@ -1658,7 +1718,7 @@ class KCFSCard extends HTMLElement { const safeName = !isSentinel(external.name) ? String(external.name).trim() : "-"; const hasFilament = safeType !== "-" && safeName !== "-"; const percentTextDisplay = hasFilament ? (external.percentText || '-') : '-'; - const displayName = hasFilament ? `${safeName} ${safeType}` : '-'; + const displayName = hasFilament ? KCFSCard._nameWithType(safeName, safeType) : '-'; return `
@@ -1682,7 +1742,7 @@ class KCFSCard extends HTMLElement { envHtml = `
${tempStr ? `
${esc(tempStr)}
` : ''} - ${humStr ? `
${esc(humStr)}
` : ''} + ${humStr ? `
${esc(humStr)}
` : ''}
`; } @@ -1823,8 +1883,7 @@ class KCFSCard extends HTMLElement { * rather than silently resolved to one of them. * * Reads hass.entities (EntityRegistryDisplayEntry carries device_id, platform - * and translation_key), so no WebSocket round trip and no admin permission is - * needed -- unlike config/entity_registry/list. + * and translation_key), so no WebSocket round trip is needed. * @returns {Promise} */ async _resolveDeviceId() { @@ -1844,9 +1903,8 @@ class KCFSCard extends HTMLElement { } // Anything hass.entities could not answer for is asked individually. It is - // not a version fallback: hass.entities can be *partially* populated, and it - // is absent entirely for a non-admin user, whose browser is not allowed - // config/entity_registry/get either. + // not a version fallback: hass.entities can be *partially* populated, for + // one while the frontend is still loading it. // // Every unresolved entity is asked, not just enough to find one device. // Accepting the first answer resolved a card spanning two printers to @@ -1866,9 +1924,9 @@ class KCFSCard extends HTMLElement { }); return { deviceId: entry?.device_id || null, failed: false }; } catch (_) { - // config/entity_registry/get is admin-only, so for a non-admin - // dashboard user every one of these fails. Treating that as "this - // entity has no device" is what let a two-printer card resolve to + // A failed lookup (an entity missing from the registry, a dropped + // connection) is not "this entity has no device". Treating it so + // is what let a two-printer card resolve to // whichever printer *was* in hass.entities, and _saveMaterial then // sent the other printer's box and slot ids to it. return { deviceId: null, failed: true }; @@ -1985,8 +2043,13 @@ class KCFSCard extends HTMLElement { const toast = document.createElement("div"); toast.className = "cfs-toast"; + // Announced by screen readers without taking focus (R42). + toast.setAttribute("role", "status"); + toast.setAttribute("aria-live", "polite"); toast.textContent = message; - this._root.appendChild(toast); + // Inside the open edit dialog when there is one: it is in the top layer, + // and a toast on the card would be painted under it. + (this._dialogEl || this._root).appendChild(toast); this._toastEl = toast; this._toastTimer = setTimeout(() => { if (toast.remove) toast.remove(); @@ -2038,7 +2101,11 @@ class KCFSCard extends HTMLElement { return; } - const overlay = document.createElement("div"); + // A native opened with showModal() renders in the top layer. The + //
overlay before it stayed inside this card's stacking context + // (`:host { z-index: 1 }`), so a second CFS card further down the page was + // painted over the bottom of the dialog (R46, seen in Chromium). + const overlay = document.createElement("dialog"); overlay.className = "edit-overlay"; const dialog = document.createElement("div"); dialog.className = "edit-dialog"; @@ -2049,11 +2116,16 @@ class KCFSCard extends HTMLElement { dialog.setAttribute("aria-modal", "true"); dialog.setAttribute("aria-label", this._t("dialog_edit_title")); dialog.tabIndex = -1; - overlay.appendChild(dialog); + // Back to whatever opened the dialog afterwards, so a keyboard user is not + // dropped at the top of the page (R42). + const opener = this._root.activeElement || null; const close = () => { overlay.removeEventListener("keydown", onKeyDown); + if (overlay.close && overlay.open) overlay.close(); if (overlay.remove) overlay.remove(); + if (this._dialogEl === overlay) this._dialogEl = null; + if (opener && opener.focus) opener.focus(); }; const onKeyDown = (ev) => { if (ev.key === "Escape" || ev.key === "Esc") { @@ -2061,11 +2133,38 @@ class KCFSCard extends HTMLElement { close(); } }; + // Tab stays inside the dialog: it is modal, and focus wandering to the + // card behind it left a keyboard user editing an invisible page (R42). + // Sentinels rather than counting fields, because ha-form keeps its inputs + // in its own shadow root where the dialog cannot see which one has focus. + const sentinel = (onFocus) => { + const el = document.createElement("span"); + el.tabIndex = 0; + el.className = "focus-sentinel"; + el.addEventListener("focus", onFocus); + return el; + }; + const before = sentinel(() => { + const buttons = Array.from(dialog.querySelectorAll("button")).filter((btn) => !btn.disabled); + const last = buttons[buttons.length - 1]; + if (last && last.focus) last.focus(); + }); + const after = sentinel(() => dialog.focus && dialog.focus()); + overlay.appendChild(before); + overlay.appendChild(dialog); + overlay.appendChild(after); overlay.addEventListener("keydown", onKeyDown); + // Escape on a modal arrives as `cancel`; close it our way. + overlay.addEventListener("cancel", (ev) => { + ev.preventDefault?.(); + close(); + }); overlay.addEventListener("click", (ev) => { if (ev.target === overlay) close(); }); dialog.appendChild(this._renderEditForm(slot, close)); this._root.appendChild(overlay); + this._dialogEl = overlay; + if (overlay.showModal) overlay.showModal(); // Focus the dialog itself rather than the first field: ha-form upgrades // asynchronously, so its inputs may not exist yet. if (dialog.focus) dialog.focus(); @@ -2274,18 +2373,55 @@ class KCFSCard extends HTMLElement { swatch.title = name; swatch.setAttribute("aria-label", name); swatch.style.background = colour; - swatch.addEventListener("click", () => apply(colour)); + let longPressed = false; + swatch.addEventListener("click", () => { + // The click that ends a long press is not a pick. + if (longPressed) { + longPressed = false; + return; + } + apply(colour); + }); if (this._presets.isCustom(name)) { swatch.classList?.add?.("custom"); - // Long-press-free management: a modifier click removes a custom preset, - // which keeps the row compact without a second list. - swatch.addEventListener("contextmenu", (ev) => { - ev.preventDefault?.(); + const removePreset = () => { if (this._presets.remove(name)) { this._showToast(this._t("toast_preset_deleted", { name })); rebuild(); } + }; + // A right-click removes a custom preset, which keeps the row compact + // without a second list. + swatch.addEventListener("contextmenu", (ev) => { + ev.preventDefault?.(); + removePreset(); + }); + // iOS Safari fires no contextmenu on a long press, so a preset could + // not be removed there at all: a touch held for 600 ms does it, and + // Delete does it from the keyboard (R46). + let pressTimer = null; + const cancelPress = () => { + clearTimeout(pressTimer); + pressTimer = null; + }; + swatch.addEventListener("pointerdown", (ev) => { + if (ev.pointerType === "mouse") return; + cancelPress(); + pressTimer = setTimeout(() => { + pressTimer = null; + longPressed = true; + removePreset(); + }, 600); + }); + for (const type of ["pointerup", "pointercancel", "pointerleave"]) { + swatch.addEventListener(type, cancelPress); + } + swatch.addEventListener("keydown", (ev) => { + if (ev.key === "Delete" || ev.key === "Backspace") { + ev.preventDefault?.(); + removePreset(); + } }); } swatches.appendChild(swatch); @@ -2469,30 +2605,20 @@ class KCFSCard extends HTMLElement { return 5; } - getLayoutOptions() { - // Count configured boxes for dynamic sizing - let boxCount = 0; - if (this._cfg) { - for (let box = 0; box < 4; box++) { - const hasBox = this._cfg[`box${box}_temp`] || this._cfg[`box${box}_humidity`] || - [0, 1, 2, 3].some(s => this._cfg[`box${box}_slot${s}_filament`] || this._cfg[`box${box}_slot${s}_color`] || this._cfg[`box${box}_slot${s}_percent`]); - if (hasBox) boxCount++; - } - } - - // Check for external filament - const hasExternal = this._cfg?.external_filament || this._cfg?.external_color || this._cfg?.external_percent; - const externalRows = hasExternal ? 1 : 0; - - // Add extra space when more than 2 rows - const totalRows = boxCount + externalRows; - const extraPadding = totalRows > 2 ? 1 : 0; - - const minRows = this._cfg?.view_mode === "compact" ? Math.max(1, totalRows + extraPadding) : 5; - + /** + * Sections-view sizing: let the grid size the cell to the card. + * + * `getLayoutOptions` (deprecated) reserved a fixed number of rows, five for + * the full view, and the card forces `height: auto` so its content is not + * clipped (#71) -- together that drew the card over whatever sat below it, + * by 176 px for a one-box CFS at phone width (R23). `rows: "auto"` makes the + * reserved height the card's own. + */ + getGridOptions() { return { - grid_rows: minRows, - grid_min_rows: minRows, + columns: 12, + rows: "auto", + min_columns: 6, }; } } @@ -2521,11 +2647,76 @@ function defineOnce(tag, cls) { defineOnce(CARD_TAG, KCFSCard); +/** + * This integration's CFS sensors for one device, as a card config patch. + * + * CFS units take card positions 0-3 in the printer's box order; the external + * spool holder is box 0 on the printer and goes to the external fields. Box and + * slot come from the sensors' `box_id`/`slot_id` attributes, falling back to + * the default entity id ("..._cfs_box_1_slot_2_filament", slot 1-based) for a + * sensor with no reading yet. Without this, a CFS card meant filling up to 51 + * entity pickers by hand (R43). + * @param {?Object} hass + * @param {string} deviceId + * @return {!Object} Config key -> entity id. + */ +function cfsEntitiesForDevice(hass, deviceId) { + const registry = hass?.entities || {}; + const states = hass?.states || {}; + const patch = {}; + if (!deviceId) return patch; + const slots = []; + const boxes = []; + for (const [entityId, entry] of Object.entries(registry)) { + if (!entry || entry.device_id !== deviceId) continue; + if (entry.platform && entry.platform !== "ha_creality_ws") continue; + const key = entry.translation_key || ""; + const attrs = states[entityId]?.attributes || {}; + let m = /^cfs_ext_(filament|color|percent)$/.exec(key); + if (m) { + patch[`external_${m[1]}`] = entityId; + continue; + } + m = /^cfs_slot_(filament|color|percent)$/.exec(key); + if (m) { + let box = attrs.box_id; + let slot = attrs.slot_id; + if (box === undefined || box === null || slot === undefined || slot === null) { + const id = /_cfs_box_(\d+)_slot_(\d+)_/.exec(entityId); + if (!id) continue; + box = Number(id[1]); + slot = Number(id[2]) - 1; + } + // Box 0 is the external spool holder; an install from before R20 still + // has "Box 0 Slot 1" sensors for it next to the External ones. + if (Number(box) >= 1) slots.push({ box: Number(box), slot: Number(slot), kind: m[1], entityId }); + continue; + } + m = /^cfs_box_(temp|humidity)$/.exec(key); + if (m) { + let box = attrs.box_id; + if (box === undefined || box === null) { + const id = /_cfs_box_(\d+)_(temperature|humidity)/.exec(entityId); + if (!id) continue; + box = Number(id[1]); + } + if (Number(box) >= 1) boxes.push({ box: Number(box), kind: m[1], entityId }); + } + } + const order = [...new Set([...slots, ...boxes].map((e) => e.box))].sort((a, b) => a - b).slice(0, 4); + for (const e of slots) { + const pos = order.indexOf(e.box); + if (pos >= 0 && e.slot >= 0 && e.slot < 4) patch[`box${pos}_slot${e.slot}_${e.kind}`] = e.entityId; + } + for (const e of boxes) { + const pos = order.indexOf(e.box); + if (pos >= 0) patch[`box${pos}_${e.kind}`] = e.entityId; + } + return patch; +} + class KCFSCardEditor extends HTMLElement { // i18n helpers ------------------------------------------------------- - _resolveLanguage() { - return _resolveLang(this._hass); - } _t(key, vars) { return _translate(this._hass, "cfs_card", CFS_TRANSLATIONS, key, vars); } @@ -2533,42 +2724,84 @@ class KCFSCardEditor extends HTMLElement { set hass(hass) { this._hass = hass; - if (this._form) this._form.hass = hass; - _requestI18n(this, hass, () => { if (this._root) this._render(); }); + _requestI18n(this, hass, () => this._refresh(true)); + this._refresh(); } setConfig(config) { - this._cfg = { ...KCFSCard.getStubConfig(), ...KCFSCard._migrateConfig(config) }; - this._render(); + this._cfg = { ...KCFSCard.defaultConfig(), ...KCFSCard._migrateConfig(config) }; + this._refresh(); } connectedCallback() { - this._render(); + this._refresh(); } - _render() { + /** + * Build once, then only update. + * + * Lovelace answers every config-changed with a fresh setConfig. Rebuilding + * the editor's DOM on each one, as this did, lost the focused field after + * every keystroke and jumped back to the first tab (R22). Data is only + * reassigned when it really differs from what the forms already show. + */ + _refresh(relabel = false) { + if (!this._cfg) return; if (!this._root) { this._root = this.attachShadow({ mode: "open" }); } + if (!this._form) { + this._build(); + relabel = true; + } + this._form.hass = this._hass; + this._themeForm.hass = this._hass; + this._deviceForm.hass = this._hass; + if (relabel) this._applyLabels(); + this._setFormData(); + } + _setFormData() { + const key = JSON.stringify(this._cfg); + if (key === this._shownKey) return; + this._shownKey = key; + this._form.data = this._cfg; + this._themeForm.data = this._cfg; + this._deviceForm.data = { device: this._cfg.device || "" }; + this._root.getElementById("refill").disabled = !this._cfg.device; + } + + _build() { const style = ` .editor-container { padding: 16px; } .tabs { display: flex; border-bottom: 1px solid var(--divider-color); margin-bottom: 16px; } - .tab { padding: 8px 16px; cursor: pointer; border-bottom: 2px solid transparent; } + .tab { padding: 8px 16px; cursor: pointer; border: none; border-bottom: 2px solid transparent; background: none; color: inherit; font: inherit; } + .tab:focus-visible { outline: 2px solid var(--primary-color); outline-offset: -2px; } .tab.active { border-bottom-color: var(--primary-color); color: var(--primary-color); } .tab-content { display: none; } .tab-content.active { display: block; } .input-helper { font-size: 0.9em; color: var(--secondary-text-color); margin-top: 4px; padding: 0 8px; } + .device-row { display: flex; align-items: center; gap: 12px; margin: 4px 0 16px; flex-wrap: wrap; } + .ghost-btn { background: none; border: 1px solid var(--divider-color); border-radius: 6px; padding: 6px 12px; + color: var(--primary-color); font: inherit; cursor: pointer; } + .ghost-btn:disabled { color: var(--disabled-text-color); cursor: default; } + .ghost-btn:focus-visible { outline: 2px solid var(--primary-color); outline-offset: 2px; } + .status { color: var(--secondary-text-color); font-size: 0.9em; } `; this._root.innerHTML = `
-
-
${this._t("tab_entities")}
-
${this._t("tab_theme")}
+
+ +
+ +
+ + +
@@ -2578,18 +2811,82 @@ class KCFSCardEditor extends HTMLElement { `; this._setupTabs(); + this._setupDeviceForm(); this._setupEntitiesForm(); this._setupThemeForm(); } + _setupDeviceForm() { + this._deviceForm = this._root.getElementById("device-form"); + this._deviceForm.schema = [{ + name: "device", + selector: { device: { filter: [{ integration: "ha_creality_ws" }] } }, + }]; + this._deviceForm.addEventListener("value-changed", (ev) => this._onDeviceChanged(ev.detail.value || {})); + this._root.getElementById("refill").addEventListener("click", () => this._applyDeviceFill(true)); + } + + _onDeviceChanged(value) { + const deviceId = value.device || ""; + if (deviceId === (this._cfg.device || "")) return; + this._cfg = { ...this._cfg, device: deviceId }; + if (deviceId) { + // Picking a device fills what is still blank; the button replaces too. + this._applyDeviceFill(false); + return; + } + this._setRefillStatus(""); + this._setFormData(); + this._dispatchConfigChange(); + } + + _applyDeviceFill(overwrite) { + const found = cfsEntitiesForDevice(this._hass, this._cfg.device); + const patch = {}; + for (const [key, entityId] of Object.entries(found)) { + if (overwrite || !this._cfg[key]) patch[key] = entityId; + } + this._cfg = { ...this._cfg, ...patch }; + const total = Object.keys(found).length; + this._setRefillStatus(total + ? this._t("status_device_filled", { filled: Object.keys(patch).length, total }) + : this._t("status_device_empty")); + this._setFormData(); + this._dispatchConfigChange(); + } + + _setRefillStatus(text) { + const el = this._root?.getElementById("refill-status"); + if (el) el.textContent = text; + } + + /** Everything that reads a translation, so a late language load relabels. */ + _applyLabels() { + this._root.getElementById("tab-entities").textContent = this._t("tab_entities"); + this._root.getElementById("tab-theme").textContent = this._t("tab_theme"); + this._root.getElementById("refill").textContent = this._t("btn_fill_from_device"); + this._deviceForm.computeLabel = () => this._t("label_device"); + // New function objects, so ha-form re-renders its labels. + this._form.computeLabel = (s) => this._entityLabel(s); + this._themeForm.schema = this._themeSchema(); + this._themeForm.computeLabel = (s) => ({ + view_mode: this._t("schema_view_mode"), + show_type_in_mini: this._t("schema_show_type_in_mini"), + }[s.name] || s.name); + } + _setupTabs() { const tabs = this._root.querySelectorAll(".tab"); const contents = this._root.querySelectorAll(".tab-content"); tabs.forEach((tab) => { tab.onclick = () => { - tabs.forEach((t) => t.classList.remove("active")); + tabs.forEach((t) => { + t.classList.remove("active"); + t.setAttribute("aria-selected", "false"); + }); contents.forEach((c) => c.classList.remove("active")); tab.classList.add("active"); + tab.setAttribute("aria-selected", "true"); this._root.getElementById(`${tab.dataset.tab}-tab`).classList.add("active"); }; }); @@ -2597,8 +2894,6 @@ class KCFSCardEditor extends HTMLElement { _setupEntitiesForm() { this._form = this._root.getElementById("form"); - this._form.hass = this._hass; - this._form.data = this._cfg; const schema = [ { name: "name", selector: { text: {} } }, { name: "external_filament", selector: { entity: { domain: "sensor" } } }, @@ -2617,7 +2912,15 @@ class KCFSCardEditor extends HTMLElement { } this._form.schema = schema; - this._form.computeLabel = (s) => { + // Assigned unconditionally: ha-form leaves computeHelper undefined until + // someone sets it, so guarding on it meant this never ran. Harmless here + // only because ha-form's own default is "no helper" either way. + this._form.computeHelper = () => ""; + + this._form.addEventListener("value-changed", (ev) => this._edited(ev.detail.value)); + } + + _entityLabel(s) { if (s.name === "name") return this._t("label_card_title"); if (s.name === "external_filament") return this._t("label_external_filament"); if (s.name === "external_color") return this._t("label_external_color"); @@ -2642,23 +2945,22 @@ class KCFSCardEditor extends HTMLElement { } return s.name; - }; - // Assigned unconditionally: ha-form leaves computeHelper undefined until - // someone sets it, so guarding on it meant this never ran. Harmless here - // only because ha-form's own default is "no helper" either way. - this._form.computeHelper = () => ""; - - this._form.addEventListener("value-changed", (ev) => { - this._cfg = { ...this._cfg, ...ev.detail.value }; - this._dispatchConfigChange(); - }); } _setupThemeForm() { - const themeForm = this._root.getElementById("theme-form"); - themeForm.hass = this._hass; - themeForm.data = this._cfg; - themeForm.schema = [ + this._themeForm = this._root.getElementById("theme-form"); + this._themeForm.addEventListener("value-changed", (ev) => this._edited(ev.detail.value)); + } + + /** One edit: show it, then tell Lovelace, whose echo is then a no-op. */ + _edited(value) { + this._cfg = { ...this._cfg, ...value }; + this._setFormData(); + this._dispatchConfigChange(); + } + + _themeSchema() { + return [ { name: "view_mode", selector: { @@ -2674,20 +2976,15 @@ class KCFSCardEditor extends HTMLElement { }, { name: "show_type_in_mini", selector: { boolean: {} } }, ]; - themeForm.computeLabel = (s) => ({ - view_mode: this._t("schema_view_mode"), - show_type_in_mini: this._t("schema_show_type_in_mini"), - }[s.name] || s.name); - - themeForm.addEventListener("value-changed", (ev) => { - this._cfg = { ...this._cfg, ...ev.detail.value }; - this._dispatchConfigChange(); - }); } _dispatchConfigChange() { + // Only what differs from the defaults (R44): the card merges them back. + const defaults = KCFSCard.defaultConfig(); + const config = Object.fromEntries(Object.entries(this._cfg) + .filter(([key, value]) => !(key in defaults) || value !== defaults[key])); this.dispatchEvent(new CustomEvent("config-changed", { - detail: { config: this._cfg }, + detail: { config }, bubbles: true, composed: true, })); @@ -2697,9 +2994,21 @@ class KCFSCardEditor extends HTMLElement { defineOnce(EDITOR_TAG, KCFSCardEditor); window.customCards = window.customCards || []; -window.customCards.push({ - type: "k-cfs-card", - name: "Creality CFS Card", - preview: true, - description: "A card to control the Creality Filament System (CFS)" -}); +// Once per page, like the element itself: a second copy of this module (two +// resource entries with different ?v=) listed the card twice in the picker. +if (!window.customCards.some((card) => card.type === "k-cfs-card")) { + const pickerEntry = { + type: "k-cfs-card", + name: CFS_TRANSLATIONS.en.picker_name, + preview: true, + description: CFS_TRANSLATIONS.en.picker_description, + }; + window.customCards.push(pickerEntry); + // The picker reads the entry when it opens, so the page's language can be + // applied once its strings arrive (R33). + const pageHass = document.querySelector?.("home-assistant")?.hass; + _requestI18n({}, pageHass, () => { + pickerEntry.name = _translate(pageHass, "cfs_card", CFS_TRANSLATIONS, "picker_name"); + pickerEntry.description = _translate(pageHass, "cfs_card", CFS_TRANSLATIONS, "picker_description"); + }); +} diff --git a/custom_components/ha_creality_ws/www/k_printer_card.js b/custom_components/ha_creality_ws/www/k_printer_card.js index 742ce8a5..47cdffa0 100644 --- a/custom_components/ha_creality_ws/www/k_printer_card.js +++ b/custom_components/ha_creality_ws/www/k_printer_card.js @@ -2,20 +2,28 @@ const CARD_TAG = "k-printer-card"; const EDITOR_TAG = "k-printer-card-editor"; const I18N_URL_BASE = "/ha_creality_ws/i18n/"; -const _i18nData = {}; -const _i18nPromises = {}; +// One cache for both cards, on the page: each module used to fetch en.json for +// itself, and a language with no file (a 404) was asked for again by every new +// card. A 404 is now remembered; only a failed request is retried. `no-cache` +// revalidates, so an updated translation is not served stale (R45). +const _i18nShared = (globalThis.__haCrealityWsI18n = globalThis.__haCrealityWsI18n || { data: {}, promises: {} }); +const _i18nData = _i18nShared.data; function _loadI18n(lang) { - if (_i18nData[lang]) return Promise.resolve(_i18nData[lang]); - if (_i18nPromises[lang]) return _i18nPromises[lang]; - _i18nPromises[lang] = fetch(`${I18N_URL_BASE}${lang}.json`) - .then((res) => (res.ok ? res.json() : null)) + if (lang in _i18nData) return Promise.resolve(_i18nData[lang]); + const promises = _i18nShared.promises; + if (promises[lang]) return promises[lang]; + promises[lang] = fetch(`${I18N_URL_BASE}${lang}.json`, { cache: "no-cache" }) + .then((res) => { + if (res.ok) return res.json(); + if (res.status === 404) return null; + throw new Error(`HTTP ${res.status}`); + }) .then((data) => { - if (data) _i18nData[lang] = data; - else _i18nPromises[lang] = null; + _i18nData[lang] = data; return data; }) - .catch(() => { _i18nPromises[lang] = null; return null; }); - return _i18nPromises[lang]; + .catch(() => { promises[lang] = null; return null; }); + return promises[lang]; } function _resolveLang(hass) { return hass?.locale?.language || hass?.language || "en"; @@ -29,9 +37,12 @@ function _translate(hass, section, fallbackDict, key, vars) { : (remoteEn && key in remoteEn) ? remoteEn[key] : (fallbackDict[lang]?.[key] ?? fallbackDict[short]?.[key] ?? fallbackDict["en"]?.[key] ?? key); if (vars) { - for (const [name, value] of Object.entries(vars)) { - text = text.replace(new RegExp(`\\{${name}\\}`, "g"), value); - } + // One pass with a function: a replacement *string* expands `$&` and + // friends, so a preset named "Teal $& Co" toasted as "Teal {name} Co", and + // a value containing "{other}" was substituted again by a later key. + text = text.replace(/\{(\w+)\}/g, (match, name) => ( + Object.prototype.hasOwnProperty.call(vars, name) ? String(vars[name]) : match + )); } return text; } @@ -48,6 +59,13 @@ function _requestI18n(instance, hass, onLoaded) { // loop if size measurement on the fresh instance keeps producing the same delta. const LL_REBUILD_MIN_INTERVAL_MS = 2000; const _lastCardRebuildDispatch = new Map(); +// The size each card last measured, by its full name/status key. Lovelace answers +// ll-rebuild by building a NEW element, which used to start at size 3, measure +// the same wrapped row again and fire again once the throttle cleared: a rebuild +// every two seconds on a narrow screen, with the reported size never sticking. +// A fresh element reads its predecessor's size from here, so it measures no +// change and stays put. +const _measuredCardSize = new Map(); // How much wider, in px, the telemetry row has to get before the units it // dropped are worth retrying. Retrying at the width that rejected them would @@ -57,7 +75,13 @@ const TELEMETRY_COMPACT_HYSTERESIS = 8; const INTEGRATION_DOMAIN = "ha_creality_ws"; // The name a card carries until the user (or the device picker) names it. -const DEFAULT_CARD_NAME = "3D Printer"; +// What the card picker used to write into every new card's YAML. Still read as +// "no name", so such a card shows the translated default and is renamed from +// its device like an unnamed one (R33). +const LEGACY_DEFAULT_CARD_NAME = "3D Printer"; +function isUnnamed(name) { + return !name || name === LEGACY_DEFAULT_CARD_NAME; +} /** * Card roles the printer's own device can fill, keyed by the translation_key @@ -249,9 +273,31 @@ function loadThemeFromStorage(cardId) { * @returns {string} Unique card identifier */ function generateCardId(config) { - // Generate a unique ID based on the card configuration - const key = `${config.name || "printer"}-${config.status || "unknown"}`; - return btoa(key).replace(/[^a-zA-Z0-9]/g, '').substring(0, 16); + return cardIdForKey(cardKey(config)); +} + +/** The full identity a card's id is derived from: its name and status entity. */ +function cardKey(config) { + return `${config.name || "printer"}-${config.status || "unknown"}`; +} + +function cardIdForKey(key) { + // `btoa` takes Latin-1 only and throws on anything above U+00FF, so a card + // named "Tiskárna č.1" (or with an en dash, CJK or an emoji in its name) + // threw out of setConfig and became an error card, and in the editor the + // throw landed inside the debounce, so the edit was silently never saved. + // Latin-1 names keep the id they always had, so a theme stored under it is + // still found; anything else is encoded as UTF-8 bytes first. + let encoded; + try { + encoded = btoa(key); + } catch (_err) { + const bytes = new TextEncoder().encode(key); + let binary = ""; + for (const byte of bytes) binary += String.fromCharCode(byte); + encoded = btoa(binary); + } + return encoded.replace(/[^a-zA-Z0-9]/g, '').substring(0, 16); } // Home Assistant rewrites a DURATION sensor's state into whichever display unit @@ -321,10 +367,68 @@ function computeColor(status) { return "var(--secondary-text-color)"; } +/** + * `config` minus what equals its default, the theme likewise (R44). + * @param {!Object} config + * @param {!Object} defaults + * @return {!Object} + */ +function withoutDefaults(config, defaults) { + const same = (a, b) => JSON.stringify(a) === JSON.stringify(b); + const out = {}; + for (const [key, value] of Object.entries(config)) { + if (key === "theme" && value && typeof value === "object") { + const theme = {}; + for (const [k, v] of Object.entries(value)) { + if (!same(v, defaults.theme?.[k])) theme[k] = v; + } + if (Object.keys(theme).length) out.theme = theme; + } else if (!(key in defaults) || !same(value, defaults[key])) { + out[key] = value; + } + } + return out; +} + +/** A theme value safe inside a ` could + // otherwise rewrite the card's CSS or break out into markup (R46). + const theme = Object.fromEntries( + Object.entries(this._cfg.theme).map(([key, value]) => [key, cssValue(value)]), + ); // Theme CSS custom properties - embedded directly in CSS const themeCSS = ` @@ -521,7 +666,7 @@ class KPrinterCard extends HTMLElement { --resume-icon: ${theme.resume_icon || '#fff'}; --stop-icon: ${theme.stop_icon || '#fff'}; --light-icon-on: ${theme.light_icon_on || '#000'}; - --light-icon-off: ${theme.light_icon_off || '#000'}; + --light-icon-off: ${autoColour(theme.light_icon_off, 'var(--primary-text-color)')}; --status-bg: ${theme.status_bg === 'auto' ? 'radial-gradient(var(--card-background-color) 62%, transparent 0)' : (theme.status_bg || 'radial-gradient(var(--card-background-color) 62%, transparent 0)')}; --telemetry-icon: ${theme.telemetry_icon === 'auto' ? 'var(--secondary-text-color)' : (theme.telemetry_icon || 'var(--secondary-text-color)')}; --telemetry-text: ${theme.telemetry_text === 'auto' ? 'var(--primary-text-color)' : (theme.telemetry_text || 'var(--primary-text-color)')}; @@ -529,11 +674,11 @@ class KPrinterCard extends HTMLElement { --custom-on-bg: ${theme.custom_on_bg || theme.custom_bg || 'rgba(33, 150, 243, .90)'}; --custom-off-bg: ${theme.custom_off_bg || 'rgba(150,150,150,.35)'}; --custom-icon: ${theme.custom_icon || '#fff'}; - --custom-icon-off: ${theme.custom_icon_off || '#000'}; + --custom-icon-off: ${autoColour(theme.custom_icon_off, 'var(--primary-text-color)')}; --power-on-bg: ${theme.power_on_bg || 'rgba(76, 175, 80, .90)'}; --power-off-bg: ${theme.power_off_bg || 'rgba(150,150,150,.35)'}; --power-icon-on: ${theme.power_icon_on || '#fff'}; - --power-icon-off: ${theme.power_icon_off || '#000'}; + --power-icon-off: ${autoColour(theme.power_icon_off, 'var(--primary-text-color)')}; } `; @@ -595,6 +740,11 @@ class KPrinterCard extends HTMLElement { color:var(--chip-fg, var(--primary-text-color)); cursor:pointer; user-select:none; border:none; outline:none; } + /* The outline is removed for mouse clicks only: a keyboard user has to + see where focus is (R42). */ + .chip:focus-visible, .title.click:focus-visible { + outline: 2px solid var(--primary-color); outline-offset: 2px; + } .chip[hidden]{ display:none !important; } .chip:active { transform: translateY(1px); } .chip.danger { --chip-bg: var(--stop-bg, rgba(244, 67, 54, .95)); --chip-fg: var(--stop-icon, #fff); } @@ -737,7 +887,8 @@ class KPrinterCard extends HTMLElement { // the stronger warning as much as a running print does. const printing = ["printing", "paused", "processing"].includes(st); const msg = printing ? this._t("confirm_power_off_printing") : this._t("confirm_power_off"); - if (!confirm(msg)) return; + this._confirmedAction(msg, "homeassistant.turn_off", eid); + return; } this._toggleEntity(eid); } else if (id === "light") { @@ -748,18 +899,23 @@ class KPrinterCard extends HTMLElement { } else if (id === "resume") { this._pressButtonEntity(this._cfg.resume_btn); } else if (id === "stop") { - if (confirm(this._t("confirm_stop"))) { - this._pressButtonEntity(this._cfg.stop_btn); - } + const eid = this._cfg.stop_btn; + const domain = (eid || "").split(".")[0]; + const service = ["button", "input_button"].includes(domain) ? `${domain}.press` : "homeassistant.turn_on"; + this._confirmedAction(this._t("confirm_stop"), service, eid); } else if (id === "custom") { - // Custom button can be a button (press), script (turn_on/run), switch (toggle), automation (trigger), etc. - // For simplicity, treat as toggle if switch/light/input_boolean, else press/turn_on + // What a tap means for each kind of entity: on/off things toggle (the + // chip already shows fans and covers as on/off, but only ever turned + // them on), an automation runs rather than being enabled, and buttons, + // scripts and scenes are pressed or turned on (R46). const eid = this._cfg.custom_btn; const domain = eid ? (eid.split(".")[0] || "").toLowerCase() : ""; - if (["switch", "light", "input_boolean"].includes(domain)) { + if (CUSTOM_TOGGLE_DOMAINS.includes(domain)) { this._toggleEntity(eid); + } else if (domain === "automation") { + this._hass?.callService("automation", "trigger", { entity_id: eid }); } else { - this._pressButtonEntity(eid); // Fallback to press (works for button domain, or generic turn_on if mapped) + this._pressButtonEntity(eid); } } }); @@ -783,7 +939,6 @@ class KPrinterCard extends HTMLElement { } disconnectedCallback() { - clearTimeout(this._initialUpdateTimer); if (this._telemetryResizeObserver) { if (this._telemetryObservedNode) { this._telemetryResizeObserver.unobserve(this._telemetryObservedNode); @@ -900,25 +1055,45 @@ class KPrinterCard extends HTMLElement { const currentSize = this._cardSize ?? 3; if (nextSize === currentSize) return; - const cardKey = this._cardId || CARD_TAG; + const throttleKey = this._sizeKey || CARD_TAG; const now = Date.now(); - const lastDispatch = _lastCardRebuildDispatch.get(cardKey) || 0; + const lastDispatch = _lastCardRebuildDispatch.get(throttleKey) || 0; // Defer the _cardSize update until the throttle clears: otherwise a throttled // call would record the new size locally without telling Lovelace, and the // next measurement would short-circuit on the equality check above -- leaving // the rebuild permanently suppressed. if (now - lastDispatch < LL_REBUILD_MIN_INTERVAL_MS) return; - _lastCardRebuildDispatch.set(cardKey, now); + _lastCardRebuildDispatch.set(throttleKey, now); this._cardSize = nextSize; + _measuredCardSize.set(this._sizeKey, nextSize); this.dispatchEvent(new CustomEvent("ll-rebuild", { bubbles: true, composed: true })); } - // Re-implement _pressButtonEntity to be smarter about non-button domains if needed, - // but existing implementation calls button.press. - // For custom buttons (e.g. scripts), we might want to default to homeassistant.turn_on if button.press fails is overkill, - // but let's keep it simple: if it's a script/automation, button.press might not work. - // Let's refine _pressButtonEntity to handle more types or create a generic helper. + /** + * Run `service` on `entityId` behind Home Assistant's own confirmation + * dialog. window.confirm() is switched off in some kiosk browsers and + * webviews, where it answers "no" without asking, so Stop and power-off + * silently did nothing there (R46). + */ + _confirmedAction(text, service, entityId) { + if (!entityId) return; + this.dispatchEvent(new CustomEvent("hass-action", { + bubbles: true, + composed: true, + detail: { + action: "tap", + config: { + tap_action: { + action: "perform-action", + perform_action: service, + target: { entity_id: entityId }, + confirmation: { text }, + }, + }, + }, + })); + } async _pressButtonEntity(eid) { if (!this._hass || !eid) return; @@ -926,7 +1101,7 @@ class KPrinterCard extends HTMLElement { if (domain === "button" || domain === "input_button") { await this._hass.callService(domain, "press", { entity_id: eid }); } else { - // Fallback for scripts, automations, scenes which act like "press" via turn_on + // Scripts and scenes run through turn_on. await this._hass.callService("homeassistant", "turn_on", { entity_id: eid }); } } @@ -1014,7 +1189,7 @@ class KPrinterCard extends HTMLElement { }; const fmtWithUnit = (eid) => fmtState(gObj(eid)); - const name = this._cfg.name || DEFAULT_CARD_NAME; + const name = isUnnamed(this._cfg.name) ? this._t("default_name") : this._cfg.name; const status = g(this._cfg.status) ?? "unknown"; const pct = clamp(Number.isFinite(gNum(this._cfg.progress)) ? gNum(this._cfg.progress) : 0, 0, 100); const timeLeft = durationToSeconds(gObj(this._cfg.time_left)); @@ -1139,11 +1314,12 @@ class KPrinterCard extends HTMLElement { if (!uniqueOrder.includes(k) && buttons[k]) uniqueOrder.push(k); }); + const hiddenByConfig = new Set(Array.isArray(this._cfg.hidden_buttons) ? this._cfg.hidden_buttons : []); let chipsHtml = ""; uniqueOrder.forEach(key => { const btn = buttons[key]; - if (btn && !btn.hidden) { - chipsHtml += ``; + if (btn && !btn.hidden && !hiddenByConfig.has(key)) { + chipsHtml += ``; } }); @@ -1189,6 +1365,9 @@ class KPrinterCard extends HTMLElement { } const CARD_TRANSLATIONS = { en: { + default_name: "3D Printer", + picker_name: "Creality Printer Card", + picker_description: "Standalone card for Creality K-Series printers", status_unknown: "Unknown", confirm_stop: "Are you sure you want to stop the print?", confirm_power_off: "Are you sure you want to power off the printer?", @@ -1266,6 +1445,7 @@ const CARD_TRANSLATIONS = { label_custom_btn: "Custom Action Entity", label_custom_btn_icon: "Custom Button Icon", label_custom_btn_hidden: "Hide Custom Button", + label_hidden_buttons: "Hidden Buttons", label_button_order: "Button Order (list)", label_hide_box_temp: "Hide Chamber Temperature", label_pause_btn_icon: "Pause Icon Override", @@ -1293,6 +1473,7 @@ const CARD_TRANSLATIONS = { helper_custom_btn: "Any entity to trigger (Button, Script, Switch, etc.)", helper_custom_btn_icon: "Icon for the custom button", helper_custom_btn_hidden: "Hide the custom button", + helper_hidden_buttons: "Never shown on the card, whatever the printer is doing", helper_button_order: "List of buttons to show in order (pause, resume, stop, light, power, custom)", helper_hide_box_temp: "Hide the chamber temperature pill even when a sensor is configured", editor_error_title: "Editor Error", @@ -1323,7 +1504,6 @@ function defineOnce(tag, cls) { } } -defineOnce(CARD_TAG, KPrinterCard); /** * Colour controls in the theme tab, grouped the way they are rendered. @@ -1355,15 +1535,15 @@ const THEME_COLOR_GROUPS = [ { key: "light_on_bg", alpha: true }, { key: "light_icon_on" }, { key: "light_off_bg", alpha: true }, - { key: "light_icon_off" }, + { key: "light_icon_off", auto: true, seed: "#000000" }, { key: "power_on_bg", alpha: true }, { key: "power_icon_on" }, { key: "power_off_bg", alpha: true }, - { key: "power_icon_off" }, + { key: "power_icon_off", auto: true, seed: "#000000" }, { key: "custom_bg", alpha: true }, { key: "custom_icon" }, { key: "custom_off_bg", alpha: true }, - { key: "custom_icon_off" }, + { key: "custom_icon_off", auto: true, seed: "#000000" }, ], }, { @@ -1405,7 +1585,7 @@ const AUTO_SUFFIX = "_auto"; /** Top-level config keys the theme tab owns, and so the reset button clears. */ const LAYOUT_RESET_KEYS = [ - "button_order", "custom_btn_hidden", "hide_box_temp", + "button_order", "custom_btn_hidden", "hidden_buttons", "hide_box_temp", "pause_btn_icon", "resume_btn_icon", "stop_btn_icon", "light_btn_icon", "power_btn_icon", "custom_btn_icon", ]; @@ -1419,7 +1599,8 @@ const EDITOR_STYLE = ` .editor-container { padding: 16px; max-width: 1200px; margin: 0 auto; } .editor-title { margin: 0 0 16px 0; font-size: 18px; color: var(--primary-text-color); } .tabs { display: flex; border-bottom: 1px solid var(--divider-color); margin-bottom: 16px; } - .tab { padding: 8px 16px; cursor: pointer; border-bottom: 2px solid transparent; } + .tab { padding: 8px 16px; cursor: pointer; border: none; border-bottom: 2px solid transparent; background: none; color: inherit; font: inherit; } + .tab:focus-visible { outline: 2px solid var(--primary-color); outline-offset: -2px; } .tab.active { border-bottom-color: var(--primary-color); color: var(--primary-color); } .tab-content { display: none; } .tab-content.active { display: block; } @@ -1493,10 +1674,23 @@ function entitiesSchema() { ]; } -function layoutSchema() { +/** The chips `hidden_buttons` can name, in their default order. */ +const HIDEABLE_BUTTONS = ["pause", "resume", "stop", "light", "power", "custom"]; + +function layoutSchema(t = (key) => key) { return [ { name: "button_order", selector: { text: {} } }, { name: "custom_btn_hidden", selector: { boolean: {} } }, + { + name: "hidden_buttons", + selector: { + select: { + multiple: true, + mode: "list", + options: HIDEABLE_BUTTONS.map((key) => ({ value: key, label: t(`chip_${key}`) })), + }, + }, + }, { name: "hide_box_temp", selector: { boolean: {} } }, { name: "pause_btn_icon", selector: { icon: {} } }, { name: "resume_btn_icon", selector: { icon: {} } }, @@ -1602,9 +1796,6 @@ function colorData(cfg, group) { /* Visual editor: entity wiring on one tab, appearance on the other. */ class KPrinterCardEditor extends HTMLElement { // i18n helpers ------------------------------------------------------- - _resolveLanguage() { - return _resolveLang(this._hass); - } _t(key, vars) { return _translate(this._hass, "printer_card", CARD_TRANSLATIONS, key, vars); } @@ -1622,7 +1813,7 @@ class KPrinterCardEditor extends HTMLElement { } setConfig(config) { - const defaults = KPrinterCard.getStubConfig(); + const defaults = KPrinterCard.defaultConfig(); this._cfg = { ...defaults, ...KPrinterCard._migrateConfig(config) }; this._cfg.theme = { ...defaults.theme, ...(this._cfg.theme || {}) }; this._refresh(); @@ -1665,9 +1856,9 @@ class KPrinterCardEditor extends HTMLElement {

-
-
-
+
+ +
@@ -1751,6 +1942,7 @@ class KPrinterCardEditor extends HTMLElement { _selectTab(name) { for (const tab of this._root.querySelectorAll(".tab")) { tab.classList.toggle("active", tab.dataset.tab === name); + tab.setAttribute("aria-selected", String(tab.dataset.tab === name)); } for (const content of this._root.querySelectorAll(".tab-content")) { content.classList.toggle("active", content.id === `${name}-tab`); @@ -1852,7 +2044,7 @@ class KPrinterCardEditor extends HTMLElement { this._applyForm("device-form", deviceSchema(), { device: this._cfg.device || "" }); this._applyForm("entities-form", entitiesSchema(), entitiesData(this._cfg)); - this._applyForm("layout-form", layoutSchema(), layoutData(this._cfg)); + this._applyForm("layout-form", layoutSchema((key) => this._t(key)), layoutData(this._cfg)); THEME_COLOR_GROUPS.forEach((group, index) => { this._root.getElementById(`group-color-${index}`).textContent = this._t(group.title); @@ -1903,7 +2095,7 @@ class KPrinterCardEditor extends HTMLElement { } _onColorChanged(group, value) { - const defaults = KPrinterCard.getStubConfig().theme; + const defaults = KPrinterCard.defaultConfig().theme; const theme = { ...this._cfg.theme }; const wasAuto = new Map(group.fields.map((field) => [field.key, isAutoColor(this._cfg, field)])); for (const field of group.fields) { @@ -1976,9 +2168,9 @@ class KPrinterCardEditor extends HTMLElement { } const device = this._hass?.devices?.[deviceId]; const deviceName = device?.name_by_user || device?.name || ""; - // Every card starts life named "3D Printer", so that counts as unset -- + // Cards used to start life named "3D Printer", so that counts as unset -- // otherwise the field the user most expects to be filled never would be. - const nameUnset = !this._cfg.name || this._cfg.name === DEFAULT_CARD_NAME; + const nameUnset = isUnnamed(this._cfg.name); if (deviceName && (overwrite || nameUnset)) patch.name = deviceName; return patch; } @@ -2008,7 +2200,7 @@ class KPrinterCardEditor extends HTMLElement { // Reset ---------------------------------------------------------------- _resetTheme() { - const defaults = KPrinterCard.getStubConfig(); + const defaults = KPrinterCard.defaultConfig(); const cfg = { ...this._cfg, theme: { ...defaults.theme } }; for (const key of LAYOUT_RESET_KEYS) cfg[key] = defaults[key]; @@ -2042,20 +2234,36 @@ class KPrinterCardEditor extends HTMLElement { // localStorage write. The card saves the same thing again from setConfig. saveThemeToStorage(generateCardId(this._cfg), this._cfg.theme); this.dispatchEvent(new CustomEvent("config-changed", { - detail: { config: this._cfg }, + detail: { config: withoutDefaults(this._cfg, KPrinterCard.defaultConfig()) }, bubbles: true, composed: true, })); } } +// Defined last, once every module-level constant the classes read exists: +// defining the card upgrades elements already in the page on the spot, and +// THEME_COLOR_FIELDS (used by _migrateTheme) was declared after it (R46). +defineOnce(CARD_TAG, KPrinterCard); defineOnce(EDITOR_TAG, KPrinterCardEditor); try { window.customCards = window.customCards || []; - window.customCards.push({ - type: CARD_TAG, - name: "Creality Printer Card", - description: "Standalone card for Creality K-Series printers", - preview: true, - }); + // Once per page, like the element itself: a second copy of this module (two + // resource entries with different ?v=) listed the card twice in the picker. + if (!window.customCards.some((card) => card.type === CARD_TAG)) { + const pickerEntry = { + type: CARD_TAG, + name: CARD_TRANSLATIONS.en.picker_name, + description: CARD_TRANSLATIONS.en.picker_description, + preview: true, + }; + window.customCards.push(pickerEntry); + // The picker reads the entry when it opens, so the page's language can be + // applied once its strings arrive (R33). + const pageHass = document.querySelector?.("home-assistant")?.hass; + _requestI18n({}, pageHass, () => { + pickerEntry.name = _translate(pageHass, "printer_card", CARD_TRANSLATIONS, "picker_name"); + pickerEntry.description = _translate(pageHass, "printer_card", CARD_TRANSLATIONS, "picker_description"); + }); + } } catch (_) { } diff --git a/custom_components/ha_washdata/__init__.py b/custom_components/ha_washdata/__init__.py index 71499705..9007026b 100644 --- a/custom_components/ha_washdata/__init__.py +++ b/custom_components/ha_washdata/__init__.py @@ -21,6 +21,8 @@ from __future__ import annotations import asyncio import json import logging +import os +import re from pathlib import Path from typing import Any @@ -28,10 +30,23 @@ 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 HomeAssistantError, ServiceValidationError +from homeassistant.exceptions import ( + HomeAssistantError, + ServiceValidationError, + Unauthorized, +) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers import device_registry as dr +import voluptuous as vol from .const import ( + FAILED_RESTORE_STORE_SUFFIX, + NOTIFY_QUEUE_STORE_SUFFIX, + PRE_IMPORT_STORE_SUFFIX, + STORAGE_KEY, +) +from .const import ( + DEVICE_COMPLETION_THRESHOLDS, DOMAIN, CONFIG_ENTRY_MINOR_VERSION, CONFIG_ENTRY_VERSION, @@ -84,9 +99,6 @@ from .const import ( CONF_PROFILE_MATCH_INTERVAL, CONF_PROFILE_MATCH_MIN_DURATION_RATIO, CONF_PROFILE_MATCH_MAX_DURATION_RATIO, - CONF_MAX_PAST_CYCLES, - CONF_MAX_FULL_TRACES_PER_PROFILE, - CONF_MAX_FULL_TRACES_UNLABELED, CONF_WATCHDOG_INTERVAL, CONF_AUTO_TUNE_NOISE_EVENTS_THRESHOLD, CONF_COMPLETION_MIN_SECONDS, @@ -94,16 +106,11 @@ from .const import ( DEFAULT_PROFILE_MATCH_INTERVAL, DEFAULT_PROFILE_MATCH_MIN_DURATION_RATIO, DEFAULT_PROFILE_MATCH_MAX_DURATION_RATIO, - DEFAULT_MAX_PAST_CYCLES, - DEFAULT_MAX_FULL_TRACES_PER_PROFILE, - DEFAULT_MAX_FULL_TRACES_UNLABELED, - DEFAULT_WATCHDOG_INTERVAL, DEFAULT_AUTO_TUNE_NOISE_EVENTS_THRESHOLD, DEFAULT_COMPLETION_MIN_SECONDS, DEFAULT_NOTIFY_BEFORE_END_MINUTES, DEFAULT_DEVICE_TYPE, DEVICE_TYPE_OTHER, - DEFAULT_START_DURATION_THRESHOLD, CONF_START_DURATION_THRESHOLD, resolve_watchdog_interval_default, resolve_start_duration_default, @@ -155,6 +162,88 @@ def _require_str(value: Any, name: str) -> str: return value +def _svc(fields: dict[Any, Any]) -> vol.Schema: + # ALLOW_EXTRA: type the declared fields without rejecting a key an automation + # already passes (submit_cycle_feedback's `dismiss` is read but undeclared). + return vol.Schema({vol.Required("device_id"): cv.string, **fields}, extra=vol.ALLOW_EXTRA) + + +# One schema per service, mirroring services.yaml (audit PLATFORM-10): none had +# one, so `profile_name: 123` raised AttributeError, a non-numeric trim_start_s a +# ValueError traceback, and `unlabel_cycles: "false"` read as true. +def _float(value: Any) -> float: + """``vol.Coerce(float)`` that also rejects an oversized JSON integer: voluptuous + catches only ValueError/TypeError, so ``float(10**400)`` escaped as OverflowError.""" + try: + return float(value) + except (TypeError, ValueError, OverflowError) as err: + raise vol.Invalid("expected a number") from err + + +_OPT_STR = vol.Any(None, cv.string) +_OPT_NUM = vol.Any(None, _float) +_SERVICE_SCHEMAS: dict[str, vol.Schema] = { + "label_cycle": _svc({vol.Required("cycle_id"): cv.string, + vol.Optional("profile_name"): _OPT_STR}), + "create_profile": _svc({vol.Required("profile_name"): cv.string, + vol.Optional("reference_cycle_id"): _OPT_STR}), + "delete_profile": _svc({vol.Required("profile_name"): cv.string, + vol.Optional("unlabel_cycles"): cv.boolean}), + "auto_label_cycles": _svc({vol.Optional("confidence_threshold"): vol.Any(None, vol.All( + _float, vol.Range(min=0.0, max=1.0)))}), + "export_config": _svc({vol.Optional("path"): _OPT_STR}), + "import_config": _svc({vol.Required("path"): cv.string}), + "submit_cycle_feedback": vol.Schema({ + vol.Optional("device_id"): _OPT_STR, + vol.Optional("entry_id"): _OPT_STR, + vol.Required("cycle_id"): cv.string, + vol.Optional("user_confirmed"): cv.boolean, + vol.Optional("corrected_profile"): _OPT_STR, + # As services.yaml's selector: YAML automations bypass it, and NaN, inf or a + # negative value reached the stored cycle's duration. + vol.Optional("corrected_duration"): vol.Any( + None, vol.All(_float, vol.Range(min=0, max=86400)) + ), + vol.Optional("notes"): _OPT_STR, + vol.Optional("dismiss"): cv.boolean, + }, extra=vol.ALLOW_EXTRA), + "record_start": _svc({}), + "record_stop": _svc({}), + "trim_cycle": _svc({vol.Required("cycle_id"): cv.string, + vol.Optional("trim_start_s"): _float, + vol.Optional("trim_end_s"): _OPT_NUM}), + "pause_cycle": _svc({}), + "resume_cycle": _svc({}), + "mark_unloaded": _svc({}), + "trigger_ml_training": _svc({}), +} + + +def _service_manager(hass: HomeAssistant, device_id: str) -> tuple[str, Any]: + """``(entry_id, manager)`` for a service call's device (audit PLATFORM-10). + + One resolver for every service: each used to copy-paste it, nine raising a bare + ValueError (an "unknown error" plus traceback in the UI) and all taking + ``next(iter(device.config_entries))`` - an arbitrary entry of the device, not + necessarily ours. + """ + device = dr.async_get(hass).async_get(device_id) + if not device: + raise ServiceValidationError( + translation_domain=DOMAIN, translation_key="device_not_found" + ) + loaded = hass.data.get(DOMAIN, {}) + entry_id = next((eid for eid in device.config_entries if eid in loaded), None) + if entry_id is None: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key=( + "integration_not_loaded" if device.config_entries else "no_config_entry" + ), + ) + return entry_id, loaded[entry_id] + + # Options keys that no code reads any more, stripped by the 3.10 -> 3.11 step and # again by the one-pass legacy migration. Named once so the two cannot drift. # The value `DEFAULT_PROFILE_MATCH_MAX_DURATION_RATIO` held before item 311 @@ -167,6 +256,36 @@ _DEAD_ABRUPT_KEYS = frozenset( ) +def _heal_seeded_cadence(options: dict[str, Any], device_type: Any) -> list[str]: + """Replace the seeded 30/5 cadence with the device default (#396), in place. + + The pre-3.9 legacy migration seeded watchdog_interval=30 and + start_duration_threshold=5. On the coarse (30 s) sampling device types those + fall below the panel's watchdog>=2*sampling and start_duration>=sampling gates, + so they are replaced with the device-resolved default, but ONLY where they still + equal the old scalar default: a value the migration seeded, never a deliberate + choice. An absent key stays absent, so the runtime default applies. + + One helper for the 3.9 -> 3.10 step and the one-pass legacy path (audit + PLATFORM-09): the bulk path copied the 3.10 -> 3.11 heal but not this one, so a + dryer migrating from 3.1 or 3.5 kept 30/5 while the same entry at 3.9 got 61/30. + A falsy device type means DEFAULT_DEVICE_TYPE, not the coarse scalar fallback. + """ + dev = device_type or DEFAULT_DEVICE_TYPE + healed: list[str] = [] + if options.get(CONF_WATCHDOG_INTERVAL) == 30: + resolved = resolve_watchdog_interval_default(dev) + if resolved != 30: + options[CONF_WATCHDOG_INTERVAL] = resolved + healed.append(CONF_WATCHDOG_INTERVAL) + if options.get(CONF_START_DURATION_THRESHOLD) == 5: + resolved_start = resolve_start_duration_default(dev) + if resolved_start != 5: + options[CONF_START_DURATION_THRESHOLD] = resolved_start + healed.append(CONF_START_DURATION_THRESHOLD) + return healed + + async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Migrate config entry to the latest version while preserving settings.""" _log = DeviceLoggerAdapter(_LOGGER, entry.title) @@ -229,36 +348,17 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _log.debug("Migrated WashData entry from 3.8 to 3.9 (no null options)") # 3.9 → 3.10: heal cadence defaults that violate the panel's own conflict rules - # (#396). The pre-3.9 legacy migration seeded watchdog_interval=30 and - # start_duration_threshold=5 into options. With sampling_interval now resolved - # per device type, those seeded values fall below the watchdog>=2*sampling and - # start_duration>=sampling gates on the coarse (30 s) sampling device types. - # Replace them with the device-resolved default ONLY where they still equal the - # old scalar default (30 / 5) - i.e. a value the migration seeded, never a - # deliberate user choice (both violated the rule). A never-seeded (absent) key - # is left absent so the runtime device-resolved default applies. + # (#396); see _heal_seeded_cadence, which the one-pass legacy path shares. if version == 3 and minor_version == 9: new_opts = dict(entry.options) # `or` (not `.get(..., default)`) so a present-but-null device type also falls # through to the data value / DEFAULT_DEVICE_TYPE: a null would otherwise resolve # to the coarse scalar defaults and wrongly heal a washing-machine-equivalent # entry's 30/5 up to 61/30. - _dt = ( - new_opts.get(CONF_DEVICE_TYPE) - or entry.data.get(CONF_DEVICE_TYPE) - or DEFAULT_DEVICE_TYPE + _healed = _heal_seeded_cadence( + new_opts, + new_opts.get(CONF_DEVICE_TYPE) or entry.data.get(CONF_DEVICE_TYPE), ) - _healed = [] - if new_opts.get(CONF_WATCHDOG_INTERVAL) == 30: - _resolved = resolve_watchdog_interval_default(_dt) - if _resolved != 30: - new_opts[CONF_WATCHDOG_INTERVAL] = _resolved - _healed.append(CONF_WATCHDOG_INTERVAL) - if new_opts.get(CONF_START_DURATION_THRESHOLD) == 5: - _resolved = resolve_start_duration_default(_dt) - if _resolved != 5: - new_opts[CONF_START_DURATION_THRESHOLD] = _resolved - _healed.append(CONF_START_DURATION_THRESHOLD) # LITERAL 10, not CONFIG_ENTRY_MINOR_VERSION, like every other step. These # blocks form a chain - each advances minor_version to exactly N+1 so the next # block picks it up - so a step that wrote "whatever is current" would, after a @@ -387,13 +487,6 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # lands. The min ratio above keeps its seed because that gate is inert for # ranking (it has never removed a true candidate on the corpus), so freezing # it costs nothing. - options.setdefault(CONF_MAX_PAST_CYCLES, DEFAULT_MAX_PAST_CYCLES) - options.setdefault( - CONF_MAX_FULL_TRACES_PER_PROFILE, DEFAULT_MAX_FULL_TRACES_PER_PROFILE - ) - options.setdefault( - CONF_MAX_FULL_TRACES_UNLABELED, DEFAULT_MAX_FULL_TRACES_UNLABELED - ) options.setdefault( CONF_WATCHDOG_INTERVAL, resolve_watchdog_interval_default(options[CONF_DEVICE_TYPE]), @@ -401,7 +494,14 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: options.setdefault( CONF_AUTO_TUNE_NOISE_EVENTS_THRESHOLD, DEFAULT_AUTO_TUNE_NOISE_EVENTS_THRESHOLD ) - options.setdefault(CONF_COMPLETION_MIN_SECONDS, DEFAULT_COMPLETION_MIN_SECONDS) + # The device's own floor, not the scalar 600 s: seeded for a pump it made every + # run shorter than 10 min `interrupted` (audit DETECT-07). + options.setdefault( + CONF_COMPLETION_MIN_SECONDS, + DEVICE_COMPLETION_THRESHOLDS.get( + options[CONF_DEVICE_TYPE], DEFAULT_COMPLETION_MIN_SECONDS + ), + ) options.setdefault( CONF_NOTIFY_BEFORE_END_MINUTES, DEFAULT_NOTIFY_BEFORE_END_MINUTES ) @@ -485,6 +585,11 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # so no stale removed value can linger there; the flow/manager read it from # options (options-first). + # Same heal as the 3.9 -> 3.10 step (audit PLATFORM-09), after the remap so it + # resolves against the device type the entry ends up on. The `setdefault` above + # keeps a stored 30/5, which is exactly what the chain heals. + _heal_seeded_cadence(options, options.get(CONF_DEVICE_TYPE)) + # Strip the now-retired running_dead_zone from options in the bulk path too # (covers entries that migrate straight from v1/v2/early-v3 in one pass). options.pop(CONF_RUNNING_DEAD_ZONE, None) @@ -559,7 +664,7 @@ async def _async_preload_ml_modules(hass: HomeAssistant) -> None: """Import the ML modules off the event loop (issue #328). ``ml.engine.resolve_scorer`` / ``resolve_regressor`` are called from the event - loop (live matching, end detection, quality gating), and Home Assistant flags + loop (end detection, ETA / energy projection), and Home Assistant flags the lazy ``importlib.import_module`` they used to do there as a blocking call. 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 @@ -627,6 +732,14 @@ async def _async_setup_shared( the panel logs and retries on the next setup), so a missing UI never fails the entry. """ + # Files left behind by appliances deleted before 0.5.8 (no remove hook then): + # swept once per start, in the background, so setup never waits on disk I/O. + if not hass.data.get(_ORPHAN_SWEEP_KEY): + hass.data[_ORPHAN_SWEEP_KEY] = True + hass.async_create_background_task( + _async_sweep_orphaned_stores(hass), "ha_washdata orphaned store sweep" + ) + # 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" @@ -727,6 +840,82 @@ async def _async_setup_shared( hass.data["ha_washdata_intents_registered"] = True +# Panel access levels, lowest first (ws_api._effective_level resolves a user's). +_SERVICE_LEVEL_ORDER = {"none": 0, "read": 1, "edit": 2, "full": 3} + +# Services that read or write files / replace a whole device's data: admin-only, +# like their WS twins in ws_api._ADMIN_COMMANDS. +_ADMIN_SERVICES = frozenset({"export_config", "import_config", "trigger_ml_training"}) + + +def _service_entry_ids(hass: HomeAssistant, call: ServiceCall) -> list[str | None]: + """Every config entry a service call names: its ``entry_id`` and its device's. + + Both are returned, and the caller authorizes each, because handlers differ in + which one they act on (``submit_cycle_feedback`` prefers ``entry_id``, every + other handler resolves ``device_id``, and the schemas allow extra keys). Checking + only ``entry_id`` let a user with edit access to entry B pass B's id beside + entry A's device and change A. ``[None]`` when the call names no entry. + """ + ids: list[str | None] = [] + entry_id = call.data.get("entry_id") + if isinstance(entry_id, str) and entry_id: + ids.append(entry_id) + device_id = call.data.get("device_id") + if isinstance(device_id, str) and device_id: + device = dr.async_get(hass).async_get(device_id) + if device is not None: + loaded = hass.data.get(DOMAIN, {}) + dev_entry = next( + (e for e in device.config_entries if e in loaded), + next(iter(device.config_entries), None), + ) + if dev_entry is not None and dev_entry not in ids: + ids.append(dev_entry) + return ids or [None] + + +async def _async_check_service_access( + hass: HomeAssistant, call: ServiceCall, level: str +) -> None: + """Authorize a service call the way the panel authorizes its WS twin. + + Every service used to skip both the WS admin gate and the panel RBAC: a + read-only user could run ``import_config`` (replace all data) or + ``export_config`` (write the store under ``/config/www``, served without + login at ``/local/``), and an RBAC "read" user could delete profiles + (audit PLATFORM-03). Calls without a user (automations, scripts, the system) + stay allowed, as for every HA admin service. + """ + user_id = call.context.user_id + if user_id is None: + return + user = await hass.auth.async_get_user(user_id) + if user is None: + raise Unauthorized(context=call.context) + if user.is_admin: + return + if level == "admin": + raise Unauthorized(context=call.context) + from .ws_api import _effective_level # pylint: disable=import-outside-toplevel + + for entry_id in _service_entry_ids(hass, call): + granted = _effective_level(hass, user, entry_id) + if _SERVICE_LEVEL_ORDER.get(granted, 0) < _SERVICE_LEVEL_ORDER.get(level, 2): + raise Unauthorized(context=call.context) + + +def _guarded_service(hass: HomeAssistant, name: str, handler: Any) -> Any: + """Wrap a service handler with the access check for its level.""" + level = "admin" if name in _ADMIN_SERVICES else "edit" + + async def _wrapped(call: ServiceCall) -> Any: + await _async_check_service_access(hass, call, level) + return await handler(call) + + return _wrapped + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up WashData from a config entry.""" _log = DeviceLoggerAdapter(_LOGGER, entry.title) @@ -812,6 +1001,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) ent_reg.async_remove(old_entity) + # Heal a non-numeric numeric setting stored by an older version (audit + # PLATFORM-13 / register item 279): it raised in the manager's constructor, + # so the entry could never set up again. Dropped, so its default applies. + from .const import drop_invalid_numeric_options # pylint: disable=import-outside-toplevel + + _clean, _dropped = drop_invalid_numeric_options(dict(entry.options)) + if _dropped: + _log.warning( + "Dropped non-numeric value(s) for %s; their defaults apply", ", ".join(_dropped) + ) + hass.config_entries.async_update_entry(entry, options=_clean) + # pylint: disable=import-outside-toplevel from .manager import WashDataManager @@ -840,21 +1041,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def handle_label_cycle(call: ServiceCall) -> None: device_id = _require_str(call.data.get("device_id"), "device_id") cycle_id = _require_str(call.data.get("cycle_id"), "cycle_id") - profile_name = call.data.get("profile_name", "").strip() + profile_name = (call.data.get("profile_name") or "").strip() # Find the config entry for this device - registry = dr.async_get(hass) - device = registry.async_get(device_id) - if not device: - raise ValueError("Device not found") - - entry_id = next(iter(device.config_entries), None) - if not entry_id: - raise ValueError("No config entry found for device") - if entry_id not in hass.data[DOMAIN]: - raise ValueError("Integration not loaded for this device") - - manager = hass.data[DOMAIN][entry_id] + entry_id, manager = _service_manager(hass, device_id) # Assign existing profile or remove label target_profile = profile_name if profile_name else None @@ -878,7 +1068,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: manager.notify_update() - hass.services.async_register(DOMAIN, "label_cycle", handle_label_cycle) + hass.services.async_register( + DOMAIN, "label_cycle", _guarded_service(hass, "label_cycle", handle_label_cycle), + schema=_SERVICE_SCHEMAS["label_cycle"] + ) # Register create_profile service if not hass.services.has_service(DOMAIN, "create_profile"): @@ -888,18 +1081,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: profile_name = _require_str(call.data.get("profile_name"), "profile_name") reference_cycle_id = call.data.get("reference_cycle_id") - registry = dr.async_get(hass) - device = registry.async_get(device_id) - if not device: - raise ValueError("Device not found") - - entry_id = next(iter(device.config_entries), None) - if not entry_id: - raise ValueError("No config entry found for device") - if entry_id not in hass.data[DOMAIN]: - raise ValueError("Integration not loaded for this device") - - manager = hass.data[DOMAIN][entry_id] + entry_id, manager = _service_manager(hass, device_id) try: await manager.profile_store.create_profile_standalone( profile_name, reference_cycle_id @@ -912,7 +1094,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) from exc manager.notify_update() - hass.services.async_register(DOMAIN, "create_profile", handle_create_profile) + hass.services.async_register( + DOMAIN, "create_profile", _guarded_service(hass, "create_profile", handle_create_profile), + schema=_SERVICE_SCHEMAS["create_profile"] + ) # Register delete_profile service if not hass.services.has_service(DOMAIN, "delete_profile"): @@ -922,55 +1107,48 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: profile_name = _require_str(call.data.get("profile_name"), "profile_name") unlabel_cycles = call.data.get("unlabel_cycles", True) - registry = dr.async_get(hass) - device = registry.async_get(device_id) - if not device: - raise ValueError("Device not found") - - entry_id = next(iter(device.config_entries), None) - if not entry_id: - raise ValueError("No config entry found for device") - if entry_id not in hass.data[DOMAIN]: - raise ValueError("Integration not loaded for this device") - - manager = hass.data[DOMAIN][entry_id] + entry_id, manager = _service_manager(hass, device_id) await manager.profile_store.delete_profile(profile_name, unlabel_cycles) manager.notify_update() - hass.services.async_register(DOMAIN, "delete_profile", handle_delete_profile) + hass.services.async_register( + DOMAIN, "delete_profile", _guarded_service(hass, "delete_profile", handle_delete_profile), + schema=_SERVICE_SCHEMAS["delete_profile"] + ) # Register auto_label_cycles service if not hass.services.has_service(DOMAIN, "auto_label_cycles"): async def handle_auto_label_cycles(call: ServiceCall) -> None: device_id = _require_str(call.data.get("device_id"), "device_id") - confidence_threshold = call.data.get("confidence_threshold", 0.75) + confidence_threshold = call.data.get("confidence_threshold") - registry = dr.async_get(hass) - device = registry.async_get(device_id) - if not device: - raise ValueError("Device not found") + entry_id, _manager = _service_manager(hass, device_id) - entry_id = next(iter(device.config_entries), None) - if not entry_id: - raise ValueError("No config entry found for device") - if entry_id not in hass.data[DOMAIN]: - raise ValueError("Integration not loaded for this device") - - manager = hass.data[DOMAIN][entry_id] - stats = await manager.profile_store.auto_label_cycles( - confidence_threshold + # The same registry task the panel starts (audit PLATFORM-05): under + # the write lock, visible as a header pill, cancellable; awaited so an + # automation step still waits for the result. + from .ws_api import ( # noqa: PLC0415 + configured_auto_label_threshold, + start_auto_label_task, ) - manager.notify_update() - manager._logger.info( - "Auto-label complete: %s labeled, %s skipped", - stats["labeled"], - stats["skipped"], - ) + # Omitted: the device's own Auto-Label Confidence, as the WS command + # does (audit UI-10), not a hardcoded 0.75 that ignored the setting. + if confidence_threshold is None: + confidence_threshold = configured_auto_label_threshold( + hass.config_entries.async_get_entry(entry_id) + ) + + _task, raw = start_auto_label_task(hass, entry_id, float(confidence_threshold)) + if raw is not None: + await raw hass.services.async_register( - DOMAIN, "auto_label_cycles", handle_auto_label_cycles + DOMAIN, + "auto_label_cycles", + _guarded_service(hass, "auto_label_cycles", handle_auto_label_cycles), + schema=_SERVICE_SCHEMAS["auto_label_cycles"], ) # Register trim_cycle service @@ -981,27 +1159,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: cycle_id = _require_str(call.data.get("cycle_id"), "cycle_id") trim_start_s = max(0.0, float(call.data.get("trim_start_s", 0))) - registry = dr.async_get(hass) - device = registry.async_get(device_id) - if not device: - raise ServiceValidationError( - translation_domain=DOMAIN, - translation_key="device_not_found", - ) - - entry_id = next(iter(device.config_entries), None) - if not entry_id: - raise ServiceValidationError( - translation_domain=DOMAIN, - translation_key="no_config_entry", - ) - if entry_id not in hass.data[DOMAIN]: - raise ServiceValidationError( - translation_domain=DOMAIN, - translation_key="integration_not_loaded", - ) - - manager = hass.data[DOMAIN][entry_id] + entry_id, manager = _service_manager(hass, device_id) store = manager.profile_store # Determine trim end - default to full cycle duration if not supplied @@ -1032,14 +1190,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) manager.notify_update() - hass.services.async_register(DOMAIN, "trim_cycle", handle_trim_cycle) + hass.services.async_register( + DOMAIN, "trim_cycle", _guarded_service(hass, "trim_cycle", handle_trim_cycle), + schema=_SERVICE_SCHEMAS["trim_cycle"] + ) # 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. + # hass.http. It is a hard manifest dependency (item 487), so HA sets it up + # before this entry; a caller that invokes async_setup_entry directly skips + # that, and there the platform forward is what processes the manifest. + # 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 @@ -1057,26 +1218,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if entry_id is None: # Prefer device_id for user-facing workflows. device_id = _require_str(device_id_raw, "device_id") - registry = dr.async_get(hass) - device = registry.async_get(device_id) - if not device: - raise ValueError("Device not found") - entry_id = next(iter(device.config_entries), None) - if not entry_id: - raise ValueError("No config entry found for device") - - if not entry_id: - raise ValueError("entry_id or device_id is required") + entry_id, _manager = _service_manager(hass, device_id) cycle_id = _require_str(call.data.get("cycle_id"), "cycle_id") user_confirmed = call.data.get("user_confirmed", False) corrected_profile = call.data.get("corrected_profile") corrected_duration = call.data.get("corrected_duration") # in seconds - notes = call.data.get("notes", "") + notes = call.data.get("notes") or "" dismiss = call.data.get("dismiss", False) if entry_id not in hass.data[DOMAIN]: - raise ValueError("Integration not loaded for this entry") + raise ServiceValidationError( + translation_domain=DOMAIN, translation_key="integration_not_loaded" + ) manager = hass.data[DOMAIN][entry_id] success = await manager.learning_manager.async_submit_cycle_feedback( @@ -1104,7 +1258,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.services.async_register( DOMAIN, SERVICE_SUBMIT_FEEDBACK.rsplit(".", maxsplit=1)[-1], - handle_submit_feedback, + _guarded_service(hass, "submit_cycle_feedback", handle_submit_feedback), + schema=_SERVICE_SCHEMAS["submit_cycle_feedback"], ) # Export store to file (per entry/device) @@ -1114,18 +1269,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_id = _require_str(call.data.get("device_id"), "device_id") file_path = call.data.get("path") - registry = dr.async_get(hass) - device = registry.async_get(device_id) - if not device: - raise ValueError("Device not found") - - entry_id = next(iter(device.config_entries), None) - if not entry_id: - raise ValueError("No config entry found for device") - if entry_id not in hass.data[DOMAIN]: - raise ValueError("Integration not loaded for this device") - - manager = hass.data[DOMAIN][entry_id] + entry_id, manager = _service_manager(hass, device_id) entry = hass.config_entries.async_get_entry(entry_id) if entry is None: raise ValueError(f"Config entry not found: {entry_id}") @@ -1155,8 +1299,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # existing file even when is_allowed_path() accepts it; exclusive # creation ("x") makes that no-overwrite check atomic (no TOCTOU # window). The default generated path may be re-written freely. + # Serialised like the WS export (audit PERF-11): HA's orjson encoder, + # compact. indent=2 put every power-trace number on its own line. + from .ws_api import _export_json # noqa: PLC0415 + def _dump_and_write(): - text = json.dumps(payload, indent=2) + text = _export_json(payload) try: if file_path: # Exclusive creation ("x") makes the no-overwrite check @@ -1185,7 +1333,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.async_add_executor_job(_dump_and_write) manager._logger.info("Exported ha_washdata entry %s to %s", entry_id, target) - hass.services.async_register(DOMAIN, "export_config", handle_export_config) + hass.services.async_register( + DOMAIN, "export_config", _guarded_service(hass, "export_config", handle_export_config), + schema=_SERVICE_SCHEMAS["export_config"] + ) # Import store from file into the target entry/device if not hass.services.has_service(DOMAIN, "import_config"): @@ -1197,18 +1348,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not file_path: raise ValueError("path is required for import") - registry = dr.async_get(hass) - device = registry.async_get(device_id) - if not device: - raise ValueError("Device not found") - - entry_id = next(iter(device.config_entries), None) - if not entry_id: - raise ValueError("No config entry found for device") - if entry_id not in hass.data[DOMAIN]: - raise ValueError("Integration not loaded for this device") - - manager = hass.data[DOMAIN][entry_id] + entry_id, manager = _service_manager(hass, device_id) entry = hass.config_entries.async_get_entry(entry_id) if entry is None: raise ValueError(f"Config entry not found: {entry_id}") @@ -1236,36 +1376,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except Exception as err: # noqa: BLE001 raise ValueError(f"Failed to read import file: {err}") from err - config_updates = await manager.profile_store.async_import_data(payload) + # Same path as the WS import (audit PLATFORM-02): under the per-entry + # write lock, local sensor/device bindings dropped, options written + # under their lock with a changelog entry, entry.data left alone. The + # service used to copy the exporter's power and door sensors over this + # device's, which silently disconnects it (register item 317). + from .ws_api import ( # pylint: disable=import-outside-toplevel + _entry_write_lock, + async_apply_imported_entry_options, + ) - # Apply imported settings to config entry if present - entry_data = config_updates.get("entry_data", {}) - entry_options = config_updates.get("entry_options", {}) - - if entry_data or entry_options: - new_data: dict[str, Any] = dict(entry.data) - new_options: dict[str, Any] = dict(entry.options) - - # Only update min_power/off_delay from data (don't overwrite power_sensor/name) - for key in [CONF_MIN_POWER, CONF_OFF_DELAY]: - if key in entry_data: - new_data[key] = entry_data[key] - - # Update all options from import, then strip any option persisted as - # null: an export taken from an already-broken entry (or a hand-edited - # file) can carry a `null`, which `.get()` hands back verbatim and the - # numeric casts that build CycleDetectorConfig then raise on - the #389 - # bricked-setup failure. The WS import paths already do this; the legacy - # import_config service is the last writer that did not. - new_options.update(entry_options) - new_options = strip_null_options(new_options) - - hass.config_entries.async_update_entry( - entry, - data=new_data, - options=new_options, + async with _entry_write_lock(hass, entry_id): + # This device's options ride along so "Undo last import" in the + # panel can put them back (register item 195). + config_updates = await manager.profile_store.async_import_data( + payload, + entry_options=dict(entry.options), + source="import_config_service", ) - manager._logger.info("Applied imported settings to config entry %s", entry_id) + if config_updates: + await async_apply_imported_entry_options( + hass, entry, config_updates, "import_config_service" + ) + manager._logger.info( + "Applied imported settings to config entry %s", entry_id + ) # An old payload re-arms the one-time banked-tail repair. Nothing here # reloads the entry unless the payload brought settings with it, so @@ -1274,40 +1409,33 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: manager._logger.info("Imported ha_washdata entry %s from %s", entry_id, source) - hass.services.async_register(DOMAIN, "import_config", handle_import_config) + hass.services.async_register( + DOMAIN, "import_config", _guarded_service(hass, "import_config", handle_import_config), + schema=_SERVICE_SCHEMAS["import_config"] + ) # Register recorder services if not hass.services.has_service(DOMAIN, "record_start"): async def handle_record_start(call: ServiceCall) -> None: device_id = _require_str(call.data.get("device_id"), "device_id") - registry = dr.async_get(hass) - device = registry.async_get(device_id) - if not device: - raise ValueError("Device not found") - entry_id = next(iter(device.config_entries), None) - if not entry_id or entry_id not in hass.data[DOMAIN]: - raise ValueError("Integration not loaded") - - manager = hass.data[DOMAIN][entry_id] + entry_id, manager = _service_manager(hass, device_id) await manager.async_start_recording() - hass.services.async_register(DOMAIN, "record_start", handle_record_start) + hass.services.async_register( + DOMAIN, "record_start", _guarded_service(hass, "record_start", handle_record_start), + schema=_SERVICE_SCHEMAS["record_start"] + ) if not hass.services.has_service(DOMAIN, "record_stop"): async def handle_record_stop(call: ServiceCall) -> None: device_id = _require_str(call.data.get("device_id"), "device_id") - registry = dr.async_get(hass) - device = registry.async_get(device_id) - if not device: - raise ValueError("Device not found") - entry_id = next(iter(device.config_entries), None) - if not entry_id or entry_id not in hass.data[DOMAIN]: - raise ValueError("Integration not loaded") - - manager = hass.data[DOMAIN][entry_id] + entry_id, manager = _service_manager(hass, device_id) await manager.async_stop_recording() - hass.services.async_register(DOMAIN, "record_stop", handle_record_stop) + hass.services.async_register( + DOMAIN, "record_stop", _guarded_service(hass, "record_stop", handle_record_stop), + schema=_SERVICE_SCHEMAS["record_stop"] + ) # Register on-device ML training trigger (Stage 4, gated by ENABLE_ML_TRAINING) from .const import ENABLE_ML_TRAINING, SERVICE_TRIGGER_ML_TRAINING @@ -1317,20 +1445,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ): async def handle_trigger_ml_training(call: ServiceCall) -> None: device_id = _require_str(call.data.get("device_id"), "device_id") - registry = dr.async_get(hass) - device = registry.async_get(device_id) - if not device: - raise ValueError("Device not found") - entry_id = next(iter(device.config_entries), None) - if not entry_id or entry_id not in hass.data[DOMAIN]: - raise ValueError("Integration not loaded") - - manager = hass.data[DOMAIN][entry_id] + entry_id, manager = _service_manager(hass, device_id) summary = await manager.async_run_ml_training(force=True) manager._logger.info("Manual ML training: %s", summary) hass.services.async_register( - DOMAIN, SERVICE_TRIGGER_ML_TRAINING, handle_trigger_ml_training + DOMAIN, + SERVICE_TRIGGER_ML_TRAINING, + _guarded_service(hass, "trigger_ml_training", handle_trigger_ml_training), + schema=_SERVICE_SCHEMAS["trigger_ml_training"], ) # Register pause/resume services @@ -1367,7 +1490,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: translation_key="no_active_cycle", ) - hass.services.async_register(DOMAIN, "pause_cycle", handle_pause_cycle) + hass.services.async_register( + DOMAIN, "pause_cycle", _guarded_service(hass, "pause_cycle", handle_pause_cycle), + schema=_SERVICE_SCHEMAS["pause_cycle"] + ) if not hass.services.has_service(DOMAIN, "resume_cycle"): async def handle_resume_cycle(call: ServiceCall) -> None: @@ -1402,7 +1528,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: translation_key="no_active_cycle", ) - hass.services.async_register(DOMAIN, "resume_cycle", handle_resume_cycle) + hass.services.async_register( + DOMAIN, "resume_cycle", _guarded_service(hass, "resume_cycle", handle_resume_cycle), + schema=_SERVICE_SCHEMAS["resume_cycle"] + ) # Unload confirmation for a device with no door sensor (#451). Deliberately # not an error when nothing is waiting: an automation wired to a physical @@ -1434,7 +1563,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry_id].mark_unloaded("mark_unloaded service") - hass.services.async_register(DOMAIN, "mark_unloaded", handle_mark_unloaded) + hass.services.async_register( + DOMAIN, "mark_unloaded", _guarded_service(hass, "mark_unloaded", handle_mark_unloaded), + schema=_SERVICE_SCHEMAS["mark_unloaded"] + ) return True @@ -1491,7 +1623,7 @@ def _apply_device_link(hass: HomeAssistant, entry: ConfigEntry) -> None: registry.async_update_device( washdata_device.id, via_device_id=linked_device_id ) - except (HomeAssistantError, ValueError) as err: + except (HomeAssistantError, ValueError, OverflowError) 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 @@ -1510,14 +1642,102 @@ async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: # Update configuration without interrupting detector await manager.async_reload_config(entry) # Options changes (e.g. linked device) reload in place without - # recreating entities, so apply the device link explicitly here. + # recreating entities, so apply the device link explicitly here, and add or + # drop the pump-only sensor if the device type changed. _apply_device_link(hass, entry) + from .sensor import async_reconcile_device_type_sensors # pylint: disable=import-outside-toplevel + + async_reconcile_device_type_sensors(hass, manager, entry) else: # Full reload if manager not found await async_unload_entry(hass, entry) await async_setup_entry(hass, entry) +_ORPHAN_SWEEP_KEY = "ha_washdata_orphan_sweep" +# Per-appliance store keys: the profile store, its active-cycle snapshot (0.5.8), its +# pre-import restore point (register item 195), its held-notification queue (audit +# MANAGER-16), its last failed snapshot restore (item 266) and the manual recorder. Global keys +# (``ha_washdata_panel``, ``ha_washdata_online``) use an underscore and never match. +_ENTRY_STORE_RE = re.compile( + r"^ha_washdata\.(?:recorder\.)?([0-9A-Za-z]{20,40})" + rf"(?:\.active|\.{PRE_IMPORT_STORE_SUFFIX}|\.{NOTIFY_QUEUE_STORE_SUFFIX}" + rf"|\.{FAILED_RESTORE_STORE_SUFFIX})?$" +) + + +def _entry_store_keys(entry_id: str) -> list[str]: + return [ + f"{STORAGE_KEY}.{entry_id}", + f"{STORAGE_KEY}.{entry_id}.active", + f"{STORAGE_KEY}.{entry_id}.{PRE_IMPORT_STORE_SUFFIX}", + f"{STORAGE_KEY}.{entry_id}.{NOTIFY_QUEUE_STORE_SUFFIX}", + f"{STORAGE_KEY}.{entry_id}.{FAILED_RESTORE_STORE_SUFFIX}", + f"{STORAGE_KEY}.recorder.{entry_id}", + ] + + +async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Delete an appliance's stored data when the user deletes the appliance. + + Until 0.5.8 there was no remove hook, so every deleted appliance left its + profiles, cycles and traces in ``.storage`` for good (6.9 MB from 15 deleted + devices on one install). HA calls this only for a deliberate delete, after the + entry has unloaded; an unload or a reload never reaches it. + """ + from homeassistant.helpers.storage import Store # noqa: PLC0415 + + for key in _entry_store_keys(entry.entry_id): + try: + await Store(hass, 1, key).async_remove() + except Exception: # noqa: BLE001 - a leftover file must not block the delete + _LOGGER.warning("Could not delete WashData store %s", key, exc_info=True) + + +async def _async_sweep_orphaned_stores(hass: HomeAssistant) -> None: + """Delete per-appliance store files whose config entry no longer exists. + + Every entry (loaded, disabled or failed) is listed by the config-entries + manager before any integration sets up, so a file whose id matches none of them + belongs to an appliance the user deleted. Never raises. + """ + try: + known = {e.entry_id for e in hass.config_entries.async_entries(DOMAIN)} + if not known: + return + storage_dir = hass.config.path(".storage") + + def _orphans() -> list[tuple[str, int]]: + out = [] + for name in os.listdir(storage_dir): + m = _ENTRY_STORE_RE.match(name) + if m and m.group(1) not in known: + try: + out.append((name, os.path.getsize(os.path.join(storage_dir, name)))) + except OSError: + continue + return out + + orphans = await hass.async_add_executor_job(_orphans) + if not orphans: + return + from homeassistant.helpers.storage import Store # noqa: PLC0415 + + removed = 0 + for name, _size in orphans: + try: + await Store(hass, 1, name).async_remove() + removed += 1 + except Exception: # noqa: BLE001 + _LOGGER.debug("Could not delete orphaned store %s", name, exc_info=True) + _LOGGER.info( + "Deleted %d WashData store file(s) left by deleted appliances (%.1f MB)", + removed, sum(size for _n, size in orphans) / 1e6, + ) + except Exception: # noqa: BLE001 + _LOGGER.debug("Orphaned store sweep failed", exc_info=True) + + 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): diff --git a/custom_components/ha_washdata/analysis.py b/custom_components/ha_washdata/analysis.py index 79a3ac80..00b2d168 100644 --- a/custom_components/ha_washdata/analysis.py +++ b/custom_components/ha_washdata/analysis.py @@ -20,6 +20,8 @@ from __future__ import annotations import logging from typing import Any, Optional +import math + import numpy as np from .const import ( @@ -36,21 +38,22 @@ from .const import ( MATCH_DTW_RESAMPLE_N, MATCH_DURATION_SCALE, MATCH_DURATION_SCALE_OVERRUN, + MATCH_PREFIX_MIN_POINTS, MATCH_PREFIX_SHAPE_MAX_RATIO, MATCH_DURATION_WEIGHT, + MATCH_DURATION_WEIGHT_IN_PROGRESS, + MATCH_MIN_RATIO_GRACE_S, MATCH_ENERGY_SCALE, + MATCH_ENERGY_REF_MIN_CYCLES, MATCH_ENERGY_WEIGHT, MATCH_KEEP_MIN_SCORE, MATCH_MAE_PEAK_FLOOR, MATCH_MAE_REF_PEAK, MATCH_MAE_SCALE, MAX_ALIGN_GRID_POINTS, - SMART_TERM_PREFIX_MAX_CANDIDATES, - SMART_TERM_PREFIX_MIN_COVERAGE, - SMART_TERM_PREFIX_MIN_POINTS, - SMART_TERM_PREFIX_MIN_RATIO, STAGE4_INTEGRATED_ENERGY_DEVICE_TYPES, ) +from .signal_processing import integrate_wh def stage4_energy_mode(device_type: str | None) -> str: @@ -157,21 +160,35 @@ def find_best_alignment( best_mae = float("inf") final_offset = best_offset + # ~0.2 n^2 element work on a complete cycle (audit MATCH-CORE-11). It is + # byte-identical to the old ``np.mean(np.abs(c_seg - r_seg))``: the same + # subtract, abs and pairwise ``add.reduce`` divided by the count, written into + # one reused buffer. Allocating three fresh n-element arrays per offset, and + # np.mean's Python wrapper, cost more than the arithmetic. There is no exact + # shortcut for an L1 distance over shifts, and a 2-D batch over offsets was + # measured slower (the overlap length differs per offset). Any other dtype + # keeps the old expression (np.mean sums an integer array through a cast). + buf = ( + np.empty(min(n_curr, n_ref)) + if curr.dtype == np.float64 and ref.dtype == np.float64 + else None + ) for off in range(int(min_off), int(max_off) + 1): # intersection c_start = max(0, off) c_end = min(n_curr, n_ref + off) - - r_start = max(0, -off) - r_end = min(n_ref, n_curr - off) - - if (c_end - c_start) < 10: + length = c_end - c_start + if length < 10: continue + r_start = max(0, -off) - c_seg = curr[c_start:c_end] - r_seg = ref[r_start:r_end] - - mae = np.mean(np.abs(c_seg - r_seg)) + if buf is None: + mae = np.mean(np.abs(curr[c_start:c_end] - ref[r_start:r_start + length])) + else: + seg = buf[:length] + np.subtract(curr[c_start:c_end], ref[r_start:r_start + length], out=seg) + np.abs(seg, out=seg) + mae = np.add.reduce(seg) / length if mae < best_mae: best_mae = mae final_offset = off @@ -198,8 +215,10 @@ def find_best_alignment( corr = 0.0 # Scale-invariant MAE: express the error relative to the current cycle's - # peak (common to every candidate, so ranking is unaffected) and calibrate - # to the legacy behaviour at MATCH_MAE_REF_PEAK. See const.py for rationale. + # peak and calibrate to the legacy behaviour at MATCH_MAE_REF_PEAK (see + # const.py). On a complete cycle the peak is common to every candidate, so + # ranking is unaffected; in prefix mode the compared slice, and so its peak, + # depends on each candidate's span (deep-dive 02, audit MR-09). current_peak = float(np.max(np.abs(curr))) if curr.size else 0.0 scaled_mae = mae * MATCH_MAE_REF_PEAK / max(current_peak, MATCH_MAE_PEAK_FLOOR) mae_score = MATCH_MAE_SCALE / (MATCH_MAE_SCALE + scaled_mae) @@ -250,7 +269,7 @@ def compute_dtw_lite( for each row are written back as a single slice assignment. For the typical matching case (n=m=200, band=0.1 → w=20, ~41 cells/row) this is ~1.9× faster than the original element-by-element NumPy indexing loop. The anti-diagonal - vectorized fill from :func:`_dtw_cost_matrix_vectorized` is NOT used here + vectorized fill from :func:`_dtw_cost_banded` is NOT used here because its per-diagonal Python setup overhead dominates for small n (it is 2× *slower* than the scalar loop for n=200 — the opposite of its large-n envelope-rebuild behaviour where it wins by 1.6–8×). @@ -317,6 +336,85 @@ def compute_dtw_lite( _LOGGER.debug("compute_dtw_lite vectorized path failed; using scalar fallback", exc_info=True) return _dtw_lite_scalar(xf, yf, n, m, w) +def dtw_lite_batch(x: np.ndarray, y: np.ndarray, band_width_ratio: float) -> np.ndarray: + """``compute_dtw_lite`` for ``k`` equal-length pairs at once (rows of ``x``, ``y``). + + The Stage-3 refines are all 200x200 with one band, so the ``top_n x 2`` DP + fills run as one row scan (audit MR-05): within a row, ``cell[j] = min(c[j], + local[j] + cell[j-1])`` with ``c = local + min(up, diag)`` unrolls to ``S[j] + + cummin(c[k] - S[k])`` over the row's prefix sums ``S``. ~8x faster for 10 DTWs; + the prefix sums reorder the additions, so results agree to ~1e-15 relative, + not bit for bit. + """ + xs = np.asarray(x, dtype=float) + ys = np.asarray(y, dtype=float) + k, n = xs.shape + m = ys.shape[1] + if n == 0 or m == 0: + return np.full(k, np.inf) + w = max(1, int(min(n, m) * band_width_ratio)) + centers = (np.arange(1, n + 1, dtype=float) * (m / n)).astype(np.intp) + start_js = np.maximum(1, centers - w) + end_js = np.minimum(m, centers + w + 1) + prev = np.full((k, m + 1), np.inf) + prev[:, 0] = 0.0 + zeros = np.zeros((k, 1)) + for i in range(n): + a, b = int(start_js[i]), int(end_js[i]) + local = np.abs(xs[:, i:i + 1] - ys[:, a - 1:b]) + up_diag = np.minimum(prev[:, a:b + 1], prev[:, a - 1:b]) + csum = np.cumsum(local, axis=1) + before = np.concatenate((zeros, csum[:, :-1]), axis=1) + row = csum + np.minimum.accumulate(up_diag - before, axis=1) + cur = np.full((k, m + 1), np.inf) + cur[:, a:b + 1] = row + prev = cur + return prev[:, m] + + +def _stage3_scores_batched( + pairs: list[tuple[np.ndarray, np.ndarray]], + current_peak: float, + *, + dtw_mode: str, + dtw_bandwidth: float, + l1_scale: float, + ddtw_scale: float, + ensemble_w: float, +) -> list[float]: + """Stage-3 scores for ``scaled`` / ``ddtw`` / ``ensemble`` in one batched DTW. + + ``pairs`` are each candidate's (current, sample) series already on the + ``MATCH_DTW_RESAMPLE_N`` grid; the arithmetic is `_dtw_component_score`'s. + """ + level = dtw_mode in ("scaled", "ensemble") + deriv = dtw_mode in ("ddtw", "ensemble") + xs: list[np.ndarray] = [] + ys: list[np.ndarray] = [] + if level: + xs.extend(a for a, _ in pairs) + ys.extend(b for _, b in pairs) + if deriv: + xs.extend(np.gradient(a) for a, _ in pairs) + ys.extend(np.gradient(b) for _, b in pairs) + dists = dtw_lite_batch(np.vstack(xs), np.vstack(ys), dtw_bandwidth) + peak = max(current_peak, MATCH_MAE_PEAK_FLOOR) + + def _score(dist: float, scale: float) -> float: + scaled = (dist / MATCH_DTW_RESAMPLE_N) * MATCH_MAE_REF_PEAK / peak + return scale / (scale + scaled) + + k = len(pairs) + if dtw_mode == "ensemble": + return [ + ensemble_w * _score(float(dists[i]), l1_scale) + + (1.0 - ensemble_w) * _score(float(dists[k + i]), ddtw_scale) + for i in range(k) + ] + scale = ddtw_scale if deriv else l1_scale + return [_score(float(d), scale) for d in dists] + + def _resample_to(arr: np.ndarray, n: int) -> np.ndarray: """Linearly resample a 1-D array to exactly ``n`` points over its index span. @@ -364,14 +462,15 @@ def _stage3_dtw_score( ddtw_scale: float, ensemble_w: float, curr_resampled: np.ndarray | None = None, -) -> tuple[float, float]: - """``(dtw_score, norm_dist)`` for one candidate: the four-way ``dtw_mode`` - branch of the Stage-3 refinement. +) -> float: + """The DTW score for one candidate: the four-way ``dtw_mode`` branch of the + Stage-3 refinement. - Lifted verbatim out of ``compute_matches_worker`` so the Stage-3 loop and the - Stage-6 prefix pass (#364) share one implementation and cannot drift apart. - Behaviour-identical to the inlined version, including ``legacy`` mode's - ``dtw_dist / len(curr_arr)`` normalisation and its ``norm_dist`` bookkeeping. + Lifted verbatim out of ``compute_matches_worker`` (for the #364 Stage-6 prefix + pass, removed in 0.5.8). Behaviour-identical to the inlined version, including ``legacy`` mode's + ``dtw_dist / len(curr_arr)`` normalisation. (The normalised distance it also + returned until audit MR-12 went to a ``dtw_dist`` candidate field nothing + read, always 0.0 under the default ``ensemble``.) """ if dtw_mode == "legacy": # Original behaviour: raw sequences, distance / len(current), @@ -379,14 +478,13 @@ def _stage3_dtw_score( dtw_dist = compute_dtw_lite(curr_arr, sample_arr, band_width_ratio=dtw_bandwidth) n_points = len(curr_arr) norm_dist = (dtw_dist / n_points) if n_points > 0 else 999.0 - return 1.0 / (1.0 + norm_dist / MATCH_DTW_DIST_SCALE), norm_dist + return 1.0 / (1.0 + norm_dist / MATCH_DTW_DIST_SCALE) if dtw_mode == "ensemble": # Blend the level-based (L1) and shape-based (derivative) DTW # scores; they are complementary signals. s_l1 = _dtw_component_score(curr_arr, sample_arr, current_peak, dtw_bandwidth, False, l1_scale, curr_resampled=curr_resampled) s_dd = _dtw_component_score(curr_arr, sample_arr, current_peak, dtw_bandwidth, True, ddtw_scale, curr_resampled=curr_resampled) - # composite; per-component distance not meaningful - return ensemble_w * s_l1 + (1.0 - ensemble_w) * s_dd, 0.0 + return ensemble_w * s_l1 + (1.0 - ensemble_w) * s_dd # "scaled" (default) or "ddtw": resample both onto one grid so the # band and normalisation are consistent, then express the distance # relative to the current peak (behaviour-neutral at @@ -395,7 +493,16 @@ def _stage3_dtw_score( scale = ddtw_scale if use_deriv else l1_scale return _dtw_component_score( curr_arr, sample_arr, current_peak, dtw_bandwidth, use_deriv, scale, curr_resampled=curr_resampled - ), 0.0 + ) + + +def _rank_key(candidate: dict[str, Any]) -> float: + """Sort key for candidate ranking: a non-finite score ranks last, never first.""" + try: + score = float(candidate.get("score", 0.0)) + except (TypeError, ValueError, OverflowError): + return float("-inf") + return score if math.isfinite(score) else float("-inf") def compute_matches_worker( @@ -419,6 +526,10 @@ def compute_matches_worker( keep_min = float(config.get("keep_min_score", MATCH_KEEP_MIN_SCORE)) corr_weight = float(config.get("corr_weight", MATCH_CORR_WEIGHT)) dur_weight = float(config.get("duration_weight", MATCH_DURATION_WEIGHT)) + if config.get("in_progress"): + dur_weight = float( + config.get("duration_weight_in_progress", MATCH_DURATION_WEIGHT_IN_PROGRESS) + ) en_weight = float(config.get("energy_weight", MATCH_ENERGY_WEIGHT)) dur_scale = float(config.get("duration_scale", MATCH_DURATION_SCALE)) dur_overrun_scale = float( @@ -432,6 +543,7 @@ def compute_matches_worker( # 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")) + min_ratio_grace_s = float(config.get("min_ratio_grace_s", MATCH_MIN_RATIO_GRACE_S)) # ...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. @@ -447,10 +559,14 @@ def compute_matches_worker( profile_duration = item["avg_duration"] sample_power = item["sample_power"] - # Duration Check + # Duration Check. The lower bound waits out MATCH_MIN_RATIO_GRACE_S of a live + # match: early on it rejects every programme longer than the cycle is old. if profile_duration > 0: ratio = current_duration / profile_duration - if ratio < min_duration_ratio or ratio > max_duration_ratio: + lower_applies = not ( + in_progress and current_duration < min_ratio_grace_s + ) + if (lower_applies and ratio < min_duration_ratio) or ratio > max_duration_ratio: continue # Core Similarity. While the cycle is mid-run this compares it against the @@ -468,12 +584,14 @@ def compute_matches_worker( shape_pair = prefix_shape_arrays( curr_arr, sample_power, current_duration, span_s ) + # The alignment offset is not kept: nothing read it, and it was in different + # units on the two paths (shared-grid index vs native index; audit MR-12). if shape_pair is not None: - score, metrics, offset = find_best_alignment( + 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( + score, metrics, _offset = find_best_alignment( current_power, sample_power, 1.0, corr_weight=corr_weight ) @@ -490,12 +608,11 @@ def compute_matches_worker( "_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. + # (devtools, playground) keep working unchanged. "sample_span_s": float(item.get("sample_span_s") or profile_duration or 0.0), - "offset": offset }) - candidates.sort(key=lambda x: x["score"], reverse=True) + candidates.sort(key=_rank_key, reverse=True) # Stage 3: DTW Refinement on the top N candidates if dtw_bandwidth > 0.0 and len(candidates) > 0: @@ -512,7 +629,35 @@ def compute_matches_worker( # Resample the current trace once — it's the same for every candidate. curr_resampled = _resample_to(curr_arr, MATCH_DTW_RESAMPLE_N) - for cand in to_refine: + batched: list[float] | None = None + if dtw_mode in ("scaled", "ddtw", "ensemble") and to_refine: + # One batched DTW for every refine (audit MR-05: Stage 3 was ~89% of + # matcher CPU). Falls back to the per-candidate path on any error. + try: + grid_pairs = [] + for cand in to_refine: + pair = cand.get("_shape_pair") + if pair is not None: + a = _resample_to(pair[0], MATCH_DTW_RESAMPLE_N) + b = _resample_to(pair[1], MATCH_DTW_RESAMPLE_N) + else: + a = curr_resampled + b = _resample_to(np.array(cand["sample"]), MATCH_DTW_RESAMPLE_N) + grid_pairs.append((a, b)) + batched = _stage3_scores_batched( + grid_pairs, current_peak, dtw_mode=dtw_mode, + dtw_bandwidth=dtw_bandwidth, l1_scale=l1_scale, + ddtw_scale=ddtw_scale, ensemble_w=ensemble_w, + ) + except Exception: # pylint: disable=broad-exception-caught + _LOGGER.debug("batched Stage-3 DTW failed; per-candidate path", exc_info=True) + batched = None + + for idx, cand in enumerate(to_refine): + if batched is not None: + cand["original_score"] = float(cand["score"]) + cand["score"] = float(blend * cand["score"] + (1.0 - blend) * batched[idx]) + continue pair = cand.get("_shape_pair") if pair is not None: warp_curr, sample_arr, cand_resampled = pair[0], pair[1], None @@ -521,7 +666,7 @@ def compute_matches_worker( curr_arr, np.array(cand["sample"]), curr_resampled ) - dtw_score, norm_dist = _stage3_dtw_score( + dtw_score = _stage3_dtw_score( warp_curr, sample_arr, current_peak, @@ -535,9 +680,8 @@ def compute_matches_worker( cand["original_score"] = float(cand["score"]) cand["score"] = float(blend * cand["score"] + (1.0 - blend) * dtw_score) - cand["dtw_dist"] = float(norm_dist) - candidates.sort(key=lambda x: x["score"], reverse=True) + candidates.sort(key=_rank_key, 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). @@ -565,6 +709,10 @@ def compute_matches_worker( # "integrated" compares true integrated energy (mean x duration). Opt-in so # the historical default is byte-for-byte preserved. See register item 99. integrated = config.get("energy_mode", "mean") == "integrated" + own_energy = ( + {str(s.get("name")): s.get("energy_ref") for s in snapshots} + if integrated and not in_progress else {} + ) cur_mean = float(np.mean(curr_arr)) cur_energy = cur_mean * current_duration if integrated else cur_mean for cand in candidates: @@ -597,6 +745,17 @@ def compute_matches_worker( # 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 + if integrated and not in_progress: + # A COMPLETED cycle is graded against the median energy of the + # profile's own cycles, not mean(template) x duration: on a warped + # envelope that inherits the reference cycle's heating length (w7g). + # Not while running: rescaling the prefix by median/template gained + # 0.8-1.6pp top-1 at 25-75% elapsed, but the live matches it moved + # are the ones the end gates read (end_gate_eval --loo: 2 new ends + # > 5 min early, washer-dryer median lag +0.7 min). + own = own_energy_ws(own_energy.get(str(cand["name"]))) + if own is not None: + cand_energy = own en_ag = _agreement(cur_energy, cand_energy, en_scale) cand["shape_score"] = float(cand["score"]) cand["score"] = float( @@ -604,14 +763,17 @@ def compute_matches_worker( + dur_w * dur_ag + en_w * en_ag ) - candidates.sort(key=lambda x: x["score"], reverse=True) + candidates.sort(key=_rank_key, reverse=True) - # Stage 6 (#364): prefix scores for the few candidates materially LONGER than - # the winner. Purely additive - it writes `prefix_score` and never touches - # `score`, so ranking is provably unchanged. Must run after the Stage-4 - # re-sort because the anchor is the winner's duration. - annotate_prefix_scores(candidates, curr_arr, current_duration, config) + # A non-finite score (a NaN in an imported template, a NaN avg_duration) used to + # sort to rank 1 with a NaN margin that was never "ambiguous" (audit + # MATCH-CORE-05). It carries no evidence: drop it. + candidates[:] = [c for c in candidates if math.isfinite(_rank_key(c))] + # (Stage 6, the #364 prefix scores for the Smart-Termination guard, was removed + # in 0.5.8: Stages 2/3 already score a running cycle on each candidate's + # truncated curve, so the guard fired on 0 of 713 genuine ends and 0 of 7 + # quiet split positives on the shipped matcher - devtools/prefix_guard_eval.py.) return candidates def prefix_mean( @@ -630,7 +792,7 @@ def prefix_mean( 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 + 12-sample floor exists because Stages 2/3 *correlate* 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. @@ -643,6 +805,69 @@ def prefix_mean( return float(np.mean(sample)), profile_duration +def member_energy_reference( + members: list[tuple[list[float], list[float], float]], +) -> dict[str, Any] | None: + """A profile's energy taken from its OWN cycles, for Stage 4. + + ``members`` are the ``(offsets, watts, duration)`` triples the envelope is built + from. Each member's energy is measured the way Stage 4 measures the cycle being + matched - mean of the linearly interpolated trace x duration - so both sides of + the agreement are the same quantity. ``{"n": members used, "median_wh": median + whole-cycle Wh}``, or None without a usable member. + + Why (w7g): Stage 4 took a profile's energy as ``mean(template) x avg_duration``. + On a DTW-warped envelope that inherits the heating length of the cycle the + members were warped onto: > 20% off its own members' median on 17 of 85 washer + profiles in the corpus, 2.5x on one. + """ + totals: list[float] = [] + for offsets, watts, duration in members: + t = np.asarray(offsets, dtype=float) + p = np.asarray(watts, dtype=float) + if t.size != p.size or t.size < 2: + continue + ok = np.isfinite(t) & np.isfinite(p) + t, p = t[ok], p[ok] + if t.size < 2: + continue + order = np.argsort(t, kind="mergesort") + t, p = t[order], p[order] + span = float(t[-1] - t[0]) + if span <= 0: + continue + try: + dur = float(duration) + except (TypeError, ValueError, OverflowError): + dur = 0.0 + if not math.isfinite(dur) or dur <= 0: + dur = span + energy_ws = integrate_wh(t, p) * 3600.0 / span * dur + if math.isfinite(energy_ws) and energy_ws > 0: + totals.append(energy_ws) + if not totals: + return None + return {"n": len(totals), "median_wh": round(float(np.median(totals)) / 3600.0, 3)} + + +def own_energy_ws(ref: dict[str, Any] | None) -> float | None: + """The whole-cycle energy (W*s) Stage 4 expects from a profile's own cycles. + + The median of a :func:`member_energy_reference`, or None (keep the template's + ``mean x duration``) without one of at least ``MATCH_ENERGY_REF_MIN_CYCLES`` + cycles or with a malformed one. + """ + if not isinstance(ref, dict): + return None + try: + if int(ref.get("n") or 0) < MATCH_ENERGY_REF_MIN_CYCLES: + return None + own_ws = float(ref["median_wh"]) * 3600.0 + except (TypeError, ValueError, KeyError, OverflowError): + return None + return own_ws if math.isfinite(own_ws) and own_ws > 0 else None + + def _prefix_point_count( n_points: int, current_duration: float, sample_span_s: float ) -> int: @@ -652,12 +877,12 @@ def _prefix_point_count( the whole template (then it is not a prefix), or when too few points remain to judge. Fraction-of-array is the right operator because every snapshot flavour is uniform in time over its own span (envelope: np.linspace; sample cycle: - resample_uniform at a fixed dt; group aggregate: np.interp onto 200 points). + resample_uniform at a fixed dt). (The group aggregate snapshot is gone, #400.) """ - if n_points < SMART_TERM_PREFIX_MIN_POINTS or sample_span_s <= 0 or current_duration <= 0: + if n_points < MATCH_PREFIX_MIN_POINTS or sample_span_s <= 0 or current_duration <= 0: return 0 k = int(round(n_points * (current_duration / sample_span_s))) - if k < SMART_TERM_PREFIX_MIN_POINTS or k >= n_points: + if k < MATCH_PREFIX_MIN_POINTS or k >= n_points: return 0 return k @@ -671,140 +896,38 @@ def prefix_shape_arrays( """``(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. + One definition of "the same stretch of both curves" for the live Stage-2/3 + shape scoring (#400). (It also fed the #364 Stage-6 prefix guard, removed in + 0.5.8; ``devtools/prefix_guard_eval.py`` keeps a copy of that rule.) 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``. + scored on a finer grid than a shorter one. 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. (The one A/B of + dropping the ``k`` term was measured on the removed guard, not on matching.) + ``devtools/dtw_ab_eval.py`` scores only complete cycles, which never take + the prefix path, so it cannot judge this function - use ``devtools/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: + if grid < MATCH_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, - current_duration: float, - sample_span_s: float, - current_peak: float, - config: dict[str, Any], -) -> float | None: - """Score the live trace against ``sample`` TRUNCATED to ``current_duration``. - - The #288 landscape guard asks whether a longer candidate has a decent shape - score against its **whole** curve - which a part-way-through trace cannot - have. This asks the question that actually matters: does the trace look like - the *beginning* of that longer programme? (#364) - - Same scale as ``shape_score`` by construction: identical Stage-2 formula - (``find_best_alignment``) and identical Stage-3 DTW blend, only the reference - array differs. Returns None when the template cannot be truncated meaningfully. - - NB prefix scoring normalizes on the shared resample ``grid`` (both series are - resampled to it), so it does not support the non-default ``dtw_mode="legacy"`` - absolute-watt/length normalization - under which cross-candidate prefix scores of - differing native length would not be comparable. This is inert in production: the - default is ``"ensemble"`` and the live ProfileStore path never sets ``dtw_mode``; - ``"legacy"`` exists only for the devtools re-sweep harness. - """ - pair = prefix_shape_arrays(curr_arr, sample, current_duration, sample_span_s) - if pair is None: - return None - 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) - - # The SAME default as `compute_matches_worker` (register item 309). Both read - # the same unmutated `config` in one match, so a caller that omits the key - # would otherwise get DEFAULT_DTW_BANDWIDTH for Stage 3 and the old 0.1 here, - # and the two stages would disagree about which candidates look like a prefix. - dtw_bandwidth = float(config.get("dtw_bandwidth", DEFAULT_DTW_BANDWIDTH)) - if dtw_bandwidth > 0.0: - dtw_score, _ = _stage3_dtw_score( - a, - b, - current_peak, - dtw_mode=str(config.get("dtw_mode", DEFAULT_DTW_MODE)), - dtw_bandwidth=dtw_bandwidth, - l1_scale=float(config.get("dtw_l1_scale", MATCH_DTW_DIST_SCALE)), - ddtw_scale=float(config.get("dtw_ddtw_scale", MATCH_DDTW_DIST_SCALE)), - ensemble_w=float(config.get("dtw_ensemble_w", MATCH_DTW_ENSEMBLE_W)), - ) - blend = float(config.get("dtw_blend", MATCH_DTW_BLEND)) - return float(blend * score + (1.0 - blend) * dtw_score) - return float(score) - - -def annotate_prefix_scores( - candidates: list[dict[str, Any]], - curr_arr: np.ndarray, - current_duration: float, - config: dict[str, Any], -) -> None: - """Stage 6 (#364): write ``prefix_score`` on the few non-winning candidates - that are materially longer than the winner. - - Mutates in place and never touches ``score``/``shape_score``, so candidate - ranking is unaffected - this only feeds the Smart-Termination prefix guard. - Every test before the first array touch is a scalar compare, so the common - case (no candidate is materially longer) costs nothing. - """ - if current_duration <= 0 or len(candidates) < 2 or curr_arr.size == 0: - return - best_dur = float(candidates[0].get("profile_duration") or 0.0) - if best_dur <= 0: - return - min_dur = best_dur * SMART_TERM_PREFIX_MIN_RATIO - current_peak = float(np.max(curr_arr)) - scored = 0 - for cand in candidates[1:]: - prof_dur = float(cand.get("profile_duration") or 0.0) - if prof_dur <= min_dur: - continue # not a longer look-alike - if prof_dur <= current_duration: - continue # we already outlasted it, so we are not inside its prefix - span = float(cand.get("sample_span_s") or prof_dur) - if span < prof_dur * SMART_TERM_PREFIX_MIN_COVERAGE: - continue # gap-truncated template: may not start at the programme's start - score = prefix_shape_score( - curr_arr, cand.get("sample") or [], current_duration, span, current_peak, config - ) - if score is None: - continue - cand["prefix_score"] = float(score) - scored += 1 - if scored >= SMART_TERM_PREFIX_MAX_CANDIDATES: - break - - def _dtw_cost_matrix_scalar( x: np.ndarray, y: np.ndarray, n: int, m: int, w: int ) -> np.ndarray: """Reference (scalar) Sakoe-Chiba DTW cost-matrix fill. Kept verbatim as the - fallback for :func:`_dtw_cost_matrix_vectorized` so behavior can never regress.""" + fallback for :func:`_dtw_cost_banded` so behavior can never regress.""" cost_matrix = np.full((n + 1, m + 1), float("inf")) cost_matrix[0, 0] = 0 for i in range(1, n + 1): @@ -819,48 +942,106 @@ def _dtw_cost_matrix_scalar( return cost_matrix -def _dtw_cost_matrix_vectorized( +class _BandedCostMatrix: + """A Sakoe-Chiba DTW cost matrix that stores only its in-band cells. + + Cells are kept per anti-diagonal ``d = i + j`` (the order the fill computes + them), each diagonal padded with one ``inf`` cell on either side. ``get`` + returns exactly what the full ``(n+1) x (m+1)`` matrix held at ``(i, j)``: + the computed value in band, ``inf`` everywhere else (audit LIVE-20). + """ + + __slots__ = ("_vals", "_first", "_count", "_start") + + def __init__(self, vals: np.ndarray, first: list[int], count: list[int], start: list[int]) -> None: + self._vals = vals + self._first = first + self._count = count + self._start = start + + def get(self, i: int, j: int) -> float: + d = i + j + k = i - self._first[d] + if 0 <= k < self._count[d]: + # .item(): a Python float, without boxing the whole array (a .tolist() + # copy of a 2000-point band costs ~4x the band itself). + return self._vals.item(self._start[d] + 1 + k) + return math.inf + + +def _dtw_cost_banded( x: np.ndarray, y: np.ndarray, n: int, m: int, w: int -) -> np.ndarray: - """Bit-identical vectorized fill of the scalar cost matrix. +) -> _BandedCostMatrix: + """Bit-identical vectorized fill of the scalar cost matrix, in-band cells only. The DTW recurrence is sequential, but all cells on one anti-diagonal - (``i + j`` constant) depend only on earlier anti-diagonals, so each diagonal - is one vectorized NumPy update instead of thousands of Python ``min``/``abs`` - calls. The Sakoe-Chiba band, the per-row bounds (``int`` truncation), the - ``local + min(up, left, diag)`` recurrence and out-of-band ``inf`` cells all - match the scalar loop exactly, so the resulting matrix - and the backtracked - path - is identical. (#311 follow-up: this fill dominates envelope rebuilds.) + (``i + j`` constant) depend only on the two before it, so each diagonal is + one vectorized NumPy update. The Sakoe-Chiba band, the per-row bounds + (``int`` truncation), the ``local + min(min(up, left), diag)`` recurrence and + the out-of-band ``inf`` cells match the scalar loop exactly, so every cell - + and the backtracked path - is identical. (#311 follow-up: this fill + dominates envelope rebuilds.) + + Until audit LIVE-20 it filled a full ``(n+1) x (m+1)`` matrix and masked all + of every anti-diagonal down to the band. Both band edges are non-decreasing + in ``i``, so the in-band cells of a diagonal are one contiguous run of rows, + found by a binary search; and a run moves by at most one row from one + diagonal to the next, so one ``inf`` pad cell per side makes every + predecessor lookup a slice. A 2000 x 2000 pair at the default 20% band + stores ~2.5x fewer cells and fills ~4-8x faster. """ xf = np.asarray(x, dtype=float) yf = np.asarray(y, dtype=float) - cost_matrix = np.full((n + 1, m + 1), np.inf) - cost_matrix[0, 0] = 0.0 # Per-row band bounds, identical to the scalar start_j/end_j (int truncates # toward zero, matching Python int()). i_idx = np.arange(1, n + 1) center = i_idx * (m / n) lo = np.maximum(1, (center - w).astype(np.int64)) hi = np.minimum(m, (center + w).astype(np.int64) + 1) + # Rows in band on diagonal d: i + lo[i] <= d <= i + hi[i] (both sides strictly + # increasing in i), inside the matrix (1 <= i <= n, 1 <= d - i <= m). + d_all = np.arange(n + m + 1) + first = np.searchsorted(i_idx + hi, d_all, side="left") + 1 + last = np.searchsorted(i_idx + lo, d_all, side="right") + first = np.maximum(first, np.maximum(1, d_all - m)) + last = np.minimum(last, np.minimum(n, d_all - 1)) + count = np.maximum(0, last - first + 1) + first[0], count[0] = 0, 1 # the origin (0, 0) + start = np.zeros(n + m + 2, dtype=np.int64) + np.cumsum(count + 2, out=start[1:]) + vals = np.full(int(start[-1]), np.inf) + vals[1] = 0.0 + first_l: list[int] = first.tolist() + count_l: list[int] = count.tolist() + start_l: list[int] = start.tolist() + + def run(e: int, p: int, q: int) -> np.ndarray: + """Cells of rows ``p..q`` on diagonal ``e`` (``inf`` outside its band).""" + fe, ce = first_l[e], count_l[e] + base = start_l[e] + 1 - fe + if p >= fe - 1 and q <= fe + ce: + return vals[base + p: base + q + 1] + out = np.full(q - p + 1, np.inf) + a, b = max(p, fe), min(q, fe + ce - 1) + if a <= b: + out[a - p: b - p + 1] = vals[base + a: base + b + 1] + return out + for d in range(2, n + m + 1): - i_lo = max(1, d - m) - i_hi = min(n, d - 1) - if i_lo > i_hi: + c = count_l[d] + if c <= 0: continue - ii = np.arange(i_lo, i_hi + 1) - jj = d - ii - inb = (jj >= lo[ii - 1]) & (jj <= hi[ii - 1]) - if not inb.any(): - continue - ib = ii[inb] - jb = jj[inb] - local = np.abs(xf[ib - 1] - yf[jb - 1]) + p = first_l[d] + q = p + c - 1 + # cells (i, d - i) for i = p..q: x[i - 1] against y[d - i - 1] + local = np.abs(xf[p - 1: q] - yf[d - q - 1: d - p][::-1]) best = np.minimum( - np.minimum(cost_matrix[ib - 1, jb], cost_matrix[ib, jb - 1]), - cost_matrix[ib - 1, jb - 1], + np.minimum(run(d - 1, p - 1, q - 1), run(d - 1, p, q)), # up, left + run(d - 2, p - 1, q - 1), # diag ) - cost_matrix[ib, jb] = local + best - return cost_matrix + s = start_l[d] + 1 + np.add(local, best, out=vals[s: s + c]) + return _BandedCostMatrix(vals, first_l, count_l, start_l) def compute_dtw_path( @@ -874,11 +1055,13 @@ def compute_dtw_path( if n == 0 or m == 0: return [] - # Pre-flight memory guard: the cost matrix is (n+1)x(m+1) float64. An - # uncapped call from a 1 Hz long cycle can request >1 GB here. If the - # allocation would exceed ~80 MB, skip DTW and return an empty path so + # Pre-flight memory guard: the scalar fallback's cost matrix is (n+1)x(m+1) + # float64. An uncapped call from a 1 Hz long cycle can request >1 GB there. + # If the allocation would exceed ~80 MB, skip DTW and return an empty path so # the caller falls back to linear interpolation (graceful degrade rather - # than OOM-killing Home Assistant — issue #388). + # than OOM-killing Home Assistant, issue #388). The banded fill needs far + # less, but the cap stays where it was so which pairs get a path is unchanged + # (``_closed_end_path_exists`` mirrors it). _DTW_CELL_BUDGET = 10_000_000 # 10 M cells x 8 B ≈ 80 MB if (n + 1) * (m + 1) > _DTW_CELL_BUDGET: _LOGGER.warning( @@ -890,12 +1073,13 @@ def compute_dtw_path( w = max(1, int(min(n, m) * band_width_ratio)) try: - cost_matrix = _dtw_cost_matrix_vectorized(x, y, n, m, w) + cost = _dtw_cost_banded(x, y, n, m, w).get except Exception: # pylint: disable=broad-exception-caught - cost_matrix = _dtw_cost_matrix_scalar(x, y, n, m, w) + full = _dtw_cost_matrix_scalar(x, y, n, m, w) + cost = lambda i, j: full[i, j] # noqa: E731 # Backtracking - if np.isinf(cost_matrix[n, m]): + if math.isinf(cost(n, m)): # Endpoint is unreachable (e.g. Sakoe-Chiba band excluded it); no valid path. return [] @@ -912,9 +1096,9 @@ def compute_dtw_path( i -= 1 else: candidates_cost = [ - (cost_matrix[i - 1, j], 0), # deletion (i-1) - (cost_matrix[i, j - 1], 1), # insertion (j-1) - (cost_matrix[i - 1, j - 1], 2) # match (both) + (cost(i - 1, j), 0), # deletion (i-1) + (cost(i, j - 1), 1), # insertion (j-1) + (cost(i - 1, j - 1), 2) # match (both) ] candidates_cost.sort(key=lambda item: item[0]) best_move = candidates_cost[0][1] @@ -967,7 +1151,7 @@ def compute_envelope_worker( try: offsets_list, values_list, *rest = curve curve_duration = rest[0] if rest else None - except (ValueError, TypeError): + except (ValueError, TypeError, OverflowError): continue if not offsets_list or not values_list: @@ -986,7 +1170,7 @@ def compute_envelope_worker( try: offsets = np.asarray(offsets_list, dtype=float) values = np.asarray(values_list, dtype=float) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): continue # Drop paired entries where either coordinate is non-finite. @@ -1202,7 +1386,8 @@ def align_trace_to_envelope( **Known limit, measured and accepted (register item 347).** The observed span here is ``t_obs[-1]``, while ``_rebuild_envelope_sync`` builds each member - over ``manual_duration or max(last_offset, stored_duration)``. Where the + over ``manual_duration`` (when plausible for its trace, audit MATCH-EVAL-17) + or ``max(last_offset, stored_duration)``. Where the stored duration runs past the last sample the build covered a slightly longer span than this re-derivation does, so the warp is not bit-for-bit the build's own. Measured over the 703 stored cycles in the maintainer's corpus it @@ -1285,6 +1470,28 @@ def align_trace_to_envelope( return _proportional(), False +_DTW_PATH_CELL_BUDGET = 10_000_000 # compute_dtw_path's matrix cap + + +def _closed_end_path_exists(n: int, m: int, band_width_ratio: float) -> bool: + """Whether ``compute_dtw_path`` would return a path for an ``n x m`` pair. + + Exactly its three failure cases without filling the matrix: an empty side, + the cell budget, and an end cell the Sakoe-Chiba band cannot reach. Each row's + band is ``[lo, hi]`` (the fill's own truncation); cells are reachable left to + right from the diagonal, so the end is reachable iff no row's band starts more + than one column past where the previous row's ends (row 0 is the origin). + """ + if n == 0 or m == 0 or (n + 1) * (m + 1) > _DTW_PATH_CELL_BUDGET: + return False + w = max(1, int(min(n, m) * band_width_ratio)) + center = np.arange(1, n + 1) * (m / n) + lo = np.maximum(1, (center - w).astype(np.int64)) + hi = np.minimum(m, (center + w).astype(np.int64) + 1) + prev_hi = np.concatenate(([0], hi[:-1])) + return bool(np.all(lo <= prev_hi + 1) and np.all(lo <= hi) and hi[-1] == m) + + def verify_profile_alignment_worker( current_power: list[float], envelope_avg_curve: list[float], @@ -1321,17 +1528,19 @@ def verify_profile_alignment_worker( if offset < 0: curr_seg = curr[-offset:] - path = compute_dtw_path(curr_seg, ref_seg, band_width_ratio=dtw_bandwidth) - - if not path: + # The DTW that used to run here was closed-end: its backtrack starts at the last + # cell, so the mapped index was ALWAYS the window's last index whenever a path + # existed - 34x the CPU and a 33-42 MiB matrix per tick for a constant (audit + # LIVE-03). Same answer, same fallback, no matrix: a path exists unless the + # window is empty, over the old cell budget, or the band cannot reach the end + # cell. The resulting position runs ALIGNMENT_CONTEXT_BUFFER // 2 steps ahead + # of the trace; measured harmless-to-helpful, so it is kept, not "fixed". + if _closed_end_path_exists(len(curr_seg), len(ref_seg), dtw_bandwidth): + mapped_idx = end_ref - 1 + else: # Fallback to linear mapping based on offset mapped_idx = min(len(ref)-1, offset + len(curr) - 1) mapped_idx = max(0, mapped_idx) - else: - # Map the final point of the current trace to the reference index - last_pair = path[-1] - ref_seg_idx = last_pair[1] - mapped_idx = start_ref + ref_seg_idx # Ensure sequences are non-empty before indexing if not envelope_time_grid or len(ref) == 0: diff --git a/custom_components/ha_washdata/binary_sensor.py b/custom_components/ha_washdata/binary_sensor.py index 60ad31fe..3ef0c0bb 100644 --- a/custom_components/ha_washdata/binary_sensor.py +++ b/custom_components/ha_washdata/binary_sensor.py @@ -18,6 +18,7 @@ from __future__ import annotations +from datetime import timedelta from typing import Any from homeassistant.components.binary_sensor import BinarySensorEntity @@ -26,10 +27,12 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.const import EntityCategory from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.translation import async_get_cached_translations from .const import ( DOMAIN, - STATE_RUNNING, + CYCLE_IN_PROGRESS_STATES, SIGNAL_WASHER_UPDATE, CONF_EXPOSE_DEBUG_ENTITIES, ) @@ -44,7 +47,10 @@ async def async_setup_entry( ) -> None: """Set up the binary sensor.""" manager: WashDataManager = hass.data[DOMAIN][entry.entry_id] - entities = [WasherRunningBinarySensor(manager, entry)] + entities = [ + WasherRunningBinarySensor(manager, entry), + WasherMaintenanceDueBinarySensor(manager, entry), + ] if entry.options.get(CONF_EXPOSE_DEBUG_ENTITIES): entities.append(WasherAmbiguitySensor(manager, entry)) @@ -56,6 +62,8 @@ async def async_setup_entry( class WasherRunningBinarySensor(BinarySensorEntity): """Binary sensor indicating if washer is running.""" + _attr_should_poll = False # pushed by the manager's update signal (PERF-02) + _attr_has_entity_name = True _attr_translation_key = "running" @@ -74,7 +82,9 @@ class WasherRunningBinarySensor(BinarySensorEntity): @property def is_on(self) -> bool | None: """Return true if the binary sensor is on.""" - return self._manager.check_state() == STATE_RUNNING + # Any in-progress state, not only `running`: a soak, a pause or the end + # wait is not "done" (audit PLATFORM-06). + return self._manager.check_state() in CYCLE_IN_PROGRESS_STATES async def async_added_to_hass(self) -> None: """Register callbacks.""" @@ -114,3 +124,102 @@ class WasherAmbiguitySensor(WasherRunningBinarySensor): """Return ambiguous candidate info.""" details = self._manager.last_match_details return {"margin": details.get("ambiguity_margin", 0.0) if details else 0.0} + + +# How often a day-based reminder is re-checked when nothing else happens. The cycle +# and log paths refresh the sensor through the update signal; only the passing of +# time needs a clock, and hourly is plenty for an interval counted in days. +_MAINTENANCE_RECHECK = timedelta(hours=1) + + +class WasherMaintenanceDueBinarySensor(BinarySensorEntity): + """On while any maintenance task is due (#461): build your own notification. + + WashData itself never notifies about maintenance (no new notification type); + this entity is the hook for a user's automation. Attributes list the due tasks + with how far past their interval each is. Built-in task names are translated + into Home Assistant's language; custom task names are the user's own text. + """ + + _attr_should_poll = False + _attr_has_entity_name = True + _attr_translation_key = "maintenance_due" + _attr_icon = "mdi:wrench-clock" + + def __init__(self, manager: WashDataManager, entry: ConfigEntry) -> None: + """Initialize.""" + self._manager = manager + self._entry = entry + self._attr_unique_id = f"{entry.entry_id}_maintenance_due" + self._attr_device_info = { + "identifiers": {(DOMAIN, entry.entry_id)}, + "name": entry.title, + "manufacturer": "WashData", + } + self._attr_is_on = False + self._attr_extra_state_attributes = {"due_task_ids": [], "due_tasks": []} + + def _builtin_names(self) -> dict[str, str]: + """Home Assistant's loaded entity strings for this integration ({} if none).""" + try: + return async_get_cached_translations( + self.hass, self.hass.config.language, "entity", DOMAIN + ) + except Exception: # noqa: BLE001 - a missing translation is not an error + return {} + + def _refresh(self) -> bool: + """Recompute state and attributes; return whether either changed. + + The update signal fires on every power reading, so the state is written + only when it changes (audit PERF-08 counts entity writes per reading). + """ + due = [row for row in self._manager.maintenance_status if row.get("due")] + strings = self._builtin_names() if any(not r.get("custom") for r in due) else {} + prefix = ( + f"component.{DOMAIN}.entity.binary_sensor.maintenance_due" + ".state_attributes.due_tasks.state." + ) + tasks = [ + { + "id": row["id"], + "name": ( + row["name"] if row.get("custom") + else strings.get(prefix + row["id"]) or row["id"] + ), + "cycles_since": row.get("cycles_since"), + "cycles_interval": row.get("cycles_interval"), + "days_since": row.get("days_since"), + "days_interval": row.get("days_interval"), + } + for row in due + ] + attrs = {"due_task_ids": [t["id"] for t in tasks], "due_tasks": tasks} + is_on = bool(tasks) + if is_on == self._attr_is_on and attrs == self._attr_extra_state_attributes: + return False + self._attr_is_on = is_on + self._attr_extra_state_attributes = attrs + return True + + async def async_added_to_hass(self) -> None: + """Register callbacks and compute the first state.""" + self._refresh() + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_WASHER_UPDATE.format(self._entry.entry_id), + self._update_callback, + ) + ) + self.async_on_remove( + async_track_time_interval( + self.hass, self._update_callback, _MAINTENANCE_RECHECK + ) + ) + + @callback + def _update_callback(self, *_args: Any) -> None: + """Write the state only when it changed.""" + if self._refresh(): + self.async_write_ha_state() diff --git a/custom_components/ha_washdata/button.py b/custom_components/ha_washdata/button.py index 4d3b7b37..1399b70a 100644 --- a/custom_components/ha_washdata/button.py +++ b/custom_components/ha_washdata/button.py @@ -52,6 +52,8 @@ async def async_setup_entry( class WashDataTerminateButton(ButtonEntity): """Button to force terminate the current cycle.""" + _attr_should_poll = False # state is pushed; nothing to poll (PERF-02) + _attr_has_entity_name = True _attr_translation_key = "force_end_cycle" _attr_icon = "mdi:stop-circle-outline" @@ -79,6 +81,8 @@ class WashDataTerminateButton(ButtonEntity): class WashDataPauseCycleButton(ButtonEntity): """Button to pause the current cycle (user-triggered).""" + _attr_should_poll = False # state is pushed; nothing to poll (PERF-02) + _attr_has_entity_name = True _attr_translation_key = "pause_cycle" _attr_icon = "mdi:pause-circle-outline" @@ -124,6 +128,8 @@ class WashDataPauseCycleButton(ButtonEntity): class WashDataResumeCycleButton(ButtonEntity): """Button to resume a user-paused cycle.""" + _attr_should_poll = False # state is pushed; nothing to poll (PERF-02) + _attr_has_entity_name = True _attr_translation_key = "resume_cycle" _attr_icon = "mdi:play-circle-outline" @@ -166,6 +172,8 @@ class WashDataResumeCycleButton(ButtonEntity): class WashDataRecordStartButton(ButtonEntity): """Button to start manually recording a clean cycle.""" + _attr_should_poll = False # state is pushed; nothing to poll (PERF-02) + _attr_has_entity_name = True _attr_translation_key = "record_start" _attr_icon = "mdi:record-circle-outline" @@ -200,7 +208,10 @@ class WashDataRecordStartButton(ButtonEntity): """Only available when not already recording and no active cycle is running.""" return ( not self._manager.recorder.is_recording - and self._manager.detector.state == "off" + # The state entities show, so a hidden standby re-probe (item 501) + # does not toggle availability on every reading. Idle (#452) is a + # switched-on appliance between cycles: recording starts from there too. + and self._manager.detector.exposed_state in ("off", "idle") ) async def async_press(self) -> None: @@ -211,6 +222,8 @@ class WashDataRecordStartButton(ButtonEntity): class WashDataRecordStopButton(ButtonEntity): """Button to stop manual recording.""" + _attr_should_poll = False # state is pushed; nothing to poll (PERF-02) + _attr_has_entity_name = True _attr_translation_key = "record_stop" _attr_icon = "mdi:stop-circle" @@ -259,6 +272,7 @@ class WashDataMarkUnloadedButton(ButtonEntity): and Home Assistant skips an unavailable entity in a service call, so an automation that presses it unconditionally is a harmless no-op. """ + _attr_should_poll = False # state is pushed; nothing to poll (PERF-02) _attr_has_entity_name = True _attr_translation_key = "mark_unloaded" diff --git a/custom_components/ha_washdata/config_flow.py b/custom_components/ha_washdata/config_flow.py index 70ffe024..d9ad3b72 100644 --- a/custom_components/ha_washdata/config_flow.py +++ b/custom_components/ha_washdata/config_flow.py @@ -23,8 +23,8 @@ from typing import Any import voluptuous as vol from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlowResult from homeassistant.const import CONF_NAME -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import selector from .const import ( @@ -164,7 +164,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # pylint: disable=a async def async_step_user( self, user_input: dict[str, Any] | None = None # pylint: disable=unused-argument - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is None: @@ -189,7 +189,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # pylint: disable=a async def async_step_reconfigure( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle reconfigure flow to change sensor, device type, or name.""" entry = self._get_reconfigure_entry() errors: dict[str, str] = {} @@ -207,10 +207,21 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # pylint: disable=a # NB: intentionally do NOT write entry.data here — post-3.6 the # structural fields live in options and the display name is carried # by the entry title (see test_reconfigure_saves_and_aborts_on_valid_input). + # Update only (audit PLATFORM-15): the entry's update listener + # already reloads in place (sensor swap included, item 61); + # async_update_reload_and_abort ALSO scheduled a full unload/setup, + # which interrupted a running cycle and raced the in-place reload. + if entry.state is config_entries.ConfigEntryState.LOADED: + self.hass.config_entries.async_update_entry( + entry, title=user_input[CONF_NAME], options=new_options + ) + return self.async_abort(reason="reconfigure_successful") + # Not loaded (a failed setup, often what the reconfigure is fixing): + # the update listener is registered only by a successful setup, so + # nothing would apply the options until a restart. Reload instead. return self.async_update_reload_and_abort( - entry, - title=user_input[CONF_NAME], - options=new_options, + entry, title=user_input[CONF_NAME], options=new_options, + reason="reconfigure_successful", ) schema = _structural_schema(entry) @@ -244,7 +255,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Show the minimal options form.""" entry = self._config_entry errors: dict[str, str] = {} diff --git a/custom_components/ha_washdata/const.py b/custom_components/ha_washdata/const.py index 33feaeb3..50b32aac 100644 --- a/custom_components/ha_washdata/const.py +++ b/custom_components/ha_washdata/const.py @@ -16,7 +16,9 @@ # along with this program. If not, see . """Constants for the WashData integration.""" +import math from enum import StrEnum +from typing import Any DOMAIN = "ha_washdata" @@ -59,7 +61,7 @@ CONF_NOTIFY_LIVE_SERVICES = "notify_live_services" CONF_NOTIFY_CYCLE_TIMERS = "notify_cycle_timers" CONF_NO_UPDATE_ACTIVE_TIMEOUT = "no_update_active_timeout" CONF_LOW_POWER_NO_UPDATE_TIMEOUT = "low_power_no_update_timeout" -CONF_SMOOTHING_WINDOW = "smoothing_window" +CONF_SMOOTHING_WINDOW = "smoothing_window" # Removed in 0.5.8: never read; key kept for old migrations CONF_SAMPLING_INTERVAL = "sampling_interval" CONF_START_DURATION_THRESHOLD = ( "start_duration_threshold" # Debounce for start detection @@ -77,15 +79,12 @@ CONF_AUTO_MAINTENANCE = "auto_maintenance" CONF_PROFILE_MATCH_INTERVAL = "profile_match_interval" CONF_PROFILE_MATCH_MIN_DURATION_RATIO = "profile_match_min_duration_ratio" CONF_PROFILE_MATCH_MAX_DURATION_RATIO = "profile_match_max_duration_ratio" -CONF_MAX_PAST_CYCLES = "max_past_cycles" -CONF_MAX_FULL_TRACES_PER_PROFILE = "max_full_traces_per_profile" -CONF_MAX_FULL_TRACES_UNLABELED = "max_full_traces_unlabeled" CONF_WATCHDOG_INTERVAL = "watchdog_interval" # Derived from sampling_interval CONF_MATCH_PERSISTENCE = "match_persistence" CONF_COMPLETION_MIN_SECONDS = "completion_min_seconds" CONF_NOTIFY_BEFORE_END_MINUTES = "notify_before_end_minutes" CONF_RUNNING_DEAD_ZONE = "running_dead_zone" # REMOVED in 0.5.3 — was never wired to detection -CONF_END_REPEAT_COUNT = "end_repeat_count" # Number of times end condition must be met +CONF_END_REPEAT_COUNT = "end_repeat_count" # Removed in 0.5.8: the detector never read it; stored values are ignored CONF_MIN_OFF_GAP = "min_off_gap" # Minimum gap to separate cycles (seconds) CONF_START_ENERGY_THRESHOLD = "start_energy_threshold" # Wh required to confirm start CONF_END_ENERGY_THRESHOLD = "end_energy_threshold" # Wh allowed during end candidates @@ -100,30 +99,6 @@ CONF_POWER_OFF_DELAY = ( "power_off_delay" # Seconds below the power-off threshold before Finished/Clean -> Off ) CONF_EXPOSE_DEBUG_ENTITIES = "expose_debug_entities" # Expose detailed debug sensors -# Per-device opt-in: blend the phase-resolved (per-role budget) ETA into the -# time-remaining estimate for phase-matching-supported device types (washing -# machine, washer-dryer). Default off. Validated by the Phase-0 ETA gate; see -# docs/superpowers/specs/2026-07-17-phase-segmented-matching-design.md. -CONF_ENABLE_PHASE_MATCHING = "enable_phase_matching" -# Phase-structure consistency advisory (Profiles tab, never a notification). -# A single-program/temperature profile should have a fairly consistent heating -# block; wildly varying heating time or heating present in only some cycles -# usually means different programs/temperatures were labelled under one profile -# (the "mixed labels" data-hygiene problem). Pure statistics from the cached -# phase profile - no relabeling (phase matching does not label better than the -# whole-cycle matcher; see the Phase-0 gate). -# Minimum member cycles before a profile's cached phase profile is trusted to -# drive the live phase-resolved ETA (mirrors the envelope's cycle_count>=2 gate); -# below this the priors are too noisy (single-cycle -> zero variance) and the -# estimate falls back to the classic one. Design §6 cold-start floor. -PHASE_PROFILE_MIN_CYCLES = 2 -PHASE_CONSISTENCY_MIN_CYCLES = 4 -# Heating-time std/mean above this -> likely mixed temperatures under one label. -# A clean single-temperature profile sits ~0.2 (load variation only); a profile -# mixing 30/40/90C sits ~0.45-0.6, so 0.45 catches genuine mixing with margin. -PHASE_HEAT_CV_WARN = 0.45 -PHASE_HEAT_OCC_MIXED_LO = 0.25 # heating present in only 25%-75% of cycles -> -PHASE_HEAT_OCC_MIXED_HI = 0.75 # mixed with a non-heating program CONF_SAVE_DEBUG_TRACES = ( "save_debug_traces" # Improve historical cycle data with rich debug info ) @@ -134,7 +109,7 @@ CONF_EXTERNAL_END_TRIGGER_INVERTED = "external_end_trigger_inverted" # Invert e CONF_ANTI_WRINKLE_ENABLED = "anti_wrinkle_enabled" # Dryer anti-wrinkle shielding CONF_ANTI_WRINKLE_MAX_POWER = "anti_wrinkle_max_power" # W threshold for anti-wrinkle spikes CONF_ANTI_WRINKLE_MAX_DURATION = "anti_wrinkle_max_duration" # Seconds to treat as anti-wrinkle -CONF_ANTI_WRINKLE_EXIT_POWER = "anti_wrinkle_exit_power" # W threshold for true-off exit +CONF_ANTI_WRINKLE_EXIT_POWER = "anti_wrinkle_exit_power" # W: quiet level in anti-wrinkle, floored at stop_threshold_w 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) @@ -291,7 +266,9 @@ CONF_LINKED_DEVICE = "linked_device" DEFAULT_NOTIFY_TITLE = "WashData: {device}" DEFAULT_NOTIFY_START_MESSAGE = "{device} started." -DEFAULT_NOTIFY_FINISH_MESSAGE = "{device} finished. Duration: {duration}m." +# "{duration} min", not "{duration}m": a voice assistant read "m" as metres (#93, #117). +# A template the user saved keeps its own text. +DEFAULT_NOTIFY_FINISH_MESSAGE = "{device} finished. Duration: {duration} min." DEFAULT_NOTIFY_PRE_COMPLETE_MESSAGE = "{device}: Less than {minutes} minutes remaining." DEFAULT_NOTIFY_REMINDER_MESSAGE = "{device}: about {minutes} minutes left." DEFAULT_NOTIFY_LIVE_WAITING_MESSAGE = "{device}: No profile matched yet." @@ -310,7 +287,7 @@ 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 DEFAULT_NOTIFY_UNLOAD_DELAY_MINUTES = 60 # 1 hour before "still waiting" nag notification -DEFAULT_NOTIFY_UNLOAD_MESSAGE = "{device} finished {duration}m ago - laundry is still inside." +DEFAULT_NOTIFY_UNLOAD_MESSAGE = "{device} finished {duration} min ago - laundry is still inside." DEFAULT_NOTIFY_UNLOAD_REPEAT = False # opt-in: re-send the unload reminder until dismissed (#374) # Safety bound on repeat mode (#374). The reminder is meant to run "until the door # opens", and the in-notification "Stop reminding" button is mobile_app-only - so a @@ -322,10 +299,6 @@ DEFAULT_NOTIFY_UNLOAD_REPEAT = False # opt-in: re-send the unload reminder unti NOTIFY_UNLOAD_REPEAT_MAX_REMINDERS = 48 DEFAULT_PEAK_RATE_MESSAGE = "Running at peak rate ({price}/kWh)." -# Quiet hours default: feature off (both hours unset). See CONF_NOTIFY_QUIET_*. -DEFAULT_NOTIFY_QUIET_START_HOUR = None -DEFAULT_NOTIFY_QUIET_END_HOUR = None - # Milestone notification defaults. DEFAULT_NOTIFY_MILESTONES = [50, 100, 500, 1000] DEFAULT_NOTIFY_MILESTONE_MESSAGE = "{device} has completed {cycle_count} cycles!" @@ -380,13 +353,18 @@ DEFAULT_PROFILE_MATCH_MIN_DURATION_RATIO = 0.10 # Allow match after 10% of expe # 2.5 measures slightly higher (+0.83pp) but regresses one device; 1.8 is the # point at which nothing gets worse. DEFAULT_PROFILE_MATCH_MAX_DURATION_RATIO = 1.8 -DEFAULT_MAX_PAST_CYCLES = 200 -DEFAULT_MAX_FULL_TRACES_PER_PROFILE = 20 -DEFAULT_MAX_FULL_TRACES_UNLABELED = 20 +# A cycle needs at least this many trace points before its ML health is scored +# (#459). It is the same floor `quality_features` uses before it falls back to a +# `has_trace = 0` row, which no model was trained on. +ML_HEALTH_MIN_TRACE_POINTS = 4 DEFAULT_WATCHDOG_INTERVAL = 30 # Floor; effective default is resolved per device +# A watchdog keepalive closing more than this many ticks was injected late (host +# suspend, loop stall, restart): the interval it closes is unobserved (item 391). +# On time it closes at most two (the first after a real reading), then one. +WATCHDOG_LATE_TICK_FACTOR = 2.5 # as max(this, 2*sampling_interval + 1) - see resolve_watchdog_interval_default (#396). DEFAULT_MATCH_PERSISTENCE = 3 -DEFAULT_END_REPEAT_COUNT = 1 # 1 = current behavior (no repeat required) +DEFAULT_END_REPEAT_COUNT = 1 # Removed setting (see CONF_END_REPEAT_COUNT) # Share of the SHORTEST known profile that the match-interval suggestion is # allowed to spend before a program can first be committed (#431). The @@ -444,22 +422,15 @@ 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 - -# ML live-match commit gate: P(top-1 is correct) threshold to commit a match -# before the persistence counter is satisfied. Set high to avoid false-early -# commits; the model's owner-holdout precision is ~0.87 at this score. -ML_MATCH_COMMIT_THRESHOLD = 0.85 - -# ML quality gate: P(cycle is a problem) threshold above which even a high- -# confidence auto-label is downgraded to a feedback request. Tuned for a -# specificity of ~0.84 (few false positives) so users are not flooded. -ML_QUALITY_SUSPICIOUS_THRESHOLD = 0.65 - -# Match ranking history: maximum number of per-cycle snapshots retained on-device. -# Each snapshot stores pre-computed live_match feature scalars (not traces) so -# footprint is small; 500 snapshots cover ~6–12 months of typical usage and are -# enough to build a per-device live_match training dataset. -MATCH_RANKING_HISTORY_MAX = 500 +# Fraction of the matched programme's expected duration below which a confident +# match holds a fallback end (`CycleDetector._should_defer_finish`). Its OWN +# constant, not a user option: since Feb 2026 the detector was fed the matcher's +# Stage-1 `profile_match_min_duration_ratio` (0.10, or 0.05 once the suggestion was +# applied) through a field of the same name, so a confident match only held an end +# below 5-10% of expected - never in practice. Restoring 0.8 is the live fix for +# item 390's split: splits 1.37 -> 1.03% over 295 replayed cycles, early ends +# unchanged, dishwashers byte-identical (audit DETECT-02). +DEFAULT_DEFER_FINISH_RATIO = 0.8 # Runtime overrun anomaly: a *soft, visible* signal (attribute + cycle metadata, # never a notification) flagged once a running cycle exceeds its matched @@ -532,8 +503,10 @@ ENERGY_ANOMALY_Z_THRESHOLD = 2.5 # |z-score| above this = energy anomaly # Profile warm-up mode: a newly-created profile with fewer than this many # labeled cycles skips auto-labeling and always requests manual confirmation. # Prevents the system from confidently mis-labeling cycles before it has seen -# enough examples of the program. -CONF_PROFILE_MIN_WARMUP_CYCLES = 5 # labeled cycles before auto-matching is enabled +# enough examples of the program. 5 until 0.5.8 (audit UI-26): eight programs +# meant up to 40 confirmations before the first auto-label; the label gates +# (margin, ambiguity, label_confidence) now carry what the extra prompts guarded. +CONF_PROFILE_MIN_WARMUP_CYCLES = 2 # labeled cycles before auto-labelling is enabled # Shape drift detection: compares the average power-curve envelope of the # earliest third of a profile's cycles against the most recent third. @@ -549,8 +522,9 @@ SHAPE_DRIFT_RESAMPLE_N = 50 # points for envelope comparison CLUSTER_SHAPE_SIMILARITY_THRESHOLD = 0.75 # min correlation for shape-similar cluster CLUSTER_RESAMPLE_N = 50 # points for pairwise comparison -# Terminal-drop fast finalize (opt-in; gated on CONF_ENABLE_ML_MODELS via the -# manager provider). A hard cliff-to-~0 at an elapsed offset EARLIER than this +# Terminal-drop fast finalize (on for TERMINAL_DROP_DEFAULT_ON_DEVICE_TYPES, else +# gated on CONF_ENABLE_ML_MODELS; detector_config.terminal_drop_enabled decides +# for the manager and the Playground). A hard cliff-to-~0 at an elapsed offset EARLIER than this # device has ever legitimately gone quiet (learned from its own completed # cycles) is an anomaly - almost certainly a real stop (plug pulled / cancelled) # rather than a soak pause - so the cycle is finalized quickly instead of waiting @@ -570,6 +544,13 @@ TERMINAL_DROP_MIN_PEAK_RATIO = 5.0 # cycle must have been clearly ON (peak # treated as potentially a NEW program and DEFERRED to the proven slow path # rather than assumed to be a stop. TERMINAL_DROP_PEAK_FAMILIAR_TOL = 0.4 +# Device types that get the terminal-drop finalize whatever the "Apply smart +# models" toggle says (audit ML-08). It is pure statistics, not a model, and only +# dishwashers gain from it: a plug pulled mid-wash closes in 3-4.5 min instead of +# waiting out 70-121 min (and being stored as completed); washers: 0 fires. +# Without the toggle it fires only on a committed, unambiguous match +# (detector_config.terminal_drop_may_fire). +TERMINAL_DROP_DEFAULT_ON_DEVICE_TYPES = frozenset({"dishwasher"}) DEFAULT_AUTO_TUNE_NOISE_EVENTS_THRESHOLD = 3 # Ghost cycles before threshold adjustment @@ -601,6 +582,11 @@ DEFAULT_DELAY_TIMEOUT_HOURS = 8.0 # h - give up waiting after this long CONF_PUMP_STUCK_DURATION = "pump_stuck_duration" # Seconds before a running pump is flagged as stuck DEFAULT_PUMP_STUCK_DURATION = 1800 # 30 min - typical sump pump runs <60 s; 30 min implies motor is jammed EVENT_PUMP_STUCK = "ha_washdata_pump_stuck" # Fired when stuck-pump threshold is exceeded +# Discussion #452: a cycle halted on a flat standby-level plateau (an unbalanced +# load, a door warning). Once per stall, display/automation only, never a +# notification (CycleDetector.stalled). +EVENT_CYCLE_STALLED = "ha_washdata_cycle_stalled" +CYCLE_ANOMALY_STALLED = "stalled" # the state sensor's cycle_anomaly while stalled # Profile Matching Thresholds CONF_PROFILE_MATCH_THRESHOLD = "profile_match_threshold" @@ -645,19 +631,23 @@ MATCH_MIN_RESAMPLED_POINTS = 12 # dtw_score = DIST_SCALE / (DIST_SCALE + scaled_dtw_distance). MATCH_DTW_BLEND = 0.5 MATCH_DTW_DIST_SCALE = 50.0 -MATCH_DTW_REFINE_TOP_N = 5 # DTW is applied to this many top candidates - # (5 tuned via dtw_ab_eval: rescues correct - # profiles Stage-2 ranked 4th-5th; +1.8pp) +MATCH_DTW_REFINE_TOP_N = 5 # DTW is applied to this many top candidates. + # On the shipped path 3 ties 5 and 8 is + # identical to 5; 1 costs -0.92pp mid-cycle + # (audit MR-05). # Stage-3 DTW modes (config key "dtw_mode"): # "legacy" - original: raw sequences, distance / len(current), fixed 50 W scale. # "scaled" - both sequences resampled to MATCH_DTW_RESAMPLE_N and the distance # expressed relative to the current peak (behaviour-neutral at -# MATCH_MAE_REF_PEAK), matching the Stage-2 MAE treatment. Default. +# MATCH_MAE_REF_PEAK), matching the Stage-2 MAE treatment. # "ddtw" - like "scaled" but warps on the first derivative (slope) of the # curves, so alignment is driven by shape rather than absolute level. -# "ensemble" - blend of "scaled" and "ddtw": ENSEMBLE_W*L1 + (1-W)*DDTW. -# Defaults tuned via devtools/dtw_ab_eval.py on cycle_data/ (leave-one-out top-1): -# off 62.4%, legacy 66.4%, scaled 69.9%, ddtw 69.0%, ensemble(w=0.7,dd=30) 70.7%. +# "ensemble" - blend of "scaled" and "ddtw": ENSEMBLE_W*L1 + (1-W)*DDTW. Default. +# Measured on the shipped path (devtools/eval.py, audit MR-05), vs ensemble: DTW off +# -2.16pp mid-cycle top-1 (-3.78 at 25%) but +0.16 at cycle end (n.s.); scaled +# alone -0.32, ddtw alone -0.16. Stage 3 earns its keep mid-cycle only. (The older +# dtw_ab_eval table, off 62.4% ... ensemble 70.7%, predates item 303 and did not +# run the shipped matcher.) DEFAULT_DTW_MODE = "ensemble" MATCH_DTW_RESAMPLE_N = 200 # common grid length for "scaled"/"ddtw" DTW MATCH_DDTW_DIST_SCALE = 30.0 # half-saturation for derivative-DTW distance @@ -715,13 +705,43 @@ MATCH_LABEL_MIN_MARGIN = 0.08 # so this is the conservative end of an accuracy/stability trade. Do not retune it # in isolation: any Stage-2 scoring change rescales the margin along with it. MATCH_DECISIVE_MARGIN = 0.12 -# Smart Termination landscape guard: when a non-winning candidate is at least this +# The first commit of a live programme (match_rules.decide_switch, Case 1). A winner +# that is AMBIGUOUS on the tick (inside MATCH_AMBIGUITY_MARGIN of the runner-up, or +# flagged by a Stage-5 safeguard) commits only once it has led for this many times +# match_persistence consecutive matches; a clear winner still commits at +# match_persistence. Until 0.5.8 an ambiguous winner committed at match_persistence +# too. Measured alone (devtools/decisive_margin_eval.py --switching --loo, 291 +# labelled cycles): first programme shown right on washers 29.8 -> 34.2%, +# dishwashers 90.8 -> 93.1%, washer switches per cycle 1.32 -> 1.12, for a median +# first commit 17.8 -> 19.5 min on washers (dishwashers unchanged at 11.1 min). +# Not "never": a stable winner of an always-close pair must still get a programme +# and an ETA (offline, waiting for a clear tick left 4 washer cycles uncommitted). +MATCH_AMBIGUOUS_COMMIT_FACTOR = 2 +# DISPLAY ONLY - never a gate (audit MATCH-DECIDE-15/18). The Status card's +# "Uncertain: X or Y, ~N% sure" figure while the live match is undecided: +# P(the leading guess is the right programme) as a monotone piecewise-linear map +# of the live top1-top2 margin, clamped at both ends. Being monotone, gating on it +# equals gating on the margin, so it adds nothing as a gate and must not become one. +# Fitted by devtools/margin_display_fit.py on devtools/eval.py --mode full, cuts +# 0.1-0.9 (2324 live prefix folds from 48 exports, matcher-produced labels +# excluded): 26% right at margin ~0 rising to 89% past 0.37; leave-one-source-out +# ECE 0.059 (Brier 0.191 vs base rate 0.229). A lone candidate has no runner-up +# (its margin is a 1.0 sentinel) and is right far less often than a real 1.0 +# margin, so it gets its own figure. +MATCH_SURE_KNOTS: tuple[tuple[float, float], ...] = ( + (0.007, 0.26), (0.036, 0.41), (0.064, 0.50), (0.099, 0.59), + (0.159, 0.73), (0.252, 0.84), (0.373, 0.89), +) +MATCH_SURE_SINGLE_CANDIDATE = 0.59 +# Prefix-landscape guard (#288): when a non-winning candidate is at least this # much longer than the matched profile AND has a decent shape score (before Stage-4 # duration penalty), the current trace may be a *prefix* of that longer program -# rather than a completed short one. Smart Termination is blocked; the power-based -# fallback timeout decides instead. Ratio chosen so that programmes within ~50% of +# rather than a completed short one. Ratio chosen so that programmes within ~50% of # each other (e.g. Quick 46 min vs Eco 60 min, ratio 1.30) do not trigger the guard # but genuine prefix pairs like Quick 46 vs Normal 88 min (ratio 1.91) always do. +# Since audit LIVE-18 it guards only the dryer anti-crease finalize +# (`MatchResult.is_prefix_ambiguous_full_shape`): at the ENDING gates it was every +# false block at a genuine end. SMART_TERM_LANDSCAPE_RATIO = 1.5 # candidate must be >= 1.5× the matched duration SMART_TERM_LANDSCAPE_MIN_SHAPE = 0.40 # minimum shape score (pre-Stage-4) to qualify @@ -735,27 +755,28 @@ SMART_TERM_LANDSCAPE_MIN_SHAPE = 0.40 # minimum shape score (pre-Stage-4) to qu # (3) the 1.5 ratio is knife-edge - on a real 13-programme washer the observed # neighbour ratios are 1.12-1.48, so the guard never fires at all. # -# Two independent additions, both shorten-only (they can only ever BLOCK an early -# finish, never end a cycle sooner). +# (a) Prefix scoring (for 2 + 3) was REMOVED in 0.5.8. It re-scored a longer +# candidate against its own curve truncated to the elapsed time and blocked the +# ENDING gates when that beat the winner's shape score by 0.15 (floor 0.40, ratio +# > 1.10, at most 3 scorings per match). #400 took its premise away: Stages 2/3 +# now score a running cycle against every candidate's truncated curve, so a longer +# programme whose start explains the trace better mostly wins the match itself. +# Measured on the shipped matcher (devtools/prefix_guard_eval.py --quiet-cuts +# --sweep, leave-one-out, 71 devices): 0 of 713 genuine ends and 0 of the 7 quiet +# split positives (ENDING quiet inside a pause power later resumed from, the only +# moment Smart Termination can split a cycle) at every point of a margin 0-0.15 x +# floor 0-0.60 x ratio 1.0-1.5 grid - their best prefix margin was -0.024. What it +# caught were mid-activity cuts that never reach ENDING, which (b) blocks. # -# (a) Prefix scoring (fixes 2 + 3). A longer candidate is re-scored against its own -# curve TRUNCATED to the elapsed duration, which is an apples-to-apples comparison -# and lands on the same 0-1 scale as `shape_score` (same find_best_alignment, same -# DTW blend). Because it compares equal-length series over the whole overlap it -# reads systematically higher than the full-envelope score, so it gets its OWN -# threshold rather than reusing SMART_TERM_LANDSCAPE_MIN_SHAPE. The load-bearing -# term is the MARGIN over the winner ("the longer programme explains this trace at -# least this much better than the short one does"), which is scale-free; the floor -# only rejects candidates that fit nothing. Measured on 20 real cycles + 7 -# envelopes (37 prefix-cut positives vs 17 genuine-cycle negatives): margin 0.15 -# catches 26/37 splits for 1/17 false blocks, while simply lowering the ratio to -# 1.35/1.15 costs 2/17 and 4/17 false blocks for no measured gain. -SMART_TERM_PREFIX_MARGIN = 0.15 # prefix score must beat the winner by this -SMART_TERM_PREFIX_MIN_SHAPE = 0.40 # absolute floor on the prefix score -SMART_TERM_PREFIX_MIN_RATIO = 1.10 # noise guard: ignore near-equal durations -SMART_TERM_PREFIX_MAX_CANDIDATES = 3 # cap prefix scorings per match (cost control) -SMART_TERM_PREFIX_MIN_POINTS = 12 # mirrors the matcher's >=12-sample floor -SMART_TERM_PREFIX_MIN_COVERAGE = 0.90 # template span must cover >=90% of its duration +# (c) Pause evidence (#424), on the #288 term. It asks whether the trace LOOKS like +# the start of a longer programme, not whether that programme could be quiet right +# now. A longer candidate can only explain a below-`stop_threshold_w` moment if it +# is a programme that pauses below that threshold mid-cycle, so a candidate whose +# stored cycles never once did no longer counts. Any stored pause of this length +# keeps the guard; a programme with no traced evidence keeps it too. (Measured when +# it also fed the ENDING gates: genuine-end fires 186/689 -> 142/689 on the old +# harness, which OR-ed both terms.) +SMART_TERM_PREFIX_MIN_PAUSE_S = 60.0 # (b) Power plausibility (fixes 1, the untrained case, which no candidate-pool guard # can reach). Both Smart-Termination paths key on `elapsed >= 0.98 * expected` and @@ -783,6 +804,8 @@ SMART_TERM_PREFIX_MIN_COVERAGE = 0.90 # template span must cover >=90% of its d # at 4-8x so they stay caught. A false block only costs a later finish (the # power-based fallback timeout still ends the cycle); a miss costs a split cycle. # ~1 in 5 of the remaining false blocks had a wrong top-1 anyway, where blocking is right. +# Re-measured 2026-10-04 on the shipped matcher (item 483, `--quiet-cuts`): at 3.5x +# it catches 156 of 508 split positives with 0 of 675 false blocks. SMART_TERM_TAIL_MAX_RATIO = 3.5 # block while trailing mean > this x profile tail SMART_TERM_TAIL_WINDOW_S = 300.0 # upper clamp on the trailing window SMART_TERM_TAIL_WINDOW_MIN_S = 60.0 # lower clamp (short programmes) @@ -806,11 +829,54 @@ REFERENCE_PROFILE_CURVE_POINTS = 50 # actually DROPPING (62.7%->59.9%). Raising weight alone at the old loose scale # inflated both recall and FP (net-negative), so both knobs move together. MATCH_DURATION_WEIGHT = 0.22 -# Despite the name, "energy" here means mean power (W), not Wh — the Stage-4 -# agreement term compares cur_energy=mean(curr_arr) vs profile_mean_power. +# Stage-4 duration weight while the cycle is still RUNNING (live match). Mid-cycle +# the duration term compares elapsed time with each candidate's FULL duration, a +# systematic pull toward shorter programmes (80% of 50%-elapsed errors picked a +# shorter one), so it carries less weight there. Measured leave-one-out on the +# shipped path: +0.97pp mid-cycle top-1 [+0.11, +1.86], completed cycles and the +# ambiguity rate unchanged (audit MR-03). The completed-cycle weight stays. +MATCH_DURATION_WEIGHT_IN_PROGRESS = 0.15 +# The Stage-1 LOWER duration-ratio gate is skipped for a live match during the first +# this-many seconds of a cycle. Matching starts as soon as the cycle runs, and at +# 5-10 min elapsed/avg_duration is below the 0.10 floor for every programme longer +# than 50-100 min, so only short programmes could compete: the gate removed the +# true programme on 69/114 user folds at 5 min and 35/243 at 10 min. Composed from +# the measured records: 5 min +83/-2, 10 min +63/-6, 15 min +17/-7, 25% of the cycle +# +0/-1 (audit MATCH-CORE-02). From 15 min on the gate stays, where it stops a much +# longer programme stealing the match. +MATCH_MIN_RATIO_GRACE_S = 900.0 + +# Hazard end gate (audit DETECT-16). Past an unambiguous match the ENDING fallback +# waits MARGIN x the longest below-stop pause the matched profile's traced +# evidence ever resumed from at or after this quiet's position (less SLACK of the +# run), never less than off_delay and never longer than before. Needs MIN_CYCLES +# traced cycles: a catalogue of one or two runs has not seen the programme's soaks. +END_GATE_HAZARD_MARGIN = 1.25 +END_GATE_HAZARD_MIN_CYCLES = 3 +END_GATE_HAZARD_POSITION_SLACK = 0.05 +# Register item 498: a revoked match (divergence revert, or every candidate +# rejected) leaves an envelope-verified pause with no expected duration, which only +# high power cleared, so a finished cycle sat until the force stop. Released once the +# gap-free quiet reaches END_GATE_HAZARD_MARGIN x the longest below-stop pause the +# revoked programme's traced cycles ever resumed from, never before max(off_delay, +# min_off_gap, ENDING_HARD_FINALIZE_MIN_QUIET_S) (what the unmatched fallback waits +# anyway) and, above that floor, never after this cap: past the corpus's longest +# resumed pause x margin (a 6838 s dishwasher drying phase before its pump-out -> +# 8548 s) and 1.5 h inside the watchdog's 4.5 h silence limit under a verified pause. +ORPHANED_PAUSE_MAX_WAIT_S = 10800.0 +# Stage-4 "energy" agreement. By default it compares mean power (W), not Wh: +# cur_energy=mean(curr_arr) vs profile_mean_power. Washing machines and +# washer-dryers compare integrated energy instead (analysis.stage4_energy_mode). +# While elapsed < the template span both modes reduce to the mean ratio, so the +# device choice only acts at cycle end or after an overrun (audit MR-09). 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 +# At cycle end Stage 4 takes a washer's expected energy from the median of the +# profile's own cycles (analysis.member_energy_reference) once it has this many; +# below it, and mid-cycle, the template's mean power x duration as before. 3 was +# measured slightly worse than 2 (eval.py full, cut 1.0: +3/-4 folds). +MATCH_ENERGY_REF_MIN_CYCLES = 2 # 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 @@ -818,13 +884,15 @@ MATCH_ENERGY_SCALE = 0.25 # ~ln ratio at which energy agreement halves # 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). +# penalty there ("we simply have not got there yet") was measured and rejected. On +# the shipped path (audit MR-02) it gains +4.65pp top-1 at 50% elapsed but costs +# -6.58pp at 98%, which is where Smart Termination reads the live match; it also +# raises the share of correct matches flagged ambiguous (11.2% -> 13.3%) and costs +# washer-dryers 18.2pp. Near the short programme's end it hands a longer sibling +# full duration agreement, so a dishwasher's 50 deg and 65 deg programmes land +# inside MATCH_AMBIGUITY_MARGIN of each other and Smart Termination is blocked +# (#393 is about finishing on time). The earlier "+0.7pp / -3.7pp at 90%" figures +# came from dtw_ab_eval, which does not run the shipped matcher. 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), @@ -835,6 +903,11 @@ MATCH_DURATION_SCALE_OVERRUN = 0.05 # 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 +# The fewest template samples a truncated prefix may keep and still be correlated +# and warped (analysis._prefix_point_count / prefix_shape_arrays), mirroring the +# matcher's >= 12-sample floor. Named SMART_TERM_PREFIX_MIN_POINTS while the +# removed #364 prefix guard shared it. +MATCH_PREFIX_MIN_POINTS = 12 # States @@ -850,10 +923,26 @@ STATE_FINISHED = "finished" STATE_ANTI_WRINKLE = "anti_wrinkle" STATE_INTERRUPTED = "interrupted" STATE_FORCE_STOPPED = "force_stopped" -STATE_RINSE = "rinse" + +# States in which a cycle is in progress: what `binary_sensor.*_running` reports. +# Only `running` used to count, so the sensor turned off during every soak, pause +# and the end wait, and automations that treat "off" as "done" fired mid-cycle +# (audit PLATFORM-06). STARTING is excluded (not yet a confirmed cycle), as is +# ANTI_WRINKLE (the cycle has finished; the drum only tumbles the load). +CYCLE_IN_PROGRESS_STATES = frozenset( + {STATE_RUNNING, STATE_PAUSED, STATE_USER_PAUSED, STATE_ENDING} +) + STATE_UNKNOWN = "unknown" STATE_CLEAN = "clean" # Cycle ended but door not yet opened (laundry still inside) +# A cycle start from one of these owns no update intervals yet, so the manager drops +# the cadence intervals earlier false starts left pending (#458, items 504 and 515). +CADENCE_RESET_FROM_STATES = frozenset({ + STATE_OFF, STATE_UNKNOWN, STATE_DELAY_WAIT, + STATE_FINISHED, STATE_INTERRUPTED, STATE_FORCE_STOPPED, +}) + # Authoritative state -> display color map. Single source of truth for the # full-screen panel (and any other frontend), surfaced over the WebSocket # get_constants command so colors are defined in exactly one place. Values are @@ -872,7 +961,6 @@ STATE_COLORS = { STATE_ANTI_WRINKLE: "var(--info-color, #2196f3)", STATE_INTERRUPTED: "var(--error-color, #f44336)", STATE_FORCE_STOPPED: "var(--error-color, #f44336)", - STATE_RINSE: "var(--info-color, #2196f3)", STATE_CLEAN: "var(--teal-color, #009688)", STATE_UNKNOWN: "var(--disabled-color, #bdbdbd)", "recording": "var(--error-color, #f44336)", @@ -954,14 +1042,31 @@ STANDBY_BAND_FINALIZE_DEVICE_TYPES = ( # safe - past expected AND >=10 min flat AND <=10% of the cycle's own peak is # an appliance that has finished, not one still working. STANDBY_BAND_MIN_RATIO = 1.0 # only past the expected duration +# ...but only for a plateau that IS the #445 shape: sitting at or just above the +# stop threshold, within max(STANDBY_BAND_NEAR_STOP_FACTOR x stop, +# stop + STANDBY_BAND_NEAR_STOP_W). 0.5.7 dropped the ratio for EVERY plateau the +# loose test below accepts - flat and under 10% of the heater peak, i.e. anything +# from a 0 W soak to a 70 W rinse on a 2 kW machine - so a run matched to a shorter +# programme was finalised mid-wash. Replaying the local corpus at 0.5.7 it fired on +# 8 washer cycles (0 at 0.5.6): 2 split, 6 lost 6-31 min of real activity, and none +# of the 8 plateaus sat within a few watts of the stop threshold. The #445 Miele +# (3.2-3.5 W idle on a 2.56 W stop), #458 (2.2 W on 1.76 W) and #427's AEG (0.7 W on +# 0.6 W) all do. +STANDBY_BAND_NEAR_STOP_FACTOR = 2.0 +STANDBY_BAND_NEAR_STOP_W = 3.0 +# Any other flat low plateau keeps the 0.5.6 gate: twice the expected duration. +STANDBY_BAND_LOOSE_MIN_RATIO = 2.0 # Ceiling on the measured post-activity quiet span a stored cycle may bank # (register item 297). `profile_terminal_quiet_seconds` is a median over that profile's own # cycles, so it is already self-limiting; this is the guard against a corrupted # or hand-edited value licensing an unbounded tail - the one thing the field -# exists to prevent. 30 min comfortably covers a dishwasher's passive drying -# phase, measured at a median 11% of the cycle and reaching 43%. -TERMINAL_QUIET_CAP_S = 1800.0 +# exists to prevent. Not a measurement of drying: 30 min did not cover it +# (register item 469). 01KGM619's Eco dries 4840-4860 s before its pump-out, so +# a run closed without one stored last activity + 1800 s (~8.0k s of an ~11.1k s +# programme). At 2 h: 12 such replays store 10.0-11.3k s, end lag unchanged on +# dishwashers but one cycle (+3 min) and no early end or split moved. +TERMINAL_QUIET_CAP_S = 7200.0 # A measured quiet span is only trusted as a tail allowance when the profile has # actually shown it repeatedly (register item 297). Measured over 20 real profiles: the two # dishwashers, which genuinely end in a passive drying phase, scored 20/20 and @@ -980,6 +1085,11 @@ BANKED_TAIL_REPAIR_KEY = "_banked_tail_repair_pending" # span is worth correcting. Measured median banking was 12.6 min, so this only # skips noise. BANKED_TAIL_REPAIR_MIN_S = 60.0 +# A dishwasher's stored end never falls before this fraction of the shortest +# length the user has vouched for in its profile (`manual_duration`, a recorder +# capture, a golden cycle). Shared by the banked-tail repair and the live +# `_keep_tail_cap` (register item 384), so the two store the same duration. +TRUSTED_LENGTH_FLOOR_FRAC = 0.9 STANDBY_BAND_WINDOW_S = 600.0 # require a >=10 min flat plateau STANDBY_BAND_MAX_FRACTION = 0.10 # plateau level <= 10% of the cycle's peak STANDBY_BAND_FLATNESS_FRACTION = 0.03 # window (max-min) <= 3% of the cycle's peak @@ -1052,10 +1162,6 @@ DEFAULT_ANTI_CREASE_FINALIZE_RATIO = 0.98 # elapsed must reach 98% of expected # 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 @@ -1080,6 +1186,14 @@ ANTI_CREASE_TERMINAL_MATCH_FRAC = 0.5 # live high-power seconds after that # 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 +# Register item 480: the envelope's max band arms the guard when ANY member's last +# block above the level is terminal, and washer spins straddle 400 W (held runs +# peak at 331-394 W, the members that "spin" at 404-437 W). Once the band arms, +# the guard stays armed only when at least this share of the profile's completed +# traced members end with their own terminal block; with fewer members than the +# floor the band (or sample) decides alone, as before. +ANTI_CREASE_SPIN_ARM_MIN_SHARE = 0.5 +ANTI_CREASE_SPIN_ARM_MIN_MEMBERS = 2 # Device Type Defaults # Device Type Defaults (Maps) @@ -1145,6 +1259,15 @@ DISHWASHER_MATCH_FREEZE_QUIET_SECONDS = 300.0 # caught by the end-spike arm first. Smaller than the 30-min window but large enough # to confirm a terminal tail rather than an inter-phase gap. DISHWASHER_END_SPIKE_QUIET_RELEASE_SECONDS = 600.0 +# ...but never shorter than this multiple of the matched profile's MEASURED quiet +# before its terminal event (`profile_terminal_quiet_seconds`, match element 11): +# a release after 600 s of quiet is premature on a programme measured to wait +# 934-1810 s before its pump-out, and ended the corpus's "65° full" 12 min early +# (register item 392). Lengthen-only, and bounded by the 30 min spike wait. +# The same margin applies to the longest below-stop pause the profile's traced +# cycles ever resumed from (match element 14, register item 465): element 11 is a +# median that a cycle closed before its pump-out drags down. +DISHWASHER_QUIET_RELEASE_TERMINAL_MARGIN = 1.1 # Confirmation window a dishwasher must spend in ENDING before Smart Termination # fires. This is deliberately a FIXED constant and NOT derived from off_delay: @@ -1330,9 +1453,18 @@ def resolve_min_off_gap_default(device_type: str) -> int: return int(DEFAULT_MIN_OFF_GAP_BY_DEVICE.get(device_type, DEFAULT_MIN_OFF_GAP)) -def resolve_off_delay_default(device_type: str) -> int: - """Device-resolved off delay (#445), published for the same reason.""" - return int(DEFAULT_OFF_DELAY_BY_DEVICE.get(device_type, DEFAULT_OFF_DELAY)) +def resolve_off_delay_default(device_type: str) -> int: # noqa: ARG001 + """The off delay a device runs on when it has none set (#445). + + ``DEFAULT_OFF_DELAY`` for every type. ``DEFAULT_OFF_DELAY_BY_DEVICE`` is the + suggestion engine's FLOOR for a proposed value, not a runtime default: no + device has ever run on it, because the config flow does not store an off + delay and the manager falls back to the scalar. Publishing the table here + made the panel show a dishwasher's unset Off Delay as 1800 s while the + detector used 180 s - and 180 is the floor of the late ENDING shortening, so + the difference is not cosmetic. The parameter is kept for the call sites. + """ + return int(DEFAULT_OFF_DELAY) def resolve_start_duration_default(device_type: str) -> float: @@ -1347,11 +1479,6 @@ def resolve_start_duration_default(device_type: str) -> float: return max(DEFAULT_START_DURATION_THRESHOLD, sampling) -# Default profile match min duration ratio by device type -DEFAULT_PROFILE_MATCH_MIN_DURATION_RATIO_BY_DEVICE = { - DEVICE_TYPE_DISHWASHER: 0.10, -} - # Default Smart-Termination duration ratio by device type (#393). Dishwashers run # fixed programs (measured spread +4%/+17% around the mean), so the conservative # 0.99 gate is defensible there; every other type keeps the scalar @@ -1408,14 +1535,14 @@ def resolve_smart_termination_duration_ratio_default(device_type: str) -> float: device_type, DEFAULT_SMART_TERMINATION_DURATION_RATIO ) -# Profile groups (Stage 5): the matcher only collapses a group into one -# aggregate candidate when its members' minimum pairwise shape similarity is at -# least this. Similarity is DTW/Sakoe-Chiba on peak-normalised envelopes, so it -# tolerates the duration (longer heating/draining) and amplitude (temp/spin) -# variation between real members. Looser groups stay individual (a blurry generic -# aggregate could out-match unrelated profiles) and are flagged in the UI. -# Calibrated on real profiles: genuine temp/spin variants score ~0.86-0.95, -# distinct programs <~0.6; 0.80 leaves margin below the 0.85 suggestion bar. +# Profile groups (Stage 5): a group is mapped to its members (and so collapsed into +# one family after every member is scored on its own curve) only when the members' +# minimum pairwise shape similarity is at least this. There is no aggregate +# candidate any more (#400). Similarity is DTW/Sakoe-Chiba on peak-normalised +# envelopes, so it tolerates the duration (longer heating/draining) and amplitude +# (temp/spin) variation between real members. Looser groups stay individual and are +# flagged in the UI. Calibrated on real profiles: genuine temp/spin variants score +# ~0.86-0.95, distinct programs <~0.6. GROUP_MIN_COHESION = 0.80 # Per-profile terminal signature (`profile_store.compute_profile_terminal_signature`). @@ -1429,6 +1556,13 @@ GROUP_MIN_COHESION = 0.80 # 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 +# ...but a gap of 120 s also sits between a dishwasher's own heating blocks +# (120-160 s on 01KGM619), so a cycle closed before its pump-out offered its LAST +# HEATING BLOCK as the terminal event (register item 469). A terminal event is a +# low-power one: over every corpus dishwasher the candidates peak at <= 9.1% of +# their cycle's peak (median 1.3%) or at >= 90% (main activity), nothing between. +# Above this fraction the last run is main activity and the trace has no event. +TERMINAL_EVENT_MAX_PEAK_FRAC = 0.25 # Below this many evidence cycles the medians describe noise, not the programme. TERMINAL_SIGNATURE_MIN_CYCLES = 3 # Cycles a profile needs before one of them can be called a duration outlier @@ -1448,18 +1582,52 @@ SELF_UNMATCHABLE_MIN_CYCLES = 3 # v9: pre-initialize additive top-level keys (lifetime_energy_wh, # settings_changelog, maintenance_log) so they are present from first load # rather than only appearing lazily on first use. -# v11 is a marker-only bump: per-phase profiles (envelope["phase_profile"]) are -# derived cache populated by async_rebuild_envelope, so no data migration is -# needed - they self-populate on the next envelope rebuild. +# v11 is a marker-only bump. It introduced a per-phase envelope cache for the +# phase-resolved ETA, removed with that stack in 0.5.8 (register item 411); the +# version stays because a store version can never go back down. # v12: initialize `backfill_cycles`, the third cycle list (issue #344). Cycles # recovered from raw power history predating the integration are auto-detected and # unverified, so they belong in neither `past_cycles` (which feeds lifetime stats, ML # training labels and the feedback queue, and is retention-evicted oldest-first) nor # `reference_cycles` (curated community-store templates, golden by construction). # Additive `setdefault`, so it is idempotent and loses nothing. -STORAGE_VERSION = 13 +# v13: marker-only, arms the one-time banked-tail repair (register item 297). +# v14: marker-only, re-arms it (#424): the v13 pass judged the drying phase at the +# wrong threshold and skipped dishwasher timeout finishes. +# v15: label provenance repair (audit MANAGER-01): cycles the user confirmed or +# corrected in the review queue are stamped `label_source="manual"`, and an answer +# the panel's Auto-label had replaced is put back. Pure data, idempotent. +# v16: review-queue cleanup (register item 433): pending requests the new rule +# would not raise are dropped, without recording an answer. Idempotent. +# v17: drops the state of the ML parts removed in 0.5.8 (the early match commit, +# the quality gate, the matcher weight tuner, and on-device training of every head +# but total_energy): `match_ranking_history`, `matching_config`, and the +# `live_match` / `quality` / `end` / `remaining_time` records in +# `ml_model_versions` / `ml_training_history`. No cycle or label is touched. +# Idempotent. +STORAGE_VERSION = 17 STORAGE_KEY = "ha_washdata" +# Restore point for "Undo last import" (register item 195): the store as it was before +# the last replace import, in its own file `ha_washdata..pre_import` so the +# main store's per-save rewrite never carries a second copy. One per device: the next +# replace import overwrites it, an undo consumes it, deleting the device removes it +# (`async_remove_entry` + the orphan sweep in `__init__.py`). The record carries the +# `STORAGE_VERSION` it was taken at, and a restore migrates it forward. +PRE_IMPORT_STORE_SUFFIX = "pre_import" +PRE_IMPORT_STORE_VERSION = 1 + +# Notifications held by quiet hours / presence when Home Assistant stops or the entry +# unloads, in `ha_washdata..notify_queue` (audit MANAGER-16). Written at the +# stop, read and deleted once HA has started again; removed with the device. +NOTIFY_QUEUE_STORE_SUFFIX = "notify_queue" + +# The last active-cycle snapshot that failed to restore, in +# `ha_washdata..failed_restore` (register item 266 follow-up): kept for the +# diagnostics download instead of being deleted with the cycle it held. Written only +# on a failure, one per device (the next failure overwrites it), removed with the device. +FAILED_RESTORE_STORE_SUFFIX = "failed_restore" + # ─── Config-entry schema version (NOT the storage version above) ─────────────── # Single source for the config-entry schema: `ConfigFlow.VERSION`/`MINOR_VERSION`, every # stepwise block in `async_migrate_entry`, and the `minor_version=` the one-pass legacy @@ -1488,8 +1656,6 @@ SERVICE_SUBMIT_FEEDBACK = ( # no panel sections render and no background work runs. # # SHOW_ML_LAB ML Lab comparison tab in the WashData panel. -# ENABLE_ML_SUGGESTIONS ML-model-driven setting suggestions (Stage 3), shown -# side-by-side with the classic statistical suggestions. # ENABLE_ML_TRAINING On-device model training loop (Stage 4): scheduled # retraining on the user's own labeled cycles. # @@ -1497,8 +1663,16 @@ SERVICE_SUBMIT_FEEDBACK = ( # are always on - they only improve the existing suggestion engine and add no # new surfaces, so they need no flag. SHOW_ML_LAB = True -ENABLE_ML_SUGGESTIONS = True ENABLE_ML_TRAINING = True +# Frozen off even when a device enables ML models (audit ML-05): replayed on 292 +# real cycles the end-guard prevented no premature stop and raised the washer +# median end lag 12.2 -> 17.5 min, every deferral the full 30 min cap. +ENABLE_ML_END_GUARD = False +# Removed in 0.5.8, after being frozen here: the remaining-time regressor (C4, +# audit ML-07: worse than the naive estimate on 7 of 8 installs), the early match +# commit (C2, audit ML-02: 31% of its early commits wrong vs 8.4% for +# persistence) and the quality gate (C3, audit ML-06: fired on 0 of the 12 +# auto-label-eligible real cycles), with the matcher weight tuner. # ─── Community store (online features) ──────────────────────────────────────── # Opt-in browsing/importing/sharing of reference cycles via the WashData Store. @@ -1519,28 +1693,71 @@ DEFAULT_ENABLE_ONLINE_FEATURES = False SHAREABLE_SETTING_KEYS: tuple[str, ...] = ( # Detection / recognition CONF_MIN_POWER, - CONF_OFF_DELAY, CONF_START_THRESHOLD_W, CONF_STOP_THRESHOLD_W, CONF_START_DURATION_THRESHOLD, CONF_START_ENERGY_THRESHOLD, CONF_COMPLETION_MIN_SECONDS, - CONF_MIN_OFF_GAP, CONF_END_ENERGY_THRESHOLD, - CONF_POWER_OFF_THRESHOLD_W, - CONF_POWER_OFF_DELAY, + # (off_delay, min_off_gap, power_off_*, profile_match_interval dropped: they + # are functions of the SHARER'S plug cadence, not the model - a 94 s plug's + # 1800 s off_delay is a 30 min end lag on a 1 s plug. Audit STORE-06.) # Matching CONF_PROFILE_MATCH_THRESHOLD, CONF_PROFILE_UNMATCH_THRESHOLD, - CONF_PROFILE_MATCH_INTERVAL, CONF_PROFILE_MATCH_MIN_DURATION_RATIO, CONF_PROFILE_MATCH_MAX_DURATION_RATIO, - CONF_PROFILE_DURATION_TOLERANCE, + # (profile_duration_tolerance dropped: nothing reads it - audit DOCS-01.) CONF_DURATION_TOLERANCE, CONF_AUTO_LABEL_CONFIDENCE, CONF_LEARNING_CONFIDENCE, ) + +# Shared settings the panel bounds to 0-1 (scores and a fraction). Every shareable +# setting is also >= 0 there. +_SHARED_UNIT_INTERVAL_KEYS = frozenset({ + CONF_PROFILE_MATCH_THRESHOLD, + CONF_PROFILE_UNMATCH_THRESHOLD, + CONF_DURATION_TOLERANCE, + CONF_AUTO_LABEL_CONFIDENCE, + CONF_LEARNING_CONFIDENCE, +}) + + +def sanitize_shared_settings(settings: Any) -> dict[str, float]: + """The allow-listed, finite, numeric subset of a shared settings map. + + Every share/adopt/export site goes through this. A value outside the panel's + own range is dropped (a match threshold of 5 can never be met by a 0-1 score). + The duration ratios are also held to the shipped bounds (audit STORE-06): + 25/25 store bundles carried a max ratio below 1.8 (12 at the 1.5 measured to + delete the true candidate on 2.3% of folds, register item 311), and min ratios + up to 0.81 forbid any match before 81% of a programme. + """ + if not isinstance(settings, dict): + return {} + out: dict[str, float] = {} + for key, value in settings.items(): + if key not in SHAREABLE_SETTING_KEYS or isinstance(value, bool): + continue + if not isinstance(value, (int, float)): + continue + try: + number = float(value) # an oversized JSON integer raises here + except OverflowError: + continue + if not math.isfinite(number): + continue + if number < 0 or (key in _SHARED_UNIT_INTERVAL_KEYS and number > 1): + continue + if key == CONF_PROFILE_MATCH_MAX_DURATION_RATIO: + value = max(value, DEFAULT_PROFILE_MATCH_MAX_DURATION_RATIO) + elif key == CONF_PROFILE_MATCH_MIN_DURATION_RATIO: + value = min(value, DEFAULT_PROFILE_MATCH_MIN_DURATION_RATIO) + out[str(key)] = value + return out + # Public Firebase web config for the community store (NOT secret - identifies the # project; access is enforced by the store's Firestore rules). STORE_PROJECT_ID = "washdata-store" @@ -1568,16 +1785,10 @@ DEFAULT_ML_TRAINING_HOUR = 2 # 02:00 local - quiet hour DEFAULT_ML_TRAINING_MIN_CYCLES = 30 # need a meaningful corpus first DEFAULT_ML_TRAINING_INTERVAL_DAYS = 7 # retrain at most weekly -# A newly trained model is only promoted over the shipped baseline when its -# held-out AUC is at least (baseline AUC - this margin). Small negative slack is -# allowed so personalisation can win even at a tiny AUC cost. -ML_TRAINING_AUC_MARGIN = 0.02 -# Separate tolerance for the calibration gate: a retrained classifier must not -# degrade balanced accuracy AT the live operating cutoff by more than this. Kept -# distinct from ML_TRAINING_AUC_MARGIN because it bounds a different metric (decision -# quality at a fixed threshold, not overall rank quality); same 0.02 default today. -ML_TRAINING_BACC_MARGIN = 0.02 -ML_TRAINING_MIN_POSITIVES = 20 # need at least this many positive examples to trust a fit +# (The classifier promotion gate - AUC margin, balanced-accuracy margin, minimum +# positives - was removed in 0.5.8 with on-device classifier training: audit ML-11 +# measured it promoting worse models on 4-7 held-out positives. Only the +# total_energy regressor is trained on-device now.) # Per-capability held-out-score history kept across training runs, so the panel # can show whether a model's fit is improving, steady, or declining over time @@ -1585,18 +1796,19 @@ ML_TRAINING_MIN_POSITIVES = 20 # need at least this many positive examples to t # are retained. ML_TRAINING_HISTORY_MAX = 30 -# Remaining-time regressor (standardized_linear). Unlike the classifier heads it -# has no shipped baseline; it is only promoted when its held-out mean-absolute -# error on the completion-fraction target beats the naive elapsed/expected -# estimate by at least this relative margin (5% lower MAE). Trained from prefixes -# of the device's own clean cycles. +# Total-energy regressor (standardized_linear), the one head trained on-device. +# It has no shipped baseline; it is only promoted when its held-out mean-absolute +# error on the energy-fraction target beats the naive elapsed/expected +# estimate by at least this relative margin (5% lower MAE) AND beats the model +# already in use, scored on the same held-out cycles (audit ML-12). Trained from +# prefixes of the device's own clean cycles. ML_TRAINING_REGRESSION_MARGIN = 0.05 ML_TRAINING_MIN_REGRESSION_ROWS = 30 # synthesized prefix rows needed to fit -# How strongly a promoted remaining-time regressor influences the live progress -# estimate. The ML completion-fraction is blended with the phase-aware estimate -# at this weight before the existing EMA smoothing/monotonicity guards run, so a -# bad model can never wholly override the proven phase estimator. -ML_PROGRESS_BLEND_WEIGHT = 0.5 +# Held-out cycles a regressor must be scored on before it can be promoted. The +# 20% holdout rested on ONE cycle for the only real promotion on record (audit +# PROGRESS-16); the split now holds out at least this many when the device has +# twice as many usable cycles, and never promotes on fewer. +ML_TRAINING_MIN_HOLDOUT_CYCLES = 5 # Service + event names for the training loop. SERVICE_TRIGGER_ML_TRAINING = "trigger_ml_training" @@ -1627,33 +1839,68 @@ DEFAULT_MAINTENANCE_REMINDER_CYCLES = { "filter_clean": 50, "drum_clean": 100, } -# Recognised maintenance event types. bearing_service / other default off (absent -# from the default reminder dict) and are opt-in. +# Recognised built-in maintenance event types. bearing_service / other default off +# (absent from the default reminder dict) and are opt-in. The last four are the +# device-type presets of discussion #461 (see MAINTENANCE_PRESETS_BY_DEVICE_TYPE). MAINTENANCE_EVENT_TYPES = ( "descale", "filter_clean", "drum_clean", "bearing_service", "other", + "salt", + "rinse_aid", + "lint_filter", + "condenser_clean", ) +# Preset defaults per device type (cycles), replacing DEFAULT_MAINTENANCE_REMINDER_CYCLES +# for that type while its reminder config was never saved. None of these can be +# measured from power: they are manufacturer ballparks set on the early side, so the +# reminder comes with headroom. Every type not listed keeps the washer default. +MAINTENANCE_PRESETS_BY_DEVICE_TYPE: dict[str, dict[str, int]] = { + DEVICE_TYPE_DISHWASHER: { + # A 1-2 kg softener reservoir lasts ~30-60 cycles at medium-hard water. + "salt": 30, + # A ~110-150 ml rinse-aid reservoir at ~3 ml per cycle lasts ~40-50 cycles. + "rinse_aid": 40, + # Same 50 as the washer default, so an existing dishwasher's reminder stays put. + "filter_clean": 50, + }, + DEVICE_TYPE_DRYER: { + # Manufacturers say every load; 10 is a backstop for a forgotten filter. + "lint_filter": 10, + # Condenser / heat-pump filters: manufacturers suggest every ~20-50 loads. + "condenser_clean": 30, + }, +} +# Built-in types the reminder editor offers per device type (in this order). Types +# not listed get the original five. A type with a saved positive threshold is shown +# whatever its device type, so no saved reminder ever disappears from the editor. +MAINTENANCE_TYPES_BY_DEVICE_TYPE: dict[str, tuple[str, ...]] = { + DEVICE_TYPE_DISHWASHER: ("salt", "rinse_aid", "filter_clean", "descale", "other"), + DEVICE_TYPE_DRYER: ("lint_filter", "condenser_clean", "other"), +} +# The preset types count "since last done" from the moment their reminder is first +# active (a baseline stamped in the store), not from odometer 0: an upgraded +# dishwasher with 300 cycles must not open with "salt due" (#461). The original five +# keep counting from the whole odometer when never logged, exactly as before. +MAINTENANCE_COUNT_FROM_ENABLE_TYPES = frozenset( + {"salt", "rinse_aid", "lint_filter", "condenser_clean"} +) +# User-defined maintenance tasks (#461), stored per device in the profile store +# ("maintenance_tasks") next to the log entries that reference them. Each has a +# free-text name and an interval in cycles and/or days (0 = off for that axis); +# it is due when either is reached. Ids carry this prefix so they can never collide +# with a built-in type. +MAINTENANCE_CUSTOM_TASK_PREFIX = "custom_" +MAINTENANCE_CUSTOM_TASK_MAX = 20 +MAINTENANCE_TASK_NAME_MAX = 60 +MAINTENANCE_INTERVAL_CYCLES_MAX = 100000 +MAINTENANCE_INTERVAL_DAYS_MAX = 3650 # A logged maintenance event of a matching type within this many days suppresses # the "needs maintenance" nag advisory (duration-trend / shape-drift). MAINTENANCE_RECENT_SUPPRESS_DAYS = 30 -# ─── Playground stress-tail constants (never used by the live integration) ───── -# These govern the synthetic idle continuation in the "Test idle termination" -# Playground toggle. All times are in seconds. -PLAYGROUND_STRESS_TRAILING_WINDOW_S: float = 60.0 # window for idle-floor derivation -PLAYGROUND_STRESS_FLOOR_PERCENTILE: float = 0.07 # p7 of window readings = standby floor -PLAYGROUND_STRESS_FLUCT_FALLBACK_FRAC: float = 0.12 # ±12% fallback when window is flat -PLAYGROUND_STRESS_DENSE_STEP_S: float = 30.0 # dense pre-fill cadence -PLAYGROUND_STRESS_DENSE_DURATION_S: float = 1200.0 # dense pre-fill length (20 min) -PLAYGROUND_STRESS_SPARSE_STEP_S: float = 1800.0 # sparse main step (30 min) -PLAYGROUND_STRESS_MAX_SPARSE_STEPS: int = 15 # max sparse steps → max 7.5 h extra -PLAYGROUND_STRESS_MAX_IDLE_W: float = 100000.0 # upper bound for a manual idle override - # (far beyond any appliance; guards against - # inf/absurd values corrupting synthesis) - # ─── Playground setting presets (sandbox snapshots, per device) ──────────────── # Named snapshots of the Playground control panel's values, stored under the # "playground_presets" store key. They never touch the live config: publishing a @@ -1677,9 +1924,8 @@ CONF_PROFILE_EVIDENCE_SOURCES = "profile_evidence_sources" EVIDENCE_REAL_CYCLES = "real_cycles" EVIDENCE_REFERENCE_CYCLES = "reference_cycles" EVIDENCE_BACKFILL_CYCLES = "backfill_cycles" -# `real_cycles`/`reference_cycles` match the export taxonomy (`_EXPORT_CATEGORIES`); the -# evidence view adds `backfill_cycles`, which the selective-export wizard does not yet -# enumerate (whole-store export still round-trips it). +# All three match the export taxonomy (`_EXPORT_CATEGORIES`), which the selective +# export/import wizard enumerates per category (register item 129e). PROFILE_EVIDENCE_SOURCES = ( EVIDENCE_REAL_CYCLES, EVIDENCE_REFERENCE_CYCLES, @@ -1696,7 +1942,9 @@ DEFAULT_PROFILE_EVIDENCE_SOURCES = list(PROFILE_EVIDENCE_SOURCES) HISTORY_IMPORT_MAX_BYTES: int = 32 * 1024 * 1024 # staged upload cap (~32 MiB of CSV text) HISTORY_IMPORT_MAX_ROWS: int = 500_000 # parsed-row cap (≈ a month at 5 s) HISTORY_IMPORT_CHUNK_BYTES: int = 512 * 1024 # per-WS-message upload chunk (frame cap is 4 MiB) -HISTORY_IMPORT_CHUNK_SAMPLES: int = 4000 # samples replayed per executor job +# Samples replayed per executor job. 4000 was ~1.0 s of GIL per job on a desktop, +# i.e. the #311 freeze pattern on a Pi (audit PLAYGROUND-11); 1000 is ~0.25 s. +HISTORY_IMPORT_CHUNK_SAMPLES: int = 1000 HISTORY_IMPORT_MIN_BLOCK_SAMPLES: int = 20 # floor for the per-block sample gate HISTORY_IMPORT_MAX_MEDIAN_INTERVAL_S: float = 120.0 # floor for the per-block cadence gate; the # effective gate is @@ -1707,6 +1955,9 @@ HISTORY_IMPORT_EDGE_GAP_S: float = 60.0 # leading samples this far # are hourly-average debris and are trimmed # (leading edge ONLY - trimming the trailing # edge eats a real cycle's low-power tail) +HISTORY_IMPORT_MAX_BRIDGE_S: float = 300.0 # an `unavailable` hole up to this long inside + # a block is bridged as a plain gap (Wi-Fi + # blip, HA restart), not a cut (PLAYGROUND-06) HISTORY_IMPORT_MAX_BLOCK_SPAN_S: float = 12 * 3600.0 # a block longer than this can only produce the # detector's 8 h `force_stopped` blob, so it is # reported rather than replayed @@ -1737,3 +1988,59 @@ HISTORY_IMPORT_RECORDER_EMPTY_DAY_STOP: int = 30 # consecutive empty days th # without this, a 10-year request would issue # thousands of pointless queries. HISTORY_IMPORT_SOURCE: str = "history_import" # `meta.source` marker on imported cycles + + +def numeric_option_keys() -> dict[str, type]: + """Option keys whose compiled default is a number -> that default's type. + + Derived from the ``CONF_X`` / ``DEFAULT_X`` naming pair, so a new numeric + setting is covered without a list to maintain (audit PLATFORM-13). + """ + g = globals() + out: dict[str, type] = {} + for name, key in g.items(): + if not name.startswith("CONF_") or not isinstance(key, str): + continue + default = g.get("DEFAULT_" + name[5:]) + if isinstance(default, (int, float)) and not isinstance(default, bool): + out[key] = type(default) + return out + + +def coerce_numeric_option(value: Any, kind: type) -> float | int | None: + """``value`` as a finite number of ``kind``'s type, or None if it is not one.""" + if isinstance(value, bool): + return None + try: + number = float(value) + except (TypeError, ValueError, OverflowError): + return None + if not math.isfinite(number): + return None + if kind is int: + return int(number) if number.is_integer() else number + return number + + +def drop_invalid_numeric_options(options: Any) -> tuple[dict[str, Any], list[str]]: + """``(options without non-numeric numeric settings, the keys dropped)``. + + A non-numeric value for a numeric setting is dropped so its default applies; + stored, it raised in the manager's constructor and the entry never set up + again (audit PLATFORM-13, register item 279). + """ + if not isinstance(options, dict): + return {}, [] + kinds = numeric_option_keys() + clean = dict(options) + dropped: list[str] = [] + for key, kind in kinds.items(): + if key in clean and clean[key] is not None: + number = coerce_numeric_option(clean[key], kind) + if number is None: + clean.pop(key) + dropped.append(key) + else: + clean[key] = number + return clean, dropped + diff --git a/custom_components/ha_washdata/cycle_detector.py b/custom_components/ha_washdata/cycle_detector.py index 0b538d78..47e5191f 100644 --- a/custom_components/ha_washdata/cycle_detector.py +++ b/custom_components/ha_washdata/cycle_detector.py @@ -21,6 +21,7 @@ from __future__ import annotations import itertools import logging import math +import traceback from dataclasses import dataclass from datetime import datetime, timedelta from typing import Any, Callable, cast @@ -28,8 +29,12 @@ import numpy as np from homeassistant.util import dt as dt_util +from . import match_rules from .log_utils import DeviceLoggerAdapter +from .time_utils import utc_now from .const import ( + TERMINAL_EVENT_PEAK_FRAC, + DISHWASHER_QUIET_RELEASE_TERMINAL_MARGIN, ANTI_WRINKLE_ELIGIBLE_REASONS, TerminationReason, STATE_OFF, @@ -43,6 +48,7 @@ from .const import ( STATE_INTERRUPTED, STATE_FORCE_STOPPED, STATE_UNKNOWN, + STATE_IDLE, DEVICE_TYPE_WASHING_MACHINE, DEVICE_TYPE_DRYER, DEVICE_TYPE_WASHER_DRYER, @@ -69,10 +75,17 @@ from .const import ( END_GATE_LATE_RATIO, resolve_end_gate_late_ratio, END_GATE_LATE_SECONDS, + END_GATE_HAZARD_MARGIN, + END_GATE_HAZARD_MIN_CYCLES, + END_GATE_HAZARD_POSITION_SLACK, STANDBY_BAND_FINALIZE_DEVICE_TYPES, STANDBY_BAND_MIN_RATIO, + STANDBY_BAND_LOOSE_MIN_RATIO, + STANDBY_BAND_NEAR_STOP_FACTOR, + STANDBY_BAND_NEAR_STOP_W, DEVICE_TYPE_DISHWASHER, TERMINAL_QUIET_CAP_S, + TRUSTED_LENGTH_FLOOR_FRAC, STANDBY_BAND_WINDOW_S, STANDBY_BAND_MAX_FRACTION, STANDBY_BAND_FLATNESS_FRACTION, @@ -115,12 +128,40 @@ ML_END_GUARD_MAX_DEFER_SECONDS = 1800.0 # cap the extra wait the guard may add # (data-clock seconds). Safe to cache: the guard only ever *defers* and terminal # drop only ever *shortens*, so both tolerate a value up to this window stale. ML_PROVIDER_THROTTLE_SECONDS = 30.0 +# ENDING energy gate vs a flat standby (audit DETECT-08, item 95 residual). The +# gate's bar is end_energy_threshold x 3600 / off_delay as a MEAN power (1.0 W at +# 180 s, 0.1 W at 1800 s), so a standby sitting between that bar and the stop +# threshold pinned it forever: an unmatched cycle ran to the 8 h cap. A window is +# a flat standby when every reading of the last max(off_delay, +# STANDBY_BAND_WINDOW_S) seconds is above 0 W and the window spans no more than +# max(FLOOR_W, FRAC x its highest reading). The non-zero floor is what keeps the +# gate's one measured catch: a dishwasher's passive drying flickering 0-0.5 W +# for 25 min before its pump-out (Hatton ECO `44d35b4ca01e`). +# +# Two tiers, like the standby band's. Just under stop (every reading >= +# NEAR_STOP_FRAC x stop_threshold_w) is the item-95 shape and ends after the +# un-shortened max(off_delay, min_off_gap). Any other flat window waits +# LOOSE_QUIET_S: flat non-zero phases far under stop DO resume in real cycles. +# Over every stored trace in the corpus (all formats) the resumed flat non-zero +# sub-stop pauses longer than that device's min_off_gap are two, and both sit at +# about half of stop: an 18.9 min washer soak at 2.7-3.0 W on a 6 W stop (it split +# with one tier at min_off_gap, end_gate_eval --shipped-watchdog) and an 80.8 min +# dishwasher drying phase at 0.7-0.9 W on a 1.5 W stop (it split replayed +# unmatched). Neither reaches 0.75 x stop; 2 h is 1.5x the longer one. +ENDING_FLAT_STANDBY_MIN_READINGS = 3 +ENDING_FLAT_STANDBY_SPREAD_FRAC = 0.25 +ENDING_FLAT_STANDBY_SPREAD_FLOOR_W = 0.2 +ENDING_FLAT_STANDBY_NEAR_STOP_FRAC = 0.75 +ENDING_FLAT_STANDBY_LOOSE_QUIET_S = 7200.0 if not 0 < DISHWASHER_END_SPIKE_MIN_PROGRESS < 1: raise ValueError("DISHWASHER_END_SPIKE_MIN_PROGRESS must be a fraction in (0, 1)") from .signal_processing import ( energy_gap_threshold_s, integrate_wh, - quiet_run_before, + median_fast, + percentile_linear, + terminal_event_end, + terminal_quiet_seen, ) _LOGGER = logging.getLogger(__name__) @@ -134,6 +175,131 @@ _LOGGER = logging.getLogger(__name__) # window expires (issue #267). STOP_LOCKOUT_RELEASE_SECONDS = 180.0 +# Register item 501: how much of start_energy_threshold a standby RE-probe (one +# that began with no reading below stop_threshold_w since the last false start) +# must fill before entities show it as `starting`. Display only: detection never +# reads it. Measured by devtools/start_gate_eval.py (flickers per idle day). +STANDBY_REPROBE_SHOW_ENERGY_FRACTION = 0.5 + +# Register item 515: a false start out of Finished / Interrupted / Force-Stopped +# returns to that state with its original entry time (as item 504 returns one +# out of DELAY_WAIT), instead of falling to OFF. With it the manager clears the +# cycle end, Clean and the unload nag only when a probe commits (RUNNING), not +# when it begins: a probe that aborted used to lose all three, and a straddling +# standby (#35) probes on almost every reading. The A/B switch for the revert +# checks and `devtools/start_gate_eval.py` (`terminal_lost`). +TERMINAL_PROBE_RETURNS = True + +# Discussion #452: two more display-only states, read by `exposed_state` and never +# by detection. +# +# STALLED. A washer that halts mid-cycle (an unbalanced load, a door warning) +# sits at its standby draw, just ABOVE stop_threshold_w, so the cycle never even +# pauses. A stall is a flat run of readings in the standby band's near-stop shape +# (`standby_near_stop_ceiling`, at most STANDBY_BAND_MAX_FRACTION of the cycle's +# peak, spread within the band's flatness limit) that has lasted longer than any +# low stretch the matched programme recorded from that position on +# (END_GATE_HAZARD_MARGIN x the longest in its near-stop pause catalogue, element +# 15; at least STALL_MIN_S) while the programme still owes work: its terminal +# high-power block not yet seen (#399's evidence), or, for a programme without +# one, the run began before STALL_OWES_MAX_POSITION of its expected duration. +# The evidence is the match the run began under (a plateau soon reads to the +# matcher as a finished SHORTER programme) AND the current match (the last tick +# before a real end can be a prefix match of a LONGER one); with no current match +# the first stands. Unmatched (or matched with fewer than +# END_GATE_HAZARD_MIN_CYCLES traced cycles): STALL_UNMATCHED_MIN_S. Shown as +# `paused` with sub-state STALL_SUB_STATE, cleared by the first reading out of +# the band. While shown it reaches detection twice: the anti-crease finalize +# waits (item 511, below), and in the standby-band finalize, +# during a run whose two matches both still owe their terminal high-power block, +# the near-stop tier is held, so the plateau waits for the loose tier +# (STANDBY_BAND_LOOSE_MIN_RATIO x expected) instead of closing a halted wash as +# finished at 1.0x (STALL_HOLDS_STANDBY_BAND, the A/B switch for +# devtools/end_gate_eval.py). +STALL_DEVICE_TYPES = STANDBY_BAND_FINALIZE_DEVICE_TYPES +STALL_MIN_S = 600.0 +STALL_UNMATCHED_MIN_S = 1800.0 +STALL_OWES_MAX_POSITION = 0.75 +STALL_SUB_STATE = "Stalled" +STALL_HOLDS_STANDBY_BAND = True +# Register item 511: a halted programme does not advance. Once a stall is over +# (the wash resumed, or the plug dropped below stop), every duration-based end +# gate (Smart Termination, the hard finalize, the item-306 shortening, the hazard +# gate, the minimum-duration deferral, both standby-band tiers, the anti-crease +# gate and its #399 spin wait) reads the elapsed time LESS that run, from its +# first reading (`_gate_elapsed_s`); the live matcher reads the trace with it cut +# out (`_match_readings`) and the #399 scan offset moves past it. Without that the +# halt carried the clock past each gate's ratio and the resumed wash ended at its +# next quiet. A stall still shown counts as before, so the standby band closes +# a display left on after a real end exactly as the #452 hold above allows (the +# loose tier bounds it), and the anti-crease finalize waits while one is shown. The +# stored duration, the displayed elapsed and the 8 h cap keep the wall clock. +# Excluding the stall in progress as well (the near-stop tier then waited out +# every shown stall) kept 26 more synthetic 45 min halts whole, but held 10 real +# ends with the display left on for 45 min until it went off (up to 57 min late) +# and one left on for twice the programme up to 6 h. The A/B switch for the +# revert checks. +STALL_EXCLUDED_FROM_GATES = True +# Register item 514: progress reads programme time too. Every finished stall +# leaves the elapsed time and the trace the progress estimate reads +# (`progress_elapsed_s` / `progress_trace`, the one input of the manager and the +# Playground replay), as the user pause leaves the elapsed time +# (`manager.net_elapsed_seconds`), and the finished stalls ride in the cycle data +# (`stall_spans`) so the cycle-end label match reads the trace without them +# (`match_rules.final_match_input`). The stall shown now as well +# (STALL_CURRENT_EXCLUDED_FROM_PROGRESS): from the moment it shows, the remaining +# time stops counting down. Measured (`eta_eval --halt-at 0.5 --halt-min 45`, 246 +# washer targets): through the halt the ETA fell a median 40.9 min before, 0.5 now; +# its median error 1 / 10 / 30 min after the resume 41.8 / 36.4 / 21.6 -> 13.2 / +# 16.7 / 13.7 min (finished stalls only: 25.0 / 16.7 / 13.7); real corpus 0 of 450 +# rows changed. A/B switches for the revert checks. +STALL_EXCLUDED_FROM_PROGRESS = True +STALL_CURRENT_EXCLUDED_FROM_PROGRESS = True +# ...and a finished user pause is programme time stood still too (item 514): the +# gate clock, the #399 scan offset and the live matcher's trace leave it out like +# a finished stall (the union of the two, `_gate_spans`), and it rides in the +# cycle data with them (`halt_spans`). The user pause blocked every finisher +# while it lasted (the verified pause) but the gates read the raw clock after +# the resume, so a pause that cuts the plug's power ended the resumed wash at +# its next quiet. Progress already leaves it out (`manager.net_elapsed_seconds`). +# Measured (`end_gate_eval --halt-at F --halt-min 45 --user-pause`, 258 cycles): +# power cut at 50% / 30% / 90%, splits 48 / 50 / 40 -> 12 / 22 / 33, early ends +# 6 / 5 / 18 -> 5 / 7 / 8 (each new one the unpaused cycle's own end, or a former +# split); paused on the display level, 9 -> 6 and 14 -> 9 (50% / 90%). +USER_PAUSE_EXCLUDED_FROM_GATES = True +# Register item 514: a halt is abrupt. A programme winds down to its end (the last +# spin's ramp, then quiet), so the reading before a display left on is low; a halt +# stops the machine where it is. A flat run whose previous reading was at least +# STALL_ABRUPT_PEAK_FRACTION of the cycle's peak (and twice the band's ceiling) +# began straight out of activity (`_stall_run_abrupt`), and when the MATCHED +# programme it began under still owes work, the current match need not agree: the +# plateau soon reads to the matcher as a finished shorter programme, which vetoed +# the display and the near-stop hold above and closed the halted wash 10 min in. +# Unmatched runs are left to the current match (a short programme's real end looks +# the same). Measured (`end_gate_eval --halt-at/--halt-min`, 258 washer, +# washer-dryer and dryer cycles): at 0.10, 45 min halts at 50% shown 196 -> 198, +# closed inside the halt 75 -> 67, splits 79 -> 71 (30%: 76 -> 61; 90%: 130 -> 118), +# early ends unchanged; a display left on after a real end (100%, 45 min and 2x): +# no new stall flag, 2 closes later, both in runs the old gates had split. At 0.05: +# 50% splits 79 -> 54 and 9 more shown, but one more display-on flag and one more +# real end held (+36 min). 0 disables (the A/B switch). +STALL_ABRUPT_PEAK_FRACTION = 0.10 +# +# IDLE. Outside a cycle a two-level appliance (display on vs switched off) shows +# `idle` at its standby level and `off` below it. The level is the user's +# power-off threshold (#284) when one is set: idle at or above it, off below it, +# debounced by power_off_delay like the #284 reset itself. Otherwise the learned +# standby level (`learned_standby_level_w`, set by the manager): idle from +# IDLE_OFF_FRACTION of it, off below IDLE_EXIT_FRACTION of that (hysteresis), each +# held IDLE_DEBOUNCE_S, and only once the appliance has been seen below that off +# level outside a cycle since start-up. A level under IDLE_MIN_STANDBY_W, or at +# or above the start threshold, is not clearly separated from off or from a +# start: no idle at all, exactly as before. +IDLE_MIN_STANDBY_W = 2.0 +IDLE_OFF_FRACTION = 0.5 +IDLE_EXIT_FRACTION = 0.75 +IDLE_DEBOUNCE_S = 60.0 + def effective_anticrease_finalize_ratio(value: Any) -> float: """The ratio the anti-crease gate will actually use for a stored value. @@ -176,6 +342,54 @@ def effective_curve_preroll_seconds(value: Any) -> float: return min(window, CURVE_PREROLL_MAX_SECONDS) +@dataclass(frozen=True) +class MatchContext: + """One live match as the detector consumes it (audit DETECT-15). + + Producers build this by name; ``update_match`` still reads the positional + sequence it always did (``as_sequence``), so a field can no longer be put in + the wrong slot or left out of one producer. Every field after the first five + defaults to "no evidence", exactly what a shorter legacy tuple meant. + """ + + profile_name: str | None + confidence: float + expected_duration: float + phase_name: str | None = None + is_confident_mismatch: bool = False + is_ambiguous: bool = False + # Element 7 is retired: it carried the #364 prefix-fit flag, removed in 0.5.8 + # (see const.py). The slot stays (always False) so 8-14 keep their positions. + # Element 8: the #288 full-shape term (anti-crease finalize only). + is_prefix_ambiguous_full_shape: bool = False + tail_power: Any = None # 9 (#364 power guard) + terminal_high: Any = None # 10 (#399 anti-crease spin look-ahead) + terminal_quiet_s: Any = None # 11 (item 297 keep-tail cap) + longest_candidate_s: float = 0.0 # 12 (item 330 fallback bar) + trusted_min_s: Any = None # 13 (item 384 trusted-length floor) + pause_catalogue: Any = None # 14 (DETECT-16 hazard gate) + # 15 (#452 stall display): the same catalogue below `standby_near_stop_ceiling`, + # or a zero-argument callable returning it: read only when a flat run lasts + # long enough to be judged, so the profile's traces are not walked for it on + # every cycle (`test_perf_budgets`). + stall_catalogue: Any = None + + def __getitem__(self, index: Any) -> Any: + return self.as_sequence()[index] + + def __len__(self) -> int: + return 15 + + def as_sequence(self) -> tuple[Any, ...]: + return ( + self.profile_name, self.confidence, self.expected_duration, self.phase_name, + self.is_confident_mismatch, self.is_ambiguous, False, + self.is_prefix_ambiguous_full_shape, self.tail_power, self.terminal_high, + self.terminal_quiet_s, self.longest_candidate_s, self.trusted_min_s, + self.pause_catalogue, self.stall_catalogue, + ) + + @dataclass class CycleDetectorConfig: """Configuration for cycle detection.""" @@ -183,17 +397,18 @@ class CycleDetectorConfig: min_power: float off_delay: int device_type: str = DEVICE_TYPE_WASHING_MACHINE - smoothing_window: int = 5 interrupted_min_seconds: int = 150 completion_min_seconds: int = 600 start_duration_threshold: float = 5.0 start_energy_threshold: float = 0.005 end_energy_threshold: float = 0.05 # 0.05 Wh (50 mWh) threshold for "still active" - end_repeat_count: int = 1 min_off_gap: int = 60 start_threshold_w: float = 2.0 stop_threshold_w: float = 2.0 - min_duration_ratio: float = 0.8 # Default deferred finish ratio + # The FINISH-DEFERRAL ratio (`_should_defer_finish`), despite the name: not the + # matcher's Stage-1 `profile_match_min_duration_ratio`. Built from + # const.DEFAULT_DEFER_FINISH_RATIO (audit DETECT-02). + min_duration_ratio: float = 0.8 # Minimum live-match confidence for a match to be trusted by Smart Termination # and the anti-crease gate. Fed from the `profile_match_threshold` option, which # up to 0.5.5 was stored and never read - so raising it (the workaround the #288 @@ -207,7 +422,6 @@ class CycleDetectorConfig: power_off_threshold_w: float = 0.0 power_off_delay: float = 30.0 match_interval: int = 300 # Default profile match interval - profile_duration_tolerance: float = 0.25 # Default tolerance (±25%) anti_wrinkle_enabled: bool = False anti_wrinkle_max_power: float = 400.0 anti_wrinkle_max_duration: float = 60.0 @@ -300,6 +514,65 @@ def trim_zero_readings( return readings[start_idx : end_idx + 1] +def standby_near_stop_ceiling(stop_threshold_w: float) -> float: + """Top of the standby band's near-stop shape (#445 / register item 383). + + A plateau at or above the stop threshold and at most this high is an + appliance sitting at its standby draw, not one doing work. Shared by the + standby-band finalize, the stall display (#452) and its pause catalogue. + """ + stop = float(stop_threshold_w) + return max(STANDBY_BAND_NEAR_STOP_FACTOR * stop, stop + STANDBY_BAND_NEAR_STOP_W) + + +#: How many of the most recent stored cycles `learned_standby_level_w` reads. +STANDBY_LEVEL_RECENT_CYCLES = 30 + + +def learned_standby_level_w( + cycles: Any, stop_threshold_w: float, start_threshold_w: float +) -> float | None: + """Discussion #452: the standby/display level of a two-level appliance, or None. + + Two measurements the suggestion engine already makes, in order: the level + every recent cycle was still drawing when it ended (``detect_standby_above_stop``, + #445), else the resting draw of the clean cycles (``resting_level_w``, item + 455), timed below the near-stop ceiling instead of below stop so that a draw + just above stop (#452: 4-5 W on a 2.8 W stop) is seen too. None unless the + level is at least IDLE_MIN_STANDBY_W and below the start threshold, i.e. + clearly separated from both "off" and a start. Reads the last + STANDBY_LEVEL_RECENT_CYCLES cycles. Pure, executor-safe, never raises. + """ + try: + # Lazy: suggestion_engine imports Home Assistant helpers this module does not need. + from .suggestion_engine import ( # noqa: PLC0415 + _cycle_readings, + detect_standby_above_stop, + resting_level_w, + select_clean_cycles, + ) + + stop = float(stop_threshold_w) + start = float(start_threshold_w) + if not (math.isfinite(stop) and math.isfinite(start)) or stop <= 0: + return None + recent = [c for c in list(cycles or [])[-STANDBY_LEVEL_RECENT_CYCLES:] if isinstance(c, dict)] + adv = detect_standby_above_stop(recent, stop) + if adv is not None: + level: float | None = float(adv["idle_w"]) + else: + clean, _excluded = select_clean_cycles(recent, stop_threshold_w=stop) + points = [p for p in (_cycle_readings(c) for c in clean) if len(p) >= 5] + level = resting_level_w(points, standby_near_stop_ceiling(stop)) + if level is None or not math.isfinite(level): + return None + if level < IDLE_MIN_STANDBY_W or level >= start: + return None + return float(level) + except Exception: # noqa: BLE001 - a display statistic must never break setup + return None + + def terminal_high_for_guards( store: Any, config: "CycleDetectorConfig", @@ -355,6 +628,19 @@ def terminal_high_for_guards( return None +def _merge_spans(spans: list[tuple[float, float]]) -> list[tuple[float, float]]: + """``(at, length)`` spans sorted and merged where they overlap (item 514: a + stall shown during a user pause is banked by both).""" + merged: list[tuple[float, float]] = [] + for at, length in sorted(spans): + if merged and at <= merged[-1][0] + merged[-1][1]: + a0, n0 = merged[-1] + merged[-1] = (a0, max(a0 + n0, at + length) - a0) + else: + merged.append((at, length)) + return merged + + class CycleDetector: """Detects washing machine cycles based on power usage. @@ -429,6 +715,12 @@ class CycleDetector: # 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 + # When the power sensor's state became unavailable / unknown / non-numeric + # (register item 266): set by the manager, cleared by the next real + # reading. Sensor state like the field above, so not reset per cycle. + self._sensor_outage_since: datetime | None = None + # Traceback of the last restore_state_snapshot that failed, else None. + self.restore_error: str | None = None self._cycle_max_power: float = 0.0 # Accumulators (dt-aware) @@ -440,18 +732,26 @@ class CycleDetector: # dishwasher quiet-release gates so a single low sample after a telemetry # dropout can't satisfy them without the quiet having been observed. self._time_below_threshold_gapfree: float = 0.0 + # The part of the current below-threshold run that fell inside a sensor + # outage and so was NOT added to `_time_below_threshold` (item 266). Only + # the hazard gate reads it, to place the quiet run's start honestly. + self._time_below_unobserved: float = 0.0 self._last_process_time: datetime | None = None # New State Machine trackers self._state_enter_time: datetime | None = None self._matched_profile: str | None = None self._verified_pause: bool = False + self._user_paused: bool = False + # Item 514: when the current user pause began, and the pauses this cycle + # already resumed from as (seconds from the cycle start, length). + self._user_pause_since: datetime | None = None + self._user_pause_spans: list[tuple[float, float]] = [] self._last_power: float | None = None self._time_in_state: float = 0.0 # Smoothing buffer - self._ma_buffer: list[float] = [] # Adaptive Sampling Tracker self._recent_dts: list[float] = [] # Track last 20 dt values @@ -465,23 +765,24 @@ class CycleDetector: # Profile Matching Tracker self._last_match_time: datetime | None = None + # Whether the manager has committed a program this cycle (set_match_committed). + self._match_committed: bool = False self._expected_duration: float = 0.0 self._last_match_confidence: float = 0.0 # Element 12: the longest expected duration among the candidates the - # matcher still considers plausible. `_match_prefix_ambiguous` means one - # of them is materially longer than the winner, so "past the expected - # end" might be "mid-soak in that longer programme" - but only up to - # THIS duration. Past it there is no longer programme left to be mid-soak - # in, and the guard's own rationale is spent. + # matcher still considers plausible. An ambiguous match may mean one of + # them is materially longer than the winner, so "past the expected end" + # might be "mid-soak in that longer programme" - but only up to THIS + # duration. Past it there is no longer programme left to be mid-soak in, + # and the guard's own rationale is spent. self._longest_candidate_duration: float = 0.0 self._end_spike_seen: bool = False self._end_spike_duration: float = 0.0 # cycle duration (s) when _end_spike_seen was last set self._match_ambiguous: bool = False # last live match was ambiguous (gates predictive end) - self._match_prefix_ambiguous: bool = False # longer candidate with good shape exists (prefix guard) - # The narrower #288-only half of the flag above. #364 widened - # _match_prefix_ambiguous with prefix scoring, which is safe for the ENDING - # Smart-Termination gate but must NOT reach the anti-crease finalize (see - # _anticrease_gate_open) where blocking can re-hang a cycle (#296). + # The #288 prefix-landscape term (element 8): a much longer candidate with + # a good full-envelope shape exists. Read only by the anti-crease finalize + # (see _anticrease_gate_open). The ENDING gates' own prefix flag (#364 + # prefix fit, element 7) was removed in 0.5.8. self._match_prefix_ambiguous_full_shape: bool = False # 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. @@ -503,9 +804,29 @@ class CycleDetector: # this element is read at all, because nothing legitimate follows their # last activity - so for them the cap can sit EARLIER than expected_end. self._matched_terminal_quiet_s: float | None = None + # Element 13 (register item 384): the shortest length the user has vouched + # for in the matched profile (a corrected `manual_duration`, a recorder + # capture, a golden cycle). A dishwasher's kept tail never ends before + # TRUSTED_LENGTH_FLOOR_FRAC of it - the same floor the banked-tail repair + # applies - because element 11 can be wrong in the direction that deletes + # a drying phase. None: nothing vouched, no floor. + self._matched_trusted_min_s: float | None = None + # Element 14 (audit DETECT-16): the matched profile's pause catalogue, + # (traced cycles, ((start_fraction, seconds), ...)), for the hazard end + # gate. None: no catalogue, the fallback waits as before. + self._matched_pause_catalogue: tuple[int, tuple[tuple[float, float], ...]] | None = None + # Register item 498: the longest pause in the catalogue of every match + # revoked this cycle, which bounds a verified pause the revoke orphaned + # (`_release_orphaned_pause`). 0.0: no evidence, the floor applies. + self._orphaned_pause_evidence_s: float = 0.0 + self._terminal_quiet_memo: tuple[Any, bool] | 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 + # Register item 393a: set while STATE_ANTI_WRINKLE was entered by the #296 + # anti-crease finalise - the level at or under which a reading is that + # tail's own baseline rather than a burst. None for every other entry. + self._anticrease_tail_floor_w: float | None = None self._last_smart_term_block_reason: str | None = None # #346 diagnostic throttle # Anti-wrinkle tracking (dryers only) @@ -548,6 +869,592 @@ class CycleDetector: # that drops back into the standby band without the machine truly # turning off. self._preserve_delay_band_on_off: bool = False + # Register item 504: when the current STARTING probe came out of DELAY_WAIT, + # the moment DELAY_WAIT was entered (its timeout anchor). A false start that + # falls back into the standby band returns to DELAY_WAIT with this anchor + # instead of to OFF, so a standby that straddles start_threshold_w keeps + # waiting until a real start, a true off, or the band's own timeout. + # None for every other probe. In memory only: a probe restored after a + # restart falls back to OFF as before. + self._probe_wait_since: datetime | None = None + # Item 515: when the current probe came out of a terminal state, that state, + # its entry time and its sub-state, for the false start to return to. None + # for every other probe; in memory only (a restored probe falls to OFF). + self._probe_terminal_from: tuple[str, datetime | None, str | None] | None = None + # Register item 501: a standby that straddles start_threshold_w (#35: 2-22 W + # around a 4.24 W threshold) re-probes on almost every reading. + # `_standby_reprobe` is set when a false start falls back into the band + # (>= stop_threshold_w) or a terminal state reads the band (item 510), and + # cleared by any reading below it and by entering any state other than + # OFF/STARTING. A probe that begins while it is set, or out + # of DELAY_WAIT (a standby by definition), is HIDDEN: it runs exactly as any + # other, but `exposed_state` keeps showing the state it began in until it + # has filled STANDBY_REPROBE_SHOW_ENERGY_FRACTION of the energy gate. + self._standby_reprobe: bool = False + self._probe_hidden: bool = False + self._probe_hidden_state: str = STATE_OFF + self._probe_hidden_sub_state: str | None = None + # Discussion #452, display only (see the STALL_* / IDLE_* constants). + # Element 15: the matched profile's low stretches below the near-stop + # ceiling, as element 14's (traced, ((fraction, seconds), ...)). + self._matched_stall_catalogue: tuple[int, tuple[tuple[float, float], ...]] | None = None + # The current flat near-stop run: its first reading and its spread. + self._stall_run_start: datetime | None = None + self._stall_run_lo: float = 0.0 + self._stall_run_hi: float = 0.0 + self._stall_active: bool = False + # The match as it stood when the current run began (`_begin_stall_run`): + # a plateau soon looks like a finished SHORTER programme to the matcher, + # so the evidence is the pre-plateau match's. And (required seconds, + # owes work, owes its terminal high-power block), computed from it once. + self._stall_match: tuple[Any, ...] | None = None + self._stall_eval: tuple[float, bool, bool] | None = None + # ...and whether the CURRENT match agrees, re-read after every match. + self._stall_now: tuple[bool, bool] | None = None + # Item 514: the current run began straight out of activity. + self._stall_run_abrupt: bool = False + # Item 511: the runs this cycle ended while shown as stalled, as (elapsed + # seconds at the run's first reading, run seconds), in order. + self._stall_spans: list[tuple[float, float]] = [] + # The learned standby level (`learned_standby_level_w`, set by the + # manager) and the debounced idle/off class it drives. + self._standby_level_w: float | None = None + self._idle_shown: bool = False + self._idle_candidate_since: datetime | None = None + # A LEARNED level shows idle only once the appliance has been seen at its + # off level outside a cycle (held IDLE_DEBOUNCE_S) since start-up: an + # appliance whose switched-off draw reaches the level would otherwise read + # idle for ever, and an automation waiting for `off` would never fire. + self._idle_off_seen: bool = False + self._idle_off_since: datetime | None = None + + @property + def exposed_state(self) -> str: + """The state entities show. Detection never reads this. + + ``state``, except: during a hidden probe the state it began in (item 501: + a standby that keeps crossing the start threshold otherwise wrote ~500 + off/starting rows a day and fired every automation keyed on + ``starting``); ``paused`` while the open cycle is stalled (#452); and + ``idle`` instead of ``off`` while a two-level appliance sits at its + standby level (#452). + """ + state = self._state + if self._probe_hidden and state == STATE_STARTING: + state = self._probe_hidden_state + if state == STATE_OFF and self._idle_shown and self._idle_bounds() is not None: + return STATE_IDLE + if self._stall_active and state in (STATE_RUNNING, STATE_ENDING): + return STATE_PAUSED + return state + + @property + def exposed_sub_state(self) -> str | None: + """``sub_state`` to show, matching :attr:`exposed_state`.""" + exposed = self.exposed_state + if exposed == STATE_IDLE: + return STATE_IDLE.capitalize() + if self._stall_active and exposed == STATE_PAUSED: + return STALL_SUB_STATE + if self._probe_hidden and self._state == STATE_STARTING: + return self._probe_hidden_sub_state + return self._sub_state + + @property + def stalled(self) -> bool: + """Whether the open cycle is stalled (#452). Display only.""" + return self._stall_active + + def stall_info(self) -> dict[str, Any] | None: + """Small, event-safe description of the current stall, or None.""" + if not self._stall_active or self._stall_run_start is None: + return None + start = self._current_cycle_start + last = self._last_process_time or self._stall_run_start + return { + "stalled_since": self._stall_run_start.isoformat(), + "stalled_for_s": round(max(0.0, (last - self._stall_run_start).total_seconds())), + "elapsed_at_stall_s": ( + round((self._stall_run_start - start).total_seconds()) if start else None + ), + "plateau_w": round((self._stall_run_lo + self._stall_run_hi) / 2.0, 2), + # The match the halt began under when the halt has since cost it. + "matched_profile": self._matched_profile or ( + self._stall_match[0] if self._stall_match else None + ), + } + + def set_standby_level(self, level_w: float | None) -> None: + """The learned standby level for the idle display (#452); None: none.""" + try: + level = float(level_w) if level_w is not None else None + except (TypeError, ValueError, OverflowError): + level = None + self._standby_level_w = level if level is not None and math.isfinite(level) else None + + def _idle_bounds(self) -> tuple[float, float, float, bool] | None: + """``(enter_w, exit_w, debounce_s, off level confirmed)`` of the idle + display, or None (#452).""" + cfg = self._config + start = float(cfg.start_threshold_w) + pot = cfg.power_off_threshold_w + if isinstance(pot, (int, float)) and 0.0 < float(pot) < float(cfg.stop_threshold_w): + # The user's own off level (#284): the same boundary and debounce the + # power-based Off uses, so a reset to off never shows idle first. + return (float(pot), float(pot), max(0.0, float(cfg.power_off_delay)), True) + level = self._standby_level_w + if level is None or level < IDLE_MIN_STANDBY_W or level >= start: + return None + enter = IDLE_OFF_FRACTION * level + return (enter, IDLE_EXIT_FRACTION * enter, IDLE_DEBOUNCE_S, False) + + def _update_idle(self, timestamp: datetime, power: float) -> None: + """Debounced idle/off class of the latest real reading (#452). O(1). + + A reading at or above the enter level votes idle, one below the exit + level votes off, one in between keeps the current class. The class + changes only once the other vote has held for the debounce, timed from + its first reading, so an excursion shorter than that never shows. + """ + bounds = self._idle_bounds() + if bounds is None: + self._idle_shown = False + self._idle_candidate_since = None + return + enter, exit_w, debounce, confirmed = bounds + if power >= enter: + vote = True + elif power < exit_w: + vote = False + else: + vote = self._idle_shown + if power < exit_w and self._state in ( + STATE_OFF, STATE_FINISHED, STATE_INTERRUPTED, STATE_FORCE_STOPPED + ): + if self._idle_off_since is None: + self._idle_off_since = timestamp + if (timestamp - self._idle_off_since).total_seconds() >= debounce: + self._idle_off_seen = True + else: + self._idle_off_since = None + if vote and not (confirmed or self._idle_off_seen): + vote = False # never seen switched off: not yet known to be two-level + if vote == self._idle_shown: + self._idle_candidate_since = None + return + if self._idle_candidate_since is None: + self._idle_candidate_since = timestamp + if (timestamp - self._idle_candidate_since).total_seconds() >= debounce: + self._idle_shown = vote + self._idle_candidate_since = None + + @classmethod + def _sanitize_stall_catalogue(cls, raw: Any) -> Any: + """Element 15, keeping only the stretches that can set a stall's wait. + + The near-stop catalogue holds every gap between two drum tumbles; a + stretch under STALL_MIN_S / END_GATE_HAZARD_MARGIN can never raise the + wait above STALL_MIN_S, so it is dropped. A callable (the producers' lazy + form) is kept as is and resolved by :meth:`_resolve_stall_catalogue`. + """ + if callable(raw): + return raw + cat = cls._sanitize_pause_catalogue(raw) + if cat is None: + return None + floor = STALL_MIN_S / END_GATE_HAZARD_MARGIN + return (cat[0], tuple(p for p in cat[1] if p[1] >= floor)) + + @staticmethod + def _sanitize_stall_spans(raw: Any) -> list[tuple[float, float]]: + """Item 511's finished stalls from a snapshot: finite, non-negative pairs + in order; anything else is dropped (an old snapshot has none).""" + return match_rules.sanitize_stall_spans(raw) + + @classmethod + def _resolve_stall_catalogue( + cls, raw: Any + ) -> tuple[int, tuple[tuple[float, float], ...]] | None: + """Element 15 as data: a lazy producer is called here, once per run.""" + if callable(raw): + try: + raw = raw() + except Exception: # noqa: BLE001 - missing evidence, not an error + return None + return cls._sanitize_stall_catalogue(raw) if raw is not None else None + + def _seed_stall_run(self) -> None: + """Re-derive the flat near-stop run in progress from the trace (#452).""" + self._clear_stall() + if self._state not in (STATE_RUNNING, STATE_PAUSED, STATE_ENDING): + return + band = self._stall_band() + if band is None: + return + lo = hi = None + start = None + for ts, p in reversed(self._power_readings): + p = float(p) + if not band[0] <= p <= band[1]: + break + nlo = p if lo is None else min(lo, p) + nhi = p if hi is None else max(hi, p) + if nhi - nlo > band[2]: + break + lo, hi, start = nlo, nhi, ts + if start is not None and lo is not None and hi is not None: + # The restored match stands in for the one the run began under. + self._begin_stall_run(start, lo) + self._stall_run_hi = hi + + def _gate_spans(self) -> list[tuple[float, float]]: + """The spans the gates' programme time leaves out, in order and merged: + every finished stall (item 511) and every finished user pause (item 514).""" + stalls = self._stall_spans if STALL_EXCLUDED_FROM_GATES else [] + pauses = self._user_pause_spans if USER_PAUSE_EXCLUDED_FROM_GATES else [] + if not pauses: + return stalls + if not stalls: + return pauses + return _merge_spans([*stalls, *pauses]) + + def _gate_elapsed_s(self, timestamp: datetime) -> float: + """Elapsed seconds the duration-based end gates read (item 511): the + cycle's wall clock less every finished stall and user pause (item 514). + 0.0 with no cycle open.""" + start = self._current_cycle_start + if start is None: + return 0.0 + elapsed = (timestamp - start).total_seconds() + spans = self._gate_spans() + if spans: + elapsed = max(0.0, elapsed - sum(length for _at, length in spans)) + return elapsed + + def _wall_offset_s(self, offset_s: float) -> float: + """A programme-time offset from the cycle start as wall-clock seconds + (item 511): each left-out span that began before it moves it later.""" + shift = 0.0 + for at, length in self._gate_spans(): + if offset_s < at - shift: + break + shift += length + return offset_s + shift + + def _match_readings(self) -> list[tuple[datetime, float]]: + """The trace the live matcher reads (item 511): each finished stall cut + out and every later reading moved back by it, so the duration, the + Stage-1 ratio and the in-progress Stage-4 stretch are programme time and + the shape has no plateau the programme never made. A stall still shown + stays in, as before: its evidence includes the current match's reading + of it. The stored trace keeps every reading.""" + return self._cut_readings(self._gate_spans()) + + def _cut_readings( + self, spans: list[tuple[float, float]] + ) -> list[tuple[datetime, float]]: + """The trace with ``spans`` cut out, every later reading moved back by them + (`match_rules.stall_cut_plan`). The trace itself when there is none.""" + start = self._current_cycle_start + if not spans or start is None: + return self._power_readings + readings = self._power_readings + plan = match_rules.stall_cut_plan( + [(ts - start).total_seconds() for ts, _p in readings], spans + ) + return [ + (readings[i][0] - timedelta(seconds=shift), readings[i][1]) if shift + else readings[i] + for i, shift in plan + ] + + def _progress_spans( + self, timestamp: datetime | None + ) -> tuple[list[tuple[float, float]], tuple[float, float] | None]: + """``(finished stalls, the stall shown now as (at, seconds so far) or None)`` + that progress leaves out (item 514).""" + if not STALL_EXCLUDED_FROM_PROGRESS: + return [], None + start, run = self._current_cycle_start, self._stall_run_start + current = None + if ( + STALL_CURRENT_EXCLUDED_FROM_PROGRESS and self._stall_active + and start is not None and run is not None and timestamp is not None + ): + current = ( + max(0.0, (run - start).total_seconds()), + max(0.0, (dt_util.as_utc(timestamp) - run).total_seconds()), + ) + return list(self._stall_spans), current + + def progress_elapsed_s(self, elapsed_s: float, timestamp: datetime | None) -> float: + """``elapsed_s`` less the stalled time progress leaves out (item 514). + + ``elapsed_s`` is the caller's own clock: the manager's net of the user + pause, the Playground's replay offset. ``timestamp`` is now (the stall + shown at this moment counts up to it).""" + finished, current = self._progress_spans(timestamp) + stalled = sum(length for _at, length in finished) + if current is not None: + stalled += current[1] + return max(0.0, float(elapsed_s) - stalled) + + def progress_trace(self, timestamp: datetime | None) -> list[tuple[datetime, float]]: + """The trace progress reads (item 514): the live matcher's, and with a + stall shown now everything from its first reading on is left out too. + A copy, like :meth:`get_power_trace`.""" + finished, current = self._progress_spans(timestamp) + if current is not None: + finished = [*finished, (current[0], math.inf)] + return list(self._cut_readings(finished)) + + def _bank_stall(self, timestamp: datetime) -> None: + """Keep the run that ends at ``timestamp`` if it is shown as stalled.""" + start, run_start = self._current_cycle_start, self._stall_run_start + if self._stall_active and start is not None and run_start is not None: + self._stall_spans.append(( + max(0.0, (run_start - start).total_seconds()), + max(0.0, (timestamp - run_start).total_seconds()), + )) + + def _clear_stall(self) -> None: + self._stall_run_start = None + self._stall_run_lo = self._stall_run_hi = 0.0 + self._stall_active = False + self._stall_match = None + self._stall_eval = None + self._stall_now = None + self._stall_run_abrupt = False + + def _begin_stall_run(self, timestamp: datetime, power: float) -> None: + """A new flat near-stop run starts at this reading: snapshot the match.""" + self._stall_run_start = timestamp + self._stall_run_lo = self._stall_run_hi = power + self._stall_eval = None + self._stall_now = None + # Item 514: straight out of activity, judged on the reading before this + # one (a restart re-derives it from the restored trace). + self._stall_run_abrupt = False + band = self._stall_band() if STALL_ABRUPT_PEAK_FRACTION > 0 else None + if band is not None: + prev = next( + (float(p) for ts, p in reversed(self._power_readings) if ts < timestamp), None + ) + self._stall_run_abrupt = prev is not None and prev >= max( + STALL_ABRUPT_PEAK_FRACTION * float(self._cycle_max_power), 2.0 * band[1] + ) + self._stall_match = ( + self._matched_profile if self._expected_duration > 0 else None, + float(self._expected_duration), + self._matched_terminal_high, + self._matched_stall_catalogue, + ) + + def _stall_evidence(self) -> tuple[float, bool, bool] | None: + """``(required s, owes work, owes its spin)`` of the current run, or None.""" + if self._stall_run_start is None: + return None + if self._stall_eval is None: + self._stall_eval = self._stall_evaluate() + return self._stall_eval + + def _stall_holds_standby_band(self) -> bool: + """Whether the current flat run says the programme is not done (#452). + + Only the strongest evidence holds a finalize: the match the run began + under ends on a terminal high-power block (#399) this cycle has not + produced yet. A matcher that re-reads the halt as a finished SHORTER + programme cannot release it, and the plain position test the display also + accepts holds nothing (a programme matched to a longer one ends "early" + every time). Independent of the display's wait: the finalize's own 10 min + window and expected-duration gate already say "long enough". + """ + if not STALL_HOLDS_STANDBY_BAND or self._stall_band() is None: + return False + evidence = self._stall_evidence() + if self._stall_abrupt_owes(): + return True # item 514: an abrupt halt under a match that owes work + return bool(evidence and evidence[2] and self._stall_now_evidence()[1]) + + def _stall_abrupt_owes(self) -> bool: + """Item 514: the run began straight out of activity under a MATCHED + programme that still owes work, so the current match's verdict (the + plateau read as a finished shorter programme) is not needed.""" + if not (self._stall_run_abrupt and self._stall_match and self._stall_match[0]): + return False + evidence = self._stall_evidence() + return bool(evidence and evidence[1]) + + def _stall_now_evidence(self) -> tuple[bool, bool]: + """``(owes work, owes its spin)`` by the CURRENT match, for the current run. + + Both matches must agree before anything shows or holds: the one the run + began under can be a prefix match of a longer programme (the last live + tick before a real end differs from the complete match on 17.5% of + cycles, audit MATCH-DECIDE-02), and the current one can be the halt read + as a finished shorter programme. Measured with ``end_gate_eval.py + --halt-at``: requiring both cut real ends held under a 45 min display-on + tail 15 -> 6 of 258 and kept 28 of the 48 halts the pre-plateau match + alone kept open. With no current match the pre-plateau verdict stands. + """ + if self._stall_now is not None: + return self._stall_now + start, run_start = self._current_cycle_start, self._stall_run_start + expected = float(self._expected_duration) + block = self._matched_terminal_high + if not (self._matched_profile and expected > 0 and start and run_start): + evidence = self._stall_evidence() + now = (bool(evidence and evidence[1]), bool(evidence and evidence[2])) + elif block is not None and block[0] >= ANTI_CREASE_TERMINAL_HIGH_MIN_FRAC: + needed = float(block[1]) * ANTI_CREASE_TERMINAL_MATCH_FRAC + offset_s = float(block[2]) if len(block) >= 3 else float(block[0]) * expected + ceiling_w = float(block[3]) if len(block) >= 4 else None + spin = needed > 0 and self._high_power_seconds_since( + offset_s, ceiling_w=ceiling_w + ) < needed + now = (spin, spin) + else: + # Programme time (item 511): an earlier stall is not progress. + position = self._gate_elapsed_s(run_start) / expected + now = (position < STALL_OWES_MAX_POSITION, False) + self._stall_now = now + return now + + def _set_stalled(self, stalled: bool, timestamp: datetime) -> None: + """Flip the stall flag (one place, so the eval harness can observe it).""" + if stalled == self._stall_active: + return + self._stall_active = stalled + if stalled: + self._logger.info( + "Cycle stalled: flat %.1f-%.1f W (stop %.2f W) for %.0fs, matched %s; " + "shown as paused (display only, the cycle stays open)", + self._stall_run_lo, + self._stall_run_hi, + self._config.stop_threshold_w, + (timestamp - (self._stall_run_start or timestamp)).total_seconds(), + self._matched_profile, + ) + else: + self._logger.debug("Cycle no longer stalled at %s", timestamp) + + def _stall_band(self) -> tuple[float, float, float] | None: + """``(low_w, high_w, flatness_w)`` a stall plateau must sit in, or None.""" + if self._config.device_type not in STALL_DEVICE_TYPES: + return None + stop = float(self._config.stop_threshold_w) + peak = float(self._cycle_max_power) + if stop <= 0 or peak <= 0: + return None + high = min(standby_near_stop_ceiling(stop), peak * STANDBY_BAND_MAX_FRACTION) + if high < stop: + return None + flat = max(STANDBY_BAND_FLATNESS_FLOOR_W, peak * STANDBY_BAND_FLATNESS_FRACTION) + return (stop, high, flat) + + def _stall_evaluate(self) -> tuple[float, bool, bool]: + """``(required seconds, owes work, owes its spin)`` for the current run, + from the match it began under.""" + start = self._current_cycle_start + run_start = self._stall_run_start + name, expected, block, cat = self._stall_match or (None, 0.0, None, None) + if not (name and expected > 0 and start and run_start): + return (STALL_UNMATCHED_MIN_S, True, False) + cat = self._resolve_stall_catalogue(cat) + position = max(0.0, self._gate_elapsed_s(run_start) / expected) # item 511 + # Owes work: the terminal high-power block (#399) not yet produced, or, + # for a programme without one, a run that began well before its end. + owes_spin = False + if block is not None and block[0] >= ANTI_CREASE_TERMINAL_HIGH_MIN_FRAC: + needed = float(block[1]) * ANTI_CREASE_TERMINAL_MATCH_FRAC + offset_s = float(block[2]) if len(block) >= 3 else float(block[0]) * expected + ceiling_w = float(block[3]) if len(block) >= 4 else None + owes_spin = needed > 0 and self._high_power_seconds_since( + offset_s, ceiling_w=ceiling_w + ) < needed + owes = owes_spin + else: + owes = position < STALL_OWES_MAX_POSITION + # Ambiguity is not a reason to refuse here: the display only waits, it + # never shortens anything (unlike the hazard gate this mirrors). + if cat is None or cat[0] < END_GATE_HAZARD_MIN_CYCLES: + return (STALL_UNMATCHED_MIN_S, owes, owes_spin) + later = [d for f, d in cat[1] if f >= position - END_GATE_HAZARD_POSITION_SLACK] + need = max(STALL_MIN_S, END_GATE_HAZARD_MARGIN * max(later, default=0.0)) + return (need, owes, owes_spin) + + def _update_stall(self, timestamp: datetime, power: float) -> None: + """Track the flat near-stop run of an open cycle and flag a stall (#452). + + O(1) per reading; the evidence is computed once per run and match. + """ + band = self._stall_band() + if band is None or not (band[0] <= power <= band[1]): + if self._stall_run_start is not None or self._stall_active: + self._bank_stall(timestamp) # item 511 + self._set_stalled(False, timestamp) + self._clear_stall() + return + if self._stall_run_start is None: + self._begin_stall_run(timestamp, power) + else: + lo = min(self._stall_run_lo, power) + hi = max(self._stall_run_hi, power) + if hi - lo > band[2] and not self._stall_active: + # Not flat: a new run starts here. A stall already shown stays + # shown while readings stay in the band. + self._begin_stall_run(timestamp, power) + else: + self._stall_run_lo, self._stall_run_hi = lo, hi + run_s = (timestamp - self._stall_run_start).total_seconds() + if self._stall_eval is None and run_s < min(STALL_MIN_S, STALL_UNMATCHED_MIN_S): + return + evidence = self._stall_evidence() + if evidence is not None: + self._set_stalled( + run_s >= evidence[0] and evidence[1] + and (self._stall_now_evidence()[0] or self._stall_abrupt_owes()), + timestamp, + ) + + def _hide_probe(self, hidden: bool) -> None: + """Item 501: whether the probe about to begin is hidden. Called BEFORE the + transition to STARTING, whose callback already refreshes the entities.""" + self._probe_hidden = hidden + self._probe_hidden_state = self._state + self._probe_hidden_sub_state = self._sub_state + + def _return_probe_to_terminal(self, timestamp: datetime, power: float) -> None: + """Item 515: a false start goes back to the terminal state it began in. + + With that state's entry time and sub-state, and without the probe's + readings, as ``reset()`` leaves a terminal state: no cycle is open, so a + later force-stop or "Done now" there records nothing. A reading still in + the band marks the next probe as a standby re-probe (items 501, 510). + """ + back = self._probe_terminal_from + if back is None: + return + state, entered, sub_state = back + self._transition_to(state, timestamp) + if entered is not None: + self._state_enter_time = entered + self._time_in_state = max(0.0, (timestamp - entered).total_seconds()) + self._sub_state = sub_state + self._power_readings = [] + self._current_cycle_start = None + self._last_active_time = None + self._cycle_max_power = 0.0 + self._energy_since_idle_wh = 0.0 + self._time_above_threshold = 0.0 + self._standby_reprobe = power >= self._config.stop_threshold_w + + def _show_probe_with_evidence(self) -> None: + """Item 501: a hidden probe is shown once it has filled part of the energy gate.""" + if self._probe_hidden and self._energy_since_idle_wh >= ( + STANDBY_REPROBE_SHOW_ENERGY_FRACTION * self._config.start_energy_threshold + ): + self._probe_hidden = False @property def _gate_cadence(self) -> float: @@ -585,7 +1492,7 @@ class CycleDetector: """ if len(self._recent_dts) < 5: return self._p95_dt - median_dt = float(np.median(self._recent_dts)) + median_dt = median_fast(self._recent_dts) return min(self._p95_dt, GATE_CADENCE_MEDIAN_FACTOR * median_dt) @property @@ -613,7 +1520,9 @@ class CycleDetector: # Calculate p95 if enough samples if len(self._recent_dts) >= 5: - self._p95_dt = float(np.percentile(self._recent_dts, 95)) + # Pure Python, bit-identical to np.percentile (audit PERF-07): on 20 + # values NumPy's call overhead was the detector's top per-sample cost. + self._p95_dt = percentile_linear(self._recent_dts, 95) else: self._p95_dt = max(dt, 1.0) @@ -643,6 +1552,28 @@ class CycleDetector: and self._matched_profile and self._time_below_threshold >= DISHWASHER_MATCH_FREEZE_QUIET_SECONDS ): + # The freeze also stops the manager's match tick, the only place the #375 + # sustained-quiet release ran: a verified pause engaged before the freeze + # was never released, and every ENDING finalize stayed blocked until the + # force stop (~8 h on a plug that keeps reporting 0 W; found by the F7 + # parity harness on three TRON4R dishwasher cycles). Run the same rule here. + if self._verified_pause and not self._user_paused and self._power_readings: + pause = match_rules.decide_pause_release( + verified_pause=True, + current_matched=self._matched_profile, + current_power=float(self._power_readings[-1][1]), + stop_threshold_w=float(self._config.stop_threshold_w), + user_paused=False, + expected_duration=float(self._expected_duration or 0.0), + current_duration=( + self._power_readings[-1][0] - self._power_readings[0][0] + ).total_seconds(), + time_below=self._time_below_threshold_gapfree, + program=self._matched_profile, + ) + for level, msg, args in pause.log: + self._logger.log(level, msg, *args) + self._verified_pause = pause.verified_pause return # Terminal-tail match freeze (anti-crease, #296): once a washer/dryer with @@ -660,14 +1591,17 @@ class CycleDetector: # Rate limiting if not force and self._last_match_time: elapsed = (timestamp - self._last_match_time).total_seconds() - if elapsed < self._config.match_interval: + interval = float(self._config.match_interval) + if not getattr(self, "_match_committed", True): + interval /= 2.0 + if elapsed < interval: return self._last_match_time = timestamp # Call the matcher try: - result = self._profile_matcher(self._power_readings) + result = self._profile_matcher(self._match_readings()) # If synchronous result returned, process it. # If None returned (async offload), the matcher is responsible for # calling update_match later. @@ -702,7 +1636,7 @@ class CycleDetector: """ try: value = float(raw) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): self._logger.debug( "%s: invalid raw_expected_duration %r, defaulting to 0.0", source, raw, @@ -751,6 +1685,84 @@ class CycleDetector: return None return min(value, TERMINAL_QUIET_CAP_S) + @staticmethod + def _sanitize_positive(raw: Any) -> float | None: + """A finite positive float from a snapshot, else None.""" + if raw is None or isinstance(raw, bool): + return None + try: + value = float(raw) + except (TypeError, ValueError, OverflowError): + return None + return value if math.isfinite(value) and value > 0.0 else None + + @staticmethod + def _sanitize_trusted_min(raw: Any) -> float | None: + """Coerce element 13 to a positive duration or None ("nothing vouched").""" + if raw is None or isinstance(raw, bool): + return None + try: + value = float(raw) + except (TypeError, ValueError, OverflowError): + return None + if not math.isfinite(value) or not 0.0 < value <= CycleDetector._SANITIZE_MAX_EXPECTED_DURATION: + return None + return value + + @staticmethod + def _sanitize_pause_catalogue( + raw: Any, + ) -> tuple[int, tuple[tuple[float, float], ...]] | None: + """Coerce element 14 to ``(traced, ((fraction, seconds), ...))`` or None.""" + try: + traced, pauses = raw + out = tuple( + (float(f), float(d)) for f, d in pauses + if math.isfinite(float(f)) and math.isfinite(float(d)) and float(d) >= 0 + ) + return (int(traced), out) if int(traced) > 0 else None + except (TypeError, ValueError, OverflowError): + return None + + def _hazard_wait(self, timestamp: datetime, base: float) -> float: + """The ENDING fallback wait the matched profile's own pauses justify. + + Audit DETECT-16: "no soak left after 1.05x expected" generalised to "no + soak this programme has ever shown from this position": END_GATE_HAZARD_ + MARGIN x the longest resumed pause its traced evidence began at or after + this quiet's start (less a slack), clamped to [off_delay, base]. Shorten- + only, unambiguous matches only, and only with END_GATE_HAZARD_MIN_CYCLES + traced cycles; otherwise ``base``. Measured (prototype, 292 cycles): washer + median lag 16.17 -> 12.50 min, early ends 0 -> 0, splits unchanged. + """ + return self._hazard_wait_for( + timestamp, base, self._matched_pause_catalogue, + self._expected_duration, self._match_ambiguous, + ) + + def _hazard_wait_for( + self, timestamp: datetime, base: float, cat: Any, expected: float, ambiguous: bool + ) -> float: + """:meth:`_hazard_wait` for a match with these values (register item 469b).""" + if ( + cat is None + or cat[0] < END_GATE_HAZARD_MIN_CYCLES + or not self._matched_profile + or expected <= 0 + or self._current_cycle_start is None + or ambiguous + ): + return base + elapsed = self._gate_elapsed_s(timestamp) # programme time (item 511) + # Where the quiet run began. An outage inside it is still part of the run + # (item 266), or the outage would read as progress and drop the very pause + # being waited out from `later`. + quiet_run = self._time_below_threshold + self._time_below_unobserved + position = max(0.0, (elapsed - quiet_run) / expected) + later = [d for f, d in cat[1] if f >= position - END_GATE_HAZARD_POSITION_SLACK] + need = END_GATE_HAZARD_MARGIN * max(later) if later else 0.0 + return max(float(self._config.off_delay), min(float(base), need)) + @staticmethod def _sanitize_longest_candidate(raw: Any) -> float: """Coerce the longest plausible candidate duration to a usable bound. @@ -981,10 +1993,37 @@ class CycleDetector: and self._in_anticrease_freeze(self._power_readings[-1][0]) ): return + if isinstance(result, MatchContext): + result = result.as_sequence() + # Register item 469(b): an ambiguous match in ENDING may not make the end + # later (see `ambiguous_ending_match_defers`). Like the freezes above, the + # detector keeps the match it has; a revoke still applies. + if ( + match_rules.HOLD_AMBIGUOUS_IN_ENDING + and isinstance(result, (list, tuple)) + and len(result) >= 6 + and result[0] is not None + and bool(result[5]) + and not bool(result[4]) + and self.ambiguous_ending_match_defers( + result[2], result[1], result[11] if len(result) >= 12 else 0.0, + result[13] if len(result) >= 14 else None, + ) + ): + self._logger.debug( + "ENDING: ambiguous match %s not applied, %s kept (item 469)", + result[0], self._matched_profile, + ) + return # Unpack 5 elements (or 4 for backward compatibility if needed, but wrapper is updated) # wrapper returns (name, confidence, duration, phase, is_mismatch) # Or MatchResult object if refactored, but currently wrapper returns tuple. + # Register item 498: what a revoke below discards, kept as the evidence + # that bounds a verified pause it leaves behind. + revoked_from = self._matched_profile + revoked_catalogue = self._matched_pause_catalogue + is_match_mismatch = False match_name: str | None = None phase_name: str | None = None @@ -1013,7 +2052,7 @@ class CycleDetector: if not math.isfinite(confidence): confidence = 0.0 self._logger.debug("update_match: invalid raw_confidence %r, defaulting to 0.0", raw_confidence) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): confidence = 0.0 self._logger.debug("update_match: invalid raw_confidence %r, defaulting to 0.0", raw_confidence) expected_duration = self._sanitize_expected_duration( @@ -1036,7 +2075,7 @@ class CycleDetector: if not math.isfinite(confidence): confidence = 0.0 self._logger.debug("update_match: invalid raw_confidence %r, defaulting to 0.0", raw_confidence) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): confidence = 0.0 self._logger.debug("update_match: invalid raw_confidence %r, defaulting to 0.0", raw_confidence) expected_duration = self._sanitize_expected_duration( @@ -1050,12 +2089,13 @@ class CycleDetector: # Store confidence + ambiguity for Smart Termination checks self._last_match_confidence = confidence or 0.0 self._match_ambiguous = ambiguous - self._match_prefix_ambiguous = bool(result_seq[6]) if len(result_seq) >= 7 else False - # Element 8 (#364): the narrower legacy verdict. A shorter tuple - # (Playground, older callers, most tests) falls back to the widened - # value, which reproduces pre-#364 behaviour exactly. + # Element 8: the #288 full-shape term. Element 7 (the removed #364 + # prefix-fit flag) is read only as its fallback for a 7-element legacy + # tuple, which carried the #288 verdict there before #364 split it out. self._match_prefix_ambiguous_full_shape = ( - bool(result_seq[7]) if len(result_seq) >= 8 else self._match_prefix_ambiguous + bool(result_seq[7]) if len(result_seq) >= 8 + else bool(result_seq[6]) if len(result_seq) >= 7 + else False ) # Element 9 (#364): the matched profile's own tail power level. Absent # or non-finite leaves the power-plausibility guard inert - so a shorter @@ -1089,20 +2129,42 @@ class CycleDetector: self._longest_candidate_duration = self._sanitize_longest_candidate( result_seq[11] if len(result_seq) >= 12 else 0.0 ) + # Element 13: cleared by a shorter tuple, like elements 9-12. + self._matched_trusted_min_s = self._sanitize_trusted_min( + result_seq[12] if len(result_seq) >= 13 else None + ) + # Element 14: cleared by a shorter tuple, like elements 9-13. + self._matched_pause_catalogue = self._sanitize_pause_catalogue( + result_seq[13] if len(result_seq) >= 14 else None + ) + # Element 15 (#452): likewise. A run already in progress keeps the + # match it began under (`_stall_match`). + self._matched_stall_catalogue = self._sanitize_stall_catalogue( + result_seq[14] if len(result_seq) >= 15 else None + ) + self._stall_now = None # the current match is re-read for a run in progress else: # Assume MatchResult object or similar (future proofing) # But for now wrapper returns tuple return if is_match_mismatch and self._matched_profile: - # Confident non-match - revert to detecting if previously matched + # Confident non-match - revert to detecting if previously matched. + # The expected duration goes with it, as in reset(): the duration- + # anchored hard finalize, the dishwasher end-spike arm gate, the + # keep-tail cap and the manager's zombie killer read it without + # checking `_matched_profile`, so a revoked programme's length kept + # steering an unmatched cycle. self._matched_profile = None + self._expected_duration = 0.0 self._match_ambiguous = False - self._match_prefix_ambiguous = False self._match_prefix_ambiguous_full_shape = False self._matched_tail_power = None self._matched_terminal_high = None self._matched_terminal_quiet_s = None + self._matched_trusted_min_s = None + self._matched_pause_catalogue = None + self._matched_stall_catalogue = None elif match_name: # If sanitization rejected the expected_duration, treat the match @@ -1126,13 +2188,136 @@ class CycleDetector: # Wrapper provides it self._expected_duration = expected_duration + if revoked_from and not self._matched_profile and revoked_catalogue is not None: + # Either path above that drops the match (a revoke, or a named match + # with an unusable duration). Max over the cycle's revokes. + self._orphaned_pause_evidence_s = max( + self._orphaned_pause_evidence_s, + max((d for _f, d in revoked_catalogue[1]), default=0.0), + ) + + def _release_orphaned_pause(self) -> None: + """Release a verified pause a revoked match left behind (register item 498). + + After a revoke nothing could clear an automatic verified pause but high + power: the 95%-of-span release needs the match, the #375 release its + expected duration. So it held every ENDING finisher until the 8 h force stop + (a plug reporting 0 W) or the watchdog's 4.5 h silence limit (a silent plug). + The bounded wait is :func:`match_rules.orphaned_pause_wait_s`; a user pause + is never released. Called on every ENDING reading (watchdog keepalives and + the Playground's replay included), after this reading's match tick. + """ + if not self._verified_pause or self._user_paused or self._matched_profile: + return + longest = self._orphaned_pause_evidence_s + pause = match_rules.decide_orphaned_pause_release( + verified_pause=True, + user_paused=False, + current_matched=None, + time_below=self._time_below_threshold_gapfree, + wait_s=match_rules.orphaned_pause_wait_s( + off_delay=self._config.off_delay, + min_off_gap=self._config.min_off_gap, + longest_pause_s=longest, + ), + longest_pause_s=longest, + ) + for level, msg, args in pause.log: + self._logger.log(level, msg, *args) + self._verified_pause = pause.verified_pause + def set_verified_pause(self, verified: bool) -> None: """Set or clear the verified pause flag.""" self._verified_pause = verified - def reset(self, target_state: str = STATE_OFF) -> None: - """Force reset the detector state to target state.""" - self._transition_to(target_state, dt_util.now()) + def set_match_committed(self, committed: bool) -> None: + """Mirror whether the caller has committed a program this cycle. + + Until it has, matches run at half ``match_interval`` (audit LIVE-17): at + the shipped 300 s, 12 resampled points on a 30 s plug need 330 s, so the + 300 s try was empty too and the first commit came a median 25 min in. + Measured: right programme shown 56.2 -> 61.0% of cycle time, never + committed 8 -> 4 of 247, +19% matcher CPU. Persistence stays a match + count, as in the measured arm. + """ + self._match_committed = bool(committed) + + def set_user_paused(self, paused: bool, timestamp: datetime | None = None) -> None: + """Mirror the user's Pause Cycle state (set by the manager). + + Kept apart from ``_verified_pause`` on purpose: the envelope auto-pause + writes that one too, and freezing Smart Termination on it would re-open + the #375 hang class. Only a USER pause blocks Smart Termination. + + Item 514: a resume banks the pause (from ``timestamp``, now by default) + as a span the gates' programme time leaves out. A pause start restored + from a snapshot is kept when the manager re-asserts the pause. + """ + paused = bool(paused) + now = dt_util.as_utc(timestamp) if timestamp is not None else utc_now() + if paused and not self._user_paused: + if self._user_pause_since is None: + self._user_pause_since = now + elif not paused: + since, start = self._user_pause_since, self._current_cycle_start + self._user_pause_since = None + if self._user_paused and since is not None and start is not None: + begin = max(since, start) + length = (now - begin).total_seconds() + if length > 0: + self._user_pause_spans = _merge_spans([ + *self._user_pause_spans, + ((begin - start).total_seconds(), length), + ]) + self._user_paused = paused + + def mark_sensor_unavailable(self, timestamp: datetime) -> None: + """Start a sensor outage: the power sensor has no usable value (item 266). + + Called by the manager when the sensor's state becomes unavailable, unknown + or non-numeric. Until the next real reading nothing is observed, so that + span is not credited as quiet, and a watchdog keepalive inside it takes no + decision (see ``process_reading``). Idempotent: an ongoing outage keeps + its first start. + """ + if self._sensor_outage_since is None: + self._sensor_outage_since = dt_util.as_utc(timestamp) + in_cycle = self._state in ( + STATE_STARTING, STATE_RUNNING, STATE_PAUSED, STATE_ENDING + ) + self._logger.log( + logging.INFO if in_cycle else logging.DEBUG, + "Power sensor unavailable from %s (state %s): quiet time is not " + "credited until it reports again", + self._sensor_outage_since, + self._state, + ) + + def reset( + self, target_state: str = STATE_OFF, timestamp: datetime | None = None + ) -> None: + """Force reset the detector state to target state. + + ``timestamp`` is the reading that caused the reset. The state entered here + is timed from it (ANTI_WRINKLE's 2 h exit reads ``_state_enter_time``), so a + replay with historical timestamps must pass it: stamping the host clock + left a replayed dryer in ANTI_WRINKLE forever (audit DETECT-10). + """ + # Item 515: the manager's Off expiry of a terminal state is a display change, + # not a fresh start. Since false starts return to the terminal state, the + # standby's re-probe flag (item 501) and the probes' pre-roll readings live + # there now, as they did in OFF, and carry into it (the cycle end emptied + # the buffer, so it holds nothing of that cycle). + idle_carry = ( + TERMINAL_PROBE_RETURNS + and target_state == STATE_OFF + and self._state in (STATE_FINISHED, STATE_INTERRUPTED, STATE_FORCE_STOPPED) + ) + carried = (self._standby_reprobe, self._preroll_buffer) if idle_carry else None + self._transition_to( + target_state, + dt_util.as_utc(timestamp) if timestamp is not None else utc_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 @@ -1141,7 +2326,6 @@ class CycleDetector: self._current_cycle_start = None self._last_active_time = None self._cycle_max_power = 0.0 - self._ma_buffer = [] self._energy_since_idle_wh = 0.0 self._time_above_threshold = 0.0 # Only reset time_below_threshold if not transitioning to ANTI_WRINKLE @@ -1149,7 +2333,9 @@ class CycleDetector: if target_state != STATE_ANTI_WRINKLE: self._time_below_threshold = 0.0 self._time_below_threshold_gapfree = 0.0 + self._time_below_unobserved = 0.0 self._last_match_time = None + self._match_committed = False self._matched_profile = None # Clear stale match state so the next cycle starts with clean defaults. # _expected_duration left at 0 tells the dishwasher end-spike gate that @@ -1157,11 +2343,14 @@ class CycleDetector: self._expected_duration = 0.0 self._last_match_confidence = 0.0 self._match_ambiguous = False - self._match_prefix_ambiguous = False self._match_prefix_ambiguous_full_shape = False self._matched_tail_power = None self._matched_terminal_high = None self._matched_terminal_quiet_s = None + self._matched_trusted_min_s = None + self._matched_pause_catalogue = None + self._matched_stall_catalogue = None + self._orphaned_pause_evidence_s = 0.0 # Element 12 belongs with them: its own comment claims a stale value can # never license a shortening for a different match, and that was only # true of the tuple path. Left here across a reset, a small positive @@ -1185,6 +2374,7 @@ class CycleDetector: self._anti_wrinkle_candidate_start = None self._anti_wrinkle_candidate_peak = 0.0 self._anti_wrinkle_candidate_start_power = 0.0 + self._anticrease_tail_floor_w = None # Reset idle time tracker for anti-wrinkle self._anti_wrinkle_idle_time = 0.0 # Reset delayed-start tracking @@ -1193,6 +2383,13 @@ class CycleDetector: self._delay_wait_true_off_seconds = 0.0 self._starting_paused_off_since = None self._delay_wait_high_start = None + # Item 501: a forced state is a fresh start, never a standby re-probe. + self._standby_reprobe = False + self._probe_hidden = False + self._probe_wait_since = None + self._probe_terminal_from = None + if carried is not None: + self._standby_reprobe, self._preroll_buffer = carried @property def state(self) -> str: @@ -1236,7 +2433,6 @@ class CycleDetector: smart_ratio: float, is_confident: bool, ambiguous: bool, - prefix_ambiguous: bool, power_plausible: bool = True, ) -> str | None: """Why the Smart-Termination fast end-path did NOT fire, for diagnostics. @@ -1255,8 +2451,6 @@ class CycleDetector: return "low_confidence" if ambiguous: return "match_ambiguous" - if prefix_ambiguous: - return "prefix_ambiguous" if not power_plausible: return "still_active" return None @@ -1330,6 +2524,16 @@ class CycleDetector: gap-free tally is reset like any other hole - at ANY step size, since the watchdog injects far more often than the outage ceiling. + A RECORDED outage (``mark_sensor_unavailable``, register item 266) goes + further, because only it says where the hole began: from its start until + the next real reading, time is not added to ``_time_below_threshold`` + either, so an outage can no longer run out an end gate. A keepalive inside + one advances the clock and nothing else - no state-machine step, no + sample in the trace - so no decision rests on a value nobody read. The + manager's staleness force-stop still ends a cycle whose sensor stays dead. + ``observed=False`` alone (a late watchdog tick, item 391) keeps its + narrower meaning: it resets the gap-free tally only. + (The `_keep_tail_cap` use this flag was originally added for, register item 238, was implemented, measured and reverted: after ``_last_active_time`` every reading is below the stop threshold anyway, so @@ -1337,8 +2541,24 @@ class CycleDetector: only shows the plug is chatty. See item 260 for two more that were measured and rejected.) """ + # Every interval below is a datetime subtraction. Two aware datetimes that + # share one tzinfo instance - which every `dt_util.now()` stamp does - are + # subtracted on their WALL-CLOCK fields, so across a DST change dt, elapsed + # and the stored duration are off by an hour (a spring-forward soak was + # credited 3960 s of quiet and split the wash; audit DETECT-01). UTC has no + # transitions, and mixed-zone comparisons elsewhere stay correct. + timestamp = dt_util.as_utc(timestamp) + outage_since = self._sensor_outage_since if not synthetic: self._last_real_reading_time = timestamp + if outage_since is not None: + # The sensor spoke: the outage is over (item 266). + self._sensor_outage_since = None + self._logger.debug( + "Power sensor reporting again after %.0fs unavailable; that " + "span was not counted as quiet", + (timestamp - outage_since).total_seconds(), + ) # Calculate dt (needed by the stop lockout below and the state machine). dt = 0.0 @@ -1407,6 +2627,9 @@ class CycleDetector: # 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). self._prior_p95_dt = self._p95_dt + # Below five intervals `_p95_dt` is just the last interval (or the 1 s + # default), not a cadence, so nothing can be called outage-sized yet. + prior_cadence_known = len(self._recent_dts) >= 5 # Only the SENSOR trains the cadence estimator (#424). The watchdog's 0 W # keepalives exist because the plug fell silent, so once it does every # interval left in `_recent_dts` is one we manufactured: p95 AND the @@ -1427,15 +2650,20 @@ class CycleDetector: # 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: - self._ma_buffer.pop(0) - # 2. Accumulators Update # Hysteresis Logic if self._state in (STATE_OFF, STATE_DELAY_WAIT, STATE_STARTING, STATE_UNKNOWN): threshold = self._config.start_threshold_w + elif self._state in (STATE_FINISHED, STATE_INTERRUPTED, STATE_FORCE_STOPPED): + # No cycle is open here either, so a terminal state starts on + # start_threshold_w like OFF (register item 510). It fell to the stop + # threshold by omission when these states were added: a display left + # on above stop probed STARTING on its first reading, a probe that + # cannot commit (STARTING measures against start_threshold_w), and the + # manager cleared Finished, Clean and the unload nag on it. The higher + # of the two, so an inverted pair (stop above start, seen in a + # contributed export) is not made easier to leave than before. + threshold = max(self._config.start_threshold_w, self._config.stop_threshold_w) else: threshold = self._config.stop_threshold_w @@ -1469,7 +2697,30 @@ class CycleDetector: # 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 + + # The part of `dt` inside a recorded sensor outage (item 266, audit + # DETECT-13): from the outage start - or the previous reading, if later - + # up to this one. 0.0 whenever the sensor never went unavailable. + unobserved = 0.0 + if outage_since is not None and dt > 0: + unobserved = min(dt, max(0.0, (timestamp - outage_since).total_seconds())) + # Outage-sized interval: clip(10x cadence, 60, 3600) like + # energy_gap_threshold_s, from the maintained p95 (O(1) in this hot path) + # as it stood BEFORE this reading, so a gap cannot widen its own ceiling. + outage_ceiling = min(3600.0, max(60.0, 10.0 * self._prior_p95_dt)) + + # Carrying the previous level forward assumes somebody was looking (item + # 266). Time inside a recorded outage, and a sensor interval the cadence + # calls an outage, is not high-power evidence either: crediting it let a + # plug that dropped out mid-heat confirm a start and bank its last power + # for the whole hole, which the stored energy (`integrate_wh` drops + # outage-sized segments) never counted. Synthetic keepalives are exempt + # from the size test for the reason given in the low branch below. + high_dt = 0.0 + if prev_high: + high_dt = dt - unobserved + if not synthetic and prior_cadence_known and dt > outage_ceiling: + high_dt = 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, @@ -1477,8 +2728,9 @@ class CycleDetector: # 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. + # The sibling paths already do this: the DELAY_WAIT seed is this same + # accumulator over its high streak (item 504) 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 ) @@ -1487,11 +2739,20 @@ class CycleDetector: self._time_above_threshold += high_dt self._time_below_threshold = 0.0 self._time_below_threshold_gapfree = 0.0 + self._time_below_unobserved = 0.0 # 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 + # Quiet nobody saw is not quiet (item 266). The watchdog keeps + # injecting while the sensor is unavailable, and crediting those + # ticks let a dropout that began on a low reading run out the + # fallback timeout: a washer was closed `completed` mid-wash and the + # rest recorded as a second cycle. Frozen, not reset, so the quiet + # observed on either side still counts and `is_waiting_low_power` + # keeps the manager's staleness force-stop armed for a dead sensor. + self._time_below_threshold += dt - unobserved + self._time_below_unobserved += unobserved # Gap-free tally: an outage-sized step is unobserved time, so restart # the observed-quiet tally from this sample instead of crediting the # gap. Ceiling mirrors energy_gap_threshold_s (clip(10x cadence, 60, @@ -1523,8 +2784,12 @@ class CycleDetector: # so during a real outage every individual `dt` sits under the # ceiling - qualifying the ceiling test left the tally accumulating # exactly as before, which is the bug this is meant to fix. - outage_ceiling = min(3600.0, max(60.0, 10.0 * self._prior_p95_dt)) - if (synthetic and not observed) or (dt > outage_ceiling and not synthetic): + # (`outage_ceiling` is computed once, above the high-power credit.) + if ( + (synthetic and not observed) + or unobserved > 0 + or (dt > outage_ceiling and not synthetic) + ): self._time_below_threshold_gapfree = 0.0 else: self._time_below_threshold_gapfree += dt @@ -1533,6 +2798,25 @@ class CycleDetector: self._time_in_state += dt self._last_power = power + if power < self._config.stop_threshold_w: + # Back on the idle floor: the next probe is a fresh one (item 501). + self._standby_reprobe = False + if not synthetic: + # #452 idle display: real readings only (a keepalive is not a level). + self._update_idle(timestamp, power) + + # A keepalive inside a recorded outage carries no observation, only the + # clock (item 266). Stepping the state machine on it would hand every + # finisher a window of the 0 W the manager injects for an unreadable + # sensor - the anti-crease finalize reads exactly such a window - and + # write that invented 0 W into the trace and the matcher's input. So it + # stops here; the next real reading picks up from the frozen tallies. + if ( + synthetic + and outage_since is not None + and self._state in (STATE_STARTING, STATE_RUNNING, STATE_PAUSED, STATE_ENDING) + ): + return anti_wrinkle_active = ( self._config.anti_wrinkle_enabled @@ -1553,7 +2837,25 @@ class CycleDetector: STATE_ANTI_WRINKLE, ): started_from_anti_wrinkle = False - if anti_wrinkle_active and self._state == STATE_ANTI_WRINKLE and is_high: + tail_floor = ( + self._anticrease_tail_floor_w + if self._state == STATE_ANTI_WRINKLE + else None + ) + if ( + anti_wrinkle_active + and tail_floor is not None + and is_high + and power <= tail_floor + ): + # Register item 393a: the #296 tail's own baseline between bursts. + # It can sit above stop_threshold (the 3.3 W Knitterschutz draw + # against a ~1.5 W threshold), where it never reset the candidate, + # so every tail left anti-wrinkle after anti_wrinkle_max_duration. + self._anti_wrinkle_candidate_start = None + self._anti_wrinkle_candidate_peak = 0.0 + self._anti_wrinkle_candidate_start_power = 0.0 + elif anti_wrinkle_active and self._state == STATE_ANTI_WRINKLE and is_high: if self._anti_wrinkle_candidate_start is None: self._anti_wrinkle_candidate_start = timestamp self._anti_wrinkle_candidate_peak = power @@ -1566,11 +2868,22 @@ class CycleDetector: candidate_duration = ( timestamp - self._anti_wrinkle_candidate_start ).total_seconds() + # Register item 393a: after the #296 finalise the tail is a train of + # sub-max-power drum bursts, and a send-on-change plug can skip the + # baseline between two of them (the reporter's 20 Knitterschutz + # tails all hold 60-111 s without a reading under stop_threshold), + # so the configured burst length opened a second cycle on the tail + # itself. The finalise accepted ANTI_CREASE_CONFIRM_WINDOW_S of such + # readings as tail; leaving it on duration takes a burst longer than + # that. A next wash's heating still leaves at once on the power test. + burst_limit = float(self._config.anti_wrinkle_max_duration) + if tail_floor is not None: + burst_limit = max(burst_limit, ANTI_CREASE_CONFIRM_WINDOW_S) exceeds = ( self._anti_wrinkle_candidate_peak > self._config.anti_wrinkle_max_power or power > self._config.anti_wrinkle_max_power - or candidate_duration > self._config.anti_wrinkle_max_duration + or candidate_duration > burst_limit ) if exceeds: @@ -1604,7 +2917,13 @@ class CycleDetector: self._anti_wrinkle_candidate_start_power = 0.0 if self._state == STATE_ANTI_WRINKLE: - # Track time in idle (below exit_power threshold) + # Track time in idle. Quiet is below the exit power OR the stop + # threshold, whichever is higher, so the exit power only matters when + # set above stop. Not a "true off" level below stop (#285 #296 #325): + # after 33 of 139 corpus washer/dryer cycles the appliance idled + # between the 0.8 W default and its stop threshold, which would hold + # anti-wrinkle (and with it Clean and the unload reminder) for the + # 2 h cap. The #296 tail's baseline above stop is the tail floor's job. effective_exit = max(self._config.anti_wrinkle_exit_power, self._config.stop_threshold_w) if power < effective_exit: # Low-power gap invalidates any burst candidate collected while in anti-wrinkle. @@ -1700,10 +3019,28 @@ class CycleDetector: # excursion - most users' "menu navigation" peaks last # less than a sample interval anyway). + terminal = self._state in ( + STATE_FINISHED, STATE_INTERRUPTED, STATE_FORCE_STOPPED + ) + if terminal and not is_high and power >= self._config.stop_threshold_w: + # Item 510: a terminal state holds through the standby band instead + # of probing on it, so the band reading marks the next probe as a + # standby re-probe (item 501), as the band probe's abort used to. + self._standby_reprobe = True + if is_high and not started_from_anti_wrinkle: # Transition to STARTING self._preserve_delay_band_on_off = self._delay_band_start is not None + self._hide_probe( + self._standby_reprobe and (self._state == STATE_OFF or terminal) + ) + back = ( + (self._state, self._state_enter_time, self._sub_state) + if terminal and TERMINAL_PROBE_RETURNS + else None + ) self._transition_to(STATE_STARTING, timestamp) + self._probe_terminal_from = back # item 515 self._current_cycle_start = timestamp self._power_readings = [(timestamp, power)] # Seed from the guarded interval (#403), not raw dt: this seed @@ -1714,6 +3051,7 @@ class CycleDetector: self._energy_since_idle_wh = high_step_wh self._cycle_max_power = power self._apply_curve_preroll(timestamp, power) + self._show_probe_with_evidence() # 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 @@ -1738,28 +3076,39 @@ class CycleDetector: if self._delay_wait_high_start is None: self._delay_wait_high_start = timestamp self._delay_wait_high_power = power + # Item 504: the streak's energy from here, credited per guarded + # interval by the accumulator above (#403), seeds the probe. + # The previous reading was not high, so nothing before the + # anchor is lost. + self._energy_since_idle_wh = 0.0 else: elapsed_high = ( timestamp - self._delay_wait_high_start ).total_seconds() if elapsed_high >= self._config.start_duration_threshold: - self._logger.info( - "Delayed start: cycle starting (power %.1fW sustained ≥ %.1fW for %.0fs)", + # Debug, like any other probe: since item 504 a straddling + # standby probes from here hundreds of times a day. + self._logger.debug( + "Delayed start: probing (power %.1fW sustained ≥ %.1fW for %.0fs)", power, self._config.start_threshold_w, elapsed_high, ) + self._hide_probe(True) # out of a standby (item 501) + wait_since = self._state_enter_time or timestamp self._transition_to(STATE_STARTING, timestamp) + self._probe_wait_since = wait_since # item 504 start_timestamp = self._delay_wait_high_start or timestamp start_power = self._delay_wait_high_power or power self._current_cycle_start = start_timestamp self._power_readings = [(start_timestamp, start_power)] - elapsed_from_anchor = (timestamp - start_timestamp).total_seconds() - self._energy_since_idle_wh = ( - start_power * (elapsed_from_anchor / 3600.0) - if elapsed_from_anchor > 0 - else 0.0 - ) + # `_energy_since_idle_wh` already holds the streak's energy + # (item 504). It was `start_power * elapsed`, which credited + # the first high reading's level for the whole window: a + # 22 W blip then 5 W for 20 s banked 0.15 of a 0.2 Wh gate, + # and once false starts return here every standby probe + # started that way (max aborted probe 0.53 -> 0.78 of the + # gate on #35). if timestamp != start_timestamp: self._power_readings.append((timestamp, power)) self._cycle_max_power = max(start_power, power) @@ -1770,6 +3119,7 @@ class CycleDetector: # 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) + self._show_probe_with_evidence() else: # Power dropped back below start threshold - clear the # high-power streak anchor so the next high reading @@ -1811,6 +3161,8 @@ class CycleDetector: # Power back up - clear any accumulated "true off" hold time. self._starting_paused_off_since = None + self._show_probe_with_evidence() # item 501 + if self._time_above_threshold >= self._config.start_duration_threshold: if self._energy_since_idle_wh >= self._config.start_energy_threshold: self._transition_to(STATE_RUNNING, timestamp) @@ -1866,11 +3218,48 @@ class CycleDetector: # preserve the band via _preserve_delay_band_on_off if it was set # at STARTING entry (line 838), so a brief high-power peak (menu # navigation) doesn't restart the delayed-start accumulation from zero. - self._transition_to(STATE_OFF, timestamp) + # Item 501: back in the band, not on the idle floor, so the next + # probe is a standby re-probe and is not shown until it has evidence. + self._standby_reprobe = power >= self._config.stop_threshold_w + wait_since = self._probe_wait_since + waited_s = ( + (timestamp - wait_since).total_seconds() + if wait_since is not None + else 0.0 + ) + if self._probe_terminal_from is not None: + # Item 515: out of Finished / Interrupted / Force-Stopped, back + # there whatever the power: a probe that never committed ends + # nothing, and the manager's expiry still owns the way to OFF. + self._return_probe_to_terminal(timestamp, power) + elif ( + wait_since is not None + and self._standby_reprobe + and waited_s < self._config.delay_timeout_seconds + ): + # Item 504: a probe out of DELAY_WAIT that falls back into the + # band goes back to waiting, keeping DELAY_WAIT's timeout + # anchor. Back to OFF lost it, and on a standby straddling + # start_threshold_w the band (OFF only) re-armed DELAY_WAIT a + # minute later: off <-> waiting ~16 times per idle day on + # #35. A drop below stop_threshold_w still ends the wait, + # and so does the timeout, checked here because on such a + # standby this abort may be the only in-band reading. + self._transition_to(STATE_DELAY_WAIT, timestamp) + self._state_enter_time = wait_since + self._time_in_state = max(0.0, waited_s) + else: + if wait_since is not None and self._standby_reprobe: + self._logger.info( + "Delayed start timeout after %.0fh → OFF", + self._config.delay_timeout_seconds / 3600.0, + ) + self._transition_to(STATE_OFF, timestamp) elif self._state == STATE_RUNNING: self._power_readings.append((timestamp, power)) self._cycle_max_power = max(self._cycle_max_power, power) + self._update_stall(timestamp, power) # #452, display + standby-band hold # Anti-crease finalize (#296): a matched cycle past its expected # duration that has settled into the low-power tumble tail is done - @@ -1894,45 +3283,7 @@ class CycleDetector: # the plateau and finalize as a normal completion (so anti-wrinkle # still engages). Cheaply gated on being well past expected before # the window scan runs. - if self._is_standby_band_stuck(timestamp): - start_time = self._current_cycle_start or timestamp - current_duration = (timestamp - start_time).total_seconds() - # The plateau sits ABOVE stop_threshold, so it keeps advancing - # _last_active_time and the default keep_tail=False trim would NOT - # remove it - inflating the stored duration/energy with minutes of - # standby. Snap the end back to the last real activity (the last - # reading above the plateau ceiling) and drop the trailing plateau. - level_ceiling = float(self._cycle_max_power) * STANDBY_BAND_MAX_FRACTION - plateau_start_idx = None - for i in range(len(self._power_readings) - 1, -1, -1): - if float(self._power_readings[i][1]) > level_ceiling: - plateau_start_idx = i - break - if ( - plateau_start_idx is not None - and plateau_start_idx < len(self._power_readings) - 1 - ): - self._power_readings = self._power_readings[: plateau_start_idx + 1] - self._last_active_time = self._power_readings[-1][0] - current_duration = ( - self._last_active_time - start_time - ).total_seconds() - self._logger.info( - "Standby-band finalize: flat plateau ~%.1fW (peak %.0fW) held " - "past expected %.0fs — appliance finished but holds a standby " - "draw above stop_threshold; finalizing (plateau trimmed, " - "duration %.0fs).", - power, - self._cycle_max_power, - self._expected_duration, - current_duration, - ) - self._finish_cycle( - timestamp, - status="completed", - termination_reason=TerminationReason.TIMEOUT, - keep_tail=False, - ) + if self._maybe_finalize_standby_band(timestamp, power): return # Max duration safety @@ -1948,6 +3299,7 @@ class CycleDetector: elif self._state == STATE_PAUSED: self._power_readings.append((timestamp, power)) + self._update_stall(timestamp, power) # #452 # Anti-crease finalize (#296) - see the RUNNING branch. if self._maybe_finalize_anticrease_tail(timestamp): @@ -1966,6 +3318,7 @@ class CycleDetector: elif self._state == STATE_ENDING: self._power_readings.append((timestamp, power)) + self._update_stall(timestamp, power) # #452 # Hard cap: ENDING must not run longer than RUNNING's 8 h safety limit. # Without this a standby baseline can hold the state open indefinitely. @@ -1987,8 +3340,18 @@ class CycleDetector: return if is_high: - start_time = self._current_cycle_start or timestamp - current_duration = (timestamp - start_time).total_seconds() + # Standby-band finalize (#296/#445) from ENDING too (DETECT-03). A + # run that dipped long enough to reach ENDING and THEN settled on a + # flat standby above stop_threshold keeps every reading "high": + # after 120 s in state each one is kept below as a terminal spike + # and returns, resetting the quiet timer, so neither the fallback + # nor the RUNNING-only plateau check could ever end it - the 8 h + # force-stop did. Same predicate and trim as RUNNING. + if self._maybe_finalize_standby_band(timestamp, power): + return + + # Programme time: a stall is not progress (item 511). + current_duration = self._gate_elapsed_s(timestamp) is_dishwasher = self._config.device_type == "dishwasher" @@ -2006,7 +3369,7 @@ class CycleDetector: self._expected_duration <= 0 or current_duration >= self._expected_duration * DISHWASHER_END_SPIKE_MIN_PROGRESS - ): + ) and self._spike_follows_terminal_quiet(timestamp): self._end_spike_seen = True self._end_spike_duration = current_duration self._logger.debug( @@ -2078,13 +3441,18 @@ class CycleDetector: else: # Periodic profile matching during ending self._try_profile_match(timestamp) + # A verified pause a revoke orphaned is released after a bounded + # quiet, so the fallback below can end the cycle (item 498). + self._release_orphaned_pause() # --- SMART TERMINATION CHECK --- # If we have a confident profile match and duration meets expectations, # we terminate early (after appropriate debounce), ignoring long arbitrary timeouts. if self._matched_profile: start_time = self._current_cycle_start or timestamp - current_duration = (timestamp - start_time).total_seconds() + # Programme time (item 511): the halt itself carried the clock + # past the ratio, so the resumed wash ended at its next quiet. + current_duration = self._gate_elapsed_s(timestamp) # --- ROBUSTNESS UPGRADE --- # 1. Require higher duration ratio for Smart path @@ -2114,17 +3482,12 @@ class CycleDetector: # Gate the predictive end on match certainty. # _match_ambiguous: top-1 vs top-2 score gap is too small to - # trust the matched profile's expected duration — fall through - # to the power-based fallback timeout instead. - # _match_prefix_ambiguous: a longer candidate with a similar - # shape score exists in the pool. The current trace may be a - # prefix of that longer program (e.g. Quick 46 min matched - # while the machine is actually running Normal 88 min and - # happens to be in a mid-cycle soak dip at the 46-min mark). - # Blocking Smart Termination here means a true Quick cycle - # waits for the fallback timeout instead of getting an early - # close — an acceptable trade-off against the alternative of - # splitting a Normal wash into two separate cycle records. + # trust the matched profile's expected duration - fall through + # to the power-based fallback timeout instead. (The #364 + # prefix-fit flag that also blocked here - "a longer candidate + # explains this trace better" - was removed in 0.5.8: since + # #400 the live matcher scores that prefix itself, and the flag + # never fired at a split moment on the corpus. See const.py.) # Surface why the fast end-path is (not) firing, throttled to # reason changes so a stuck cycle's cause is visible in the log # without spamming every reading. Pure diagnostic (#346). @@ -2134,7 +3497,6 @@ class CycleDetector: smart_ratio, is_confident_match, self._match_ambiguous, - self._match_prefix_ambiguous, _power_plausible, ) if _block_reason != self._last_smart_term_block_reason: @@ -2142,13 +3504,12 @@ class CycleDetector: if _block_reason is not None: self._logger.debug( "Smart Termination not applied (%s): dur=%.0fs/%.0fs conf=%.2f " - "ambiguous=%s prefix_ambiguous=%s trailing_power=%s profile_tail=%s", + "ambiguous=%s trailing_power=%s profile_tail=%s", _block_reason, current_duration, self._expected_duration * smart_ratio, getattr(self, "_last_match_confidence", 0.0), self._match_ambiguous, - self._match_prefix_ambiguous, self._trailing_mean_power(timestamp, self._tail_window_s()), self._matched_tail_power, ) @@ -2157,7 +3518,11 @@ class CycleDetector: current_duration >= (self._expected_duration * smart_ratio) and is_confident_match and not self._match_ambiguous - and not self._match_prefix_ambiguous + # A user pause is authoritative: every other finisher honours + # it, and the paused time itself pushes elapsed past the + # ratio, so a washer paused near its end was finished by + # Smart Termination while the plug sat at 0 W (DETECT-05). + and not getattr(self, "_user_paused", False) # #364: the clock says "done", but if we are still drawing # several times what this profile draws at its own end, the # match is a shorter look-alike and we are mid-wash. Block; @@ -2191,7 +3556,20 @@ class CycleDetector: else: smart_debounce = 120.0 - if self._time_in_state >= smart_debounce: + # Quiet time, not time in ENDING, for everything but a + # dishwasher: a wash that resumes after > 120 s in ENDING + # stays in ENDING, so `_time_in_state` had the debounce + # pre-paid by washing time and Smart fired on the next dip, + # turning the final spin into a second cycle (DETECT-04; + # 0/295 corpus cycles move). A dishwasher keeps state time, + # or every pump-out would add up to 300 s. + if self._config.device_type == "dishwasher": + _smart_quiet = self._time_in_state + else: + _smart_quiet = min( + self._time_in_state, self._time_below_threshold + ) + if _smart_quiet >= smart_debounce: # --- END SPIKE WAIT PERIOD (Dishwashers) --- # Dishwashers should see the real end-of-cycle # pump-out (which arms _end_spike_seen via the 85% @@ -2233,7 +3611,7 @@ class CycleDetector: ) or ( current_duration >= self._expected_duration and self._time_below_threshold_gapfree - >= self._config.dishwasher_end_spike_quiet_release + >= self._dishwasher_quiet_release_s() ) if ( self._config.device_type == "dishwasher" @@ -2310,14 +3688,13 @@ class CycleDetector: ): self._logger.info( "Duration-anchored finalize: cycle in ENDING at %.0fs " - "(%.1fx expected %.0fs), quiet %.0fs — Smart Termination " - "was blocked (ambiguous=%s prefix=%s); finalizing.", + "(%.1fx expected %.0fs), quiet %.0fs - Smart Termination " + "was blocked (ambiguous=%s); finalizing.", current_duration, current_duration / self._expected_duration, self._expected_duration, self._time_below_threshold, self._match_ambiguous, - self._match_prefix_ambiguous, ) self._finish_cycle( timestamp, @@ -2329,7 +3706,9 @@ class CycleDetector: # --- FALLBACK TIMEOUT CHECK --- # Rule: To separate cycles, we must wait at least min_off_gap. - effective_off_delay = max(self._config.off_delay, self._config.min_off_gap) + effective_off_delay = self._hazard_wait( + timestamp, max(self._config.off_delay, self._config.min_off_gap) + ) # Progress-aware shortening (register item 306). `min_off_gap` is # there to bridge mid-cycle soak periods; once the run is past the @@ -2376,19 +3755,19 @@ class CycleDetector: # reach for the device type that needed it most. Early ends stayed # at 0.00% for washers at every ratio measured; the dishwashers, # which do produce early ends below 1.0, keep 1.05. - # Gated on the SAME guards Smart Termination respects. The rule + # Gated on the SAME guard Smart Termination respects. The rule # keys on `_expected_duration`, so it must not fire while the - # matcher says that duration is in doubt: `_match_prefix_ambiguous` - # means a much longer look-alike is still plausible, and then "past + # matcher says that duration is in doubt: an ambiguous match may + # mean a much longer look-alike is still plausible, and then "past # the expected end" may really be "mid-soak in a longer programme". - # Without this the fallback timeout walks straight through the - # prefix-landscape guard and re-opens the #288 split-cycle bug - - # caught by test_smart_termination_blocked_by_prefix_ambiguous, - # where a 450 s soak dip sits right at the short profile's end. + # Without this the fallback timeout walks straight through and + # re-opens the #288 split-cycle bug - caught by + # test_smart_termination_blocked_by_ambiguous_match, where a 450 s + # soak dip sits right at the short profile's end. # NOT also gated on `_last_match_confidence >= # match_confidence_threshold`, and that is deliberate, not an - # oversight - the paragraph above says "the SAME guards Smart - # Termination respects" and means the two ambiguity flags. + # oversight - the paragraph above says "the SAME guard Smart + # Termination respects" and means the ambiguity flag. # Smart Termination does check confidence, because it ENDS a cycle # early on a prediction; this rule only shortens a wait that is # already past the programme's own expected end, so the asymmetry @@ -2411,13 +3790,13 @@ class CycleDetector: and self._expected_duration > 0 and self._current_cycle_start is not None ): - _elapsed = (timestamp - self._current_cycle_start).total_seconds() + _elapsed = self._gate_elapsed_s(timestamp) # item 511 # The bar this run has to clear. Normally the matched # programme's own expected end; while the matcher still thinks # a materially LONGER programme is plausible, that longer one's # end instead (register item 330). # - # Blocking outright on the two ambiguity flags - which is what + # Blocking outright on the ambiguity flags - which is what # this did until item 330 - was costing almost every cycle the # shortening. Measured on `devtools/end_gate_eval.py`: of the # 94 cycles that ever pass 1.05x their own expected duration, @@ -2426,12 +3805,14 @@ class CycleDetector: # pre-reporter-export corpus) and the median cycle still # waited out the full `min_off_gap`. # - # The flags are not wrong, they are too coarse. Both exist to + # The flags were not wrong, they were too coarse. They exist to # protect `_expected_duration` against "this is really a prefix # of something longer" (#288 / #364) - a statement about - # DURATION, not about which label wins. Two programmes that - # score within the ambiguity margin and run the same length - # leave "past the expected end" true either way. So instead of + # DURATION, not about which label wins. (Only `_match_ambiguous` + # is left: the #364 prefix-fit flag was removed in 0.5.8.) + # Two programmes that score within the ambiguity margin and + # run the same length leave "past the expected end" true + # either way. So instead of # refusing, raise the bar to the longest duration still in # play: past THAT, no candidate is left for this to be a # mid-soak of, which is exactly the condition the guard was @@ -2443,26 +3824,22 @@ class CycleDetector: # and an ambiguous match with no candidate durations to # compare must block exactly as it did before - otherwise the # #288 split-cycle reproduction - # `test_smart_termination_blocked_by_prefix_ambiguous` walks + # `test_smart_termination_blocked_by_ambiguous_match` walks # straight through, which is how the first draft of this was # caught. - _bar = self._expected_duration - _blocked = False - _bar_raised = False - if self._match_prefix_ambiguous or self._match_ambiguous: - if self._longest_candidate_duration > _bar: - _bar = self._longest_candidate_duration - _bar_raised = True - elif self._longest_candidate_duration <= 0.0: - # No information: a caller that does not send element - # 12 keeps the old refusal (see below). The bound - # itself is now derived from the FULL candidate - # population, the same one `_match_prefix_ambiguity` - # judges, so a longer candidate ranked sixth or lower - # can no longer set the flag while hiding from the - # bar - which it could when this read - # `MatchResult.candidates`, i.e. `candidates[:5]`. - _blocked = True + # + # The bar is `_fallback_shortening_bar` (one implementation + # with the item-469b ENDING hold): None, i.e. blocked, for an + # ambiguous match with no candidate durations. The bound + # itself is derived from the FULL pre-collapse candidate + # population, so a longer candidate ranked sixth or lower + # cannot hide from the bar - which it could when this read + # `MatchResult.candidates`, i.e. `candidates[:5]`. + _bar_s = self._fallback_shortening_bar( + self._expected_duration, + self._match_ambiguous, + self._longest_candidate_duration, + ) # Device-resolved (register item 355): 1.05 is out of reach # for a load-adaptive washer, which reaches a median 0.83 of # its EXPECTED duration before it stops. @@ -2480,12 +3857,7 @@ class CycleDetector: # rest as a second cycle, which is #288. The item-355 measurement # was taken against the expected duration and says nothing about # this case, so a raised bar keeps the original 1.05. - _late_ratio = ( - END_GATE_LATE_RATIO - if _bar_raised - else resolve_end_gate_late_ratio(self._config.device_type) - ) - if not _blocked and _elapsed >= _late_ratio * _bar: + if _bar_s is not None and _elapsed >= _bar_s: effective_off_delay = max( self._config.off_delay, min(self._config.min_off_gap, END_GATE_LATE_SECONDS), @@ -2557,7 +3929,7 @@ class CycleDetector: if not recent_window: # Check deferred finish for matched profiles start_time = self._current_cycle_start or timestamp - current_duration = (timestamp - start_time).total_seconds() + current_duration = self._gate_elapsed_s(timestamp) # item 511 if self._should_defer_finish(current_duration): return @@ -2582,9 +3954,23 @@ class CycleDetector: max_gap_s = energy_gap_threshold_s(recent_ts) recent_e = integrate_wh(recent_ts, recent_p, max_gap_s=max_gap_s) - if recent_e <= self.config.end_energy_threshold: + energy_ok = recent_e <= self.config.end_energy_threshold + if not energy_ok and self._flat_standby_tail(timestamp): + # DETECT-08: the window's energy is a flat standby, not + # activity. Never before the full un-shortened wait, so a + # flat sub-stop phase the item-306 / hazard shortening + # would otherwise cut is still bridged by min_off_gap; + # far under stop, not before 2 h (see the constants). + self._logger.debug( + "Energy gate skipped: %.4fWh is a flat standby " + "after %.0fs quiet", + recent_e, + self._time_below_threshold, + ) + energy_ok = True + if energy_ok: start_time = self._current_cycle_start or timestamp - current_duration = (timestamp - start_time).total_seconds() + current_duration = self._gate_elapsed_s(timestamp) # item 511 if self._should_defer_finish(current_duration): return @@ -2629,6 +4015,12 @@ class CycleDetector: STATE_STARTING, STATE_DELAY_WAIT, STATE_UNKNOWN, + ) and not ( + # Item 515: a false start out of a terminal state returns there, not to + # OFF; it keeps recording as OFF did, so the next probe can still chain + # it. The buffer is empty there until a probe (the cycle end reset it). + self._preroll_buffer + and self._state in (STATE_FINISHED, STATE_INTERRUPTED, STATE_FORCE_STOPPED) ): return self._preroll_buffer.append((timestamp, float(power))) @@ -2753,6 +4145,20 @@ class CycleDetector: if new_state != STATE_ENDING: self._ml_defer_start_duration = None + # Item 501: hiding is per probe, and a re-probe only follows a false start + # straight back in OFF (any other state means the standby run is over). + if new_state != STATE_STARTING: + self._probe_hidden = False + self._probe_wait_since = None # item 504: per probe, like the hiding + self._probe_terminal_from = None # item 515, likewise + if new_state not in (STATE_OFF, STATE_STARTING): + self._standby_reprobe = False + if new_state not in (STATE_RUNNING, STATE_PAUSED, STATE_ENDING): + self._clear_stall() # #452: a stall belongs to one open cycle + self._stall_spans = [] # item 511 + self._user_pause_spans = [] # item 514 + self._user_pause_since = None + # Reset energy accumulator on transition to OFF if new_state == STATE_OFF: self._energy_since_idle_wh = 0.0 @@ -2786,6 +4192,8 @@ class CycleDetector: self._delay_wait_high_power = None self._sub_state = "Waiting to Start" self._preserve_delay_band_on_off = False + # Like OFF: a false start returning here (item 504) leaves no energy. + self._energy_since_idle_wh = 0.0 elif new_state == STATE_ANTI_WRINKLE: self._anti_wrinkle_candidate_start = None self._anti_wrinkle_candidate_peak = 0.0 @@ -2842,6 +4250,49 @@ class CycleDetector: self._ml_end_cache = (now_ts, exp, start, result) return result + def _flat_standby_tail(self, timestamp: datetime) -> bool: + """Whether the ENDING quiet is a flat standby the energy gate must not pin. + + DETECT-08. A sampled (no outage hole) window of the last + max(off_delay, STANDBY_BAND_WINDOW_S) seconds whose readings are all above + 0 W and within max(ENDING_FLAT_STANDBY_SPREAD_FLOOR_W, + ENDING_FLAT_STANDBY_SPREAD_FRAC x the highest) of each other, after + max(off_delay, min_off_gap) of quiet when every reading is at least + ENDING_FLAT_STANDBY_NEAR_STOP_FRAC x stop_threshold_w, else after + ENDING_FLAT_STANDBY_LOOSE_QUIET_S. A window reaching back past the start + of the quiet holds an above-stop reading and so is not flat. + """ + quiet = self._time_below_threshold + base_wait = float(max(self._config.off_delay, self._config.min_off_gap)) + if quiet < base_wait: + return False + span = max(float(self._config.off_delay), STANDBY_BAND_WINDOW_S) + powers: list[float] = [] + window_ts: list[datetime] = [] + boundary: datetime | None = None + for ts, p in reversed(self._power_readings): + if (timestamp - ts).total_seconds() <= span: + powers.append(float(p)) + window_ts.append(ts) + else: + boundary = ts + break + if boundary is None or len(powers) < ENDING_FLAT_STANDBY_MIN_READINGS: + return False + lo, hi = min(powers), max(powers) + if lo <= 0.0: + return False + if (hi - lo) > max( + ENDING_FLAT_STANDBY_SPREAD_FLOOR_W, ENDING_FLAT_STANDBY_SPREAD_FRAC * hi + ): + return False + near_stop = lo >= ENDING_FLAT_STANDBY_NEAR_STOP_FRAC * float( + self._config.stop_threshold_w + ) + if not near_stop and quiet < max(base_wait, ENDING_FLAT_STANDBY_LOOSE_QUIET_S): + return False + return not self._window_has_outage_gap([boundary, *window_ts]) + def _window_has_outage_gap(self, window_ts: list[datetime]) -> bool: """Whether a 'sustained window' contains a data-outage-sized hole. @@ -2871,8 +4322,10 @@ class CycleDetector: ) return energy_gap_threshold_s(all_ts) - def _is_standby_band_stuck(self, timestamp: datetime) -> bool: - """Whether a RUNNING cycle is stuck on a flat standby plateau (#296). + def _standby_band_plateau(self, timestamp: datetime) -> float | None: + """The stuck plateau's highest reading, or None when the cycle is not stuck. + + Whether a RUNNING cycle is stuck on a flat standby plateau (#296). Returns True only when ALL of the following hold, so this can never end an active low-power phase: @@ -2891,17 +4344,17 @@ class CycleDetector: so normal cycles never pay for it. """ if self._config.device_type not in STANDBY_BAND_FINALIZE_DEVICE_TYPES: - return False + return None if getattr(self, "_verified_pause", False): - return False + return None if not (self._matched_profile and self._expected_duration > 0): - return False + return None start = self._current_cycle_start if start is None: - return False - current_duration = (timestamp - start).total_seconds() + return None + current_duration = self._gate_elapsed_s(timestamp) # item 511 if current_duration < self._expected_duration * STANDBY_BAND_MIN_RATIO: - return False + return None # #399 interaction, load-bearing since the gate above dropped from 2.0x to # 1.0x expected (#445): a washer can sit quiet below anti_wrinkle_max_power # for minutes BEFORE its final spin, and that quiet is a flat sub-10%-of-peak @@ -2941,10 +4394,10 @@ class CycleDetector: # wash), so it bought the last split for three long waits. 0.10 was the # maintainer's call. if self._anticrease_spin_pending(timestamp): - return False + return None peak = float(self._cycle_max_power) if peak <= 0: - return False + return None level_ceiling = peak * STANDBY_BAND_MAX_FRACTION # Walk the tail; readings are chronological so we can break once outside @@ -2983,16 +4436,87 @@ class CycleDetector: else window_ts ) ): - return False + return None hi = max(window) lo = min(window) if hi > level_ceiling: - return False # a real active reading in the window - not standby + return None # a real active reading in the window - not standby flatness_limit = max( STANDBY_BAND_FLATNESS_FLOOR_W, peak * STANDBY_BAND_FLATNESS_FRACTION ) if (hi - lo) > flatness_limit: - return False # fluctuating - still doing work + return None # fluctuating - still doing work + # Two tiers. At or just above the stop threshold is the #445 shape - an + # appliance whose standby the end gates cannot see - and it closes at the + # expected duration. Anything else that passed the loose test above (a 0 W + # soak, a 60 W rinse; flat and under 10% of a heater peak) waits for twice + # the expected duration, as it did before 0.5.7. See the constants. + stop = float(self._config.stop_threshold_w) + near_stop_ceiling = standby_near_stop_ceiling(stop) + if ( + stop > 0 + and float(np.median(window)) >= stop + and hi <= near_stop_ceiling + # #452: a halt says the programme is not done (it stopped on its + # standby draw before its end), so the near-stop tier waits; the + # loose tier below still bounds it at STANDBY_BAND_LOOSE_MIN_RATIO x + # expected. + and not self._stall_holds_standby_band() + ): + return hi + if current_duration >= self._expected_duration * STANDBY_BAND_LOOSE_MIN_RATIO: + return hi + return None + + def _maybe_finalize_standby_band(self, timestamp: datetime, power: float) -> bool: + """Finish the cycle when it is stuck on a standby plateau; True if it did. + + Called from RUNNING on every reading and from ENDING on every above-stop + reading (DETECT-03); `_standby_band_plateau` holds every gate. + """ + plateau_hi = self._standby_band_plateau(timestamp) + if plateau_hi is None: + return False + start_time = self._current_cycle_start or timestamp + current_duration = (timestamp - start_time).total_seconds() + # The plateau sits ABOVE stop_threshold, so it keeps advancing + # _last_active_time and the default keep_tail=False trim would NOT + # remove it - inflating the stored duration/energy with minutes of + # standby. Snap the end back to the last reading above the PLATEAU + # and drop only the plateau run. This used to snap back to the last + # reading above 10% of the cycle's peak, which cut every lower-power + # phase after the last heating burst - 31 min of a Miele's rinse and + # spin in one replayed cycle. + trim_ceiling = plateau_hi + max(0.5, 0.1 * plateau_hi) + plateau_start_idx = None + for i in range(len(self._power_readings) - 1, -1, -1): + if float(self._power_readings[i][1]) > trim_ceiling: + plateau_start_idx = i + break + if ( + plateau_start_idx is not None + and plateau_start_idx < len(self._power_readings) - 1 + ): + self._power_readings = self._power_readings[: plateau_start_idx + 1] + self._last_active_time = self._power_readings[-1][0] + current_duration = (self._last_active_time - start_time).total_seconds() + self._logger.info( + "Standby-band finalize (%s): flat plateau ~%.1fW (peak %.0fW) held " + "past expected %.0fs - appliance finished but holds a standby " + "draw above stop_threshold; finalizing (plateau trimmed, " + "duration %.0fs).", + self._state, + power, + self._cycle_max_power, + self._expected_duration, + current_duration, + ) + self._finish_cycle( + timestamp, + status="completed", + termination_reason=TerminationReason.TIMEOUT, + keep_tail=False, + ) return True def _anticrease_gate_open(self, timestamp: datetime) -> bool: @@ -3032,8 +4556,9 @@ class CycleDetector: return False if self._last_match_confidence < self._config.match_confidence_threshold: return False - # Deliberately the NARROW #288-only verdict, not the #364-widened flag: a - # false block here disables the finalise AND the match freeze, and because + # The #288 full-shape verdict only (the wider #364 prefix-fit flag never + # reached this gate, and was removed in 0.5.8): a false block here + # disables the finalise AND the match freeze, and because # the tumble bursts recur faster than off_delay neither the fallback timeout # nor ENDING_HARD_FINALIZE can close the cycle - that is the #296 hang. if self._match_ambiguous or self._match_prefix_ambiguous_full_shape: @@ -3045,7 +4570,7 @@ class CycleDetector: start = self._current_cycle_start if start is None: return False - current_duration = (timestamp - start).total_seconds() + current_duration = self._gate_elapsed_s(timestamp) # programme time, item 511 # 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. @@ -3107,6 +4632,11 @@ class CycleDetector: """ if not self._anticrease_gate_open(timestamp): return False + # Item 511: a stalled wash is halted, not in its tumble tail (the #296 + # bursts leave the stall band, so a real tail never shows as stalled). The + # standby band's loose tier still bounds the plateau. + if STALL_EXCLUDED_FROM_GATES and self._stall_active: + 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 @@ -3134,7 +4664,7 @@ class CycleDetector: # it) and have enough points to judge - not just a couple of recent samples. # ``saw_older`` plus a coverage sanity and an adjacent-gap check # (``_window_has_outage_gap``) is robust to sample phase/granularity while - # rejecting a dropout-sized hole (mirrors _is_standby_band_stuck). + # rejecting a dropout-sized hole (mirrors _standby_band_plateau). if ( oldest_in_window is None or not saw_older @@ -3176,7 +4706,8 @@ class CycleDetector: 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. + missing input, so it cannot reproduce the #296 hang. The cap reads the + gates' programme time (item 511). """ block = self._matched_terminal_high if block is None: @@ -3188,7 +4719,7 @@ class CycleDetector: start = self._current_cycle_start if expected <= 0 or start is None: return False - current_duration = (timestamp - start).total_seconds() + current_duration = self._gate_elapsed_s(timestamp) 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 @@ -3272,6 +4803,9 @@ class CycleDetector: start = self._current_cycle_start if start is None or not self._power_readings: return 0.0 + if self._stall_spans or self._user_pause_spans: + # The offset is on the profile's grid: programme time (items 511, 514). + offset_s = self._wall_offset_s(offset_s) ceiling = ( float(self._config.anti_wrinkle_max_power) if ceiling_w is None @@ -3312,6 +4846,21 @@ class CycleDetector: return False start_time = self._current_cycle_start or timestamp current_duration = (timestamp - start_time).total_seconds() + # Register item 393a: the tail's baseline, from the window that was just + # accepted as tail (read before _finish_cycle clears the readings). Twice + # its lowest reading, never under the anti-wrinkle exit level. + tail_floor = max( + float(self._config.anti_wrinkle_exit_power), + float(self._config.stop_threshold_w), + 2.0 * min( + ( + float(p) + for ts, p in self._power_readings + if (timestamp - ts).total_seconds() <= ANTI_CREASE_CONFIRM_WINDOW_S + ), + default=0.0, + ), + ) self._logger.info( "Anti-crease finalize: matched '%s' past expected %.0fs (elapsed %.0fs), " "settled into the low-power tumble tail — finalizing into anti-wrinkle.", @@ -3349,6 +4898,9 @@ class CycleDetector: # expected_end" no longer does, and this paragraph used to claim it. tail_cap=self._keep_tail_cap(start_time), ) + # After _finish_cycle, whose reset() clears it (register item 393a). + if self._state == STATE_ANTI_WRINKLE: + self._anticrease_tail_floor_w = tail_floor return True def _is_terminal_drop(self) -> bool: @@ -3415,18 +4967,15 @@ class CycleDetector: # early-wash dip and records the rest of the programme as a second # cycle, which is the expensive failure. A low-confidence match to a # short look-alike is exactly how that happens, so it does not get to - # lower the bar. Uses the WIDER `_match_prefix_ambiguous`, not the - # narrow full-shape flag Smart Termination takes: a false block here - # only keeps the 30 min floor, where for the anti-crease finalize it can - # re-hang the cycle (#296). No-op on the whole corpus either way - every - # corpus dishwasher profile is over 90 minutes. + # lower the bar; nor does an ambiguous one. (It also honoured the #364 + # prefix-fit flag until that was removed in 0.5.8.) No-op on the whole + # corpus either way - every corpus dishwasher profile is over 90 minutes. _dw_floor = DISHWASHER_MIN_CYCLE_DURATION_S if ( self._matched_profile and self._expected_duration > 0 and self._last_match_confidence >= self._config.match_confidence_threshold and not self._match_ambiguous - and not self._match_prefix_ambiguous ): _dw_floor = min(_dw_floor, float(self._expected_duration)) if self._config.device_type == "dishwasher" and duration < _dw_floor: @@ -3525,7 +5074,7 @@ class CycleDetector: quiet_released = ( duration >= self._expected_duration and self._time_below_threshold_gapfree - >= self._config.dishwasher_end_spike_quiet_release + >= self._dishwasher_quiet_release_s() ) if ( self._config.device_type == "dishwasher" @@ -3547,7 +5096,7 @@ class CycleDetector: self._expected_duration, self._dishwasher_end_spike_wait_s(), self._time_below_threshold_gapfree, - self._config.dishwasher_end_spike_quiet_release, + self._dishwasher_quiet_release_s(), self._matched_profile, ) return True @@ -3567,12 +5116,6 @@ class CycleDetector: ) return False - # Also use profile tolerance to handle variable cycle lengths (e.g. long drying) - # Allow deferral up to Expected * (1 + tolerance) - upper_threshold = self._expected_duration * ( - 1.0 + self._config.profile_duration_tolerance - ) - # Primary check: Is duration significantly below expectation? if duration < (self._expected_duration * ratio): self._logger.debug( @@ -3585,13 +5128,234 @@ class CycleDetector: ) return True - # Secondary check: If within valid completion window (ratio to tolerance), allow finish. - if duration <= upper_threshold: - return False - - # Tertiary check: If duration exceeded max tolerance, allow finish (failsafe). + # (A "ratio to 1 + profile_duration_tolerance" window used to follow, but both + # of its branches returned False - the tolerance changed nothing here.) return False + def _fallback_shortening_bar( + self, expected: float, ambiguous: bool, longest: float + ) -> float | None: + """Elapsed seconds from which the ENDING fallback shortens, None when blocked. + + The item 306/330/355 bar for a match with these values: ``expected`` at the + device's late ratio, or, while the match is ambiguous, the longest candidate + at the plain ``END_GATE_LATE_RATIO`` when that is longer, and blocked when + there are no candidate durations to compare (see the gate in + ``process_reading``). Shared by that gate and + :meth:`ambiguous_ending_match_defers`, so the two cannot disagree. + """ + bar = float(expected) + raised = False + if ambiguous: + if longest > bar: + bar = float(longest) + raised = True + elif longest <= 0.0: + return None + ratio = ( + END_GATE_LATE_RATIO + if raised + else resolve_end_gate_late_ratio(self._config.device_type) + ) + return ratio * bar + + def _ending_wait_left_s( + self, expected: float, ambiguous: bool, longest: float, confidence: float, cat: Any + ) -> float: + """Seconds until the ENDING fallback would finish under continued quiet, for + a match with these values (register item 469b). + + The fallback gate in ``process_reading``, run forward: the hazard wait + before the shortening bar, the shortened wait after it (the gate swaps one + for the other, so the shortening can also lengthen), then + ``_should_defer_finish``'s duration floor, which needs the confidence. The + energy gate and Smart Termination are left out: neither depends on which + programme is matched in a way an ambiguous tick changes, except that the + ambiguity blocks Smart Termination (see the caller). + """ + now = self._power_readings[-1][0] + elapsed = self._gate_elapsed_s(now) # the gate's own clock (item 511) + quiet = float(self._time_below_threshold) + hazard = self._hazard_wait_for( + now, max(self._config.off_delay, self._config.min_off_gap), cat, expected, ambiguous + ) + left = max(0.0, hazard - quiet) + bar = self._fallback_shortening_bar(expected, ambiguous, longest) + if bar is not None and elapsed + left >= bar: + short = max( + self._config.off_delay, + min(self._config.min_off_gap, END_GATE_LATE_SECONDS), + ) + left = max(0.0, short - quiet, bar - elapsed) + if confidence >= DEFAULT_DEFER_FINISH_CONFIDENCE: + left = max(left, expected * float(self._config.min_duration_ratio) - elapsed) + return left + + def ambiguous_ending_match_defers( + self, expected: Any, confidence: Any, longest: Any, catalogue: Any = None + ) -> bool: + """Would an AMBIGUOUS match with these values make this ENDING wait longer? + + Register item 469(b). A tick in ENDING matches a trace that ends in its idle + tail, which reads as the prefix of a longer programme pausing, so its top-1 + drifts longer and ties with its runner-up. Applied as is, a shorter match + interval (what "Apply all" suggests) let such a tick defer the end: 01KXGA3C + 62f39dfc34f4, a 29 min wool wash, waited 23.2 min instead of 6.7 min at an + 87 s interval. ``update_match`` refuses such a match when this says the + fallback would finish later with it (:meth:`_ending_wait_left_s` for both), + or as late when the current match is clear (only the ambiguous one blocks + Smart Termination). Not "a longer programme": the shortening bar is not + monotone in the expected duration. A match to the LONGEST candidate does + not raise it, so it can end sooner than a shorter ambiguous one whose bar + the longest raised (01KBWSV8 1dbc19ccba79 / daea1437efcc: 6.7 min applied, + 23-25 min held), and a shorter one can raise it (01KXGA3C 9c2624675652). + A dishwasher's drying and pump-out waits follow the expected duration + alone, so there a longer one is refused outright. + + False outside ENDING or without a matched programme: nothing to keep. + """ + if ( + self._state != STATE_ENDING + or not self._matched_profile + or self._expected_duration <= 0 + or self._current_cycle_start is None + or not self._power_readings + ): + return False + try: + new_expected = float(expected) + new_confidence = float(confidence or 0.0) + new_longest = float(longest or 0.0) + except (TypeError, ValueError, OverflowError): + return True + if not (math.isfinite(new_expected) and new_expected > 0): + return True # would unmatch the cycle: the unshortened fallback + if ( + self._config.device_type == DEVICE_TYPE_DISHWASHER + and new_expected > self._expected_duration + ): + return True + new_left = self._ending_wait_left_s( + new_expected, True, new_longest, new_confidence, + self._sanitize_pause_catalogue(catalogue), + ) + old_left = self._ending_wait_left_s( + self._expected_duration, self._match_ambiguous, + self._longest_candidate_duration, self._last_match_confidence, + self._matched_pause_catalogue, + ) + if new_left > old_left + 1.0: + return True + return not self._match_ambiguous and new_left >= old_left - 1.0 + + def _dishwasher_quiet_release_s(self) -> float: + """Sustained quiet past expected that releases the pump-out wait (#379). + + The configured value - raised to DISHWASHER_QUIET_RELEASE_TERMINAL_MARGIN x + the matched profile's measured quiet before its terminal event while this + run has NOT yet been through that quiet (register item 392). The release + exists for a pump-out that already happened or never comes; a run still + short of the quiet its programme always has before the pump-out has not + reached that point. A run that has been through it - #424's Beko and the + Hatton ECO, whose element-11 event sits mid-cycle - keeps the configured + value (or the item-465 floor below). Lengthen-only; the 30 min spike wait + still bounds the whole wait. + + Also never shorter than the same margin x the longest below-stop pause + the profile's traced evidence ever came back from (element 14, register + item 465), whether or not the run has been through its terminal quiet. + Element 11 is a MEDIAN over the profile's last events, and a cycle closed + before its pump-out contributes the wrong event: on 01KGM619's Eco half + the stored cycles end on their last heating block (100-160 s gap), the + rest on a pump-out 4840-4860 s after it, so the median lands at 2500 s, + a quiet no cycle has. 1.1 x that released 6 of 6 pump-out cycles 6-11 min + before their pump-out, and each stored ~8.1k s instead of ~11.1k s, which + drags `avg_duration`, and with it the next release, earlier. A pause the + programme has resumed from says "activity can still follow a quiet this + long" directly. No position filter: the release is only asked past the + expected end, and the catalogue's fractions are of each cycle's own span, + which is longer than `expected` on exactly the cycles that kept their + pump-out. Lengthen-only, bounded by the same spike wait. + """ + base = max( + float(self._config.dishwasher_end_spike_quiet_release), + self._resumed_pause_release_s(), + ) + quiet = self._matched_terminal_quiet_s + if not (self._matched_profile and quiet) or not self._power_readings: + return base + now = self._power_readings[-1][0] + if self._spike_follows_terminal_quiet(now, latest=True): + return base + return max(base, DISHWASHER_QUIET_RELEASE_TERMINAL_MARGIN * float(quiet)) + + def _resumed_pause_release_s(self) -> float: + """Margin x the longest pause the matched profile resumed from, else 0. + + Read from the hazard gate's catalogue (element 14) with its evidence bar + (END_GATE_HAZARD_MIN_CYCLES traced cycles). See + :meth:`_dishwasher_quiet_release_s` (register item 465). + """ + cat = self._matched_pause_catalogue + if ( + cat is None + or cat[0] < END_GATE_HAZARD_MIN_CYCLES + or not cat[1] + or not self._matched_profile + ): + return 0.0 + return DISHWASHER_QUIET_RELEASE_TERMINAL_MARGIN * max(d for _f, d in cat[1]) + + def _spike_follows_terminal_quiet( + self, timestamp: datetime, *, latest: bool = False + ) -> bool: + """May this ENDING spike be the dishwasher's terminal pump-out? (item 392) + + True unless the matched profile's quiet before its terminal event has been + measured (element 11) and this run has not yet been through it - asked + with the same `terminal_quiet_seen` the keep-tail cap and the banked-tail + repair use. On the corpus's "65° full" (934 s measured, 17/17 cycles) a + 24 W fan blip 211 s after the last heating armed the end at 85% of + expected, and Smart Termination closed the cycle 12 min before the real + pump-out. Only ever withholds the arm, so it can delay an end, never + bring one forward; every other device type, and an unmeasured profile, + is unaffected. + """ + quiet = self._matched_terminal_quiet_s + start = self._current_cycle_start + if ( + self._config.device_type != DEVICE_TYPE_DISHWASHER + or not self._matched_profile + or not quiet + or start is None + or not self._power_readings + ): + return True + # Memoised per reading: the release asks this on every ENDING reading past + # the expected end, and the answer only changes when a reading arrives. + key = (len(self._power_readings), timestamp, latest) + hit = self._terminal_quiet_memo + if hit is not None and hit[0] == key: + return hit[1] + pts = [((ts - start).total_seconds(), float(pw)) for ts, pw in self._power_readings] + last_off = (timestamp - start).total_seconds() + if latest: + # "Has the run been through it by now?": measured from the last + # activity, not from a spike that has not happened. + last_active = self._last_active_time + if last_active is None: + return True + last_off = (last_active - start).total_seconds() + seen = terminal_quiet_seen( + pts, + last_off, + self._config.stop_threshold_w, + float(quiet), + TERMINAL_EVENT_PEAK_FRAC, + ) + self._terminal_quiet_memo = (key, seen) + return seen + def _dishwasher_end_spike_wait_s(self) -> float: """Grace past the expected end while waiting for the terminal pump-out. @@ -3693,6 +5457,29 @@ class CycleDetector: return expected_end if self._config.device_type != DEVICE_TYPE_DISHWASHER: return last_active + cap = self._dishwasher_tail_cap(start_time, expected_end, last_active) + # Never before the length the user has vouched for (register item 384), + # the floor `ProfileStore.async_repair_banked_tails` applies, so live and + # the repair store the same duration. Element 11 is measured from the + # profile's own traces, and on a machine that dries silently AFTER its + # last activity it reads the pre-drying pause instead: the Hatton ECO + # export stores 230-235 min (one corrected to 234), and without this the + # cap snapped three of its four cycles to their last activity at ~120 min. + # Lengthen-only, and only for a run that has itself lasted that long: a + # cancelled or short run never reaches the floor, and lifting its cap + # would bank its whole post-appliance wait (#424). Same outcome as the + # repair, which never lengthens a cycle. + trusted = self._matched_trusted_min_s + if trusted and self._power_readings: + floor = start_time + timedelta(seconds=TRUSTED_LENGTH_FLOOR_FRAC * trusted) + if cap < floor <= self._power_readings[-1][0]: + return floor + return cap + + def _dishwasher_tail_cap( + self, start_time: datetime, expected_end: datetime, last_active: datetime + ) -> datetime: + """The dishwasher half of :meth:`_keep_tail_cap`, before the trusted floor.""" # Only a spike LATE enough to be the terminal pump-out licenses snapping # the stored end back to the last activity. `_end_spike_seen` is set from # DISHWASHER_END_SPIKE_MIN_PROGRESS (0.85), but this file does not treat @@ -3721,6 +5508,11 @@ class CycleDetector: # question; this path did not, so the same cycle got one duration live and # another when the repair re-judged it. Same helper, same 0.5 bar, so the # two cannot drift again (register item 347). + # + # And it has to ask at the threshold the allowance was MEASURED at (#424): + # see `terminal_quiet_seen` for the Beko that banked 10 min on every cycle + # because this test ran at the stop threshold while the statistic it + # guards is taken at a fraction of the cycle's peak. if self._current_cycle_start is not None and self._power_readings: _start = self._current_cycle_start _pts = [ @@ -3728,10 +5520,18 @@ class CycleDetector: for ts, pw in self._power_readings ] _last_off = (last_active - _start).total_seconds() - if quiet_run_before( - _pts, _last_off, self._config.stop_threshold_w - ) >= 0.5 * float(quiet): - return last_active + if terminal_quiet_seen( + _pts, + _last_off, + self._config.stop_threshold_w, + float(quiet), + TERMINAL_EVENT_PEAK_FRAC, + ): + # ...at the terminal event, which may sit below the stop + # threshold and so after `last_active` (register item 384). + return _start + timedelta( + seconds=terminal_event_end(_pts, _last_off, TERMINAL_EVENT_PEAK_FRAC) + ) return last_active + timedelta(seconds=min(quiet, TERMINAL_QUIET_CAP_S)) def _finish_cycle( @@ -3769,7 +5569,7 @@ class CycleDetector: end_time = self._last_active_time or timestamp if not self._current_cycle_start: - self.reset() + self.reset(timestamp=timestamp) return duration = (end_time - self._current_cycle_start).total_seconds() @@ -3794,6 +5594,19 @@ class CycleDetector: if final_readings: last_t, last_p = final_readings[-1] if last_t < end_time: + # Not at ACTIVE power when a capped tail ends past the last kept + # sample: interpolation would read the whole drying allowance (up + # to TERMINAL_QUIET_CAP_S) as full draw, in the stored trace and in + # every envelope built from it. Readings past a cap at or after + # `_last_active_time` are quiet by construction, so the first of + # them is a real observation of this level - the rule the + # banked-tail repair applies to the same point (register item 384). + if last_p >= self._config.stop_threshold_w: + later = next( + (p for t, p in self._power_readings if t > end_time), None + ) + if later is not None and later < self._config.stop_threshold_w: + last_p = later final_readings.append((end_time, last_p)) start_ts = self._current_cycle_start.timestamp() @@ -3810,6 +5623,15 @@ class CycleDetector: "termination_reason": termination_reason, "power_data": [[round(t.timestamp() - start_ts, 1), p] for t, p in final_readings], } + spans = [ + [round(at, 1), round(n, 1)] + for at, n in _merge_spans([*self._stall_spans, *self._user_pause_spans]) + if at < duration + ] + if spans: + # Item 514: the finished stalls and user pauses, so the cycle-end label + # match reads the trace in programme time (`match_rules.final_match_input`). + cycle_data["halt_spans"] = spans self._logger.info("Cycle Finished: %s, %.1f min", status, duration / 60) self._on_cycle_end(cycle_data) @@ -3831,14 +5653,14 @@ class CycleDetector: ): target = STATE_ANTI_WRINKLE - self.reset(target_state=target) + self.reset(target_state=target, timestamp=timestamp) # Stub methods for compatibility or simpler logic def force_end(self, timestamp: datetime) -> None: """Force the cycle to end immediately.""" if self._state != STATE_OFF: self._finish_cycle( - timestamp, + dt_util.as_utc(timestamp), status="force_stopped", termination_reason=TerminationReason.FORCE_STOPPED, keep_tail=False, # Force stop usually implies snap back to reality @@ -3848,12 +5670,26 @@ class CycleDetector: def user_stop(self) -> None: """Handle user-initiated stop.""" if self._state != STATE_OFF: - now = dt_util.now() + now = utc_now() + # "Done now" keeps the tail only while the machine is still running. + # Pressed after the wash had already gone quiet - which is exactly when + # a user stops it, because WashData missed the end - the whole wait was + # banked as cycle time (30 min late -> a 60 min wash stored as 90) and + # fed avg_duration like item 297's Smart tail. Cap it the same way + # (audit DETECT-06). + below_stop = bool(self._power_readings) and ( + self._power_readings[-1][1] < self._config.stop_threshold_w + ) self._finish_cycle( now, status="completed", termination_reason=TerminationReason.USER, keep_tail=True, # User implies "Done Now" + tail_cap=( + self._keep_tail_cap(self._current_cycle_start or now) + if below_stop + else None + ), ) # Prevent immediate restart if power is still high self._ignore_power_until_idle = True @@ -3896,19 +5732,55 @@ class CycleDetector: "end_spike_seen": self._end_spike_seen, "end_spike_duration": self._end_spike_duration, "match_ambiguous": self._match_ambiguous, - "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, "matched_terminal_quiet_s": self._matched_terminal_quiet_s, + "matched_trusted_min_s": self._matched_trusted_min_s, + # Element 14. A dishwasher restored into its terminal-tail match freeze + # never re-matches, so without it the item-465 release floor was gone + # for the rest of the cycle (the hazard gate only lost a shortening). + "matched_pause_catalogue": ( + [ + self._matched_pause_catalogue[0], + [list(p) for p in self._matched_pause_catalogue[1]], + ] + if self._matched_pause_catalogue is not None + else None + ), "longest_candidate_duration": self._longest_candidate_duration, "ml_defer_start_duration": self._ml_defer_start_duration, + # Without it a restart dropped the confidence to 0.0: Smart Termination + # was then blocked as low_confidence, and a dishwasher restored into + # its terminal-tail match freeze could never re-match (DETECT-09). + "last_match_confidence": self._last_match_confidence, + # Register item 393a: the #296 tail's baseline and the idle clock of + # the anti-wrinkle state. Without the floor a restart mid-tail fell + # back to the configured burst length, which splits the tail again. + "anticrease_tail_floor_w": self._anticrease_tail_floor_w, + "anti_wrinkle_idle_time": self._anti_wrinkle_idle_time, + # Item 511: the stalls already over, which the end gates' clock + # excludes (the one in progress is re-derived from the trace). + "stall_spans": [[at, length] for at, length in self._stall_spans], + # Item 514: the user pauses already resumed from, and the current one. + "user_pause_spans": [[at, length] for at, length in self._user_pause_spans], + "user_pause_since": ( + self._user_pause_since.isoformat() if self._user_pause_since else None + ), } + @property + def in_anticrease_tail(self) -> bool: + """STATE_ANTI_WRINKLE entered by the #296 anti-crease finalise (item 393a).""" + return ( + self._state == STATE_ANTI_WRINKLE + and self._anticrease_tail_floor_w is not None + ) + def get_elapsed_seconds(self) -> float: """Return seconds elapsed in current cycle.""" if self._current_cycle_start: - return (dt_util.now() - self._current_cycle_start).total_seconds() + return (utc_now() - self._current_cycle_start).total_seconds() return 0.0 def is_waiting_low_power(self) -> bool: @@ -3918,11 +5790,20 @@ class CycleDetector: and self._time_below_threshold > 0 ) - def restore_state_snapshot(self, snapshot: dict[str, Any]) -> None: - """Restore state from snapshot.""" + def restore_state_snapshot(self, snapshot: dict[str, Any]) -> bool: + """Restore state from snapshot; False when it could not be restored. + + A snapshot that raises leaves the detector OFF (never half-restored) and is + logged with its traceback. The caller keeps the snapshot for diagnostics + instead of deleting it (register item 266 follow-up): until then a restore + that raised lost the running cycle with one ERROR line and no record. + """ try: self._state = snapshot.get("state", STATE_OFF) self._sub_state = snapshot.get("sub_state") + # Not persisted (item 501): a restored probe is shown. + self._standby_reprobe = False + self._probe_hidden = False self._energy_since_idle_wh = snapshot.get("accumulated_energy_wh", 0.0) self._time_above_threshold = snapshot.get("time_above", 0.0) self._time_below_threshold = snapshot.get("time_below", 0.0) @@ -3935,6 +5816,8 @@ class CycleDetector: self._time_below_threshold_gapfree = float( snapshot.get("time_below_gapfree", 0.0) or 0.0 ) + # Not persisted: the restart's own gap is in neither tally either. + self._time_below_unobserved = 0.0 self._cycle_max_power = snapshot.get("cycle_max_power", 0.0) # Sanitize via the same helper as update_match so the class # invariant on _expected_duration holds across restarts and the @@ -3963,11 +5846,13 @@ class CycleDetector: self._end_spike_seen = snapshot.get("end_spike_seen", False) self._end_spike_duration = float(snapshot.get("end_spike_duration", 0.0)) self._match_ambiguous = snapshot.get("match_ambiguous", False) - self._match_prefix_ambiguous = snapshot.get("match_prefix_ambiguous", False) - # A pre-#364 snapshot has no narrow flag: fall back to the widened - # value so a restart cannot loosen the anti-crease gate. + # A pre-#364 snapshot has no narrow flag: fall back to the old single + # `match_prefix_ambiguous` (the #364 prefix-fit flag, removed in 0.5.8, + # and before #364 the #288 verdict itself) so a restart cannot loosen + # the anti-crease gate. Newer snapshots no longer write that key. self._match_prefix_ambiguous_full_shape = snapshot.get( - "match_prefix_ambiguous_full_shape", self._match_prefix_ambiguous + "match_prefix_ambiguous_full_shape", + snapshot.get("match_prefix_ambiguous", False), ) self._matched_tail_power = self._sanitize_tail_power( snapshot.get("matched_tail_power") @@ -3978,6 +5863,12 @@ class CycleDetector: self._matched_terminal_quiet_s = self._sanitize_terminal_quiet( snapshot.get("matched_terminal_quiet_s") ) + self._matched_trusted_min_s = self._sanitize_trusted_min( + snapshot.get("matched_trusted_min_s") + ) + self._matched_pause_catalogue = self._sanitize_pause_catalogue( + snapshot.get("matched_pause_catalogue") + ) # Unconditionally, because the hazard is the value already on the # object, not the one in the snapshot: this restores the ambiguity # flags the ENDING gate reads, so leaving the bound untouched pairs @@ -3987,6 +5878,27 @@ class CycleDetector: snapshot.get("longest_candidate_duration") ) self._ml_defer_start_duration = snapshot.get("ml_defer_start_duration") + try: + self._last_match_confidence = float( + snapshot.get("last_match_confidence") or 0.0 + ) + except (TypeError, ValueError, OverflowError): + self._last_match_confidence = 0.0 + # Item 393a. Meaningful only in the tail; an older snapshot (or any + # other state) yields None / 0.0, i.e. the configured burst length. + # The burst candidate is not restored: the restart broke the run. + in_tail = self._state == STATE_ANTI_WRINKLE + self._anticrease_tail_floor_w = ( + self._sanitize_positive(snapshot.get("anticrease_tail_floor_w")) + if in_tail else None + ) + self._anti_wrinkle_idle_time = ( + self._sanitize_positive(snapshot.get("anti_wrinkle_idle_time")) or 0.0 + if in_tail else 0.0 + ) + self._anti_wrinkle_candidate_start = None + self._anti_wrinkle_candidate_peak = 0.0 + self._anti_wrinkle_candidate_start_power = 0.0 # Restore state enter time and recompute time_in_state from it enter_time = snapshot.get("state_enter_time") @@ -3994,7 +5906,12 @@ class CycleDetector: try: self._state_enter_time = dt_util.parse_datetime(enter_time) if self._state_enter_time: - elapsed = (dt_util.now() - self._state_enter_time).total_seconds() + if self._state_enter_time.tzinfo is None: + self._state_enter_time = self._state_enter_time.replace( + tzinfo=dt_util.now().tzinfo + ) + self._state_enter_time = dt_util.as_utc(self._state_enter_time) + elapsed = (utc_now() - self._state_enter_time).total_seconds() self._time_in_state = max(0.0, elapsed) except Exception: # pylint: disable=broad-exception-caught self._logger.warning("Failed to parse state enter time") @@ -4008,7 +5925,7 @@ class CycleDetector: # Fix Naive Timestamp (Legacy Data) dt_start = dt_start.replace(tzinfo=dt_util.now().tzinfo) self._logger.warning("Restored Naive start_time, assuming local: %s", dt_start) - self._current_cycle_start = dt_start + self._current_cycle_start = dt_util.as_utc(dt_start) if dt_start else None except Exception: # pylint: disable=broad-exception-caught self._logger.warning("Failed to parse start time: %s", start) @@ -4031,8 +5948,8 @@ class CycleDetector: has_naive_readings = True value = float(reading[1]) if math.isfinite(value): - self._power_readings.append((t, value)) - except (TypeError, ValueError) as exc: + self._power_readings.append((dt_util.as_utc(t), value)) + except (TypeError, ValueError, OverflowError) as exc: self._logger.debug("Skipping malformed power reading %s: %s", r, exc) if has_naive_readings: @@ -4047,10 +5964,30 @@ class CycleDetector: dt_last = dt_util.parse_datetime(last_active) if dt_last and dt_last.tzinfo is None: dt_last = dt_last.replace(tzinfo=dt_util.now().tzinfo) - self._last_active_time = dt_last + self._last_active_time = dt_util.as_utc(dt_last) if dt_last else None else: self._last_active_time = self._current_cycle_start + # #452: a stall survives a restart from the restored trace (element + # 15 is not persisted: the next match tick supplies it again). + self._stall_spans = self._sanitize_stall_spans(snapshot.get("stall_spans")) + self._user_pause_spans = self._sanitize_stall_spans( + snapshot.get("user_pause_spans") + ) + _since = snapshot.get("user_pause_since") + _since_dt = dt_util.parse_datetime(_since) if isinstance(_since, str) else None + self._user_pause_since = dt_util.as_utc(_since_dt) if _since_dt else None + self._seed_stall_run() - except Exception as e: # pylint: disable=broad-exception-caught - self._logger.error("Failed restore: %s", e) - self.reset() \ No newline at end of file + except Exception: # pylint: disable=broad-exception-caught + self.restore_error = traceback.format_exc() + self._logger.warning( + "Could not restore the active-cycle snapshot (state %r, cycle start " + "%r); starting from OFF", + snapshot.get("state") if isinstance(snapshot, dict) else None, + snapshot.get("current_cycle_start") if isinstance(snapshot, dict) else None, + exc_info=True, + ) + self.reset() + return False + self.restore_error = None + return True \ No newline at end of file diff --git a/custom_components/ha_washdata/diagnostics.py b/custom_components/ha_washdata/diagnostics.py index ea4b008a..0989e7fb 100644 --- a/custom_components/ha_washdata/diagnostics.py +++ b/custom_components/ha_washdata/diagnostics.py @@ -56,13 +56,26 @@ _SENSITIVE_KEYS = { "switch_entity", "energy_price_entity", "energy_sensor", + # The active-cycle snapshot's meter entity (kept in `failed_restore`). + "energy_meter_source", } +# A record that names a setting in a field instead of using it as the dict key - +# a settings-changelog row is `{"key": "power_sensor", "old": ..., "new": ...}` - +# carries the sensitive value under these generic names, which slipped past the +# key-based redaction: entity ids, person.* and notify targets (audit PLATFORM-08). +_VALUE_FIELDS = ("old", "new", "value") + + def _redact(obj: Any) -> Any: if isinstance(obj, dict): + named = obj.get("key") + names_sensitive = isinstance(named, str) and named in _SENSITIVE_KEYS return { - k: "**REDACTED**" if k in _SENSITIVE_KEYS else _redact(v) + k: "**REDACTED**" + if k in _SENSITIVE_KEYS or (names_sensitive and k in _VALUE_FIELDS) + else _redact(v) for k, v in obj.items() } if isinstance(obj, list): @@ -70,11 +83,23 @@ def _redact(obj: Any) -> Any: return obj +async def _failed_restore(manager: WashDataManager) -> Any: + """The kept failed-restore record, redacted; None on any problem reading it.""" + try: + return _redact(await manager.profile_store.async_get_failed_restore()) + except Exception: # noqa: BLE001 - the download must never fail on this + return None + + async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - manager: WashDataManager = hass.data[DOMAIN][entry.entry_id] + manager: WashDataManager | None = hass.data.get(DOMAIN, {}).get(entry.entry_id) + if manager is None: + # Setup failed or the entry is unloaded: the download must still work - it + # is how such a failure gets reported (audit PLATFORM-14). + return {"entry": _redact(entry.as_dict()), "manager_state": None} # Full store export - same payload as the export_config service, but the # entry_data / entry_options pass through the redactor to strip personal keys. @@ -138,4 +163,8 @@ async def async_get_config_entry_diagnostics( # state_history: [{ts, from, to, program}, ...] - detector state changes # logs: [{ts, lvl}, ...] - log timestamps and levels (msg removed) "live_diagnostics": manager.diag_buffer.redacted_snapshot(), + # The last active-cycle snapshot that failed to restore, with the error and + # its age (register item 266 follow-up); None when none ever has. A download, + # not a bus event, so the 32 KB event-data limit does not apply. + "failed_restore": await _failed_restore(manager), } diff --git a/custom_components/ha_washdata/frontend.py b/custom_components/ha_washdata/frontend.py index 0d0476e3..76d388ad 100644 --- a/custom_components/ha_washdata/frontend.py +++ b/custom_components/ha_washdata/frontend.py @@ -220,6 +220,47 @@ def _ensure_gzip(path: Path) -> None: pass +def _gzip_is_current(path: Path) -> bool: + """True when ``.gz`` exists and decompresses to exactly ``path``'s bytes. + + A content check, not an mtime check (see :func:`_ensure_gzip` for why mtimes + cannot be trusted here). Decompressing is ~20x cheaper than recompressing. + """ + import gzip + import zlib + + gz = path.with_suffix(path.suffix + ".gz") + try: + return gzip.decompress(gz.read_bytes()) == path.read_bytes() + except (OSError, EOFError, zlib.error): # missing, truncated or corrupt + return False + + +def _prepare_translations(directory: Path) -> bool: + """Pre-compress every panel translation file; True if the directory exists. + + Audit UI-18: the panel fetches its English and user-language dictionaries + (130-250 KB each) on every cold load, and aiohttp serves a ``.gz`` sibling only + if one exists. Thirty-five files take ~0.6 s to compress at level 9, so unlike + the two JS assets an unchanged ``.gz`` is kept (verified by content, ~30 ms for + the whole directory) and only a missing or stale one is rebuilt. A ``.gz`` whose + source was removed is deleted: aiohttp would otherwise keep serving it. + """ + if not directory.is_dir(): + return False + try: + sources = sorted(directory.glob("*.json")) + for src in sources: + if src.is_file() and not _gzip_is_current(src): + _ensure_gzip(src) + for gz in directory.glob("*.json.gz"): + if not gz.with_suffix("").is_file(): + gz.unlink(missing_ok=True) + except OSError as exc: # read-only install: serve uncompressed + _LOGGER.debug("Could not pre-compress panel translations (%s)", exc) + return True + + def _prepare_asset(source_name: str, www: Path | None = None) -> Path: """Resolve the best variant of an asset and make sure its .gz is current. @@ -289,8 +330,11 @@ def get_cache_buster(filename: str = CARD_NAME) -> str: src_mtime = os.stat(base / "www" / filename).st_mtime_ns try: panel_dir = base / "translations" / "panel" + # Sources only: the .gz siblings are rewritten at startup, and folding + # their mtime in would change the URL (and refetch the panel) on every + # restart. trans_mtime = max( - (os.stat(f).st_mtime_ns for f in panel_dir.iterdir() if f.is_file()), + (os.stat(f).st_mtime_ns for f in panel_dir.glob("*.json") if f.is_file()), default=0, ) except OSError: @@ -622,9 +666,9 @@ async def _do_register_panel(hass: HomeAssistant, src: Path) -> bool: served = await hass.async_add_executor_job(_prepare_asset, PANEL_JS_NAME) await _async_register_path(hass, PANEL_JS_URL, str(served)) - # Per-language translation files. + # Per-language translation files, pre-compressed like the JS (audit UI-18). trans_src = Path(__file__).parent / "translations" / PANEL_TRANSLATIONS_DIRNAME - if await hass.async_add_executor_job(trans_src.is_dir): + if await hass.async_add_executor_job(_prepare_translations, trans_src): await _async_register_path(hass, PANEL_TRANSLATIONS_URL, str(trans_src)) # Brand icon (panel header). Track registration so ws_get_constants diff --git a/custom_components/ha_washdata/intents.py b/custom_components/ha_washdata/intents.py index a4bed59c..e4afc2f2 100644 --- a/custom_components/ha_washdata/intents.py +++ b/custom_components/ha_washdata/intents.py @@ -29,7 +29,7 @@ The single intent registered here is :data:`INTENT_STATUS` Wiring trigger sentences ------------------------ Registering the :class:`intent.IntentHandler` (via :func:`async_setup_intents`) -makes the intent handleable — it can be fired immediately from automations, the +makes the intent handleable: it can be fired immediately from automations, the ``intent_script`` integration, developer tools, or the Assist pipeline **once a sentence maps text to it**. Home Assistant has no public runtime API for a *custom* integration to inject sentences into the built-in conversation agent, @@ -60,7 +60,12 @@ declare the same intent via the ``intent_script`` integration. This module has no import-time side effects: it only defines constants, helpers and the handler class. Registration happens when :func:`async_setup_intents` is -called from ``async_setup_entry`` (guarded to run once per HA instance). +called from ``async_setup_entry`` (guarded to run once per HA instance). It +needs neither the ``conversation`` nor the ``intent`` integration loaded: +``intent.async_register`` stores the handler in ``hass.data`` and every consumer +(the Assist agent, ``intent_script``, the LLM API) looks it up per request, so +``conversation`` is only an after-dependency in the manifest (item 487). A hard +dependency made a broken or absent Assist stack block the whole integration. """ from __future__ import annotations @@ -85,7 +90,6 @@ from .const import ( STATE_ENDING, STATE_FINISHED, STATE_PAUSED, - STATE_RINSE, STATE_RUNNING, STATE_STARTING, STATE_USER_PAUSED, @@ -104,7 +108,6 @@ _ACTIVE_STATES = frozenset( STATE_ENDING, STATE_PAUSED, STATE_USER_PAUSED, - STATE_RINSE, STATE_ANTI_WRINKLE, } ) @@ -136,7 +139,7 @@ def _minutes_from_seconds(seconds: Any) -> int | None: """Return whole minutes (>=1) from a seconds value, or None when unusable.""" try: value = float(seconds) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): return None if value <= 0: return None @@ -149,7 +152,7 @@ def _minutes_since(end: Any, now: datetime) -> int | None: return None try: delta = (now - end).total_seconds() - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): return None if delta < 0: return 0 @@ -306,7 +309,7 @@ async def _localized_templates( continue try: loaded = await hass.async_add_executor_job(_load_intent_file, lg) - except (AttributeError, TypeError): + except (AttributeError, TypeError, OverflowError): # Only a hass without a usable executor (the minimal test stand-in) # reads on the loop. A broad except here would also catch a real # executor failure - e.g. "cannot schedule new futures after diff --git a/custom_components/ha_washdata/learning.py b/custom_components/ha_washdata/learning.py index 291916c9..332e4bb4 100644 --- a/custom_components/ha_washdata/learning.py +++ b/custom_components/ha_washdata/learning.py @@ -20,8 +20,9 @@ from __future__ import annotations import asyncio import logging +from collections import deque from datetime import datetime -from collections.abc import Callable +from collections.abc import Callable, Coroutine from typing import Any, Optional, TYPE_CHECKING import numpy as np @@ -39,10 +40,12 @@ from .const import ( DEFAULT_LEARNING_CONFIDENCE, MIN_SUGGESTION_COOLDOWN_CYCLES, MIN_SUGGESTION_REL_DELTA, - ML_QUALITY_SUSPICIOUS_THRESHOLD, + TerminationReason, ) from .suggestion_engine import SuggestionEngine from .log_utils import DeviceLoggerAdapter +from .profile_store import _AUTO_LABEL_SOURCES +from .detector_config import effective_option_values if TYPE_CHECKING: from .profile_store import ProfileStore @@ -70,6 +73,14 @@ def _suggestion_min_abs_delta(key: str) -> float: return 0.05 +def _ended_on_its_own(cycle: dict[str, Any]) -> bool: + """Completed, and not cut short by the user (#458; cf. ``select_clean_cycles``).""" + return ( + cycle.get("status") == "completed" + and cycle.get("termination_reason") != TerminationReason.USER + ) + + class StatisticalModel: """Helper to track running stats for a metric.""" @@ -125,23 +136,45 @@ class LearningManager: profile_store: "ProfileStore", device_type: str | None = None, device_name: str = "", + spawn: Callable[[Coroutine[Any, Any, Any]], Any] | None = None, ) -> None: - """Initialize the learning manager.""" + """Initialize the learning manager. + + ``spawn`` starts every background task this class creates. The manager + passes its ``_spawn_tracked`` (audit MANAGER-13): these tasks save the + ProfileStore, and an untracked one survives an entry reload and can write + the OLD store over the new one under the same Store key. Without it (unit + tests) a plain ``hass.async_create_task`` is used. + """ self._logger = DeviceLoggerAdapter(_LOGGER, device_name) self.hass = hass + self._spawn_fn = spawn self.entry_id = entry_id self.profile_store = profile_store self.device_type = device_type self.suggestion_engine = SuggestionEngine( - hass, entry_id, profile_store, device_type + hass, entry_id, profile_store, device_type, spawn=spawn ) # Operational Stats self._sample_interval_model = StatisticalModel(max_samples=200) + # Update intervals seen during the CURRENT cycle, held back until it ends + # (#458). A cycle that never ends on its own - force-stopped after hours of + # a plug reporting standby at its idle cadence - would otherwise fill the + # whole 200-sample window with that idle cadence, and the watchdog and + # no-update timeouts would be re-suggested from it every five minutes. + # Bounded like the model itself: only its last 200 survive a commit anyway. + self._pending_intervals: deque[tuple[float, datetime]] = deque(maxlen=200) self._last_suggestion_update: datetime | None = None self._last_batch_simulation_count: int = 0 # track when to re-run batch self._last_suggestions_labeled_count: int = 0 # gate model/detection passes + def _spawn(self, coro: Coroutine[Any, Any, Any]) -> Any: + """Start a background task through the manager's tracked spawner.""" + if self._spawn_fn is not None: + return self._spawn_fn(coro) + return self.hass.async_create_task(coro) + def _apply_suggestions_and_notify(self, suggestions: dict[str, Any]) -> None: """Apply suggestions that pass quality gates.""" if not suggestions: @@ -154,12 +187,18 @@ class LearningManager: current_options = {**entry.data, **entry.options} # Cooldown: how many cycles have elapsed since the user last applied suggestions? - past_cycles = self.profile_store.get_past_cycles() + # Counted on the odometer, not len(past_cycles): at the retention cap the + # list length stops growing, so one Apply-all silenced every non-corrective + # suggestion forever (audit SUGGEST-05). + cycles_now = self.profile_store.get_lifetime_cycle_count() last_apply_count = self.profile_store.get_suggestion_apply_cycle_count() cooldown_active = ( last_apply_count > 0 - and (len(past_cycles) - last_apply_count) < MIN_SUGGESTION_COOLDOWN_CYCLES + and (cycles_now - last_apply_count) < MIN_SUGGESTION_COOLDOWN_CYCLES ) + # Compare against what each key RUNS with when it is unset (audit + # SUGGEST-10), not against None, which skipped every gate below. + effective = effective_option_values(current_options, self.device_type) # Locked keys (#343): the user has told the auto-tuner to stop proposing # these (e.g. thresholds that break an anti-crease-tuned device). Drop them @@ -180,6 +219,8 @@ class LearningManager: continue if isinstance(data, dict) and "value" in data: current_val = current_options.get(key) + if current_val is None: + current_val = effective.get(key) suggested_val = data["value"] if current_val is not None and suggested_val is not None: try: @@ -204,16 +245,16 @@ class LearningManager: # After the user applies suggestions, wait for a few more # cycles before surfacing new ones (avoids immediately # re-suggesting a slightly-different value on the next cycle). - if cooldown_active: + if cooldown_active and not data.get("corrective"): continue - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): pass filtered_suggestions[key] = data if not filtered_suggestions: if any_deleted: - self.hass.async_create_task(self.profile_store.async_save()) + self._spawn(self.profile_store.async_save()) return self.suggestion_engine.apply_suggestions(filtered_suggestions) @@ -226,7 +267,8 @@ class LearningManager: delta = (now - last_reading_time).total_seconds() # Ignore ultra-small jitter (<0.1s) and massive gaps (>1800s - likely downtime) if 0.1 < delta < 1800: - self._sample_interval_model.add_sample(delta, now) + # Held until the cycle's outcome is known (#458): see close_cycle_cadence. + self._pending_intervals.append((delta, now)) # Periodically update suggestions based on operational stats if ( @@ -235,6 +277,39 @@ class LearningManager: ): self._update_operational_suggestions(now) + def discard_cycle_cadence(self) -> None: + """Drop held intervals without committing them (a new start from idle).""" + self._pending_intervals.clear() + + def close_cycle_cadence(self, cycle_data: dict[str, Any] | None) -> bool: + """Commit or drop the update intervals held for the cycle that just ended. + + Committed only when the cycle ended on its own (``status == "completed"`` + and not user-stopped): the same line ``select_clean_cycles`` draws, and for + the same reason (#458). A force-stopped or user-stopped cycle's intervals + describe however long the plug sat reporting standby before something gave + up, not how the appliance reports while it works. Called for EVERY cycle + end, persisted or not, so one cycle's intervals can never leak into the + next. Returns whether anything was committed. + """ + pending = list(self._pending_intervals) + self._pending_intervals.clear() + if not pending or not isinstance(cycle_data, dict): + return False + if cycle_data.get("status") != "completed": + return False + if cycle_data.get("termination_reason") in ( + TerminationReason.USER, + TerminationReason.FORCE_STOPPED, + ): + return False + for delta, ts in pending: + self._sample_interval_model.add_sample(delta, ts) + # The periodic refresh only runs while a cycle is active, so surface what + # this cycle taught now rather than at the start of the next one. + self._update_operational_suggestions(pending[-1][1]) + return True + def process_cycle_end( self, cycle_data: dict[str, Any], @@ -242,8 +317,14 @@ class LearningManager: confidence: float = 0.0, predicted_duration: float | None = None, match_result: Any | None = None, + label_allowed: bool = True, ) -> None: """Analyze completed cycle for learning. + + ``label_allowed`` is the manager's cycle-end label gate verdict (margin, + ambiguity and margin owner). When it refused, this pass must not label the + cycle anyway on confidence alone (audit MANAGER-02 / MATCH-DECIDE-01); it + asks the user instead. Args: cycle_data: Completed cycle data @@ -252,33 +333,35 @@ class LearningManager: predicted_duration: Expected duration in seconds match_result: MatchResult from profile_store.async_match_profile() (optional) """ - # 1. Trigger single-cycle simulation — only for cleanly-completed, labeled, - # non-noise cycles. Skipping force_stopped/unlabeled/noise avoids deriving - # start/stop thresholds from mis-detected or truncated cycles. - _profile = detected_profile or cycle_data.get("profile_name") - _is_clean = ( - cycle_data.get("power_data") - and _profile - and _profile != "noise" - and cycle_data.get("status") == "completed" + # (No per-cycle stop/start simulation: re-deriving both from the last cycle + # alone made them a random walk - audit SUGGEST-06. The batch pass below + # derives them across cycles.) + + # 1b. Standby above the stop threshold (#458). Every cycle end, whatever its + # status: a force-stopped cycle is the evidence this pass exists for. + self._dispatch_scan_and_apply( + self.suggestion_engine.for_job().generate_standby_floor_suggestions, + "Standby floor", ) - if _is_clean: - self.hass.async_create_task(self._async_run_simulation(cycle_data)) # 2. Check if we should request feedback self._maybe_request_feedback( - cycle_data, detected_profile, confidence, predicted_duration, match_result + cycle_data, detected_profile, confidence, predicted_duration, match_result, + label_allowed=label_allowed, ) # 3+3b. Heavy per-profile suggestion passes — only run when the labeled # cycle count has grown since the last update (skips passes for unlabeled / # noise / duplicate ends with no new data). + # New EVIDENCE, not new rows (#458): a force-stopped or user-stopped cycle + # is dropped by `select_clean_cycles` inside every pass this gates, so + # counting it re-ran them on unchanged data. labeled_count = sum( 1 for c in self.profile_store.get_past_cycles() if isinstance(c, dict) and c.get("profile_name") and c.get("profile_name") != "noise" - and c.get("status") in ("completed", "force_stopped") + and _ended_on_its_own(c) ) if labeled_count > self._last_suggestions_labeled_count: self._last_suggestions_labeled_count = labeled_count @@ -301,7 +384,10 @@ class LearningManager: and c.get("power_data") and c.get("status") in ("completed", "force_stopped") ] - current_count = len(labeled_cycles) + # The list still carries force-stopped cycles - the min_off_gap merge + # ceiling needs the user's real turnaround - but only cycles that ended + # on their own are new evidence for the re-run cadence (#458). + current_count = sum(1 for c in labeled_cycles if _ended_on_its_own(c)) if current_count < _BATCH_MIN: return @@ -309,7 +395,7 @@ class LearningManager: return self._last_batch_simulation_count = current_count - self.hass.async_create_task(self._async_run_batch_simulation(labeled_cycles)) + self._spawn(self._async_run_batch_simulation(labeled_cycles)) async def _async_run_batch_simulation(self, cycles: list[dict[str, Any]]) -> None: """Run multi-cycle batch simulation asynchronously.""" @@ -328,21 +414,6 @@ class LearningManager: except Exception as e: # pylint: disable=broad-exception-caught self._logger.error("Batch simulation failed: %s", e) - async def _async_run_simulation(self, cycle_data: dict[str, Any]) -> None: - """Run simulation asynchronously.""" - try: - # Simulation runner derives optimal thresholds - # Offload to executor since simulation can be heavy (CPU bound) - engine = self.suggestion_engine.for_job() - new_suggestions = await self.hass.async_add_executor_job( - engine.run_simulation, cycle_data - ) - if new_suggestions: - self._apply_suggestions_and_notify(new_suggestions) - self._logger.debug("Post-cycle simulation completed with suggestions: %s", new_suggestions.keys()) - except Exception as e: - self._logger.error("Background simulation failed: %s", e) - def _update_operational_suggestions(self, now: datetime) -> None: """Generate suggestions for operational parameters (intervals, timeouts). @@ -407,7 +478,7 @@ class LearningManager: if suggestions: self._apply_suggestions_and_notify(suggestions) return - self.hass.async_create_task(self._async_scan_and_apply(generate, label)) + self._spawn(self._async_scan_and_apply(generate, label)) async def _async_scan_and_apply( self, generate: Callable[[], dict[str, Any]], label: str @@ -426,7 +497,7 @@ class LearningManager: Offloaded to an executor because it scans power traces across up to 200 cycles for the clean-cycle health checks. """ - self.hass.async_create_task(self._async_run_detection_suggestions()) + self._spawn(self._async_run_detection_suggestions()) async def _async_run_detection_suggestions(self) -> None: """Run the detection-suggestion pass off the event loop.""" @@ -487,6 +558,7 @@ class LearningManager: confidence: float, predicted_duration: float | None, match_result: Any | None = None, + label_allowed: bool = True, ) -> None: """Check if feedback should be requested for this completed cycle.""" if ( @@ -503,6 +575,15 @@ class LearningManager: self._logger.warning("Cycle data missing ID, cannot request feedback") return + # The user already chose this cycle's programme. Asking them to confirm the + # matcher's guess on top of it (warm-up, or a gate-refused match) is the + # request the v16 cleanup drops (_dismiss_unneeded_feedback); never raise it. + if cycle_data.get("label_source") == "manual": + self._logger.debug( + "Cycle %s was labelled by hand; no confirmation needed", cycle_id + ) + return + # Get Configured Thresholds entry = self.hass.config_entries.async_get_entry(self.entry_id) if not entry: @@ -527,7 +608,9 @@ class LearningManager: # remains the real match score that gets displayed and persisted, so warmup # clamping never fabricates the value shown to the user. route_conf = confidence - if confidence >= auto_label_conf: + # Warm-up applies wherever the cycle would otherwise go unasked: the + # auto-label band, and (since 0.5.8) a cycle the cycle-end gate labelled. + if confidence >= auto_label_conf or (label_allowed and confidence >= learning_conf): _wm_count = self.profile_store.get_profile_labeled_count(detected_profile) # Imported reference profiles are trusted downloaded templates: the user # expects to match immediately, so they skip the local warm-up gate. @@ -555,46 +638,48 @@ class LearningManager: if learning_conf + 0.001 < auto_label_conf: route_conf = max(route_conf, learning_conf + 0.001) - # Auto-label if very high confidence — but skip auto-labeling when the ML - # quality model flagged this cycle as suspicious (P(problem) >= threshold), - # even if the matcher was confident. Downgrade to a feedback request so - # the user can verify the match; this catches confident but wrong labels. - ml_quality = cycle_data.get("ml_quality_score") - # Use float() so numpy scalars (float32/float64) returned by resolve_scorer - # are accepted — isinstance(numpy_float, float) is False in NumPy ≥ 2.0. - # Wrap in try/except so non-numeric sentinel values are silently ignored. - try: - ml_suspicious = ( - ml_quality is not None - and float(ml_quality) >= ML_QUALITY_SUSPICIOUS_THRESHOLD - ) - except (TypeError, ValueError): - ml_suspicious = False - # Also downgrade when the cycle's power trace is mostly outside the - # profile envelope band (low conformance = the shape matched but the - # actual power levels are inconsistent with the profile). + # Auto-label if very high confidence - but not when the cycle's power trace + # is mostly outside the profile envelope band (low conformance = the shape + # matched but the actual power levels are inconsistent with the profile). + # (The ML quality gate that also downgraded here, C3, was removed in 0.5.8: + # it fired on 0 of the auto-label-eligible real cycles, audit ML-06. A + # legacy `ml_quality_score` on an older cycle is ignored.) _conformance = cycle_data.get("envelope_conformance") try: envelope_suspicious = ( _conformance is not None and float(_conformance) < 0.40 ) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): envelope_suspicious = False + # The cycle-end pass already labelled this cycle with a DIFFERENT programme + # (the post-cycle match on the complete trace): re-labelling here would + # overwrite that label with this pass's guess. (A hand-picked label returned + # at the top.) + _existing_label = cycle_data.get("profile_name") + if route_conf >= auto_label_conf and _existing_label and _existing_label != detected_profile: + self._logger.debug( + "Cycle %s already labelled '%s' at cycle end; not auto-labelling it", + cycle_id, _existing_label, + ) + return if route_conf >= auto_label_conf: - if ml_suspicious or envelope_suspicious: - if ml_suspicious: - self._logger.info( - "ML quality model flagged cycle %s as suspicious (score=%.3f >= %.2f); " - "downgrading auto-label to feedback request.", - cycle_id, ml_quality, ML_QUALITY_SUSPICIOUS_THRESHOLD, - ) - if envelope_suspicious: - self._logger.info( - "Envelope conformance for cycle %s is low (%.2f < 0.40); " - "downgrading auto-label to feedback request.", - cycle_id, _conformance, - ) + if not label_allowed: + self._logger.info( + "Cycle %s: the label gate refused '%s' (too close to the runner-up " + "or flagged ambiguous); requesting confirmation instead of " + "auto-labelling it.", + cycle_id, detected_profile, + ) + # Fall through to the feedback-request path below. + elif envelope_suspicious: + # Not a review request on its own (register item 433): a cycle + # the cycle-end gate already labelled returns below unasked. + self._logger.info( + "Envelope conformance for cycle %s is low (%.2f < 0.40); " + "not auto-labelling it here.", + cycle_id, _conformance, + ) # Fall through to feedback-request path below. else: labeled = self.auto_label_high_confidence( @@ -605,12 +690,34 @@ class LearningManager: ) if labeled: # Rebuild envelope first, then persist (issue #131) - self.hass.async_create_task( + self._spawn( self._async_rebuild_and_save_profile(detected_profile) ) self._logger.debug("Auto-labeled high-confidence cycle %s", cycle_id) return + # A cycle the cycle-end gate already labelled with this programme (a clear + # margin over the runner-up, not ambiguous, above the learning floor) needs no + # confirmation. Measured leave-one-out over 604 cycle ends, those labels are + # 91.5% right; asking about every 0.6-0.9 match instead put 81% of all + # cycles in the review queue, most of them already labelled correctly. What + # predicts a wrong label is a small margin, not a modest confidence: at + # 0.7-0.9 a clear margin is 93-96% right and a refused gate 33-54% + # (register item 433). Warm-up still asks. Low envelope conformance does + # NOT: labelled cycles under 0.40 are still 85.6% right, so asking about them + # costs 7 questions per wrong label (and on a device with loose envelopes it + # re-queued nearly every cycle). + if ( + label_allowed + and not warmup_request + and cycle_data.get("profile_name") == detected_profile + ): + self._logger.debug( + "Cycle %s labelled '%s' at cycle end with a clear margin; no confirmation needed", + cycle_id, detected_profile, + ) + return + # Skip low-confidence matches below learning threshold — but a warmup cycle # always requests confirmation, even if the thresholds are misconfigured. if route_conf < learning_conf and not warmup_request: @@ -637,7 +744,7 @@ class LearningManager: # Persist pending feedback request so it survives restart. # The pending review is surfaced in the panel's Cycles review queue; # WashData intentionally does not raise a persistent notification here. - self.hass.async_create_task(self.profile_store.async_save()) + self._spawn(self.profile_store.async_save()) def request_cycle_verification( self, @@ -669,7 +776,7 @@ class LearningManager: "metrics": cand.get("metrics", {}), "profile_duration": float(cand.get("profile_duration", 0.0)), }) - except (TypeError, ValueError, KeyError, AttributeError): + except (TypeError, ValueError, KeyError, AttributeError, OverflowError): continue feedback_req: dict[str, Any] = { @@ -740,7 +847,7 @@ class LearningManager: if corrected_duration is not None: try: duration_sec = float(corrected_duration) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): self._logger.warning( "Invalid corrected_duration %r for cycle %s, ignoring", corrected_duration, @@ -767,7 +874,11 @@ class LearningManager: elif user_confirmed: profile_name = pending.get("detected_profile") if isinstance(profile_name, str) and profile_name: - self._auto_label_cycle(cycle_id, profile_name, duration_sec) + left = self._auto_label_cycle( + cycle_id, profile_name, duration_sec, source="manual" + ) + if left: + profiles_to_rebuild.add(left) if duration_sec is not None: cycles = self.profile_store.get_past_cycles() confirmed_cycle = next((c for c in cycles if c.get("id") == cycle_id), None) @@ -781,10 +892,12 @@ class LearningManager: detected_profile_name = pending.get("detected_profile") if isinstance(target_profile, str) and target_profile: - self._apply_correction_learning( + left = self._apply_correction_learning( cycle_id, target_profile, duration_sec ) profiles_to_rebuild.add(target_profile) + if left: + profiles_to_rebuild.add(left) if ( isinstance(detected_profile_name, str) and detected_profile_name @@ -946,34 +1059,74 @@ class LearningManager: ) return True - def _auto_label_cycle(self, cycle_id: str, profile_name: str, manual_duration: float | None = None) -> None: + def _auto_label_cycle( + self, + cycle_id: str, + profile_name: str, + manual_duration: float | None = None, + *, + source: str = "auto_match", + ) -> str | None: + """Write a label and its provenance; returns the profile the cycle left. + + ``source`` is ``"manual"`` when the user answered a review request (confirm + or correct) and ``"auto_match"`` when the matcher's guess is recorded. + These paths used to set only ``profile_name``, so a user's correction still + read as a matcher guess and the panel's Auto-label reverted it - storing the + user's answer as ``original_auto_label`` (audit MANAGER-01). + + The returned name (None when the label did not move) is the profile whose + envelope lost a member: it is the cycle's real old label, which need not + be the profile the review request detected (register item 493). + """ cycles = self.profile_store.get_past_cycles() cycle = next((c for c in cycles if c.get("id") == cycle_id), None) + left: str | None = None if cycle: + old = cycle.get("profile_name") + if ( + source == "manual" + and old + and old != profile_name + and not cycle.get("original_auto_label") + and cycle.get("label_source") in _AUTO_LABEL_SOURCES + ): + cycle["original_auto_label"] = old cycle["profile_name"] = profile_name cycle["auto_labeled"] = True + cycle["label_source"] = source if manual_duration: cycle["manual_duration"] = manual_duration + if old and old != profile_name: + # The profile the cycle left must not keep it as its sample. + self.profile_store.heal_profile_sample(old) + if isinstance(old, str): + left = old + return left def _apply_correction_learning( self, cycle_id: str, corrected_profile: str, corrected_duration: Optional[float] = None, - ) -> None: + ) -> str | None: """Apply user correction to a cycle (fix for issue #131). Note: We do not update avg_duration here with EMA. Instead, the envelope rebuild in async_submit_cycle_feedback() will recalculate all statistics - (min/max/avg) from labeled cycles, ensuring accuracy. + (min/max/avg) from labeled cycles, ensuring accuracy. Returns the profile + the cycle left (see :meth:`_auto_label_cycle`). """ - self._auto_label_cycle(cycle_id, corrected_profile, corrected_duration) + left = self._auto_label_cycle( + cycle_id, corrected_profile, corrected_duration, source="manual" + ) if corrected_duration is not None: cycles = self.profile_store.get_past_cycles() cycle = next((c for c in cycles if c.get("id") == cycle_id), None) if cycle: cycle["duration"] = corrected_duration # Profile stats will be recalculated when envelope is rebuilt + return left async def _async_rebuild_profile_envelope(self, profile_name: str) -> None: """Async helper to rebuild a profile's envelope (issue #131 fix). diff --git a/custom_components/ha_washdata/manager.py b/custom_components/ha_washdata/manager.py index e2cfb391..7d9835a6 100644 --- a/custom_components/ha_washdata/manager.py +++ b/custom_components/ha_washdata/manager.py @@ -22,11 +22,13 @@ from __future__ import annotations import logging import hashlib -import inspect +import json import math import re +import traceback import uuid import asyncio +import functools from asyncio import Task from collections.abc import Callable, Coroutine from datetime import datetime, timedelta @@ -36,6 +38,7 @@ import numpy as np if TYPE_CHECKING: from .store import StoreBridge +from homeassistant.components import persistent_notification from homeassistant.config_entries import ConfigEntry from homeassistant.core import Context, Event, HomeAssistant, State, callback from homeassistant.helpers.event import ( @@ -46,7 +49,7 @@ from homeassistant.helpers.event import ( ) from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.exceptions import HomeAssistantError -from homeassistant.const import STATE_UNAVAILABLE, STATE_HOME +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, STATE_UNAVAILABLE, STATE_HOME from homeassistant.util import dt as dt_util import voluptuous as vol @@ -54,8 +57,13 @@ import homeassistant.helpers.event as evt from homeassistant.helpers import config_validation as cv from homeassistant.helpers import script as script_helper from homeassistant.helpers import translation +from homeassistant.helpers.start import async_at_started +from homeassistant.helpers.storage import Store +from .const import NOTIFY_QUEUE_STORE_SUFFIX, STORAGE_KEY from .const import ( + resolve_off_delay_default, + CADENCE_RESET_FROM_STATES, DOMAIN, CONF_POWER_SENSOR, CONF_PROFILE_EVIDENCE_SOURCES, @@ -73,61 +81,25 @@ from .const import ( CONF_NOTIFY_EVENTS, CONF_NO_UPDATE_ACTIVE_TIMEOUT, CONF_LOW_POWER_NO_UPDATE_TIMEOUT, # Import new constant - CONF_SMOOTHING_WINDOW, - CONF_PROFILE_DURATION_TOLERANCE, - CONF_INTERRUPTED_MIN_SECONDS, CONF_PROGRESS_RESET_DELAY, CONF_LEARNING_CONFIDENCE, - CONF_DURATION_TOLERANCE, CONF_AUTO_LABEL_CONFIDENCE, CONF_AUTO_MAINTENANCE, CONF_MAINTENANCE_REMINDER_CYCLES, - DEFAULT_MAINTENANCE_REMINDER_CYCLES, CONF_PROFILE_MATCH_INTERVAL, CONF_PROFILE_MATCH_MIN_DURATION_RATIO, CONF_PROFILE_MATCH_MAX_DURATION_RATIO, - CONF_MAX_PAST_CYCLES, - CONF_MAX_FULL_TRACES_PER_PROFILE, - CONF_MAX_FULL_TRACES_UNLABELED, CONF_WATCHDOG_INTERVAL, CONF_AUTO_TUNE_NOISE_EVENTS_THRESHOLD, - CONF_COMPLETION_MIN_SECONDS, CONF_NOTIFY_BEFORE_END_MINUTES, - CONF_PROFILE_MATCH_THRESHOLD, CONF_PROFILE_UNMATCH_THRESHOLD, CONF_DEVICE_TYPE, - CONF_START_DURATION_THRESHOLD, - CONF_END_REPEAT_COUNT, - CONF_MIN_OFF_GAP, - CONF_START_ENERGY_THRESHOLD, - CONF_END_ENERGY_THRESHOLD, - CONF_START_THRESHOLD_W, - CONF_STOP_THRESHOLD_W, - CONF_POWER_OFF_THRESHOLD_W, - CONF_POWER_OFF_DELAY, CONF_SAMPLING_INTERVAL, CONF_SAVE_DEBUG_TRACES, CONF_DTW_BANDWIDTH, CONF_EXTERNAL_END_TRIGGER_ENABLED, CONF_EXTERNAL_END_TRIGGER, CONF_EXTERNAL_END_TRIGGER_INVERTED, - CONF_ANTI_WRINKLE_ENABLED, - CONF_ANTI_WRINKLE_MAX_POWER, - CONF_ANTI_WRINKLE_MAX_DURATION, - CONF_ANTI_WRINKLE_EXIT_POWER, - CONF_ANTI_WRINKLE_IDLE_TIMEOUT, - 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, - CONF_DELAY_CONFIRM_SECONDS, - CONF_DELAY_TIMEOUT_HOURS, CONF_PUMP_STUCK_DURATION, DEFAULT_PUMP_STUCK_DURATION, EVENT_PUMP_STUCK, @@ -140,40 +112,22 @@ from .const import ( NOTIFY_EVENT_TIMER, EVENT_CYCLE_STARTED, EVENT_CYCLE_ENDED, + EVENT_CYCLE_STALLED, + CYCLE_ANOMALY_STALLED, DEFAULT_MIN_POWER, DEFAULT_OFF_DELAY, DEFAULT_NO_UPDATE_ACTIVE_TIMEOUT, DEFAULT_NO_UPDATE_ACTIVE_TIMEOUT_BY_DEVICE, - DEFAULT_SMOOTHING_WINDOW, - DEFAULT_PROFILE_DURATION_TOLERANCE, - DEFAULT_INTERRUPTED_MIN_SECONDS, - DEFAULT_COMPLETION_MIN_SECONDS, DEFAULT_NOTIFY_BEFORE_END_MINUTES, - DEFAULT_PROFILE_MATCH_THRESHOLD, DEFAULT_PROFILE_UNMATCH_THRESHOLD, - DEFAULT_SAMPLING_INTERVAL, DEFAULT_PROGRESS_RESET_DELAY, - DEFAULT_POWER_OFF_THRESHOLD_W, DEFAULT_PROFILE_EVIDENCE_SOURCES, - DEFAULT_POWER_OFF_DELAY, DEFAULT_LEARNING_CONFIDENCE, - DEFAULT_DURATION_TOLERANCE, DEFAULT_AUTO_LABEL_CONFIDENCE, DEFAULT_AUTO_MAINTENANCE, DEFAULT_PROFILE_MATCH_INTERVAL, DEFAULT_PROFILE_MATCH_MIN_DURATION_RATIO, - DEFAULT_PROFILE_MATCH_MIN_DURATION_RATIO_BY_DEVICE, - DEFAULT_ANTI_WRINKLE_ENABLED, - DEFAULT_ANTI_WRINKLE_MAX_POWER, - DEFAULT_ANTI_WRINKLE_MAX_DURATION, - DEFAULT_ANTI_WRINKLE_EXIT_POWER, - DEFAULT_ANTI_WRINKLE_IDLE_TIMEOUT, - DEFAULT_DELAY_START_DETECT_ENABLED, - DEFAULT_DELAY_CONFIRM_SECONDS, - DEFAULT_DELAY_TIMEOUT_HOURS, DEFAULT_PROFILE_MATCH_MAX_DURATION_RATIO, - DEFAULT_MAX_PAST_CYCLES, - DEFAULT_MAX_FULL_TRACES_PER_PROFILE, CONF_NOTIFY_TITLE, CONF_NOTIFY_ICON, CONF_NOTIFY_ICON_COLOR, @@ -244,38 +198,21 @@ from .const import ( DEFAULT_NOTIFY_CHANNEL, DEFAULT_NOTIFY_FINISH_CHANNEL, - DEFAULT_MAX_FULL_TRACES_UNLABELED, DEFAULT_DTW_BANDWIDTH, - DEFAULT_WATCHDOG_INTERVAL, + WATCHDOG_LATE_TICK_FACTOR, resolve_sampling_interval_default, resolve_watchdog_interval_default, - resolve_start_duration_default, - resolve_smart_termination_duration_ratio_default, CONF_MATCH_PERSISTENCE, DEFAULT_MATCH_PERSISTENCE, - DEFAULT_MATCH_REVERT_RATIO, - MATCH_DECISIVE_MARGIN, MATCH_LABEL_MIN_MARGIN, + ENABLE_ML_END_GUARD, DEFAULT_AUTO_TUNE_NOISE_EVENTS_THRESHOLD, DEFAULT_DEVICE_TYPE, - DEFAULT_START_DURATION_THRESHOLD, - DEFAULT_END_REPEAT_COUNT, - resolve_min_off_gap_default, 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, - DEFAULT_END_ENERGY_THRESHOLD, - DEVICE_COMPLETION_THRESHOLDS, CYCLE_UNDERRUN_ANOMALY_RATIO, ENERGY_ANOMALY_Z_THRESHOLD, - TERMINAL_DROP_MIN_CLEAN_CYCLES, - TERMINAL_DROP_MIN_QUIET_SPAN_S, - TERMINAL_DROP_EARLINESS_RATIO, - TERMINAL_DROP_MIN_PEAK_RATIO, - TERMINAL_DROP_PEAK_FAMILIAR_TOL, - ML_MATCH_COMMIT_THRESHOLD, STATE_RUNNING, STATE_OFF, STATE_STARTING, @@ -287,19 +224,32 @@ from .const import ( STATE_IDLE, STATE_UNKNOWN, ) +from .detector_config import ( + apply_detector_config, + build_detector_config, + terminal_drop_baseline_for, + terminal_drop_enabled, + terminal_drop_fires, + terminal_drop_may_fire, +) from .cycle_detector import ( + MatchContext, CycleDetector, - CycleDetectorConfig, + STANDBY_LEVEL_RECENT_CYCLES, + TERMINAL_PROBE_RETURNS, + learned_standby_level_w, + standby_near_stop_ceiling, terminal_high_for_guards, ) from .learning import LearningManager from .profile_store import ( + MatchResult, ProfileStore, decompress_power_data, - is_terminal_drop, - terminal_drop_baseline, ) from .signal_processing import ( + median_fast, + percentile_linear, integrate_wh, energy_gap_threshold_s, compact_price_timeline, @@ -314,11 +264,12 @@ from .log_utils import DeviceLoggerAdapter # HA restart, and never carries over between different configured entities. _UNLOAD_CONFIRM_ANCHOR_KEY = f"{DOMAIN}_unload_confirm_anchors" from .options_utils import option_float, option_int -from .time_utils import power_data_to_offsets +from .time_utils import power_data_to_offsets, utc_now from . import analysis from . import progress as progress_mod from . import notification_rules as notif_rules -from .phase_segmenter import phase_matching_enabled +from . import match_rules +from .maintenance import effective_reminders from .frontend import PANEL_URL_PATH _LOGGER = logging.getLogger(__name__) @@ -341,6 +292,19 @@ _QUIET_HOURS_EVENT_TYPES = frozenset( {NOTIFY_EVENT_FINISH, NOTIFY_EVENT_CLEAN, "pre_complete"} ) +# Held notifications persisted across a restart (audit MANAGER-16). Not persisted: +# a live update (the next tick replaces it), a cycle timer (its Resume action is +# wired for one session) and the unload nag (the Clean state it belongs to is not +# restored, and the door may have opened meanwhile). Start and pre-complete belong +# to the cycle under way, so they are restored only while one still is. +_NOTIFY_QUEUE_TRANSIENT_EVENTS = frozenset( + {NOTIFY_EVENT_LIVE, NOTIFY_EVENT_TIMER, NOTIFY_EVENT_CLEAN} +) +_NOTIFY_QUEUE_CYCLE_EVENTS = frozenset({NOTIFY_EVENT_START, "pre_complete"}) +# A saved queue older than this is dropped on restore instead of delivered: a +# "finished" from days ago is noise. Longer than any quiet window. +_NOTIFY_QUEUE_MAX_AGE_S = 24 * 3600 + # Detector states in which the power sensor must not be swapped out. Every state # with an in-flight cycle, plus ANTI_WRINKLE: its tumble pulses are still being @@ -410,6 +374,25 @@ def _finite_power(raw: Any) -> float | None: return power +def _snapshot_time(raw: Any) -> datetime | None: + """A timestamp from the active-cycle snapshot as aware UTC, else None. + + A naive value is a legacy local stamp, read in HA's zone like every other + snapshot field. Never raises: a junk value restores as "unknown". + """ + if not isinstance(raw, str) or not raw: + return None + try: + parsed = dt_util.parse_datetime(raw) + except (TypeError, ValueError, OverflowError): + return None + if parsed is None: + return None + if parsed.tzinfo is None: + parsed = parsed.replace(tzinfo=dt_util.now().tzinfo) + return dt_util.as_utc(parsed) + + def _coerce_price_timeline(raw: Any) -> list[tuple[float, float]]: """Coerce a persisted price timeline back into ``(ts, price)`` tuples (#426). @@ -425,7 +408,7 @@ def _coerce_price_timeline(raw: Any) -> list[tuple[float, float]]: continue try: result.append((float(entry[0]), float(entry[1]))) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): continue result.sort(key=lambda item: item[0]) return result @@ -444,6 +427,55 @@ def _sanitize_ranking(raw_list: list[dict[str, Any]], limit: int = 5) -> list[di }) return out + +def _apply_post_cycle_anomalies(cycle_data: dict[str, Any], store: Any) -> None: + """Stamp the post-cycle A1 underrun and A2 energy anomalies onto ``cycle_data``. + + Runs at cycle end after the runtime overrun anomaly is frozen onto the cycle. + ``store`` is the device's ProfileStore (only its median-duration and energy-stats + lookups are used, and only when the guards pass). Each rule is independent and + never raises: a failure leaves that rule's fields unset. Purely informational, + never a notification. + """ + # A1: Underrun check - computed post-cycle only, not a live signal. + # Only applied when no runtime anomaly was detected (underrun and overrun are mutually exclusive). + try: + if not cycle_data.get("anomaly") or cycle_data["anomaly"] == "none": + _uc_profile = cycle_data.get("profile_name") + _uc_dur = float(cycle_data.get("duration", 0)) + if _uc_profile and _uc_dur > 0: + _uc_median = store.get_profile_median_duration(_uc_profile) + if ( + isinstance(_uc_median, (int, float)) + and not isinstance(_uc_median, bool) + and _uc_median > 0 + and _uc_dur < _uc_median * CYCLE_UNDERRUN_ANOMALY_RATIO + ): + cycle_data["anomaly"] = "underrun" + cycle_data["underrun_ratio"] = round(_uc_dur / _uc_median, 3) + except Exception: # noqa: BLE001 + pass + + # A2: Energy spike/low anomaly - stored separately from duration anomaly. + try: + _ea_profile = cycle_data.get("profile_name") + _ea_energy = float(cycle_data.get("energy_wh", 0)) + if _ea_profile and _ea_energy > 0: + _ea_stats = store.get_profile_energy_stats(_ea_profile) + if ( + isinstance(_ea_stats, dict) + and isinstance(_ea_stats.get("std_wh"), (int, float)) + and _ea_stats["std_wh"] > 0 + ): + _ea_z = (_ea_energy - _ea_stats["avg_wh"]) / _ea_stats["std_wh"] + cycle_data["energy_z_score"] = round(_ea_z, 2) + if _ea_z > ENERGY_ANOMALY_Z_THRESHOLD: + cycle_data["energy_anomaly"] = "energy_spike" + elif _ea_z < -ENERGY_ANOMALY_Z_THRESHOLD: + cycle_data["energy_anomaly"] = "energy_low" + except Exception: # noqa: BLE001 + pass + # Notification-data keys that may only be forwarded to mobile_app_* notify targets. # Strict-schema platforms (e.g. Signal) reject unknown keys, so these are added per # service only when the target is a mobile app. Includes the iOS Live Activity @@ -472,44 +504,86 @@ def _pn_create( *, title: str | None = None, notification_id: str | None = None, -) -> None: - """Best-effort persistent notification creation. +) -> bool: + """Best-effort persistent notification creation; True when it was posted. - Deliberately goes through ``hass.components.persistent_notification`` rather than - a direct ``homeassistant.components.persistent_notification`` import: the test - suite stubs out the whole ``homeassistant`` module and mocks this dynamic - attribute, so a direct import would both fail under test and bypass those mocks. - Failures are logged at debug (not silently swallowed) so a stuck notification is - at least visible in the logs. + Calls ``homeassistant.components.persistent_notification`` directly. This used + to go through the ``components`` accessor on ``hass``, which Home Assistant + removed: ``getattr`` found nothing and the helper returned silently, so every + sidebar card (the fallback for users with no notify target, the auto-pause + timer card) was dropped while the caller logged it as delivered - and the 12 + test modules that mocked that accessor kept passing (audit PLATFORM-01). """ try: - components = getattr(cast(Any, hass), "components", None) - pn = getattr(cast(Any, components), "persistent_notification", None) - if pn is None: - return - result = pn.async_create(message, title=title, notification_id=notification_id) - if inspect.iscoroutine(result): - hass.async_create_task(result) + persistent_notification.async_create( + hass, message, title=title, notification_id=notification_id + ) + return True except Exception: # noqa: BLE001 - best-effort; surface the failure in logs - _LOGGER.debug("persistent_notification create failed (id=%s)", notification_id, exc_info=True) + _LOGGER.warning( + "persistent_notification create failed (id=%s)", notification_id, exc_info=True + ) + return False def _pn_dismiss(hass: HomeAssistant, notification_id: str) -> None: - """Best-effort persistent notification dismissal. - - Uses the ``hass.components`` accessor for the same test-mocking reason as - :func:`_pn_create`; failures are logged at debug rather than swallowed. - """ + """Best-effort persistent notification dismissal (see :func:`_pn_create`).""" try: - components = getattr(cast(Any, hass), "components", None) - pn = getattr(cast(Any, components), "persistent_notification", None) - if pn is None: - return - result = pn.async_dismiss(notification_id) - if inspect.iscoroutine(result): - hass.async_create_task(result) + persistent_notification.async_dismiss(hass, notification_id) except Exception: # noqa: BLE001 - best-effort; surface the failure in logs - _LOGGER.debug("persistent_notification dismiss failed (id=%s)", notification_id, exc_info=True) + _LOGGER.warning( + "persistent_notification dismiss failed (id=%s)", notification_id, exc_info=True + ) + + +def _read_switch_state(mgr: Any) -> match_rules.SwitchState: + """The manager's switching fields as the shared rules read them. + + Module-level, not a method, so a test binding only + ``_async_do_perform_matching`` onto a stub manager still runs the real path. + The two dicts are passed by reference, as the rules always mutated them. + """ + return match_rules.SwitchState( + current_program=mgr._current_program, + matched_duration=mgr._matched_profile_duration, + last_confidence=mgr._last_match_confidence, + last_member_confidence=mgr._last_member_confidence, + score_history=mgr._score_history, + persistence_counter=mgr._match_persistence_counter, + unmatch_counter=mgr._unmatch_persistence_counter, + current_candidate=mgr._current_match_candidate, + ) + + +def _write_switch_state( + mgr: Any, state: match_rules.SwitchState, log: list[match_rules.LogLine] +) -> None: + """Write back what the shared rules decided, then emit their log lines.""" + mgr._current_program = state.current_program + mgr._matched_profile_duration = state.matched_duration + mgr._last_match_confidence = state.last_confidence + mgr._last_member_confidence = state.last_member_confidence + mgr._score_history = state.score_history + mgr._match_persistence_counter = state.persistence_counter + mgr._unmatch_persistence_counter = state.unmatch_counter + mgr._current_match_candidate = state.current_candidate + _emit_rule_log(mgr._logger, log) + + +def _emit_rule_log(logger: Any, log: list[match_rules.LogLine]) -> None: + for level, msg, args in log: + logger.log(level, msg, *args) + + +def _option_then_data(config_entry: Any, key: str, default: Any) -> Any: + """``options[key]``, else ``data[key]``, else ``default``. + + A device added after its last schema migration keeps its structural keys + (min_power, off_delay) in ``entry.data`` only, and ``ws_set_options`` writes + ``{**options, **changes}`` - so options-only reads on reload reset them to the + defaults at the first unrelated settings save (register item 388a). + """ + return config_entry.options.get(key, config_entry.data.get(key, default)) class WashDataManager: @@ -535,10 +609,16 @@ class WashDataManager: self.power_sensor_entity_id = config_entry.options.get( CONF_POWER_SENSOR, config_entry.data.get(CONF_POWER_SENSOR) ) + # A sensor change saved while a cycle was under way, applied once the + # detector leaves _SENSOR_SWAP_BLOCKED_STATES (audit MANAGER-11). + self._pending_power_sensor: str | None = None self.device_type = config_entry.options.get( CONF_DEVICE_TYPE, config_entry.data.get(CONF_DEVICE_TYPE, DEFAULT_DEVICE_TYPE), ) + # The sensor platform's add callback, kept so an in-place device type change + # can add the pump-only sensor (sensor.async_reconcile_device_type_sensors). + self.sensor_add_entities: Any = None # Initialize attributes to satisfy pylint self._off_delay = float(DEFAULT_OFF_DELAY) @@ -572,6 +652,11 @@ class WashDataManager: # at the end of the window by a single async_call_later timer. self._quiet_pending_notifications: list[dict[str, Any]] = [] self._remove_quiet_hours_timer: Any | None = None + # Both queues outlive a restart through this file (audit MANAGER-16). + self._notify_queue_store: Store[dict[str, Any]] | None = None + self._notify_queue_on_disk = False + self._remove_ha_stop_listener: Callable[[], None] | None = None + self._remove_notify_queue_restore: Callable[[], None] | None = None self._remove_notify_people_listener = None self._live_notification_sent_count = 0 @@ -609,7 +694,7 @@ class WashDataManager: # Pause tracking (user-triggered) self._user_pause_start: datetime | None = None self._total_user_paused_seconds: float = 0.0 - self._is_user_paused: bool = False + self._user_paused_flag: bool = False self._pause_cuts_power: bool = bool( config_entry.options.get(CONF_PAUSE_CUTS_POWER, False) ) @@ -676,7 +761,6 @@ class WashDataManager: # persistent-notification fallback). The clean-laundry nag uses its own tag # since it fires up to an hour after finish and should not clobber the thread. self._lifecycle_tag = f"ha_washdata_{self.entry_id}_lifecycle" - self._lifecycle_pn_id = self._lifecycle_tag self._clean_tag = f"ha_washdata_{self.entry_id}_clean" # #446: the live progress updates need their OWN tag, because on iOS a Live # Activity is a separate UI surface from the notification and is ended only @@ -693,8 +777,10 @@ class WashDataManager: self._live_notification_tag = f"ha_washdata_{self.entry_id}_live" self._start_event_fired = False self._cycle_start_time: datetime | None = None - # Per-cycle UUID used to key ranking snapshots; prevents cross-contamination - # between cycles that happen to share the same second-resolution start_time. + # Per-cycle UUID: the identity token the live match and the cycle-end tail + # check so work for one cycle never lands on the next, even when both share + # a second-resolution start_time. (Named for the live_match ranking + # snapshots it first keyed; those were removed in 0.5.8.) self._ranking_snapshot_cycle_id: str = "" # State @@ -710,13 +796,20 @@ class WashDataManager: self._remove_power_off_timer: Any | None = None self._last_reading_time: datetime | None = None self._last_real_reading_time: datetime | None = None # Track last real sensor update + # Register item 266, restart hazard. The silence clock and sensor value a + # restored cycle's snapshot carried, consumed once by the setup read; and + # the report that read took, which the resync must not mistake for a + # missed one (it would hand the restart's own write back as fresh). + self._restored_sensor_clock: tuple[datetime, float | None] | None = None + self._setup_report_ts: datetime | None = None self._noise_events: list[datetime] = [] self._noise_max_powers: list[float] = [] self._last_match_result = None self._last_phase_estimate_time = None - self._sample_intervals: list[float] = [] - self._sample_interval_stats: dict[str, Any] = {} self._matching_task: Task[Any] | None = None + # True while a power reading is being handled: the handler refreshes the + # entities once when it returns, so what it calls does not (register item 456). + self._in_power_event = False self._cycle_end_task: Task[Any] | None = None self._banked_tail_repair_task: Task[Any] | None = None # Detached store-touching tasks (matching trigger, active-cycle clear, @@ -729,11 +822,11 @@ class WashDataManager: self._remove_state_expiry_timer = None # Components - match_threshold = config_entry.options.get( - CONF_PROFILE_MATCH_THRESHOLD, DEFAULT_PROFILE_MATCH_THRESHOLD - ) - unmatch_threshold = config_entry.options.get( - CONF_PROFILE_UNMATCH_THRESHOLD, DEFAULT_PROFILE_UNMATCH_THRESHOLD + # Coerced (audit F7 finding): a non-numeric stored value raised inside every + # match tick (`float()` in the initial commit, `<` in the unmatch check). + unmatch_threshold = option_float( + config_entry.options.get(CONF_PROFILE_UNMATCH_THRESHOLD), + DEFAULT_PROFILE_UNMATCH_THRESHOLD, ) self._unmatch_threshold = unmatch_threshold @@ -742,16 +835,13 @@ class WashDataManager: self.entry_id, min_duration_ratio=config_entry.options.get( CONF_PROFILE_MATCH_MIN_DURATION_RATIO, - DEFAULT_PROFILE_MATCH_MIN_DURATION_RATIO_BY_DEVICE.get( - self.device_type, DEFAULT_PROFILE_MATCH_MIN_DURATION_RATIO - ), + DEFAULT_PROFILE_MATCH_MIN_DURATION_RATIO, ), max_duration_ratio=config_entry.options.get( CONF_PROFILE_MATCH_MAX_DURATION_RATIO, DEFAULT_PROFILE_MATCH_MAX_DURATION_RATIO, ), save_debug_traces=config_entry.options.get(CONF_SAVE_DEBUG_TRACES, False), - match_threshold=match_threshold, unmatch_threshold=unmatch_threshold, device_name=config_entry.title, ) @@ -769,6 +859,9 @@ class WashDataManager: self.learning_manager = LearningManager( hass, self.entry_id, self.profile_store, self.device_type, device_name=config_entry.title, + # Its store saves and suggestion passes are cancelled with ours on an + # unload (audit MANAGER-13), not left writing the swapped-out store. + spawn=self._spawn_tracked, ) self.recorder = CycleRecorder(hass, self.entry_id, device_name=config_entry.title) self._store_bridge: Any = None # lazy community-store bridge (online features) @@ -777,22 +870,13 @@ class WashDataManager: min_power = config_entry.options.get( CONF_MIN_POWER, config_entry.data.get(CONF_MIN_POWER, DEFAULT_MIN_POWER) ) - off_delay = config_entry.options.get( - CONF_OFF_DELAY, config_entry.data.get(CONF_OFF_DELAY, DEFAULT_OFF_DELAY) + off_delay = _option_then_data( + config_entry, CONF_OFF_DELAY, resolve_off_delay_default(self.device_type) ) progress_reset_delay = config_entry.options.get( CONF_PROGRESS_RESET_DELAY, DEFAULT_PROGRESS_RESET_DELAY ) - self._no_update_active_timeout = float( - config_entry.options.get( - CONF_NO_UPDATE_ACTIVE_TIMEOUT, - DEFAULT_NO_UPDATE_ACTIVE_TIMEOUT, - ) - ) - self._low_power_no_update_timeout = float( - 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._load_runtime_options(config_entry) # 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. @@ -808,17 +892,6 @@ class WashDataManager: # 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 = option_float( - config_entry.options.get(CONF_AUTO_LABEL_CONFIDENCE, DEFAULT_AUTO_LABEL_CONFIDENCE), - DEFAULT_AUTO_LABEL_CONFIDENCE, - ) self._profile_match_interval = int( config_entry.options.get( @@ -885,31 +958,6 @@ class WashDataManager: ) ) - # Advanced options - smoothing_window = int(config_entry.options.get("smoothing_window", 5)) - interrupted_min_seconds = int( - config_entry.options.get("interrupted_min_seconds", 150) - ) - # Get device specific default for completion threshold - device_default_completion = DEVICE_COMPLETION_THRESHOLDS.get( - self.device_type, DEFAULT_COMPLETION_MIN_SECONDS - ) - completion_min_seconds = int( - config_entry.options.get( - CONF_COMPLETION_MIN_SECONDS, device_default_completion - ) - ) - - start_duration_threshold = float( - config_entry.options.get( - CONF_START_DURATION_THRESHOLD, - resolve_start_duration_default(self.device_type), - ) - ) - end_repeat_count = int( - config_entry.options.get(CONF_END_REPEAT_COUNT, DEFAULT_END_REPEAT_COUNT) - ) - self._logger.info( "Manager init: min_power=%sW, off_delay=%ss, type=%s", min_power, @@ -917,155 +965,10 @@ class WashDataManager: self.device_type, ) - config = CycleDetectorConfig( - min_power=float(min_power), - off_delay=int(off_delay), - smoothing_window=smoothing_window, - interrupted_min_seconds=interrupted_min_seconds, - completion_min_seconds=completion_min_seconds, - start_duration_threshold=start_duration_threshold, - end_repeat_count=end_repeat_count, - min_off_gap=int( - config_entry.options.get( - CONF_MIN_OFF_GAP, resolve_min_off_gap_default(self.device_type) - ) - ), - # Read here as well as on reload (item 351): the reload path was the - # only writer, so until the user next saved a setting the detector ran - # a tolerance of 0.25 no matter what the panel showed - and the - # deferral ceiling in `_should_defer_finish` reads it live. - profile_duration_tolerance=float( - config_entry.options.get( - CONF_PROFILE_DURATION_TOLERANCE, DEFAULT_PROFILE_DURATION_TOLERANCE - ) - ), - start_energy_threshold=float( - config_entry.options.get( - CONF_START_ENERGY_THRESHOLD, - DEFAULT_START_ENERGY_THRESHOLDS_BY_DEVICE.get(self.device_type, 0.2) - ) - ), - end_energy_threshold=float( - config_entry.options.get(CONF_END_ENERGY_THRESHOLD, DEFAULT_END_ENERGY_THRESHOLD) - ), - start_threshold_w=float( - config_entry.options.get( - CONF_START_THRESHOLD_W, - float(min_power) + max(1.0, 0.1 * float(min_power)), - ) - ), - stop_threshold_w=float( - config_entry.options.get( - CONF_STOP_THRESHOLD_W, - float(min_power) * 0.6 if float(min_power) > 0 else 2.0, - ) - ), - power_off_threshold_w=float( - config_entry.options.get( - CONF_POWER_OFF_THRESHOLD_W, DEFAULT_POWER_OFF_THRESHOLD_W - ) - ), - power_off_delay=float( - config_entry.options.get(CONF_POWER_OFF_DELAY, DEFAULT_POWER_OFF_DELAY) - ), - min_duration_ratio=float( - config_entry.options.get( - CONF_PROFILE_MATCH_MIN_DURATION_RATIO, - DEFAULT_PROFILE_MATCH_MIN_DURATION_RATIO_BY_DEVICE.get( - self.device_type, DEFAULT_PROFILE_MATCH_MIN_DURATION_RATIO - ), - ) - ), - match_interval=int( - config_entry.options.get( - CONF_PROFILE_MATCH_INTERVAL, DEFAULT_PROFILE_MATCH_INTERVAL - ) - ), - # `profile_match_threshold` was stored on the ProfileStore and never read - # anywhere, so the #288 workaround (raise it so near-duplicate profiles - # stop being trusted mid-cycle) silently did nothing. Wire it to the gate - # it was always documented to control; the default is the value that used - # to be hard-coded there, so nothing changes unless it was tuned. - match_confidence_threshold=float( - config_entry.options.get( - CONF_PROFILE_MATCH_THRESHOLD, DEFAULT_PROFILE_MATCH_THRESHOLD - ) - ), - anti_wrinkle_enabled=bool( - config_entry.options.get( - CONF_ANTI_WRINKLE_ENABLED, DEFAULT_ANTI_WRINKLE_ENABLED - ) - ), - anti_wrinkle_max_power=float( - config_entry.options.get( - CONF_ANTI_WRINKLE_MAX_POWER, DEFAULT_ANTI_WRINKLE_MAX_POWER - ) - ), - anti_wrinkle_max_duration=float( - config_entry.options.get( - CONF_ANTI_WRINKLE_MAX_DURATION, DEFAULT_ANTI_WRINKLE_MAX_DURATION - ) - ), - anti_wrinkle_exit_power=float( - config_entry.options.get( - CONF_ANTI_WRINKLE_EXIT_POWER, DEFAULT_ANTI_WRINKLE_EXIT_POWER - ) - ), - anti_wrinkle_idle_timeout=float( - config_entry.options.get( - CONF_ANTI_WRINKLE_IDLE_TIMEOUT, DEFAULT_ANTI_WRINKLE_IDLE_TIMEOUT - ) - ), - dishwasher_end_spike_quiet_release=float( - config_entry.options.get( - CONF_DISHWASHER_END_SPIKE_QUIET_RELEASE, - DISHWASHER_END_SPIKE_QUIET_RELEASE_SECONDS, - ) - ), - # #393: resolve the device-type default HERE (not in the gate) so the - # field always carries a real float - playground.effective_settings() - # skips a None-valued field, which would desync the sim from the detector. - smart_termination_duration_ratio=float( - config_entry.options.get( - CONF_SMART_TERMINATION_DURATION_RATIO, - 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 - ) - ), - delay_confirm_seconds=float( - config_entry.options.get( - CONF_DELAY_CONFIRM_SECONDS, DEFAULT_DELAY_CONFIRM_SECONDS - ) - ), - delay_timeout_seconds=float( - config_entry.options.get( - CONF_DELAY_TIMEOUT_HOURS, DEFAULT_DELAY_TIMEOUT_HOURS - ) - ) * 3600.0, - # #378: without this the detector keeps the WASHING_MACHINE default and - # every non-washing-machine device runs the washing-machine detection - # path (the whole dishwasher/#43 branch is otherwise dead in production). - device_type=self.device_type, + # One builder for the constructor, the options reload and every replay + # harness (audit F2 / DETECT-12; item 351 was these copies drifting). + config = build_detector_config( + config_entry.options, config_entry.data, self.device_type ) self._config = config @@ -1130,15 +1033,27 @@ class WashDataManager: self.profile_store.profile_terminal_quiet_seconds( self._current_program ), + # Element 12: no candidates on a manual pin; 0.0 is "none". + 0.0, + # Element 13 (register item 384): the pinned profile's + # user-vouched length, which floors a dishwasher's kept tail. + self.profile_store.profile_trusted_min_duration( + self._current_program + ), ) if not readings: return None - # Snapshotted for thread safety indirectly by task logic - # We don't need a wrapper task if we unify with _update_estimates matching - # but for now let's keep the detector callback as a trigger - self._spawn_tracked(self._async_perform_combined_matching(readings)) + # Which cycle this match is FOR, captured NOW: the detector calls this + # synchronously from an active state. The task body runs later, and on + # the reading that finishes the cycle the detector has already reset by + # then, so identity captured inside the task described the NEXT state + # and the stale result re-armed the finished detector (audit LIVE-01). + identity = self._match_identity() + self._spawn_tracked( + self._async_perform_combined_matching(readings, identity) + ) return None self.detector = CycleDetector( @@ -1171,37 +1086,18 @@ class WashDataManager: resolve_watchdog_interval_default(self.device_type), ) ) - # 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( CONF_SAMPLING_INTERVAL, resolve_sampling_interval_default(self.device_type), ) ) - self._noise_events_threshold = int( - config_entry.options.get( - CONF_AUTO_TUNE_NOISE_EVENTS_THRESHOLD, - DEFAULT_AUTO_TUNE_NOISE_EVENTS_THRESHOLD, - ) - ) self._current_program: str = "off" self._time_remaining: float | None = None self._total_duration: float | None = None - self._last_total_duration_update: datetime | None = None self._cycle_progress: float = 0.0 self._smoothed_progress: float = 0.0 # Smoothed progress tracking for EMA + self._smoothed_for_program: str | None = None # the program that EMA tracks # Live projected total energy/cost for the running cycle (None until a # reliable progress estimate exists). Derived from accumulated energy and # the (ML-blended) progress fraction; surfaced as progress-sensor attrs. @@ -1236,11 +1132,6 @@ class WashDataManager: # 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( - config_entry.options.get("profile_duration_tolerance", 0.25) - ) self._remove_maintenance_scheduler = None @@ -1249,7 +1140,6 @@ class WashDataManager: self._ml_training_running = False # True while a training run is in flight self._profile_sample_repair_stats: dict[str, int] | None = None - self._last_suggestion_update: datetime | None = None # Pump Monitor state self._pump_stuck_duration: int = int( @@ -1269,8 +1159,34 @@ class WashDataManager: self._unmatch_persistence_counter: int = 0 # Tracks consecutive low-confidence matches self._current_match_candidate: str | None = None # Pending profile name + _MATCH_ACTIVE_STATES = (STATE_STARTING, STATE_RUNNING, STATE_PAUSED, STATE_ENDING) + + def _match_identity(self) -> tuple[str, datetime | None, bool]: + """The cycle a live match belongs to: snapshot id, cycle start, active.""" + return ( + self._ranking_snapshot_cycle_id, + self.detector.current_cycle_start, + self.detector.state in self._MATCH_ACTIVE_STATES, + ) + + def _match_still_current(self, identity: tuple[str, datetime | None, bool]) -> bool: + """True while the cycle a live match was dispatched for is still running. + + Checked after EVERY await in the match task (items 388e, audit LIVE-01/02): + the cycle can end, or the next one start, while the matcher or the + alignment check is in the executor. A match dispatched while no cycle was + active (a direct call) only requires the same cycle identity. + """ + token, start, was_active = identity + cur_token, cur_start, is_active = self._match_identity() + if cur_token != token or cur_start != start: + return False + return is_active or not was_active + async def _async_perform_combined_matching( - self, readings: list[tuple[datetime, float]] + self, + readings: list[tuple[datetime, float]], + identity: tuple[str, datetime | None, bool] | None = None, ) -> None: """PRIMARY matching task: Updates both Manager and Detector using best method.""" self._logger.debug( @@ -1294,16 +1210,32 @@ class WashDataManager: self._logger.debug("Matching skipped: no real profiles configured yet") return - self._matching_task = self.hass.async_create_task(self._async_do_perform_matching(readings)) + self._matching_task = self.hass.async_create_task( + self._async_do_perform_matching( + readings, identity if identity is not None else self._match_identity() + ) + ) except Exception as e: self._logger.error("Perform combined matching trigger failed: %s", e) - async def _async_do_perform_matching(self, readings: list[tuple[datetime, float]]) -> None: + async def _async_do_perform_matching( + self, + readings: list[tuple[datetime, float]], + identity: tuple[str, datetime | None, bool] | None = None, + ) -> None: """Inner task to handle actual matching logic.""" + if identity is None: + identity = self._match_identity() try: end_time = readings[-1][0] start_time = readings[0][0] current_duration = (end_time - start_time).total_seconds() + # Which cycle this match is FOR (item 388e) is `identity`, captured at + # dispatch. The await below yields the loop, and the cycle can end - or + # the next one start - before it returns; applying the result then + # rewrote `_current_program` and the expected duration on a finished + # cycle, and pushed it into the detector, so the next cycle reached + # RUNNING already "matched". # 1. RUN BETTER ASYNC MATCHING # in_progress: this is the live match on a cycle that is still running, @@ -1315,320 +1247,54 @@ class WashDataManager: readings, current_duration, in_progress=True, + # Lets the #288 prefix term ignore longer programmes that never + # pause below this threshold, so cannot explain a quiet (#424). + stop_threshold_w=float(self.detector.config.stop_threshold_w), ) + if not self._match_still_current(identity): + self._logger.debug( + "Discarding a live match that returned after its cycle ended " + "(detector %s)", self.detector.state, + ) + return + # 2. UPDATE MANAGER STATE (Estimates, Program Name, etc.) self._last_match_result = result self._last_match_ambiguous = result.is_ambiguous - profile_name = result.best_profile - confidence = result.confidence - matched_duration = result.expected_duration - phase_name = result.matched_phase - # --- Switching Logic (Temporal Persistence) --- - should_switch = False - switch_reason = "" - - # Identify current program score from results - current_program_score = 0.0 - for c in result.candidates: - if c.get("name") == self._current_program: - current_program_score = c.get("score", 0.0) - break - - # How far clear of the runner-up the winner is. Measured over 594 - # cycles x 10 checkpoints, this separates right from wrong far better - # than the absolute score does mid-cycle (AUC 0.773 vs 0.535), which is - # why the mid-cycle switch below keys on it. Register item 305. - # Measured against the best OTHER candidate rather than by list index: - # Stage-5 group collapsing rebuilds the result, so `best_profile` is not - # guaranteed to be `candidates[0]`. - match_margin = 1.0 - _runner_up = None - for c in result.candidates: - if c.get("name") == profile_name: - continue - try: - cs = float(c.get("score", 0.0) or 0.0) - except (TypeError, ValueError): - continue - if _runner_up is None or cs > _runner_up: - _runner_up = cs - if _runner_up is not None: - match_margin = float(confidence) - _runner_up - - # CASE: Divergence Detection (Score Drop) - # If current matched program has a significant drop from its own peak score, - # we should consider unmatching it even if it's still the "best" candidate. - if ( - self._current_program not in ("detecting...", "off", "starting", "unknown") - and profile_name == self._current_program - ): - history: list[float] = self._score_history.get(self._current_program, []) - if len(history) > 3: - peak_score = max(history) - # If score drops by more than 40% from peak AND is below threshold, unmatch. - # This catches divergence faster than waiting for fixed unmatch_threshold. - if confidence < peak_score * (1.0 - DEFAULT_MATCH_REVERT_RATIO): - self._unmatch_persistence_counter += 1 - if self._unmatch_persistence_counter >= self._match_persistence: - self._current_program = "detecting..." - self._matched_profile_duration = None - self._unmatch_persistence_counter = 0 - self._logger.info( - "Divergence detected for profile '%s' (confidence %.3f < 60%% of peak %.3f). " - "Reverting to detection.", - profile_name, confidence, peak_score - ) - # Reset profile_name so Case 3 doesn't re-trigger - profile_name = "detecting..." - - # Update persistence for the best profile - if profile_name and profile_name != "detecting...": - self._match_persistence_counter[profile_name] = self._match_persistence_counter.get(profile_name, 0) + 1 - - # Check if this is the same candidate as before - if profile_name != self._current_match_candidate: - # Reset counter for old candidate if it wasn't locked in - self._current_match_candidate = profile_name - self._match_persistence_counter[profile_name] = 1 - else: - self._current_match_candidate = None - - is_persistent = profile_name and self._match_persistence_counter.get(profile_name, 0) >= self._match_persistence - - # --- Live-match features: compute always for ranking history + ML gate --- - # Features are cheap scalars derived from the current trace. We compute - # them whenever there is a non-ambiguous candidate so they can be recorded - # as a training snapshot regardless of whether ML models are opted in. - # The opt-in ML commit check then uses the same features when enabled. - ml_commit_score: float | None = None - _live_feat: dict[str, float] | None = None - _top2_score: float | None = None - if ( - profile_name - and profile_name != "detecting..." - and not result.is_ambiguous - and self._current_program in ("detecting...",) - ): - try: - from .ml.feature_extraction import live_match_features # noqa: PLC0415 - - first_ts = readings[0][0] - pts = [ - ((ts - first_ts).total_seconds(), float(pw)) - for ts, pw in readings - ] - top1_dist = max(0.0, 1.0 - confidence) - top2_raw: float | None = None - if len(result.candidates) > 1: - s2 = result.candidates[1].get("score", 0.0) or 0.0 - top2_raw = max(0.0, 1.0 - float(s2)) - _top2_score = float(result.candidates[1].get("score", 0.0) or 0.0) - n_profiles = len(self.profile_store.get_profiles()) - _live_feat = live_match_features( - points=pts, - elapsed_s=current_duration, - top1_distance=top1_dist, - top2_distance=top2_raw, - top1_median_duration_s=float(result.expected_duration or 0), - candidate_count=max(1, n_profiles), - ) - # --- ML early-commit gate (opt-in) --- - # When the user has opted into experimental ML models, query the - # live_match_commit model for P(top-1 is correct). - try: - from .ml.engine import ml_models_enabled, resolve_scorer # noqa: PLC0415 - if ml_models_enabled(self.config_entry.options): - match_fn, _ = resolve_scorer("live_match", self.profile_store) - if match_fn is not None: - ml_commit_score = float(match_fn(_live_feat)) - self._logger.debug( - "Live-match ML commit score for '%s': %.3f (threshold %.2f)", - profile_name, - ml_commit_score, - ML_MATCH_COMMIT_THRESHOLD, - ) - except Exception: # noqa: BLE001 - pass - except Exception: # noqa: BLE001 - ML must never break matching - pass - - # --- Record ranking snapshot for live_match on-device training --- - # Snapshot is recorded unconditionally (not gated on ML opt-in) so that - # training data accumulates even before the user enables ML models. - # Confirmed labels are back-filled at cycle end. - if _live_feat is not None and self._cycle_start_time: - try: - self.profile_store.record_match_ranking_snapshot( - start_time_iso=self._cycle_start_time.isoformat(), - features=_live_feat, - top1_profile=profile_name or "", - top1_score=float(confidence), - top2_score=_top2_score, - candidate_count=max(1, len(self.profile_store.get_profiles())), - cycle_id=self._ranking_snapshot_cycle_id, - ) - except Exception: # noqa: BLE001 - never break matching - pass - - ml_early_commit = ( - ml_commit_score is not None - and ml_commit_score >= ML_MATCH_COMMIT_THRESHOLD - and confidence >= 0.30 + # The rules live in `match_rules` (audit PLAYGROUND-01/03), shared with + # the Playground replay so it makes the decisions made here. Step 1: + # margin, divergence revert, persistence. `profile_name` is the tick's + # name from here on - "detecting..." after a divergence revert, which is + # also what the detector is handed below. + switch_state = _read_switch_state(self) + tick = match_rules.begin_tick( + switch_state, result, self._match_persistence, current_duration ) + _write_switch_state(self, switch_state, tick.log) + profile_name = tick.profile_name + confidence = tick.confidence + matched_duration = tick.matched_duration + phase_name = tick.phase_name - # Case 1: Initial Match from "detecting..." - if ( - profile_name - and confidence >= 0.15 - and (not result.is_ambiguous or is_persistent) - and (not self._matched_profile_duration or self._current_program == "detecting...") - ): - if is_persistent: - should_switch = True - switch_reason = f"initial_match (persistent {self._match_persistence_counter[profile_name]}x)" - elif ml_early_commit: - should_switch = True - switch_reason = ( - f"initial_match (ML commit score {ml_commit_score:.3f} >= {ML_MATCH_COMMIT_THRESHOLD})" - ) - else: - self._logger.debug( - "Match persistence: %s at %d/%d matches. Stay at detecting...", - profile_name, self._match_persistence_counter.get(profile_name, 0), self._match_persistence - ) + # Step 2 (match_rules.decide_switch): Case 1 initial commit, Case 2 + # decisive-margin / trend switch, Case 3 unmatch, and the switch itself. + switch_state = _read_switch_state(self) + switch_log = match_rules.decide_switch( + switch_state, + tick, + result, + self._match_persistence, + self._unmatch_threshold, + ) + _write_switch_state(self, switch_state, switch_log) - # Case 2: Mid-cycle override (different profile) - elif ( - profile_name - and self._current_program != profile_name - and self._current_program not in ("detecting...", "off", "starting", "unknown") - ): - # Decisive Margin Override: bypass persistence when the winner is - # far clear of the runner-up (register item 305). - # - # This replaces a "High Confidence Override" keyed on - # `confidence > 0.8`, whose premise - a very strong match needs no - # confirmation - is backwards mid-cycle. Mid-run the query is a - # PREFIX, and a prefix of a long programme looks exactly like a - # *finished* short one, so a score above 0.8 measured 31.5% correct - # (n=73) against 69.6% for the 0.6-0.8 band it was skipping the wait - # for; those cases pick a shorter programme 46% of the time (vs 16.5% - # at large). Replaying all 594 cycles through this switching logic, - # the old rule was also **unreachable in practice** - its outcomes - # land within 0.2pp of having no override at all. - # - # The margin is the signal that works: mid-cycle AUC 0.773 vs 0.535 - # for the absolute score. Replayed, keying the bypass on it lifts - # end-of-cycle correctness 70.4% -> 72.6% (16 cycles better, 3 worse, - # McNemar p = 0.0044) for 0.14 displayed switches per cycle against - # 0.07. The sweep is monotone, so this is the conservative end of an - # accuracy/stability trade: 0.05 -> +6.7pp at 0.27 flips/cycle, - # 0.08 -> +5.1, 0.10 -> +3.0, 0.12 -> +2.2, 0.15 -> +1.7. - # The `> current_program_score` guard measured neutral (it never binds - # at this margin) and is kept because switching to something scoring - # below what is already displayed is never right. - # The 1.0 sentinel `match_margin` carries when nothing else scored - # is LOAD-BEARING, not a gap. Requiring a real runner-up here was - # tried (PR #448 round 6) and measured on - # `devtools/decisive_margin_eval.py` over 1977 mid-cycle - # checkpoints from the real corpus: a single surviving candidate - # occurs at 2.58% of them and is the **correct** programme - # **94.0% (47/50)** of the time, against **77.8% (669/860)** for - # the real-margin bypass it would have been held to. Stage 1/2 - # rejecting every other profile is strong evidence, not absent - # evidence, so making those checkpoints wait for persistence - # delays the matcher's most reliable signal. Reverted. - if ( - match_margin > MATCH_DECISIVE_MARGIN - and confidence > current_program_score - ): - should_switch = True - switch_reason = ( - f"decisive_margin (margin {match_margin:.3f} > " - f"{MATCH_DECISIVE_MARGIN}, {confidence:.3f} vs {current_program_score:.3f})" - ) - - # Normal Switch: Requires persistence AND either better score + trend - elif is_persistent: - if confidence > current_program_score and self._analyze_trend(profile_name): - # Add a minimum score gap for mid-cycle switching (0.05) to prevent flapping - if (confidence - current_program_score) > 0.05: - should_switch = True - switch_reason = f"positive_trend_persistent ({confidence:.3f} > {current_program_score:.3f})" - - # Case 3: Unmatching (confidence drop) - elif ( - self._current_program not in ("detecting...", "off", "starting", "unknown") - and profile_name == self._current_program - and confidence < self._unmatch_threshold - ): - self._unmatch_persistence_counter += 1 - is_unmatch_persistent = self._unmatch_persistence_counter >= self._match_persistence - - if is_unmatch_persistent: - self._current_program = "detecting..." - self._matched_profile_duration = None - self._unmatch_persistence_counter = 0 - self._logger.info( - "Unmatched profile '%s' (confidence %.3f < threshold %.3f persistent %dx). " - "Reverting to detection.", - profile_name, - confidence, - self._unmatch_threshold, - self._match_persistence - ) - else: - self._logger.debug( - "Unmatch persistence: %s at %d/%d low-confidence matches. Stay at %s...", - profile_name, self._unmatch_persistence_counter, self._match_persistence, profile_name - ) - - # Reset unmatch counter if confidence is healthy - # AND we didn't just detect a divergence - elif ( - profile_name == self._current_program - and confidence >= self._unmatch_threshold - and not (len(self._score_history.get(self._current_program, [])) > 3 and confidence < max(self._score_history[self._current_program]) * (1.0 - DEFAULT_MATCH_REVERT_RATIO)) - ): - self._unmatch_persistence_counter = 0 - - if should_switch: - if profile_name is None: - self._current_program = "detecting..." - 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 - - 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), - ) - 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..." - - self._last_estimate_time = dt_util.now() + self._last_estimate_time = utc_now() # Update score history for all candidates to track trends - for cand in result.candidates: - cname = cand.get("name") - if cname: - history = self._score_history.setdefault(cname, []) - history.append(float(cand.get("score", 0.0))) - if len(history) > 20: - history.pop(0) + match_rules.record_scores(switch_state, result.candidates) # Note: _update_remaining_only() and notify move to end of flow @@ -1644,7 +1310,10 @@ class WashDataManager: # authoritative and must not be re-judged by the envelope heuristic # (see the verified_pause override below). stop_thresh = float(self.detector.config.stop_threshold_w) - if current_matched and current_power < stop_thresh and not self._is_user_paused: + alignment: tuple[bool, float] | None = None + if match_rules.needs_alignment_check( + current_matched, current_power, stop_thresh, self._is_user_paused + ): formatted = power_data_to_offsets(cast(list[list[Any] | tuple[Any, ...]], readings)) try: profile_store_any = cast(Any, self.profile_store) @@ -1652,6 +1321,15 @@ class WashDataManager: is_confirmed, mapped_time, _ = ( await verify_alignment(current_matched, formatted) ) + if not self._match_still_current(identity): + # Same race as the matcher await above (audit LIVE-02): a + # verified pause and the old match must not land on a + # finished detector and carry into the next cycle. + self._logger.debug( + "Discarding a live match whose alignment check returned " + "after its cycle ended" + ) + return except Exception as e: # pylint: disable=broad-exception-caught self._logger.error( "Alignment verification crashed for profile %s: %s", @@ -1659,175 +1337,113 @@ class WashDataManager: ) is_confirmed = False mapped_time = 0.0 + alignment = (is_confirmed, mapped_time) - if is_confirmed: - if not verified_pause: - self._logger.info( - "Envelope verified expected low power phase for %s. Enabling verified pause.", - current_matched - ) - verified_pause = True - # Smart Termination within Envelope block. Compare the mapped - # position against the envelope's OWN time span (not avg_duration, - # a differently-derived trimmed mean): mapped_time is capped at the - # grid span, so span/avg_duration < 1 would make the 0.95 release - # 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( - "Smart Termination: near end of profile (%.0f/%.0fs). Releasing pause lock.", - mapped_time, span, - ) - else: - # Diagnostic (#346): the release is held; show how far the - # trace mapped vs the 95%% release point (no behaviour change). - self._logger.debug( - "Smart Termination held for %s: mapped %.0f/%.0fs (%.0f%%) below 95%% release%s", - current_matched, mapped_time, span, - (100.0 * mapped_time / span) if span > 0 else 0.0, - "" if span > 0 else " (envelope span unavailable)", - ) - except Exception as e: - self._logger.debug("Smart Termination alignment verification failed: %s", e) - else: - if verified_pause: - self._logger.info( - "Envelope indicates UNEXPECTED low power for %s. Disabling verified pause.", - current_matched - ) - verified_pause = False - - # --- High Power Clear --- - stop_threshold = getattr(self.detector.config, "stop_threshold_w", 5.0) - - if current_power > stop_threshold * 10: - verified_pause = False - - # --- Sustained-quiet release of an auto-detected pause (issue #375) --- - # An envelope-verified pause bridges a genuine low-power phase (e.g. a - # dishwasher's passive drying). When the appliance instead goes truly - # silent at the real end, the envelope alignment can keep re-confirming - # against a long near-zero drying tail baked into the profile by earlier - # force-stopped cycles, and Smart Termination's >95%-of-span release is - # unreachable because the trace goes quiet BEFORE that learned tail ends. - # The flag then freezes True and every ENDING finalize backstop (all - # gated on `not _verified_pause`) is defeated, so the cycle hangs for - # hours until the watchdog's multi-hour silence limit force-ends it. - # Release the auto-pause once the cycle has completed its expected - # duration AND has been continuously sub-threshold for the finalize - # quiet floor: the drying (if any) is over, so let the normal end path - # finalize. This mirrors the dishwasher `quiet_released` gate the - # detector already trusts in `_should_defer_finish`. A real user pause - # is authoritative and re-asserted below, so it is never released here. - expected_dur = self.detector.expected_duration_seconds - # Gap-free tally only: a telemetry outage is unobserved time and must - # not satisfy the quiet floor that releases the auto-detected pause - # (mirrors the detector's dishwasher quiet-release gates). Fall back to - # the plain tally if the attribute is missing (older detector). - time_below = getattr( - self.detector, - "_time_below_threshold_gapfree", - getattr(self.detector, "_time_below_threshold", 0.0), + # Confirmed alignment, the 95%-of-span release, the high-power clear, + # the #375 sustained-quiet release and the user-pause override + # (match_rules.decide_verified_pause). The quiet tally is the GAP-FREE + # one: a telemetry outage is unobserved time and must not satisfy the + # #375 quiet floor (falls back to the plain tally on an older detector). + pause = match_rules.decide_alignment_pause( + verified_pause=verified_pause, + current_matched=current_matched, + alignment=alignment, + envelope_span=self.profile_store.envelope_time_span, ) - if ( - verified_pause - and not self._is_user_paused - and expected_dur > 0 - and current_duration >= expected_dur - and time_below >= ENDING_HARD_FINALIZE_MIN_QUIET_S - ): - self._logger.info( - "Releasing auto-detected pause for %s: reached expected " - "duration (%.0fs >= %.0fs) and sustained-quiet %.0fs - " - "allowing normal cycle finish (issue #375).", - current_matched or self._current_program, - current_duration, - expected_dur, - time_below, - ) - verified_pause = False + if pause.envelope_position is not None: + self._envelope_position = pause.envelope_position + _emit_rule_log(self._logger, pause.log) + pause = match_rules.decide_pause_release( + verified_pause=pause.verified_pause, + current_matched=current_matched, + current_power=current_power, + stop_threshold_w=getattr(self.detector.config, "stop_threshold_w", 5.0), + user_paused=self._is_user_paused, + expected_duration=self.detector.expected_duration_seconds, + current_duration=current_duration, + time_below=getattr( + self.detector, + "_time_below_threshold_gapfree", + getattr(self.detector, "_time_below_threshold", 0.0), + ), + program=self._current_program, + ) + _emit_rule_log(self._logger, pause.log) + verified_pause = pause.verified_pause - # A user-initiated pause (Pause Cycle button, or the door-open soft - # pause) stays in force until the user resumes (issue #306). The - # heuristics above only govern *auto-detected* low-power phases; without - # this override they clear verified_pause and the cycle is finalized - # (leaving the "Paused by user" state, e.g. a dishwasher closes at the - # 1 h min-off-gap timeout) instead of waiting for Resume. Re-asserting - # here also repairs the flag after a restart, since the detector state - # snapshot does not persist _verified_pause. - if self._is_user_paused: - verified_pause = True - - # --- Consistency Override --- - # If envelope verified or mismatched, ensure manager program matches - if profile_name != self._current_program and (verified_pause or result.is_confident_mismatch): - 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 = self._profile_duration( - prof.get("avg_duration") - ) - except Exception as e: - self._logger.debug("Failed to fetch profile duration on switch: %s", e) - else: - self._current_program = "detecting..." - self._matched_profile_duration = None - - # --- HEURISTICS (Descriptive Phases) --- - if not phase_name: - if self.device_type == "dishwasher" and self.detector.is_waiting_low_power(): - phase_name = "Drying" - elif self.device_type == "washing_machine" and current_power > 200: - phase_name = "Spinning" - elif self.device_type == "washing_machine" and self.detector.is_waiting_low_power(): - phase_name = "Rinsing/Soaking" + # --- Consistency Override (verified pause / confident mismatch) --- + switch_state = _read_switch_state(self) + override_log = match_rules.consistency_override( + switch_state, tick, result, verified_pause, self.profile_store.get_profile + ) + _write_switch_state(self, switch_state, override_log) + # Register item 469(b): an ambiguous tick in ENDING engages no new + # verified pause (the detector refuses its match if it would defer). + pause = match_rules.hold_in_ending( + ending=self.detector.state == STATE_ENDING, + is_ambiguous=bool(result.is_ambiguous), + current_matched=current_matched, + prev_verified=getattr(self.detector, "_verified_pause", False), + verified_pause=verified_pause, + user_paused=self._is_user_paused, + ) + _emit_rule_log(self._logger, pause.log) # 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; - # element 10 is its terminal high-power block for the #399 anti-crease - # guard; element 11 is its measured post-activity quiet span, which - # bounds the tail Smart Termination may bank (register item 297). The detector - # tolerates shorter tuples, so other callers stay valid. + self.detector.set_verified_pause(pause.verified_pause) + # A divergence revert revokes the detector's match (no name, revoke flag). + profile_name, revoke = match_rules.detector_match(tick, result) + # Built by name (audit DETECT-15); see CycleDetector.MatchContext. terminal_high = self._terminal_high_for_guards(profile_name) - 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, - terminal_high, - # Element 11 (register item 297): the matched profile's measured post-activity - # quiet span, which bounds how much of Smart Termination's - # confirmation delay _keep_tail_cap may store as cycle time. - self.profile_store.profile_terminal_quiet_seconds(profile_name) - if profile_name else None, - # Element 12 (register item 330): the longest expected duration - # still in play across the candidates. The ENDING fallback gate - # raises its bar to this while the match is ambiguous, instead of - # refusing to shorten at all. - # From the FULL candidate population, carried on the result - - # `result.candidates` is `candidates[:5]` and would hide the - # very programme `_match_prefix_ambiguous` is warning about. - float(getattr(result, "longest_candidate_duration_s", 0.0) or 0.0)) + # Half-interval matching until the first commit (audit LIVE-17). + self.detector.set_match_committed( + match_rules.program_is_committed(self._current_program) ) + self.detector.update_match(MatchContext( + profile_name=profile_name, + confidence=confidence, + expected_duration=matched_duration, + phase_name=phase_name, + is_confident_mismatch=revoke, + is_ambiguous=result.is_ambiguous, + is_prefix_ambiguous_full_shape=result.is_prefix_ambiguous_full_shape, + tail_power=( + self.profile_store.profile_tail_power(profile_name) if profile_name else None + ), + terminal_high=terminal_high, + # Item 297: the profile's measured post-activity quiet span, which + # bounds how much of Smart Termination's delay the tail may keep. + terminal_quiet_s=( + self.profile_store.profile_terminal_quiet_seconds(profile_name) + if profile_name else None + ), + # Item 330: from the FULL candidate population, carried on the + # result (`result.candidates` is the top 5 and would hide the very + # programme an ambiguous match may be warning about). + longest_candidate_s=float( + getattr(result, "longest_candidate_duration_s", 0.0) or 0.0 + ), + # Item 384: the shortest length the user has vouched for. + trusted_min_s=( + self.profile_store.profile_trusted_min_duration(profile_name) + if profile_name else None + ), + # Audit DETECT-16: the pause catalogue for the hazard end gate. + pause_catalogue=( + self.profile_store.profile_pause_catalogue( + profile_name, float(self.detector.config.stop_threshold_w) + ) if profile_name else None + ), + # #452: the same below the near-stop ceiling, for the stall display. + # Lazy: read only when a flat run is long enough to be judged. + stall_catalogue=( + functools.partial( + self.profile_store.profile_pause_catalogue, + profile_name, + standby_near_stop_ceiling(self.detector.config.stop_threshold_w), + ) if profile_name else None + ), + )) # --- LOGGING (Unified) --- self._logger.info( @@ -1850,7 +1466,7 @@ class WashDataManager: "device_type": self.device_type, "program": self._current_program, "start_time": ( - self._cycle_start_time or dt_util.now() + self._cycle_start_time or utc_now() ).isoformat(), }, ) @@ -1889,7 +1505,7 @@ class WashDataManager: self._check_pre_completion_notification() self._check_live_progress_notification() - self._notify_update() + self._notify_update_deferrable() except Exception as e: self._logger.error("Perform combined matching failed: %s", e, exc_info=True) @@ -1911,23 +1527,18 @@ class WashDataManager: return _sanitize_ranking(raw_list) @property - def phase_description(self) -> str: - """Return a description of the current phase. + def phase_description(self) -> str | None: + """The current phase: a name from the matched profile's ranges, or None. - Prefers the *functional* progress-driven phase (the visual per-profile - phase configurator's ranges, indexed by the live ML-blended progress) so - the readout stays accurate even when a cycle runs longer/shorter than the - profile's nominal timeline. Falls back to the matcher's phase, then the - detector sub-state/state. + Only the *functional* progress-driven phase (the visual per-profile phase + configurator's ranges, indexed by the live ML-blended progress). None - + the sensor's ``unknown`` - when no range applies (audit PROGRESS-11): the + old fallbacks were the matcher's nearest-range guess, power heuristics + ("Spinning" over 200 W, so a 2 kW heater read Spinning) and the detector + sub-state, all English free text no translation could reach. The cycle + state itself stays on the state sensor (a translated slug). """ - live = self._current_phase_from_progress() - if live: - return live - if self._last_match_result and self._last_match_result.matched_phase: - return self._last_match_result.matched_phase - if self.detector.sub_state: - return self.detector.sub_state - return self.detector.state + return self._current_phase_from_progress() def _current_phase_from_progress(self) -> str | None: """Live phase from the profile's configured ranges + ML-blended progress. @@ -1935,15 +1546,18 @@ class WashDataManager: This is the *merge* of the visual phase configurator with the runtime estimator: one phase definition (the per-profile ranges the user draws), indexed by the smoothed progress fraction rather than raw elapsed seconds, - so overrun/underrun cycles still name the phase correctly. Returns None - (caller falls back) when not running, no profile is matched, or the - profile has no configured phase ranges. Never raises. + so overrun/underrun cycles still name the phase correctly. The fraction + maps onto the matched profile's expected duration (or the ranges' end if + they run longer), so ranges read at their real minutes (audit + PROGRESS-10). Returns None when not running, no profile is matched, the + profile has no phase ranges, or no range covers this point. Never raises. """ return progress_mod.current_phase( self.profile_store, self.detector.state, self._current_program, self._cycle_progress, + self._matched_profile_duration, ) @property @@ -1961,6 +1575,26 @@ class WashDataManager: return None return getattr(result, "ambiguity_margin", None) + @property + def match_uncertainty(self) -> dict[str, Any] | None: + """Top two of an undecided live match, for the panel (MATCH-DECIDE-15). + + Display only (``match_rules.live_match_uncertainty``). None while idle, on + a hand-picked program, or once the match is decided. + """ + try: + if self.manual_program_active: + return None + if self.detector.state not in ( + STATE_STARTING, STATE_RUNNING, STATE_PAUSED, STATE_USER_PAUSED, STATE_ENDING, + ): + return None + return match_rules.live_match_uncertainty( + self._last_match_result, self._current_program + ) + except Exception: # pylint: disable=broad-exception-caught + return None + # Note: last_match_details property is defined later in the class # It returns MatchResult from _last_match_result async def _attempt_state_restoration(self) -> None: @@ -1993,9 +1627,54 @@ class WashDataManager: active_snapshot if isinstance(active_snapshot, dict) else None ) + snap = active_snapshot_to_restore or {} + snap_last_real = _snapshot_time(snap.get("last_real_reading_time")) + + def is_live_silent_tail(now: datetime) -> bool: + """A cycle waiting out a silent low-power tail (register item 266). + + The windows in ``is_viable_restore`` age the snapshot, but a silent + tail is not stale: the watchdog keeps such a cycle for as long as its + staleness budget allows silence (1 h, a dishwasher 4 h, the profile's + remaining time, a verified pause), and saves came only with real + readings, so a drying dishwasher's snapshot aged past 30 min while + Home Assistant was watching it. Restore what the watchdog would hold: + same budget, judged on the sensor's real silence, and only while the + sensor still reads low. Past that budget the watchdog would have + force-ended it, so it is genuinely stale and is still dropped. + """ + if snap_last_real is None or not power_is_valid: + return False + if current_power >= self._config.min_power: + return False + if snap.get("state") not in (STATE_RUNNING, STATE_PAUSED, STATE_ENDING): + return False + try: + waiting = float(snap.get("time_below") or 0.0) > 0.0 + expected = float(snap.get("expected_duration") or 0.0) + except (TypeError, ValueError, OverflowError): + return False + if not waiting: + return False + start = _snapshot_time(snap.get("current_cycle_start")) + elapsed = (now - start).total_seconds() if start is not None else 0.0 + budget = self._low_power_silence_budget_s( + elapsed, expected, bool(snap.get("is_user_paused")) + ) + silence = (now - snap_last_real).total_seconds() + if silence > budget: + return False + self._logger.info( + "Restoring a cycle in a silent low-power tail: sensor silent " + "%.0fs, within the watchdog's %.0fs budget", + silence, + budget, + ) + return True + # Helper to check if a snapshot is viable def is_viable_restore(last_save_time: datetime) -> bool: - now = dt_util.now() + now = utc_now() # Handle timezone mismatch gracefully if last_save_time.tzinfo is None: # Assume naive means local system time, convert to aware @@ -2013,7 +1692,7 @@ class WashDataManager: and current_power >= self._config.min_power ): return True - return False + return is_live_silent_tail(now) last_save = self.profile_store.get_last_active_save() if last_save and last_save.tzinfo is None: @@ -2022,8 +1701,8 @@ class WashDataManager: if active_snapshot_to_restore is not None and last_save and is_viable_restore(last_save): should_restore = True - age = (dt_util.now() - last_save).total_seconds() - age = (dt_util.now() - last_save).total_seconds() + age = (utc_now() - last_save).total_seconds() + age = (utc_now() - last_save).total_seconds() self._logger.info( "Found recently saved active cycle (last_save=%s, age=%.0fs), restoring...", last_save, @@ -2037,84 +1716,43 @@ class WashDataManager: # might have missed data active_snapshot_to_restore["dynamic_min_duration"] = None - # FALLBACK: Resurrection Logic - if not should_restore: - past_cycles = self.profile_store.get_past_cycles() - if past_cycles: - last_cycle = past_cycles[-1] - last_end_str = last_cycle.get("end_time") - if last_end_str: - last_end = dt_util.parse_datetime(last_end_str) - if last_end: - gap = (dt_util.now() - last_end).total_seconds() - is_recent = gap < 1200 # 20 mins - status = last_cycle.get("status") + # A snapshot of a cycle that is already stored is not an active cycle: an + # HA restart between the cycle-end tail persisting the cycle and clearing + # the snapshot used to restore it, so the cycle ended twice - two stored + # copies, two "finished" pushes and lifetime energy counted twice on a + # TOTAL_INCREASING sensor (audit MANAGER-04). + if should_restore and active_snapshot_to_restore is not None: + snap_start = dt_util.parse_datetime( + str(active_snapshot_to_restore.get("current_cycle_start") or "") + ) + if snap_start is not None and snap_start.tzinfo is None: + snap_start = snap_start.replace(tzinfo=dt_util.now().tzinfo) + if snap_start is not None: + for stored in self.profile_store.get_past_cycles()[-5:]: + stored_start = dt_util.parse_datetime( + str(stored.get("start_time") or "") + ) + if stored_start is not None and stored_start.tzinfo is None: + stored_start = stored_start.replace(tzinfo=dt_util.now().tzinfo) + if stored_start is not None and abs( + (stored_start - snap_start).total_seconds() + ) < 1.0: + self._logger.info( + "Active-cycle snapshot belongs to stored cycle %s; " + "not restoring it", + stored.get("id"), + ) + should_restore = False + active_snapshot_to_restore = None + await self.profile_store.async_clear_active_cycle() + break - if is_recent and status != "completed": - self._logger.info( - "Found recent interrupted cycle in history " - "(id=%s, gap=%.0fs). Resurrecting...", - last_cycle["id"], - gap, - ) - try: - power_data = decompress_power_data(last_cycle) - if power_data: - # decompress_power_data returns (offset_seconds, watts) - # tuples, but restore_state_snapshot parses reading[0] - # as an ISO datetime. Convert offsets to absolute ISO - # timestamps (base = cycle start) so the resurrected - # trace is not silently dropped (B4). - _res_start = dt_util.parse_datetime( - last_cycle["start_time"] - ) - if _res_start is not None: - if _res_start.tzinfo is None: - _res_start = _res_start.replace( - tzinfo=dt_util.now().tzinfo - ) - power_readings = [ - ( - ( - _res_start - + timedelta(seconds=float(off)) - ).isoformat(), - p, - ) - for off, p in power_data - ] - else: - power_readings = power_data - active_snapshot_to_restore = { - # Reconstruct basic running state - "state": "running", - "sub_state": "Resurrected", - "current_cycle_start": last_cycle["start_time"], - "last_active_time": last_cycle["end_time"], - "low_power_start": None, - "cycle_max_power": ( - max([p for _, p in power_data]) - if power_data - else 0 - ), - "power_readings": power_readings, - "ma_buffer": ( - [p for _, p in power_data[-10:]] - if power_data - else [] - ), - "end_condition_count": 0, - "extension_count": 0, - "dynamic_min_duration": None, - "matched_profile": last_cycle.get( - "profile_name" - ), - } - should_restore = True - past_cycles.pop() - await self.profile_store.async_save() - except Exception as e: - self._logger.error("Failed to resurrect cycle: %s", e) + # (The "resurrection" fallback that re-opened the last stored cycle when it + # was interrupted or force-stopped less than 20 min ago is gone: it popped + # the stored cycle and re-ended it on any restart or settings save, so the + # lifetime energy was counted twice, a second "finished" push went out and + # a force_stopped cycle was re-stored as completed - audit MANAGER-03. The + # 60 s active-cycle snapshot is what covers a mid-cycle restart.) if should_restore and active_snapshot_to_restore: try: @@ -2132,7 +1770,15 @@ class WashDataManager: "state": STATE_PAUSED, } - self.detector.restore_state_snapshot(active_snapshot_to_restore) + if self.detector.restore_state_snapshot(active_snapshot_to_restore) is False: + # The detector already logged the traceback. + await self._async_keep_failed_restore( + active_snapshot_to_restore, + last_save, + self.detector.restore_error or "restore_state_snapshot failed", + traceback_logged=True, + ) + return # Anti-wrinkle keepalive anchor (#339). The keepalive in # _handle_state_expiry needs a "sensor last spoke" timestamp, but @@ -2143,10 +1789,23 @@ class WashDataManager: # last_save is the best available proxy. Scoped to ANTI_WRINKLE so # no other timer sees a synthetic anchor. if self.detector.state == STATE_ANTI_WRINKLE and last_save: - self._last_real_reading_time = last_save + self._last_real_reading_time = snap_last_real or last_save + # The cycle end started the expiry timer that runs that + # keepalive; a restart into the tail must start it too. + self._start_state_expiry_timer() # Restore if in any active state (Running, Paused, Ending) if self.detector.state in (STATE_RUNNING, STATE_PAUSED, STATE_ENDING): + # The sensor's pre-restart silence clock (register item 266). + # The setup read then decides whether the entity's startup + # write is a new report or just the old value written again. + if snap_last_real is not None: + self._last_real_reading_time = snap_last_real + self._restored_sensor_clock = ( + snap_last_real, + _finite_power(snap.get("last_sensor_power")), + ) + # Restore manual program flag if present self._manual_program_active = active_snapshot_to_restore.get( "manual_program", False @@ -2181,11 +1840,34 @@ class WashDataManager: self._config.min_power, ) - if self.detector.matched_profile: - self._current_program = self.detector.matched_profile + # The committed program and its expected duration. Until the + # snapshot carried them, the detector's match was all there was: + # the last tick's raw winner, not necessarily the displayed + # program, and no `_matched_profile_duration`, so the ETA was blank + # and the first tick another candidate led reverted the program to + # "detecting..." (decide_switch's no-duration branch). + if "committed_program" in active_snapshot_to_restore: + committed = active_snapshot_to_restore.get("committed_program") + committed_duration = active_snapshot_to_restore.get( + "committed_program_duration" + ) + else: # snapshot from an older version + committed = self.detector.matched_profile + committed_duration = self.detector.expected_duration_seconds + committed_profile = ( + self.profile_store.get_profile(committed) + if isinstance(committed, str) and committed + else None + ) + if committed_profile is not None: + self._current_program = committed + self._matched_profile_duration = self._profile_duration( + committed_duration + ) or self._profile_duration(committed_profile.get("avg_duration")) self._logger.info( - "Restored/Resurrected washer cycle with profile: %s", + "Restored washer cycle with profile: %s (duration=%.0fs)", self._current_program, + self._matched_profile_duration or 0.0, ) else: self._current_program = "detecting..." @@ -2234,6 +1916,23 @@ class WashDataManager: self._start_event_fired = bool( active_snapshot_to_restore.get("start_event_fired", False) ) + # One-shot per-cycle state (audit MANAGER-08); junk is dropped. + _fired = active_snapshot_to_restore.get("fired_cycle_timers") + self._fired_cycle_timers = { + int(i) for i in (_fired if isinstance(_fired, list) else []) + if isinstance(i, int) and not isinstance(i, bool) + } + self._notified_pre_completion = bool( + active_snapshot_to_restore.get("notified_pre_completion", False) + ) + _gaps = active_snapshot_to_restore.get("restart_gaps") + self._restart_gaps = [ + g for g in (_gaps if isinstance(_gaps, list) else []) + if isinstance(g, dict) + ] + self._live_activity_started = bool( + active_snapshot_to_restore.get("live_activity_started", False) + ) # Restore user-pause state from snapshot. self._is_user_paused = bool( @@ -2267,7 +1966,7 @@ class WashDataManager: # gap-fill analysis); synthetic fill is intentionally NOT added # to _power_readings to prevent circular-bias inflation. if last_save: - gap_end = dt_util.now() + gap_end = utc_now() gap_secs = (gap_end - last_save).total_seconds() if gap_secs > 30: self._restart_gaps.append({ @@ -2275,8 +1974,11 @@ class WashDataManager: "end_ts": gap_end.isoformat(), "gap_seconds": round(gap_secs, 1), "profile": self.detector.matched_profile, + # `_last_match_confidence`: the detector has no + # `match_confidence` attribute, so this was always + # None (audit DETECT-09). "match_confidence": getattr( - self.detector, "match_confidence", None + self.detector, "_last_match_confidence", None ), }) self._logger.info( @@ -2289,15 +1991,57 @@ class WashDataManager: self._start_watchdog() else: await self.profile_store.async_clear_active_cycle() - except Exception as err: - self._logger.warning("Failed to restore active cycle: %s, clearing", err) - await self.profile_store.async_clear_active_cycle() + except Exception: # noqa: BLE001 + await self._async_keep_failed_restore( + active_snapshot_to_restore, last_save, traceback.format_exc() + ) else: if last_save: - age = (dt_util.now() - last_save).total_seconds() + age = (utc_now() - last_save).total_seconds() self._logger.info("Active cycle too stale (age=%.0fs), clearing", age) await self.profile_store.async_clear_active_cycle() + async def _async_keep_failed_restore( + self, + snapshot: dict[str, Any], + last_save: datetime | None, + error: str, + *, + traceback_logged: bool = False, + ) -> None: + """A snapshot that could not be restored: say so, keep it, start from OFF. + + It used to be deleted after one log line, so the running cycle vanished + without a trace (register item 266 follow-up). It is kept, the last one per + device, for the diagnostics download; the active slot is cleared so the + next restart does not trip over it again, and the detector starts OFF so + the appliance's next cycle is detected normally. Never raises. + """ + now = utc_now() + age = (now - last_save).total_seconds() if last_save is not None else None + self._logger.warning( + "Could not restore the active cycle (state %r, snapshot age %s); kept it " + "for diagnostics and starting from OFF%s", + snapshot.get("state"), + f"{age:.0f}s" if age is not None else "unknown", + "" if traceback_logged else f":\n{error}", + ) + try: + self.detector.reset() + except Exception: # noqa: BLE001 + self._logger.debug("Detector reset after a failed restore raised", exc_info=True) + await self.profile_store.async_keep_failed_restore({ + "failed_at": now.isoformat(), + "last_active_save": last_save.isoformat() if last_save is not None else None, + "age_s": round(age, 1) if age is not None else None, + "error": error, + "snapshot": snapshot, + }) + try: + await self.profile_store.async_clear_active_cycle() + except Exception: # noqa: BLE001 + self._logger.debug("Clearing a failed snapshot raised", exc_info=True) + async def _async_repair_banked_tails(self) -> None: """One-time repair of cycles that banked the end-of-cycle confirmation delay as cycle time (register item 297). @@ -2385,6 +2129,62 @@ class WashDataManager: profile_name, ) + def _load_runtime_options(self, config_entry: Any) -> None: + """Manager-level tunables, read the same way at setup and on reload (388c).""" + options = config_entry.options + self._no_update_active_timeout = float( + options.get(CONF_NO_UPDATE_ACTIVE_TIMEOUT, DEFAULT_NO_UPDATE_ACTIVE_TIMEOUT) + ) + self._low_power_no_update_timeout = float( + options.get(CONF_LOW_POWER_NO_UPDATE_TIMEOUT, 3600.0) + ) + self._off_delay = float( + _option_then_data( + config_entry, CONF_OFF_DELAY, resolve_off_delay_default(self.device_type) + ) + ) + # 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( + options.get(CONF_LEARNING_CONFIDENCE, DEFAULT_LEARNING_CONFIDENCE), + DEFAULT_LEARNING_CONFIDENCE, + ) + self._auto_label_confidence = option_float( + options.get(CONF_AUTO_LABEL_CONFIDENCE, DEFAULT_AUTO_LABEL_CONFIDENCE), + DEFAULT_AUTO_LABEL_CONFIDENCE, + ) + # Clamped to >= 1, the same floor `SuggestionEngine` applies to this key: + # its interval cap is computed FROM this number, and zero would disable the + # gate rather than tighten it (every `counter >= 0` is true). + self._match_persistence = option_int( + options.get(CONF_MATCH_PERSISTENCE, DEFAULT_MATCH_PERSISTENCE), + DEFAULT_MATCH_PERSISTENCE, + minimum=1, + ) + self._unmatch_threshold = option_float( + options.get(CONF_PROFILE_UNMATCH_THRESHOLD), DEFAULT_PROFILE_UNMATCH_THRESHOLD + ) + store = getattr(self, "profile_store", None) + if store is not None and hasattr(store, "_unmatch_threshold"): + store._unmatch_threshold = self._unmatch_threshold # pylint: disable=protected-access + self._progress_reset_delay = int( + options.get(CONF_PROGRESS_RESET_DELAY, DEFAULT_PROGRESS_RESET_DELAY) + ) + # Read here, not only in the constructor (audit MANAGER-15): the last + # manager tunable an options reload left stale. + self._noise_events_threshold = option_int( + options.get( + CONF_AUTO_TUNE_NOISE_EVENTS_THRESHOLD, + DEFAULT_AUTO_TUNE_NOISE_EVENTS_THRESHOLD, + ), + DEFAULT_AUTO_TUNE_NOISE_EVENTS_THRESHOLD, + ) + async def async_setup(self) -> None: """Set up the manager.""" await self.profile_store.async_load() @@ -2411,30 +2211,6 @@ class WashDataManager: } except Exception: # noqa: BLE001 pass - # Apply configurable duration tolerance to profile store - try: - self.profile_store.set_duration_tolerance(self._profile_duration_tolerance) - self.profile_store.set_retention_limits( - max_past_cycles=int( - self.config_entry.options.get( - CONF_MAX_PAST_CYCLES, DEFAULT_MAX_PAST_CYCLES - ) - ), - max_full_traces_per_profile=int( - self.config_entry.options.get( - CONF_MAX_FULL_TRACES_PER_PROFILE, - DEFAULT_MAX_FULL_TRACES_PER_PROFILE, - ) - ), - max_full_traces_unlabeled=int( - self.config_entry.options.get( - CONF_MAX_FULL_TRACES_UNLABELED, - DEFAULT_MAX_FULL_TRACES_UNLABELED, - ) - ), - ) - except Exception: - pass # Re-scope custom phases stranded under another device type (#450). Cheap, # idempotent and saves only on a change, so it runs on every setup rather @@ -2453,6 +2229,9 @@ class WashDataManager: except Exception: # pylint: disable=broad-exception-caught self._logger.exception("Failed re-scoping custom phases for %s", self.entry_id) + # Preset maintenance reminders count from when they first apply (#461). + self._sync_maintenance_baselines() + # Repair broken sample_cycle_id references (can happen after aggressive retention) try: stats = await self.profile_store.async_repair_profile_samples() @@ -2471,6 +2250,9 @@ class WashDataManager: "Failed repairing profile sample references for %s", self.entry_id ) + # The idle display's standby level, from the stored cycles (#452). + await self._async_refresh_standby_level() + # Subscribe to power sensor updates (state changes AND unchanged re-reports) self._subscribe_power_sensor() @@ -2508,42 +2290,15 @@ class WashDataManager: 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): - # 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 + self._read_power_state_at_setup() # Trigger migration/compression of old cycle format # This is safe to run repeatedly (it skips already compressed cycles) await self.profile_store.async_migrate_cycles_to_compressed() - # Backfill match_confidence for labeled cycles that predate the field - self.hass.async_create_task( - self.profile_store.async_backfill_match_confidence() - ) + # Backfill match_confidence for labeled cycles that predate the field. + # Tracked (audit MANAGER-13): it saves the store. + self._spawn_tracked(self.profile_store.async_backfill_match_confidence()) # Subscribe to external cycle end trigger (if enabled) await self._setup_external_end_trigger() @@ -2560,6 +2315,16 @@ class WashDataManager: # Subscribe to person presence changes for notification gating await self._setup_notify_people_listener() + # HA does not unload entries on a stop, so the stop gets its own hook to keep + # the held notifications and a fresh snapshot (audit MANAGER-16); what it + # kept is re-dispatched once HA has started and notify services exist. + self._remove_ha_stop_listener = self.hass.bus.async_listen( + EVENT_HOMEASSISTANT_STOP, self._async_on_ha_stop + ) + self._remove_notify_queue_restore = async_at_started( + self.hass, self._schedule_notify_queue_restore + ) + # Register schedulers (maintenance + ML training). These are also re- # registered on every config reload; calling them here ensures they # survive HA restarts without requiring the user to re-save settings. @@ -2628,51 +2393,7 @@ class WashDataManager: self.config_entry = config_entry # Check if power sensor changed - new_sensor = config_entry.options.get( - CONF_POWER_SENSOR, config_entry.data.get(CONF_POWER_SENSOR) - ) - if new_sensor and new_sensor != self.power_sensor_entity_id: - # Block sensor changes when a cycle is active to prevent inconsistent state - d_state = self.detector.state - self._logger.debug( - "Reloading config: detector.state=%r (type=%s), RUNNING=%r", - d_state, - type(d_state), - STATE_RUNNING, - ) - if d_state in _SENSOR_SWAP_BLOCKED_STATES: - # Skip the sensor change but continue with the other config - # updates: returning here would silently drop every setting - # saved alongside the sensor in the same submission. - self._logger.warning( - "Cannot change power sensor from %s to %s while the " - "detector is in state %s. Please wait for the current " - "cycle to complete before changing the power sensor.", - self.power_sensor_entity_id, - new_sensor, - d_state, - ) - else: - self._logger.info( - "Power sensor changed: %s -> %s", self.power_sensor_entity_id, new_sensor - ) - self.power_sensor_entity_id = new_sensor - # Re-attach change + report listeners to the new sensor - # (helper removes the old ones first). - self._subscribe_power_sensor() - # 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): - _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 a " - "finite number: %r", - self.power_sensor_entity_id, - state.state, - ) + self._apply_power_sensor_option() # Update device type self.device_type = config_entry.options.get( @@ -2682,6 +2403,9 @@ 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 + # A saved reminder change (or an import) may switch a preset maintenance + # reminder on or off: re-stamp its counting origin (#461). + self._sync_maintenance_baselines() # Recompute the device-scaled unmatched-guard ceiling (#404): it tracks # device_type, which the reconfigure flow can change. self._unmatched_watchdog_ceiling = float( @@ -2693,42 +2417,19 @@ class WashDataManager: # Update detector config in-place old_min_power = self.detector.config.min_power old_off_delay = self.detector.config.off_delay - old_smoothing = self.detector.config.smoothing_window old_interrupted_min = self.detector.config.interrupted_min_seconds - # Get new values from config - new_min_power = float( - config_entry.options.get(CONF_MIN_POWER, DEFAULT_MIN_POWER) - ) - new_off_delay = int(config_entry.options.get(CONF_OFF_DELAY, DEFAULT_OFF_DELAY)) - # Device-resolved, like the constructor: the default is 8 min on a washing - # machine and an hour on a dishwasher, so falling back to the scalar would - # silently shorten the bridge on a reload. - new_min_off_gap = int( - config_entry.options.get( - CONF_MIN_OFF_GAP, resolve_min_off_gap_default(self.device_type) - ) - ) - new_smoothing = int( - config_entry.options.get(CONF_SMOOTHING_WINDOW, DEFAULT_SMOOTHING_WINDOW) - ) - new_interrupted_min = int( - config_entry.options.get( - CONF_INTERRUPTED_MIN_SECONDS, DEFAULT_INTERRUPTED_MIN_SECONDS - ) - ) - self.detector.config.match_interval = int( - config_entry.options.get( - CONF_PROFILE_MATCH_INTERVAL, DEFAULT_PROFILE_MATCH_INTERVAL - ) - ) - # Keep the detector's copy in sync so a panel edit takes effect without a - # restart, exactly like min_duration_ratio below. - self.detector.config.match_confidence_threshold = float( - config_entry.options.get( - CONF_PROFILE_MATCH_THRESHOLD, DEFAULT_PROFILE_MATCH_THRESHOLD - ) + # Every detector field from the one builder the constructor uses, applied + # onto the live config object in place (audit F2): the field-by-field copy + # that lived here is how the reload drifted from setup (items 351, 388a). + new_detector_config = build_detector_config( + config_entry.options, config_entry.data, self.device_type ) + new_min_power = new_detector_config.min_power + new_off_delay = new_detector_config.off_delay + new_interrupted_min = new_detector_config.interrupted_min_seconds + apply_detector_config(self.detector.config, new_detector_config) + self.profile_store.dtw_bandwidth = float( config_entry.options.get(CONF_DTW_BANDWIDTH, DEFAULT_DTW_BANDWIDTH) ) @@ -2759,151 +2460,6 @@ class WashDataManager: # swapped out (see _spawn_tracked). self._spawn_tracked(self.profile_store.async_rebuild_all_envelopes()) - # Device default - dev_def = DEVICE_COMPLETION_THRESHOLDS.get( - self.device_type, DEFAULT_COMPLETION_MIN_SECONDS - ) - new_completion_min = int( - config_entry.options.get(CONF_COMPLETION_MIN_SECONDS, dev_def) - ) - - new_start_threshold = float( - config_entry.options.get( - CONF_START_DURATION_THRESHOLD, - resolve_start_duration_default(self.device_type), - ) - ) - new_end_repeat_count = int( - config_entry.options.get(CONF_END_REPEAT_COUNT, DEFAULT_END_REPEAT_COUNT) - ) - - # Power Hysteresis Thresholds - new_start_threshold_w = float( - config_entry.options.get( - CONF_START_THRESHOLD_W, - float(new_min_power) + max(1.0, 0.1 * float(new_min_power)), - ) - ) - new_stop_threshold_w = float( - config_entry.options.get( - CONF_STOP_THRESHOLD_W, - max(0.0, float(new_min_power) - max(0.5, 0.1 * float(new_min_power))), - ) - ) - new_power_off_threshold_w = float( - config_entry.options.get( - CONF_POWER_OFF_THRESHOLD_W, DEFAULT_POWER_OFF_THRESHOLD_W - ) - ) - new_power_off_delay = float( - config_entry.options.get(CONF_POWER_OFF_DELAY, DEFAULT_POWER_OFF_DELAY) - ) - - new_start_energy = float( - config_entry.options.get( - CONF_START_ENERGY_THRESHOLD, - DEFAULT_START_ENERGY_THRESHOLDS_BY_DEVICE.get(self.device_type, 0.2) - ) - ) - new_end_energy = float( - config_entry.options.get(CONF_END_ENERGY_THRESHOLD, DEFAULT_END_ENERGY_THRESHOLD) - ) - - new_anti_wrinkle_enabled = bool( - config_entry.options.get( - CONF_ANTI_WRINKLE_ENABLED, DEFAULT_ANTI_WRINKLE_ENABLED - ) - ) - new_anti_wrinkle_max_power = float( - config_entry.options.get( - CONF_ANTI_WRINKLE_MAX_POWER, DEFAULT_ANTI_WRINKLE_MAX_POWER - ) - ) - new_anti_wrinkle_max_duration = float( - config_entry.options.get( - CONF_ANTI_WRINKLE_MAX_DURATION, DEFAULT_ANTI_WRINKLE_MAX_DURATION - ) - ) - new_anti_wrinkle_exit_power = float( - config_entry.options.get( - CONF_ANTI_WRINKLE_EXIT_POWER, DEFAULT_ANTI_WRINKLE_EXIT_POWER - ) - ) - new_anti_wrinkle_idle_timeout = float( - config_entry.options.get( - CONF_ANTI_WRINKLE_IDLE_TIMEOUT, DEFAULT_ANTI_WRINKLE_IDLE_TIMEOUT - ) - ) - new_dishwasher_end_spike_quiet_release = float( - config_entry.options.get( - CONF_DISHWASHER_END_SPIKE_QUIET_RELEASE, - DISHWASHER_END_SPIKE_QUIET_RELEASE_SECONDS, - ) - ) - new_smart_termination_duration_ratio = float( - config_entry.options.get( - CONF_SMART_TERMINATION_DURATION_RATIO, - 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 - ) - ) - new_delay_confirm_seconds = float( - config_entry.options.get( - CONF_DELAY_CONFIRM_SECONDS, DEFAULT_DELAY_CONFIRM_SECONDS - ) - ) - new_delay_timeout_seconds = float( - config_entry.options.get( - CONF_DELAY_TIMEOUT_HOURS, DEFAULT_DELAY_TIMEOUT_HOURS - ) - ) * 3600.0 - - # Apply all detector config updates - # #378: keep the detector's device_type in sync when the appliance type - # is changed in the UI, so the correct detection branch runs after reload. - self.detector.config.device_type = self.device_type - self.detector.config.min_power = new_min_power - self.detector.config.off_delay = new_off_delay - self.detector.config.min_off_gap = new_min_off_gap - self.detector.config.smoothing_window = new_smoothing - self.detector.config.interrupted_min_seconds = new_interrupted_min - self.detector.config.completion_min_seconds = new_completion_min - self.detector.config.start_duration_threshold = new_start_threshold - self.detector.config.end_repeat_count = new_end_repeat_count - self.detector.config.start_threshold_w = new_start_threshold_w - self.detector.config.stop_threshold_w = new_stop_threshold_w - self.detector.config.power_off_threshold_w = new_power_off_threshold_w - self.detector.config.power_off_delay = new_power_off_delay - self.detector.config.start_energy_threshold = new_start_energy - self.detector.config.end_energy_threshold = new_end_energy - self.detector.config.anti_wrinkle_enabled = new_anti_wrinkle_enabled - self.detector.config.anti_wrinkle_max_power = new_anti_wrinkle_max_power - self.detector.config.anti_wrinkle_max_duration = new_anti_wrinkle_max_duration - self.detector.config.anti_wrinkle_exit_power = new_anti_wrinkle_exit_power - 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 - # Pump Monitor setting self._pump_stuck_duration = int( config_entry.options.get(CONF_PUMP_STUCK_DURATION, DEFAULT_PUMP_STUCK_DURATION) @@ -2912,18 +2468,15 @@ class WashDataManager: if ( old_min_power != new_min_power or old_off_delay != new_off_delay - or old_smoothing != new_smoothing or old_interrupted_min != new_interrupted_min ): self._logger.info( "Updated detector config: min_power %.1fW->%.1fW, off_delay %ds->%ds, " - "smoothing %d->%d, interrupted_min %ds->%ds", + "interrupted_min %ds->%ds", old_min_power, new_min_power, old_off_delay, new_off_delay, - old_smoothing, - new_smoothing, old_interrupted_min, new_interrupted_min, ) @@ -2934,9 +2487,7 @@ class WashDataManager: new_min_ratio = float( config_entry.options.get( CONF_PROFILE_MATCH_MIN_DURATION_RATIO, - DEFAULT_PROFILE_MATCH_MIN_DURATION_RATIO_BY_DEVICE.get( - self.device_type, DEFAULT_PROFILE_MATCH_MIN_DURATION_RATIO - ), + DEFAULT_PROFILE_MATCH_MIN_DURATION_RATIO, ) ) new_max_ratio = float( @@ -2950,10 +2501,6 @@ class WashDataManager: self.profile_store.set_duration_ratio_limits( min_ratio=new_min_ratio, max_ratio=new_max_ratio ) - # Keep the detector's copy in sync: _should_defer_finish() reads - # detector.config.min_duration_ratio, which otherwise keeps the - # construction-time value until a restart (diverging from the matcher). - self.detector.config.min_duration_ratio = new_min_ratio self._logger.info( "Updated duration ratios: min %.2f→%.2f, max %.2f→%.2f", old_min_ratio, @@ -2974,11 +2521,6 @@ class WashDataManager: self._logger.info("Updated match interval: %ds→%ds", old_interval, new_interval) # Update other configurable options - self._profile_duration_tolerance = float( - config_entry.options.get( - CONF_PROFILE_DURATION_TOLERANCE, DEFAULT_PROFILE_DURATION_TOLERANCE - ) - ) # Update notification settings @@ -3106,9 +2648,6 @@ class WashDataManager: # Trigger entity updates to reflect any changes async_dispatcher_send(self.hass, f"ha_washdata_update_{self.entry_id}") - if self.detector: - self.detector.config.profile_duration_tolerance = self._profile_duration_tolerance - # Schedule midnight maintenance if enabled await self._setup_maintenance_scheduler() @@ -3149,11 +2688,94 @@ class WashDataManager: self._stop_watchdog() self._start_watchdog() - # RESTORE STATE (only if recent enough, otherwise treat as stale) - await self._attempt_state_restoration() + # Manager-level settings the reload used to skip (item 388c): one loader + # shared with __init__, so the two cannot drift again. + self._load_runtime_options(config_entry) + + # RESTORE STATE (only if recent enough, otherwise treat as stale) - but + # never over a cycle that is running right now (item 388b). An options + # reload keeps the live detector; restoring the snapshot (up to 60 s old) + # over it rolled the cycle back: readings dropped, a phantom restart gap + # written onto the cycle, the quiet timers reset. + # The in-place reload never restores the snapshot at all (audit MANAGER-04): + # the live detector is always fresher than a snapshot of up to 60 s ago, and + # an IDLE detector with a snapshot still present means the cycle-end tail + # has not cleared it yet - restoring then re-opened the finished cycle and + # ended it a second time (two stored copies, two pushes, double energy). + self._logger.debug( + "Options reload: keeping the live detector state (%s), not restoring " + "the snapshot", + self.detector.state, + ) + # The idle display's standby level depends on the stop/start thresholds. + await self._async_refresh_standby_level() self._logger.info("Configuration reloaded successfully") + def _apply_power_sensor_option(self) -> None: + """Re-point the power listener at the configured sensor, if it changed. + + Never while a cycle is under way (_SENSOR_SWAP_BLOCKED_STATES): the swap is + parked in ``_pending_power_sensor`` and ``_on_state_change`` applies it once + the detector leaves those states (audit MANAGER-11). It used to be dropped + until the next reload, while the panel already showed the new sensor. + """ + new_sensor = self.config_entry.options.get( + CONF_POWER_SENSOR, self.config_entry.data.get(CONF_POWER_SENSOR) + ) + if not new_sensor or new_sensor == self.power_sensor_entity_id: + self._pending_power_sensor = None + return + d_state = self.detector.state + if d_state in _SENSOR_SWAP_BLOCKED_STATES: + # Park the change but continue with the other config updates: returning + # from the reload would silently drop every setting saved alongside the + # sensor in the same submission. + self._pending_power_sensor = new_sensor + self._logger.warning( + "Power sensor change %s -> %s deferred: the detector is in state %s. " + "It takes effect when the current cycle ends.", + self.power_sensor_entity_id, + new_sensor, + d_state, + ) + return + self._pending_power_sensor = None + self._logger.info( + "Power sensor changed: %s -> %s", self.power_sensor_entity_id, new_sensor + ) + self.power_sensor_entity_id = new_sensor + # Re-attach change + report listeners to the new sensor + # (helper removes the old ones first). + self._subscribe_power_sensor() + # 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): + _reload_power = _finite_power(state.state) + if _reload_power is not None: + self.detector.process_reading(_reload_power, utc_now()) + else: + self._logger.debug( + "Initial power value for %s after config reload is not a " + "finite number: %r", + self.power_sensor_entity_id, + state.state, + ) + + async def _async_apply_pending_power_sensor(self) -> None: + """Apply a parked sensor swap (see _apply_power_sensor_option). + + A task, not inline in _on_state_change: that callback runs inside the + detector's own process_reading, and the swap feeds the new sensor's + reading straight back into it. HA starts tasks eagerly, so yield once + first or the body would still run inside that call. + """ + await asyncio.sleep(0) + if self._is_shutdown or self._pending_power_sensor is None: + return + self._apply_power_sensor_option() + self._notify_update() + def _spawn_tracked(self, coro: Coroutine[Any, Any, Any]) -> Task[Any]: """Create a detached task and track it so shutdown can cancel it. @@ -3191,6 +2813,14 @@ class WashDataManager: # Drain cancelled tasks so they don't race the freshly-reloaded ProfileStore. if _to_await: await asyncio.gather(*_to_await, return_exceptions=True) + for _unsub_name in ("_remove_ha_stop_listener", "_remove_notify_queue_restore"): + _unsub = getattr(self, _unsub_name, None) + if _unsub is not None: + _unsub() + setattr(self, _unsub_name, None) + # Keep the held notifications for the entry's next setup (audit MANAGER-16) + # before the queues are cleared below. + await self._async_persist_notification_queues() if self._remove_listener: self._remove_listener() if self._remove_report_listener: @@ -3250,10 +2880,29 @@ class WashDataManager: self._logger.debug("Failed to clear live notification on shutdown", exc_info=True) # Save active state before shutdown - if self.detector.state in {STATE_RUNNING, STATE_PAUSED, STATE_STARTING, STATE_ENDING}: + if ( + self.detector.state in {STATE_RUNNING, STATE_PAUSED, STATE_STARTING, STATE_ENDING} + or self._in_anticrease_tail() + ): snapshot = self._augment_active_snapshot(self.detector.get_state_snapshot()) await self.profile_store.async_save_active_cycle(snapshot) + # A debounced cycle-end write still pending must land before a reload loads + # this store again from disk (HA's own final write covers only a stop), and + # nothing may be debounced past this point. + # A task cancelled above may have been on its way to a save (the learning + # pass's feedback request, a suggestion cleanup, audit MANAGER-13): its + # change is in memory but never reached disk, so write the store once now + # rather than lose it to the reload that reads the file next. + try: + self.profile_store.coalesce_saves(0) + if _to_await: + await self.profile_store.async_save() + else: + await self.profile_store.async_flush_saves() + except Exception: # noqa: BLE001 - never block an unload on a save + self._logger.debug("Flushing pending store writes failed", exc_info=True) + self._last_reading_time = None async def _setup_external_end_trigger(self) -> None: @@ -3341,7 +2990,7 @@ class WashDataManager: # by the distinct entities a user has ever chosen here. anchors = self.hass.data.setdefault(_UNLOAD_CONFIRM_ANCHOR_KEY, {}) if isinstance(anchors, dict): - anchors.setdefault(entity_id, dt_util.now()) + anchors.setdefault(entity_id, utc_now()) async def _setup_price_listener(self) -> None: """Subscribe to the dynamic energy price entity (#426). @@ -3384,7 +3033,7 @@ class WashDataManager: return try: price = float(new_state.state) - except (ValueError, TypeError): + except (ValueError, TypeError, OverflowError): # unknown/unavailable/non-numeric: carry the last known price forward # rather than charging the cycle at zero for the outage. return @@ -3408,7 +3057,7 @@ class WashDataManager: return try: value = round(float(price), PRICE_TIMELINE_PRICE_DECIMALS) - except (ValueError, TypeError): + except (ValueError, TypeError, OverflowError): return if not math.isfinite(value): # Same rule as _finite_power: "nan"/"inf" parse cleanly and would ride @@ -3417,7 +3066,7 @@ class WashDataManager: return if self._price_timeline and self._price_timeline[-1][1] == value: return - self._price_timeline.append((dt_util.now().timestamp(), value)) + self._price_timeline.append((utc_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. @@ -3490,7 +3139,7 @@ class WashDataManager: self.detector.set_verified_pause(True) if not self._is_user_paused: self._is_user_paused = True - self._user_pause_start = dt_util.now() + self._user_pause_start = utc_now() self._notify_update() else: # Door closed: cancel a pending auto-open finalize (it was a brief open, @@ -3575,7 +3224,7 @@ class WashDataManager: return anchors = self.hass.data.setdefault(_UNLOAD_CONFIRM_ANCHOR_KEY, {}) if isinstance(anchors, dict): - anchors[entity_id] = dt_util.now() + anchors[entity_id] = utc_now() def _in_unload_confirm_replay_window(self) -> bool: """Whether this entity came back too recently to trust ``unknown -> value``. @@ -3594,7 +3243,7 @@ class WashDataManager: if not isinstance(anchor, datetime): return True try: - elapsed = (dt_util.now() - anchor).total_seconds() + elapsed = (utc_now() - anchor).total_seconds() except (TypeError, ValueError, OverflowError): return True return elapsed < UNLOAD_CONFIRM_REPLAY_GRACE_S @@ -3705,7 +3354,7 @@ class WashDataManager: stop_thr = 0.0 try: stop_thr = float(getattr(self.detector.config, "stop_threshold_w", 0.0) or 0.0) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): stop_thr = 0.0 if stop_thr > 0.0 and self._current_power >= stop_thr: self._logger.debug( @@ -3887,7 +3536,7 @@ class WashDataManager: try: hour = int(opts.get(CONF_ML_TRAINING_HOUR, DEFAULT_ML_TRAINING_HOUR)) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): hour = DEFAULT_ML_TRAINING_HOUR hour = max(0, min(23, hour)) @@ -3918,8 +3567,8 @@ class WashDataManager: return {"ok": False, "reason": "ml_training_disabled"} opts = {**self.config_entry.data, **self.config_entry.options} - # Snapshot on the event loop before any executor offload (training + - # matcher tuning): get_past_cycles() returns the live mutable list, so a + # Snapshot on the event loop before any executor offload (training): + # get_past_cycles() returns the live mutable list, so a # concurrent cycle add / retention trim could otherwise change the input # mid-run. cycles = list(self.profile_store.get_past_cycles()) @@ -3945,7 +3594,7 @@ class WashDataManager: ) last = self._last_ml_training_at() if last is not None: - age_days = (dt_util.now() - last).total_seconds() / 86400.0 + age_days = (utc_now() - last).total_seconds() / 86400.0 if age_days < interval_days: self._logger.debug( "Skipping scheduled ML training: retrained %.1fd ago (<%dd)", @@ -3972,8 +3621,8 @@ class WashDataManager: promoted = list(summary.get("promoted", {}).keys()) if promoted: self._ml_training_failures = 0 - # Consumers (ML Lab, MLSuggestionEngine) read the trained specs live - # from the store via ml.engine.resolve_scorer, so no refresh is needed. + # Consumers read the trained specs live from the store via + # ml.engine.resolve_regressor, so no refresh is needed. self._logger.info("On-device ML training promoted models: %s", promoted) else: self._ml_training_failures += 1 @@ -3982,17 +3631,12 @@ class WashDataManager: self._ml_training_failures, ) - # Stage 4/5: tune the matcher's scoring weights from this device's own - # cycles (same held-out promotion discipline as the models). Independent - # of model promotion; runs on every training pass. - matching = await self._tune_matching_config(cycles) - # Record that training *ran* now, regardless of whether anything was # promoted, so "Last trained" advances on every run (a run that doesn't # beat the baseline previously left the timestamp stuck at the last # promotion). Never let a persistence hiccup break the run. try: - _run_iso = dt_util.now().isoformat() + _run_iso = utc_now().isoformat() await self.profile_store.set_ml_last_training_run(_run_iso) # Track each capability's held-out score over time (drift/fit trend). await self.profile_store.append_ml_training_history( @@ -4008,60 +3652,17 @@ class WashDataManager: "device_name": self.config_entry.title, "promoted": promoted, "results": summary.get("results", []), - "matching": matching, }, ) - # A promoted model changes the health-model signature, so recompute the - # persisted per-cycle health now rather than lazily on the next view. - if promoted: - try: - await self.async_recompute_cycle_health() - except Exception as err: # noqa: BLE001 - self._logger.debug("Post-training health recompute failed: %s", err) + # (No health recompute: the per-cycle health reads the quality / end + # models, and since 0.5.8 training promotes only total_energy.) return { "ok": True, "promoted": promoted, "results": summary.get("results", []), - "matching": matching, } - async def _tune_matching_config(self, cycles: list[dict[str, Any]]) -> dict[str, Any]: - """Tune + (if it beats the shipped defaults on a held-out split) persist - the matcher's scoring weights for this device. Executor-offloaded and - never raises. Returns the tuner status dict for logging / the UI event. - """ - try: - from .ml.matching_tuner import tune_matching_config - - result = await self.hass.async_add_executor_job( - tune_matching_config, cycles, self.device_type - ) - except Exception as err: # noqa: BLE001 - tuning must never break training - self._logger.debug("Matching-config tuning failed: %s", err) - return {"promoted": False, "reason": "exception", "error": str(err)} - - if result.get("promoted") and result.get("config"): - record = { - "config": result["config"], - "trained_at": dt_util.now().isoformat(), - "cycle_count": len(cycles), - "baseline_test_top1": result.get("baseline_test_top1"), - "tuned_test_top1": result.get("tuned_test_top1"), - } - await self.profile_store.set_matching_config(record) - self._logger.info( - "On-device matcher tuning promoted (top-1 %.3f -> %.3f): %s", - result.get("baseline_test_top1") or 0.0, - result.get("tuned_test_top1") or 0.0, - result["config"], - ) - else: - self._logger.debug( - "On-device matcher tuning not promoted: %s", result.get("reason") - ) - return result - async def async_recompute_cycle_health(self) -> int: """Recompute + persist per-cycle ML health against the current model. @@ -4076,12 +3677,10 @@ class WashDataManager: from .ws_api import _compute_ml_comparison # pylint: disable=import-outside-toplevel except Exception: # pylint: disable=broad-exception-caught return 0 - opts = {**self.config_entry.data, **self.config_entry.options} - off_delay = int(opts.get(CONF_OFF_DELAY, DEFAULT_OFF_DELAY)) try: result = await self.hass.async_add_executor_job( functools.partial( - _compute_ml_comparison, self.profile_store, off_delay, force_recompute=True + _compute_ml_comparison, self.profile_store, force_recompute=True ) ) except Exception as err: # noqa: BLE001 @@ -4153,7 +3752,7 @@ class WashDataManager: 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)) + horizon = utc_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 @@ -4227,7 +3826,7 @@ class WashDataManager: continue try: parsed = dt_util.parse_datetime(ts) - except (ValueError, TypeError): + except (ValueError, TypeError, OverflowError): parsed = None if parsed is not None and (latest is None or parsed > latest): latest = parsed @@ -4265,11 +3864,17 @@ class WashDataManager: """Handle power sensor state change.""" event_data = cast(dict[str, Any], getattr(event, "data", {})) new_state = cast(State | None, event_data.get("new_state")) + # A state with no usable value is a dead sensor, not a silent one: record + # it, so the detector stops crediting quiet until the next real reading + # (register item 266, audit DETECT-13). Returning without a trace left + # the watchdog's keepalives to run out the end gates during a dropout. if new_state is None or new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE): + self.detector.mark_sensor_unavailable(utc_now()) return power = _finite_power(new_state.state) if power is None: + self.detector.mark_sensor_unavailable(utc_now()) return # Capture every raw sensor reading before any throttling or processing. @@ -4285,11 +3890,11 @@ class WashDataManager: if self.recorder.is_recording: self.recorder.process_reading(power) self._current_power = power - self._last_reading_time = dt_util.now() + self._last_reading_time = utc_now() self._notify_update() return - now = dt_util.now() + now = utc_now() # Throttle updates to avoid CPU overload on noisy sensors. # Low-power readings bypass throttling when: @@ -4343,27 +3948,92 @@ class WashDataManager: power, now, self._last_reading_time ) self._last_reading_time = now - self._last_real_reading_time = now # Track real update + if self._restored_sensor_clock is not None: + # The entity appeared only after setup, so this is its startup write + # (register item 266): same rule as the setup read. + self._seed_real_reading_clock( + power, report_ts if isinstance(report_ts, datetime) else now + ) + else: + self._last_real_reading_time = now # Track real update self._current_power = power - self.detector.process_reading(power, now) + # One entity refresh per reading, the one at the end (register item 456): + # _update_estimates and a match that completes inside process_reading (a + # trace still too short to score returns without awaiting) each sent their + # own first, all of ~20 entities rewritten 2-4 times for one reading. + self._in_power_event = True + try: + self.detector.process_reading(power, now) + self._check_stall_event() # #452: only a real reading can start a stall - if self._cycle_start_time is None and self.detector.current_cycle_start is not None: - self._cycle_start_time = self.detector.current_cycle_start + if self._cycle_start_time is None and self.detector.current_cycle_start is not None: + self._cycle_start_time = self.detector.current_cycle_start - # If running (or paused/ending), try to match profile and update estimates - if self.detector.state in ( - STATE_RUNNING, - STATE_PAUSED, - STATE_ENDING, - STATE_STARTING, - ): - self._update_estimates() - # Periodically save state every 60s to avoid flash wear - # We need a tracker. - self._check_state_save(now) + # If running (or paused/ending), try to match profile and update estimates + if self.detector.state in ( + STATE_RUNNING, + STATE_PAUSED, + STATE_ENDING, + STATE_STARTING, + ): + self._update_estimates() + # Periodically save state every 60s to avoid flash wear + # We need a tracker. + self._check_state_save(now) + finally: + self._in_power_event = False self._notify_update() + def _check_stall_event(self) -> None: + """Fire EVENT_CYCLE_STALLED once per stall (discussion #452). + + Display and automation only, never a notification. The payload is the + detector's small ``stall_info`` (no trace), far under the 32 KB limit. + """ + if getattr(self.detector, "stalled", False) is not True: + return + info = self.detector.stall_info() + if not isinstance(info, dict): + return + key = info.get("stalled_since") + if key == getattr(self, "_stall_event_key", None): + return + self._stall_event_key = key + if not self._notify_fire_events: + return + self.hass.bus.async_fire( + EVENT_CYCLE_STALLED, + { + "entry_id": self.entry_id, + "device_name": self.config_entry.title, + "device_type": self.device_type, + "program": self._current_program, + **info, + }, + ) + + async def _async_refresh_standby_level(self) -> None: + """Re-learn the standby level the idle display reads (#452). Never raises.""" + try: + cfg = self.detector.config + cycles = [ + dict(c) for c in list(self.profile_store.get_past_cycles() or [])[ + -STANDBY_LEVEL_RECENT_CYCLES: + ] + if isinstance(c, dict) + ] + level = await self.hass.async_add_executor_job( + learned_standby_level_w, + cycles, + float(cfg.stop_threshold_w), + float(cfg.start_threshold_w), + ) + self.detector.set_standby_level(level) + self._logger.debug("Idle display standby level: %s W", level) + except Exception: # noqa: BLE001 - a display statistic must never break setup + self._logger.debug("Could not learn the standby level", exc_info=True) + def _check_state_save(self, now: datetime) -> None: """Periodically save active state.""" last_save = getattr(self, "_last_state_save", None) @@ -4372,57 +4042,52 @@ class WashDataManager: # Inject manual program flag into snapshot before saving snapshot = self._augment_active_snapshot(self.detector.get_state_snapshot()) - self.hass.async_create_task( - self.profile_store.async_save_active_cycle(snapshot) - ) + # Tracked (audit MANAGER-13): an unload cancels it and writes its own. + self._spawn_tracked(self.profile_store.async_save_active_cycle(snapshot)) self._last_state_save = now - async def _run_final_match_from_cycle_data(self, cycle_data: dict[str, Any]) -> None: - """Run final profile match using the cycle's power data before it's saved. + def _save_snapshot_while_silent(self, now: datetime) -> None: + """Keep the active snapshot current through a silence (register item 266). - This is called from _on_cycle_end when _current_program is still 'detecting...' - to ensure we try matching with complete cycle data before persistence. + Saves were driven by real readings only, so a cycle waiting out a silent + tail kept the snapshot of its last report: a crash then restored that + moment's quiet tally, recorded the watched silence as a restart gap, and + aged the snapshot by time Home Assistant had in fact been watching - past + the restore window on a long drying tail. Same 60 s throttle. Never + raises: a lost save only costs a restore, the watchdog tick must finish. + """ + if self.detector.state not in ( + STATE_STARTING, STATE_RUNNING, STATE_PAUSED, STATE_ENDING + ): + return + try: + self._check_state_save(now) + except Exception: # noqa: BLE001 + self._logger.debug("Could not save the active cycle in a silence", exc_info=True) + + async def _run_final_match_from_cycle_data( + self, cycle_data: dict[str, Any] + ) -> MatchResult | None: + """Match the COMPLETE cycle once, before it is saved; None if too short. + + No side effects: the caller decides what to adopt, because a new cycle can + start during the await and the live fields would then belong to it (B1). """ # Cycle data from detector stores power_data as [[offset_seconds, power], ...], - # where offsets are relative to cycle start. - power_data = cycle_data.get("power_data", []) - duration = cycle_data.get("duration", 0) - - if not power_data or len(power_data) < 10: + # where offsets are relative to cycle start. Shared with the Playground's + # would_label (match_rules.final_match_input). + final_input = match_rules.final_match_input(cycle_data) + if final_input is None: self._logger.debug("Insufficient power data for final match (< 10 readings)") - return + return None + power_data, duration = final_input - # power_data is already in [[offset_seconds, power], ...] format for matching. - self._logger.info( + self._logger.debug( "Running final match from cycle data: %s samples, %.0fs duration", len(power_data), duration, ) - - result = await self.profile_store.async_match_profile(power_data, duration) - profile_name = result.best_profile - confidence = result.confidence - - # Store result for debug data - self._last_match_result = result - - # Accept match at lower threshold since cycle is complete - # Also ignore ambiguity for completed cycles - pick the best match - if profile_name and confidence >= 0.15: - self._logger.info( - "Final match from cycle data: '%s' with confidence %.3f", - profile_name, - confidence, - ) - 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)", - profile_name, - confidence, - ) + return await self.profile_store.async_match_profile(power_data, duration) def _start_watchdog(self) -> None: """Start the watchdog timer when a cycle begins.""" @@ -4523,6 +4188,10 @@ class WashDataManager: not self._cycle_completed_time or self.detector.state == STATE_RUNNING or self.detector.state == STATE_DELAY_WAIT + # A probe out of a terminal state (item 515): the overlay waits for its + # outcome. Resetting the detector here would kill a real start (#267), + # and a nag must not fire into a new load; an abort resumes the timers. + or self.detector.state == STATE_STARTING ): # Cycle is running or not completed, don't reset return @@ -4569,6 +4238,7 @@ class WashDataManager: fallback_template=DEFAULT_NOTIFY_UNLOAD_MESSAGE, device=self.config_entry.title, duration=duration_min, + duration_hm=self._format_duration_hm(duration_min), delay=self._notify_unload_delay_minutes, ) extra_vars: dict[str, Any] = {"tag": self._clean_tag} @@ -4761,11 +4431,11 @@ class WashDataManager: if self._power_off_below_since is None or self._current_power >= pot: return if ( - dt_util.now() - self._power_off_below_since + utc_now() - self._power_off_below_since ).total_seconds() < cfg.power_off_delay: return # Honour the clean-laundry unload nag hold (mirrors the poll path). - if self._unload_nag_active(dt_util.now()): + if self._unload_nag_active(utc_now()): return self._logger.debug( "Power-based Off (one-shot timer): %.2fW below %.2fW for >= %.0fs in %s. " @@ -4823,6 +4493,70 @@ class WashDataManager: return None return power, report_ts + def _read_power_state_at_setup(self) -> None: + """Feed the power entity's current state once at setup and seed the caches.""" + state = self.hass.states.get(self.power_sensor_entity_id) + if state and state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE): + # 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 = utc_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._seed_real_reading_clock( + power, + getattr(state, "last_reported", None) or state.last_updated, + ) + except (ValueError, TypeError, OverflowError): + pass + + def _seed_real_reading_clock(self, power: float, report_ts: datetime) -> None: + """Set the silence clock from the power entity's first state after a + restart (register item 266): the setup read, or the first event when the + entity only appears after setup. + + After a restart every entity is written afresh, so the entity's timestamp + says "the sensor just spoke" even when the plug has been silent for an + hour. For a restored cycle whose sensor still holds the value it held + before the restart, that write carries nothing new: keep the restored + clock, so the watchdog's keepalive, ghost and staleness rules go on + measuring the real silence. A different value is a genuine report and + takes the entity's time, as does every setup with nothing restored. The + report is remembered so the watchdog's resync does not hand it back as + a missed one and reseed the clock a tick later. + """ + restored, self._restored_sensor_clock = self._restored_sensor_clock, None + if ( + restored is not None + and restored[1] is not None + and math.isclose(power, restored[1], rel_tol=0.0, abs_tol=1e-6) + and self.detector.state in (STATE_RUNNING, STATE_PAUSED, STATE_ENDING) + ): + self._last_real_reading_time = restored[0] + self._setup_report_ts = report_ts + self._logger.info( + "Power sensor still reads %.2fW, as before the restart: keeping its " + "last report at %s as the silence clock", + power, + restored[0], + ) + return + self._last_real_reading_time = 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). @@ -4848,7 +4582,7 @@ class WashDataManager: missed = ( self._last_real_reading_time is None or report_ts > self._last_real_reading_time - ) + ) and report_ts != self._setup_report_ts # setup already took it (item 266) if feed_detector and missed: self._logger.debug( "Resync: sensor reported %.2fW at %s but the last processed reading " @@ -4863,6 +4597,34 @@ class WashDataManager: self._last_real_reading_time = report_ts self._current_power = power + def _low_power_silence_budget_s( + self, elapsed: float, expected: float, verified_pause: bool + ) -> float: + """How long a low-power wait may go without a real reading before the + watchdog force-ends it as stale. The restart path asks the same question + of a snapshot (register item 266), so the two cannot drift apart. + """ + # Dishwashers can have very long silent drying phases (up to 2h) + # We use the device-specific timeout as the floor for this effective timeout. + # The floor is applied unconditionally - dishwashers have passive drying phases + # even when no profile has been matched yet. The original restriction to matched + # cycles caused premature kills: with the default 3600s timeout, an unmatched + # dishwasher cycle was killed ~1h after the last sensor update, while the + # physical drying phase could still have 1-2h of silent runtime remaining. + budget = max( + float(DEFAULT_NO_UPDATE_ACTIVE_TIMEOUT_BY_DEVICE.get(self.device_type, 0)), + float(self._low_power_no_update_timeout), + ) + # Profile-Aware Extension: with a matched profile, never kill during the + # expected duration - remaining + 1800 s (30 min buffer for drying/pause). + if expected > 0 and elapsed < expected: + budget = max(budget, expected - elapsed + 1800) + # Verified Pause Extension: a confirmed legitimate pause (e.g. drying) gets + # up to the global deferral limit + the same buffer. + if verified_pause: + budget = max(budget, DEFAULT_MAX_DEFERRAL_SECONDS + 1800) + return budget + 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): @@ -4896,8 +4658,12 @@ class WashDataManager: # readings; it no-ops until a profile is matched. Kept ahead of the # keepalive/force-end branches below so even a verified-pause drying tail # (which skips those branches) still ticks down. + # The "almost done" reminder reads that same estimate, so it is checked here + # too: on the power path alone a silent tail delayed it to the next reading, + # typically the final pump-out, alongside the finish (audit PROGRESS-07). if self.detector.state in (STATE_RUNNING, STATE_PAUSED, STATE_ENDING): self._update_remaining_only() + self._check_pre_completion_notification() self._notify_update() time_since_any_update = (now - self._last_reading_time).total_seconds() @@ -5046,43 +4812,9 @@ class WashDataManager: # If we are in a low power state (waiting for off_delay or drying profile), # we treat silence leniently. We inject keepalives until the stricter # low_power_no_update_timeout is reached. - - # Dishwashers can have very long silent drying phases (up to 2h) - # We use the device-specific timeout as the floor for this effective timeout. - # The floor is applied unconditionally - dishwashers have passive drying phases - # even when no profile has been matched yet. The original restriction to matched - # cycles caused premature kills: with the default 3600s timeout, an unmatched - # dishwasher cycle was killed ~1h after the last sensor update, while the - # physical drying phase could still have 1-2h of silent runtime remaining. - low_power_floor = DEFAULT_NO_UPDATE_ACTIVE_TIMEOUT_BY_DEVICE.get( - self.device_type, 0 + effective_low_power_timeout = self._low_power_silence_budget_s( + elapsed, expected, bool(getattr(self.detector, "_verified_pause", False)) ) - effective_low_power_timeout = max( - low_power_floor, self._low_power_no_update_timeout - ) - - # Profile-Aware Extension: - # If we have a matched profile, ensure we don't kill during the expected duration. - if expected > 0 and elapsed < expected: - # Extend timeout to cover the remaining expected duration + buffer - remaining = expected - elapsed - # Allow silence up to remaining + 1800s (30m buffer for drying/pause) - extended_timeout = remaining + 1800 - if extended_timeout > effective_low_power_timeout: - effective_low_power_timeout = extended_timeout - - # Verified Pause Extension: - # If the manager/store has confirmed this is a legitimate pause (e.g. Drying), - # allow even more leniency up to the global deferral limit. - if getattr(self.detector, "_verified_pause", False): - # Allow silence up to DEFAULT_MAX_DEFERRAL_SECONDS (default 2h) + buffer - pause_limit = DEFAULT_MAX_DEFERRAL_SECONDS + 1800 - if pause_limit > effective_low_power_timeout: - effective_low_power_timeout = pause_limit - self._logger.debug( - "Watchdog: Extending timeout to %.0fs due to verified pause", - effective_low_power_timeout - ) if self.detector.is_waiting_low_power(): @@ -5133,6 +4865,15 @@ class WashDataManager: # is untouched by how often we sample. if time_since_real_update > self._watchdog_interval: _ka_w, _ka_obs = self._keepalive_reading() + # On time, this tick closes at most two intervals (the first one + # after a real reading) and then one. A longer one means the tick + # itself was late - host suspend, event-loop stall, a restart - + # and nobody watched the sensor in between, so the gap-free tally + # must not bank it (register item 391). Only the two shorten-only + # consumers of that tally read `observed`, so a false "late" + # costs end lag, never an early end. + if time_since_any_update > WATCHDOG_LATE_TICK_FACTOR * self._watchdog_interval: + _ka_obs = False self._logger.debug( "Watchdog: Low-power sensor silence (%.0fs > watchdog interval " "%ss). Injecting %.2fW keepalive (observed=%s) to advance " @@ -5151,6 +4892,7 @@ class WashDataManager: ) self._last_reading_time = now self._current_power = _ka_w + self._save_snapshot_while_silent(now) self._notify_update() return @@ -5175,6 +4917,7 @@ class WashDataManager: ) self._last_reading_time = now self._current_power = _ka_w + self._save_snapshot_while_silent(now) self._notify_update() return @@ -5221,6 +4964,7 @@ class WashDataManager: limit, ) self._last_reading_time = now + self._save_snapshot_while_silent(now) self._notify_update() return @@ -5239,14 +4983,27 @@ class WashDataManager: """Handle state change from detector.""" self._logger.debug("Washer state changed: %s -> %s", old_state, new_state) self.diag_buffer.record_state( - old_state, new_state, self._current_program, dt_util.now() + old_state, new_state, self._current_program, utc_now() ) - # A new cycle starting while we are still showing the completed/Clean - # overlay (the progress-reset window) must clear that overlay and cancel - # the expiry timer right away, so the UI leaves "Finished" and the unload - # nag stops immediately instead of waiting for the reset window - and so - # the expiry timer cannot race the new cycle and reset us to OFF (#267). - if new_state == STATE_STARTING and self._cycle_completed_time is not None: + # A start from idle owns no update intervals yet: drop anything a false + # start (STARTING -> OFF, which ends no cycle) left pending, so it is not + # committed with this cycle (#458). DELAY_WAIT is idle too, and since item + # 504 its false starts return there, so hours of standby probes would + # otherwise reach the next completed cycle; since item 515 so do the + # terminal states' (the cycle end already closed the previous cycle's). + if new_state == STATE_STARTING and old_state in CADENCE_RESET_FROM_STATES: + self.learning_manager.discard_cycle_cadence() + # The completed/Clean overlay (the cycle end, Clean, the unload nag, the + # 100 % progress) is cleared when a new cycle COMMITS, in the RUNNING + # branch below, not when a probe begins (register item 515): most probes + # out of Finished abort, the detector returns to the terminal state, and + # clearing here lost Clean and the nag to a blip. The expiry timer keeps + # running through the probe and skips STARTING (#267, _handle_state_expiry). + if ( + new_state == STATE_STARTING + and not TERMINAL_PROBE_RETURNS + and self._cycle_completed_time is not None + ): self._cycle_completed_time = None self._is_clean_state = False self._clean_state_start = None @@ -5256,13 +5013,22 @@ class WashDataManager: self._power_off_below_since = None self._cancel_power_off_timer() self._stop_state_expiry_timer() + if old_state == STATE_STARTING and new_state in ( + STATE_FINISHED, STATE_INTERRUPTED, STATE_FORCE_STOPPED + ): + # Item 515: a false start back in its terminal state ended no cycle. + self._cycle_start_time = None if new_state == STATE_RUNNING: new_cycle_detected = old_state in (STATE_OFF, STATE_STARTING, STATE_UNKNOWN) # Only reset estimates if we are truly starting a NEW cycle (from off or starting) # If we transition from PAUSED or ENDING, it's a resume - keep estimates! if new_cycle_detected: + # The previous cycle's completed/Clean overlay ends here (item 515; + # Clean and the nag tracking are reset further down). self._cycle_completed_time = None self._stop_state_expiry_timer() + self._power_off_below_since = None + self._cancel_power_off_timer() self._current_program = "detecting..." self._manual_program_active = False @@ -5273,7 +5039,7 @@ class WashDataManager: # (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, + # then feeds 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 @@ -5282,7 +5048,16 @@ class WashDataManager: self._time_remaining = None self._total_duration = None self._cycle_progress = 0 + # ...and the EMA behind it (item 388d): an armed program sets the + # duration before the "no profile" reset can run, so the new + # cycle started from the previous one's smoothed figure (89% two + # minutes into a two-hour wash). + self._smoothed_progress = 0.0 self._matched_profile_duration = None + # The ML expectation is the median of the profile's last 20 cycles; + # cached per profile only, it stayed frozen until another programme + # was matched or HA restarted (audit PROGRESS-16). Once per cycle. + self._ml_end_expectation_cache = None self._last_estimate_time = None self._score_history = {} # Reset score history on new cycle self._match_persistence_counter = {} # Reset persistence counter @@ -5291,7 +5066,7 @@ class WashDataManager: self._notified_start = False # Reset start notification state self._start_event_fired = False self._last_cycle_post_anomaly = {} # Clear previous cycle's anomaly cache - self._cycle_start_time = self.detector.current_cycle_start or dt_util.now() + self._cycle_start_time = self.detector.current_cycle_start or utc_now() self._ranking_snapshot_cycle_id = str(uuid.uuid4()) self._reset_live_notification_state() # Snapshot the external energy meter (issue #316) so cycle end can @@ -5397,6 +5172,13 @@ class WashDataManager: self._stop_watchdog() # Stop watchdog regardless of previous state self._cycle_start_time = None + # A power-sensor change saved mid-cycle lands now (audit MANAGER-11). + if ( + getattr(self, "_pending_power_sensor", None) is not None + and new_state not in _SENSOR_SWAP_BLOCKED_STATES + ): + self._spawn_tracked(self._async_apply_pending_power_sensor()) + self._notify_update() def _discard_cycle_cleanup(self) -> None: @@ -5413,7 +5195,7 @@ class WashDataManager: self._spawn_tracked(self.profile_store.async_clear_active_cycle()) # Anchor the terminal state so _handle_state_expiry (and power-off) can act, # then arm the expiry timer that resets terminal -> Off after the reset delay. - self._cycle_completed_time = dt_util.now() + self._cycle_completed_time = utc_now() self._start_state_expiry_timer() def _on_cycle_end(self, cycle_data: dict[str, Any]) -> None: @@ -5421,13 +5203,30 @@ class WashDataManager: duration = cycle_data["duration"] max_power = cycle_data.get("max_power", 0) + # Coalesce this cycle end's store writes from its first one: the cadence + # commit below already spawns a suggestion pass that saves (item 456). + try: + self.profile_store.coalesce_saves() + except Exception: # noqa: BLE001 - a save policy must never break cycle end + self._logger.debug("Could not coalesce cycle-end saves", exc_info=True) + + # First, and synchronously: every end - ghost, pump-out, persisted or not + # - commits this cycle's update intervals to the cadence model or drops + # them, so they can never ride into the next cycle (#458). It used to run + # at the end of the async pipeline, which the ghost and pump-out branches + # below return before. + try: + self.learning_manager.close_cycle_cadence(cycle_data) + except Exception: # pylint: disable=broad-exception-caught + self._logger.debug("Cadence commit failed", exc_info=True) + # IMMEDIATELY stop all active timers when cycle determined to have ended self._stop_watchdog() # Stop active cycle watchdog self._stop_state_expiry_timer() # Cancel any pending progress reset self._clear_timer_pause_notification() self._cancel_door_end_dwell() # Discard stale auto-open dwell (#342) prev_cycle_end_time = self._last_cycle_end_time - self._last_cycle_end_time = dt_util.now() + self._last_cycle_end_time = utc_now() self._pump_stuck = False # Reset for next pump cycle # Auto-Tune: Check for ghost cycles (short duration AND low energy) @@ -5439,7 +5238,7 @@ class WashDataManager: for p in power_data: try: valid.append((float(p[0]), float(p[1]))) - except (TypeError, ValueError, IndexError): + except (TypeError, ValueError, IndexError, OverflowError): pass if len(valid) >= 2: try: @@ -5447,7 +5246,7 @@ class WashDataManager: ts = np.array([v[0] for v in valid]) ps = np.array([v[1] for v in valid]) # Shared trapezoidal integrator with a data-driven outage gap - # (single source with ProfileStore.add_cycle). + # (single source with ProfileStore.async_add_cycle). cycle_energy_wh = integrate_wh( ts, ps, max_gap_s=energy_gap_threshold_s(ts) ) @@ -5530,6 +5329,8 @@ class WashDataManager: features are unavailable. ``None`` means the detector keeps its existing power/energy-based behavior, so this can only ever *defer* a completion. """ + if not ENABLE_ML_END_GUARD: + return None try: from .ml.engine import ml_models_enabled, resolve_scorer @@ -5542,7 +5343,7 @@ class WashDataManager: or profile_name not in self.profile_store.get_profiles() ): return None - end_fn, _ = resolve_scorer("end", self.profile_store) + end_fn, _ = resolve_scorer("end") if end_fn is None: return None expectation = self._profile_end_expectation(profile_name, expected_duration) @@ -5593,25 +5394,29 @@ class WashDataManager: ``is_terminal_drop``) - a cycle drawing power unlike anything in its history is treated as a possible new program and deferred. - Returns ``False`` (keep the proven slow path) when the ML/anomaly opt-in - is off, there is too little history to trust the baseline, the cycle - looks novel, or the drop is not anomalously early. Never raises - the + Returns ``False`` (keep the proven slow path) when it is off for this + device (``detector_config.terminal_drop_enabled``: always on for + dishwashers, behind the "Apply smart models" toggle otherwise - audit + ML-08), a default-on dishwasher has no committed unambiguous match yet + (``terminal_drop_may_fire``), there is too little history to trust the + baseline, the cycle looks novel, or the drop is not anomalously early. + Never raises - the anomaly signal must never break detection. """ try: - from .ml.engine import ml_models_enabled - - if not ml_models_enabled(self.config_entry.options): + options = self.config_entry.options + if not terminal_drop_enabled(self.device_type, options): return False - earliest, peak_range = self._terminal_drop_baseline() - return is_terminal_drop( + # Default-on dishwashers fire only on a committed, unambiguous match. + if not terminal_drop_may_fire( + self.device_type, options, self.detector, + pinned=bool(self._manual_program_active), + ): + return False + return terminal_drop_fires( points, - earliest, - peak_range, + self._terminal_drop_baseline(), float(self.detector.config.stop_threshold_w), - TERMINAL_DROP_EARLINESS_RATIO, - TERMINAL_DROP_MIN_PEAK_RATIO, - TERMINAL_DROP_PEAK_FAMILIAR_TOL, ) except Exception as err: # noqa: BLE001 - anomaly signal must never break detection self._logger.debug("Terminal-drop detection skipped: %s", err) @@ -5657,11 +5462,7 @@ class WashDataManager: cycles = list(self.profile_store.get_past_cycles()) stop_threshold = float(self.detector.config.stop_threshold_w) earliest, peak_range = await self.hass.async_add_executor_job( - terminal_drop_baseline, - cycles, - stop_threshold, - TERMINAL_DROP_MIN_QUIET_SPAN_S, - TERMINAL_DROP_MIN_CLEAN_CYCLES, + terminal_drop_baseline_for, cycles, stop_threshold ) self._terminal_drop_cache = (len(cycles), earliest, peak_range) except Exception as err: # noqa: BLE001 - anomaly signal must never break detection @@ -5670,141 +5471,6 @@ class WashDataManager: if self._terminal_drop_refresh_n == n: self._terminal_drop_refresh_n = None - def _ml_progress_percent( - self, - trace: list[tuple[datetime, float]], - profile_name: str, - ) -> float | None: - """ML completion-fraction estimate (0-100) for the running cycle, or None. - - Uses the on-device ``remaining_time`` regressor (a ``standardized_linear`` - head with no shipped baseline) to predict how far through the cycle we - are, learning this device's own progress curve rather than assuming the - matched profile's median duration. Gated on the ML opt-in and only active - once training has promoted a regressor; otherwise returns ``None`` so the - caller keeps the proven phase-aware estimate untouched. Never raises. - """ - return progress_mod.ml_progress_percent( - self.profile_store, - self.config_entry.options, - float(self._matched_profile_duration or 0.0), - trace, - profile_name, - self._profile_end_expectation, - self._logger, - ) - - def _ml_energy_total( - self, - trace: list[tuple[datetime, float]], - profile_name: str, - ) -> float | None: - """Predicted total cycle energy (Wh) from the on-device ``total_energy`` - regressor, or None. - - The regressor predicts the *energy-completion fraction* (energy so far ÷ - final energy); total = ``energy_so_far / fraction``. Because energy - accumulates non-linearly (heating front-loads it), this is more stable — - especially early — than dividing accumulated energy by *time* progress - (the fallback in :meth:`_update_projected_energy`). Same gating as the - remaining-time regressor: opt-in, inert until a model is promoted. Never - raises. - """ - return progress_mod.ml_energy_total( - self.profile_store, - self.config_entry.options, - float(self._matched_profile_duration or 0.0), - trace, - profile_name, - self._profile_end_expectation, - self._logger, - ) - - def _compute_cycle_quality_score( - self, - cycle_data: dict[str, Any], - past_cycles_snapshot: list[dict[str, Any]] | None = None, - ) -> None: - """Score a just-finished cycle with the hybrid_curve_quality model (opt-in). - - When ML models are enabled for this device, computes P(cycle is a problem) - and stores it under ``cycle_data["ml_quality_score"]``. A high score means - the cycle may be mis-detected or corrupt; the learning manager uses it to - downgrade auto-labeling to a feedback request so the user can confirm. - Never raises — scoring failure is silently ignored to keep cycle storage safe. - """ - try: - from .ml.engine import ml_models_enabled, resolve_scorer - from .ml.feature_extraction import quality_features - - if not ml_models_enabled(self.config_entry.options): - return - quality_fn, _ = resolve_scorer("quality", self.profile_store) - if quality_fn is None: - return - - profile_name = cycle_data.get("profile_name") - if not profile_name: - return - - points = decompress_power_data(cycle_data) - if not points or len(points) < 4: - return - - # Build profile median stats from stored labeled cycles. - durations: list[float] = [] - energies: list[float] = [] - peaks: list[float] = [] - # This function runs in an executor thread and the event loop may - # append cycles concurrently; iterating (or even copying) the live list - # here is a data race. Prefer the snapshot taken on the event loop at - # the call site; only fall back to a local copy for direct callers. - cycles = ( - past_cycles_snapshot - if past_cycles_snapshot is not None - else list(self.profile_store.get_past_cycles()) - ) - for c in cycles: - if c.get("profile_name") != profile_name: - continue - if c.get("duration") is not None: - durations.append(float(c["duration"])) - if c.get("energy_wh") is not None: - energies.append(float(c["energy_wh"])) - if c.get("max_power") is not None: - peaks.append(float(c["max_power"])) - - if not durations: - return - - med_dur = float(np.median(durations)) - med_energy = float(np.median(energies)) if energies else 500.0 - med_peak = float(np.median(peaks)) if peaks else 500.0 - - match_conf = float(cycle_data.get("match_confidence") or 0.0) - conf_known = match_conf > 0 - proxy_dist = max(0.0, 1.0 - match_conf) if conf_known else 0.25 - proxy_margin = match_conf if conf_known else 0.30 - proxy_fit = match_conf if conf_known else 0.75 - - feat = quality_features( - points=points, - profile_median_duration_s=med_dur, - profile_median_energy_wh=med_energy, - profile_median_peak_w=med_peak, - profile_distance=proxy_dist, - label_margin=proxy_margin, - profile_fit_score=proxy_fit, - flag_count=len(cycle_data.get("artifacts", [])), - ) - score = round(float(quality_fn(feat)), 3) - cycle_data["ml_quality_score"] = score - self._logger.debug( - "ML quality score (profile=%s): %.3f", profile_name, score - ) - 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). @@ -5869,7 +5535,7 @@ class WashDataManager: if state is not None: try: value = float(state.state) - except (ValueError, TypeError): + except (ValueError, TypeError, OverflowError): pass else: # A non-finite reading is treated as no reading, exactly like an @@ -5881,7 +5547,7 @@ class WashDataManager: if static is not None: try: value = float(static) - except (ValueError, TypeError): + except (ValueError, TypeError, OverflowError): pass else: if math.isfinite(value): @@ -5920,7 +5586,7 @@ class WashDataManager: for state in res.get(entity_id, []) or []: try: price = float(state.state) - except (ValueError, TypeError): + except (ValueError, TypeError, OverflowError): # unknown/unavailable: the previous price stays in force. continue ts = state.last_changed.timestamp() @@ -6075,7 +5741,7 @@ class WashDataManager: return None try: value = float(state.state) - except (ValueError, TypeError): + except (ValueError, TypeError, OverflowError): return None unit = str(state.attributes.get("unit_of_measurement") or "").strip().lower() # Normalize to Wh. An unrecognised unit is treated as unusable so a @@ -6129,13 +5795,26 @@ class WashDataManager: if meter is not None: try: return float(meter) - except (ValueError, TypeError): + except (ValueError, TypeError, OverflowError): pass try: return float(cycle_data.get("energy_wh", 0.0)) - except (ValueError, TypeError): + except (ValueError, TypeError, OverflowError): return 0.0 + def _in_anticrease_tail(self) -> bool: + """The detector sits in a #296 anti-crease tail (register item 393a). + + Saved at stop and unload like an active cycle: a restart that starts from + OFF reads the tail's next drum bursts as a new cycle (one ~20 min cycle on + the item-393 shape). The cycle end has already cleared the active slot, and + the restore consumes it again, so a stale tail cannot come back later. + """ + return ( + self.detector.state == STATE_ANTI_WRINKLE + and getattr(self.detector, "in_anticrease_tail", False) is True + ) + def _augment_active_snapshot(self, snapshot: dict[str, Any]) -> dict[str, Any]: """Add manager-owned fields to a detector snapshot before persisting. @@ -6151,6 +5830,15 @@ class WashDataManager: snapshot["manual_program_name"] = ( self._current_program if self._manual_program_active else None ) + # The auto-detected program on display and its expected duration, for the + # same reason: the detector holds only the last tick's raw winner. + committed = not self._manual_program_active and match_rules.program_is_committed( + self._current_program + ) + snapshot["committed_program"] = self._current_program if committed else None + snapshot["committed_program_duration"] = ( + self._matched_profile_duration if committed else None + ) snapshot["notified_start"] = self._notified_start snapshot["start_event_fired"] = self._start_event_fired snapshot["is_user_paused"] = self._is_user_paused @@ -6165,8 +5853,41 @@ class WashDataManager: snapshot["price_timeline"] = [ [ts, price] for ts, price in self._price_timeline ] + # One-shot per-cycle state (audit MANAGER-08): without it a restart re-fired + # every passed cycle timer ("Add softener" twice; an auto_pause timer paused + # again and, with pause_cuts_power, switched the appliance off), could send + # the pre-completion reminder twice, and a second restart lost the first gap. + snapshot["fired_cycle_timers"] = sorted(self._fired_cycle_timers) + snapshot["notified_pre_completion"] = bool(self._notified_pre_completion) + snapshot["restart_gaps"] = list(self._restart_gaps) + snapshot["live_activity_started"] = bool(self._live_activity_started) + # When the power sensor last really reported, and the value the manager + # held for it (register item 266). Without them a restart took the + # entity's own startup write for a report, so the watchdog's silence + # clock restarted at zero and a cycle in a long silent tail looked as if + # its plug had just spoken. + snapshot["last_real_reading_time"] = ( + self._last_real_reading_time.isoformat() + if self._last_real_reading_time is not None + else None + ) + snapshot["last_sensor_power"] = self._current_power return snapshot + @staticmethod + def _format_duration_hm(minutes: Any) -> str: + """The ``{duration_hm}`` template variable: ``"1 h 05 min"``, ``"45 min"``. + + Unit symbols a voice assistant reads correctly (#93, #117: it read + ``{duration}m`` as metres). Whole minutes in; never raises. + """ + try: + total = max(0, int(minutes)) + except (TypeError, ValueError, OverflowError): + return "" + hours, mins = divmod(total, 60) + return f"{hours} h {mins:02d} min" if hours else f"{mins} min" + @staticmethod def _format_vs_typical( duration: float, @@ -6187,14 +5908,14 @@ class WashDataManager: if not median or float(median) <= 0: return "" pct = round((float(duration) - float(median)) / float(median) * 100) - except (ValueError, TypeError, ZeroDivisionError): + except (ValueError, TypeError, ZeroDivisionError, OverflowError): return "" try: if pct >= 1: return longer_template.format(pct=pct) if pct <= -1: return shorter_template.format(pct=abs(pct)) - except (KeyError, IndexError, ValueError): + except (KeyError, IndexError, ValueError, OverflowError): # Malformed translation template; fall back to the English default. if pct >= 1: return f"{pct}% longer than usual" @@ -6223,7 +5944,7 @@ class WashDataManager: device=self.config_entry.title, price=f"{float(price):.3f}", ) - except (ValueError, TypeError): + except (ValueError, TypeError, OverflowError): return "" async def _async_process_cycle_end( @@ -6231,6 +5952,31 @@ class WashDataManager: cycle_data: dict[str, Any], cycle_token: str | None = None, price_timeline: list[tuple[float, float]] | None = None, + ) -> None: + """Process cycle completion, then close it whatever failed (MANAGER-12). + + An exception anywhere in the steps used to end the task before the flush + and the terminal reset: the UI stayed on the finished cycle with no expiry + timer. On shutdown the task is cancelled and ``async_shutdown`` flushes the + store itself; arming the expiry timer then would outlive the unload. + """ + failed = False + try: + await self._async_cycle_end_steps(cycle_data, cycle_token, price_timeline) + except Exception: # pylint: disable=broad-exception-caught + failed = True + self._logger.exception("Cycle-end processing failed; closing the cycle anyway") + finally: + if not self._is_shutdown: + await self._async_close_cycle_end( + cycle_token, failed, cycle_status=cycle_data.get("status") + ) + + async def _async_cycle_end_steps( + 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). @@ -6241,185 +5987,143 @@ class WashDataManager: 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 - if self._current_program in ("detecting...", "restored..."): - await self._run_final_match_from_cycle_data(cycle_data) - - # B1: freeze THIS cycle's live context into immutable locals now, before the - # persistence / auto-label / lifetime-energy awaits below. A new cycle can - # start synchronously during any of those awaits (a back-to-back load drives - # the detector into a fresh RUNNING via _on_state_change, which rolls - # _current_program back to "detecting..." and resets the match fields). The - # tail (event payload, finish notification, learning inputs) must describe the - # cycle that just finished, not whatever the live fields hold by the time each - # await returns. The final match above is what determines these values for - # this cycle, so capture right after it. + # B1: freeze THIS cycle's live context into immutable locals BEFORE the first + # await. A new cycle can start synchronously during any await below (a + # back-to-back load drives the detector into a fresh RUNNING via + # _on_state_change, which rolls _current_program back to "detecting..." and + # resets the match fields). The tail (event payload, finish notification, + # learning inputs) must describe the cycle that just finished, not whatever + # the live fields hold by the time each await returns. program = self._current_program - match_result = self._last_match_result + live_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 - # 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() - ): - # `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 - # How far clear of the runner-up the winner finished. The absolute - # score is a weak guide to being right (AUC 0.625) where this margin - # is a strong one (0.792), and labelling is the asymmetric decision: - # a wrong label reshapes avg_duration and every future estimate, - # while a missed one only asks the user. Register item 310. - # - # A separate constant from MATCH_AMBIGUITY_MARGIN on purpose - that - # one also gates Smart Termination, so widening it would defer - # cycle ends and undo item 306. - # - # The margin describes the RESULT's own winner, and that is not - # always `program`. `match_result` is `_last_match_result`, which the - # live matcher overwrites on every run whether or not a switch - # commits, while `program` (`_current_program`) only moves through - # the persistence / decisive-margin / consistency paths. Inside a - # persistence window the newest result can have challenger X winning - # while P is still displayed - and then `ambiguity_margin` says how - # far X leads ITS runner-up, which may be P itself. Reading it as - # evidence for P inverts the gate: the more decisively X won, the - # more readily P got labelled. Require the margin's owner to be the - # programme being labelled. The post-cycle gate below has no such - # problem, because there `res.best_profile` is what it labels. - _margin = getattr(match_result, "ambiguity_margin", None) - _margin_owner = ( - getattr(match_result, "best_profile", None) - if match_result is not None - else program + # ONE match over the complete trace; every label decision below reads it + # (audit MATCH-DECIDE-02). The last live tick is a PREFIX match (prefix + # shapes, the in-progress duration kernel, up to profile_match_interval + # stale, the ENDING quiet tail inside its duration), and its winner differed + # from the complete-cycle winner on 17.5% of corpus cycles, while + # MATCH_LABEL_MIN_MARGIN was calibrated on complete folds (item 310). + # It runs BEFORE the cycle is stored, so a raise here used to lose the + # cycle and strand the device on it (audit MATCH-CORE-06 / MANAGER-12). A + # failed match leaves an empty result: the cycle is stored unlabelled, not + # labelled from the live prefix match instead. + try: + final_result = await self._run_final_match_from_cycle_data(cycle_data) + except Exception: # pylint: disable=broad-exception-caught + self._logger.exception( + "Final match failed; storing the cycle without a label" ) - # A result with NO winner is not a challenger. `best_profile is None` - # comes with `confidence` and `ambiguity_margin` both 0.0, so it - # carries no evidence about any programme - yet the owner check below - # treats it as "someone else won" and reports `%r` as None. The - # outcome is right either way (no label), the stated reason is not. - # Drop the margin too, so nothing downstream can read a - # no-winner 0.0 as a measured one; a real challenger keeps its margin. - _no_winner = match_result is not None and _margin_owner is None - if _no_winner: - _margin = None - _margin_ok = ( - not _no_winner - and _margin_owner == program - and (_margin is None or float(_margin) >= MATCH_LABEL_MIN_MARGIN) - ) - if manual_program: - cycle_data["profile_name"] = program - cycle_data["label_source"] = "manual" - elif label_confidence >= float(self._learning_confidence or 0.0) and _margin_ok: - cycle_data["profile_name"] = program - cycle_data["label_source"] = "auto_match" - elif ( - label_confidence >= float(self._learning_confidence or 0.0) - and _margin_owner != program - ): - # Distinct branch: `_margin_ok` is False for two different - # reasons and the message below only describes one of them. When - # the owner differs, `_margin` is the CHALLENGER's lead and can - # be large, so that message reads "only 0.400 clear of the next - # candidate, under the 0.08 a label needs" - self-contradictory, - # and it hides the actual reason. - if _no_winner: - self._logger.info( - "Not labeling cycle as '%s': the latest match produced no " - "winner at all, so there is no margin to judge it by. It " - "stays unlabelled rather than reshaping '%s' on the " - "confidence of an earlier tick.", - program, program, - ) - else: - self._logger.info( - "Not labeling cycle as '%s': the latest match was won by " - "%r, so its margin is evidence about that program, not " - "this one. It stays unlabelled rather than reshaping '%s' " - "on a number that was never measured for it.", - program, _margin_owner, program, - ) - elif label_confidence >= float(self._learning_confidence or 0.0): + final_result = MatchResult(None, 0.0, 0.0, None, [], False, 0.0) + same_cycle = cycle_token is None or self._ranking_snapshot_cycle_id == cycle_token + if program in ("detecting...", "restored...") and final_result is not None: + # Never committed live: the complete match names the program for DISPLAY + # at a low floor, since the trace is complete. Labelling is decided below. + if final_result.best_profile and final_result.confidence >= 0.15: + program = final_result.best_profile + match_confidence = final_result.confidence + member_confidence = final_result.member_confidence + if same_cycle: + self._current_program = program + self._last_match_result = final_result + self._last_match_confidence = match_confidence + self._last_member_confidence = member_confidence self._logger.info( - "Not labeling cycle as '%s': confident enough (%.2f) but only " - "%.3f clear of the next candidate, under the %.2f a label needs. " - "It stays unlabelled rather than reshaping that program's " - "statistics on a coin flip.", - program, label_confidence, float(_margin or 0.0), - MATCH_LABEL_MIN_MARGIN, + "Final match from cycle data: '%s' with confidence %.3f", + program, match_confidence, ) else: + self._logger.info( + "No confident match from cycle data (best: %s, conf=%.3f)", + final_result.best_profile, final_result.confidence, + ) + match_result = final_result if final_result is not None else live_result + + # The number every label / persistence decision gates on: the member-aware + # score (item 206), not a Stage-5 group's, which is its best SIBLING's. + try: + label_confidence = float(getattr(match_result, "label_confidence", 0.0) or 0.0) + except (TypeError, ValueError, OverflowError): + label_confidence = 0.0 + if label_confidence > 0 and ( + not manual_program or getattr(match_result, "best_profile", None) == program + ): + # Recorded whether or not we label, so the panel can show what WashData + # suspected without the cycle claiming it as its program. Not on a + # hand-picked cycle the matcher would have called something else: the + # number would read as confidence in the user's pick. + cycle_data["match_confidence"] = label_confidence + + # A label is not a display value: it makes the cycle evidence for that + # profile, moving avg_duration / target_duration, which arm Smart + # Termination and the anti-crease finalize - so a weak guess recorded as fact + # seeds the next mis-detection (#400). The verdict is the learning floor (the + # panel's ladder: unmatch < match < learning < auto-label) plus the margin + # and Stage-5 checks, on the complete match; the label is that match's own + # winner. A hand-picked program bypasses it and is stamped "manual", since + # "auto_match" is what lets bulk auto-labelling overwrite a label later. + # The verdict also reaches learning.process_cycle_end, which must not + # auto-label a cycle refused here (audit MANAGER-02 / MATCH-DECIDE-01). + profiles = self.profile_store.get_profiles() + learning_floor = float(self._learning_confidence or 0.0) + label_gate_ok = False + if manual_program and program and program in profiles: + cycle_data["profile_name"] = program + cycle_data["label_source"] = "manual" + label_gate_ok = True + else: + # Shared with the Playground's would_label (match_rules). + verdict, reason = match_rules.cycle_end_label_verdict( + match_result, learning_floor, profiles + ) + best = getattr(match_result, "best_profile", None) + if verdict: + cycle_data["profile_name"] = verdict + cycle_data["label_source"] = "auto_match" + label_gate_ok = True + if verdict != program: + self._logger.info( + "Labelled cycle as '%s' (the complete-cycle winner, %.2f) " + "although '%s' was shown while it ran.", + verdict, label_confidence, program, + ) + elif reason == "below_floor": + _group_conf = float(getattr(match_result, "confidence", 0.0) or 0.0) 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)" - ), + "learning threshold %.2f.%s", + best, label_confidence, learning_floor, + f" (its profile group scored {_group_conf:.2f}, but that was a " + "different member of the group)" + if _group_conf > label_confidence + 1e-9 else "", + ) + elif reason == "ambiguous": + self._logger.info( + "Not labeling cycle as '%s': the matcher flagged its own pick as " + "uncertain (a profile-group member that fits poorly, or a run " + "past that member's length).", + best, + ) + elif reason == "margin": + self._logger.info( + "Not labeling cycle as '%s': confident enough (%.2f) but only " + "%.3f clear of the next candidate, under the %.2f a label needs.", + best, label_confidence, + float(getattr(match_result, "ambiguity_margin", 0.0) or 0.0), + MATCH_LABEL_MIN_MARGIN, + ) + elif reason == "unknown_profile": + self._logger.info( + "Not labeling cycle as '%s': that profile no longer exists.", best ) - # Attach extensive debug data if available (and configured) + # Attach extensive debug data if available (and configured). From the + # complete match, so the stored ranking describes the finished cycle. if match_result: ranking = getattr(match_result, "ranking", []) # Top-5 ranking stored unconditionally (small, high training value). @@ -6432,81 +6136,12 @@ class WashDataManager: "ambiguous": getattr(match_result, "is_ambiguous", False), } - # Post-Cycle Auto-Labeling (if not already matched) - # Offload this match too if needed - if not cycle_data.get("profile_name") and self._auto_label_confidence > 0: - res = await self.profile_store.async_match_profile( - cycle_data["power_data"], cycle_data["duration"] - ) - # 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. - # The SAME margin gate the live label gate applies (item 310). The - # comment above calls this pass "the better judge" because it sees the - # complete trace at a higher confidence threshold - but confidence is - # the weak axis (AUC 0.625 against the margin's 0.792), and this path - # runs precisely when the live gate declined, margin refusals included. - # Without it a cycle refused a label for finishing too close to the - # runner-up was relabelled here a few lines later, on the same data. - _post_margin = getattr(res, "ambiguity_margin", None) - _post_margin_ok = ( - _post_margin is None or float(_post_margin) >= MATCH_LABEL_MIN_MARGIN - ) - if ( - res.best_profile - and res.label_confidence >= self._auto_label_confidence - and _post_margin_ok - ): - cycle_data["profile_name"] = res.best_profile - cycle_data["label_source"] = "auto_label_post" - 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 - # EVENT_CYCLE_ENDED under HA's 32KB event-data limit). - cycle_data["match_ranking_top5"] = _sanitize_ranking( - getattr(res, "ranking", []) - ) - self._logger.info( - "Post-cycle auto-labeled as '%s' (confidence: %.2f)", - res.best_profile, - res.label_confidence, - ) - elif res.best_profile and res.label_confidence >= self._auto_label_confidence: - self._logger.info( - "Not post-cycle labeling as '%s': confident enough (%.2f) but " - "only %.3f clear of the next candidate, under the %.2f a label " - "needs. It is offered for confirmation instead.", - res.best_profile, - res.label_confidence, - float(_post_margin or 0.0), - MATCH_LABEL_MIN_MARGIN, - ) - - # Back-fill confirmed label on any ranking snapshots captured during this cycle - # so the live_match on-device trainer can use them as labelled examples. - _start_iso = cycle_data.get("start_time") - _confirmed_profile = cycle_data.get("profile_name") - if _start_iso and _confirmed_profile: - try: - self.profile_store.confirm_match_ranking_snapshots( - _start_iso, - _confirmed_profile, - # Use the token captured when THIS cycle ended, not the live - # field, which may already have rolled to a newly-started cycle - # during the awaits above (else this cycle's snapshots go - # unlabelled and a new cycle's snapshot gets mislabelled). - cycle_id=cycle_token or None, - ) - except Exception: # noqa: BLE001 - pass - # Compute envelope conformance for the matched profile. # Stored as cycle_data["envelope_conformance"] so the panel and quality # gate can display/use it. Only runs when we have a profile + power trace. _ep = cycle_data.get("profile_name") _pd = cycle_data.get("power_data") + _start_iso = cycle_data.get("start_time") if _ep and isinstance(_pd, list) and len(_pd) >= 4: try: from .time_utils import power_data_to_offsets # noqa: PLC0415 @@ -6546,44 +6181,8 @@ class WashDataManager: if overrun_ratio > 0: cycle_data["overrun_ratio"] = round(float(overrun_ratio), 3) - # A1: Underrun check — computed post-cycle only, not a live signal. - # Only applied when no runtime anomaly was detected (underrun and overrun are mutually exclusive). - try: - if not cycle_data.get("anomaly") or cycle_data["anomaly"] == "none": - _uc_profile = cycle_data.get("profile_name") - _uc_dur = float(cycle_data.get("duration", 0)) - if _uc_profile and _uc_dur > 0: - _uc_median = self.profile_store.get_profile_median_duration(_uc_profile) - if ( - isinstance(_uc_median, (int, float)) - and not isinstance(_uc_median, bool) - and _uc_median > 0 - and _uc_dur < _uc_median * CYCLE_UNDERRUN_ANOMALY_RATIO - ): - cycle_data["anomaly"] = "underrun" - cycle_data["underrun_ratio"] = round(_uc_dur / _uc_median, 3) - except Exception: # noqa: BLE001 - pass - - # A2: Energy spike/low anomaly — stored separately from duration anomaly. - try: - _ea_profile = cycle_data.get("profile_name") - _ea_energy = float(cycle_data.get("energy_wh", 0)) - if _ea_profile and _ea_energy > 0: - _ea_stats = self.profile_store.get_profile_energy_stats(_ea_profile) - if ( - isinstance(_ea_stats, dict) - and isinstance(_ea_stats.get("std_wh"), (int, float)) - and _ea_stats["std_wh"] > 0 - ): - _ea_z = (_ea_energy - _ea_stats["avg_wh"]) / _ea_stats["std_wh"] - cycle_data["energy_z_score"] = round(_ea_z, 2) - if _ea_z > ENERGY_ANOMALY_Z_THRESHOLD: - cycle_data["energy_anomaly"] = "energy_spike" - elif _ea_z < -ENERGY_ANOMALY_Z_THRESHOLD: - cycle_data["energy_anomaly"] = "energy_low" - except Exception: # noqa: BLE001 - pass + # A1 underrun + A2 energy spike/low (post-cycle only, never raises). + _apply_post_cycle_anomalies(cycle_data, self.profile_store) # Cache post-cycle anomaly data so sensor attributes surface it while idle. self._last_cycle_post_anomaly = { @@ -6609,27 +6208,6 @@ class WashDataManager: # 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. - # Must run BEFORE async_add_cycle so get_past_cycles() inside the scorer does - # not yet include the current cycle, keeping reference statistics uncontaminated. - # Only opted-in devices reach the scorer, and only then is it offloaded to the - # executor: on a long trace its NumPy feature extraction is O(N) and must not - # block the event loop (mirrors the profile matcher). Gating here avoids a - # pointless thread-hop for the default (ML-off) case where the scorer no-ops. - # The scorer mutates only cycle_data (nothing else touches it here) and never - # raises, so this is executor-safe. - from .ml.engine import ml_models_enabled # noqa: PLC0415 - - if ml_models_enabled(self.config_entry.options): - # Snapshot past_cycles on the event loop before offloading: iterating - # (or copying) the live list inside the executor races the loop - # appending this just-finished cycle. - past_cycles_snapshot = list(self.profile_store.get_past_cycles()) - await self.hass.async_add_executor_job( - self._compute_cycle_quality_score, cycle_data, past_cycles_snapshot - ) - # Add cycle to store immediately (still sync but offloadable parts optimized # internally if possible) # Note: add_cycle is mostly safe (signature calc is O(N) but fast enough for @@ -6637,8 +6215,26 @@ class WashDataManager: # We could offload signature calc to analysis logic if really needed, but let's # stick to match profile optimization first. cycle_persisted = False + # Read the odometer BEFORE the add: its getter floors at len(past_cycles), so + # read afterwards it already counted this cycle and `+ 1` double-stepped - a + # fresh install read 2/3/4 after 1/2/3 cycles and milestones fired one cycle + # early (audit MANAGER-05). try: - await self.profile_store.async_add_cycle(cycle_data) + odometer_before_add: int | None = self._lifetime_cycle_count() + except Exception: # noqa: BLE001 - counter must never break cycle end + odometer_before_add = None + # Every save from here until the follow-up work has settled is debounced, + # and what has to be durable is written once by async_flush_saves below: + # this pipeline and the tasks it spawns used to rewrite the whole store six + # times (register item 456). Re-armed here (_on_cycle_end opened it) so + # the window runs from the add, however long the final match took. + self.profile_store.coalesce_saves() + # The envelopes this cycle end changed: the labelled profile's, and those of + # any profile retention trimmed. Nothing else needs rebuilding (the nightly + # maintenance still rebuilds them all). + touched_profiles: list[str] = [] + try: + retained = await self.profile_store.async_add_cycle(cycle_data) cycle_persisted = True # The cycle (with its restart_gaps) is now durably stored, so it is safe # to drop the live buffer. Doing this only after a confirmed persist means @@ -6647,7 +6243,20 @@ class WashDataManager: self._restart_gaps.clear() profile_name = cycle_data.get("profile_name") if profile_name: + touched_profiles.append(profile_name) await self.profile_store.async_rebuild_envelope(profile_name) + if isinstance(retained, (set, frozenset, list, tuple)): + for name in sorted(p for p in retained if isinstance(p, str)): + if not name or name in touched_profiles: + continue + touched_profiles.append(name) + try: + await self.profile_store.async_rebuild_envelope(name) + except Exception: # pylint: disable=broad-exception-caught + self._logger.debug( + "Envelope rebuild after retention failed for %s", + name, exc_info=True, + ) except Exception as e: # pylint: disable=broad-exception-caught self._logger.error("Failed to add cycle to store: %s", e) @@ -6659,9 +6268,9 @@ class WashDataManager: # save immediately after (same store, one save). prev_lifetime_count: int | None = None cur_lifetime_count: int | None = None - if cycle_persisted: + if cycle_persisted and odometer_before_add is not None: try: - prev_lifetime_count = self._lifetime_cycle_count() + prev_lifetime_count = odometer_before_add cur_lifetime_count = prev_lifetime_count + 1 # In-memory only; persisted by the batched lifetime-energy save below. self.profile_store.set_lifetime_cycle_count(cur_lifetime_count) @@ -6694,8 +6303,8 @@ class WashDataManager: if cycle_token is None or self._ranking_snapshot_cycle_id == cycle_token: self._spawn_tracked(self.profile_store.async_clear_active_cycle()) - # Auto post-process: merge fragmented cycles from last 3 hours - self._spawn_tracked(self._run_post_cycle_processing()) + # Refresh the artifacts that read the envelopes rebuilt above. + self._spawn_tracked(self._run_post_cycle_processing(touched_profiles)) # Prepare cycle data for event (enrich if needed) # IMPORTANT: Exclude large fields to prevent exceeding HA's 32KB event data limit @@ -6709,10 +6318,30 @@ class WashDataManager: k: v for k, v in cycle_data.items() if k not in excluded_fields } event_cycle_data["device_type"] = self.device_type - # Add program if missing or generic (use THIS cycle's captured program, not - # the live field which may already belong to a newly-started cycle). - if "profile_name" not in event_cycle_data and program: - event_cycle_data["profile_name"] = program + # The program to SHOW: the stored label, else THIS cycle's captured live + # program (not the live field, which may already belong to a newly-started + # cycle). `_add_cycle_data` always writes `profile_name` (None when the + # label gate refused), so the old "key missing" fill-in never ran and every + # unlabelled cycle announced "Washer finished None" (audit MANAGER-06). + # The stored cycle keeps profile_name None: this is display, not a label. + display_program = event_cycle_data.get("profile_name") + if not display_program and program and program not in ( + "off", "detecting...", "restored...", "starting", "unknown" + ): + display_program = program + display_program = display_program or "unknown" + # MATCH-DECIDE-15: how sure the complete-cycle match was, and whether the + # stored cycle was labelled with `program` or it is only the best guess + # shown for display. The margin is None with no winner (1.0 when only one + # programme was a candidate); `label_applied` covers a hand-picked one. + match_margin: float | None = None + if getattr(match_result, "best_profile", None): + try: + match_margin = round( + float(getattr(match_result, "ambiguity_margin", 0.0) or 0.0), 3 + ) + except (TypeError, ValueError, OverflowError): + match_margin = None if self._notify_fire_events: self.hass.bus.async_fire( @@ -6721,10 +6350,12 @@ class WashDataManager: "entry_id": self.entry_id, "device_name": self.config_entry.title, "cycle_data": event_cycle_data, - "program": event_cycle_data.get("profile_name", "unknown"), + "program": display_program, + "match_margin": match_margin, + "label_applied": bool(label_gate_ok), "duration": event_cycle_data.get("duration"), "start_time": event_cycle_data.get("start_time"), - "end_time": event_cycle_data.get("end_time") or dt_util.now().isoformat(), + "end_time": event_cycle_data.get("end_time") or utc_now().isoformat(), }, ) @@ -6753,11 +6384,23 @@ class WashDataManager: if _same_cycle: self._clear_live_progress_notification(clear_services=False) + # No "finished" push for an interrupted cycle (audit MANAGER-10): a false + # start or a cancelled programme finished nothing. Nothing replaces the + # start card on the lifecycle tag then, so clear it the way the shutdown + # path does when no finished notification follows - unless a newer cycle + # already owns that tag. + cycle_status = cycle_data.get("status") + announce_finish = notif_rules.cycle_end_is_finish(cycle_status) + if not announce_finish and _same_cycle: + self._send_tag_clear(self._lifecycle_tag) + # Send notification if enabled - if self._notify_finish_services or self._notify_actions: + if announce_finish and (self._notify_finish_services or self._notify_actions): msg_template = self.config_entry.options.get(CONF_NOTIFY_FINISH_MESSAGE, DEFAULT_NOTIFY_FINISH_MESSAGE) duration_min = int(cycle_data['duration'] / 60) - program_name = event_cycle_data.get("profile_name", "unknown") + program_name = display_program + # `completed` or `force_stopped` (interrupted cycles never get here). + status_str = str(cycle_status or "completed") energy_kwh = round(self._cycle_report_energy_wh(cycle_data) / 1000, 3) @@ -6792,12 +6435,14 @@ class WashDataManager: fallback_template=DEFAULT_NOTIFY_FINISH_MESSAGE, device=self.config_entry.title, duration=duration_min, + duration_hm=self._format_duration_hm(duration_min), program=program_name, energy_kwh=f"{energy_kwh:.3f}", cost=cost_str, time_finished=time_finished, cycle_count=finished_cycle_count, vs_typical=vs_typical, + status=status_str, ) self._dispatch_notification( msg, @@ -6811,6 +6456,7 @@ class WashDataManager: "time_finished": time_finished, "cycle_count": finished_cycle_count, "vs_typical": vs_typical, + "status": status_str, # Same lifecycle tag as start/live so the finished alert replaces # the live notification in place. No live_update/alert_once here, # so the companion app surfaces it with sound. @@ -6845,14 +6491,62 @@ class WashDataManager: # dangle forever. Use THIS cycle's captured match context (not the live # fields, which may already belong to a newly-started cycle after the awaits). if cycle_persisted: + # Ask about what the complete match picked, not the live tick's program. + feedback_profile = ( + cycle_data.get("profile_name") + or getattr(match_result, "best_profile", None) + or program + ) + feedback_duration = ( + matched_profile_duration + if feedback_profile == program + else (profiles.get(feedback_profile) or {}).get("avg_duration") + ) self.learning_manager.process_cycle_end( cycle_data, - detected_profile=program, + detected_profile=feedback_profile, confidence=label_confidence, - predicted_duration=matched_profile_duration, + predicted_duration=feedback_duration, match_result=match_result, + label_allowed=label_gate_ok or bool(cycle_data.get("profile_name")), ) + async def _async_close_cycle_end( + self, + cycle_token: str | None, + tail_failed: bool = False, + cycle_status: str | None = None, + ) -> None: + """The end of the cycle-end tail, run whatever failed before it (MANAGER-12). + + Flushes the coalesced cycle-end write (item 456) and resets the terminal + state, unless a newer cycle has started (B1). ``tail_failed`` means the + follow-up stopped part-way, possibly before the live notification was + handed over, so the live tag is cleared here instead of being left to + count its chronometer into negative numbers. ``cycle_status`` is the + ended cycle's status: an interrupted one never enters the Clean state + (audit MANAGER-10, ``notification_rules.cycle_end_is_finish``). + """ + # The one immediate write of this cycle end: the cycle, its counters, the + # rebuilt envelopes and the feedback request the learning pass just queued. + # No await since the lifetime-energy save that used to write first, so the + # cycle is durable at the same point as before. Only what the follow-ups + # derive (refreshed artifacts, suggestions) waits for the debounced write. + try: + await self.profile_store.async_flush_saves() + except Exception as e: # pylint: disable=broad-exception-caught + self._logger.error("Failed to save the finished cycle: %s", e) + # The idle display's standby level now includes this cycle (#452). + await self._async_refresh_standby_level() + + if tail_failed and ( + cycle_token is None or self._ranking_snapshot_cycle_id == cycle_token + ): + try: + self._clear_live_progress_notification() + except Exception: # noqa: BLE001 - cleanup must not stop the reset + self._logger.debug("Clearing the live notification failed", exc_info=True) + # B1: a new cycle may have started while the heavy post-processing above was # awaiting. If so, the manager's live-cycle fields (_current_program, # _cycle_start_time, _ranking_snapshot_cycle_id, progress) now belong to the @@ -6882,7 +6576,7 @@ class WashDataManager: self._last_estimate_time = None self._last_match_result = None # Clear so phase sensor resets to "Off" (issue #192) self._cycle_progress = 100.0 # 100% = cycle complete - self._cycle_completed_time = dt_util.now() + self._cycle_completed_time = utc_now() self._cycle_start_time = None self._ranking_snapshot_cycle_id = "" self._reset_live_notification_state() @@ -6897,11 +6591,15 @@ class WashDataManager: self._clean_state_start = None self._notified_clean_laundry = False self._reset_unload_nag_tracking() - if self._door_sensor_entity: + if not notif_rules.cycle_end_is_finish(cycle_status): + # An interrupted cycle finished nothing, so there is nothing to unload + # and no reminder to nag with (audit MANAGER-10). + self._logger.debug("Cycle ended %s: not entering Clean state", cycle_status) + elif self._door_sensor_entity: door_state = self.hass.states.get(self._door_sensor_entity) if door_state and door_state.state == "off": # binary_sensor: off = closed self._is_clean_state = True - self._clean_state_start = dt_util.now() + self._clean_state_start = utc_now() self._logger.debug( "Cycle ended with door closed: entering Clean state" ) @@ -6910,7 +6608,7 @@ class WashDataManager: # other way (#451): a button entity, or the Mark Unloaded button / # service driven by their own automation. self._is_clean_state = True - self._clean_state_start = dt_util.now() + self._clean_state_start = utc_now() self._logger.debug( "Cycle ended, unload confirmation configured: entering Clean state" ) @@ -6947,8 +6645,10 @@ class WashDataManager: 22:00-06:59). The end hour is exclusive at the hour granularity, so a window of start=22, end=7 covers hours 22, 23, 0..6. """ + # Quiet hours are local clock hours; interval stamps in this module are + # UTC (audit DETECT-01), so convert whatever the caller passes. return notif_rules.in_quiet_hours( - self._quiet_hours_bounds(), when or dt_util.now() + self._quiet_hours_bounds(), dt_util.as_local(when or utc_now()) ) def _seconds_until_quiet_end(self, when: datetime | None = None) -> float: @@ -6956,8 +6656,10 @@ class WashDataManager: Returns 0.0 when the feature is off or when not currently in quiet hours. """ + # Quiet hours are local clock hours; interval stamps in this module are + # UTC (audit DETECT-01), so convert whatever the caller passes. return notif_rules.seconds_until_quiet_end( - self._quiet_hours_bounds(), when or dt_util.now() + self._quiet_hours_bounds(), dt_util.as_local(when or utc_now()) ) def _queue_quiet_hours_notification( @@ -7030,6 +6732,140 @@ class WashDataManager: self._remove_quiet_hours_timer() self._remove_quiet_hours_timer = None + # ------------------------------------------------------------------ + # Held notifications across a restart (audit MANAGER-16) + # ------------------------------------------------------------------ + def _get_notify_queue_store(self) -> Store[dict[str, Any]]: + if getattr(self, "_notify_queue_store", None) is None: + self._notify_queue_store = Store( + self.hass, 1, f"{STORAGE_KEY}.{self.entry_id}.{NOTIFY_QUEUE_STORE_SUFFIX}" + ) + return self._notify_queue_store + + @staticmethod + def _persistable_notifications(queue: list[dict[str, Any]]) -> list[dict[str, Any]]: + """The queued entries worth keeping, as JSON-safe copies.""" + out: list[dict[str, Any]] = [] + for entry in queue: + if entry.get("event_type") in _NOTIFY_QUEUE_TRANSIENT_EVENTS: + continue + try: + json.dumps(entry) + except (TypeError, ValueError, OverflowError): + continue + out.append(dict(entry)) + return out + + async def _async_persist_notification_queues(self) -> None: + """Write the quiet-hours and presence queues to disk. Never raises. + + Touches storage only when there is something to keep, or a file this + manager wrote earlier is now stale. + """ + try: + quiet = self._persistable_notifications( + getattr(self, "_quiet_pending_notifications", None) or [] + ) + presence = self._persistable_notifications( + getattr(self, "_pending_notifications", None) or [] + ) + if quiet or presence: + await self._get_notify_queue_store().async_save( + { + "saved_at": utc_now().isoformat(), + "quiet": quiet, + "presence": presence, + } + ) + self._notify_queue_on_disk = True + self._logger.info( + "Kept %d held notification(s) for after the restart", + len(quiet) + len(presence), + ) + elif getattr(self, "_notify_queue_on_disk", False): + await self._get_notify_queue_store().async_remove() + self._notify_queue_on_disk = False + except Exception: # noqa: BLE001 - a stop or unload must not fail on this + self._logger.debug("Could not persist held notifications", exc_info=True) + + async def _async_on_ha_stop(self, _event: Event) -> None: + """Persist what lives only in memory before Home Assistant stops. + + HA does not unload config entries on a stop, so ``async_shutdown`` never runs + on a restart: the held notifications were lost and the active-cycle snapshot + was up to a minute old. Nothing is sent from here. + """ + if self._is_shutdown: + return + try: + if self.detector.state in { + STATE_RUNNING, STATE_PAUSED, STATE_STARTING, STATE_ENDING + } or self._in_anticrease_tail(): + snapshot = self._augment_active_snapshot( + self.detector.get_state_snapshot() + ) + await self.profile_store.async_save_active_cycle(snapshot) + except Exception: # noqa: BLE001 + self._logger.debug("Could not save the active cycle at stop", exc_info=True) + await self._async_persist_notification_queues() + + @callback + def _schedule_notify_queue_restore(self, _hass: HomeAssistant) -> None: + """Restore the held notifications once HA has started (notify services exist).""" + self._remove_notify_queue_restore = None + if not self._is_shutdown: + self._spawn_tracked(self._async_restore_notification_queues()) + + async def _async_restore_notification_queues(self) -> None: + """Re-dispatch the notifications held when HA last stopped. Never raises. + + Each one goes back through the normal gates, so it is held again if quiet + hours are still on or nobody is home, and delivered otherwise. The file is + deleted on read, so a later restart cannot deliver it twice. + """ + try: + store = self._get_notify_queue_store() + data = await store.async_load() + if data is None: + return + await store.async_remove() + except Exception: # noqa: BLE001 + self._logger.debug("Could not restore held notifications", exc_info=True) + return + if self._is_shutdown or not isinstance(data, dict): + return + saved_at = dt_util.parse_datetime(str(data.get("saved_at") or "")) + if ( + saved_at is None + or (utc_now() - dt_util.as_utc(saved_at)).total_seconds() + > _NOTIFY_QUEUE_MAX_AGE_S + ): + self._logger.info("Dropped held notifications saved at %s: too old", saved_at) + return + in_progress = self.detector.state in _CYCLE_IN_PROGRESS_STATES + restored = 0 + for key in ("quiet", "presence"): + entries = data.get(key) + for entry in entries if isinstance(entries, list) else []: + if not isinstance(entry, dict) or not isinstance(entry.get("message"), str): + continue + event_type = entry.get("event_type") + if event_type in _NOTIFY_QUEUE_TRANSIENT_EVENTS: + continue + if event_type in _NOTIFY_QUEUE_CYCLE_EVENTS and not in_progress: + continue + extra = entry.get("extra_vars") + self._dispatch_notification( + entry["message"], + title=entry.get("title"), + icon=entry.get("icon"), + event_type=event_type, + extra_vars=extra if isinstance(extra, dict) else None, + ) + restored += 1 + if restored: + self._logger.info("Restored %d held notification(s) after restart", restored) + # ------------------------------------------------------------------ # C2 - Milestone (cycle-count achievement) notifications # ------------------------------------------------------------------ @@ -7137,7 +6973,7 @@ class WashDataManager: """ try: pct = int(round(float(progress_pct))) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): pct = 0 pct = max(0, min(100, pct)) extras: dict[str, Any] = { @@ -7543,9 +7379,17 @@ class WashDataManager: # changes that, which is why the setting's help text says so (item 372). # Note the app reads the iOS-named key FIRST - harmless only because # both carry one value here, so do not let them diverge. + # + # #465: on an ordinary iOS notification the two keys mean different + # things: `color` is the avatar disc and `notification_icon_color` the + # glyph on it (white by default), so sending one value to both drew the + # icon in the disc's own colour and it vanished. Only a Live Activity + # reads `notification_icon_color` as the icon tint, so only the live + # update carries it; Android falls back to `color` either way. if icon_color and self._is_mobile_notify_service(notify_service): svc_data["color"] = icon_color - svc_data["notification_icon_color"] = icon_color + if event_type == NOTIFY_EVENT_LIVE: + svc_data["notification_icon_color"] = icon_color svc_data["progress_bar_color"] = icon_color state = ( @@ -7631,13 +7475,12 @@ class WashDataManager: # Reuse the notification's tag as a stable persistent-notification id so # the HA notifications tab collapses the lifecycle thread to one entry # instead of accumulating a new card per cycle (issue #248/#249 clutter). - _pn_create( + return _pn_create( self.hass, message, title=title, notification_id=ev.get("tag"), ) - return True return sent @@ -7673,7 +7516,7 @@ class WashDataManager: err, ) return False - except (ValueError, TypeError, HomeAssistantError) as err: + except (ValueError, TypeError, HomeAssistantError, OverflowError) as err: self._logger.error( "Invalid notification action configuration for %s: %s", self.config_entry.title, @@ -7798,7 +7641,7 @@ class WashDataManager: self._live_waiting_notification_sent = True else: self._live_notification_sent_count += 1 - self._last_live_notification_time = dt_util.now() + self._last_live_notification_time = utc_now() # The queued entry carries the same `activity: "start"` the direct # paths send, so the phone has a live activity either way and the # cycle-end teardown has to know about it (#446). Recorded only @@ -7865,7 +7708,7 @@ class WashDataManager: def _handle_noise_cycle(self, max_power: float) -> None: """Handle a detected noise cycle.""" # Clean up old noise events > 24h - now = dt_util.now() + now = utc_now() self._noise_events = [ t for t in getattr(self, "_noise_events", []) @@ -7879,7 +7722,8 @@ class WashDataManager: # If noise events exceed threshold in 24h, trigger tune if len(self._noise_events) >= self._noise_events_threshold: - self.hass.async_create_task(self._tune_threshold()) + # Tracked (audit MANAGER-13): it saves the store. + self._spawn_tracked(self._tune_threshold()) async def _tune_threshold(self) -> None: """Increase the minimum power threshold.""" @@ -7933,17 +7777,23 @@ class WashDataManager: self._current_program = "off" self._time_remaining = None self._total_duration = None - self._cycle_progress = 0.0 + # A probe out of a completed cycle keeps its 100 % (item 515): most + # abort back to Finished; RUNNING zeroes it when one commits. + if not ( + self.detector.state == STATE_STARTING + and self._cycle_completed_time is not None + ): + self._cycle_progress = 0.0 self._projected_energy_wh = None 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() + self._notify_update_deferrable() return - now = dt_util.now() + now = utc_now() # Throttle heavy matching to configured interval (default: 5 minutes) effective_match_interval = self._profile_match_interval @@ -7965,7 +7815,7 @@ class WashDataManager: # Also check notifications in loop self._check_pre_completion_notification() self._check_live_progress_notification() - self._notify_update() + self._notify_update_deferrable() return # No matching task trigger here anymore! @@ -7974,30 +7824,7 @@ class WashDataManager: self._update_remaining_only() self._check_pre_completion_notification() self._check_live_progress_notification() - self._notify_update() - - # _async_run_matching removed in favor of _async_perform_combined_matching - - def _analyze_trend(self, profile_name: str) -> bool: - """Analyze score history to detect positive trend. - - Returns True if score has increased in at least 7 of the last 10 intervals. - Requires at least 5 samples history to make a determination. - """ - history = self._score_history.get(profile_name, []) - if len(history) < 5: - return False - - # Use last 11 points to get 10 intervals (or fewer if history short) - recent = history[-11:] - if len(recent) < 2: - return False - - up_count = sum(1 for i in range(1, len(recent)) if recent[i] > recent[i - 1]) - total_intervals = len(recent) - 1 - - # Proportional threshold (7/10 => 0.7) - return (up_count / total_intervals) >= 0.70 + self._notify_update_deferrable() def _reset_live_notification_state( self, *, keep_activity_started: bool = False @@ -8190,7 +8017,7 @@ class WashDataManager: return interval = max(30, int(self._notify_live_interval_seconds)) - now = dt_util.now() + now = utc_now() if self._last_live_notification_time and ( now - self._last_live_notification_time ).total_seconds() < interval: @@ -8382,6 +8209,13 @@ class WashDataManager: or entry.get("event_type") in {NOTIFY_EVENT_START, "pre_complete"} ) ] + # ...and a "minutes left" reminder parked by quiet hours (audit MANAGER-09): + # it was delivered at the window's end, hours after the cycle, right + # before "finished". + self._quiet_pending_notifications = [ + entry for entry in self._quiet_pending_notifications + if entry.get("event_type") != "pre_complete" + ] # Always emit the clear when the user has any live channel configured. # The in-memory sent-count is unreliable after an HA restart (it resets @@ -8714,7 +8548,7 @@ class WashDataManager: self._envelope_position = None return - now = dt_util.now() + now = utc_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 @@ -8751,6 +8585,15 @@ class WashDataManager: if duration_so_far <= 0.0: return + # Item 514: a halt is not progress either. The detector's programme view + # leaves its stalls out of the elapsed time and the trace, like the + # Playground replay (`progress_elapsed_s` / `progress_trace`); the cycle + # timers above and the projected energy below keep the real figures. + prog_elapsed = getattr(self.detector, "progress_elapsed_s", None) + prog_elapsed = prog_elapsed(duration_so_far, now) if callable(prog_elapsed) else None + if isinstance(prog_elapsed, (int, float)): + duration_so_far = float(prog_elapsed) + if not (self._matched_profile_duration and self._matched_profile_duration > 0): # No profile matched - don't provide misleading time estimates. self._time_remaining = None @@ -8767,44 +8610,30 @@ class WashDataManager: ) return - # Compute the phase-aware and ML progress inputs via the manager's own - # wrappers (so per-call caching + test mocks apply), then hand them to the - # shared pure smoothing/back-calc in :mod:`progress` - the identical math - # the Playground simulation runs. + # Compute the phase-aware progress input via the manager's own wrapper + # (so per-call caching + test mocks apply), then hand it to the shared + # pure smoothing/back-calc in :mod:`progress` - the identical math the + # Playground simulation runs. trace = self.detector.get_power_trace() + prog_trace = getattr(self.detector, "progress_trace", None) + prog_trace = prog_trace(now) if callable(prog_trace) else None + if isinstance(prog_trace, list): + trace = prog_trace # item 514, see above phase_result = None if len(trace) >= 10 and self._current_program != "detecting...": phase_result = self._estimate_phase_progress( trace, duration_so_far, self._current_program ) - ml_pct = self._ml_progress_percent(trace, self._current_program) - - # Opt-in phase-resolved ETA (washing machine / washer-dryer only). Segment - # the observed-so-far trace, match against cached per-profile phase profiles, - # and blend the per-role budget remaining into the estimate (progress.py - # owns the blend). Gated + guarded: any failure leaves the proven estimate - # untouched (phase_remaining_s stays None -> byte-identical behaviour). - phase_remaining_s: float | None = None - if ( - len(trace) >= 10 - and self._current_program not in ("detecting...", "off", None) - and phase_matching_enabled(self.config_entry.options, self.device_type) - ): - pr = self.profile_store.phase_remaining( - trace, self.device_type, self._current_program - ) - if pr is not None: - phase_remaining_s = pr.get("remaining_s") result = progress_mod.compute_progress( self.device_type, float(self._matched_profile_duration), duration_so_far, - self._smoothed_progress, + progress_mod.ema_seed( + self._smoothed_progress, self._smoothed_for_program, self._current_program + ), phase_result, - ml_pct, self._logger, - phase_remaining_s=phase_remaining_s, # Real gap since the previous estimate, so the progress EMA keeps its # time constant instead of its step count - a plug that reports every # 30 s must not lag 6x further behind than one reporting every 5 s. @@ -8815,9 +8644,9 @@ class WashDataManager: self._cycle_progress = result.progress self._smoothed_progress = result.smoothed + self._smoothed_for_program = self._current_program self._time_remaining = result.remaining self._total_duration = result.total - self._last_total_duration_update = now self._update_projected_energy() self._update_cycle_anomaly(duration_so_far) @@ -8983,6 +8812,12 @@ class WashDataManager: """Notify entities of update.""" async_dispatcher_send(self.hass, SIGNAL_WASHER_UPDATE.format(self.entry_id)) + def _notify_update_deferrable(self) -> None: + """Notify, unless a power reading is being handled: its handler notifies + once when it returns, after everything this caller changed.""" + if not getattr(self, "_in_power_event", False): + self._notify_update() + def notify_update(self) -> None: """Public method to notify entities of update.""" self._notify_update() @@ -8992,6 +8827,21 @@ class WashDataManager: """Return True if cycle is currently user-paused.""" return self._is_user_paused + @property + def _is_user_paused(self) -> bool: + return self._user_paused_flag + + @_is_user_paused.setter + def _is_user_paused(self, value: bool) -> None: + # Mirrored into the detector so Smart Termination honours a user pause too: + # it was the one finisher that did not, and the pause itself carried the + # elapsed time past the ratio (audit DETECT-05). + self._user_paused_flag = bool(value) + detector = getattr(self, "detector", None) + setter = getattr(detector, "set_user_paused", None) + if callable(setter): + setter(self._user_paused_flag) + @property def is_clean_state(self) -> bool: """Return True if machine is in Clean state (cycle ended, door not yet opened).""" @@ -9003,23 +8853,32 @@ class WashDataManager: raw = float(self.detector.get_elapsed_seconds()) paused = self._total_user_paused_seconds if self._user_pause_start is not None: - paused += (dt_util.now() - self._user_pause_start).total_seconds() + paused += (utc_now() - self._user_pause_start).total_seconds() return max(0.0, raw - paused) def check_state(self): - """Return current detector state.""" + """Return the state entities show (the detector's exposed state).""" if self.recorder.is_recording: return STATE_RUNNING + state = self.detector.state + # The detector's display layer: a standby re-probe reads as off until it + # has evidence (item 501), a stalled cycle as paused and a two-level + # appliance at its standby level as idle (#452). A stand-in detector + # without one shows its raw state. + exposed = getattr(self.detector, "exposed_state", None) + if isinstance(exposed, str): + state = exposed # A completed cycle ends in STATE_FINISHED, not STATE_OFF; accept both # or the door-sensor Clean state (#153) is never surfaced (#282). - if self._is_clean_state and self.detector.state in ( + if self._is_clean_state and state in ( STATE_OFF, + STATE_IDLE, STATE_FINISHED, ): return STATE_CLEAN if self._is_user_paused: return STATE_USER_PAUSED - return self.detector.state + return state def list_phase_catalog(self, device_type: str) -> list[dict[str, Any]]: """Return the merged phase catalog for a device type.""" @@ -9041,6 +8900,10 @@ class WashDataManager: """Return more granular state info (e.g. current phase).""" if self.recorder.is_recording: return "Recording" + # Item 501 / #452, as in check_state. + exposed = getattr(self.detector, "exposed_sub_state", NotImplemented) + if exposed is None or isinstance(exposed, str): + return exposed return self.detector.sub_state @property @@ -9058,11 +8921,6 @@ class WashDataManager: """Return total predicted duration in seconds.""" return self._total_duration - @property - def last_total_duration_update(self) -> datetime | None: - """Return when total duration was last refined.""" - return self._last_total_duration_update - @property def cycle_progress(self): """Return cycle progress as a percentage.""" @@ -9080,7 +8938,13 @@ class WashDataManager: @property def cycle_anomaly(self) -> str: - """Runtime anomaly state for the current cycle ("none" | "overrun").""" + """Runtime anomaly state for the current cycle ("none" | "overrun" | "stalled"). + + ``stalled`` (#452) wins while the detector shows the cycle stalled; like + ``overrun`` it is visible only and never a notification. + """ + if getattr(self.detector, "stalled", False) is True: + return CYCLE_ANOMALY_STALLED return self._cycle_anomaly @property @@ -9117,19 +8981,56 @@ class WashDataManager: @property def maintenance_due(self) -> list[str]: - """Maintenance event types whose reminder threshold has been reached (E2). + """Ids of the maintenance tasks that are due (E2, #461): built-in types, then + custom tasks. - Surfaced as a state-sensor attribute + read by the panel banner. Never a - notification. Returns an empty list on any error. + Surfaced as a state-sensor attribute, the Maintenance-due binary sensor and + the panel banner. Never a notification. Returns an empty list on any error. """ try: - cfg = self.config_entry.options.get(CONF_MAINTENANCE_REMINDER_CYCLES) - if not isinstance(cfg, dict) or not cfg: - cfg = DEFAULT_MAINTENANCE_REMINDER_CYCLES - return self.profile_store.get_maintenance_due(cfg) + return self.profile_store.get_maintenance_due( + effective_reminders( + self.device_type, + self.config_entry.options.get(CONF_MAINTENANCE_REMINDER_CYCLES), + ) + ) except Exception: # noqa: BLE001 return [] + @property + def maintenance_status(self) -> list[dict[str, Any]]: + """Every active maintenance reminder with its progress (#461). Never raises. + + Rows from ``ProfileStore.get_maintenance_status`` against the device-type + aware reminder config (``maintenance.effective_reminders``). + """ + try: + return self.profile_store.get_maintenance_status( + effective_reminders( + self.device_type, + self.config_entry.options.get(CONF_MAINTENANCE_REMINDER_CYCLES), + ) + ) + except Exception: # noqa: BLE001 + return [] + + def _sync_maintenance_baselines(self) -> None: + """Stamp/clear the preset reminder types' counting origin (#461). Never raises. + + Run on every setup and config reload, after the store is loaded, so a + preset reminder that starts applying (an upgrade, or the user switching it + on) counts from now rather than opening as due. + """ + try: + self.profile_store.sync_maintenance_baselines( + effective_reminders( + self.device_type, + self.config_entry.options.get(CONF_MAINTENANCE_REMINDER_CYCLES), + ) + ) + except Exception: # noqa: BLE001 + self._logger.debug("Maintenance baseline sync failed", exc_info=True) + @property def current_power(self): """Return current power reading in watts. @@ -9169,8 +9070,16 @@ class WashDataManager: @property def sample_interval_stats(self): - """Return statistics about sampling intervals.""" - return self._sample_interval_stats + """The detector's own cadence window (audit MANAGER-14: this dict was never + filled, so the debug sensor's `sampling_p95` and diagnostics were empty).""" + dts = list(getattr(self.detector, "_recent_dts", None) or []) + if not dts: + return {} + return { + "p95": round(percentile_linear(dts, 95), 2), + "median": round(median_fast(dts), 2), + "count": len(dts), + } @property def pump_stuck(self) -> bool: @@ -9186,7 +9095,7 @@ class WashDataManager: """ if self.device_type != DEVICE_TYPE_PUMP: return 0 - cutoff = dt_util.now().timestamp() - 86400.0 + cutoff = utc_now().timestamp() - 86400.0 count = 0 for cycle in self.profile_store.get_past_cycles(): start_raw = cycle.get("start_time") @@ -9202,7 +9111,7 @@ class WashDataManager: ts = float(start_raw) if ts >= cutoff: count += 1 - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): continue return count @@ -9326,15 +9235,10 @@ class WashDataManager: 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). + inputs (register items 279/280). The rule is ``match_rules.profile_duration``, + shared with the switching rules and the Playground replay. """ - try: - avg = float(value) - except (TypeError, ValueError, OverflowError): - return None - if not math.isfinite(avg) or avg <= 0: - return None - return avg + return match_rules.profile_duration(value) def _apply_manual_program( self, profile_name: str, profile: dict[str, Any] | None @@ -9462,7 +9366,7 @@ class WashDataManager: self._logger.info("Cycle paused by user") prev_verified = self.detector._verified_pause self._is_user_paused = True - self._user_pause_start = dt_util.now() + self._user_pause_start = utc_now() self.detector.set_verified_pause(True) if self._pause_cuts_power: @@ -9492,7 +9396,7 @@ class WashDataManager: self._cancel_door_end_dwell() snapshot = self._augment_active_snapshot(self.detector.get_state_snapshot()) - self.hass.async_create_task(self.profile_store.async_save_active_cycle(snapshot)) + self._spawn_tracked(self.profile_store.async_save_active_cycle(snapshot)) self._notify_update() return True @@ -9508,7 +9412,7 @@ class WashDataManager: self._logger.debug("async_resume_cycle: not user-paused, ignoring") return False - now = dt_util.now() + now = utc_now() prev_pause_start = self._user_pause_start accumulated = ( (now - prev_pause_start).total_seconds() @@ -9552,7 +9456,7 @@ class WashDataManager: self._clear_timer_pause_notification() snapshot = self._augment_active_snapshot(self.detector.get_state_snapshot()) - self.hass.async_create_task(self.profile_store.async_save_active_cycle(snapshot)) + self._spawn_tracked(self.profile_store.async_save_active_cycle(snapshot)) self._notify_update() return True @@ -9560,6 +9464,14 @@ class WashDataManager: """Force terminate the current cycle via user request.""" self._logger.warning("Force terminating cycle by user request") + # A manual recording pins the shown state at running (check_state) while the + # detector is fed nothing, so user_stop() alone was a no-op and a forgotten + # recording kept the device "running" through restarts (#376, #383). Stop it + # as the Stop Recording button does: the run is kept for processing. + if self.recorder.is_recording: + self._logger.info("Force terminate: stopping the active manual recording") + await self.recorder.stop_recording() + # Trigger natural cycle end via detector # This will call _on_cycle_end callback, which handles: # - Saving to profile store @@ -9622,33 +9534,38 @@ class WashDataManager: return self._manual_program_active = False - # If running, revert to detecting so auto-detection can resume? - if self.detector.state == "running": + # A live cycle goes back to auto-detection. PAUSED and ENDING are live too + # (audit MANAGER-15): comparing against "running" alone showed "off" for a + # cycle still under way. The refresh mirrors set_manual_program's. + state = self.detector.state + self._matched_profile_duration = None + if state in (STATE_RUNNING, STATE_PAUSED, STATE_ENDING): self._current_program = "detecting..." - self._matched_profile_duration = None - self._update_estimates() # Trigger immediate re-detection attempt + self._last_phase_estimate_time = None + if state == STATE_RUNNING: + self._update_estimates() # Trigger immediate re-detection attempt + else: + self._update_remaining_only() else: - # If not running, clear the forced program + # No cycle under way: clear the forced program self._current_program = "off" - self._matched_profile_duration = None self._notify_update() self._logger.info("Manual program cleared, reverting to auto-detection") - async def _run_post_cycle_processing(self) -> None: - """Run post-cycle processing (merge fragments, split anomalies).""" + async def _run_post_cycle_processing(self, profiles: Any = ()) -> None: + """Refresh what the cycle that just ended invalidated (``profiles``' artifacts). + + Not the full maintenance any more (register item 456): that rebuilt every + envelope, recomputed every cycle's artifacts and re-matched every unlabelled + cycle at each cycle end. Those global passes run nightly. + """ try: - # User Feedback: Use 5 hour lookback and configured gap settings - stats = await self.profile_store.async_run_maintenance() - - # Log significant actions - merged = stats.get("merged_cycles", 0) - split = stats.get("split_cycles", 0) - if merged > 0 or split > 0: + stats = await self.profile_store.async_post_cycle_refresh(profiles) + if stats.get("orphaned_profiles"): self._logger.info( - "Post-cycle processing: Merged %s, Split %s cycle(s)", merged, split + "Post-cycle processing: removed %s orphaned profile(s)", + stats["orphaned_profiles"], ) - - # Note: async_run_maintenance saves automatically if changes occur except Exception as e: # pylint: disable=broad-exception-caught self._logger.error("Post-cycle processing failed: %s", e) \ No newline at end of file diff --git a/custom_components/ha_washdata/manifest.json b/custom_components/ha_washdata/manifest.json index 2df0222d..b5e088b6 100644 --- a/custom_components/ha_washdata/manifest.json +++ b/custom_components/ha_washdata/manifest.json @@ -2,10 +2,9 @@ "domain": "ha_washdata", "name": "WashData", "after_dependencies": [ - "lovelace", - "http", + "conversation", "frontend", - "websocket_api", + "lovelace", "recorder" ], "codeowners": [ @@ -13,13 +12,14 @@ ], "config_flow": true, "dependencies": [ - "conversation" + "http", + "websocket_api" ], "documentation": "https://github.com/3dg1luk43/ha_washdata", - "iot_class": "local_polling", + "iot_class": "calculated", "issue_tracker": "https://github.com/3dg1luk43/ha_washdata/issues", "requirements": [ "numpy" ], - "version": "0.5.7" + "version": "0.5.8" } diff --git a/custom_components/ha_washdata/ml/engine.py b/custom_components/ha_washdata/ml/engine.py index b2a9cb6e..f93318d0 100644 --- a/custom_components/ha_washdata/ml/engine.py +++ b/custom_components/ha_washdata/ml/engine.py @@ -21,11 +21,11 @@ This package holds compact, NumPy-only models trained offline in the ``promoted_manifest.json`` for provenance). The integration runtime stays NumPy-only; no sklearn/torch/scipy are imported. -The single runtime entry point is :func:`resolve_scorer`, which returns a scoring -callable for a capability, preferring an on-device trained spec over the shipped -embedded baseline. All live ML consumers go through it (the panel's ``ml_health`` -shadow comparison in ``ws_api`` and :class:`MLSuggestionEngine`), and any new -runtime consumer should too — feature extraction lives in ``feature_extraction`` +The runtime entry points are :func:`resolve_scorer`, which returns the shipped +embedded baseline's scoring callable for a classifier capability, and +:func:`resolve_regressor`, which returns an on-device trained regressor. All live +ML consumers go through them (the panel's ``ml_health`` shadow comparison in +``ws_api``), and any new runtime consumer should too - feature extraction lives in ``feature_extraction`` and gating in :func:`ml_models_enabled`, so there is no separate engine object. Each model consumes a feature mapping whose keys are the model's @@ -48,7 +48,6 @@ CONF_ENABLE_ML_MODELS = "enable_ml_models" # Logical capability -> generated model module name (without the _model suffix). _MODEL_MODULES = { "quality": "hybrid_curve_quality_model", - "live_match": "live_match_commit_model", "end": "cycle_end_detector_model", } @@ -56,8 +55,8 @@ _MODEL_MODULES = { _SIBLING_MODULES = ("trainer", "feature_extraction") # Imported baseline model modules, keyed by module name. Importing a module is a -# blocking call Home Assistant forbids inside the event loop, and every -# resolve_scorer() consumer (live matching, end detection, quality gating) runs +# blocking call Home Assistant forbids inside the event loop, and +# resolve_scorer() consumers (end detection, the panel's cycle health) run # there - so the modules are imported once from an import executor at setup # (:func:`preload_models`) and every later resolution is a dict lookup. A failed # import is cached as ``None`` so a broken install warns once instead of retrying @@ -127,123 +126,49 @@ def ml_models_enabled(options: Mapping[str, object] | None) -> bool: return bool(options.get(CONF_ENABLE_ML_MODELS, False)) -def resolve_scorer(capability: str, store: object | None): - """Return ``(score_fn, source)`` for a capability, preferring an on-device - trained spec over the shipped embedded baseline. +def resolve_scorer(capability: str): + """Return ``(score_fn, source)`` for a classifier capability's shipped baseline. ``score_fn`` maps a feature mapping -> float in [0,1]; ``source`` is - ``"on_device"`` or ``"baseline"``. Returns ``(None, None)`` when neither is - available. This is the single bridge that lets trained models (Stage 4) - actually reach inference (ML Lab shadow comparison + MLSuggestionEngine) - while transparently falling back to the baseline. + ``"baseline"``. Returns ``(None, None)`` when the capability has no embedded + model or it failed to import. The lookup hits the module cache warmed by + :func:`preload_models`, so no import happens in the event loop. (The + on-device classifier specs this used to prefer can no longer exist: their + training was removed in 0.5.8 and storage v17 drops their records.) """ - def _baseline(): - """Resolve the shipped embedded baseline scorer for this capability. + module_name = _MODEL_MODULES.get(capability) + if module_name is None: + return (None, None) + module = _load_model_module(module_name) + if module is None: + return (None, None) - Kept as a lazily-invoked helper so the baseline module is only looked up - when the on-device spec is absent *or* fails at call time. The lookup hits - the module cache warmed by :func:`preload_models`, so no import happens in - the event loop. - """ - module_name = _MODEL_MODULES.get(capability) - if module_name is None: - return (None, None) - module = _load_model_module(module_name) - if module is None: - return (None, None) - - def _baseline_score(feats, _m=module): - # The embedded baseline must never raise into live inference either - # (mirrors _on_device_score's call-time guard): on any scoring error - # log and return a neutral 0.0 so a gate treats the signal as absent - # rather than letting the exception reach live detection/matching. - try: - return float(_m.score(feats)) - except Exception as exc: # noqa: BLE001 - never raise into live inference - _LOGGER.warning( - "Embedded baseline scorer for capability %r failed at call " - "time, returning neutral 0.0: %s", capability, exc, - ) - return 0.0 - - return (_baseline_score, "baseline") - - # 1) On-device trained spec from the store. - if store is not None: + def _baseline_score(feats, _m=module): + # The embedded baseline must never raise into live inference: on any + # scoring error log and return a neutral 0.0 so a gate treats the signal + # as absent rather than letting the exception reach detection/matching. try: - versions = store.get_ml_model_versions() or {} # type: ignore[attr-defined] - record = versions.get(capability) - spec = record.get("spec") if isinstance(record, dict) else None - # Only treat a spec as a classifier here. A regression spec - # (standardized_linear) must never be sigmoid-squashed by score_spec; - # classifier and regression capability keys are disjoint today, but this - # guard keeps it safe if a key were ever reused. - if isinstance(spec, dict) and spec.get("kind") != "standardized_linear": - # Feature-column schema guard: a spec promoted under an older - # FEATURE_COLUMNS must be dropped rather than silently scoring on a - # stale/neutral-filled schema. The call-time guard already catches - # shape mismatches, but this catches them at load time and logs - # clearly, so users see a single warm-up warning instead of a - # per-inference warning storm. - module_name = _MODEL_MODULES.get(capability) - if module_name is not None: - try: - _bm = _load_model_module(module_name) - _expected = list(getattr(_bm, "FEATURE_COLUMNS", [])) - _stored = list(spec.get("feature_columns") or []) - if _expected and _stored and _stored != _expected: - _LOGGER.warning( - "Promoted spec for %r has stale feature schema " - "(%d cols vs current %d); reverting to baseline.", - capability, len(_stored), len(_expected), - ) - return _baseline() - except Exception: # noqa: BLE001 - schema check must not break inference - pass - - score_spec = _sibling_attr("trainer", "score_spec") - if score_spec is None: - return _baseline() - - def _on_device_score(feats, _s=spec): - # A malformed / dimensionally-incompatible promoted spec must - # never raise into live detection/matching: on any call-time - # error fall back to the embedded baseline (or a neutral 0.0). - try: - return float(score_spec(_s, feats)) - except Exception as exc: # noqa: BLE001 - never raise into live inference - _LOGGER.warning( - "Trained scorer for capability %r failed at call time, " - "falling back to baseline: %s", capability, exc, - ) - fn, _src = _baseline() - if fn is not None: - try: - return fn(feats) - except Exception: # noqa: BLE001 - baseline must not raise either - pass - return 0.0 - - return (_on_device_score, "on_device") - except Exception as exc: # noqa: BLE001 - never let a bad store break inference + return float(_m.score(feats)) + except Exception as exc: # noqa: BLE001 - never raise into live inference _LOGGER.warning( - "Failed to load trained spec for capability %r, falling back to baseline: %s", - capability, exc, + "Embedded baseline scorer for capability %r failed at call " + "time, returning neutral 0.0: %s", capability, exc, ) - # 2) Shipped embedded baseline module. - return _baseline() + return 0.0 + + return (_baseline_score, "baseline") def resolve_regressor(capability: str, store: object | None): """Return ``(predict_fn, source)`` for a regression capability. - Regression models (``"remaining_time"`` and ``"total_energy"``) have **no** + Regression models (since 0.5.8 only ``"total_energy"``) have **no** shipped embedded baseline - they are trained purely on-device (Stage 4) and stored as ``standardized_linear`` specs. This returns ``(None, None)`` until on-device training promotes one, so live behaviour is unchanged until then. ``predict_fn`` maps a feature mapping -> float in the model's target units - (a completion fraction in ~[0, 1] for both regression capabilities). + (for ``total_energy``, the energy fraction so far in ~[0, 1]). """ if store is None: return (None, None) diff --git a/custom_components/ha_washdata/ml/feature_extraction.py b/custom_components/ha_washdata/ml/feature_extraction.py index 991f60c4..a404cb84 100644 --- a/custom_components/ha_washdata/ml/feature_extraction.py +++ b/custom_components/ha_washdata/ml/feature_extraction.py @@ -24,12 +24,13 @@ Three feature extractors are implemented: - **Cycle-end detector** (``END_FEATURE_COLUMNS``): self-contained from a live power series + profile expectation; call ``latest_end_event_features``. -- **Live-match commit confidence** (``LIVE_MATCH_FEATURE_COLUMNS``): requires the - match ranking from ``ProfileStore`` plus the observed prefix; call - ``live_match_features``. +- **Remaining-time / energy regressors** (``PROGRESS_FEATURE_COLUMNS``): a + running-cycle prefix + profile expectation; call ``progress_features``. - **Hybrid cycle quality** (``QUALITY_FEATURE_COLUMNS``): requires the complete cycle power trace plus profile/match context; call ``quality_features``. +(The live-match commit features went with the early match commit in 0.5.8.) + All inputs are plain Python/NumPy (offset-seconds, watts), so this module has no Home Assistant dependency and is unit-tested directly. It is only invoked when the user opts into experimental ML models (see engine.py). @@ -108,38 +109,6 @@ def profile_expectation(cycles_points: Sequence[Sequence[Point]]) -> dict[str, f } -def profile_expectations(cycles: list[dict]) -> dict[str, dict[str, float]]: - """Median duration/energy/peak per profile from stored cycle dicts. - - The dict-based counterpart of :func:`profile_expectation` (which works from - decompressed traces): reads the ``duration``/``energy_wh``/``max_power`` - scalar fields already stored on each cycle. Shared by on-device training - (``training_task``) and the ML suggestion engine so the "profile expectation" - definition lives in one place. Profiles with no usable duration are skipped; - missing energy/peak default to 500. - """ - stats: dict[str, dict[str, list[float]]] = {} - for c in cycles: - name = c.get("profile_name") - if not isinstance(name, str) or not name: - continue - s = stats.setdefault(name, {"d": [], "e": [], "p": []}) - for key, field in (("d", "duration"), ("e", "energy_wh"), ("p", "max_power")): - v = c.get(field) - if isinstance(v, (int, float)) and not isinstance(v, bool): - s[key].append(float(v)) - out: dict[str, dict[str, float]] = {} - for name, s in stats.items(): - if not s["d"]: - continue - out[name] = { - "duration": float(np.median(s["d"])), - "energy": float(np.median(s["e"])) if s["e"] else 500.0, - "peak": float(np.median(s["p"])) if s["p"] else 500.0, - } - return out - - def latest_end_event_features( points: Sequence[Point], expectation: dict[str, float], @@ -195,83 +164,11 @@ def latest_end_event_features( } -# --------------------------------------------------------------------------- -# Live-match commit confidence -# --------------------------------------------------------------------------- - -# Mirrors ml_washdata/wash_ml/live_matching.py COMMIT_FEATURE_COLUMNS. -# The test suite asserts this equals the embedded model's FEATURE_COLUMNS. -LIVE_MATCH_FEATURE_COLUMNS = [ - "match_progress_top1", - "top1_distance", - "margin", - "distance_ratio", - "candidate_count_log", - "prefix_active_fraction", - "duration_ratio_top1", - "elapsed_log", -] - - -def live_match_features( - points: Sequence[Point], - elapsed_s: float, - top1_distance: float, - top2_distance: float | None, - top1_median_duration_s: float, - candidate_count: int, -) -> dict[str, float]: - """Features for the live-match commit-confidence model. - - Args: - points: Observed power readings (offset_s, watts) for the current prefix. - elapsed_s: Seconds elapsed since cycle start. - top1_distance: Blended RMSE+DTW shape distance to the top-1 candidate - prefix (as returned by the profile matcher). - top2_distance: Distance to the top-2 candidate; pass ``None`` or ``0.0`` - when only one candidate is available (margin defaults to 1.0). - top1_median_duration_s: Expected (median) duration of the top-1 candidate - profile in seconds. - candidate_count: Number of candidate profiles on this device. - - Returns a dict with exactly ``LIVE_MATCH_FEATURE_COLUMNS`` keys. - """ - elapsed = max(0.0, float(elapsed_s)) - top1 = max(0.0, float(top1_distance)) - top2_raw = float(top2_distance) if top2_distance is not None else 0.0 - top2 = top2_raw if top2_raw > 1e-9 else top1 + 1.0 - margin = max(0.0, top2 - top1) - dur = float(top1_median_duration_s) - progress = (elapsed / dur) if dur > 0 else 1.0 - - # prefix_active_fraction: fraction of prefix readings clearly above idle. - # Lab uses > 0.05 on a peak-normalised trace; equivalent here is > 5% of - # peak, with a 1 W floor so a cold trace never divides by near-zero. - if points: - powers = np.asarray([max(0.0, float(p)) for _, p in points], dtype=float) - peak = float(np.max(powers)) if powers.size else 0.0 - active_thr = max(1.0, 0.05 * peak) - prefix_active_fraction = float(np.mean(powers > active_thr)) if powers.size else 0.0 - else: - prefix_active_fraction = 0.0 - - return { - "match_progress_top1": float(min(progress, 2.0)), - "top1_distance": float(top1), - "margin": float(margin), - "distance_ratio": float(top1 / top2) if top2 > 1e-9 else 1.0, - "candidate_count_log": float(math.log1p(max(0, int(candidate_count)))), - "prefix_active_fraction": float(prefix_active_fraction), - "duration_ratio_top1": float(min(progress, 2.0)), - "elapsed_log": float(math.log1p(elapsed)), - } - - # --------------------------------------------------------------------------- # Remaining-time / progress regressor # --------------------------------------------------------------------------- -# Feature columns for the on-device remaining-time regressor. Unlike the three +# Feature columns for the on-device remaining-time regressor. Unlike the # classifier heads this model is a ``standardized_linear`` regressor whose target # is the cycle completion fraction (elapsed / total_actual). There is no shipped # baseline: the model exists only once on-device training promotes one over the diff --git a/custom_components/ha_washdata/ml/live_match_commit_feature_contract.json b/custom_components/ha_washdata/ml/live_match_commit_feature_contract.json deleted file mode 100644 index a31bc182..00000000 --- a/custom_components/ha_washdata/ml/live_match_commit_feature_contract.json +++ /dev/null @@ -1,42 +0,0 @@ -[ - { - "feature": "match_progress_top1", - "group": "live_match", - "runtime_source": "elapsed_seconds / top-1 candidate expected duration" - }, - { - "feature": "top1_distance", - "group": "live_match", - "runtime_source": "blended RMSE+DTW shape distance to the top-1 candidate prefix" - }, - { - "feature": "margin", - "group": "live_match", - "runtime_source": "top-2 distance minus top-1 distance (decision confidence)" - }, - { - "feature": "distance_ratio", - "group": "live_match", - "runtime_source": "top-1 distance / top-2 distance" - }, - { - "feature": "candidate_count_log", - "group": "live_match", - "runtime_source": "log1p(number of candidate profiles on the device)" - }, - { - "feature": "prefix_active_fraction", - "group": "live_match", - "runtime_source": "fraction of the observed prefix above the active threshold" - }, - { - "feature": "duration_ratio_top1", - "group": "live_match", - "runtime_source": "elapsed / top-1 expected duration (clipped)" - }, - { - "feature": "elapsed_log", - "group": "live_match", - "runtime_source": "log1p(elapsed seconds since cycle start)" - } -] \ No newline at end of file diff --git a/custom_components/ha_washdata/ml/live_match_commit_model.py b/custom_components/ha_washdata/ml/live_match_commit_model.py deleted file mode 100644 index 7cc47076..00000000 --- a/custom_components/ha_washdata/ml/live_match_commit_model.py +++ /dev/null @@ -1,129 +0,0 @@ -# WashData - Home Assistant integration for appliance cycle monitoring via smart plugs. -# Copyright (C) 2026 Lukas Bandura -# SPDX-License-Identifier: AGPL-3.0-or-later -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published -# by the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -"""Auto-generated by ml_washdata/wash_ml/promotion.py. Do not edit by hand. - -Embedded WashData model: 'live_match_commit' (target: 'match_top1_correct'). -Kind: standardized logistic regression. Runtime dependency: NumPy only. - -Regenerate with ``./ml.sh experiment`` in the ml_washdata lab and copy the new -file. Determinism check at generation time: max_abs_score_diff=1.6467e-08 -over 2392 rows. - -Usage in the integration:: - - from .live_match_commit_model import score, predict, FEATURE_COLUMNS - features = build_runtime_features(...) # must populate FEATURE_COLUMNS - is_positive = predict(features) -""" - -from __future__ import annotations - -import base64 -import gzip -import json -from typing import Mapping - -import numpy as np - -MODEL_NAME = 'live_match_commit' -MODEL_TARGET = 'match_top1_correct' -MODEL_KIND = 'standardized_logistic' -TARGET_UNITS = '' -# Training artifact for select_threshold only — NOT used at live inference. -# The live early-commit gate reads ML_MATCH_COMMIT_THRESHOLD from const.py. -THRESHOLD = 0.371786 - -FEATURE_COLUMNS = [ - 'match_progress_top1', - 'top1_distance', - 'margin', - 'distance_ratio', - 'candidate_count_log', - 'prefix_active_fraction', - 'duration_ratio_top1', - 'elapsed_log', -] - -# Provenance (metrics at training time): -MODEL_METRICS = json.loads("""{ - "owner_holdout": { - "accuracy": 0.771789, - "balanced_accuracy": 0.741539, - "f1": 0.836751, - "fn": 120, - "fp": 79, - "positive_rate": 0.675459, - "precision": 0.865874, - "problem_recall": 0.809524, - "rows": 872, - "specificity": 0.673554, - "tn": 163, - "tp": 510 - } -}""") - -_MODEL_BLOB = ( - 'H4sIAAAAAAACA1VU246kNhD9FYunRKGJDb7BKpGi0UaKtJONktmn1Qq5wXRbAxgZdzqb1fx7jmG6NXmiqMupOnXxt+zozJo1tNCM' - 'aVELnWednaMNWfOZFoJyJcqS57Rglaoha4gVo5UQsoQoGOe6ZiJnRc2kkoolX64oE7pi+RsEVWheUa7FF2TwdtjwS0UlFzo5Ms2o' - 'oila8lJQIWV+YEXJeF1zKfIDUJlAjUwmWai6qsSW7I6R1JwBRaqUI1gTbd+amDVZSUt5oPJQ1k8lbXjdUPEDpQ2lWZ4N8LsE23Z+' - 'vEwzevE5m0zszu0S/CnYdW2jXxgc06ft3RrN3Fn8Tyac3AzhpmuDic5D0Zm5dz3SA/Qyx3b0J2iXYAf3T2u66P627RCS4Lf4yxY4' - '7/G3dHY0ywoCKRh8Ti4CbZpc4iOpMFqW8Hp2cw9FKqA3oXf/7hGoyHWpRhuD60DqW+avsw3t2Y+9v8SkMF2HxN3XNH2lmNJ1nh3N' - 'mJigbW+N6HwF48C2RamkEgx/c9awkkJYskbBvPjVbdRAwyZP+HFRb8Q7tyauKVwKrXhS+uNopxY2M46bhdYCi5IFf0XBWpV5ti6I' - 'HFzn4tcdEHsHj5hSywoCUgtGX17ybDYTkmZjKmCf32u3YPLRbnP9AONhM5LdeOj8PLjegnJD/vgOnT8wcp8ecSuJZwvfgCoj2RbC' - 'TN8XwHzY4kn0xJCU82YkO/z1bGeyItCSn38CCPYodf4d8QAMV7da8mztsnu7+USQk6xXd481pHfDYANu8U1BR4u67UpuZcdUylMw' - 'brY98TO5WvNMzCX6w2iOdlzfkYD9c5Ml02WNifRyAczDx8fH357aX9//8vTpz/ftw8cPnx5//4sMwU8b4Y3QTiSYGSt2KtIKYm8Q' - '3t7eB1rQu27FENF+llSvcjpvXWnNucCVUq1Lzfj2JJSqqvFspJeE0ppj99KbwSvFZbVdNB4UhcD8fwh4B7A5FfuSMpztZDDtq1nP' - 'aIwp0P3Jp3OffG/HH7dzxXnadCz7NmzX+zrJu7W9zC5iNbKkuQ0pMavSPciX/wDk5KXIHwUAAA==' -) - -_MODEL_CACHE: dict | None = None - - -def _load() -> dict: - global _MODEL_CACHE - if _MODEL_CACHE is None: - payload = gzip.decompress(base64.b64decode(_MODEL_BLOB.encode("ascii"))) - spec = json.loads(payload.decode("utf-8")) - _MODEL_CACHE = { - "center": np.asarray(spec["center"], dtype=float), - "scale": np.asarray(spec["scale"], dtype=float), - "coef": np.asarray(spec["coef"], dtype=float), - "bias": float(spec["bias"]), - "threshold": float(spec["threshold"]), - "output_center": float(spec.get("output_center") or 0.0), - "output_scale": float(spec.get("output_scale") if spec.get("output_scale") is not None else 1.0), - "feature_columns": list(spec["feature_columns"]), - } - return _MODEL_CACHE - - -def score(features: Mapping[str, float]) -> float: - """Return the model probability in [0, 1] for one feature mapping.""" - model = _load() - vector = np.array( - [float(features.get(column) or 0.0) for column in model["feature_columns"]], - dtype=float, - ) - scaled = (vector - model["center"]) / model["scale"] - logit = float(scaled @ model["coef"] + model["bias"]) - logit = max(-60.0, min(60.0, logit)) - return 1.0 / (1.0 + np.exp(-logit)) - - -def predict(features: Mapping[str, float]) -> bool: - """True when the example crosses the embedded decision threshold.""" - return score(features) >= _load()["threshold"] diff --git a/custom_components/ha_washdata/ml/live_match_commit_parity.json b/custom_components/ha_washdata/ml/live_match_commit_parity.json deleted file mode 100644 index 897c78b0..00000000 --- a/custom_components/ha_washdata/ml/live_match_commit_parity.json +++ /dev/null @@ -1,110 +0,0 @@ -{ - "cases": [ - { - "expected_score": 0.0409139, - "features": { - "candidate_count_log": 2.30258509, - "distance_ratio": 0.99324714, - "duration_ratio_top1": 0.0713484, - "elapsed_log": 6.43615037, - "margin": 0.0003374, - "match_progress_top1": 0.0713484, - "prefix_active_fraction": 1.0, - "top1_distance": 0.04962666 - } - }, - { - "expected_score": 0.23638102, - "features": { - "candidate_count_log": 2.30258509, - "distance_ratio": 0.9767654, - "duration_ratio_top1": 0.77657169, - "elapsed_log": 9.10509096, - "margin": 0.00796904, - "match_progress_top1": 0.77657169, - "prefix_active_fraction": 0.28125, - "top1_distance": 0.33501266 - } - }, - { - "expected_score": 0.36610898, - "features": { - "candidate_count_log": 1.79175947, - "distance_ratio": 0.98383705, - "duration_ratio_top1": 0.67719326, - "elapsed_log": 8.00670085, - "margin": 0.00441228, - "match_progress_top1": 0.67719326, - "prefix_active_fraction": 0.328125, - "top1_distance": 0.26857515 - } - }, - { - "expected_score": 0.50799834, - "features": { - "candidate_count_log": 2.30258509, - "distance_ratio": 0.56963091, - "duration_ratio_top1": 0.56899238, - "elapsed_log": 8.35501068, - "margin": 0.12556275, - "match_progress_top1": 0.56899238, - "prefix_active_fraction": 0.40625, - "top1_distance": 0.16619322 - } - }, - { - "expected_score": 0.67345544, - "features": { - "candidate_count_log": 2.30258509, - "distance_ratio": 0.61349302, - "duration_ratio_top1": 0.99052133, - "elapsed_log": 8.74369111, - "margin": 0.12364826, - "match_progress_top1": 0.99052133, - "prefix_active_fraction": 0.296875, - "top1_distance": 0.19626385 - } - }, - { - "expected_score": 0.88134857, - "features": { - "candidate_count_log": 1.94591015, - "distance_ratio": 0.37757139, - "duration_ratio_top1": 1.01608929, - "elapsed_log": 6.5539334, - "margin": 0.1729941, - "match_progress_top1": 1.01608929, - "prefix_active_fraction": 0.859375, - "top1_distance": 0.10493994 - } - }, - { - "expected_score": 0.95653616, - "features": { - "candidate_count_log": 1.09861229, - "distance_ratio": 0.21720322, - "duration_ratio_top1": 0.30021136, - "elapsed_log": 7.89561849, - "margin": 0.51761286, - "match_progress_top1": 0.30021136, - "prefix_active_fraction": 0.234375, - "top1_distance": 0.14362244 - } - }, - { - "expected_score": 0.99762013, - "features": { - "candidate_count_log": 1.09861229, - "distance_ratio": 0.02465625, - "duration_ratio_top1": 1.00014576, - "elapsed_log": 9.5269014, - "margin": 1.86460354, - "match_progress_top1": 1.00014576, - "prefix_active_fraction": 0.09375, - "top1_distance": 0.04713633 - } - } - ], - "kind": "standardized_logistic", - "model": "live_match_commit" -} \ No newline at end of file diff --git a/custom_components/ha_washdata/ml/matching_tuner.py b/custom_components/ha_washdata/ml/matching_tuner.py deleted file mode 100644 index 79c51673..00000000 --- a/custom_components/ha_washdata/ml/matching_tuner.py +++ /dev/null @@ -1,471 +0,0 @@ -# WashData - Home Assistant integration for appliance cycle monitoring via smart plugs. -# Copyright (C) 2026 Lukas Bandura -# SPDX-License-Identifier: AGPL-3.0-or-later -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published -# by the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -"""On-device tuning of the matcher's scoring weights (Stage 4/5, opt-in). - -Mirrors the offline ``devtools/dtw_ab_eval.py`` methodology but as a shippable, -NumPy-only, executor-safe pure function: it does leave-one-out matching over the -device's own labelled cycles, sweeps a small grid of the highest-impact scoring -weights (corr/MAE split, duration agreement weight, energy agreement weight, and -DTW ensemble weight independently), and - only if a candidate beats the shipped -defaults on a HELD-OUT split by a margin - returns a per-device config override. The caller persists it; the matcher reads it live -and falls back to the const defaults otherwise. - -Discipline (same as model promotion): tune on a train split, gate on a held-out -split, require a margin, cap the grid to bounded scoring weights (never -structural behaviour). This guards against over-fitting the small, partly -manually-labelled per-user cycle set. -""" -from __future__ import annotations - -from typing import Any - -import numpy as np - -from .. import analysis -from ..const import ( - DEFAULT_DTW_BANDWIDTH, - DEFAULT_DTW_MODE, - DEFAULT_PROFILE_MATCH_MAX_DURATION_RATIO, - DEFAULT_PROFILE_MATCH_MIN_DURATION_RATIO, -) -from ..signal_processing import resample_adaptive, resample_uniform - -#: Matches the production matcher (``profile_store.async_match_profile``). -_MIN_DT = 5.0 -_GAP_S = 21600.0 - - -def _series(cycle: dict[str, Any]) -> tuple[np.ndarray, np.ndarray] | None: - """``(offsets_s, watts)`` from a stored cycle, or None if unusable. - - The offsets matter: ``analysis.find_best_alignment`` compares the two curves - **index by index** (its ``dt`` argument is explicitly unused), so both sides - must be on the same seconds-per-sample grid or the MAE compares different - moments of the cycle. Dropping the offsets - as this module did until register - item 303 - makes that impossible to honour. - """ - pd = cycle.get("power_data") or [] - ts: list[float] = [] - pw: list[float] = [] - for p in pd: - # Both values are converted BEFORE either is appended: a row whose offset - # parses and whose power does not would otherwise leave `ts` one element - # longer than `pw`, and `resample_uniform` raises inside `np.interp` on - # unequal arrays - a tuning run lost to one malformed sample. - # OverflowError too: `json` keeps an oversized integer literal as an - # unbounded int, and `float()` on one raises rather than returning inf. - try: - offset, watts = float(p[0]), float(p[1]) - except (TypeError, ValueError, IndexError, OverflowError): - continue - ts.append(offset) - pw.append(watts) - if len(pw) < 4: - return None - return np.asarray(ts, dtype=float), np.asarray(pw, dtype=float) - - -def _longest(segments: list[Any]) -> Any | None: - return max(segments, key=lambda s: len(s.power)) if segments else None - - -def _prep(cycles: list[dict[str, Any]]) -> dict[str, list[dict[str, Any]]]: - """Group labelled cycles by profile, keeping the raw time series.""" - by_profile: dict[str, list[dict[str, Any]]] = {} - for c in cycles: - name = c.get("profile_name") - series = _series(c) - if not name or series is None: - continue - try: - dur = float(c.get("duration")) - except (TypeError, ValueError): - dur = 0.0 - if dur <= 0: - # No reliable wall-clock duration: skip rather than fabricate one from the - # sample count (len(pw)), which distorts duration scoring on devices that - # sample every 30-60 s. Real cycles always carry a 'duration', so this only - # drops degenerate entries. - continue - ts, pw = series - review = c.get("ml_review") - golden = bool(review.get("golden")) if isinstance(review, dict) else False - by_profile.setdefault(name, []).append( - {"ts": ts, "pw": pw, "dur": dur, "golden": golden} - ) - return by_profile - - -def _regrid(item: dict[str, Any], dt: float, cache: dict) -> list[float] | None: - """One cycle's curve on a ``dt``-second grid, cached per (cycle, dt).""" - key = (id(item), round(float(dt), 2)) - if key in cache: - return cache[key] - seg = _longest(resample_uniform(item["ts"], item["pw"], dt_s=dt, gap_s=_GAP_S)) - out = seg.power.tolist() if seg is not None and len(seg.power) >= 2 else None - cache[key] = out - return out - - - -def _envelope_avg( - name: str, pool: list[dict[str, Any]], excluded: int | None, dt: float, cache: dict -) -> list[float] | None: - """The pool's DTW-warped envelope average, re-gridded to the query's ``dt``. - - This is what `ProfileStore.async_match_profile` scores against once a profile - has >= 2 confirmed cycles and no pinned golden cycle, i.e. the common case - (register item 347(e)). - - Cheap enough to do honestly, which the deferral used to deny (item 354): an - envelope is built from traces, so it does not depend on the config being - tuned and survives the whole grid search, and leave-one-out changes only the - target's OWN profile - so the number of distinct envelopes is - ``profiles + targets``, not ``folds x profiles``. Both caches below are keyed - accordingly. - """ - ekey = ("env", name, excluded) - built = cache.get(ekey) - if built is None: - raw = [ - (it["ts"].tolist(), it["pw"].tolist(), float(it["dur"])) - for it in pool - ] - try: - built = analysis.compute_envelope_worker(raw, _BASE_CFG["dtw_bandwidth"]) - except Exception: # pylint: disable=broad-exception-caught - built = False # cache the failure; do not retry per target - cache[ekey] = built - if not built: - return None - grid, _lo, _hi, avg, _std, _target = built - if len(grid) < 2 or len(avg) != len(grid): - return None - gkey = ("envgrid", name, excluded, round(float(dt), 2)) - if gkey in cache: - return cache[gkey] - seg = _longest( - resample_uniform( - np.asarray(grid, dtype=float), np.asarray(avg, dtype=float), - dt_s=dt, gap_s=_GAP_S, - ) - ) - out = seg.power.tolist() if seg is not None and len(seg.power) >= 2 else None - cache[gkey] = out - return out - -def _snaps( - by_profile: dict[str, list[dict]], - exclude: tuple[str, int] | None, - dt: float, - cache: dict, -) -> list[dict[str, Any]]: - """One snapshot per profile, re-gridded to the QUERY's ``dt``. - - Mirrors production: ``profile_store`` resamples the current cycle with - ``resample_adaptive`` and then re-grids every candidate to that same - ``used_dt`` via ``_get_cached_sample_segment``, so index *i* is the same - elapsed time on both sides. - - **The template follows production's three-way rule, in production's order** - (`ProfileStore.async_match_profile`), which is register item 356 closing item - 347(e): a pinned golden cycle's own sharp trace, else the DTW-warped ENVELOPE - AVERAGE once the pool holds >= 2 cycles, else the single representative - sample - which is also the safety net when an envelope will not build. The - middle branch is the common case, and it is the one that used to be missing: - scoring one representative cycle where live matching scores the envelope - average can favour weights that win on a trace nothing is matched against. - - **Cost, and the trap inside it.** An envelope is built from traces, so it does - not depend on the weights being tuned and survives the whole grid search plus - every holdout call; and leave-one-out excludes exactly ONE cycle, so only the - target's own profile gets a different template while every other profile - keeps the full-pool one, shared by every target. Distinct templates are - therefore `profiles + targets`, not `folds x profiles` - measured on the worst - real export in `cycle_data/` (12 profiles with >= 2 cycles, 63 targets), 75 - templates at 79 ms a DTW warp, ~5.9 s one-off. **That figure only holds if the - cache outlives one `_top1` call.** It did not at first: `_top1` built a fresh - cache and is called ~59 times (1 base + 48 grid + 10 holdout), which measured - 16.1 s -> 360.6 s on a real export. Hence the `cache` argument threaded - through the whole run - nothing in it depends on `cfg`. Do not re-scope it. - - The cheap approximation (averaging the pool's regridded curves without the - DTW warp) is measured WORSE and is not what `_envelope_avg` does. - - The exposure is bounded regardless: `tune_matching_config` can - only move the bounded scoring weights, never structural matching behaviour, - `revert_matching_config` undoes it, and promotion is gated on **held-out - top-1 accuracy**: `tune_matching_config` promotes only when the tuned config - beats the baseline by `margin` on at least `min_wins` of `n_splits` held-out - subsamples (4 of 5) AND the mean held-out top-1 gain is itself >= `margin`. - Not AUC - no AUC is computed anywhere in this module. The AUC gate in - CLAUDE.md is `ML_TRAINING_AUC_MARGIN`, which governs the ml/ CLASSIFIERS in - `training_task.py` and has nothing to do with the matcher's scoring weights. - """ - snaps = [] - for name, items in by_profile.items(): - excluded = exclude[1] if (exclude is not None and exclude[0] == name) else None - pool = [ - it for idx, it in enumerate(items) - if exclude is None or (name, idx) != exclude - ] - if not pool: - continue - durs = [it["dur"] for it in pool] - avg = float(np.mean(durs)) - # Production's three-way template rule, in production's order - # (`ProfileStore.async_match_profile`). Getting this wrong is register - # item 347(e): scoring one representative cycle where live matching - # scores the envelope average can favour weights that lose on the curve - # that actually does the matching. - curve = None - golden = [it for it in pool if it.get("golden")] - if golden: - # 1. a pinned golden cycle keeps its own sharp trace; the average - # smears the wash-phase peaks, which is why production prefers it. - curve = _regrid(golden[0], dt, cache) - elif len(pool) >= 2: - # 2. the common case: the DTW-warped envelope average. - curve = _envelope_avg(name, pool, excluded, dt, cache) - if not curve: - # 3. the single-sample fallback, and the safety net for a profile - # whose envelope will not build (too few usable points, a trace - # that resamples to nothing). Representative rather than - # arbitrary: closest to the pool's mean duration. - rep = min(pool, key=lambda it: abs(it["dur"] - avg)) - curve = _regrid(rep, dt, cache) - if not curve: - continue - snaps.append({ - "name": name, - "avg_duration": avg, - "sample_power": curve, - }) - return snaps - - -def _top1( - by_profile: dict[str, list[dict]], - targets: list[tuple[str, int]], - cfg: dict[str, Any], - cache: dict | None = None, -) -> float: - """Fraction of the given (profile, idx) targets whose true profile ranks #1 - under leave-one-out matching with the given config. - - ``cache`` is shared ACROSS calls on purpose. Nothing in it depends on - ``cfg``: the re-gridded curves are keyed by (cycle, dt) and the envelopes by - (profile, excluded index), while ``cfg`` only changes the scoring weights. - Left per-call - as it was - the grid search rebuilds every envelope for each - of its ~59 configs, which measured 16 s -> 361 s on one real export. This is - what makes the `profiles + targets` cost in register item 354 real rather - than theoretical.""" - if not targets: - return 0.0 - correct = 0 - total = 0 - if cache is None: - cache = {} - for name, idx in targets: - it = by_profile[name][idx] - # The query defines the grid, exactly as in production. - segments, used_dt = resample_adaptive( - it["ts"], it["pw"], min_dt=_MIN_DT, gap_s=_GAP_S - ) - seg = _longest(segments) - if seg is None or len(seg.power) < 4: - continue - snaps = _snaps(by_profile, (name, idx), used_dt, cache) - if len(snaps) < 2: - continue - cands = analysis.compute_matches_worker( - seg.power.tolist(), it["dur"], snaps, cfg - ) - total += 1 - if cands and cands[0]["name"] == name: - correct += 1 - return correct / total if total else 0.0 - - -# The full production matcher config. A PARTIAL config does not inherit the -# production defaults - `compute_matches_worker` has its own fallbacks - so -# omitting a key here would tune the weights against a pipeline that never -# ships. `energy_mode` is added per device type at the call site. -_BASE_CFG = { - "min_duration_ratio": DEFAULT_PROFILE_MATCH_MIN_DURATION_RATIO, - "max_duration_ratio": DEFAULT_PROFILE_MATCH_MAX_DURATION_RATIO, - "dtw_bandwidth": DEFAULT_DTW_BANDWIDTH, - "dtw_mode": DEFAULT_DTW_MODE, -} - -#: Bounded scoring weights the tuner may promote. All live in [0, 1], so a tuned -#: config can only shift emphasis (shape vs level vs energy, and how much the DTW -#: ensemble leans on the derivative/DDTW component) - never structural behaviour. -OVERRIDE_KEYS = ("corr_weight", "duration_weight", "energy_weight", "dtw_ensemble_w") - - -def _grid() -> list[dict[str, Any]]: - """Small, high-impact grid over four bounded scoring weights. - - Axes: corr/MAE split × duration agreement weight × energy agreement weight - × DTW ensemble weight. The duration and energy axes are now independent so - the tuner can find asymmetric configurations (e.g. a device with highly - variable energy but stable duration benefits from a low energy_weight and a - high duration_weight). All values are bounded scoring weights (see - OVERRIDE_KEYS) so a promoted config can never change structural behaviour. - Grid size: 4 × 2 × 2 × 3 = 48 configurations (was 4 × 2 × 3 = 24). - """ - out = [] - for cw in (0.40, 0.45, 0.50, 0.60): - for dur_w in (0.15, 0.22): - for en_w in (0.15, 0.22): - for ew in (0.55, 0.70, 0.85): - out.append({ - "corr_weight": cw, - "duration_weight": dur_w, - "energy_weight": en_w, - "dtw_ensemble_w": ew, - }) - return out - - -def tune_matching_config( - cycles: list[dict[str, Any]], - device_type: str | None = None, - *, - min_cycles: int = 25, - # Kept intentionally low so per-device tuning becomes useful early; the noise - # a small sample would introduce is controlled by the multi-split majority gate - # below (a lucky single split can't promote), not by a large ``min_targets``. - min_targets: int = 12, - margin: float = 0.03, - seed: int = 0, -) -> dict[str, Any]: - """Leave-one-out per-device tuning of matcher scoring weights. - - Methodology (no target leakage between selection and gating): - 1. Partition the device's labelled cycles ONCE into a *search* pool and an - untouched *holdout* pool; no target is ever used for both. - 2. **Select** the candidate config as the grid entry with the best - leave-one-out top-1 on the SEARCH pool only. (Reference snapshots are - built from all cycles — as in production, where a query is matched - against aggregates of the full profile library; only the *query* targets - are partitioned.) - 3. **Gate** the fixed candidate on the HOLDOUT pool: it must beat the - shipped defaults by at least ``margin`` on a MAJORITY of reshuffled - holdout subsamples (a variance check that rejects a lucky single split) - AND on the holdout mean. ``min_targets`` is kept intentionally low so - per-device tuning becomes useful early; the majority gate — not a large - sample — controls the noise. - - Returns a status dict; ``promoted`` is True only when both holdout gates pass. - When promoted, ``config`` holds the override to persist (bounded scoring - weights only — never structural matching behaviour). Never raises for data - reasons; returns {"promoted": False, "reason": ...}. - """ - by_profile = _prep(cycles) - multi = {n: items for n, items in by_profile.items() if len(items) >= 2} - n_cycles = sum(len(v) for v in by_profile.values()) - if len(multi) < 2 or n_cycles < min_cycles: - return {"promoted": False, "reason": "insufficient data", "n_cycles": n_cycles, "n_profiles": len(by_profile)} - - # Partition targets ONCE, up front, into a search pool (used to pick the - # candidate config) and an untouched holdout pool (used only to gate it). No - # target is ever used for both selection and gating -> no target leakage. - rng = np.random.default_rng(seed) - targets = [(n, i) for n, items in multi.items() for i in range(len(items))] - rng.shuffle(targets) - if len(targets) < min_targets: - return {"promoted": False, "reason": "too few targets", "n_targets": len(targets)} - cut = max(1, len(targets) // 2) - search_pool, holdout_pool = targets[:cut], targets[cut:] - if not holdout_pool: - return {"promoted": False, "reason": "too few targets", "n_targets": len(targets)} - - # Tune under the same Stage-4 energy mode production uses for this device type, - # so promoted weights are consistent with the live matcher. - base = {**_BASE_CFG, "energy_mode": analysis.stage4_energy_mode(device_type)} - # Candidate: the grid config with the best top-1 on the SEARCH pool only. - # One cache for the entire run: the grid search and both holdout arms all - # reuse the same envelopes and re-gridded curves (see `_top1`). - shared: dict = {} - best_search = _top1(by_profile, search_pool, base, shared) - best_cfg = base - for extra in _grid(): - acc = _top1(by_profile, search_pool, {**base, **extra}, shared) - if acc > best_search: - best_search, best_cfg = acc, {**base, **extra} - override = {k: best_cfg[k] for k in OVERRIDE_KEYS if k in best_cfg} - - # Gate the FIXED candidate on the held-out pool: require it to beat the defaults - # by ``margin`` on a MAJORITY of reshuffled subsamples of the holdout (variance - # check), rejecting a lucky single split while keeping min_targets low. - n_splits, min_wins = 5, 4 - base_tests: list[float] = [] - tuned_tests: list[float] = [] - wins = 0 - for k in range(n_splits): - r = np.random.default_rng(seed + 1 + k) - pool = list(holdout_pool) - r.shuffle(pool) - held = pool[: max(1, len(pool) // 2)] - bt = _top1(by_profile, held, base, shared) - tt = _top1(by_profile, held, best_cfg, shared) - base_tests.append(bt) - tuned_tests.append(tt) - if tt - bt >= margin: - wins += 1 - mean_base = float(np.mean(base_tests)) if base_tests else 0.0 - mean_tuned = float(np.mean(tuned_tests)) if tuned_tests else 0.0 - has_override = bool(override) - enough_wins = wins >= min_wins - enough_margin = (mean_tuned - mean_base) >= margin - promoted = has_override and enough_wins and enough_margin - if promoted: - reason = f"beat baseline on {wins}/{n_splits} held-out subsamples" - elif not has_override: - # AUDITED and correct (register item 356), because it fires on every real - # export in `cycle_data/` and that looks like a stuck mechanism. It is - # not. `_BASE_CFG` deliberately omits the four OVERRIDE_KEYS, so - # `compute_matches_worker` falls back to `MATCH_CORR_WEIGHT` / - # `MATCH_DURATION_WEIGHT` / `MATCH_ENERGY_WEIGHT` / - # `MATCH_DTW_ENSEMBLE_W` - the tuner's baseline IS production's default, - # not a partial config with different fallbacks. `override` is therefore - # empty exactly when no grid entry beat that baseline, which is what this - # string says. The weights do reach the scorer: on the least saturated - # real device (12 profiles, 63 targets, base top-1 0.635) the grid - # produces three distinct scores, all <= base. The grid also contains the - # exact default combination (0.45 / 0.22 / 0.22 / 0.70), so the defaults - # are evaluated on equal terms, and `acc > best_search` is strict so a tie - # keeps them. Consistent with the corpus result that matcher top-1 is - # near-saturated: there is nothing here for per-device tuning to win. - reason = "defaults already optimal (no override)" - elif not enough_wins: - reason = f"only {wins}/{n_splits} held-out subsamples beat baseline by margin" - else: - reason = f"mean held-out gain {mean_tuned - mean_base:+.3f} below margin {margin}" - return { - "promoted": promoted, - "config": override if promoted else None, - "baseline_test_top1": round(mean_base, 3), - "tuned_test_top1": round(mean_tuned, 3), - "train_top1": round(best_search, 3), - "holdout_wins": wins, - "holdout_splits": n_splits, - "n_targets": len(targets), - "reason": reason, - } diff --git a/custom_components/ha_washdata/ml/promoted_manifest.json b/custom_components/ha_washdata/ml/promoted_manifest.json index a812a6be..02456180 100644 --- a/custom_components/ha_washdata/ml/promoted_manifest.json +++ b/custom_components/ha_washdata/ml/promoted_manifest.json @@ -65,32 +65,7 @@ "name": "hybrid_curve_quality", "target": "problem_cycle", "target_units": "" - }, - { - "created_at": "2026-06-29T20:49:05+00:00", - "git_commit": "605a862", - "kind": "standardized_logistic", - "metrics": { - "owner_holdout": { - "accuracy": 0.771789, - "balanced_accuracy": 0.741539, - "f1": 0.836751, - "fn": 120, - "fp": 79, - "positive_rate": 0.675459, - "precision": 0.865874, - "problem_recall": 0.809524, - "rows": 872, - "specificity": 0.673554, - "tn": 163, - "tp": 510 - } - }, - "module": "live_match_commit_model.py", - "name": "live_match_commit", - "target": "match_top1_correct", - "target_units": "" } ], "source": "ml_washdata/output/promoted" -} \ No newline at end of file +} diff --git a/custom_components/ha_washdata/ml/trainer.py b/custom_components/ha_washdata/ml/trainer.py index fc5bfc51..2d4a6338 100644 --- a/custom_components/ha_washdata/ml/trainer.py +++ b/custom_components/ha_washdata/ml/trainer.py @@ -16,13 +16,11 @@ # along with this program. If not, see . """On-device, NumPy-only model training for WashData (Stage 4). -This is the runtime counterpart of the offline lab's promotion pipeline. All -three embedded models are ``standardized_logistic`` heads - a mean/std scaler -plus a weight vector, bias, and decision threshold - which the lab fits with a -short pure-NumPy gradient descent (``wash_ml/end_detection.py::_fit_logistic``). -This module reproduces that fit and the exact scoring math the embedded -``*_model.py`` modules use, so a model trained here on the user's own cycles is -byte-compatible with the shipped baseline and can be scored identically. +Since 0.5.8 only the ``total_energy`` regressor is trained on-device +(:func:`fit_ridge` + :func:`build_regression_spec`, scored by +:func:`predict_value_spec`). The classifier training and the logistic spec +scoring that used to live here went with the on-device classifier heads +(register items 419, 518); the classifiers run their shipped baselines only. No new dependencies: NumPy only. Nothing here runs unless the caller (behind the ``ENABLE_ML_TRAINING`` flag) invokes it. @@ -38,261 +36,14 @@ import numpy as np PROMOTION_SCHEMA = "washdata.promoted_model/1" -def _sigmoid(values: np.ndarray) -> np.ndarray: - clipped = np.clip(values, -60.0, 60.0) - return 1.0 / (1.0 + np.exp(-clipped)) - - -def fit_logistic( - matrix: np.ndarray, - labels: np.ndarray, - *, - l2: float = 0.01, - learning_rate: float = 0.2, - iterations: int = 4000, -) -> dict[str, np.ndarray | float]: - """Fit a class-balanced, L2-regularised logistic head with NumPy GD. - - Identical in shape to the lab's ``_fit_logistic``: mean/std standardisation, - inverse-frequency class weights, and a fixed-step gradient descent. Returns - ``{center, scale, coef, bias}``. - """ - matrix = np.asarray(matrix, dtype=float) - labels = np.asarray(labels, dtype=float) - if matrix.ndim != 2 or matrix.shape[0] == 0: - raise ValueError("matrix must be a non-empty 2D array") - if labels.shape[0] != matrix.shape[0]: - raise ValueError("labels/matrix row mismatch") - if len(np.unique(labels)) < 2: - raise ValueError( - f"fit_logistic requires both positive and negative examples; " - f"got labels: {np.unique(labels)}" - ) - - center = np.mean(matrix, axis=0) - scale = np.std(matrix, axis=0) - scale = np.where(scale <= 1e-8, 1.0, scale) - scaled = (matrix - center) / scale - - weight = np.ones(labels.size, dtype=float) - for label_value in (0.0, 1.0): - mask = labels == label_value - count = float(np.sum(mask)) - if count > 0: - weight[mask] = labels.size / (2.0 * count) - normalized = weight / (float(np.sum(weight)) or 1.0) - - coef = np.zeros(matrix.shape[1], dtype=float) - bias = 0.0 - for _ in range(iterations): - predictions = _sigmoid(scaled @ coef + bias) - residual = (predictions - labels) * normalized - coef -= learning_rate * (scaled.T @ residual + l2 * coef) - bias -= learning_rate * float(np.sum(residual)) - return {"center": center, "scale": scale, "coef": coef, "bias": float(bias)} - - -def _safe_ratio(numerator: float, denominator: float) -> float: - return float(numerator) / float(denominator) if denominator else 0.0 - - -def binary_metrics(labels: np.ndarray, scores: np.ndarray, threshold: float) -> dict[str, Any]: - """Confusion-matrix metrics at a threshold (pure NumPy).""" - labels = np.asarray(labels, dtype=float) - scores = np.asarray(scores, dtype=float) - if labels.size == 0: - return {} - predictions = (scores >= threshold).astype(int) - tp = int(np.sum((labels == 1) & (predictions == 1))) - fp = int(np.sum((labels == 0) & (predictions == 1))) - tn = int(np.sum((labels == 0) & (predictions == 0))) - fn = int(np.sum((labels == 1) & (predictions == 0))) - precision = _safe_ratio(tp, tp + fp) - recall = _safe_ratio(tp, tp + fn) - specificity = _safe_ratio(tn, tn + fp) - f1 = _safe_ratio(2.0 * precision * recall, precision + recall) - accuracy = _safe_ratio(tp + tn, labels.size) - positive_rate = _safe_ratio(int(np.sum(labels == 1)), labels.size) - # Key names mirror the shipped MODEL_METRICS schema (see *_model.py): - # ``problem_recall`` (recall of the positive/"problem" class) and - # ``positive_rate`` (base rate of positives), so on-device-trained metrics - # are schema-identical to the embedded baselines they are compared against. - return { - "rows": int(labels.size), - "tp": tp, "fp": fp, "tn": tn, "fn": fn, - "precision": round(precision, 6), - "problem_recall": round(recall, 6), - "positive_rate": round(positive_rate, 6), - "specificity": round(specificity, 6), - "balanced_accuracy": round((recall + specificity) / 2.0, 6), - "f1": round(f1, 6), - "accuracy": round(accuracy, 6), - } - - -def auc(labels: np.ndarray, scores: np.ndarray) -> float: - """Rank-based ROC AUC (Mann-Whitney U). 0.5 when one class is absent.""" - labels = np.asarray(labels, dtype=float) - scores = np.asarray(scores, dtype=float) - finite_mask = np.isfinite(scores) - scores = scores[finite_mask] - labels = labels[finite_mask] - if len(scores) == 0: - return 0.5 - pos = scores[labels == 1] - neg = scores[labels == 0] - if pos.size == 0 or neg.size == 0: - return 0.5 - order = np.argsort(scores, kind="mergesort") - ranks = np.empty(scores.size, dtype=float) - ranks[order] = np.arange(1, scores.size + 1, dtype=float) - # Average ranks over ties so AUC is exact for discrete scores. - _assign_tie_ranks(scores, ranks, order) - rank_sum_pos = float(np.sum(ranks[labels == 1])) - n_pos = float(pos.size) - n_neg = float(neg.size) - u = rank_sum_pos - n_pos * (n_pos + 1.0) / 2.0 - return float(u / (n_pos * n_neg)) - - -def _assign_tie_ranks(scores: np.ndarray, ranks: np.ndarray, order: np.ndarray) -> None: - sorted_scores = scores[order] - i = 0 - n = scores.size - while i < n: - j = i - while j + 1 < n and sorted_scores[j + 1] == sorted_scores[i]: - j += 1 - if j > i: - avg = (ranks[order[i]] + ranks[order[j]]) / 2.0 - for k in range(i, j + 1): - ranks[order[k]] = avg - i = j + 1 - - -def select_threshold( - labels: np.ndarray, - scores: np.ndarray, - *, - default: float = 0.5, -) -> float: - """Pick the threshold maximising balanced accuracy (model-agnostic). - - Ties break toward the ``default`` so the operating point stays stable when - the data does not clearly prefer one cut. - """ - labels = np.asarray(labels, dtype=float) - scores = np.asarray(scores, dtype=float) - if scores.size == 0: - return default - candidates = np.unique( - np.concatenate([ - np.quantile(scores, np.linspace(0.05, 0.95, 37)), - np.linspace(0.1, 0.95, 86), - ]) - ) - candidates = candidates[(candidates >= 0.05) & (candidates <= 0.999)] - if candidates.size == 0: - pos_scores = scores[labels == 1] - return float(np.min(pos_scores)) if len(pos_scores) > 0 else default - best_key: tuple[float, float] | None = None - best_threshold = default - best_ba = 0.5 - for threshold in candidates: - m = binary_metrics(labels, scores, float(threshold)) - bal = float(m.get("balanced_accuracy") or 0.0) - key = (bal, -abs(float(threshold) - default)) - if best_key is None or key > best_key: - best_key = key - best_threshold = float(threshold) - best_ba = bal - if best_ba == 0.5: - pos_scores = scores[labels == 1] - return float(np.min(pos_scores)) if len(pos_scores) > 0 else default - return round(best_threshold, 6) - - -def build_spec( - *, - name: str, - target: str, - feature_columns: Sequence[str], - fit: Mapping[str, Any], - threshold: float, - metrics: Mapping[str, Any] | None = None, - trained_at: str = "", - cycle_count: int = 0, -) -> dict[str, Any]: - """Assemble a runtime model spec (same schema as the shipped bundles). - - The returned dict is JSON-serialisable and can be scored by - :func:`score_spec` with math identical to the embedded ``*_model.py``. - """ - return { - "schema": PROMOTION_SCHEMA, - "name": name, - "kind": "standardized_logistic", - "target": target, - "target_units": "", - "feature_columns": list(feature_columns), - "center": [round(float(v), 8) for v in np.asarray(fit["center"], dtype=float)], - "scale": [round(float(v), 8) for v in np.asarray(fit["scale"], dtype=float)], - "coef": [round(float(v), 8) for v in np.asarray(fit["coef"], dtype=float)], - "bias": round(float(fit["bias"]), 8), - "threshold": round(float(threshold), 8), - "output_center": 0.0, - "output_scale": 1.0, - "metrics": dict(metrics or {}), - "notes": ["Trained on-device from the user's own labelled cycles."], - "created_at": trained_at, - "cycle_count": int(cycle_count), - "source": "on_device", - } - - -def score_matrix_spec(spec: Mapping[str, Any], matrix: np.ndarray) -> np.ndarray: - """Pure-NumPy probabilities for a (rows, features) matrix from a spec.""" - matrix = np.asarray(matrix, dtype=float) - if matrix.size == 0: - return np.empty(0, dtype=float) - center = np.asarray(spec["center"], dtype=float) - scale = np.asarray(spec["scale"], dtype=float) - coef = np.asarray(spec["coef"], dtype=float) - raw = ((matrix - center) / scale) @ coef + float(spec["bias"]) - return _sigmoid(raw) - - -def score_spec(spec: Mapping[str, Any], features: Mapping[str, float]) -> float: - """Pure-NumPy probability for one feature mapping. - - Byte-identical to the embedded ``score()`` in ``*_model.py`` for *complete* - feature mappings (the normal case: the extractors in ``feature_extraction`` - always populate every ``FEATURE_COLUMNS`` key). The two intentionally differ - only on the defensive missing-key fallback: this fills a missing feature with - the training center (standardises to 0.0 = neutral, avoiding 8+ SD corruption - of inference), whereas the embedded ``score()`` fills raw 0.0. That path is - not exercised by the parity fixtures and is not reachable in practice. - """ - columns = spec["feature_columns"] - center = np.asarray(spec["center"], dtype=float) - row = [] - for i, col in enumerate(columns): - val = features.get(col) - row.append(float(center[i]) if val is None else float(val)) - vector = np.array(row, dtype=float) - return float(score_matrix_spec(spec, vector.reshape(1, -1))[0]) - - - # --------------------------------------------------------------------------- -# Regression head (standardized_linear) - remaining-time / progress regressor. +# Regression head (standardized_linear) - the total_energy regressor. # -# The three classifier heads above are logistic. The remaining-time model is a -# ridge-regularised linear regressor over standardised features with a +# A ridge-regularised linear regressor over standardised features with a # standardised target; prediction un-standardises back to target units using the -# spec's ``output_center``/``output_scale``. Same NumPy-only, JSON-serialisable -# spec schema as :func:`build_spec` so it is stored/loaded identically, but it is -# scored with :func:`predict_matrix_spec` (no sigmoid) rather than ``score_spec``. +# spec's ``output_center``/``output_scale``. Same JSON-serialisable spec schema as +# the shipped logistic bundles, scored with :func:`predict_matrix_spec` (no +# sigmoid). # --------------------------------------------------------------------------- diff --git a/custom_components/ha_washdata/ml/training_task.py b/custom_components/ha_washdata/ml/training_task.py index c95d1b3f..ad7b660c 100644 --- a/custom_components/ha_washdata/ml/training_task.py +++ b/custom_components/ha_washdata/ml/training_task.py @@ -16,25 +16,29 @@ # along with this program. If not, see . """On-device training orchestration (Stage 4, gated by ENABLE_ML_TRAINING). -Gathers the user's own labelled cycles, derives training labels from data the -integration already has, fits NumPy-only logistic heads with :mod:`.trainer`, -and promotes a retrained model over the shipped baseline only when it is at -least as good on a held-out split. Nothing here runs unless the training loop -(behind the feature flag + per-device opt-in) invokes it. +Trains the one head with a live consumer: the ``total_energy`` ridge regressor +behind the projected energy / cost (``progress.ml_energy_total``). Its rows are +synthesised from prefixes of the user's own clean cycles, with each profile's +expectation built by the same function the live projection calls. A fit is +promoted only when, on at least ``ML_TRAINING_MIN_HOLDOUT_CYCLES`` held-out +cycles, it beats both the naive elapsed/expected projection (by +``ML_TRAINING_REGRESSION_MARGIN``) and the model already in use (audit ML-12, +PROGRESS-16). Every record carries a ``reason_code`` the panel shows when nothing +was promoted. Nothing here runs unless the training loop (behind the feature flag ++ per-device opt-in) invokes it. -Label sources (no manual labelling required to start): - * end detector - from trace geometry: a completed cycle's final low-power - event is a true end (positive); earlier pauses that resumed are non-ends. - * quality model - from cycle status + optional ML-Lab review labels: clean - completed / "good" / "golden" -> not a problem; force_stopped / interrupted - / "bad" / "unusable" -> a problem. - * live_match - from match-ranking-history snapshots: :func:`_live_match_dataset` - labels each snapshot 1/0 by comparing its top-1 candidate to the confirmed - profile (wired into :func:`train_from_cycles` via ``ranking_history``). +Removed in 0.5.8: the ``quality`` and ``live_match`` heads with their consumers +(the quality gate and the early match commit; audit ML-02/06/10), and on-device +training of the ``end`` classifier, whose consumer is frozen off (audit +ML-05/11: promotion on 4-7 held-out positives admitted worse models) and runs its +shipped baseline, and the ``remaining_time`` regressor with its consumer (audit +ML-07). """ from __future__ import annotations +import functools import logging +import math from typing import Any import numpy as np @@ -42,51 +46,22 @@ import numpy as np _LOGGER = logging.getLogger(__name__) from ..const import ( - DEFAULT_DEFER_FINISH_CONFIDENCE, - ML_MATCH_COMMIT_THRESHOLD, - ML_QUALITY_SUSPICIOUS_THRESHOLD, - ML_TRAINING_AUC_MARGIN, - ML_TRAINING_BACC_MARGIN, - ML_TRAINING_MIN_POSITIVES, + ML_TRAINING_MIN_HOLDOUT_CYCLES, ML_TRAINING_MIN_REGRESSION_ROWS, ML_TRAINING_REGRESSION_MARGIN, ) from . import trainer as T -# Capability -> (embedded module name, target label). Mirrors engine._MODEL_MODULES. -# The target label MUST match each baseline module's MODEL_TARGET (and -# promoted_manifest.json) so a promoted on-device spec records the same target as the -# shipped baseline it replaces. -_CAPABILITIES = { - "end": ("cycle_end_detector_model", "cycle_truly_ended"), - "quality": ("hybrid_curve_quality_model", "problem_cycle"), - "live_match": ("live_match_commit_model", "match_top1_correct"), -} - -# The FIXED probability cutoff each live consumer applies to this capability's -# score. AUC alone is calibration-blind, so on-device retraining must also not -# degrade balanced accuracy AT the operating point the model is actually used at -# (else a "better AUC" model can silently shift decision rates). See _train_capability. -_OPERATING_THRESHOLD = { - "end": DEFAULT_DEFER_FINISH_CONFIDENCE, - "quality": ML_QUALITY_SUSPICIOUS_THRESHOLD, - "live_match": ML_MATCH_COMMIT_THRESHOLD, -} - # Regression capabilities have no embedded baseline module - they are promoted # only when they beat a naive analytic estimate on held-out data. capability -> # (target label, target units). _REGRESSION_CAPABILITIES = { - "remaining_time": ("progress_fraction", "fraction"), "total_energy": ("energy_fraction", "fraction"), } # Elapsed fractions at which each clean cycle is cut to synthesize a training row. _PROGRESS_CUT_FRACTIONS = (0.15, 0.30, 0.45, 0.60, 0.75, 0.90) -_ACTIVE_FLOOR_RATIO = 0.02 -_MIN_ROWS = 40 - def _read_points(cycle: dict[str, Any]) -> list[tuple[float, float]]: """Return power data as offset-seconds/watts pairs, handling str and datetime start_time.""" @@ -97,6 +72,55 @@ def _read_points(cycle: dict[str, Any]) -> list[tuple[float, float]]: return [] +class _CycleListView: + """The one store method :func:`progress.profile_end_expectation` reads.""" + + def __init__(self, cycles: list[dict[str, Any]]) -> None: + self._cycles = cycles + + def get_past_cycles(self) -> list[dict[str, Any]]: + return self._cycles + + +def live_expectations( + cycles: list[dict[str, Any]], + names: set[str] | list[str], + expected_durations: dict[str, float] | None = None, +) -> dict[str, dict[str, float]]: + """Each profile's expectation exactly as the live projection builds it. + + Live, ``progress.ml_energy_total`` takes its expectation from + ``progress.profile_end_expectation``: the median trace-integrated energy, + duration and peak of the profile's last 20 stored cycles, with the duration + overridden by the matched profile's expected duration. Training used the + median of the stored ``duration`` / ``energy_wh`` / ``max_power`` fields + instead (audit PROGRESS-16), so the model was fitted on one feature scale and + served on another. This calls the live function over the same cycle list; + ``expected_durations`` is the matcher's per-profile duration, read on the event + loop by the caller (absent, the median trace duration stands, as it does live + for an unmatched duration). Pure; never raises. + """ + from ..progress import profile_end_expectation # noqa: PLC0415 + + view = _CycleListView(cycles) + durations = expected_durations or {} + out: dict[str, dict[str, float]] = {} + for name in names: + if not isinstance(name, str) or not name: + continue + try: + duration = float(durations.get(name) or 0.0) + except (TypeError, ValueError, OverflowError): + duration = 0.0 + try: + expectation, _cache = profile_end_expectation(view, name, duration, None) + except Exception: # noqa: BLE001 - one bad profile must not stop training + expectation = None + if expectation: + out[name] = expectation + return out + + def _matrix(rows: list[dict[str, float]], columns: list[str]) -> np.ndarray: if not rows: return np.empty((0, len(columns)), dtype=float) @@ -105,222 +129,6 @@ def _matrix(rows: list[dict[str, float]], columns: list[str]) -> np.ndarray: ) -def _end_dataset( - clean: list[dict[str, Any]], - expectations: dict[str, dict[str, float]], - stop_thr: float, -) -> tuple[np.ndarray, np.ndarray, list[str], np.ndarray]: - """Positives = each completed clean cycle's final end; negatives = pauses that resumed. - - Also returns a per-row ``groups`` array (source-cycle index) so the holdout split - keeps every row from a given cycle on the same side (this dataset emits 1+N rows - per cycle; row-level splitting would leak siblings across train/test — B5).""" - from .feature_extraction import END_FEATURE_COLUMNS, latest_end_event_features - - rows: list[dict[str, float]] = [] - labels: list[float] = [] - groups: list[int] = [] - for ci, c in enumerate(clean): - exp = expectations.get(c.get("profile_name")) - if not exp: - continue - points = _read_points(c) - if len(points) < 6: - continue - peak = max((p for _, p in points), default=0.0) - if peak <= 0: - continue - active_thr = max(stop_thr, _ACTIVE_FLOOR_RATIO * peak) - in_low = False - low_start = 0.0 - for i, (t, p) in enumerate(points): - if not in_low and p < active_thr: - in_low = True - low_start = t - elif in_low and p >= active_thr: - if (points[i - 1][0] - low_start) >= 30.0: - feat = latest_end_event_features(points[:i], exp) - if feat is not None: - rows.append(feat) - labels.append(0.0) # resumed -> not the end - groups.append(ci) - in_low = False - feat_end = latest_end_event_features(points, exp) - if feat_end is not None: - rows.append(feat_end) - labels.append(1.0) # trace ends here -> true end - groups.append(ci) - return (_matrix(rows, list(END_FEATURE_COLUMNS)), np.array(labels, dtype=float), - list(END_FEATURE_COLUMNS), np.array(groups, dtype=int)) - - -def _quality_label(cycle: dict[str, Any]) -> float | None: - """1 = problem, 0 = clean, None = unknown (skip).""" - review = cycle.get("ml_review") - if isinstance(review, dict): - if review.get("golden"): - return 0.0 # pinned reference cycle -> definitely clean - q = review.get("quality") - if q in ("good", "golden"): - return 0.0 - if q in ("bad", "unusable"): - return 1.0 - status = cycle.get("status") - if status in ("force_stopped", "interrupted"): - return 1.0 - if status == "completed": - return 0.0 - return None - - -def _quality_dataset( - cycles: list[dict[str, Any]], - expectations: dict[str, dict[str, float]], -) -> tuple[np.ndarray, np.ndarray, list[str], np.ndarray]: - """Uses ALL cycles (not clean-filtered) so mis-detected cycles are the positives. - - Emits at most one row per cycle, so ``groups`` is unique-per-row (splitting by - group is equivalent to row-level here) — returned for a uniform split API.""" - from .feature_extraction import QUALITY_FEATURE_COLUMNS, quality_features - - rows: list[dict[str, float]] = [] - labels: list[float] = [] - groups: list[int] = [] - for ci, c in enumerate(cycles): - exp = expectations.get(c.get("profile_name")) - if not exp: - continue - label = _quality_label(c) - if label is None: - continue - points = _read_points(c) - if len(points) < 6: - continue - raw_conf = c.get("match_confidence") - if isinstance(raw_conf, (int, float)) and not isinstance(raw_conf, bool) and raw_conf > 0: - conf = float(raw_conf) - proxy_dist, proxy_margin, proxy_fit = max(0.0, 1.0 - conf), conf, conf - else: - proxy_dist, proxy_margin, proxy_fit = 0.25, 0.30, 0.75 - # Use the cycle's real detected-artifact count so the flag_pressure feature - # is not train-time-constant (which would zero its learned coefficient and - # blind the AUC gate to it). Mirrors inference in manager._compute_cycle_quality_score. - arts = c.get("artifacts") - flag_count = len(arts) if isinstance(arts, list) else 0 - try: - feat = quality_features( - points, exp["duration"], exp["energy"], exp["peak"], - proxy_dist, proxy_margin, proxy_fit, flag_count, - ) - except Exception: # pylint: disable=broad-exception-caught - continue - rows.append(feat) - labels.append(label) - groups.append(ci) - return (_matrix(rows, list(QUALITY_FEATURE_COLUMNS)), np.array(labels, dtype=float), - list(QUALITY_FEATURE_COLUMNS), np.array(groups, dtype=int)) - - -def _live_match_dataset( - snapshots: list[dict[str, Any]], -) -> tuple[np.ndarray, np.ndarray, list[str], np.ndarray]: - """Build a training matrix from accumulated match ranking snapshots. - - Each snapshot was captured mid-cycle; at cycle end the confirmed profile - was back-filled as ``confirmed_label``. We label by whether the model's - top-1 candidate at recording time matched the final confirmed label: - 1.0 = top-1 was correct (should commit), 0.0 = wrong (should not commit). - Snapshots without a confirmed label are skipped. - - One cycle produces several snapshots (matching re-runs every ~5 min), all - back-filled with the same label, so ``groups`` keys rows by source cycle - (``cycle_id`` → ``start_time_iso`` → unique) to stop the holdout split leaking - correlated same-cycle snapshots across train/test (B5). - """ - from .feature_extraction import LIVE_MATCH_FEATURE_COLUMNS - - columns = list(LIVE_MATCH_FEATURE_COLUMNS) - rows: list[dict[str, float]] = [] - labels: list[float] = [] - group_keys: list[str] = [] - for i, snap in enumerate(snapshots): - if not isinstance(snap, dict): - continue - confirmed = snap.get("confirmed_label") - if not isinstance(confirmed, str) or not confirmed: - continue - top1 = snap.get("top1_profile") - if not isinstance(top1, str): - continue - feat = snap.get("features") - if not isinstance(feat, dict): - continue - label = 1.0 if confirmed == top1 else 0.0 - rows.append({col: float(feat.get(col) or 0.0) for col in columns}) - labels.append(label) - group_keys.append(str(snap.get("cycle_id") or snap.get("start_time_iso") or f"_row{i}")) - return (_matrix(rows, columns), np.array(labels, dtype=float), - columns, _group_ids(group_keys)) - - -def _group_ids(keys: list[Any]) -> np.ndarray: - """Map an ordered list of group keys to stable integer ids (first-seen order).""" - seen: dict[Any, int] = {} - out: list[int] = [] - for k in keys: - if k not in seen: - seen[k] = len(seen) - out.append(seen[k]) - return np.array(out, dtype=int) - - -def _progress_dataset( - clean: list[dict[str, Any]], - expectations: dict[str, dict[str, float]], -) -> tuple[np.ndarray, np.ndarray, list[str], np.ndarray]: - """Synthesize (features, completion_fraction) rows for the remaining-time model. - - Each clean completed cycle is cut at several elapsed fractions; the target is - the true completion fraction of the prefix (``prefix_elapsed / total``). This - turns every stored trace into a handful of supervised progress examples, so - the regressor learns the device's own progress curve (e.g. a program that - reliably runs longer than its labelled duration) rather than the naive - elapsed/expected assumption. - """ - from .feature_extraction import PROGRESS_FEATURE_COLUMNS, progress_features - - columns = list(PROGRESS_FEATURE_COLUMNS) - rows: list[dict[str, float]] = [] - labels: list[float] = [] - groups: list[int] = [] - for ci, c in enumerate(clean): - exp = expectations.get(c.get("profile_name")) - if not exp: - continue - points = _read_points(c) - if len(points) < 12: - continue - t0 = points[0][0] - total = points[-1][0] - t0 - if total <= 60.0: - continue - for frac in _PROGRESS_CUT_FRACTIONS: - cut_t = t0 + frac * total - prefix = [(o, p) for o, p in points if o <= cut_t] - if len(prefix) < 4: - continue - feat = progress_features(prefix, exp) - if feat is None: - continue - actual_elapsed = prefix[-1][0] - t0 - label = actual_elapsed / total - rows.append(feat) - labels.append(float(min(max(label, 0.0), 1.0))) - groups.append(ci) - return (_matrix(rows, columns), np.array(labels, dtype=float), - columns, np.array(groups, dtype=int)) - - def _energy_dataset( clean: list[dict[str, Any]], expectations: dict[str, dict[str, float]], @@ -331,7 +139,7 @@ def _energy_dataset( energy accumulates *non-linearly* over the cycle (heating front-loads it) rather than assuming it tracks elapsed time. The naive baseline in ``_train_regression_capability`` is ``elapsed_over_expected`` (time progress), - which is exactly the current ``energy_so_far / progress`` projection — so a + which is exactly the current ``energy_so_far / progress`` projection - so a model is only promoted when it beats that. """ from .feature_extraction import ( @@ -376,10 +184,14 @@ def _energy_dataset( def _group_holdout_indices( - groups: np.ndarray, frac: float, seed: int + groups: np.ndarray, frac: float, seed: int, min_test_groups: int = 0 ) -> tuple[np.ndarray, np.ndarray] | None: """Assign whole groups to train/test so no group straddles the split (B5). + ``min_test_groups`` raises the held-out count to that many groups when there + are at least twice as many groups in total (so training keeps as many), which + lets a 10-24 cycle device be judged on 5 cycles instead of 2-4. + Returns (train_idx, test_idx) row-index arrays, or None if there are too few distinct groups to hold any out while leaving ≥1 training group. """ @@ -389,6 +201,8 @@ def _group_holdout_indices( rng = np.random.default_rng(seed) perm = rng.permutation(uniq) n_test_groups = max(1, int(round(uniq.size * frac))) + if n_test_groups < min_test_groups <= uniq.size // 2: + n_test_groups = min_test_groups if uniq.size - n_test_groups < 1: n_test_groups = uniq.size - 1 test_groups = set(perm[:n_test_groups].tolist()) @@ -400,29 +214,66 @@ def _group_holdout_indices( return train_idx, test_idx -def _regression_split( +def _regression_holdout( X: np.ndarray, y: np.ndarray, groups: np.ndarray | None = None, - *, frac: float = 0.2, seed: int = 0 -) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: - """Seeded train/test split for regression (no class balancing). + *, frac: float = 0.2, seed: int = 0, min_test_groups: int = 0, +) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, int]: + """Seeded train/test split for regression, plus how many units were held out. When ``groups`` is given, splits by group so correlated same-cycle rows never - span train and test; falls back to in-sample eval if it cannot. + span train and test. The unit is a source cycle (group) when ``groups`` is + given, else a row. An in-sample fallback (``X_tr is X and X_te is X``) + reports 0. """ n = X.shape[0] if groups is not None and getattr(groups, "size", 0) == n: - split = _group_holdout_indices(groups, frac, seed) + split = _group_holdout_indices(groups, frac, seed, min_test_groups) if split is not None and split[0].size >= 2: train_idx, test_idx = split - return X[train_idx], y[train_idx], X[test_idx], y[test_idx] - return X, y, X, y + held_out = int(np.unique(groups[test_idx]).size) + return X[train_idx], y[train_idx], X[test_idx], y[test_idx], held_out + return X, y, X, y, 0 rng = np.random.default_rng(seed) idx = rng.permutation(n) n_test = max(1, int(round(n * frac))) if n - n_test < 2: # keep at least a couple of training rows - return X, y, X, y + return X, y, X, y, 0 test_idx, train_idx = idx[:n_test], idx[n_test:] - return X[train_idx], y[train_idx], X[test_idx], y[test_idx] + return X[train_idx], y[train_idx], X[test_idx], y[test_idx], int(n_test) + + +def _incumbent_mae( + spec: Any, columns: list[str], X_te: np.ndarray, y_te: np.ndarray +) -> float | None: + """Held-out MAE of the model already in use, or None when there is none. + + Scored on exactly the candidate's held-out rows, clipped the same way. A spec + on another feature schema is None: ``resolve_regressor`` already treats it as + inert, so nothing is in use to protect. Never raises. + """ + if not isinstance(spec, dict) or spec.get("kind") != "standardized_linear": + return None + if list(spec.get("feature_columns") or []) != list(columns) or not y_te.size: + return None + try: + preds = np.clip(T.predict_matrix_spec(spec, X_te), 0.0, 1.0) + except Exception: # noqa: BLE001 - a malformed stored spec is not in use either + return None + if preds.shape != y_te.shape or not np.all(np.isfinite(preds)): + return None + return float(np.mean(np.abs(preds - y_te))) + + +def _not_promoted( + record: dict[str, Any], code: str, params: dict[str, Any], reason: str +) -> dict[str, Any]: + """Stamp why a run promoted nothing: a code + params for the panel's ``_t()``, + and the English ``reason`` for the log.""" + record["promoted"] = False + record["reason_code"] = code + record["reason_params"] = params + record["reason"] = reason + return record def _train_regression_capability( @@ -434,32 +285,55 @@ def _train_regression_capability( columns: list[str], trained_at: str, groups: np.ndarray | None = None, + *, + incumbent_spec: dict[str, Any] | None = None, ) -> dict[str, Any]: - """Fit + gate one regression capability against a naive analytic baseline. + """Fit + gate one regression capability. - The naive baseline for the completion-fraction target is - ``elapsed_over_expected`` (the first feature column) clamped to [0, 1] - i.e. - the current profile-duration assumption. A trained regressor is only promoted - when its held-out MAE is at least ``ML_TRAINING_REGRESSION_MARGIN`` lower. + Two bars, both on the same held-out rows (audit ML-12): + + * the naive baseline for the completion-fraction target is + ``elapsed_over_expected`` (the first feature column) clamped to [0, 1] - + the current profile-duration assumption. The fit's MAE must be at least + ``ML_TRAINING_REGRESSION_MARGIN`` lower; + * ``incumbent_spec``, the model in use, when there is one: the fit's MAE must + be strictly lower. Before, it was never scored, so any candidate that beat + the naive estimate replaced it however much better it was. + + Nothing is promoted on fewer than ``ML_TRAINING_MIN_HOLDOUT_CYCLES`` held-out + cycles (PROGRESS-16). A record that promotes nothing carries ``reason_code`` / + ``reason_params`` (shown in the panel) and an English ``reason`` (logged). """ n = X.shape[0] + # Distinct source cycles: each clean cycle contributes several prefix rows via + # `groups`, so ``n`` (rows) overstates how many real cycles trained the model. + grouped = groups is not None and getattr(groups, "size", 0) == n + n_cycles = int(np.unique(groups).size) if grouped else n + record: dict[str, Any] = { + "capability": capability, "promoted": False, "rows": n, "cycle_count": n_cycles, + } if n < ML_TRAINING_MIN_REGRESSION_ROWS: - return {"capability": capability, "promoted": False, - "reason": f"insufficient data (rows={n})"} + return _not_promoted( + record, "insufficient_rows", + {"rows": n, "min": ML_TRAINING_MIN_REGRESSION_ROWS, "cycles": n_cycles}, + f"insufficient data (rows={n})", + ) - X_tr, y_tr, X_te, y_te = _regression_split(X, y, groups) + X_tr, y_tr, X_te, y_te, held_out = _regression_holdout( + X, y, groups, min_test_groups=ML_TRAINING_MIN_HOLDOUT_CYCLES, + ) # Detect in-sample fallback (too few rows to split). in_sample = X_tr is X and X_te is X if in_sample: _LOGGER.warning( - "ML training '%s': too few rows (%d) to split for regression — " + "ML training '%s': too few rows (%d) to split for regression - " "evaluating in-sample; NOT promoting. Add more cycles for a reliable holdout.", capability, n, ) try: fit = T.fit_ridge(X_tr, y_tr, alpha=1.0) except ValueError as err: - return {"capability": capability, "promoted": False, "reason": str(err)} + return _not_promoted(record, "fit_failed", {"error": str(err)}, str(err)) spec_probe = { "center": fit["center"], "scale": fit["scale"], "coef": fit["coef"], @@ -468,218 +342,54 @@ def _train_regression_capability( } preds = np.clip(T.predict_matrix_spec(spec_probe, X_te), 0.0, 1.0) metrics = T.regression_metrics(y_te, preds) - # Explicit None check — `or 1.0` would coerce MAE=0.0 to 1.0, rejecting a + # Explicit None check - `or 1.0` would coerce MAE=0.0 to 1.0, rejecting a # perfect regressor and blocking promotion. model_mae = float(metrics.get("mae") if metrics.get("mae") is not None else 1.0) naive_col = columns.index("elapsed_over_expected") if "elapsed_over_expected" in columns else 0 naive = np.clip(X_te[:, naive_col], 0.0, 1.0) naive_mae = float(np.mean(np.abs(naive - y_te))) if y_te.size else 1.0 + incumbent_mae = None if in_sample else _incumbent_mae(incumbent_spec, columns, X_te, y_te) - # Distinct source cycles: each clean cycle contributes several prefix rows via - # `groups`, so ``n`` (rows) overstates how many real cycles trained the model. - n_cycles = ( - int(np.unique(groups).size) - if groups is not None and getattr(groups, "size", 0) == n - else n - ) - # Never promote on an in-sample (non-held-out) evaluation. - promote = (model_mae <= naive_mae * (1.0 - ML_TRAINING_REGRESSION_MARGIN)) and not in_sample - record: dict[str, Any] = { - "capability": capability, - "promoted": bool(promote), - "rows": n, - "cycle_count": n_cycles, + record.update({ + "held_out_cycles": held_out, "model_mae": round(model_mae, 5), "naive_mae": round(naive_mae, 5), "metrics": metrics, - } - if promote: - record["spec"] = T.build_regression_spec( - name=capability, target=target, feature_columns=columns, fit=fit, - target_units=target_units, - metrics={"holdout": metrics, "model_mae": round(model_mae, 5), - "naive_mae": round(naive_mae, 5)}, - trained_at=trained_at, cycle_count=n_cycles, + }) + if incumbent_mae is not None: + record["incumbent_mae"] = round(incumbent_mae, 5) + + # Never promote on an in-sample (non-held-out) evaluation, nor on a handful + # of held-out cycles. + if in_sample or held_out < ML_TRAINING_MIN_HOLDOUT_CYCLES: + return _not_promoted( + record, "holdout_too_small", + {"held_out": held_out, "min": ML_TRAINING_MIN_HOLDOUT_CYCLES, "cycles": n_cycles}, + f"only {held_out} held-out cycles (need {ML_TRAINING_MIN_HOLDOUT_CYCLES}); not promoted", ) - record["trained_at"] = trained_at - elif in_sample: - record["reason"] = "no held-out split (in-sample eval); not promoted" - else: - record["reason"] = f"MAE {model_mae:.4f} not below naive {naive_mae:.4f} - margin" - return record - - -def _holdout_split( - X: np.ndarray, y: np.ndarray, groups: np.ndarray | None = None, - *, frac: float = 0.2, seed: int = 0 -) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: - """Seeded split that keeps both classes in the test set when possible. - - When ``groups`` is given, splits by group (no same-cycle row spans the split, B5); - if the resulting split loses a class from either side it retries a few seeds, then - falls back to in-sample eval. - """ - n = X.shape[0] - if groups is not None and getattr(groups, "size", 0) == n: - for s in range(seed, seed + 8): - split = _group_holdout_indices(groups, frac, s) - if split is None: - break - train_idx, test_idx = split - if (len(np.unique(y[test_idx])) >= 2 and len(np.unique(y[train_idx])) >= 2): - return X[train_idx], y[train_idx], X[test_idx], y[test_idx] - return X, y, X, y - rng = np.random.default_rng(seed) - idx = rng.permutation(n) - n_test = max(1, int(round(n * frac))) - test_idx, train_idx = idx[:n_test], idx[n_test:] - # Guarantee both classes present in test; otherwise fall back to all-data eval. - if len(np.unique(y[test_idx])) < 2 or len(np.unique(y[train_idx])) < 2: - return X, y, X, y - return X[train_idx], y[train_idx], X[test_idx], y[test_idx] - - -def _embedded_module(capability: str): - """Embedded baseline module for a capability, via the shared engine cache.""" - module_name = _CAPABILITIES.get(capability, (None, None))[0] - if module_name is None: - return None - try: - from .engine import _load_model_module - - return _load_model_module(module_name) - except Exception: # pylint: disable=broad-exception-caught - return None - - -def _baseline_scores(capability: str, X_test: np.ndarray, columns: list[str]) -> np.ndarray | None: - """Embedded-baseline probabilities on X_test, or None if it can't load/score.""" - module = _embedded_module(capability) - if module is None: - return None - try: - return np.array( - [float(module.score(dict(zip(columns, row)))) for row in X_test], dtype=float + if model_mae > naive_mae * (1.0 - ML_TRAINING_REGRESSION_MARGIN): + return _not_promoted( + record, "not_better_than_naive", + {"model": f"{model_mae:.3f}", "naive": f"{naive_mae:.3f}"}, + f"MAE {model_mae:.4f} not below naive {naive_mae:.4f} - margin", ) - except Exception: # pylint: disable=broad-exception-caught - return None - - -def _baseline_threshold(capability: str, default: float) -> float: - module = _embedded_module(capability) - thr = getattr(module, "THRESHOLD", None) if module is not None else None - return float(thr) if isinstance(thr, (int, float)) else default - - -def _train_capability( - capability: str, - target: str, - X: np.ndarray, - y: np.ndarray, - columns: list[str], - trained_at: str, - groups: np.ndarray | None = None, -) -> dict[str, Any]: - """Fit + gate one capability. Returns a status record (promoted or not).""" - n = X.shape[0] - n_pos = int(np.sum(y == 1)) - n_neg = int(np.sum(y == 0)) - if n < _MIN_ROWS or n_pos < ML_TRAINING_MIN_POSITIVES or n_neg < 5: - return {"capability": capability, "promoted": False, - "reason": f"insufficient data (rows={n}, pos={n_pos}, neg={n_neg})"} - - X_tr, y_tr, X_te, y_te = _holdout_split(X, y, groups) - # Detect in-sample fallback (holdout returned full dataset for both splits). - in_sample = X_tr is X and X_te is X - if in_sample: - _LOGGER.warning( - "ML training '%s': dataset too small or imbalanced to split " - "(n=%d, pos=%d, neg=%d) — AUC evaluated in-sample; NOT promoting " - "(an in-sample AUC is optimistic). Add more labeled cycles.", - capability, n, n_pos, n_neg, + if incumbent_mae is not None and model_mae >= incumbent_mae: + return _not_promoted( + record, "not_better_than_incumbent", + {"model": f"{model_mae:.3f}", "incumbent": f"{incumbent_mae:.3f}"}, + f"MAE {model_mae:.4f} not below the current model's {incumbent_mae:.4f}; kept it", ) - fit = T.fit_logistic(X_tr, y_tr) - default_thr = _baseline_threshold(capability, 0.5) - spec_probe = {"center": fit["center"], "scale": fit["scale"], "coef": fit["coef"], - "bias": fit["bias"], "feature_columns": columns} - train_scores = T.score_matrix_spec(spec_probe, X_tr) - threshold = T.select_threshold(y_tr, train_scores, default=default_thr) - test_scores = T.score_matrix_spec(spec_probe, X_te) - new_auc = T.auc(y_te, test_scores) - metrics = T.binary_metrics(y_te, test_scores, threshold) - # Distinct source cycles (some capabilities emit >1 row per cycle, e.g. an - # end classifier with several candidate events); mirror the regression path. - n_cycles = ( - int(np.unique(groups).size) - if groups is not None and getattr(groups, "size", 0) == n - else n + record["promoted"] = True + record["spec"] = T.build_regression_spec( + name=capability, target=target, feature_columns=columns, fit=fit, + target_units=target_units, + metrics={"holdout": metrics, "model_mae": round(model_mae, 5), + "naive_mae": round(naive_mae, 5)}, + trained_at=trained_at, cycle_count=n_cycles, ) - base_scores = _baseline_scores(capability, X_te, columns) - if base_scores is None: - # Every classifier capability ships an embedded baseline; None here means - # it failed to load/score, NOT that it is legitimately absent. Don't promote - # against a fabricated 0.5 bar (that would let a near-random model win). - return {"capability": capability, "promoted": False, - "rows": n, "positives": n_pos, "negatives": n_neg, - "cycle_count": n_cycles, "new_auc": round(new_auc, 4), - "threshold": threshold, "metrics": metrics, - "reason": "embedded baseline unavailable; cannot gate promotion"} - baseline = T.auc(y_te, base_scores) - - # Calibration-aware gate: the live consumer applies a FIXED probability cutoff to - # this capability, so AUC (rank quality) alone isn't enough — a retrained model - # must also not degrade balanced accuracy AT that operating cutoff, else a - # differently-calibrated on-device model silently shifts decision rates. - op_thr = _OPERATING_THRESHOLD.get(capability) - trained_op_bacc: float | None = None - base_op_bacc: float | None = None - calib_ok = True - if op_thr is not None: - trained_op_bacc = float( - T.binary_metrics(y_te, test_scores, op_thr).get("balanced_accuracy") or 0.0 - ) - base_op_bacc = float( - T.binary_metrics(y_te, base_scores, op_thr).get("balanced_accuracy") or 0.0 - ) - calib_ok = trained_op_bacc >= (base_op_bacc - ML_TRAINING_BACC_MARGIN) - - # Never promote on an in-sample (non-held-out) evaluation: the AUC is optimistic. - promote = ( - (new_auc >= (baseline - ML_TRAINING_AUC_MARGIN)) and not in_sample and calib_ok - ) - record: dict[str, Any] = { - "capability": capability, - "promoted": bool(promote), - "rows": n, "positives": n_pos, "negatives": n_neg, - "cycle_count": n_cycles, - "new_auc": round(new_auc, 4), - "baseline_auc": round(baseline, 4), - "threshold": threshold, - "metrics": metrics, - } - if op_thr is not None: - record["operating_threshold"] = op_thr - record["op_balanced_accuracy"] = round(trained_op_bacc or 0.0, 4) - record["baseline_op_balanced_accuracy"] = round(base_op_bacc or 0.0, 4) - if promote: - record["spec"] = T.build_spec( - name=capability, target=target, feature_columns=columns, - fit=fit, threshold=threshold, - metrics={"holdout": metrics, "auc": round(new_auc, 4), "baseline_auc": round(baseline, 4)}, - trained_at=trained_at, cycle_count=n_cycles, - ) - record["trained_at"] = trained_at - elif in_sample: - record["reason"] = "no held-out split (in-sample eval); not promoted" - elif not calib_ok: - record["reason"] = ( - f"balanced accuracy at operating threshold {op_thr} " - f"({trained_op_bacc:.3f}) below baseline ({base_op_bacc:.3f}) - margin" - ) - else: - record["reason"] = f"AUC {new_auc:.3f} below baseline {baseline:.3f} - margin" + record["trained_at"] = trained_at return record @@ -688,59 +398,40 @@ def train_from_cycles( device_type: str | None, stop_threshold_w: float = 2.0, trained_at: str = "", - ranking_history: list[dict[str, Any]] | None = None, + *, + incumbents: dict[str, Any] | None = None, + expected_durations: dict[str, float] | None = None, ) -> dict[str, Any]: - """Pure function (executor-safe): build datasets, train, gate all capabilities. + """Pure function (executor-safe): build the dataset, train, gate the capability. + + ``incumbents`` is the store's ``ml_model_versions`` (capability -> record with + a ``spec``): each candidate must beat the model in use. ``expected_durations`` + is the matcher's duration per profile, so the training expectation is the live + one (:func:`live_expectations`). Both are read on the event loop by + :func:`async_run_training`. Returns ``{"results": [record, ...], "promoted": {capability: record}}``. Caller persists the promoted records via ``profile_store.set_ml_model_version``. - - ``ranking_history`` is the accumulated match ranking snapshots from the store - (see :meth:`.ProfileStore.get_match_ranking_history`). When provided it - unlocks on-device training for the ``live_match`` capability. """ from ..suggestion_engine import select_clean_cycles - from .feature_extraction import profile_expectations clean, _excluded = select_clean_cycles(cycles, stop_threshold_w=stop_threshold_w) - expectations = profile_expectations(cycles) - - datasets: dict[str, tuple[np.ndarray, np.ndarray, list[str], np.ndarray]] = { - "end": _end_dataset(clean, expectations, stop_threshold_w), - "quality": _quality_dataset(cycles, expectations), - "live_match": _live_match_dataset(ranking_history or []), - } + expectations = live_expectations( + cycles, {c.get("profile_name") for c in clean}, expected_durations + ) results: list[dict[str, Any]] = [] promoted: dict[str, Any] = {} - for capability, (module_name, target) in _CAPABILITIES.items(): - X, y, columns, groups = datasets[capability] - try: - record = _train_capability(capability, target, X, y, columns, trained_at, groups) - except ValueError as exc: - _LOGGER.debug("Skipping %s training: %s", capability, exc) - results.append({"capability": capability, "promoted": False, "reason": str(exc)}) - continue - results.append(record) - if record.get("promoted") and "spec" in record: - promoted[capability] = { - "spec": record["spec"], - "trained_at": trained_at, - "cycle_count": record["cycle_count"], - "metrics": record["metrics"], - "new_auc": record["new_auc"], - "baseline_auc": record["baseline_auc"], - } - # Regression capabilities (no embedded baseline; gated against a naive estimate). reg_datasets: dict[str, tuple[np.ndarray, np.ndarray, list[str], np.ndarray]] = { - "remaining_time": _progress_dataset(clean, expectations), "total_energy": _energy_dataset(clean, expectations), } for capability, (target, target_units) in _REGRESSION_CAPABILITIES.items(): X, y, columns, groups = reg_datasets[capability] + incumbent = (incumbents or {}).get(capability) record = _train_regression_capability( - capability, target, target_units, X, y, columns, trained_at, groups + capability, target, target_units, X, y, columns, trained_at, groups, + incumbent_spec=incumbent.get("spec") if isinstance(incumbent, dict) else None, ) results.append(record) if record.get("promoted") and "spec" in record: @@ -751,66 +442,99 @@ def train_from_cycles( "metrics": record["metrics"], "model_mae": record["model_mae"], "naive_mae": record["naive_mae"], + "held_out_cycles": record["held_out_cycles"], } + if "incumbent_mae" in record: + promoted[capability]["incumbent_mae"] = record["incumbent_mae"] return {"results": results, "promoted": promoted} +def _expected_durations(store: Any) -> dict[str, float]: + """Per profile, the duration the live matcher reports as ``expected_duration``. + + ``ProfileStore._stage1_duration_for`` mirrors the snapshot builder's + precedence (envelope ``target_duration`` first unless a golden cycle is + pinned, then ``avg_duration``), which is what the live projection is handed + as the matched duration. Loop-only: it reads the live store. Never raises. + """ + out: dict[str, float] = {} + try: + profiles = store.get_profiles() or {} + resolve = getattr(store, "_stage1_duration_for", None) + for name, profile in list(profiles.items()): + if not isinstance(profile, dict): + continue + try: + duration = ( + float(resolve(name, profile)) if callable(resolve) + else float(profile.get("avg_duration") or 0.0) + ) + except (TypeError, ValueError, OverflowError): + continue # one bad profile must not drop every expectation + if duration > 0 and np.isfinite(duration): + out[name] = duration + except Exception: # noqa: BLE001 - fall back to the trace medians + return {} + return out + + +def training_stop_threshold(merged: dict[str, Any]) -> float: + """The stop threshold training cleans cycles with: the entry's Stop Threshold, + else its min_power, else 2.0 W, skipping a value that is not positive and finite + (under +inf no reading is active). ``merged`` is entry data overlaid by options.""" + from ..const import CONF_MIN_POWER, CONF_STOP_THRESHOLD_W + + for key in (CONF_STOP_THRESHOLD_W, CONF_MIN_POWER): + try: + v = float(merged.get(key)) + except (TypeError, ValueError, OverflowError): + continue + if v > 0 and math.isfinite(v): + return v + return 2.0 + + async def async_run_training(hass: Any, manager: Any) -> dict[str, Any]: """Public entry point: train on this device's cycles and persist winners. Offloads the CPU work to an executor thread and persists any promoted model specs into the profile store. Returns a summary for logging / the event. """ - from ..const import CONF_MIN_POWER, CONF_STOP_THRESHOLD_W - store = manager.profile_store entry = hass.config_entries.async_get_entry(manager.entry_id) - merged = {**(entry.data if entry else {}), **(entry.options if entry else {})} - stop_thr = 2.0 - for key in (CONF_STOP_THRESHOLD_W, CONF_MIN_POWER): - try: - v = float(merged.get(key)) - except (TypeError, ValueError): - continue - if v > 0: - stop_thr = v - break + stop_thr = training_stop_threshold( + {**(entry.data if entry else {}), **(entry.options if entry else {})} + ) from homeassistant.util import dt as dt_util trained_at = dt_util.now().isoformat() - cycles = list(store.get_past_cycles()) # snapshot before executor to avoid data race - # get_match_ranking_history() already returns a shallow copy of the top-level - # list, but wrap it in list(...) too so the executor never iterates a list that - # the event loop could mutate mid-training - matching the get_past_cycles() - # snapshot above. - ranking_history = list(store.get_match_ranking_history()) + # Snapshot everything the executor reads, on the loop, before offloading. + cycles = list(store.get_past_cycles()) + incumbents = dict(store.get_ml_model_versions() or {}) + expected_durations = _expected_durations(store) _LOGGER.info( - "On-device ML training starting: %d cycles, %d ranking snapshots, " - "device_type=%s, stop_threshold=%.1fW", - len(cycles), len(ranking_history), manager.device_type, stop_thr, + "On-device ML training starting: %d cycles, device_type=%s, stop_threshold=%.1fW", + len(cycles), manager.device_type, stop_thr, ) summary = await hass.async_add_executor_job( - train_from_cycles, cycles, manager.device_type, stop_thr, trained_at, ranking_history + functools.partial( + train_from_cycles, cycles, manager.device_type, stop_thr, trained_at, + incumbents=incumbents, expected_durations=expected_durations, + ) ) for record in summary.get("results", []): - is_regression = "model_mae" in record - if record.get("promoted") and is_regression: + if record.get("promoted"): _LOGGER.info( - "ML training PROMOTED %s: MAE %.4f vs naive %.4f (rows=%s)", + "ML training PROMOTED %s: MAE %.4f vs naive %.4f, current %s (rows=%s, held out=%s)", record["capability"], record.get("model_mae", 0), record.get("naive_mae", 0), - record.get("rows"), - ) - elif record.get("promoted"): - _LOGGER.info( - "ML training PROMOTED %s: AUC %.3f vs baseline %.3f (rows=%s, pos=%s)", - record["capability"], record.get("new_auc", 0), record.get("baseline_auc", 0), - record.get("rows"), record.get("positives"), + record.get("incumbent_mae", "none"), record.get("rows"), + record.get("held_out_cycles"), ) else: _LOGGER.info( - "ML training kept baseline for %s: %s", + "ML training kept the current model for %s: %s", record["capability"], record.get("reason", "not promoted"), ) for capability, record in summary.get("promoted", {}).items(): diff --git a/custom_components/ha_washdata/notification_rules.py b/custom_components/ha_washdata/notification_rules.py index c6039477..834de1c2 100644 --- a/custom_components/ha_washdata/notification_rules.py +++ b/custom_components/ha_washdata/notification_rules.py @@ -35,7 +35,25 @@ from __future__ import annotations from datetime import datetime, timedelta from typing import Any -from .const import CONF_NOTIFY_QUIET_END_HOUR, CONF_NOTIFY_QUIET_START_HOUR +from .const import ( + CONF_NOTIFY_QUIET_END_HOUR, + CONF_NOTIFY_QUIET_START_HOUR, + STATE_INTERRUPTED, +) + + +def cycle_end_is_finish(status: Any) -> bool: + """Whether a cycle that ended with ``status`` counts as a finished run. + + Decides both the "finished" notification and entry into the Clean state (and + so the unload reminder) - audit MANAGER-10. An ``interrupted`` cycle is a + false start, a cancelled programme or a plug pulled early (the terminal-drop + finalize files those as interrupted too): nothing finished, nothing to unload. + A ``force_stopped`` cycle (watchdog or the user's Force Stop) ran a programme + the user may still want to hear about, so it keeps both; the finish template's + ``{status}`` tells it apart. A missing/unknown status keeps today's behaviour. + """ + return status != STATE_INTERRUPTED def quiet_hours_bounds(options: Any) -> tuple[int, int] | None: @@ -54,7 +72,7 @@ def quiet_hours_bounds(options: Any) -> tuple[int, int] | None: end = int(raw_end) if start != raw_start or end != raw_end: return None - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): return None if not (0 <= start <= 23) or not (0 <= end <= 23): return None @@ -99,7 +117,11 @@ def seconds_until_quiet_end( if target <= when: # End hour is earlier today (wrap-around window) -> it lands tomorrow. target = target + timedelta(days=1) - return max(0.0, (target - when).total_seconds()) + # The target is a local wall-clock time, but the wait is real seconds: + # subtracting two datetimes that share one ZoneInfo uses their wall-clock + # fields, so on a DST night the hold ended an hour early (autumn) or late + # (spring). `timestamp()` honours each side's own UTC offset. + return max(0.0, target.timestamp() - when.timestamp()) def milestone_crossed(prev_count: int, cur_count: int, milestones: Any) -> int | None: @@ -124,7 +146,7 @@ def milestone_crossed(prev_count: int, cur_count: int, milestones: Any) -> int | continue try: m = int(raw) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): continue if m != raw or m <= 0: continue diff --git a/custom_components/ha_washdata/phase_catalog.py b/custom_components/ha_washdata/phase_catalog.py index 1920fb9b..4a590e97 100644 --- a/custom_components/ha_washdata/phase_catalog.py +++ b/custom_components/ha_washdata/phase_catalog.py @@ -23,8 +23,11 @@ from copy import deepcopy from typing import Any from .const import ( + DEVICE_TYPE_AIR_FRYER, + DEVICE_TYPE_BREAD_MAKER, DEVICE_TYPE_DISHWASHER, DEVICE_TYPE_DRYER, + DEVICE_TYPE_PUMP, DEVICE_TYPE_WASHER_DRYER, DEVICE_TYPE_WASHING_MACHINE, ) @@ -202,6 +205,75 @@ DEFAULT_PHASES_BY_DEVICE: dict[str, list[PhaseItem]] = { "is_default": True, }, ], + # #190: these two lists went with the removed device types in 0.5.0 (4fde004) + # although both types stayed, so a bread maker was offered Pre-Wash and Spin. + DEVICE_TYPE_BREAD_MAKER: [ + { + "name": "Kneading", + "description": "Motor-driven dough mixing and development. High power draw.", + "translation_key": "phase_desc.kneading", + "is_default": True, + }, + { + "name": "Resting", + "description": "Short low-power pause between kneading stages for gluten relaxation.", + "translation_key": "phase_desc.resting", + "is_default": True, + }, + { + "name": "Proving", + "description": "Low-heat rising period to allow yeast fermentation and dough expansion.", + "translation_key": "phase_desc.proving", + "is_default": True, + }, + { + "name": "Baking", + "description": "High-temperature heating element active for crust and crumb formation.", + "translation_key": "phase_desc.baking", + "is_default": True, + }, + { + "name": "Keep Warm", + "description": "Low-heat holding stage to keep the loaf warm after baking.", + "translation_key": "phase_desc.keep_warm_bm", + "is_default": True, + }, + ], + DEVICE_TYPE_AIR_FRYER: [ + { + "name": "Pre-Heat", + "description": "Initial chamber heating before full cooking.", + "translation_key": "phase_desc.pre_heat", + "is_default": True, + }, + { + "name": "Cooking", + "description": "Main cooking phase with active heater and fan.", + "translation_key": "phase_desc.cooking", + "is_default": True, + }, + { + "name": "Pause", + "description": "Short pause for shaking or inspection.", + "translation_key": "phase_desc.pause_af", + "is_default": True, + }, + { + "name": "Cool Down", + "description": "Fan-only cool-down stage after heating.", + "translation_key": "phase_desc.cool_down_af", + "is_default": True, + }, + { + "name": "Keep Warm", + "description": "Low-heat holding stage to keep food warm.", + "translation_key": "phase_desc.keep_warm", + "is_default": True, + }, + ], + # A pump run is one phase; without an entry it fell back to the union of every + # list above (washing phases included). + DEVICE_TYPE_PUMP: [], } @@ -326,6 +398,9 @@ def merge_phase_catalog(device_type: str, custom_phases: list[PhaseItem] | None) "name": normalized_name, "description": str(item.get("description", "")).strip(), "is_default": False, + # An edited built-in: deleting it restores the built-in, so the + # panel offers "Reset" rather than "Delete" on it. + "is_override": True, } continue @@ -342,6 +417,9 @@ def merge_phase_catalog(device_type: str, custom_phases: list[PhaseItem] | None) # New phase: guard against universal overrides leaking into unrelated catalogs. # For legacy items with no device_type, first try matching against the active # catalog device_type before discarding, so legacy overrides are preserved. + # Only an override is guarded (no id, or a built-in's id): a phase the user + # created has its own id and stays, even when a built-in list added later + # shares its name (the air fryer's "Pause", 0.5.8). if not item_device_type and normalized_name.casefold() in all_builtin_names: active_dt_key = (str(device_type or "").strip().casefold(), normalized_name.casefold()) if active_dt_key in builtin_by_name: @@ -350,7 +428,9 @@ def merge_phase_catalog(device_type: str, custom_phases: list[PhaseItem] | None) if new_desc: merged[idx]["description"] = new_desc merged[idx]["is_default"] = False - continue + continue + if not phase_id or get_builtin_phase_by_id(phase_id) is not None: + continue # Deduplicate before appending. if phase_id and phase_id in seen_ids: diff --git a/custom_components/ha_washdata/phase_match.py b/custom_components/ha_washdata/phase_match.py deleted file mode 100644 index 16898008..00000000 --- a/custom_components/ha_washdata/phase_match.py +++ /dev/null @@ -1,320 +0,0 @@ -# WashData - Home Assistant integration for appliance cycle monitoring via smart plugs. -# Copyright (C) 2026 Lukas Bandura -# SPDX-License-Identifier: AGPL-3.0-or-later -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published -# by the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -"""Phase-aware profile matching and phase-resolved ETA (Phase 0 prototype). - -Given a cycle segmented into phases (:mod:`phase_segmenter`), this module: - -* builds a per-profile :class:`PhaseProfile` (per-role duration/energy priors) - from the profile's member cycles; -* scores candidate profiles by **per-role** duration + energy agreement, so two - profiles that differ mainly in heating length (i.e. temperature) are separated - by a large per-phase signal instead of a small whole-cycle-correlation delta; -* projects **time-remaining** as a per-role budget (Σ expected_role_total − - consumed_role), which personalises the ETA to the matched variant. - -The matcher handles **partial** (observed-so-far) cycles for progressive -narrowing: completed roles are compared fully; the open current role is scored -one-sided (a candidate is only penalised if the observed duration already -*exceeds* its expected total), so the correct larger-heating variant is not -prematurely ruled out while heating is still in progress. - -Constraint: NumPy only, no Home Assistant imports. Pure and executor-safe; -consumed live (behind the default-off ``enable_phase_matching`` per-device gate) -by ``ProfileStore.phase_remaining`` and the progress blend. See -`docs/superpowers/specs/2026-07-17-phase-segmented-matching-design.md`. -""" -from __future__ import annotations - -import math -from dataclasses import asdict, dataclass - -from .phase_segmenter import ( - ROLE_HEATING, - ROLE_IDLE, - ROLE_SPIN, - ROLE_WASH, - PhaseSegment, -) - -# Per-role weight in the match score. Heating dominates because it is the -# temperature discriminator; the wash middle is load-variable; spin/idle are -# weak/noisy signals. Overridable via the match config. -_ROLE_WEIGHTS: dict[str, float] = { - ROLE_HEATING: 0.50, - ROLE_WASH: 0.25, - ROLE_SPIN: 0.15, - ROLE_IDLE: 0.10, -} -_DUR_SCALE = 0.35 # log-ratio agreement scale for per-role duration -_EN_SCALE = 0.40 # ... and per-role energy -# Agreement score credited when a role is present on one side but absent on the -# other (structural mismatch, e.g. a heating vs no-heating cycle). 0.0 = full -# penalty (structural difference is a strong, wanted discriminator); raise toward -# 1.0 to soften. Config-overridable via ``occ_penalty``. -_OCC_PENALTY = 0.0 - - -@dataclass(frozen=True) -class RoleStat: - """Aggregated per-role prior across a profile's member cycles.""" - - dur_mean: float - dur_std: float - dur_p50: float - en_mean: float - occurrence: float # fraction of member cycles that contain this role - - -@dataclass(frozen=True) -class PhaseProfile: - """Per-profile phase model: per-role priors + total-duration prior.""" - - name: str - roles: dict[str, RoleStat] - total_dur_mean: float - total_dur_std: float - n_cycles: int - - -@dataclass(frozen=True) -class PhaseMatchResult: - name: str - score: float - - -def phase_profile_to_dict(profile: PhaseProfile) -> dict: - """Serialize a :class:`PhaseProfile` for storage (JSON-safe).""" - return { - "name": profile.name, - "n_cycles": profile.n_cycles, - "total_dur_mean": profile.total_dur_mean, - "total_dur_std": profile.total_dur_std, - "roles": {role: asdict(stat) for role, stat in profile.roles.items()}, - } - - -def phase_profile_from_dict(data: dict | None) -> PhaseProfile | None: - """Rebuild a :class:`PhaseProfile` from stored form. Never raises.""" - if not isinstance(data, dict): - return None - try: - roles = { - str(role): RoleStat( - dur_mean=float(s.get("dur_mean", 0.0)), - dur_std=float(s.get("dur_std", 0.0)), - dur_p50=float(s.get("dur_p50", 0.0)), - en_mean=float(s.get("en_mean", 0.0)), - occurrence=float(s.get("occurrence", 0.0)), - ) - for role, s in (data.get("roles") or {}).items() - if isinstance(s, dict) - } - if not roles: - return None - return PhaseProfile( - name=str(data.get("name", "")), - roles=roles, - total_dur_mean=float(data.get("total_dur_mean", 0.0)), - total_dur_std=float(data.get("total_dur_std", 0.0)), - n_cycles=int(data.get("n_cycles", 0)), - ) - except (TypeError, ValueError, AttributeError): - return None - - -def _agree(observed: float, expected: float, scale: float) -> float: - """Log-ratio agreement in (0, 1]; 1.0 when equal, sharper for small scale.""" - if observed <= 0.0 or expected <= 0.0: - # Both ~zero → perfect agreement; one zero → no agreement. - return 1.0 if observed <= 0.0 and expected <= 0.0 else 0.0 - scale = scale if scale > 1e-9 else 1e-9 # avoid div-by-zero on a 0 config scale - return 1.0 / (1.0 + abs(math.log(observed / expected)) / scale) - - -def _role_totals(segments: list[PhaseSegment]) -> dict[str, dict[str, float]]: - """Sum duration + energy per role across a cycle's segments.""" - totals: dict[str, dict[str, float]] = {} - for seg in segments: - acc = totals.setdefault(seg.role, {"dur": 0.0, "en": 0.0}) - acc["dur"] += max(0.0, seg.duration_s) - acc["en"] += max(0.0, seg.energy_wh) - return totals - - -def build_phase_profile(name: str, segmented_cycles: list[list[PhaseSegment]]) -> PhaseProfile | None: - """Aggregate a profile's member cycles into per-role priors. Never raises. - - Returns ``None`` when no usable cycles are supplied. - """ - cycles = [c for c in segmented_cycles if c] - if not cycles: - return None - n = len(cycles) - per_role_durs: dict[str, list[float]] = {} - per_role_ens: dict[str, list[float]] = {} - role_count: dict[str, int] = {} - totals_dur: list[float] = [] - for segs in cycles: - totals = _role_totals(segs) - totals_dur.append(sum(v["dur"] for v in totals.values())) - for role, v in totals.items(): - per_role_durs.setdefault(role, []).append(v["dur"]) - per_role_ens.setdefault(role, []).append(v["en"]) - role_count[role] = role_count.get(role, 0) + 1 - - def _mean(xs: list[float]) -> float: - return float(sum(xs) / len(xs)) if xs else 0.0 - - def _std(xs: list[float], m: float) -> float: - return float((sum((x - m) ** 2 for x in xs) / len(xs)) ** 0.5) if xs else 0.0 - - def _p50(xs: list[float]) -> float: - if not xs: - return 0.0 - s = sorted(xs) - mid = len(s) // 2 - return float(s[mid] if len(s) % 2 else (s[mid - 1] + s[mid]) / 2.0) - - roles: dict[str, RoleStat] = {} - for role, durs in per_role_durs.items(): - m = _mean(durs) - roles[role] = RoleStat( - dur_mean=m, dur_std=_std(durs, m), dur_p50=_p50(durs), - en_mean=_mean(per_role_ens[role]), - occurrence=role_count[role] / n, - ) - tm = _mean(totals_dur) - return PhaseProfile( - name=name, roles=roles, - total_dur_mean=tm, total_dur_std=_std(totals_dur, tm), n_cycles=n, - ) - - -def match_phase_profiles( - observed: list[PhaseSegment], - candidates: list[PhaseProfile], - config: dict | None = None, -) -> list[PhaseMatchResult]: - """Rank ``candidates`` for the ``observed`` (full or partial) cycle. Never raises. - - Score = weighted mean over roles of ``sqrt(dur_agree * energy_agree)``, with a - structural (occurrence-mismatch) penalty. The observed cycle's *open* role - (partial cycle) is scored one-sided so a larger-heating candidate is not - ruled out mid-heating. - """ - if not observed or not candidates: - return [] - cfg = config or {} - weights = {**_ROLE_WEIGHTS, **(cfg.get("role_weights") or {})} - dur_scale = float(cfg.get("dur_scale", _DUR_SCALE)) - en_scale = float(cfg.get("en_scale", _EN_SCALE)) - occ_pen = float(cfg.get("occ_penalty", _OCC_PENALTY)) - - totals = _role_totals(observed) - open_role = next((s.role for s in observed if s.open), None) - is_partial = open_role is not None - - results: list[PhaseMatchResult] = [] - for cand in candidates: - all_roles = set(totals) | set(cand.roles) - num = 0.0 - den = 0.0 - for role in all_roles: - w = weights.get(role, 0.1) - if w <= 0: - continue - obs = totals.get(role, {"dur": 0.0, "en": 0.0}) - stat = cand.roles.get(role) - if stat is None: - # Observed a role the candidate never exhibits: structural miss. - if obs["dur"] > 0: - num += w * occ_pen - den += w - continue - if role not in totals: - # Candidate expects a role not yet observed. Future phase on a - # partial cycle → neutral (skip). On a completed cycle → - # structural miss (candidate does a phase this cycle never had). - if not is_partial: - num += w * occ_pen - den += w - continue - if role == open_role: - # One-sided: only penalise if the in-progress duration/energy - # already EXCEEDS what this candidate expects for the whole role - # (a larger-budget candidate must not be ruled out mid-phase). - # Energy is the cleanest temperature discriminator, so score it - # one-sided too rather than dropping it (weaker narrowing). - da = 1.0 if obs["dur"] <= stat.dur_mean else _agree(obs["dur"], stat.dur_mean, dur_scale) - ea = 1.0 if obs["en"] <= stat.en_mean else _agree(obs["en"], stat.en_mean, en_scale) - agree = math.sqrt(da * ea) - else: - da = _agree(obs["dur"], stat.dur_mean, dur_scale) - ea = _agree(obs["en"], stat.en_mean, en_scale) - agree = math.sqrt(da * ea) - num += w * agree - den += w - score = (num / den) if den > 0 else 0.0 - results.append(PhaseMatchResult(name=cand.name, score=float(score))) - - results.sort(key=lambda r: r.score, reverse=True) - return results - - -def phase_eta( - observed: list[PhaseSegment], - profile: PhaseProfile, -) -> float | None: - """Project remaining seconds as a per-role budget. Never raises. - - Classifies each profile role against the observed-so-far segments (design §8): - - * **current** (the open, in-progress role): contribute - ``max(0, dur_mean − consumed)`` using the CONDITIONAL mean (the role is - known present, so no occurrence discount); - * **completed** (a role already observed but not the open one): contribute - **0** — it is done, even if it ran shorter than the profile mean (fixing - the "phantom remaining" over-count); - * **future** (a profile role not yet observed): contribute the - occurrence-weighted prior ``dur_mean × occurrence`` (we do not yet know - whether it will occur). - - Returns ``None`` when no phase priors are available (caller falls back to the - current estimator). - """ - if profile is None or not profile.roles: - return None - consumed = _role_totals(observed) if observed else {} - # The open (in-progress) role. If the caller did not mark one (e.g. a - # completed trace), treat the last observed role as current so completed - # roles still contribute 0 rather than a spurious prior. - open_role = next((s.role for s in (observed or []) if s.open), None) - if open_role is None and observed: - open_role = observed[-1].role - remaining = 0.0 - for role, stat in profile.roles.items(): - done = consumed.get(role, {}).get("dur", 0.0) - if role == open_role: - # current: conditional mean (occurrence -> 1, role is present) - remaining += max(0.0, stat.dur_mean - done) - elif role in consumed: - # completed: nothing more to spend on it - continue - else: - # future: unconditional prior (may or may not occur) - remaining += stat.dur_mean * max(0.0, min(1.0, stat.occurrence)) - return float(max(0.0, remaining)) diff --git a/custom_components/ha_washdata/phase_segmenter.py b/custom_components/ha_washdata/phase_segmenter.py deleted file mode 100644 index 77410fb7..00000000 --- a/custom_components/ha_washdata/phase_segmenter.py +++ /dev/null @@ -1,317 +0,0 @@ -# WashData - Home Assistant integration for appliance cycle monitoring via smart plugs. -# Copyright (C) 2026 Lukas Bandura -# SPDX-License-Identifier: AGPL-3.0-or-later -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published -# by the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -"""Unsupervised power-trace phase segmentation (Phase 0 prototype). - -Turns a cycle's power trace into an ordered list of :class:`PhaseSegment` -(role, start/end offset, duration, energy, mean/peak power) via a hysteresis -regime classifier (idle / active / high) + minimum-run merge + role assignment -driven by a per-device-type :class:`PhaseModel`. - -Constraint: NumPy only, no Home Assistant imports. Pure and executor-safe; -``segment_cycle`` never raises - it returns ``[]`` on malformed input. - -Consumed live (behind the default-off ``enable_phase_matching`` per-device gate) -by `profile_store.ProfileStore` (phase-profile cache + `phase_remaining`) and, -offline, by the side-by-side harness (`devtools/eta_phase_eval.py`). See -`docs/superpowers/specs/2026-07-17-phase-segmented-matching-design.md`. -""" -from __future__ import annotations - -from dataclasses import dataclass, field -from typing import Mapping, Optional - -import numpy as np - -from .const import CONF_ENABLE_PHASE_MATCHING -from .signal_processing import energy_gap_threshold_s, integrate_wh - -# Regime tokens (internal, not user-facing). -_IDLE = 0 -_ACTIVE = 1 -_HIGH = 2 - -# Role tokens. Physically detectable roles; mapping to display phase names -# (phase_catalog.DEFAULT_PHASES_BY_DEVICE) is a later (Phase 6) concern. -ROLE_HEATING = "heating" -ROLE_WASH = "wash" -ROLE_SPIN = "spin" -ROLE_IDLE = "idle" - - -@dataclass(frozen=True) -class PhaseSegment: - """One contiguous phase of a cycle.""" - - role: str - t_start: float # seconds from cycle start - t_end: float - duration_s: float - energy_wh: float - mean_w: float - peak_w: float - open: bool = False # True when this is the final segment of a partial cycle - - -@dataclass(frozen=True) -class PhaseModel: - """Per-device-type segmentation parameters and role vocabulary. - - ``high`` threshold = ``max(high_w_floor, high_frac * robust_peak)`` where the - robust peak is the 95th percentile of the trace, so a heating element is - detected relative to the device's own scale without a single spike inflating - it. ``active_w`` separates motor/fill activity from idle. A terminal - ``active`` block whose mean power >= ``spin_min_w`` and which begins in the - last ``spin_tail_frac`` of the (completed) cycle is labelled ``spin``. - """ - - device_type: str - high_w_floor: float - high_frac: float - active_w: float - spin_min_w: float - spin_tail_frac: float - min_run_s: float - roles: tuple[str, ...] = field(default=(ROLE_HEATING, ROLE_WASH, ROLE_SPIN, ROLE_IDLE)) - - -# Only washing_machine is intended for live rollout first; dishwasher and -# washer-dryer models exist for OFFLINE evaluation in the Phase-0 harness. -_MODELS: dict[str, PhaseModel] = { - "washing_machine": PhaseModel( - device_type="washing_machine", - high_w_floor=800.0, high_frac=0.5, - active_w=30.0, spin_min_w=200.0, spin_tail_frac=0.30, - min_run_s=90.0, - ), - "washer_dryer": PhaseModel( - device_type="washer_dryer", - high_w_floor=800.0, high_frac=0.5, - active_w=30.0, spin_min_w=200.0, spin_tail_frac=0.30, - min_run_s=90.0, - ), - # Dishwasher: heating element is the dominant HIGH regime; the long passive - # dry tail lands in IDLE. Deferred for LIVE rollout (delicate end-of-cycle - # logic) - present here only for offline measurement. - "dishwasher": PhaseModel( - device_type="dishwasher", - high_w_floor=800.0, high_frac=0.5, - active_w=25.0, spin_min_w=250.0, spin_tail_frac=0.25, - min_run_s=120.0, - ), -} - - -# Device types phase matching is rolled out for LIVE (validated by the Phase-0 -# ETA gate). Dishwasher has a model for OFFLINE evaluation only (its ETA gate was -# a no-go and its end-of-cycle logic is delicate), so it is deliberately excluded -# here even though ``phase_model_for("dishwasher")`` returns a model. -LIVE_PHASE_DEVICE_TYPES: tuple[str, ...] = ("washing_machine", "washer_dryer") - - -def phase_model_for(device_type: str | None) -> PhaseModel | None: - """Return the :class:`PhaseModel` for a device type, or ``None`` to fall back. - - ``None`` means the device type has no validated phase model and must use the - existing whole-cycle pipeline (zero-regression fallback). Returns a model for - every type with one defined (incl. dishwasher, for the offline harness). - """ - if not device_type: - return None - return _MODELS.get(str(device_type)) - - -def phase_matching_live_supported(device_type: str | None) -> bool: - """True when phase matching is enabled for LIVE use on this device type. - - Stricter than ``phase_model_for`` - only the Phase-0-validated rollout types - (washing machine, washer-dryer). Used to gate phase-profile caching and the - live ETA path; the offline harness uses ``phase_model_for`` directly. - """ - return ( - device_type in LIVE_PHASE_DEVICE_TYPES - and phase_model_for(device_type) is not None - ) - - -def phase_matching_enabled(options: "Mapping[str, object] | None", device_type: str | None) -> bool: - """True when the user opted in AND the device type is live-supported. - - The opt-in gate for the live phase-resolved ETA blend, mirroring - ``ml.engine.ml_models_enabled``. Both conditions are required: the per-device - ``enable_phase_matching`` option and a validated live phase model. - """ - if not options: - return False - if not bool(options.get(CONF_ENABLE_PHASE_MATCHING, False)): - return False - return phase_matching_live_supported(device_type) - - -def _classify(power: np.ndarray, model: PhaseModel) -> tuple[np.ndarray, float]: - """Per-sample regime tokens + the robust peak used for the HIGH threshold.""" - robust_peak = float(np.percentile(power, 95)) if power.size else 0.0 - high_thr = max(model.high_w_floor, model.high_frac * robust_peak) - reg = np.full(power.shape, _IDLE, dtype=int) - reg[power >= model.active_w] = _ACTIVE - reg[power >= high_thr] = _HIGH - return reg, robust_peak - - -def _runs(reg: np.ndarray) -> list[list[int]]: - """Contiguous same-regime runs as ``[regime, start_idx, end_idx]`` (inclusive). - - Replaces the nested while-loop (O(n) Python iterations) with np.diff + - np.flatnonzero (C-level), then a short list comprehension over only the - run-boundary indices (~10-30 for a real cycle vs 1 400+ element visits). - """ - if len(reg) == 0: - return [] - changes = np.flatnonzero(np.diff(reg)) # indices where value changes - starts = np.empty(len(changes) + 1, dtype=np.intp) - ends = np.empty(len(changes) + 1, dtype=np.intp) - starts[0] = 0 - starts[1:] = changes + 1 - ends[:-1] = changes - ends[-1] = len(reg) - 1 - return [[int(reg[s]), int(s), int(e)] for s, e in zip(starts, ends)] - - -def _merge_short(runs: list[list[int]], t: np.ndarray, min_run_s: float) -> list[list[int]]: - """Absorb runs shorter than ``min_run_s`` into their neighbour. - - A short leading run merges forward; every other short run merges into the - preceding (already-accepted) run. This prevents brief motor spikes during - tumbling from fragmenting a wash phase, and brief dips from splitting a - heating block, without erasing genuine phases. - """ - if not runs: - return runs - merged: list[list[int]] = [] - for reg, a, b in runs: - dur = float(t[b] - t[a]) - if merged and dur < min_run_s: - merged[-1][2] = b # extend previous run to absorb this short one - else: - merged.append([reg, a, b]) - # A short LEADING run has no preceding run to absorb it, so it would survive - # as its own segment (e.g. a startup/inrush HIGH spike becoming a phantom - # heating block that pollutes the temperature prior). Fold it FORWARD into - # the following run, adopting that run's regime. - while len(merged) > 1 and float(t[merged[0][2]] - t[merged[0][1]]) < min_run_s: - merged[1][1] = merged[0][1] # extend 2nd run's start back over the lead - merged.pop(0) - return merged - - -def segment_cycle( - timestamps: "np.ndarray | list[float]", - power: "np.ndarray | list[float]", - model: PhaseModel, - *, - partial: bool = False, -) -> list[PhaseSegment]: - """Segment a power trace into ordered phases. Never raises. - - Args: - timestamps: seconds from cycle start (ascending). - power: watts, same length as ``timestamps``. - model: the device-type :class:`PhaseModel`. - partial: when True the trace is an observed-so-far prefix of a running - cycle; the final segment is marked ``open=True``. - - Returns: - List of :class:`PhaseSegment` in time order, or ``[]`` when the input is - too short/degenerate to segment. Any internal error also yields ``[]``. - """ - try: - return _segment_impl(timestamps, power, model, partial=partial) - except Exception: # noqa: BLE001 - segmentation must never break a live cycle - return [] - - -def _segment_impl( - timestamps: "np.ndarray | list[float]", - power: "np.ndarray | list[float]", - model: PhaseModel, - *, - partial: bool = False, -) -> list[PhaseSegment]: - t = np.asarray(timestamps, dtype=float) - w = np.asarray(power, dtype=float) - if t.size < 4 or w.size != t.size: - return [] - if not (np.all(np.isfinite(t)) and np.all(np.isfinite(w))): - finite = np.isfinite(t) & np.isfinite(w) - t, w = t[finite], w[finite] - if t.size < 4: - return [] - # Enforce ascending time. - order = np.argsort(t, kind="stable") - t, w = t[order], w[order] - - reg, _peak = _classify(w, model) - runs = _merge_short(_runs(reg), t, model.min_run_s) - if not runs: - return [] - - gap_s = energy_gap_threshold_s(t) - total = float(t[-1] - t[0]) - spin_zone_start = t[0] + (1.0 - model.spin_tail_frac) * total if total > 0 else t[-1] - - # First pass: build raw segments with stats. - raw: list[dict] = [] - for reg_tok, a, b in runs: - seg_t = t[a:b + 1] - seg_w = w[a:b + 1] - dur = float(seg_t[-1] - seg_t[0]) if seg_t.size > 1 else 0.0 - energy = integrate_wh(seg_t, seg_w, max_gap_s=gap_s) if seg_t.size > 1 else 0.0 - raw.append({ - "reg": reg_tok, "a": a, "b": b, - "t0": float(seg_t[0]), "t1": float(seg_t[-1]), - "dur": dur, "energy": float(energy), - "mean": float(np.mean(seg_w)), "peak": float(np.max(seg_w)), - }) - - # Determine which ACTIVE run (if any) is the spin: the last non-idle run, - # elevated and starting in the terminal zone. Only on a completed cycle - - # a partial cycle can't know its terminal segment yet. - spin_idx: Optional[int] = None - if not partial: - for idx in range(len(raw) - 1, -1, -1): - seg = raw[idx] - if seg["reg"] == _IDLE: - continue - if (seg["reg"] == _ACTIVE and seg["mean"] >= model.spin_min_w - and seg["t0"] >= spin_zone_start): - spin_idx = idx - break # only inspect the last non-idle run - - out: list[PhaseSegment] = [] - for idx, seg in enumerate(raw): - if seg["reg"] == _HIGH: - role = ROLE_HEATING - elif seg["reg"] == _IDLE: - role = ROLE_IDLE - else: # _ACTIVE - role = ROLE_SPIN if idx == spin_idx else ROLE_WASH - out.append(PhaseSegment( - role=role, t_start=seg["t0"], t_end=seg["t1"], - duration_s=seg["dur"], energy_wh=seg["energy"], - mean_w=seg["mean"], peak_w=seg["peak"], - open=(partial and idx == len(raw) - 1), - )) - return out diff --git a/custom_components/ha_washdata/playground.py b/custom_components/ha_washdata/playground.py index 993991bf..c7a850e4 100644 --- a/custom_components/ha_washdata/playground.py +++ b/custom_components/ha_washdata/playground.py @@ -25,10 +25,7 @@ Main entry points: - :func:`simulate_cycle_detail` - faithful single-cycle replay with per-step progress/remaining-time/phase/energy series and typed event log. - :func:`run_playground_history` - per-cycle rows + optional before/after diff. -- :func:`run_playground_sweep` - objective 1D/2D grid sweep. -- :func:`dtw_debug_payload` - the score breakdown (Stage 2 / DTW / Stage 4), - the two resampled traces on a shared grid, and the DTW warping path for one - cycle vs one profile (the DTW visualizer). +- :func:`run_playground_sweep` - objective 1D grid sweep. All top-level entry points are defensive: they never raise, returning an ``{"error": ...}`` marker instead so the WS handlers can relay it. @@ -45,12 +42,11 @@ import numpy as np from homeassistant.util import dt as dt_util -from . import analysis +from . import match_rules from . import notification_rules as notif_rules from . import progress as progress_mod -from .phase_segmenter import phase_matching_enabled +from .options_utils import option_float, option_int from .signal_processing import ( - resample_adaptive, compact_price_timeline, cycle_cost, energy_gap_threshold_s, @@ -67,8 +63,11 @@ from .const import ( CONF_ANTI_WRINKLE_MAX_DURATION, CONF_ANTI_WRINKLE_MAX_POWER, CONF_COMPLETION_MIN_SECONDS, - CONF_END_REPEAT_COUNT, + CONF_END_ENERGY_THRESHOLD, + CONF_PROFILE_MATCH_INTERVAL, + CONF_PROFILE_MATCH_THRESHOLD, CONF_INTERRUPTED_MIN_SECONDS, + CONF_LEARNING_CONFIDENCE, CONF_MATCH_PERSISTENCE, CONF_MIN_OFF_GAP, CONF_MIN_POWER, @@ -80,65 +79,53 @@ from .const import ( CONF_OFF_DELAY, CONF_PROFILE_MATCH_MAX_DURATION_RATIO, CONF_PROFILE_MATCH_MIN_DURATION_RATIO, + CONF_PROFILE_UNMATCH_THRESHOLD, CONF_START_DURATION_THRESHOLD, CONF_START_THRESHOLD_W, CONF_STOP_THRESHOLD_W, + CONF_WATCHDOG_INTERVAL, CYCLE_OVERRUN_ANOMALY_RATIO, CYCLE_UNDERRUN_ANOMALY_RATIO, - DEFAULT_DTW_BANDWIDTH, + DEFAULT_LEARNING_CONFIDENCE, DEFAULT_MATCH_PERSISTENCE, + DEFAULT_MAX_DEFERRAL_SECONDS, + DISHWASHER_END_SPIKE_WAIT_SECONDS, DEFAULT_NOTIFY_BEFORE_END_MINUTES, DEFAULT_NOTIFY_MILESTONES, DEFAULT_PROFILE_MATCH_MAX_DURATION_RATIO, DEFAULT_PROFILE_MATCH_MIN_DURATION_RATIO, - MATCH_CORR_WEIGHT, - MATCH_DDTW_DIST_SCALE, - MATCH_DTW_BLEND, - MATCH_DTW_DIST_SCALE, - MATCH_DTW_ENSEMBLE_W, - MATCH_DTW_REFINE_TOP_N, - MATCH_DTW_RESAMPLE_N, - MATCH_DURATION_SCALE, - MATCH_DURATION_WEIGHT, - MATCH_ENERGY_SCALE, - MATCH_ENERGY_WEIGHT, - MATCH_KEEP_MIN_SCORE, - 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, - PLAYGROUND_STRESS_FLUCT_FALLBACK_FRAC, - PLAYGROUND_STRESS_MAX_IDLE_W, - PLAYGROUND_STRESS_MAX_SPARSE_STEPS, - PLAYGROUND_STRESS_SPARSE_STEP_S, - PLAYGROUND_STRESS_TRAILING_WINDOW_S, + DEFAULT_PROFILE_UNMATCH_THRESHOLD, STATE_ENDING, STATE_FINISHED, STATE_IDLE, STATE_OFF, - STATE_PAUSED, STATE_RUNNING, STATE_STARTING, STATE_UNKNOWN, TerminationReason, + resolve_watchdog_interval_default, ) from .cycle_detector import ( + MatchContext, CycleDetector, CycleDetectorConfig, effective_anticrease_finalize_ratio, effective_curve_preroll_seconds, + standby_near_stop_ceiling, terminal_high_for_guards, ) from .profile_store import ( - _ambiguity_from_candidates, - _match_prefix_ambiguity, - collapse_group_candidates, - longest_candidate_duration, + MatchResult, + ProfileStore, decompress_power_data, ) +from .detector_config import ( + terminal_drop_baseline_for, + terminal_drop_enabled, + terminal_drop_fires, + terminal_drop_may_fire, +) +from .time_utils import power_data_to_offsets _LOGGER = logging.getLogger(__name__) @@ -197,78 +184,45 @@ _OVERRIDE_FIELD_MAP: dict[str, tuple[str, Callable[[Any], Any]]] = { CONF_OFF_DELAY: ("off_delay", int), CONF_MIN_OFF_GAP: ("min_off_gap", int), CONF_COMPLETION_MIN_SECONDS: ("completion_min_seconds", int), - CONF_END_REPEAT_COUNT: ("end_repeat_count", int), CONF_START_THRESHOLD_W: ("start_threshold_w", float), CONF_STOP_THRESHOLD_W: ("stop_threshold_w", float), CONF_START_DURATION_THRESHOLD: ("start_duration_threshold", float), CONF_INTERRUPTED_MIN_SECONDS: ("interrupted_min_seconds", int), + # Suggested settings the Playground could not what-if (audit SUGGEST-19). + CONF_END_ENERGY_THRESHOLD: ("end_energy_threshold", float), + CONF_PROFILE_MATCH_THRESHOLD: ("match_confidence_threshold", float), + CONF_PROFILE_MATCH_INTERVAL: ("match_interval", int), } # Matching options the Playground honours, mapped to the ``match_config`` key -# they drive. The two duration ratios are real user settings (a good value found -# here can be applied for real). The remaining keys are the Stage 2-4 scoring -# weights / DTW knobs: they are NOT persistent settings (they are ML-tuned -# defaults), but they ARE exposed here as SANDBOX-ONLY overrides so power users -# can experiment with how each stage of the matcher scores their own cycles in -# the Playground. They never persist - a match config built from them lives only -# for the simulation. Anything else in ``settings_override`` is ignored. +# they drive: the two Stage-1 duration ratios, both real user settings. The Stage +# 2-4 scoring weights and DTW knobs were sandbox-only overrides until 0.5.8; they +# could not persist, matching is saturated on them, and tuning them on 20 +# in-sample cycles only overfit. Anything else in ``settings_override`` is ignored. _MATCH_OVERRIDE_KEYS: dict[str, tuple[str, Callable[[Any], Any]]] = { - # Stage 1 - duration gate (real settings) CONF_PROFILE_MATCH_MIN_DURATION_RATIO: ("min_duration_ratio", float), CONF_PROFILE_MATCH_MAX_DURATION_RATIO: ("max_duration_ratio", float), - # Stage 2 - core similarity (sandbox-only) - "corr_weight": ("corr_weight", float), - "keep_min_score": ("keep_min_score", float), - # Stage 3 - DTW refinement (sandbox-only) - "dtw_bandwidth": ("dtw_bandwidth", float), - "dtw_blend": ("dtw_blend", float), - "dtw_ensemble_w": ("dtw_ensemble_w", float), - "dtw_ddtw_scale": ("dtw_ddtw_scale", float), - "dtw_refine_top_n": ("dtw_refine_top_n", int), - # Stage 4 - duration/energy agreement (sandbox-only) - "duration_weight": ("duration_weight", float), - "energy_weight": ("energy_weight", float), - "duration_scale": ("duration_scale", float), - "energy_scale": ("energy_scale", float), } # Canonical default for every matching override key, keyed by the OPTION key the -# Playground uses. The Stage 2-4 entries are code constants (not stored options), -# so this table is the only place the panel can read them from; ``ws_get_constants`` -# ships it as ``pg_match_defaults`` and ``effective_settings`` falls back to it for -# any key the live matcher config does not carry. +# Playground uses. ``ws_get_constants`` ships it as ``pg_match_defaults`` and +# ``effective_settings`` falls back to it for any key the live matcher config does +# not carry. MATCH_DEFAULTS_BY_OPTION: dict[str, Any] = { CONF_PROFILE_MATCH_MIN_DURATION_RATIO: DEFAULT_PROFILE_MATCH_MIN_DURATION_RATIO, CONF_PROFILE_MATCH_MAX_DURATION_RATIO: DEFAULT_PROFILE_MATCH_MAX_DURATION_RATIO, - "corr_weight": MATCH_CORR_WEIGHT, - "keep_min_score": MATCH_KEEP_MIN_SCORE, - "dtw_bandwidth": DEFAULT_DTW_BANDWIDTH, - "dtw_blend": MATCH_DTW_BLEND, - "dtw_ensemble_w": MATCH_DTW_ENSEMBLE_W, - "dtw_ddtw_scale": MATCH_DDTW_DIST_SCALE, - "dtw_refine_top_n": MATCH_DTW_REFINE_TOP_N, - "duration_weight": MATCH_DURATION_WEIGHT, - "energy_weight": MATCH_ENERGY_WEIGHT, - "duration_scale": MATCH_DURATION_SCALE, - "energy_scale": MATCH_ENERGY_SCALE, } # Every option key the Playground control panel may carry (detection + matching). -# This is the allow-list for a saved Playground preset: anything else submitted by -# a client is dropped rather than stored. +# Anything else submitted by a client is dropped. SETTING_KEYS: frozenset[str] = frozenset(_OVERRIDE_FIELD_MAP) | frozenset(_MATCH_OVERRIDE_KEYS) -# The subset a user may publish from the Playground back into the live config: -# exactly the override keys that are REAL config-entry options (``CONF_*``). The -# Stage 2-4 scoring knobs above are sandbox-only code constants - there is no -# option behind them, so writing them into ``entry.options`` would create dead -# keys the integration never reads. The panel gates its publish buttons on this -# list (shipped by ``get_playground_settings``). -PUBLISHABLE_SETTING_KEYS: frozenset[str] = frozenset(_OVERRIDE_FIELD_MAP) | { - CONF_PROFILE_MATCH_MIN_DURATION_RATIO, - CONF_PROFILE_MATCH_MAX_DURATION_RATIO, -} +# The keys a user may publish from the Playground back into the live config. Every +# Playground key is now a real config-entry option, so this is all of them; the +# panel still gates its publish buttons on this list (shipped by +# ``get_playground_settings``). +PUBLISHABLE_SETTING_KEYS: frozenset[str] = SETTING_KEYS def effective_settings( @@ -289,7 +243,7 @@ def effective_settings( continue try: out[opt_key] = coerce(value) - except (TypeError, ValueError): # pragma: no cover - defensive + except (TypeError, ValueError, OverflowError): # pragma: no cover - defensive continue cfg = match_config or {} for opt_key, (cfg_key, coerce) in _MATCH_OVERRIDE_KEYS.items(): @@ -298,7 +252,7 @@ def effective_settings( continue try: out[opt_key] = coerce(value) - except (TypeError, ValueError): # pragma: no cover - defensive + except (TypeError, ValueError, OverflowError): # pragma: no cover - defensive continue return out @@ -307,7 +261,7 @@ def sanitize_setting_values(values: Any) -> dict[str, Any]: """Filter a client-supplied settings map down to storable Playground values. Keeps only keys in :data:`SETTING_KEYS`, coerced with the same coercers the - simulation uses, so a preset can never carry an unknown key or a value that + simulation uses, so an override can never carry an unknown key or a value that would be silently ignored at replay time. Never raises. """ if not isinstance(values, dict): @@ -322,7 +276,7 @@ def sanitize_setting_values(values: Any) -> dict[str, Any]: _target, coerce = mapping try: coerced = coerce(value) - # OverflowError: the preset payload is JSON-decoded, so an oversized + # OverflowError: the override 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. @@ -360,7 +314,7 @@ def apply_match_overrides( continue try: out[cfg_key] = coerce(val) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): pass return out @@ -384,13 +338,13 @@ def build_sim_config( field, coerce = mapping try: changes[field] = coerce(value) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): continue if not changes: return base try: return replace(base, **changes) - except (TypeError, ValueError): # pragma: no cover - defensive + except (TypeError, ValueError, OverflowError): # pragma: no cover - defensive return base @@ -419,6 +373,15 @@ def _cycle_label(cycle: dict[str, Any]) -> str | None: return None +# The grid a replay's snapshots are first built on: `resample_adaptive`'s floor, +# which is what most cycles resolve to (it is max(5 s, the trace's median step)). +_PLAYGROUND_START_DT = 5.0 + +# Bound on the keepalives emulated inside one silent stretch (8 h at a 30 s +# watchdog): the detector's own 8 h cap ends any cycle long before this. +_MAX_KEEPALIVES_PER_GAP = 960 + + def _build_match_snapshots( store: Any, ) -> tuple[list[dict[str, Any]], dict[str, Any], dict[str, list[str]], dict[str, Any]]: @@ -442,130 +405,180 @@ def _build_match_snapshots( cohesive groups exist ``group_members`` and ``member_snaps`` are both empty dicts and behaviour is identical to before. """ - snapshots: list[dict[str, Any]] = [] + # 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) + # The live builder (item 387a), on the grid a replayed cycle starts on; + # `_SimStore` re-grids per match, as live does. A store without it (the sim + # run with no store at all) has nothing to match against. The hand-rolled + # builder that used to serve a MagicMock store here had no production caller. + if not callable(getattr(type(store), "build_match_snapshots", None)): + return [], config, {}, {} try: - data = getattr(store, "_data", {}) or {} - # Snapshot the profiles dict before iterating: this runs in an executor thread - # (ws_api dispatches _build_match_snapshots via async_add_executor_job) while the - # event loop may add/remove a profile (cycle-end creation, GC, auto-label), and a - # live `.items()` walk would raise "dictionary changed size during iteration" - - # the same race get_export_inventory was moved on-loop to avoid. dict() is a cheap - # shallow copy of the top-level mapping (values are read-only here). iter_evidence_ - # cycles() below returns a fresh list, so its .extend() is already snapshot-safe. - profiles = dict(data.get("profiles", {}) or {}) - # Include every cycle the live matcher would consider, via the store's own - # evidence view: an import-only profile samples from reference_cycles or - # backfill_cycles, so a snapshot pool built from past_cycles alone would drop it - # as a candidate and the Playground's auto-detect would never match a downloaded - # or backfilled profile - silently reporting it as unmatched. Reading the same - # gated view the matcher reads also keeps the sandbox honest when the user has - # excluded a category from shaping profiles. - try: - pool = store.iter_evidence_cycles() - except Exception: # pylint: disable=broad-exception-caught - # Older store without the evidence view: fall back to the raw lists. Include - # backfill_cycles too (the evidence view does), else a profile whose sample - # lives only in imported history has no snapshot and the sim reports it - # unmatched though live matching can use it. - pool = ( - list(data.get("past_cycles", []) or []) - + list(data.get("reference_cycles", []) or []) - + list(data.get("backfill_cycles", []) or []) - ) - by_id = {c.get("id"): c for c in pool if isinstance(c, dict)} - for name, profile in profiles.items(): - if not isinstance(profile, dict): - continue - sample_cycle = by_id.get(profile.get("sample_cycle_id")) - if not sample_cycle: - continue - sample_p = decompress_power_data(sample_cycle) - if not sample_p: - continue - avg_dur = ( - profile.get("avg_duration") - or sample_cycle.get("duration") - or 0.0 - ) - snapshots.append( - { - "name": name, - "avg_duration": float(avg_dur), - "sample_power": [p for _, p in sample_p], - # The trace's own time span, which is NOT avg_duration (a trimmed - # mean across cycles). `analysis._prefix_point_count` converts - # elapsed time to an index with it, so omitting it made the sim - # truncate the prefix at a different point than production. - "sample_span_s": float(sample_p[-1][0] - sample_p[0][0]), - } - ) + snapshots = store.build_match_snapshots(_PLAYGROUND_START_DT) except Exception as exc: # pylint: disable=broad-exception-caught - _LOGGER.debug("Playground: snapshot build failed: %s", exc) - - # Stage-5: collapse cohesive profile groups into aggregate candidates. - group_members: dict[str, list[str]] = {} - member_snaps: dict[str, Any] = {} + _LOGGER.debug("Playground: live snapshot builder failed: %s", exc) + snapshots = [] + # Stage-5: map cohesive profile groups to their members; every member is + # scored on its own curve and collapse_group_candidates forms the family. try: grouped_snaps, group_members, member_snaps = store._grouped_snapshots( # pylint: disable=protected-access snapshots ) except Exception as exc: # pylint: disable=broad-exception-caught _LOGGER.debug("Playground: _grouped_snapshots failed: %s", exc) - grouped_snaps = snapshots - - # 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) + grouped_snaps, group_members, member_snaps = snapshots, {}, {} return grouped_snaps, config, group_members, member_snaps 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)), - "max_duration_ratio": float(getattr(store, "_max_duration_ratio", 1.5)), + """Live matcher config from the store (the live matcher runs no overrides).""" + return { + "min_duration_ratio": float(getattr(store, "_min_duration_ratio", DEFAULT_PROFILE_MATCH_MIN_DURATION_RATIO)), + "max_duration_ratio": float(getattr(store, "_max_duration_ratio", DEFAULT_PROFILE_MATCH_MAX_DURATION_RATIO)), "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 - if isinstance(overrides, dict): - config.update(overrides) - except Exception: # pylint: disable=broad-exception-caught - pass - return config -def decide_commit( - raw_name: str | None, - is_ambiguous: bool, - commit_state: dict[str, Any], - persistence: int, -) -> str | None: - """Advance the persistence-gated match-commit state (mirror of the manager's - core rule) and report the event to emit. +class _InlineExecutor: + """``hass`` for :class:`_SimStore`: an executor job runs inline, in this thread. - Mutates ``commit_state`` (``candidate``/``count``/``name``): a candidate must - be the non-ambiguous top-1 for ``persistence`` consecutive calls before it is - committed, and the committed ``name`` is held (a one-off wobble resets the - streak but never switches the commit). Returns ``"match_commit"`` on the first - commit, ``"match_changed"`` on a later switch, or ``None`` otherwise. Pure + - unit-testable; event emission / reporting stay in the caller. + The replay is already in an executor thread (or a harness with no loop), and + the store's real ``hass`` belongs to the event loop, so it must not be used. """ - if not raw_name or is_ambiguous: - return None - if raw_name == commit_state["candidate"]: - commit_state["count"] += 1 - else: - commit_state["candidate"] = raw_name - commit_state["count"] = 1 - if commit_state["count"] >= persistence and commit_state["name"] != raw_name: - prev = commit_state["name"] - commit_state["name"] = raw_name - return "match_changed" if prev else "match_commit" - return None + + async def async_add_executor_job(self, fn: Callable[..., Any], *args: Any) -> Any: + return fn(*args) + + +def _run_inline(coro: Any) -> Any: + """Drive a store coroutine whose only awaits are :class:`_InlineExecutor` jobs. + + Those complete without suspending, so the coroutine finishes on its first step. + If a future change gives it a real suspension point this raises instead of + returning a wrong answer, and the replay reports the failure. + """ + try: + coro.send(None) + except StopIteration as stop: + return stop.value + coro.close() + raise RuntimeError("store coroutine suspended; the Playground cannot await it") + + +#: Distinct query grids a replay keeps candidate templates for. +_MAX_SNAPSHOT_GRIDS = 64 +#: How long the synthetic tail may wait out a held verified pause: the watchdog's +#: own limit for silence under one (``manager._watchdog_check_stuck_cycle``). +_TAIL_VERIFIED_PAUSE_CAP_S = DEFAULT_MAX_DEFERRAL_SECONDS + 1800.0 + + +def _tail_span_s(config: Any) -> float: + """How long the synthetic 0 W tail runs so a natural end can fire. + + Past the longest ordinary end gate (off delay / min off gap), plus margin. A + dishwasher also waits up to ``DISHWASHER_END_SPIKE_WAIT_SECONDS`` for a late + pump-out, so its tail covers that: sized on the two settings alone, a what-if + that lowered them (as Apply all does) force-stopped a cycle the detector would + have ended normally (one Eco cycle needed 1530 s against a 600 s tail; found by + devtools/suggestion_loop_eval.py, register item 455). + """ + gate = max(float(config.off_delay or 0.0), float(config.min_off_gap or 0.0)) + if getattr(config, "device_type", None) == "dishwasher": + gate = max(gate, DISHWASHER_END_SPIKE_WAIT_SECONDS) + return gate * 1.5 + 300.0 + + +class _SimStore: + """The device's store as the live matcher sees it, callable from a replay. + + The replay runs the REAL ``ProfileStore.async_match_profile`` and + ``ProfileStore.async_verify_alignment`` with this object as ``self`` (item 387a, + audit PLAYGROUND-01): every attribute not defined here is the store's own, so + the candidate pool, Stage 1-5 (incl. the in-progress member preference), the + 12-point floor, ambiguity, the prefix flags, the member confidence, the phase + lookup and the alignment thresholds are the live code rather than a copy that + can drift. Three things differ, all deliberate: + + * ``hass`` runs executor jobs inline (:class:`_InlineExecutor`); + * the matcher config is the sim's - the live config plus any what-if override + of the Stage-1 ratios - returned from ``_matching_overrides``, which + ``async_match_profile`` merges last; + * candidate templates are cached per query grid, seeded with the prebuilt 5 s + set a batch shares. A store without the live builder (a MagicMock in tests) + gets the prebuilt set whatever the grid, as before. + """ + + def __init__( + self, + store: Any, + match_config: dict[str, Any], + prebuilt: tuple[Any, Any, Any, Any], + ) -> None: + self._store = store + self.hass = _InlineExecutor() + self._config = {k: v for k, v in (match_config or {}).items() if k != "in_progress"} + self.dtw_bandwidth = self._config.get( + "dtw_bandwidth", getattr(store, "dtw_bandwidth", 0.2) + ) + snaps, _cfg, group_members, member_snaps = prebuilt + self._prebuilt = (snaps, (snaps, group_members or {}, member_snaps or {})) + self._live_builder = callable(getattr(type(store), "build_match_snapshots", None)) + self._grids: dict[float, tuple[Any, Any]] = ( + {float(_PLAYGROUND_START_DT): self._prebuilt} if self._live_builder else {} + ) + self._pending: tuple[Any, Any] | None = None + + def __getattr__(self, name: str) -> Any: + return getattr(self._store, name) + + def _matching_overrides(self) -> dict[str, Any]: + return dict(self._config) + + def build_match_snapshots(self, used_dt: float) -> list[dict[str, Any]]: + if not self._live_builder: + self._pending = self._prebuilt + return self._prebuilt[0] + key = float(used_dt) + hit = self._grids.get(key) + if hit is None: + snaps = self._store.build_match_snapshots(used_dt) + hit = (snaps, self._store._grouped_snapshots(snaps)) # noqa: SLF001 + if len(self._grids) >= _MAX_SNAPSHOT_GRIDS: + self._grids.clear() + self._grids[key] = hit + self._pending = hit + return hit[0] + + def _grouped_snapshots(self, snapshots: list[dict[str, Any]]) -> Any: + pending = self._pending + if pending is not None and pending[0] is snapshots: + return pending[1] + return self._store._grouped_snapshots(snapshots) # noqa: SLF001 + + def match( + self, + readings: Any, + duration: float, + in_progress: bool = False, + stop_threshold_w: float | None = None, + ) -> MatchResult: + """``ProfileStore.async_match_profile`` on this view, run to completion.""" + return _run_inline( + ProfileStore.async_match_profile( + self, readings, duration, # type: ignore[arg-type] + in_progress=in_progress, stop_threshold_w=stop_threshold_w, + ) + ) + + def verify_alignment(self, profile_name: str, trace: Any) -> tuple[bool, float, float]: + """``ProfileStore.async_verify_alignment`` on this view, run to completion.""" + return _run_inline( + ProfileStore.async_verify_alignment(self, profile_name, trace) # type: ignore[arg-type] + ) def _readings_from_cycle( @@ -581,11 +594,43 @@ def _readings_from_cycle( # ─── Single-cycle faithful simulation (Simulate mode) ─────────────────────────── -_PROGRESS_STATES = (STATE_RUNNING, STATE_PAUSED, STATE_ENDING) # States in which no progress estimate is shown (mirrors _update_remaining_only). _DEAD_STATES = (STATE_OFF, STATE_UNKNOWN, STATE_IDLE) _SIM_SERIES_THROTTLE_S = 30.0 # cap estimator calls; 5s matched cadence made this a no-op +# Terminal-drop baselines per stored-cycle list (audit ML-08). A History/Optimize +# batch replays many cycles against one store and the baseline decompresses every +# completed trace, so it is built once per list. The list is held, so its id +# cannot be recycled while cached; an append changes the length in the key. +_TERMINAL_DROP_BASELINES: dict[tuple[int, int, float], tuple[Any, Any]] = {} +_MAX_TERMINAL_DROP_BASELINES = 16 + + +def _sim_terminal_drop_baseline( + store: Any, stop_threshold_w: float +) -> tuple[float | None, tuple[float, float] | None]: + """The live terminal-drop baseline over ``store``'s stored cycles. Never raises. + + Built from every stored cycle, the replayed one included, like the rest of the + Playground (in-sample). That can only make a completed cycle LESS likely to + fire: its own first quiet span is in the baseline. + """ + try: + cycles = store.get_past_cycles() + if not isinstance(cycles, list): + return None, None + key = (id(cycles), len(cycles), float(stop_threshold_w)) + hit = _TERMINAL_DROP_BASELINES.get(key) + if hit is not None and hit[0] is cycles: + return hit[1] + baseline = terminal_drop_baseline_for(list(cycles), stop_threshold_w) + if len(_TERMINAL_DROP_BASELINES) >= _MAX_TERMINAL_DROP_BASELINES: + _TERMINAL_DROP_BASELINES.clear() + _TERMINAL_DROP_BASELINES[key] = (cycles, baseline) + return baseline + except Exception: # pylint: disable=broad-exception-caught + return None, None + def simulate_cycle_detail( cycle: dict[str, Any], @@ -596,15 +641,14 @@ def simulate_cycle_detail( price: float | None = None, compute_series: bool = True, prebuilt: tuple[Any, Any, Any, Any] | None = None, - stress_tail: bool = False, - stress_idle_w: float | None = None, ) -> dict[str, Any]: """Faithful single-cycle replay for the Playground "Simulate" view. - Drives the REAL :class:`CycleDetector` + real Stage 1-4 matcher over the - cycle's own trace and, at production's 5s cadence, calls the SAME + Drives the REAL :class:`CycleDetector`, the real matcher and the manager's + :mod:`match_rules` over the cycle's own trace, and calls the SAME :mod:`progress` and :mod:`notification_rules` functions the live integration - uses - so the returned timeline is byte-for-byte what would happen live. No + uses (the estimator every ``_SIM_SERIES_THROTTLE_S`` = 30 s of replay time, with + the live per-second EMA scaling) - so the timeline is what would happen live. No detection/progress/notification math is implemented here; this only orchestrates the shared code. Never raises; returns ``{"error": ...}`` on failure. Read-only: nothing is persisted and no notifications are sent. @@ -616,7 +660,7 @@ def simulate_cycle_detail( try: return _simulate_cycle_detail_inner( cycle, base_config, settings_override, store, options, price, - compute_series, prebuilt, stress_tail, stress_idle_w, + compute_series, prebuilt, ) except Exception as exc: # pylint: disable=broad-exception-caught _LOGGER.debug("Playground detail sim failed for %s: %s", cycle.get("id"), exc) @@ -627,32 +671,6 @@ def _device_type_of(config: CycleDetectorConfig) -> str: return getattr(config, "device_type", "washing_machine") -def simulate_cycle_detail_by_id( - store: Any, - cycle_id: str, - base_config: CycleDetectorConfig, - settings_override: dict[str, Any] | None, - options: dict[str, Any] | None, - price: float | None = None, -) -> dict[str, Any]: - """Find a stored cycle by id and simulate it. Runs the (in-memory) store - lookup and the replay together so the WS handler can offload the whole thing - to an executor thread. Returns ``{"error": "not_found", ...}`` when the id is - unknown. Never raises.""" - try: - cycle = next( - (c for c in store.get_past_cycles() if c.get("id") == cycle_id), None - ) - except Exception as exc: # pylint: disable=broad-exception-caught - _LOGGER.debug("Playground detail lookup failed for %s: %s", cycle_id, exc) - return {"error": str(exc), "cycle_id": cycle_id} - if cycle is None: - return {"error": "not_found", "cycle_id": cycle_id} - return simulate_cycle_detail( - cycle, base_config, settings_override, store, options, price - ) - - def build_cycle_detail_sim_by_id( store: Any, cycle_id: str, @@ -660,8 +678,6 @@ def build_cycle_detail_sim_by_id( settings_override: dict[str, Any] | None, options: dict[str, Any] | None, price: float | None = None, - stress_tail: bool = False, - stress_idle_w: float | None = None, ) -> "_DetailSim | dict[str, Any]": """Look up a stored cycle by id and build a resumable :class:`_DetailSim`. @@ -681,10 +697,7 @@ def build_cycle_detail_sim_by_id( if cycle is None: return {"error": "not_found", "cycle_id": cycle_id} try: - return _DetailSim( - cycle, base_config, settings_override, store, options, price, - stress_tail=stress_tail, stress_idle_w=stress_idle_w, - ) + return _DetailSim(cycle, base_config, settings_override, store, options, price) except Exception as exc: # pylint: disable=broad-exception-caught _LOGGER.debug("Playground detail sim build failed for %s: %s", cycle_id, exc) return {"error": str(exc), "cycle_id": cycle_id} @@ -699,8 +712,6 @@ def _simulate_cycle_detail_inner( price: float | None, compute_series: bool = True, prebuilt: tuple[Any, Any, Any, Any] | None = None, - stress_tail: bool = False, - stress_idle_w: float | None = None, ) -> dict[str, Any]: """One-shot faithful replay: build the resumable sim and run it to completion. @@ -708,33 +719,30 @@ def _simulate_cycle_detail_inner( :class:`_DetailSim` and calls ``step``/``run_tail``/``finalize`` across many small executor jobs so the event loop breathes on very long cycles (issue #311). Because both paths drive the identical object in the identical order, - the timeline is byte-for-byte the same (golden test in test_playground_detail). + the timeline is byte-for-byte the same (tests/test_playground_chunked_parity.py). """ sim = _DetailSim( cycle, base_config, settings_override, store, options, price, compute_series, prebuilt, - stress_tail=stress_tail, stress_idle_w=stress_idle_w, ) if not sim.ready: return sim.empty_payload() sim.step(0, sim.n_readings) - if sim.stress_tail: - sim.run_stress_tail() - else: - sim.run_tail() + sim.run_tail() return sim.finalize() class _DetailSim: """Resumable single-cycle Playground "Simulate" replay. - Drives the REAL :class:`CycleDetector` + real Stage 1-4 matcher over the - cycle's own trace and, at production's 5s cadence, calls the SAME - :mod:`progress` and :mod:`notification_rules` functions the live integration - uses - so the returned timeline is byte-for-byte what would happen live. No - detection/progress/notification math is implemented here; this only - orchestrates the shared code. Read-only: nothing is persisted and no - notifications are sent. + Drives the REAL :class:`CycleDetector` + the real matcher + (``ProfileStore.async_match_profile``, via :class:`_SimStore`) over the + cycle's own trace, applies each match with the manager's own rules + (:mod:`match_rules`: switching, verified pause, confident-mismatch revoke) + and calls the SAME :mod:`progress` and :mod:`notification_rules` functions + the live integration uses. No detection/matching/progress/notification math + is implemented here; this only orchestrates the shared code. Read-only: + nothing is persisted and no notifications are sent. The replay is split into :meth:`step` (a slice of the real readings), :meth:`run_tail` (the synthetic quiet tail + flush) and :meth:`finalize` @@ -752,8 +760,6 @@ class _DetailSim: price: float | None, compute_series: bool = True, prebuilt: tuple[Any, Any, Any, Any] | None = None, - stress_tail: bool = False, - stress_idle_w: float | None = None, ) -> None: self.cycle = cycle self.store = store @@ -776,6 +782,18 @@ class _DetailSim: self.label = _cycle_label(cycle) self.readings, _points, self.base = _readings_from_cycle(cycle) self.stored_duration = _safe_float(cycle.get("duration")) + # When the appliance actually ran, for the Optimize end-timing objectives + # (audit PLAYGROUND-08): first/last reading above the LIVE stop threshold, + # never the override's, so a swept threshold cannot move its own yardstick. + # Same definition as devtools/end_gate_eval.py (`_active_span`). + _truth_stop = float(getattr(base_config, "stop_threshold_w", 0.0) or 0.0) + _active = [ + (ts - self.base).total_seconds() for ts, p in self.readings if p > _truth_stop + ] + self.active_end_s: float | None = _active[-1] if _active else None + self.active_span_s: float | None = ( + _active[-1] - _active[0] if len(_active) >= 2 else None + ) self.outcome: dict[str, Any] = { "detected": False, @@ -790,22 +808,25 @@ class _DetailSim: "overrun_ratio": None, "projected_energy_wh": None, "projected_cost": None, - "stress": None, + # Would the manager auto-label the (primary) finished cycle, as which + # profile, and why not: match_rules.cycle_end_label_verdict reasons + # (ok / no_winner / below_floor / ambiguous / margin / unknown_profile), + # plus no_cycle / no_store / error from the replay itself. + "would_label": False, + "label_profile": None, + "label_reason": "no_cycle", + "end_offset_s": None, } - self.stress_tail = stress_tail - self.stress_idle_override = stress_idle_w - self._stress_outcome: dict[str, Any] | None = None - if prebuilt is not None: - snapshots, match_config, group_members, member_snaps = prebuilt - else: - snapshots, match_config, group_members, member_snaps = _build_match_snapshots(store) + if prebuilt is None: + prebuilt = _build_match_snapshots(store) # Overlay any matcher-knob overrides. Because history/sweep run through this # same class, a swept matching value flows in via settings_override too; # applying to a copy keeps the shared prebuilt match_config untouched. - self.snapshots = snapshots - self.match_config = apply_match_overrides(match_config, settings_override) - self.group_members = group_members - self.member_snaps = member_snaps + self.match_config = apply_match_overrides(prebuilt[1], settings_override) + # The live matcher and alignment check, run inline (see _SimStore). + self.view: _SimStore | None = ( + _SimStore(store, self.match_config, prebuilt) if store is not None else None + ) self.ready = len(self.readings) >= 5 # Per-sim end-expectation cache, threaded through the shared progress helpers @@ -820,17 +841,30 @@ class _DetailSim: "name": None, "conf": 0.0, "ambiguous": False, "expected": 0.0, } self.last_logged = {"kind": None, "name": None} - # Persistence-gated commit mirroring the manager: a candidate must be top-1 - # for `match_persistence` consecutive matches (and not ambiguous) before it - # is committed, and the committed match is HELD (a one-off wobble doesn't - # switch it). The detector still receives the raw top-1 (detection - # unchanged); only the reported series/events use the committed match, so the - # Playground shows what the live integration would show - not raw churn. - self.match_persistence = max(1, int( - self.options.get(CONF_MATCH_PERSISTENCE, DEFAULT_MATCH_PERSISTENCE) - )) - self.commit_state: dict[str, Any] = {"candidate": None, "count": 0, "name": None} - self.smoothed = {"v": 0.0} + # The manager's switching state and the options its rules read, resolved + # exactly as `manager._load_runtime_options` resolves them (audit + # PLAYGROUND-03): the reported program is the one live would display. + self.switch = match_rules.SwitchState() + self.match_persistence = option_int( + self.options.get(CONF_MATCH_PERSISTENCE, DEFAULT_MATCH_PERSISTENCE), + DEFAULT_MATCH_PERSISTENCE, + minimum=1, + ) + self.unmatch_threshold = self.options.get( + CONF_PROFILE_UNMATCH_THRESHOLD, DEFAULT_PROFILE_UNMATCH_THRESHOLD + ) + self.learning_floor = float( + option_float( + self.options.get(CONF_LEARNING_CONFIDENCE, DEFAULT_LEARNING_CONFIDENCE), + DEFAULT_LEARNING_CONFIDENCE, + ) + or 0.0 + ) + # The last live MatchResult (manager._last_match_result) and, per finished + # cycle, what the manager held when it ended (its cycle-end inputs). + self._last_result: Any = None + self.cycle_ends: list[dict[str, Any]] = [] + self.smoothed: dict[str, Any] = {"v": 0.0, "program": None} self.flags = {"detected": False, "pre_complete": False, "start": False} # --- notification config (decisions reuse notification_rules) --- @@ -848,13 +882,49 @@ class _DetailSim: self.last_sample_t = -1e9 self._aborted = False + # Watchdog cadence (item 390). Live injects a keepalive on this cadence + # while a cycle sits below the stop threshold and the plug is silent; the + # sim does the same inside a silent stretch of the trace (see step()). + try: + _wd = float( + {**self.options, **(settings_override or {})}.get( + CONF_WATCHDOG_INTERVAL, + resolve_watchdog_interval_default(self.device_type), + ) + ) + except (TypeError, ValueError, OverflowError): + _wd = float(resolve_watchdog_interval_default(self.device_type)) + self.watchdog_s = _wd if math.isfinite(_wd) and _wd > 0 else 0.0 + self._last_real: tuple[datetime, float] | None = None if self.ready: self.detector = CycleDetector( self.config, self._on_state_change, self._on_cycle_end, profile_matcher=self._matcher, device_name="playground-detail", + terminal_drop_provider=self._terminal_drop_provider( + {**self.options, **(settings_override or {})} + ), ) + def _terminal_drop_provider( + self, options: dict[str, Any] + ) -> Callable[[list[tuple[float, float]], float], bool] | None: + """``manager._terminal_drop_provider`` for this replay; None where live + would not run it (``detector_config.terminal_drop_enabled``, audit ML-08).""" + if not terminal_drop_enabled(self.device_type, options): + return None + stop = float(getattr(self.config, "stop_threshold_w", 0.0) or 0.0) + baseline = _sim_terminal_drop_baseline(self.store, stop) + device_type = self.device_type + + def _provider(points: list[tuple[float, float]], _expected: float) -> bool: + # The live gate on the sim's own detector (built after this closure). + if not terminal_drop_may_fire(device_type, options, self.detector): + return False + return terminal_drop_fires(points, baseline, stop) + + return _provider + @property def n_readings(self) -> int: return len(self.readings) @@ -864,6 +934,9 @@ class _DetailSim: "cycle_id": self.cycle.get("id"), "label": self.label, "duration_s": self.stored_duration, + "start_time": self.cycle.get("start_time"), + "active_end_s": _safe_float(self.active_end_s), + "active_span_s": _safe_float(self.active_span_s), "config_summary": _sim_config_summary(self.config), "series": [], "events": [], @@ -885,18 +958,19 @@ class _DetailSim: ) def _held(self, offset: float) -> bool: + # Quiet hours are local clock hours and replay timestamps are UTC, so + # `.hour` on the raw stamp held the wrong hours (audit PROGRESS-13). return notif_rules.in_quiet_hours( - self.quiet_bounds, self.base + timedelta(seconds=offset) + self.quiet_bounds, dt_util.as_local(self.base + timedelta(seconds=offset)) ) def _on_state_change(self, old_state: str, new_state: str) -> None: self._emit("state", f"{old_state}->{new_state}") - # A new cycle is starting after a previous one ended: clear the inherited - # match-persistence streak so this cycle matches fresh (see _on_cycle_end). - # PAUSED->RUNNING resumes don't arm pending_reset, so they are unaffected. - if new_state == STATE_RUNNING and self.flags.get("pending_reset"): - self.flags["pending_reset"] = False - self.commit_state.update(candidate=None, count=0, name=None) + # A new cycle: the switching state starts fresh, exactly when + # `manager._on_state_change` resets it (RUNNING from OFF / STARTING / + # UNKNOWN). PAUSED/ENDING -> RUNNING is a resume and keeps it. + if new_state == STATE_RUNNING and old_state in (STATE_OFF, STATE_STARTING, STATE_UNKNOWN): + self.switch.start_cycle() self.last_match.update(name=None, conf=0.0, expected=0.0, ambiguous=False) self.last_logged.update(kind=None, name=None) if ( @@ -916,213 +990,245 @@ class _DetailSim: self.captured.append(cycle_data) reason = cycle_data.get("termination_reason") self._emit("finished", f"reason={reason} status={cycle_data.get('status')}", "info") - # Arm a match-state reset for the NEXT cycle. We reset at the next cycle's - # start (not here) so the final cycle's committed match survives to be read - # into `outcome` after the loop; a second sub-cycle then starts a fresh - # match-persistence streak, mirroring the live manager (per-cycle reset). - self.flags["pending_reset"] = True + # What `manager._async_process_cycle_end` freezes before its first await: + # the displayed program and the last live match (its label inputs). + st = self.switch + program = st.current_program if match_rules.program_is_committed(st.current_program) else None + self.cycle_ends.append({ + "program": program, + "confidence": st.last_confidence if program else None, + "expected": st.matched_duration if program else None, + "ambiguous": bool(self.last_match.get("ambiguous")), + "live_result": self._last_result, + # Replay offset at which WashData said "done" (end lag, PLAYGROUND-08). + "t": self.cursor["t"], + }) + # ...and the terminal reset at its tail: the next cycle starts from "off" + # with no live result, as live does once the cycle has been processed. + st.current_program = "off" + st.matched_duration = None + self._last_result = None - 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) - 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). + def _has_real_profiles(self) -> bool: + """The gate `manager._async_perform_combined_matching` checks first.""" + if self.store is None: + return False 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() + return bool(self.store.has_real_profiles) 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) + return False + + def _verify_alignment( + self, profile_name: str, det_readings: list[tuple[datetime, float]] + ) -> tuple[bool, float]: + """The live alignment check; a failure counts as unconfirmed, as live.""" try: - candidates = analysis.compute_matches_worker( - powers, duration, self.snapshots, self.match_config - ) - except Exception as exc: # pylint: disable=broad-exception-caught - _LOGGER.debug("Playground detail match failed: %s", exc) - candidates = [] - if not candidates: + assert self.view is not None + formatted = power_data_to_offsets(det_readings) # type: ignore[arg-type] + is_confirmed, mapped_time, _ = self.view.verify_alignment(profile_name, formatted) + return bool(is_confirmed), mapped_time + except Exception: # pylint: disable=broad-exception-caught + _LOGGER.debug("Playground alignment check failed", exc_info=True) + return False, 0.0 + + def _get_profile(self, name: str) -> Any: + return self.store.get_profile(name) + + def _matcher(self, det_readings: list[tuple[datetime, float]]) -> Any: + """One live match tick: ``manager._async_do_perform_matching`` in sequence. + + The real matcher (``ProfileStore.async_match_profile`` via + :class:`_SimStore`), then the manager's own rules from + :mod:`match_rules`: switching, the envelope verified pause and its + releases, the consistency override (which is also how a confident mismatch + drops the program). Like the manager it pushes the + verified pause and the commit flag to the detector, and hands it the + tick's name - which after a divergence revert is "detecting...", as live - + not the displayed program. Returns the match context the detector applies, + or None where live would not have matched at all (no real profiles). + + The alignment check runs synchronously here. Live awaits it, and the + matcher, while readings keep arriving; the replay applies the tick at the + reading that triggered it. + """ + if not det_readings or not self._has_real_profiles(): + return None + assert self.view is not None + det = self.detector + current_duration = (det_readings[-1][0] - det_readings[0][0]).total_seconds() + stop_w = float(det.config.stop_threshold_w) + result = self.view.match( + det_readings, current_duration, in_progress=True, stop_threshold_w=stop_w + ) + self._last_result = result + + st = self.switch + prev_program = st.current_program + tick = match_rules.begin_tick(st, result, self.match_persistence, current_duration) + match_rules.decide_switch( + st, tick, result, self.match_persistence, self.unmatch_threshold + ) + match_rules.record_scores(st, result.candidates) + + current_matched = det.matched_profile + prev_verified = getattr(det, "_verified_pause", False) + current_power = det_readings[-1][1] + # The detector mirrors the manager's user pause (`set_user_paused`); a replay + # never pauses, but a harness can (`end_gate_eval --user-pause`, item 514). + user_paused = getattr(det, "_user_paused", False) is True + alignment: tuple[bool, float] | None = None + if match_rules.needs_alignment_check(current_matched, current_power, stop_w, user_paused): + alignment = self._verify_alignment(current_matched, det_readings) + pause = match_rules.decide_alignment_pause( + verified_pause=prev_verified, + current_matched=current_matched, + alignment=alignment, + envelope_span=self.view.envelope_time_span, + ) + pause = match_rules.decide_pause_release( + verified_pause=pause.verified_pause, + current_matched=current_matched, + current_power=current_power, + stop_threshold_w=getattr(det.config, "stop_threshold_w", 5.0), + user_paused=user_paused, + expected_duration=det.expected_duration_seconds, + current_duration=current_duration, + time_below=getattr( + det, "_time_below_threshold_gapfree", getattr(det, "_time_below_threshold", 0.0) + ), + program=st.current_program, + ) + verified = pause.verified_pause + match_rules.consistency_override(st, tick, result, verified, self._get_profile) + # The manager's ENDING pause hold (register item 469b). + verified = match_rules.hold_in_ending( + ending=det.state == STATE_ENDING, + is_ambiguous=bool(result.is_ambiguous), + current_matched=current_matched, + prev_verified=prev_verified, + verified_pause=verified, + user_paused=user_paused, + ).verified_pause + det.set_verified_pause(verified) + det.set_match_committed(match_rules.program_is_committed(st.current_program)) + self._report_tick(prev_program, bool(prev_verified), bool(verified), result) + return self._match_context(tick, tick.phase_name, result) + + def _report_tick( + self, prev_program: Any, prev_verified: bool, verified: bool, result: Any + ) -> None: + """Events + the reported match after a tick: what live would display.""" + st = self.switch + program = st.current_program if match_rules.program_is_committed(st.current_program) else None + before = prev_program if match_rules.program_is_committed(prev_program) else None + if program != before: + if program and not before: + self._emit("match_commit", f"{program} (conf={float(st.last_confidence):.2f})") + elif program: + self._emit( + "match_changed", + f"{before} -> {program} (conf={float(st.last_confidence):.2f})", + ) + else: + self._emit("match_reverted", f"{before} -> detecting") + self.last_logged.update(kind="matched" if program else "reverted", name=program) + if result.is_confident_mismatch: if self.last_logged["kind"] != "unmatched": self._emit("unmatched", "no candidate") self.last_logged["kind"] = "unmatched" - # 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). - # Captured before the collapse for element 12, exactly as - # async_match_profile does: the collapse drops every sibling but the - # best, and a longer one leaving the list lowers the ENDING gate's bar. - pre_collapse_candidates = list(candidates) - 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_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 - # detector AND to the #364 prefix guard below. - resolved = dict(candidates[0], name=member_name) - if member_dur: - resolved["profile_duration"] = float(member_dur) - candidates[0] = resolved - except Exception: # pylint: disable=broad-exception-caught - 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) - - # Persistence-gated commit (mirror of the manager's core rule), extracted - # into decide_commit() for unit-testability. - commit_event = decide_commit( - raw_name, is_ambiguous, self.commit_state, self.match_persistence - ) - if commit_event: - self._emit(commit_event, f"{raw_name} (conf={raw_conf:.2f})") - self.last_logged.update(kind="matched", name=raw_name) - elif is_ambiguous and not self.commit_state["name"]: - # Ambiguous before any commit: stay 'detecting', surface it once. - if self.last_logged["kind"] != "ambiguous" or self.last_logged["name"] != raw_name: - runner = candidates[1].get("name") if len(candidates) > 1 else None - self._emit("match_ambiguous", f"{raw_name} vs {runner} (margin={margin:.3f})", "warn") - self.last_logged.update(kind="ambiguous", name=raw_name) - - # Reported state = the COMMITTED match (held); its confidence/expected are - # that profile's own values this interval (looked up among the candidates). - cname = self.commit_state["name"] - if cname: - cc = next((c for c in candidates if c.get("name") == cname), None) - self.last_match.update( - name=cname, - conf=float(cc.get("score") or 0.0) if cc else (self.last_match.get("conf") or 0.0), - expected=float(cc.get("profile_duration") or 0.0) if cc else (self.last_match.get("expected") or 0.0), - ambiguous=False, - ) - else: - self.last_match.update(name=None, conf=0.0, expected=0.0, ambiguous=bool(is_ambiguous)) - - # The DETECTOR still receives the RAW top-1, so detection / smart-termination - # behaviour is byte-identical to before this reporting change. - # Elements 7-9 (#364): without them the prefix-landscape and power-plausibility - # guards were never exercised in a simulation, so the exact failure the - # Playground exists to reproduce was invisible here. - full_shape_hit, prefix_fit_hit = _match_prefix_ambiguity(candidates, raw_expected) - # Guard the store call like iter_evidence_cycles above: on an older store or a - # partial test double without profile_tail_power the AttributeError would - # 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) and the standby-band arm on top of it (register - # item 351). This used to be a hand-copy of the manager's version, and - # the two had already drifted in their error handling; it is now the - # SAME function, which is the only way this replay can be guaranteed - # byte-identical to live on element 10. `end_gate_eval.py` drives the - # detector through here, so an arm present in only one of them is - # invisible to every measurement made with that harness - exactly how - # item 352 first measured as a no-op. - det = getattr(self, "detector", None) - if det is not None: - terminal_high = terminal_high_for_guards( - self.store, - det.config, - getattr(det, "_cycle_max_power", 0.0), - raw_name, + elif result.is_ambiguous and not program and result.best_profile: + # Ambiguous before any commit: stay 'detecting', surface it once per name. + raw = result.best_profile + if self.last_logged["kind"] != "ambiguous" or self.last_logged["name"] != raw: + cands = result.candidates or [] + runner = cands[1].get("name") if len(cands) > 1 else None + self._emit( + "match_ambiguous", + f"{raw} vs {runner} (margin={float(result.ambiguity_margin):.3f})", + "warn", ) - # Element 11 (item 297) and element 12 (item 330). Both were missing, so - # the sim's detector ran without the tail bound Smart Termination uses and - # without the longest-candidate bar the ENDING gate uses - i.e. the replay - # was NOT byte-identical to live on either, which is the sim's whole - # contract. `end_gate_eval.py` drives the detector through here, so a - # missing element silently measures the wrong gate. - terminal_quiet = None - if self.store is not None and raw_name: - try: - terminal_quiet = self.store.profile_terminal_quiet_seconds(raw_name) - except Exception: # pylint: disable=broad-exception-caught - terminal_quiet = None - return ( - raw_name, - raw_conf, - raw_expected, - None, - False, - bool(is_ambiguous), - bool(full_shape_hit or prefix_fit_hit), - bool(full_shape_hit), - tail_power, - terminal_high, - terminal_quiet, - # The FULL population, matching live: `async_match_profile` computes - # `MatchResult.longest_candidate_duration_s` from the candidates as - # they stood BEFORE the group collapse and before the [:5] - # truncation. An earlier version of this line used `candidates[:5]` - # to match what live *then* did - and live was the thing that was - # wrong, twice. - longest_candidate_duration(pre_collapse_candidates), + self.last_logged.update(kind="ambiguous", name=raw) + if verified != prev_verified: + self._emit("verified_pause", "engaged" if verified else "released") + self.last_match.update( + name=program, + conf=float(st.last_confidence or 0.0) if program else 0.0, + expected=float(st.matched_duration or 0.0) if program else 0.0, + # The raw tick's flag, as `manager._last_match_ambiguous` holds it. + ambiguous=bool(result.is_ambiguous), ) + def _match_context(self, tick: Any, phase_name: str | None, result: Any) -> MatchContext: + """The named context the manager builds for ``update_match`` (DETECT-15).""" + store = self.store + det = self.detector + # The manager's own rule: a divergence revert revokes the detector's match. + name, revoke = match_rules.detector_match(tick, result) + + def _ask(fn: Callable[[], Any]) -> Any: + # Guarded like the rest of the sim: a partial test double without one + # of these methods must not turn every match into "unmatched". + try: + return fn() + except Exception: # pylint: disable=broad-exception-caught + return None + + stop_w = float(det.config.stop_threshold_w) + return MatchContext( + profile_name=name, + confidence=tick.confidence, + expected_duration=tick.matched_duration, + phase_name=phase_name, + is_confident_mismatch=revoke, + is_ambiguous=result.is_ambiguous, + is_prefix_ambiguous_full_shape=result.is_prefix_ambiguous_full_shape, + tail_power=_ask(lambda: store.profile_tail_power(name)) if name else None, + # One implementation with the manager's `_terminal_high_for_guards`. + terminal_high=_ask(lambda: terminal_high_for_guards( + store, det.config, getattr(det, "_cycle_max_power", 0.0), name + )), + terminal_quiet_s=( + _ask(lambda: store.profile_terminal_quiet_seconds(name)) if name else None + ), + longest_candidate_s=float(getattr(result, "longest_candidate_duration_s", 0.0) or 0.0), + trusted_min_s=( + _ask(lambda: store.profile_trusted_min_duration(name)) if name else None + ), + pause_catalogue=( + _ask(lambda: store.profile_pause_catalogue(name, stop_w)) if name else None + ), + # #452, as the manager supplies it (the stall display and its + # standby-band hold). + stall_catalogue=( + (lambda: _ask(lambda: store.profile_pause_catalogue( + name, standby_near_stop_ceiling(stop_w) + ))) if name else None + ), + ) + + def _label_decision(self, cycle_data: dict[str, Any], live_result: Any) -> tuple[str | None, str]: + """Would the manager auto-label this finished cycle? ``(profile, reason)``. + + The manager's cycle-end path: ONE complete-cycle match on the stored trace + (``match_rules.final_match_input`` + ``async_match_profile``, not in + progress), falling back to the last live match when the trace is too short, + then ``match_rules.cycle_end_label_verdict`` at the learning floor. + """ + if self.view is None: + return None, "no_store" + try: + final = None + final_input = match_rules.final_match_input(cycle_data) + if final_input is not None: + final = self.view.match(final_input[0], final_input[1]) + match_result = final if final is not None else live_result + return match_rules.cycle_end_label_verdict( + match_result, self.learning_floor, self.store.get_profiles() + ) + except Exception: # pylint: disable=broad-exception-caught + _LOGGER.debug("Playground label decision failed", exc_info=True) + return None, "error" + def _price_at(self, offset_s: float) -> float | None: """The tariff in force at a replay offset, or the flat price with no timeline. @@ -1188,35 +1294,27 @@ class _DetailSim: "confidence": round(self.last_match["conf"], 3) if self.last_match["name"] else None, "matched_profile": self.last_match["name"], } + if getattr(self.detector, "stalled", False) is True: + pt["stalled"] = True # #452: shown as paused / Stalled live matched_dur = float(self.last_match["expected"] or 0.0) program = self.last_match["name"] if state not in _DEAD_STATES and program and matched_dur > 0: + # Item 514, as live: progress reads programme time (a halt is not + # progress); the energy and the cost below read the whole trace. + prog_trace = self.detector.progress_trace(ts) + prog_t = self.detector.progress_elapsed_s(offset, ts) phase_result = None - if len(trace) >= 10 and program != "detecting...": + if len(prog_trace) >= 10 and program != "detecting...": phase_result = progress_mod.estimate_phase_progress( - self.store, trace, offset, program, + self.store, prog_trace, prog_t, program, quiet_threshold_w=float( getattr(self.detector.config, "stop_threshold_w", 0.0) or 0.0 ), ) - ml_pct = progress_mod.ml_progress_percent( - self.store, self.options, matched_dur, trace, program, self._end_exp_fn - ) - # Opt-in phase-resolved ETA blend - identical gating + call as the live - # manager, so the Playground stays a faithful mirror of the estimator. - phase_remaining_s = None - if ( - self.store is not None - and len(trace) >= 10 - and program not in ("detecting...", "off", None) - and phase_matching_enabled(self.options, self.device_type) - ): - pr = self.store.phase_remaining(trace, self.device_type, program) - if pr is not None: - phase_remaining_s = pr.get("remaining_s") result = progress_mod.compute_progress( - self.device_type, matched_dur, offset, self.smoothed["v"], phase_result, ml_pct, - phase_remaining_s=phase_remaining_s, + self.device_type, matched_dur, prog_t, + progress_mod.ema_seed(self.smoothed["v"], self.smoothed["program"], program), + phase_result, # Same time-scaled smoothing as live: the sim steps the estimator # at its own throttle, so without this the replay would smooth # over 30 s steps as if they were the manager's 5 s ones. @@ -1226,10 +1324,11 @@ class _DetailSim: ) if result is not None: self.smoothed["v"] = result.smoothed + self.smoothed["program"] = program pt["progress"] = round(result.progress, 1) pt["remaining_s"] = round(result.remaining, 0) pt["phase"] = progress_mod.current_phase( - self.store, state, program, result.progress + self.store, state, program, result.progress, matched_dur ) sim_cost = self._cost_so_far(trace) wh, cost = progress_mod.projected_energy( @@ -1260,8 +1359,11 @@ class _DetailSim: return try: for ts, power in self.readings[i0:i1]: + self._watchdog_keepalives(ts) self.cursor["t"] = (ts - self.base).total_seconds() self.detector.process_reading(power, ts) + self._note_stall() + self._last_real = (ts, power) self._sample(ts) except Exception as exc: # pylint: disable=broad-exception-caught self._aborted = True @@ -1269,54 +1371,82 @@ class _DetailSim: "Playground detail replay failed for %s: %s", self.cycle.get("id"), exc ) - def _derive_idle_level(self) -> tuple[float, float]: - """Derive (idle_w, fluct_w) from the standby floor of the real cycle tail. + def _note_stall(self) -> None: + """A ``stalled`` event when the detector starts showing a stall (#452). - idle_w = p7 of readings in the last PLAYGROUND_STRESS_TRAILING_WINDOW_S; - this is the between-burst standby floor, not a contaminated mean. - fluct_w = std-dev of the low band (readings ≤ idle_w × 1.5); falls back - to ±PLAYGROUND_STRESS_FLUCT_FALLBACK_FRAC when the tail is flat. + The same display rule live shows (``CycleDetector.stalled``) and the + moment the manager fires ``ha_washdata_cycle_stalled``: real readings only. """ - if not self.readings: - return 3.0, 0.36 - cutoff = self.readings[-1][0] - timedelta(seconds=PLAYGROUND_STRESS_TRAILING_WINDOW_S) - window = [p for ts, p in self.readings if ts >= cutoff] - if not window: - window = [self.readings[-1][1]] - sorted_w = sorted(window) - p7_idx = max(0, int(len(sorted_w) * PLAYGROUND_STRESS_FLOOR_PERCENTILE)) - idle_w = float(sorted_w[p7_idx]) - low_band = [p for p in window if p <= idle_w * 1.5] - if len(low_band) >= 2: - mean_lb = sum(low_band) / len(low_band) - variance = sum((p - mean_lb) ** 2 for p in low_band) / len(low_band) - fluct_w = variance ** 0.5 - if fluct_w < 0.01: - fluct_w = idle_w * PLAYGROUND_STRESS_FLUCT_FALLBACK_FRAC - else: - fluct_w = idle_w * PLAYGROUND_STRESS_FLUCT_FALLBACK_FRAC - return max(0.0, idle_w), max(0.01, fluct_w) + stalled = getattr(self.detector, "stalled", False) is True + if stalled == getattr(self, "_stall_shown", False): + return + self._stall_shown = stalled + if stalled: + info = self.detector.stall_info() or {} + self._emit("stalled", f"stalled at {info.get('plateau_w')} W (display only)", "warn") + + def _watchdog_keepalives(self, until: datetime) -> None: + """Inject the keepalives live would have injected before ``until`` (item 390). + + Mirrors the low-power branch of ``manager._watchdog_check_stuck_cycle``: + while the detector waits below the stop threshold and the plug has been + silent for more than ``watchdog_interval``, each tick feeds the sensor's + last value as an observed synthetic reading. Without it a silent stretch + reached the detector as one interval at the next real reading - after the + power had already come back - so the end gates were never evaluated + inside it, and a soak that live 0.5.7 ends on (item 290 credits the + silence) replayed as one cycle. Ticks sit half an interval into each + period, the mean phase of a free-running timer. Traces recorded on 0.5.7+ + already hold these readings, so for them this is a no-op. The watchdog's + staleness force-end and ghost/zombie branches are not emulated. + """ + last = self._last_real + step = self.watchdog_s + if last is None or step <= 0: + return + prev_ts, prev_w = last + k = 1 + while k <= _MAX_KEEPALIVES_PER_GAP: + ts = prev_ts + timedelta(seconds=step * (k + 0.5)) + if ts >= until or not self.detector.is_waiting_low_power(): + return + self.cursor["t"] = (ts - self.base).total_seconds() + self.detector.process_reading(prev_w, ts, synthetic=True, observed=True) + self._sample(ts) + k += 1 def run_tail(self) -> None: - """Synthetic quiet tail so a natural end can fire.""" + """Synthetic quiet tail so a natural end can fire. + + While the detector holds an envelope-verified pause every ENDING finalize is + deferred, and live keeps waiting - its watchdog extends its own silence + limit to ``DEFAULT_MAX_DEFERRAL_SECONDS`` + 30 min for the same reason. So + the tail runs on for as long as one is held (bounded by that same limit), + then the usual span, instead of force-ending a cycle the release rules + (95% of the envelope, #375) would have ended a few minutes later. + """ if self._aborted or not self.ready: return try: last_ts = self.readings[-1][0] - tail_span = max( - float(self.config.off_delay or 0.0), float(self.config.min_off_gap or 0.0) - ) * 1.5 + 300.0 + tail_span = _tail_span_s(self.config) step = 30.0 n_steps = min(int(tail_span / step) + 1, 400) - for i in range(1, n_steps + 1): + max_steps = n_steps + int(_TAIL_VERIFIED_PAUSE_CAP_S / step) + budget = n_steps + i = 0 + while i < budget: + i += 1 ts = last_ts + timedelta(seconds=step * i) self.cursor["t"] = (ts - self.base).total_seconds() self.detector.process_reading(0.0, ts) self._sample(ts) if self.detector.state in (STATE_OFF, STATE_FINISHED) and self.captured: break + if getattr(self.detector, "_verified_pause", False): + budget = min(max_steps, max(budget, i + n_steps)) if not self.captured and self.detector.state != STATE_OFF: - flush_ts = last_ts + timedelta(seconds=step * (n_steps + 2)) + flush_ts = last_ts + timedelta(seconds=step * (i + 2)) self.cursor["t"] = (flush_ts - self.base).total_seconds() self.detector.force_end(flush_ts) except Exception as exc: # pylint: disable=broad-exception-caught @@ -1324,128 +1454,36 @@ class _DetailSim: "Playground detail replay failed for %s: %s", self.cycle.get("id"), exc ) - def run_stress_tail(self) -> None: - """Synthetic idle continuation for the idle termination test. - - Replaces the quiet tail when ``stress_tail=True``. Never raises. - - Two phases keep CPU near zero while preserving detector fidelity: - - * **Dense pre-fill** (DENSE_DURATION_S at DENSE_STEP_S): populates - ``_power_readings`` so ``_is_standby_band_stuck`` (10-min plateau check) - and ``_window_has_outage_gap`` can fire correctly. - * **Sparse main** (SPARSE_STEP_S steps): ``_time_below_threshold += dt`` - accumulates correctly for large dt; the 8h hard cap fires normally. - - ``_sample()`` is called on every step so the series carries progress / - remaining_s / phase through the overrun, identical to the live estimator. - """ - if self._aborted or not self.ready: - return - try: - idle_w, fluct_w = self._derive_idle_level() - override_applied = False - if self.stress_idle_override is not None: - # The schema accepts any float. Reject non-finite (nan/inf) by keeping the - # auto-derived floor, and clamp to [0, MAX] so a negative can't surface as a - # nonsensical "-Xw" draw and a huge/inf value can't corrupt the synthetic - # power samples (NaN/inf detector math). fluct_w stays from the call above. - ov = float(self.stress_idle_override) - if math.isfinite(ov): - idle_w = max(0.0, min(ov, PLAYGROUND_STRESS_MAX_IDLE_W)) - override_applied = True - - synthetic_from_s = self.cursor["t"] - stop_thresh = float(getattr(self.config, "stop_threshold_w", 2.0)) - idle_above = idle_w >= stop_thresh - - seed = sum(ord(c) for c in (self.cycle.get("id") or "pg")) & 0xFFFF - - self._emit( - "stress_tail_start", - f"idle ~{idle_w:.1f}W fluct={fluct_w:.2f}W above_stop_thresh={idle_above}", - ) - - last_ts = self.readings[-1][0] - captured_before = len(self.captured) - - n_dense = int(PLAYGROUND_STRESS_DENSE_DURATION_S / PLAYGROUND_STRESS_DENSE_STEP_S) - dense_end_ts = last_ts - for i in range(1, n_dense + 1): - ts = last_ts + timedelta(seconds=PLAYGROUND_STRESS_DENSE_STEP_S * i) - power = max(0.0, idle_w + fluct_w * math.sin(seed + i * 1.1)) - self.cursor["t"] = (ts - self.base).total_seconds() - self.detector.process_reading(power, ts) - self._sample(ts) - dense_end_ts = ts - if len(self.captured) > captured_before: - break - - if len(self.captured) == captured_before: - for i in range(1, PLAYGROUND_STRESS_MAX_SPARSE_STEPS + 1): - ts = dense_end_ts + timedelta(seconds=PLAYGROUND_STRESS_SPARSE_STEP_S * i) - power = max(0.0, idle_w + fluct_w * math.sin(seed + n_dense + i * 1.1)) - self.cursor["t"] = (ts - self.base).total_seconds() - self.detector.process_reading(power, ts) - self._sample(ts) - if len(self.captured) > captured_before: - break - - if len(self.captured) == captured_before: - flush_ts = dense_end_ts + timedelta( - seconds=PLAYGROUND_STRESS_SPARSE_STEP_S * (PLAYGROUND_STRESS_MAX_SPARSE_STEPS + 2) - ) - self.cursor["t"] = (flush_ts - self.base).total_seconds() - self.detector.force_end(flush_ts) - # Record a series sample at the forced-stop timestamp so the plotted tail - # reaches the reported elapsed time (otherwise the last point sits up to two - # sparse steps short of terminated_after_s). - self._sample(flush_ts) - - terminated = len(self.captured) > captured_before - terminated_after_s: float | None = None - term_reason: Any = None - hit_cap = False - if terminated: - primary = max( - self.captured[captured_before:], - key=lambda c: float(c.get("duration") or 0.0), - ) - term_reason = primary.get("termination_reason") - hit_cap = str(term_reason) == str(TerminationReason.FORCE_STOPPED) - terminated_after_s = self.cursor["t"] - synthetic_from_s - - self._stress_outcome = { - "enabled": True, - "idle_w": round(idle_w, 2), - "fluct_w": round(fluct_w, 3), - "manual_override": override_applied, - "synthetic_from_s": round(synthetic_from_s, 1), - "stop_threshold_w": round(stop_thresh, 2), - "idle_above_threshold": idle_above, - "terminated": terminated, - "terminated_after_s": ( - round(terminated_after_s, 1) if terminated_after_s is not None else None - ), - "termination_reason": str(term_reason) if term_reason is not None else None, - "hit_cap": hit_cap, - } - except Exception as exc: # pylint: disable=broad-exception-caught - _LOGGER.debug( - "Playground stress tail failed for %s: %s", self.cycle.get("id"), exc - ) - def finalize(self) -> dict[str, Any]: outcome = self.outcome - last_match = self.last_match + last_match = dict(self.last_match) # --- outcome --- if self.captured: - primary = max(self.captured, key=lambda c: float(c.get("duration") or 0.0)) + idx = max( + range(len(self.captured)), + key=lambda i: float(self.captured[i].get("duration") or 0.0), + ) + primary = self.captured[idx] outcome["detected"] = True outcome["detected_count"] = len(self.captured) outcome["termination_reason"] = primary.get("termination_reason") outcome["status"] = primary.get("status") outcome["final_duration_s"] = _safe_float(primary.get("duration")) + # The program the manager displayed when THAT cycle ended, not whatever + # a later sub-cycle left behind. + end = self.cycle_ends[idx] if idx < len(self.cycle_ends) else None + if end is not None: + outcome["end_offset_s"] = _safe_float(end.get("t")) + last_match.update( + name=end["program"], + conf=float(end["confidence"] or 0.0), + expected=float(end["expected"] or 0.0), + ambiguous=end["ambiguous"], + ) + label, reason = self._label_decision(primary, end["live_result"]) + outcome["would_label"] = label is not None + outcome["label_profile"] = label + outcome["label_reason"] = reason outcome["matched_profile"] = last_match["name"] outcome["confidence"] = ( round(float(last_match["conf"]), 3) if last_match["name"] else None @@ -1465,11 +1503,14 @@ class _DetailSim: # --- finish + milestone markers (reuse production predicates) --- if self.captured and self.finish_configured: - held = self._held(self.cursor["t"]) - self._emit( - "notify_held" if held else "notify_finish", - "finish notification" + (" (held: quiet hours)" if held else ""), - ) + # No finished push for an interrupted cycle (audit MANAGER-10); the + # milestone below does not depend on it, as live. + if notif_rules.cycle_end_is_finish(outcome["status"]): + held = self._held(self.cursor["t"]) + self._emit( + "notify_held" if held else "notify_finish", + "finish notification" + (" (held: quiet hours)" if held else ""), + ) try: prev_life = int(self.store.get_lifetime_cycle_count()) except Exception: # pylint: disable=broad-exception-caught @@ -1533,53 +1574,6 @@ class _DetailSim: alerts.append({"code": "underrun", "severity": "warn", "detail": f"Finished at {ratio:.0%} of typical duration."}) - # --- stress-tail verdict --- - st = self._stress_outcome - if st and st.get("enabled"): - outcome["stress"] = st - idle_w = st["idle_w"] - stop_thresh = st["stop_threshold_w"] - if st.get("terminated") and not st.get("hit_cap"): - after_s = float(st.get("terminated_after_s") or 0.0) - h, rem = divmod(int(after_s), 3600) - m = rem // 60 - reason = st.get("termination_reason") or "?" - alerts.append({ - "code": "stress_terminated", "severity": "info", - "detail_key": "msg.pg_stress_terminated_detail", - "detail_params": {"idle": f"{idle_w:.1f}", "h": h, "m": m, "reason": reason}, - "detail": ( - f"Settled to ~{idle_w:.1f}W idle -- cycle ended " - f"{h}h {m}m later via {reason}." - ), - }) - elif st.get("hit_cap"): - if st.get("idle_above_threshold"): - alerts.append({ - "code": "stress_above_threshold", "severity": "warn", - "detail_key": "msg.pg_stress_above_threshold_detail", - "detail_params": {"idle": f"{idle_w:.1f}", "stop": f"{stop_thresh:.1f}"}, - "detail": ( - f"Idle draw ~{idle_w:.1f}W is at or above the effective stop " - f"threshold ({stop_thresh:.1f}W) -- the cycle never registered " - f"as quiet. Raise stop_threshold_w to fix." - ), - }) - # Report the actual elapsed synthetic time (the cap is ~7h50m of synthetic - # tail plus the pre-tail cycle length, not a flat 8 h). - after_s = float(st.get("terminated_after_s") or 0.0) - h, rem = divmod(int(after_s), 3600) - m = rem // 60 - alerts.append({ - "code": "stress_hit_cap", "severity": "error", - "detail_key": "msg.pg_stress_hit_cap_detail", - "detail_params": {"h": h, "m": m, "idle": f"{idle_w:.1f}", "stop": f"{stop_thresh:.1f}"}, - "detail": ( - f"Cycle ran {h}h {m}m without stopping -- force-stopped by the safety cap. " - f"Idle draw {idle_w:.1f}W vs stop threshold {stop_thresh:.1f}W." - ), - }) - series = self.series if len(series) > MAX_SERIES_PER_CYCLE: # Thin evenly so the shape is preserved (first + last always kept). @@ -1592,6 +1586,9 @@ class _DetailSim: "cycle_id": self.cycle.get("id"), "label": self.label, "duration_s": self.stored_duration, + "start_time": self.cycle.get("start_time"), + "active_end_s": _safe_float(self.active_end_s), + "active_span_s": _safe_float(self.active_span_s), "config_summary": _sim_config_summary(self.config), "series": series, "events": self.events, @@ -1631,9 +1628,44 @@ def _sim_config_summary(config: CycleDetectorConfig) -> dict[str, Any]: # ─── Test-on-history rows + before/after diff ─────────────────────────────────── +# A detected end more than this before the appliance's last activity is an early +# end (devtools/end_gate_eval.py's `early_1min`): the cycle was cut short. +_EARLY_END_S = 60.0 +# A run whose longest piece covers less than this share of its active span did not +# survive as one cycle (end_gate_eval's split rule). +_SPLIT_SPAN_FRAC = 0.9 + + +def _end_timing(detail: dict[str, Any]) -> tuple[float | None, bool, bool]: + """``(end_lag_s, early_end, split)`` of one replay (audit PLAYGROUND-08). + + The lag is when WashData said "done" minus when the appliance last drew power, + the number ``devtools/end_gate_eval.py`` measures; the stored duration is + trimmed back to the last activity (item 297) and cannot show it. A run that + never finished counts as split: it did not survive as one cycle. + """ + o = detail.get("outcome", {}) or {} + if not o.get("detected"): + return None, False, True + end_t = o.get("end_offset_s") + active_end = detail.get("active_end_s") + lag = ( + round(float(end_t) - float(active_end), 1) + if end_t is not None and active_end is not None + else None + ) + span = detail.get("active_span_s") + final = o.get("final_duration_s") + split = int(o.get("detected_count") or 0) > 1 or bool( + span and final is not None and float(final) < _SPLIT_SPAN_FRAC * float(span) + ) + return lag, lag is not None and lag < -_EARLY_END_S, split + + def _detail_to_row(detail: dict[str, Any]) -> dict[str, Any]: """Compact per-cycle row for the Test-on-history table from a detail sim.""" o = detail.get("outcome", {}) + end_lag, early_end, split = _end_timing(detail) return { "cycle_id": detail.get("cycle_id"), "label": detail.get("label"), @@ -1650,7 +1682,16 @@ def _detail_to_row(detail: dict[str, Any]) -> dict[str, Any]: "stored_duration_s": detail.get("duration_s"), "expected_s": o.get("expected_s"), "overrun_ratio": o.get("overrun_ratio"), + "would_label": bool(o.get("would_label")), + "label_profile": o.get("label_profile"), + "label_reason": o.get("label_reason"), "alerts": [a.get("code") for a in detail.get("alerts", [])], + "end_lag_s": end_lag, + "early_end": early_end, + "split": split, + # "Last N" can reach past the panel's loaded page of cycles, so the row + # carries its own date (PLAYGROUND-18). + "start_time": (detail.get("start_time") if isinstance(detail.get("start_time"), str) else None), } @@ -1670,6 +1711,8 @@ def _rows_summary(rows: list[dict[str, Any]]) -> dict[str, Any]: "match_wrong": wrong, "unmatched": unmatched, "false_end": false_end, + "early_end": sum(1 for r in rows if r.get("early_end")), + "split": sum(1 for r in rows if r.get("split")), } @@ -1699,6 +1742,26 @@ def _run_rows( return rows +def _select_cycles( + store: Any, cycle_ids: list[str] | None, count: int | None = None +) -> list[dict[str, Any]]: + """The stored cycles a History / Optimize run replays, at most + ``MAX_BATCH_CYCLES``: ``cycle_ids`` in order (unknown ids dropped); else the + ``count`` most recent, newest first - the panel's "Last N", which its loaded + page of 25 cycles could not supply as ids (audit PLAYGROUND-18); else the most + recent ``DEFAULT_RECENT_CYCLES`` in stored order.""" + past = [c for c in (store.get_past_cycles() or []) if isinstance(c, dict)] + if cycle_ids: + by_id = {c.get("id"): c for c in past} + selected = [by_id[c] for c in cycle_ids if c in by_id] + elif count: + n = max(1, min(MAX_BATCH_CYCLES, int(count))) + selected = list(reversed(past[-n:])) + else: + selected = past[-DEFAULT_RECENT_CYCLES:] + return selected[:MAX_BATCH_CYCLES] + + def run_playground_history( store: Any, cycle_ids: list[str] | None, @@ -1714,21 +1777,14 @@ def run_playground_history( options = options or {} try: concurrency = max(1, min(MAX_BATCH_CYCLES, int(concurrency))) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): concurrency = MAX_BATCH_CYCLES try: - past = list(store.get_past_cycles() or []) + selected = _select_cycles(store, cycle_ids)[:concurrency] except Exception as exc: # pylint: disable=broad-exception-caught _LOGGER.debug("Playground history: get_past_cycles failed: %s", exc) return {"rows": [], "summary": _rows_summary([])} - by_id = {c.get("id"): c for c in past if isinstance(c, dict)} - if cycle_ids: - selected = [by_id[c] for c in cycle_ids if c in by_id] - else: - selected = past[-DEFAULT_RECENT_CYCLES:] - selected = selected[:concurrency] - override = settings_override or None rows = _run_rows(store, selected, base_config, override, options, price, prebuilt) payload: dict[str, Any] = {"rows": rows, "summary": _rows_summary(rows)} @@ -1783,7 +1839,7 @@ def _diff_rows( } -# ─── Parameter sweep (1D curve + 2D heatmap) ──────────────────────────────────── +# ─── Parameter sweep (1D curve) ──────────────────────────────────────────────────── _SWEEP_OBJECTIVES = ( @@ -1792,6 +1848,12 @@ _SWEEP_OBJECTIVES = ( "false_end_rate", "median_overrun", "ambiguity_rate", + # What the end-gate settings actually move (audit PLAYGROUND-08): none of the + # five above changed while off_delay 60 -> 1800 s moved the mean end lag + # 19.8 -> 25.4 min, because the stored duration is trimmed to the activity. + "end_lag", + "early_end_rate", + "split_rate", ) # Objectives where a LOWER metric is better (best = minimum), so the sweep picks # the right winner and the panel colours the heatmap consistently. @@ -1799,7 +1861,16 @@ _SWEEP_LOWER_IS_BETTER = frozenset({ "false_end_rate", "median_overrun", "ambiguity_rate", + "end_lag", + "early_end_rate", + "split_rate", }) +# Smallest change worth recommending over the current value. A rate moves in +# steps of one cycle (1/N, passed by the caller), so anything less is the +# denominator moving, not a cycle getting better; the end lag is measured on a +# replay whose tail steps are 30 s; the overrun deviation is a share of the +# profile's length. +_SWEEP_MIN_GAIN = {"end_lag": 60.0, "median_overrun": 0.01} def _sweep_is_better(candidate: float, best: float, objective: str) -> bool: @@ -1808,44 +1879,111 @@ def _sweep_is_better(candidate: float, best: float, objective: str) -> bool: return candidate > best -def finalize_sweep_1d( - param: str, objective: str, points: list[dict[str, Any]], current_value: Any +def _sweep_min_gain(objective: str, n_cycles: int) -> float: + if objective in _SWEEP_MIN_GAIN: + return _SWEEP_MIN_GAIN[objective] + # A tiny epsilon below one cycle so 1/N itself, after rounding, still counts. + return (1.0 / n_cycles - 1e-6) if n_cycles > 0 else 0.0 + + +def _sweep_regresses(summary: Any, baseline: Any) -> bool: + """Does ``summary`` lose what the current setting has? The hard guard: a value + that ends more cycles early, splits more of them, or detects fewer can never be + recommended, whatever its objective says (audit PLAYGROUND-08).""" + if not isinstance(summary, dict) or not isinstance(baseline, dict): + return False + return ( + int(summary.get("early_end") or 0) > int(baseline.get("early_end") or 0) + or int(summary.get("split") or 0) > int(baseline.get("split") or 0) + or int(summary.get("detected") or 0) < int(baseline.get("detected") or 0) + ) + + +def sweep_baseline( + store: Any, + cycle_ids: list[str] | None, + base_config: CycleDetectorConfig, + objective: str, + options: dict[str, Any] | None, + price: float | None, + prebuilt: tuple[Any, Any, Any, Any] | None = None, ) -> dict[str, Any]: - """Assemble a 1D sweep payload from per-value points collected across chunks, - picking the best via the same direction rule as :func:`run_playground_sweep`.""" + """The current settings' metric and summary over the sweep's cycles: what every + swept value has to beat. Executor-safe; never raises.""" + try: + selected = _select_cycles(store, cycle_ids) + rows = _run_rows(store, selected, base_config, None, options or {}, price, prebuilt) + metric = objective_metric(rows, objective) + return { + "metric": round(metric, 4) if metric is not None else None, + "summary": _rows_summary(rows), + } + except Exception as exc: # pylint: disable=broad-exception-caught + _LOGGER.debug("Playground sweep baseline failed: %s", exc) + return {"metric": None, "summary": None} + + +def finalize_sweep_1d( + param: str, + objective: str, + points: list[dict[str, Any]], + current_value: Any, + baseline: dict[str, Any] | None = None, +) -> dict[str, Any]: + """Assemble a 1D sweep payload from per-value points collected across chunks. + + With a ``baseline`` (:func:`sweep_baseline`, the current settings on the same + cycles) the recommendation is conservative (audit PLAYGROUND-08): a value that + regresses early ends, splits or detection is never best; and unless the best + value beats the current one by at least one cycle (:func:`_sweep_min_gain`) the + answer is to keep the current value - a tie used to pick the first swept value, + so "Apply best" offered ``off_delay=60``. Without one, ties still prefer the + current value over the first.""" + base_summary = (baseline or {}).get("summary") + base_metric = (baseline or {}).get("metric") + n_cycles = int((base_summary or {}).get("cycles") or 0) or max( + (int((p.get("summary") or {}).get("cycles") or 0) for p in points), default=0 + ) + + def _is_current(value: Any) -> bool: + try: + return current_value is not None and abs(float(value) - float(current_value)) < 1e-6 + except (TypeError, ValueError, OverflowError): + return False + best: dict[str, Any] | None = None for p in points: m = p.get("metric") - if m is None: + guarded = base_summary is not None and _sweep_regresses(p.get("summary"), base_summary) + p["guarded"] = guarded + if m is None or guarded: continue - if best is None or _sweep_is_better(m, best["metric"], objective): + if ( + best is None + or _sweep_is_better(m, best["metric"], objective) + or (m == best["metric"] and _is_current(p["value"])) + ): best = {"value": p["value"], "metric": m} + keep_current = False + if base_metric is not None: + gain = _sweep_min_gain(objective, n_cycles) + if best is None or not ( + (base_metric - best["metric"] if objective in _SWEEP_LOWER_IS_BETTER + else best["metric"] - base_metric) >= gain + ): + best = {"value": current_value, "metric": base_metric} + keep_current = True + elif best is not None and _is_current(best["value"]): + keep_current = True return { "param": param, "objective": objective, "points": points, "current_value": current_value, + "current_metric": base_metric, + "current_summary": base_summary, "best_value": best["value"] if best else None, "best_metric": best["metric"] if best else None, - "lower_is_better": objective in _SWEEP_LOWER_IS_BETTER, - } - - -def finalize_sweep_2d( - param_x: str, param_y: str, objective: str, - x_values: list[float], y_values: list[float], - grid: list[list[float | None]], current: dict[str, Any], -) -> dict[str, Any]: - """Assemble a 2D sweep payload from grid cells collected across chunks.""" - best: dict[str, Any] | None = None - for j, row in enumerate(grid): - for i, v in enumerate(row or []): - if v is None: - continue - if best is None or _sweep_is_better(v, best["metric"], objective): - best = {"x": x_values[i], "y": y_values[j], "metric": v} - return { - "param_x": param_x, "param_y": param_y, "objective": objective, - "x_values": x_values, "y_values": y_values, "grid": grid, - "best": best, "current": current, + "keep_current": keep_current, + "cycles": n_cycles, "lower_is_better": objective in _SWEEP_LOWER_IS_BETTER, } @@ -1886,6 +2024,19 @@ def objective_metric(rows: list[dict[str, Any]], objective: str) -> float | None if abs(dur - ref) <= 0.10 * ref: ok += 1 return (ok / n) if n else None + if objective == "end_lag": + # Median seconds from the appliance's last activity to WashData's "done". + lags = [float(r["end_lag_s"]) for r in detected if r.get("end_lag_s") is not None] + if not lags: + return None + lags.sort() + mid = len(lags) // 2 + return lags[mid] if len(lags) % 2 else (lags[mid - 1] + lags[mid]) / 2.0 + if objective in ("early_end_rate", "split_rate"): + # Over EVERY replayed cycle, not just the detected ones: a value that stops + # detecting a hard cycle must not shrink its own denominator and win. + key = "early_end" if objective == "early_end_rate" else "split" + return sum(1 for r in rows if r.get(key)) / len(rows) if objective == "median_overrun": # Score by the median duration's DEVIATION from the profile's expected # duration (|ratio - 1|), so "best" is the value that makes cycles land @@ -1910,7 +2061,7 @@ def _coerce_param(base_config: CycleDetectorConfig, param: str, value: float) -> _field, coerce = mapping try: return coerce(value) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): return value @@ -1924,31 +2075,24 @@ def run_playground_sweep( options: dict[str, Any] | None, price: float | None, concurrency: int, - param_y: str | None = None, - values_y: list[float] | None = None, prebuilt: tuple[Any, Any, Any, Any] | None = None, ) -> dict[str, Any]: - """Sweep one param (1D curve) or two params (2D heatmap) and score each point - by ``objective`` computed from the per-cycle rows. Executor-safe; never raises. + """Sweep one param and score each value by ``objective`` computed from the + per-cycle rows. Executor-safe; never raises. (The 2D heatmap was removed: + the panel never sent a second parameter, audit PLAYGROUND.) """ options = options or {} if objective not in _SWEEP_OBJECTIVES: objective = "match_accuracy" try: concurrency = max(1, min(MAX_BATCH_CYCLES, int(concurrency))) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): concurrency = MAX_BATCH_CYCLES try: - past = list(store.get_past_cycles() or []) + selected = _select_cycles(store, cycle_ids)[:concurrency] except Exception as exc: # pylint: disable=broad-exception-caught _LOGGER.debug("Playground sweep: get_past_cycles failed: %s", exc) return {"error": "no cycles"} - by_id = {c.get("id"): c for c in past if isinstance(c, dict)} - if cycle_ids: - selected = [by_id[c] for c in cycle_ids if c in by_id] - else: - selected = past[-DEFAULT_RECENT_CYCLES:] - selected = selected[:concurrency] def _metric_for(override: dict[str, Any]) -> tuple[float | None, dict[str, Any]]: rows = _run_rows(store, selected, base_config, override, options, price, prebuilt) @@ -1957,35 +2101,18 @@ def run_playground_sweep( current_x = _sim_config_summary(base_config).get( _OVERRIDE_FIELD_MAP.get(param, (param,))[0] ) - - if param_y and values_y: - grid: list[list[float | None]] = [] - best: dict[str, Any] | None = None - for vy in values_y: - row_metrics: list[float | None] = [] - for vx in values: - override = { - param: _coerce_param(base_config, param, vx), - param_y: _coerce_param(base_config, param_y, vy), - } - metric, _ = _metric_for(override) - row_metrics.append(round(metric, 4) if metric is not None else None) - if metric is not None and ( - best is None or _sweep_is_better(metric, best["metric"], objective) - ): - best = {"x": vx, "y": vy, "metric": round(metric, 4)} - grid.append(row_metrics) - current_y = _sim_config_summary(base_config).get( - _OVERRIDE_FIELD_MAP.get(param_y, (param_y,))[0] - ) - return { - "param_x": param, "param_y": param_y, "objective": objective, - "x_values": values, "y_values": values_y, "grid": grid, "best": best, - "current": {"x": current_x, "y": current_y}, - } + if current_x is None: + # The summary carries only part of the config, so 8 sweepable keys (the + # completion/interrupted/start-duration thresholds, the two ratios, ...) + # reported no current value and the panel drew no marker for it. + try: + current_x = effective_settings( + base_config, prebuilt[1] if prebuilt else None + ).get(param) + except Exception: # pylint: disable=broad-exception-caught + current_x = None points: list[dict[str, Any]] = [] - best_1d: dict[str, Any] | None = None for vx in values: override = {param: _coerce_param(base_config, param, vx)} metric, summary = _metric_for(override) @@ -1993,16 +2120,9 @@ def run_playground_sweep( {"value": vx, "metric": round(metric, 4) if metric is not None else None, "summary": summary} ) - if metric is not None and ( - best_1d is None or _sweep_is_better(metric, best_1d["metric"], objective) - ): - best_1d = {"value": vx, "metric": round(metric, 4)} - return { - "param": param, "objective": objective, "points": points, - "current_value": current_x, - "best_value": best_1d["value"] if best_1d else None, - "best_metric": best_1d["metric"] if best_1d else None, - } + # One selection rule for the one-shot and the chunked task (which adds the + # current-settings baseline the guard and the keep-current rule need). + return finalize_sweep_1d(param, objective, points, current_x) # ─── DTW debug ──────────────────────────────────────────────────────────────── @@ -2013,207 +2133,5 @@ def _safe_float(value: Any) -> float | None: if value is None: return None return round(float(value), 2) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): return None - - -def _profile_trace(store: Any, profile_name: str) -> tuple[list[float], float] | None: - """Return (power_values, duration_s) for a profile's average envelope. - - Prefers the cached envelope ``avg`` curve; falls back to the profile's - sample cycle trace. Returns None when nothing usable exists. - """ - try: - env = store.get_envelope(profile_name) - except Exception: # pylint: disable=broad-exception-caught - env = None - if env and env.get("avg"): - avg = env["avg"] - powers: list[float] = [] - times: list[float] = [] - for pt in avg: - if isinstance(pt, (list, tuple)) and len(pt) >= 2: - times.append(float(pt[0])) - powers.append(float(pt[1])) - else: - powers.append(float(pt)) - if powers: - duration = float(env.get("target_duration") or 0.0) - if not duration and len(times) > 1: - duration = times[-1] - times[0] - return powers, duration - - # Fallback: the profile's sample cycle. - try: - data = getattr(store, "_data", {}) or {} - profile = (data.get("profiles", {}) or {}).get(profile_name) - if not isinstance(profile, dict): - return None - # past + imported reference cycles: an import-only profile's sample lives in - # reference_cycles, so resolve against both (mirrors _build_match_snapshots). - pool = (data.get("past_cycles", []) or []) + (data.get("reference_cycles", []) or []) - sample = next( - ( - c - for c in pool - if isinstance(c, dict) and c.get("id") == profile.get("sample_cycle_id") - ), - None, - ) - if not sample: - return None - pts = decompress_power_data(sample) - if not pts: - return None - duration = float( - profile.get("avg_duration") - or sample.get("duration") - or (pts[-1][0] if pts else 0.0) - ) - return [p for _, p in pts], duration - except Exception: # pylint: disable=broad-exception-caught - return None - - -def dtw_debug_payload( - store: Any, cycle_id: str, profile_name: str | None -) -> dict[str, Any]: - """Score breakdown + resampled traces + DTW warp path for a cycle vs profile. - - Returns ``{"error": }`` when the cycle or profile is unavailable. - Executor-safe; never raises. - """ - try: - cycle = next( - (c for c in store.get_past_cycles() if c.get("id") == cycle_id), None - ) - except Exception as exc: # pylint: disable=broad-exception-caught - return {"error": "store_error", "detail": str(exc)} - if cycle is None: - return {"error": "cycle_not_found"} - - target_profile = profile_name or _cycle_label(cycle) - if not target_profile: - return {"error": "no_profile"} - - cycle_pts = decompress_power_data(cycle) - if not cycle_pts or len(cycle_pts) < 2: - return {"error": "cycle_no_data", "profile_name": target_profile} - - prof = _profile_trace(store, target_profile) - if prof is None: - return {"error": "profile_not_found", "profile_name": target_profile} - prof_powers, prof_duration = prof - if len(prof_powers) < 2: - return {"error": "profile_no_data", "profile_name": target_profile} - - try: - return _compute_dtw_debug( - store, cycle, cycle_pts, target_profile, prof_powers, prof_duration - ) - except Exception as exc: # pylint: disable=broad-exception-caught - _LOGGER.debug("Playground dtw_debug failed for %s: %s", cycle_id, exc) - return { - "error": "compute_error", - "detail": str(exc), - "profile_name": target_profile, - } - - -def _compute_dtw_debug( - store: Any, - cycle: dict[str, Any], - cycle_pts: list[tuple[float, float]], - profile_name: str, - prof_powers: list[float], - prof_duration: float, -) -> dict[str, Any]: - cfg = _matching_config(store) - corr_weight = float(cfg.get("corr_weight", MATCH_CORR_WEIGHT)) - dur_weight = float(cfg.get("duration_weight", MATCH_DURATION_WEIGHT)) - en_weight = float(cfg.get("energy_weight", MATCH_ENERGY_WEIGHT)) - dur_scale = float(cfg.get("duration_scale", MATCH_DURATION_SCALE)) - en_scale = float(cfg.get("energy_scale", MATCH_ENERGY_SCALE)) - band = float(cfg.get("dtw_bandwidth", 0.2)) - blend = float(cfg.get("dtw_blend", MATCH_DTW_BLEND)) - l1_scale = float(cfg.get("dtw_l1_scale", MATCH_DTW_DIST_SCALE)) - ddtw_scale = float(cfg.get("dtw_ddtw_scale", MATCH_DDTW_DIST_SCALE)) - ensemble_w = float(cfg.get("dtw_ensemble_w", MATCH_DTW_ENSEMBLE_W)) - - cycle_powers = [p for _, p in cycle_pts] - cycle_duration = float(cycle_pts[-1][0] - cycle_pts[0][0]) - current_peak = float(max(cycle_powers)) if cycle_powers else 0.0 - - # --- Stage 2: core similarity on the raw traces (matcher-faithful) --- - score, metrics, _offset = analysis.find_best_alignment( - cycle_powers, prof_powers, corr_weight=corr_weight - ) - corr = float(metrics.get("corr", 0.0)) - mae = float(metrics.get("mae", 0.0)) - scaled_mae = mae * MATCH_MAE_REF_PEAK / max(current_peak, MATCH_MAE_PEAK_FLOOR) - mae_score = MATCH_MAE_SCALE / (MATCH_MAE_SCALE + scaled_mae) - stage2_score = float(score) - - # --- DTW components on a common resampled grid --- - curr_arr = np.asarray(cycle_powers, dtype=float) - sample_arr = np.asarray(prof_powers, dtype=float) - l1_score = analysis._dtw_component_score( - curr_arr, sample_arr, current_peak, band, False, l1_scale - ) - ddtw_score = analysis._dtw_component_score( - curr_arr, sample_arr, current_peak, band, True, ddtw_scale - ) - ensemble_score = ensemble_w * l1_score + (1.0 - ensemble_w) * ddtw_score - blended_score = blend * stage2_score + (1.0 - blend) * ensemble_score - - # --- Stage 4: duration + energy agreement over the DTW-blended score --- - cur_mean = float(np.mean(curr_arr)) if curr_arr.size else 0.0 - prof_mean = float(np.mean(sample_arr)) if sample_arr.size else 0.0 - dur_ag = analysis._agreement(cycle_duration, prof_duration, dur_scale) - en_ag = analysis._agreement(cur_mean, prof_mean, en_scale) - shape_w = 1.0 - dur_weight - en_weight - final_score = shape_w * blended_score + dur_weight * dur_ag + en_weight * en_ag - - # --- Resampled traces on one shared grid (progress fraction 0..1) --- - n = MATCH_DTW_RESAMPLE_N - a = analysis._resample_to(curr_arr, n) - b = analysis._resample_to(sample_arr, n) - grid = np.linspace(0.0, 1.0, n) - cycle_trace = [[round(float(g), 4), round(float(p), 1)] for g, p in zip(grid, a)] - profile_trace = [[round(float(g), 4), round(float(p), 1)] for g, p in zip(grid, b)] - - # --- DTW warping path on the same resampled arrays --- - try: - raw_path = analysis.compute_dtw_path(a, b, band_width_ratio=band) - warp_path = [[int(i), int(j)] for i, j in raw_path] - except Exception as exc: # pylint: disable=broad-exception-caught - _LOGGER.debug("Playground warp path failed: %s", exc) - warp_path = [] - - return { - "cycle_id": cycle.get("id"), - "profile_name": profile_name, - "grid_n": n, - "cycle_duration_s": round(cycle_duration, 1), - "profile_duration_s": round(float(prof_duration), 1), - "cycle_trace": cycle_trace, - "profile_trace": profile_trace, - "stage2": { - "correlation": round(corr, 4), - "mae_score": round(float(mae_score), 4), - "score": round(stage2_score, 4), - }, - "dtw": { - "l1_score": round(float(l1_score), 4), - "ddtw_score": round(float(ddtw_score), 4), - "ensemble_score": round(float(ensemble_score), 4), - "blend_weight": round(blend, 4), - "blended_score": round(float(blended_score), 4), - }, - "stage4": { - "duration_agreement": round(float(dur_ag), 4), - "energy_agreement": round(float(en_ag), 4), - "final_score": round(float(final_score), 4), - }, - "warp_path": warp_path, - } diff --git a/custom_components/ha_washdata/profile_store.py b/custom_components/ha_washdata/profile_store.py index 54b3d87f..69664bb0 100644 --- a/custom_components/ha_washdata/profile_store.py +++ b/custom_components/ha_washdata/profile_store.py @@ -18,8 +18,10 @@ from __future__ import annotations +import collections import contextlib import dataclasses +import functools import hashlib import json import logging @@ -28,13 +30,15 @@ import os import re import statistics import threading +import time import uuid +from collections.abc import Callable from datetime import datetime, timedelta from typing import Any, TypeAlias, cast import numpy as np -from homeassistant.core import HomeAssistant +from homeassistant.core import CoreState, HomeAssistant from homeassistant.helpers.storage import Store from homeassistant.util import dt as dt_util @@ -42,6 +46,7 @@ from .const import ( CLUSTER_RESAMPLE_N, CLUSTER_SHAPE_SIMILARITY_THRESHOLD, GROUP_MIN_COHESION, + TERMINAL_EVENT_MAX_PEAK_FRAC, TERMINAL_EVENT_PEAK_FRAC, TERMINAL_QUIET_MIN_S, TERMINAL_SIGNATURE_MIN_CYCLES, @@ -52,39 +57,43 @@ from .const import ( TERMINAL_QUIET_CAP_S, TERMINAL_QUIET_MIN_CONSISTENCY, TERMINAL_QUIET_MIN_OBSERVATIONS, + MAINTENANCE_COUNT_FROM_ENABLE_TYPES, + MAINTENANCE_CUSTOM_TASK_MAX, + MAINTENANCE_CUSTOM_TASK_PREFIX, MAINTENANCE_EVENT_TYPES, + MAINTENANCE_INTERVAL_CYCLES_MAX, + MAINTENANCE_INTERVAL_DAYS_MAX, MAINTENANCE_RECENT_SUPPRESS_DAYS, MATCH_AMBIGUITY_MARGIN, MATCH_MIN_RESAMPLED_POINTS, - PHASE_CONSISTENCY_MIN_CYCLES, - PHASE_PROFILE_MIN_CYCLES, - PHASE_HEAT_CV_WARN, - PHASE_HEAT_OCC_MIXED_LO, - PHASE_HEAT_OCC_MIXED_HI, PLAYGROUND_PRESET_MAX, PLAYGROUND_PRESET_NAME_MAX, REFERENCE_PROFILE_CURVE_POINTS, SHAPE_DRIFT_MIN_CYCLES, SHAPE_DRIFT_RESAMPLE_N, SHAPE_DRIFT_THRESHOLD, - SHAREABLE_SETTING_KEYS, + sanitize_shared_settings, SMART_TERM_LANDSCAPE_RATIO, SMART_TERM_LANDSCAPE_MIN_SHAPE, - SMART_TERM_PREFIX_MARGIN, - SMART_TERM_PREFIX_MIN_RATIO, - SMART_TERM_PREFIX_MIN_SHAPE, + MATCH_LABEL_MIN_MARGIN, + CONF_PROFILE_MIN_WARMUP_CYCLES, + SMART_TERM_PREFIX_MIN_PAUSE_S, SMART_TERM_TAIL_WINDOW_FRAC, STORAGE_KEY, STORAGE_VERSION, - DEFAULT_MAX_PAST_CYCLES, - DEFAULT_MAX_FULL_TRACES_PER_PROFILE, - DEFAULT_MAX_FULL_TRACES_UNLABELED, + FAILED_RESTORE_STORE_SUFFIX, + PRE_IMPORT_STORE_SUFFIX, + PRE_IMPORT_STORE_VERSION, EVIDENCE_BACKFILL_CYCLES, EVIDENCE_REAL_CYCLES, EVIDENCE_REFERENCE_CYCLES, PROFILE_EVIDENCE_SOURCES, DEFAULT_DTW_BANDWIDTH, TerminationReason, + TRUSTED_LENGTH_FLOOR_FRAC, + ANTI_CREASE_TERMINAL_HIGH_MIN_FRAC, + ANTI_CREASE_SPIN_ARM_MIN_MEMBERS, + ANTI_CREASE_SPIN_ARM_MIN_SHARE, ) from .features import compute_signature from .signal_processing import ( @@ -93,13 +102,18 @@ from .signal_processing import ( Segment, integrate_wh, energy_gap_threshold_s, - quiet_run_before as _quiet_run_before, + has_resumed_pause as _has_resumed_pause, + resumed_pauses as _resumed_pauses, + terminal_event_end as _terminal_event_end, + terminal_quiet_seen as _terminal_quiet_seen, ) from . import analysis from .time_utils import ( migrate_power_data_to_offsets, power_data_to_offsets, + utc_now, ) +from .maintenance import clean_task_name, coerce_interval, is_due from .phase_catalog import ( DEFAULT_PHASES_BY_DEVICE, _builtin_phase_id, @@ -107,14 +121,6 @@ from .phase_catalog import ( merge_phase_catalog, normalize_phase_name, ) -from .phase_segmenter import phase_matching_live_supported, phase_model_for, segment_cycle -from .phase_match import ( - build_phase_profile, - match_phase_profiles, - phase_eta, - phase_profile_from_dict, - phase_profile_to_dict, -) from .log_utils import DeviceLoggerAdapter _LOGGER = logging.getLogger(__name__) @@ -131,6 +137,18 @@ _DEGENERATE_POWER_FLOOR = 15.0 # watts # absorbs that rounding without letting the kept window grow measurably. _TRIM_SNAP_TOLERANCE_S = 1.0 +# Cycle-end save coalescing (register item 456, see ProfileStore.coalesce_saves). +# The window covers the executor jobs a cycle end spawns (suggestion scans, the +# artifact refresh); the delay is HA's own convention for debounced Store writes. +# Only what those follow-ups derive (suggestions, refreshed artifacts) waits for +# the delay: the cycle itself is flushed at once. +_BURST_SAVE_WINDOW_S = 120.0 +_BURST_SAVE_DELAY_S = 10.0 + +# Floor of the resampled sample-segment cache (audit ML-12, ~20-45 KB an entry); +# see ProfileStore._sample_segment_cache_cap. +SAMPLE_SEGMENT_CACHE_MIN = 64 + JSONDict: TypeAlias = dict[str, Any] CycleDict: TypeAlias = dict[str, Any] @@ -144,6 +162,64 @@ _AUTO_LABEL_SOURCES = ( ) +def label_verdict(result: Any, floor: float) -> tuple[str | None, str]: + """May ``result`` label its cycle? Returns ``(profile, reason)``; profile None = no. + + The one gate every auto-label path applies to a COMPLETE-cycle match (audit + MATCH-DECIDE-02/F-12): a winner, the member-aware ``label_confidence`` at or + above ``floor`` (item 206), a clear margin over the runner-up (item 310) and + no Stage-5 safeguard flag (item 387b). ``is True`` so a result without the + field cannot block. Reasons: ``ok``, ``no_winner``, ``below_floor``, + ``ambiguous``, ``margin``. + """ + best = getattr(result, "best_profile", None) if result is not None else None + if not best: + return None, "no_winner" + try: + conf = float(getattr(result, "label_confidence", 0.0) or 0.0) + except (TypeError, ValueError, OverflowError): + conf = 0.0 + if conf < float(floor or 0.0): + return None, "below_floor" + if getattr(result, "is_ambiguous", False) is True: + return None, "ambiguous" + margin = getattr(result, "ambiguity_margin", None) + if margin is not None and float(margin) < MATCH_LABEL_MIN_MARGIN: + return None, "margin" + return str(best), "ok" + + +def _trusted_min_by_profile(cycles: Any) -> dict[str, float]: + """Shortest length the USER has vouched for, per profile name (item 384). + + A cycle is vouched for when it carries a corrected ``manual_duration``, came + from the manual recorder, or is pinned golden. Shared by the banked-tail + repair and the live keep-tail floor (match tuple element 13), which both stop + a dishwasher's stored end from falling below ``TRUSTED_LENGTH_FLOOR_FRAC`` of + it. Metadata only, no trace decompression. Never raises. + """ + out: dict[str, float] = {} + for c in cycles or []: + try: + if not isinstance(c, dict) or not c.get("profile_name"): + continue + rev = c.get("ml_review") + vouched = ( + c.get("manual_duration") + or _is_recorded_cycle(c) + or (isinstance(rev, dict) and rev.get("golden")) + ) + if not vouched: + continue + d = float(c.get("manual_duration") or c.get("duration") or 0.0) + except (TypeError, ValueError, OverflowError): + continue + if math.isfinite(d) and d > 0: + n = str(c["profile_name"]) + out[n] = min(out.get(n, d), d) + return out + + def _is_recorded_cycle(cycle: dict[str, Any]) -> bool: """True when a cycle was produced by the manual recorder. @@ -247,16 +323,52 @@ def _parse_start_dt(value: Any) -> datetime | None: except (OSError, OverflowError, ValueError): return None if isinstance(value, str) and value: - parsed = dt_util.parse_datetime(value) + # `parse_datetime` RAISES for a well-formed but impossible value (month 13, + # a +99:00 offset) rather than returning None; one such row aborted the + # banked-tail repair halfway through (register item 384). + try: + parsed = dt_util.parse_datetime(value) + except (TypeError, ValueError, OverflowError): + parsed = None if parsed is not None: return parsed try: return datetime.fromtimestamp(float(value), tz=dt_util.UTC) - except (TypeError, ValueError): + except (TypeError, ValueError, OSError, OverflowError): return None return None +def _newest_cycle(candidates: list[Any]) -> Any: + """The most recent candidate cycle, ordered by parsed instant (``None`` if empty). + + 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. Sample repair would then adopt an older cycle's trace and duration. + + 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: 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=_key) + except Exception: # pylint: disable=broad-exception-caught + return candidates[-1] + + def _parse_maintenance_dt(value: Any) -> datetime | None: """Parse a maintenance-log date (ISO date or datetime) into an aware datetime. @@ -292,7 +404,7 @@ def _value_to_timestamp(value: Any) -> float | None: return parsed.timestamp() try: return float(value) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): return None return None @@ -401,12 +513,11 @@ class MatchResult: debug_details: dict[str, Any] = dataclasses.field(default_factory=_empty_debug_details) is_confident_mismatch: bool = False mismatch_reason: str | None = None - is_prefix_ambiguous: bool = False - # The LEGACY (#288-only, full-envelope shape) half of the verdict above. - # `is_prefix_ambiguous` is widened by #364's prefix scoring, which is safe for - # the ENDING Smart-Termination gate (a false fire only delays the finish) but - # 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`, the #364 prefix-fit flag the ENDING gates read, was + # removed in 0.5.8: on the shipped matcher it never fired at a split moment. + # See the #364 block in const.py.) + # The #288 full-envelope-shape term, read only by the anti-crease finalize, + # where blocking can re-hang a cycle the way #296 described. 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 @@ -421,12 +532,14 @@ class MatchResult: member_confidence: float | None = None # Longest expected duration across the FULL candidate population, not the # `candidates[:5]` this result carries. The ENDING fallback gate raises its - # bar to this while the match is ambiguous (register item 330), and - # `_match_prefix_ambiguity` - which decides whether it is ambiguous - also - # runs over the full list. Deriving the bound from the truncated one let a - # sixth-ranked longer candidate set the flag while staying invisible to the - # bar, so the gate shortened against a programme it had been warned about. + # bar to this while the match is ambiguous (register item 330). Deriving the + # bound from the truncated list let a sixth-ranked longer candidate set the + # (since removed) prefix flag while staying invisible to the bar, so the gate + # shortened against a programme it had been warned about. longest_candidate_duration_s: float = 0.0 + # The elapsed duration this match scored (the query's), so a candidate's + # duration ratio can be stated against the cycle (audit MATCH-DECIDE-13). + query_duration_s: float = 0.0 @property def label_confidence(self) -> float: @@ -504,7 +617,7 @@ def _envelope_y(raw: list[Any] | None) -> np.ndarray: return np.asarray( [p[1] if isinstance(p, (list, tuple)) else p for p in raw], dtype=float ) - except (TypeError, ValueError, IndexError): + except (TypeError, ValueError, IndexError, OverflowError): return np.array([], dtype=float) @@ -734,7 +847,7 @@ def compress_power_data(cycle: CycleDict) -> list[Any] | None: last_saved_p = p_val last_saved_t = offset - except (ValueError, TypeError): + except (ValueError, TypeError, OverflowError): continue return compressed except Exception: @@ -945,13 +1058,10 @@ class WashDataStore(Store[JSONDict]): old_data.setdefault("reference_cycles", []) if old_major_version < 11: - # Marker-only bump. Per-phase profiles (envelope["phase_profile"], used - # by phase-segmented matching / phase-resolved ETA) are DERIVED CACHE - # built by async_rebuild_envelope, not stored data - so there is nothing - # to migrate. They self-populate on the next envelope rebuild (which - # runs on every cycle end / label change); until then consumers fall - # back to the existing estimator via lazy absent-key handling. No data - # is added, removed, or altered here. + # Marker-only bump. It introduced a per-phase envelope cache for the + # phase-resolved ETA, which 0.5.8 removed (register item 411); the step + # stays so older stores still pass through it. No data is added, + # removed, or altered here. _LOGGER.info("Migrating storage from v%s to v11 (phase-profile cache marker)", old_major_version) @@ -984,8 +1094,268 @@ class WashDataStore(Store[JSONDict]): ) old_data.setdefault(BANKED_TAIL_REPAIR_KEY, True) + if old_major_version < 14: + # Marker-only, and deliberately an assignment, not a `setdefault` + # (#424). The v13 repair has already run and cleared the key on every + # 0.5.7 store, but it judged "did the drying already happen?" at the + # stop threshold while the allowance it withholds is measured at a + # fraction of the cycle's peak - so on #424's Beko it banked 10 min of + # standby into every cycle instead of removing it, and it never + # looked at a dishwasher's fallback-timeout finishes at all. Re-arm + # it once so the corrected rule (`terminal_quiet_seen`) re-judges + # history. Idempotent: the repair only ever trims, and re-running it + # on a repaired cycle reclaims nothing. + _LOGGER.info( + "Migrating storage from v%s to v14 (banked-tail repair re-run)", + old_major_version, + ) + old_data[BANKED_TAIL_REPAIR_KEY] = True + + if old_major_version < 15: + # Label provenance repair (audit MANAGER-01). Confirming or correcting a + # cycle from the review queue set only `profile_name`, so the user's + # answer kept the matcher's `label_source` and the panel's Auto-label + # (overwrite=True) re-matched and replaced it, storing the answer as + # `original_auto_label`. Pure data, idempotent: re-running finds every + # answered cycle already stamped. + summary = _repair_answered_feedback_provenance(old_data) + _LOGGER.info( + "Migrating storage from v%s to v15 (review-queue answers stamped " + "manual: %d stamped, %d restored)", + old_major_version, summary["stamped"], summary["restored"], + ) + + if old_major_version < 16: + # Review queue cleanup (register item 433). Requests the 0.5.8 rule would + # not raise - the cycle already carries the detected programme with a + # clear margin and the programme is past warm-up - are dropped, as are + # requests whose cycle is gone or was labelled by hand. Pure data, + # idempotent: a second pass finds nothing left to drop. + summary = _dismiss_unneeded_feedback(old_data) + _LOGGER.info( + "Migrating storage from v%s to v16 (review queue: %d dismissed, " + "%d already answered by hand, %d without a stored cycle, %d kept)", + old_major_version, summary["dismissed"], summary["answered"], + summary["stale"], summary["kept"], + ) + + if old_major_version < 17: + # The ML early match commit, the quality gate, the matcher weight tuner + # and on-device training of every head but total_energy were removed in + # 0.5.8; drop the state only they wrote. No cycle, label or review + # answer is touched. Idempotent. + summary = _drop_removed_ml_state(old_data) + _LOGGER.info( + "Migrating storage from v%s to v17 (removed ML state: %d ranking " + "snapshots, matcher tuning %s, model records %s)", + old_major_version, summary["ranking_snapshots"], + "dropped" if summary["matching_config"] else "absent", + summary["model_records"] or "none", + ) + return old_data +#: Trained-model capabilities no longer trained on-device since 0.5.8: the early +#: match commit (C2) and the quality gate's head (C3), removed with their consumers, +#: and the end guard (C1) and remaining-time (C4) heads, whose consumers are frozen +#: off (audit ML-11). A promoted spec for one could never be retrained, so it goes; +#: the shipped quality / end baselines still score the panel's per-cycle health. +#: Only ``total_energy`` is trained (and kept). +_REMOVED_ML_CAPABILITIES = ("live_match", "quality", "end", "remaining_time") + + +def _drop_removed_ml_state(data: JSONDict) -> dict[str, Any]: + """Drop the stored state of the ML parts removed in 0.5.8 (storage v17). + + ``match_ranking_history`` (the live_match training snapshots), + ``matching_config`` (the matcher weight tuner's record) and the + ``_REMOVED_ML_CAPABILITIES`` entries of ``ml_model_versions`` and + ``ml_training_history``. Everything else, every cycle list and the + ``total_energy`` record included, is left as it is. Never raises. + """ + summary: dict[str, Any] = { + "ranking_snapshots": 0, "matching_config": False, "model_records": [], + } + try: + history = data.pop("match_ranking_history", None) + if isinstance(history, list): + summary["ranking_snapshots"] = len(history) + summary["matching_config"] = bool(data.pop("matching_config", None)) + dropped: list[str] = [] + for key in ("ml_model_versions", "ml_training_history"): + records = data.get(key) + if not isinstance(records, dict): + continue + for cap in _REMOVED_ML_CAPABILITIES: + if records.pop(cap, None) is not None: + dropped.append(f"{key}.{cap}") + summary["model_records"] = dropped + except Exception: # pylint: disable=broad-exception-caught + _LOGGER.debug("Dropping removed ML state failed", exc_info=True) + return summary + + +def _repair_answered_feedback_provenance(data: JSONDict) -> dict[str, int]: + """Stamp ``label_source="manual"`` on cycles the user answered in the review queue. + + For every ``feedback_history`` record where the user confirmed the detected + programme or corrected it, the user's answer is ground truth: + + * the cycle still carries that answer -> stamp it ``manual``; + * the panel's Auto-label replaced it (``label_source == "auto_label_service"`` + with the answer saved as ``original_auto_label``) -> put the answer back, + keep the matcher's replacement as ``original_auto_label``, stamp ``manual``. + + A cycle relabelled by hand since (already ``manual``) and dismissed requests + are left alone. Never raises. + """ + summary = {"stamped": 0, "restored": 0} + try: + history = data.get("feedback_history") + past = data.get("past_cycles") + if not isinstance(history, dict) or not isinstance(past, list): + return summary + by_id = {c.get("id"): c for c in past if isinstance(c, dict)} + for cycle_id, record in history.items(): + if not isinstance(record, dict): + continue + if record.get("user_confirmed"): + answer = record.get("original_detected_profile") + else: + answer = record.get("corrected_profile") + if not isinstance(answer, str) or not answer: + continue + cycle = by_id.get(record.get("cycle_id") or cycle_id) + if cycle is None or cycle.get("label_source") == "manual": + continue + if cycle.get("profile_name") == answer: + cycle["label_source"] = "manual" + summary["stamped"] += 1 + elif ( + cycle.get("label_source") == "auto_label_service" + and cycle.get("original_auto_label") == answer + ): + cycle["original_auto_label"] = cycle.get("profile_name") + cycle["profile_name"] = answer + cycle["label_source"] = "manual" + summary["restored"] += 1 + except Exception: # noqa: BLE001 - a repair must never cost the user their store + _LOGGER.warning("Review-answer provenance repair failed", exc_info=True) + return summary + + +def _dismiss_unneeded_feedback(data: JSONDict) -> dict[str, int]: + """Drop pending review requests the 0.5.8 feedback rule would not raise. + + Kept: an unlabelled cycle (the label gate refused it), a cycle labelled with a + different programme than the request names (two opinions, the user decides), a + margin under ``MATCH_LABEL_MIN_MARGIN`` in the stored ranking, a ranking whose + top is not the detected programme, and a programme still in warm-up. + Dropped: everything else, plus requests whose cycle is no longer stored or was + labelled by hand since. Nothing is written to ``feedback_history``: the user + did not answer, so no answer is recorded and label provenance is unchanged. + Never raises. + """ + summary = {"dismissed": 0, "answered": 0, "stale": 0, "kept": 0} + try: + pending = data.get("pending_feedback") + past = data.get("past_cycles") + if not isinstance(pending, dict) or not isinstance(past, list): + return summary + by_id = {c.get("id"): c for c in past if isinstance(c, dict)} + labelled = collections.Counter( + c.get("profile_name") for c in past + if isinstance(c, dict) and c.get("profile_name") + ) + imported = { + c.get("profile_name") for c in (data.get("reference_cycles") or []) + if isinstance(c, dict) and c.get("profile_name") + } + for key, rec in list(pending.items()): + if not isinstance(rec, dict): + continue + cycle = by_id.get(rec.get("cycle_id") or key) + if cycle is None: + del pending[key] + summary["stale"] += 1 + continue + if cycle.get("label_source") == "manual": + del pending[key] + summary["answered"] += 1 + continue + detected = rec.get("detected_profile") + ranking = [r for r in (rec.get("ranking") or []) if isinstance(r, dict)] + scores = [ + float(r["score"]) for r in ranking + if isinstance(r.get("score"), (int, float)) and not isinstance(r.get("score"), bool) + ] + margin = (scores[0] - scores[1]) if len(scores) > 1 else (scores[0] if scores else 0.0) + keep = ( + not detected + or cycle.get("profile_name") != detected + or not ranking + or ranking[0].get("name") != detected + or margin < MATCH_LABEL_MIN_MARGIN + or (labelled[detected] < CONF_PROFILE_MIN_WARMUP_CYCLES and detected not in imported) + ) + if keep: + summary["kept"] += 1 + continue + del pending[key] + summary["dismissed"] += 1 + except Exception: # noqa: BLE001 - a cleanup must never cost the user their store + _LOGGER.warning("Review-queue cleanup failed", exc_info=True) + return summary + + +#: A cycle's ``manual_duration`` is trusted only inside this multiple of its own +#: trace span (audit MATCH-EVAL-17). +MANUAL_DURATION_SPAN_BOUNDS = (0.3, 3.0) + + +def _sane_manual_duration(value: Any, trace_span_s: float) -> float | None: + """``value`` as seconds when it is plausible for a trace spanning ``trace_span_s``. + + ``manual_duration`` overrides the duration a cycle shapes its profile with, + unchecked. One export held 385200 s and 378000 s on 107- and 105-minute traces, + exactly 60x (seconds stored as minutes twice), which put the profile's + ``avg_duration`` at 36-71 h: Stage 1 then rejected every cycle of it and its ETA + was useless (audit MATCH-EVAL-17). A correction is a trim or an extension of + the recorded run, so outside :data:`MANUAL_DURATION_SPAN_BOUNDS` of the trace it + is a unit slip, not a correction: None, and the caller uses the trace. With no + span to judge by (a trace of one instant) the value is trusted as before. + """ + try: + dur = float(value) + except (TypeError, ValueError, OverflowError): + return None + if not math.isfinite(dur) or dur <= 0: + return None + lo, hi = MANUAL_DURATION_SPAN_BOUNDS + if trace_span_s > 0 and not lo * trace_span_s <= dur <= hi * trace_span_s: + return None + return dur + + +def envelope_shape_count(envelope: Any) -> int: + """How many cycles shaped ``envelope`` (``shape_cycle_count``). + + The envelope's ``cycle_count`` is a USAGE figure: once any reference or + backfill cycle exists it counts real cycles only (and counts them even when + the evidence choice keeps real cycles out of the curve). Gating the envelope + template on it made a backfill-only or store-only profile match one sample + cycle, never the envelope its cycles built (audit MR-11). An envelope built + before ``shape_cycle_count`` existed falls back to ``cycle_count``. + """ + if not isinstance(envelope, dict): + return 0 + raw = envelope.get("shape_cycle_count", envelope.get("cycle_count")) + try: + return int(raw or 0) + except (TypeError, ValueError, OverflowError): + return 0 + + def longest_candidate_duration(candidates: Any) -> float: """Longest expected duration among the candidates still in play. @@ -1012,6 +1382,14 @@ def longest_candidate_duration(candidates: Any) -> float: return longest +class _EnvelopeBuild(tuple): # type: ignore[type-arg] + """``(envelope result, durations)`` from ``_rebuild_envelope_sync``, plus the + members' Stage-4 energy reference (``analysis.member_energy_reference``) as + ``energy_ref``. Still unpacks as the 2-tuple every caller reads.""" + + energy_ref: dict[str, Any] | None = None + + def collapse_group_candidates( candidates: list[dict], group_members: dict[str, list[str]] ) -> list[dict]: @@ -1069,64 +1447,51 @@ def _ambiguity_from_candidates(candidates: list[dict]) -> tuple[float, bool]: def _match_prefix_ambiguity( - candidates: list[dict], best_duration: float -) -> tuple[bool, bool]: - """``(full_shape_hit, prefix_fit_hit)`` for the prefix-landscape guard. + candidates: list[dict], + best_duration: float, + pauses_below: Callable[[str], bool | None] | None = None, +) -> bool: + """The #288 prefix-landscape term: ``MatchResult.is_prefix_ambiguous_full_shape``. - Both terms answer the same question - "might this trace be a *prefix* of a - longer programme rather than a complete short one?" - by two different routes, - and either one blocks Smart Termination: + "Might this trace be a *prefix* of a longer programme rather than a complete + short one?" - True when a non-winning candidate is more than + ``SMART_TERM_LANDSCAPE_RATIO`` x the winner's duration and still scored + ``SMART_TERM_LANDSCAPE_MIN_SHAPE`` against its **full** envelope. Read only + by the anti-crease finalize (audit LIVE-18 took it off the ENDING gates). The + #364 prefix-fit term that used to be returned beside it was removed in 0.5.8 + (see the #364 block in const.py). - * ``full_shape_hit`` (#288, unchanged): a non-winning candidate is at least - ``SMART_TERM_LANDSCAPE_RATIO`` longer than the winner and still scored - ``SMART_TERM_LANDSCAPE_MIN_SHAPE`` against its **full** envelope. - * ``prefix_fit_hit`` (#364): a longer candidate's score against its curve - **truncated to the elapsed duration** (``prefix_score``, computed in - ``analysis.annotate_prefix_scores``) beats the winner's own score by - ``SMART_TERM_PREFIX_MARGIN``. The margin is the load-bearing term - it is - scale-free, and asks whether the longer programme explains the trace - materially better than the short one does. The absolute floor only rejects - candidates that fit nothing. + Pure function, no I/O. - The full-envelope term alone has three structural false negatives on real - devices (see the #364 block in const.py); the prefix term exists because a - trace part-way through a longer programme cannot score well against that - programme's whole curve. Returned separately because the widened verdict is - only safe for the ENDING gate - see ``MatchResult.is_prefix_ambiguous_full_shape``. - - Pure function, no I/O. ``prefix_score`` absent (Stage 4 skipped, a mocked - executor, an older snapshot) degrades to the legacy term alone, so this can - never fire *less* often than the #288 predicate did. + ``pauses_below`` (#424) maps a candidate name to whether that programme has + ever paused below the stop threshold mid-cycle (``ProfileStore. + profile_pauses_below``). A candidate it answers ``False`` for cannot be the + programme this quiet belongs to, so it does not count. ``None`` (no + callable, or no traced evidence for that name) keeps the old behaviour - see + ``SMART_TERM_PREFIX_MIN_PAUSE_S``. A collapsed Stage-5 family arrives under + its ``__group__`` key, which owns no cycles, so it always reads as no + evidence and keeps the guard: deliberately, since any one sibling pausing + would have to keep it anyway. """ if best_duration <= 0 or len(candidates) < 2: - return False, False - # Compare against the winner's SHAPE score, not its blended final score: prefix_score - # is a shape-scale value (Stage-2 + Stage-3, no Stage-4 duration/energy agreement), so - # measuring the margin against the blended score mixed scales and made 0.15 too strict. - # Fall back to the blended score only when shape_score is absent (older snapshot). - _best_shape = candidates[0].get("shape_score") - best_score = float( - (_best_shape if _best_shape is not None else candidates[0].get("score")) or 0.0 - ) - full_shape_hit = False - prefix_fit_hit = False + return False for cand in candidates[1:]: prof_dur = float(cand.get("profile_duration") or 0) - if prof_dur > best_duration * SMART_TERM_LANDSCAPE_RATIO and float( - cand.get("shape_score", cand.get("score", 0)) - ) >= SMART_TERM_LANDSCAPE_MIN_SHAPE: - full_shape_hit = True - prefix_score = cand.get("prefix_score") - if ( - prefix_score is not None - and prof_dur > best_duration * SMART_TERM_PREFIX_MIN_RATIO - and float(prefix_score) >= SMART_TERM_PREFIX_MIN_SHAPE - and float(prefix_score) >= best_score + SMART_TERM_PREFIX_MARGIN + if not ( + prof_dur > best_duration * SMART_TERM_LANDSCAPE_RATIO + and float(cand.get("shape_score", cand.get("score", 0))) + >= SMART_TERM_LANDSCAPE_MIN_SHAPE ): - prefix_fit_hit = True - if full_shape_hit and prefix_fit_hit: - break - return full_shape_hit, prefix_fit_hit + continue + if pauses_below is not None: + try: + paused = pauses_below(str(cand.get("name") or "")) + except Exception: # noqa: BLE001 - no opinion, keep the guard + paused = None + if paused is False: + continue + return True + return False # ── Selective export/import taxonomy ──────────────────────────────────────────── @@ -1148,24 +1513,32 @@ _EXPORT_CATEGORIES: dict[str, dict[str, Any]] = { "group_by": "profile_name"}, "reference_cycles": {"keys": ["reference_cycles"], "kind": "list", "enumerable": True, "group_by": "profile_name"}, + # Cycles recovered from this machine's raw power history (#344). Device-specific: + # a backfill cycle claims to be THIS appliance's own past, which a different + # device type's history is not (register item 129e). + "backfill_cycles": {"keys": ["backfill_cycles"], "kind": "list", "enumerable": True, + "group_by": "profile_name", "device_specific": True}, "custom_phases": {"keys": ["custom_phases"], "kind": "list"}, "profile_groups": {"keys": ["profile_groups"], "kind": "dict"}, - "matching_config": {"keys": ["matching_config"], "kind": "dict", "device_specific": True}, + # (`matching_config`, the matcher weight tuner's record, was a category until + # the tuner was removed in 0.5.8; an older export's copy is no longer offered.) "suggestions": {"keys": ["suggestions", "suggestion_apply_cycle_count"], "kind": "mixed"}, "feedback": {"keys": ["feedback_history", "pending_feedback"], "kind": "dict"}, - "maintenance_log": {"keys": ["maintenance_log"], "kind": "list"}, + # The custom task definitions travel with the log entries that reference them + # (#461). The preset types' counting baselines do not: they are re-stamped on + # the target at its next config load. + "maintenance_log": {"keys": ["maintenance_log", "maintenance_tasks"], "kind": "list"}, "ml_models": {"keys": ["ml_model_versions", "ml_training_history", "ml_last_training_run"], "kind": "mixed", "device_specific": True}, - "history_logs": {"keys": ["auto_adjustments", "match_ranking_history", "settings_changelog"], - "kind": "list"}, + "history_logs": {"keys": ["auto_adjustments", "settings_changelog"], "kind": "list"}, "lifetime_stats": {"keys": ["lifetime_energy_wh", "lifetime_cycle_count"], "kind": "scalar"}, "settings": {"keys": [], "kind": "options", "device_specific": True}, } -# Categories whose payload rides at the envelope level (entry_options), not inside "data". -_ENVELOPE_LEVEL_CATEGORIES = frozenset({"settings"}) # Enumerable/structural categories handled explicitly (not as plain leaf keys). -_STRUCTURAL_CATEGORIES = frozenset({"profiles", "real_cycles", "reference_cycles", "settings"}) +_STRUCTURAL_CATEGORIES = frozenset( + {"profiles", "real_cycles", "reference_cycles", "backfill_cycles", "settings"} +) def _strip_redacted_dict(d: Any) -> dict[str, Any]: @@ -1182,13 +1555,7 @@ def _shareable_settings(opts: Any) -> dict[str, Any]: only options that may travel with an export are recognition/matching thresholds, never identity keys or the power-sensor binding. """ - if not isinstance(opts, dict): - return {} - return { - k: opts[k] - for k in SHAREABLE_SETTING_KEYS - if k in opts and isinstance(opts[k], (int, float)) and not isinstance(opts[k], bool) - } + return sanitize_shared_settings(opts) def _selected_categories(selection: Any) -> set[str]: @@ -1237,16 +1604,19 @@ def _scan_data(data_dict: Any, entry_options: Any = None) -> dict[str, dict[str, profiles = dd.get("profiles") if isinstance(dd.get("profiles"), dict) else {} past = dd.get("past_cycles") if isinstance(dd.get("past_cycles"), list) else [] refs = dd.get("reference_cycles") if isinstance(dd.get("reference_cycles"), list) else [] + backfill = dd.get("backfill_cycles") if isinstance(dd.get("backfill_cycles"), list) else [] past_by = _cycles_by_profile(past) ref_by = _cycles_by_profile(refs) + backfill_by = _cycles_by_profile(backfill) - # profiles: each profile with its local real/reference cycle counts + # profiles: each profile with its local real/reference/backfill cycle counts prof_items = [ { "name": name, "real_cycles": len(past_by.get(name, [])), "reference_cycles": len(ref_by.get(name, [])), + "backfill_cycles": len(backfill_by.get(name, [])), } for name in profiles ] @@ -1275,6 +1645,9 @@ def _scan_data(data_dict: Any, entry_options: Any = None) -> dict[str, dict[str, out["real_cycles"] = {"present": bool(past), "count": len(past), "groups": _groups(past_by)} out["reference_cycles"] = {"present": bool(refs), "count": len(refs), "groups": _groups(ref_by)} + out["backfill_cycles"] = { + "present": bool(backfill), "count": len(backfill), "groups": _groups(backfill_by) + } # leaf categories (everything except the structural ones above + settings) for cat_id, spec in _EXPORT_CATEGORIES.items(): @@ -1306,7 +1679,7 @@ def _scan_data(data_dict: Any, entry_options: Any = None) -> dict[str, dict[str, def _export_predates_banked_tail_repair(meta: dict[str, Any]) -> bool: - """Does this import payload predate the v12 -> v13 banked-tail repair? + """Does this import payload predate the current banked-tail repair (v14)? Only `WashDataStore._async_migrate_func` sets `BANKED_TAIL_REPAIR_KEY`, and only on storage LOAD - the import paths assign `_data` directly and never run @@ -1321,11 +1694,26 @@ def _export_predates_banked_tail_repair(meta: dict[str, Any]) -> bool: it on an unrepaired one is permanent. """ try: - return int(meta.get("version") or 1) < 13 + return int(meta.get("version") or 1) < 14 except (TypeError, ValueError, OverflowError): return True +#: Shape each core store key must have for an import payload to count as carrying it. +_IMPORT_CORE_KEY_TYPES: dict[str, type] = { + "profiles": dict, + "past_cycles": list, + "reference_cycles": list, + "backfill_cycles": list, + "envelopes": dict, +} + +#: Keys a wholesale import never keeps from the target even when the payload lacks +#: them. Envelopes are derived from the payload's profiles and cycles (a target one +#: would describe a profile the import replaced); the in-flight cycle is per-install. +_WHOLESALE_IMPORT_PAYLOAD_OWNED_KEYS = frozenset({"envelopes", "active_cycle", "last_active_save"}) + + def unwrap_import_payload(payload: Any) -> tuple[dict[str, Any], dict[str, Any]]: """Normalize any supported export/import wrapper into ``(data_dict, meta)``. @@ -1338,8 +1726,9 @@ def unwrap_import_payload(payload: Any) -> tuple[dict[str, Any], dict[str, Any]] Returns a shallow-copied, shape-repaired ``data_dict`` (so the caller can mutate or assign it without aliasing the input) plus a ``meta`` dict carrying format, version, redaction-stripped ``entry_data``/``entry_options``, the device - fingerprint and the export timestamp. Raises ``ValueError`` on a payload that - is not a usable object. + fingerprint, the export timestamp and ``supplied_keys`` (the data keys the + payload actually carried, as opposed to ones the shape repair filled in). + Raises ``ValueError`` on a payload that is not a usable object. """ if isinstance(payload, dict) and "home_assistant" in payload and "data" in payload: payload = payload["data"] @@ -1355,14 +1744,25 @@ def unwrap_import_payload(payload: Any) -> tuple[dict[str, Any], dict[str, Any]] "past_cycles": payload.get("past_cycles", []), "envelopes": payload.get("envelopes", {}), } + present = {k for k in data_dict if k in payload} fmt = "v1" else: data = payload.get("data") if not isinstance(data, dict): raise ValueError("Invalid export payload (missing or invalid 'data' key)") data_dict = dict(data) # shallow copy: caller may assign/mutate freely + present = set(data_dict) fmt = "v2" + # What the payload really carried, read before the repair below fills the core + # keys in: a null, or a core key of the wrong type, carried nothing. The + # wholesale import keeps every target key outside this set (register item 195). + supplied_keys = frozenset( + key for key in present + if data_dict.get(key) is not None + and isinstance(data_dict.get(key), _IMPORT_CORE_KEY_TYPES.get(key, object)) + ) + # Repair top-level shape so downstream code can assume the core keys. if not isinstance(data_dict.get("profiles"), dict): data_dict["profiles"] = {} @@ -1373,6 +1773,9 @@ def unwrap_import_payload(payload: Any) -> tuple[dict[str, Any], dict[str, Any]] if not isinstance(data_dict.get("backfill_cycles"), list): data_dict["backfill_cycles"] = [] data_dict.setdefault("envelopes", {}) + # An export from before 0.5.8 still carries the state of the ML parts removed + # then (storage v17); every import path goes through here, so drop it once. + _drop_removed_ml_state(data_dict) fingerprint = payload.get("device_fingerprint") meta: dict[str, Any] = { @@ -1382,6 +1785,7 @@ def unwrap_import_payload(payload: Any) -> tuple[dict[str, Any], dict[str, Any]] "entry_options": _strip_redacted_dict(payload.get("entry_options", {})), "device_fingerprint": fingerprint if isinstance(fingerprint, dict) else {}, "exported_at": payload.get("exported_at"), + "supplied_keys": supplied_keys, } return data_dict, meta @@ -1458,7 +1862,7 @@ def _usable_reference_pairs(points: Any) -> list[list[float]] | None: continue try: x, y = float(p[0]), float(p[1]) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): continue if np.isfinite(x) and np.isfinite(y): pairs.append([x, y]) @@ -1470,6 +1874,75 @@ def _usable_reference_pairs(points: Any) -> list[list[float]] | None: return pairs +#: Community-store import quality bounds (audit STORE-03). The census of 427 +#: harvested store cycles held 2-point straight lines, a 36 s "cycle", a 77 h wool +#: wash and traces with one gap over 30 min; each enters the profile's envelope. +STORE_TRACE_MIN_POINTS = 30 +STORE_TRACE_MAX_SPAN_S = 6 * 3600.0 +STORE_TRACE_MIN_GAP_CAP_S = 900.0 +STORE_TRACE_GAP_CAP_FRAC = 0.10 + + +def _store_trace_ok(pairs: list[list[float]]) -> bool: + """Whether a validated store trace is fit to shape a profile (audit STORE-03). + + Span in (0, 6 h], at least 30 points, no single gap over max(15 min, 10% of + the span), and a peak above the degenerate-power floor. + """ + if len(pairs) < STORE_TRACE_MIN_POINTS: + return False + span = float(pairs[-1][0] - pairs[0][0]) + if not 0.0 < span <= STORE_TRACE_MAX_SPAN_S: + return False + gaps = np.diff(np.asarray([p[0] for p in pairs], dtype=float)) + if gaps.size and float(gaps.max()) > max( + STORE_TRACE_MIN_GAP_CAP_S, STORE_TRACE_GAP_CAP_FRAC * span + ): + return False + return max(p[1] for p in pairs) >= _DEGENERATE_POWER_FLOOR + + +def trace_content_hash(pairs: Any) -> str | None: + """Profile-agnostic hash of a trace's content (audit STORE-05). + + Offsets re-based to the first sample and rounded to the second, watts to the + watt, so the same recording filed under another program name, device or id + hashes alike. None for a trace with fewer than two usable points. + """ + pts = _usable_reference_pairs(pairs) + if pts is None: + return None + t0 = pts[0][0] + body = ";".join(f"{round(t - t0)}:{round(w)}" for t, w in pts) + return hashlib.sha1(body.encode(), usedforsecurity=False).hexdigest() + + +def _is_store_import(cycle: Any) -> bool: + meta = cycle.get("meta") if isinstance(cycle, dict) else None + src = meta.get("source") if isinstance(meta, dict) else None + return isinstance(src, str) and src.startswith("store") + + +def _effective_golden_flags(cycles: list[Any]) -> list[bool]: + """Which of a profile's cycles define its reference shape and template. + + The user's own golden cycles (pinned, or recorded). A community-store import + counts only in a profile with no cycle of the user's own (audit STORE-04): + forced golden, one import became the shape every real cycle was warped onto + and the matching template, costing 31-43% of the user's own margins. + """ + user = [ + bool(isinstance(c.get("ml_review"), dict) and c["ml_review"].get("golden")) + and not _is_store_import(c) + for c in cycles + ] + if any(user): + return user + if cycles and all(_is_store_import(c) for c in cycles): + return [True] * len(cycles) + return [False] * len(cycles) + + def _merge_list_dedup(base: list[Any], incoming: list[Any]) -> None: """Append items from ``incoming`` to ``base`` in place, skipping duplicates. @@ -1481,7 +1954,7 @@ def _merge_list_dedup(base: list[Any], incoming: list[Any]) -> None: for x in base: try: seen_sigs.add(json.dumps(x, sort_keys=True, default=str)) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): pass for item in incoming: if isinstance(item, dict) and item.get("id") is not None: @@ -1493,7 +1966,7 @@ def _merge_list_dedup(base: list[Any], incoming: list[Any]) -> None: continue try: sig = json.dumps(item, sort_keys=True, default=str) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): sig = None if sig is not None: if sig in seen_sigs: @@ -1502,6 +1975,90 @@ def _merge_list_dedup(base: list[Any], incoming: list[Any]) -> None: base.append(item) +def _last_high_block( + times: list[float], powers: list[float], level: float +) -> tuple[float, float, float] | None: + """``(start_frac, seconds, start_offset_s)`` of the LAST contiguous run above + ``level`` in one trace, or None (#399). The measurement behind + :meth:`ProfileStore.profile_terminal_high_block`, applied to the envelope's + ``max`` band, the sample cycle or one member's own trace (register item 207). + """ + # 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) + + class ProfileStore: """Manages storage of washer profiles and past cycles.""" @@ -1512,7 +2069,6 @@ class ProfileStore: min_duration_ratio: float = 0.50, max_duration_ratio: float = 1.50, save_debug_traces: bool = False, - match_threshold: float = 0.4, unmatch_threshold: float = 0.35, device_name: str = "", ) -> None: @@ -1522,7 +2078,6 @@ class ProfileStore: self._logger = DeviceLoggerAdapter(_LOGGER, device_name) self._min_duration_ratio = min_duration_ratio self._max_duration_ratio = max_duration_ratio - self._match_threshold = match_threshold self._unmatch_threshold = unmatch_threshold self.dtw_bandwidth: float = DEFAULT_DTW_BANDWIDTH # Stage-4 energy-agreement mode ("mean"|"integrated"); the manager sets it @@ -1535,9 +2090,16 @@ class ProfileStore: self._evidence_sources: tuple[str, ...] = tuple(PROFILE_EVIDENCE_SOURCES) self._save_debug_traces = save_debug_traces - # Cache for resampled sample segments: key=(cycle_id, dt) + # Cache for resampled sample segments: key=(cycle_id, dt), least recently + # used first, bounded by _sample_segment_cache_cap (audit ML-12). Written + # from the Playground's executor thread too, so every access goes through + # the lock (audit PLAYGROUND-22). Never held across an await. self._cached_sample_segments: dict[tuple[str, float], Segment] = {} + self._sample_segment_lock = threading.Lock() self._terminal_quiet_cache: dict[str, tuple[tuple[int, str, int], float | None]] = {} + # Legacy (unstamped) maintenance entries: task id -> ((log time, odometer, + # record count), cycles since). See _maintenance_since. + self._legacy_maintenance_cache: dict[str, tuple[tuple[str, int, int], int]] = {} # Cache for group cohesion scores to avoid re-running DTW on the event loop # every 5 minutes. Keyed by sorted-members tuple; invalidated when profile_groups # content changes (tracked by a simple generation counter). @@ -1550,16 +2112,30 @@ class ProfileStore: # duplicate the DTW work. A plain thread lock, never held across an await. self._cohesion_cache_lock = threading.Lock() # Profile duration tolerance (set by manager; reserved for duration-based heuristics) - self._duration_tolerance: float = 0.25 # Retention policy: cap total cycles and number of full-resolution traces per profile - self._max_past_cycles = DEFAULT_MAX_PAST_CYCLES - self._max_full_traces_per_profile = DEFAULT_MAX_FULL_TRACES_PER_PROFILE - self._max_full_traces_unlabeled = DEFAULT_MAX_FULL_TRACES_UNLABELED # Separate store for each entry to avoid giant files # Use WashDataStore to handle migration self._store: Store[JSONDict] = WashDataStore( hass, STORAGE_VERSION, f"{STORAGE_KEY}.{entry_id}" ) + # The in-flight cycle snapshot lives in its own small file (audit PERF-03 / + # MANAGER-07): written every 60 s of every cycle, it used to rewrite the + # whole store - 6.7 MB per save, 1.1 GB over one 4 h cycle on the loop. + self._active_store: Store[JSONDict] = Store( + hass, 1, f"{STORAGE_KEY}.{entry_id}.active" + ) + self._active_data: JSONDict = {} + # The last snapshot that failed to restore (register item 266 follow-up): + # written only on a failure, read only by the diagnostics download. + self._failed_restore_store: Store[JSONDict] = Store( + hass, 1, f"{STORAGE_KEY}.{entry_id}.{FAILED_RESTORE_STORE_SUFFIX}" + ) + # "Undo last import" restore point (register item 195), its own file for the + # same reason as the active cycle. Status reads are memoised: Store does not + # cache a completed load, and the record is a whole store. + self._pre_import_store: Store[JSONDict] = self._new_pre_import_store() + self._pre_import_meta: JSONDict | None = None + self._pre_import_meta_loaded = False self._data: JSONDict = { "profiles": {}, "past_cycles": [], @@ -1745,7 +2321,7 @@ class ProfileStore: return {} async def set_ml_model_version(self, capability: str, record: dict[str, Any]) -> None: - """Persist a trained model record for a capability (quality/live_match/end).""" + """Persist a trained model record for a capability (``total_energy`` since 0.5.8).""" versions: JSONDict = self._data.setdefault("ml_model_versions", {}) versions[capability] = record await self.async_save() @@ -1773,20 +2349,24 @@ class ProfileStore: def get_ml_training_history(self) -> dict[str, list[dict[str, Any]]]: """Per-capability held-out-score history across training runs. - ``{capability: [{"ts": iso, "score": float, "higher_better": bool}, ...]}`` - oldest-first. Lets the panel show whether a model's fit is improving, - steady, or declining over time. Empty until training has run. + ``{capability: [{"ts": iso, "promoted": bool, "score": float, + "higher_better": bool, "reason_code": str, "reason_params": dict}, ...]}`` + oldest-first, one entry per run. ``score`` is absent when the run had no + held-out metric, ``reason_code`` when it promoted. Entries written before + 0.5.8 carry no ``promoted``. Empty until training has run. """ raw = self._data.get("ml_training_history") return cast(dict[str, list[dict[str, Any]]], raw) if isinstance(raw, dict) else {} async def append_ml_training_history(self, run_iso: str, results: list[dict[str, Any]]) -> None: - """Append each capability's held-out score from a training run. + """Append one entry per capability for a training run. ``results`` is ``train_from_cycles``'s per-capability records; classifiers report ``new_auc`` (higher is better), regressors ``model_mae`` (lower is - better). Capabilities without a held-out metric this run (insufficient - data) are skipped. Retains at most ``ML_TRAINING_HISTORY_MAX`` runs each. + better). Every run is recorded, with whether it promoted and, when it did + not, why (audit ML-20: the panel's trend badge read every candidate, + promoted or not, and a run that promoted nothing left no reason to show). + Retains at most ``ML_TRAINING_HISTORY_MAX`` runs each. """ from .const import ML_TRAINING_HISTORY_MAX # noqa: PLC0415 @@ -1798,69 +2378,36 @@ class ProfileStore: cap = rec.get("capability") if not isinstance(cap, str) or not cap: continue + entry: JSONDict = {"ts": run_iso, "promoted": bool(rec.get("promoted"))} if rec.get("new_auc") is not None: - score, higher_better = float(rec["new_auc"]), True + entry.update(score=round(float(rec["new_auc"]), 5), higher_better=True) elif rec.get("model_mae") is not None: - score, higher_better = float(rec["model_mae"]), False - else: - continue + entry.update(score=round(float(rec["model_mae"]), 5), higher_better=False) + if not entry["promoted"]: + params = rec.get("reason_params") + entry["reason_code"] = str(rec.get("reason_code") or "unknown") + entry["reason_params"] = dict(params) if isinstance(params, dict) else {} + entry["reason"] = str(rec.get("reason") or "") series = hist.setdefault(cap, []) if not isinstance(series, list): series = hist[cap] = [] - series.append({"ts": run_iso, "score": round(score, 5), "higher_better": higher_better}) + series.append(entry) del series[:-ML_TRAINING_HISTORY_MAX] changed = True if changed: await self.async_save() - # ─── On-device matching-config tuning (Stage 4/5, opt-in) ────────────────── + def _matching_overrides(self) -> dict[str, Any]: + """Matcher config merged last by ``async_match_profile``: none live. - #: Only these bounded scoring weights (all in [0, 1]) may be overridden - #: on-device, so a stored record can never change structural matching - #: behaviour. Mirrors ml.matching_tuner.OVERRIDE_KEYS. - _MATCHING_OVERRIDE_KEYS = ("corr_weight", "duration_weight", "energy_weight", "dtw_ensemble_w") - - def get_matching_config(self) -> dict[str, Any]: - """Return the on-device tuned matcher scoring-weight record, if any. - - Record shape: ``{config, trained_at, cycle_count, baseline_test_top1, - tuned_test_top1}`` where ``config`` holds the bounded scoring weights - (``corr_weight``/``duration_weight``/``energy_weight``). Empty when the - matcher is using the shipped defaults. + The live matcher always runs the shipped defaults. This is the seam the + Playground's ``_SimStore`` overrides to replay with its own config (the + what-if Stage-1 ratios). It also carried the on-device matcher weight + tuner's record until that tuner was removed in 0.5.8 (audit MR-10: it + never promoted on a real export). """ - raw = self._data.get("matching_config") - if isinstance(raw, dict): - return cast(JSONDict, raw).copy() return {} - async def set_matching_config(self, record: dict[str, Any]) -> None: - """Persist the tuned matcher scoring-weight override + metadata.""" - self._data["matching_config"] = record - await self.async_save() - - async def clear_matching_config(self) -> None: - """Revert the matcher to the shipped default scoring weights.""" - self._data.pop("matching_config", None) - await self.async_save() - - def _matching_overrides(self) -> dict[str, float]: - """Bounded scoring-weight overrides to merge into the matcher config. - - Only the whitelisted keys are honoured, each clamped to ``[0, 1]``, so a - stored record can never alter structural matching behaviour - only the - emphasis between shape, level and energy agreement. - """ - rec = self._data.get("matching_config") - cfg = rec.get("config") if isinstance(rec, dict) else None - out: dict[str, float] = {} - if isinstance(cfg, dict): - for k in self._MATCHING_OVERRIDE_KEYS: - try: - out[k] = min(1.0, max(0.0, float(cfg[k]))) - except (KeyError, TypeError, ValueError): - continue - return out - async def set_cycle_review( self, cycle_id: str, @@ -1873,9 +2420,9 @@ class ProfileStore: """Attach an ML-Lab review to a cycle (Stage 4b). The review (``quality``/``golden``/``tags``/``notes``) is stored under the - cycle's ``ml_review`` key and becomes a strong training label for the - on-device quality model. Only the provided fields are updated. Returns - True when the cycle was found and updated. + cycle's ``ml_review`` key (``golden`` pins a reference cycle; the quality + model it once labelled stopped training in 0.5.8). Only the provided fields + are updated. Returns True when the cycle was found and updated. """ cycle = next( (c for c in self._data.get("past_cycles", []) if c.get("id") == cycle_id), @@ -1993,10 +2540,10 @@ class ProfileStore: def get_profile(self, name: str) -> JSONDict | None: - """Return a single profile by name with calculated stats (via list_profiles).""" - # Reuse list_profiles logic to ensure consistency and avoid duplication - all_profiles = self.list_profiles() - return next((p for p in all_profiles if p["name"] == name), None) + """Return a single profile by name with calculated stats (as list_profiles).""" + _rows, by_name = self._profile_summaries() + row = by_name.get(name) + return dict(row) if row is not None else None def get_profiles(self) -> dict[str, JSONDict]: """Return mutable profiles mapping (profile_name -> profile data).""" @@ -2012,30 +2559,16 @@ class ProfileStore: return cast(list[CycleDict], raw) return [] - # ── Community-store account (connect handoff) ───────────────────────────── + # ── Legacy per-device community-store account ───────────────────────────── def get_store_account(self) -> dict[str, Any]: - """Full persisted store account incl. the refresh token (credential).""" + """The pre-global per-device store account incl. the refresh token. + + Read once by ``_migrate_online_to_global``, which hoists it into + ``store_account.py`` and then clears it; nothing writes it any more. + """ raw = self._data.get("store_account") return dict(raw) if isinstance(raw, dict) else {} - def get_store_identity(self) -> dict[str, Any]: - """Safe account view for status/UI - never includes the refresh token.""" - a = self.get_store_account() - return { - "connected": bool(a.get("refresh_token")), - "uid": a.get("uid"), - "name": a.get("name"), - "brand": a.get("brand"), - "model": a.get("model"), - } - - async def set_store_account(self, account: dict[str, Any]) -> None: - """Persist/merge the store account (refresh_token, uid, name, brand, model).""" - cur = self.get_store_account() - cur.update({k: v for k, v in account.items() if v is not None}) - self._data["store_account"] = cur - await self.async_save() - async def clear_store_account(self) -> None: self._data.pop("store_account", None) await self.async_save() @@ -2185,23 +2718,66 @@ class ProfileStore: out.sort(key=lambda r: r.get("start_time") or "", reverse=True) return out + def stored_trace_hashes(self) -> set[str]: + """Content hashes of every stored cycle's trace (audit STORE-05).""" + out: set[str] = set() + for c in self.iter_stored_cycles(): + try: + h = trace_content_hash(decompress_power_data(c)) + except Exception: # noqa: BLE001 - one bad trace must not block imports + h = None + if h: + out.add(h) + return out + + def reference_import_verdict( + self, points: Any, meta: dict[str, Any], known_hashes: set[str] | None = None + ) -> str: + """``ok``, ``invalid``, ``low_quality`` or ``duplicate`` for an import. + + The quality and duplicate checks apply to community-store downloads only + (``meta.community``, set by the store bridge). Not ``store_cycle_id``: the + selective import passes one for the user's own exported cycles too, and + the user's own data is never second-guessed. + """ + pairs = _usable_reference_pairs(points) + if pairs is None: + return "invalid" + if not meta.get("community"): + return "ok" + if not _store_trace_ok(pairs): + return "low_quality" + h = trace_content_hash(pairs) + hashes = known_hashes if known_hashes is not None else self.stored_trace_hashes() + if h and h in hashes: + return "duplicate" + return "ok" + def _add_reference_cycle_nosave( self, profile_name: str, points: list[list[float]], meta: dict[str, Any], - *, id_pool: set[Any] | None = None, + *, id_pool: set[Any] | None = None, known_hashes: set[str] | None = None, ) -> str: """Validate + append one reference cycle. NO envelope rebuild, NO save. The mutation core shared by ``add_reference_cycle`` (single-cycle: rebuild + save after) and ``async_import_data_selective`` (bulk: rebuild each touched profile once + a single save at the end). Returns the new cycle id, or ``""`` - when the trace is unusable (mutating nothing). + when the trace is unusable, or a store trace fails the quality bar or is + already stored (mutating nothing). ``known_hashes`` lets a bulk caller hash + the store once; it is updated with each import, so a bundle dedups itself. """ # Validate the trace BEFORE creating any persistent state (profile entry / reference # cycle): a garbage trace returns "" and mutates nothing. Same rule the import guard # uses to decide whether a replace has anything usable to refill a destination with. + if self.reference_import_verdict(points, meta, known_hashes) != "ok": + return "" pairs = _usable_reference_pairs(points) if pairs is None: return "" + if known_hashes is not None and meta.get("community"): + h = trace_content_hash(pairs) + if h: + known_hashes.add(h) duration = float(pairs[-1][0] - pairs[0][0]) # Re-base to offset 0 so envelope reconstruction and DTW work correctly. if pairs[0][0] != 0.0: @@ -2218,7 +2794,8 @@ class ProfileStore: "end_time": (now + timedelta(seconds=duration)).isoformat(), "duration": duration, "status": "completed", - "ml_review": {"golden": True}, + # Not golden (audit STORE-04): `_effective_golden_flags` lets a store + # import define the shape only in a profile with none of the user's own. "meta": { "source": f"store:{store_id}" if store_id else "store", "store_uploaded_at": meta.get("store_uploaded_at"), @@ -2237,6 +2814,12 @@ class ProfileStore: _reason = meta.get("termination_reason") if isinstance(_reason, str) and _reason: cycle["termination_reason"] = _reason + _manual = meta.get("manual_duration") + if _manual is not None and not isinstance(_manual, bool): + with contextlib.suppress(TypeError, ValueError, OverflowError): + _md = float(_manual) + if math.isfinite(_md) and _md > 0: + cycle["manual_duration"] = _md # A reference cycle implies its program exists locally; create a minimal profile # entry if absent so the matcher iterates it and the rebuild can set its template. profiles = self._data.setdefault("profiles", {}) @@ -2246,18 +2829,20 @@ class ProfileStore: return str(cycle.get("id", "")) async def add_reference_cycle( - self, profile_name: str, points: list[list[float]], meta: dict[str, Any] + self, profile_name: str, points: list[list[float]], meta: dict[str, Any], + *, known_hashes: set[str] | None = None, ) -> str: """Import a reference cycle downloaded from the store into ``reference_cycles``. ``points`` is a raw trace of ``[offset_seconds, watts]`` pairs. ``meta`` may carry ``store_cycle_id`` (-> ``meta.source = "store:"``), ``store_uploaded_at`` and ``sampling_interval``. The cycle is stamped with import-time timestamps (its real - run time is meaningless locally), forced ``status="completed"`` and - ``ml_review.golden=True`` so it seeds the envelope shape, then the envelope is - rebuilt. Never accumulates lifetime energy or touches ``past_cycles``. + run time is meaningless locally) and forced ``status="completed"``, then the + envelope is rebuilt. Never accumulates lifetime energy or touches ``past_cycles``. """ - cid = self._add_reference_cycle_nosave(profile_name, points, meta) + cid = self._add_reference_cycle_nosave( + profile_name, points, meta, known_hashes=known_hashes + ) if not cid: return "" await self.async_rebuild_envelope(profile_name) @@ -2270,9 +2855,10 @@ class ProfileStore: """Return mutable profile-groups mapping (group_name -> {members, created_at}). A group bundles near-duplicate profiles (e.g. the same program at - different temperature/spin). The matcher treats the group as one - aggregate candidate and only picks the exact member once the group wins. - Groups with fewer than 2 present members are ignored by the matcher. + different temperature/spin). The matcher scores every member on its own + curve, collapses a cohesive group into one family afterwards, and picks the + exact member once the group wins (no aggregate curve since #400). Groups + with fewer than 2 present members are ignored by the matcher. """ raw = self._data.setdefault("profile_groups", {}) if not isinstance(raw, dict): @@ -2391,9 +2977,9 @@ class ProfileStore: """Minimum pairwise shape similarity among a group's members (1.0 = identical shapes; see _shape_similarity for the DTW/peak-normalised metric). Low cohesion means the members are not really the same program, - so their averaged aggregate would be too generic and could out-match - unrelated profiles - the matcher refuses to aggregate below - GROUP_MIN_COHESION and the UI warns the user. + so a family win would say little about which one ran - the matcher does + not map a group below GROUP_MIN_COHESION (its members compete on their + own) and the UI warns the user. Results are cached per member-set and invalidated via ``_cohesion_cache_generation`` (incremented by group mutation methods) so @@ -2409,11 +2995,11 @@ class ProfileStore: curves = [c for c in (self._profile_curve(m) for m in members) if c is not None] if len(members) < 2: # A genuinely single-member group is trivially cohesive (and is never - # collapsed anyway — nothing to aggregate). + # mapped as a family anyway). result = 1.0 elif len(curves) < 2: # Multi-member but too few built curves -> insufficient evidence, treat as - # NOT cohesive so the group isn't collapsed into a blurry aggregate yet. + # NOT cohesive, so its members keep competing on their own for now. result = 0.0 else: result = 1.0 @@ -2459,6 +3045,7 @@ class ProfileStore: self, current_power: list[float], current_duration: float, members: list[str], member_snaps: dict[str, dict[str, Any]], in_progress: bool = False, + preferred: str | None = None, ) -> tuple[str, float | None, float | None]: """Within a winning group, pick the member whose integrated ENERGY best matches the cycle. @@ -2483,6 +3070,12 @@ class ProfileStore: 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. + While the cycle is RUNNING, ``preferred`` - the sibling the full pipeline + scored best (``group_best_member``) - wins instead: mid-cycle the energy + pick lost to it, 29.8% vs 63.8% right member at 90% elapsed over 47 group + wins, and on user groups 51.0% vs 62.7% (audit MATCH-DECIDE-04, MR-04). At + cycle end the energy pick stands; the two measurements there disagree. + 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) @@ -2501,7 +3094,13 @@ class ProfileStore: return 1.0 / (1.0 + abs(math.log(a / b)) / scale) best_m, best_sc, best_dur = members[0], -1.0, None - for m in members: + use_preferred = bool( + in_progress and preferred in members and member_snaps.get(preferred) + ) + if use_preferred: + best_m = str(preferred) + best_dur = float(member_snaps[best_m].get("avg_duration") or 0.0) or None + for m in () if use_preferred else members: snap = member_snaps.get(m) if not snap: continue @@ -2529,72 +3128,6 @@ class ProfileStore: fit = None return best_m, fit, best_dur - def suggest_profile_groups(self, dur_tol: float = 0.60, sim_min: float = 0.85) -> list[dict[str, Any]]: - """Detect clusters of near-duplicate profiles not already fully grouped. - - Two profiles cluster when their durations are within a (loose) ``dur_tol`` - sanity bound AND their DTW/peak-normalised shape similarity exceeds - ``sim_min`` (same program shape; they may differ in temperature/spin and in - phase length, which the DTW alignment tolerates). The duration bound is - loose because higher-temp/lower-rpm variants legitimately run longer - it - only rules out grouping, say, a 20-min rinse with a 3-hour cotton. - Returns {"members": [...], "existing_group": name|None} suggestions the - user confirms. Never mutates state. - """ - profiles = self.get_profiles() - envelopes = self._data.get("envelopes", {}) - # Build per-profile (avg curve resampled to N, avg duration). - N = 150 - reps: dict[str, tuple[np.ndarray, float]] = {} - for name, prof in profiles.items(): - env = envelopes.get(name) if isinstance(envelopes, dict) else None - avg = env.get("avg") if isinstance(env, dict) else None - dur = float(prof.get("avg_duration") or 0.0) - if not avg or dur <= 0 or not isinstance(avg[0], (list, tuple)): - continue - ys = np.asarray([float(p[1]) for p in avg], dtype=float) - if ys.size < 4: - continue - curve = np.interp(np.linspace(0, 1, N), np.linspace(0, 1, ys.size), ys) - reps[name] = (curve, dur) - names = list(reps) - # Union-find near-duplicates. - parent = {n: n for n in names} - def find(x: str) -> str: - while parent[x] != x: - parent[x] = parent[parent[x]]; x = parent[x] - return x - lim = math.log(1.0 + dur_tol) - for i in range(len(names)): - for j in range(i + 1, len(names)): - a, b = names[i], names[j] - (ca, da), (cb, db) = reps[a], reps[b] - if da <= 0 or db <= 0 or abs(math.log(da / db)) > lim: - continue - if self._shape_similarity(ca, cb) > sim_min: - parent[find(a)] = find(b) - clusters: dict[str, list[str]] = {} - for n in names: - clusters.setdefault(find(n), []).append(n) - # Only clusters of >=2; annotate with any existing group overlap; skip - # clusters already fully contained in one group. - out: list[dict[str, Any]] = [] - groups = self.get_profile_groups() - for members in clusters.values(): - if len(members) < 2: - continue - existing = None - for gname, g in groups.items(): - gmembers = set(g.get("members") or []) - if gmembers & set(members): - existing = gname - if set(members) <= gmembers: - members = [] # already grouped - break - if members: - out.append({"members": sorted(members), "existing_group": existing}) - return out - # ------------------------------------------------------------------ # A1: Underrun anomaly helpers # ------------------------------------------------------------------ @@ -2676,7 +3209,7 @@ class ProfileStore: """ try: add = max(0.0, float(wh)) - except (ValueError, TypeError): + except (ValueError, TypeError, OverflowError): return base = self.get_lifetime_energy_wh() self._data["lifetime_energy_wh"] = round(base + add, 3) @@ -2722,7 +3255,7 @@ class ProfileStore: """ try: value = int(count) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): return if value < 0: return @@ -2835,11 +3368,14 @@ class ProfileStore: ) -> dict[str, Any]: """Append a maintenance event and persist. Returns the created entry. - ``event_type`` must be one of :data:`MAINTENANCE_EVENT_TYPES` (else raises - ``ValueError``). ``date`` defaults to the current timestamp; a short unique - id is generated for the entry. + ``event_type`` must be one of :data:`MAINTENANCE_EVENT_TYPES` or the id of + one of this device's custom tasks (else raises ``ValueError``). ``date`` + defaults to the current timestamp; a short unique id is generated for the + entry. A custom task's entry also records its name at the time, so the log + still reads after the task is renamed or removed (#461). """ - if event_type not in MAINTENANCE_EVENT_TYPES: + custom = self._find_maintenance_task(event_type) + if event_type not in MAINTENANCE_EVENT_TYPES and custom is None: 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 @@ -2860,13 +3396,15 @@ class ProfileStore: "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 + # scan of `past_cycles`, which shrinks when the user deletes a record (and + # was capped at 200 until 0.5.8) - 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), } + if custom is not None: + entry["task_name"] = str(custom.get("name") or "") log = self._data.setdefault("maintenance_log", []) if not isinstance(log, list): log = [] @@ -2892,7 +3430,9 @@ class ProfileStore: # nothing here ever reaches the live detector or matcher - the user publishes # individual values to entry.options explicitly (ws_set_options) if they want # them live. Stored per device because the values are device-scale (watts, - # seconds tuned for THIS appliance). + # seconds tuned for THIS appliance). Stored records are kept as written: a + # key the Playground no longer offers (the pre-0.5.8 matcher weights) is + # filtered when the list is read for the panel, never deleted here. def get_playground_presets(self) -> dict[str, JSONDict]: """Return the mutable playground-presets mapping (name -> record). @@ -2969,8 +3509,8 @@ class ProfileStore: ) -> int: """Stored cycles that started after *since* (all of them if None). - 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 + A scan of the stored history, so it shrinks when a record is deleted (and was + capped at 200 cycles until 0.5.8). 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. @@ -3040,67 +3580,371 @@ class ProfileStore: 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*. + def _latest_maintenance_by_type(self) -> dict[str, tuple[datetime, dict[str, Any]]]: + """The most recent log entry per event type, as ``(date, entry)``. One pass. + + Entries whose date does not parse are skipped, as they always were when + picking the latest event for a type. Never raises. + """ + latest: dict[str, tuple[datetime, dict[str, Any]]] = {} + try: + for e in self._data.get("maintenance_log", []) or []: + if not isinstance(e, dict): + continue + event_type = e.get("event_type") + if not isinstance(event_type, str): + continue + dt = _parse_maintenance_dt(e.get("date")) + if dt is None: + continue + current = latest.get(event_type) + if current is None or dt > current[0]: + latest[event_type] = (dt, e) + except Exception: # noqa: BLE001 + return {} + return latest + + def _maintenance_origin(self, task_id: str) -> tuple[int, datetime | None] | None: + """Where a never-logged task starts counting: ``(odometer, date)``, or None. + + A custom task counts from when it was created or last switched on, a preset + type from the baseline :meth:`sync_maintenance_baselines` stamped when its + reminder first applied. The original five types have no origin: never + logged, they count the whole odometer, as before #461. Never raises. + """ + try: + if task_id.startswith(MAINTENANCE_CUSTOM_TASK_PREFIX): + record = self._find_maintenance_task(task_id) + count, when = ( + (record.get("since_cycle_count"), record.get("since")) + if record is not None else (None, None) + ) + elif task_id in MAINTENANCE_COUNT_FROM_ENABLE_TYPES: + baselines = self._data.get("maintenance_baselines") + stamp = baselines.get(task_id) if isinstance(baselines, dict) else None + count, when = ( + (stamp.get("cycle_count"), stamp.get("date")) + if isinstance(stamp, dict) else (None, None) + ) + else: + return None + if not isinstance(count, (int, float)) or isinstance(count, bool): + return None + return int(count), _parse_maintenance_dt(when) + except Exception: # noqa: BLE001 + return None + + @staticmethod + def _days_since(when: datetime | None) -> float | None: + """Days elapsed since *when* (never negative), or None without a date. + + An interval, so it is taken on UTC: two aware local stamps subtract on + wall-clock fields and are an hour off across a DST change (DETECT-01). + """ + if when is None: + return None + try: + return max(0.0, (utc_now() - dt_util.as_utc(when)).total_seconds() / 86400.0) + except (TypeError, ValueError, OverflowError): + return None + + def _maintenance_since( + self, + task_id: str, + latest: dict[str, tuple[datetime, dict[str, Any]]], + ) -> tuple[int, float | None]: + """``(cycles, days)`` since a task was last done (or started counting). 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). + deletion-proof, which the previous ``past_cycles`` scan was not - past the + old 200-cycle cap 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. - 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. + Never logged: from the task's origin (:meth:`_maintenance_origin`) when it + has one; a preset type or custom task without one has not started counting + yet (0, the next config load stamps it); one of the original five counts the + whole odometer. ``days`` is None when there is no date to count from. + """ + odometer = self.get_lifetime_cycle_count() + hit = latest.get(task_id) + if hit is not None: + when, entry = hit + stamp = entry.get("cycle_count_at_log") + if isinstance(stamp, (int, float)) and not isinstance(stamp, bool): + cycles = max(0, odometer - int(stamp)) + else: + # The date scan reads every stored record, and the maintenance sensor + # asks on every power reading, so it is cached until a cycle is added + # (odometer), deleted (record count) or the task is logged again. + key = (when.isoformat(), odometer, len(self.get_past_cycles())) + cached = self._legacy_maintenance_cache.get(task_id) + if cached is not None and cached[0] == key: + cycles = cached[1] + else: + cycles = self._cycles_after(when) + self._legacy_maintenance_cache[task_id] = (key, cycles) + return cycles, self._days_since(when) + origin = self._maintenance_origin(task_id) + if origin is not None: + count, when = origin + return max(0, odometer - count), self._days_since(when) + if ( + task_id in MAINTENANCE_COUNT_FROM_ENABLE_TYPES + or task_id.startswith(MAINTENANCE_CUSTOM_TASK_PREFIX) + ): + return 0, None + return odometer, None - 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. + def cycles_since_maintenance(self, event_type: str) -> int: + """Cycles this appliance has run since it was last serviced for *event_type*. + + See :meth:`_maintenance_since`. 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: - continue - dt = _parse_maintenance_dt(e.get("date")) - if dt is not None and (latest_dt is None or dt > latest_dt): - latest_dt = dt - 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) + return self._maintenance_since( + str(event_type), self._latest_maintenance_by_type() + )[0] except Exception: # noqa: BLE001 return 0 + def get_maintenance_status(self, reminder_cfg: dict[str, Any] | None) -> list[dict[str, Any]]: + """Every active reminder with how far along it is (#461). Never raises. + + Built-in types with a positive interval in ``reminder_cfg`` (in its order), + then this device's custom tasks with any interval set. Each row is + ``{"id", "custom", "name", "cycles_interval", "days_interval", + "cycles_since", "days_since", "due"}``; ``name`` is the user's text for a + custom task and None for a built-in type (callers translate those); + ``days_since`` is whole days, or None when there is no date to count from. + A task is due when either of its intervals is reached. + """ + rows: list[dict[str, Any]] = [] + try: + latest = self._latest_maintenance_by_type() + + def _row(task_id: str, name: str | None, cycles: int, days: int) -> None: + since_cycles, since_days = self._maintenance_since(task_id, latest) + rows.append({ + "id": task_id, + "custom": name is not None, + "name": name, + "cycles_interval": cycles, + "days_interval": days, + "cycles_since": since_cycles, + "days_since": None if since_days is None else int(since_days), + "due": is_due(since_cycles, cycles, since_days, days), + }) + + if isinstance(reminder_cfg, dict): + for event_type, threshold in reminder_cfg.items(): + try: + thr = int(threshold) + except (TypeError, ValueError, OverflowError): + continue + if thr > 0: + _row(str(event_type), None, thr, 0) + for task in self.get_maintenance_tasks(): + cycles = int(task.get("cycles") or 0) + days = int(task.get("days") or 0) + if cycles > 0 or days > 0: + _row(task["id"], str(task.get("name") or task["id"]), cycles, days) + except Exception: # noqa: BLE001 + self._logger.debug("Maintenance status failed", exc_info=True) + return rows + def get_maintenance_due(self, reminder_cfg: dict[str, Any] | None) -> list[str]: - """Return event types whose cycles-since threshold has been reached. + """Ids of the tasks that are due: built-in types, then custom tasks. For each event type with a positive integer threshold in ``reminder_cfg``, - includes it when ``cycles_since_maintenance(event_type) >= threshold``. - Never raises. + includes it when ``cycles_since_maintenance(event_type) >= threshold``; a + custom task is due when its cycle or day interval is reached. Never raises. + """ + return [row["id"] for row in self.get_maintenance_status(reminder_cfg) if row["due"]] + + def sync_maintenance_baselines(self, reminder_cfg: dict[str, Any] | None) -> bool: + """Stamp or clear the counting origin of the preset types (#461). + + A preset type (salt, rinse aid, lint filter, condenser) counts from the + moment its reminder applies, not from odometer 0, so neither an upgrade nor + switching the reminder on opens with "due". Called on every config load: + a type that is on and has no baseline gets one at the current odometer; a + type that is off loses its baseline, so switching it on again later counts + from then. A logged event still takes precedence over the baseline. + + Cheap and idempotent; returns whether anything changed and, if so, + schedules a debounced write rather than awaiting one (this runs inside + ``async_setup``, whose awaits are billed to startup time, #408). Never raises. """ try: - if not isinstance(reminder_cfg, dict): - return [] - due: list[str] = [] - for event_type, threshold in reminder_cfg.items(): + raw = self._data.get("maintenance_baselines") + baselines = dict(raw) if isinstance(raw, dict) else {} + changed = not isinstance(raw, dict) and raw is not None + cfg = reminder_cfg if isinstance(reminder_cfg, dict) else {} + for event_type in sorted(MAINTENANCE_COUNT_FROM_ENABLE_TYPES): try: - thr = int(threshold) - except (TypeError, ValueError): - continue - if thr <= 0: - continue - if self.cycles_since_maintenance(str(event_type)) >= thr: - due.append(str(event_type)) - return due + enabled = int(cfg.get(event_type) or 0) > 0 + except (TypeError, ValueError, OverflowError): + enabled = False + if enabled and not isinstance(baselines.get(event_type), dict): + baselines[event_type] = { + "cycle_count": self.get_lifetime_cycle_count(), + "date": dt_util.now().isoformat(), + } + changed = True + elif not enabled and event_type in baselines: + del baselines[event_type] + changed = True + if not changed: + return False + self._data["maintenance_baselines"] = baselines + self._delayed_save_pending = True + self._store.async_delay_save(self._data_for_delayed_write, _BURST_SAVE_DELAY_S) + return True except Exception: # noqa: BLE001 + self._logger.debug("Maintenance baseline sync failed", exc_info=True) + return False + + # ── Custom maintenance tasks (#461) ────────────────────────────────────── + + def _maintenance_task_list(self) -> list[dict[str, Any]]: + """The live custom-task list, self-healed to a list. Never raises.""" + raw = self._data.get("maintenance_tasks") + if not isinstance(raw, list): + raw = [] + self._data["maintenance_tasks"] = raw + return raw + + def _find_maintenance_task(self, task_id: Any) -> dict[str, Any] | None: + """The stored record of a custom task by id, or None. Never raises.""" + if not isinstance(task_id, str) or not task_id.startswith(MAINTENANCE_CUSTOM_TASK_PREFIX): + return None + raw = self._data.get("maintenance_tasks") + if not isinstance(raw, list): + return None + for task in raw: + if isinstance(task, dict) and task.get("id") == task_id: + return task + return None + + def get_maintenance_tasks(self) -> list[dict[str, Any]]: + """This device's custom maintenance tasks, oldest first (copies). Never raises. + + Each is ``{"id", "name", "cycles", "days", "since", "since_cycle_count"}``: + intervals of 0 are off; ``since`` / ``since_cycle_count`` are the date and + odometer it counts from until it is first logged. + """ + raw = self._data.get("maintenance_tasks") + if not isinstance(raw, list): return [] + return [ + dict(t) for t in raw + if isinstance(t, dict) + and isinstance(t.get("id"), str) + and t["id"].startswith(MAINTENANCE_CUSTOM_TASK_PREFIX) + ] + + def _maintenance_name_taken(self, name: str, exclude_id: str | None = None) -> bool: + folded = name.casefold() + return any( + str(t.get("name") or "").casefold() == folded + for t in self.get_maintenance_tasks() + if t.get("id") != exclude_id + ) + + async def async_add_maintenance_task( + self, name: Any, cycles: Any = 0, days: Any = 0 + ) -> dict[str, Any]: + """Create a custom maintenance task and persist it. Returns the record. + + Raises ``ValueError`` for an empty or duplicate name, an invalid interval, + or when the per-device task limit is reached. It counts from now: a task + just defined is not due until its interval has passed. + """ + clean = clean_task_name(name) + if not clean: + raise ValueError("A task needs a name") + cycles_i = coerce_interval(cycles, MAINTENANCE_INTERVAL_CYCLES_MAX) + days_i = coerce_interval(days, MAINTENANCE_INTERVAL_DAYS_MAX) + tasks = self._maintenance_task_list() + if len(self.get_maintenance_tasks()) >= MAINTENANCE_CUSTOM_TASK_MAX: + raise ValueError( + f"Task limit reached ({MAINTENANCE_CUSTOM_TASK_MAX}); remove one first" + ) + if self._maintenance_name_taken(clean): + raise ValueError(f"A task named {clean!r} already exists") + task: dict[str, Any] = { + "id": f"{MAINTENANCE_CUSTOM_TASK_PREFIX}{uuid.uuid4().hex[:10]}", + "name": clean, + "cycles": cycles_i, + "days": days_i, + "since": dt_util.now().isoformat(), + "since_cycle_count": self.get_lifetime_cycle_count(), + } + tasks.append(task) + await self.async_save() + return dict(task) + + async def async_update_maintenance_task( + self, + task_id: str, + *, + name: Any = None, + cycles: Any = None, + days: Any = None, + ) -> dict[str, Any]: + """Rename a custom task or change its intervals; persist. Returns the record. + + ``None`` leaves a field as it is. Raises ``ValueError`` for an unknown + task, an empty or duplicate name or an invalid interval. A task switched + back on after being off (both intervals 0) counts from now, so it does not + reopen as due for the time it spent switched off. Log entries keep the name + they were written with. + """ + task = self._find_maintenance_task(task_id) + if task is None: + raise ValueError(f"Unknown maintenance task: {task_id!r}") + updates: dict[str, Any] = {} + if name is not None: + clean = clean_task_name(name) + if not clean: + raise ValueError("A task needs a name") + if self._maintenance_name_taken(clean, exclude_id=task_id): + raise ValueError(f"A task named {clean!r} already exists") + updates["name"] = clean + if cycles is not None: + updates["cycles"] = coerce_interval(cycles, MAINTENANCE_INTERVAL_CYCLES_MAX) + if days is not None: + updates["days"] = coerce_interval(days, MAINTENANCE_INTERVAL_DAYS_MAX) + was_off = int(task.get("cycles") or 0) <= 0 and int(task.get("days") or 0) <= 0 + task.update(updates) + now_on = int(task.get("cycles") or 0) > 0 or int(task.get("days") or 0) > 0 + if was_off and now_on: + task["since"] = dt_util.now().isoformat() + task["since_cycle_count"] = self.get_lifetime_cycle_count() + await self.async_save() + return dict(task) + + async def async_delete_maintenance_task(self, task_id: str) -> bool: + """Remove a custom task; report whether one was removed. + + Its log entries stay (they carry the task's name), so the history of what + was done is not lost with the reminder. + """ + raw = self._data.get("maintenance_tasks") + if not isinstance(raw, list): + return False + remaining = [t for t in raw if not (isinstance(t, dict) and t.get("id") == task_id)] + if len(remaining) == len(raw): + return False + self._data["maintenance_tasks"] = remaining + await self.async_save() + return True def has_recent_maintenance( self, event_type: str, days: int = MAINTENANCE_RECENT_SUPPRESS_DAYS @@ -3235,13 +4079,29 @@ class ProfileStore: def suggest_coverage_gaps( self, recent_window: int = 30, - min_unmatched: int = 5, + min_unmatched: int = 3, min_unmatched_rate: float = 0.20, low_confidence_threshold: float = 0.40, duration_bucket_s: float = 900.0, + *, + cluster_mode: str = "relative", + relative_tol: float = 0.25, + shape_mode: str = "index", + subset: bool = True, + max_traces: int = 5, + shape_threshold: float | None = None, + max_unmatched_confidence: float | None = 0.7, ) -> dict[str, Any]: """Detect gaps in profile coverage from recent unlabelled cycles. + Defaults measured with ``devtools/advisory_usefulness_eval.py`` (register + item 436): 3 unmatched cycles (was 5), duration groups chained at 25% + (fixed 15-min buckets split one programme's runs), the largest + shape-similar subset (one odd cycle sank a whole bucket), and unmatched + cycles that matched a known programme at >= 0.7 left out (they were + refused for a small margin, not unknown). Held-out programmes caught: + realistic 20% -> 55%, live 23% -> 39%; live precision 94% -> 82%. + Looks at the ``recent_window`` most recent cycles. When there are enough unmatched (no ``profile_name``) or low-confidence cycles, returns a record with: @@ -3306,58 +4166,17 @@ class ProfileStore: "avg_duration_s": round(float(np.mean(durs)), 1), }) - # A3: Shape-similarity clustering within duration buckets - profile_suggestions: list[dict[str, Any]] = [] - for bucket, durs in sorted(bucket_dur.items(), key=lambda kv: -len(kv[1])): - if len(durs) < 2: - continue - bucket_cycles = [ - c for c in unmatched - if c.get("power_data") - and c.get("duration") - and int(float(c["duration"]) // duration_bucket_s) == bucket - ] - if len(bucket_cycles) < 2: - continue - try: - from .signal_processing import resample_to_n # noqa: PLC0415 - traces: list[np.ndarray] = [] - ids: list[str] = [] - for c in bucket_cycles[:5]: # cap at 5 per bucket for performance - raw = decompress_power_data(c) - if not raw: - continue - pwr = [float(p) for _, p in raw] - if len(pwr) < 5: - continue - t = np.asarray(resample_to_n(pwr, CLUSTER_RESAMPLE_N), dtype=float) - mx = t.max() - if mx > 0: - t = t / mx - traces.append(t) - ids.append(str(c.get("id", ""))) - if len(traces) < 2: - continue - corrs = [ - float(np.corrcoef(traces[i], traces[j])[0, 1]) - for i in range(len(traces)) - for j in range(i + 1, len(traces)) - ] - valid_corrs = [r for r in corrs if np.isfinite(r)] - if not valid_corrs: - continue - avg_corr = float(np.mean(valid_corrs)) - if avg_corr >= CLUSTER_SHAPE_SIMILARITY_THRESHOLD: - avg_dur_s = float(np.mean(durs)) - profile_suggestions.append({ - "suggested_name": f"~{int(avg_dur_s // 60)} min program", - "cycle_ids": [cid for cid in ids if cid], - "avg_duration_s": round(avg_dur_s, 1), - "count": len(ids), - "similarity": round(avg_corr, 3), - }) - except Exception: # noqa: BLE001 - continue + # A3: shape-similarity clustering within duration groups. The knobs + # (cluster_mode / shape_mode / subset / max_traces) exist so + # devtools/advisory_usefulness_eval.py can measure alternatives; the + # defaults are the shipped behaviour. + profile_suggestions = self._coverage_shape_clusters( + unmatched, bucket_dur, duration_bucket_s, + cluster_mode=cluster_mode, relative_tol=relative_tol, + shape_mode=shape_mode, subset=subset, max_traces=max_traces, + threshold=(CLUSTER_SHAPE_SIMILARITY_THRESHOLD if shape_threshold is None else shape_threshold), + max_confidence=max_unmatched_confidence, + ) # Most-recent unmatched cycle id, so the setup advisor's phase-2 # "unmatched" nudge can deep-link straight to it (open_cycle:) @@ -3381,6 +4200,133 @@ class ProfileStore: except Exception: # noqa: BLE001 return {} + def _coverage_shape_clusters( + self, + unmatched: list[dict[str, Any]], + bucket_dur: dict[int, list[float]], + duration_bucket_s: float, + *, + cluster_mode: str, + relative_tol: float, + shape_mode: str, + subset: bool, + max_traces: int, + threshold: float, + max_confidence: float | None = None, + ) -> list[dict[str, Any]]: + """Shape-similar clusters of unmatched cycles for :meth:`suggest_coverage_gaps`. + + ``cluster_mode``: ``bucket`` groups by fixed ``duration_bucket_s`` buckets; + ``relative`` chains cycles whose durations are within ``relative_tol`` of + their neighbour (sorted), so one program's runs are not split at a bucket + edge. ``shape_mode``: ``index`` (Pearson on index-resampled traces), + ``time`` (Pearson on time-resampled traces) or ``dtw`` (the matcher's + peak-normalised DTW similarity, as group cohesion uses). ``subset`` keeps + the largest set of traces similar to a seed instead of requiring the whole + group's average to clear ``threshold``. ``max_confidence`` leaves out an + unmatched cycle whose stored ``match_confidence`` reached it: that cycle + matched a known programme well and was only refused for a small margin, so + it is not evidence of a programme the user has not created. Never raises.""" + from .signal_processing import resample_to_n # noqa: PLC0415 + + timed = [ + c for c in unmatched + if c.get("power_data") and isinstance(c.get("duration"), (int, float)) + and not isinstance(c.get("duration"), bool) and float(c["duration"]) > 0 + and not ( + max_confidence is not None + and isinstance(c.get("match_confidence"), (int, float)) + and not isinstance(c.get("match_confidence"), bool) + and float(c["match_confidence"]) >= max_confidence + ) + ] + groups: list[list[dict[str, Any]]] = [] + if cluster_mode == "relative": + ordered = sorted(timed, key=lambda c: float(c["duration"])) + cur: list[dict[str, Any]] = [] + for c in ordered: + if cur and float(c["duration"]) > float(cur[-1]["duration"]) * (1.0 + relative_tol): + groups.append(cur) + cur = [] + cur.append(c) + if cur: + groups.append(cur) + else: + for bucket, durs in sorted(bucket_dur.items(), key=lambda kv: -len(kv[1])): + if len(durs) < 2: + continue + groups.append([ + c for c in timed if int(float(c["duration"]) // duration_bucket_s) == bucket + ]) + groups = sorted((g for g in groups if len(g) >= 2), key=len, reverse=True) + + def _trace(c: dict[str, Any]) -> np.ndarray | None: + raw = decompress_power_data(c) + if not raw or len(raw) < 5: + return None + pw = np.asarray([float(p) for _, p in raw], dtype=float) + if shape_mode == "time": + ts = np.asarray([float(o) for o, _ in raw], dtype=float) + if np.all(np.diff(ts) >= 0) and ts[-1] > ts[0]: + return np.interp(np.linspace(ts[0], ts[-1], CLUSTER_RESAMPLE_N), ts, pw) + return np.asarray(resample_to_n(pw.tolist(), CLUSTER_RESAMPLE_N), dtype=float) + + def _sim(a: np.ndarray, b: np.ndarray) -> float: + if shape_mode == "dtw": + return float(self._shape_similarity(a, b)) + na, nb = a / (a.max() or 1.0), b / (b.max() or 1.0) + r = float(np.corrcoef(na, nb)[0, 1]) + return r if np.isfinite(r) else float("nan") + + out: list[dict[str, Any]] = [] + for group in groups: + try: + traces: list[np.ndarray] = [] + ids: list[str] = [] + durs: list[float] = [] + for c in group[:max_traces]: + t = _trace(c) + if t is None: + continue + traces.append(t) + ids.append(str(c.get("id", ""))) + durs.append(float(c["duration"])) + if len(traces) < 2: + continue + n = len(traces) + sim = np.full((n, n), np.nan) + for i in range(n): + for j in range(i + 1, n): + sim[i, j] = sim[j, i] = _sim(traces[i], traces[j]) + if subset: + # Seed = the trace most similar to the rest; keep everything + # that clears the threshold against it. + seed = int(np.nanargmax(np.nanmean(np.where(np.isnan(sim), -1.0, sim), axis=1))) + keep = [seed] + [j for j in range(n) if j != seed and sim[seed, j] >= threshold] + if len(keep) < 2: + continue + pair = [sim[i, j] for i in keep for j in keep if i < j and np.isfinite(sim[i, j])] + else: + keep = list(range(n)) + pair = [sim[i, j] for i in range(n) for j in range(i + 1, n) if np.isfinite(sim[i, j])] + if not pair: + continue + avg = float(np.mean(pair)) + if avg < threshold: + continue + kd = [durs[i] for i in keep] + avg_dur_s = float(np.mean(kd)) + out.append({ + "suggested_name": f"~{int(avg_dur_s // 60)} min program", + "cycle_ids": [ids[i] for i in keep if ids[i]], + "avg_duration_s": round(avg_dur_s, 1), + "count": len(keep), + "similarity": round(avg, 3), + }) + except Exception: # noqa: BLE001 + continue + return out + # ------------------------------------------------------------------ # Profile trend analysis (pure stats, appliance performance drift) # ------------------------------------------------------------------ @@ -3486,8 +4432,8 @@ class ProfileStore: 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 + and it cannot veto a shorter look-alike through a candidate-pool guard either, + since those only inspect 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. @@ -3537,7 +4483,7 @@ class ProfileStore: env_usable = False if ( isinstance(env, dict) - and int(env.get("cycle_count") or 0) >= 2 + and envelope_shape_count(env) >= 2 and isinstance(env_avg, list) and len(env_avg) > 1 and isinstance(env_avg[0], (list, tuple)) @@ -3545,7 +4491,7 @@ class ProfileStore: ): try: span = float(env_avg[-1][0]) - float(env_avg[0][0]) - except (TypeError, ValueError, IndexError): + except (TypeError, ValueError, IndexError, OverflowError): span = 0.0 env_usable = bool( env.get("target_duration") or profile.get("avg_duration") or span @@ -3581,7 +4527,7 @@ class ProfileStore: # 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 envelope_shape_count(env) >= 2 and isinstance(env_avg, list) ) out[name] = ( @@ -3593,7 +4539,11 @@ class ProfileStore: except Exception: # noqa: BLE001 return {} - def compute_profile_advisories(self) -> list[dict[str, Any]]: + def compute_profile_advisories( + self, + health: dict[str, Any] | None = None, + trends: dict[str, Any] | None = None, + ) -> list[dict[str, Any]]: """Actionable per-profile maintenance advisories from existing signals. Pure statistics (no ML): consolidates :meth:`compute_profile_health` and @@ -3602,11 +4552,13 @@ class ProfileStore: These surface as recommendations in the Profiles tab - never a notification. Each item is ``{profile, severity, code, message}``; severity is ``"warning"`` or ``"info"``. Returns ``[]`` on error (never raises). + ``health``/``trends`` let a caller that already computed them skip the + recomputation. """ try: advisories: list[dict[str, Any]] = [] - health = self.compute_profile_health() or {} - trends = self.compute_profile_trends() or {} + health = (health if health is not None else self.compute_profile_health()) or {} + trends = (trends if trends is not None else 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 @@ -3618,18 +4570,14 @@ class ProfileStore: # 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." + f"'{name}' can never match: its cycle has no usable duration. " + "Re-record this program or set its expected duration." ) 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." + f"'{name}' can never match: no cycle with power data is " + "behind it. Label or record a cycle of this program to fix it." ) message_key = "msg.advisory_unmatchable" advisories.append({ @@ -3671,17 +4619,17 @@ class ProfileStore: "severity": "warning", "code": "duration_outlier", "message": ( - f"{len(offenders)} cycle(s) labelled '{name}' are too far " - f"from its usual length to ever match it (worst: " + f"{len(offenders)} cycle(s) labelled ‘{name}’ are too far " + f"from its usual length to ever match (worst: " # Two decimals, not one: the values that reach this # advisory are by definition the ones far from 1.0, and # `.1f` collapsed the informative end of that range - a # cycle at 0.04x its profile rendered as "0.0x", which # states the cycle had no length and reads like a bug. # Real corpus cycles run as short as 0.148x (item 311). - f"{worst['ratio']:.2f}x). They are probably mislabelled, or " - "one recording captured two runs. Re-label or split them so " - "they stop skewing this program's time estimate." + f"{worst['ratio']:.2f}x). They are probably mislabelled or hold " + "two runs. Relabel or split them so they stop skewing its time " + "estimate." ), "message_key": "msg.advisory_duration_outlier", "message_params": { @@ -3699,9 +4647,9 @@ class ProfileStore: advisories.append({ "profile": name, "severity": "warning", "code": "poor_health", "message": ( - f"'{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." + f"'{name}' fits poorly: its recent cycles vary a lot or " + "match weakly. Review its cycles or re-record it to keep " + "matching and estimates accurate." ), # Localization: panel renders _t(message_key, message_params, # message). The English `message` above is the fallback. @@ -3714,10 +4662,9 @@ class ProfileStore: advisories.append({ "profile": name, "severity": "info", "code": "shape_drift", "message": ( - f"'{name}' has drifted significantly from its original " - f"power shape{corr_str}. The appliance may have changed " - "behaviour over time (e.g. limescale, wear). Consider " - "re-recording this profile with recent cycles." + f"'{name}' has drifted from its original power " + f"shape{corr_str}, perhaps from limescale or wear. " + "Consider re-recording it with recent cycles." ), "message_key": ( "msg.advisory_shape_drift_corr" @@ -3742,9 +4689,9 @@ class ProfileStore: advisories.append({ "profile": name, "severity": "info", "code": "duration_trend_up", "message": ( - f"'{name}' cycles are running progressively longer " - f"(about +{pct:.0f}% per cycle). If the appliance's " - "behaviour changed, re-record or rebuild this profile." + f"'{name}' cycles keep getting longer " + f"(about +{pct:.0f}% per cycle). If the appliance " + "changed, re-record or rebuild this profile." ), "message_key": "msg.advisory_duration_trend_up", "message_params": {"name": name, "pct": f"{pct:.0f}"}, @@ -3755,55 +4702,14 @@ class ProfileStore: advisories.append({ "profile": name, "severity": "info", "code": "energy_trend_up", "message": ( - f"'{name}' is drawing progressively more energy " - f"(about +{pct:.0f}% per cycle) - worth checking the " - "appliance if that is unexpected." + f"'{name}' uses more energy each cycle " + f"(about +{pct:.0f}% per cycle). Check the appliance " + "if that is unexpected." ), "message_key": "msg.advisory_energy_trend_up", "message_params": {"name": name, "pct": f"{pct:.0f}"}, }) - # Phase-structure consistency (phase-matching device types only). Uses - # the cached per-role phase profile: a profile whose member cycles heat - # for wildly different times, or where only some cycles heat at all, - # most likely mixes different programs/temperatures under one label - - # which hurts both matching and the phase-resolved ETA. Advisory only - # (Profiles tab); no relabeling (phase matching does not label better). - _sd = getattr(self, "_data", None) - _envs = _sd.get("envelopes") if isinstance(_sd, dict) else None - for pname, penv in (_envs if isinstance(_envs, dict) else {}).items(): - if health.get(pname, {}).get("health_status") == "poor": - continue # avoid double advice - pp = penv.get("phase_profile") if isinstance(penv, dict) else None - if not isinstance(pp, dict): - continue - try: - if int(pp.get("n_cycles") or 0) < PHASE_CONSISTENCY_MIN_CYCLES: - continue - heat = (pp.get("roles") or {}).get("heating") or {} - heat_mean = float(heat.get("dur_mean") or 0.0) - heat_std = float(heat.get("dur_std") or 0.0) - heat_occ = float(heat.get("occurrence") or 0.0) - heat_cv = (heat_std / heat_mean) if heat_mean > 60.0 else 0.0 - mixed_temp = heat_cv > PHASE_HEAT_CV_WARN - mixed_prog = PHASE_HEAT_OCC_MIXED_LO <= heat_occ <= PHASE_HEAT_OCC_MIXED_HI - if not (mixed_temp or mixed_prog): - continue - advisories.append({ - "profile": pname, "severity": "warning", - "code": "phase_inconsistent", - "message": ( - f"'{pname}' looks like it mixes different programs or " - "temperatures - its cycles heat for very different lengths " - "of time. Splitting it into separate profiles (e.g. per " - "temperature) will improve matching and time estimates." - ), - "message_key": "msg.advisory_phase_inconsistent", - "message_params": {"name": pname}, - }) - except (TypeError, ValueError): - continue - # E1: suppress the "needs maintenance" nag (duration-trending-longer / # shape-drift/poor-fit) when the user recently logged a descale, filter # clean, or drum clean — any recent maintenance clears the reminder. @@ -3825,87 +4731,6 @@ class ProfileStore: except Exception: # noqa: BLE001 return [] - # ------------------------------------------------------------------ - # Match ranking history (per-cycle ML commit snapshots) - # ------------------------------------------------------------------ - - def record_match_ranking_snapshot( - self, - start_time_iso: str, - features: dict[str, float], - top1_profile: str, - top1_score: float, - top2_score: float | None, - candidate_count: int, - cycle_id: str = "", - ) -> None: - """Append a live-match ranking snapshot for the active cycle. - - Snapshots are keyed by ``start_time_iso`` (and optionally ``cycle_id``) so - that ``confirm_match_ranking_snapshots`` can back-fill the confirmed label - when the cycle ends. Pre-computed feature scalars are stored (not raw traces) - to keep footprint small. The store is NOT persisted here — the caller must - schedule ``async_save``. - """ - from .const import MATCH_RANKING_HISTORY_MAX # pylint: disable=import-outside-toplevel - history: list[dict[str, Any]] = self._data.setdefault("match_ranking_history", []) - history.append({ - "start_time_iso": start_time_iso, - "cycle_id": str(cycle_id) if cycle_id else "", - "features": dict(features), - "top1_profile": str(top1_profile), - "top1_score": round(float(top1_score), 4), - "top2_score": round(float(top2_score), 4) if top2_score is not None else None, - "candidate_count": int(candidate_count), - "confirmed_label": None, - }) - # Trim to the most recent N snapshots. - if len(history) > MATCH_RANKING_HISTORY_MAX: - del history[: len(history) - MATCH_RANKING_HISTORY_MAX] - - def confirm_match_ranking_snapshots( - self, - start_time_iso: str, - confirmed_label: str, - cycle_id: str = "", - ) -> int: - """Back-fill the confirmed label for all snapshots belonging to a cycle. - - When ``cycle_id`` is provided (and the snapshot was recorded with one), matches - by ``cycle_id`` to avoid cross-contamination between cycles that share the same - second-resolution ``start_time_iso``. Falls back to ``start_time_iso`` matching - for snapshots recorded without a cycle_id (backward compatibility). - Returns the number of snapshots updated. - """ - history = self._data.get("match_ranking_history") - if not isinstance(history, list): - return 0 - updated = 0 - for snap in history: - if not isinstance(snap, dict): - continue - snap_cid = snap.get("cycle_id") or "" - if cycle_id and snap_cid: - # Both sides have an ID — match by ID for precision. - if snap_cid == cycle_id: - snap["confirmed_label"] = str(confirmed_label) - updated += 1 - else: - # Legacy path: match by timestamp (no cycle_id on one or both sides). - if snap.get("start_time_iso") == start_time_iso: - snap["confirmed_label"] = str(confirmed_label) - updated += 1 - return updated - - def get_match_ranking_history(self) -> list[dict[str, Any]]: - """Return all ranking snapshots (confirmed and pending). - - Callers that build a training dataset should filter to snapshots where - ``confirmed_label`` is not None. - """ - raw = self._data.get("match_ranking_history") - return list(raw) if isinstance(raw, list) else [] - def _get_shared_custom_phases(self) -> list[dict[str, Any]]: """Return mutable shared custom phase list with legacy flattening.""" raw = self._data.setdefault("custom_phases", []) @@ -4118,20 +4943,40 @@ class ProfileStore: """Delete a custom phase by id and remove matching profile assignments. Returns number of removed assignments. - Raises ValueError('phase_not_found') if no custom phase has this id. - Raises ValueError('cannot_delete_builtin') if the id is a built-in phase. + Raises ValueError('phase_not_found') if no custom phase has this id, which + includes an unedited built-in. + + An edited built-in is stored as an override under the built-in's id (see + async_update_custom_phase) and listed as editable, so the panel offers + Delete on it. Deleting it used to raise 'cannot_delete_builtin' every time; + it now drops the override, which restores the built-in, and points the + profiles' ranges back at the built-in name instead of removing them. """ phases = self._get_shared_custom_phases() found = next((p for p in phases if str(p.get("id", "")) == phase_id), None) if found is None: raise ValueError("phase_not_found") - if get_builtin_phase_by_id(phase_id) is not None: - raise ValueError("cannot_delete_builtin") phase_name = str(found.get("name", "")) phase_scope = str(found.get("device_type", "")).strip() self._data["custom_phases"] = [p for p in phases if str(p.get("id", "")) != phase_id] + builtin = get_builtin_phase_by_id(phase_id) + if builtin is not None: + builtin_name = str(builtin.get("name", "")) + for profile in self.get_profiles().values(): + profile_device_type = str(profile.get("device_type", "")).strip() + if phase_scope and profile_device_type != phase_scope: + continue + assigned = profile.get("phases", []) + if not isinstance(assigned, list): + continue + for p in cast(list[dict[str, Any]], assigned): + if str(p.get("name", "")).casefold() == phase_name.casefold(): + p["name"] = builtin_name + await self.async_save() + return 0 + removed_assignments = 0 for profile in self.get_profiles().values(): profile_device_type = str(profile.get("device_type", "")).strip() @@ -4169,7 +5014,7 @@ class ProfileStore: try: start = float(phase.get("start", 0.0)) end = float(phase.get("end", 0.0)) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): continue if end <= start: continue @@ -4220,7 +5065,7 @@ class ProfileStore: try: start = float(item.get("start", 0.0)) end = float(item.get("end", 0.0)) - except (TypeError, ValueError) as e: + except (TypeError, ValueError, OverflowError) as e: raise ValueError("invalid_phase_range") from e if end <= start: raise ValueError("invalid_phase_range") @@ -4236,28 +5081,6 @@ class ProfileStore: profile["phases"] = normalized await self.async_save() - def set_duration_tolerance(self, tolerance: float) -> None: - """Set the profile duration tolerance used by matching heuristics.""" - try: - self._duration_tolerance = float(tolerance) - except (TypeError, ValueError): - pass - - def set_retention_limits( - self, - *, - max_past_cycles: int, - max_full_traces_per_profile: int, - max_full_traces_unlabeled: int, - ) -> None: - """Set retention caps for stored cycles and full-resolution traces.""" - try: - self._max_past_cycles = int(max_past_cycles) - self._max_full_traces_per_profile = int(max_full_traces_per_profile) - self._max_full_traces_unlabeled = int(max_full_traces_unlabeled) - except (TypeError, ValueError): - pass - def get_duration_ratio_limits(self) -> tuple[float, float]: """Return (min_duration_ratio, max_duration_ratio) used for duration matching.""" return (float(self._min_duration_ratio), float(self._max_duration_ratio)) @@ -4267,7 +5090,7 @@ class ProfileStore: try: self._min_duration_ratio = float(min_ratio) self._max_duration_ratio = float(max_ratio) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): pass def _migrate_phase_ids(self) -> bool: @@ -4301,6 +5124,7 @@ class ProfileStore: data = await self._store.async_load() if data: self._data = data + await self._async_load_active() # 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. @@ -4338,12 +5162,26 @@ class ProfileStore: 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. + A sample that is **another programme's run** (or an unlabelled one) is broken + too, and repaired the same way: re-pointed at the profile's own newest cycle, + or cleared when it has none. A relabel used to leave the profile it moved off + pointing at the moved cycle, so that profile kept competing in the matcher with + another programme's trace as its template (tron4r's "Oberhemden 30°", whose + only "sample" was a "Pflegeleicht 40°" run, beat the right programme on + "Pflegeleicht 30°" cycles). Every relabel path now heals the pointer + (:meth:`heal_profile_sample`); this pass fixes stores written before that. + Clearing keeps the profile (pending state, as above) - it is never deleted + here. State-based and idempotent, so it needs no storage-version marker. + + Returns stats dict (``profiles_repaired`` includes ``foreign_samples``, the + count of such cross-programme pointers re-pointed or cleared). + ``cycles_labeled_as_sample`` is retained for diagnostics continuity and is + now always 0. """ stats = { "profiles_checked": 0, "profiles_repaired": 0, + "foreign_samples": 0, "cycles_labeled_as_sample": 0, } @@ -4367,43 +5205,16 @@ class ProfileStore: 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=_key) - except Exception: # pylint: disable=broad-exception-caught - return candidates[-1] - for profile_name, profile in profiles.items(): stats["profiles_checked"] += 1 sample_id = profile.get("sample_cycle_id") sample = by_id.get(sample_id) if sample_id else None + # A stored cycle that is not labelled to this profile is not its sample. + foreign = sample is not None and sample.get("profile_name") != profile_name - # Sample is valid only if it exists and still has power_data - if sample and sample.get("power_data"): + # Sample is valid only if it is one of this profile's own cycles and still + # has power_data. + if sample and sample.get("power_data") and not foreign: continue # Newest cycle already labelled to this profile that still has power_data. @@ -4411,11 +5222,11 @@ class ProfileStore: # 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([ + chosen = _newest_cycle([ c for c in cycles if c.get("profile_name") == profile_name and c.get("power_data") - ]) or newest([ + ]) or _newest_cycle([ c for c in self.iter_stored_cycles() if c.get("profile_name") == profile_name and c.get("power_data") @@ -4423,12 +5234,23 @@ class ProfileStore: 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. + # cannot adopt an unrelated cycle's shape and duration - and drop a + # pointer at another programme's run for the same reason. + if foreign: + profile["sample_cycle_id"] = None + stats["foreign_samples"] += 1 + stats["profiles_repaired"] += 1 + self._logger.info( + "Cleared profile '%s' sample: cycle %s is labelled %r", + profile_name, sample_id, sample.get("profile_name"), + ) continue profile["sample_cycle_id"] = chosen.get("id") if chosen.get("duration"): profile["avg_duration"] = chosen["duration"] + if foreign: + stats["foreign_samples"] += 1 stats["profiles_repaired"] += 1 try: @@ -4439,25 +5261,85 @@ class ProfileStore: return stats async def async_save(self) -> None: - """Save data to storage.""" + """Save data to storage. + + Inside a cycle-end burst (:meth:`coalesce_saves`) the write is debounced + instead: HA's Store rewrites the whole file on every save (MBs on a real + store), and one cycle end used to write it six times (register item 456). + """ + if time.monotonic() < getattr(self, "_coalesce_until", 0.0): + self._delayed_save_pending = True + self._store.async_delay_save(self._data_for_delayed_write, _BURST_SAVE_DELAY_S) + return + self._delayed_save_pending = False await self._store.async_save(self._data) + def _data_for_delayed_write(self) -> JSONDict: + """What a debounced write serialises: the store as it is when it fires.""" + self._delayed_save_pending = False + return self._data + + def coalesce_saves(self, window_s: float = _BURST_SAVE_WINDOW_S) -> None: + """Debounce every save of the next ``window_s`` seconds into one write. + + Opened by the cycle end. The work that follows it - the post-cycle artifact + refresh, the learning pass's feedback request and the suggestion passes it + spawns as executor jobs - each used to write the whole store on its own. + What must be durable at once is written by :meth:`async_flush_saves`; a + debounced write that has not fired yet is flushed by HA's final write on + shutdown and by the manager on unload. + """ + self._coalesce_until = time.monotonic() + max(0.0, float(window_s)) + + async def async_flush_saves(self) -> None: + """Write now if a debounced save is pending (no-op otherwise).""" + if getattr(self, "_delayed_save_pending", False): + self._delayed_save_pending = False + await self._store.async_save(self._data) + + async def _async_load_active(self) -> None: + """Load the active-cycle file; adopt a snapshot the main store still holds. + + One-way migration from the pre-0.5.8 layout, where the snapshot lived in + the main store: it is moved here and dropped from there, so a restart in + the middle of a cycle upgraded across this change still restores it. + """ + try: + active = await self._active_store.async_load() + except Exception as err: # noqa: BLE001 - a lost snapshot only loses a restore + self._logger.warning("Could not read the active-cycle snapshot: %s", err) + active = None + self._active_data = dict(active) if isinstance(active, dict) else {} + legacy = self._data.pop("active_cycle", None) + legacy_saved = self._data.pop("last_active_save", None) + if not self._active_data.get("active_cycle") and isinstance(legacy, dict): + self._active_data = {"active_cycle": legacy, "last_active_save": legacy_saved} + await self._async_write_active() + + async def _async_write_active(self) -> None: + try: + await self._active_store.async_save(self._active_data) + except Exception as err: # noqa: BLE001 - never break the cycle for a snapshot + self._logger.warning("Could not write the active-cycle snapshot: %s", err) + async def async_save_active_cycle(self, detector_snapshot: JSONDict) -> None: - """Save the active cycle state to storage (throttled by Manager).""" - self._data["active_cycle"] = detector_snapshot - self._data["last_active_save"] = dt_util.now().isoformat() - await self._store.async_save(self._data) + """Save the active cycle state to its own file (throttled by Manager).""" + self._active_data = { + "active_cycle": detector_snapshot, + "last_active_save": dt_util.now().isoformat(), + } + await self._async_write_active() def get_active_cycle(self) -> JSONDict | None: """Get the saved active cycle.""" - raw = self._data.get("active_cycle") + raw = self._active_data.get("active_cycle") if isinstance(raw, dict): return cast(JSONDict, raw) return None def get_last_active_save(self) -> datetime | None: """Return the last time the active cycle snapshot was persisted.""" - raw = self._data.get("last_active_save") + raw = self._active_data.get("last_active_save") if not isinstance(raw, str) or not raw: return None try: @@ -4465,21 +5347,42 @@ class ProfileStore: except ValueError: return None + async def async_keep_failed_restore(self, record: JSONDict) -> None: + """Keep the last active-cycle snapshot that failed to restore. + + One per device, overwritten by the next failure, so the diagnostics download + can show what broke instead of the cycle vanishing without a trace. Never + raises: losing this record must not stop setup. + """ + try: + await self._failed_restore_store.async_save(record) + except Exception as err: # noqa: BLE001 + self._logger.warning("Could not keep the failed snapshot: %s", err) + + async def async_get_failed_restore(self) -> JSONDict | None: + """The last snapshot that failed to restore, or None. Never raises.""" + try: + raw = await self._failed_restore_store.async_load() + except Exception: # noqa: BLE001 + return None + return cast(JSONDict, raw) if isinstance(raw, dict) else None + async def async_clear_active_cycle(self) -> None: """Clear the active cycle snapshot from storage.""" - if "active_cycle" in self._data: - del self._data["active_cycle"] - await self._store.async_save(self._data) + if self._active_data: + self._active_data = {} + await self._async_write_active() - def add_cycle(self, cycle_data: CycleDict) -> None: - """Add a completed cycle to history (sync wrapper, schedules async tasks).""" - self._add_cycle_data(cycle_data) - self.hass.async_create_task(self.async_enforce_retention()) + async def async_add_cycle(self, cycle_data: CycleDict) -> set[str]: + """Add a completed cycle to history asynchronously. - async def async_add_cycle(self, cycle_data: CycleDict) -> None: - """Add a completed cycle to history asynchronously.""" + Returns the profiles retention changed, which the caller rebuilds. Since + 0.5.8 retention keeps every cycle and trace (register item 463), so this is + empty. (The sync ``add_cycle`` wrapper, which scheduled retention as an + untracked task, had no caller left and is gone.) + """ self._add_cycle_data(cycle_data) - await self.async_enforce_retention() + return await self.async_enforce_retention() def _add_cycle_data( self, @@ -4537,7 +5440,7 @@ class ProfileStore: try: ts = float(start_time_raw) start_time_iso = dt_util.utc_from_timestamp(ts).isoformat() - except (ValueError, OSError): + except (ValueError, OSError, OverflowError): self._logger.debug( "add_cycle: unparseable string start_time %r, falling back", start_time_raw, @@ -4547,7 +5450,7 @@ class ProfileStore: elif isinstance(start_time_raw, (int, float)): try: start_time_iso = dt_util.utc_from_timestamp(float(start_time_raw)).isoformat() - except (ValueError, OSError): + except (ValueError, OSError, OverflowError): pass if start_time_iso is not None: cycle_data["start_time"] = start_time_iso @@ -4625,143 +5528,37 @@ class ProfileStore: # Apply retention after adding - async def async_enforce_retention(self) -> None: - """Apply retention policy asynchronously.""" - affected = self._enforce_retention_data() - for p in affected: - try: - # Use async rebuild task - self.hass.async_create_task(self.async_rebuild_envelope(p)) - except Exception as e: # pylint: disable=broad-exception-caught - self._logger.warning("Failed to schedule envelope rebuild for %s: %s", p, e) + async def async_enforce_retention(self) -> set[str]: + """Apply retention policy; returns the profiles it changed (see async_add_cycle). + + Only the debug-trace rule is left (item 380), which changes no profile, so + nothing is rebuilt here. The per-profile rebuild it used to schedule was an + untracked task an unload could not cancel (audit MANAGER-13), unreachable + since item 463; a rule that changes profiles again must have its callers + rebuild what this returns, the way the cycle end does. + """ + return self._enforce_retention_data() def _enforce_retention_data(self) -> set[str]: - """Internal retention logic (data operations only). - Returns set of affected profile names.""" + """Retention, data operations only. Returns the profiles it changed. + + Since 0.5.8 every cycle and its full power trace are kept: the 200-cycle cap + and the per-program trace caps are gone (register item 463), so nothing here + can change what shapes a profile and the result is always empty. What is + left is the debug-trace rule (register item 380): ``debug_data`` is the + matcher's full ranking for one cycle, read by nothing but a human looking at + that cycle, so it goes with a trace that is missing (older stores pruned + some) and with every one while "save debug traces" is off. + """ raw_cycles = self._data.get("past_cycles", []) cycles: list[CycleDict] = ( cast(list[CycleDict], raw_cycles) if isinstance(raw_cycles, list) else [] ) - if not cycles: - return set() - - def _start_time(cycle: CycleDict) -> str: - return str(cycle.get("start_time", "")) - - affected_profiles: set[str] = set() - - # 1) Cap total cycles - if len(cycles) > self._max_past_cycles: - # Sort by start_time and drop oldest beyond cap - try: - cycles.sort(key=_start_time) - except Exception: # pylint: disable=broad-exception-caught - pass - drop_count = len(cycles) - self._max_past_cycles - to_drop = cycles[:drop_count] - - # Maintain profile sample references when dropping - sample_refs = { - name: p.get("sample_cycle_id") - for name, p in self._data.get("profiles", {}).items() - } - for cy in to_drop: - # Track affected profile - p_name = cy.get("profile_name") - if p_name: - affected_profiles.add(p_name) - - cy_id = cy.get("id") - # If a profile sample points here, try to move to most recent cycle of that profile - for name, ref_id in list(sample_refs.items()): - if ref_id == cy_id: - # find newest cycle for that profile - newest = next( - ( - c - for c in reversed(cycles) - if c.get("profile_name") == name and c not in to_drop - ), - None, - ) - if newest: - self._data["profiles"][name]["sample_cycle_id"] = ( - newest.get("id") - ) - else: - # No replacement available - self._data["profiles"][name].pop("sample_cycle_id", None) - # Actually drop - del cycles[:drop_count] - - # 2) Strip older full traces per profile - by_profile: dict[str | None, list[CycleDict]] = {} + keep_debug = getattr(self, "_save_debug_traces", True) for cy in cycles: - key_any = cy.get("profile_name") # None for unlabeled - key: str | None = key_any if isinstance(key_any, str) and key_any else None - by_profile.setdefault(key, []).append(cy) - - # Collect cycle IDs that have pending feedback - never strip their power_data - pending_feedback_ids: set[str] = set(self._data.get("pending_feedback", {}).keys()) - - for key, group in by_profile.items(): - # newest first based on start_time - try: - group.sort(key=_start_time) - except Exception: # pylint: disable=broad-exception-caught - pass - # determine cap - cap = ( - self._max_full_traces_unlabeled - if key - in ( - None, - "", - ) - else self._max_full_traces_per_profile - ) - # count existing full traces - full_indices = [i for i, c in enumerate(group) if c.get("power_data")] - if len(full_indices) > cap: - # preserve last 'cap' full traces (newest at end after sort), strip older ones - keep_set = set(full_indices[-cap:]) - - # Get sample cycle ID for this profile - sample_id: str | None = None - if key and key in self._data.get("profiles", {}): - sample_id = self._data["profiles"][key].get("sample_cycle_id") - - for i, c in enumerate(group): - if i in keep_set: - continue - - # EXEMPTION: Never strip power data from the profile's sample cycle! - if sample_id and c.get("id") == sample_id: - continue - - # EXEMPTION: Never strip power data from cycles awaiting feedback review - if c.get("id") in pending_feedback_ids: - continue - - # EXEMPTION: Never strip power data from user-pinned "golden" cycles. - # The matcher uses a golden trace as the sharp single-cycle template - # (has_golden in the snapshot builder), and golden_profiles membership - # is gated on the trace still being present — trimming it would flip - # has_golden false and silently drop the profile back to the smeared - # envelope average. - rev = c.get("ml_review") - if isinstance(rev, dict) and rev.get("golden"): - continue - - if c.get("power_data"): - c.pop("power_data", None) - c.pop("sampling_interval", None) - if key: - affected_profiles.add(key) - - return affected_profiles - - + if "debug_data" in cy and (not keep_debug or not cy.get("power_data")): + cy.pop("debug_data", None) + return set() def cleanup_orphaned_profiles(self) -> int: """Remove profiles that reference non-existent cycles. @@ -4786,6 +5583,27 @@ class ProfileStore: return len(orphaned) + def prune_orphaned_envelopes(self) -> int: + """Drop envelopes whose profile no longer exists; returns how many. + + ``delete_profile`` now removes its own, but older deletes, the orphan GC + above and a rebuild for a label with no profile left them behind, in + storage and in exports (audit ML-09). + """ + envelopes = self._data.get("envelopes") + if not isinstance(envelopes, dict): + return 0 + profiles = self._data.get("profiles") or {} + stale = [name for name in list(envelopes) if name not in profiles] + for name in stale: + del envelopes[name] + if stale: + self._logger.info( + "Removed %d envelope(s) of deleted profiles: %s", + len(stale), ", ".join(sorted(map(str, stale))), + ) + return len(stale) + async def async_run_maintenance(self) -> dict[str, int]: """Run full maintenance: cleanup orphans, merge fragments, trim old cycles. @@ -4824,6 +5642,7 @@ class ProfileStore: "Envelope rebuild failed for %s during maintenance", profile_name, exc_info=True ) stats["rebuilt_envelopes"] = rebuilt + stats["pruned_envelopes"] = self.prune_orphaned_envelopes() # 5. Refresh the frozen per-cycle artifact list. These are computed once at # cycle end and then cached on the cycle, so they describe whatever the @@ -4846,11 +5665,47 @@ class ProfileStore: return stats + async def async_post_cycle_refresh(self, profiles: Any) -> dict[str, int]: + """What one finished cycle invalidates, and nothing more (register item 456). + + The cycle end used to run :meth:`async_run_maintenance`: every envelope + rebuilt, every stored cycle's artifacts recomputed (a decompression each), + every unlabelled cycle re-matched, two whole-store writes - work that grew + with the store and repeated at every cycle end. A cycle end changes only the + envelopes of the profiles it touched (the labelled one, and any retention + trimmed), which the manager rebuilds before it saves; this refreshes the + artifacts that read those envelopes. The global passes stay in the nightly + maintenance. + + Kept here: the orphaned-profile GC. It is O(cycles) with no decompression, + and it is the safety net for a sample pointer the cycle lists' changes left + dangling (retention re-points the ones it drops, so normally a no-op). + """ + names = frozenset(p for p in (profiles or ()) if isinstance(p, str) and p) + stats = { + "orphaned_profiles": self.cleanup_orphaned_profiles(), + "refreshed_artifacts": 0, + } + if names: + stats["refreshed_artifacts"] = self._apply_cycle_artifact_updates( + await self.hass.async_add_executor_job( + self._collect_cycle_artifact_updates, names + ) + ) + if any(stats.values()): + await self.async_save() + return stats + def _collect_cycle_artifact_updates( self, + profiles: frozenset[str] | None = None, ) -> list[tuple[CycleDict, list[dict[str, Any]]]]: """Recompute every stored cycle's cached ``artifacts`` against today's bands. + ``profiles`` narrows it to the cycles labelled with one of those profiles: + a cycle's artifacts read only its own profile's envelope, so after a cycle + end that rebuilt one envelope nothing else can have changed. + Computes only; the caller applies the updates on the event loop. This runs in an executor over dictionaries that live inside ``self._data``, and while it is awaited any other coroutine can reach ``async_save`` - @@ -4868,6 +5723,8 @@ class ProfileStore: try: for cycle in self.iter_stored_cycles(): name = cycle.get("profile_name") + if profiles is not None and name not in profiles: + continue if not name: # Unlabelled: there is no envelope to judge it against, so a # stale list from a label since removed has to go. @@ -4876,6 +5733,10 @@ class ProfileStore: continue pairs = decompress_power_data(cycle) if len(pairs) < 6: + # No trace left to mark (#459): retention pruned it, so a list + # computed while it existed points at nothing. + if not pairs and cycle.get("artifacts") is not None: + pending.append((cycle, [])) continue fresh = self.detect_cycle_artifacts(str(name), pairs) if fresh != (cycle.get("artifacts") or []): @@ -4961,7 +5822,7 @@ class ProfileStore: shifted_data.append([round(row[0] - first_offset, 1), row[1]]) cycle["power_data"] = shifted_data processed_count += 1 - except (ValueError, TypeError) as e: + except (ValueError, TypeError, OverflowError) as e: self._logger.warning("Failed to shift start_time for trimmed cycle: %s", e) else: # Only trailing trimmed or no shift needed @@ -5110,7 +5971,22 @@ class ProfileStore: return count - + def _warn_ignored_manual_duration( + self, cycle: CycleDict, value: Any, trace_span_s: float + ) -> None: + """Log, once per cycle per run, a ``manual_duration`` the envelope ignored.""" + warned: set[str] = self.__dict__.setdefault("_manual_duration_ignored", set()) + cid = str(cycle.get("id")) + if cid in warned: + return + warned.add(cid) + self._logger.warning( + "Ignoring manual_duration %r on cycle %s of '%s': outside %.1f-%.1fx its " + "%.0f s trace (a unit slip?). Its trace length shapes the profile instead; " + "correct the cycle's duration to clear this.", + value, cid, cycle.get("profile_name"), + MANUAL_DURATION_SPAN_BOUNDS[0], MANUAL_DURATION_SPAN_BOUNDS[1], trace_span_s, + ) def _rebuild_envelope_sync( self, labeled_cycles: list[CycleDict] @@ -5127,18 +6003,22 @@ class ProfileStore: # can judge degeneracy relative to the profile (works for both a 2000W # dishwasher and a low-power pump). parsed: list[tuple[list[float], list[float], float, bool, float]] = [] - for cycle in labeled_cycles: - pairs = decompress_power_data(cycle) - if len(pairs) < 3: - continue + decoded = [(c, decompress_power_data(c)) for c in labeled_cycles] + decoded = [(c, p) for c, p in decoded if len(p) >= 3] + flags = _effective_golden_flags([c for c, _ in decoded]) + for (cycle, pairs), is_golden in zip(decoded, flags): offsets = [p[0] for p in pairs] values = [p[1] for p in pairs] stored_dur = float(cycle.get("duration", 0.0) or 0.0) authoritative_dur = float(max(offsets[-1], stored_dur)) man_dur = cycle.get("manual_duration") - final_dur = float(man_dur) if man_dur else authoritative_dur - review = cycle.get("ml_review") - is_golden = bool(review.get("golden")) if isinstance(review, dict) else False + final_dur = authoritative_dur + if man_dur: + checked = _sane_manual_duration(man_dur, offsets[-1] - offsets[0]) + if checked is not None: + final_dur = checked + else: + self._warn_ignored_manual_duration(cycle, man_dur, offsets[-1] - offsets[0]) peak = max(values) if values else 0.0 parsed.append((offsets, values, final_dur, is_golden, peak)) @@ -5190,12 +6070,34 @@ class ProfileStore: if not result: return None - return result, durations + built = _EnvelopeBuild((result, durations)) + # Stage 4's expected energy, from the same members (w7g): the warped + # average inherits the reference cycle's heating length. + built.energy_ref = analysis.member_energy_reference(raw_cycles_data) + return built def _cycle_peak(self, cycle: CycleDict) -> float: - """Peak power of a cycle's trace (0.0 if it has none).""" + """Peak power of a cycle's trace (0.0 if it has none). + + Memoised per cycle on the stored trace's shape (length and end points): the + reference-cycle pick calls this for every cycle of a profile on every + envelope rebuild, on the event loop, and each call decompressed the whole + trace (audit PERF-08 budgets: ~4 decompressions per stored cycle per cycle + end). A trim, split or repair changes the key, so a stale peak is never read. + """ + raw = cycle.get("power_data") + key = None + if isinstance(raw, list) and raw: + key = (str(cycle.get("id") or ""), len(raw), repr(raw[0]), repr(raw[-1])) + cache = self.__dict__.setdefault("_peak_cache", {}) + hit = cache.get(key[0]) + if hit is not None and hit[0] == key: + return hit[1] pairs = decompress_power_data(cycle) - return max((p[1] for p in pairs), default=0.0) if pairs else 0.0 + peak = max((p[1] for p in pairs), default=0.0) if pairs else 0.0 + if key is not None: + self.__dict__.setdefault("_peak_cache", {})[key[0]] = (key, peak) + return peak def _select_reference_cycle_id( self, profile_name: str, target_duration: float | None = None @@ -5220,10 +6122,7 @@ class ProfileStore: med_peak = sorted(peaks.values())[len(peaks) // 2] if peaks else 0.0 degen_floor = max(_DEGENERATE_POWER_FLOOR, 0.10 * med_peak) - golden = [ - c for c in cands - if isinstance(c.get("ml_review"), dict) and c["ml_review"].get("golden") - ] + golden = [c for c, g in zip(cands, _effective_golden_flags(cands)) if g] if golden: pool = golden else: @@ -5236,6 +6135,71 @@ class ProfileStore: best = max(pool, key=lambda c: float(c.get("duration") or 0.0)) return best.get("id") + def _own_sample_cycle_id(self, profile_name: str) -> str | None: + """A sample for ``profile_name`` drawn from its OWN cycles, or ``None``. + + The envelope rebuild's pick first (:meth:`_select_reference_cycle_id`, on the + evidence view). When that finds nothing - every own cycle is excluded from + evidence, or none is a completed trace - the newest stored cycle labelled to + it that has a trace (the :meth:`async_repair_profile_samples` rule), so an + import-only profile keeps a sample of its own. Never another programme's + run, never an unlabelled one. + """ + profile = (self._data.get("profiles") or {}).get(profile_name) + target: float | None = None + if isinstance(profile, dict): + try: + target = float(profile.get("avg_duration") or 0.0) or None + except (TypeError, ValueError, OverflowError): + target = None + chosen = self._select_reference_cycle_id(profile_name, target) + if chosen: + return chosen + own = _newest_cycle([ + c for c in self.iter_stored_cycles() + if c.get("profile_name") == profile_name and c.get("power_data") and c.get("id") + ]) + return own.get("id") if own else None + + def heal_profile_sample(self, profile_name: str | None) -> bool: + """Stop ``profile_name`` using a cycle that is no longer its own as its sample. + + Called by every path that moves a label off a cycle (manual relabel, + auto-label, review-queue correction, create-from-cycle, split, merge). When + the profile's ``sample_cycle_id`` is a stored cycle now labelled to another + programme, or unlabelled, it is re-pointed at one of the profile's own cycles + (:meth:`_own_sample_cycle_id`) or cleared when it has none. Left alone, the + matcher admitted the profile with another programme's run as its template: + with no envelope of its own (fewer than two cycles) the sample IS the + candidate curve, so the profile competed with a copy of the other + programme's trace. + + A cleared profile is kept (the pending state ``cleanup_orphaned_profiles`` + and :meth:`async_repair_profile_samples` preserve; reported by + :meth:`unmatchable_profiles`), never deleted: a user-created programme can + lose its last cycle to a relabel and gain one back. A dangling id (no such + cycle) is not touched - that is the repair's and the orphan GC's case. + Synchronous, no rebuild, no save. Returns True when the pointer changed. + """ + if not profile_name: + return False + profile = (self._data.get("profiles") or {}).get(profile_name) + if not isinstance(profile, dict): + return False + sample_id = profile.get("sample_cycle_id") + if not sample_id: + return False + sample, _origin = self.find_stored_cycle(sample_id) + if sample is None or sample.get("profile_name") == profile_name: + return False + new_id = self._own_sample_cycle_id(profile_name) + profile["sample_cycle_id"] = new_id + self._logger.debug( + "Profile '%s' sample %s is now labelled %r: re-pointed to %s", + profile_name, sample_id, sample.get("profile_name"), new_id, + ) + return True + async def async_rebuild_all_envelopes(self) -> int: """Rebuild envelopes for all profiles. Returns count of envelopes rebuilt.""" count = 0 @@ -5276,7 +6240,7 @@ class ProfileStore: continue try: repaired_rows.append([round(float(pt[0]) + start_ts, 1), round(float(pt[1]), 1)]) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): continue if not repaired_rows: continue # all rows malformed - leave original trace untouched @@ -5313,11 +6277,90 @@ class ProfileStore: ) return repaired + def _envelope_inputs( + self, profile_name: str + ) -> tuple[tuple[Any, ...], tuple[Any, ...], list[Any]]: + """What :meth:`async_rebuild_envelope` reads for ``profile_name``. + + ``(evidence, built, refs)``: ``evidence`` is everything the build is + computed from (the evidence setting, the DTW band, and per stored cycle of + the profile, in list order: the dict and its trace by identity plus the + trace's length and end points - the key ``_cycle_peak`` already trusts - + and every field the build or the reference pick reads); ``built`` is what + the build itself writes (the profile's stats, the envelope object). + ``refs`` keeps every object whose ``id()`` is in the key alive, so an id + can never be recycled into a false match. O(stored cycles), no decoding. + """ + refs: list[Any] = [] + rows: list[tuple[Any, ...]] = [] + for list_name in ("past_cycles", "reference_cycles", "backfill_cycles"): + seq = self._data.get(list_name) + if not isinstance(seq, list): + continue + for c in seq: + if not isinstance(c, dict) or c.get("profile_name") != profile_name: + continue + pd = c.get("power_data") + rev = c.get("ml_review") + meta = c.get("meta") + refs.append(c) + refs.append(pd) + has_pts = isinstance(pd, list) and bool(pd) + rows.append(( + list_name, id(c), id(pd), + len(pd) if isinstance(pd, list) else -1, + repr(pd[0]) if has_pts else None, + repr(pd[-1]) if has_pts else None, + c.get("id"), c.get("status"), c.get("duration"), + c.get("manual_duration"), c.get("energy_wh"), c.get("start_time"), + repr(rev.get("golden")) if isinstance(rev, dict) else None, + repr(meta.get("source")) if isinstance(meta, dict) else None, + )) + profiles = self._data.get("profiles") + prof = profiles.get(profile_name) if isinstance(profiles, dict) else None + envelopes = self._data.get("envelopes") + env = envelopes.get(profile_name) if isinstance(envelopes, dict) else None + refs.extend((prof, env)) + built: tuple[Any, ...] = (id(env), id(prof)) + if isinstance(prof, dict): + built += tuple( + repr(prof.get(k)) + for k in ("min_duration", "max_duration", "avg_duration", "sample_cycle_id") + ) + evidence = (tuple(self._evidence_sources), self.dtw_bandwidth, tuple(rows)) + return evidence, built, refs + async def async_rebuild_envelope(self, profile_name: str) -> bool: """ Build/rebuild statistical envelope for a profile asynchronously. Offloads heavy DTW/normalization to executor. + + A no-op when nothing it reads changed since this profile's last successful + build (:meth:`_envelope_inputs`): the build is deterministic, so it would + reproduce the same envelope and stats. A cycle end used to rebuild the + labelled profile twice (the manager, then the learning pass confirming the + same label) and every other profile once more (register item 456). """ + before = self._envelope_inputs(profile_name) + memo: dict[str, tuple[tuple[Any, ...], tuple[Any, ...], list[Any]]] = ( + self.__dict__.setdefault("_envelope_built_from", {}) + ) + last = memo.get(profile_name) + if last is not None and last[0] == before[0] and last[1] == before[1]: + return True + memo.pop(profile_name, None) + # Rebuilds of one profile are not serialised (cycle end, label edits, the + # learning pass, maintenance) and the executor job below takes seconds, so + # an older build finishing last overwrote a newer one, and a build that + # straddled a delete or rename wrote an envelope under the dead name + # (audit ML-08). Only the newest build of a profile writes, and not once + # the profile it started for is gone. + generations: dict[str, int] = self.__dict__.setdefault( + "_envelope_rebuild_generation", {} + ) + generation = generations.get(profile_name, 0) + 1 + generations[profile_name] = generation + existed = profile_name in (self._data.get("profiles") or {}) # A rebuild changes this profile's curve, which feeds group cohesion, so # invalidate the cohesion cache (not only on group mutations) to avoid stale # cohesion approving/rejecting a collapse against outdated shapes. @@ -5365,6 +6408,14 @@ class ProfileStore: shape_cycles ) + if generations.get(profile_name) != generation or ( + existed and profile_name not in (self._data.get("profiles") or {}) + ): + self._logger.debug( + "Discarding a superseded envelope build for %s", profile_name + ) + return False + if not result_pkg: # Envelope shape couldn't be built (no power data / too few points). # Still update profile min/max/avg from raw cycle durations so that @@ -5385,6 +6436,7 @@ class ProfileStore: return False result, durations = result_pkg + energy_ref = getattr(result_pkg, "energy_ref", None) # Update profile stats in storage (Fast metadata update) if durations and profile_name in self._data.get("profiles", {}): @@ -5452,173 +6504,33 @@ class ProfileStore: "avg": to_points(avg_curve), "std": to_points(std_curve), "cycle_count": cycle_count, + # The cycles the curve was actually built from: what gates the envelope + # as a matching template (envelope_shape_count, audit MR-11). + "shape_cycle_count": len(durations), "avg_energy": avg_energy, "duration_std_dev": duration_std_dev, "updated": dt_util.now().isoformat(), } - - # Derived cache: per-phase profile (per-role duration/energy priors) used by - # phase-segmented matching / phase-resolved ETA. Built only for device types - # phase matching is live-supported for; absent otherwise (consumers fall back - # to the whole-cycle pipeline). Pure/cheap - segmentation is O(samples). - device_type = str( - self._data.get("profiles", {}).get(profile_name, {}).get("device_type") or "" - ) - # Offload the per-cycle segmentation to the executor (it can be tens of ms - # for very long traces; keep it off the event loop, like the envelope DTW). - phase_profile = await self.hass.async_add_executor_job( - self._compute_phase_profile, profile_name, shape_cycles, device_type - ) - if phase_profile is not None: - envelope_data["phase_profile"] = phase_profile + if energy_ref: + # Stage 4's expected energy from the members' own traces + # (analysis.member_energy_reference); absent on envelopes built before. + envelope_data["energy_ref"] = energy_ref if "envelopes" not in self._data: self._data["envelopes"] = {} self._data["envelopes"][profile_name] = envelope_data + # Remember what this build was computed from, unless the evidence moved + # while the executor job ran (then the next request must build again). + after = self._envelope_inputs(profile_name) + if after[0] == before[0]: + memo[profile_name] = after + # A deleted profile's entry would pin its old cycles in memory. + live = self._data.get("profiles") + for stale in [k for k in memo if not isinstance(live, dict) or k not in live]: + memo.pop(stale, None) return True - def _compute_phase_profile( - self, profile_name: str, cycles: list[CycleDict], device_type: str - ) -> dict[str, Any] | None: - """Segment each member cycle and aggregate a per-role phase profile. - - Returns a JSON-safe dict for ``envelope["phase_profile"]`` or ``None`` when - phase matching is not live-supported for this device type or no cycle could - be segmented. Never raises (phase support must never break envelope rebuild). - """ - try: - if not phase_matching_live_supported(device_type): - return None - model = phase_model_for(device_type) - if model is None: - return None - segmented: list = [] - for cycle in cycles: - offsets = power_data_to_offsets(cycle.get("power_data") or []) - if len(offsets) < 4: - continue - t = [float(o) for o, _ in offsets] - w = [float(p) for _, p in offsets] - segs = segment_cycle(t, w, model) - if segs: - segmented.append(segs) - if not segmented: - return None - profile = build_phase_profile(profile_name, segmented) - return phase_profile_to_dict(profile) if profile is not None else None - except Exception: # noqa: BLE001 - phase caching must never break rebuild - self._logger.debug("phase-profile build failed for %s", profile_name, exc_info=True) - return None - - def _group_scope(self, program: str) -> set[str] | None: - """Phase-narrowing scope for the matched ``program``: - - * If ``program`` is in a group with >= 2 members, return that group's - members - narrow WITHIN the family (design §9). This is both coherent - (same program family as the displayed program) and accurate (picks the - right temperature/spin variant among siblings). - * Otherwise return ``None`` = no scope filter (consider ALL of the - device's phase profiles). The Phase-0 gate showed that constraining an - UNGROUPED cycle to only the whole-cycle-matched program regresses the - ETA whenever that match is wrong (common on mislabeled data): the best - ETA comes from letting the phase matcher pick the best-fitting profile, - bounded by the ambiguity gate + cold-start floor. Grouping variants is - the recommended workflow and restores full coherence. - """ - try: - for grp in self.get_profile_groups().values(): - members = grp.get("members") if isinstance(grp, dict) else None - if isinstance(members, list) and program in members: - sib = {m for m in members if isinstance(m, str)} - if len(sib) >= 2: - return sib - except Exception: # noqa: BLE001 - self._logger.debug("_group_scope failed for %r", program, exc_info=True) - return None - - def _candidate_phase_profiles(self, scope: set[str] | None = None) -> list: - """Cached per-profile PhaseProfiles (from envelope['phase_profile']). - - Restricted to ``scope`` (profile names) when given, and always filtered to - profiles with >= ``PHASE_PROFILE_MIN_CYCLES`` member cycles so a noisy - single-cycle prior can't drive the ETA (cold-start floor). - """ - out = [] - for name, env in (self._data.get("envelopes") or {}).items(): - if scope is not None and name not in scope: - continue - if isinstance(env, dict): - pp = phase_profile_from_dict(env.get("phase_profile")) - if pp is not None and pp.n_cycles >= PHASE_PROFILE_MIN_CYCLES: - out.append(pp) - return out - - def phase_remaining( - self, - power_data: list, - device_type: str, - program: str | None = None, - ) -> dict[str, Any] | None: - """Phase-resolved remaining-time for a running cycle. Never raises. - - Segments the observed-so-far trace and matches it against the matched - ``program``'s phase profile (and its group siblings, design §9), returning - the winning member's per-role budget remaining. Returns ``None`` (caller - keeps the current estimate) when: phase matching is not live-supported for - the device type; ``program`` is unknown / has no cached phase profile - (or too few cycles - cold-start floor); segmentation is degenerate; or the - top two candidates are within ``MATCH_AMBIGUITY_MARGIN`` (ambiguous - do - not commit a variant, design §7). - - This is the *phase* half of the blended ETA; the blend with the current - estimator lives in ``progress.compute_progress`` (single source of truth). - Pure and cheap (segmentation + per-role agreement, no DTW) - safe to call - inline from the async matching path. - """ - try: - if not phase_matching_live_supported(device_type): - return None - model = phase_model_for(device_type) - if model is None or not program: - return None - candidates = self._candidate_phase_profiles(self._group_scope(program)) - if not candidates: - return None - offsets = power_data_to_offsets(power_data or []) - if len(offsets) < 4: - return None - t = [float(o) for o, _ in offsets] - w = [float(p) for _, p in offsets] - segs = segment_cycle(t, w, model, partial=True) - if not segs: - return None - ranked = match_phase_profiles(segs, candidates, {}) - if not ranked: - return None - # Ambiguity gate: a near-tie among group members is not a confident - # variant call - fall back rather than swing the ETA between budgets. - if (len(ranked) >= 2 - and (ranked[0].score - ranked[1].score) < MATCH_AMBIGUITY_MARGIN): - return None - best = next((c for c in candidates if c.name == ranked[0].name), None) - remaining = phase_eta(segs, best) if best is not None else None - if remaining is None: - return None - return { - "remaining_s": float(remaining), - "matched": ranked[0].name, - "score": float(ranked[0].score), - } - except Exception: # noqa: BLE001 - phase ETA must never break the estimate - self._logger.debug("phase_remaining failed", exc_info=True) - return None - - - - - - def get_envelope(self, profile_name: str) -> JSONDict | None: """Get cached envelope for a profile, or None if not available.""" envelopes = self._data.get("envelopes", {}) @@ -5669,7 +6581,7 @@ class ProfileStore: # Inside the try: a stored time_grid with non-numeric entries must # yield the safe None fallback, not propagate to async_verify_alignment. return [float(t) for t in env_time], env_power - except (TypeError, ValueError, IndexError): + except (TypeError, ValueError, IndexError, OverflowError): return None @staticmethod @@ -5800,7 +6712,9 @@ class ProfileStore: ``past_cycles`` AND ``reference_cycles`` are scanned - see the item-353 paragraph below for why the second list is in scope, and note that this means the repair CAN rewrite cycles marked ``golden``. Within both, only - cycles recorded as ``TerminationReason.SMART`` are touched. + cycles recorded as ``TerminationReason.SMART`` are touched - plus, on a + dishwasher, ``TerminationReason.TIMEOUT``, whose finish keeps its tail + through the same cap (#424). ``backfill_cycles`` were replayed from raw history and never went through Smart Termination at all, so they cannot carry a banked tail; a user-stopped, unattributed, or hand-corrected cycle is left alone for the @@ -5844,20 +6758,36 @@ class ProfileStore: # `profile_terminal_quiet_seconds` runs # `compute_profile_terminal_signature`, which decompresses and scans # every evidence cycle of the profile - quadratic over the loop, on - # the event loop, with up to `max_past_cycles` long dishwasher - # traces. Worse, calling it per cycle makes the result ORDER + # the event loop, with every stored long dishwasher trace (unbounded + # since 0.5.8). Worse, calling it per cycle makes the result ORDER # DEPENDENT: a cycle repaired earlier in this loop has a trimmed # trace and an appended terminal sample, so later calls measure a # history this very loop has been rewriting. One value per profile, # taken from the pre-repair history, removes both. quiet_by_profile: dict[str, float | None] = {} + # The shortest length the USER has vouched for, per profile: a + # corrected `manual_duration`, a recorder capture, or a pinned golden + # cycle. A dishwasher repair never cuts below 0.9 x that. The terminal + # quiet statistic can be wrong in exactly the direction that destroys + # data: on a machine whose drying runs ~113 min silent before the + # pump-out, three of the four cycles it was measured on had been closed + # early by Smart Termination, so it read 649 s and the repair cut three + # ECO cycles from ~235 min to ~121 min - irreversibly, the trace past the + # cut is deleted - while the user's own correction of a sibling said + # 234 min. Taken from the pre-repair history, like `quiet_by_profile`. + trusted_min = _trusted_min_by_profile(cycles) for cycle in cycles: if not isinstance(cycle, dict): continue summary["examined"] += 1 - # Only Smart Termination ever banked a confirmation delay, and it - # is the only live path that passes a `tail_cap` (`_keep_tail_cap` - # at cycle_detector.py:2133/2372/2394/3094). `user_stop` also + # A cycle the user trimmed by hand is theirs, like `manual_duration` + # below - the trim deletes `manual_duration`, so that exemption no + # longer covers it and the repair re-cut user trims. + _meta = cycle.get("meta") + if isinstance(_meta, dict) and (_meta.get("edited") or _meta.get("trim")): + continue + # Smart Termination is where the confirmation delay was banked + # (item 297). `user_stop` also # finishes with `keep_tail=True` but DELIBERATELY uncapped - "User # implies Done Now" - so repairing it would truncate a tail the # user asked to keep, and rewrite the profile statistics with it. @@ -5866,7 +6796,19 @@ class ProfileStore: # rewrite must never guess about history it cannot verify. This # also matches what was measured (item 297): smart-terminated # cycles banked a median 12.6 min, every other path ~0. - if cycle.get("termination_reason") != TerminationReason.SMART: + # + # But it is not the only path that passes a `tail_cap`: a + # dishwasher's fallback-timeout finishes keep their tail too + # (`keep_tail = device_type == "dishwasher"`, same `_keep_tail_cap`), + # so on a dishwasher they are in scope as well + # (#424): the Samsung in that report banked 10.3 min on a timeout + # finish exactly the way the Beko did on its smart ones. Every + # other type's timeout snaps back to the last activity, so there + # is nothing to repair there. + _reason = cycle.get("termination_reason") + if _reason != TerminationReason.SMART and not ( + is_dishwasher and _reason == TerminationReason.TIMEOUT + ): continue # A corrected duration is the same kind of statement `user_stop` # is, and gets the same exemption. `manual_duration` is what the @@ -5920,18 +6862,41 @@ class ProfileStore: # Double-counting here shrinks the reclaim below # BANKED_TAIL_REPAIR_MIN_S and leaves the banked tail in # place, so the bug hides itself. - if _quiet_run_before(points, last_active, stop_threshold_w) >= ( - 0.5 * float(quiet) + if _terminal_quiet_seen( + points, + last_active, + stop_threshold_w, + float(quiet), + TERMINAL_EVENT_PEAK_FRAC, ): allowance = 0.0 + # The terminal event may sit below the stop threshold, + # after `last_active`; keep it (register item 384). + last_active = _terminal_event_end( + points, last_active, TERMINAL_EVENT_PEAK_FRAC + ) new_duration = last_active + allowance + if is_dishwasher: + _floor = trusted_min.get(str(cycle.get("profile_name") or "")) + if _floor: + new_duration = max(new_duration, TRUSTED_LENGTH_FLOOR_FRAC * _floor) try: old_duration = float(cycle.get("duration") or 0.0) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): continue if old_duration - new_duration < BANKED_TAIL_REPAIR_MIN_S: continue - self._apply_repaired_duration(cycle, new_duration) + # One malformed row is skipped, not allowed to abort the pass: an + # abort left every cycle before it repaired, every one after it + # not, and the pending key set, so each restart stopped on the + # same row (register item 384). + try: + self._apply_repaired_duration(cycle, new_duration) + except Exception: # noqa: BLE001 + self._logger.debug( + "Banked-tail repair: skipped cycle %s", cycle.get("id"), exc_info=True + ) + continue summary["repaired"] += 1 summary["reclaimed_s"] += old_duration - new_duration name = cycle.get("profile_name") @@ -5962,13 +6927,9 @@ class ProfileStore: the shortened cycles, the template and its duration would describe different traces until the next restart. """ - cycle["duration"] = round(float(new_duration), 3) cycle_id = str(cycle.get("id") or "") if cycle_id: - for key in [ - k for k in self._cached_sample_segments if k[0] == cycle_id - ]: - del self._cached_sample_segments[key] + self._drop_cached_sample_segments(cycle_id) start_raw = cycle.get("start_time") start_dt = _parse_start_dt(start_raw) if start_raw else None if start_dt is not None: @@ -6048,6 +7009,30 @@ class ProfileStore: cycle.get("id"), exc_info=True, ) + # Last, so a row that fails above keeps its old duration with its old trace + # rather than a cut duration over an uncut one (register item 384). + cycle["duration"] = round(float(new_duration), 3) + + def profile_trusted_min_duration(self, profile_name: str) -> float | None: + """Shortest user-vouched length of this profile, or None (item 384). + + Element 13 of the live match tuple. Read from ``past_cycles`` and + ``reference_cycles``, the lists the banked-tail repair takes it from, so + the live floor and the repair's floor are the same number. Never raises. + """ + try: + past = self._data.get("past_cycles") + refs = self._data.get("reference_cycles") + cycles = [ + c for c in ( + *(past if isinstance(past, list) else []), + *(refs if isinstance(refs, list) else []), + ) + if isinstance(c, dict) and c.get("profile_name") == profile_name + ] + return _trusted_min_by_profile(cycles).get(profile_name) + except Exception: # noqa: BLE001 - a statistic must never break matching + return None def profile_terminal_quiet_seconds(self, profile_name: str) -> float | None: """How long this programme is measured to stay quiet after its last real @@ -6077,8 +7062,9 @@ class ProfileStore: # decompresses and scans every evidence cycle of the profile, and this # runs on the EVENT LOOP: it is element 11 of the live match tuple, so # a match tick pays for it. Measured on the worst export in - # `cycle_data/` (20 cycles, 24172 samples) it is 0.77 ms, but at the - # 200-cycle retention cap with long dishwasher traces it is **91 ms**, + # `cycle_data/` (20 cycles, 24172 samples) it is 0.77 ms, but at 200 + # long dishwasher traces it is **91 ms** (and history is unbounded since + # 0.5.8), # which is a visible stall however rare the tick. The repair loop # already memoises this per run for the same reason plus determinism. # @@ -6129,6 +7115,99 @@ class ProfileStore: # remembering one would pin "no opinion" until the evidence changes. return None + def profile_pauses_below( + self, profile_name: str, stop_threshold_w: float + ) -> bool | None: + """Has this programme ever paused below ``stop_threshold_w`` mid-cycle? (#424) + + True when at least one of its traced evidence cycles holds a run below the + threshold lasting ``SMART_TERM_PREFIX_MIN_PAUSE_S`` that power later + RESUMED from - a soak, a passive drying phase, anything the end gates could + mistake for an end. False when it has traced evidence and none of it ever + did. None when there is no traced evidence: no opinion. + + Read by the #288 prefix-landscape term (``_match_prefix_ambiguity``, now + the anti-crease finalize's guard only). A longer candidate that has never + paused below this threshold cannot be the programme a quiet below it + belongs to. Measured on #424's second reporter, when the term still gated + Smart Termination: a dishwasher whose programmes never drop below 1.44 W + until it switches itself off had every cycle blocked by a three-hour + programme sharing its first hour, and ended on a one-hour fallback. + + Any single pause keeps the guard (the strictest reading, and the one the + measurement used); see ``signal_processing.has_resumed_pause`` for how a + pause is timed. Cached on the evidence fingerprint and the threshold. + Never raises. + """ + try: + stop = float(stop_threshold_w) + if not math.isfinite(stop) or stop <= 0: + return None + cache = getattr(self, "_pause_cache", None) + if cache is None: + cache = self._pause_cache = {} + key = (profile_name, round(stop, 4)) + fingerprint = self._terminal_quiet_fingerprint(profile_name) + hit = cache.get(key) + if hit is not None and hit[0] == fingerprint: + return hit[1] + traced = 0 + seen = False + for cycle in self.iter_evidence_cycles(): + if cycle.get("profile_name") != profile_name or not cycle.get("power_data"): + continue + points = decompress_power_data(cast(Any, cycle)) + if len(points) < 2: + continue + traced += 1 + if _has_resumed_pause(points, stop, SMART_TERM_PREFIX_MIN_PAUSE_S): + seen = True + break + value: bool | None = seen if traced else None + cache[key] = (fingerprint, value) + return value + except Exception: # noqa: BLE001 - a statistic must never break matching + return None + + def profile_pause_catalogue( + self, profile_name: str, stop_threshold_w: float + ) -> tuple[int, tuple[tuple[float, float], ...]] | None: + """``(traced cycles, pauses)`` for the hazard end gate (audit DETECT-16). + + Every below-``stop_threshold_w`` pause that power later resumed from, as + ``(start_fraction, seconds)``, across the profile's traced evidence cycles. + The detector asks "has this programme ever paused this long from this far + in?" and waits only that long. None without traced evidence. Cached on the + evidence fingerprint and the threshold; never raises. + """ + try: + stop = float(stop_threshold_w) + if not math.isfinite(stop) or stop <= 0: + return None + cache = getattr(self, "_pause_catalogue_cache", None) + if cache is None: + cache = self._pause_catalogue_cache = {} + key = (profile_name, round(stop, 4)) + fingerprint = self._terminal_quiet_fingerprint(profile_name) + hit = cache.get(key) + if hit is not None and hit[0] == fingerprint: + return hit[1] + traced = 0 + pauses: list[tuple[float, float]] = [] + for cycle in self.iter_evidence_cycles(): + if cycle.get("profile_name") != profile_name or not cycle.get("power_data"): + continue + points = decompress_power_data(cast(Any, cycle)) + if len(points) < 10: + continue + traced += 1 + pauses.extend(_resumed_pauses(points, stop)) + value = (traced, tuple(pauses)) if traced else None + cache[key] = (fingerprint, value) + return value + except Exception: # noqa: BLE001 - a statistic must never break matching + return None + def _terminal_quiet_fingerprint(self, profile_name: str) -> tuple[int, str, int]: """Cheap stand-in for "has this profile's evidence changed?". @@ -6187,6 +7266,24 @@ class ProfileStore: 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``. + + **The band decides only whether the guard arms (register item 207).** Its + members are DTW-warped onto the reference before the pointwise max, so its + terminal block starts later than the spins of the cycles that formed it + (8372 s against eight own spins at 7490-8168 s); a run whose spin lands + before that never earned ``seen`` and the finalise waited out the 1.25x + cap. Once armed, the scan position is the earliest of the members' own + terminal spins and the length the shortest of them, each capped at the + band's value, so the change can only shorten a hold. Members never arm a + profile the band leaves unarmed. + + **They can disarm it (register item 480).** The band is a pointwise max, so + one member whose spin crossed the level arms the programme for every run, + and a washer's spin straddles 400 W: 8 of the 14 runs held to the cap on the + corpus spun at 331-394 W and could never earn the release. Fewer than + ``ANTI_CREASE_SPIN_ARM_MIN_SHARE`` of the completed traced members with a + terminal block of their own (out of at least + ``ANTI_CREASE_SPIN_ARM_MIN_MEMBERS``) returns None. """ try: level = float(threshold_w) @@ -6213,84 +7310,86 @@ class ProfileStore: 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: + block = _last_high_block(list(times), list(powers), level) + if block is None or block[0] < ANTI_CREASE_TERMINAL_HIGH_MIN_FRAC: + return block # not armed; the members never arm a profile (item 207) + spins = self._member_terminal_spins(profile_name, level) + if spins is None: + return block # no completed traced member: the band decides + starts, lengths, members = spins + # Register item 480: a programme most of whose runs end without a block + # above the level does not owe this run one. Measured (end_gate_eval + # --loo --all-formats --anti-wrinkle force --tumble-tail): cap releases + # 14 -> 9, early releases 6 -> 6, cut 9 -> 9. + if ( + members >= ANTI_CREASE_SPIN_ARM_MIN_MEMBERS + and len(starts) < ANTI_CREASE_SPIN_ARM_MIN_SHARE * members + ): 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) + if not starts: + return block + # Register item 207: scan from the earliest member spin and ask for half + # the SHORTEST member spin, which every observed run of this programme + # reaches. Never later or longer than the band's, so a hold can only get + # shorter. Measured (end_gate_eval --loo --all-formats --anti-wrinkle + # force --tumble-tail): cap holds 18 -> 14, no new early release. A 3% + # earlier scan released 3 more but split one run on an interim spin. + return (block[0], min(block[1], lengths[0]), min(block[2], starts[0])) except Exception: # noqa: BLE001 return None + def _member_terminal_spins( + self, profile_name: str, level: float + ) -> tuple[tuple[float, ...], tuple[float, ...], int] | None: + """``(starts, lengths, members)``: the start offsets and lengths, both + sorted, of the terminal block above ``level`` in each of the profile's + completed traced evidence cycles that has one (register item 207), and how + many such cycles were read (item 480). None when there are none to read. + + A member offers a position only when its OWN last block above ``level`` is + terminal (``ANTI_CREASE_TERMINAL_HIGH_MIN_FRAC`` of its quiet-trimmed span), so a run + whose spin stayed under the level does not offer its heating as a spin + position. Only ``completed`` cycles: an interrupted or force-stopped trace + ends wherever it was cut, and its last block reads as terminal at any + position. Cached per level on the evidence fingerprint, because this + decompresses every member and runs on the event loop with each match tick. + """ + cache = self.__dict__.setdefault("_member_terminal_spins_cache", {}) + key = (profile_name, round(level, 4)) + fingerprint = self._terminal_quiet_fingerprint(profile_name) + hit = cache.get(key) + if hit is not None and hit[0] == fingerprint: + return cast( + "tuple[tuple[float, ...], tuple[float, ...], int] | None", hit[1] + ) + starts: list[float] = [] + lengths: list[float] = [] + members = 0 + for cycle in self.iter_evidence_cycles(): + if ( + cycle.get("profile_name") != profile_name + or cycle.get("status") != "completed" + or not cycle.get("power_data") + ): + continue + points = decompress_power_data(cycle) + if len(points) < 10: + continue + members += 1 + block = _last_high_block( + [float(pt[0]) for pt in points], [float(pt[1]) for pt in points], level + ) + if block is not None and block[0] >= ANTI_CREASE_TERMINAL_HIGH_MIN_FRAC: + starts.append(block[2]) + lengths.append(block[1]) + value = ( + (tuple(sorted(starts)), tuple(sorted(lengths)), members) if members else None + ) + if len(cache) > 64: + cache.clear() + cache[key] = (fingerprint, value) + return value + def reference_curve( self, profile_name: str, n: int = REFERENCE_PROFILE_CURVE_POINTS ) -> JSONDict | None: @@ -6316,11 +7415,35 @@ class ProfileStore: Pure statistics (no ML); never raises - returns ``None`` when the envelope is missing or too short to be meaningful. + + Memoised per envelope revision: the program sensor reads it on every + state write, and a rebuild replaces the envelope dict (register item 456). + The memo holds the envelope itself, so the identity test cannot be fooled + by a recycled ``id()``. """ try: env = self.get_envelope(profile_name) if not isinstance(env, dict): return None + memo = self.__dict__.setdefault("_reference_curve_memo", {}) + hit = memo.get((profile_name, n)) + if hit is None or hit[0] is not env or hit[1] != env.get("updated"): + if len(memo) > 256: + memo.clear() + hit = (env, env.get("updated"), ProfileStore._compute_reference_curve(env, n)) + memo[(profile_name, n)] = hit + curve = hit[2] + if curve is None: + return None + # A copy: the attribute dict goes to HA's state machine. + return {**curve, "points": [list(p) for p in curve["points"]]} + except Exception: # pragma: no cover - defensive; never break the sensor + return None + + @staticmethod + def _compute_reference_curve(env: JSONDict, n: int) -> JSONDict | None: + """Downsampled avg curve of one envelope (see :meth:`reference_curve`).""" + try: avg = env.get("avg") if ( not isinstance(avg, list) @@ -6377,6 +7500,27 @@ class ProfileStore: env = self.get_envelope(profile_name) if not isinstance(env, dict): return [] + # Memoised per envelope revision: every profile sensor reads this on + # every state write, and the envelope only changes on a rebuild, which + # replaces the dict (audit PERF-01). + memo_key = (profile_name, id(env), env.get("updated"), float(interval_s)) + memo = getattr(self, "_power_profile_memo", None) + if memo is None: + memo = self._power_profile_memo = {} + if memo_key in memo: + return list(memo[memo_key]) + result = self._compute_profile_power_profile(env, interval_s) + if len(memo) > 256: + memo.clear() + memo[memo_key] = result + return list(result) + except Exception: # noqa: BLE001 + return [] + + @staticmethod + def _compute_profile_power_profile(env: JSONDict, interval_s: float) -> list[float]: + """Fixed-interval bucket means of an envelope's avg curve (see caller).""" + try: avg = env.get("avg") if ( not isinstance(avg, list) @@ -6495,10 +7639,14 @@ class ProfileStore: 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 + ``measured`` counts the cycles that showed it plus + those that stayed quiet at least ``quiet_before_s`` + without it; with nothing seen, every measurable one. + A degenerate trace (too few points, all-zero, zero + span) is never counted + censored cycles left out of ``measured``: they ended before + ``quiet_before_s`` of quiet, so cannot say whether + the event would have come (register item 469) consistency seen_in / measured, 0-1 **Read `consistency` before trusting the rest.** The same appliance emits @@ -6522,6 +7670,20 @@ class ProfileStore: standby is not distinguishable from its running load at this resolution), so it reports ``seen_in: 0`` rather than a fabricated event. + **Main activity is not a terminal event, and a cut-off trace is censored** + (register item 469). The last run must peak at no more than + ``TERMINAL_EVENT_MAX_PEAK_FRAC`` of the cycle's peak: a dishwasher's heating + blocks are 120-160 s apart on 01KGM619, which ``TERMINAL_QUIET_MIN_S`` + cannot separate, so a cycle closed before its pump-out offered its last + heating block, and the median of 4840-4860 s pump-out quiets and 120-160 s + heating gaps read 2500 s, a quiet no cycle has. A cycle whose last run is + main activity (or follows it within ``TERMINAL_QUIET_MIN_S``) observed no + event, and its quiet tail says only "none came in this long": it counts + against ``consistency`` once that tail reaches ``quiet_before_s`` (the event + is due and did not come), and is censored otherwise. Counting every such + cycle as an absence made the same profile read 6/20 consistent instead of + 6/9, because most of its stored cycles had been closed before the pump-out. + Never raises; returns ``None`` on any error. """ try: @@ -6538,6 +7700,8 @@ class ProfileStore: watts: list[float] = [] watt_fracs: list[float] = [] positions: list[float] = [] + # Quiet tail of each cycle that observed no event (item 469). + no_event_tails: list[float] = [] measured = 0 for cycle in cycles: @@ -6556,8 +7720,9 @@ class ProfileStore: # 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. + # Every cycle BELOW this line either showed the event or records + # how long it stayed quiet without one, which decides after the + # loop whether it is an absence or censored (item 469). measured += 1 # Walk back to the last run above the threshold, then to the start @@ -6572,8 +7737,6 @@ class ProfileStore: 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 @@ -6583,15 +7746,20 @@ class ProfileStore: if points[i][1] > threshold: prev_i = i break - if prev_i is None: - continue # nothing before it: this is the main activity + event_peak = max(p for _t, p in points[start_i : end_i + 1]) + if ( + prev_i is None # the run reaches the start, or nothing precedes it + or points[start_i][0] - points[prev_i][0] < TERMINAL_QUIET_MIN_S + or event_peak > peak * TERMINAL_EVENT_MAX_PEAK_FRAC + ): + # The last run is the main activity (item 469): no event seen, + # only how long the trace stayed quiet after it. + no_event_tails.append(points[-1][0] - points[end_i][0]) + continue 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) @@ -6607,16 +7775,23 @@ class ProfileStore: "position_frac": None, "seen_in": 0, "measured": measured, + "censored": 0, "consistency": 0.0, } + quiet_before = float(np.median(quiet)) + # A trace that stayed quiet as long as the event usually takes and saw + # none is an absence; one that ended sooner is censored (item 469). + absent = sum(1 for tail in no_event_tails if tail >= quiet_before) + measured = len(quiet) + absent return { - "quiet_before_s": round(float(np.median(quiet)), 1), + "quiet_before_s": round(quiet_before, 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, + "censored": len(no_event_tails) - absent, "consistency": round(len(quiet) / measured, 3), } except Exception: # noqa: BLE001 - a statistic must never break the panel @@ -6779,7 +7954,7 @@ class ProfileStore: Mirrors the precedence in `_build_match_snapshots`: a profile with a pinned golden cycle keeps its own `avg_duration`, but otherwise an - envelope with `cycle_count >= 2` supplies `target_duration` FIRST - + envelope shaped by >= 2 cycles (`envelope_shape_count`) supplies `target_duration` FIRST - which is the duration of the cycle nearest the median (`compute_envelope_worker`), not the outlier-filtered mean. @@ -6799,13 +7974,13 @@ class ProfileStore: for c in self.iter_evidence_cycles() ) envelope = (self._data.get("envelopes") or {}).get(name) - if not has_golden and envelope and envelope.get("cycle_count", 0) >= 2: + if not has_golden and envelope and envelope_shape_count(envelope) >= 2: env_avg = envelope.get("avg") span = 0.0 if env_avg and len(env_avg) > 1: try: span = float(env_avg[-1][0]) - float(env_avg[0][0]) - except (TypeError, ValueError, IndexError): + except (TypeError, ValueError, IndexError, OverflowError): span = 0.0 return float( envelope.get("target_duration") @@ -6813,9 +7988,13 @@ class ProfileStore: or span or 0.0 ) - return float(profile.get("avg_duration") or 0.0) + # The sample-cycle path: avg_duration, then the sample's own duration + # and trace span, as the snapshot builder takes them. + return float(self.resolve_profile_duration(name) or 0.0) except Exception: # pylint: disable=broad-exception-caught - return float(profile.get("avg_duration") or 0.0) + # The envelope path's float() can be what raised (an oversized + # target_duration); the fallback must not raise again. + return float(self.resolve_profile_duration(name) or 0.0) def _self_unmatchable_cycles(self) -> dict[str, list[dict[str, Any]]]: """Labelled cycles that fall outside the duration gate for their OWN profile. @@ -6852,7 +8031,7 @@ class ProfileStore: continue try: dur = float(cycle.get("duration") or 0.0) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): continue if dur > 60: by_profile.setdefault(str(name), []).append(cycle) @@ -6991,8 +8170,8 @@ class ProfileStore: resumes = (j + 1 < n) and float(np.max(p_obs[j + 1:])) > active_thr if dur >= min_dur[s] and (s != "pause" or resumes): if s == "pause": - detail = (f"Power dropped to near zero for ~{int(dur)}s then resumed — " - "likely the door was opened mid-cycle or the cycle was paused.") + detail = (f"Power dropped to near zero for ~{int(dur)}s, then resumed. " + "The door was probably opened or the cycle paused.") detail_key = "msg.artifact_pause_detail" elif s == "dip": detail = f"Drew below the usual power band for ~{int(dur)}s." @@ -7040,10 +8219,13 @@ class ProfileStore: corr = round(metrics.get("corr", 0.0), 3) profile_duration = candidate.get("profile_duration", 0.0) - actual_duration = match_result.expected_duration + # The cycle's elapsed duration at this match. It used to be the + # WINNER's expected duration, so the winner always read +0.0% and + # the rest their ratio to it (audit MATCH-DECIDE-13). + actual_duration = float(getattr(match_result, "query_duration_s", 0.0) or 0.0) duration_ratio = ( round((actual_duration / profile_duration - 1.0) * 100, 1) - if profile_duration > 0 + if profile_duration > 0 and actual_duration > 0 else 0.0 ) @@ -7054,11 +8236,221 @@ class ProfileStore: "correlation": corr, "duration_ratio": duration_ratio, # ±% from expected }) - except (TypeError, ValueError, KeyError): + except (TypeError, ValueError, KeyError, OverflowError): continue return candidates + def _envelope_template_on_grid( + self, name: str, envelope: JSONDict, used_dt: float + ) -> list[float]: + """The envelope's average curve resampled onto the query's grid. + + Stage 2 compares template and query sample by sample, so both must share a + time step. Sample-cycle templates were re-gridded to ``used_dt``; envelope + templates - the template for every profile with two or more cycles - were + passed on their own grid (the raw median cadence of the members), so the + comparison paired different moments of the cycle wherever the two cadences + differed: item 303's bug, still live for the common case. 31% of cycles with + an envelope sat more than 20% off-grid; re-gridding measured +1.28 to + +1.44pp complete-cycle top-1 (8-10 better, 0-1 worse; audit MATCH-CORE-01, + MR-01, MATCH-EVAL-02). Cached per envelope revision and grid step. + """ + avg = envelope.get("avg") or [] + key = (name, id(envelope), envelope.get("updated"), float(round(used_dt, 2))) + cache = getattr(self, "_envelope_template_cache", None) + if cache is None: + cache = self._envelope_template_cache = {} + hit = cache.get(key) + if hit is not None and hit[0] is envelope: + return hit[1] + values = [float(p[1]) for p in avg] + try: + ts = np.asarray([float(p[0]) for p in avg], dtype=float) + pw = np.asarray(values, dtype=float) + finite = np.isfinite(ts) & np.isfinite(pw) + ts, pw = ts[finite], pw[finite] + if ts.size >= 2 and used_dt > 0: + segs = resample_uniform(ts, pw, dt_s=used_dt, gap_s=21600.0) + if segs: + seg = max(segs, key=lambda q: len(q.power)) + if len(seg.power) >= 2: + values = [float(v) for v in seg.power] + except Exception: # noqa: BLE001 - fall back to the raw curve, never fail a match + pass + if len(cache) > 512: + cache.clear() + # The envelope itself is held, so its id cannot be recycled while cached. + cache[key] = (envelope, values) + return values + + def build_match_snapshots(self, used_dt: float) -> list[dict[str, Any]]: + """The candidate templates a match scores against, on the query's grid. + + One builder for live matching and the Playground (register item 387a). The + Playground used to build its own - always the sample cycle's raw, unevenly + spaced points sized by ``avg_duration`` - while live matching uses the + envelope average (sized by ``target_duration``) once a profile has two + cycles, or the pinned golden cycle, and re-grids a sample cycle to + ``used_dt``. Top-1 differed on 26.5% of matches, so every replay harness + measured a different matcher from the one users run. + + Reads the store but writes only the sample-segment cache; iterates a copy of + the profile map so it is safe from the executor (the Playground's thread). + Does not group: callers pass the result to ``_grouped_snapshots``. + """ + # Prepare Snapshots. Imported reference cycles and backfilled history + # cycles are eligible as matching templates alongside real cycles (so an + # import-only profile can match) - subject to the user's evidence choice, so + # the pool and the envelope always agree about what a profile looks like. + all_cycles = self.iter_evidence_cycles() + # Precompute per-profile lookups ONCE so the loop below is O(profiles), + # not O(profiles x cycles). Rescanning all_cycles with next()/any() for + # every profile made matching quadratic and stalled low-power hosts on + # auto-label (many matches x many cycles) - issue #311. Selections are + # byte-identical: cycles_by_id keeps the FIRST occurrence (== next()), + # labeled_by_profile keeps the first eligible cycle in all_cycles order, + # and golden_profiles mirrors the any(...) golden test. + cycles_by_id: dict[str, CycleDict] = {} + labeled_by_profile: dict[str, CycleDict] = {} + golden_profiles: set[str] = set() + for c in all_cycles: + cid = c.get("id") + if cid is not None and cid not in cycles_by_id: + cycles_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 + rev = c.get("ml_review") + if isinstance(rev, dict) and rev.get("golden"): + golden_profiles.add(pname) + + snapshots: list[dict[str, Any]] = [] + skipped_profiles: list[str] = [] + for name, profile in list((self._data.get("profiles") or {}).items()): + # Try sample_cycle_id first, fall back to any labeled cycle + sample_id = profile.get("sample_cycle_id") + sample_cycle = cycles_by_id.get(sample_id) if sample_id else None + # Fallback: find ANY completed cycle labeled with this profile + if not sample_cycle: + sample_cycle = labeled_by_profile.get(name) + # Boost user-pinned "golden" cycles: when a profile has one, use + # its sharp single-cycle trace as the matching template instead + # of the envelope average. The envelope average smears the + # wash-phase peaks (each cycle's spikes land at slightly + # different times), which hurts correlation for sharply-shaped + # programs; a trusted golden cycle preserves that shape. + has_golden = name in golden_profiles + + # Prefer envelope avg curve when ≥2 labeled cycles have been + # confirmed - it gives a more representative reference signal + # than the original sample alone, so confidence improves over + # time as the user keeps confirming correct detections. Skipped + # when a golden cycle is pinned (see above). + envelope = self._data.get("envelopes", {}).get(name) + _env_avg = envelope.get("avg") if envelope else None + if ( + not has_golden + and envelope + and envelope_shape_count(envelope) >= 2 + and _env_avg + and isinstance(_env_avg[0], (list, tuple)) + and len(_env_avg[0]) >= 2 + ): + avg_y = self._envelope_template_on_grid(name, envelope, used_dt) + _env_ts_duration = ( + float(_env_avg[-1][0]) - float(_env_avg[0][0]) + if len(_env_avg) > 1 else 0.0 + ) + avg_duration = ( + envelope.get("target_duration") or + profile.get("avg_duration") or + _env_ts_duration or + None + ) + if not avg_duration: + skipped_profiles.append( + f"{name}: no valid duration (envelope has no target_duration, avg_duration, or timestamp span)" + ) + continue + snapshots.append({ + "name": name, + "avg_duration": float(avg_duration), + "sample_power": avg_y, + # True wall-clock span of `sample_power` (#364). NOT the same + # as avg_duration, which prefers target_duration / the + # profile's rolling mean - so index fraction only equals time + # fraction against this. Needed to truncate the curve to an + # elapsed duration for prefix scoring. + "sample_span_s": float(_env_ts_duration or avg_duration), + "energy_ref": envelope.get("energy_ref"), + }) + continue + + if not sample_cycle: + skipped_profiles.append( + f"{name}: no sample cycle (sample_id={sample_id})" + ) + continue + + # Prepare sample segment (using cache) + sample_seg = self._get_cached_sample_segment(sample_cycle, used_dt) + if not sample_seg: + skipped_profiles.append( + f"{name}: failed to resample cycle {sample_cycle.get('id')}" + ) + continue + # avg_duration preference order: + # 1. profile["avg_duration"] (rolling average, most accurate) + # 2. sample_cycle["duration"] (raw cycle field) + # 3. timestamp span of sample_seg (estimate from the resampled data) + # Profiles created before avg_duration tracking was added may have + # 0 or a missing value; falling back to the segment estimate prevents + # update_match() from always seeing expected_duration=0, which + # silences time-remaining estimates and logs a misleading warning. + _seg_ts_duration = ( + float(sample_seg.timestamps[-1]) - float(sample_seg.timestamps[0]) + if len(sample_seg.timestamps) > 1 else 0.0 + ) + avg_dur = ( + profile.get("avg_duration") or + sample_cycle.get("duration") or + _seg_ts_duration + ) + if not avg_dur: + skipped_profiles.append( + f"{name}: no valid duration (avg_duration, cycle duration, and timestamp span all zero/missing)" + ) + continue + snapshots.append({ + "name": name, + "avg_duration": float(avg_dur), + "sample_power": sample_seg.power.tolist(), + "sample_dt": used_dt, + # True wall-clock span of `sample_power` (#364). _get_cached_sample_segment + # keeps only the LONGEST gap-free segment, so a cycle with an internal + # outage yields a curve covering less than avg_dur - truncating by a + # fraction of avg_dur would then cut the wrong place. + "sample_span_s": float(_seg_ts_duration or avg_dur), + # Stage 4 still takes the expected energy from the profile's own + # cycles when it has them (a golden template, or one cycle). + "energy_ref": envelope.get("energy_ref") if isinstance(envelope, dict) else None, + }) + + if skipped_profiles: + self._logger.debug( + "Profile matching skipped %d profiles: %s", + len(skipped_profiles), + "; ".join(skipped_profiles) + ) + + return snapshots + def _get_cached_sample_segment( self, sample_cycle: dict[str, Any], dt: float ) -> Segment | None: @@ -7071,8 +8463,12 @@ class ProfileStore: dt_key = float(round(dt, 2)) key = (cycle_id, dt_key) - if key in self._cached_sample_segments: - return self._cached_sample_segments[key] + with self._segment_cache_lock(): + cache = self._cached_sample_segments + hit = cache.pop(key, None) + if hit is not None: + cache[key] = hit # most recently used last + return hit # Miss: Compute sample_data = sample_cycle.get("power_data") @@ -7081,8 +8477,16 @@ class ProfileStore: try: if len(sample_data) > 0 and isinstance(sample_data[0], (list, tuple)): - s_ts = np.array([x[0] for x in sample_data]) - s_p = np.array([x[1] for x in sample_data]) + s_ts = np.array([x[0] for x in sample_data], dtype=float) + s_p = np.array([x[1] for x in sample_data], dtype=float) + # Imported data can carry NaN (json.loads accepts it); envelopes + # already drop non-finite rows, the sample template did not + # (audit MATCH-CORE-05). + finite = np.isfinite(s_ts) & np.isfinite(s_p) + if not finite.all(): + s_ts, s_p = s_ts[finite], s_p[finite] + if s_ts.size < 2: + return None else: return None @@ -7092,18 +8496,58 @@ class ProfileStore: sample_seg = max(s_segments, key=lambda s: len(s.power)) - # Store - self._cached_sample_segments[key] = sample_seg + # Store, evicting the least recently used. Live matching's grid step + # drifts on change-only plugs (2.75 distinct steps per cycle, up to + # 22) and nothing dropped a deleted cycle's entries, so the cache + # grew for the life of the install (audit ML-12). + cap = self._sample_segment_cache_cap() + with self._segment_cache_lock(): + cache = self._cached_sample_segments + cache.pop(key, None) + cache[key] = sample_seg + while len(cache) > cap: + del cache[next(iter(cache))] return sample_seg except Exception as e: # pylint: disable=broad-exception-caught self._logger.warning("Error caching sample segment %s: %s", cycle_id, e) return None + def _segment_cache_lock(self) -> threading.Lock: + """The sample-segment cache's lock (created on first use for bare fixtures).""" + lock = self.__dict__.get("_sample_segment_lock") + if lock is None: + lock = self.__dict__.setdefault("_sample_segment_lock", threading.Lock()) + return cast(threading.Lock, lock) + + def _sample_segment_cache_cap(self) -> int: + """Entries the sample-segment cache keeps. + + At least two grid steps per profile, so one match (one lookup per + sample-template profile) never evicts its own entries; never below + ``SAMPLE_SEGMENT_CACHE_MIN``. + """ + profiles = self._data.get("profiles") if isinstance(self._data, dict) else None + count = len(profiles) if isinstance(profiles, dict) else 0 + return max(SAMPLE_SEGMENT_CACHE_MIN, 2 * count) + + def _drop_cached_sample_segments(self, cycle_id: str) -> None: + """Forget every cached segment of ``cycle_id`` (its trace changed). + + Iterates a copy under the lock: the Playground inserts from an executor + thread, and a live walk raised "dictionary changed size during + iteration" (audit PLAYGROUND-22). + """ + with self._segment_cache_lock(): + cache = self._cached_sample_segments + for key in [k for k in list(cache) if k[0] == cycle_id]: + cache.pop(key, None) + async def async_match_profile( 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, + stop_threshold_w: float | None = None, ) -> MatchResult: """Run profile matching asynchronously in executor. @@ -7112,6 +8556,10 @@ class ProfileStore: 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. + + ``stop_threshold_w`` lets the prefix-landscape guard drop longer candidates + that have never paused below it (#424, ``profile_pauses_below``). Omitted, + the guard behaves as before. """ # 1. Prepare data in main thread (Access ProfileStore state safely) group_members: dict[str, list[str]] = {} @@ -7154,151 +8602,7 @@ class ProfileStore: current_power_list = current_seg.power.tolist() - # Prepare Snapshots. Imported reference cycles and backfilled history - # cycles are eligible as matching templates alongside real cycles (so an - # import-only profile can match) - subject to the user's evidence choice, so - # the pool and the envelope always agree about what a profile looks like. - all_cycles = self.iter_evidence_cycles() - # Precompute per-profile lookups ONCE so the loop below is O(profiles), - # not O(profiles x cycles). Rescanning all_cycles with next()/any() for - # every profile made matching quadratic and stalled low-power hosts on - # auto-label (many matches x many cycles) - issue #311. Selections are - # byte-identical: cycles_by_id keeps the FIRST occurrence (== next()), - # labeled_by_profile keeps the first eligible cycle in all_cycles order, - # and golden_profiles mirrors the any(...) golden test. - cycles_by_id: dict[str, CycleDict] = {} - labeled_by_profile: dict[str, CycleDict] = {} - golden_profiles: set[str] = set() - for c in all_cycles: - cid = c.get("id") - if cid is not None and cid not in cycles_by_id: - cycles_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 - rev = c.get("ml_review") - if isinstance(rev, dict) and rev.get("golden"): - golden_profiles.add(pname) - - snapshots: list[dict[str, Any]] = [] - skipped_profiles: list[str] = [] - for name, profile in self._data["profiles"].items(): - # Try sample_cycle_id first, fall back to any labeled cycle - sample_id = profile.get("sample_cycle_id") - sample_cycle = cycles_by_id.get(sample_id) if sample_id else None - # Fallback: find ANY completed cycle labeled with this profile - if not sample_cycle: - sample_cycle = labeled_by_profile.get(name) - # Boost user-pinned "golden" cycles: when a profile has one, use - # its sharp single-cycle trace as the matching template instead - # of the envelope average. The envelope average smears the - # wash-phase peaks (each cycle's spikes land at slightly - # different times), which hurts correlation for sharply-shaped - # programs; a trusted golden cycle preserves that shape. - has_golden = name in golden_profiles - - # Prefer envelope avg curve when ≥2 labeled cycles have been - # confirmed - it gives a more representative reference signal - # than the original sample alone, so confidence improves over - # time as the user keeps confirming correct detections. Skipped - # when a golden cycle is pinned (see above). - envelope = self._data.get("envelopes", {}).get(name) - _env_avg = envelope.get("avg") if envelope else None - if ( - not has_golden - and envelope - and envelope.get("cycle_count", 0) >= 2 - and _env_avg - and isinstance(_env_avg[0], (list, tuple)) - and len(_env_avg[0]) >= 2 - ): - avg_y = [float(p[1]) for p in _env_avg] - _env_ts_duration = ( - float(_env_avg[-1][0]) - float(_env_avg[0][0]) - if len(_env_avg) > 1 else 0.0 - ) - avg_duration = ( - envelope.get("target_duration") or - profile.get("avg_duration") or - _env_ts_duration or - None - ) - if not avg_duration: - skipped_profiles.append( - f"{name}: no valid duration (envelope has no target_duration, avg_duration, or timestamp span)" - ) - continue - snapshots.append({ - "name": name, - "avg_duration": float(avg_duration), - "sample_power": avg_y, - # True wall-clock span of `sample_power` (#364). NOT the same - # as avg_duration, which prefers target_duration / the - # profile's rolling mean - so index fraction only equals time - # fraction against this. Needed to truncate the curve to an - # elapsed duration for prefix scoring. - "sample_span_s": float(_env_ts_duration or avg_duration), - }) - continue - - if not sample_cycle: - skipped_profiles.append( - f"{name}: no sample cycle (sample_id={sample_id})" - ) - continue - - # Prepare sample segment (using cache) - sample_seg = self._get_cached_sample_segment(sample_cycle, used_dt) - if not sample_seg: - skipped_profiles.append( - f"{name}: failed to resample cycle {sample_cycle.get('id')}" - ) - continue - # avg_duration preference order: - # 1. profile["avg_duration"] (rolling average, most accurate) - # 2. sample_cycle["duration"] (raw cycle field) - # 3. timestamp span of sample_seg (estimate from the resampled data) - # Profiles created before avg_duration tracking was added may have - # 0 or a missing value; falling back to the segment estimate prevents - # update_match() from always seeing expected_duration=0, which - # silences time-remaining estimates and logs a misleading warning. - _seg_ts_duration = ( - float(sample_seg.timestamps[-1]) - float(sample_seg.timestamps[0]) - if len(sample_seg.timestamps) > 1 else 0.0 - ) - avg_dur = ( - profile.get("avg_duration") or - sample_cycle.get("duration") or - _seg_ts_duration - ) - if not avg_dur: - skipped_profiles.append( - f"{name}: no valid duration (avg_duration, cycle duration, and timestamp span all zero/missing)" - ) - continue - snapshots.append({ - "name": name, - "avg_duration": float(avg_dur), - "sample_power": sample_seg.power.tolist(), - "sample_dt": used_dt, - # True wall-clock span of `sample_power` (#364). _get_cached_sample_segment - # keeps only the LONGEST gap-free segment, so a cycle with an internal - # outage yields a curve covering less than avg_dur - truncating by a - # fraction of avg_dur would then cut the wrong place. - "sample_span_s": float(_seg_ts_duration or avg_dur), - }) - - if skipped_profiles: - self._logger.debug( - "Profile matching skipped %d profiles: %s", - len(skipped_profiles), - "; ".join(skipped_profiles) - ) + snapshots = self.build_match_snapshots(used_dt) # Stage 5: map cohesive near-duplicate groups to their members (loose # groups stay individual). The members are scored individually and the @@ -7311,7 +8615,7 @@ class ProfileStore: "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. + # Empty live; the Playground's replay config (see _matching_overrides). **self._matching_overrides(), } @@ -7319,14 +8623,20 @@ class ProfileStore: self._logger.error("Preparation for async match failed: %s", e) return MatchResult(None, 0.0, 0.0, None, [], False, 0.0) - # 2. Run Heavy Logic in Executor - candidates = await self.hass.async_add_executor_job( - analysis.compute_matches_worker, - current_power_list, - current_duration, - cast(Any, snapshots), - config - ) + # 2. Run Heavy Logic in Executor. Guarded like the preparation above: the + # cycle-end final match runs before the cycle is stored, so a raise here + # must cost the match, never the cycle (audit MATCH-CORE-06). + try: + candidates = await self.hass.async_add_executor_job( + analysis.compute_matches_worker, + current_power_list, + current_duration, + cast(Any, snapshots), + config + ) + except Exception as e: # pylint: disable=broad-exception-caught + self._logger.error("Profile matching failed: %s", e) + return MatchResult(None, 0.0, 0.0, None, [], False, 0.0) # 3. Process Result (Main Thread) if not candidates: @@ -7380,6 +8690,7 @@ class ProfileStore: chosen, member_fit, member_dur = self._stage5_pick_member( current_power_list, current_duration, group_members[best_name], member_snaps, in_progress=in_progress, + preferred=best.get("group_best_member"), ) best_name = chosen if member_dur: @@ -7393,9 +8704,9 @@ class ProfileStore: if _chosen_cand is not None: try: member_confidence = float(_chosen_cand["score"]) - except (TypeError, ValueError, KeyError): + except (TypeError, ValueError, KeyError, OverflowError): member_confidence = None - # Safeguard #2: the group aggregate matched but if the chosen member + # Safeguard #2: the group won 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 + @@ -7436,9 +8747,9 @@ class ProfileStore: # 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. + # MatchResult.label_confidence gates labelling on. The ranking therefore + # records all three numbers and never implies the chosen member earned + # its sibling's score. candidates = [ { **best, @@ -7457,16 +8768,18 @@ class ProfileStore: # show user-assigned phase names even when confidence is moderate. matched_phase = self.check_phase_match(best_name, current_duration) - # Detect "prefix ambiguity": a non-winning candidate whose duration is - # significantly longer than the matched profile AND whose shape matched - # well before Stage-4 penalised its duration. When this is true the - # current trace may be a prefix of that longer program, not a complete - # short cycle. Signal cycle_detector to block Smart Termination; the - # power-based fallback timeout will decide instead. - full_shape_hit, prefix_fit_hit = _match_prefix_ambiguity( - candidates, best_duration or 0.0 + # The #288 prefix-landscape term: a non-winning candidate much longer + # than the matched profile whose shape matched well before Stage 4 + # penalised its duration, so the trace may be a prefix of that longer + # program. Only the anti-crease finalize reads it (audit LIVE-18). + pauses_fn: Callable[[str], bool | None] | None = None + if stop_threshold_w is not None: + pauses_fn = functools.partial( + self.profile_pauses_below, stop_threshold_w=float(stop_threshold_w) + ) + full_shape_hit = _match_prefix_ambiguity( + candidates, best_duration or 0.0, pauses_fn ) - is_prefix_ambiguous = full_shape_hit or prefix_fit_hit # From the pre-collapse population, not just before the [:5] truncation # below: see `pre_collapse_candidates` above for why the collapse is the # more dangerous of the two filters here. @@ -7481,10 +8794,10 @@ class ProfileStore: is_ambiguous, margin, 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, longest_candidate_duration_s=longest_candidate_s, + query_duration_s=float(current_duration or 0.0), ) async def async_verify_alignment( @@ -7522,7 +8835,7 @@ class ProfileStore: try: offsets = [float(x[0]) for x in current_power_data] powers = [float(x[1]) for x in current_power_data] - except (TypeError, ValueError, IndexError): + except (TypeError, ValueError, IndexError, OverflowError): # A legacy ISO-timestamped trace (x[0] is a string) lands here. The # signature admits ``list[tuple[Any, ...]]``, so normalize through the # shared helper instead of bailing out: returning early skipped @@ -7537,7 +8850,7 @@ class ProfileStore: try: offsets = [float(x[0]) for x in normalized] powers = [float(x[1]) for x in normalized] - except (TypeError, ValueError, IndexError): + except (TypeError, ValueError, IndexError, OverflowError): return False, 0.0, 9999.0 current_power_list = self._resample_trace_to_grid(offsets, powers, env_time) @@ -7620,6 +8933,9 @@ class ProfileStore: if not cycle: raise ValueError("Cycle not found") + # The cycle may be moving here from another profile: that profile must not + # keep it as its sample, and its envelope loses the cycle. + leaving = cycle.get("profile_name") cycle["profile_name"] = name self._data.setdefault("profiles", {})[name] = { @@ -7627,6 +8943,9 @@ class ProfileStore: "sample_cycle_id": source_cycle_id, } + if leaving and leaving != name: + self.heal_profile_sample(leaving) + await self.async_rebuild_envelope(leaving) await self.async_rebuild_envelope(name) # Save to persist the label await self.async_save() @@ -7651,21 +8970,105 @@ class ProfileStore: } return bool(assigned.intersection(profile_names)) - def list_profiles(self) -> list[dict[str, Any]]: - """List all profiles with metadata.""" + def _profiles_fingerprint(self) -> tuple[Any, ...]: + """Everything :meth:`list_profiles` reads, in a cheap comparable form. + + The summaries are rebuilt only when this changes, so the cache is correct + by construction rather than relying on every mutation site to invalidate + it. Building it is O(profiles + cycles) tuple work, far below the summary + build it guards (audit PERF-01). + """ + data = self._data + raw_profiles = data.get("profiles", {}) + raw_envs = data.get("envelopes", {}) + past = data.get("past_cycles", []) + prof_fp: tuple[Any, ...] = () + if isinstance(raw_profiles, dict): + prof_fp = tuple( + ( + name, + id(meta), + meta.get("avg_duration"), + meta.get("min_duration"), + meta.get("max_duration"), + meta.get("sample_cycle_id"), + ) + if isinstance(meta, dict) + else (name, None) + for name, meta in raw_profiles.items() + ) + env_fp: tuple[Any, ...] = () + if isinstance(raw_envs, dict): + env_fp = tuple( + (name, id(env), env.get("updated") if isinstance(env, dict) else None) + for name, env in raw_envs.items() + ) + past_fp: tuple[Any, ...] = () + if isinstance(past, list): + past_fp = tuple( + (c.get("profile_name"), c.get("start_time"), c.get("cost")) + for c in past + if isinstance(c, dict) + ) + ref_fp = tuple(c.get("profile_name") for c in self.get_reference_cycles()) + bf_fp = tuple(c.get("profile_name") for c in self.get_backfill_cycles()) + return ( + id(raw_profiles), prof_fp, id(raw_envs), env_fp, id(past), past_fp, + ref_fp, bf_fp, + ) + + def _profile_summaries(self) -> tuple[list[JSONDict], dict[str, JSONDict]]: + """Cached profile summaries (sorted list + by-name map); see list_profiles. + + Every entity write used to rebuild every profile's statistics: ``get_profile`` + was ``list_profiles()`` plus a scan, called 3x per profile sensor per power + reading, so a 13-profile washer spent ~200 ms of event loop per reading + (audit PERF-01). Returned rows are shared; callers get copies. + """ + fingerprint = self._profiles_fingerprint() + cached = getattr(self, "_profile_summary_cache", None) + if cached is not None and cached[0] == fingerprint: + return cached[1], cached[2] + rows = self._build_profile_summaries() + by_name = {row["name"]: row for row in rows} + self._profile_summary_cache = (fingerprint, rows, by_name) + return rows, by_name + + def profile_summaries_revision(self) -> object: + """A token that is a new object whenever the profile summaries are rebuilt. + + Compare with ``is``. The per-profile sensors read only the summaries and + the envelopes, both covered by the summary cache's fingerprint, so they + need a state write only when this changes (register item 456). The caller + holds the old token, which keeps its identity unique. + """ + return self._profile_summaries()[0] + + def _build_profile_summaries(self) -> list[JSONDict]: + """Build one summary per profile in a single pass over the cycle lists.""" profiles: list[JSONDict] = [] raw_profiles = self._data.get("profiles", {}) profiles_map = ( cast(dict[str, Any], raw_profiles) if isinstance(raw_profiles, dict) else {} ) + cycles_by_profile: dict[str, list[CycleDict]] = {} + for c in self._data.get("past_cycles", []): + if isinstance(c, dict): + cycles_by_profile.setdefault(c.get("profile_name"), []).append(c) + reference_names = { + c.get("profile_name") for c in self.get_reference_cycles() if isinstance(c, dict) + } + backfill_counts: dict[Any, int] = {} + for c in self.get_backfill_cycles(): + if isinstance(c, dict): + key = c.get("profile_name") + backfill_counts[key] = backfill_counts.get(key, 0) + 1 + for name, data in profiles_map.items(): profile_meta = cast(JSONDict, data) if isinstance(data, dict) else {} # Calculate count and last_run - p_cycles = [ - c for c in self._data.get("past_cycles", []) - if c.get("profile_name") == name - ] + p_cycles = cycles_by_profile.get(name, []) cycle_count = len(p_cycles) last_run = None @@ -7724,18 +9127,37 @@ class ProfileStore: "avg_cost": avg_cost, "total_cost": total_cost, "signature_curve": sig_curve, - "is_imported": self.profile_has_reference_cycles(name), + "is_imported": name in reference_names, # Cycles recovered from imported history (#344). Reported separately # from cycle_count, which counts only real observed cycles, so a # profile built purely from backfilled history does not look empty. - "backfill_count": sum( - 1 for c in self.get_backfill_cycles() - if c.get("profile_name") == name - ), + "backfill_count": backfill_counts.get(name, 0), } ) return sorted(profiles, key=lambda p: profile_sort_key(p.get("name", ""))) + def list_profiles(self) -> list[dict[str, Any]]: + """List all profiles with metadata (copies of the cached summaries).""" + rows, _by_name = self._profile_summaries() + return [dict(row) for row in rows] + + def matcher_label_counts(self) -> dict[str, int]: + """Per profile: observed cycles the matcher labelled on its own (STORE-21). + + Only ``past_cycles`` (cycles recorded on this appliance) whose label came + from the matcher (``_AUTO_LABEL_SOURCES``); a label the user set or + corrected is the user's decision, not matcher evidence. Local only: shown + on imported program cards so an import that never fits can be pruned. + """ + counts: dict[str, int] = {} + for cycle in self._data.get("past_cycles", []) or []: + if not isinstance(cycle, dict): + continue + name = cycle.get("profile_name") + if name and cycle.get("label_source") in _AUTO_LABEL_SOURCES: + counts[name] = counts.get(name, 0) + 1 + return counts + async def create_profile_standalone( self, name: str, @@ -7771,6 +9193,10 @@ class ProfileStore: # Create profile with minimal data (will be updated when cycles are labeled) profile_data.setdefault("phases", []) self._data.setdefault("profiles", {})[name] = profile_data + # A reference cycle that already belongs to another programme seeds the + # duration only: as the sample it would make this profile a copy of that + # programme's run in the matcher (the state the setup repair clears). + self.heal_profile_sample(name) # Build the envelope from any already-labeled cycles (e.g. reference cycle above) await self.async_rebuild_envelope(name) @@ -7778,6 +9204,27 @@ class ProfileStore: await self.async_save() self._logger.info("Created standalone profile '%s'", name) + def learned_duration_profiles(self) -> set[str]: + """Profiles whose ``avg_duration`` the envelope rebuild computes (#158). + + The rebuild's own test: an evidence cycle labelled with the profile that + completed (or was force-stopped) and ran for over a minute. For these the + stored duration is derived from the cycles; a hand edit cannot stick. + """ + out: set[str] = set() + for cycle in self.iter_evidence_cycles(): + if not isinstance(cycle, dict): + continue + name = cycle.get("profile_name") + if not name or cycle.get("status") not in ("completed", "force_stopped"): + continue + try: + if float(cycle.get("duration") or 0.0) > 60: + out.add(str(name)) + except (TypeError, ValueError, OverflowError): + continue + return out + async def update_profile( self, old_name: str, new_name: str, avg_duration: float | None = None ) -> int: @@ -7806,13 +9253,19 @@ class ProfileStore: target_name = new_name if renamed else old_name - # Handle Duration Update - if avg_duration is not None and avg_duration > 0: + # Handle Duration Update. A profile with cycles has its duration computed + # by every envelope rebuild, so a hand-set value lasted only until the next + # cycle (#158); the panel shows that duration read-only and this refuses it. + learned = self.learned_duration_profiles() + if avg_duration is not None and avg_duration > 0 and ( + old_name in learned or target_name in learned + ): + self._logger.info( + "Ignored a manual duration for '%s': it is computed from its cycles", + target_name, + ) + elif avg_duration is not None and avg_duration > 0: profiles[target_name]["avg_duration"] = float(avg_duration) - # If there's an envelope, we ideally update its target_duration too, - # but envelope is usually rebuilt from data. - # However, for manual profiles, envelope might be empty or theoretical. - # Let's log it. self._logger.info( "Updated baseline duration for '%s' to %ss", target_name, @@ -7867,8 +9320,12 @@ class ProfileStore: if name not in self._data.get("profiles", {}): raise ValueError(f"Profile '{name}' not found") - # Delete profile + # Delete profile, and its envelope with it: nothing else removed it, so the + # orphan stayed in storage and in exports (audit ML-09). del self._data["profiles"][name] + envelopes = self._data.get("envelopes") + if isinstance(envelopes, dict): + envelopes.pop(name, None) # Handle cycles from every list: imported and backfilled cycles carry # profile_name too, so they would otherwise keep a dangling label for a @@ -7897,10 +9354,9 @@ class ProfileStore: self._data["feedback_history"] = {} self._data["pending_feedback"] = {} self._data["auto_adjustments"] = [] - self._data["active_cycle"] = None - self._data["last_active_save"] = None + self._active_data = {} # Newer persisted state must also be wiped, else a "wipe all" leaves trained - # models, groups, matcher tuning and histories behind. The two lifetime + # models, groups and histories behind. The two lifetime # odometers are the documented exception - see the note below. self._data["custom_phases"] = [] self._data["ml_model_versions"] = {} @@ -7908,8 +9364,10 @@ class ProfileStore: self._data["maintenance_log"] = [] self._data.pop("armed_program", None) self._data["playground_presets"] = {} - self._data["matching_config"] = {} - self._data["match_ranking_history"] = [] + # State of the ML parts removed in 0.5.8 (storage v17 drops it on load; an + # older wholesale import can bring it back, inert). + self._data.pop("matching_config", None) + self._data.pop("match_ranking_history", None) self._data["ml_last_training_run"] = None self._data["ml_training_history"] = {} # The two lifetime odometers deliberately SURVIVE a wipe (#414). They record @@ -7924,6 +9382,7 @@ class ProfileStore: self._cohesion_cache = {} self._cohesion_cache_generation += 1 await self.async_save() + await self._async_write_active() self._logger.info("Cleared all WashData storage") async def assign_profile_to_cycle( @@ -7964,13 +9423,20 @@ class ProfileStore: if not profile.get("sample_cycle_id"): profile["sample_cycle_id"] = cycle_id profile["avg_duration"] = cycle["duration"] + self.heal_profile_sample(profile_name) + + # The profile this cycle left must not keep it as its sample: with no other + # cycle it had no envelope, so the moved run stayed its whole matching template + # (the rebuild below re-points only a profile with cycles left to build from). + if old_profile and old_profile != profile_name: + self.heal_profile_sample(old_profile) # Rebuild envelopes for affected profiles if old_profile and old_profile != profile_name: await self.async_rebuild_envelope(old_profile) # Old profile lost a cycle if profile_name: await self.async_rebuild_envelope(profile_name) # New profile gained a cycle - # Apply retention after labeling, in case profile now exceeds cap + # Retention after labelling: only the debug-trace rule is left (item 463). await self.async_enforce_retention() await self.async_save() @@ -7988,8 +9454,9 @@ class ProfileStore: rebuild and save **once** instead of per cycle - the same batching the real-cycle path uses, and the difference between one store write and one per imported cycle. - Synchronous and self-contained: it validates the target, moves the label, clears a - stale ``sample_cycle_id`` on the profile the cycle is leaving, and drops that + Synchronous and self-contained: it validates the target, moves the label, heals a + stale ``sample_cycle_id`` on the profile the cycle is leaving + (:meth:`heal_profile_sample`: re-pointed at its own cycle, else cleared), and drops that profile outright when nothing is left in it (mirrors `_delete_non_real_cycle`; without it a sampleless profile would later be re-populated by sample repair stealing an unrelated real cycle). Returns the names whose envelope the caller @@ -7998,13 +9465,11 @@ class ProfileStore: if profile_name and profile_name not in self._data.get("profiles", {}): raise ValueError(f"Profile '{profile_name}' not found. Create it first.") old_profile = ref.get("profile_name") - ref_id = ref.get("id") ref["profile_name"] = profile_name if profile_name else None touched: set[str] = set() if old_profile and old_profile != profile_name: - op = self._data.get("profiles", {}).get(old_profile) - if op is not None and op.get("sample_cycle_id") == ref_id: - op["sample_cycle_id"] = None + # Re-point (or clear) a sample that was this cycle, as the real path does. + self.heal_profile_sample(old_profile) old_has_cycles = any( c.get("profile_name") == old_profile for c in self.iter_stored_cycles() @@ -8052,13 +9517,21 @@ class ProfileStore: ) async def auto_label_cycles( - self, confidence_threshold: float = 0.75, overwrite: bool = False + self, + confidence_threshold: float = 0.75, + overwrite: bool = False, + *, + progress: Callable[[int, int], None] | None = None, + should_cancel: Callable[[], bool] | None = None, ) -> dict[str, int]: """Auto-label cycles retroactively using profile matching. Args: confidence_threshold: Min confidence to apply a label. overwrite: If True, re-evaluates already labeled cycles. + progress: ``(done, total)`` before each cycle (the registry task). + should_cancel: polled before each cycle; True stops the pass, keeping + the labels applied so far. Returns stats: {labeled: int, relabeled: int, skipped: int, total: int} @@ -8099,7 +9572,12 @@ class ProfileStore: # store write per imported cycle. touched: set[str] = set() - for cycle, is_backfill in candidates: + for done, (cycle, is_backfill) in enumerate(candidates): + if should_cancel is not None and should_cancel(): + stats["cancelled"] = 1 + break + if progress is not None: + progress(done, len(candidates)) # Never overwrite a user's manual label, even with overwrite=True: a manual # correction is exactly the ground truth this pass should defer to (real and # non-real alike - the non-real manual relabel now stamps this too). @@ -8116,8 +9594,8 @@ class ProfileStore: # Try to match result = await self.async_match_profile(power_data, cycle["duration"]) - # Honor the ambiguity safeguard: never auto-label a close/ambiguous match. - if result.best_profile and result.confidence >= confidence_threshold and not result.is_ambiguous: + # The same verdict the cycle-end labeller applies (item 387c). + if label_verdict(result, confidence_threshold)[0]: current_label = cycle.get("profile_name") # Sanitize: strip heavy current/sample arrays before persisting. ranking_top5 = [ @@ -8154,8 +9632,17 @@ class ProfileStore: self._relabel_non_real_cycle(target, match.best_profile) ) else: + leaving = target.get("profile_name") target["profile_name"] = match.best_profile - target["match_confidence"] = float(match.confidence) + # Both envelopes changed: the gaining one has a new member, + # the leaving one lost one (register item 493). Rebuilt once + # each at the end, like the backfill moves. + if match.best_profile: + touched.add(match.best_profile) + if leaving and leaving != match.best_profile: + self.heal_profile_sample(leaving) + touched.add(leaving) + target["match_confidence"] = float(match.label_confidence) target["label_source"] = source if ranking: target["match_ranking_top5"] = ranking @@ -8175,7 +9662,7 @@ class ProfileStore: cycle["id"], current_label, result.best_profile, - result.confidence, + result.label_confidence, ) else: _apply() @@ -8184,16 +9671,16 @@ class ProfileStore: "Auto-labeled cycle %s as '%s' (confidence: %.2f)", cycle["id"], result.best_profile, - result.confidence, + result.label_confidence, ) else: stats["skipped"] += 1 if stats["labeled"] > 0 or stats["relabeled"] > 0: - # A labelled backfill cycle exists only to shape its profile's envelope, so - # rebuild what changed before saving - otherwise the import accomplishes - # nothing until some unrelated later trigger. - for name in touched: + # A labelled cycle shapes its profile's envelope (a backfill one exists + # only for that), so rebuild what changed before saving - otherwise the + # pass changes nothing the matcher sees until the nightly maintenance. + for name in sorted(touched): await self.async_rebuild_envelope(name) await self.async_save() # Trigger smart processing after bulk labeling @@ -8213,13 +9700,19 @@ class ProfileStore: Runs the matcher once per cycle with profile_name set but no match_confidence, and persists the resulting confidence if the same profile is returned. Returns the number of cycles updated. Safe to - call repeatedly - already-backfilled cycles are skipped. + call repeatedly - already-backfilled cycles are skipped, and so is a + cycle the matcher already disagreed with: it runs on every setup, and + without the marker each such cycle (typically one the user relabelled by + hand) cost one full match per Home Assistant start, forever. """ cycles = self._data.get("past_cycles", []) or [] updated = 0 + tried = 0 for cycle in cycles: if cycle.get("match_confidence") is not None: continue + if cycle.get("match_confidence_backfill_tried"): + continue profile_name = cycle.get("profile_name") if not profile_name: continue @@ -8232,10 +9725,14 @@ class ProfileStore: ) except Exception: # pylint: disable=broad-exception-caught continue - if result.best_profile == profile_name and result.confidence > 0: - cycle["match_confidence"] = float(result.confidence) + # Member-aware, like every other writer of this field (item 387c). + if result.best_profile == profile_name and result.label_confidence > 0: + cycle["match_confidence"] = float(result.label_confidence) updated += 1 - if updated: + else: + cycle["match_confidence_backfill_tried"] = True + tried += 1 + if updated or tried: await self.async_save() self._logger.info("Backfilled match_confidence on %d cycles", updated) return updated @@ -8400,13 +9897,16 @@ class ProfileStore: include_real = "real_cycles" in cats include_ref = "reference_cycles" in cats + include_backfill = "backfill_cycles" in cats if include_real: out["past_cycles"] = _filter_cycles("past_cycles", "real_cycle_ids") if include_ref: out["reference_cycles"] = _filter_cycles("reference_cycles", "reference_cycle_ids") + if include_backfill: + out["backfill_cycles"] = _filter_cycles("backfill_cycles", "backfill_cycle_ids") # "profiles empty" rule: carry cached envelopes when profiles ship without cycles. - if "profiles" in cats and not include_real and not include_ref: + if "profiles" in cats and not include_real and not include_ref and not include_backfill: envs = data.get("envelopes") if isinstance(data.get("envelopes"), dict) else {} carried = {n: envs[n] for n in (selected_profiles or set()) if n in envs} if carried: @@ -8421,13 +9921,188 @@ class ProfileStore: out[key] = data[key] return out - async def async_import_data(self, payload: dict[str, Any]) -> dict[str, Any]: + # ── Pre-import restore point: "Undo last import" (register item 195) ───────── + # + # A replace import writes the store as it is (plus this device's options) to its + # own Store file first, so the user can put it back. One slot per device: the + # next replace import overwrites it and a successful undo consumes it. A plain + # additive merge loses nothing and leaves the slot alone. + + def _new_pre_import_store(self) -> Store[JSONDict]: + return Store( + self.hass, PRE_IMPORT_STORE_VERSION, + f"{STORAGE_KEY}.{self.entry_id}.{PRE_IMPORT_STORE_SUFFIX}", + ) + + @staticmethod + def _pre_import_counts(data: Any) -> dict[str, int]: + """Headline counts shown next to the restore point (never raises).""" + d = data if isinstance(data, dict) else {} + + def _n(key: str) -> int: + value = d.get(key) + return len(value) if isinstance(value, (list, dict)) else 0 + + return { + "profiles": _n("profiles"), + "real_cycles": _n("past_cycles"), + "reference_cycles": _n("reference_cycles"), + "backfill_cycles": _n("backfill_cycles"), + } + + async def async_save_pre_import_snapshot( + self, source: str, entry_options: JSONDict | None = None + ) -> bool: + """Write the store as it is now to the restore point, before a replace import. + + Returns whether a restore point now describes the pre-import state. Best + effort: a snapshot that cannot be written must not block the import the user + confirmed, so a failure is logged and reported as ``False`` and the previous + slot is dropped, because it no longer describes "before the last import". + + ``entry_options`` is this device's options at that moment, put back by the + undo; ``None`` (a caller without an entry) leaves the options alone on undo. + """ + # A save during shutdown is deferred to HA's final write, which would + # serialise the store as it is AFTER the import mutated it. + if getattr(self.hass, "state", None) is CoreState.stopping: + await self.async_discard_pre_import_snapshot() + return False + record: JSONDict = { + "created_at": dt_util.now().isoformat(), + "source": str(source or "import"), + "store_version": STORAGE_VERSION, + "counts": self._pre_import_counts(self._data), + "entry_options": dict(entry_options) if isinstance(entry_options, dict) else None, + # Aliases the live store: Store.async_save serialises before it returns + # and the caller mutates nothing until then (it holds the write lock). + "data": self._data, + } + try: + await self._pre_import_store.async_save(record) + except Exception as err: # noqa: BLE001 - never let the safety net block the import + self._logger.warning("Could not save the pre-import restore point: %s", err) + await self.async_discard_pre_import_snapshot() + return False + self._pre_import_meta = {k: record[k] for k in ("created_at", "source", "counts")} + self._pre_import_meta_loaded = True + self._logger.info("Saved a restore point before the import (%s)", record["source"]) + return True + + async def _async_load_pre_import_snapshot(self) -> JSONDict | None: + """The whole restore-point record, or ``None`` when there is none (never raises).""" + try: + raw = await self._pre_import_store.async_load() + except Exception as err: # noqa: BLE001 - a damaged slot must not break the panel + self._logger.debug("Pre-import restore point unreadable: %s", err) + return None + if not isinstance(raw, dict) or not isinstance(raw.get("data"), dict): + return None + return raw + + async def async_get_pre_import_snapshot(self) -> JSONDict | None: + """``{created_at, source, counts}`` of the restore point, or ``None``. + + Never returns the data itself. Memoised, so only the first status read after + setup touches the file. + """ + if not self._pre_import_meta_loaded: + raw = await self._async_load_pre_import_snapshot() + meta: JSONDict | None = None + if raw is not None: + counts = raw.get("counts") + meta = { + "created_at": raw.get("created_at"), + "source": raw.get("source"), + "counts": counts if isinstance(counts, dict) + else self._pre_import_counts(raw.get("data")), + } + self._pre_import_meta = meta + self._pre_import_meta_loaded = True + return dict(self._pre_import_meta) if self._pre_import_meta else None + + async def async_discard_pre_import_snapshot(self) -> None: + """Delete the restore point (never raises).""" + self._pre_import_meta = None + self._pre_import_meta_loaded = True + try: + await self._pre_import_store.async_remove() + except Exception as err: # noqa: BLE001 + self._logger.debug("Could not delete the pre-import restore point: %s", err) + # A fresh Store object: the old one may still hold the loaded record. + self._pre_import_store = self._new_pre_import_store() + + async def async_restore_pre_import_snapshot(self) -> JSONDict: + """Put the store back exactly as it was before the last replace import. + + Raises ``ValueError`` when there is no usable restore point. The snapshot is + consumed: deleted once the restored store is written, so an undo can never be + applied twice over newer data. Returns ``restored_from`` (when the snapshot + was taken), ``counts`` and ``entry_options`` (``None`` = leave them alone); + the caller writes the options, which live on the config entry. + """ + raw = await self._async_load_pre_import_snapshot() + if raw is None: + # The memo can outlive the file (deleted by hand): stop offering it. + self._pre_import_meta = None + self._pre_import_meta_loaded = True + raise ValueError("There is no import to undo") + try: + version = int(raw.get("store_version")) + except (TypeError, ValueError, OverflowError) as err: + raise ValueError("The restore point has no readable storage version") from err + if version > STORAGE_VERSION: + raise ValueError("The restore point was saved by a newer version of WashData") + data: JSONDict = dict(raw["data"]) + if version < STORAGE_VERSION: + # Taken before an upgrade: same forward migration the main store runs on + # load, which an import path never does by itself. + migrator = WashDataStore( + self.hass, STORAGE_VERSION, + f"{STORAGE_KEY}.{self.entry_id}.{PRE_IMPORT_STORE_SUFFIX}", + ) + data = await migrator._async_migrate_func(version, 1, data) # pylint: disable=protected-access + # The in-flight cycle lives in its own file since 0.5.8; never resurrect one. + data.pop("active_cycle", None) + data.pop("last_active_save", None) + self._data = data + self._cached_sample_segments = {} + self._cohesion_cache_generation += 1 + await self.async_save() + await self.async_flush_saves() + await self.async_discard_pre_import_snapshot() + self._logger.info("Restored the store from the %s restore point", raw.get("created_at")) + entry_options = raw.get("entry_options") + return { + "restored_from": raw.get("created_at"), + "counts": self._pre_import_counts(data), + "entry_options": dict(entry_options) if isinstance(entry_options, dict) else None, + } + + async def async_import_data( + self, + payload: dict[str, Any], + *, + entry_options: JSONDict | None = None, + source: str = "import_config", + ) -> dict[str, Any]: """Wholesale import: replace the entire store from a JSON payload. Migration/wrapper aware via ``unwrap_import_payload`` (HA diagnostics, integration ``store_export``, v1 flat, v2 nested). This is the destructive "replace everything" path used by the legacy service / raw-JSON fallback; selective merge lives in ``async_import_data_selective``. + + Every key the payload carries replaces the target's. A key the payload does + not carry is **kept** (register item 195): an older or thinner export has no + ``backfill_cycles``, ``matching_config``, ``ml_training_history`` or lifetime + counters, and assigning the payload wholesale set those to nothing, including + state earned on-device over weeks that no re-import can rebuild. Only + ``_WHOLESALE_IMPORT_PAYLOAD_OWNED_KEYS`` always come from the payload. + + The store as it was is saved as the device's restore point first (register + item 195); ``entry_options`` (this device's, so the undo can put them back) + and ``source`` only describe that snapshot and are never applied here. """ data_dict, meta = unwrap_import_payload(payload) self._logger.info( @@ -8440,23 +10115,57 @@ class ProfileStore: raise ValueError( "Import payload contains no profiles or cycles — aborting to prevent data loss" ) - self._data = data_dict + # After the guard (a refused import must not burn the restore point), before + # the first mutation. + restore_point_saved = await self.async_save_pre_import_snapshot(source, entry_options) + supplied = meta.get("supplied_keys") or frozenset() + kept = { + key: value + for key, value in self._data.items() + if key not in supplied and key not in _WHOLESALE_IMPORT_PAYLOAD_OWNED_KEYS + } + if kept: + self._logger.info( + "Import payload lacks %s; keeping this device's", ", ".join(sorted(kept)) + ) + self._data = {**data_dict, **kept} + # The payload's envelopes were built without any cycle list kept above, so + # rebuild the profiles those lists label. A list kept from the target also + # means a profile kept from it can lack a payload envelope. + cycle_lists = ("past_cycles", "reference_cycles", "backfill_cycles") + stale_from = cycle_lists if "profiles" in kept else [k for k in cycle_lists if k in kept] + merged_profiles = self._data.get("profiles") or {} + stale_profiles = sorted({ + str(c["profile_name"]) + for key in stale_from + for c in (self._data.get(key) or []) + if isinstance(c, dict) and c.get("profile_name") in merged_profiles + }) + # Another install's in-flight cycle is never this device's (exports older + # than the separate active-cycle file carried it in the main data). + self._data.pop("active_cycle", None) + self._data.pop("last_active_save", None) # An import bypasses _async_migrate_func, so the repair marker has to be # re-armed here for a payload old enough to carry banked tails. if _export_predates_banked_tail_repair(meta): self._data[BANKED_TAIL_REPAIR_KEY] = True self._cached_sample_segments = {} + # profile_groups may have been replaced under the same member names. + self._cohesion_cache_generation += 1 # 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() + for name in stale_profiles: + await self.async_rebuild_envelope(name) await self.async_save() return { "entry_data": meta.get("entry_data", {}), "entry_options": meta.get("entry_options", {}), + "restore_point_saved": restore_point_saved, } async def async_import_data_selective( @@ -8469,6 +10178,7 @@ class ProfileStore: cycle_destination: str = "reference", apply_settings: bool = True, local_device_type: str = "", + entry_options: JSONDict | None = None, ) -> dict[str, Any]: """Selectively import chosen categories/items, merging into existing data. @@ -8480,9 +10190,22 @@ class ProfileStore: ``conflict_resolutions`` maps a source profile name to ``keep_mine`` / ``overwrite`` / ``import_as_copy``. + ``backfill_cycles`` (history recovered by the #344 import) always land back in + ``backfill_cycles`` and are **additive in both modes**: a replace never wipes + them. They are rebuilt only by re-scanning raw history that HA's recorder has + usually purged by then, and the one restore point (register item 195) is + overwritten by the next replace, so a wipe would become permanent. Re-imports + dedupe on the same start/overlap rule as + the history-import apply, and the per-device backfill cap applies. + Reuses the community-adopt primitives (``_add_reference_cycle_nosave``, ``_add_cycle_data``) and rebuilds each cycle-receiving profile's envelope exactly once. Never routes through the wholesale replace path. + + A destructive import (``mode="replace"``, or a merge told to ``overwrite`` a + name clash) saves the device's restore point first (register item 195); + ``entry_options`` is this device's, kept for the undo and never applied here. + A plain additive merge loses nothing and leaves the restore point alone. """ data_dict, meta = unwrap_import_payload(payload) conflict_resolutions = conflict_resolutions or {} @@ -8509,6 +10232,7 @@ class ProfileStore: src_profiles = data_dict.get("profiles") if isinstance(data_dict.get("profiles"), dict) else {} src_past = [c for c in (data_dict.get("past_cycles") or []) if isinstance(c, dict)] src_refs = [c for c in (data_dict.get("reference_cycles") or []) if isinstance(c, dict)] + src_backfill = [c for c in (data_dict.get("backfill_cycles") or []) if isinstance(c, dict)] prof_filter = selection.get("profiles") if isinstance(selection.get("profiles"), list) else None real_id_filter = ( @@ -8535,6 +10259,12 @@ class ProfileStore: # The cycles that will actually be imported once the per-item id selection is applied. selected_refs = _apply_id_filter(src_refs, ref_id_filter) selected_past = _apply_id_filter(src_past, real_id_filter) + backfill_id_filter = ( + selection.get("backfill_cycle_ids") + if isinstance(selection.get("backfill_cycle_ids"), list) + else None + ) + selected_backfill = _apply_id_filter(src_backfill, backfill_id_filter) def _any_usable(cs: list[dict[str, Any]]) -> bool: # A cycle bound for reference storage only refills the destination if its trace is @@ -8572,6 +10302,17 @@ class ProfileStore: "Import payload contains no real cycles — aborting to prevent data loss" ) + # Restore point: after the wipe guards (a refused import must not burn it), + # before the first mutation below. + restore_point_saved = False + if mode == "replace" or ( + "profiles" in cats + and any(str(v) == "overwrite" for v in conflict_resolutions.values()) + ): + restore_point_saved = await self.async_save_pre_import_snapshot( + f"import_selective_{mode}", entry_options + ) + local_profiles = self._data.setdefault("profiles", {}) name_remap: dict[str, str] = {} touched: set[str] = set() @@ -8777,6 +10518,17 @@ class ProfileStore: if device_type_match else None ), + # The user's own length correction (register item + # 384). The envelope build prefers it over the trace + # span and the banked-tail repair exempts a cycle that + # carries it; dropped here, the repair could cut a + # cycle the user had made longer, and the live floor + # lost its vouched length. Same device gate as above. + "manual_duration": ( + c.get("manual_duration") + if device_type_match + else None + ), }, id_pool=ref_id_pool, ) @@ -8811,6 +10563,72 @@ class ProfileStore: malformed_skipped += 1 self._logger.debug("selective import: skipped a malformed cycle", exc_info=True) + # ── Step 3b: backfill cycles (this machine's recovered history) ──────── + # Additive in both modes (see the docstring), deduped against every stored + # cycle like the history-import apply, so a round trip into the same device + # adds nothing. Category-gated on a device-type mismatch above. + backfill_imported = 0 + if "backfill_cycles" in cats and selected_backfill: + # Local import: history_import pulls in the detector, which the store does + # not otherwise need at load time. + from . import history_import # pylint: disable=import-outside-toplevel + from .const import ( # pylint: disable=import-outside-toplevel + HISTORY_IMPORT_MAX_TOTAL_CYCLES, + HISTORY_IMPORT_SOURCE, + ) + + backfill_target = self.get_backfill_cycles() + stored = self.iter_stored_cycles() + seen_keys = history_import.existing_dedup_keys(stored) + intervals = history_import.stored_intervals(stored) + backfill_id_pool: set[Any] = { + c.get("id") for c in backfill_target if isinstance(c, dict) + } + room = max(0, HISTORY_IMPORT_MAX_TOTAL_CYCLES - len(backfill_target)) + for c in selected_backfill: + try: + start, duration = c.get("start_time"), c.get("duration") + if start is None or duration is None: + malformed_skipped += 1 # _add_cycle_data needs both + continue + key = history_import.dedup_key(start, duration) + if (key is not None and key in seen_keys) or history_import.overlaps_stored( + start, duration, intervals + ): + continue # already on record (live, imported or backfilled) + if backfill_imported >= room: + malformed_skipped += 1 # over the per-device backfill cap + continue + orig = str(c.get("profile_name") or "") + target = name_remap.get(orig, orig) + if target: + _ensure_profile(target, duration) + cyc = dict(c) + cyc["profile_name"] = target or None + meta_c = dict(cyc.get("meta") or {}) + meta_c.setdefault("source", HISTORY_IMPORT_SOURCE) + orig_id = str(c.get("id") or "") + if orig_id: + meta_c["imported_from"] = orig_id + cyc["meta"] = meta_c + review = cyc.get("ml_review") + if isinstance(review, dict) and "golden" in review: + # Never golden: nothing verified an auto-detected segment. + cyc["ml_review"] = {k: v for k, v in review.items() if k != "golden"} + cyc.pop("id", None) # _add_cycle_data assigns a collision-safe id + self._add_cycle_data(cyc, target=backfill_target, id_pool=backfill_id_pool) + if key is not None: + seen_keys.add(key) + intervals = sorted([*intervals, *history_import.stored_intervals([cyc])]) + if target: + touched.add(target) + backfill_imported += 1 + except Exception: # noqa: BLE001 - skip a malformed record, never abort the import + malformed_skipped += 1 + self._logger.debug( + "selective import: skipped a malformed backfill cycle", exc_info=True + ) + # ── Step 4: leaf categories (additive merge / replace) ────────────────── for cat_id in cats: if cat_id in _STRUCTURAL_CATEGORIES: @@ -8851,6 +10669,20 @@ class ProfileStore: local_envs[new_name] = src_envs[orig] # keep it matchable without raw traces for p in sorted(touched): await self.async_rebuild_envelope(p) + # A profile definition carries the SOURCE store's sample id, and imported + # cycles get fresh ids. Dangling, it made `cleanup_orphaned_profiles` delete a + # definition-only import at the next maintenance or label edit; on a round + # trip it can name a local cycle of another programme. Re-point it at one of + # the profile's own cycles, else clear it (the carried envelope still matches). + for new_name in sorted(set(name_remap.values())): + prof = local_profiles.get(new_name) + if not isinstance(prof, dict): + continue + sid = prof.get("sample_cycle_id") + if sid and self.find_stored_cycle(sid)[0] is None: + prof["sample_cycle_id"] = self._own_sample_cycle_id(new_name) + else: + self.heal_profile_sample(new_name) self._cached_sample_segments = {} # Re-apply the odometer floor: an import can add or replace past_cycles @@ -8869,11 +10701,13 @@ class ProfileStore: "profiles_imported": created_profiles, "real_cycles_imported": real_imported, "reference_cycles_imported": ref_imported, + "backfill_cycles_imported": backfill_imported, "conflicts_resolved": conflicts_resolved, "skipped_cycles": malformed_skipped, "categories_applied": sorted(cats), "device_type_match": device_type_match, "settings": settings_out, + "restore_point_saved": restore_point_saved, } async def delete_cycle(self, cycle_id: str) -> bool: @@ -9102,9 +10936,7 @@ class ProfileStore: # Invalidate cached sample segments for this cycle so future lookups # are recomputed from the trimmed data - stale_keys = [k for k in self._cached_sample_segments if k[0] == cycle_id] - for k in stale_keys: - del self._cached_sample_segments[k] + self._drop_cached_sample_segments(cycle_id) # Rebuild envelope for the associated profile profile_name = cycle.get("profile_name") @@ -9262,7 +11094,7 @@ class ProfileStore: if seg_e <= seg_s: return [] materialized_segs.append((seg_s, seg_e, seg_p)) - except (TypeError, ValueError, KeyError, IndexError): + except (TypeError, ValueError, KeyError, IndexError, OverflowError): return [] # A split into fewer than two pieces is not a real split; guard here so # an empty or single-element segments list can't pop the source cycle and @@ -9326,15 +11158,26 @@ class ProfileStore: new_cycles_objs = [c for c in cycles if c["id"] in new_ids] for c in new_cycles_objs: + # Only a segment that kept the original label: a piece the user labelled + # as another programme is not this profile's run. + if c.get("profile_name") != original_profile: + continue d = c.get("duration", 0) if d > longest_dur: longest_dur = d best_replacement_id = c["id"] - if best_replacement_id and original_profile: - p_data = self._data["profiles"].get(original_profile) - if p_data and p_data.get("sample_cycle_id") == original_sample_id: + # Every profile that sampled the split (now deleted) cycle: the original one + # moves to its longest own segment; any other (or the original, when no + # segment kept its label) to one of its own cycles, else None. A pointer left + # at the deleted id made `cleanup_orphaned_profiles` delete the profile. + for p_name, p_data in self.get_profiles().items(): + if not isinstance(p_data, dict) or p_data.get("sample_cycle_id") != original_sample_id: + continue + if original_profile and p_name == original_profile and best_replacement_id: p_data["sample_cycle_id"] = best_replacement_id + else: + p_data["sample_cycle_id"] = self._own_sample_cycle_id(p_name) # Rebuild envelopes ONLY for the profiles whose dataset actually changed: # the original profile (it lost the parent cycle) plus any profile a labeled @@ -9536,6 +11379,12 @@ class ProfileStore: self._data["past_cycles"] = [ c for c in cycles if c.get("id") not in ids_to_remove ] + # The merged record carries `target_profile`'s label: a profile that sampled + # one of the parts under another label re-points at its own cycle (after the + # removal above, so a consumed part cannot be picked) or clears. + for p_name, p_data in self.get_profiles().items(): + if isinstance(p_data, dict) and p_data.get("sample_cycle_id") == new_id: + self.heal_profile_sample(p_name) # Update signature try: diff --git a/custom_components/ha_washdata/progress.py b/custom_components/ha_washdata/progress.py index 1ffb5840..ba9fb95e 100644 --- a/custom_components/ha_washdata/progress.py +++ b/custom_components/ha_washdata/progress.py @@ -18,7 +18,7 @@ Single source of truth for the cycle-progress math. Both the live integration (``manager.WashDataManager`` - thin wrappers over these functions) and the -Playground's headless simulation (``playground.SimRunner``) call the SAME +Playground's headless replay (``playground.py``) call the SAME functions here, so the panel's what-if replay is byte-for-byte what the running integration computes. Nothing here touches Home Assistant; every function is pure given a ``ProfileStore`` (read-only), the entry options mapping, and a @@ -34,6 +34,7 @@ import logging import math from dataclasses import dataclass from datetime import datetime +from operator import le as _le from typing import Any, cast import numpy as np @@ -41,19 +42,23 @@ import numpy as np from .const import ( CYCLE_OVERRUN_ANOMALY_RATIO, DEVICE_SMOOTHING_THRESHOLDS, - ML_PROGRESS_BLEND_WEIGHT, STATE_ENDING, STATE_PAUSED, STATE_RUNNING, ) -from .profile_store import decompress_power_data +from .profile_store import _envelope_y, decompress_power_data from .time_utils import power_data_to_offsets _LOGGER = logging.getLogger(__name__) -# Minimum progress before an energy projection is trusted (mirrors the manager -# class constant of the same purpose). -PROJECTION_MIN_PROGRESS = 3.0 +# Minimum progress before an energy projection is shown. 10, not 3 (audit +# PROGRESS-04): at 3% both divisors were off by 77-101% MAPE over 670 LOO folds +# (devtools/energy_projection_eval.py), at 10% the energy-share divisor is 50%. +PROJECTION_MIN_PROGRESS = 10.0 + +# Floor on the matched profile's cumulative-energy share used as the projection +# divisor: below it the curve's start is noise and the division explodes. +PROJECTION_MIN_ENERGY_FRACTION = 0.05 # The progress EMA weights below are per *estimate*, and were chosen against the # manager's 5 s estimate throttle. See :func:`_dt_scaled_alpha`. @@ -62,6 +67,9 @@ SMOOTHING_NOMINAL_DT_S = 5.0 # Cache type for profile_end_expectation: (profile_name, base_expectation_dict). EndExpCache = tuple[str, dict[str, float]] | None +# How many of a profile's most recent traces its end expectation is taken from. +_END_EXPECTATION_CYCLES = 20 + @dataclass class ProgressResult: @@ -93,14 +101,24 @@ def profile_end_expectation( else: from .ml.feature_extraction import profile_expectation + # The 20 most recent non-empty traces, oldest first - walked newest-first + # and stopped there, so a long history is not decompressed just to be + # thrown away (49-248 ms on the largest corpus profiles, on the event + # loop, once per cycle start). + cycles = store.get_past_cycles() or [] + if not isinstance(cycles, (list, tuple)): + cycles = list(cycles) points_list: list[list[tuple[float, float]]] = [] - for cycle in store.get_past_cycles(): + for cycle in reversed(cycles): if cycle.get("profile_name") != profile_name: continue pts = decompress_power_data(cycle) if pts: points_list.append(pts) - base = profile_expectation(points_list[-20:]) + if len(points_list) >= _END_EXPECTATION_CYCLES: + break + points_list.reverse() + base = profile_expectation(points_list) if base is None: return None, cache cache = (profile_name, dict(base)) @@ -113,61 +131,6 @@ def profile_end_expectation( EndExpFn = Any # Callable[[str, float], dict[str, float] | None] -def ml_progress_percent( - store: Any, - options: Any, - matched_duration: float, - trace: list[tuple[datetime, float]], - profile_name: str, - end_expectation_fn: EndExpFn, - logger: logging.Logger | None = None, -) -> float | None: - """ML completion-fraction estimate (0-100) for the running cycle, or None. - - Uses the on-device ``remaining_time`` regressor; gated on the ML opt-in and - inert until training promotes a regressor. ``end_expectation_fn(name, dur)`` - supplies the profile expectation (the manager passes its cached - ``_profile_end_expectation``; the Playground wraps :func:`profile_end_expectation`) - so history is only decompressed after the cheap gates pass. Never raises. - """ - logger = logger or _LOGGER - try: - from .ml.engine import ml_models_enabled, resolve_regressor - - if not ml_models_enabled(options): - return None - if ( - not profile_name - or profile_name in ("off", "detecting...", "restored...") - or profile_name not in store.get_profiles() - ): - return None - predict_fn, _src = resolve_regressor("remaining_time", store) - if predict_fn is None: - return None - if not trace or len(trace) < 4: - return None - expectation = end_expectation_fn( - profile_name, float(matched_duration or 0.0) - ) - if expectation is None: - return None - t0 = trace[0][0] - pts = [(float((t - t0).total_seconds()), float(p)) for t, p in trace] - from .ml.feature_extraction import progress_features - - feat = progress_features(pts, expectation) - if feat is None: - return None - frac = float(predict_fn(feat)) - if not math.isfinite(frac): - return None - return float(min(max(frac, 0.0), 0.99)) * 100.0 - except Exception as err: # noqa: BLE001 - ML must never break estimates - logger.debug("ML progress estimate skipped: %s", err) - return None - - def ml_energy_total( store: Any, options: Any, @@ -178,8 +141,12 @@ def ml_energy_total( logger: logging.Logger | None = None, ) -> float | None: """Predicted total cycle energy (Wh) from the on-device ``total_energy`` - regressor, or None. ``end_expectation_fn`` as in :func:`ml_progress_percent`. - Never raises. + regressor, or None. Never raises. + + ``end_expectation_fn(name, dur)`` supplies the profile expectation (the + manager passes its cached ``_profile_end_expectation``; the Playground wraps + :func:`profile_end_expectation`) so history is only decompressed after the + cheap gates pass. """ logger = logger or _LOGGER try: @@ -225,37 +192,13 @@ def ml_energy_total( return None -def estimate_phase_progress( - store: Any, - current_power_data: list[tuple[datetime, float]] | list[tuple[str, float]], - current_duration: float, - profile_name: str, - logger: logging.Logger | None = None, - quiet_threshold_w: float = 0.0, -) -> tuple[float, float] | None: - """Estimate cycle progress by analyzing which phase we're in. +_PHASE_ENVELOPE_CACHE: dict[tuple[Any, int, Any], tuple[Any, tuple[dict[str, Any], Any, float]]] = {} - Uses cached statistical envelope built from ALL cycles labeled with this - profile, normalized by TIME to account for different sampling rates. Returns - ``(progress_pct, variance_watts)`` or ``None`` if estimation fails. - ``quiet_threshold_w`` is the detector's own off-noise floor - (``CycleDetectorConfig.stop_threshold_w``, itself derived from the configured - minimum power). A window that never rises above it is *not* the appliance - doing something, so it carries no phase information and the scan declines - rather than guessing (#386); a dead-flat window declines for the same reason - at any power level. The default 0.0 leaves only the flatness rule for callers - that do not know the floor. - """ - logger = logger or _LOGGER - # Get cached envelope (fast - already computed and stored) - envelope = store.get_envelope(profile_name) - - if envelope is None: - logger.debug("No envelope cached for profile %s", profile_name) - return None - - # Convert cached lists back to numpy arrays +def _parse_phase_envelope( + envelope: dict[str, Any], profile_name: str, logger: logging.Logger +) -> tuple[dict[str, Any], Any, float] | None: + """``(arrays, time_grid, target_duration)`` of a stored envelope, read-only.""" try: env_min = envelope.get("min", []) env_max = envelope.get("max", []) @@ -287,9 +230,124 @@ def estimate_phase_progress( envelope.get("time_grid", []), dtype=float ) target_duration = float(envelope.get("target_duration", 0.0) or 0.0) - except (KeyError, ValueError, TypeError, IndexError) as e: + except (KeyError, ValueError, TypeError, IndexError, OverflowError) as e: logger.warning("Invalid envelope format for %s: %s", profile_name, e) return None + for _arr in (*envelope_arrays.values(), time_grid): + _arr.setflags(write=False) + return envelope_arrays, time_grid, target_duration + + +def _window_values( + power_data: Any, window_s: float +) -> np.ndarray[Any, np.dtype[np.float64]] | None: + """Powers of the trailing ``window_s`` of a ``(datetime, power)`` trace. + + Exactly what ``power_data_to_offsets`` + the ``offsets >= last - window`` + mask in :func:`estimate_phase_progress` select (same anchor, same 0.1 s + rounding, same skipped rows), without converting the whole trace. Only the + ``datetime`` format the detector hands out takes this path, and only when + its timestamps never go backwards: then everything before the first row + that falls out of the window is out of it too. Anything else returns None + and the caller converts the whole trace as before. + """ + try: + if not isinstance(power_data, (list, tuple)) or not power_data: + return None + first = power_data[0] + if not ( + isinstance(first, (list, tuple)) + and len(first) >= 2 + and isinstance(first[0], datetime) + ): + return None + stamps = [row[0] for row in power_data] + if not all(map(_le, stamps, stamps[1:])): + return None + + def _row(row: Any) -> tuple[datetime, float] | None: + # The same rows `power_data_to_offsets` keeps (and the same order of + # checks, so the same one anchors the offsets). + try: + ts = row[0] + if not isinstance(ts, datetime): + return None + return ts, float(row[1]) + except (TypeError, ValueError, AttributeError, IndexError, OverflowError): + return None + + anchor: float | None = None + for row in power_data: + kept = _row(row) + if kept is not None: + anchor = kept[0].timestamp() + break + if anchor is None: + return np.array([], dtype=float) + window_start: float | None = None + tail: list[float] = [] + for row in reversed(power_data): + kept = _row(row) + if kept is None: + continue + offset = round(kept[0].timestamp() - anchor, 1) + if window_start is None: + window_start = max(0, offset - window_s) + if offset < window_start: + break + tail.append(kept[1]) + tail.reverse() + return np.array(tail) + except Exception: # pylint: disable=broad-exception-caught + return None + + +def estimate_phase_progress( + store: Any, + current_power_data: list[tuple[datetime, float]] | list[tuple[str, float]], + current_duration: float, + profile_name: str, + logger: logging.Logger | None = None, + quiet_threshold_w: float = 0.0, +) -> tuple[float, float] | None: + """Estimate cycle progress by analyzing which phase we're in. + + Uses cached statistical envelope built from ALL cycles labeled with this + profile, normalized by TIME to account for different sampling rates. Returns + ``(progress_pct, variance_watts)`` or ``None`` if estimation fails. + + ``quiet_threshold_w`` is the detector's own off-noise floor + (``CycleDetectorConfig.stop_threshold_w``, itself derived from the configured + minimum power). A window that never rises above it is *not* the appliance + doing something, so it carries no phase information and the scan declines + rather than guessing (#386); a dead-flat window declines for the same reason + at any power level. The default 0.0 leaves only the flatness rule for callers + that do not know the floor. + """ + logger = logger or _LOGGER + # Get cached envelope (fast - already computed and stored) + envelope = store.get_envelope(profile_name) + + if envelope is None: + logger.debug("No envelope cached for profile %s", profile_name) + return None + + # Parse the stored lists into arrays once per envelope build, not on every + # 5 s estimate (audit PERF-06: ~20% of a 17 ms call, on the event loop). + # Keyed on the envelope object and its `updated` stamp; arrays are read-only. + _key = (profile_name, id(envelope), envelope.get("updated")) + _hit = _PHASE_ENVELOPE_CACHE.get(_key) + if _hit is not None and _hit[0] is envelope: + _parsed = _hit[1] + else: + _parsed = _parse_phase_envelope(envelope, profile_name, logger) + if _parsed is None: + return None + if len(_PHASE_ENVELOPE_CACHE) > 32: + _PHASE_ENVELOPE_CACHE.clear() + # The envelope itself is held, so its id cannot be recycled while cached. + _PHASE_ENVELOPE_CACHE[_key] = (envelope, _parsed) + envelope_arrays, time_grid, target_duration = _parsed if len(time_grid) == 0 or target_duration <= 0: if target_duration > 0 and len(envelope_arrays["avg"]) > 0: @@ -305,23 +363,33 @@ def estimate_phase_progress( logger.debug("Envelope missing time grid/duration, cannot estimate phase") return None - # Extract power offsets from current cycle (any format -> [offset, power]) - current_offsets_list = power_data_to_offsets( - cast(list[list[Any] | tuple[Any, ...]], current_power_data) - ) - current_offsets = np.array([o for o, _ in current_offsets_list]) - current_values = np.array([p for _, p in current_offsets_list]) - if current_offsets.size == 0: - logger.debug("No valid current power offsets, cannot estimate phase") - return None - # Use sliding window on TIME, not sample count window_duration = min(60.0, target_duration * 0.25) - current_time = current_offsets[-1] - window_start_time = max(0, current_time - window_duration) + # Only the last `window_duration` seconds of the trace are read, so convert + # only those (audit PROGRESS-17: the whole trace was converted on every 5 s + # estimate, on the event loop). `_window_values` returns exactly what the + # full conversion + time mask selects, or None to take that full path. + _windowed = _window_values(current_power_data, window_duration) + if _windowed is None: + # Extract power offsets from current cycle (any format -> [offset, power]) + current_offsets_list = power_data_to_offsets( + cast(list[list[Any] | tuple[Any, ...]], current_power_data) + ) + current_offsets = np.array([o for o, _ in current_offsets_list]) + current_values = np.array([p for _, p in current_offsets_list]) + if current_offsets.size == 0: + logger.debug("No valid current power offsets, cannot estimate phase") + return None + current_time = current_offsets[-1] + window_start_time = max(0, current_time - window_duration) - window_mask = current_offsets >= window_start_time - current_window_values = current_values[window_mask] + window_mask = current_offsets >= window_start_time + current_window_values = current_values[window_mask] + elif _windowed.size == 0: + logger.debug("No valid current power offsets, cannot estimate phase") + return None + else: + current_window_values = _windowed if len(current_window_values) < 3: logger.debug("Insufficient data in current window for phase estimation") @@ -605,7 +673,7 @@ def _dt_scaled_alpha(alpha: float, dt_s: float | None) -> float: alpha_dt = 1 - (1 - alpha) ** (dt / SMOOTHING_NOMINAL_DT_S) ``dt_s`` of ``None`` (or <= 0) keeps the nominal weight, so every caller that - does not track its own cadence - and the golden snapshot - is unchanged. + does not track its own cadence (and the Playground replay) is unchanged. """ if dt_s is None or not math.isfinite(dt_s) or dt_s <= 0.0: return alpha @@ -615,23 +683,36 @@ def _dt_scaled_alpha(alpha: float, dt_s: float | None) -> float: return 1.0 - (1.0 - alpha) ** steps +def ema_seed( + prev_smoothed: float, prev_program: str | None, program: str | None +) -> float: + """The EMA state an estimate for ``program`` continues from (audit PROGRESS-09). + + A programme switch or a pin re-seeds to 0.0 (a cold start, i.e. the raw + estimate for the new programme). Carrying the old percent onto the new + duration read 62-67 min against a 90 min truth and took up to 12 min to + settle; an honest backwards jump at a switch is the correct information. + """ + if prev_program is not None and program != prev_program: + return 0.0 + return prev_smoothed + + def _compute_progress_base( device_type: str, matched_duration: float, duration_so_far: float, prev_smoothed: float, phase_result: tuple[float, float] | None, - ml_pct: float | None, logger: logging.Logger | None = None, dt_seconds: float | None = None, ) -> ProgressResult | None: - """The blend + EMA + monotonicity + back-calculation body of the estimate loop. + """The EMA + monotonicity + back-calculation body of the estimate loop. Pure arithmetic: the caller supplies ``phase_result`` (from - :func:`estimate_phase_progress`, or ``None`` to force the linear fallback) and - ``ml_pct`` (from :func:`ml_progress_percent`, or ``None``); both the live - manager and the Playground compute those via the same functions, so this is - the single implementation of the smoothing/back-calc. Returns ``None`` when no + :func:`estimate_phase_progress`, or ``None`` to force the linear fallback); + the live manager and the Playground compute it via the same function, so this + is the single implementation of the smoothing/back-calc. Returns ``None`` when no profile duration is known (caller clears the estimate). Behavior-identical to the matched-duration branch of ``manager._update_remaining_only``. """ @@ -643,10 +724,6 @@ def _compute_progress_base( if phase_result is not None: phase_progress, phase_variance = phase_result - if ml_pct is not None: - w = ML_PROGRESS_BLEND_WEIGHT - phase_progress = (1.0 - w) * phase_progress + w * ml_pct - if prev_smoothed == 0.0: smoothed = phase_progress else: @@ -665,8 +742,12 @@ def _compute_progress_base( smoothing_threshold = DEVICE_SMOOTHING_THRESHOLDS.get(device_type, 5.0) if phase_progress < current_smoothed - smoothing_threshold: # Backward step: damping here exists to resist regression, not to - # track, so it stays per-estimate (unscaled) on purpose. - smoothed = (current_smoothed * 0.95) + (phase_progress * 0.05) + # track. It is still a time constant, not a step count (audit + # PROGRESS-13): per estimate, the Playground's 30 s steps (and a + # plug reporting every 30 s live) gave way to a real drop 6x + # slower than a 5 s plug. dt=None keeps the plain 95/5 step. + beta = _dt_scaled_alpha(0.05, dt_seconds) + smoothed = (current_smoothed * (1.0 - beta)) + (phase_progress * beta) logger.debug( "Progress drop detected (%.1f%% < %.1f%% - %.1f%%), " "applying heavy damping for %s", @@ -680,10 +761,22 @@ def _compute_progress_base( smoothed = (prev_smoothed * (1.0 - alpha)) + (phase_progress * alpha) smoothed = min(99.0, smoothed) + if duration_so_far >= matched_duration and prev_smoothed > smoothed: + # Past the expected end the cycle is finishing, not going backwards. + # In an overrun tail the phase scan declines on quiet windows, so the + # branches alternate: the linear one reaches 100%, then the next phase + # estimate's backward step (and its 99% cap) pulled the shown progress + # back to ~97% (audit PROGRESS-13 follow-up). Hold what was shown. + smoothed = prev_smoothed progress = smoothed remaining = matched_duration * (1.0 - (progress / 100.0)) remaining = max(0.0, remaining) + if duration_so_far >= matched_duration: + # Overrun: the 99% cap would pin remaining at 1% of the profile for as + # long as the run lasts, re-arming the live chronometer "now + 36 s" + # every tick (audit PROGRESS-06). The linear branch already says 0. + remaining = 0.0 total = duration_so_far + remaining logger.debug( @@ -699,18 +792,16 @@ def _compute_progress_base( remaining = max(matched_dur - duration_so_far, 0.0) progress = (duration_so_far / matched_dur) * 100.0 - if ml_pct is not None: - w = ML_PROGRESS_BLEND_WEIGHT - progress = (1.0 - w) * progress + w * ml_pct - remaining = max(matched_dur * (1.0 - progress / 100.0), 0.0) - if prev_smoothed > 0: lin_alpha = _dt_scaled_alpha(0.1, dt_seconds) smoothed = (prev_smoothed * (1.0 - lin_alpha)) + (progress * lin_alpha) else: smoothed = progress - progress = max(0.0, min(smoothed, 100.0)) + # Clamped in the carried state too: unclamped, a run past a short mis-match + # carried 146% into the correct longer programme (audit PROGRESS-09). + smoothed = max(0.0, min(smoothed, 100.0)) + progress = smoothed remaining = max(matched_dur * (1.0 - progress / 100.0), 0.0) total = duration_so_far + remaining logger.debug( @@ -727,83 +818,74 @@ def compute_progress( duration_so_far: float, prev_smoothed: float, phase_result: tuple[float, float] | None, - ml_pct: float | None, logger: logging.Logger | None = None, - phase_remaining_s: float | None = None, dt_seconds: float | None = None, ) -> ProgressResult | None: - """Progress/remaining estimate, optionally blended with a phase-resolved ETA. - - When ``phase_remaining_s`` is provided (opt-in phase matching for a supported - device type), the phase-budget remaining is converted to a completion PERCENT - and blended into the phase-progress signal **before** delegating to - :func:`_compute_progress_base` - so the blend rides the proven, golden-locked - EMA + monotonicity + back-calculation guards (design §8, "one smoothing - implementation"), rather than re-deriving a raw, unsmoothed progress. The - blend leans on the phase budget early (low base progress) and on the proven - phase estimate late:: - - phase_pct = duration_so_far / (duration_so_far + phase_remaining_s) * 100 - f = base_phase_progress / 100 - blended = (1 - f) * phase_pct + f * base_phase_progress - - Because this feeds the percent-domain smoothing, the displayed progress stays - monotone/smoothed (no tick-to-tick jitter or collapse-to-99%), and remaining - is re-derived by the base from ``matched_duration``. - - Behaviour is BYTE-IDENTICAL to before when ``phase_remaining_s is None`` (the - default) - the golden progress snapshot and every existing caller are - unaffected. This is the single implementation of the blend; the manager and - the Playground SimRunner both go through it. - """ - blended = False - if phase_remaining_s is not None and matched_duration and matched_duration > 0: - try: - pr = float(phase_remaining_s) - except (TypeError, ValueError): - pr = float("nan") - if math.isfinite(pr) and pr >= 0.0: - denom = duration_so_far + pr - phase_pct = (duration_so_far / denom * 100.0) if denom > 0 else 0.0 - phase_pct = max(0.0, min(100.0, phase_pct)) - if phase_result is not None: - base_pp, variance = phase_result - f = max(0.0, min(1.0, float(base_pp) / 100.0)) - phase_result = ((1.0 - f) * phase_pct + f * float(base_pp), variance) - else: - # No envelope phase-progress: blend the phase budget's implied - # percent with the linear (elapsed/matched) percent, still leaning - # on the phase budget early and the linear estimate late. - lin_pct = max(0.0, min(100.0, duration_so_far / matched_duration * 100.0)) - f = lin_pct / 100.0 - phase_result = ((1.0 - f) * phase_pct + f * lin_pct, 0.0) - blended = True - - base = _compute_progress_base( + """Progress/remaining estimate: the one entry point for the manager and the + Playground replay (the phase-resolved ETA blend that used to sit here was + removed, audit PROGRESS-01/02: it never ran in production, and revived it was + 10% worse at 25% on washers).""" + return _compute_progress_base( device_type, matched_duration, duration_so_far, prev_smoothed, - phase_result, ml_pct, logger, dt_seconds, - ) - if base is None or not blended: - return base - # Relabel the source for diagnostics; values already reflect the blend. - return ProgressResult( - base.progress, base.smoothed, base.remaining, base.total, - base.phase_progress, "phase_blend", + phase_result, logger, dt_seconds, ) +def phase_timeline_span( + ranges: list[dict[str, Any]], expected_duration: float | None +) -> float: + """Seconds the progress fraction maps onto: ``max(last range end, expected)``. + + Phase ranges are minutes into the programme, so a profile that marks only + Wash 0-30 / Rinse 30-60 on a 100 min programme reads Rinse at minute 45 and + no phase at minute 80 (audit PROGRESS-10). Stretching the ranges over the + whole cycle (the old scale, the last range end) named Wash at 45%. Ranges + that run past the expected duration keep their own end. 0.0 when unusable. + """ + span = max((float(r.get("end") or 0.0) for r in ranges), default=0.0) + try: + expected = float(expected_duration or 0.0) + except (TypeError, ValueError, OverflowError): + expected = 0.0 + if math.isfinite(expected) and expected > span: + span = expected + return span if math.isfinite(span) and span > 0.0 else 0.0 + + +def phase_at( + ranges: list[dict[str, Any]], position_s: float, span_s: float +) -> str | None: + """The range containing ``position_s``: ``[start, end)``, the timeline's own + end included. None in a gap or past every range - no nearest-phase guess. + The panel's Status timeline applies the same rule.""" + at_end = position_s >= span_s + for r in sorted(ranges, key=lambda x: float(x.get("start") or 0.0)): + start = float(r.get("start") or 0.0) + end = float(r.get("end") or 0.0) + if end <= start: + continue + if start <= position_s < end or (at_end and end >= span_s and start <= position_s): + name = str(r.get("name") or "").strip() + return name or None + return None + + def current_phase( store: Any, state: str, current_program: str | None, cycle_progress: float, + expected_duration: float | None = None, ) -> str | None: - """Live phase from the profile's configured ranges + ML-blended progress. + """Live phase from the profile's configured ranges + the smoothed progress. Indexed by the smoothed progress fraction rather than raw elapsed seconds, so - overrun/underrun cycles still name the phase correctly. Returns ``None`` when - not running, no profile is matched, or the profile has no configured phase - ranges. Never raises. + overrun/underrun cycles still name the phase correctly; the fraction maps onto + :func:`phase_timeline_span` (the matched profile's ``expected_duration`` unless + the ranges run longer), so ranges are read at their real minutes. Returns + ``None`` when not running, no profile is matched, the profile has no phase + ranges, or no range covers this point (audit PROGRESS-11: no guessed phase). + Never raises. """ try: if state not in (STATE_RUNNING, STATE_PAUSED, STATE_ENDING): @@ -814,15 +896,63 @@ def current_phase( ranges = store.get_profile_phase_ranges(profile) if not ranges: return None - nominal = max((float(r.get("end") or 0.0) for r in ranges), default=0.0) - if nominal <= 0.0: + span = phase_timeline_span(ranges, expected_duration) + if span <= 0.0: return None frac = max(0.0, min(1.0, float(cycle_progress) / 100.0)) - return store.check_phase_match(profile, frac * nominal) + return phase_at(ranges, frac * span, span) except Exception: # noqa: BLE001 - phase readout must never break return None +_ENERGY_CURVES: dict[tuple[str, int, Any], tuple[Any, tuple[np.ndarray, np.ndarray] | None]] = {} + + +def envelope_energy_fraction( + store: Any, program: str | None, progress_pct: float +) -> float | None: + """Share of the matched profile's energy used by ``progress_pct`` (audit PROGRESS-04). + + The cumulative integral of the envelope's ``avg`` curve, read at the same + fraction of its time grid. Energy does not accrue linearly in time - heaters + front-load it - so ``energy / time_fraction`` projected washers 1.89x too high + at 25%. None without a usable envelope (the caller falls back to that). + """ + if not program or store is None: + return None + try: + env = store.get_envelope(program) + except Exception: # noqa: BLE001 - a projection input, never fatal + return None + if not isinstance(env, dict): + return None + key = (program, id(env), env.get("updated")) + hit = _ENERGY_CURVES.get(key) + if hit is None or hit[0] is not env: + if len(_ENERGY_CURVES) > 64: + _ENERGY_CURVES.clear() + curve = None + try: + tg = np.asarray(env.get("time_grid") or [], dtype=float) + avg = _envelope_y(env.get("avg")) + if tg.size >= 2 and avg.size == tg.size and np.all(np.isfinite(avg)): + cum = np.concatenate( + ([0.0], np.cumsum(np.diff(tg) * (avg[1:] + avg[:-1]) / 2.0)) + ) + if cum[-1] > 0 and tg[-1] > tg[0]: + curve = (tg, cum / cum[-1]) + except (TypeError, ValueError, OverflowError): + curve = None + # The envelope itself is held, so its id cannot be recycled while cached. + _ENERGY_CURVES[key] = (env, curve) + curve = _ENERGY_CURVES[key][1] + if curve is None: + return None + tg, frac = curve + x = tg[0] + (tg[-1] - tg[0]) * min(max(float(progress_pct) / 100.0, 0.0), 1.0) + return max(float(np.interp(x, tg, frac)), PROJECTION_MIN_ENERGY_FRACTION) + + def projected_energy( store: Any, options: Any, @@ -839,8 +969,10 @@ def projected_energy( ) -> 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 + Prefers the on-device ``total_energy`` regressor; otherwise divides + ``energy_so_far`` by the matched profile's cumulative-energy share at this + progress (:func:`envelope_energy_fraction`), and by the time fraction only + when the profile has no usable envelope. 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 @@ -868,13 +1000,16 @@ def projected_energy( end_expectation_fn, logger, ) if projected_wh is None: - projected_wh = energy_so_far / (progress / 100.0) + fraction = envelope_energy_fraction(store, current_program, progress) + projected_wh = energy_so_far / ( + fraction if fraction is not None else progress / 100.0 + ) projected_wh = max(projected_wh, energy_so_far) # A valid price of 0 (free/zero tariff) must yield cost 0.0, not None; only an # absent or non-numeric price is "unknown". try: price_val = float(price) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): price_val = None if price_val is None: cost = None @@ -885,7 +1020,7 @@ def projected_energy( if cost_so_far_wh is not None: try: charged_wh = float(cost_so_far_wh) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): 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 diff --git a/custom_components/ha_washdata/recorder.py b/custom_components/ha_washdata/recorder.py index a5b20029..71fcc847 100644 --- a/custom_components/ha_washdata/recorder.py +++ b/custom_components/ha_washdata/recorder.py @@ -31,12 +31,17 @@ from .const import ( STORAGE_KEY, ) from .log_utils import DeviceLoggerAdapter +from .time_utils import utc_now _LOGGER = logging.getLogger(__name__) STORAGE_KEY_RECORDER = f"{STORAGE_KEY}.recorder" +# Seconds between buffer saves while recording (audit PLATFORM-18). +_SAVE_INTERVAL_S = 300.0 + + class RecorderStore(Store[dict[str, Any]]): """Store for recorder data with migration support.""" @@ -87,7 +92,7 @@ class CycleRecorder: def current_duration(self) -> float: """Return current recording duration in seconds.""" if self._start_time: - return (dt_util.now() - self._start_time).total_seconds() + return (utc_now() - self._start_time).total_seconds() return 0.0 async def async_load(self) -> None: @@ -188,7 +193,7 @@ class CycleRecorder: "last_run": self._last_run, } await self._store.async_save(data) - self._last_save = dt_util.now() + self._last_save = utc_now() async def start_recording(self) -> None: """Start a new recording.""" @@ -200,7 +205,7 @@ class CycleRecorder: # Previous recordings are kept until explicitly cleared or overwritten self._is_recording = True - self._start_time = dt_util.now() + self._start_time = utc_now() self._buffer = [] await self._async_save() @@ -209,14 +214,15 @@ class CycleRecorder: if not self._is_recording: return - now = dt_util.now() + now = utc_now() # Append to buffer self._buffer.append((now.isoformat(), float(power))) - # Periodic save every 60s to ensure data persistence - # Better safe than sorry: save if last save was > 1 minute ago - if self._last_save and (now - self._last_save).total_seconds() > 60: - self.hass.add_job(self._async_save) - elif not self._last_save: + # Periodic save every 5 min (audit PLATFORM-18): each save rewrites the + # whole buffer and the previous recording, ~200 MB to the SD card over a + # 4 h recording at 60 s. Stamped when SCHEDULED, not when the write + # finishes, so the readings that arrive meanwhile cannot each schedule one. + if not self._last_save or (now - self._last_save).total_seconds() > _SAVE_INTERVAL_S: + self._last_save = now self.hass.add_job(self._async_save) diff --git a/custom_components/ha_washdata/select.py b/custom_components/ha_washdata/select.py index b3adf65a..5f4be23d 100644 --- a/custom_components/ha_washdata/select.py +++ b/custom_components/ha_washdata/select.py @@ -49,6 +49,8 @@ async def async_setup_entry( class WashDataProgramSelect(SelectEntity): """Select entity to manually choose the running program.""" + _attr_should_poll = False # pushed by the manager's update signal (PERF-02) + _attr_has_entity_name = True _attr_translation_key = "program_select" diff --git a/custom_components/ha_washdata/sensor.py b/custom_components/ha_washdata/sensor.py index 1c3e9290..f655514b 100644 --- a/custom_components/ha_washdata/sensor.py +++ b/custom_components/ha_washdata/sensor.py @@ -21,6 +21,7 @@ from __future__ import annotations from asyncio import Task import hashlib import logging +import math from typing import Any from homeassistant.components.sensor import ( @@ -33,10 +34,14 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.const import EntityCategory, UnitOfEnergy from homeassistant.helpers import entity_registry -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util +from .time_utils import utc_now from .const import ( CONF_AUTO_LABEL_CONFIDENCE, CONF_DURATION_TOLERANCE, @@ -71,7 +76,6 @@ from .const import ( STATE_DELAY_WAIT, STATE_INTERRUPTED, STATE_FORCE_STOPPED, - STATE_RINSE, STATE_UNKNOWN, STATE_CLEAN, ) @@ -79,6 +83,12 @@ from .manager import WashDataManager _LOGGER = logging.getLogger(__name__) +# The per-profile count sensors' own signal (register item 456). They read only +# the profile summaries and envelopes, which change at a cycle end or a profile +# edit, so rewriting all of them on every live refresh was pure cost (audit +# PERF-01). Sent by WasherProfileSensorManager when the summaries change. +SIGNAL_WASHER_PROFILES_UPDATE = "ha_washdata_profiles_update_{}" + _STATIC_DIAGNOSTIC_SUFFIXES = { "debug_info", @@ -200,6 +210,7 @@ async def async_setup_entry( ) async_add_entities(entities) + manager.sensor_add_entities = async_add_entities # Reconcile diagnostics at startup so stale unavailable entries are auto-removed. cleanup_orphaned_diagnostic_entities(hass, manager, entry) @@ -210,10 +221,33 @@ async def async_setup_entry( entry.async_on_unload(profile_sensor_manager.unsubscribe) +@callback +def async_reconcile_device_type_sensors( + hass: HomeAssistant, manager: WashDataManager, entry: ConfigEntry +) -> None: + """Add or drop the pump-only sensor after an in-place device type change. + + Options apply through the update listener without re-running platform setup + (audit PLATFORM-15), so a washer turned pump never got PumpRunsTodaySensor + and a pump turned washer kept it until a restart. Removing the registry entry + also removes the live entity. + """ + if manager.device_type == DEVICE_TYPE_PUMP and manager.sensor_add_entities is not None: + entity_id = entity_registry.async_get(hass).async_get_entity_id( + "sensor", DOMAIN, f"{entry.entry_id}_pump_runs_today" + ) + if entity_id is None or hass.states.get(entity_id) is None: + manager.sensor_add_entities([PumpRunsTodaySensor(manager, entry)]) + cleanup_orphaned_diagnostic_entities(hass, manager, entry) + + class WasherBaseSensor(SensorEntity): """Base sensor for ha_washdata.""" _attr_has_entity_name = True + # Pushed by the manager's update signal. Polling re-wrote every entity every + # 30 s, idle included (audit PERF-02); only clock-driven values poll. + _attr_should_poll = False def __init__(self, manager: WashDataManager, entry: ConfigEntry) -> None: """Initialize.""" @@ -264,7 +298,6 @@ class WasherStateSensor(WasherBaseSensor): STATE_DELAY_WAIT, STATE_INTERRUPTED, STATE_FORCE_STOPPED, - STATE_RINSE, STATE_UNKNOWN, STATE_CLEAN, ], @@ -291,8 +324,10 @@ class WasherStateSensor(WasherBaseSensor): @property def extra_state_attributes(self): # type: ignore[override] + # No per-reading counter here (audit PLATFORM-07): `samples_recorded` + # changed on every power reading, so each reading wrote a new state row + # and a new attribute row to the recorder. The debug sensor keeps it. attrs: dict[str, Any] = { - "samples_recorded": self._manager.samples_recorded, "current_program_guess": self._manager.current_program, "sub_state": self._manager.sub_state, } @@ -435,7 +470,9 @@ class WasherTimeRemainingSensor(WasherBaseSensor): @property def native_value(self): # type: ignore[override] - if self._manager.check_state() in (STATE_OFF, STATE_ANTI_WRINKLE, STATE_DELAY_WAIT): + if self._manager.check_state() in ( + STATE_OFF, STATE_IDLE, STATE_ANTI_WRINKLE, STATE_DELAY_WAIT + ): return None if self._manager.time_remaining is not None: return int(self._manager.time_remaining / 60) @@ -461,22 +498,39 @@ class WasherTotalDurationSensor(WasherBaseSensor): @property def native_value(self): # type: ignore[override] - if self._manager.check_state() == STATE_OFF: + if self._manager.check_state() in (STATE_OFF, STATE_IDLE): # idle: #452 return None if self._manager.total_duration: return int(self._manager.total_duration / 60) return None - @property - def extra_state_attributes(self): # type: ignore[override] - """Return extra state attributes.""" - return { - "last_updated": self._manager.last_total_duration_update, - } + # No `last_updated` attribute any more (audit PERF-13): it was stamped on every + # estimate, so the entity wrote a state_changed event, i.e. one recorder row, + # every 5 s even while the whole-minute value stood still. An unrecorded + # attribute would not help: the recorder writes a row for every state_changed. + # The entity's own `last_changed` says when the total last moved. + + +def _whole_percent(value: Any) -> int | None: + """Progress rounded half-up to a whole percent (the card's Math.round).""" + if value is None: + return None + try: + pct = float(value) + except (TypeError, ValueError, OverflowError): + return None + if not math.isfinite(pct): + return None + return int(math.floor(pct + 0.5)) class WasherProgressSensor(WasherBaseSensor): - """Sensor for cycle progress percentage.""" + """Sensor for cycle progress percentage. + + The state is a whole percent (audit PERF-13): as a raw float it changed on + every estimate, and every state_changed event is a recorder row. The panel + reads the unrounded figure over the WS API, not from this entity. + """ def __init__(self, manager: WashDataManager, entry: ConfigEntry) -> None: """Initialize the progress sensor.""" @@ -484,14 +538,17 @@ class WasherProgressSensor(WasherBaseSensor): key="cycle_progress", translation_key="cycle_progress", native_unit_of_measurement="%", - suggested_display_precision=1, + suggested_display_precision=0, icon="mdi:progress-clock", ) super().__init__(manager, entry) + # The projection attributes, refreshed when the shown percent moves. + self._attrs_key: tuple[Any, ...] | None = None + self._attrs: dict[str, float] | None = None @property def native_value(self): # type: ignore[override] - return self._manager.cycle_progress + return _whole_percent(self._manager.cycle_progress) @property def extra_state_attributes(self): # type: ignore[override] @@ -500,15 +557,23 @@ class WasherProgressSensor(WasherBaseSensor): Derived from accumulated energy and the (ML-blended) progress estimate. Keys are present only while a projection is available, so the attributes stay clean when idle or early in a cycle. + + Refreshed when the shown percent changes or a projection appears or goes, + not on every estimate: the projection moves with each reading, so it would + otherwise write the state_changed row the whole-percent state saves. """ - attrs: dict[str, float] = {} projected_wh = self._manager.projected_energy_wh - if projected_wh is not None: - attrs["projected_energy_kwh"] = round(float(projected_wh) / 1000.0, 3) projected_cost = self._manager.projected_cost - if projected_cost is not None: - attrs["projected_cost"] = round(float(projected_cost), 2) - return attrs or None + key = (self.native_value, projected_wh is None, projected_cost is None) + if key != self._attrs_key: + attrs: dict[str, float] = {} + if projected_wh is not None: + attrs["projected_energy_kwh"] = round(float(projected_wh) / 1000.0, 3) + if projected_cost is not None: + attrs["projected_cost"] = round(float(projected_cost), 2) + self._attrs_key = key + self._attrs = attrs or None + return self._attrs class WasherPowerSensor(WasherBaseSensor): @@ -533,6 +598,9 @@ class WasherPowerSensor(WasherBaseSensor): class WasherElapsedTimeSensor(WasherBaseSensor): """Sensor for elapsed cycle time.""" + # Elapsed time advances with the clock even when a quiet plug sends nothing. + _attr_should_poll = True + def __init__(self, manager: WashDataManager, entry: ConfigEntry) -> None: """Initialize the elapsed time sensor.""" self.entity_description = SensorEntityDescription( @@ -540,6 +608,12 @@ class WasherElapsedTimeSensor(WasherBaseSensor): translation_key="elapsed_time", native_unit_of_measurement="s", device_class=SensorDeviceClass.DURATION, + # Shown in minutes like time remaining / total duration (#232). The + # native unit stays seconds: HA applies a suggested unit only when the + # entity is first registered, so an existing entity keeps the unit its + # history and automations use (no state_class, so no statistics) until + # its owner picks another one in the entity settings, which HA converts. + suggested_unit_of_measurement="min", suggested_display_precision=0, icon="mdi:timer-outline", ) @@ -547,12 +621,15 @@ class WasherElapsedTimeSensor(WasherBaseSensor): @property def native_value(self): # type: ignore[override] - if self._manager.check_state() == STATE_OFF: + if self._manager.check_state() in (STATE_OFF, STATE_IDLE): # idle: #452 return 0 start = self._manager.cycle_start_time if start: - delta = dt_util.now() - start - return int(delta.total_seconds()) + # Whole minutes (audit PLATFORM-07): to the second it changed on every + # state write, one recorder row per power reading. UTC, like every + # interval (DST). + delta = utc_now() - dt_util.as_utc(start) + return int(delta.total_seconds()) // 60 * 60 return 0 @@ -580,9 +657,20 @@ class WasherDebugSensor(WasherBaseSensor): detector = self._manager.detector stats = self._manager.sample_interval_stats # pylint: disable=protected-access + # The confidence comes from the same result as top_candidates and + # last_match_details (audit MATCH-DECIDE-14), not the committed program's + # `_last_match_confidence`, which moves only when the switching rules commit + # or re-confirm the program. + # That one is still the Match Confidence sensor's state; it is the fallback + # here only while there is no result to describe. + last = getattr(self._manager, "_last_match_result", None) attrs: dict[str, Any] = { "sub_state": detector.sub_state, - "match_confidence": getattr(self._manager, "_last_match_confidence", 0.0), + "match_confidence": ( + float(getattr(last, "confidence", 0.0) or 0.0) + if last is not None + else getattr(self._manager, "_last_match_confidence", 0.0) + ), "cycle_id": getattr(detector, "_current_cycle_start", None), "samples": detector.samples_recorded, "energy_accum": getattr(detector, "_energy_since_idle_wh", 0.0), @@ -713,11 +801,43 @@ class WasherProfileCountSensor(WasherBaseSensor): # Override unique ID to be profile specific self._attr_unique_id = f"{entry.entry_id}_profile_count_{self._profile_token}" + # One state write reads `available`, `native_value` and + # `extra_state_attributes`; take one profile snapshot for all three instead of + # three lookups per write (audit PERF-01). + _write_snapshot: dict[str, Any] | None = None + _write_snapshot_valid: bool = False + + def _profile(self) -> dict[str, Any] | None: + if self._write_snapshot_valid: + return self._write_snapshot + return self._manager.profile_store.get_profile(self._profile_name) + + async def async_added_to_hass(self) -> None: + """Listen to the profiles signal, not the live one (register item 456).""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_WASHER_PROFILES_UPDATE.format(self._entry.entry_id), + self._update_callback, + ) + ) + + @callback + def _update_callback(self) -> None: + """Write state from a single profile snapshot.""" + self._write_snapshot = self._manager.profile_store.get_profile(self._profile_name) + self._write_snapshot_valid = True + try: + self.async_write_ha_state() + finally: + self._write_snapshot = None + self._write_snapshot_valid = False + @property def native_value(self) -> int: # type: ignore[override] """Return the cycle count.""" # Fetch fresh count from store if available - profile = self._manager.profile_store.get_profile(self._profile_name) + profile = self._profile() if profile: return profile.get("cycle_count", 0) return 0 @@ -725,12 +845,12 @@ class WasherProfileCountSensor(WasherBaseSensor): @property def available(self) -> bool: # type: ignore[override] """Return True if profile still exists.""" - return self._manager.profile_store.get_profile(self._profile_name) is not None + return self._profile() is not None @property def extra_state_attributes(self) -> dict[str, Any] | None: # type: ignore[override] """Return profile statistics.""" - profile = self._manager.profile_store.get_profile(self._profile_name) + profile = self._profile() if not profile: return None @@ -796,17 +916,22 @@ class WasherProfileSensorManager: self._entry = entry self._async_add_entities = async_add_entities self._sensors: dict[str, WasherProfileCountSensor] = {} - self._diagnostics_cleanup_done: bool = False # Determine the signal string. It must match SIGNAL_WASHER_UPDATE from const.py # which is "washdata_update_{}" self._signal = SIGNAL_WASHER_UPDATE.format(entry.entry_id) self._update_task: Task[None] | None = None self._pending_update: bool = False + # The summaries revision last acted on; held, so `is` stays meaningful. + # Seeded with today's: the sensors about to be created are written from it. + self._seen_revision: object | None = None + try: + self._seen_revision = manager.profile_store.profile_summaries_revision() + except Exception: # noqa: BLE001 - None just means "refresh on first notify" + self._seen_revision = None - # Register callback for ALL updates (simplest hook we have) - # Ideally we'd have a specific profile update signal, but general update is fine - # as long as we debounce or check efficiently. + # Every live refresh comes through here; it is turned into the profiles + # signal only when the profile summaries actually changed. self._unsub_dispatcher = async_dispatcher_connect( manager.hass, self._signal, @@ -818,7 +943,6 @@ class WasherProfileSensorManager: cleanup_orphaned_diagnostic_entities( self._manager.hass, self._manager, self._entry ) - self._diagnostics_cleanup_done = True def unsubscribe(self) -> None: """Remove the dispatcher subscription.""" @@ -834,7 +958,18 @@ class WasherProfileSensorManager: @callback def _update_callback(self) -> None: - """Handle updates.""" + """Handle updates: act only when the profile summaries changed.""" + try: + revision = self._manager.profile_store.profile_summaries_revision() + except Exception: # noqa: BLE001 - a failed check must not freeze the sensors + revision = object() + if revision is self._seen_revision: + return + self._seen_revision = revision + async_dispatcher_send( + self._manager.hass, + SIGNAL_WASHER_PROFILES_UPDATE.format(self._entry.entry_id), + ) if self._update_task and not self._update_task.done(): self._pending_update = True return @@ -968,6 +1103,8 @@ class PumpRunsTodaySensor(WasherBaseSensor): Only created when device type is ``pump``. """ + # A rolling 24 h count: runs age out with the clock, not with an update. + _attr_should_poll = True def __init__(self, manager: WashDataManager, entry: ConfigEntry) -> None: self.entity_description = SensorEntityDescription( @@ -987,8 +1124,8 @@ class WasherCycleCountSensor(WasherBaseSensor): """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: + stored-history number shrinks when the user deletes a record (and was capped at + 200 until 0.5.8), 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. diff --git a/custom_components/ha_washdata/services.yaml b/custom_components/ha_washdata/services.yaml index 52214f02..2719ed7e 100644 --- a/custom_components/ha_washdata/services.yaml +++ b/custom_components/ha_washdata/services.yaml @@ -17,7 +17,7 @@ label_cycle: text: profile_name: name: Profile Name - description: The name of an existing profile (create profiles in Manage Profiles menu). Leave blank to remove label. + description: An existing profile name (create profiles in the WashData panel). Leave blank to remove the label. required: false selector: text: @@ -84,9 +84,8 @@ auto_label_cycles: integration: ha_washdata confidence_threshold: name: Confidence Threshold - description: Minimum match confidence (0.50-0.95) to apply labels. + description: Minimum match confidence (0.50-0.95) to apply labels. Leave empty to use the device's Auto-Label Confidence setting. required: false - default: 0.75 selector: number: min: 0.50 @@ -95,7 +94,7 @@ auto_label_cycles: export_config: name: Export Config - description: Export this washer's profiles and cycles to a JSON file (per device). + description: Export this device's profiles, cycles and settings to a JSON file. fields: device_id: name: Device @@ -132,8 +131,8 @@ import_config: submit_cycle_feedback: name: Submit Cycle Feedback description: > - Confirm or correct an auto-detected program after a completed cycle. - Provide either `entry_id` (advanced) or `device_id` (recommended). + Confirm or correct the detected program of a finished cycle. + Give `device_id` (recommended) or `entry_id`. fields: device_id: name: Device @@ -274,7 +273,7 @@ resume_cycle: mark_unloaded: name: Mark Unloaded - description: Confirm the finished load has been taken out, clearing the Clean state and any unload reminder. Does nothing when no load is waiting. + description: Confirm the finished load was taken out. Clears the Clean state and any unload reminder. fields: device_id: name: Device diff --git a/custom_components/ha_washdata/setup_advisor.py b/custom_components/ha_washdata/setup_advisor.py index 65a456d8..24b0edca 100644 --- a/custom_components/ha_washdata/setup_advisor.py +++ b/custom_components/ha_washdata/setup_advisor.py @@ -29,30 +29,57 @@ def compute_setup_phase( ref_profile_names: set[str], coverage_gap: dict | None, suggestions: list[dict], - profile_groups: list[dict], skipped_steps: dict[str, str | None], now: datetime, + backfill_cycles: list[dict] | None = None, ) -> SetupPhaseResult: """Compute the current adoption phase for a device. + Every cycle argument is the **evidence** view (``iter_evidence_cycles``), not the + stored one: each check here asks whether a profile can be matched, and a profile + whose only cycles the user excluded cannot. That keeps phase0 equal to + ``ProfileStore.has_real_profiles`` being False, which is when the manager skips + matching and leaves this card to explain why (register item 129d). + Args: device_type: HA device type string (washing_machine, dishwasher, ...). profile_names: All profile names stored for this device. - past_cycles: All past cycles (each may have profile_name and meta.source). - ref_profile_names: Profile names that have reference cycles (store-adopted). + past_cycles: Live cycles that count as evidence (each may have profile_name + and meta.source). + ref_profile_names: Profile names backed by evidence reference cycles + (store-adopted). coverage_gap: Result of profile_store.suggest_coverage_gaps(), or None. suggestions: Actionable suggestions from SuggestionEngine (empty list = none). - profile_groups: Profile groups list from store (empty list = none pending). skipped_steps: Dict of step_key -> "never" | ISO timestamp | None. now: Current aware datetime for snooze comparisons. + backfill_cycles: Evidence cycles recovered from this machine's raw power + history (#344). They are its own history, so a profile they back is a + self-built profile, never a community one; they are never recordings. """ - real = _real_profile_names(profile_names, past_cycles) + backfill = list(backfill_cycles or []) + own_cycles = [*past_cycles, *backfill] + real = _real_profile_names(profile_names, own_cycles) has_real = bool(real) + # Only the recorder writes meta.source == "recorder", and it writes past_cycles. has_recorded = _has_recorded_cycles(past_cycles, real) has_store = bool(ref_profile_names) - has_self_cycles = bool(real) # any cycle assigned to a real profile + has_self_cycles = bool(real) # any own cycle assigned to a real profile # ── Phase 0 ────────────────────────────────────────────────────────────── + if not has_real and not has_store and any(not c.get("profile_name") for c in backfill): + # Imported history is waiting to be named: labelling it is the next step, + # recording is the alternative. "Start recording" first told a user who had + # just imported weeks of cycles that the device had nothing (item 129d). + return SetupPhaseResult( + phase="phase0", + message_key="setup.phase0.generic", + cta_label_key="setup.cta.label_detected_cycle", + cta_action="open_cycles_unlabeled", + secondary_label_key="setup.cta.start_recording", + secondary_action="open_recorder", + skippable=False, + dismissible=False, + ) if not has_real and not has_store: msg_key = { "washing_machine": "setup.phase0.washer", @@ -114,7 +141,7 @@ def compute_setup_phase( ) # ── Phase 3 — tuning items ──────────────────────────────────────────────── - item = _phase3_pending_item(suggestions, profile_groups, skipped_steps, now) + item = _phase3_pending_item(suggestions, skipped_steps, now) if item: return item @@ -131,7 +158,7 @@ def compute_setup_phase( # ≥5 cycles assigned to real profiles. At that point the "record your first # cycle" nudge is stale and misleading regardless of whether the user ever # clicked Skip. - _established = len(real) >= 2 or _real_cycle_count(past_cycles, real) >= 5 + _established = len(real) >= 2 or _real_cycle_count(own_cycles, real) >= 5 _coverage_gap_actionable = bool(coverage_gap and coverage_gap.get("suggest_create")) _phase1_suppressed = _is_step_suppressed("setup_skip_phase1", skipped_steps, now) if has_real and not _established and not _coverage_gap_actionable and not _phase1_suppressed: @@ -209,7 +236,7 @@ def _is_step_suppressed(step_key: str, skipped_steps: dict, now: datetime) -> bo if until.tzinfo is None: until = until.replace(tzinfo=timezone.utc) return now < until - except (ValueError, TypeError): + except (ValueError, TypeError, OverflowError): return False @@ -221,7 +248,6 @@ def _phase2_active(coverage_gap: dict | None, skipped_steps: dict, now: datetime def _phase3_pending_item( suggestions: list[dict], - profile_groups: list[dict], skipped_steps: dict, now: datetime, ) -> SetupPhaseResult | None: @@ -235,14 +261,4 @@ def _phase3_pending_item( dismissible=True, step_key="setup_skip_phase3_suggestions", ) - if profile_groups and not _is_step_suppressed("setup_skip_phase3_groups", skipped_steps, now): - return SetupPhaseResult( - phase="phase3", - message_key="setup.phase3.groups", - cta_label_key="setup.cta.organise_profiles", - cta_action="open_profiles_groups", - skippable=True, - dismissible=True, - step_key="setup_skip_phase3_groups", - ) return None diff --git a/custom_components/ha_washdata/signal_processing.py b/custom_components/ha_washdata/signal_processing.py index 82bd90f4..6c6014eb 100644 --- a/custom_components/ha_washdata/signal_processing.py +++ b/custom_components/ha_washdata/signal_processing.py @@ -23,6 +23,8 @@ Constraint: Resampling must be segment-based (no interpolation across gaps). from __future__ import annotations +import math + from dataclasses import dataclass from collections.abc import Sequence from typing import List, Tuple @@ -53,7 +55,7 @@ def energy_gap_threshold_s(timestamps: np.ndarray) -> float: Ten times the median sample interval, clamped to ``[60, 3600]``. Segments longer than this are treated as sensor outages and excluded from the energy sum, without masking valid slow-sampling configurations. Single source for - both persistence paths (``manager._on_cycle_end`` / ``ProfileStore.add_cycle``). + both persistence paths (``manager._on_cycle_end`` / ``ProfileStore.async_add_cycle``). """ ts = np.asarray(timestamps, dtype=float) if ts.size < 2: @@ -475,3 +477,179 @@ def quiet_run_before( if prev_active is None: return 0.0 return max(0.0, run_start - prev_active) + + +def terminal_quiet_seen( + points: list[tuple[float, float]], + last_active: float, + stop_threshold_w: float, + quiet_s: float, + peak_frac: float, +) -> bool: + """Has this trace ALREADY been through the quiet phase its profile ends with? + + ``quiet_s`` is ``compute_profile_terminal_signature``'s ``quiet_before_s``: the + quiet measured before the LAST run above ``peak * peak_frac`` of each cycle. + A kept tail may add up to that much past the last activity, on the reasoning + that a run which has not yet dried is still drying - so the allowance must be + withheld when this run already did. + + Asked two ways, and either one answers yes (shorten-only): + + * at ``stop_threshold_w``, from ``last_active`` - the original test (register + item 347); + * at the SIGNATURE'S OWN threshold, from the last sample above it (#424). The + statistic and the test of whether it has happened must use the same level. + On #424's Beko the quiet is measured at 7.9 W (0.4% of a 1.97 kW peak) while + its stop threshold is 0.96 W; its run ends 15-20 W drain -> 1.3 W -> 0.3 W, + so at the stop threshold the last activity is the 1.3 W wind-down sample, + preceded by the drain, and the test found no quiet at all. Every cycle then + banked the full 611 s allowance as cycle time. At 7.9 W the same traces show + 605-630 s of quiet, i.e. the phase had happened. + + Shared by ``CycleDetector._keep_tail_cap`` and the banked-tail repair so a + cycle is judged the same way live and in history. Pure; never raises. + """ + try: + if quiet_s <= 0 or not points: + return False + need = 0.5 * float(quiet_s) + if quiet_run_before(points, last_active, stop_threshold_w) >= need: + return True + peak = max(p for _o, p in points) + if peak <= 0: + return False + thr = peak * peak_frac + last_event: float | None = None + for offset, power in reversed(points): + if power > thr: + last_event = offset + break + if last_event is None: + return False + return quiet_run_before(points, last_event, thr) >= need + except Exception: # noqa: BLE001 - a statistic must never break a finish + return False + + +def terminal_event_end( + points: Sequence[tuple[float, float]], last_active: float, peak_frac: float +) -> float: + """Where a run that has been through its terminal quiet actually ends. + + ``last_active`` (the last sample above the stop threshold), or the last sample + above ``peak * peak_frac`` when that is later: a stop threshold above the + signature's level leaves a quiet pump-out BELOW it, and when only + :func:`terminal_quiet_seen`'s peak-fraction test fires, ending at + ``last_active`` cut that pump-out off (register item 384). Shared by the live + cap and the banked-tail repair, like ``terminal_quiet_seen``. Never raises. + """ + try: + peak = max((p for _o, p in points), default=0.0) + if peak <= 0: + return last_active + thr = peak * peak_frac + for offset, power in reversed(points): + if power > thr: + return max(float(last_active), float(offset)) + return last_active + except Exception: # noqa: BLE001 + return last_active + + +def has_resumed_pause( + points: Sequence[tuple[float, float]], threshold_w: float, min_pause_s: float +) -> bool: + """Does this trace hold a pause below ``threshold_w`` of ``min_pause_s`` or more + that power later came back from? (#424) + + Timed on the wall clock from the first below-threshold sample to the next one at + or above it, so a change-only plug that reports one 0 W row and then nothing + still counts its silence. A run still open at the end of the trace never + resumed: that is the cycle's own end, not a pause. A run that starts on the + first sample is the standby before the cycle (a curve pre-roll), not a pause + either. Pure; never raises. + """ + return longest_resumed_pause_s(points, threshold_w, skip_leading=True) >= min_pause_s + + +def longest_resumed_pause_s( + points: Sequence[tuple[float, float]], threshold_w: float, *, skip_leading: bool = True +) -> float: + """Longest pause below ``threshold_w`` that power later came back from (#458). + + Timed like :func:`has_resumed_pause`. With ``skip_leading`` a run that starts on + the trace's first sample is ignored: that is standby before the cycle began (a + curve pre-roll), which the end gates never see. A run still open at the end of + the trace is the cycle's own end and is ignored too. Pure; never raises. + """ + return max( + (d for _f, d in resumed_pauses(points, threshold_w, skip_leading=skip_leading)), + default=0.0, + ) + + +def resumed_pauses( + points: Sequence[tuple[float, float]], threshold_w: float, *, skip_leading: bool = True +) -> list[tuple[float, float]]: + """``(start_fraction, seconds)`` of every pause below ``threshold_w`` that power + later came back from, timed like :func:`has_resumed_pause`. + + ``start_fraction`` is where the pause began as a share of the trace's span: the + hazard end gate's per-profile pause catalogue (audit DETECT-16). Pure; never + raises. + """ + try: + out: list[tuple[float, float]] = [] + if not points: + return out + t0 = float(points[0][0]) + span = float(points[-1][0]) - t0 + first_below: float | None = None + leading = True + for offset, power in points: + if power < threshold_w: + if first_below is None: + first_below = offset + else: + if first_below is not None and not (skip_leading and leading): + out.append(( + (first_below - t0) / span if span > 0 else 0.0, + offset - first_below, + )) + first_below = None + leading = False + return out + except Exception: # noqa: BLE001 + return [] + + +def percentile_linear(values: Sequence[float], q: float) -> float: + """``np.percentile(values, q)`` (linear method) without NumPy, bit for bit. + + For the detector's 20-interval cadence window, where NumPy's per-call + overhead (40-185 us) dwarfed the arithmetic (audit PERF-07). Same virtual + index ``(n - 1) * q`` and the same two-sided lerp NumPy uses. ``values`` + must be non-empty. + """ + a = sorted(float(v) for v in values) + n = len(a) + virtual = (n - 1) * (q / 100.0) + lo = math.floor(virtual) + hi = min(lo + 1, n - 1) + lo = min(max(lo, 0), n - 1) + t = virtual - lo + x, y = a[lo], a[hi] + diff = y - x + return y - diff * (1.0 - t) if t >= 0.5 else x + diff * t + + +def median_fast(values: Sequence[float]) -> float: + """``np.median(values)`` without NumPy, bit for bit; ``values`` non-empty.""" + a = sorted(float(v) for v in values) + n = len(a) + mid = n // 2 + if n % 2: + return a[mid] + return (a[mid - 1] + a[mid]) / 2.0 + diff --git a/custom_components/ha_washdata/store.py b/custom_components/ha_washdata/store.py index 2c23ed2c..bf513fd3 100644 --- a/custom_components/ha_washdata/store.py +++ b/custom_components/ha_washdata/store.py @@ -24,13 +24,15 @@ brand/model stay per-device. Nothing here runs unless online features are enable from __future__ import annotations import logging +import math +from collections.abc import Callable from typing import Any from homeassistant.core import HomeAssistant from . import store_account from .const import QC_EDITED, QC_MANUAL, QC_RECORDING -from .store_client import device_id, get_client, profile_id, trace_hash +from .store_client import STORE_UNREACHABLE, device_id, get_client, profile_id, trace_hash _LOGGER = logging.getLogger(__name__) @@ -122,6 +124,34 @@ def _downsample(points: list[list[float]], max_n: int = 10000) -> list[list[floa return sampled +# Points kept per browse-row trace: the panel's sparkline is 120 px wide. +_BROWSE_TRACE_POINTS = 200 + + +def _browse_rows(cycles: list[dict[str, Any]]) -> list[dict[str, Any]]: + """Browse copies of store cycle rows: no ``importable``, trace downsampled to + ``_BROWSE_TRACE_POINTS`` (see ``StoreBridge.get_cycles``). Executor-safe; never + raises (a non-numeric point is dropped, store data is untrusted).""" + out: list[dict[str, Any]] = [] + for cyc in cycles: + if not isinstance(cyc, dict): + continue + row = {k: v for k, v in cyc.items() if k != "importable"} + trace = cyc.get("trace") + if isinstance(trace, dict) and isinstance(trace.get("points"), list): + pts: list[list[float]] = [] + for p in trace["points"]: + try: + o, w = float(p[0]), float(p[1]) + except (TypeError, ValueError, IndexError, KeyError, OverflowError): + continue + if math.isfinite(o) and math.isfinite(w): + pts.append([o, w]) + row["trace"] = {**trace, "points": _downsample(pts, _BROWSE_TRACE_POINTS)} + out.append(row) + return out + + def _cycle_upload_stats(cyc: dict[str, Any], pts: list[list[float]]) -> dict[str, Any]: """Build the community-upload stats for a cycle from its stored metadata + trace. @@ -140,7 +170,7 @@ def _cycle_upload_stats(cyc: dict[str, Any], pts: list[list[float]]) -> dict[str } try: energy = float(cyc.get("energy_wh")) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): energy = 0.0 if energy > 0: stats["energy_wh"] = energy @@ -184,17 +214,36 @@ class StoreBridge: return {"enabled": store_account.online_enabled(self._hass), **store_account.get_identity(self._hass)} async def connect(self, refresh_token: str, uid: str, name: str | None) -> dict[str, Any]: - # Validate the refresh token by exchanging it once before persisting. + """Validate the refresh token by exchanging it once, then persist the account. + + The stored uid is the one the token endpoint returned for that exchange, never + the caller's ``uid`` (audit STORE-18): a mismatch made every later write 403 + under the store rules' ``uid == request.auth.uid`` checks. ``uid`` is kept in + the signature for the WS contract; it is only compared, for a log line. + """ # ensure_id_token writes the client-wide _last_error slot on failure, so it # takes the same lock the upload paths use: otherwise a sign-in failing here # could overwrite the reason a concurrent share is about to report. async with self._client.write_lock: token = await self._client.ensure_id_token(refresh_token) - if not token: + verified = self._client.verified_uid(refresh_token) if token else None + if not token or not verified: return {"error": "token_invalid"} - await store_account.async_set_account(self._hass, {"refresh_token": refresh_token, "uid": uid, "name": name}) + if uid and uid != verified: + _LOGGER.warning("Store connect: ignoring a uid that does not match the token's account") + await store_account.async_set_account( + self._hass, {"refresh_token": refresh_token, "uid": verified, "name": name}, + ) return store_account.get_identity(self._hass) + def _uploader_name(self, acct: dict[str, Any]) -> str | None: + """The name published on a shared cycle: the account's display name only when + the user opted in (``share_name``), else None (audit STORE-14).""" + if not store_account.get_pref(self._hass, "share_name"): + return None + name = acct.get("name") + return name if isinstance(name, str) and name.strip() else None + async def disconnect(self) -> dict[str, Any]: await store_account.async_clear_account(self._hass) return {"connected": False} @@ -228,10 +277,13 @@ class StoreBridge: self._client.refresh_catalog() return {"ok": True} - async def get_profiles(self, device_id: str, *, include_pending: bool = True) -> list[dict[str, Any]]: + async def get_profiles( + self, device_id: str, *, include_pending: bool = True, + ) -> list[dict[str, Any]] | None: """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).""" + browse cannot silently drift back to approved-only (which showed nothing). + ``None`` = the store could not be reached, not "no programs" (audit STORE-09).""" 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]: @@ -239,11 +291,20 @@ class StoreBridge: dialog's profile picker). Maps the HA device type to the catalog type first.""" return await self._client.device_profiles(brand, model, store_appliance_type(appliance_type)) - async def get_cycles(self, profile_id: str) -> list[dict[str, Any]]: - return await self._client.get_cycles(profile_id) + async def get_cycles(self, profile_id: str) -> list[dict[str, Any]] | None: + """A program's shared cycles for the browse list; ``None`` = store unreachable. + + Slimmed for the wire (audit STORE-20): the panel draws each trace as a 120 px + sparkline, yet every row carried the full trace twice (``trace.points`` and + ``importable``, up to 7k points each, 50 rows). Import re-fetches the cycle by + id, so the browse rows drop ``importable`` and carry a peak-preserving + downsample. The download path reads the client directly and keeps full traces. + """ + cycles = await self._client.get_cycles(profile_id) + if not cycles: + return cycles + return await self._hass.async_add_executor_job(_browse_rows, cycles) - async def get_device_quality(self, device_id: str) -> dict[str, Any]: - return await self._client.get_device_quality(device_id) # ── community actions (authed writes) ──────────────────────────────────────── @@ -280,11 +341,18 @@ class StoreBridge: profile = raw_profile.strip() if isinstance(raw_profile, str) else "" if not profile: return {"error": "profile_name_required"} - local_id = await self._ps.add_reference_cycle(profile, pts, { + meta = { "store_cycle_id": cyc.get("id"), "store_uploaded_at": cyc.get("createdAt"), "sampling_interval": (cyc.get("trace") or {}).get("sampleIntervalSec"), - }) + "community": True, + } + # Too short / gappy / implausible, or a copy of a trace already stored + # under any name (audit STORE-03/05): refused before it shapes a profile. + verdict = self._ps.reference_import_verdict(pts, meta) + if verdict != "ok": + return {"error": "invalid_trace" if verdict == "invalid" else verdict} + local_id = await self._ps.add_reference_cycle(profile, pts, meta) if not local_id: # trace failed validation in add_reference_cycle return {"error": "invalid_trace"} # Credit the download on the source store cycle + record one community-wide @@ -326,7 +394,7 @@ class StoreBridge: # write and the read that interprets it have to be one critical section. async with self._client.write_lock: new_id = await self._client.upload_reference_cycle( - acct["refresh_token"], acct.get("uid", ""), acct.get("name"), + acct["refresh_token"], acct.get("uid", ""), self._uploader_name(acct), meta, downsampled, stats, derive_qc(cyc), ) if not new_id: @@ -400,7 +468,7 @@ class StoreBridge: device_meta["settings"] = dict(settings) async with self._client.write_lock: # see share_cycle res = await self._client.upload_device_bundle( - acct["refresh_token"], acct.get("uid", ""), acct.get("name"), device_meta, bundle_items, + acct["refresh_token"], acct.get("uid", ""), self._uploader_name(acct), device_meta, bundle_items, ) # Return the raw bundle result ({ok, cycle_ids, errors}) so the caller can # tell a partial upload (some cycle_ids present) from a total failure. @@ -409,7 +477,14 @@ class StoreBridge: res = {**res, "detail": self._client.last_error()} return res - async def download_device(self, device_id_: str, device_type: str = "") -> dict[str, Any]: + async def download_device( + self, + device_id_: str, + device_type: str = "", + *, + progress: Callable[[int, int], None] | None = None, + should_cancel: Callable[[], bool] | None = None, + ) -> dict[str, Any]: """Adopt a whole-device bundle: for each downloaded profile, import its reference cycles into ``reference_cycles`` (merge/upsert; real past_cycles are never touched) and, when the profile carries a phase map, replace the local @@ -421,38 +496,72 @@ class StoreBridge: Idempotent: a store cycle already imported locally (``meta.source == "store:"``) is skipped, so re-downloading the same device does not accumulate duplicate reference cycles. + + A store that could not be read is reported as ``{"error": + "store_unreachable"}``, never as an empty adopt (audit STORE-09: offline used + to read "Nothing new - already on your device"). When only some programs' + cycles could not be fetched, the result carries ``partial`` + + ``failed_profiles``; if nothing new was imported at all it is that error. """ bundle = await self._client.get_device_bundle(device_id_) + if bundle.get("error"): + return {"error": str(bundle["error"])} already = { str((c.get("meta") or {}).get("source") or "") for c in self._ps.get_reference_cycles() } + # Hashed once for the whole bundle; each import adds its own, so two + # programs carrying the same recording keep only the first (STORE-05). + known_hashes = self._ps.stored_trace_hashes() profiles_adopted = 0 cycles_imported = 0 + cycles_skipped = 0 phases_applied = 0 imported_store_ids: list[str] = [] - for prof in bundle.get("profiles", []) or []: + # Batched (audit STORE-10): every cycle used to rebuild its envelope and + # rewrite the whole store - 40 saves for a 40-cycle bundle, 7 s vs 1.5 s. + # Now one rebuild per touched program and one save at the end. + touched: list[str] = [] + profiles_list = [p for p in (bundle.get("profiles", []) or []) if isinstance(p, dict)] + failed_profiles = sum(1 for p in profiles_list if p.get("cycles_unavailable")) + total = sum(len(p.get("cycles") or []) for p in profiles_list) + seen = 0 + cancelled = False + for prof in profiles_list: + if cancelled: + break program = str(prof.get("program") or prof.get("program_lc") or "").strip() if not program: continue adopted_any = False for cyc in prof.get("cycles", []) or []: + if should_cancel is not None and should_cancel(): + cancelled = True + break + if progress is not None: + progress(seen, total) + seen += 1 pts = cyc.get("importable") if not pts: continue store_cid = cyc.get("id") if store_cid and f"store:{store_cid}" in already: continue # already imported on a previous download - local_id = await self._ps.add_reference_cycle(program, pts, { + local_id = self._ps._add_reference_cycle_nosave(program, pts, { # noqa: SLF001 "store_cycle_id": store_cid, "store_uploaded_at": cyc.get("createdAt"), "sampling_interval": (cyc.get("trace") or {}).get("sampleIntervalSec"), - }) + "community": True, + }, known_hashes=known_hashes) if local_id: + if program not in touched: + touched.append(program) cycles_imported += 1 adopted_any = True if store_cid: imported_store_ids.append(store_cid) + else: + cycles_skipped += 1 if adopted_any: profiles_adopted += 1 # Stage 2: apply the bundled phase map (replace) + reconcile labels. Never @@ -467,12 +576,27 @@ class StoreBridge: # community-wide "download" (adoption) for the store's usage dashboard -- the real # metric of how many people actually pulled this into their integration. Fired in # the background so store latency never delays the adopt-bundle response. + for program in touched: + await self._ps.async_rebuild_envelope(program) + if touched: + await self._ps.async_save() if imported_store_ids: self._fire_download_telemetry(imported_store_ids) + if failed_profiles and not cycles_imported and not cancelled: + # Nothing new arrived and part of the setup could not be read: a failed + # download, not "already on your device" (audit STORE-09). + return { + "error": STORE_UNREACHABLE, "failed_profiles": failed_profiles, + "cycles_skipped": cycles_skipped, "phases_applied": phases_applied, + } settings = bundle.get("settings") if isinstance(bundle.get("settings"), dict) else {} return { "profiles_adopted": profiles_adopted, "cycles_imported": cycles_imported, + # Refused by the quality bar or as a duplicate (audit STORE-03/05). + "cycles_skipped": cycles_skipped, + **({"cancelled": True} if cancelled else {}), + **({"partial": True, "failed_profiles": failed_profiles} if failed_profiles else {}), "phases_applied": phases_applied, "settings": settings, } @@ -492,7 +616,7 @@ class StoreBridge: name = str(p.get("name", "")).strip() try: start, end = float(p.get("start", 0)), float(p.get("end", 0)) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): continue if name and end > start: ranges.append({"name": name, "start": start, "end": end}) diff --git a/custom_components/ha_washdata/store_account.py b/custom_components/ha_washdata/store_account.py index 1ad42cb3..cbf4c15a 100644 --- a/custom_components/ha_washdata/store_account.py +++ b/custom_components/ha_washdata/store_account.py @@ -46,6 +46,10 @@ _LOAD_LOCK_KEY = f"{DOMAIN}_online_load_lock" # plumbing carries it end-to-end with no further wiring. _DEFAULT_PREFS: dict[str, Any] = { "show_contributor": True, # show "by " attribution in the pickers + # Publish the connected GitHub display name (often a real name) as the uploader of + # cycles this install shares. Opt-in, like the website's showName consent; off + # means shared cycles carry no name (audit STORE-14). + "share_name": False, } diff --git a/custom_components/ha_washdata/store_client.py b/custom_components/ha_washdata/store_client.py index a1833f58..91642595 100644 --- a/custom_components/ha_washdata/store_client.py +++ b/custom_components/ha_washdata/store_client.py @@ -26,9 +26,11 @@ Never raises into the event loop - failures return ``None``/empty and are logged from __future__ import annotations import asyncio +import base64 import hashlib import json import logging +import math import re import time import unicodedata @@ -42,9 +44,10 @@ from homeassistant.util import dt as dt_util from .const import ( DOMAIN, - SHAREABLE_SETTING_KEYS, + sanitize_shared_settings, STORE_API_KEY, STORE_PROJECT_ID, + STORE_WEB_ORIGIN, SUPPORTED_CYCLE_SCHEMA_VERSIONS, ) @@ -52,8 +55,11 @@ _LOGGER = logging.getLogger(__name__) _APPLIANCE_TYPES = {"washer", "dryer", "dishwasher", "washer_dryer"} -# Max concurrent per-cycle rating aggregations when listing a profile's cycles. -_RATING_FANOUT_LIMIT = 8 +# Error marker for a read the store could not answer (network failure, timeout, 5xx, +# rate limit). Distinct from an empty result on purpose: an offline store used to read +# as "no shared programs" and a failed download as "already on your device" (audit +# STORE-09). +STORE_UNREACHABLE = "store_unreachable" # Max profiles hydrated concurrently when downloading a whole-device bundle. One query # each (the bundle skips the per-cycle rating fan-out), kept small to stay well under the @@ -183,6 +189,42 @@ def _decode(v: dict[str, Any]) -> Any: return None +def _rating_from_doc(doc: dict[str, Any]) -> dict[str, Any]: + """``{"avg", "count"}`` from a doc's denormalized ``ratingSum`` / ``ratingCount``. + + Never raises; missing or malformed fields read as no ratings.""" + try: + count = int(doc.get("ratingCount") or 0) + total = float(doc.get("ratingSum") or 0) + avg = total / count if count > 0 else None + except (TypeError, ValueError, AttributeError, OverflowError): + # OverflowError: an integerValue decodes to an unbounded int. + return {"avg": None, "count": 0} + if avg is None or not math.isfinite(avg): + return {"avg": None, "count": 0} + return {"avg": avg, "count": count} + + +def _token_uid(body: dict[str, Any]) -> str | None: + """The Firebase uid the token endpoint vouches for, or None. + + ``user_id`` of the refresh-token exchange response, else the ``user_id`` / ``sub`` + claim of the ID token that same response carried. Both come from Google over TLS + in reply to our own request, so neither needs a signature check here; what must + NOT be trusted is the uid a caller hands to ``connect`` (audit STORE-18). + """ + uid = body.get("user_id") + if isinstance(uid, str) and uid: + return uid + try: + payload = str(body.get("id_token") or "").split(".")[1] + claims = json.loads(base64.urlsafe_b64decode(payload + "=" * (-len(payload) % 4))) + uid = claims.get("user_id") or claims.get("sub") + except Exception: # noqa: BLE001 - malformed token -> no verified uid + return None + return uid if isinstance(uid, str) and uid else None + + def _decode_doc(doc: dict[str, Any]) -> dict[str, Any]: out = {k: _decode(x) for k, x in doc.get("fields", {}).items()} name = doc.get("name", "") @@ -190,6 +232,53 @@ def _decode_doc(doc: dict[str, Any]) -> dict[str, Any]: return out +def _favorite_count(value: Any) -> int | float: + """A row's ``favoriteCount`` as a number the browse can sort by: 0 when it is + missing, a bool, a string or NaN (index rows and delta rows are both remote).""" + if isinstance(value, bool) or not isinstance(value, (int, float)) or value != value: + return 0 + return value + + +def _shape_index(raw: Any) -> dict[str, Any] | None: + """The published ``search-index.json`` (schema 1) as dict rows, or None. + + Rows are positional arrays under ``fields``; a brand's id IS its ``brand_lc``, + a device's ``brand_lc`` / ``model_lc`` are its lowercased names, as stored. + """ + if not isinstance(raw, dict) or raw.get("schema") != 1 or not raw.get("generatedAt"): + return None + fields = raw.get("fields") + # Remote data, validated here so a bad index falls back to the direct queries + # instead of raising in a consumer: every one keys rows by ``id``. + if not isinstance(fields, dict) or not all( + isinstance(fields.get(k), list) and "id" in fields[k] for k in ("brands", "devices") + ): + return None + + def _rows(name: str) -> list[dict[str, Any]]: + names = fields[name] + out = [] + for row in raw.get(name) or []: + if isinstance(row, list) and len(row) == len(names): + item = dict(zip(names, row)) + if isinstance(item.get("id"), str) and item["id"]: + out.append(item) + return out + + brands = _rows("brands") + for b in brands: + b["brand_lc"] = str(b.get("id") or "").lower() + devices = _rows("devices") + for d in devices: + d["favoriteCount"] = _favorite_count(d.get("favoriteCount")) # the browse sorts by it + # As the device documents store it (lowercased display name), NOT the id's + # normalised token ("aeg lavamat" vs "aeg-lavamat"): brand filters compare it. + d["brand_lc"] = str(d.get("brand") or "").lower() + d["model_lc"] = str(d.get("model") or "").lower() + return {"generatedAt": str(raw["generatedAt"]), "brands": brands, "devices": devices} + + # Firestore forbids directly-nested arrays, so a trace can't be stored as # [[offset, watts], ...]. On the wire we store an array of {o, w} maps and convert # to/from [[offset, watts], ...] pairs at the boundary (matches lib/trace.js). @@ -267,10 +356,14 @@ class StoreClient: self._id_token: str | None = None self._id_token_exp: float = 0.0 self._id_token_rt: str | None = None # refresh token that produced the cached id_token + self._id_token_uid: str | None = None # uid the token endpoint returned with it self._last_error: str | None = None # short reason for the last failed write, for the UI self._base = f"{self._FS}/projects/{project_id}/databases/(default)/documents" # key -> (expiry_epoch, value). Read-only catalog/config responses; see class docstring. self._read_cache: dict[str, tuple[float, Any]] = {} + # Status changes this client made (device id -> (status, UTC stamp)): the daily + # search index only learns of them at its next build. See _devices_from_index. + self._status_overrides: dict[str, tuple[str, str]] = {} # Bumped on every catalog invalidation; a read captures it before its query and only # caches the result if it is unchanged afterwards, so an in-flight read that spans an # invalidation cannot re-cache a pre-write snapshot. @@ -336,15 +429,16 @@ class StoreClient: """Drop cached brand/device catalog reads (call after a create/upload/promote write so a just-contributed or newly-approved entry appears immediately, not after the TTL). - Covers both the list queries (``brands:``/``devices:``) and the single-document + Covers the list queries (``brands:``/``devices:``), the single-document lookups behind the identity badges (``brand:``/``device:``) -- a create writes the very document those resolve, so a stale hit there would show "not in the catalog" - for an entry the user just added. + for an entry the user just added -- and the search index's ``delta:`` query, + which is where a just-created entry appears while the index predates it. """ self._cache_gen += 1 for key in [ k for k in self._read_cache - if k.startswith(("brands:", "devices:", "brand:", "device:")) + if k.startswith(("brands:", "devices:", "brand:", "device:", "delta:")) ]: self._read_cache.pop(key, None) @@ -385,12 +479,29 @@ class StoreClient: return None self._id_token = body.get("id_token") self._id_token_rt = refresh_token + self._id_token_uid = _token_uid(body) try: self._id_token_exp = now + float(body.get("expires_in", 3600)) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): self._id_token_exp = now + 3600 return self._id_token + def verified_uid(self, refresh_token: str) -> str | None: + """The uid the token endpoint returned for ``refresh_token``'s last exchange. + + Only meaningful right after ``ensure_id_token(refresh_token)`` succeeded (same + cache key, so another account's uid is never returned). Every authed write + stamps this, not the uid stored at connect: the store rules require + ``uploaderUid`` / ``createdByUid`` / the confirmation doc id to equal + ``request.auth.uid``, so a mismatched stored uid made every write 403 + (audit STORE-18). + """ + return self._id_token_uid if self._id_token_rt == refresh_token else None + + def _write_uid(self, refresh_token: str, stored_uid: str) -> str: + """The uid an authed write must carry (see ``verified_uid``).""" + return self.verified_uid(refresh_token) or stored_uid + # ── reads (public, no token) ──────────────────────────────────────────────── async def _run_query(self, sq: dict[str, Any], parent: str = "") -> list[dict[str, Any]] | None: @@ -519,6 +630,86 @@ class StoreClient: return cached if include_pending else self._approved_only(cached) return None + # ── published catalog index (audit STORE-07) ───────────────────────────────── + # + # The store's deploy builds `search-index.json` daily (brands, devices, + # profiles, as row arrays under a `fields` header). Type-wide device lists and the + # brand list come from it, plus the entries created since it was built (one + # small `createdAt` query, the same delta the website runs), so a Store search + # costs ~0 Firestore reads instead of every device of the type (473 per install + # per cache hour, silently truncated at 500). Any failure falls back to the + # direct queries below. + + _INDEX_TTL_S = 3600.0 + _INDEX_MISS_TTL_S = 300.0 + _INDEX_DELTA_LIMIT = 50 + + async def _catalog_index(self) -> dict[str, Any] | None: + hit = self._cache_get("catalog_index") + if hit is not None: + return hit or None + gen = self._cache_gen + index: dict[str, Any] | None = None + try: + async with self._sess().get(f"{STORE_WEB_ORIGIN}/search-index.json", timeout=15) as resp: + if resp.status == 200: + index = _shape_index(await resp.json(content_type=None)) + except Exception as exc: # noqa: BLE001 - fall back to the direct queries + _LOGGER.debug("Store search index unavailable: %s", exc) + if gen == self._cache_gen: + self._cache_put( + "catalog_index", index or {}, + self._INDEX_TTL_S if index else self._INDEX_MISS_TTL_S, + ) + return index + + async def _index_delta( + self, collection: str, generated_at: str, fields: tuple[str, ...] + ) -> list[dict[str, Any]]: + """Entries of ``collection`` created after the index was built (any status + the browse shows); filtered in memory by the caller.""" + key = f"delta:{collection}:{generated_at}" + return await self._cached_catalog_query(key, lambda: { + "from": [{"collectionId": collection}], + "select": {"fields": [{"fieldPath": f} for f in (*fields, "createdAt")]}, + "where": self._where([ + self._status_filter(True), + {"fieldFilter": {"field": {"fieldPath": "createdAt"}, "op": "GREATER_THAN", + "value": {"timestampValue": generated_at}}}, + ]), + "orderBy": [{"field": {"fieldPath": "createdAt"}, "direction": "DESCENDING"}], + "limit": self._INDEX_DELTA_LIMIT, + }) + + async def _devices_from_index( + self, brand: str | None, appliance_type: str | None, include_pending: bool, + page_size: int, + ) -> list[dict[str, Any]] | None: + index = await self._catalog_index() + if index is None: + return None + rows = {r["id"]: r for r in index["devices"]} + for r in await self._index_delta("devices", index["generatedAt"], _DEVICE_LIST_FIELDS): + r.setdefault("brand_lc", str(r.get("brand") or "").lower()) + r.setdefault("model_lc", str(r.get("model") or "").lower()) + rows[r["id"]] = r + # The delta only carries entries CREATED after the build, so a device this + # client just promoted (confirm_device) still reads "pending" from the index. + for did, (status, at) in self._status_overrides.items(): + if did in rows and at > index["generatedAt"]: + rows[did] = {**rows[did], "status": status} + bl = (brand or "").lower() + out = [ + r for r in rows.values() + if (include_pending or r.get("status") == "approved") + and r.get("status") in ("approved", "pending") + and (not appliance_type or r.get("applianceType") == appliance_type) + and (not bl or r.get("brand_lc") == bl) + ] + # Delta rows come straight from Firestore, uncleaned: sort through the same guard. + out.sort(key=lambda r: (-_favorite_count(r.get("favoriteCount")), str(r.get("id")))) + return out[:page_size] + 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, @@ -538,6 +729,10 @@ class StoreClient: rows = [r for r in rows if str(r.get("model_lc", "")).startswith(p)] return rows + from_index = await self._devices_from_index(brand, appliance_type, include_pending, page_size) + if from_index is not None: + return _finish(from_index) + shared = self._serve_from_superset(f"{base}:1:{page_size}", include_pending, page_size) if shared is not None: return _finish(shared) @@ -608,6 +803,20 @@ class StoreClient: dropdown" case, and truncating it would make brands past the cap unfindable. """ prefix = (q or "").strip().lower() + index = await self._catalog_index() + if index is not None: + rows = {r["id"]: r for r in index["brands"]} + for r in await self._index_delta("brands", index["generatedAt"], _BRAND_LIST_FIELDS): + r.setdefault("brand_lc", str(r.get("id") or "").lower()) + rows[r["id"]] = r + out = [ + r for r in rows.values() + if (include_pending or r.get("status") == "approved") + and r.get("status") in ("approved", "pending") + and str(r.get("brand_lc", "")).startswith(prefix) + ] + out.sort(key=lambda r: str(r.get("brand_lc", ""))) + return out[:page_size] select = {"fields": [{"fieldPath": f} for f in _BRAND_LIST_FIELDS]} order = [{"field": {"fieldPath": "brand_lc"}, "direction": "ASCENDING"}] @@ -651,10 +860,21 @@ class StoreClient: successful lookups are cached (a miss may simply mean "not contributed yet", which flips as soon as the user contributes it). """ + return (await self._fetch_doc(path, cache_key=cache_key))[0] + + async def _fetch_doc( + self, path: str, *, cache_key: str | None = None, + ) -> tuple[dict[str, Any] | None, bool]: + """``(doc, reached)``: ``_get_doc`` plus whether the store answered at all. + + ``reached`` is False only when the store could not answer (network error, + timeout, 5xx, rate limit), so a caller can tell "no such document" (403/404, + ``(None, True)``) from "offline" (``(None, False)``). + """ if cache_key is not None: cached = self._cache_get(cache_key) if cached is not None: - return cached + return cached, True # Capture the generation BEFORE the request: if an invalidation lands while this # read is in flight, caching its result would re-pin a pre-write document for the # full TTL. Same discipline as _fetch_and_cache. @@ -662,17 +882,18 @@ class StoreClient: try: async with self._sess().get(f"{self._base}/{path}", timeout=15) as resp: if resp.status != 200: - if resp.status not in (403, 404): - _LOGGER.debug("Store get %s HTTP %s", path, resp.status) - return None + if resp.status in (403, 404): + return None, True + _LOGGER.debug("Store get %s HTTP %s", path, resp.status) + return None, False doc = await resp.json() except Exception as exc: # noqa: BLE001 _LOGGER.debug("Store get %s error: %s", path, exc) - return None + return None, False out = _decode_doc(doc) if cache_key is not None and gen == self._cache_gen: self._cache_put(cache_key, out, self._CATALOG_CACHE_TTL_S) - return out + return out, True async def get_device(self, device_id: str) -> dict[str, Any] | None: return await self._get_doc(f"devices/{_seg(device_id)}", cache_key=f"device:{device_id}") @@ -726,50 +947,14 @@ class StoreClient: self._cache_put("config:site", cfg, self._CONFIG_CACHE_TTL_S) return cfg - async def _rating_agg(self, parent_path: str) -> dict[str, Any]: - """count + average over the `ratings` subcollection under ``parent_path``. - - Public (unauthenticated) aggregation -- ratings are world-readable. Returns - ``{"avg": float|None, "count": int}`` and never raises. - """ - body = {"structuredAggregationQuery": { - "structuredQuery": {"from": [{"collectionId": "ratings"}]}, - "aggregations": [ - {"alias": "cnt", "count": {}}, - # NB: the Firestore aggregation operator is `avg`, NOT `average` -- - # the wrong name 400s the whole query and silently zeroes ratings. - {"alias": "avg", "avg": {"field": {"fieldPath": "rating"}}}, - ], - }} - try: - async with self._sess().post( - f"{self._base}/{parent_path}:runAggregationQuery", - json=body, timeout=15, - ) as resp: - if resp.status != 200: - return {"avg": None, "count": 0} - rows = await resp.json() - except Exception as exc: # noqa: BLE001 - _LOGGER.debug("Store rating aggregation error (%s): %s", parent_path, exc) - return {"avg": None, "count": 0} - agg = next((r["result"]["aggregateFields"] for r in rows if isinstance(r, dict) and "result" in r), None) - if not agg: - return {"avg": None, "count": 0} - cnt = _decode(agg["cnt"]) if "cnt" in agg else 0 - avg = _decode(agg["avg"]) if ("avg" in agg and "nullValue" not in agg["avg"]) else None - return {"avg": avg if (cnt and avg is not None) else None, "count": cnt or 0} - - async def get_device_quality(self, device_id: str) -> dict[str, Any]: - """count + average of the device's 5-star quality ratings (info only).""" - return await self._rating_agg(f"devices/{_seg(device_id)}") - - async def cycle_rating(self, cycle_id: str) -> dict[str, Any]: - """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 = True, 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]] | None: """Shared programs for one catalog appliance, most-recent-first. + ``None`` when the store could not be reached, ``[]`` when the appliance really + has no shared programs: the two used to be the same ``[]`` (audit STORE-09). + ``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 @@ -788,13 +973,16 @@ class StoreClient: "orderBy": [{"field": {"fieldPath": "createdAt"}, "direction": "DESCENDING"}], "limit": page_size, } - return await self._run_query(sq) or [] + return await self._run_query(sq) async def device_profiles(self, brand: str, model: str, appliance_type: str) -> dict[str, Any]: """Resolve the store deviceId from brand/model/type and return its profiles - (approved + the caller's own pending), for the Share dialog's profile picker.""" + (approved + the caller's own pending), for the Share dialog's profile picker. + Carries ``error`` when the store could not be reached (audit STORE-09).""" dev_id = device_id(appliance_type, brand, model) items = await self.get_profiles(dev_id, include_pending=True) + if items is None: + return {"device_id": dev_id, "items": [], "error": STORE_UNREACHABLE} return {"device_id": dev_id, "items": items} async def get_device_bundle(self, dev_id: str, include_pending: bool = True) -> dict[str, Any]: @@ -803,45 +991,61 @@ class StoreClient: ``cycles`` (hydrated by get_cycles). One device GET + one profiles query + one cycles query per profile. Never raises. - Star ratings are deliberately skipped here. They are browse-only decoration and - the adopt path (``StoreBridge.download_device``) never reads them, but they cost - one aggregation request *per cycle*: a 15-profile device turned a ~17-request - download into ~60, all on the user's critical path. + Failures are reported, never flattened into an empty bundle (audit STORE-09): + ``error`` when the device doc or the profile list could not be read (nothing is + known about the setup), and per profile ``cycles_unavailable`` plus a top-level + ``failed_profiles`` count when that program's cycle query failed. """ - device = await self.get_device(dev_id) or {} - settings = device.get("settings") if isinstance(device.get("settings"), dict) else {} + unreachable = {"device_id": dev_id, "settings": {}, "profiles": [], "error": STORE_UNREACHABLE} + device, reached = await self._fetch_doc( + f"devices/{_seg(dev_id)}", cache_key=f"device:{dev_id}", + ) + if not reached: + return unreachable profiles = await self.get_profiles(dev_id, include_pending=include_pending) + if profiles is None: + return unreachable + device = device or {} + settings = device.get("settings") if isinstance(device.get("settings"), dict) else {} # Bound the per-profile fan-out: an unbounded gather over a device with many # profiles could burst hundreds of concurrent requests and trip the store's rate # limiter. A shared semaphore caps how many profiles hydrate at once. sem = asyncio.Semaphore(_BUNDLE_HYDRATE_LIMIT) - async def _cycles_for(p: dict[str, Any]) -> list[dict[str, Any]]: + async def _cycles_for(p: dict[str, Any]) -> list[dict[str, Any]] | None: pid = p.get("id") if not pid: return [] async with sem: - return await self.get_cycles(pid, include_pending=include_pending, include_ratings=False) + return await self.get_cycles(pid, include_pending=include_pending) # Fetch profiles' cycles concurrently (bounded) rather than one at a time. cycle_lists = await asyncio.gather(*(_cycles_for(p) for p in profiles)) + failed = 0 for p, cycles in zip(profiles, cycle_lists): - p["cycles"] = cycles - return {"device_id": dev_id, "settings": settings, "profiles": profiles} + if cycles is None: + failed += 1 + p["cycles_unavailable"] = True + p["cycles"] = cycles or [] + bundle: dict[str, Any] = {"device_id": dev_id, "settings": settings, "profiles": profiles} + if failed: + bundle["failed_profiles"] = failed + return bundle async def get_cycles( self, prof_id: str, include_pending: bool = True, page_size: int = 50, - *, include_ratings: bool = True, - ) -> list[dict[str, Any]]: - """Reference cycles for a profile, most-recent-first. + ) -> list[dict[str, Any]] | None: + """Reference cycles for a profile, most-recent-first; ``None`` when the store + could not be reached (``[]`` = genuinely none shared; audit STORE-09). ``include_pending`` (default True) also returns still-awaiting-approval recordings so they can be browsed/imported before the community votes them in (they are publicly readable, shown with an "awaiting approval" tag). - Each cycle gets a ``rating`` = ``{"avg", "count"}`` summary attached, unless - ``include_ratings`` is False -- one extra aggregation request per cycle that - only the browse UI displays (see get_device_bundle). + Each cycle carries a ``rating`` = ``{"avg", "count"}`` summary read from the + denormalized ``ratingSum`` / ``ratingCount`` the store keeps on the cycle doc, + so it costs no extra request. Until 0.5.8 it was one aggregation query per + listed cycle (audit STORE-15); a cycle without the fields has no ratings. """ sq = { "from": [{"collectionId": "cycles"}], @@ -852,23 +1056,12 @@ class StoreClient: "orderBy": [{"field": {"fieldPath": "createdAt"}, "direction": "DESCENDING"}], "limit": page_size, } - cycles = [self._with_decoded_trace(c) for c in (await self._run_query(sq) or [])] - if not include_ratings: - return cycles - # Attach each cycle's 5-star rating summary (info-only; the aggregation lives - # in a subcollection so it can't ride the list query). Bound concurrency with - # a semaphore so a large page can't fan out into dozens of simultaneous - # aggregation requests. - sem = asyncio.Semaphore(_RATING_FANOUT_LIMIT) - async def _rate(cyc: dict[str, Any]) -> dict[str, Any]: - cid = cyc.get("id") - if not cid: - return {"avg": None, "count": 0} - async with sem: - return await self.cycle_rating(cid) - summaries = await asyncio.gather(*(_rate(c) for c in cycles), return_exceptions=True) - for cyc, summary in zip(cycles, summaries): - cyc["rating"] = summary if isinstance(summary, dict) else {"avg": None, "count": 0} + rows = await self._run_query(sq) + if rows is None: + return None + cycles = [self._with_decoded_trace(c) for c in rows] + for cyc in cycles: + cyc["rating"] = _rating_from_doc(cyc) return cycles async def get_cycle(self, cycle_id: str) -> dict[str, Any] | None: @@ -967,6 +1160,7 @@ class StoreClient: token = await self.ensure_id_token(refresh_token) if not token: return _out(None, False) + uid = self._write_uid(refresh_token, uid) # Preserve the documented never-raise contract: malformed metadata/points must # return a failure marker (with _last_error set), not propagate an exception to @@ -989,7 +1183,7 @@ class StoreClient: program = required["program"] try: interval = float(meta.get("sampleIntervalSec") or 0) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): interval = 0.0 if appliance not in _APPLIANCE_TYPES: _LOGGER.warning("Store upload: invalid applianceType %r", appliance) @@ -1002,7 +1196,7 @@ class StoreClient: qc_code = qc if qc in (1, 2, 3) else 3 try: pts = [[float(p[0]), float(p[1])] for p in (points or [])[:10000] if len(p) >= 2] - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): self._last_error = "malformed trace points" return _out(None, False) if len(pts) < 2: @@ -1028,11 +1222,7 @@ class StoreClient: # Defense in depth at the store boundary: keep only allow-listed, numeric # settings (never trust the caller to have filtered) so nothing arbitrary is # ever written to the shared device doc. - filtered = { - str(k): v for k, v in settings.items() - if k in SHAREABLE_SETTING_KEYS - and isinstance(v, (int, float)) and not isinstance(v, bool) - } + filtered = sanitize_shared_settings(settings) if filtered: device_fields["settings"] = filtered ok = ok and await self._commit_create(token, f"devices/{d_id}", device_fields) @@ -1052,7 +1242,7 @@ class StoreClient: # 0.0 (which would ship a bogus zero-length phase to the catalog). try: return {"name": str(p.get("name", "")), "start": float(p["start"]), "end": float(p["end"])} - except (KeyError, TypeError, ValueError): + except (KeyError, TypeError, ValueError, OverflowError): return None valid_phases = [ vp for vp in (_valid_phase(p) for p in phases if isinstance(p, dict)) if vp is not None @@ -1083,9 +1273,10 @@ class StoreClient: cyc_ok, created = await self._commit_create_ex(token, f"cycles/{cyc_id}", cycle_fields) if not cyc_ok: return _out(None, False) - # NB: cycle/profile counts are CALCULATED on the store (COUNT aggregation over - # approved+pending), not maintained as a running total here -- a best-effort - # increment that a rule denied is what left the browse counters stuck at 0. + # NB: the integration never maintains cycle/profile counts: the website + # increments them in the same batched write that creates the item (the store + # rules only allow +1 alongside a new child). A best-effort increment from + # here that a rule denied is what once left the browse counters stuck at 0. return _out(cyc_id, created) async def upload_device_bundle( @@ -1167,6 +1358,7 @@ class StoreClient: token = await self.ensure_id_token(refresh_token) if not token: return None + uid = self._write_uid(refresh_token, uid) dev_path = self._doc_path(f"devices/{device_id}") conf_path = self._doc_path(f"devices/{device_id}/confirmations/{uid}") writes = [ @@ -1198,7 +1390,7 @@ class StoreClient: status = dev.get("status") try: threshold = int((await self.get_config()).get("confirmThreshold") or 5) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): threshold = 5 if status == "pending" and count >= threshold: promote = [{ @@ -1208,26 +1400,52 @@ class StoreClient: }] if (await self._commit(token, promote))[0]: status = "approved" + self._status_overrides[device_id] = ( + status, time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime()) + "Z" + ) # Promotion changes catalog visibility (pending -> approved); drop cached # listings so the newly-approved device shows in approved-only searches now. self._invalidate_catalog_cache() return {"confirmed": True, "confirmCount": count, "status": status} async def rate_device(self, refresh_token: str, uid: str, device_id: str, rating: int) -> bool: - """Set this user's 5-star quality rating for a device (info only).""" + """Set this user's 5-star quality rating for a device (info only). + + The device's denormalized ``ratingSum`` / ``ratingCount`` move in the SAME + batch, as the website's ``rateDevice`` does (the store rules tie the two with + ``existsAfter`` / ``getAfter``). Writing only the rating doc, as this did until + 0.5.8, left every integration rating out of the totals the store shows (audit + STORE-15). The prior rating is read uncached so an edit shifts the sum by the + difference instead of counting a second rating.""" if rating not in (1, 2, 3, 4, 5): return False token = await self.ensure_id_token(refresh_token) if not token: return False + uid = self._write_uid(refresh_token, uid) + prev_doc = await self._get_doc(f"devices/{_seg(device_id)}/ratings/{_seg(uid)}") + prev = prev_doc.get("rating") if isinstance(prev_doc, dict) else None path = self._doc_path(f"devices/{device_id}/ratings/{uid}") - writes = [{ + dev_path = self._doc_path(f"devices/{device_id}") + writes: list[dict[str, Any]] = [{ "update": {"name": path, "fields": {"uid": _encode(uid), "rating": _encode(rating)}}, "updateTransforms": [{"fieldPath": "updatedAt", "setToServerValue": "REQUEST_TIME"}], }] + if prev not in (1, 2, 3, 4, 5): + writes.append({"transform": {"document": dev_path, "fieldTransforms": [ + {"fieldPath": "ratingCount", "increment": _encode(1)}, + {"fieldPath": "ratingSum", "increment": _encode(rating)}, + ]}}) + elif prev != rating: + writes.append({"transform": {"document": dev_path, "fieldTransforms": [ + {"fieldPath": "ratingSum", "increment": _encode(rating - prev)}, + ]}}) ok, body = await self._commit(token, writes) if not ok: _LOGGER.warning("Store rate_device failed: %s", body[:200]) + else: + # The device doc's totals just changed; drop the cached point read. + self._invalidate_catalog_cache() return ok async def bump_downloads(self, cycle_ids: list[str]) -> None: diff --git a/custom_components/ha_washdata/strings.json b/custom_components/ha_washdata/strings.json index 6f81ed1d..c9270282 100644 --- a/custom_components/ha_washdata/strings.json +++ b/custom_components/ha_washdata/strings.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "WashData Setup", - "description": "Configure your washing machine or other appliance.\n\nPower sensor is required.\n\nAfter setup, open the WashData panel from the sidebar (or go to [/ha-washdata](/ha-washdata)) to view cycles, tune detection and manage profiles.", + "description": "Set up a washing machine or other appliance. A power sensor is required.\n\nAfter setup, open the WashData panel from the sidebar ([/ha-washdata](/ha-washdata)) to see cycles, profiles and all settings.", "data": { "name": "Device Name", "device_type": "Device Type", @@ -12,14 +12,14 @@ }, "data_description": { "name": "A friendly name for this device (e.g., 'Washing Machine', 'Dishwasher').", - "device_type": "What type of appliance is this? Helps tailor detection and labeling.", - "power_sensor": "The sensor entity that reports real-time power consumption (in watts) from your smart plug.", - "min_power": "Power readings above this threshold (in watts) indicate the appliance is running. Start with 2W for most devices." + "device_type": "Sets detection defaults for this kind of appliance.", + "power_sensor": "The smart plug sensor that reports live power in watts.", + "min_power": "Readings above this (in watts) mean the appliance is running. 2 W suits most devices." } }, "reconfigure": { "title": "Reconfigure WashData", - "description": "Update the device name, appliance type, or power sensor.\n\nThe WashData panel (sidebar, or [/ha-washdata](/ha-washdata)) is where detection, cycles, profiles and notifications are managed.", + "description": "Change the device name, appliance type or power sensor.\n\nEverything else is in the WashData panel (sidebar, or [/ha-washdata](/ha-washdata)).", "data": { "name": "Device Name", "device_type": "Device Type", @@ -29,8 +29,6 @@ } }, "error": { - "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication", "unknown": "Unexpected error", "invalid_power": "Power threshold must be greater than 0" }, @@ -43,7 +41,7 @@ "step": { "init": { "title": "WashData Settings", - "description": "Rename the device or change its core hardware settings. All detection, matching and notification settings are in the [WashData panel](/ha-washdata) (also reachable from the sidebar).", + "description": "Rename the device or change its core settings. Everything else is in the [WashData panel](/ha-washdata) in the sidebar.", "data": { "name": "Device Name", "device_type": "Device Type", @@ -93,7 +91,7 @@ }, "profile_name": { "name": "Profile Name", - "description": "The name of an existing profile (create profiles in Manage Profiles menu). Leave blank to remove label." + "description": "An existing profile name (create profiles in the WashData panel). Leave blank to remove the label." } } }, @@ -143,13 +141,13 @@ }, "confidence_threshold": { "name": "Confidence Threshold", - "description": "Minimum match confidence (0.50-0.95) to apply labels." + "description": "Minimum match confidence (0.50-0.95) to apply labels. Leave empty to use the device's Auto-Label Confidence setting." } } }, "export_config": { "name": "Export Config", - "description": "Export this device's profiles, cycles, and settings to a JSON file (per device).", + "description": "Export this device's profiles, cycles and settings to a JSON file.", "fields": { "device_id": { "name": "Device", @@ -177,7 +175,7 @@ }, "submit_cycle_feedback": { "name": "Submit Cycle Feedback", - "description": "Confirm or correct an auto-detected program after a completed cycle. Provide either `entry_id` (advanced) or `device_id` (recommended).", + "description": "Confirm or correct the detected program of a finished cycle. Give `device_id` (recommended) or `entry_id`.", "fields": { "device_id": { "name": "Device", @@ -283,7 +281,7 @@ }, "mark_unloaded": { "name": "Mark Unloaded", - "description": "Confirm the finished load has been taken out of a WashData device, clearing the Clean state and any unload reminder.", + "description": "Confirm the finished load was taken out. Clears the Clean state and any unload reminder.", "fields": { "device_id": { "name": "Device", @@ -299,6 +297,32 @@ }, "match_ambiguity": { "name": "Match Ambiguity" + }, + "maintenance_due": { + "name": "Maintenance due", + "state": { + "on": "Due", + "off": "Not due" + }, + "state_attributes": { + "due_task_ids": { + "name": "Due task IDs" + }, + "due_tasks": { + "name": "Due tasks", + "state": { + "descale": "Descale", + "filter_clean": "Clean filter", + "drum_clean": "Clean drum", + "bearing_service": "Bearing service", + "other": "Other", + "salt": "Refill salt", + "rinse_aid": "Refill rinse aid", + "lint_filter": "Clean lint filter", + "condenser_clean": "Clean condenser" + } + } + } } }, "sensor": { @@ -316,10 +340,18 @@ "anti_wrinkle": "Anti-Wrinkle", "interrupted": "Interrupted", "force_stopped": "Force Stopped", - "rinse": "Rinse", "unknown": "Unknown", "clean": "Clean", "delay_wait": "Delay Start" + }, + "state_attributes": { + "cycle_anomaly": { + "name": "Cycle anomaly", + "state": { + "overrun": "Running long", + "stalled": "Stalled" + } + } } }, "washer_program": { diff --git a/custom_components/ha_washdata/suggestion_engine.py b/custom_components/ha_washdata/suggestion_engine.py index 623443ea..ff4162fa 100644 --- a/custom_components/ha_washdata/suggestion_engine.py +++ b/custom_components/ha_washdata/suggestion_engine.py @@ -23,12 +23,17 @@ import logging import math from decimal import ROUND_CEILING, ROUND_FLOOR, Decimal from datetime import datetime -from typing import Any, TYPE_CHECKING, cast +from typing import Any, Callable, Coroutine, TYPE_CHECKING, cast import numpy as np from homeassistant.core import HomeAssistant, callback from .const import ( + DEFAULT_NO_UPDATE_ACTIVE_TIMEOUT_BY_DEVICE, + DEFAULT_NO_UPDATE_ACTIVE_TIMEOUT, + DEFAULT_PROFILE_MATCH_MAX_DURATION_RATIO, + DEFAULT_PROFILE_MATCH_MIN_DURATION_RATIO, + resolve_off_delay_default, TerminationReason, CONF_WATCHDOG_INTERVAL, CONF_NO_UPDATE_ACTIVE_TIMEOUT, @@ -36,25 +41,14 @@ from .const import ( CONF_PROFILE_MATCH_INTERVAL, CONF_PROFILE_MATCH_MAX_DURATION_RATIO, CONF_PROFILE_MATCH_MIN_DURATION_RATIO, - CONF_DURATION_TOLERANCE, - CONF_PROFILE_DURATION_TOLERANCE, CONF_START_THRESHOLD_W, CONF_STOP_THRESHOLD_W, CONF_POWER_OFF_THRESHOLD_W, CONF_END_ENERGY_THRESHOLD, CONF_MIN_OFF_GAP, CONF_MIN_POWER, - CONF_SAMPLING_INTERVAL, - CONF_SMOOTHING_WINDOW, CONF_COMPLETION_MIN_SECONDS, - CONF_AUTO_LABEL_CONFIDENCE, - CONF_LEARNING_CONFIDENCE, - CONF_PROFILE_MATCH_THRESHOLD, - CONF_PROFILE_UNMATCH_THRESHOLD, - CONF_END_REPEAT_COUNT, - CONF_START_DURATION_THRESHOLD, CONF_ANTI_WRINKLE_MAX_POWER, - CONF_ANTI_WRINKLE_EXIT_POWER, CONF_ANTI_WRINKLE_ENABLED, DEFAULT_ANTI_WRINKLE_ENABLED, DEFAULT_ANTI_WRINKLE_MAX_POWER, @@ -69,12 +63,12 @@ from .const import ( DEFAULT_OFF_DELAY, 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 .signal_processing import longest_resumed_pause_s from .time_utils import power_data_to_offsets # ─── Clean-cycle selection ──────────────────────────────────────────────────── @@ -325,7 +319,7 @@ def _cycle_readings(cycle: dict[str, Any]) -> list[tuple[float, float]]: cast(list[list[float] | tuple[Any, float]], raw), start_iso ) return [(float(o), float(p)) for o, p in pairs] - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): return [] @@ -469,7 +463,7 @@ def select_clean_cycles( try: duration = float(c.get("duration") or 0.0) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): duration = 0.0 readings = _cycle_readings(c) @@ -647,6 +641,162 @@ def detect_standby_above_stop( return None +# Self-correcting stop threshold (#458). When `detect_standby_above_stop` shows the +# appliance idling ABOVE the stop threshold, no cycle can end on its own, and the +# batch/single-cycle anchor (`0.8 x p05 lowest active`) lands at exactly 0.8 x that +# standby draw once standby-level samples sit inside the stored cycles - which is +# how #458 (2.2 W -> 1.76 W) and #445 (3.2 W -> 2.56 W) were configured into it. The +# floor is the standby level plus a margin for plug jitter: any single reading at or +# above the threshold resets the end timer. +STANDBY_FLOOR_RATIO = 1.25 +STANDBY_FLOOR_MIN_MARGIN_W = 0.2 +# ...and it is only proposed when the appliance's own clean history says it cannot +# end a cycle early: no paused stretch below the floor that power later resumed +# from may reach this fraction of the off delay (the detector's time-below-threshold +# has to reach the whole off delay before a fallback finish). Measured with the +# floor at 1.25 x standby: real washers' in-cycle low plateaus sit at 1.8-2x their +# standby, so they clear it, while the #445 Miele - whose 3.4 W dips between tumble +# bursts ARE its standby level - pauses 110 s under a 180 s off delay and is left +# with the advisory alone. Too few clean cycles to check means no proposal. +STANDBY_FLOOR_MAX_PAUSE_FRAC = 0.5 +STANDBY_FLOOR_MIN_CLEAN_CYCLES = 5 + + +def standby_stop_floor( + cycles: list[dict[str, Any]], stop_threshold_w: float, off_delay_s: float +) -> dict[str, Any] | None: + """The lowest stop threshold this appliance's standby allows, if it needs one. + + None unless ``detect_standby_above_stop`` fires on ``cycles`` (which must + include the force- and user-stopped ones: on an appliance with this fault they + are often the only cycles there are). Otherwise:: + + idle_w the standby level the advisory measured + floor_w max(idle * STANDBY_FLOOR_RATIO, idle + STANDBY_FLOOR_MIN_MARGIN_W) + safe whether the clean history allows it (see the constants) + clean_cycles how many clean traced cycles were checked + longest_pause_s the longest resumed in-cycle pause below ``floor_w`` + + Pure statistics, executor-safe, never raises. + """ + try: + adv = detect_standby_above_stop(cycles, stop_threshold_w) + if adv is None: + return None + idle = float(adv["idle_w"]) + floor = round(max(idle * STANDBY_FLOOR_RATIO, idle + STANDBY_FLOOR_MIN_MARGIN_W), 2) + clean, _excl = select_clean_cycles(cycles, stop_threshold_w=stop_threshold_w) + checked = 0 + longest = 0.0 + for cycle in clean: + points = _cycle_readings(cycle) + if len(points) < 5: + continue + checked += 1 + longest = max(longest, longest_resumed_pause_s(points, floor)) + safe = ( + checked >= STANDBY_FLOOR_MIN_CLEAN_CYCLES + and off_delay_s > 0 + and longest < STANDBY_FLOOR_MAX_PAUSE_FRAC * float(off_delay_s) + ) + return { + "idle_w": idle, + "floor_w": floor, + "safe": bool(safe), + "clean_cycles": checked, + "longest_pause_s": round(longest, 1), + } + except Exception: # noqa: BLE001 - a statistic must never break a suggestion pass + return None + + +def apply_standby_floor( + suggestions: dict[str, Any], floor: dict[str, Any] | None +) -> dict[str, Any]: + """Keep stop/start suggestions out of the standby band (#458). Mutates and returns. + + With a ``safe`` floor, a stop suggestion is raised to it and a start suggestion + to ``STANDBY_FLOOR_RATIO`` above it, so the reconciler's start-is-primary rule + cannot pull the stop back under. Without one, a stop or start suggestion at or + below the standby level is dropped: it is provably wrong (no cycle could ever + end), and the advisory already says so. + """ + if not floor: + return suggestions + idle = float(floor["idle_w"]) + stop_entry = suggestions.get(CONF_STOP_THRESHOLD_W) + start_entry = suggestions.get(CONF_START_THRESHOLD_W) + if floor.get("safe"): + stop_min = float(floor["floor_w"]) + start_min = round(stop_min * STANDBY_FLOOR_RATIO, 2) + for key, minimum in ((CONF_STOP_THRESHOLD_W, stop_min), (CONF_START_THRESHOLD_W, start_min)): + entry = suggestions.get(key) + if isinstance(entry, dict) and _num(entry.get("value")) is not None and _num(entry.get("value")) < minimum: + suggestions[key] = _standby_floor_entry(minimum, floor) + return suggestions + for key, entry in ((CONF_STOP_THRESHOLD_W, stop_entry), (CONF_START_THRESHOLD_W, start_entry)): + if isinstance(entry, dict) and (_num(entry.get("value")) or 0.0) <= idle: + suggestions.pop(key, None) + return suggestions + + +def resting_level_w( + cycles: list[list[tuple[float, float]]], stop_threshold_w: float +) -> float | None: + """Where the appliance rests while ``stop_threshold_w`` reads it as quiet (#455). + + Per cycle: the time-weighted median of the readings below the threshold, each + held until the next reading (an outage-sized gap carries no weight). That is + the level the end gates actually time - the drum stopped between tumbles, a + dishwasher's passive drying, the draw a Smart Termination tail kept. Device + level: the median over the cycles that show one, None below + ``STANDBY_FLOOR_MIN_CLEAN_CYCLES`` of them. Pure; never raises. + """ + try: + max_gap_s = _MAX_PAUSE_GAP_H * 3600 + levels: list[float] = [] + for points in cycles: + vals: list[float] = [] + weights: list[float] = [] + for (t0, p0), (t1, _p1) in zip(points, points[1:]): + dt = float(t1) - float(t0) + if p0 < stop_threshold_w and 0 < dt <= max_gap_s: + vals.append(float(p0)) + weights.append(dt) + if not weights: + continue + order = np.argsort(vals) + cum = np.cumsum(np.asarray(weights)[order]) + levels.append(float(np.asarray(vals)[order][np.searchsorted(cum, cum[-1] / 2.0)])) + if len(levels) < STANDBY_FLOOR_MIN_CLEAN_CYCLES: + return None + return float(np.median(levels)) + except Exception: # noqa: BLE001 - a statistic must never break a suggestion pass + return None + + +def _standby_floor_entry(value: float, floor: dict[str, Any]) -> dict[str, Any]: + idle = float(floor["idle_w"]) + return { + "value": round(value, 2), + "reason": ( + f"Every recent cycle was still drawing {idle:.1f}W when it ended, so a " + f"threshold below that can never see the appliance switch off. Kept above " + f"it; the {int(floor.get('clean_cycles') or 0)} clean cycles on record " + f"never paused under it for more than {float(floor.get('longest_pause_s') or 0):.0f}s." + ), + "reason_key": "suggestion.reason.standby_floor", + "reason_params": { + "idle": f"{idle:.1f}", + "cycles": int(floor.get("clean_cycles") or 0), + "pause": f"{float(floor.get('longest_pause_s') or 0):.0f}", + }, + # Corrects a setting no cycle can finish under, so it is not held back by + # the post-apply cooldown (learning._apply_suggestions_and_notify). + "corrective": True, + } + + def _format_exclusions(excluded: dict[str, int]) -> str: """English exclusion note for the suggestion ``reason`` fallback string. @@ -725,7 +875,7 @@ def reconcile_suggestions( in ``out`` (live-vs-live conflicts are the frontend's responsibility). """ out: dict[str, Any] = {k: dict(v) if isinstance(v, dict) else v for k, v in suggestions.items()} - # Track which keys the engine originally proposed — used for direction logic. + # Track which keys the engine originally proposed - used for direction logic. original_keys: frozenset[str] = frozenset( k for k, v in out.items() if isinstance(v, dict) and v.get("value") is not None ) @@ -773,7 +923,7 @@ def reconcile_suggestions( base = entry.get("reason", "") entry["reason"] = f"{base} Adjusted to {rounded} for consistency with {why}.".strip() # The composed English reason now differs from the base suggestion's - # localization key, so drop the sidecars — the panel falls back to the + # localization key, so drop the sidecars - the panel falls back to the # (updated) English ``reason``. Reconcile-composed reasons embed both a # nested base reason and a "why" fragment, which the flat single-key # _t() mechanism cannot recompose; leaving English here is correct. @@ -801,7 +951,12 @@ def reconcile_suggestions( stop = eff(CONF_STOP_THRESHOLD_W) if start is not None and stop is not None and start <= stop and in_out(CONF_START_THRESHOLD_W, CONF_STOP_THRESHOLD_W): if is_original(CONF_START_THRESHOLD_W): - adjust(CONF_STOP_THRESHOLD_W, round(start * 0.8, 1), "the start threshold") + if round(start * 0.8, 1) < start: + adjust(CONF_STOP_THRESHOLD_W, round(start * 0.8, 1), "the start threshold") + else: + # Item 515: at start <= 0.2 W the 0.1 W rounding lands back on + # start, which left the pair inverted; floor at 0.01 W instead. + adjust(CONF_STOP_THRESHOLD_W, start * 0.8, "the start threshold", round_dir="down") else: adjust(CONF_START_THRESHOLD_W, round(max(stop + 0.5, stop * 1.25), 1), "the stop threshold") stop = eff(CONF_STOP_THRESHOLD_W) @@ -825,66 +980,19 @@ def reconcile_suggestions( if off_delay is not None and min_gap is not None and min_gap < off_delay and in_out(CONF_MIN_OFF_GAP, CONF_OFF_DELAY): adjust(CONF_MIN_OFF_GAP, off_delay, "the off delay") - # ── Rule 3a: watchdog_interval >= 2 × sampling_interval ─────────────── - sampling = eff(CONF_SAMPLING_INTERVAL) + # (Rule 3a, watchdog >= 2 x sampling_interval, is gone with the + # sampling_interval suggestion - audit SUGGEST-04: chained to a ratcheting + # throttle it lifted the watchdog with it.) watchdog = eff(CONF_WATCHDOG_INTERVAL) - if sampling is not None and watchdog is not None and watchdog < 2.0 * sampling and in_out(CONF_SAMPLING_INTERVAL, CONF_WATCHDOG_INTERVAL): - adjust(CONF_WATCHDOG_INTERVAL, 2.0 * sampling + 1.0, "the sampling interval") - watchdog = eff(CONF_WATCHDOG_INTERVAL) # ── Rule 3b: no_update_active_timeout > watchdog_interval ───────────── timeout = eff(CONF_NO_UPDATE_ACTIVE_TIMEOUT) if watchdog is not None and timeout is not None and timeout <= watchdog and in_out(CONF_WATCHDOG_INTERVAL, CONF_NO_UPDATE_ACTIVE_TIMEOUT): adjust(CONF_NO_UPDATE_ACTIVE_TIMEOUT, round(watchdog * 2.0, 1), "the watchdog interval") - # ── Rule 4: start_duration_threshold >= sampling_interval ───────────── - start_dur = eff(CONF_START_DURATION_THRESHOLD) - if sampling is not None and start_dur is not None and start_dur < sampling and in_out(CONF_SAMPLING_INTERVAL, CONF_START_DURATION_THRESHOLD): - adjust(CONF_START_DURATION_THRESHOLD, sampling, "the sampling interval") - - # ── Rule 5: match_threshold <= learning_confidence, match <= auto_label ─ - # The confidence ladder is unmatch < match < learning < auto_label (#396): - # the verify-band floor (learning) sits AT OR ABOVE the live match-trust - # gate (match), which itself sits at or below the auto-label ceiling. - # Reconcile match<=auto first so a later fix cannot re-break the ordering. - match_thr = eff(CONF_PROFILE_MATCH_THRESHOLD) - auto = eff(CONF_AUTO_LABEL_CONFIDENCE) - if match_thr is not None and auto is not None and match_thr > auto and in_out(CONF_PROFILE_MATCH_THRESHOLD, CONF_AUTO_LABEL_CONFIDENCE): - # Anchor on whichever the engine actually proposed, like every other - # two-sided rule. match_threshold now drives detection (it is - # CycleDetectorConfig.match_confidence_threshold), so when the engine - # deliberately RAISED it, lift the auto-label ceiling to keep it rather - # than silently undoing the raise; only cascade it downward when it was - # not the proposed key. - if is_original(CONF_PROFILE_MATCH_THRESHOLD): - # raise the ceiling to (>=) match: ceil so 2-dp rounding can't drop it back under - adjust(CONF_AUTO_LABEL_CONFIDENCE, match_thr, "the profile match threshold", "up") - auto = eff(CONF_AUTO_LABEL_CONFIDENCE) - else: - # lower match to (<=) auto: floor so it stays at/below the ceiling - adjust(CONF_PROFILE_MATCH_THRESHOLD, auto, "the auto-label confidence", "down") - match_thr = eff(CONF_PROFILE_MATCH_THRESHOLD) - learn = eff(CONF_LEARNING_CONFIDENCE) - if learn is not None and match_thr is not None and learn < match_thr and in_out(CONF_LEARNING_CONFIDENCE, CONF_PROFILE_MATCH_THRESHOLD): - # raise learning to (>=) match: ceil - adjust(CONF_LEARNING_CONFIDENCE, match_thr, "the profile match threshold", "up") - # Top of the ladder: learning <= auto. `_add_confidence_suggestions` derives the - # two independently (learning from p05 of manual labels, auto from p15 of - # uncorrected auto-labels), so a device with few high-confidence manual labels can - # yield learning > auto. Cascade-RAISE the auto ceiling to the verify floor (the - # conservative direction — never lower the verify band); keeps the full declared - # ordering intact instead of enforcing only its middle two rungs. - learn = eff(CONF_LEARNING_CONFIDENCE) - auto = eff(CONF_AUTO_LABEL_CONFIDENCE) - if learn is not None and auto is not None and learn > auto and in_out(CONF_LEARNING_CONFIDENCE, CONF_AUTO_LABEL_CONFIDENCE): - # raise the auto ceiling to (>=) learning: ceil - adjust(CONF_AUTO_LABEL_CONFIDENCE, learn, "the learning confidence", "up") - - # ── Rule 6: profile_unmatch_threshold < profile_match_threshold ──────── - unmatch = eff(CONF_PROFILE_UNMATCH_THRESHOLD) - match_thr2 = eff(CONF_PROFILE_MATCH_THRESHOLD) - if unmatch is not None and match_thr2 is not None and unmatch >= match_thr2 and in_out(CONF_PROFILE_UNMATCH_THRESHOLD, CONF_PROFILE_MATCH_THRESHOLD): - adjust(CONF_PROFILE_UNMATCH_THRESHOLD, round(match_thr2 - 0.05, 2), "the profile match threshold") + # (Rules 4-6 - start debounce vs sampling interval, the confidence ladder, + # unmatch < match - are gone with the suggestions they reconciled; audit + # SUGGEST-01/04. The panel's own conflict checks still guard hand edits.) # ── Rule 7: power_off_threshold_w < stop_threshold_w (when > 0) ─────── pot = eff(CONF_POWER_OFF_THRESHOLD_W) @@ -892,15 +1000,14 @@ def reconcile_suggestions( if pot is not None and pot > 0.0 and stop_eff is not None and pot >= stop_eff and in_out(CONF_POWER_OFF_THRESHOLD_W, CONF_STOP_THRESHOLD_W): adjust(CONF_POWER_OFF_THRESHOLD_W, round(stop_eff * 0.6, 1), "the stop threshold") - # ── Rule 8: anti_wrinkle_exit_power < stop_threshold_w ──────────────── + # (Rule 8, anti_wrinkle_exit_power < stop_threshold_w, is gone: the detector + # counts quiet below max(exit power, stop threshold), so the rule moved the + # exit power to exactly where it has no effect and erased a deliberate raise. + # Never fired on the corpus, suggestion_loop_eval.py; #285 #296 #325.) # Anti-wrinkle only applies to washing machines, dryers, and washer-dryer - # combos; skip the constraint for all other device types. + # combos; skip its constraints for all other device types. _dt = current.get(CONF_DEVICE_TYPE) _aw_eligible = _dt is None or _dt in {DEVICE_TYPE_WASHING_MACHINE, DEVICE_TYPE_DRYER, DEVICE_TYPE_WASHER_DRYER} - if _aw_eligible: - aw_exit = eff(CONF_ANTI_WRINKLE_EXIT_POWER) - if stop_eff is not None and aw_exit is not None and aw_exit >= stop_eff and in_out(CONF_ANTI_WRINKLE_EXIT_POWER, CONF_STOP_THRESHOLD_W): - adjust(CONF_ANTI_WRINKLE_EXIT_POWER, round(stop_eff * 0.4, 1), "the stop threshold") # ── Rule 9: anti_wrinkle_max_power > start_threshold_w ──────────────── if _aw_eligible: @@ -984,14 +1091,22 @@ class SuggestionEngine: entry_id: str, profile_store: "ProfileStore", device_type: str | None = None, + spawn: Callable[[Coroutine[Any, Any, Any]], Any] | None = None, ) -> None: - """Initialize the suggestion engine.""" + """Initialize the suggestion engine. + + ``spawn`` starts the store save :meth:`apply_suggestions` schedules. The + learning manager passes the manager's ``_spawn_tracked`` so an unload can + cancel it (audit MANAGER-13); without it a plain ``hass.async_create_task`` + is used. + """ self.hass = hass self.entry_id = entry_id self.profile_store = profile_store self.device_type = device_type - # Loop-affine config snapshot, refreshed by refresh_options_snapshot() - # immediately before an executor dispatch. See _entry_options(). + self._spawn_fn = spawn + # Loop-affine config snapshot, set by for_job() on a per-job copy + # before an executor dispatch. See _entry_options(). self._options_snapshot: dict[str, Any] | None = None @callback @@ -1030,11 +1145,10 @@ class SuggestionEngine: # derivation for a publish-on-change sensor that skips at most one # sample. The old 3x multiple polled ~6x slower than the sensor updates # and delayed end detection for no safety benefit. - # Floor at 2 x median + 1 so the suggestion pre-satisfies reconciler - # Rule 3a (watchdog >= 2 x sampling_interval) when CONF_SAMPLING_INTERVAL - # is suggested from the same median_dt. Without this, a regular sensor - # (p95 ≈ median) would produce ceil(p95)+1 which Rule 3a then silently - # overwrites, leaving a stored value whose reason text no longer matches. + # Floor at 2 x median + 1: a publish-on-change sensor can skip one sample, + # so the watchdog must outlast two update intervals. (This used to also + # pre-satisfy reconciler Rule 3a, removed with the sampling_interval + # suggestion, audit SUGGEST-04.) suggested_watchdog = int(max(30, max(math.ceil(p95_dt) + 1, 2 * math.ceil(median_dt) + 1))) suggestions[CONF_WATCHDOG_INTERVAL] = { @@ -1049,12 +1163,34 @@ class SuggestionEngine: } # 2. No Update Timeout - suggested_timeout = int(max(60, p95_dt * 20)) + # Never below the device's own default (register item 165): a dishwasher's + # 4 h is deliberate (drying), and p95 x 20 took it to 10-30 min. + timeout_floor = int( + DEFAULT_NO_UPDATE_ACTIVE_TIMEOUT_BY_DEVICE.get( + self.device_type or "", DEFAULT_NO_UPDATE_ACTIVE_TIMEOUT + ) + ) + suggested_timeout = int(max(60, timeout_floor, p95_dt * 20)) + reason_to = f"Based on observed update cadence (p95={p95_dt:.1f}s) * 20 (min 60s)." + reason_to_key = "suggestion.reason.no_update_timeout" + reason_to_params: dict[str, Any] = {"p95": f"{p95_dt:.1f}"} + if suggested_timeout == timeout_floor and timeout_floor > max(60, p95_dt * 20): + # The device default won: say so, as the off-delay floor branch does. + if self.device_type and self.device_type in DEFAULT_NO_UPDATE_ACTIVE_TIMEOUT_BY_DEVICE: + reason_to = ( + f"Used device-specific safe minimum for {self.device_type} ({timeout_floor}s)." + ) + reason_to_key = "suggestion.reason.off_delay_device_floor" + reason_to_params = {"device": self.device_type, "floor": timeout_floor} + else: + reason_to = f"Used generic safe minimum ({timeout_floor}s)." + reason_to_key = "suggestion.reason.off_delay_generic_floor" + reason_to_params = {"floor": timeout_floor} suggestions[CONF_NO_UPDATE_ACTIVE_TIMEOUT] = { "value": suggested_timeout, - "reason": f"Based on observed update cadence (p95={p95_dt:.1f}s) * 20 (min 60s).", - "reason_key": "suggestion.reason.no_update_timeout", - "reason_params": {"p95": f"{p95_dt:.1f}"}, + "reason": reason_to, + "reason_key": reason_to_key, + "reason_params": reason_to_params, } # 3. Off Delay @@ -1092,21 +1228,33 @@ class SuggestionEngine: # devices the update gap is dominated by the inter-burst quiet period, so # the result often exceeds the burst interval and resets the end timer on # every tumble burst. Skip it when anti-crease is enabled (#343 gap B). - suggested_off_delay = int(max(device_floor, p95_dt * 5)) + # With real traces on hand (and no long pause among them) the blind + # per-device prior is stale evidence: the dishwasher's 1800 s was handed + # to a device whose 5 traced cycles showed no pause at all (audit + # SUGGEST-07). Floor at the measured-path minimum instead. + traced = sum(1 for c in clean if c.get("power_data")) + fallback_floor = ( + _measured_off_delay_floor(device_floor) if traced >= 5 else device_floor + ) + suggested_off_delay = int(max(fallback_floor, p95_dt * 5)) reason_off = f"Based on observed update cadence (p95={p95_dt:.1f}s) * 5" reason_off_key: str = "suggestion.reason.off_delay_cadence" reason_off_params: dict[str, Any] = {"p95": f"{p95_dt:.1f}"} - if suggested_off_delay == device_floor: - if self.device_type and self.device_type in DEFAULT_OFF_DELAY_BY_DEVICE: + if suggested_off_delay == fallback_floor: + # A floor set the value, not the cadence: name the floor that did. + if ( + fallback_floor == device_floor + and self.device_type and self.device_type in DEFAULT_OFF_DELAY_BY_DEVICE + ): reason_off = ( f"Used device-specific safe minimum for {self.device_type} ({device_floor}s)." ) reason_off_key = "suggestion.reason.off_delay_device_floor" reason_off_params = {"device": self.device_type, "floor": device_floor} else: - reason_off = f"Used generic safe minimum ({DEFAULT_OFF_DELAY}s)." + reason_off = f"Used generic safe minimum ({fallback_floor}s)." reason_off_key = "suggestion.reason.off_delay_generic_floor" - reason_off_params = {"floor": DEFAULT_OFF_DELAY} + reason_off_params = {"floor": fallback_floor} suggestions[CONF_OFF_DELAY] = { "value": suggested_off_delay, "reason": reason_off, @@ -1158,12 +1306,12 @@ class SuggestionEngine: # 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"The shortest program ({shortest_profile_s:.0f}s) 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"minimum, so the minimum is used: {persistence} consecutive matches " + f"take {budget}s, over {pct:.0f}% of that program. " f"Matching only runs when a reading arrives (median=" - f"{median_dt:.1f}s), so the minimum costs nothing." + f"{median_dt:.1f}s), so this costs nothing." ) reason_match_key = "suggestion.reason.match_interval_floored" reason_match_params = { @@ -1258,7 +1406,7 @@ class SuggestionEngine: try: avg = float(prof.get("avg_duration") or 0.0) dur = float(c.get("duration") or 0.0) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): continue if avg > 60 and dur > 60: r = dur / avg @@ -1267,76 +1415,59 @@ class SuggestionEngine: if len(ratios) >= 10: arr: np.ndarray[Any, np.dtype[np.float64]] = np.array(ratios, dtype=float) - - # Per-profile tolerance: each profile contributes its own p95 - # duration deviation, so a tight profile is not penalised by a loose - # one. The global suggestion is the p75 across profiles (covers most - # without over-widening). Falls back to the pooled p95 when no - # profile has enough cycles for its own estimate. - per_profile_p95: list[float] = [] - for _pname, prs in ratios_by_profile.items(): - if len(prs) >= 2: - devs = np.abs(np.array(prs, dtype=float) - 1.0) - per_profile_p95.append(float(np.percentile(devs, 95))) - if per_profile_p95: - agg_dev = float(np.percentile(per_profile_p95, 75)) - reason_tol = ( - f"p75 of per-profile duration variance across " - f"{len(per_profile_p95)} profiles ({len(ratios)} cycles); " - f"tight profiles not penalised." - ) - reason_tol_key = "suggestion.reason.tol_per_profile" - reason_tol_params: dict[str, Any] = { - "profiles": len(per_profile_p95), - "cycles": len(ratios), - } - else: - agg_dev = float(np.percentile(np.abs(arr - 1.0), 95)) - reason_tol = ( - f"Based on pooled duration variance of {len(ratios)} recent " - f"labeled cycles (p95 dev={agg_dev:.2f})." - ) - reason_tol_key = "suggestion.reason.tol_pooled" - reason_tol_params = {"cycles": len(ratios), "dev": f"{agg_dev:.2f}"} - - suggested_tol = min(0.50, max(0.10, round(agg_dev + 0.05, 2))) - - suggestions[CONF_DURATION_TOLERANCE] = { - "value": suggested_tol, - "reason": reason_tol, - "reason_key": reason_tol_key, - "reason_params": reason_tol_params, - } - suggestions[CONF_PROFILE_DURATION_TOLERANCE] = { - "value": suggested_tol, - "reason": reason_tol, - "reason_key": reason_tol_key, - "reason_params": reason_tol_params, - } - p95_ratio = float(np.percentile(arr, 95)) + options = self._entry_options() - # min_duration_ratio governs how EARLY a running cycle may match a - # profile. Goal: as low as possible so a program is recognised ASAP. - # It is not bounded by full-cycle duration variance - the confidence - # and ambiguity gates already prevent premature commits - so keep it - # aggressively low rather than tied to p05 of observed durations. - min_r = 0.05 + # (No duration_tolerance / profile_duration_tolerance suggestions - audit + # SUGGEST-12: the first only feeds a cosmetic flag, the second is read + # by nothing that changes an outcome.) + + # max_duration_ratio is the Stage-1 fast reject. Never below the shipped + # 1.8 (register item 311 measured 1.5 deleting the true candidate on + # 2.3% of folds; p95 + 0.1 landed at 1.15-1.52 on every corpus device, + # undoing it - audit SUGGEST-09). Only ever widen, for a device whose + # cycles genuinely run that long. max_r = min(3.0, round(p95_ratio + 0.1, 2)) - - if min_r < max_r - 0.2: - suggestions[CONF_PROFILE_MATCH_MIN_DURATION_RATIO] = { - "value": min_r, - "reason": "Kept as low as possible so a program is recognised early in the cycle; the confidence and ambiguity gates prevent premature commits.", - "reason_key": "suggestion.reason.min_duration_ratio", - "reason_params": {}, - } + if max_r > DEFAULT_PROFILE_MATCH_MAX_DURATION_RATIO: suggestions[CONF_PROFILE_MATCH_MAX_DURATION_RATIO] = { "value": max_r, "reason": f"Based on labeled cycle durations (p95={p95_ratio:.2f}).", "reason_key": "suggestion.reason.max_duration_ratio", "reason_params": {"p95": f"{p95_ratio:.2f}"}, } + else: + current_max = _num(options.get(CONF_PROFILE_MATCH_MAX_DURATION_RATIO)) + if ( + current_max is not None + and current_max < DEFAULT_PROFILE_MATCH_MAX_DURATION_RATIO + ): + suggestions[CONF_PROFILE_MATCH_MAX_DURATION_RATIO] = { + "value": DEFAULT_PROFILE_MATCH_MAX_DURATION_RATIO, + "reason": ( + "Back to the default: a lower ceiling rejects the right " + "programme whenever a run is longer than usual." + ), + "reason_key": "suggestion.reason.max_duration_ratio_default", + "reason_params": {}, + "corrective": True, + } + # min_duration_ratio: corrective only. A raised floor rejects the right + # programme for the first part of every cycle (one corpus device at 0.63 + # had no candidate at all for most of a run; forcing the shipped value + # was +3.84pp top-1 at 50% elapsed - audit MATCH-EVAL-03). + default_min = DEFAULT_PROFILE_MATCH_MIN_DURATION_RATIO + current_min = _num(options.get(CONF_PROFILE_MATCH_MIN_DURATION_RATIO)) + if current_min is not None and current_min > default_min: + suggestions[CONF_PROFILE_MATCH_MIN_DURATION_RATIO] = { + "value": default_min, + "reason": ( + "Back to the default: a higher floor stops a running cycle " + "from being recognised until it is well under way." + ), + "reason_key": "suggestion.reason.min_duration_ratio_default", + "reason_params": {}, + "corrective": True, + } # Min-off-gap: measured bridge requirement, capped by back-to-back headroom min_off_gap = self._suggest_min_off_gap( @@ -1375,12 +1506,57 @@ class SuggestionEngine: raw = options.get(key) try: val = float(raw) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): continue if val > 0: return val return 2.0 + def _effective_thresholds(self, options: dict[str, Any]) -> dict[str, Any]: + """Stop, start and off delay as the detector runs them, set or not. + + An unset Stop Threshold runs at 0.6 x min_power (#450 fresh entries), not at + min_power, which ``_current_stop_threshold`` falls back to for clean-cycle + checks: the standby check must compare against what really ends a cycle. + """ + from .detector_config import effective_option_values # noqa: PLC0415 + + return effective_option_values(options, self.device_type or "") + + def _standby_floor(self, options: dict[str, Any]) -> dict[str, Any] | None: + """``standby_stop_floor`` over this device's recent history (#458).""" + eff = self._effective_thresholds(options) + stop = float(eff[CONF_STOP_THRESHOLD_W]) + off_delay = float(eff[CONF_OFF_DELAY]) + if off_delay <= 0: + off_delay = float(resolve_off_delay_default(self.device_type or "")) + cycles = self.profile_store.get_past_cycles()[-200:] + return standby_stop_floor(list(cycles), stop, off_delay) + + def generate_standby_floor_suggestions(self) -> dict[str, Any]: + """Raise a stop threshold that sits under the appliance's standby (#458). + + Runs after every cycle end, including force-stopped and user-stopped ones: + those are exactly the cycles this fault produces, and on such an appliance + they may be the only ones there are, so waiting for clean evidence would + wait forever. Proposes nothing unless ``standby_stop_floor`` judges the + floor safe against the device's clean history. Executor-safe. + """ + options = self._entry_options() + floor = self._standby_floor(options) + if not floor or not floor.get("safe"): + return {} + floor_w = float(floor["floor_w"]) + eff = self._effective_thresholds(options) + if float(eff[CONF_STOP_THRESHOLD_W]) >= floor_w: + return {} + out: dict[str, Any] = {CONF_STOP_THRESHOLD_W: _standby_floor_entry(floor_w, floor)} + start_min = round(floor_w * STANDBY_FLOOR_RATIO, 2) + start = _num(eff[CONF_START_THRESHOLD_W]) + if start is None or start < start_min: + out[CONF_START_THRESHOLD_W] = _standby_floor_entry(start_min, floor) + return out + def generate_detection_suggestions(self) -> dict[str, Any]: """Statistical suggestions for detection/model settings not covered by the operational or model passes. @@ -1402,61 +1578,13 @@ class SuggestionEngine: suggestions: dict[str, dict[str, Any]] = {} - # --- Observed sampling interval (drives smoothing + start debounce) --- - sampling_vals: list[float] = [] - for c in clean: - try: - si = float(c.get("sampling_interval") or 0.0) - except (TypeError, ValueError): - continue - if si > 0: - sampling_vals.append(si) - observed_si: float | None = None - if len(sampling_vals) >= 5: - observed_si = float(np.median(sampling_vals)) - suggestions[CONF_SAMPLING_INTERVAL] = { - "value": round(observed_si, 1), - "reason": ( - f"Median update interval observed across {len(sampling_vals)} " - f"clean cycles ({observed_si:.1f}s).{excl_note}" - ), - "reason_key": "suggestion.reason.sampling_interval", - "reason_params": { - "cycles": len(sampling_vals), - "si": f"{observed_si:.1f}", - "excl": excl_note, - }, - "exclusions": excl_summary, - } - - si_for_calc = observed_si if observed_si else DEFAULT_SAMPLING_INTERVAL - - # --- Smoothing window: ~30 s of readings --- - suggested_smooth = int(min(15, max(2, round(30.0 / max(si_for_calc, 1.0))))) - suggestions[CONF_SMOOTHING_WINDOW] = { - "value": suggested_smooth, - "reason": ( - f"Sized to smooth ~30s of readings at {si_for_calc:.0f}s sampling " - f"({suggested_smooth} samples)." - ), - "reason_key": "suggestion.reason.smoothing_window", - "reason_params": {"si": f"{si_for_calc:.0f}", "samples": suggested_smooth}, - } - - # --- Start debounce --- - # Goal: begin capturing a cycle as soon as possible. Set to one sampling - # interval - the minimum that still needs a sustained (not single-sample - # transient) reading to confirm a start. - suggested_start_dur = round(max(2.0, si_for_calc), 1) - suggestions[CONF_START_DURATION_THRESHOLD] = { - "value": suggested_start_dur, - "reason": ( - f"Kept short (~one {si_for_calc:.0f}s sample interval) so detection " - f"starts as early as possible while still ignoring single-sample spikes." - ), - "reason_key": "suggestion.reason.start_duration", - "reason_params": {"si": f"{si_for_calc:.0f}"}, - } + # (No sampling_interval / smoothing_window / start_duration_threshold + # suggestions - audit SUGGEST-04/12. The stored per-cycle sampling interval + # is measured AFTER the manager's reading throttle, which is set by the very + # option suggested, so applying it ratcheted the throttle without bound + # (2 -> 34 s on one washer, unbounded on 6 of 16 devices) and dragged the + # watchdog and start debounce up with it; smoothing_window sizes a buffer + # nothing reads.) # --- min_power: keep the noise gate below the lowest genuine draw --- # Strip the anti-crease tail before taking the per-cycle minimum so that the @@ -1490,16 +1618,35 @@ class SuggestionEngine: } # --- completion_min_seconds: filter ghosts below half the shortest run --- + # The basis includes LABELLED interrupted cycles: a cycle shorter than this + # setting is stored `interrupted`, which `select_clean_cycles` drops, so + # judging it on clean cycles alone erased the shortest programme from its + # own evidence and the next suggestion rose again - a ratchet that turned a + # 48 min "Quick wash" interrupted (audit SUGGEST-03). And it never exceeds + # half the shortest learned programme. + # _num, not float(): an oversized stored integer (10**400) raised + # OverflowError out of this comprehension and lost the whole pass. durations = [ - float(c["duration"]) - for c in clean - if isinstance(c.get("duration"), (int, float)) - and not isinstance(c.get("duration"), bool) - and float(c["duration"]) > 0 + d for d in ( + _num(c.get("duration")) if isinstance(c.get("duration"), (int, float)) else None + for c in [ + *clean, + *( + c for c in all_cycles + if isinstance(c, dict) + and c.get("status") == "interrupted" + and c.get("profile_name") + ), + ] + ) + if d is not None and d > 0 ] if len(durations) >= 10: p05d = float(np.percentile(durations, 5)) suggested_cms = int(max(120, round(p05d * 0.5))) + shortest = self._shortest_profile_duration() + if shortest is not None and shortest > 0: + suggested_cms = int(min(suggested_cms, max(120, round(shortest * 0.5)))) suggestions[CONF_COMPLETION_MIN_SECONDS] = { "value": suggested_cms, "reason": ( @@ -1515,144 +1662,16 @@ class SuggestionEngine: "exclusions": excl_summary, } - # --- Confidence-calibrated thresholds (labeled clean cycles only) --- - self._add_confidence_suggestions(clean, suggestions) - - # --- end_repeat_count: false-end pressure --- - erc = self._suggest_end_repeat_count(clean, stop_thr) - if erc is not None: - suggestions[CONF_END_REPEAT_COUNT] = erc + # (No learning / auto-label / match-threshold suggestions - audit SUGGEST-01. + # They were low percentiles of "uncorrected auto-labels", which only exist + # ABOVE the thresholds already in force, so every apply pulled the ladder + # down: on every device with >= 15 auto labels it collapsed to ~0.6-0.77 + # within 1-3 applies, review requests fell from 47-100% to 0-12%, and the + # match threshold - which gates Smart Termination - followed. No + # end_repeat_count suggestion either: the detector never reads that key.) return suggestions - def _add_confidence_suggestions( - self, clean: list[dict[str, Any]], suggestions: dict[str, dict[str, Any]] - ) -> None: - """Derive confidence thresholds from the match_confidence distribution. - - Uses the ``label_source`` provenance (auto vs manual) so we can tell - which cycles the user trusted. Auto-labels the user never corrected are - the ground truth for "matching was reliable at this confidence". - """ - manual_conf: list[float] = [] - auto_ok_conf: list[float] = [] - for c in clean: - raw_conf = c.get("match_confidence") - if ( - not isinstance(raw_conf, (int, float)) - or isinstance(raw_conf, bool) - or raw_conf <= 0 - ): - continue - conf = float(raw_conf) - src = c.get("label_source") - if src == "manual": - manual_conf.append(conf) - elif src in ("auto_match", "auto_label_post", "auto_label_service") and not c.get( - "original_auto_label" - ): - auto_ok_conf.append(conf) - - if len(manual_conf) >= 10: - p05c = float(np.percentile(manual_conf, 5)) - suggestions[CONF_LEARNING_CONFIDENCE] = { - "value": round(min(max(p05c, 0.3), 0.9), 2), - "reason": ( - f"p05 confidence of {len(manual_conf)} user-labeled cycles " - f"({p05c:.2f}); below this, request verification." - ), - "reason_key": "suggestion.reason.learning_confidence", - "reason_params": {"cycles": len(manual_conf), "p05": f"{p05c:.2f}"}, - } - - if len(auto_ok_conf) >= 15: - p15 = float(np.percentile(auto_ok_conf, 15)) - suggestions[CONF_AUTO_LABEL_CONFIDENCE] = { - "value": round(min(max(p15, 0.5), 0.98), 2), - "reason": ( - f"15th-percentile confidence of {len(auto_ok_conf)} auto-labels " - f"the user never corrected ({p15:.2f})." - ), - "reason_key": "suggestion.reason.auto_label_confidence", - "reason_params": {"cycles": len(auto_ok_conf), "p15": f"{p15:.2f}"}, - } - p10 = float(np.percentile(auto_ok_conf, 10)) - suggestions[CONF_PROFILE_MATCH_THRESHOLD] = { - "value": round(min(max(p10, 0.3), 0.9), 2), - "reason": ( - f"p10 confidence of {len(auto_ok_conf)} correct auto-labels " - f"({p10:.2f}); safe live-commit floor." - ), - "reason_key": "suggestion.reason.profile_match_threshold", - "reason_params": {"cycles": len(auto_ok_conf), "p10": f"{p10:.2f}"}, - } - - def _suggest_end_repeat_count( - self, clean: list[dict[str, Any]], stop_threshold_w: float - ) -> dict[str, Any] | None: - """Recommend how many end confirmations to require, from false-end rate. - - A "false end" is an internal low-power run (>= 60 s) that resumed - the - kind of pause that can trip a premature cycle end. If many clean cycles - contain one, requiring extra end confirmations avoids splitting cycles. - """ - n_total = 0 - n_false_end = 0 - for c in clean: - readings = _cycle_readings(c) - if len(readings) < 10: - continue - n_total += 1 - powers = [p for _, p in readings] - peak = max(powers) if powers else 0.0 - if peak <= 0: - continue - active_thr = max(stop_threshold_w, _CLEAN_ACTIVE_FLOOR_RATIO * peak) - max_gap_s = _MAX_PAUSE_GAP_H * 3600 - # A "false end" is a >=60 s internal quiet run that resumed into - # *sustained* activity. Reuse the shared pause locator so a brief - # terminal blip (a pump-out / drying tick after a soak) is absorbed - # back into the quiet tail rather than mis-counted as a resume -- the - # same sustained-resume + outage-gap gate used by the off_delay - # heuristics (_suggest_off_delay_from_pauses / _scored_pauses). - for low_start_s, resume_idx in _resumed_low_runs(readings, active_thr, max_gap_s): - # Same measurement correction as the off_delay heuristic (#445): - # a "false end" is quiet time the END GATES would have banked, so - # it is timed against stop_threshold_w, not against the - # 2%-of-peak activity floor that only decides whether this was a - # pause at all. Without it, an appliance that works in bursts - # scores a false end in every cycle and end_repeat_count is - # driven up on evidence the detector never saw. - if _measured_quiet_span_s( - readings, low_start_s, resume_idx, stop_threshold_w - ) >= 60.0: - n_false_end += 1 - break - - if n_total < 15: - return None - frac = n_false_end / n_total - if frac >= 0.55: - val = 3 - elif frac >= 0.30: - val = 2 - else: - val = 1 - return { - "value": val, - "reason": ( - f"{n_false_end}/{n_total} clean cycles ({frac * 100:.0f}%) had a " - f">60s internal pause that resumed; require {val} end confirmation(s)." - ), - "reason_key": "suggestion.reason.end_repeat_count", - "reason_params": { - "false": n_false_end, - "total": n_total, - "pct": f"{frac * 100:.0f}", - "val": val, - }, - } - def _suggest_off_delay_from_pauses( self, cycles: list[dict[str, Any]], @@ -1778,11 +1797,12 @@ class SuggestionEngine: Falls back to the historical inter-cycle-gap heuristic when there are too few traces to measure a bridge requirement. - Validated with ``devtools/min_off_gap_eval.py``: replaying all 152 clean - cycles across the ``cycle_data/`` corpus through a real unmatched - ``CycleDetector`` at the proposed value produces zero splits (the only - trace that splits is tron4r's known back-to-back *merged* 206-min cycle, - where splitting is the correct outcome). + Validated with ``devtools/min_off_gap_eval.py`` (production detector + config, unmatched replay with watchdog keepalives; item 483): on the + export corpus the shipped value splits 0 of 68 cycles and merges none. + With ``--all-formats`` it splits 6 of 151 and merges 3, all on devices + where every candidate value fails the same way, so ``min_off_gap`` is + not their cause. """ # Only consider completed, labeled cycles with valid timestamps timed_cycles: list[tuple[float, float]] = [] @@ -1804,7 +1824,7 @@ class SuggestionEngine: if start is None or end is None or end <= start: continue timed_cycles.append((start, end)) - except (TypeError, ValueError, KeyError): + except (TypeError, ValueError, KeyError, OverflowError): continue if len(timed_cycles) < 3: @@ -1946,45 +1966,6 @@ class SuggestionEngine: DEVICE_TYPE_WASHER_DRYER, ) - def _strip_anti_crease_tail( - self, - ordered_powers: np.ndarray, - options: dict[str, Any] | None = None, - ) -> np.ndarray: - """Drop the post-cycle anti-crease tail from an ordered power trace (#343). - - Stop/start thresholds detect the MAIN cycle; the anti-crease tumble-pulse - tail is governed by its own ``anti_wrinkle_*`` settings, but its near-zero - between-pulse baseline is the global minimum of the stored trace and used - to poison the min-active statistic (the tuner then proposes thresholds just - above that baseline, breaking end-detection). - - The tail is everything after the last sample that reaches - ``anti_wrinkle_max_power`` - by the config's own rule a pulse above that - ends anti-wrinkle, so nothing in the tail can reach it. Returns the trace - unchanged when anti-crease is off, the device type is ineligible, or no - sample reaches the ceiling (no identifiable main phase) - so it can never - over-exclude for a non-anti-crease device or a gentle program. - - Pass ``options`` when calling from a loop to avoid repeated config-entry - reads (``hass.config_entries.async_get_entry`` is loop-affine). - """ - if self.device_type not in self._ANTI_CREASE_DEVICE_TYPES: - return ordered_powers - opts = options if options is not None else self._entry_options() - if not opts.get(CONF_ANTI_WRINKLE_ENABLED, DEFAULT_ANTI_WRINKLE_ENABLED): - return ordered_powers - try: - max_power = float(opts.get(CONF_ANTI_WRINKLE_MAX_POWER, DEFAULT_ANTI_WRINKLE_MAX_POWER)) - except (TypeError, ValueError): - max_power = DEFAULT_ANTI_WRINKLE_MAX_POWER - if max_power <= 0 or ordered_powers.size == 0: - return ordered_powers - above = np.flatnonzero(ordered_powers >= max_power) - if above.size == 0: - return ordered_powers # no main high-power phase -> nothing to strip - return ordered_powers[: int(above[-1]) + 1] - def _is_anti_crease_enabled(self, options: dict[str, Any] | None = None) -> bool: """True when anti-crease mode is active on an eligible device type.""" if self.device_type not in self._ANTI_CREASE_DEVICE_TYPES: @@ -1997,7 +1978,7 @@ class SuggestionEngine: readings: list[tuple[float, float]], options: dict[str, Any] | None = None, ) -> list[tuple[float, float]]: - """Time-domain equivalent of _strip_anti_crease_tail for (offset, power) pairs. + """Trim (offset, power) readings to the main cycle, before the anti-crease tail. Returns the readings list trimmed to the last sample >= anti_wrinkle_max_power so that pause-duration and min-power statistics ignore the anti-crease tail @@ -2014,7 +1995,7 @@ class SuggestionEngine: return readings try: max_power = float(opts.get(CONF_ANTI_WRINKLE_MAX_POWER, DEFAULT_ANTI_WRINKLE_MAX_POWER)) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): max_power = DEFAULT_ANTI_WRINKLE_MAX_POWER if max_power <= 0: return readings @@ -2026,75 +2007,17 @@ class SuggestionEngine: return readings # no main high-power phase identifiable return readings[: last_above + 1] - def run_simulation(self, cycle_data: dict[str, Any]) -> dict[str, Any]: - """Replay a single cycle with varied parameters to find optimal settings. - - For richer, multi-cycle suggestions use :meth:`run_batch_simulation`. - """ - power_data_raw: Any = cycle_data.get("power_data", []) - if not isinstance(power_data_raw, list): - return {} - power_data = cast(list[list[float] | tuple[Any, float]], power_data_raw) - if len(power_data) < 10: - return {} - - start_time_raw = cycle_data.get("start_time") - start_time_iso = ( - start_time_raw if isinstance(start_time_raw, str) and start_time_raw else None - ) - - # Normalise power_data to [[offset_sec, power], ...] regardless of source format. - readings_list = power_data_to_offsets(power_data, start_time_iso) - - readings: list[tuple[float, float]] = [ - (float(offset), float(power)) for offset, power in readings_list - ] - - if not readings: - return {} - - powers = np.array([p[1] for p in readings]) - # Exclude the anti-crease tail so its near-zero baseline does not poison the - # stop/start thresholds on anti-crease devices (#343). No-op otherwise. - main_powers = self._strip_anti_crease_tail(powers) - active_powers = main_powers[main_powers > 0.5] - - if len(active_powers) < 5: - return {} - - min_active = float(np.min(active_powers)) - - suggested_stop = round(min_active * 0.8, 2) - suggested_start = round(min_active * 1.2, 2) - - # end_energy_threshold is intentionally NOT suggested from a single cycle: - # a context-free 0.05 Wh was below the anti-crease baseline energy accumulated - # over the off_delay window, so the end gate never fired (#343 gap D). The - # batch path (run_batch_simulation) derives a cycle-energy-proportional floor - # from actual false-end events once 5+ cycles exist; use that instead. - return { - CONF_STOP_THRESHOLD_W: { - "value": suggested_stop, - "reason": f"Based on minimum active power ({min_active:.1f}W) observed in last cycle.", - "reason_key": "suggestion.reason.min_active", - "reason_params": {"min": f"{min_active:.1f}"}, - }, - CONF_START_THRESHOLD_W: { - "value": suggested_start, - "reason": f"Based on minimum active power ({min_active:.1f}W) observed in last cycle.", - "reason_key": "suggestion.reason.min_active", - "reason_params": {"min": f"{min_active:.1f}"}, - }, - } - def run_batch_simulation(self, cycles: list[dict[str, Any]]) -> dict[str, Any]: """Derive parameter suggestions from a collection of labeled cycles. - Unlike :meth:`run_simulation` (single-cycle heuristics), this method + Stop/start come ONLY from here, across many cycles: the per-cycle + ``run_simulation`` that re-derived them from the last cycle alone made them a + random walk (changed after 19 of 19 cycles on one dishwasher; audit + SUGGEST-06). This method aggregates statistics across *multiple* cycles for robustness: - - Power thresholds from the 5th-percentile minimum active power. - - Dead zone from the 75th-percentile of early dips across cycles. + - Power thresholds from the 5th-percentile minimum active power, withheld + when that minimum is where the appliance rests (:func:`resting_level_w`). - End-energy threshold from the maximum false-end energy seen. - Min-off-gap from the 5th-percentile inter-cycle gap. @@ -2109,6 +2032,9 @@ class SuggestionEngine: _batch_opts = self._entry_options() stop_thr = self._current_stop_threshold(_batch_opts) + # What the detector really ends cycles at (0.6 x min_power when the Stop + # Threshold is unset, #450). stop_thr stays the clean-cycle check's level. + eff_stop = float(self._effective_thresholds(_batch_opts)[CONF_STOP_THRESHOLD_W]) # Keep the unfiltered list: the min_off_gap merge ceiling must see the # user's real turnaround, which dropping a cycle would fuse away. raw_cycles = list(cycles) @@ -2144,6 +2070,7 @@ class SuggestionEngine: # --- Power thresholds --- lowest_active: list[float] = [] + main_cycles: list[list[tuple[float, float]]] = [] cycle_energies: list[float] = [] # per-cycle total energy (Wh) for proportional floor false_end_energies: list[float] = [] max_gap_s = _MAX_PAUSE_GAP_H * 3600 @@ -2154,10 +2081,9 @@ class SuggestionEngine: # non-anti-crease devices. Trim the (offset, power) readings once so the # threshold stat and the energy scan consume the same main-cycle data. main_readings = self._strip_anti_crease_readings(readings, options=_batch_opts) + main_cycles.append(main_readings) powers = np.array([p for _, p in main_readings]) active = powers[powers > 0.5] - peak = float(np.max(powers)) if powers.size else 0.0 - active_thr = max(stop_thr, _CLEAN_ACTIVE_FLOOR_RATIO * peak) if active.size > 0: lowest_active.append(float(np.min(active))) @@ -2210,59 +2136,86 @@ class SuggestionEngine: # correctly per appliance (a few W for washers, ~steady load for pumps). suggested_stop = round(p05_min * 0.8, 2) suggested_start = round(max(suggested_stop + 0.1, p05_min * 1.05), 2) - reason_thr = ( - f"Kept just above the p05 lowest active power across {n} cycles " - f"({p05_min:.1f}W) so a start is caught as early as possible and the " - f"stop threshold stays below the machine's lowest running power." + # ...but "active" here is any reading over 0.5 W, so on an appliance + # that RESTS above that (drum stopped between tumbles, passive drying, + # the draw a Smart Termination tail kept) the p05 is the resting level + # itself and both values land on or under it (#455). The resting draw + # then reads as running - no end gate can time it, so a cycle that used + # to end there waits for the appliance to switch off - and once the + # cycle is over it reads as a start. Measured with + # devtools/suggestion_loop_eval.py: a dishwasher resting at 0.8 W was + # offered stop 0.56 W (10 Eco cycles then had nothing left to end on), a + # washer resting at 3.3 W start 3.23 W / stop 2.46 W (its post-end draw + # split two labelled cycles). There the anchor says nothing about the + # lowest RUNNING power, so it proposes nothing: the thresholds in force + # demonstrably end cycles at that level. Same margin as the standby + # floor (#458), so plug jitter at the resting level stays quiet. + resting = resting_level_w(main_cycles, eff_stop) + resting_floor = ( + max(resting * STANDBY_FLOOR_RATIO, resting + STANDBY_FLOOR_MIN_MARGIN_W) + if resting is not None and resting > 0 + else None ) - reason_thr_params = {"cycles": n, "p05": f"{p05_min:.1f}"} - suggestions[CONF_STOP_THRESHOLD_W] = { - "value": suggested_stop, - "reason": reason_thr, - "reason_key": "suggestion.reason.thr_batch", - "reason_params": reason_thr_params, - } - suggestions[CONF_START_THRESHOLD_W] = { - "value": suggested_start, - "reason": reason_thr, - "reason_key": "suggestion.reason.thr_batch", - "reason_params": reason_thr_params, - } + if resting_floor is not None and suggested_stop < resting_floor: + _LOGGER.debug( + "Stop/start suggestion withheld: p05 lowest active %.2fW is the " + "resting level (%.2fW), stop %.2fW would sit under %.2fW", + p05_min, resting, suggested_stop, resting_floor, + ) + else: + reason_thr = ( + f"Just above the lowest active power (p05) across {n} cycles " + f"({p05_min:.1f}W), so starts are caught early and the stop " + f"threshold stays below the lowest running power." + ) + reason_thr_params = {"cycles": n, "p05": f"{p05_min:.1f}"} + suggestions[CONF_STOP_THRESHOLD_W] = { + "value": suggested_stop, + "reason": reason_thr, + "reason_key": "suggestion.reason.thr_batch", + "reason_params": reason_thr_params, + } + suggestions[CONF_START_THRESHOLD_W] = { + "value": suggested_start, + "reason": reason_thr, + "reason_key": "suggestion.reason.thr_batch", + "reason_params": reason_thr_params, + } - # End-energy: p95 of resuming-pause energies (outlier-robust) with a - # floor proportional to the cycle's own energy, not a fixed Wh value. - median_energy = float(np.median(cycle_energies)) if cycle_energies else 0.0 - prop_floor = 0.002 * median_energy # 0.2% of a typical cycle - if false_end_energies: - p95_false = float(np.percentile(false_end_energies, 95)) - suggested_end = round(max(0.01, prop_floor, p95_false * 1.1), 4) - reason_end = ( - f"p95 false-end energy ({p95_false:.4f}Wh) across {len(valid_cycles)} " - f"cycles, floored at 0.2% of median cycle energy ({prop_floor:.4f}Wh)." + # End-energy: corrective only (audit SUGGEST-12). The end gate is checked + # only once the run has been below stop for the whole off_delay window, so + # any value at or above stop x off_delay / 3600 never decides anything - the + # false-end statistic that used to be suggested here changed the value by a + # median 1759% for no effect. A value BELOW that floor forbids what the + # power gate allows (#376), so that one is corrected. + try: + cur_stop = eff_stop + cur_off_delay = float( + _batch_opts.get(CONF_OFF_DELAY) + or resolve_off_delay_default(self.device_type or "") ) - reason_end_key = "suggestion.reason.end_energy_false" - reason_end_params: dict[str, Any] = { - "p95": f"{p95_false:.4f}", - "cycles": len(valid_cycles), - "floor": f"{prop_floor:.4f}", - } - else: - suggested_end = round(max(0.01, prop_floor), 4) - reason_end = ( - f"No false ends across {len(valid_cycles)} cycles; floored at 0.2% " - f"of median cycle energy ({prop_floor:.4f}Wh)." - ) - reason_end_key = "suggestion.reason.end_energy_no_false" - reason_end_params = { - "cycles": len(valid_cycles), - "floor": f"{prop_floor:.4f}", - } - suggestions[CONF_END_ENERGY_THRESHOLD] = { - "value": suggested_end, - "reason": reason_end, - "reason_key": reason_end_key, - "reason_params": reason_end_params, - } + cur_end = _num(_batch_opts.get(CONF_END_ENERGY_THRESHOLD)) + except (TypeError, ValueError, OverflowError): + cur_end = None + if ( + cur_end is not None and cur_off_delay > 0 + and math.isfinite(cur_stop) and math.isfinite(cur_off_delay) + ): + floor_wh = math.ceil(cur_stop * cur_off_delay / 36.0) / 100.0 + if cur_end < floor_wh: + suggestions[CONF_END_ENERGY_THRESHOLD] = { + "value": floor_wh, + "reason": ( + f"Raised to the floor the stop threshold ({cur_stop:g} W) over " + f"the off delay ({cur_off_delay:.0f} s) implies; below it the " + f"cycle can only end through a fallback path." + ), + "reason_key": "suggestion.reason.end_energy_floor", + "reason_params": { + "stop": f"{cur_stop:g}", "off_delay": f"{cur_off_delay:.0f}", + }, + "corrective": True, + } min_off_gap = self._suggest_min_off_gap( cycles, stop_threshold_w=stop_thr, gap_cycles=raw_cycles @@ -2270,7 +2223,9 @@ class SuggestionEngine: if min_off_gap is not None: suggestions[CONF_MIN_OFF_GAP] = min_off_gap - return suggestions + # The p05 anchor above is the standby draw whenever standby-level samples + # sit inside the cycles, so 0.8 x it is under standby (#458). + return apply_standby_floor(suggestions, self._standby_floor(_batch_opts)) def apply_suggestions(self, suggestions: dict[str, Any]) -> None: """Persist suggestions to the profile store, then reconcile the full set. @@ -2294,7 +2249,11 @@ class SuggestionEngine: self._reconcile_stored_suggestions() if self.hass and suggestions: - self.hass.async_create_task(self.profile_store.async_save()) + save = self.profile_store.async_save() + if self._spawn_fn is not None: + self._spawn_fn(save) + else: + self.hass.async_create_task(save) def _reconcile_stored_suggestions(self) -> None: """Reconcile the accumulated stored suggestions against current options.""" @@ -2313,293 +2272,3 @@ class SuggestionEngine: ) if changed: _LOGGER.info("Reconciled coupled parameters for consistency: %s", ", ".join(sorted(changed))) - - -# ─── ML-calibrated suggestions (Stage 3, gated by ENABLE_ML_SUGGESTIONS) ────── - - -class MLSuggestionEngine: - """Setting suggestions calibrated with the embedded ML models. - - Runs *alongside* :class:`SuggestionEngine` and never mutates it. It produces - a parallel set of recommendations for the ML Lab side-by-side comparison, - using the end-detector and quality models to judge cycle behaviour rather - than fixed statistical heuristics. All work is NumPy-only and safe to run in - an executor thread. - - The models are loaded lazily; if the ML package is unavailable the engine - simply yields no suggestions. - """ - - def __init__(self, classic: SuggestionEngine) -> None: - self._classic = classic - self.profile_store = classic.profile_store - self.device_type = classic.device_type - - def _load_models(self) -> tuple[Any, Any, Any, Any] | None: - """Resolve (end_score_fn, quality_score_fn, end_feat_fn, quality_feat_fn). - - Score fns prefer an on-device trained spec over the embedded baseline - (via :func:`ml.engine.resolve_scorer`), so ML-calibrated suggestions use - the user's personalised model once one has been trained. - """ - try: - from .ml.engine import resolve_scorer - from .ml.feature_extraction import ( - latest_end_event_features, - quality_features, - ) - except Exception: # pylint: disable=broad-exception-caught - return None - end_fn, _ = resolve_scorer("end", self.profile_store) - quality_fn, _ = resolve_scorer("quality", self.profile_store) - if end_fn is None and quality_fn is None: - return None - return (end_fn, quality_fn, latest_end_event_features, quality_features) - - def _profile_expectations( - self, clean: list[dict[str, Any]] - ) -> dict[str, dict[str, float]]: - """Median duration / energy / peak per profile (shared helper).""" - from .ml.feature_extraction import profile_expectations - - return profile_expectations(clean) - - def _scored_pauses( - self, - points: list[tuple[float, float]], - expectation: dict[str, float], - stop_threshold_w: float, - end_score_fn: Any, - end_feat_fn: Any, - ) -> list[tuple[float, float | None]]: - """Return (duration_s, P(end)) for each internal pause (>=30s) that - resumed. P(end) is the end-detector's score for a prefix ending in that - pause; ``None`` if scoring failed.""" - if not points or len(points) < 6: - return [] - powers = [p for _, p in points] - peak = max(powers) if powers else 0.0 - if peak <= 0: - return [] - active_thr = max(stop_threshold_w, _CLEAN_ACTIVE_FLOOR_RATIO * peak) - # Data-outage ceiling: the same pause-gap bound used elsewhere in this file - # (a real intra-cycle pause never exceeds ~1h). A gap larger than this - # between consecutive samples is a sensor dropout / restart, not a genuine - # pause; its span must not be counted as pause duration (it would inflate - # dur and, downstream, the p95 that sizes _ml_off_delay / off_delay_pauses). - max_gap_s = _MAX_PAUSE_GAP_H * 3600 - out: list[tuple[float, float | None]] = [] - # Same pause detector as the classic off_delay heuristic: a low run that - # resumed into sustained activity (a terminal drying/pump-out blip that does - # not sustain is not a pause). ``resume_idx`` is the first active sample of - # the resume, so the tail prefix ``points[:resume_idx]`` ends in the low run. - for low_start_s, resume_idx in _resumed_low_runs(points, active_thr, max_gap_s): - # Timed against stop_threshold_w like the classic heuristic (#445), so - # the ML-calibrated off_delay is fitted to the same quantity the end - # gates measure rather than to burst-phase spans. - dur, quiet_end = _measured_quiet_span( - points, low_start_s, resume_idx, stop_threshold_w - ) - if dur < 30.0: # ignore motor micro-dips - continue - score: float | None = None - try: - # Score the prefix ending on the span `dur` was measured from, not - # merely the one before the resume: a low run split by readings - # between stop_threshold_w and active_thr holds several quiet - # spans, and pairing the longest span's duration with the last - # span's score is what made the `score < 0.4` filter keep the - # wrong pauses. - tail_end = resume_idx if quiet_end is None else quiet_end + 1 - feat = end_feat_fn(points[:tail_end], expectation) - if feat is not None: - score = float(end_score_fn(feat)) - except Exception: # pylint: disable=broad-exception-caught - pass - out.append((dur, score)) - return out - - def generate_ml_suggestions(self) -> dict[str, Any]: - """Produce ML-calibrated suggestions from clean cycle history.""" - models = self._load_models() - if models is None: - return {} - end_score_fn, quality_score_fn, end_feat_fn, quality_feat_fn = models - - raw_cycles = self.profile_store.get_past_cycles()[-200:] - stop_thr = self._classic._current_stop_threshold(self._classic._entry_options()) - clean, _excluded = select_clean_cycles(raw_cycles, stop_threshold_w=stop_thr) - if len(clean) < 5: - return {} - - expectations = self._profile_expectations(clean) - device_floor = ( - DEFAULT_OFF_DELAY_BY_DEVICE.get(self.device_type, DEFAULT_OFF_DELAY) - if self.device_type is not None - else DEFAULT_OFF_DELAY - ) - - out: dict[str, dict[str, Any]] = {} - off_delay = self._ml_off_delay( - clean, expectations, stop_thr, end_score_fn, end_feat_fn, device_floor - ) - if off_delay is not None: - out[CONF_OFF_DELAY] = off_delay - - erc = self._ml_end_repeat_count(clean, expectations, stop_thr, end_score_fn, end_feat_fn) - if erc is not None: - out[CONF_END_REPEAT_COUNT] = erc - - alc = self._ml_auto_label_confidence(clean, expectations, quality_score_fn, quality_feat_fn) - if alc is not None: - out[CONF_AUTO_LABEL_CONFIDENCE] = alc - - if out: - _LOGGER.info("ML-calibrated suggestions from %d clean cycles: %s", len(clean), ", ".join(sorted(out))) - return out - - def _ml_off_delay( - self, - clean: list[dict[str, Any]], - expectations: dict[str, dict[str, float]], - stop_thr: float, - end_score_fn: Any, - end_feat_fn: Any, - device_floor: int, - ) -> dict[str, Any] | None: - """Off-delay from end-detector-confirmed pauses (P(end) < 0.4). - - Floored by :func:`_measured_off_delay_floor` for the same reason as the - classic twin: a model-verified pause measurement outranks the blind - per-device prior. - """ - confirmed: list[float] = [] - n_cycles = 0 - for c in clean: - exp = expectations.get(c.get("profile_name")) - if not exp: - continue - points = _cycle_readings(c) - if len(points) < 6: - continue - n_cycles += 1 - for dur, score in self._scored_pauses(points, exp, stop_thr, end_score_fn, end_feat_fn): - if score is not None and score < 0.4: - confirmed.append(dur) - if n_cycles < 5 or len(confirmed) < 3: - return None - p95 = float(np.percentile(confirmed, 95)) - floor = _measured_off_delay_floor(device_floor) - value = int(max(floor, round(p95 + 60.0))) - return { - "value": value, - "reason": ( - f"End-detector-confirmed pauses: p95 {p95:.0f}s + 60s buffer, from " - f"{len(confirmed)} model-verified pauses across {n_cycles} cycles " - f"(floor {floor}s)." - ), - "reason_key": "suggestion.reason.ml_off_delay", - "reason_params": { - "p95": f"{p95:.0f}", - "pauses": len(confirmed), - "cycles": n_cycles, - "floor": floor, - }, - } - - def _ml_end_repeat_count( - self, - clean: list[dict[str, Any]], - expectations: dict[str, dict[str, float]], - stop_thr: float, - end_score_fn: Any, - end_feat_fn: Any, - ) -> dict[str, Any] | None: - """Require extra end confirmations when the end-detector is fooled by - pauses (scores a resuming pause > 0.5).""" - n_total = 0 - n_false = 0 - for c in clean: - exp = expectations.get(c.get("profile_name")) - if not exp: - continue - points = _cycle_readings(c) - if len(points) < 6: - continue - n_total += 1 - for _dur, score in self._scored_pauses(points, exp, stop_thr, end_score_fn, end_feat_fn): - if score is not None and score > 0.5: - n_false += 1 - break - if n_total < 15: - return None - frac = n_false / n_total - val = 3 if frac >= 0.5 else 2 if frac >= 0.25 else 1 - return { - "value": val, - "reason": ( - f"{n_false}/{n_total} cycles ({frac * 100:.0f}%) had a pause the " - f"end-detector scored >50%; require {val} end confirmation(s)." - ), - "reason_key": "suggestion.reason.ml_end_repeat", - "reason_params": { - "false": n_false, - "total": n_total, - "pct": f"{frac * 100:.0f}", - "val": val, - }, - } - - def _ml_auto_label_confidence( - self, - clean: list[dict[str, Any]], - expectations: dict[str, dict[str, float]], - quality_score_fn: Any, - quality_feat_fn: Any, - ) -> dict[str, Any] | None: - """Lowest match-confidence band the quality model still rates as clean.""" - clean_confs: list[float] = [] - for c in clean: - raw_conf = c.get("match_confidence") - if ( - not isinstance(raw_conf, (int, float)) - or isinstance(raw_conf, bool) - or raw_conf <= 0 - ): - continue - exp = expectations.get(c.get("profile_name")) - if not exp: - continue - points = _cycle_readings(c) - if len(points) < 6: - continue - conf = float(raw_conf) - try: - feat = quality_feat_fn( - points=points, - profile_median_duration_s=exp["duration"], - profile_median_energy_wh=exp["energy"], - profile_median_peak_w=exp["peak"], - profile_distance=max(0.0, 1.0 - conf), - label_margin=conf, - profile_fit_score=conf, - flag_count=0, - ) - q = float(quality_score_fn(feat)) - except Exception: # pylint: disable=broad-exception-caught - continue - if q < 0.15: - clean_confs.append(conf) - if len(clean_confs) < 10: - return None - p10 = float(np.percentile(clean_confs, 10)) - return { - "value": round(min(max(p10, 0.5), 0.98), 2), - "reason": ( - f"Lowest confidence the quality model still rated clean " - f"(p10 of {len(clean_confs)} clean cycles = {p10:.2f})." - ), - "reason_key": "suggestion.reason.ml_auto_label", - "reason_params": {"cycles": len(clean_confs), "p10": f"{p10:.2f}"}, - } \ No newline at end of file diff --git a/custom_components/ha_washdata/task_registry.py b/custom_components/ha_washdata/task_registry.py index 73009792..01a4cb28 100644 --- a/custom_components/ha_washdata/task_registry.py +++ b/custom_components/ha_washdata/task_registry.py @@ -58,7 +58,7 @@ class Task: id: str entry_id: str - kind: str # 'reprocess' | 'ml_training' | 'pg_history' | 'pg_sweep' + kind: str # reprocess, rebuild, ml_training, auto_label, trim, split, merge, store_download, history_import, history_import_apply, pg_history, pg_sweep, pg_detail (ws_api's reg.create calls) label: str # English fallback shown only if no label_key resolves # Panel-localizable label: the pill renders _t(label_key, label_params, label) # so per-step progress text is translated. When label_key is None the pill diff --git a/custom_components/ha_washdata/time_utils.py b/custom_components/ha_washdata/time_utils.py index 0e16ef2f..aba9238d 100644 --- a/custom_components/ha_washdata/time_utils.py +++ b/custom_components/ha_washdata/time_utils.py @@ -38,6 +38,21 @@ import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) + +def utc_now() -> datetime: + """Return the current time as an aware UTC datetime, for interval arithmetic. + + Two aware datetimes that share one tzinfo instance are subtracted on their + wall-clock fields, and every ``dt_util.now()`` stamp shares HA's ZoneInfo, so + ``now() - earlier_now()`` is an hour wrong across a DST change (audit + DETECT-01). Use this for anything that is subtracted, compared or stored to + be subtracted later; keep ``dt_util.now()`` only where the LOCAL clock is the + point (quiet hours, "finished at 14:05"). It reads ``dt_util.now()`` rather + than ``dt_util.utcnow()`` so tests that freeze ``dt_util.now`` keep + controlling the clock. + """ + return dt_util.as_utc(dt_util.now()) + # Type aliases PowerPoint = list[Any] | tuple[Any, ...] PowerData = list[PowerPoint] @@ -103,7 +118,7 @@ def power_data_to_offsets( parsed_start = dt_util.parse_datetime(start_time_iso) if parsed_start is not None: base_ts = parsed_start.timestamp() - except (ValueError, OSError) as e: + except (ValueError, OSError, OverflowError) as e: _LOGGER.debug("Failed to parse start_time_iso %s: %s", start_time_iso, e) result: list[list[float]] = [] for item in power_data: @@ -114,7 +129,7 @@ def power_data_to_offsets( base_ts = ts_abs # use first reading as anchor offset = round(ts_abs - base_ts, 1) result.append([max(0.0, offset), p]) - except (TypeError, ValueError, IndexError): + except (TypeError, ValueError, IndexError, OverflowError): continue return result @@ -124,7 +139,7 @@ def power_data_to_offsets( for item in power_data: try: result.append([float(item[0]), float(item[1])]) - except (TypeError, ValueError, IndexError): + except (TypeError, ValueError, IndexError, OverflowError): continue return result @@ -135,7 +150,7 @@ def power_data_to_offsets( parsed_start = dt_util.parse_datetime(start_time_iso) if parsed_start is not None: start_ts = parsed_start.timestamp() - except (ValueError, OSError) as e: + except (ValueError, OSError, OverflowError) as e: _LOGGER.debug("Failed to parse datetime %s: %s", start_time_iso, e) result: list[list[float]] = [] for item in power_data: @@ -148,7 +163,7 @@ def power_data_to_offsets( if start_ts is None: start_ts = ts.timestamp() result.append([round(ts.timestamp() - start_ts, 1), p]) - except (TypeError, ValueError, AttributeError, IndexError): + except (TypeError, ValueError, AttributeError, IndexError, OverflowError): continue return result @@ -161,7 +176,7 @@ def power_data_to_offsets( if parsed is None: return [] base_ts = parsed.timestamp() - except (ValueError, OSError) as e: + except (ValueError, OSError, OverflowError) as e: _LOGGER.debug("Failed to parse datetime %s: %s", start_time_iso, e) return [] @@ -198,7 +213,7 @@ def power_data_to_offsets( offset, p, len(result), ) result.append([max(0.0, offset), p]) - except (TypeError, ValueError, AttributeError, IndexError): + except (TypeError, ValueError, AttributeError, IndexError, OverflowError): continue return result @@ -206,39 +221,6 @@ def power_data_to_offsets( return [] -def power_data_offsets_to_datetimes( - power_data: PowerData, - start_time_iso: str, -) -> list[tuple[datetime, float]]: - """Convert stored ``[[offset_sec, power], ...]`` to ``[(datetime, power), ...]``. - - Args: - power_data: Offset-format power data. - start_time_iso: ISO-8601 cycle start time. - - Returns: - List of ``(datetime, power)`` tuples. Empty list on failure. - """ - try: - start_dt = dt_util.parse_datetime(start_time_iso) - if start_dt is None: - return [] - start_ts = start_dt.timestamp() - except Exception: # pylint: disable=broad-exception-caught - return [] - - result: list[tuple[datetime, float]] = [] - for item in power_data: - try: - offset = float(item[0]) - p = float(item[1]) - ts = datetime.fromtimestamp(start_ts + offset, tz=start_dt.tzinfo) - result.append((ts, p)) - except (TypeError, ValueError, IndexError): - continue - return result - - def migrate_power_data_to_offsets(cycle: dict[str, Any]) -> bool: """Migrate a single cycle's power_data to offset format in-place. diff --git a/custom_components/ha_washdata/translations/bg.json b/custom_components/ha_washdata/translations/bg.json index a259f705..9536d516 100644 --- a/custom_components/ha_washdata/translations/bg.json +++ b/custom_components/ha_washdata/translations/bg.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "Настройка на WashData", - "description": "Конфигурирайте вашата пералня или друг уред.\n\nНеобходим е сензор за мощност.\n\nСлед настройката отворете панела на WashData от страничната лента (или отидете на [/ha-washdata](/ha-washdata)), за да преглеждате цикли, да настроите откриването и да управлявате профили.", + "description": "Настройте пералня или друг уред. Нужен е сензор за мощност.\n\nСлед настройката отворете панела на WashData от страничната лента ([/ha-washdata](/ha-washdata)): там са циклите, профилите и всички настройки.", "data": { "name": "Име на устройството", "device_type": "Тип устройство", @@ -12,43 +12,41 @@ }, "data_description": { "name": "Удобно име за това устройство (напр. „Перална машина“, „Миялна машина“).", - "device_type": "Какъв тип уред е това? Помага за персонализиране на откриването и етикетирането.", - "power_sensor": "Сензорният обект, който отчита консумацията на енергия в реално време (във ватове) от вашия интелигентен щепсел.", - "min_power": "Отчитанията на мощност над този праг (във ватове) показват, че уредът работи. Започнете с 2W за повечето устройства." + "device_type": "Задава стойностите по подразбиране за откриване за този вид уред.", + "power_sensor": "Сензорът на смарт контакта, който отчита текущата мощност във ватове.", + "min_power": "Стойности над тази (във ватове) означават, че уредът работи. 2 W е подходящо за повечето устройства." } }, "reconfigure": { - "title": "Reconfigure WashData", - "description": "Обновете името на устройството, типа на уреда или сензора за мощност.\n\nПанелът на WashData (странична лента или [/ha-washdata](/ha-washdata)) е мястото, където се управляват откривате, циклите, профилите и известията.", + "title": "Преконфигуриране на WashData", + "description": "Променете името на устройството, типа на уреда или сензора за мощност.\n\nВсичко останало е в панела на WashData (странична лента или [/ha-washdata](/ha-washdata)).", "data": { - "name": "Device Name", - "device_type": "Device Type", - "power_sensor": "Power Sensor", - "min_power": "Minimum Power Threshold (W)" + "name": "Име на устройството", + "device_type": "Тип устройство", + "power_sensor": "Сензор за мощност", + "min_power": "Минимален праг на мощност (W)" } } }, "error": { - "cannot_connect": "Неуспешно свързване", - "invalid_auth": "Невалидно удостоверяване", "unknown": "Неочаквана грешка", - "invalid_power": "Power threshold must be greater than 0" + "invalid_power": "Прагът на мощност трябва да е по-голям от 0" }, "abort": { "already_configured": "Устройството вече е конфигурирано", - "reconfigure_successful": "Reconfiguration was successful" + "reconfigure_successful": "Преконфигурирането беше успешно" } }, "options": { "step": { "init": { - "title": "WashData Settings", - "description": "Преименувайте устройството или променете основните му хардуерни настройки. Всички настройки за откриване, съвпадение и известия са в [панела на WashData](/ha-washdata) (достъпен и от страничната лента).", + "title": "Настройки на WashData", + "description": "Преименувайте устройството или променете основните му настройки. Всичко останало е в [панела на WashData](/ha-washdata) в страничната лента.", "data": { "name": "Име на устройството", - "device_type": "Device Type", - "power_sensor": "Power Sensor", - "min_power": "Minimum Power Threshold (W)" + "device_type": "Тип устройство", + "power_sensor": "Сензор за мощност", + "min_power": "Минимален праг на мощност (W)" } } }, @@ -60,7 +58,7 @@ "notify_live_waiting_message": "{device}: Все още няма съвпадащ профил.", "vs_typical_longer": "{pct}% по-дълго от обичайното", "vs_typical_shorter": "{pct}% по-кратко от обичайното", - "invalid_power": "Power threshold must be greater than 0" + "invalid_power": "Прагът на мощност трябва да е по-голям от 0" } }, "selector": { @@ -84,7 +82,7 @@ "description": "Присвоете съществуващ профил към минал цикъл или премахнете етикета.", "fields": { "device_id": { - "name": "устройство", + "name": "Устройство", "description": "Устройството на пералната машина за етикетиране." }, "cycle_id": { @@ -93,7 +91,7 @@ }, "profile_name": { "name": "Име на профил", - "description": "Името на съществуващ профил (създайте профили в менюто Управление на профили). Оставете празно, за да премахнете етикета." + "description": "Име на съществуващ профил (профилите се създават в панела на WashData). Оставете празно, за да премахнете етикета." } } }, @@ -143,13 +141,13 @@ }, "confidence_threshold": { "name": "Праг на доверие", - "description": "Минимална степен на доверие (0,50-0,95) за прилагане на етикети." + "description": "Минимална степен на доверие за съвпадение (0,50-0,95) за прилагане на етикети. Оставете празно, за да се използва настройката „Автоматично маркиране доверие“ на устройството." } } }, "export_config": { "name": "Експортиране на конфигурация", - "description": "Експортирайте профилите, циклите и настройките на това устройство в JSON файл (на устройство).", + "description": "Експортиране на профилите, циклите и настройките на това устройство в JSON файл.", "fields": { "device_id": { "name": "устройство", @@ -177,7 +175,7 @@ }, "submit_cycle_feedback": { "name": "Изпратете обратна връзка за цикъл", - "description": "Потвърдете или коригирайте автоматично открита програма след завършен цикъл. Въведете или `entry_id` (разширено) или `device_id` (препоръчително).", + "description": "Потвърдете или коригирайте откритата програма на завършен цикъл. Въведете `device_id` (препоръчително) или `entry_id`.", "fields": { "device_id": { "name": "устройство", @@ -272,18 +270,18 @@ } }, "trigger_ml_training": { - "name": "Trigger ML Training", + "name": "Стартиране на ML обучение", "description": "Ръчно преквалификация на моделите ML на устройство от собствените етикети на това устройство (експериментални).", "fields": { "device_id": { - "name": "Device", - "description": "The WashData device to retrain models for." + "name": "Устройство", + "description": "Устройството WashData, чиито модели да се преобучат." } } }, "mark_unloaded": { "name": "Маркирай като разтоварено", - "description": "Потвърдете, че готовото съдържание е извадено от устройство WashData, с което се изчиства състоянието \"Чисто\" и всяко напомняне за разтоварване.", + "description": "Потвърдете, че готовото пране е извадено. Изчиства състоянието \"Чисто\" и напомнянето за разтоварване.", "fields": { "device_id": { "name": "Устройство", @@ -299,6 +297,32 @@ }, "match_ambiguity": { "name": "Неяснота на съвпадението" + }, + "maintenance_due": { + "name": "Предстои поддръжка", + "state": { + "on": "Предстои", + "off": "Не предстои" + }, + "state_attributes": { + "due_task_ids": { + "name": "ID на предстоящи задачи" + }, + "due_tasks": { + "name": "Предстоящи задачи", + "state": { + "descale": "Премахване на котлен камък", + "filter_clean": "Почистване на филтъра", + "drum_clean": "Почистване на барабана", + "bearing_service": "Сервиз на лагерите", + "other": "Друго", + "salt": "Допълване на сол", + "rinse_aid": "Допълване на препарат за изплакване", + "lint_filter": "Почистване на филтъра за мъх", + "condenser_clean": "Почистване на кондензатора" + } + } + } } }, "sensor": { @@ -316,14 +340,22 @@ "anti_wrinkle": "Против бръчки", "interrupted": "Прекъснат", "force_stopped": "Принудително спряно", - "rinse": "Изплакване", "unknown": "Неизвестен", "clean": "Чисто", "delay_wait": "Изчакване за стартиране" + }, + "state_attributes": { + "cycle_anomaly": { + "name": "Аномалия на цикъла", + "state": { + "overrun": "Работи дълго", + "stalled": "Заседнал" + } + } } }, "washer_program": { - "name": "програма" + "name": "Програма" }, "time_remaining": { "name": "Оставащо време" diff --git a/custom_components/ha_washdata/translations/bs.json b/custom_components/ha_washdata/translations/bs.json index ce9a0b98..0822f8a5 100644 --- a/custom_components/ha_washdata/translations/bs.json +++ b/custom_components/ha_washdata/translations/bs.json @@ -2,8 +2,8 @@ "config": { "step": { "user": { - "title": "WashData Setup", - "description": "Konfigurišite svoju mašinu za pranje veša ili drugi uređaj.\n\nSenzor snage je obavezan.\n\nNakon postavljanja otvorite WashData panel s bočne trake (ili idite na [/ha-washdata](/ha-washdata)) za pregled ciklusa, podešavanje otkrivanja i upravljanje profilima.", + "title": "Postavljanje WashData", + "description": "Postavite mašinu za pranje veša ili drugi uređaj. Potreban je senzor snage.\n\nNakon postavljanja otvorite WashData panel s bočne trake ([/ha-washdata](/ha-washdata)) za cikluse, profile i sve postavke.", "data": { "name": "Naziv uređaja", "device_type": "Tip uređaja", @@ -12,25 +12,23 @@ }, "data_description": { "name": "Prijateljski naziv za ovaj uređaj (npr. 'Mašina za pranje veša', 'Mašina za pranje sudova').", - "device_type": "Koja je ovo vrsta aparata? Pomaže u otkrivanju i označavanju po mjeri.", - "power_sensor": "Senzorski entitet koji izvještava o potrošnji energije u realnom vremenu (u vatima) iz vašeg pametnog priključka.", - "min_power": "Očitavanja snage iznad ovog praga (u vatima) pokazuju da uređaj radi. Počnite sa 2W za većinu uređaja." + "device_type": "Postavlja zadane vrijednosti detekcije za ovu vrstu uređaja.", + "power_sensor": "Senzor pametnog utikača koji javlja trenutnu snagu u vatima.", + "min_power": "Očitanja iznad ove vrijednosti (u vatima) znače da uređaj radi. 2 W odgovara većini uređaja." } }, "reconfigure": { - "title": "Reconfigure WashData", - "description": "Ažurirajte naziv uređaja, tip uređaja ili senzor snage.\n\nWashData panel (bočna traka ili [/ha-washdata](/ha-washdata)) služi za upravljanje otkrivanjem, ciklusima, profilima i obavještenjima.", + "title": "Rekonfiguracija WashData", + "description": "Promijenite naziv uređaja, tip uređaja ili senzor snage.\n\nSve ostalo je u WashData panelu (bočna traka ili [/ha-washdata](/ha-washdata)).", "data": { - "name": "Device Name", - "device_type": "Device Type", - "power_sensor": "Power Sensor", - "min_power": "Minimum Power Threshold (W)" + "name": "Naziv uređaja", + "device_type": "Tip uređaja", + "power_sensor": "Senzor snage", + "min_power": "Minimalni prag snage (W)" } } }, "error": { - "cannot_connect": "Povezivanje nije uspjelo", - "invalid_auth": "Nevažeća autentifikacija", "unknown": "Neočekivana greška", "invalid_power": "Prag snage mora biti veći od 0" }, @@ -42,13 +40,13 @@ "options": { "step": { "init": { - "title": "WashData Settings", - "description": "Preimenujte uređaj ili promijenite njegove osnovne postavke hardvera. Sve postavke otkrivanja, podudaranja i obavještavanja nalaze se na [WashData panelu](/ha-washdata) (dostupan i s bočne trake).", + "title": "Postavke WashData", + "description": "Preimenujte uređaj ili promijenite njegove osnovne postavke. Sve ostalo je u [WashData panelu](/ha-washdata) na bočnoj traci.", "data": { "name": "Naziv uređaja", - "device_type": "Device Type", - "power_sensor": "Power Sensor", - "min_power": "Minimum Power Threshold (W)" + "device_type": "Tip uređaja", + "power_sensor": "Senzor snage", + "min_power": "Minimalni prag snage (W)" } } }, @@ -74,7 +72,7 @@ "bread_maker": "Pekač hljeba", "pump": "Pumpa / Drenažna pumpa", "generic": "Ostalo (napredno)", - "other": "Threshold Device" + "other": "Uređaj s pragom" } } }, @@ -93,7 +91,7 @@ }, "profile_name": { "name": "Ime profila", - "description": "Naziv postojećeg profila (kreirajte profile u meniju Upravljanje profilima). Ostavite prazno da uklonite naljepnicu." + "description": "Naziv postojećeg profila (profile kreirajte u WashData panelu). Ostavite prazno da uklonite oznaku." } } }, @@ -143,13 +141,13 @@ }, "confidence_threshold": { "name": "Prag povjerenja", - "description": "Minimalna pouzdanost podudaranja (0,50-0,95) za primjenu oznaka." + "description": "Minimalna pouzdanost podudaranja (0,50-0,95) za primjenu oznaka. Ostavite prazno da se koristi postavka uređaja „Pouzdanost automatskog označavanja“." } } }, "export_config": { "name": "Izvoz konfig", - "description": "Izvezite profile, cikluse i postavke ovog uređaja u JSON datoteku (po uređaju).", + "description": "Izvezite profile, cikluse i postavke ovog uređaja u JSON datoteku.", "fields": { "device_id": { "name": "Uređaj", @@ -177,7 +175,7 @@ }, "submit_cycle_feedback": { "name": "Pošaljite povratne informacije o ciklusu", - "description": "Potvrdite ili ispravite automatski otkriveni program nakon završenog ciklusa. Navedite ili `entry_id` (napredno) ili `device_id` (preporučeno).", + "description": "Potvrdite ili ispravite otkriveni program završenog ciklusa. Navedite `device_id` (preporučeno) ili `entry_id`.", "fields": { "device_id": { "name": "Uređaj", @@ -283,7 +281,7 @@ }, "mark_unloaded": { "name": "Označi kao istovareno", - "description": "Potvrdite da je gotov sadržaj izvađen iz WashData uređaja, čime se briše stanje \"Čisto\" i svaki podsjetnik za istovar.", + "description": "Potvrdite da je gotov sadržaj izvađen. Briše stanje \"Čisto\" i podsjetnik za istovar.", "fields": { "device_id": { "name": "Uređaj", @@ -299,6 +297,32 @@ }, "match_ambiguity": { "name": "Dvosmislenost podudaranja" + }, + "maintenance_due": { + "name": "Dospjelo održavanje", + "state": { + "on": "Dospjelo", + "off": "Nije dospjelo" + }, + "state_attributes": { + "due_task_ids": { + "name": "ID-ovi dospjelih zadataka" + }, + "due_tasks": { + "name": "Dospjeli zadaci", + "state": { + "descale": "Uklanjanje kamenca", + "filter_clean": "Očisti filter", + "drum_clean": "Očisti bubanj", + "bearing_service": "Servis ležajeva", + "other": "Ostalo", + "salt": "Dopuni so", + "rinse_aid": "Dopuni sredstvo za ispiranje", + "lint_filter": "Očisti filter za dlačice", + "condenser_clean": "Očisti kondenzator" + } + } + } } }, "sensor": { @@ -316,10 +340,18 @@ "anti_wrinkle": "Anti-gužvanje", "interrupted": "Prekinut", "force_stopped": "Prisilno zaustavljeno", - "rinse": "Isperite", "unknown": "Nepoznato", "clean": "Čisto", "delay_wait": "Čeka se početak" + }, + "state_attributes": { + "cycle_anomaly": { + "name": "Anomalija ciklusa", + "state": { + "overrun": "Traje predugo", + "stalled": "Zaglavljeno" + } + } } }, "washer_program": { diff --git a/custom_components/ha_washdata/translations/cs.json b/custom_components/ha_washdata/translations/cs.json index 5aaa6125..b9cca49f 100644 --- a/custom_components/ha_washdata/translations/cs.json +++ b/custom_components/ha_washdata/translations/cs.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "Nastavení WashData", - "description": "Nakonfigurujte si pračku nebo jiný spotřebič.\n\nSenzor výkonu je vyžadován.\n\nPo nastavení otevřete panel WashData z postranního panelu (nebo přejděte na [/ha-washdata](/ha-washdata)) pro zobrazení cyklů, ladění detekce a správu profilů.", + "description": "Nastavte pračku nebo jiný spotřebič. Je potřeba senzor výkonu.\n\nPo nastavení otevřete panel WashData z postranního panelu ([/ha-washdata](/ha-washdata)), kde najdete cykly, profily a všechna nastavení.", "data": { "name": "Název zařízení", "device_type": "Typ zařízení", @@ -12,25 +12,23 @@ }, "data_description": { "name": "Popisný název pro toto zařízení (např. 'Pračka', 'Myčka nádobí').", - "device_type": "O jaký typ spotřebiče se jedná? Pomáhá přizpůsobit detekci a označování.", - "power_sensor": "Entita senzoru, která hlásí spotřebu energie v reálném čase (ve wattech) z vaší chytré zástrčky.", - "min_power": "Údaje o výkonu nad touto prahovou hodnotou (ve wattech) indikují, že spotřebič běží. U většiny zařízení začněte s 2W." + "device_type": "Nastaví výchozí hodnoty detekce pro tento druh spotřebiče.", + "power_sensor": "Senzor chytré zásuvky, který hlásí aktuální výkon ve wattech.", + "min_power": "Hodnoty nad touto mezí (ve wattech) znamenají, že spotřebič běží. Pro většinu zařízení stačí 2 W." } }, "reconfigure": { - "title": "Reconfigure WashData", - "description": "Aktualizujte název zařízení, typ spotřebiče nebo senzor výkonu.\n\nPanel WashData (postranní panel nebo [/ha-washdata](/ha-washdata)) slouží ke správě detekce, cyklů, profilů a oznámení.", + "title": "Rekonfigurace WashData", + "description": "Změňte název zařízení, typ spotřebiče nebo senzor výkonu.\n\nVše ostatní je v panelu WashData (postranní panel nebo [/ha-washdata](/ha-washdata)).", "data": { - "name": "Device Name", - "device_type": "Device Type", - "power_sensor": "Power Sensor", - "min_power": "Minimum Power Threshold (W)" + "name": "Název zařízení", + "device_type": "Typ zařízení", + "power_sensor": "Výkonový senzor", + "min_power": "Minimální práh výkonu (W)" } } }, "error": { - "cannot_connect": "Připojení se nezdařilo", - "invalid_auth": "Neplatné ověření", "unknown": "Neočekávaná chyba", "invalid_power": "Práh výkonu musí být větší než 0" }, @@ -42,13 +40,13 @@ "options": { "step": { "init": { - "title": "WashData Settings", - "description": "Přejmenujte zařízení nebo změňte jeho základní hardwarová nastavení. Veškerá nastavení detekce, porovnávání a oznámení jsou na [panelu WashData](/ha-washdata) (dostupný také z postranního panelu).", + "title": "Nastavení WashData", + "description": "Přejmenujte zařízení nebo změňte jeho základní nastavení. Vše ostatní je v [panelu WashData](/ha-washdata) v postranním panelu.", "data": { "name": "Název zařízení", - "device_type": "Device Type", - "power_sensor": "Power Sensor", - "min_power": "Minimum Power Threshold (W)" + "device_type": "Typ zařízení", + "power_sensor": "Výkonový senzor", + "min_power": "Minimální práh výkonu (W)" } } }, @@ -93,7 +91,7 @@ }, "profile_name": { "name": "Název profilu", - "description": "Název existujícího profilu (profily vytvořte v nabídce Správa profilů). Chcete-li štítek odstranit, ponechte prázdné." + "description": "Název existujícího profilu (profily vytvoříte v panelu WashData). Ponechte prázdné pro odebrání štítku." } } }, @@ -143,13 +141,13 @@ }, "confidence_threshold": { "name": "Práh důvěry", - "description": "Minimální spolehlivost shody (0,50–0,95) pro použití štítků." + "description": "Minimální spolehlivost shody (0,50–0,95) pro použití štítků. Ponechte prázdné, chcete-li použít nastavení zařízení „Důvěra auto-označení“." } } }, "export_config": { "name": "Export konfigurace", - "description": "Exportujte profily, cykly a nastavení tohoto zařízení do souboru JSON (na zařízení).", + "description": "Exportuje profily, cykly a nastavení tohoto zařízení do souboru JSON.", "fields": { "device_id": { "name": "Zařízení", @@ -177,7 +175,7 @@ }, "submit_cycle_feedback": { "name": "Odeslat zpětnou vazbu k cyklu", - "description": "Po dokončení cyklu potvrďte nebo opravte automaticky detekovaný program. Zadejte buď `entry_id` (pokročilé) nebo `device_id` (doporučeno).", + "description": "Potvrďte nebo opravte detekovaný program dokončeného cyklu. Zadejte `device_id` (doporučeno) nebo `entry_id`.", "fields": { "device_id": { "name": "Zařízení", @@ -283,7 +281,7 @@ }, "mark_unloaded": { "name": "Označit jako vyložené", - "description": "Potvrďte, že dokončená náplň byla vyjmuta ze zařízení WashData, čímž se zruší stav „Čistý“ i případná připomínka vyložení.", + "description": "Potvrďte, že dokončená náplň byla vyjmuta. Zruší stav „Čistý“ i připomínku vyložení.", "fields": { "device_id": { "name": "Zařízení", @@ -299,6 +297,32 @@ }, "match_ambiguity": { "name": "Nejednoznačnost shody" + }, + "maintenance_due": { + "name": "Údržba na řadě", + "state": { + "on": "Na řadě", + "off": "Není na řadě" + }, + "state_attributes": { + "due_task_ids": { + "name": "ID úkolů na řadě" + }, + "due_tasks": { + "name": "Úkoly na řadě", + "state": { + "descale": "Odvápnění", + "filter_clean": "Vyčistit filtr", + "drum_clean": "Vyčistit buben", + "bearing_service": "Servis ložisek", + "other": "Jiné", + "salt": "Doplnit sůl", + "rinse_aid": "Doplnit leštidlo", + "lint_filter": "Vyčistit filtr na vlákna", + "condenser_clean": "Vyčistit kondenzátor" + } + } + } } }, "sensor": { @@ -316,10 +340,18 @@ "anti_wrinkle": "Proti pomačkání", "interrupted": "Přerušeno", "force_stopped": "Nuceně zastaveno", - "rinse": "Máchání", "unknown": "Neznámý", "clean": "Čistý", "delay_wait": "Čekání na spuštění" + }, + "state_attributes": { + "cycle_anomaly": { + "name": "Anomálie cyklu", + "state": { + "overrun": "Běží dlouho", + "stalled": "Zaseknuto" + } + } } }, "washer_program": { diff --git a/custom_components/ha_washdata/translations/da.json b/custom_components/ha_washdata/translations/da.json index cf59215a..cf96f293 100644 --- a/custom_components/ha_washdata/translations/da.json +++ b/custom_components/ha_washdata/translations/da.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "WashData opsætning", - "description": "Konfigurer din vaskemaskine eller andet apparat.\n\nStrømsensor er påkrævet.\n\nEfter opsætning skal du åbne WashData-panelet fra sidebjælken (eller gå til [/ha-washdata](/ha-washdata)) for at se cyklusser, justere registrering og administrere profiler.", + "description": "Opsæt en vaskemaskine eller et andet apparat. En effektsensor er påkrævet.\n\nÅbn efter opsætningen WashData-panelet fra sidebjælken ([/ha-washdata](/ha-washdata)) for at se cyklusser, profiler og alle indstillinger.", "data": { "name": "Enhedens navn", "device_type": "Enhedstype", @@ -12,55 +12,53 @@ }, "data_description": { "name": "Et venligt navn for denne enhed (f.eks. \"Vaskemaskine\", \"Opvaskemaskine\").", - "device_type": "Hvilken type apparat er dette? Hjælper med at skræddersy detektering og mærkning.", - "power_sensor": "Sensorenheden, der rapporterer strømforbrug i realtid (i watt) fra dit smartstik.", - "min_power": "Effektaflæsninger over denne tærskel (i watt) indikerer, at apparatet kører. Start med 2W for de fleste enheder." + "device_type": "Indstiller registreringsstandarder for denne type apparat.", + "power_sensor": "Smartstikkets sensor, der rapporterer aktuel effekt i watt.", + "min_power": "Målinger over denne værdi (i watt) betyder, at apparatet kører. 2 W passer til de fleste enheder." } }, "reconfigure": { - "title": "Reconfigure WashData", - "description": "Opdater enhedsnavnet, apparattypen eller strømsensoren.\n\nWashData-panelet (sidebjælken eller [/ha-washdata](/ha-washdata)) er, hvor registrering, cyklusser, profiler og notifikationer administreres.", + "title": "Genkonfigurer WashData", + "description": "Skift enhedsnavn, apparattype eller effektsensor.\n\nAlt andet findes i WashData-panelet (sidebjælken eller [/ha-washdata](/ha-washdata)).", "data": { - "name": "Device Name", - "device_type": "Device Type", - "power_sensor": "Power Sensor", - "min_power": "Minimum Power Threshold (W)" + "name": "Enhedens navn", + "device_type": "Enhedstype", + "power_sensor": "Strømsensor", + "min_power": "Minimum effekttærskel (W)" } } }, "error": { - "cannot_connect": "Kunne ikke oprette forbindelse", - "invalid_auth": "Ugyldig godkendelse", "unknown": "Uventet fejl", - "invalid_power": "Power threshold must be greater than 0" + "invalid_power": "Effekttærsklen skal være større end 0" }, "abort": { "already_configured": "Enheden er allerede konfigureret", - "reconfigure_successful": "Reconfiguration was successful" + "reconfigure_successful": "Genkonfigurationen lykkedes" } }, "options": { "step": { "init": { - "title": "WashData Settings", - "description": "Omdøb enheden eller skift dens grundlæggende hardwareindstillinger. Alle indstillinger for registrering, matching og notifikationer findes i [WashData-panelet](/ha-washdata) (også tilgængeligt fra sidebjælken).", + "title": "WashData-indstillinger", + "description": "Omdøb enheden eller skift dens grundindstillinger. Alt andet findes i [WashData-panelet](/ha-washdata) i sidebjælken.", "data": { "name": "Enhedsnavn", - "device_type": "Device Type", - "power_sensor": "Power Sensor", - "min_power": "Minimum Power Threshold (W)" + "device_type": "Enhedstype", + "power_sensor": "Strømsensor", + "min_power": "Minimum effekttærskel (W)" } } }, "error": { - "timer_default_message": "{device}: {minutes} min timer", + "timer_default_message": "{device}: timer på {minutes} min", "timer_pause_action_title": "Genoptag cyklus", "timer_pause_body_suffix": "Cyklussen er sat på pause. Åbn WashData-panelet for at genoptage.", "unload_dismiss_action_title": "Stop påmindelser", "notify_live_waiting_message": "{device}: Ingen profil genkendt endnu.", "vs_typical_longer": "{pct}% længere end normalt", "vs_typical_shorter": "{pct}% kortere end normalt", - "invalid_power": "Power threshold must be greater than 0" + "invalid_power": "Effekttærsklen skal være større end 0" } }, "selector": { @@ -70,7 +68,7 @@ "dryer": "Tørretumbler", "washer_dryer": "Vaske-tørretumbler kombi", "dishwasher": "Opvaskemaskine", - "air_fryer": "Air Fryer", + "air_fryer": "Airfryer", "bread_maker": "Brødmaskine", "pump": "Pumpe / Sump Pumpe", "generic": "Andet (avanceret)", @@ -93,7 +91,7 @@ }, "profile_name": { "name": "Profilnavn", - "description": "Navnet på en eksisterende profil (opret profiler i menuen Administrer profiler). Lad være tomt for at fjerne etiketten." + "description": "Navnet på en eksisterende profil (opret profiler i WashData-panelet). Lad være tomt for at fjerne mærkningen." } } }, @@ -143,13 +141,13 @@ }, "confidence_threshold": { "name": "Tillidsgrænse", - "description": "Mindste matchkonfidens (0,50-0,95) for at anvende etiketter." + "description": "Mindste matchkonfidens (0,50-0,95) for at anvende etiketter. Lad feltet stå tomt for at bruge enhedens indstilling \"Auto-mærkning konfidens\"." } } }, "export_config": { "name": "Eksporter Konfig", - "description": "Eksporter denne enheds profiler, cyklusser og indstillinger til en JSON-fil (pr. enhed).", + "description": "Eksporter denne enheds profiler, cyklusser og indstillinger til en JSON-fil.", "fields": { "device_id": { "name": "Enhed", @@ -177,7 +175,7 @@ }, "submit_cycle_feedback": { "name": "Indsend cyklusfeedback", - "description": "Bekræft eller ret et auto-detekteret program efter en afsluttet cyklus. Angiv enten \"entry_id\" (avanceret) eller \"device_id\" (anbefales).", + "description": "Bekræft eller ret det registrerede program for en afsluttet cyklus. Angiv `device_id` (anbefales) eller `entry_id`.", "fields": { "device_id": { "name": "Enhed", @@ -272,18 +270,18 @@ } }, "trigger_ml_training": { - "name": "Trigger ML Training", - "description": "Manually retrain the on-device ML models from this device's own labelled cycles (experimental).", + "name": "Start ML-træning", + "description": "Gentræn ML-modellerne på enheden manuelt ud fra enhedens egne mærkede cyklusser (eksperimentel).", "fields": { "device_id": { - "name": "Device", - "description": "The WashData device to retrain models for." + "name": "Enhed", + "description": "WashData-enheden, hvis modeller skal gentrænes." } } }, "mark_unloaded": { "name": "Markér som aflæsset", - "description": "Bekræft, at den færdige vask er taget ud af en WashData-enhed, hvilket rydder tilstanden \"Ren\" og en eventuel aflæsningspåmindelse.", + "description": "Bekræft, at den færdige vask er taget ud. Rydder tilstanden \"Ren\" og en eventuel aflæsningspåmindelse.", "fields": { "device_id": { "name": "Enhed", @@ -299,6 +297,32 @@ }, "match_ambiguity": { "name": "Matchtvetydighed" + }, + "maintenance_due": { + "name": "Vedligeholdelse forfalder", + "state": { + "on": "Forfalden", + "off": "Ikke forfalden" + }, + "state_attributes": { + "due_task_ids": { + "name": "Id'er for forfaldne opgaver" + }, + "due_tasks": { + "name": "Forfaldne opgaver", + "state": { + "descale": "Afkalk", + "filter_clean": "Rengør filter", + "drum_clean": "Rengør tromle", + "bearing_service": "Lejeservice", + "other": "Andet", + "salt": "Påfyld salt", + "rinse_aid": "Påfyld afspændingsmiddel", + "lint_filter": "Rengør fnugfilter", + "condenser_clean": "Rengør kondensator" + } + } + } } }, "sensor": { @@ -316,10 +340,18 @@ "anti_wrinkle": "Anti-Rynke", "interrupted": "Afbrudt", "force_stopped": "Tving stoppet", - "rinse": "Skylle", "unknown": "Ukendt", "clean": "Ren", "delay_wait": "Venter på at starte" + }, + "state_attributes": { + "cycle_anomaly": { + "name": "Cyklusanomali", + "state": { + "overrun": "Kører længere", + "stalled": "Gået i stå" + } + } } }, "washer_program": { diff --git a/custom_components/ha_washdata/translations/de.json b/custom_components/ha_washdata/translations/de.json index 7836d648..682bfc4f 100644 --- a/custom_components/ha_washdata/translations/de.json +++ b/custom_components/ha_washdata/translations/de.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "WashData-Setup", - "description": "Konfigurieren Sie Ihre Waschmaschine oder ein anderes Gerät.\n\nLeistungssensor ist erforderlich.\n\nNach der Einrichtung öffnen Sie das WashData-Panel über die Seitenleiste (oder gehen Sie zu [/ha-washdata](/ha-washdata)), um Zyklen anzuzeigen, die Erkennung anzupassen und Profile zu verwalten.", + "description": "Richten Sie eine Waschmaschine oder ein anderes Gerät ein. Ein Leistungssensor ist erforderlich.\n\nÖffnen Sie nach der Einrichtung das WashData-Panel über die Seitenleiste ([/ha-washdata](/ha-washdata)), um Zyklen, Profile und alle Einstellungen zu sehen.", "data": { "name": "Gerätename", "device_type": "Gerätetyp", @@ -12,43 +12,41 @@ }, "data_description": { "name": "Ein benutzerfreundlicher Name für dieses Gerät (z. B. „Waschmaschine“, „Geschirrspüler“).", - "device_type": "Um welche Art von Gerät handelt es sich? Dies hilft bei der individuellen Erkennung und Zuordnung.", - "power_sensor": "Die Sensoreinheit, die den Stromverbrauch (in Watt) Ihres Smart Plugs in Echtzeit meldet.", - "min_power": "Leistungswerte über diesem Schwellwert (in Watt) zeigen an, dass das Gerät läuft. Für die meisten Geräte ist 2W ein guter Anfang." + "device_type": "Legt die Erkennungsstandards für diese Geräteart fest.", + "power_sensor": "Der Smart-Plug-Sensor, der die aktuelle Leistung in Watt meldet.", + "min_power": "Werte über diesem Wert (in Watt) bedeuten, dass das Gerät läuft. 2 W passen für die meisten Geräte." } }, "reconfigure": { - "title": "Reconfigure WashData", - "description": "Aktualisieren Sie den Gerätenamen, den Gerätetyp oder den Leistungssensor.\n\nDas WashData-Panel (Seitenleiste oder [/ha-washdata](/ha-washdata)) ist der Ort, an dem Erkennung, Zyklen, Profile und Benachrichtigungen verwaltet werden.", + "title": "WashData neu konfigurieren", + "description": "Ändern Sie den Gerätenamen, den Gerätetyp oder den Leistungssensor.\n\nAlles andere finden Sie im WashData-Panel (Seitenleiste oder [/ha-washdata](/ha-washdata)).", "data": { - "name": "Device Name", - "device_type": "Device Type", - "power_sensor": "Power Sensor", - "min_power": "Minimum Power Threshold (W)" + "name": "Gerätename", + "device_type": "Gerätetyp", + "power_sensor": "Leistungssensor", + "min_power": "Mindestleistungsschwelle (W)" } } }, "error": { - "cannot_connect": "Verbindung konnte nicht hergestellt werden", - "invalid_auth": "Ungültige Authentifizierung", "unknown": "Unerwarteter Fehler", - "invalid_power": "Power threshold must be greater than 0" + "invalid_power": "Die Leistungsschwelle muss größer als 0 sein" }, "abort": { "already_configured": "Gerät ist bereits konfiguriert", - "reconfigure_successful": "Reconfiguration was successful" + "reconfigure_successful": "Die Neukonfiguration war erfolgreich" } }, "options": { "step": { "init": { - "title": "WashData Settings", - "description": "Benennen Sie das Gerät um oder ändern Sie seine grundlegenden Hardware-Einstellungen. Alle Erkennungs-, Abgleich- und Benachrichtigungseinstellungen befinden sich im [WashData-Panel](/ha-washdata) (auch über die Seitenleiste erreichbar).", + "title": "WashData-Einstellungen", + "description": "Benennen Sie das Gerät um oder ändern Sie seine Grundeinstellungen. Alles andere finden Sie im [WashData-Panel](/ha-washdata) in der Seitenleiste.", "data": { "name": "Name des Geräts", - "device_type": "Device Type", - "power_sensor": "Power Sensor", - "min_power": "Minimum Power Threshold (W)" + "device_type": "Gerätetyp", + "power_sensor": "Leistungssensor", + "min_power": "Mindestleistungsschwelle (W)" } } }, @@ -60,7 +58,7 @@ "notify_live_waiting_message": "{device}: Noch kein Profil erkannt.", "vs_typical_longer": "{pct}% länger als üblich", "vs_typical_shorter": "{pct}% kürzer als üblich", - "invalid_power": "Power threshold must be greater than 0" + "invalid_power": "Die Leistungsschwelle muss größer als 0 sein" } }, "selector": { @@ -93,7 +91,7 @@ }, "profile_name": { "name": "Profilname", - "description": "Der Name eines vorhandenen Profils (Profile erstellen im Menü „Profile verwalten“). Feld leer lassen, um die Zuordnung zu entfernen." + "description": "Name eines vorhandenen Profils (Profile erstellen Sie im WashData-Panel). Leer lassen, um die Beschriftung zu entfernen." } } }, @@ -143,13 +141,13 @@ }, "confidence_threshold": { "name": "Vertrauensschwellwert", - "description": "Mindestübereinstimmungskonfidenz (0,50–0,95) zum Anwenden von Zuordnungen." + "description": "Mindestübereinstimmungskonfidenz (0,50–0,95) zum Anwenden von Zuordnungen. Lassen Sie das Feld leer, um die Geräteeinstellung „Automatische Beschriftungskonfidenz“ zu verwenden." } } }, "export_config": { "name": "Konfiguration exportieren", - "description": "Profile, Zyklen und Einstellungen dieses Geräts in eine JSON-Datei exportieren (pro Gerät).", + "description": "Profile, Zyklen und Einstellungen dieses Geräts in eine JSON-Datei exportieren.", "fields": { "device_id": { "name": "Gerät", @@ -177,7 +175,7 @@ }, "submit_cycle_feedback": { "name": "Zyklus-Feedback bestätigen", - "description": "Ein automatisch erkanntes Programm nach einem abgeschlossenen Zyklus bestätigen oder korrigieren. Geben Sie entweder „entry_id“ (erweitert) oder „device_id“ (empfohlen) an.", + "description": "Das erkannte Programm eines abgeschlossenen Zyklus bestätigen oder korrigieren. Geben Sie `device_id` (empfohlen) oder `entry_id` an.", "fields": { "device_id": { "name": "Gerät", @@ -272,18 +270,18 @@ } }, "trigger_ml_training": { - "name": "Trigger ML Training", - "description": "Manually retrain the on-device ML models from this device's own labelled cycles (experimental).", + "name": "ML-Training starten", + "description": "Die ML-Modelle auf dem Gerät manuell aus den eigenen zugeordneten Zyklen dieses Geräts neu trainieren (experimentell).", "fields": { "device_id": { - "name": "Device", - "description": "The WashData device to retrain models for." + "name": "Gerät", + "description": "Das WashData-Gerät, dessen Modelle neu trainiert werden sollen." } } }, "mark_unloaded": { "name": "Als entladen markieren", - "description": "Bestätigen Sie, dass die fertige Ladung aus einem WashData-Gerät entnommen wurde. Damit werden der Zustand \"Sauber\" und eine eventuelle Entladeerinnerung zurückgesetzt.", + "description": "Bestätigen, dass die fertige Ladung entnommen wurde. Setzt den Zustand „Sauber“ und eine eventuelle Entladeerinnerung zurück.", "fields": { "device_id": { "name": "Gerät", @@ -299,6 +297,32 @@ }, "match_ambiguity": { "name": "Übereinstimmungsmehrdeutigkeit" + }, + "maintenance_due": { + "name": "Wartung fällig", + "state": { + "on": "Fällig", + "off": "Nicht fällig" + }, + "state_attributes": { + "due_task_ids": { + "name": "IDs fälliger Aufgaben" + }, + "due_tasks": { + "name": "Fällige Aufgaben", + "state": { + "descale": "Entkalken", + "filter_clean": "Filter reinigen", + "drum_clean": "Trommel reinigen", + "bearing_service": "Lagerwartung", + "other": "Sonstiges", + "salt": "Salz nachfüllen", + "rinse_aid": "Klarspüler nachfüllen", + "lint_filter": "Flusensieb reinigen", + "condenser_clean": "Kondensator reinigen" + } + } + } } }, "sensor": { @@ -316,10 +340,18 @@ "anti_wrinkle": "Knitterschutz", "interrupted": "Unterbrochen", "force_stopped": "Erzwungen beendet", - "rinse": "Spülen", "unknown": "Unbekannt", "clean": "Sauber", "delay_wait": "Warten auf den Start" + }, + "state_attributes": { + "cycle_anomaly": { + "name": "Zyklusanomalie", + "state": { + "overrun": "Läuft länger", + "stalled": "Stillstand" + } + } } }, "washer_program": { diff --git a/custom_components/ha_washdata/translations/el.json b/custom_components/ha_washdata/translations/el.json index db0a5b56..1de48274 100644 --- a/custom_components/ha_washdata/translations/el.json +++ b/custom_components/ha_washdata/translations/el.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "Ρύθμιση WashData", - "description": "Ρυθμίστε το πλυντήριο ρούχων ή άλλη συσκευή σας.\n\nΑπαιτείται αισθητήρας ισχύος.\n\nΜετά τη ρύθμιση, ανοίξτε τον πίνακα WashData από την πλευρική γραμμή (ή μεταβείτε στο [/ha-washdata](/ha-washdata)) για να δείτε κύκλους, να ρυθμίσετε την ανίχνευση και να διαχειριστείτε προφίλ.", + "description": "Ρυθμίστε ένα πλυντήριο ρούχων ή άλλη συσκευή. Απαιτείται αισθητήρας ισχύος.\n\nΜετά τη ρύθμιση, ανοίξτε τον πίνακα WashData από την πλευρική γραμμή ([/ha-washdata](/ha-washdata)) για να δείτε κύκλους, προφίλ και όλες τις ρυθμίσεις.", "data": { "name": "Όνομα συσκευής", "device_type": "Τύπος συσκευής", @@ -12,43 +12,41 @@ }, "data_description": { "name": "Ένα φιλικό όνομα για αυτή τη συσκευή (π.χ. «Πλυντήριο ρούχων», «Πλυντήριο πιάτων»).", - "device_type": "Τι είδους συσκευή είναι αυτή; Βοηθά στην προσαρμογή της ανίχνευσης και της επισήμανσης.", - "power_sensor": "Η οντότητα αισθητήρα που αναφέρει σε πραγματικό χρόνο την κατανάλωση ισχύος (σε watt) από το έξυπνο βύσμα σας.", - "min_power": "Μετρήσεις ισχύος πάνω από αυτό το όριο (σε watt) υποδεικνύουν ότι η συσκευή λειτουργεί. Ξεκινήστε με 2 W για τις περισσότερες συσκευές." + "device_type": "Ορίζει τις προεπιλογές ανίχνευσης για αυτό το είδος συσκευής.", + "power_sensor": "Ο αισθητήρας του έξυπνου βύσματος που αναφέρει τη ζωντανή ισχύ σε watt.", + "min_power": "Μετρήσεις πάνω από αυτό (σε watt) σημαίνουν ότι η συσκευή λειτουργεί. Τα 2 W ταιριάζουν στις περισσότερες συσκευές." } }, "reconfigure": { - "title": "Reconfigure WashData", - "description": "Ενημερώστε το όνομα της συσκευής, τον τύπο συσκευής ή τον αισθητήρα ισχύος.\n\nΟ πίνακας WashData (πλευρική γραμμή ή [/ha-washdata](/ha-washdata)) είναι το μέρος όπου διαχειρίζεται η ανίχνευση, οι κύκλοι, τα προφίλ και οι ειδοποιήσεις.", + "title": "Επαναδιαμόρφωση WashData", + "description": "Αλλάξτε το όνομα, τον τύπο συσκευής ή τον αισθητήρα ισχύος.\n\nΌλα τα υπόλοιπα βρίσκονται στον πίνακα WashData (πλευρική γραμμή ή [/ha-washdata](/ha-washdata)).", "data": { - "name": "Device Name", - "device_type": "Device Type", - "power_sensor": "Power Sensor", - "min_power": "Minimum Power Threshold (W)" + "name": "Όνομα συσκευής", + "device_type": "Τύπος συσκευής", + "power_sensor": "Αισθητήρας ισχύος", + "min_power": "Ελάχιστο όριο ισχύος (W)" } } }, "error": { - "cannot_connect": "Αποτυχία σύνδεσης", - "invalid_auth": "Μη έγκυρη αυθεντικοποίηση", "unknown": "Απρόσμενο σφάλμα", - "invalid_power": "Power threshold must be greater than 0" + "invalid_power": "Το όριο ισχύος πρέπει να είναι μεγαλύτερο από 0" }, "abort": { "already_configured": "Η συσκευή έχει ήδη διαμορφωθεί", - "reconfigure_successful": "Reconfiguration was successful" + "reconfigure_successful": "Η εκ νέου διαμόρφωση ήταν επιτυχής" } }, "options": { "step": { "init": { - "title": "WashData Settings", - "description": "Μετονομάστε τη συσκευή ή αλλάξτε τις βασικές ρυθμίσεις υλικού. Όλες οι ρυθμίσεις ανίχνευσης, αντιστοίχισης και ειδοποιήσεων βρίσκονται στον [πίνακα WashData](/ha-washdata) (επίσης προσβάσιμος από την πλευρική γραμμή).", + "title": "Ρυθμίσεις WashData", + "description": "Μετονομάστε τη συσκευή ή αλλάξτε τις βασικές ρυθμίσεις της. Όλα τα υπόλοιπα βρίσκονται στον [πίνακα WashData](/ha-washdata) στην πλευρική γραμμή.", "data": { "name": "Όνομα συσκευής", - "device_type": "Device Type", - "power_sensor": "Power Sensor", - "min_power": "Minimum Power Threshold (W)" + "device_type": "Τύπος συσκευής", + "power_sensor": "Αισθητήρας ισχύος", + "min_power": "Ελάχιστο όριο ισχύος (W)" } } }, @@ -60,7 +58,7 @@ "notify_live_waiting_message": "{device}: Δεν έχει αναγνωριστεί ακόμη κανένα προφίλ.", "vs_typical_longer": "{pct}% περισσότερο από το συνηθισμένο", "vs_typical_shorter": "{pct}% λιγότερο από το συνηθισμένο", - "invalid_power": "Power threshold must be greater than 0" + "invalid_power": "Το όριο ισχύος πρέπει να είναι μεγαλύτερο από 0" } }, "selector": { @@ -93,7 +91,7 @@ }, "profile_name": { "name": "Όνομα προφίλ", - "description": "Το όνομα ενός υπάρχοντος προφίλ (δημιουργήστε προφίλ από το μενού Διαχείριση προφίλ). Αφήστε κενό για αφαίρεση ετικέτας." + "description": "Όνομα υπάρχοντος προφίλ (τα προφίλ δημιουργούνται στον πίνακα WashData). Αφήστε κενό για να αφαιρέσετε την ετικέτα." } } }, @@ -143,13 +141,13 @@ }, "confidence_threshold": { "name": "Κατώφλι εμπιστοσύνης", - "description": "Ελάχιστη εμπιστοσύνη αντιστοίχισης (0,50-0,95) για εφαρμογή ετικετών." + "description": "Ελάχιστη εμπιστοσύνη αντιστοίχισης (0,50-0,95) για εφαρμογή ετικετών. Αφήστε το κενό για να χρησιμοποιηθεί η ρύθμιση Εμπιστοσύνη Αυτόματης Επισήμανσης της συσκευής." } } }, "export_config": { "name": "Εξαγωγή διαμόρφωσης", - "description": "Εξαγωγή προφίλ, κύκλων και ρυθμίσεων αυτής της συσκευής σε αρχείο JSON (ανά συσκευή).", + "description": "Εξαγωγή των προφίλ, των κύκλων και των ρυθμίσεων αυτής της συσκευής σε αρχείο JSON.", "fields": { "device_id": { "name": "Συσκευή", @@ -177,7 +175,7 @@ }, "submit_cycle_feedback": { "name": "Υποβολή ανατροφοδότησης κύκλου", - "description": "Επιβεβαιώστε ή διορθώστε ένα αυτόματα εντοπισμένο πρόγραμμα μετά από ολοκληρωμένο κύκλο. Δώστε είτε `entry_id` (σύνθετο) ή `device_id` (συνιστάται).", + "description": "Επιβεβαιώστε ή διορθώστε το πρόγραμμα που εντοπίστηκε σε έναν ολοκληρωμένο κύκλο. Δώστε `device_id` (συνιστάται) ή `entry_id`.", "fields": { "device_id": { "name": "Συσκευή", @@ -272,18 +270,18 @@ } }, "trigger_ml_training": { - "name": "Trigger ML Training", + "name": "Έναρξη εκπαίδευσης ML", "description": "Χειροκίνητα επανεκπαιδεύστε τα μοντέλα on-device ML από τους χαρακτηρισμένους κύκλους αυτής της συσκευής (πειραματικό).", "fields": { "device_id": { - "name": "Device", - "description": "The WashData device to retrain models for." + "name": "Συσκευή", + "description": "Η συσκευή WashData της οποίας τα μοντέλα θα επανεκπαιδευτούν." } } }, "mark_unloaded": { "name": "Σήμανση Αποφόρτωσης", - "description": "Επιβεβαιώνει ότι το ολοκληρωμένο φορτίο έχει αφαιρεθεί από μια συσκευή WashData, ακυρώνοντας την κατάσταση Καθαρός και κάθε υπενθύμιση αποφόρτωσης.", + "description": "Επιβεβαιώνει ότι το ολοκληρωμένο φορτίο αφαιρέθηκε. Ακυρώνει την κατάσταση Καθαρός και κάθε υπενθύμιση αποφόρτωσης.", "fields": { "device_id": { "name": "Συσκευή", @@ -299,6 +297,32 @@ }, "match_ambiguity": { "name": "Αμφισημία αντιστοίχισης" + }, + "maintenance_due": { + "name": "Οφειλόμενη συντήρηση", + "state": { + "on": "Οφείλεται", + "off": "Δεν οφείλεται" + }, + "state_attributes": { + "due_task_ids": { + "name": "Αναγνωριστικά οφειλόμενων εργασιών" + }, + "due_tasks": { + "name": "Οφειλόμενες εργασίες", + "state": { + "descale": "Αφαλάτωση", + "filter_clean": "Καθαρισμός φίλτρου", + "drum_clean": "Καθαρισμός κάδου", + "bearing_service": "Σέρβις ρουλεμάν", + "other": "Άλλο", + "salt": "Αναπλήρωση αλατιού", + "rinse_aid": "Αναπλήρωση λαμπρυντικού", + "lint_filter": "Καθαρισμός φίλτρου χνουδιού", + "condenser_clean": "Καθαρισμός συμπυκνωτή" + } + } + } } }, "sensor": { @@ -316,10 +340,18 @@ "anti_wrinkle": "Κατά ρυτίδων", "interrupted": "Διακόπηκε", "force_stopped": "Αναγκαστική διακοπή", - "rinse": "Ξέπλυμα", "unknown": "Άγνωστο", "clean": "Καθαρός", "delay_wait": "Αναμονή για την έναρξη" + }, + "state_attributes": { + "cycle_anomaly": { + "name": "Ανωμαλία κύκλου", + "state": { + "overrun": "Καθυστερεί", + "stalled": "Κόλλησε" + } + } } }, "washer_program": { diff --git a/custom_components/ha_washdata/translations/en.json b/custom_components/ha_washdata/translations/en.json index 6f81ed1d..c9270282 100644 --- a/custom_components/ha_washdata/translations/en.json +++ b/custom_components/ha_washdata/translations/en.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "WashData Setup", - "description": "Configure your washing machine or other appliance.\n\nPower sensor is required.\n\nAfter setup, open the WashData panel from the sidebar (or go to [/ha-washdata](/ha-washdata)) to view cycles, tune detection and manage profiles.", + "description": "Set up a washing machine or other appliance. A power sensor is required.\n\nAfter setup, open the WashData panel from the sidebar ([/ha-washdata](/ha-washdata)) to see cycles, profiles and all settings.", "data": { "name": "Device Name", "device_type": "Device Type", @@ -12,14 +12,14 @@ }, "data_description": { "name": "A friendly name for this device (e.g., 'Washing Machine', 'Dishwasher').", - "device_type": "What type of appliance is this? Helps tailor detection and labeling.", - "power_sensor": "The sensor entity that reports real-time power consumption (in watts) from your smart plug.", - "min_power": "Power readings above this threshold (in watts) indicate the appliance is running. Start with 2W for most devices." + "device_type": "Sets detection defaults for this kind of appliance.", + "power_sensor": "The smart plug sensor that reports live power in watts.", + "min_power": "Readings above this (in watts) mean the appliance is running. 2 W suits most devices." } }, "reconfigure": { "title": "Reconfigure WashData", - "description": "Update the device name, appliance type, or power sensor.\n\nThe WashData panel (sidebar, or [/ha-washdata](/ha-washdata)) is where detection, cycles, profiles and notifications are managed.", + "description": "Change the device name, appliance type or power sensor.\n\nEverything else is in the WashData panel (sidebar, or [/ha-washdata](/ha-washdata)).", "data": { "name": "Device Name", "device_type": "Device Type", @@ -29,8 +29,6 @@ } }, "error": { - "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication", "unknown": "Unexpected error", "invalid_power": "Power threshold must be greater than 0" }, @@ -43,7 +41,7 @@ "step": { "init": { "title": "WashData Settings", - "description": "Rename the device or change its core hardware settings. All detection, matching and notification settings are in the [WashData panel](/ha-washdata) (also reachable from the sidebar).", + "description": "Rename the device or change its core settings. Everything else is in the [WashData panel](/ha-washdata) in the sidebar.", "data": { "name": "Device Name", "device_type": "Device Type", @@ -93,7 +91,7 @@ }, "profile_name": { "name": "Profile Name", - "description": "The name of an existing profile (create profiles in Manage Profiles menu). Leave blank to remove label." + "description": "An existing profile name (create profiles in the WashData panel). Leave blank to remove the label." } } }, @@ -143,13 +141,13 @@ }, "confidence_threshold": { "name": "Confidence Threshold", - "description": "Minimum match confidence (0.50-0.95) to apply labels." + "description": "Minimum match confidence (0.50-0.95) to apply labels. Leave empty to use the device's Auto-Label Confidence setting." } } }, "export_config": { "name": "Export Config", - "description": "Export this device's profiles, cycles, and settings to a JSON file (per device).", + "description": "Export this device's profiles, cycles and settings to a JSON file.", "fields": { "device_id": { "name": "Device", @@ -177,7 +175,7 @@ }, "submit_cycle_feedback": { "name": "Submit Cycle Feedback", - "description": "Confirm or correct an auto-detected program after a completed cycle. Provide either `entry_id` (advanced) or `device_id` (recommended).", + "description": "Confirm or correct the detected program of a finished cycle. Give `device_id` (recommended) or `entry_id`.", "fields": { "device_id": { "name": "Device", @@ -283,7 +281,7 @@ }, "mark_unloaded": { "name": "Mark Unloaded", - "description": "Confirm the finished load has been taken out of a WashData device, clearing the Clean state and any unload reminder.", + "description": "Confirm the finished load was taken out. Clears the Clean state and any unload reminder.", "fields": { "device_id": { "name": "Device", @@ -299,6 +297,32 @@ }, "match_ambiguity": { "name": "Match Ambiguity" + }, + "maintenance_due": { + "name": "Maintenance due", + "state": { + "on": "Due", + "off": "Not due" + }, + "state_attributes": { + "due_task_ids": { + "name": "Due task IDs" + }, + "due_tasks": { + "name": "Due tasks", + "state": { + "descale": "Descale", + "filter_clean": "Clean filter", + "drum_clean": "Clean drum", + "bearing_service": "Bearing service", + "other": "Other", + "salt": "Refill salt", + "rinse_aid": "Refill rinse aid", + "lint_filter": "Clean lint filter", + "condenser_clean": "Clean condenser" + } + } + } } }, "sensor": { @@ -316,10 +340,18 @@ "anti_wrinkle": "Anti-Wrinkle", "interrupted": "Interrupted", "force_stopped": "Force Stopped", - "rinse": "Rinse", "unknown": "Unknown", "clean": "Clean", "delay_wait": "Delay Start" + }, + "state_attributes": { + "cycle_anomaly": { + "name": "Cycle anomaly", + "state": { + "overrun": "Running long", + "stalled": "Stalled" + } + } } }, "washer_program": { diff --git a/custom_components/ha_washdata/translations/es.json b/custom_components/ha_washdata/translations/es.json index 7ce4e344..bddc7985 100644 --- a/custom_components/ha_washdata/translations/es.json +++ b/custom_components/ha_washdata/translations/es.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "Configuración de WashData", - "description": "Configura tu lavadora u otro electrodoméstico.\n\nSe requiere un sensor de potencia.\n\nTras la configuración, abre el panel de WashData desde la barra lateral (o ve a [/ha-washdata](/ha-washdata)) para ver los ciclos, ajustar la detección y gestionar los perfiles.", + "description": "Configura una lavadora u otro electrodoméstico. Se requiere un sensor de potencia.\n\nTras la configuración, abre el panel de WashData desde la barra lateral ([/ha-washdata](/ha-washdata)) para ver ciclos, perfiles y todos los ajustes.", "data": { "name": "Nombre del dispositivo", "device_type": "Tipo de dispositivo", @@ -12,14 +12,14 @@ }, "data_description": { "name": "Un nombre descriptivo para este dispositivo (p. ej., «Lavadora», «Lavavajillas»).", - "device_type": "¿Qué tipo de electrodoméstico es este? Ayuda a personalizar la detección y el etiquetado.", - "power_sensor": "La entidad de sensor que informa el consumo de potencia en tiempo real (en vatios) desde tu enchufe inteligente.", - "min_power": "Las lecturas de potencia por encima de este umbral (en vatios) indican que el electrodoméstico está en funcionamiento. Empieza con 2 W para la mayoría de los dispositivos." + "device_type": "Define los valores de detección predeterminados para este tipo de electrodoméstico.", + "power_sensor": "El sensor del enchufe inteligente que informa la potencia actual en vatios.", + "min_power": "Las lecturas por encima de este valor (en vatios) indican que el electrodoméstico está en marcha. 2 W sirve para la mayoría de los dispositivos." } }, "reconfigure": { "title": "Reconfigurar WashData", - "description": "Actualiza el nombre del dispositivo, el tipo de electrodoméstico o el sensor de potencia.\n\nEl panel de WashData (barra lateral, o [/ha-washdata](/ha-washdata)) es donde se gestionan la detección, los ciclos, los perfiles y las notificaciones.", + "description": "Cambia el nombre del dispositivo, el tipo de electrodoméstico o el sensor de potencia.\n\nTodo lo demás está en el panel de WashData (barra lateral, o [/ha-washdata](/ha-washdata)).", "data": { "name": "Nombre del dispositivo", "device_type": "Tipo de dispositivo", @@ -29,8 +29,6 @@ } }, "error": { - "cannot_connect": "No se pudo conectar", - "invalid_auth": "Autenticación no válida", "unknown": "Error inesperado", "invalid_power": "El umbral de potencia debe ser mayor que 0" }, @@ -43,7 +41,7 @@ "step": { "init": { "title": "Ajustes de WashData", - "description": "Cambia el nombre del dispositivo o su configuración de hardware principal. Todos los ajustes de detección, coincidencia y notificaciones están en el [panel de WashData](/ha-washdata) (también accesible desde la barra lateral).", + "description": "Cambia el nombre del dispositivo o sus ajustes principales. Todo lo demás está en el [panel de WashData](/ha-washdata) de la barra lateral.", "data": { "name": "Nombre del dispositivo", "device_type": "Tipo de dispositivo", @@ -93,7 +91,7 @@ }, "profile_name": { "name": "Nombre del perfil", - "description": "El nombre de un perfil existente (crea perfiles en el menú Gestionar perfiles). Deja vacío para quitar la etiqueta." + "description": "Nombre de un perfil existente (crea perfiles en el panel de WashData). Déjalo vacío para quitar la etiqueta." } } }, @@ -143,13 +141,13 @@ }, "confidence_threshold": { "name": "Umbral de confianza", - "description": "Confianza mínima de coincidencia (0,50-0,95) para aplicar etiquetas." + "description": "Confianza mínima de coincidencia (0,50-0,95) para aplicar etiquetas. Déjelo vacío para usar el ajuste Confianza de etiquetado automático del dispositivo." } } }, "export_config": { "name": "Exportar configuración", - "description": "Exporta los perfiles, ciclos y ajustes de este dispositivo a un archivo JSON (por dispositivo).", + "description": "Exporta los perfiles, ciclos y ajustes de este dispositivo a un archivo JSON.", "fields": { "device_id": { "name": "Dispositivo", @@ -177,7 +175,7 @@ }, "submit_cycle_feedback": { "name": "Enviar retroalimentación del ciclo", - "description": "Confirma o corrige un programa detectado automáticamente tras un ciclo completado. Proporciona `entry_id` (avanzado) o `device_id` (recomendado).", + "description": "Confirma o corrige el programa detectado de un ciclo terminado. Indica `device_id` (recomendado) o `entry_id`.", "fields": { "device_id": { "name": "Dispositivo", @@ -283,7 +281,7 @@ }, "mark_unloaded": { "name": "Marcar como descargado", - "description": "Confirmar que la carga terminada se ha sacado de un dispositivo WashData, borrando el estado Limpio y cualquier recordatorio de descarga.", + "description": "Confirma que se ha sacado la carga terminada. Borra el estado Limpio y cualquier recordatorio de descarga.", "fields": { "device_id": { "name": "Dispositivo", @@ -299,6 +297,32 @@ }, "match_ambiguity": { "name": "Ambigüedad de coincidencia" + }, + "maintenance_due": { + "name": "Mantenimiento pendiente", + "state": { + "on": "Pendiente", + "off": "No pendiente" + }, + "state_attributes": { + "due_task_ids": { + "name": "ID de tareas pendientes" + }, + "due_tasks": { + "name": "Tareas pendientes", + "state": { + "descale": "Descalcificar", + "filter_clean": "Limpiar filtro", + "drum_clean": "Limpiar tambor", + "bearing_service": "Servicio de rodamientos", + "other": "Otro", + "salt": "Rellenar sal", + "rinse_aid": "Rellenar abrillantador", + "lint_filter": "Limpiar filtro de pelusa", + "condenser_clean": "Limpiar condensador" + } + } + } } }, "sensor": { @@ -316,10 +340,18 @@ "anti_wrinkle": "Antiarrugas", "interrupted": "Interrumpido", "force_stopped": "Parada forzada", - "rinse": "Aclarado", "unknown": "Desconocido", "clean": "Limpio", "delay_wait": "Esperando para comenzar" + }, + "state_attributes": { + "cycle_anomaly": { + "name": "Anomalía del ciclo", + "state": { + "overrun": "Con retraso", + "stalled": "Atascado" + } + } } }, "washer_program": { diff --git a/custom_components/ha_washdata/translations/et.json b/custom_components/ha_washdata/translations/et.json index 21d01e1c..890e78cb 100644 --- a/custom_components/ha_washdata/translations/et.json +++ b/custom_components/ha_washdata/translations/et.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "WashData häälestus", - "description": "Seadistage oma pesumasin või muu seade.\n\nVajalik on võimsusandur.\n\nPärast seadistamist avage WashData paneel külgribalt (või minge [/ha-washdata](/ha-washdata)), et vaadata tsükleid, häälestada tuvastust ja hallata profiile.", + "description": "Seadistage pesumasin või muu seade. Vajalik on võimsusandur.\n\nPärast seadistamist avage külgribalt WashData paneel ([/ha-washdata](/ha-washdata)), et näha tsükleid, profiile ja kõiki seadeid.", "data": { "name": "Seadme nimi", "device_type": "Seadme tüüp", @@ -12,14 +12,14 @@ }, "data_description": { "name": "Selle seadme sõbralik nimi (nt 'pesumasin', 'nõudepesumasin').", - "device_type": "Mis tüüpi seade see on? Aitab kohandada tuvastamist ja märgistamist.", - "power_sensor": "Anduri olem, mis teatab teie nutika pistiku reaalajas energiatarbimisest (vattides).", - "min_power": "Sellest läviväärtusest kõrgemad võimsusnäidud (vattides) näitavad, et seade töötab. Enamiku seadmete puhul alustage 2W-st." + "device_type": "Määrab seda tüüpi seadme tuvastamise vaikesätted.", + "power_sensor": "Nutipistiku andur, mis teatab reaalajas võimsust vattides.", + "min_power": "Sellest suuremad näidud (vattides) tähendavad, et seade töötab. Enamikule seadmetele sobib 2 W." } }, "reconfigure": { "title": "WashData uuesti seadistamine", - "description": "Uuendage seadme nime, seadmetüüpi või võimsusandurit.\n\nWashData paneelis (külgriba või [/ha-washdata](/ha-washdata)) hallatakse tuvastust, tsükleid, profiile ja teavitusi.", + "description": "Muutke seadme nime, seadmetüüpi või võimsusandurit.\n\nKõik muu on WashData paneelis (külgriba või [/ha-washdata](/ha-washdata)).", "data": { "name": "Seadme nimi", "device_type": "Seadme tüüp", @@ -29,8 +29,6 @@ } }, "error": { - "cannot_connect": "Ühenduse loomine ebaõnnestus", - "invalid_auth": "Kehtetu autentimine", "unknown": "Ootamatu viga", "invalid_power": "Võimsuslävi peab olema suurem kui 0" }, @@ -43,7 +41,7 @@ "step": { "init": { "title": "WashData seadistused", - "description": "Nimetage seade ümber või muutke selle põhilisi riistvara seadeid. Kõik tuvastus-, sobitamis- ja teavitusseaded asuvad [WashData paneelil](/ha-washdata) (ligipääsetav ka külgribalt).", + "description": "Nimetage seade ümber või muutke selle põhiseadeid. Kõik muu on külgribal olevas [WashData paneelis](/ha-washdata).", "data": { "name": "Seadme nimi", "device_type": "Seadme tüüp", @@ -93,7 +91,7 @@ }, "profile_name": { "name": "Profiili nimi", - "description": "Olemasoleva profiili nimi (profiilide loomine menüüs Profiilide haldamine). Sildi eemaldamiseks jätke tühjaks." + "description": "Olemasoleva profiili nimi (profiile saab luua WashData paneelis). Sildi eemaldamiseks jätke tühjaks." } } }, @@ -143,13 +141,13 @@ }, "confidence_threshold": { "name": "Usalduse lävi", - "description": "Minimaalne vaste usaldusväärsus (0,50–0,95) siltide rakendamiseks." + "description": "Minimaalne vaste usaldusväärsus (0,50-0,95) siltide rakendamiseks. Jätke tühjaks, et kasutada seadme sätet Automaatse Sildi Usaldus." } } }, "export_config": { "name": "Ekspordi konfiguratsioon", - "description": "Eksportige selle seadme profiilid, tsüklid ja seaded JSON-faili (seadme kohta).", + "description": "Eksportige selle seadme profiilid, tsüklid ja seaded JSON-faili.", "fields": { "device_id": { "name": "Seade", @@ -177,7 +175,7 @@ }, "submit_cycle_feedback": { "name": "Esitage tsükli tagasiside", - "description": "Pärast tsükli lõppemist kinnitage või parandage automaatselt tuvastatud programm. Sisestage kas „entry_id” (täpsem) või „device_id” (soovitatav).", + "description": "Kinnitage või parandage lõppenud tsükli tuvastatud programm. Sisestage `device_id` (soovitatav) või `entry_id`.", "fields": { "device_id": { "name": "Seade", @@ -283,7 +281,7 @@ }, "mark_unloaded": { "name": "Märgi mahalaadituks", - "description": "Kinnitage, et lõpetatud tsükli sisu on WashData seadmest välja võetud: olek „Puhas“ ja mahalaadimise meeldetuletus lähtestatakse.", + "description": "Kinnitage, et lõppenud tsükli sisu on välja võetud. Lähtestab oleku „Puhas“ ja kõik mahalaadimise meeldetuletused.", "fields": { "device_id": { "name": "Seade", @@ -299,6 +297,32 @@ }, "match_ambiguity": { "name": "Vaste ebamäärasus" + }, + "maintenance_due": { + "name": "Hooldus vajalik", + "state": { + "on": "Vajalik", + "off": "Pole vajalik" + }, + "state_attributes": { + "due_task_ids": { + "name": "Vajalike ülesannete ID-d" + }, + "due_tasks": { + "name": "Vajalikud ülesanded", + "state": { + "descale": "Katlakivi eemaldamine", + "filter_clean": "Puhasta filter", + "drum_clean": "Puhasta trummel", + "bearing_service": "Laagrite hooldus", + "other": "Muu", + "salt": "Lisa soola", + "rinse_aid": "Lisa loputusvahendit", + "lint_filter": "Puhasta ebemefilter", + "condenser_clean": "Puhasta kondensaator" + } + } + } } }, "sensor": { @@ -316,10 +340,18 @@ "anti_wrinkle": "Kortsudevastane", "interrupted": "Katkestatud", "force_stopped": "Sundpeatatud", - "rinse": "Loputamine", "unknown": "Tundmatu", "clean": "Puhas", "delay_wait": "Ootab algust" + }, + "state_attributes": { + "cycle_anomaly": { + "name": "Tsükli anomaalia", + "state": { + "overrun": "Töötab kaua", + "stalled": "Seiskunud" + } + } } }, "washer_program": { diff --git a/custom_components/ha_washdata/translations/fi.json b/custom_components/ha_washdata/translations/fi.json index e7d05596..01460011 100644 --- a/custom_components/ha_washdata/translations/fi.json +++ b/custom_components/ha_washdata/translations/fi.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "WashData-asetukset", - "description": "Määritä pesukoneesi tai muu laite.\n\nTehoanturi tarvitaan.\n\nAsetuksen jälkeen avaa WashData-paneeli sivupalkista (tai siirry osoitteeseen [/ha-washdata](/ha-washdata)) nähdäksesi syklit, säätääksesi tunnistuksen ja hallitaksesi profiileja.", + "description": "Määritä pesukone tai muu laite. Tehoanturi tarvitaan.\n\nAsetuksen jälkeen avaa WashData-paneeli sivupalkista ([/ha-washdata](/ha-washdata)) nähdäksesi syklit, profiilit ja kaikki asetukset.", "data": { "name": "Laitteen nimi", "device_type": "Laitteen tyyppi", @@ -12,14 +12,14 @@ }, "data_description": { "name": "Ystävällinen nimi tälle laitteelle (esim. 'pesukone', 'astianpesukone').", - "device_type": "Minkä tyyppinen laite tämä on? Auttaa räätälöimään havaitsemista ja merkitsemistä.", - "power_sensor": "Anturientiteetti, joka raportoi reaaliaikaisen virrankulutuksen (watteina) älypistokkeesta.", - "min_power": "Tämän kynnyksen ylittävät teholukemat (watteina) osoittavat, että laite on käynnissä. Aloita 2 watilla useimmissa laitteissa." + "device_type": "Asettaa tunnistuksen oletukset tämän tyyppiselle laitteelle.", + "power_sensor": "Älypistokkeen anturi, joka raportoi reaaliaikaisen tehon watteina.", + "min_power": "Tämän ylittävät lukemat (watteina) tarkoittavat, että laite on käynnissä. 2 W sopii useimmille laitteille." } }, "reconfigure": { "title": "Konfiguroi WashData uudelleen", - "description": "Päivitä laitteen nimi, laitteen tyyppi tai tehoanturi.\n\nWashData-paneelissa (sivupalkki tai [/ha-washdata](/ha-washdata)) hallitaan tunnistusta, syklejä, profiileja ja ilmoituksia.", + "description": "Muuta laitteen nimeä, laitetyyppiä tai tehoanturia.\n\nKaikki muu on WashData-paneelissa (sivupalkki tai [/ha-washdata](/ha-washdata)).", "data": { "name": "Laitteen nimi", "device_type": "Laitteen tyyppi", @@ -29,8 +29,6 @@ } }, "error": { - "cannot_connect": "Yhteyden muodostaminen epäonnistui", - "invalid_auth": "Virheellinen todennus", "unknown": "Odottamaton virhe", "invalid_power": "Tehokynnyksen on oltava suurempi kuin 0" }, @@ -43,7 +41,7 @@ "step": { "init": { "title": "WashData-asetukset", - "description": "Nimeä laite uudelleen tai muuta sen ydinlaitteiston asetuksia. Kaikki tunnistus-, täsmäytys- ja ilmoitusasetukset ovat [WashData-paneelissa](/ha-washdata) (saavutettavissa myös sivupalkista).", + "description": "Nimeä laite uudelleen tai muuta sen perusasetuksia. Kaikki muu on sivupalkin [WashData-paneelissa](/ha-washdata).", "data": { "name": "Laitteen nimi", "device_type": "Laitteen tyyppi", @@ -70,7 +68,7 @@ "dryer": "Kuivausrumpu", "washer_dryer": "Pesukone-kuivausrumpu yhdistelmä", "dishwasher": "Astianpesukone", - "air_fryer": "Air Fryer", + "air_fryer": "Airfryer", "bread_maker": "Leipäkone", "pump": "Pumppu / sumpupumppu", "generic": "Muu (edistynyt)", @@ -93,7 +91,7 @@ }, "profile_name": { "name": "Profiilin nimi", - "description": "Olemassa olevan profiilin nimi (luo profiilit Profiilien hallinta -valikossa). Jätä tyhjäksi poistaaksesi tarran." + "description": "Olemassa olevan profiilin nimi (luo profiilit WashData-paneelissa). Jätä tyhjäksi poistaaksesi merkinnän." } } }, @@ -143,13 +141,13 @@ }, "confidence_threshold": { "name": "Luottamuskynnys", - "description": "Vähimmäisosumavarmuus (0,50–0,95) tunnisteiden lisäämistä varten." + "description": "Vähimmäisosumavarmuus (0,50-0,95) tunnisteiden lisäämistä varten. Jätä tyhjäksi käyttääksesi laitteen Automaattimerkinnän varmuus -asetusta." } } }, "export_config": { "name": "Vie kokoonpano", - "description": "Vie tämän laitteen profiilit, syklit ja asetukset JSON-tiedostoon (laitetta kohti).", + "description": "Vie tämän laitteen profiilit, syklit ja asetukset JSON-tiedostoon.", "fields": { "device_id": { "name": "Laite", @@ -177,7 +175,7 @@ }, "submit_cycle_feedback": { "name": "Lähetä syklipalaute", - "description": "Vahvista tai korjaa automaattisesti tunnistettu ohjelma suoritetun jakson jälkeen. Anna joko \"entry_id\" (lisäasetukset) tai \"device_id\" (suositus).", + "description": "Vahvista tai korjaa valmiin syklin tunnistettu ohjelma. Anna `device_id` (suositus) tai `entry_id`.", "fields": { "device_id": { "name": "Laite", @@ -283,7 +281,7 @@ }, "mark_unloaded": { "name": "Merkitse puretuksi", - "description": "Vahvista, että valmiin syklin sisältö on otettu pois WashData-laitteesta: Puhdas-tila ja purkamismuistutus nollataan.", + "description": "Vahvista, että valmis kuorma on otettu pois. Poistaa Puhdas-tilan ja mahdollisen purkamismuistutuksen.", "fields": { "device_id": { "name": "Laite", @@ -299,6 +297,32 @@ }, "match_ambiguity": { "name": "Vastaavuuden epäselvyys" + }, + "maintenance_due": { + "name": "Huolto tarpeen", + "state": { + "on": "Tarpeen", + "off": "Ei tarpeen" + }, + "state_attributes": { + "due_task_ids": { + "name": "Ajankohtaisten tehtävien tunnukset" + }, + "due_tasks": { + "name": "Ajankohtaiset tehtävät", + "state": { + "descale": "Kalkinpoisto", + "filter_clean": "Puhdista suodatin", + "drum_clean": "Puhdista rumpu", + "bearing_service": "Laakerihuolto", + "other": "Muu", + "salt": "Lisää suolaa", + "rinse_aid": "Lisää huuhtelukirkastetta", + "lint_filter": "Puhdista nukkasihti", + "condenser_clean": "Puhdista lauhdutin" + } + } + } } }, "sensor": { @@ -316,10 +340,18 @@ "anti_wrinkle": "Ryppyjenesto", "interrupted": "Keskeytynyt", "force_stopped": "Pakotettu pysäytys", - "rinse": "Huuhtelu", "unknown": "Tuntematon", "clean": "Puhdas", "delay_wait": "Odotetaan aloitusta" + }, + "state_attributes": { + "cycle_anomaly": { + "name": "Syklin poikkeama", + "state": { + "overrun": "Käy pitkään", + "stalled": "Jumissa" + } + } } }, "washer_program": { diff --git a/custom_components/ha_washdata/translations/fr.json b/custom_components/ha_washdata/translations/fr.json index 05c23cb0..44e856b7 100644 --- a/custom_components/ha_washdata/translations/fr.json +++ b/custom_components/ha_washdata/translations/fr.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "Configuration des données de lavage", - "description": "Configurez votre lave-linge ou autre appareil.\n\nUn capteur de puissance est requis.\n\nUne fois la configuration terminée, ouvrez le panneau WashData depuis la barre latérale (ou accédez à [/ha-washdata](/ha-washdata)) pour consulter les cycles, affiner la détection et gérer les profils.", + "description": "Configurez un lave-linge ou un autre appareil. Un capteur de puissance est requis.\n\nAprès la configuration, ouvrez le panneau WashData depuis la barre latérale ([/ha-washdata](/ha-washdata)) pour voir les cycles, les profils et tous les réglages.", "data": { "name": "Nom de l'appareil", "device_type": "Type d'appareil", @@ -12,14 +12,14 @@ }, "data_description": { "name": "Un nom convivial pour cet appareil (par exemple, « Machine à laver », « Lave-vaisselle »).", - "device_type": "De quel type d'appareil s'agit-il ? Aide à adapter la détection et l’étiquetage.", - "power_sensor": "L'entité de capteur qui signale la consommation d'énergie en temps réel (en watts) de votre prise intelligente.", - "min_power": "Les lectures de puissance supérieures à ce seuil (en watts) indiquent que l'appareil est en marche. Commencez avec 2 W pour la plupart des appareils." + "device_type": "Définit les valeurs de détection par défaut pour ce type d'appareil.", + "power_sensor": "Le capteur de la prise connectée qui indique la puissance actuelle en watts.", + "min_power": "Au-dessus de cette valeur (en watts), l'appareil est considéré en marche. 2 W convient à la plupart des appareils." } }, "reconfigure": { "title": "Reconfigurer WashData", - "description": "Mettez à jour le nom de l'appareil, le type d'appareil ou le capteur de puissance.\n\nLe panneau WashData (barre latérale ou [/ha-washdata](/ha-washdata)) permet de gérer la détection, les cycles, les profils et les notifications.", + "description": "Modifiez le nom de l'appareil, son type ou le capteur de puissance.\n\nTout le reste se trouve dans le panneau WashData (barre latérale ou [/ha-washdata](/ha-washdata)).", "data": { "name": "Nom de l'appareil", "device_type": "Type d'appareil", @@ -29,8 +29,6 @@ } }, "error": { - "cannot_connect": "Échec de la connexion", - "invalid_auth": "Authentification invalide", "unknown": "Erreur inattendue", "invalid_power": "Le seuil de puissance doit être supérieur à 0" }, @@ -43,7 +41,7 @@ "step": { "init": { "title": "Paramètres WashData", - "description": "Renommez l'appareil ou modifiez ses paramètres matériels de base. Tous les paramètres de détection, de correspondance et de notification se trouvent dans le [panneau WashData](/ha-washdata) (également accessible depuis la barre latérale).", + "description": "Renommez l'appareil ou modifiez ses réglages de base. Tout le reste se trouve dans le [panneau WashData](/ha-washdata) de la barre latérale.", "data": { "name": "Nom du périphérique", "device_type": "Type d'appareil", @@ -93,7 +91,7 @@ }, "profile_name": { "name": "Nom du profil", - "description": "Le nom d'un profil existant (créer des profils dans le menu Gérer les profils). Laissez vide pour supprimer l’étiquette." + "description": "Nom d'un profil existant (créez les profils dans le panneau WashData). Laissez vide pour retirer l'étiquette." } } }, @@ -143,13 +141,13 @@ }, "confidence_threshold": { "name": "Seuil de confiance", - "description": "Confiance de correspondance minimale (0,50-0,95) pour appliquer les étiquettes." + "description": "Confiance de correspondance minimale (0,50-0,95) pour appliquer les étiquettes. Laissez vide pour utiliser le réglage Confiance d'étiquetage automatique de l'appareil." } } }, "export_config": { "name": "Exporter la configuration", - "description": "Exportez les profils, cycles et paramètres de cet appareil vers un fichier JSON (par appareil).", + "description": "Exportez les profils, cycles et réglages de cet appareil vers un fichier JSON.", "fields": { "device_id": { "name": "Appareil", @@ -177,7 +175,7 @@ }, "submit_cycle_feedback": { "name": "Soumettre des commentaires sur le cycle", - "description": "Confirmez ou corrigez un programme détecté automatiquement après un cycle terminé. Fournissez soit `entry_id` (avancé) ou `device_id` (recommandé).", + "description": "Confirmez ou corrigez le programme détecté d'un cycle terminé. Indiquez `device_id` (recommandé) ou `entry_id`.", "fields": { "device_id": { "name": "Appareil", @@ -283,7 +281,7 @@ }, "mark_unloaded": { "name": "Marquer comme déchargé", - "description": "Confirmer que la charge terminée a été sortie d'un appareil WashData, ce qui efface l'état Propre et tout rappel de déchargement.", + "description": "Confirmez que la charge terminée a été sortie. Efface l'état Propre et tout rappel de déchargement.", "fields": { "device_id": { "name": "Appareil", @@ -299,6 +297,32 @@ }, "match_ambiguity": { "name": "Ambiguïté des correspondances" + }, + "maintenance_due": { + "name": "Entretien à effectuer", + "state": { + "on": "À effectuer", + "off": "À jour" + }, + "state_attributes": { + "due_task_ids": { + "name": "ID des tâches à effectuer" + }, + "due_tasks": { + "name": "Tâches à effectuer", + "state": { + "descale": "Détartrer", + "filter_clean": "Nettoyer le filtre", + "drum_clean": "Nettoyer le tambour", + "bearing_service": "Entretien des roulements", + "other": "Autre", + "salt": "Recharger le sel", + "rinse_aid": "Recharger le liquide de rinçage", + "lint_filter": "Nettoyer le filtre à peluches", + "condenser_clean": "Nettoyer le condenseur" + } + } + } } }, "sensor": { @@ -316,10 +340,18 @@ "anti_wrinkle": "Anti-faux plis", "interrupted": "Interrompu", "force_stopped": "Arrêt forcé", - "rinse": "Rinçage", "unknown": "Inconnu", "clean": "Propre", "delay_wait": "En attendant de commencer" + }, + "state_attributes": { + "cycle_anomaly": { + "name": "Anomalie du cycle", + "state": { + "overrun": "Dépassement", + "stalled": "Bloqué" + } + } } }, "washer_program": { diff --git a/custom_components/ha_washdata/translations/hr.json b/custom_components/ha_washdata/translations/hr.json index 2d81171c..cfe71b97 100644 --- a/custom_components/ha_washdata/translations/hr.json +++ b/custom_components/ha_washdata/translations/hr.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "Postavljanje WashData", - "description": "Konfigurirajte svoju perilicu rublja ili neki drugi uređaj.\n\nSenzor snage je obavezan.\n\nNakon postavljanja otvorite WashData panel s bočne trake (ili idite na [/ha-washdata](/ha-washdata)) za pregled ciklusa, podešavanje otkrivanja i upravljanje profilima.", + "description": "Postavite perilicu rublja ili drugi uređaj. Potreban je senzor snage.\n\nNakon postavljanja otvorite WashData panel s bočne trake ([/ha-washdata](/ha-washdata)) za cikluse, profile i sve postavke.", "data": { "name": "Naziv uređaja", "device_type": "Vrsta uređaja", @@ -12,43 +12,41 @@ }, "data_description": { "name": "Prijateljski naziv za ovaj uređaj (npr. \"Perilica rublja\", \"Perilica posuđa\").", - "device_type": "Koja je ovo vrsta uređaja? Pomaže u prilagođavanju otkrivanja i označavanja.", - "power_sensor": "Entitet senzora koji prijavljuje potrošnju energije u stvarnom vremenu (u vatima) iz vašeg pametnog utikača.", - "min_power": "Očitavanje snage iznad ovog praga (u vatima) znači da uređaj radi. Počnite s 2 W za većinu uređaja." + "device_type": "Postavlja zadane vrijednosti otkrivanja za ovu vrstu uređaja.", + "power_sensor": "Senzor pametnog utikača koji javlja trenutnu snagu u vatima.", + "min_power": "Očitanja iznad ove vrijednosti (u vatima) znače da uređaj radi. 2 W odgovara većini uređaja." } }, "reconfigure": { - "title": "Reconfigure WashData", - "description": "Ažurirajte naziv uređaja, vrstu uređaja ili senzor snage.\n\nWashData panel (bočna traka ili [/ha-washdata](/ha-washdata)) služi za upravljanje otkrivanjem, ciklusima, profilima i obavijestima.", + "title": "Ponovna konfiguracija WashData", + "description": "Promijenite naziv uređaja, vrstu uređaja ili senzor snage.\n\nSve ostalo je u WashData panelu (bočna traka ili [/ha-washdata](/ha-washdata)).", "data": { - "name": "Device Name", - "device_type": "Device Type", - "power_sensor": "Power Sensor", - "min_power": "Minimum Power Threshold (W)" + "name": "Naziv uređaja", + "device_type": "Vrsta uređaja", + "power_sensor": "Senzor snage", + "min_power": "Minimalni prag snage (W)" } } }, "error": { - "cannot_connect": "Povezivanje nije uspjelo", - "invalid_auth": "Nevažeća provjera autentičnosti", "unknown": "Neočekivana pogreška", - "invalid_power": "Power threshold must be greater than 0" + "invalid_power": "Prag snage mora biti veći od 0" }, "abort": { "already_configured": "Uređaj je već konfiguriran", - "reconfigure_successful": "Reconfiguration was successful" + "reconfigure_successful": "Ponovna konfiguracija je uspjela" } }, "options": { "step": { "init": { - "title": "WashData Settings", - "description": "Preimenujte uređaj ili promijenite njegove osnovne postavke hardvera. Sve postavke otkrivanja, podudaranja i obavijesti nalaze se na [WashData panelu](/ha-washdata) (dostupan i s bočne trake).", + "title": "Postavke WashData", + "description": "Preimenujte uređaj ili promijenite njegove osnovne postavke. Sve ostalo je u [WashData panelu](/ha-washdata) na bočnoj traci.", "data": { "name": "Naziv uređaja", - "device_type": "Device Type", - "power_sensor": "Power Sensor", - "min_power": "Minimum Power Threshold (W)" + "device_type": "Vrsta uređaja", + "power_sensor": "Senzor snage", + "min_power": "Minimalni prag snage (W)" } } }, @@ -60,7 +58,7 @@ "notify_live_waiting_message": "{device}: Nijedan profil još nije podudaran.", "vs_typical_longer": "{pct}% dulje nego obično", "vs_typical_shorter": "{pct}% kraće nego obično", - "invalid_power": "Power threshold must be greater than 0" + "invalid_power": "Prag snage mora biti veći od 0" } }, "selector": { @@ -93,7 +91,7 @@ }, "profile_name": { "name": "Ime profila", - "description": "Naziv postojećeg profila (izradite profile u izborniku Upravljanje profilima). Ostavite prazno za uklanjanje oznake." + "description": "Naziv postojećeg profila (profile izradite u WashData panelu). Ostavite prazno za uklanjanje oznake." } } }, @@ -143,13 +141,13 @@ }, "confidence_threshold": { "name": "Prag povjerenja", - "description": "Minimalna pouzdanost podudaranja (0,50-0,95) za primjenu oznaka." + "description": "Minimalna pouzdanost podudaranja (0,50-0,95) za primjenu oznaka. Ostavite prazno za korištenje postavke uređaja „Pouzdanost za automatsko označavanje”." } } }, "export_config": { "name": "Izvoz konfiguracije", - "description": "Izvezite profile, cikluse i postavke ovog uređaja u JSON datoteku (po uređaju).", + "description": "Izvezite profile, cikluse i postavke ovog uređaja u JSON datoteku.", "fields": { "device_id": { "name": "Uređaj", @@ -177,7 +175,7 @@ }, "submit_cycle_feedback": { "name": "Pošaljite povratne informacije o ciklusu", - "description": "Potvrdite ili ispravite automatski otkriveni program nakon završenog ciklusa. Navedite `entry_id` (napredno) ili `device_id` (preporučeno).", + "description": "Potvrdite ili ispravite otkriveni program završenog ciklusa. Navedite `device_id` (preporučeno) ili `entry_id`.", "fields": { "device_id": { "name": "Uređaj", @@ -272,18 +270,18 @@ } }, "trigger_ml_training": { - "name": "Trigger ML Training", - "description": "Manually retrain the on-device ML models from this device's own labelled cycles (experimental).", + "name": "Pokreni ML treniranje", + "description": "Ručno ponovno istrenirajte ML modele na uređaju pomoću vlastitih označenih ciklusa ovog uređaja (eksperimentalno).", "fields": { "device_id": { - "name": "Device", - "description": "The WashData device to retrain models for." + "name": "Uređaj", + "description": "WashData uređaj čije modele treba ponovno istrenirati." } } }, "mark_unloaded": { "name": "Označi kao istovareno", - "description": "Potvrdite da je gotov sadržaj izvađen iz WashData uređaja, čime se briše stanje \"Čist\" i svaki podsjetnik za istovar.", + "description": "Potvrdite da je gotov sadržaj izvađen. Briše stanje \"Čist\" i podsjetnik za istovar.", "fields": { "device_id": { "name": "Uređaj", @@ -299,6 +297,32 @@ }, "match_ambiguity": { "name": "Dvosmislenost podudaranja" + }, + "maintenance_due": { + "name": "Održavanje na redu", + "state": { + "on": "Na redu", + "off": "Nije na redu" + }, + "state_attributes": { + "due_task_ids": { + "name": "ID-ovi zadataka na redu" + }, + "due_tasks": { + "name": "Zadaci na redu", + "state": { + "descale": "Uklanjanje kamenca", + "filter_clean": "Očisti filtar", + "drum_clean": "Očisti bubanj", + "bearing_service": "Servis ležajeva", + "other": "Ostalo", + "salt": "Dopuni sol", + "rinse_aid": "Dopuni sredstvo za ispiranje", + "lint_filter": "Očisti filtar za dlačice", + "condenser_clean": "Očisti kondenzator" + } + } + } } }, "sensor": { @@ -316,10 +340,18 @@ "anti_wrinkle": "Protiv bora", "interrupted": "Prekinut", "force_stopped": "Prisilno zaustavljeno", - "rinse": "Ispiranje", "unknown": "Nepoznato", "clean": "Čist", "delay_wait": "Čekanje na početak" + }, + "state_attributes": { + "cycle_anomaly": { + "name": "Anomalija ciklusa", + "state": { + "overrun": "Traje predugo", + "stalled": "Zaglavljeno" + } + } } }, "washer_program": { diff --git a/custom_components/ha_washdata/translations/hu.json b/custom_components/ha_washdata/translations/hu.json index 9547a69f..2d868d1f 100644 --- a/custom_components/ha_washdata/translations/hu.json +++ b/custom_components/ha_washdata/translations/hu.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "WashData beállítása", - "description": "Állítsa be a mosógépet vagy más készüléket.\n\nTeljesítményérzékelő szükséges.\n\nA beállítás után nyissa meg a WashData panelt az oldalsávból (vagy lépjen ide: [/ha-washdata](/ha-washdata)), ahol megtekintheti a ciklusokat, finomhangolhatja a felismerést és kezelheti a profilokat.", + "description": "Állítson be egy mosógépet vagy más készüléket. Teljesítményérzékelő szükséges.\n\nA beállítás után nyissa meg a WashData panelt az oldalsávból ([/ha-washdata](/ha-washdata)) a ciklusok, profilok és az összes beállítás megtekintéséhez.", "data": { "name": "Eszköz neve", "device_type": "Eszköz típusa", @@ -12,43 +12,41 @@ }, "data_description": { "name": "Barátságos név ennek az eszköznek (pl. „Mosógép”, „Mosogatógép”).", - "device_type": "Milyen típusú készülék ez? Segít személyre szabni az észlelést és a címkézést.", - "power_sensor": "Az érzékelő entitás, amely valós idejű energiafogyasztást jelent (wattban) az okosdugóról.", - "min_power": "Az e küszöbérték feletti teljesítményértékek (wattban) azt jelzik, hogy a készülék működik. Kezdje 2 W-tal a legtöbb eszköz esetében." + "device_type": "Beállítja az észlelési alapértékeket ehhez a készüléktípushoz.", + "power_sensor": "Az okoskonnektor érzékelője, amely az élő teljesítményt jelenti wattban.", + "min_power": "Az e feletti leolvasások (wattban) azt jelentik, hogy a készülék működik. A legtöbb eszközhöz 2 W megfelelő." } }, "reconfigure": { - "title": "Reconfigure WashData", - "description": "Frissítse az eszköz nevét, a készülék típusát vagy a teljesítményérzékelőt.\n\nA WashData panel (oldalsáv vagy [/ha-washdata](/ha-washdata)) a felismerés, ciklusok, profilok és értesítések kezelésére szolgál.", + "title": "WashData újrakonfigurálása", + "description": "Módosítsa az eszköz nevét, a készülék típusát vagy a teljesítményérzékelőt.\n\nMinden más a WashData panelen található (oldalsáv vagy [/ha-washdata](/ha-washdata)).", "data": { - "name": "Device Name", - "device_type": "Device Type", - "power_sensor": "Power Sensor", - "min_power": "Minimum Power Threshold (W)" + "name": "Eszköz neve", + "device_type": "Eszköz típusa", + "power_sensor": "Teljesítmény érzékelő", + "min_power": "Minimális teljesítmény küszöb (W)" } } }, "error": { - "cannot_connect": "Nem sikerült csatlakozni", - "invalid_auth": "Érvénytelen hitelesítés", "unknown": "Váratlan hiba", - "invalid_power": "Power threshold must be greater than 0" + "invalid_power": "A teljesítményküszöbnek 0-nál nagyobbnak kell lennie" }, "abort": { "already_configured": "Az eszköz már konfigurálva van", - "reconfigure_successful": "Reconfiguration was successful" + "reconfigure_successful": "Az újrakonfigurálás sikeres volt" } }, "options": { "step": { "init": { - "title": "WashData Settings", - "description": "Nevezze át az eszközt, vagy módosítsa az alap hardverbeállításait. Az összes felismerési, egyeztetési és értesítési beállítás a [WashData panelen](/ha-washdata) található (az oldalsávból is elérhető).", + "title": "WashData beállításai", + "description": "Nevezze át az eszközt, vagy módosítsa az alapbeállításait. Minden más az oldalsáv [WashData paneljén](/ha-washdata) található.", "data": { "name": "Eszköz neve", - "device_type": "Device Type", - "power_sensor": "Power Sensor", - "min_power": "Minimum Power Threshold (W)" + "device_type": "Eszköz típusa", + "power_sensor": "Teljesítmény érzékelő", + "min_power": "Minimális teljesítmény küszöb (W)" } } }, @@ -60,7 +58,7 @@ "notify_live_waiting_message": "{device}: Még nincs illeszkedő profil.", "vs_typical_longer": "{pct}%-kal hosszabb a szokásosnál", "vs_typical_shorter": "{pct}%-kal rövidebb a szokásosnál", - "invalid_power": "Power threshold must be greater than 0" + "invalid_power": "A teljesítményküszöbnek 0-nál nagyobbnak kell lennie" } }, "selector": { @@ -70,7 +68,7 @@ "dryer": "Szárítógép", "washer_dryer": "Mosó-szárító kombó", "dishwasher": "Mosogatógép", - "air_fryer": "Air Fryer", + "air_fryer": "Forrólevegős sütő", "bread_maker": "Kenyérsütő", "pump": "Szivattyú / búvárszivattyú", "generic": "Egyéb (haladó)", @@ -93,7 +91,7 @@ }, "profile_name": { "name": "Profil neve", - "description": "Egy meglévő profil neve (profilok létrehozása a Profilok kezelése menüben). Hagyja üresen a címke eltávolításához." + "description": "Egy meglévő profil neve (profilokat a WashData panelen hozhat létre). Hagyja üresen a címke eltávolításához." } } }, @@ -143,13 +141,13 @@ }, "confidence_threshold": { "name": "Magabiztossági küszöb", - "description": "Minimális egyezési megbízhatóság (0,50–0,95) a címkék alkalmazásához." + "description": "Minimális egyezési megbízhatóság (0,50-0,95) a címkék alkalmazásához. Hagyja üresen az eszköz Automatikus jelölési megbízhatóság beállításának használatához." } } }, "export_config": { "name": "Konfiguráció exportálása", - "description": "Exportálja az eszköz profiljait, ciklusait és beállításait JSON-fájlba (eszközönként).", + "description": "Az eszköz profiljainak, ciklusainak és beállításainak exportálása JSON-fájlba.", "fields": { "device_id": { "name": "Eszköz", @@ -177,7 +175,7 @@ }, "submit_cycle_feedback": { "name": "Ciklus visszajelzés küldése", - "description": "Erősítse meg vagy javítsa ki az automatikusan észlelt programot a ciklus befejezése után. Adja meg az „entry_id” (speciális) vagy az „device_id” (ajánlott) értéket.", + "description": "Egy befejezett ciklus észlelt programjának megerősítése vagy javítása. Adja meg a `device_id` (ajánlott) vagy az `entry_id` értéket.", "fields": { "device_id": { "name": "Eszköz", @@ -272,18 +270,18 @@ } }, "trigger_ml_training": { - "name": "Trigger ML Training", - "description": "Manually retrain the on-device ML models from this device's own labelled cycles (experimental).", + "name": "ML-tanítás indítása", + "description": "Tanítsa újra kézzel az eszközön futó ML-modelleket az eszköz saját címkézett ciklusaiból (kísérleti).", "fields": { "device_id": { - "name": "Device", - "description": "The WashData device to retrain models for." + "name": "Eszköz", + "description": "A WashData eszköz, amelynek modelljeit újra szeretné tanítani." } } }, "mark_unloaded": { "name": "Megjelölés kirakodottként", - "description": "Annak megerősítése, hogy a kész adagot kivették a WashData eszközből, ami törli a „Tiszta” állapotot és a kirakodási emlékeztetőt.", + "description": "Megerősíti, hogy a kész adagot kivették. Törli a „Tiszta” állapotot és a kirakodási emlékeztetőt.", "fields": { "device_id": { "name": "Eszköz", @@ -299,6 +297,32 @@ }, "match_ambiguity": { "name": "Találat kétértelműsége" + }, + "maintenance_due": { + "name": "Esedékes karbantartás", + "state": { + "on": "Esedékes", + "off": "Nem esedékes" + }, + "state_attributes": { + "due_task_ids": { + "name": "Esedékes feladatok azonosítói" + }, + "due_tasks": { + "name": "Esedékes feladatok", + "state": { + "descale": "Vízkőmentesítés", + "filter_clean": "Szűrő tisztítása", + "drum_clean": "Dob tisztítása", + "bearing_service": "Csapágy szerviz", + "other": "Egyéb", + "salt": "Só utántöltése", + "rinse_aid": "Öblítőszer utántöltése", + "lint_filter": "Szöszszűrő tisztítása", + "condenser_clean": "Kondenzátor tisztítása" + } + } + } } }, "sensor": { @@ -316,10 +340,18 @@ "anti_wrinkle": "Ránctalanító", "interrupted": "Megszakított", "force_stopped": "Kényszer leállított", - "rinse": "Öblítés", "unknown": "Ismeretlen", "clean": "Tiszta", "delay_wait": "Várakozás a kezdésre" + }, + "state_attributes": { + "cycle_anomaly": { + "name": "Ciklusrendellenesség", + "state": { + "overrun": "Sokáig fut", + "stalled": "Elakadt" + } + } } }, "washer_program": { diff --git a/custom_components/ha_washdata/translations/is.json b/custom_components/ha_washdata/translations/is.json index 93d3a657..772a6165 100644 --- a/custom_components/ha_washdata/translations/is.json +++ b/custom_components/ha_washdata/translations/is.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "WashData uppsetning", - "description": "Stilltu þvottavélina þína eða annað tæki.\n\nKraftskynjari er nauðsynlegur.\n\nEftir uppsetningu skaltu opna WashData-spjaldið frá hliðarstikunni (eða fara á [/ha-washdata](/ha-washdata)) til að skoða hringrás, stilla skynjun og stjórna sniðum.", + "description": "Settu upp þvottavél eða annað tæki. Aflskynjari er nauðsynlegur.\n\nEftir uppsetningu skaltu opna WashData-spjaldið frá hliðarstikunni ([/ha-washdata](/ha-washdata)) til að sjá lotur, snið og allar stillingar.", "data": { "name": "Nafn tækis", "device_type": "Tegund tækis", @@ -12,55 +12,53 @@ }, "data_description": { "name": "Vingjarnlegt nafn fyrir þetta tæki (t.d. „Þvottavél“, „Uppþvottavél“).", - "device_type": "Hvers konar tæki er þetta? Hjálpar til við að sérsníða greiningu og merkingu.", - "power_sensor": "Skynjareiningin sem tilkynnir um orkunotkun í rauntíma (í vöttum) frá snjalltenginu þínu.", - "min_power": "Aflmælingar yfir þessum viðmiðunarmörkum (í vöttum) gefa til kynna að heimilistækið sé í gangi. Byrjaðu með 2W fyrir flest tæki." + "device_type": "Stillir sjálfgefin greiningargildi fyrir þessa tegund tækis.", + "power_sensor": "Skynjari snjalltengilsins sem gefur upp núverandi afl í vöttum.", + "min_power": "Mælingar yfir þessu (í vöttum) þýða að tækið er í gangi. 2 W hentar flestum tækjum." } }, "reconfigure": { - "title": "Reconfigure WashData", - "description": "Uppfærðu heiti tækisins, tegund tækis eða kraftskynjara.\n\nWashData-spjaldið (hliðarstikan eða [/ha-washdata](/ha-washdata)) er þar sem skynjun, hringrás, sniðmát og tilkynningar eru stjórnað.", + "title": "Endurstilla WashData", + "description": "Breyttu heiti tækisins, tegund tækis eða aflskynjara.\n\nAllt annað er í WashData-spjaldinu (hliðarstikan eða [/ha-washdata](/ha-washdata)).", "data": { - "name": "Device Name", - "device_type": "Device Type", - "power_sensor": "Power Sensor", - "min_power": "Minimum Power Threshold (W)" + "name": "Nafn tækis", + "device_type": "Tegund tækis", + "power_sensor": "Aflskynjari", + "min_power": "Lágmarksaflsþröskuldur (W)" } } }, "error": { - "cannot_connect": "Tókst ekki að tengjast", - "invalid_auth": "Ógild auðkenning", "unknown": "Óvænt villa", - "invalid_power": "Power threshold must be greater than 0" + "invalid_power": "Aflsþröskuldur verður að vera stærri en 0" }, "abort": { "already_configured": "Tæki er þegar stillt", - "reconfigure_successful": "Reconfiguration was successful" + "reconfigure_successful": "Endurstilling heppnaðist" } }, "options": { "step": { "init": { - "title": "WashData Settings", - "description": "Endurnefndu tækið eða breyttu grunnstillingum vélbúnaðar. Allar stillingar fyrir skynjun, samsvörun og tilkynningar eru í [WashData-spjaldinu](/ha-washdata) (einnig aðgengilegt frá hliðarstikunni).", + "title": "Stillingar WashData", + "description": "Endurnefndu tækið eða breyttu grunnstillingum þess. Allt annað er í [WashData-spjaldinu](/ha-washdata) á hliðarstikunni.", "data": { "name": "Nafn tækis", - "device_type": "Device Type", - "power_sensor": "Power Sensor", - "min_power": "Minimum Power Threshold (W)" + "device_type": "Tegund tækis", + "power_sensor": "Aflskynjari", + "min_power": "Lágmarksaflsþröskuldur (W)" } } }, "error": { "timer_default_message": "{device}: {minutes} mín tímataki", - "timer_pause_action_title": "Halda áfram hringrás", - "timer_pause_body_suffix": "Hringrásin er í bið. Opnaðu WashData-gluggann til að halda áfram.", + "timer_pause_action_title": "Halda lotu áfram", + "timer_pause_body_suffix": "Lotan er í bið. Opnaðu WashData-spjaldið til að halda henni áfram.", "unload_dismiss_action_title": "Hætta að minna", "notify_live_waiting_message": "{device}: Ekkert snið hefur enn verið greint.", "vs_typical_longer": "{pct}% lengri en venjulega", "vs_typical_shorter": "{pct}% styttri en venjulega", - "invalid_power": "Power threshold must be greater than 0" + "invalid_power": "Aflsþröskuldur verður að vera stærri en 0" } }, "selector": { @@ -70,7 +68,7 @@ "dryer": "Þurrkari", "washer_dryer": "Þvottavél-þurrkari samsettur", "dishwasher": "Uppþvottavél", - "air_fryer": "Air Fryer", + "air_fryer": "Loftsteikingarpottur", "bread_maker": "Brauðgerðarmaður", "pump": "Dæla / Sump Pump", "generic": "Annað (háþróað)", @@ -80,7 +78,7 @@ }, "services": { "label_cycle": { - "name": "Merkja hringrás", + "name": "Merkja lotu", "description": "Úthlutaðu núverandi prófíl við fyrri lotu eða fjarlægðu merkið.", "fields": { "device_id": { @@ -88,12 +86,12 @@ "description": "Þvottavél tæki til að merkja." }, "cycle_id": { - "name": "Auðkenni hringrásar", + "name": "Auðkenni lotu", "description": "Auðkenni lotunnar sem á að merkja." }, "profile_name": { "name": "Nafn prófíls", - "description": "Heiti sniðs sem fyrir er (búið til snið í valmyndinni Stjórna sniðum). Skildu eftir autt til að fjarlægja merkimiðann." + "description": "Heiti sniðs sem er til (búðu til snið í WashData-spjaldinu). Skildu eftir autt til að fjarlægja merkinguna." } } }, @@ -117,7 +115,7 @@ }, "delete_profile": { "name": "Eyða prófíl", - "description": "Eyddu sniði og afmerktu hringrás með því að nota það.", + "description": "Eyddu sniði og fjarlægðu ef vill merkinguna af lotum sem nota það.", "fields": { "device_id": { "name": "Tæki", @@ -128,13 +126,13 @@ "description": "Prófíllinn til að eyða." }, "unlabel_cycles": { - "name": "Afmerkja hringrásir", + "name": "Afmerkja lotur", "description": "Fjarlægðu prófílmerki úr lotum með því að nota þetta prófíl." } } }, "auto_label_cycles": { - "name": "Sjálfvirk merking á gömlum hringrásum", + "name": "Sjálfvirk merking gamalla lota", "description": "Merktu afturvirkt ómerktar lotur með því að nota prófílsamsvörun.", "fields": { "device_id": { @@ -143,13 +141,13 @@ }, "confidence_threshold": { "name": "Sjálfstraustsþröskuldur", - "description": "Lágmarksöryggi (0,50-0,95) til að setja á merki." + "description": "Lágmarksöryggi samsvörunar (0,50-0,95) til að setja á merki. Skildu reitinn eftir auðan til að nota stillingu tækisins „Sjálfvirk merkingaöryggi“." } } }, "export_config": { "name": "Flytja út stillingar", - "description": "Flyttu út prófíla, lotur og stillingar þessa tækis í JSON skrá (á hvert tæki).", + "description": "Flyttu snið, lotur og stillingar þessa tækis út í JSON-skrá.", "fields": { "device_id": { "name": "Tæki", @@ -176,8 +174,8 @@ } }, "submit_cycle_feedback": { - "name": "Senda endurgjöf um hringrás", - "description": "Staðfestu eða leiðréttu sjálfvirkt forrit sem greint hefur verið frá eftir að lotunni er lokið. Gefðu upp annað hvort 'entry_id' (háþróað) eða 'device_id' (ráðlagt).", + "name": "Senda endurgjöf um lotu", + "description": "Staðfestu eða leiðréttu greint kerfi lotu sem er lokið. Gefðu upp `device_id` (ráðlagt) eða `entry_id`.", "fields": { "device_id": { "name": "Tæki", @@ -188,16 +186,16 @@ "description": "Auðkenni stillingarfærslu fyrir tækið (valkostur við device_id)." }, "cycle_id": { - "name": "Auðkenni hringrásar", - "description": "Auðkenni hringrásarinnar sem sýnt er í endurgjöfstilkynningunni / logs." + "name": "Auðkenni lotu", + "description": "Auðkenni lotunnar sem sýnt er í endurgjafartilkynningunni / annálunum." }, "user_confirmed": { - "name": "Staðfestu uppgötvað forrit", - "description": "Stilltu satt ef forritið sem fannst er rétt." + "name": "Staðfesta greint kerfi", + "description": "Stilltu á satt ef greinda kerfið er rétt." }, "corrected_profile": { "name": "Leiðrétt prófíl", - "description": "Ef það er ekki staðfest skaltu gefa upp rétt snið/nafn forrits." + "description": "Ef það er ekki staðfest skaltu gefa upp rétt heiti sniðs/kerfis." }, "corrected_duration": { "name": "Leiðrétt lengd (sekúndur)", @@ -210,7 +208,7 @@ } }, "record_start": { - "name": "Hefja upptöku hringrásar", + "name": "Hefja upptöku lotu", "description": "Byrjaðu handvirkt að taka upp hreina lotu (framhjá öllum samsvarandi rökfræði).", "fields": { "device_id": { @@ -230,7 +228,7 @@ } }, "trim_cycle": { - "name": "Klipptu hringrás", + "name": "Klippa lotu til", "description": "Klipptu aflgögn fyrri lotu í ákveðinn tímaglugga.", "fields": { "device_id": { @@ -238,8 +236,8 @@ "description": "WashData tækið." }, "cycle_id": { - "name": "Auðkenni hringrásar", - "description": "Auðkenni hringrásarinnar sem á að klippa." + "name": "Auðkenni lotu", + "description": "Auðkenni lotunnar sem á að klippa til." }, "trim_start_s": { "name": "Klippiupphaf (sekúndur)", @@ -252,8 +250,8 @@ } }, "pause_cycle": { - "name": "Gera hlé á hringrás", - "description": "Gerðu hlé á virku hringrásinni fyrir WashData tæki.", + "name": "Gera hlé á lotu", + "description": "Gerðu hlé á virku lotunni í WashData-tæki.", "fields": { "device_id": { "name": "Tæki", @@ -262,7 +260,7 @@ } }, "resume_cycle": { - "name": "Halda áfram hringrás", + "name": "Halda lotu áfram", "description": "Haltu áfram að gera hlé á lotu fyrir WashData tæki.", "fields": { "device_id": { @@ -273,7 +271,7 @@ }, "trigger_ml_training": { "name": "Kveikja á ML þjálfun", - "description": "Endurþjálfaðu ML líkönin á tækinu handvirkt með þjálfuðum hringrásum þessa tækis (tilraunakenndar).", + "description": "Endurþjálfaðu ML-líkönin á tækinu handvirkt út frá merktum lotum þessa tækis (tilraunaeiginleiki).", "fields": { "device_id": { "name": "Tæki", @@ -283,7 +281,7 @@ }, "mark_unloaded": { "name": "Merkja sem losað", - "description": "Staðfestu að búið sé að taka fullunna þvottinn úr WashData tæki, sem hreinsar stöðuna \"Hreint\" og hvers kyns losunaráminningu.", + "description": "Staðfestu að þvotturinn hafi verið tekinn út. Hreinsar stöðuna \"Hreint\" og hvers kyns losunaráminningu.", "fields": { "device_id": { "name": "Tæki", @@ -299,6 +297,32 @@ }, "match_ambiguity": { "name": "Samsvörun tvíræðni" + }, + "maintenance_due": { + "name": "Viðhald tímabært", + "state": { + "on": "Tímabært", + "off": "Ekki tímabært" + }, + "state_attributes": { + "due_task_ids": { + "name": "Auðkenni tímabærra verka" + }, + "due_tasks": { + "name": "Tímabær verk", + "state": { + "descale": "Afkalka", + "filter_clean": "Hreinsa síu", + "drum_clean": "Hreinsa tromlu", + "bearing_service": "Þjónusta á legum", + "other": "Annað", + "salt": "Fylla á salt", + "rinse_aid": "Fylla á gljáa", + "lint_filter": "Hreinsa lósíu", + "condenser_clean": "Hreinsa eimsvala" + } + } + } } }, "sensor": { @@ -316,14 +340,22 @@ "anti_wrinkle": "Anti-hrukku", "interrupted": "Truflað", "force_stopped": "Þvingun stöðvuð", - "rinse": "Skolaðu", "unknown": "Óþekkt", "clean": "Hreint", "delay_wait": "Bíður eftir að byrja" + }, + "state_attributes": { + "cycle_anomaly": { + "name": "Frávik í lotu", + "state": { + "overrun": "Keyrir lengur", + "stalled": "Stöðvuð" + } + } } }, "washer_program": { - "name": "Forrit" + "name": "Kerfi" }, "time_remaining": { "name": "Tími sem eftir er" @@ -360,7 +392,7 @@ }, "profile_cycle_count": { "name": "Prófíl {profile_name} Talning", - "unit_of_measurement": "hringrásir" + "unit_of_measurement": "lotur" }, "suggestions": { "name": "Tillögur að stillingum" @@ -369,7 +401,7 @@ "name": "Dæla gengur (síðasti 24 klst.)" }, "cycle_count": { - "name": "Talning hringrásar" + "name": "Fjöldi lota" }, "energy_total": { "name": "Heildarorka" @@ -377,7 +409,7 @@ }, "select": { "program_select": { - "name": "Hringrásarforrit", + "name": "Kerfi lotu", "state": { "auto_detect": "Sjálfvirk uppgötvun" } @@ -388,16 +420,16 @@ "name": "Þvingaðu lokahring" }, "pause_cycle": { - "name": "Gera hlé á hringrás" + "name": "Gera hlé á lotu" }, "resume_cycle": { - "name": "Halda áfram hringrás" + "name": "Halda lotu áfram" }, "record_start": { - "name": "Hefja upptöku hringrásar" + "name": "Hefja upptöku lotu" }, "record_stop": { - "name": "Stöðva upptöku hringrásar" + "name": "Stöðva upptöku lotu" }, "mark_unloaded": { "name": "Merkja sem losað" @@ -424,10 +456,10 @@ "message": "Samþætting ekki hlaðin fyrir þetta tæki." }, "cycle_not_found_or_no_power": { - "message": "Hringrás fannst ekki eða hefur engin orkugögn." + "message": "Lota fannst ekki eða hefur engin orkugögn." }, "trim_failed_empty_window": { - "message": "Trim mistókst - hringrás fannst ekki, engin orkugögn eða gluggi sem myndast er tómur." + "message": "Klipping mistókst - lota fannst ekki, engin orkugögn eru til eða glugginn sem eftir stendur er tómur." }, "trim_invalid_range": { "message": "trim_end_s verða að vera stærri en trim_start_s." diff --git a/custom_components/ha_washdata/translations/it.json b/custom_components/ha_washdata/translations/it.json index 2c325cc8..73da664d 100644 --- a/custom_components/ha_washdata/translations/it.json +++ b/custom_components/ha_washdata/translations/it.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "Configurazione WashData", - "description": "Configura la tua lavatrice o altro elettrodomestico.\n\nÈ richiesto il sensore di potenza.\n\nDopo la configurazione, apri il pannello WashData dalla barra laterale (o vai su [/ha-washdata](/ha-washdata)) per visualizzare i cicli, regolare il rilevamento e gestire i profili.", + "description": "Configura una lavatrice o un altro elettrodomestico. È richiesto un sensore di potenza.\n\nDopo la configurazione, apri il pannello WashData dalla barra laterale ([/ha-washdata](/ha-washdata)) per vedere cicli, profili e tutte le impostazioni.", "data": { "name": "Nome dispositivo", "device_type": "Tipo di dispositivo", @@ -12,43 +12,41 @@ }, "data_description": { "name": "Un nome descrittivo per questo dispositivo (ad esempio \"Lavatrice\", \"Lavastoviglie\").", - "device_type": "Che tipo di elettrodomestico è questo? Aiuta a personalizzare il rilevamento e l'etichettatura.", - "power_sensor": "L'entità sensore che segnala il consumo energetico in tempo reale (in watt) dalla tua presa intelligente.", - "min_power": "Le letture di potenza superiori a questa soglia (in watt) indicano che l'elettrodomestico è in funzione. Inizia con 2 W per la maggior parte dei dispositivi." + "device_type": "Imposta i valori di rilevamento predefiniti per questo tipo di elettrodomestico.", + "power_sensor": "Il sensore della presa intelligente che riporta la potenza attuale in watt.", + "min_power": "Letture sopra questo valore (in watt) indicano che l'elettrodomestico è in funzione. 2 W va bene per la maggior parte dei dispositivi." } }, "reconfigure": { - "title": "Reconfigure WashData", - "description": "Aggiorna il nome del dispositivo, il tipo di elettrodomestico o il sensore di potenza.\n\nIl pannello WashData (barra laterale, o [/ha-washdata](/ha-washdata)) è dove vengono gestiti rilevamento, cicli, profili e notifiche.", + "title": "Riconfigura WashData", + "description": "Cambia il nome del dispositivo, il tipo di elettrodomestico o il sensore di potenza.\n\nTutto il resto è nel pannello WashData (barra laterale, o [/ha-washdata](/ha-washdata)).", "data": { - "name": "Device Name", - "device_type": "Device Type", - "power_sensor": "Power Sensor", - "min_power": "Minimum Power Threshold (W)" + "name": "Nome dispositivo", + "device_type": "Tipo di dispositivo", + "power_sensor": "Sensore di potenza", + "min_power": "Soglia di potenza minima (W)" } } }, "error": { - "cannot_connect": "Connessione non riuscita", - "invalid_auth": "Autenticazione non valida", "unknown": "Errore imprevisto", - "invalid_power": "Power threshold must be greater than 0" + "invalid_power": "La soglia di potenza deve essere maggiore di 0" }, "abort": { "already_configured": "Il dispositivo è già configurato", - "reconfigure_successful": "Reconfiguration was successful" + "reconfigure_successful": "La riconfigurazione è avvenuta con successo" } }, "options": { "step": { "init": { - "title": "WashData Settings", - "description": "Rinomina il dispositivo o modifica le sue impostazioni hardware principali. Tutte le impostazioni di rilevamento, corrispondenza e notifica si trovano nel [pannello WashData](/ha-washdata) (accessibile anche dalla barra laterale).", + "title": "Impostazioni WashData", + "description": "Rinomina il dispositivo o modifica le sue impostazioni principali. Tutto il resto è nel [pannello WashData](/ha-washdata) nella barra laterale.", "data": { "name": "Nome del dispositivo", - "device_type": "Device Type", - "power_sensor": "Power Sensor", - "min_power": "Minimum Power Threshold (W)" + "device_type": "Tipo di dispositivo", + "power_sensor": "Sensore di potenza", + "min_power": "Soglia di potenza minima (W)" } } }, @@ -60,7 +58,7 @@ "notify_live_waiting_message": "{device}: nessun profilo ancora riconosciuto.", "vs_typical_longer": "{pct}% più lungo del solito", "vs_typical_shorter": "{pct}% più corto del solito", - "invalid_power": "Power threshold must be greater than 0" + "invalid_power": "La soglia di potenza deve essere maggiore di 0" } }, "selector": { @@ -93,7 +91,7 @@ }, "profile_name": { "name": "Nome profilo", - "description": "Il nome di un profilo esistente (crea profili nel menu Gestisci profili). Lascia vuoto per rimuovere l'etichetta." + "description": "Nome di un profilo esistente (crea i profili nel pannello WashData). Lascia vuoto per rimuovere l'etichetta." } } }, @@ -143,13 +141,13 @@ }, "confidence_threshold": { "name": "Soglia di confidenza", - "description": "Confidenza di corrispondenza minima (0,50-0,95) per applicare le etichette." + "description": "Confidenza di corrispondenza minima (0,50-0,95) per applicare le etichette. Lascia vuoto per usare l'impostazione Confidenza etichettatura automatica del dispositivo." } } }, "export_config": { "name": "Esporta configurazione", - "description": "Esporta profili, cicli e impostazioni di questo dispositivo in un file JSON (per dispositivo).", + "description": "Esporta profili, cicli e impostazioni di questo dispositivo in un file JSON.", "fields": { "device_id": { "name": "Dispositivo", @@ -177,7 +175,7 @@ }, "submit_cycle_feedback": { "name": "Invia feedback sul ciclo", - "description": "Conferma o correggi un programma rilevato automaticamente dopo un ciclo completato. Fornisci `entry_id` (avanzato) o `device_id` (consigliato).", + "description": "Conferma o correggi il programma rilevato di un ciclo terminato. Indica `device_id` (consigliato) o `entry_id`.", "fields": { "device_id": { "name": "Dispositivo", @@ -283,7 +281,7 @@ }, "mark_unloaded": { "name": "Segna come scaricato", - "description": "Confermare che il carico terminato è stato tolto da un dispositivo WashData, azzerando lo stato Pulito e ogni promemoria di scarico.", + "description": "Conferma che il carico terminato è stato tolto. Azzera lo stato Pulito e ogni promemoria di scarico.", "fields": { "device_id": { "name": "Dispositivo", @@ -299,6 +297,32 @@ }, "match_ambiguity": { "name": "Ambiguità corrispondenza" + }, + "maintenance_due": { + "name": "Manutenzione in scadenza", + "state": { + "on": "In scadenza", + "off": "Non in scadenza" + }, + "state_attributes": { + "due_task_ids": { + "name": "ID attività in scadenza" + }, + "due_tasks": { + "name": "Attività in scadenza", + "state": { + "descale": "Decalcifica", + "filter_clean": "Pulisci filtro", + "drum_clean": "Pulisci cestello", + "bearing_service": "Manutenzione cuscinetti", + "other": "Altro", + "salt": "Ricarica sale", + "rinse_aid": "Ricarica brillantante", + "lint_filter": "Pulisci filtro lanugine", + "condenser_clean": "Pulisci condensatore" + } + } + } } }, "sensor": { @@ -316,10 +340,18 @@ "anti_wrinkle": "Antirughe", "interrupted": "Interrotto", "force_stopped": "Fermato forzatamente", - "rinse": "Risciacquo", "unknown": "Sconosciuto", "clean": "Pulito", "delay_wait": "In attesa di iniziare" + }, + "state_attributes": { + "cycle_anomaly": { + "name": "Anomalia del ciclo", + "state": { + "overrun": "In ritardo", + "stalled": "Bloccato" + } + } } }, "washer_program": { diff --git a/custom_components/ha_washdata/translations/ja.json b/custom_components/ha_washdata/translations/ja.json index 7de761a8..54f8dacf 100644 --- a/custom_components/ha_washdata/translations/ja.json +++ b/custom_components/ha_washdata/translations/ja.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "WashData セットアップ", - "description": "洗濯機またはその他の家電製品を設定します。\n\n電力センサーが必要です。\n\nセットアップ後、サイドバーの WashData パネルを開くか(または [/ha-washdata](/ha-washdata) にアクセスして)、サイクルの確認、検出の調整、プロファイルの管理を行ってください。", + "description": "洗濯機などの家電を設定します。電力センサーが必要です。\n\nセットアップ後、サイドバーから WashData パネル([/ha-washdata](/ha-washdata))を開くと、サイクル、プロファイル、すべての設定を確認できます。", "data": { "name": "デバイス名", "device_type": "デバイスの種類", @@ -12,43 +12,41 @@ }, "data_description": { "name": "このデバイスのわかりやすい名前(例:「洗濯機」、「食器洗い機」)。", - "device_type": "家電製品の種類を選択します。検出とラベル付けの精度向上に役立ちます。", - "power_sensor": "スマートプラグからリアルタイムの消費電力(ワット)を報告するセンサーエンティティ。", - "min_power": "このしきい値(ワット)を超える電力値は、家電製品が動作中であることを示します。ほとんどのデバイスでは 2W から始めてください。" + "device_type": "この種類の家電向けの検出のデフォルトを設定します。", + "power_sensor": "リアルタイムの電力をワット単位で報告するスマートプラグのセンサー。", + "min_power": "この値(ワット)を超える測定値は、家電が動作中であることを示します。ほとんどのデバイスには 2 W が適しています。" } }, "reconfigure": { - "title": "Reconfigure WashData", - "description": "デバイス名、家電の種類、または電力センサーを更新します。\n\nWashData パネル(サイドバー、または [/ha-washdata](/ha-washdata))で、検出、サイクル、プロファイル、通知を管理できます。", + "title": "WashData の再設定", + "description": "デバイス名、家電の種類、電力センサーを変更します。\n\nその他の設定はすべて WashData パネル(サイドバー、または [/ha-washdata](/ha-washdata))にあります。", "data": { - "name": "Device Name", - "device_type": "Device Type", - "power_sensor": "Power Sensor", - "min_power": "Minimum Power Threshold (W)" + "name": "デバイス名", + "device_type": "デバイスの種類", + "power_sensor": "電力センサー", + "min_power": "最小電力しきい値 (W)" } } }, "error": { - "cannot_connect": "接続に失敗しました", - "invalid_auth": "認証に失敗しました", "unknown": "予期しないエラーが発生しました", - "invalid_power": "Power threshold must be greater than 0" + "invalid_power": "電力しきい値は 0 より大きくする必要があります" }, "abort": { "already_configured": "このデバイスはすでに設定されています", - "reconfigure_successful": "Reconfiguration was successful" + "reconfigure_successful": "再設定が成功しました" } }, "options": { "step": { "init": { - "title": "WashData Settings", - "description": "デバイス名を変更するか、主要なハードウェア設定を変更します。すべての検出・照合・通知の設定は [WashData パネル](/ha-washdata) で確認できます(サイドバーからもアクセス可能)。", + "title": "WashData 設定", + "description": "デバイス名や主要な設定を変更します。その他の設定はすべて、サイドバーの [WashData パネル](/ha-washdata) にあります。", "data": { "name": "デバイス名", - "device_type": "Device Type", - "power_sensor": "Power Sensor", - "min_power": "Minimum Power Threshold (W)" + "device_type": "デバイスの種類", + "power_sensor": "電力センサー", + "min_power": "最小電力しきい値 (W)" } } }, @@ -60,7 +58,7 @@ "notify_live_waiting_message": "{device}: まだ一致するプロファイルがありません。", "vs_typical_longer": "通常より {pct}% 長い", "vs_typical_shorter": "通常より {pct}% 短い", - "invalid_power": "Power threshold must be greater than 0" + "invalid_power": "電力しきい値は 0 より大きくする必要があります" } }, "selector": { @@ -93,7 +91,7 @@ }, "profile_name": { "name": "プロファイル名", - "description": "既存のプロファイル名(プロファイル管理メニューでプロファイルを作成)。ラベルを削除するには空白にします。" + "description": "既存のプロファイル名(プロファイルは WashData パネルで作成)。空欄にするとラベルを削除します。" } } }, @@ -143,13 +141,13 @@ }, "confidence_threshold": { "name": "信頼度しきい値", - "description": "ラベルを適用する最小照合信頼度(0.50-0.95)。" + "description": "ラベルを適用する最小照合信頼度(0.50-0.95)。空欄にすると、デバイスの「自動ラベル信頼度」設定が使用されます。" } } }, "export_config": { "name": "設定をエクスポート", - "description": "このデバイスのプロファイル、サイクル、設定を JSON ファイルにエクスポートします(デバイスごと)。", + "description": "このデバイスのプロファイル、サイクル、設定を JSON ファイルにエクスポートします。", "fields": { "device_id": { "name": "デバイス", @@ -177,7 +175,7 @@ }, "submit_cycle_feedback": { "name": "サイクルフィードバックを送信", - "description": "完了したサイクルの自動検出されたプログラムを確認または修正します。`entry_id`(上級者向け)または `device_id`(推奨)のいずれかを指定します。", + "description": "完了したサイクルで検出されたプログラムを確認または修正します。`device_id`(推奨)または `entry_id` を指定します。", "fields": { "device_id": { "name": "デバイス", @@ -283,7 +281,7 @@ }, "mark_unloaded": { "name": "取り出し済みとしてマーク", - "description": "WashData デバイスから終了した中身を取り出したことを確認し、クリーン状態と取り出しリマインダーを解除します。", + "description": "終了した中身を取り出したことを確認します。クリーン状態と取り出しリマインダーを解除します。", "fields": { "device_id": { "name": "デバイス", @@ -299,6 +297,32 @@ }, "match_ambiguity": { "name": "照合の曖昧さ" + }, + "maintenance_due": { + "name": "メンテナンス時期", + "state": { + "on": "要対応", + "off": "対応不要" + }, + "state_attributes": { + "due_task_ids": { + "name": "要対応タスク ID" + }, + "due_tasks": { + "name": "要対応タスク", + "state": { + "descale": "スケール除去", + "filter_clean": "フィルターの清掃", + "drum_clean": "ドラムの清掃", + "bearing_service": "ベアリングの整備", + "other": "その他", + "salt": "塩の補充", + "rinse_aid": "リンス剤の補充", + "lint_filter": "糸くずフィルターの清掃", + "condenser_clean": "コンデンサーの清掃" + } + } + } } }, "sensor": { @@ -316,10 +340,18 @@ "anti_wrinkle": "しわ防止", "interrupted": "中断", "force_stopped": "強制停止", - "rinse": "すすぎ", "unknown": "不明", "clean": "クリーン", "delay_wait": "開始を待っています" + }, + "state_attributes": { + "cycle_anomaly": { + "name": "サイクル異常", + "state": { + "overrun": "超過中", + "stalled": "停滞中" + } + } } }, "washer_program": { diff --git a/custom_components/ha_washdata/translations/ko.json b/custom_components/ha_washdata/translations/ko.json index 078eabc9..728879ec 100644 --- a/custom_components/ha_washdata/translations/ko.json +++ b/custom_components/ha_washdata/translations/ko.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "WashData 설정", - "description": "세탁기 또는 기타 가전제품을 구성하세요.\n\n전력 센서가 필요합니다.\n\n설정 후, 사이드바에서 WashData 패널을 열거나 [/ha-washdata](/ha-washdata)로 이동하여 사이클 확인, 검출 조정 및 프로파일 관리를 하세요.", + "description": "세탁기 또는 기타 가전제품을 설정합니다. 전력 센서가 필요합니다.\n\n설정 후 사이드바에서 WashData 패널([/ha-washdata](/ha-washdata))을 열면 사이클, 프로필, 모든 설정을 볼 수 있습니다.", "data": { "name": "장치 이름", "device_type": "장치 유형", @@ -12,25 +12,23 @@ }, "data_description": { "name": "이 장치의 이름입니다(예: '세탁기', '식기세척기').", - "device_type": "이 가전제품의 종류는 무엇입니까? 감지 및 라벨링 맞춤에 도움이 됩니다.", - "power_sensor": "스마트 플러그에서 실시간 전력 소비(와트)를 보고하는 센서 엔터티입니다.", - "min_power": "이 임계값(와트)보다 높은 전력 수치는 가전제품이 작동 중임을 나타냅니다. 대부분의 장치에서는 2W로 시작하세요." + "device_type": "이 종류의 가전제품에 맞는 감지 기본값을 설정합니다.", + "power_sensor": "실시간 전력을 와트 단위로 보고하는 스마트 플러그 센서입니다.", + "min_power": "이 값(와트)보다 높은 측정값은 가전제품이 작동 중임을 뜻합니다. 대부분의 장치에는 2 W가 적합합니다." } }, "reconfigure": { - "title": "Reconfigure WashData", - "description": "장치 이름, 가전 유형 또는 전력 센서를 업데이트합니다.\n\nWashData 패널(사이드바 또는 [/ha-washdata](/ha-washdata))에서 검출, 사이클, 프로파일 및 알림을 관리합니다.", + "title": "WashData 재구성", + "description": "장치 이름, 가전 유형 또는 전력 센서를 변경합니다.\n\n그 밖의 모든 설정은 WashData 패널(사이드바 또는 [/ha-washdata](/ha-washdata))에 있습니다.", "data": { - "name": "Device Name", - "device_type": "Device Type", - "power_sensor": "Power Sensor", - "min_power": "Minimum Power Threshold (W)" + "name": "장치 이름", + "device_type": "장치 유형", + "power_sensor": "전력 센서", + "min_power": "최소 전력 임계값 (W)" } } }, "error": { - "cannot_connect": "연결하지 못했습니다", - "invalid_auth": "인증 실패", "unknown": "예기치 않은 오류", "invalid_power": "전력 임계값은 0보다 커야 합니다" }, @@ -42,13 +40,13 @@ "options": { "step": { "init": { - "title": "WashData Settings", - "description": "장치 이름을 변경하거나 핵심 하드웨어 설정을 변경합니다. 모든 검출, 매칭 및 알림 설정은 [WashData 패널](/ha-washdata)에서 확인할 수 있습니다(사이드바에서도 접근 가능).", + "title": "WashData 설정", + "description": "장치 이름이나 핵심 설정을 변경합니다. 그 밖의 모든 설정은 사이드바의 [WashData 패널](/ha-washdata)에 있습니다.", "data": { "name": "장치 이름", - "device_type": "Device Type", - "power_sensor": "Power Sensor", - "min_power": "Minimum Power Threshold (W)" + "device_type": "장치 유형", + "power_sensor": "전력 센서", + "min_power": "최소 전력 임계값 (W)" } } }, @@ -93,7 +91,7 @@ }, "profile_name": { "name": "프로필 이름", - "description": "기존 프로필의 이름입니다(프로필 관리 메뉴에서 프로필 생성). 라벨을 제거하려면 비워두세요." + "description": "기존 프로필 이름입니다(프로필은 WashData 패널에서 생성). 비워 두면 라벨이 제거됩니다." } } }, @@ -143,13 +141,13 @@ }, "confidence_threshold": { "name": "신뢰도 임계값", - "description": "라벨을 적용하기 위한 최소 일치 신뢰도(0.50-0.95)입니다." + "description": "라벨을 적용하기 위한 최소 일치 신뢰도(0.50-0.95)입니다. 비워 두면 기기의 자동 라벨 신뢰도 설정이 사용됩니다." } } }, "export_config": { "name": "구성 내보내기", - "description": "이 장치의 프로필, 사이클 및 설정을 JSON 파일로 내보냅니다(장치별).", + "description": "이 장치의 프로필, 사이클 및 설정을 JSON 파일로 내보냅니다.", "fields": { "device_id": { "name": "장치", @@ -177,7 +175,7 @@ }, "submit_cycle_feedback": { "name": "사이클 피드백 제출", - "description": "사이클이 완료된 후 자동 감지된 프로그램을 확인하거나 수정합니다. `entry_id`(고급) 또는 `device_id`(권장)를 제공하세요.", + "description": "완료된 사이클에서 감지된 프로그램을 확인하거나 수정합니다. `device_id`(권장) 또는 `entry_id`를 지정하세요.", "fields": { "device_id": { "name": "장치", @@ -272,18 +270,18 @@ } }, "trigger_ml_training": { - "name": "Trigger ML Training", + "name": "ML 훈련 시작", "description": "이 기기의 자체 라벨이 지정된 주기에서 기기 내 ML 모델을 수동으로 재교육합니다(실험용).", "fields": { "device_id": { - "name": "Device", - "description": "The WashData device to retrain models for." + "name": "장치", + "description": "모델을 재훈련할 WashData 장치입니다." } } }, "mark_unloaded": { "name": "꺼냄으로 표시", - "description": "WashData 장치에서 완료된 내용물을 꺼냈음을 확인하여 깨끗한 상태와 모든 꺼내기 알림을 해제합니다.", + "description": "완료된 내용물을 꺼냈음을 확인합니다. 깨끗한 상태와 모든 꺼내기 알림을 해제합니다.", "fields": { "device_id": { "name": "장치", @@ -299,6 +297,32 @@ }, "match_ambiguity": { "name": "일치 모호도" + }, + "maintenance_due": { + "name": "유지보수 예정", + "state": { + "on": "예정", + "off": "예정 없음" + }, + "state_attributes": { + "due_task_ids": { + "name": "예정 작업 ID" + }, + "due_tasks": { + "name": "예정 작업", + "state": { + "descale": "석회질 제거", + "filter_clean": "필터 청소", + "drum_clean": "드럼 청소", + "bearing_service": "베어링 정비", + "other": "기타", + "salt": "소금 보충", + "rinse_aid": "린스 보충", + "lint_filter": "보풀 필터 청소", + "condenser_clean": "콘덴서 청소" + } + } + } } }, "sensor": { @@ -316,10 +340,18 @@ "anti_wrinkle": "주름 방지", "interrupted": "중단됨", "force_stopped": "강제 정지됨", - "rinse": "헹굼", "unknown": "알 수 없음", "clean": "깨끗한", "delay_wait": "시작을 기다리는 중" + }, + "state_attributes": { + "cycle_anomaly": { + "name": "사이클 이상", + "state": { + "overrun": "지연 중", + "stalled": "멈춤" + } + } } }, "washer_program": { diff --git a/custom_components/ha_washdata/translations/lt.json b/custom_components/ha_washdata/translations/lt.json index 46474f35..e2c270f6 100644 --- a/custom_components/ha_washdata/translations/lt.json +++ b/custom_components/ha_washdata/translations/lt.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "WashData sąranka", - "description": "Sukonfigūruokite skalbimo mašiną ar kitą prietaisą.\n\nReikalingas galios jutiklis.\n\nBaigę sąranką, atidarykite WashData skydelį iš šoninės juostos (arba eikite į [/ha-washdata](/ha-washdata)), kad peržiūrėtumėte ciklus, sureguliuotumėte aptikimą ir tvarkytumėte profilius.", + "description": "Sukonfigūruokite skalbimo mašiną ar kitą prietaisą. Reikalingas galios jutiklis.\n\nBaigę sąranką, atidarykite WashData skydelį šoninėje juostoje ([/ha-washdata](/ha-washdata)), kad matytumėte ciklus, profilius ir visus nustatymus.", "data": { "name": "Įrenginio pavadinimas", "device_type": "Įrenginio tipas", @@ -12,14 +12,14 @@ }, "data_description": { "name": "Patogus šio įrenginio pavadinimas (pvz., 'Skalbimo mašina', 'Indaplovė').", - "device_type": "Kokio tipo tai prietaisas? Padeda pritaikyti aptikimą ir ženklinimą.", - "power_sensor": "Jutiklio objektas, kuris realiuoju laiku praneša energijos suvartojimą (vatais) iš jūsų išmaniojo kištuko.", - "min_power": "Galios rodmenys, viršijantys šią ribą (vatais), rodo, kad prietaisas veikia. Pradėkite nuo 2 W daugeliui įrenginių." + "device_type": "Nustato šio prietaiso tipo numatytuosius aptikimo nustatymus.", + "power_sensor": "Išmaniojo kištuko jutiklis, pranešantis esamą galią vatais.", + "min_power": "Rodmenys virš šios ribos (vatais) reiškia, kad prietaisas veikia. Daugeliui įrenginių tinka 2 W." } }, "reconfigure": { "title": "WashData rekonfigūracija", - "description": "Atnaujinkite įrenginio pavadinimą, prietaiso tipą arba galios jutiklį.\n\nWashData skydelyje (šoninė juosta arba [/ha-washdata](/ha-washdata)) tvarkomas aptikimas, ciklai, profiliai ir pranešimai.", + "description": "Pakeiskite įrenginio pavadinimą, prietaiso tipą arba galios jutiklį.\n\nVisa kita yra WashData skydelyje (šoninė juosta arba [/ha-washdata](/ha-washdata)).", "data": { "name": "Įrenginio pavadinimas", "device_type": "Įrenginio tipas", @@ -29,8 +29,6 @@ } }, "error": { - "cannot_connect": "Nepavyko prisijungti", - "invalid_auth": "Neteisingas autentifikavimas", "unknown": "Netikėta klaida", "invalid_power": "Galios slenkstis turi būti didesnis nei 0" }, @@ -43,7 +41,7 @@ "step": { "init": { "title": "WashData nustatymai", - "description": "Pervadinkite įrenginį arba pakeiskite pagrindinius aparatinės įrangos nustatymus. Visi aptikimo, atitikimo ir pranešimų nustatymai yra [WashData skydelyje](/ha-washdata) (taip pat pasiekiami iš šoninės juostos).", + "description": "Pervadinkite įrenginį arba pakeiskite pagrindinius nustatymus. Visa kita yra [WashData skydelyje](/ha-washdata) šoninėje juostoje.", "data": { "name": "Įrenginio pavadinimas", "device_type": "Įrenginio tipas", @@ -93,7 +91,7 @@ }, "profile_name": { "name": "Profilio pavadinimas", - "description": "Esamo profilio pavadinimas (sukurkite profilius meniu Tvarkyti profilius). Palikite tuščią, kad pašalintumėte etiketę." + "description": "Esamo profilio pavadinimas (profiliai kuriami WashData skydelyje). Palikite tuščią, kad pašalintumėte žymę." } } }, @@ -143,13 +141,13 @@ }, "confidence_threshold": { "name": "Patikimumo slenkstis", - "description": "Minimalus atitikties patikimumas (0,50–0,95) etiketėms taikyti." + "description": "Minimalus atitikties patikimumas (0,50-0,95) etiketėms taikyti. Palikite tuščią, kad būtų naudojamas įrenginio nustatymas „Automatinio žymėjimo pasitikėjimas“." } } }, "export_config": { "name": "Eksportuoti konfigūraciją", - "description": "Eksportuoti šio įrenginio profilius, ciklus ir nustatymus į JSON failą (kiekvienam įrenginiui).", + "description": "Eksportuoti šio įrenginio profilius, ciklus ir nustatymus į JSON failą.", "fields": { "device_id": { "name": "Įrenginys", @@ -177,7 +175,7 @@ }, "submit_cycle_feedback": { "name": "Pateikti ciklo atsiliepimą", - "description": "Patvirtinti arba pataisyti automatiškai aptiktą programą po baigto ciklo. Pateikite `entry_id` (išplėstinis) arba `device_id` (rekomenduojama).", + "description": "Patvirtinti arba pataisyti baigto ciklo aptiktą programą. Nurodykite `device_id` (rekomenduojama) arba `entry_id`.", "fields": { "device_id": { "name": "Įrenginys", @@ -283,7 +281,7 @@ }, "mark_unloaded": { "name": "Pažymėti kaip iškrautą", - "description": "Patvirtinkite, kad baigto ciklo turinys išimtas iš „WashData“ įrenginio: būsena „Švarus“ ir iškrovimo priminimas bus panaikinti.", + "description": "Patvirtinti, kad baigto ciklo turinys išimtas. Išvalo būseną „Švarus“ ir iškrovimo priminimą.", "fields": { "device_id": { "name": "Įrenginys", @@ -299,6 +297,32 @@ }, "match_ambiguity": { "name": "Atitikties dviprasmiškumas" + }, + "maintenance_due": { + "name": "Reikia priežiūros", + "state": { + "on": "Reikia", + "off": "Nereikia" + }, + "state_attributes": { + "due_task_ids": { + "name": "Reikalingų užduočių ID" + }, + "due_tasks": { + "name": "Reikalingos užduotys", + "state": { + "descale": "Kalkių šalinimas", + "filter_clean": "Valyti filtrą", + "drum_clean": "Valyti būgną", + "bearing_service": "Guolių aptarnavimas", + "other": "Kita", + "salt": "Papildyti druskos", + "rinse_aid": "Papildyti skalavimo priemonės", + "lint_filter": "Valyti pūkų filtrą", + "condenser_clean": "Valyti kondensatorių" + } + } + } } }, "sensor": { @@ -316,10 +340,18 @@ "anti_wrinkle": "Apsauga nuo raukšlių", "interrupted": "Nutraukta", "force_stopped": "Priverstinai sustabdyta", - "rinse": "Skalavimas", "unknown": "Nežinoma", "clean": "Švarus", "delay_wait": "Laukiama pradžios" + }, + "state_attributes": { + "cycle_anomaly": { + "name": "Ciklo anomalija", + "state": { + "overrun": "Veikia ilgai", + "stalled": "Užstrigo" + } + } } }, "washer_program": { diff --git a/custom_components/ha_washdata/translations/lv.json b/custom_components/ha_washdata/translations/lv.json index 6a7ca3e4..9158d1ea 100644 --- a/custom_components/ha_washdata/translations/lv.json +++ b/custom_components/ha_washdata/translations/lv.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "WashData iestatīšana", - "description": "Konfigurējiet veļas mašīnu vai citu ierīci.\n\nNepieciešams jaudas sensors.\n\nPēc iestatīšanas atveriet WashData paneli no sānjoslas (vai dodieties uz [/ha-washdata](/ha-washdata)), lai skatītu ciklus, pielāgotu noteikšanu un pārvaldītu profilus.", + "description": "Iestatiet veļas mašīnu vai citu ierīci. Nepieciešams jaudas sensors.\n\nPēc iestatīšanas atveriet WashData paneli no sānjoslas ([/ha-washdata](/ha-washdata)), lai redzētu ciklus, profilus un visus iestatījumus.", "data": { "name": "Ierīces nosaukums", "device_type": "Ierīces veids", @@ -12,14 +12,14 @@ }, "data_description": { "name": "Šīs ierīces draudzīgs nosaukums (piemēram, “Veļas mašīna”, “Trauku mazgājamā mašīna”).", - "device_type": "Kāda veida ierīce šī ir? Palīdz pielāgot noteikšanu un marķēšanu.", - "power_sensor": "Sensora entītija, kas ziņo reāllaika enerģijas patēriņu (vatos) no jūsu viedā spraudņa.", - "min_power": "Jaudas rādījumi, kas pārsniedz šo slieksni (vatos), norāda, ka ierīce darbojas. Lielākajai daļai ierīču sāciet ar 2 W." + "device_type": "Iestata šāda veida ierīcei piemērotus noteikšanas noklusējumus.", + "power_sensor": "Viedā spraudņa sensors, kas ziņo pašreizējo jaudu vatos.", + "min_power": "Rādījumi virs šīs vērtības (vatos) nozīmē, ka ierīce darbojas. Lielākajai daļai ierīču der 2 W." } }, "reconfigure": { "title": "WashData pārkonfigurēšana", - "description": "Atjauniniet ierīces nosaukumu, ierīces veidu vai jaudas sensoru.\n\nWashData panelī (sānjosla vai [/ha-washdata](/ha-washdata)) tiek pārvaldīta noteikšana, cikli, profili un paziņojumi.", + "description": "Mainiet ierīces nosaukumu, ierīces veidu vai jaudas sensoru.\n\nViss pārējais ir WashData panelī (sānjosla vai [/ha-washdata](/ha-washdata)).", "data": { "name": "Ierīces nosaukums", "device_type": "Ierīces veids", @@ -29,8 +29,6 @@ } }, "error": { - "cannot_connect": "Neizdevās izveidot savienojumu", - "invalid_auth": "Nederīga autentifikācija", "unknown": "Negaidīta kļūda", "invalid_power": "Jaudas slieksnim jābūt lielākam par 0" }, @@ -43,7 +41,7 @@ "step": { "init": { "title": "WashData iestatījumi", - "description": "Pārdēvējiet ierīci vai mainiet tās galvenos aparatūras iestatījumus. Visi noteikšanas, saskaņošanas un paziņojumu iestatījumi atrodas [WashData panelī](/ha-washdata) (pieejams arī no sānjoslas).", + "description": "Pārdēvējiet ierīci vai mainiet tās galvenos iestatījumus. Viss pārējais ir [WashData panelī](/ha-washdata) sānjoslā.", "data": { "name": "Ierīces nosaukums", "device_type": "Ierīces veids", @@ -93,7 +91,7 @@ }, "profile_name": { "name": "Profila nosaukums", - "description": "Esoša profila nosaukums (izveidojiet profilus izvēlnē Pārvaldīt profilus). Atstājiet tukšu, lai noņemtu etiķeti." + "description": "Esoša profila nosaukums (profilus izveido WashData panelī). Atstājiet tukšu, lai noņemtu etiķeti." } } }, @@ -143,13 +141,13 @@ }, "confidence_threshold": { "name": "Pārliecības slieksnis", - "description": "Minimālā atbilstības ticamība (0,50–0,95), lai lietotu etiķetes." + "description": "Minimālā atbilstības ticamība (0,50-0,95), lai lietotu etiķetes. Atstājiet tukšu, lai izmantotu ierīces iestatījumu Automātiskās marķēšanas pārliecība." } } }, "export_config": { "name": "Eksportēt konfigurāciju", - "description": "Eksportējiet šīs ierīces profilus, ciklus un iestatījumus JSON failā (katrai ierīcei).", + "description": "Eksportējiet šīs ierīces profilus, ciklus un iestatījumus JSON failā.", "fields": { "device_id": { "name": "Ierīce", @@ -177,7 +175,7 @@ }, "submit_cycle_feedback": { "name": "Iesniedziet cikla atsauksmes", - "description": "Pēc cikla pabeigšanas apstipriniet vai labojiet automātiski noteikto programmu. Norādiet vai nu “entry_id” (papildu) vai “device_id” (ieteicams).", + "description": "Apstipriniet vai labojiet pabeigta cikla noteikto programmu. Norādiet `device_id` (ieteicams) vai `entry_id`.", "fields": { "device_id": { "name": "Ierīce", @@ -283,7 +281,7 @@ }, "mark_unloaded": { "name": "Atzīmēt kā izkrautu", - "description": "Apstipriniet, ka pabeigtā cikla saturs ir izņemts no WashData ierīces: stāvoklis „Tīrs“ un izkraušanas atgādinājums tiks notīrīti.", + "description": "Apstipriniet, ka pabeigtā cikla saturs ir izņemts. Notīra stāvokli „Tīrs“ un izkraušanas atgādinājumu.", "fields": { "device_id": { "name": "Ierīce", @@ -299,6 +297,32 @@ }, "match_ambiguity": { "name": "Atbilstības neskaidrība" + }, + "maintenance_due": { + "name": "Jāveic apkope", + "state": { + "on": "Jāveic", + "off": "Nav jāveic" + }, + "state_attributes": { + "due_task_ids": { + "name": "Jāveicamo uzdevumu ID" + }, + "due_tasks": { + "name": "Jāveicamie uzdevumi", + "state": { + "descale": "Atkaļķošana", + "filter_clean": "Tīrīt filtru", + "drum_clean": "Tīrīt tromeli", + "bearing_service": "Gultņu apkope", + "other": "Cits", + "salt": "Papildināt sāli", + "rinse_aid": "Papildināt skalošanas līdzekli", + "lint_filter": "Tīrīt plūksnu filtru", + "condenser_clean": "Tīrīt kondensatoru" + } + } + } } }, "sensor": { @@ -316,10 +340,18 @@ "anti_wrinkle": "Pretgrumbu", "interrupted": "Pārtrauca", "force_stopped": "Piespiedu kārtā apturēta", - "rinse": "Noskalo", "unknown": "Nezināms", "clean": "Tīrs", "delay_wait": "Gaida sākumu" + }, + "state_attributes": { + "cycle_anomaly": { + "name": "Cikla anomālija", + "state": { + "overrun": "Darbojas ilgi", + "stalled": "Iestrēdzis" + } + } } }, "washer_program": { diff --git a/custom_components/ha_washdata/translations/mk.json b/custom_components/ha_washdata/translations/mk.json index 6de59072..b47a1532 100644 --- a/custom_components/ha_washdata/translations/mk.json +++ b/custom_components/ha_washdata/translations/mk.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "Поставување податоци за перење", - "description": "Конфигурирајте ја вашата машина за перење или друг уред.\n\nПотребен е сензор за напојување.\n\nПо поставувањето, отворете го панелот на WashData од страничната лента (или одете на [/ha-washdata](/ha-washdata)) за преглед на циклусите, подесување на откривањето и управување со профили.", + "description": "Поставете машина за перење или друг апарат. Потребен е сензор за напојување.\n\nПо поставувањето отворете го панелот WashData од страничната лента ([/ha-washdata](/ha-washdata)): таму се циклусите, профилите и сите поставки.", "data": { "name": "Име на уред", "device_type": "Тип на уред", @@ -12,14 +12,14 @@ }, "data_description": { "name": "Пријателско име за овој уред (на пр., „Машина за перење“, „Миење садови“).", - "device_type": "Каков тип на апарат е ова? Помага при прилагодување на откривањето и етикетирањето.", - "power_sensor": "Сензорскиот ентитет кој известува за потрошувачката на енергија во реално време (во вати) од вашиот паметен приклучок.", - "min_power": "Читањата на моќноста над овој праг (во вати) покажуваат дека апаратот работи. Започнете со 2W за повеќето уреди." + "device_type": "Ги поставува стандардните вредности за откривање за овој вид апарат.", + "power_sensor": "Сензорот на паметниот приклучок што ја јавува тековната моќност во вати.", + "min_power": "Читања над оваа вредност (во вати) значат дека апаратот работи. 2 W одговара за повеќето уреди." } }, "reconfigure": { "title": "Реконфигурирај WashData", - "description": "Ажурирајте го името на уредот, типот на апаратот или сензорот за напојување.\n\nПанелот на WashData (странична лента или [/ha-washdata](/ha-washdata)) е местото каде се управуваат откривањето, циклусите, профилите и известувањата.", + "description": "Сменете го името на уредот, типот на апаратот или сензорот за напојување.\n\nСè друго е во панелот WashData (странична лента или [/ha-washdata](/ha-washdata)).", "data": { "name": "Име на уред", "device_type": "Тип на уред", @@ -29,8 +29,6 @@ } }, "error": { - "cannot_connect": "Не успеа да се поврзе", - "invalid_auth": "Неважечка автентикација", "unknown": "Неочекувана грешка", "invalid_power": "Прагот на моќност мора да биде поголем од 0" }, @@ -42,8 +40,8 @@ "options": { "step": { "init": { - "title": "WashData Settings", - "description": "Преименувајте го уредот или сменете ги неговите основни хардверски поставки. Сите поставки за откривање, совпаѓање и известување се во [панелот на WashData](/ha-washdata) (достапен и од страничната лента).", + "title": "Поставки на WashData", + "description": "Преименувајте го уредот или сменете ги неговите основни поставки. Сè друго е во [панелот WashData](/ha-washdata) во страничната лента.", "data": { "name": "Име на уред", "device_type": "Тип на уред", @@ -93,7 +91,7 @@ }, "profile_name": { "name": "Име на профилот", - "description": "Името на постоечки профил (создајте профили во менито Управување со профили). Оставете празно за да ја отстраните етикетата." + "description": "Име на постоечки профил (профилите се создаваат во панелот WashData). Оставете празно за да ја отстраните етикетата." } } }, @@ -143,13 +141,13 @@ }, "confidence_threshold": { "name": "Праг на доверба", - "description": "Минимална сигурност за совпаѓање (0,50-0,95) за примена на етикети." + "description": "Минимална сигурност за совпаѓање (0,50-0,95) за примена на етикети. Оставете празно за да се користи поставката „Доверба за автоматско означување“ на уредот." } } }, "export_config": { "name": "Извези конфигурација", - "description": "Извезете ги профилите, циклусите и поставките на овој уред во датотека JSON (по уред).", + "description": "Извезете ги профилите, циклусите и поставките на овој уред во JSON датотека.", "fields": { "device_id": { "name": "Уред", @@ -177,7 +175,7 @@ }, "submit_cycle_feedback": { "name": "Испратете повратни информации за циклусот", - "description": "Потврдете или поправете програма што е автоматски откриена по завршен циклус. Наведете „idry_id“ (напредно) или „device_id“ (препорачано).", + "description": "Потврдете ја или поправете ја откриената програма на завршен циклус. Наведете `device_id` (препорачано) или `entry_id`.", "fields": { "device_id": { "name": "Уред", @@ -272,7 +270,7 @@ } }, "trigger_ml_training": { - "name": "Trigger ML Training", + "name": "Покрени ML обука", "description": "Рачно обучете ги моделите ML на уредот од означените циклуси на овој уред (експериментални).", "fields": { "device_id": { @@ -283,7 +281,7 @@ }, "mark_unloaded": { "name": "Означи како истоварено", - "description": "Потврдете дека завршената содржина е извадена од уредот WashData, со што се брише состојбата \"Чиста\" и секој потсетник за истовар.", + "description": "Потврдете дека готовата содржина е извадена. Ја брише состојбата \"Чиста\" и потсетникот за истовар.", "fields": { "device_id": { "name": "Уред", @@ -299,6 +297,32 @@ }, "match_ambiguity": { "name": "Нејасност на совпаѓање" + }, + "maintenance_due": { + "name": "Треба одржување", + "state": { + "on": "Треба", + "off": "Не треба" + }, + "state_attributes": { + "due_task_ids": { + "name": "ID на задачи за извршување" + }, + "due_tasks": { + "name": "Задачи за извршување", + "state": { + "descale": "Отстранување на бигор", + "filter_clean": "Исчисти го филтерот", + "drum_clean": "Исчисти го барабанот", + "bearing_service": "Сервис на лежишта", + "other": "Друго", + "salt": "Дополни сол", + "rinse_aid": "Дополни средство за плакнење", + "lint_filter": "Исчисти го филтерот за мовчиња", + "condenser_clean": "Исчисти го кондензаторот" + } + } + } } }, "sensor": { @@ -316,10 +340,18 @@ "anti_wrinkle": "Против брчки", "interrupted": "Прекинато", "force_stopped": "Сила запре", - "rinse": "Исплакнете", "unknown": "Непознат", "clean": "Чиста", "delay_wait": "Се чека да започне" + }, + "state_attributes": { + "cycle_anomaly": { + "name": "Аномалија на циклусот", + "state": { + "overrun": "Трае предолго", + "stalled": "Заглавено" + } + } } }, "washer_program": { diff --git a/custom_components/ha_washdata/translations/nb.json b/custom_components/ha_washdata/translations/nb.json index a95781ec..27b87505 100644 --- a/custom_components/ha_washdata/translations/nb.json +++ b/custom_components/ha_washdata/translations/nb.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "WashData-oppsett", - "description": "Konfigurer vaskemaskinen din eller et annet apparat.\n\nStrømsensor er nødvendig.\n\nEtter oppsett åpner du WashData-panelet fra sidefeltet (eller gå til [/ha-washdata](/ha-washdata)) for å se sykluser, justere registrering og administrere profiler.", + "description": "Sett opp en vaskemaskin eller et annet apparat. En strømsensor er påkrevd.\n\nÅpne etter oppsettet WashData-panelet fra sidefeltet ([/ha-washdata](/ha-washdata)) for å se sykluser, profiler og alle innstillinger.", "data": { "name": "Enhetsnavn", "device_type": "Enhetstype", @@ -12,14 +12,14 @@ }, "data_description": { "name": "Et vennlig navn for denne enheten (f.eks. \"Vaskemaskin\", \"Oppvaskmaskin\").", - "device_type": "Hva slags apparat er dette? Hjelper med å skreddersy deteksjon og merking.", - "power_sensor": "Sensorenheten som rapporterer strømforbruk i sanntid (i watt) fra smartpluggen din.", - "min_power": "Effektavlesninger over denne terskelen (i watt) indikerer at apparatet er i gang. Start med 2W for de fleste enheter." + "device_type": "Angir deteksjonsstandarder for denne typen apparat.", + "power_sensor": "Smartpluggens sensor som rapporterer gjeldende effekt i watt.", + "min_power": "Målinger over denne verdien (i watt) betyr at apparatet går. 2 W passer for de fleste enheter." } }, "reconfigure": { "title": "Rekonfigurer WashData", - "description": "Oppdater enhetsnavnet, apparattypen eller strømsensoren.\n\nWashData-panelet (sidefeltet eller [/ha-washdata](/ha-washdata)) er der registrering, sykluser, profiler og varsler administreres.", + "description": "Endre enhetsnavn, apparattype eller strømsensor.\n\nAlt annet finnes i WashData-panelet (sidefeltet eller [/ha-washdata](/ha-washdata)).", "data": { "name": "Enhetsnavn", "device_type": "Enhetstype", @@ -29,8 +29,6 @@ } }, "error": { - "cannot_connect": "Kunne ikke koble til", - "invalid_auth": "Ugyldig autentisering", "unknown": "Uventet feil", "invalid_power": "Effektterskelen må være større enn 0" }, @@ -43,7 +41,7 @@ "step": { "init": { "title": "WashData-innstillinger", - "description": "Gi enheten nytt navn eller endre de grunnleggende maskinvareinnstillingene. Alle innstillinger for registrering, matching og varsler finnes i [WashData-panelet](/ha-washdata) (også tilgjengelig fra sidefeltet).", + "description": "Gi enheten nytt navn eller endre grunninnstillingene. Alt annet finnes i [WashData-panelet](/ha-washdata) i sidefeltet.", "data": { "name": "Enhetsnavn", "device_type": "Enhetstype", @@ -70,7 +68,7 @@ "dryer": "Tørketrommel", "washer_dryer": "Vaske-tørketrommel kombi", "dishwasher": "Oppvaskmaskin", - "air_fryer": "Air Fryer", + "air_fryer": "Airfryer", "bread_maker": "Brødbaker", "pump": "Pumpe / Sump Pump", "generic": "Annet (avansert)", @@ -93,7 +91,7 @@ }, "profile_name": { "name": "Profilnavn", - "description": "Navnet på en eksisterende profil (opprett profiler i Administrer profiler-menyen). La stå tomt for å fjerne etiketten." + "description": "Navnet på en eksisterende profil (opprett profiler i WashData-panelet). La stå tomt for å fjerne merkingen." } } }, @@ -143,13 +141,13 @@ }, "confidence_threshold": { "name": "Konfidensgrense", - "description": "Minste matchkonfidens (0,50–0,95) for å bruke etiketter." + "description": "Minste matchkonfidens (0,50–0,95) for å bruke etiketter. La feltet stå tomt for å bruke enhetens innstilling \"Auto-merking konfidensgrad\"." } } }, "export_config": { "name": "Eksporter konfig", - "description": "Eksporter denne enhetens profiler, sykluser og innstillinger til en JSON-fil (per enhet).", + "description": "Eksporter denne enhetens profiler, sykluser og innstillinger til en JSON-fil.", "fields": { "device_id": { "name": "Enhet", @@ -177,7 +175,7 @@ }, "submit_cycle_feedback": { "name": "Send tilbakemelding om syklus", - "description": "Bekreft eller korriger et automatisk oppdaget program etter en fullført syklus. Oppgi enten «entry_id» (avansert) eller «device_id» (anbefalt).", + "description": "Bekreft eller korriger det oppdagede programmet for en fullført syklus. Oppgi `device_id` (anbefalt) eller `entry_id`.", "fields": { "device_id": { "name": "Enhet", @@ -283,7 +281,7 @@ }, "mark_unloaded": { "name": "Merk som utlastet", - "description": "Bekreft at den ferdige vasken er tatt ut av en WashData-enhet, noe som fjerner tilstanden \"Rengjøre\" og en eventuell utlastingspåminnelse.", + "description": "Bekreft at den ferdige vasken er tatt ut. Fjerner tilstanden \"Ren\" og en eventuell utlastingspåminnelse.", "fields": { "device_id": { "name": "Enhet", @@ -299,6 +297,32 @@ }, "match_ambiguity": { "name": "Match Tvetydighet" + }, + "maintenance_due": { + "name": "Vedlikehold forfaller", + "state": { + "on": "Forfalt", + "off": "Ikke forfalt" + }, + "state_attributes": { + "due_task_ids": { + "name": "ID-er for forfalte oppgaver" + }, + "due_tasks": { + "name": "Forfalte oppgaver", + "state": { + "descale": "Avkalking", + "filter_clean": "Rengjør filter", + "drum_clean": "Rengjør trommel", + "bearing_service": "Lagerservice", + "other": "Annet", + "salt": "Fyll på salt", + "rinse_aid": "Fyll på glansemiddel", + "lint_filter": "Rengjør lofilter", + "condenser_clean": "Rengjør kondensator" + } + } + } } }, "sensor": { @@ -316,10 +340,18 @@ "anti_wrinkle": "Anti-rynke", "interrupted": "Avbrutt", "force_stopped": "Tving stoppet", - "rinse": "Skylle", "unknown": "Ukjent", - "clean": "Rengjøre", + "clean": "Ren", "delay_wait": "Venter på å starte" + }, + "state_attributes": { + "cycle_anomaly": { + "name": "Syklusavvik", + "state": { + "overrun": "Kjører lenger", + "stalled": "Stoppet opp" + } + } } }, "washer_program": { diff --git a/custom_components/ha_washdata/translations/nl.json b/custom_components/ha_washdata/translations/nl.json index 04e444ec..a5e71448 100644 --- a/custom_components/ha_washdata/translations/nl.json +++ b/custom_components/ha_washdata/translations/nl.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "WashData-instellingen", - "description": "Configureer uw wasmachine of ander apparaat.\n\nVermogenssensor is vereist.\n\nNa de installatie opent u het WashData-paneel via de zijbalk (of ga naar [/ha-washdata](/ha-washdata)) om cycli te bekijken, detectie af te stellen en profielen te beheren.", + "description": "Stel een wasmachine of ander apparaat in. Een vermogenssensor is vereist.\n\nOpen na de installatie het WashData-paneel via de zijbalk ([/ha-washdata](/ha-washdata)) voor cycli, profielen en alle instellingen.", "data": { "name": "Apparaatnaam", "device_type": "Apparaattype", @@ -12,14 +12,14 @@ }, "data_description": { "name": "Een beschrijvende naam voor dit apparaat (bijvoorbeeld 'Wasmachine', 'Vaatwasser').", - "device_type": "Welk type apparaat is dit? Helpt bij het op maat maken van detectie en labeling.", - "power_sensor": "De sensorentiteit die het realtime energieverbruik (in watt) van uw slimme stekker rapporteert.", - "min_power": "Vermogensmetingen boven deze drempel (in watt) geven aan dat het apparaat werkt. Begin met 2W voor de meeste apparaten." + "device_type": "Stelt de detectiestandaarden in voor dit soort apparaat.", + "power_sensor": "De sensor van de slimme stekker die het actuele vermogen in watt meldt.", + "min_power": "Metingen boven deze waarde (in watt) betekenen dat het apparaat draait. 2 W past bij de meeste apparaten." } }, "reconfigure": { "title": "WashData opnieuw configureren", - "description": "Werk de apparaatnaam, het type apparaat of de vermogenssensor bij.\n\nHet WashData-paneel (zijbalk of [/ha-washdata](/ha-washdata)) is waar detectie, cycli, profielen en meldingen worden beheerd.", + "description": "Wijzig de apparaatnaam, het apparaattype of de vermogenssensor.\n\nAl het andere staat in het WashData-paneel (zijbalk of [/ha-washdata](/ha-washdata)).", "data": { "name": "Apparaatnaam", "device_type": "Apparaattype", @@ -29,8 +29,6 @@ } }, "error": { - "cannot_connect": "Kan geen verbinding maken", - "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout", "invalid_power": "Vermogensdrempel moet groter zijn dan 0" }, @@ -43,7 +41,7 @@ "step": { "init": { "title": "WashData-instellingen", - "description": "Geef het apparaat een nieuwe naam of wijzig de basishardware-instellingen. Alle instellingen voor detectie, matching en meldingen bevinden zich in het [WashData-paneel](/ha-washdata) (ook bereikbaar via de zijbalk).", + "description": "Geef het apparaat een nieuwe naam of wijzig de basisinstellingen. Al het andere staat in het [WashData-paneel](/ha-washdata) in de zijbalk.", "data": { "name": "Apparaatnaam", "device_type": "Apparaattype", @@ -93,7 +91,7 @@ }, "profile_name": { "name": "Profielnaam", - "description": "De naam van een bestaand profiel (maak profielen aan in het menu Profielen beheren). Laat leeg om het label te verwijderen." + "description": "Naam van een bestaand profiel (maak profielen aan in het WashData-paneel). Laat leeg om het label te verwijderen." } } }, @@ -143,13 +141,13 @@ }, "confidence_threshold": { "name": "Betrouwbaarheidsdrempel", - "description": "Minimale matchbetrouwbaarheid (0,50-0,95) om labels toe te passen." + "description": "Minimale matchbetrouwbaarheid (0,50-0,95) om labels toe te passen. Laat leeg om de instelling \"Auto-label betrouwbaarheid\" van het apparaat te gebruiken." } } }, "export_config": { "name": "Configuratie exporteren", - "description": "Exporteer de profielen, cycli en instellingen van dit apparaat naar een JSON-bestand (per apparaat).", + "description": "Exporteer de profielen, cycli en instellingen van dit apparaat naar een JSON-bestand.", "fields": { "device_id": { "name": "Apparaat", @@ -177,7 +175,7 @@ }, "submit_cycle_feedback": { "name": "Cyclusfeedback indienen", - "description": "Bevestig of corrigeer een automatisch gedetecteerd programma na een voltooide cyclus. Geef `entry_id` (geavanceerd) of `device_id` (aanbevolen) op.", + "description": "Bevestig of corrigeer het gedetecteerde programma van een voltooide cyclus. Geef `device_id` (aanbevolen) of `entry_id` op.", "fields": { "device_id": { "name": "Apparaat", @@ -283,7 +281,7 @@ }, "mark_unloaded": { "name": "Markeren als uitgeladen", - "description": "Bevestig dat de voltooide lading uit een WashData-apparaat is gehaald, waarmee de staat \"Schoon\" en een eventuele uitlaadherinnering worden gewist.", + "description": "Bevestig dat de voltooide lading is uitgehaald. Wist de staat \"Schoon\" en een eventuele uitlaadherinnering.", "fields": { "device_id": { "name": "Apparaat", @@ -299,6 +297,32 @@ }, "match_ambiguity": { "name": "Match-dubbelzinnigheid" + }, + "maintenance_due": { + "name": "Onderhoud nodig", + "state": { + "on": "Nodig", + "off": "Niet nodig" + }, + "state_attributes": { + "due_task_ids": { + "name": "ID's van openstaande taken" + }, + "due_tasks": { + "name": "Openstaande taken", + "state": { + "descale": "Ontkalken", + "filter_clean": "Filter reinigen", + "drum_clean": "Trommel reinigen", + "bearing_service": "Lageronderhoud", + "other": "Overig", + "salt": "Zout bijvullen", + "rinse_aid": "Glansmiddel bijvullen", + "lint_filter": "Pluizenfilter reinigen", + "condenser_clean": "Condensor reinigen" + } + } + } } }, "sensor": { @@ -316,10 +340,18 @@ "anti_wrinkle": "Anti-kreuk", "interrupted": "Onderbroken", "force_stopped": "Geforceerd gestopt", - "rinse": "Spoelen", "unknown": "Onbekend", "clean": "Schoon", "delay_wait": "Wachten om te beginnen" + }, + "state_attributes": { + "cycle_anomaly": { + "name": "Cyclusafwijking", + "state": { + "overrun": "Loopt langer", + "stalled": "Vastgelopen" + } + } } }, "washer_program": { diff --git a/custom_components/ha_washdata/translations/panel/bg.json b/custom_components/ha_washdata/translations/panel/bg.json index e38a5b78..72b567c8 100644 --- a/custom_components/ha_washdata/translations/panel/bg.json +++ b/custom_components/ha_washdata/translations/panel/bg.json @@ -3,6 +3,7 @@ "min": "мин", "entity_not_found": "Обектът не е намерен", "running_long": "Работи дълго", + "stalled": "Заседнал", "remaining": "остават", "progress": "напредък", "show_time_remaining": "Показване на оставащото време", @@ -48,16 +49,16 @@ } }, "badge": { - "quiet_tail_tip": "Близо до края тази програма утихва за около {mins} мин, след което консумира около {watts} Вт за {secs} сек, преди да завърши. Наблюдавано при {seen}/{measured} измерени цикъла: уредът не го прави при всяко пускане.", + "quiet_tail_tip": "Към края тази програма утихва за около {mins} мин, след което консумира около {watts} Вт за {secs} сек. Наблюдавано в {seen} от {measured} измерени цикъла.", "quiet_tail": "~{mins} мин тишина накрая", "artifact_tip": "{n} открити аномалии (напр. отворена врата по средата на цикъла) – отворете, за да ги видите на графиката", + "artifact_tip_one": "{n} открита аномалия (напр. отворена врата по средата на цикъла) – отворете, за да я видите на графиката", + "artifact_tip_other": "{n} открити аномалии (напр. отворена врата по средата на цикъла) – отворете, за да ги видите на графиката", "auto": "(автоматично разпознато)", "built_in_tag": "вградена", "declining": "↘ намалява", "energy_low": "По-ниска енергия от обичайното", "energy_spike": "По-висока енергия от обичайното", - "fair_fit": "приемливо качество", - "fair_fit_tip": "Умерена последователност на съвпадението – някои цикли, присвоени на този профил, имат по-ниски резултати за доверие. Маркирайте повече цикли или запишете отново профила, за да подобрите точността.", "feedback_requested": "Иска се обратна връзка", "golden_cycle": "Записан референтен цикъл", "duration_outlier": "Цикли с необичайна продължителност: {n}", @@ -67,26 +68,35 @@ "overrun": "Бяга по-дълго от обикновено", "underrun": "Завърши по-бързо от обикновено", "poor_fit": "лошо качество", - "poor_fit_tip": "Непоследователна история на съвпаденията – помислете за възстановяване на този профил", - "restart_gap_tip": "{n} HA пропуск при рестартиране: графиката на мощността има дупка", + "restart_gap_tip": "{n} HA пропуска при рестартиране: графиката на мощността има дупки", + "restart_gap_tip_one": "{n} HA пропуск при рестартиране: графиката на мощността има дупка", + "restart_gap_tip_other": "{n} HA пропуска при рестартиране: графиката на мощността има дупки", "reviewed": "Прегледано", "steady": "→ стабилен", - "using_defaults": "Използване на доставени настройки по подразбиране", - "using_tuned": "Използване на настроени тежести", "overrun_ratio": "({x}× от очакваното)", "underrun_ratio": "({pct}% от очакваното)", - "awaiting_tip": "Изчаква одобрение от общността", - "awaiting_n": "Изчаква одобрение · {n} потвърдени", - "approved": "Одобрено", - "awaiting": "Изчаква одобрение", "imported_tip": "Импортирано от магазина на общността. Използва се само за съпоставяне, не се брои в статистиката.", "unmatchable": "не може да се съпостави", "not_importable": "неприложимо", "exists": "съществува", "backfilled_tip": "Открито в импортирана история на мощността. Влияе само на съпоставянето на програми, не се брои в статистиката.", - "armed": "(важи за следващия цикъл)" + "armed": "(важи за следващия цикъл)", + "awaiting_tip": "Изчаква одобрение от общността", + "awaiting_n": "Изчаква одобрение · {n} потвърдени", + "awaiting": "Изчаква одобрение", + "approved": "Одобрено", + "matcher_unused": "Все още не е съпоставено с ваши цикли", + "matcher_used": "Съпоставено с {n} ваши цикъла", + "matcher_used_one": "Съпоставено с {n} ваш цикъл", + "matcher_used_other": "Съпоставено с {n} ваши цикъла", + "matcher_used_tip": "Цикли на този уред, които WashData сам е съпоставил с тази импортирана програма. Импорт, който никога не се съпоставя, може да не пасва на уреда ви; можете да го премахнете. Отчита се локално; нищо не се изпраща към магазина." }, "btn": { + "pg_preset_delete": "Изтриване на конфигурация", + "pg_preset_load": "Зареди конфигурация", + "pg_preset_load_tip": "Заменяне на стойностите в тестовата среда с тази конфигурация (активните настройки остават непроменени)", + "pg_preset_save": "Запазване като конфигурация", + "pg_preset_save_tip": "Запазване на всички стойности по-долу като именувана конфигурация за това устройство", "reset_zoom": "Нулиране на мащаба", "set_brand_model": "Задаване на марка и модел", "refresh_catalog": "Опресняване на каталога", @@ -97,12 +107,11 @@ "add_timer": "+ Добавете таймер", "apply_all": "Приложете всички", "apply_label": "Прилагане на етикет", - "apply_set_b": "Прилагане на набор B", "apply_split": "Приложете Разделяне", "apply_trim": "Приложете изрязването", "auto_detect_split": "Автоматично откриване", "auto_label_cycles": "Цикли на автоматично етикетиране", - "auto_label_cycles_tip": "Автоматично присвоява имена на профили на немаркирани цикли, чиято степен на достоверност на съвпадение надвишава прага", + "auto_label_cycles_tip": "Именувай немаркираните цикли, чиято увереност на съвпадение надхвърля прага.", "cancel": "Отказ", "clear_debug": "Изчистване на данните за отстраняване на грешки", "clear_splits": "Изчистване", @@ -112,8 +121,6 @@ "convert_to_automation": "Преобразувайте в автоматизация", "correct": "Коригирай…", "create": "Създавайте", - "create_from_cluster": "Създайте профил от {count} цикъла", - "create_profile": "+ Създаване на профил", "delete": "Изтриване", "delete_group": "Изтриване на група", "delete_group_tip": "Изтриване само на тази група - профилите на членовете се запазват", @@ -122,39 +129,30 @@ "discard_tip": "Изхвърли записаната следа без запазване", "dismiss": "Отхвърляне", "mute_suggestion": "Спрете да предлагате тази настройка", - "pg_load_calibrated": "Зареди калибрирани (ML) ({n})", - "pg_load_calibrated_tip": "Зареди ML-калибрираните настройки като замени в тестовата среда", "pg_load_live": "Зареди активните настройки", "pg_load_live_tip": "Повторно прочитане на текущите настройки на интеграцията и отхвърляне на всички промени в тестовата среда", "pg_load_suggested": "Зареди предложени ({n})", "pg_load_suggested_tip": "Зареди текущите предложения на автоматичния настройчик като замени в тестовата среда", - "pg_preset_delete": "Изтриване на конфигурация", - "pg_preset_load": "Зареди конфигурация", - "pg_preset_load_tip": "Заменяне на стойностите в тестовата среда с тази конфигурация (активните настройки остават непроменени)", - "pg_preset_save": "Запазване като конфигурация", - "pg_preset_save_tip": "Запазване на всички стойности по-долу като именувана конфигурация за това устройство", "pg_publish_all": "Приложи {n} към интеграцията", "pg_publish_one": "Приложи {label} към интеграцията", "reset_muted": "Нулиране на заглушените", + "reset_phase_tip": "Отменете редакцията си и възстановете вградената фаза", "done": "Готово", "edit": "Редактиране", "export": "Експортиране", - "export_json": "Експортиране в JSON", "force_stop": "Принудително спиране", "force_stop_tip": "Незабавно прекрати текущия цикъл и го маркирай като принудително спрян", "from_template": "От шаблон ▾", "ignore": "Игнорирайте", "import_json": "Импортиране от JSON", - "import_overwrite": "Импортиране (презаписва данни)", "inspect": "Инспектирайте", "label": "Етикет", - "load": "Зареди", "load_more": "Зареждане на още", "manage": "Управлявайте", "merge": "Обединяване", "new_automation": "+ Нова автоматизация", "new_group": "+ Нова група", - "new_group_tip": "Групирай почти идентични профили (еднаква форма/продължителност, различна температура или центрофуга), за да избира съпоставянето надеждно между тях", + "new_group_tip": "Групирай почти еднакви профили (същата форма, друга температура или центрофуга), за да може съпоставянето да ги различава.", "new_phase": "+ Нова фаза", "new_profile": "+ Нов профил", "new_profile_tip": "Създай нов профил на програма от съществуващ маркиран цикъл или запис", @@ -162,23 +160,19 @@ "on_cycle_started": "При стартиране на цикъла", "pause_cycle": "Пауза", "pause_cycle_tip": "Постави на пауза текущия цикъл – уредът ще продължи от там, където е спрял", - "pg_apply_device": "Приложи към устройството", - "pg_autofill": "Автоматично попълване", - "play": "Възпроизведи", "process": "Процес", "process_history": "Обработете сега", "process_recording": "Запазване на записа", "process_tip": "Запази записаната следа като нов или съществуващ профил", "rebuild": "Възстановете пликовете", "rebuild_envelope": "Възстановяване на плика", - "rebuild_tip": "Преизчисли очаквания плик на мощността (мин./макс. лента) за всички профили от техните маркирани цикли – стартирай след маркиране на нови цикли или коригиране на стари", + "rebuild_tip": "Изгради наново очакваната лента на мощност на всеки профил от маркираните му цикли. Пускай след маркиране или коригиране на цикли.", "rec_start_tip": "Начни запис на следата на мощността на уреда – стартирай малко преди да пуснеш цикъл", "rec_stop": "Спрете", "rec_stop_tip": "Спри записа и задръж записаната следа за преглед", "record": "Започнете запис", "refresh": "Опресняване", "refresh_diag_tip": "Презареди статистиката за съхранение", - "refresh_logs_tip": "Презареди последните записи в журнала", "refresh_settings_tip": "Презареди настройките от сървъра", "relabel": "Преетикетиране ({count})", "remove": "Премахнете", @@ -186,7 +180,6 @@ "reset": "Нулиране", "reset_ml_models_tip": "Изхвърлете фино настроените модели и се върнете към вградените. WashData може да ги научи отново по-късно.", "reset_to_builtin": "Нулирайте до вградени модели", - "reset_to_defaults": "Възстановяване на настройките по подразбиране", "resume_cycle": "Продължи", "resume_cycle_tip": "Продължи поставения на пауза цикъл", "revert_settings": "Отхвърли промените", @@ -199,8 +192,6 @@ "run_analysis": "🔍 Изпълнете анализ на предложенията", "run_analysis_tip": "Анализирайте вашите записани цикли сега и обновете предложенията за настройка", "run_auto_label": "Стартирайте Auto-Label", - "run_both": "Изпълнение на A срещу B", - "run_simulation": "Изпълнение на симулация", "save": "Запазване", "save_access_control": "Запазване на контрола на достъпа", "save_group": "Запазване на групата", @@ -212,10 +203,8 @@ "save_settings": "Запазване на настройките", "select": "Изберете", "select_all": "Избиране на всички", - "select_last20": "Последните 20", "select_none": "Изчистване", "show_only": "Покажи само", - "skip_setup": "Пропускане на настройката", "split": "Сплит", "stop": "Спри", "submit_correction": "Изпратете корекция", @@ -224,30 +213,24 @@ "trim_split": "Изрязване / Разделяне", "undo": "Отмяна", "unlabel_selected": "Премахване на етикета от избраните ({n})", - "unlabel_selected_tip": "Премахва етикета на профила от избраните цикли. Те остават в историята ви и могат да бъдат етикетирани отново по-късно - само спират да оформят този профил.", + "unlabel_selected_tip": "Премахни етикета от избраните цикли. Остават в историята, но спират да оформят този профил.", "use": "Използвайте", "wipe_all": "Изтриване на всички данни", "delete_selected": "Изтриване на избраните ({n})", - "group_suggest": "Група {n}: {members}", "pg_apply_best": "Приложи най-доброто", - "pg_apply_to_settings": "Запазване в настройките", "pg_apply_to_settings_tip": "Копиране на стойностите, редактирани тук, в активните настройки на това устройство", "import": "Импортиране", "disconnect": "Прекъсване на връзката", "connect_github": "Свързване с GitHub", - "save_appliance": "Запазване на уреда", "share_to_store": "Споделяне в магазина", "share": "Споделяне", - "confirm_appliance": "Потвърдете този уред", - "confirmed": "Потвърдено", "browse_community_setups": "Преглед на настройките на общността", "download_device": "Изтегли настройките на устройството", "share_device": "Сподели настройките на устройството", - "share_device_tip": "Споделете програмите и настройките на това устройство с общността", + "share_device_tip": "Сподели този уред и еталонните му цикли в магазина на общността, за да могат собствениците на същата машина да ги използват.", "share_n": "Споделяне на цикли ({n})", "export_selected": "Експорт (избор на данни)", "export_all": "Бърз експорт на всичко", - "import_raw": "Разширено: замяна на всичко от JSON", "download_export": "Изтегляне на експорта", "analyze_import": "Анализ на файл", "import_selected": "Импортиране на избраното", @@ -260,7 +243,15 @@ "save_total_cycles": "Запазване на общия брой", "clear": "Изчистване", "add_brand": "Добавете марка", - "add_appliance": "Добавете уред" + "log_done_task": "Отбележи като изпълнено: {task}", + "add_task": "Добавяне на задача", + "add_appliance": "Добавете уред", + "retry": "Опитай отново", + "confirm_appliance": "Потвърдете този уред", + "confirmed": "Потвърдено", + "create_profile_from_gap": "Създаване на профил", + "undo_import": "Отмяна на последното импортиране", + "pg_rerun": "Обнови симулацията" }, "note": { "off_delay": { @@ -268,10 +259,6 @@ } }, "conflict": { - "anti_wrinkle_exit": { - "exit": "Трябва да е под Праг на Спиране ({stop} Вт); иначе изходната мощност на режима против бръчки се игнорира", - "stop": "Трябва да е над Изходна Мощност на Режима Против Бръчки ({exit} Вт)" - }, "anti_wrinkle_max": { "max": "Трябва да е над Праг на Стартиране ({start} Вт); иначе ограничението на продължителността против бръчки се заобикаля", "start": "Трябва да е под Макс. Мощност Против Бръчки ({max} Вт)" @@ -279,10 +266,10 @@ "attn_sub": "Поправете конфликтите преди запазване", "attn_title": "Конфликти в настройките: {n}", "price_entity": { - "power_sensor": "Това е Сензорът за мощност на уреда, а не цена. Обект за цена има предимство пред статичната цена по-долу, така че циклите биха се остойностявали по ватове за kWh - изчистете това поле.", - "energy_sensor": "Това е Измервателният уред за енергия на уреда, а не цена. Обект за цена има предимство пред статичната цена по-долу, така че циклите биха се остойностявали по показанието на уреда за kWh - изчистете това поле.", - "device_class": "Класът на този сензор е {dc}, а не цена за kWh. Обект за цена има предимство пред статичната цена по-долу - изчистете това поле или го насочете към сензор за тарифа.", - "unit": "Този сензор отчита в {unit}, което не е цена за kWh. Обект за цена има предимство пред статичната цена по-долу - изчистете това поле или го насочете към сензор за тарифа." + "power_sensor": "Това е Сензорът за мощност на устройството, а не цена. Циклите биха се таксували във ватове за kWh; изчистете това поле.", + "energy_sensor": "Това е Измервателният уред за енергия на устройството, а не цена. Циклите биха се таксували по показанието му за kWh; изчистете това поле.", + "device_class": "Този сензор измерва {dc}, а не цена за kWh. Изчистете това поле или изберете сензор за тарифа.", + "unit": "Този сензор отчита в {unit}, а не цена за kWh. Изчистете това поле или изберете сензор за тарифа." }, "settings_banner": "Конфликти в настройките: {n}. Проверете маркираните раздели и ги поправете преди запазване.", "settings_banner_btn": "Към първия", @@ -317,8 +304,8 @@ "threshold": "Трябва да е под Праг на Спиране ({stop} Вт), за да влезе в сила" }, "pump_stuck": { - "duration": "Трябва да е по-малък от Таймаута Без Актуализация ({to} с), така че алармата за задръстена помпа да се задейства преди наблюдателят да спре цикъла", - "timeout": "Трябва да надвишава Продължителността при Задръстена Помпа ({ps} с), така че алармата да се задейства преди цикълът да бъде принудително спрян" + "duration": "Трябва да е по-малко от Таймаута без актуализация ({to} с), за да се задейства първо алармата за заседнала помпа", + "timeout": "Трябва да надвишава Продължителността при заседнала помпа ({ps} с), за да се задейства първо алармата за заседнала помпа" }, "start_dur": { "sampling": "Интервалът на Вземане на Проби надвишава Продължителността на Стартиране ({sdt} с); единични пикове могат да отворят цикъл", @@ -334,10 +321,15 @@ "watchdog": { "interval": "Трябва да е поне 2x Интервала на Вземане на Проби ({si} с)", "sampling": "Интервалът на Вземане на Проби трябва да е не повече от половината на Интервала на Наблюдател ({wi} с)" + }, + "end_energy": { + "energy": "Твърде строго за Праг на спиране ({w} Вт) по време на Закъснение при изключване ({d} с); цикълът може да завърши само по резервен път", + "stop": "Крайна енергия ({e} Вт·ч за {d} с) допуска само {p} Вт" } }, "hdr": { - "ab_comparison": "Сравнение на настройки A/B", + "custom_tasks": "Ваши задачи", + "maintenance_supporter": "Нужно ви е повече?", "access_control": "Контрол на достъпа", "add_maintenance": "Добавяне на събитие за поддръжка", "automation_delete": "Изтрийте тази автоматизация", @@ -346,28 +338,22 @@ "automation_review_this_cycle": "Прегледайте този цикъл", "automations": "Автоматизация", "clear_debug": "Изчистване на следите за отстраняване на грешки", - "cycle_simulator": "Симулатор на цикли", "diagnostics_pref": "Диагностика", "display": "Дисплей", - "dtw_inspector": "DTW инспектор", "export_import": "Експорт/Импорт", - "getting_started": "Първи стъпки", "live_power": "Мощност на живо", "logs": "Журнали", - "logs_diagnostics": "Диагностика", "maintenance": "Действия по поддръжката", "maintenance_log": "Дневник на поддръжката", "maintenance_reminders": "Напомняния за сервиз", "manual_recording": "Ръчен запис", - "manual_recording_tip": "Стартирай цикъл умишлено, докато WashData записва следата на мощността. Стартирай малко преди уредът да тръгне, Спри когато завърши, след това Обработи за запазване като именуван профил.", + "manual_recording_tip": "Запишете цикъл нарочно: натиснете „Започнете запис“ точно преди уредът да тръгне и „Спрете“, когато свърши, след това „Процес“, за да го запазите като профил.", "ml_learned": "Какво научи WashData", - "ml_matching_tuning": "Фина настройка на програмното съвпадение", "ml_settings_card": "Настройки", "ml_smart_learning": "Интелигентно учене", "my_preferences": "Моите предпочитания", "no_automations": "Все още няма автоматизации, които да споменават това устройство.", "panel_settings": "Настройки на панела", - "param_sweep": "Обхождане на параметър", "pg_settings_source": "Източник на настройките", "phase_catalog": "Фазов каталог", "playground": "Тестова среда", @@ -376,11 +362,8 @@ "status": "Статус", "status_graph": "Графика на състоянието", "storage_stats": "Статистика за съхранение", - "test_history": "Проверка върху историята", "toggle_sidebar": "Превключване на страничната лента на Home Assistant", - "tools_and_data": "Инструменти и данни", "wipe_history": "Изтриване на историята", - "recommendations": "Препоръки ({n})", "pg_detection_params": "Настройки за откриване", "pg_outcome": "Резултат от симулацията", "pg_across_cycles": "За всички ваши цикли", @@ -388,7 +371,8 @@ "online_account": "Магазин на общността и онлайн функции", "import_power_history": "Импортиране на историята на мощността", "service_status": "Статус на сервиза", - "online_account_tab": "Онлайн и общност" + "online_account_tab": "Онлайн и общност", + "live_match_debug": "Диагностика на съвпадението на живо" }, "health": { "fair": "Приемливо качество на съвпадение", @@ -396,11 +380,16 @@ "poor": "⚠ Лошо качество на съвпадение" }, "lbl": { - "envelope_position_tip": "Позиция върху записаната крива на съпоставената програма, {pct}%. Измерва се чрез подравняване на този цикъл спрямо профила, а не чрез броене на времето, затова остава вярна, когато едно пускане е по-дълго или по-кратко от обичайното. Обновява се, докато уредът е тих, затова може да изостава по време на активна фаза.", + "envelope_position_tip": "Позиция върху записаната крива на съпоставената програма: {pct}%. Определя се чрез подравняване на цикъла с профила, затова остава вярна и при по-дълго или по-кратко изпълнение. Обновява се, когато уредът е тих, затова може да изостава.", "envelope_position": "крива {pct}%", "aria_playground_chart3": "Интерактивна графика на мощността на цикъла: посочете, за да видите време/мощност, превъртете или използвайте два пръста за мащабиране, плъзнете за преместване", "device_unresolved": "Недостъпно устройство ({id})", "drag_to_resize": "Плъзнете за преоразмеряване", + "task_name": "Задача", + "every_cycles": "Интервал (цикли)", + "every_days": "Интервал (дни)", + "new_task_name": "Нова задача", + "days_since_service": "{since} / {total} дни", "actions": "Действия", "activity": "активност", "administrators": "Администратори", @@ -408,10 +397,8 @@ "aria_cycle_chart": "Крива на мощността на цикъла", "aria_envelope_chart": "Графика на обвивката на мощността на профила", "aria_phase_chart": "Графика на редактора на фази", - "aria_playground_chart": "Крива на мощността на цикъла с плъзгащи се прагове на откриване", "aria_power_chart": "Графика на потреблението на мощност", "aria_spaghetti_chart": "Наложени криви на мощността на циклите", - "aria_sweep_chart": "Графика с резултати от сканиране на параметри", "artifact_anomaly": "Аномалия", "artifact_high_power": "Висока мощност", "artifact_interruption": "Прекъсване", @@ -425,87 +412,61 @@ "cohesion_good": "кохезия {pct}%", "cohesion_low": "⚠ ниска кохезия {pct}%", "compare_profiles": "Сравнете с профили", - "concurrency": "Паралелност", "confidence": "Увереност", "confidence_threshold": "Праг на доверие", "correct_duration": "Правилна продължителност (мин., по избор)", "correct_profile": "Правилен профил", - "correlation": "Корелация", "cost": "цена", "create_new_profile": "+ Създаване на нов профил...", "current_phase": "Текуща фаза", - "curves": "Криви", "cycle": "Цикъл", "cycle_date_display": "Показване на датата на цикъла", - "cycle_health": "Здраве на цикъла", - "cycle_index": "индекс на цикъла →", - "cycles": "Цикли", "cycles_count": "Цикли", "cycles_lc": "цикъла", - "dashed": "прекъсната", "date": "Дата", "debug_traces": "Следи за отстраняване на грешки", - "default": "По подразбиране", "default_access_level": "Ниво по подразбиране за потребители, които не са изброени по-долу", "default_other": "По подразбиране (други устройства)", "default_tab": "Раздел по подразбиране при отваряне на панела", "font_size": "Размер на шрифта на панела", - "delta": "Δ", "description": "Описание", - "dtw_blend_w": "Тегло на смесване", - "dtw_blended": "Смесен резултат", - "dtw_ddtw": "DDTW резултат", - "dtw_l1": "L1 резултат", - "dtw_matched_profile": "(съвпаднал профил на цикъла)", "duration": "Продължителност", - "duration_agreement": "Съответствие на продължителността", - "emphasis": "Акцент", "enable_access_control": "Активиране на контрол на достъпа за всеки потребител", "energy": "енергия", - "energy_agreement": "Съответствие на енергията", - "event_timeline": "Времева линия на събитията", "event_type": "Тип събитие", "expected": "Очаквано", + "suspected_profile": "{name} (предполагаема)", + "runner_up_profile": "{name} (втора най-добра)", "expected_duration": "Очаквана продължителност (мин.)", "file_kb": "Файл (kB)", - "final_score": "Краен резултат", "from": "От", "gap_s": "Празнина (и)", "group_name": "Име на групата", "head_trim": "Изрязване в началото (с)", - "health": "здраве", "hide_tabs": "Скриване на раздели за неадминистратори", - "in_use": "В употреба", "json_data": "JSON данни", "label": "Съвпадаща програма", "last": "Последни", "last_checked": "Последна проверка:", "load_from_file": "Зареждане от файл", - "mae_score": "MAE резултат", "manual_duration": "Ръчна продължителност (мин., по избор)", "match": "Съвпадение", "members": "Членове", - "metric": "Показател", "mode_existing_profile": "Добавяне към съществуващ профил", "mode_new_profile": "Създаване на нов профил", "models_fine_tuned": "(фино настроени модели: {count})", - "n_classic_suggestions": "{n} класически", - "n_ml_suggestions": "{n} ML", "n_selected": "{n} избрани", - "n_tuning_suggestions": "{n} предложение за настройка", - "na": "н/д", + "n_tuning_suggestions": "{n} предложения за настройка", + "n_tuning_suggestions_one": "{n} предложение за настройка", + "n_tuning_suggestions_other": "{n} предложения за настройка", "new_profile_name": "Ново име на профила", "notes": "Бележки", - "of": "от", - "outcome": "Резултат", "panel_default_tab": "Раздел по подразбиране", "panel_language": "Език на панела", "per_cycle_short": "цикъл", - "pg_autofill_tip": "Автоматично попълване на диапазона от текущата стойност ({lo}–{hi})", "pg_best_value": "Намерена най-добра стойност", "pg_current_value": "Текуща стойност:", "pg_drag_hint": "Плъзнете линията върху графиката", - "pg_dtw": "DTW", "pg_ev_ending": "Завършва", "pg_ev_running": "Работи", "pg_group_advanced": "Гранични случаи", @@ -513,63 +474,39 @@ "pg_group_timing": "Правила за време", "pg_idle": "Празен ход", "pg_anti_wrinkle": "Против бръчки", - "pg_last_run": "Последно изпълнение: {date}", - "pg_match_score": "Оценка на съвпадение", - "pg_pct_of": "{p}% от {total}", "pg_poor_match": "Лошо съвпадение", "pg_preset": "Конфигурация на тестовата среда", "pg_preset_name": "Име на новата конфигурация", "pg_preset_none": "Изберете конфигурация…", "pg_strong_match": "Силно съвпадение", - "pg_tile_ambiguous": "Двусмислени", - "pg_tile_ambiguous_tip": "Цикли с почти равно съвпадение между две програми", - "pg_tile_detected": "Открити", - "pg_tile_detected_tip": "Цикли, при които мощността е преминала прага за стартиране достатъчно дълго, за да се отчете като работа", - "pg_tile_matched": "Съвпаднали", - "pg_tile_matched_tip": "Цикли, правилно отнесени към позната програма", - "pg_time_left": "Оставащо време", "pg_weak_match": "Слабо съвпадение", "phase_name": "Име на фаза", "name": "Име", "phase_timeline": "Времева линия на фазите", "power": "Мощност", "profile": "Профил", - "profile_index": "индекс на профила →", "profile_name": "Име на профил", "program": "Програма", - "program_tip": "Замества кой профил се съпоставя с текущия цикъл. Автоматичното разпознаване позволява на интеграцията да избере най-доброто съвпадение автоматично. Закачете конкретна програма, за да я принудите, когато авт. разпознаването греши или знаете какво работи. Изберете програма, преди да пуснете уреда, и тя ще се приложи веднага щом започне следващият цикъл.", + "program_tip": "Изберете с коя програма се съпоставя текущият цикъл. Автоматичното откриване избира най-доброто съвпадение. Закачете програма, когато автоматичното откриване греши; програма, избрана преди старта, важи за следващия цикъл.", "progress": "Напредък", "raw_socket": "Суров сигнал от контакта", - "read_only": "Само за четене", "ref_cycle": "Референтен цикъл (по избор)", - "refresh_interval": "Интервал на опресняване (и)", "remaining": "оставащи", "reminder_every": "Напомняне на всеки (цикли)", "remove_label": "- Премахване на етикета -", "rename_profile": "Преименуване на профил", - "replay_duration": "Възпроизвеждане", "resulting_profile": "Резултатен профил", "save_mode": "Режим на запазване", - "score": "Резултат", - "score_breakdown": "Разбивка на резултата", "seconds_from_start": "Секунди от началото", "select_profile": "Изберете Профил", "setting": "Настройка", - "settings_a": "Настройки A (текущи)", - "settings_a_short": "A", "settings_advanced": "Разширени", - "settings_b": "Настройки B (тест)", - "settings_b_short": "B", "settings_basic": "Основни", "settings_detail_level": "Ниво на детайлност на настройките", - "settings_overrides": "Замествания на настройки", "show_debug": "Показване на карта за отстраняване на грешки при съвпадението на живо на страницата за състояние (увереност, неяснота, най-добри кандидати)", "show_expected": "Показване на очакваното наслагване на крива (съвпадащ профил, оранжево)", "show_raw": "Показване на превключвателя за необработени данни от контакта в живия графикон на мощността", "sparkline": "Тенденция на продължителността на последните цикли", - "stage2": "Етап 2 – основно сходство", - "stage3": "Етап 3 – DTW", - "stage4": "Етап 4 – съответствие", "status": "Статус", "tail_trim": "Изрязване в края (с)", "tap_to_dismiss": "докоснете графиката, за да затворите", @@ -580,7 +517,6 @@ "ungrouped": "Негрупиран", "unlabelled": "Без етикет", "unlabelled_paren": "(без етикет)", - "warp_path": "Warp път", "input": "Вход:", "start": "Начало", "end": "Край", @@ -605,9 +541,8 @@ "n_shown": ", показани {n}", "cycles_title": "Цикли ({n})", "pg_detecting": "Откриване", - "pg_best_marker": "Най-добро: {v}", "pg_time_left_model": "Оставащо време (модел)", - "pg_time_left_model_tip": "Оценка на оставащото време от модела (оценител на фази + ML), а не статичен обратен брояч", + "pg_time_left_model_tip": "Оценка по фазите на програмата, а не фиксирано обратно броене", "phase": "Фаза", "pg_mode_history": "Тест върху история", "pg_ev_detected": "Открит", @@ -647,18 +582,8 @@ "pg_mode_optimize": "Оптимизиране", "pg_group_matching": "Съпоставяне на програми", "pg_zoom_hint": "двойно щракване нулира мащаба", - "aria_playground_chart2": "Интерактивна графика на мощността на цикъла: посочете, за да видите време/мощност, превъртете за мащабиране, плъзнете за преместване", "pg_alert_timeout_end": "Прекратено поради изтичане на времето, а не по прогноза", "pg_alert_indefinite": "Би работил безкрайно", - "pg_alert_stress_cap": "Достигнат защитен лимит", - "pg_alert_stress_ok": "Открит режим на готовност: цикълът е спрян", - "pg_alert_stress_warn": "Консумацията в режим на готовност е над прага за спиране", - "pg_stress_group": "Тест за спиране при готовност", - "pg_stress_idle_auto": "Auto", - "pg_stress_idle_w": "Ниво в режим на готовност (Вт)", - "pg_stress_idle_w_desc": "Заменете автоматично определения минимален праг на консумация в режим на готовност (оставете празно за автоматично определяне).", - "pg_stress_toggle": "Тествайте спирането при готовност", - "pg_stress_toggle_desc": "Симулира, че уредът остава при консумацията си в режим на готовност след края на записа -- показва дали и кога WashData ще спре цикъла.", "task_pg_history": "Тест върху историята", "task_pg_sweep": "Оптимизиране", "task_reprocess": "Повторна обработка", @@ -668,16 +593,9 @@ "pg_recent_runs": "Скорошни изпълнения", "pg_load_run": "Презареди това изпълнение", "pg_match_confidence": "Увереност в съвпадението", - "pg_envelope_fit": "Съответствие с обвивката", - "pg_envelope_fit_tip": "Колко точно цикълът се вписва в запазената обвивка на мощността на този профил, различен поглед от увереността в съвпадението.", "n_imported_note": ", {n} импортирани", "enable_online": "Активиране на онлайн функциите", - "store_brand": "Марка на уреда", - "store_model": "Модел на уреда", "anonymous": "Анонимно", - "rate_quality": "Оценете качеството", - "rate_n": "{n} звезди", - "approved_only": "Само одобрени", "adopt_settings": "Приеми настройките", "flags": "Маркери", "include_phase_map": "Включи карта на фазите", @@ -694,7 +612,6 @@ "cat_custom_phases": "Персонализирани фази", "cat_profile_groups": "Групи профили", "cat_settings": "Настройки за откриване и разпознаване", - "cat_matching_config": "Фина настройка на разпознаването", "cat_ml_models": "ML модели", "cat_feedback": "Обратна връзка и етикети за преглед", "cat_suggestions": "Предложения", @@ -714,7 +631,6 @@ "conflict_overwrite": "Презапис", "hist_csv_data": "CSV данни", "hist_from_recorder": "Или го прочетете от Home Assistant", - "days": "дни", "hist_keep": "Запази този цикъл", "hist_looks_complete": "завършен", "peak_power_short": "Пик", @@ -735,7 +651,40 @@ "zoom_hint_touch_zoomed": "мащабирайте с два пръста · двойно докосване за нулиране", "cycles_since_service": "{since} / {total} цикъла", "total_cycles_run": "цикъла общо", - "correct_total_cycles": "Коригиране на общия брой" + "correct_total_cycles": "Коригиране на общия брой", + "hist_reason_already_recorded": "припокрива се с цикъл, който WashData вече е записал", + "share_consent": "Потвърждавам, че тези цикли са завършили нормално, без прекъсване", + "show_internal": "Показване на вътрешните настройки", + "show_internal_tip": "Настройки, които WashData настройва вместо вас (вземане на проби, наблюдател, честота на съпоставяне, прагове). Показват се, когато устройството има собствена стойност, конфликт или предложение.", + "rate_n": "{n} звезди", + "rate_quality": "Оценете качеството", + "pg_autolabel": "Автоматично етикетиране", + "pg_label_review": "Изисква преглед", + "pg_ev_match_reverted": "Съвпадението е отменено", + "pg_ev_verified_pause": "Потвърдена пауза", + "n_samples": "{n} проби", + "n_samples_one": "{n} проба", + "n_samples_other": "{n} проби", + "n_cycles": "{n} цикъла", + "n_cycles_one": "{n} цикъл", + "n_cycles_other": "{n} цикъла", + "energy_per_cycle": "{v}/цикъл", + "energy_total": "общо {v}", + "automation_off": "(изкл.)", + "ref_cycle_none": "Няма", + "share_name": "Показване на името ми в споделеното", + "cat_backfill_cycles": "Цикли, намерени в импортирана история на мощността", + "pg_compare_profile": "Сравни с профил", + "pg_obj_early": "Честота на ранни завършвания", + "pg_obj_endlag": "Закъснение на завършването (медиана)", + "pg_obj_split": "Честота на разделени цикли", + "pg_sweep_cycles": "Последни цикли", + "cycle_context": "История от recorder около цикъла", + "cycle_context_off": "Изключено", + "cycle_context_minutes": "{min} мин", + "cycle_context_series": "История от recorder", + "before_start": "Преди началото", + "after_end": "След края" }, "log": { "all_levels": "Всички нива", @@ -745,11 +694,9 @@ }, "ml": { "builtin_models": "● Използване на модели за вграждане", - "fit_fair": "Приемлив", "fit_good": "добре", "fit_slight": "Леко", "fit_strong": "Силен", - "fit_weak": "слаб", "fit_word": "годни", "personalized": "● Персонализирано за тази машина", "better_than_baseline": "{pct}% по-добре от базовата оценка ({metric})", @@ -757,21 +704,29 @@ "trend_declining_tip": "Съответствието на този модел се влоши при последните повторни проверки; прегледът на повече цикли може да му помогне да се преобучи.", "trend_steady_tip": "Съответствието на този модел остана приблизително стабилно при последните повторни проверки.", "cap_blurb": { - "end": "Разпознаване кога цикълът наистина е приключил", - "live_match": "По-бързо разпознаване на изпълняваната програма", - "quality": "Откриване на погрешно разпознати или повредени цикли", - "remaining_time": "Прогнозиране на оставащото време", "total_energy": "Прогнозиране на общата консумация на енергия и разходите" }, "cap_label": { - "end": "Разпознаване на края на цикъла", - "live_match": "Разпознаване на програмата", - "quality": "Проверка на качеството на цикъла", - "remaining_time": "Оценка на оставащото време", "total_energy": "Оценка на консумацията на енергия" }, - "metric_auc": "AUC {auc} върху отделени данни", - "metric_mae": "грешка {model} спрямо базовата стойност {baseline}" + "metric_mae": "грешка {model} спрямо базовата стойност {baseline}", + "fine_tuned_at": "фино настроен {when}", + "held_out_count": "измерено върху {count} отделени цикъла", + "held_out_count_one": "измерено върху {count} отделен цикъл", + "held_out_count_other": "измерено върху {count} отделени цикъла", + "last_run_not_promoted": "Последна проверка ({when}): {reason}", + "never": "никога", + "skip_reason": { + "fit_failed": "Обучението е неуспешно: {error}", + "holdout_too_small": "Твърде малко цикли за честна проверка на нов модел: за проверка могат да се отделят {held_out}, а са нужни {min}.", + "insufficient_rows": "Все още няма достатъчно използваеми цикли: досега {count} етикетирани, чисти цикъла.", + "insufficient_rows_one": "Все още няма достатъчно използваеми цикли: досега {count} етикетиран, чист цикъл.", + "insufficient_rows_other": "Все още няма достатъчно използваеми цикли: досега {count} етикетирани, чисти цикъла.", + "not_better_than_incumbent": "Новият модел не беше по-добър от използвания (грешка {model} спрямо {incumbent}), затова текущият беше запазен.", + "not_better_than_naive": "Новият модел не беше ясно по-добър от простата оценка (грешка {model} спрямо {naive}).", + "unknown": "Нищо ново не беше научено." + }, + "trained_unknown": "в неизвестен момент" }, "modal": { "auto_label": "Цикли на автоматично етикетиране", @@ -780,7 +735,6 @@ "cycle": "Цикъл", "edit_group": "Редактиране на профилна група", "edit_phase": "Редактиране на фаза", - "import_config": "Импортиране на конфигурация", "label_cycle": "Цикъл на етикета", "merge_cycles": "Обединяване на {n} цикли", "new_group": "Нова профилна група", @@ -795,6 +749,8 @@ "delete_automation_title": "Изтриване на автоматизацията", "delete_automation_msg": "Да се изтрие ли автоматизацията „{name}“ от Home Assistant? Това не може да бъде отменено.", "remove_legacy_title": "Премахване на остарелите действия", + "reset_phase_msg": "Да се отмени ли редакцията на „{name}“ и да се възстанови вградената фаза? Програмите запазват своите диапазони.", + "reset_phase_title": "Нулиране на фаза", "remove_legacy_msg": "Да се премахнат ли остарелите персонализирани действия? Те ще спрат да се задействат при събития на цикъла. Това не може да бъде отменено от панела.", "discard_recording_title": "Отхвърляне на записа", "discard_recording_msg": "Да се отхвърли ли запазеният запис? Това не може да бъде отменено.", @@ -815,32 +771,44 @@ "store_share_device": "Сподели настройките на устройството", "export_select": "Експорт - избор на данни", "import_wizard": "Импорт - избор на данни", - "history_import": "Импортиране на историята на мощността" + "history_import": "Импортиране на историята на мощността", + "apply_suggestions_title": "Прилагане на предложенията", + "remove_task_title": "Премахване на задача", + "remove_task_msg": "Да се премахне ли напомнянето „{task}“? Записите му в дневника на поддръжката се запазват.", + "apply_suggestions_msg": "Да се променят ли тези настройки ({n})? Интеграцията се презарежда след запазване.", + "discard_settings_title": "Да се отхвърлят ли незапазените промени?", + "discard_settings_msg": "Незапазени промени в настройките: {n}. Искате ли да напуснете Настройки и да ги отхвърлите?", + "profile_title": "Профил · {name}", + "undo_import_msg": "Данните и настройките на това устройство се връщат към състоянието преди последния импорт ({when}). Записаните оттогава цикли и направените промени ще бъдат загубени.", + "undo_import_ok": "Отмяна на импортирането", + "undo_import_title": "Отмяна на последното импортиране" }, "msg": { "pg_canvas_empty3": "Изберете цикъл по-горе и натиснете „Стартиране“, за да го симулирате. След това посочете, за да видите стойностите, превъртете или използвайте два пръста за мащабиране и плъзнете за преместване.", + "pg_preset_delete_confirm": "Да се изтрие ли конфигурацията „{name}“?", + "pg_preset_limit": "Достигнат е лимитът на конфигурации ({n})", + "pg_preset_overwrite": "Да се презапише ли конфигурацията „{name}“?", "tail_trim_hint": "Премахнете толкова секунди от края", - "store_sibling_hint": "Няма нищо споделено за точния ви модел? Близък модел от същата марка обикновено е добра отправна точка.", - "store_declare_appliance": "Кажете на WashData кой уред притежавате и този раздел ще показва конфигурациите, които други хора са споделили за него. Можете също да въведете марка по-горе, за да разгледате каталога.", - "refresh_catalog_hint": "Списъците с марки и уреди на общността се кешират, за да остане магазинът на общността в рамките на дневния си лимит. Опреснете, за да видите записите, добавени или одобрени от други.", + "store_sibling_hint": "Няма нищо споделено за вашия модел? Сроден модел от същата марка обикновено е добро начало.", + "store_declare_appliance": "Кажете на WashData какъв уред имате, за да видите настройки, които други са споделили за него. Или въведете марка по-горе и разгледайте.", + "refresh_catalog_hint": "Списъците с марки и уреди на общността се кешират заради дневния лимит на магазина. Опреснете, за да видите записите, добавени или одобрени от други.", "head_trim_hint": "Премахнете толкова секунди от началото", "appliance_monitor": "Монитор на уреди", "artifact_dip_detail": "Падна под обичайния диапазон на мощността за ~{n} сек.", "artifact_footer": "Подчертано на графиката по-горе. Това са преходни артефакти (напр. вратата се отвори по средата на цикъла), а не непременно проблеми.", "artifact_header": "{n} аномалии, открити по време на този цикъл", - "artifact_pause_detail": "Мощността падна до почти нула за ~{n} сек. и след това се възобнови – вероятно вратата е отворена по средата на цикъла или цикълът е бил поставен на пауза.", + "artifact_header_one": "{n} аномалия, открита по време на този цикъл", + "artifact_header_other": "{n} аномалии, открити по време на този цикъл", + "artifact_pause_detail": "Мощността падна почти до нула за ~{n}s, след което се върна. Вероятно вратата е била отворена или цикълът е бил на пауза.", "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": "Профили на наслагване (бледи)", - "compare_profiles_tip": "Наслагнете други профилни пликове върху диаграмата по-горе, за да видите кой най-добре пасва на този цикъл.", + "compare_profiles_tip": "Наслагвайте други профили върху графиката, за да видите кой пасва най-добре на този цикъл.", "compare_selected_cycles": "Избрани цикли (плътни) – показване / скриване", - "consider_new_profile": "Помислете за създаване на нов профил.", - "coverage_gap": "последните цикли нямат съответстващ профил ({pct}% от последните 30).", - "coverage_gap_similar_cycles": "{count} сходни немаркирани цикъла намерени – създайте профил, за да започнете тяхното съпоставяне.", "cycles_deleted": "Изтрити цикли: {count}", "enough_data": "Достатъчно данни за обучение ({current}/{min} цикъла).", "export_description": "Изберете точно кои профили, цикли, настройки и други да се експортират в JSON, или анализирайте файл и импортирайте само желаните части.", @@ -848,30 +816,26 @@ "feedback_prompt": "Потвърдете, че е правилно, коригирайте програмата или игнорирайте.", "feedback_relabel_hint": "Преетикетирането на този цикъл също го разрешава.", "filter_by_profile": "Филтриране по профил…", - "group_modal_help": "Групирайте програми с еднаква форма, които се различават по температура/центрофуга (продължителността може да варира). Съпоставянето оценява групата като един кандидат, след което избира най-подходящия член. Изберете поне 2; наслагването показва колко си приличат.", + "group_modal_help": "Групирайте програми с еднаква форма, които се различават по температура или центрофуга. Съпоставянето оценява групата като цяло, след това избира най-подходящия член. Изберете поне 2; наслагването показва колко си приличат.", "group_not_cohesive": "Тези профили не са достатъчно сходни, за да бъдат групирани надеждно, така че съпоставянето ги третира индивидуално, докато не премахнете отклонението или не разделите групата.", "group_preview_hint": "Изберете 2 или повече члена, за да прегледате и сравните кривите им на мощност.", - "import_intro": "Заредете експортиран файл или поставете JSON полезен товар по-долу.", - "legacy_actions_title": "{count} наследено персонализирано действие все още се изпълнява", + "legacy_actions_title": "{count} наследени персонализирани действия все още се изпълняват", + "legacy_actions_title_one": "{count} наследено персонализирано действие все още се изпълнява", + "legacy_actions_title_other": "{count} наследени персонализирани действия все още се изпълняват", "live_chart_loading": "Графиката на мощността на живо се появява, когато пристигнат показанията.", "loading": "Зарежда се...", "loading_curve": "Кривата на зареждане...", "loading_settings": "Настройките се зареждат...", "log_buffer_hint": "Първо най-новите · буферира последните 500 ha_washdata записа след рестартиране · плъзнете долния край, за да преоразмерите.", - "maintenance_advisory": "Увеличаването на продължителността/енергията може да означава, че е необходима поддръжка на уреда (напр. отстраняване на котлен камък, почистване на филтъра).", "maintenance_due": "Предстои поддръжка: {items}", - "maintenance_intro": "Записвайте сервиза, който извършвате на този уред, и получавайте напомняне, когато всяка задача отново предстои.", + "maintenance_intro": "Записвайте обслужването на този уред и получавайте напомняне, когато всяка задача отново стане на ред.", "maintenance_load_error": "Данните за поддръжката не можаха да бъдат заредени: {error}", "maintenance_requires_access": "Поддръжката и експортът/импортът изискват пълен достъп.", - "manual_duration_hint": "Средната/очакваната продължителност на цикъла на профила, използвана за прогнози за оставащото време. Редактирайте, за да го зададете; оставяйки го непроменен запазва текущата стойност.", - "matching_tuning_intro": "Когато се учи, WashData също настройва колко съвпадение на програмата претегля формата спрямо продължителността и енергията.", + "manual_duration_hint": "Очакваната дължина на цикъла на профила, използва се за оценка на оставащото време. Оставете без промяна, за да запазите текущата стойност.", "merge_intro": "Избраните цикли се комбинират в един (хронологичен ред; пропуски, запълнени с 0 W). Изберете получения профил.", "ml_intro": "WashData се доставя с интелигентни модели, които работят веднага.", "ml_learned_intro": "Модели, прецизно настроени към тази машина.", - "ml_loading": "зареждане на ML...", "ml_settings_intro": "Два независими превключвателя: единият прилага моделите, докато цикълът работи, другият позволява на WashData да ги прецизира към вашата машина с течение на времето.", - "name_first_program": "Имате достатъчно цикли – наименувайте първата си програма, за да започне съвпадението.", - "near_duplicate_cluster": "открит почти дублиращ се профилен клъстер. Групирането позволява съпоставянето надеждно да избира между сходни програми (напр. същата програма при различна температура/центрофуга).", "no_cycles_match": "Няма цикли, съответстващи на текущия филтър.", "no_cycles_profile": "Няма цикли за този профил.", "no_cycles_selected": "Първо изберете поне един цикъл.", @@ -896,79 +860,60 @@ "no_suggestions": "Няма активни предложения.", "notify_services_hint": "Използвайте ID на услугите {entity} (разделени със запетая за повече). Променливи на шаблона: {vars}.", "old_actions_warning": "Конфигуриран със стария редактор на действия (вече премахнат). Те все още се задействат при събития от цикъл, но вече не могат да се редактират тук. Преобразувайте ги в нормална автоматизация или ги премахнете.", - "onboarding_progress": "{n} / 3 наблюдавани цикъла", - "onboarding_watching": "Оставете уреда да работи нормално – WashData наблюдава. След 3 цикъла ще започне съвпадението на програми.", "pending_feedback": "Очаква се обратна връзка за откриване", "performance_trend": "Тенденция на ефективността ({n} цикъла)", - "pg_analysis_empty": "Заредете цикъл, за да видите анализ на съвпадението.", "pg_apply_confirm": "Прилагане на най-добрата стойност: {label} = {value}?", - "pg_canvas_empty": "Изберете цикъл по-горе, след което натиснете „Зареди“, за да видите неговата крива на мощността.", - "pg_chart_caption": "Дял на откриване/съвпадение по стойности на {param}", "pg_ctrl_intro": "Стойностите тръгват от активните настройки на интеграцията за това устройство. Промените остават в тестовата среда, докато не ги приложите.", "pg_live_unavailable": "Активните настройки не са налични, показани са стойностите по подразбиране.", "pg_load_live_confirm": "Да се отхвърлят ли промените в тестовата среда ({n}) и да се заредят текущите настройки на интеграцията?", - "pg_match_rate": "{pct}% съвпадения върху {total} цикъла", "pg_matches_live": "Съвпада с активните настройки", "pg_n_changed": "Промени спрямо активните настройки: {n}", "pg_sugg_none": "Всички предложения вече съответстват на текущите настройки", - "pg_preset_delete_confirm": "Да се изтрие ли конфигурацията „{name}“?", - "pg_preset_limit": "Достигнат е лимитът на конфигурации ({n})", - "pg_preset_overwrite": "Да се презапише ли конфигурацията „{name}“?", "pg_publish_one_confirm": "Да се запази ли {label} = {value} в настройките на това устройство?", "pg_restart_note": "Рестартирайте Home Assistant, за да активирате инструментите за симулация.", - "pg_sim_progress": "{done} / {total} цикъла", - "pg_starting": "Стартиране…", - "pg_stress_synthetic": "тук започва симулацията на готовност", - "pg_sweep_intro": "Какво ако {param} беше различен? Тествайте {steps} стойности върху последните ви {cycles} цикъла, за да намерите настройката, при която най-много цикли съвпадат правилно.", - "pg_sweep_step": "Стъпка {done} / {total}", - "pg_verdict_bad": "Изисква внимание: много цикли остават неоткрити.", - "pg_verdict_good": "Добре настроено: повечето цикли се откриват и съвпадат правилно.", - "pg_verdict_ok": "Приемливо: някои цикли са пропуснати. Опитайте да намалите прага за стартиране.", "phase_catalog_intro": "Именувани сегменти от цикъл (Предпране, Нагряване, Центрофуга…). Присвоете ги на профил от неговия контролен панел.", "phase_ranges_intro": "Фазови диапазони (минути от началото на цикъла), насложени върху средната крива. Редактирайте стойностите за визуализация на живо.", - "playground_intro": "Безопасна среда за възпроизвеждане на минали цикли с различни настройки, сравняване на две конфигурации една до друга и разглеждане как един цикъл се подравнява с профил. Нищо тук не променя активните ви данни, докато изрично не го приложите.", - "playground_needs_restart": "Рестартирайте Home Assistant, за да активирате инструментите на Тестовата среда.", - "preferences_admin": "Предпочитания, панел и контрол на достъпа", - "preferences_adv": "Предпочитания", + "playground_intro": "Пробвайте настройки върху реалните данни на циклите си. Нищо тук не променя активните настройки, докато не ги приложите.", "prefs_personal": "Те се отнасят само за вашия акаунт в Home Assistant.", "font_size_hint": "Направете всичко в този панел по-голямо или по-малко. Прилага се за вашия акаунт на това устройство.", - "process_history_hint": "Повторно стартирайте съпоставянето за всички съхранени цикли, обновете предложенията за настройка, обучете отново ML моделите (ако е активирано), преизчислете разходите спрямо записаните цени на енергията и преизчислете качеството на циклите. Стартирайте това след пакет от прегледи.", + "process_history_hint": "Пуска наново съпоставянето на всички запазени цикли, опреснява предложенията, преобучава ML моделите (ако са включени), преизчислява цената на циклите по записаните цени и преизчислява състоянието на циклите. Пускайте след серия прегледи.", "profile_deleted": "Профилът е изтрит", - "profile_poor_health_detail": "Циклите, присвоени на този профил, имат непоследователни форми или ниска степен на доверие. Помислете за повторно изграждане на обвивката или преглед на циклите с етикети.", "profiles_intro": "Щракнете върху профил за статистика, фази и почистване. Групирайте почти идентични програми (еднаква форма/продължителност, различна температура или центрофуга), така че съвпадението надеждно избира между тях.", - "rbac_hint": "Когато е изключено, всеки потребител на Home Assistant има пълен достъп (по подразбиране). Администраторите винаги имат пълен достъп и могат да управляват всички.", - "recent_logs": "Последни записи ha_washdata", + "rbac_hint": "Изключено: всеки потребител на Home Assistant има пълен достъп (по подразбиране). Администраторите винаги имат пълен достъп и могат да управляват всички.", "recording_in_progress": "Записва се", "reminders_intro": "Показване на напомняне в панела толкова цикъла след последния сервиз. Оставете празно или 0, за да изключите напомнянето.", "restart_gap_footer": "Маркирано на графиката. Данните за мощността липсват за тези интервали; съпоставянето използва само реални показания.", - "restart_gap_header": "{n} HA пропуск при рестартиране по време на този цикъл", + "restart_gap_header": "{n} HA пропуска при рестартиране по време на този цикъл", + "restart_gap_header_one": "{n} HA пропуск при рестартиране по време на този цикъл", + "restart_gap_header_other": "{n} HA пропуска при рестартиране по време на този цикъл", "restart_gap_item": "{dur} пропуск", - "review_confirm_help": "Потвърдете дали този цикъл е открит правилно. Вашите отзиви обучават модела на вашата машина - колкото повече цикли потвърдите, толкова по-добро съвпадение и оценка за здравето получавате. Бързо Добро/Лошо е достатъчно.", + "review_confirm_help": "Правилно ли е открит този цикъл? Потвърждаването на цикли подобрява съпоставянето за вашата машина. Достатъчно е бързо Добре/Лошо.", "review_in_settings": "Преглед в Настройки", "review_notes_placeholder": "Бележки (по избор)", "review_notes_tip": "Бележки със свободен текст за ваша справка. Не се използва при съвпадение или обучение.", - "review_profile_tip": "Програмата, с която е означен този цикъл. Ако автоматично откритата програма е грешна, коригирайте я тук - етикетирането учи на съвпадение за бъдещи цикли.", - "review_quality_tip": "Колко чист е този цикъл. Добър = учебникарски пример за тази програма; Лош = открит, но шумен или нетипичен; Неизползваем = неправилно открит (обединен, съкратен или фалшив). Управлява здравния резултат и кои цикли са разрешени за обучение на модела.", - "review_recorded_tip": "Маркирайте това като ръчно избран референтен цикъл за неговата програма - същата роля като ръчно записан цикъл. Референтните цикли винаги се запазват, зареждат съответстващия шаблон и никога не се изтриват от почистване. (Това е „златният“/записан флаг; и двете са едно и също нещо.)", - "review_tags_tip": "Незадължителни флагове, описващи какво се е объркало с този цикъл, така че обучението и почистването могат да го обяснят.", + "review_profile_tip": "Програмата, с която е маркиран този цикъл. Ако е грешна, коригирайте я тук; етикетите обучават съпоставянето.", + "review_quality_tip": "Колко чист е този цикъл. Добре: образцово изпълнение на тази програма. Лошо: открит, но шумен или необичаен. Неизползваем: грешно открит (слят, прекъснат или фалшив). Влияе на оценката на състоянието и на това кои цикли обучават модела.", + "review_recorded_tip": "Маркирайте като ръчно избран еталонен цикъл на програмата му, като ръчно записан цикъл. Еталонните цикли винаги се пазят, задават шаблона за съпоставяне и почистването никога не ги премахва.", + "review_tags_tip": "Незадължителни флагове за това какво се е объркало, за да могат обучението и почистването да го отчетат.", "review_to_cycles": "Отворете опашката за преглед на Cycles", "samples_decimated": "Показани {shown} от {total} проби (разредени за показване; пиковете са запазени). Широка празнина тук е разреждане, а не липсващи данни.", - "saving_triggers_reload": "Запазването задейства презареждане на интеграцията. Обектите на HA може за кратко да се показват като неналични.", + "saving_triggers_reload": "Запазването презарежда интеграцията. Обектите може за кратко да са недостъпни.", "search_placeholder": "Настройки за търсене...", "see_recorder": "Вижте джаджа за записващо устройство по-долу", "setting_changed": "Променено от {old} на {new} на {date}", "settings_basic_note": "Показват се основните настройки. Превключете на Разширени за пълния списък.", - "shape_drift_advisory": "⚠ Дрейф на формата", - "shape_drift_detail": "Моделът на мощността за този профил се е изместил с времето – вероятно износване на уреда или необходима поддръжка (напр. отстраняване на котлен камък, почистване на филтъра).", "show_all_settings": "Показване на всички настройки", - "showing_suggestions": "Показване на {count} настройка с предложения.", - "split_intro": "Щракнете върху графиката, за да добавите или премахнете точка на разделяне или автоматично откриване по неактивни празнини. Всеки получен сегмент може да получи свой собствен профил.", - "storage_diagnostics": "Статистика за съхранение, поддръжка, експорт/импорт", + "showing_suggestions": "Показване на {count} настройки с предложения.", + "showing_suggestions_one": "Показване на {count} настройка с предложения.", + "showing_suggestions_other": "Показване на {count} настройки с предложения.", + "split_intro": "Щракнете върху графиката, за да добавите или премахнете точка на разделяне, или я намерете автоматично по паузите в празен ход. Всяка част може да получи свой профил.", "sug_staged": "Зададено {key} = {val}. Запазете, за да приложите.", "sug_muted": "Тази настройка няма да се предлага повече", "sug_mute_failed": "Настройката не можа да бъде заглушена", "sug_unmuted_all": "Заглушените предложения са нулирани", - "n_suggestions_muted": "{count} заглушени; авто-настройката няма да предлага тези.", + "n_suggestions_muted": "{count} заглушени предложения; авто-настройката няма да ги предлага.", + "n_suggestions_muted_one": "{count} заглушено предложение; авто-настройката няма да го предлага.", + "n_suggestions_muted_other": "{count} заглушени предложения; авто-настройката няма да ги предлага.", "toast_auto_detect_enabled": "Автоматичното разпознаване е активирано", "toast_auto_label_complete": "Автоматичното етикетиране е завършено", "toast_auto_label_failed": "Автоматичното етикетиране е неуспешно: {error}", @@ -984,7 +929,6 @@ "toast_no_automation": "Тук не може да се създаде автоматизация", "toast_profile_name_required": "Изисква се име на профил", "toast_program_set": "Програмата е зададена: {program}", - "toast_rebuild_failed": "Повторното изграждане е неуспешно: {error}", "toast_resume_failed": "Възобновяването е неуспешно: {error}", "toast_revert_failed": "Отмяната е неуспешна: {error}", "toast_reverted": "{key} е върнато", @@ -995,95 +939,80 @@ "trend_energy_up": "Тенденция към нарастване на енергията ({pct}%/цикъл) – скорошно средно {avg}", "trim_destructive_confirm": "Това запазва само {pct}% от цикъла и не може да бъде отменено. Да се продължи ли?", "trim_intro": "Плъзнете червените манипулатори или въведете стойности. Всичко извън прозореца се премахва.", - "tuning_suggestions_available": "{count} предложение за настройка налично от наблюдавани цикли. Те се появяват до съответните полета.", + "tuning_suggestions_available": "{count} предложения за настройка налични от наблюдавани цикли. Те се появяват до съответните полета.", + "tuning_suggestions_available_one": "{count} предложение за настройка налично от наблюдавани цикли. То се появява до съответното поле.", + "tuning_suggestions_available_other": "{count} предложения за настройка налични от наблюдавани цикли. Те се появяват до съответните полета.", "unsure_detected_prefix": "WashData не е сигурен, че е открит", - "updated": "Актуализирано", "warmup_badge": "Все още учи ({done}/{needed} цикъла)", - "warmup_detail": "Този профил се нуждае от {needed} маркирани цикъла, преди автоматичното съпоставяне да започне. Всеки потвърден цикъл помага на обучението.", + "warmup_detail": "Съпоставянето вече работи. WashData ви моли да потвърдите първите {needed} цикъла на този профил, преди да започне да маркира сам.", "wipe_history_warning": "Изтриване за постоянно на всички цикли и профили. Не може да се отмени.", "compare_overlay_tip": "Наслагнете пликовете на научените профили, за да видите на коя програма прилича всеки цикъл.", - "duration_trend_up_tip": "Продължителност нараства ({pct}%/цикъл)", - "duration_trend_down_tip": "Продължителност намалява ({pct}%/цикъл)", - "energy_trend_up_tip": "Енергия нараства ({pct}%/цикъл)", - "energy_trend_down_tip": "Енергия намалява ({pct}%/цикъл)", - "performance_trending": "Тенденция на ефективността", - "pg_select_cycle_hint": "Изберете цикъл и щракнете ↺, за да заредите", - "pg_sweep_progress": "Стъпка {done} / {total}", - "pg_analysis_hint": "Изберете цикъл и щракнете ↺, за да заредите анализа.", "diagnostics_load_failed": "Диагностиката не можа да се зареди: {error}", - "manual_duration_ref_hint": "Използва се само когато не е избран референтен цикъл. Референтният цикъл задава продължителността според собствената си дължина.", + "manual_duration_ref_hint": "Използва се само без еталонен цикъл; еталонният цикъл задава продължителността по своята дължина.", + "duration_learned_hint": "Изчислено от циклите на тази програма (без отклоненията) и обновявано след всеки нов цикъл. За да го промените, изрежете или премахнете неточните цикли.", "toast_convert_rolled_back": "Конвертирането е неуспешно и беше отменено (без оставена автоматизация): {error}", "toast_convert_orphan": "Автоматизацията беше създадена, но изчистването на старите действия е неуспешно. Не опитвайте отново: премахнете старите действия ръчно, за да избегнете дублирана автоматизация.", - "advisory_duration_trend_up": "Циклите на '{name}' стават все по-дълги (около +{pct}% на цикъл). Ако поведението на уреда се е променило, запишете отново или обновете този профил.", - "advisory_energy_trend_up": "'{name}' консумира все повече енергия (около +{pct}% на цикъл) - струва си да проверите уреда, ако това е неочаквано.", - "advisory_poor_health": "Профилът '{name}' има ниска оценка на съответствие - последните му цикли варират значително или се разпознават слабо. Прегледайте циклите му или запишете профила отново, за да останат точни разпознаването и оценките на времето.", - "advisory_shape_drift": "Профилът '{name}' се е отклонил значително от първоначалната си форма на консумация на мощност. Уредът може да е променил поведението си с времето (напр. котлен камък, износване). Обмислете повторно записване на този профил с последните цикли.", - "advisory_shape_drift_corr": "Профилът '{name}' се е отклонил значително от първоначалната си форма на консумация на мощност (корелация {corr}). Уредът може да е променил поведението си с времето (напр. котлен камък, износване). Обмислете повторно записване на този профил с последните цикли.", - "advisory_unmatchable": "Профилът '{name}' никога няма да бъде съпоставен: зад него няма нито един цикъл с данни за мощността, така че WashData няма с какво да сравни текущия цикъл. Маркирайте цикъл с тази програма (или запишете такъв), за да го върнете в съпоставянето.", - "advisory_unmatchable_no_duration": "Профилът '{name}' никога няма да бъде съпоставен: има цикъл с данни за мощността, но без използваема продължителност, така че WashData няма еталонна дължина, спрямо която да мери съпоставянето. Запишете тази програма отново или задайте очакваната ѝ продължителност, за да я върнете в съпоставянето.", + "advisory_duration_trend_up": "Циклите на '{name}' стават все по-дълги (около +{pct}% на цикъл). Ако уредът се е променил, запишете отново или изградете наново този профил.", + "advisory_energy_trend_up": "'{name}' консумира повече енергия с всеки цикъл (около +{pct}% на цикъл). Проверете уреда, ако това е неочаквано.", + "advisory_poor_health": "'{name}' пасва слабо: последните му цикли се различават силно или съвпадат слабо. Прегледайте циклите му или го запишете отново, за да останат съпоставянето и оценките точни.", + "advisory_shape_drift": "'{name}' се е отклонил от първоначалната си форма на мощността, може би заради котлен камък или износване. Помислете за ново записване по последните цикли.", + "advisory_shape_drift_corr": "'{name}' се е отклонил от първоначалната си форма на мощността (корелация {corr}), може би заради котлен камък или износване. Помислете за ново записване по последните цикли.", + "advisory_unmatchable": "'{name}' никога няма да се съпостави: зад него няма нито един цикъл с данни за мощност. Маркирайте или запишете цикъл на тази програма, за да го поправите.", + "advisory_unmatchable_no_duration": "'{name}' никога няма да се съпостави: цикълът му няма използваема продължителност. Запишете тази програма отново или задайте очакваната ѝ продължителност.", "pg_no_alerts": "В това изпълнение не са открити проблеми.", "pg_override_active": "Използват се вашите редактирани настройки вместо текущите", "pg_history_empty": "Натиснете „Изпълни“, за да възпроизведете циклите си.", "pg_history_summary": "{detected}/{total} открити · {correct} съвпаднали правилно", "pg_sweep_no_metric": "Няма достатъчно данни за оценка на тази цел.", "pg_sweep_intro2": "Намерете настройката, която най-добре постига дадена цел за последните ви цикли. Нищо не се променя, докато не я приложите.", - "pg_running_history": "Възпроизвеждане на вашите цикли…", - "pg_running_sweep": "Изпробване на стойности по всички ваши цикли…", "pg_row_load_hint": "Зареждане на този цикъл в графиката по-горе", - "pg_history_intro2": "Възпроизведете скорошните си цикли през реалния детектор и модула за съпоставяне с настройките по-горе. Щракнете върху който и да е ред, за да заредите този цикъл в графиката; променете настройка, за да видите сравнение преди и след.", + "pg_history_intro2": "Пуснете последните цикли през истинския детектор и съпоставяне с настройките по-горе. Щракнете върху ред, за да го заредите в графиката; променете настройка, за да сравните преди и след.", "pg_simulating": "Симулиране на цикъл…", - "pg_canvas_empty2": "Изберете цикъл по-горе и натиснете „Стартиране“, за да го симулирате. След това посочете, за да видите стойностите, превъртете за мащабиране и плъзнете за преместване.", "pg_apply_settings_confirm": "Да се запазят ли тези {n} настройки на това устройство? {list}", - "pg_stress_terminated_detail": "Установи се на ~{idle}W в режим на готовност -- цикълът приключи {h} ч {m} мин по-късно чрез {reason}.", - "pg_stress_above_threshold_detail": "Консумацията в режим на готовност ~{idle}W е равна или над ефективния праг за спиране ({stop}W) -- цикълът никога не беше отчетен като спокоен. За да поправите това, увеличете stop_threshold_w.", - "pg_stress_hit_cap_detail": "Цикълът работи {h} ч {m} мин без да спре -- принудително спрян от защитния лимит. Консумация в режим на готовност {idle}W спрямо прага за спиране {stop}W.", "no_logs_match": "Няма записи в дневника, отговарящи на тези филтри.", "pg_analysis_hint2": "Изберете цикъл и натиснете Изпълни, за да заредите анализа на съвпадението.", - "pg_analysis_empty2": "Натиснете Изпълни, за да видите анализа на съвпадението.", - "imported_readonly": "Импортирано от магазина на общността. Показва се за справка и съпоставяне. Не се брои във вашата статистика и не може да се редактира.", + "imported_readonly": "Импортиран от магазина на общността. Служи за еталон и съпоставяне, не се брои в статистиката и не може да се редактира.", "merge_no_imports": "Импортираните записи не могат да се обединят с реален цикъл. Премахнете избора им, за да обедините.", - "store_enable_hint": "Активирайте онлайн функциите в настройките, за да разглеждате и импортирате еталонни цикли на общността.", - "advisory_duration_outlier": "Цикли, означени като '{name}' ({n}), са твърде далеч от обичайната ѝ продължителност, за да бъдат някога съпоставени с нея (най-голямо отклонение: {ratio}x). Вероятно са означени погрешно или едно записване е обхванало две изпълнения. Преозначете ги или ги разделете, за да спрат да изкривяват оценката на времето за тази програма.", + "store_enable_hint": "Включете онлайн функциите в Предпочитания > Онлайн и общност, за да разглеждате и импортирате еталонни цикли на общността.", + "advisory_duration_outlier": "Циклите с етикет ‘{name}’ ({n}) са твърде далеч от обичайната му дължина, за да се съпоставят някога (най-лош: {ratio}x). Вероятно са грешно маркирани или съдържат две изпълнения. Маркирайте ги отново или ги разделете, за да не изкривяват оценката на времето.", "standby_above_stop_title": "Все още консумира {idle} Вт при приключване на цикъла", "standby_above_stop_sub": "{n} от последните {total} цикъла. Закъснението при изключване започва да тече едва когато мощността падне под Прага на спиране ({stop} Вт), така че циклите завършват със закъснение или трябва да се спират ръчно.", - "online_intro": "Споделяйте и изтегляйте еталонни цикли с други потребители на WashData. Всички онлайн функции са по избор и по подразбиране са изключени.", "store_share_intro": "Качете този еталонен цикъл, за да могат да го използват други с този уред. Той се преглежда, преди да стане публичен.", - "online_intro_global": "Разглеждайте и споделяйте еталонни записи с други потребители на WashData и потвърждавайте записите за уреди. Една връзка важи за цялата ви интеграция WashData; марката и моделът на уреда се задават за всеки уред в раздела „Основни“. Всички онлайн функции са по избор и по подразбиране са изключени.", + "online_intro_global": "Разглеждайте и споделяйте еталонни записи с други потребители на WashData. Една връзка важи за всичките ви устройства WashData; марката и моделът се задават за всяко устройство в „Основни“. Онлайн функциите са изключени, докато не ги включите.", "online_unavailable": "Онлайн функциите не са налични на този сървър.", "store_picker_offline": "Активирайте онлайн функциите от иконата за настройки (зъбно колело), за да избирате от каталога на общността.", "pick_brand_first": "Първо изберете марка на уреда.", - "model_not_found": "Все още не е в каталога.", - "connect_to_confirm": "Свържете се от иконата за настройки (зъбно колело), за да потвърждавате или оценявате.", + "custom_task_limit": "Достигнахте максималния брой задачи ({max}).", + "custom_tasks_intro": "Добавете всичко друго, за което искате напомняне. Задачата предстои, когато бъде достигнат който и да е от интервалите; оставете единия празен или 0, за да се използва само другият.", + "maintenance_due_sensor_hint": "Двоичният сензор „Предстои поддръжка“ е включен, докато има задача за изпълнение; използвайте го в своите автоматизации за известия.", + "maintenance_supporter_intro": "Maintenance Supporter е отделна интеграция, която може много повече: всякакъв уред или задача, графици по време, употреба или сензор, известия и история. Може да използва брояча на цикли на WashData.", "adopt_settings_hint": "Приложи настройките за разпознаване и съпоставяне на споделеното устройство към това устройство", - "include_settings_hint": "Включи прагове за разпознаване, тегла за съпоставяне и други параметри в споделения пакет", + "include_settings_hint": "Споделете праговете за откриване и съпоставяне на това устройство (без известия, обекти и цена на енергията). Другите сами избират дали да ги приложат.", "onboard_download": "Изтегли готови настройки от магазина на общността", - "share_consent": "Споделяте реални данни от вашия уред. Не споделяйте, ако моделите ви на използване са лични.", "share_device_none": "Все още няма настроени устройства. Първо добавете устройство.", "share_guideline_naming": "Използвайте ясни имена на програмите (напр. 'Cotton 40', 'Eco 60'), за да могат другите да ги разпознаят", - "share_guideline_quality": "Споделяйте само цикли, завършили нормално -- без прекъсвания по време на цикъла, отваряне на вратата или трептения в захранването.", + "share_guideline_quality": "Споделяйте само цикли, завършили нормално: без прекъсвания, отваряне на вратата и спирания на тока.", "share_guideline_review": "Прегледайте профилите си преди споделяне -- премахнете тези, които изглеждат грешни", "share_guidelines_title": "Преди споделяне", - "store_download_device_intro": "Изтеглете настройките на устройство от общността и ги приложете към ново или съществуващо устройство", + "store_download_device_intro": "Добавете към устройството си всички споделени програми и еталонните им цикли. Собствените ви цикли и статистика остават незасегнати.", "store_share_device_intro": "Качете {brand} {model} с референтните цикли, които изберете. Други със същия уред могат да приемат вашите програми. Публикациите се преглеждат, преди да станат публични.", "share_profile_no_cycles": "Няма референтни цикли - маркирайте цикъл със ⭐ в раздела Цикли, за да включите този профил", - "advisory_phase_inconsistent": "Изглежда, че '{name}' смесва различни програми или температури - циклите му загряват за много различно време. Разделянето му на отделни профили (напр. по температура) ще подобри съпоставянето и оценките на времето.", - "advisory_phase_inconsistent_title": "⚠ Възможно смесени програми", - "export_select_intro": "Отбележете какво точно да се включи. Изборът на профили без техните цикли пак експортира разпознаваема програма (научената ѝ форма се пренася заедно с нея).", - "import_analyze_hint": "Заредете експортиран файл (или поставете неговия JSON). WashData го анализира и показва какво точно може да се импортира, преди нещо да се промени.", + "export_select_intro": "Отметнете какво да се включи. Профилите без циклите си пак експортират разпознаваема програма.", + "import_analyze_hint": "Заредете експортиран файл (или поставете неговия JSON). WashData показва какво може да се импортира, преди нещо да се промени.", "analyzing": "Анализиране…", - "device_type_mismatch_warn": "Този експорт е от друг тип уред ({src} вместо {local}). Програмите и циклите пак могат да се импортират като справочни данни, но специфичните за устройството настройки и импортът на реална история са изключени.", + "device_type_mismatch_warn": "Този експорт е от друг тип уред ({src} vs {local}). Програмите и циклите пак могат да се импортират като еталонни данни, но без настройките на устройството и реалната история.", "merge_hint": "Импортираните елементи се добавят; нищо локално не се губи. Конфликтите на имена се разрешават по-долу.", "replace_warn": "Всяка отбелязана категория се изтрива и заменя от файла. Неотбелязаните категории остават непроменени.", "dest_reference_hint": "Импортираните цикли само подобряват разпознаването на програми и никога не влияят на статистиката за използване/енергия.", - "dest_real_history_hint": "Импортираните цикли се броят като собствена история на това устройство и захранват статистиката за енергия/използване. Използвайте за преместване на един уред към нова инсталация.", - "import_history_description": "Имали сте умен контакт още преди WashData? Качете експорт на историята на неговия сензор за мощност или я прочетете направо от Home Assistant, и обичайното откриване минава по нея, така че миналите цикли се появяват в списъка „Цикли“ готови за наименуване.", - "hist_input_hint": "Качете CSV файл, изтеглен от панела „История“ (обект, състояние, последна промяна), или оставете WashData да прочете историята на сензора директно. След това откриването минава по нея точно както в реално време, а вие избирате кои от намерените цикли да запазите.", - "hist_recorder_hint": "Чете данните от избраната дата до сега. По подразбиране Home Assistant пази подробна история 10 дни, а след това само средни стойности на час, които са прекалено груби за откриване на цикли - избирайте по-ранна дата само ако вашият recorder е настроен да пази повече.", - "hist_scanning": "Историята ви се възпроизвежда през детектора. Това се изпълнява във фонов режим - можете да затворите този диалог и да се върнете по-късно.", + "dest_real_history_hint": "Импортираните цикли се броят за собствена история и статистика на това устройство. Използвайте при преместване на уред в нова инсталация.", + "import_history_description": "Имахте смарт контакт преди WashData? Качете историята на мощността му или я прочетете от Home Assistant. Откриването минава през данните и старите цикли се появяват в списъка „Цикли“, готови за именуване.", + "hist_input_hint": "Качете CSV от панела „История“ (обект, състояние, последна промяна) или оставете WashData да прочете историята на сензора директно. Откриването на живо минава през данните, а вие избирате кои цикли да запазите.", + "hist_recorder_hint": "Чете от избраната дата до сега. Home Assistant по подразбиране пази подробна история 10 дни; по-старите средни часови стойности са твърде груби за откриване на цикли.", + "hist_scanning": "Историята ви минава през детектора на заден план. Можете да затворите този прозорец и да се върнете по-късно.", "hist_imported_count": "Импортирани цикли: {n}.", "hist_duplicates": "Вече импортирани и пропуснати: {n}.", "hist_capped": "Достигнат е лимитът на импортирани цикли за устройството; останалите не бяха запазени.", - "hist_next_step": "Те са в списъка „Цикли“, отбелязани като импортирана история. Отворете един и използвайте „Етикет“, за да посочите програмата му.", + "hist_next_step": "Те са в списъка „Цикли“, маркирани като импортирана история. Отворете един и с бутона „Етикет“ назовете програмата му.", "hist_rows_read": "Прочетени показания: {n}", "hist_breaks": "Пропуски, при които сензорът е бил недостъпен: {n}", "hist_other_entity": "Пропуснати показания на други обекти: {n}", @@ -1095,25 +1024,72 @@ "hist_scan_capped": "Показани са само първите кандидати (намерени: {n}).", "hist_recorder_empty": "Home Assistant няма подробна история за този сензор в този период.", "hist_scan_failed": "Сканирането е неуспешно.", - "hist_scan_expired": "Това сканиране вече не е налично. Моля, сканирайте отново.", + "hist_scan_expired": "Това сканиране вече не е налично. Сканирайте отново.", "hist_import_failed": "Импортирането е неуспешно.", - "imported_history_readonly": "Открито в импортирана история на мощността. Влияе на съпоставянето на програми, но не се брои във вашата статистика и не може да се изрязва или разделя. Използвайте „Етикет“, за да посочите програмата.", - "odometer_intro": "Този общ брой само расте. Изтриването на запис за цикъл не го променя и той продължава да се увеличава и след ограничението за съхранявана история, така че напомнянията за сервиз остават точни.", - "odometer_correct_hint": "Задайте това, ако е записан цикъл, който никога не се е изпълнявал, или ако уредът вече е изпълнявал цикли преди инсталирането на WashData.", + "imported_history_readonly": "Намерен в импортирана история на мощността. Помага на съпоставянето, но не се брои в статистиката и не може да се изрязва или разделя. Маркирайте го, за да назовете програмата.", + "odometer_intro": "Този сбор само расте. Изтриването на цикъл не го намалява, затова напомнянията за обслужване остават верни.", + "odometer_correct_hint": "Задайте това, ако WashData е отчел цикъл, който не се е състоял, или уредът е работил преди инсталирането на WashData.", "no_reminders_set": "Все още няма зададени напомняния за сервиз.", "toast_program_armed": "Програмата е зададена за следващия цикъл: {program}", "toast_range_required": "Задайте валиден диапазон От/До", - "no_profiles_option": "(няма профили, добавете нов)" + "no_profiles_option": "(няма профили, добавете нов)", + "compare_cycles_title": "Сравняване на цикли ({count})", + "edit_builtin_phase": "Това е вградена фаза. Запазването създава собствено копие; изтрийте копието, за да възстановите оригинала.", + "fetch_error": "Неуспешно зареждане на данните.", + "connection_lost": "Връзката е прекъсната - показани са данни от {time}.", + "pg_notify_partial": "Маркерите показват само известията за старт, почти готово, завършване, етапи и тихи часове. Актуализациите на живо, напомнянията за разтоварване и предупрежденията за превишаване не се симулират.", + "connect_to_confirm": "Свържете се от иконата за настройки (зъбно колело), за да потвърждавате или оценявате.", + "coverage_gap_cluster": "Последните немаркирани цикли ({count}) с продължителност около {min} мин изглеждат като една програма, която още не сте създали.", + "pg_label_yes": "В края на цикъла би получил етикет „{name}“", + "pg_label_no": "Би бил изпратен за преглед: {reason}", + "live_match_debug_tip": "Увереност: колко точно текущата крива на мощността съвпада с най-добрия профил кандидат (0-100%). Двусмислено: резултатите на двата най-добри кандидата се различават с по-малко от 5%, така че етикетът е несигурен до края на цикъла.", + "automation_alias_started": "{device}: цикълът започна", + "automation_alias_finished": "{device}: цикълът завърши", + "automation_desc_started": "Изпълнява се при стартиране на цикъл на {device}. Добавете свои действия (известия, осветление, ...).", + "automation_desc_finished": "Изпълнява се при завършване на цикъл на {device}. Добавете свои действия (известия, осветление, ...).", + "feedback_detected": "WashData откри: {name}", + "import_undo_hint": "Пази се точка за възстановяване отпреди последното импортиране ({when}): {profiles} профил(а) и {cycles} цикъл(а).", + "replace_undo_hint": "Първо се записва точка за възстановяване, така че можете да отмените това импортиране от „Експорт/Импорт“.", + "pg_in_sample_note": "Тези цикли са помогнали за изграждането на профилите, с които се сравняват, затова резултатите са оптимистични: при нови цикли очаквайте малко по-малко верни съвпадения.", + "hist_tail_trimmed": "Отрязани са {min} мин изчакване след края на цикъла, както прави работещата интеграция", + "hist_truncated": "спряно при лимита на показанията ({n}), затова останалата част от историята не беше сканирана", + "hist_warn_kw": "Най-високата стойност е само {w}. Изглежда този сензор отчита киловати; WashData се нуждае от ватове, затова не могат да се открият цикли.", + "hist_warn_naive_time": "{n} времеви клейма нямат часова зона и бяха прочетени като UTC. Ако файлът използва местно време, циклите са изместени с вашата разлика от UTC, а стойностите около смяната на часа може да се загубят.", + "hist_warn_naive_time_one": "{n} времево клеймо няма часова зона и беше прочетено като UTC. Ако файлът използва местно време, циклите са изместени с вашата разлика от UTC, а стойностите около смяната на часа може да се загубят.", + "hist_warn_naive_time_other": "{n} времеви клейма нямат часова зона и бяха прочетени като UTC. Ако файлът използва местно време, циклите са изместени с вашата разлика от UTC, а стойностите около смяната на часа може да се загубят.", + "pg_compare_profile_tip": "Избира само профила, изчертан върху графиката. Симулацията винаги сама съпоставя програмата с цикъла, както прави работещата интеграция.", + "pg_outcome_stale": "Настройките са променени след изпълнението на тази симулация. Тя все още показва стария резултат.", + "pg_sweep_guarded": "Завършва повече цикли преждевременно, разделя повече от тях или пропуска цикли, които текущата настройка улавя, затова никога не се препоръчва.", + "pg_sweep_keep_current": "Запазете текущата стойност ({value}, {metric}): никоя стойност не беше по-добра с поне един цикъл.", + "pg_sweep_n_cycles": "върху {count} цикъла", + "pg_sweep_n_cycles_one": "върху {count} цикъл", + "pg_sweep_n_cycles_other": "върху {count} цикъла", + "pg_task_busy": "За това устройство вече тече „Тест върху история“ или „Оптимизиране“. Изчакайте да завърши или го отменете.", + "cycle_context_loading": "Зареждане на историята от recorder…", + "cycle_context_note": "В сиво: историята на сензора за мощност от recorder около този цикъл. Не е част от цикъла и не влияе на продължителността, енергията или съпоставянето.", + "cycle_context_none": "В recorder няма отчитания около този цикъл (по подразбиране Home Assistant пази 10 дни история).", + "cycle_context_tip": "Само за показване: показва историята на сензора за мощност от recorder около този цикъл, ако още се пази. За да добавяте неуспешните опити за старт към бъдещи цикли, използвайте Предзапис на кривата (Настройки > Откриване > Начало на цикъла), което променя записаните продължителности.", + "invalid_threshold_pair": "Прагът на спиране ({stop} Вт) трябва да е под Прага на стартиране ({start} Вт). Нищо не е запазено; променете двете стойности заедно." }, "phase_desc": { "anti_crease": "Случайни кратки движения след завършване за намаляване на бръчките.", "anti_wrinkle": "Периодично търкаляне след цикъл за намаляване на бръчките.", + "baking": "Нагряване на висока температура, при което се оформят коричката и средата на хляба.", + "cooking": "Основна фаза на готвене с включени нагревател и вентилатор.", "cool_down": "Преобръщане без топлина близо до края на цикъла.", + "cool_down_af": "Охлаждане само с вентилатор след нагряването.", "drain_and_switch": "Период на преход от режим пране към режим сушене.", "dry": "Етап на сушене с помощта на нагревател и/или остатъчна топлина.", "drying": "Период на основно загрято въртене.", "heat_up": "Първоначално загряване на нагревателя преди да започне пълното изсъхване.", + "keep_warm": "Слабо нагряване, за да остане храната топла.", + "keep_warm_bm": "Слабо нагряване, за да остане хлябът топъл след печенето.", + "kneading": "Моторът смесва и меси тестото. Висока консумация.", + "pause_af": "Кратка пауза за разклащане или проверка.", + "pre_heat": "Начално загряване на камерата преди същинското готвене.", "pre_rinse": "Първоначално пръскане преди измиване с препарат.", + "proving": "Втасване при слаба топлина: маята ферментира и тестото набъбва.", + "resting": "Кратка пауза с ниска мощност между месенията, за да се отпусне глутенът.", "pre_wash": "Първоначално накисване или предварителна обработка преди основното пране.", "rinse": "Етап на изплакване с чиста вода. Тази фаза може да се повтори многократно.", "sanitize": "Етап на високотемпературно почистване за програми за дезинфекция.", @@ -1126,6 +1102,7 @@ "wash_dw": "Основно пране с препарат с нагряване." }, "placeholder": { + "custom_task": "напр. Почистване на уплътнението на вратата", "add": "добавете…", "group_name": "напр. Памук 2:47", "json_buttons": "[{\"action\":\"ID\",\"title\":\"Label\"}]", @@ -1152,11 +1129,11 @@ }, "section": { "dishwasher": { - "intro": "Обработка на края на цикъла за съдомиялни машини, които обикновено завършват с дълга фаза на сушене почти без консумация на енергия преди кратко финално източване.", + "intro": "Обработка на края за съдомиялни, които често сушат дълго и тихо преди кратко финално източване.", "label": "Съдомиялна машина" }, "anti_wrinkle": { - "intro": "Режимът против бръчки/против набръчкване открива импулси на ниска мощност след основната фаза и ги прикрепя към завършения цикъл.", + "intro": "Оставя кратките импулси на барабана с ниска мощност след основната фаза на сушилнята към завършения цикъл.", "label": "Против бръчки" }, "basic": { @@ -1164,7 +1141,7 @@ "label": "Основни" }, "delay": { - "intro": "Детекцията на отложен старт идентифицира кога уредът е захранван, но все още не е започнал своя цикъл.", + "intro": "Открива уред, който е включен и чака отложен старт.", "label": "Отложен старт" }, "detection": { @@ -1188,157 +1165,149 @@ "label": "Синхронизиране и наблюдател" }, "triggers": { - "intro": "Незадължителни външни сигнали: задействане на края, сензор за врата, превключвател за пауза и напомнянето за разтоварване.", + "intro": "Незадължителни допълнения: задействане на края, сензор за врата, превключвател за пауза и напомняне за разтоварване.", "label": "Задействания и врати" - }, - "phase_eta": { - "label": "Оставащо време", - "intro": "Оценка на оставащото време, съобразена с фазите, за машини, чиято продължителност на цикъла зависи от температурата или центрофугирането." } }, "setting": { "dishwasher_end_spike_quiet_release": { - "doc": "След като цикълът надхвърли очакваната си продължителност, колко дълго съдомиялната машина трябва да остане неактивна (под «Праг на спиране»), преди WashData да спре да чака финално източване и да завърши цикъла. Увеличете го, ако вашата машина има дълга тиха фаза на сушене преди късно финално източване, което се пропуска - по-широк прозорец позволява на научената продължителност да следва сезонните промени (по-студена входяща вода = по-дълги цикли), вместо да се фиксира към старата средна стойност. То само съкращава изчакването спрямо вътрешния 30-минутен лимит за изчакване на финалния пик (източване на вода), но никога не го удължава.", + "doc": "След очакваната продължителност: колко дълго съдомиялната трябва да остане под Прага на спиране, преди WashData да спре да чака финалното източване. Увеличете, ако късното източване след дълго сушене се пропуска. Чакането никога не надвишава 30 минути.", "label": "Тихо завършване на пасивното сушене" }, "anti_crease_finalize_ratio": { - "doc": "Колко навътре в очакваната продължителност на съпоставената програма трябва да е навлязъл цикълът, преди WashData да може да прецени, че наблюдаваното въртене на барабана при ниска мощност е опашката против бръчки, и да завърши цикъла там. Очакваната продължителност е средната за програмата, затова при сушилня, чието време на сушене с датчик за влажност зависи от товара, повечето пускания никога не достигат 98% от нея и опашката виси чак до резервния таймаут - често 15 до 40 минути след като машината реално е спряла. При такава сушилня намалете стойността (напр. 0.75). При перални оставете стойността по подразбиране: пералнята прекарва по-голямата част от цикъла под Макс. антибръчкова мощност, така че този дял е единственото, което отделя тиха фаза в средата на прането от опашката, и намаляването му може да съкрати прането наполовина. Оставете празно за стойността по подразбиране (0.98). Това е различна настройка от Коефициент на интелигентно прекратяване, която управлява отделна проверка за предсрочно завършване.", + "doc": "Дял от средната продължителност на програмата, който цикълът трябва да достигне, преди въртенето на барабана с ниска мощност да се брои за опашка против бръчки. Намалете го (напр. 0,75) при сушилни, които се съобразяват с количеството пране. При перални оставете стойността по подразбиране (0,98), иначе прането може да приключи по средата.", "label": "Коефициент на завършване на режима против бръчки" }, "anti_wrinkle_enabled": { - "doc": "Разпознайте кратките импулси на сушене с ниска мощност, които сушилнята излъчва след основната топлинна фаза, и ги запазете прикрепени към завършения цикъл, вместо да ги четете като нови цикли.", + "doc": "Оставяйте кратките импулси на барабана с ниска мощност след основната фаза на нагряване на сушилнята към завършения цикъл, вместо да се отчитат като нови цикли.", "label": "Активиране на антибръчково засичане" }, "anti_wrinkle_exit_power": { - "doc": "Мощността трябва да падне под това между импулсите, за да остане активен режимът против бръчки.", + "doc": "В режим против бръчки мощността под тази стойност се смята за покой между импулсите на въртене, а покой, по-дълъг от Макс. пауза между импулсите, прекратява режима. Ако Прагът на спиране е по-висок, се използва той, така че настройката има ефект само над Прага на спиране.", "label": "Праг на изходна мощност" }, "anti_wrinkle_idle_timeout": { "label": "Макс. пауза между импулсите", - "doc": "Колко дълго машината може да остане в покой между два импулса на въртене на барабана, преди режимът против бръчки да приключи. Задайте я над най-дългата пауза, която сушилнята ви оставя между импулсите, иначе всеки следващ импулс се чете като фалшив старт." + "doc": "Колко дълго сушилнята може да мълчи между импулсите на барабана, преди режимът против бръчки да приключи. Задайте над най-дългата пауза на сушилнята си, иначе следващите импулси ще се отчитат като фалшиви стартове." }, "anti_wrinkle_max_duration": { - "doc": "Импулсите, по-дълги от това, се третират като истински цикъл, а не като търкаляне против бръчки.", + "doc": "Импулс, по-дълъг от това, се брои за истински цикъл, а не за въртене против бръчки.", "label": "Максимална продължителност" }, "anti_wrinkle_max_power": { - "doc": "Пулс над тази мощност се третира като истински нов цикъл, а не като търкаляне против бръчки. Задайте точно над импулсната мощност.", + "doc": "Импулс над тази мощност се брои за истински нов цикъл, а не за въртене на барабана. Задайте малко над мощността на импулсите.", "label": "Макс. антибръчкова мощност" }, "auto_label_confidence": { - "doc": "Ако резултатът за съвпадение в края на цикъла е равен или над този, програмата се етикетира автоматично без подкана за потвърждение. Повишете го, за да изисква по-висока сигурност преди автоматично етикетиране; намалете го, за да автоматизирате повече. Работи във връзка с Learning Confidence под него.", + "doc": "Оценка на съвпадението в края на цикъла, при която или над която програмата се маркира без питане. Повишете за повече сигурност; понижете, за да се маркират автоматично повече цикли. Дръжте над Доверие в обучението.", "label": "Автоматично маркиране доверие" }, "auto_maintenance": { - "doc": "Изпълнявайте нощно почистване: възстановявайте обвивките на профили, преизчислявайте изправността на цикъла, изрязвайте следите за отстраняване на грешки и запазвайте най-новите цикли.", + "doc": "Нощно почистване: маркира стари цикли, които вече ясно съвпадат с програма, изгражда наново кривите на програмите, опреснява отметките за аномалии и състоянието на циклите и премахва следите за отстраняване на грешки, ако „Запазване на следи за отстраняване на грешки“ е изключено. Циклите и записите на мощността им винаги се пазят.", "label": "Автоматична поддръжка (нощно почистване)" }, "completion_min_seconds": { - "doc": "Цикли, по-кратки от този, се отхвърлят като призрачни цикли (тестови изпълнения, отваряне на вратата за добавяне на чорап).", + "doc": "Циклите, по-кратки от това (пробни пускания, отваряне на вратата за добавяне на чорап), се записват като прекъснати и не получават известие за завършване.", "label": "Мин. продължителност на цикъл" }, "delay_confirm_seconds": { - "doc": "Захранването трябва да остане в диапазона на готовност за това време, преди уредът да бъде третиран като чакащ за стартиране, а не работещ.", + "doc": "Мощността трябва да остане толкова време в лентата на готовност, преди уредът да се брои за чакащ старт.", "label": "Секунди за потвърждение" }, "delay_start_detect_enabled": { - "doc": "Открива кога уредът е включен и чака (отложен старт/в режим на готовност), но не е започнал цикъла си, така че тегленето в режим на готовност да не се бърка с работещ цикъл.", + "doc": "Откривайте уред, който е включен и чака отложен старт, за да не се приема консумацията му в готовност за работещ цикъл.", "label": "Активиране на засичане на отложен старт" }, "delay_timeout_hours": { - "doc": "Спрете да чакате в режим на отложен старт след толкова много часове и се върнете към празен ход, така че машина, оставена на захранване, но никога не стартирана, не чака вечно.", + "doc": "След толкова часа спрете да чакате отложения старт и се върнете в празен ход.", "label": "Часове на таймаут" }, "device_type": { - "doc": "Клас на уреда. Задава разумни стойности за откриване по подразбиране (прагове, забавяне на изключване, обработка при край), настроени за този тип уред; променете го само ако устройството първоначално е било настроено като грешен тип.", + "doc": "Задава стойностите по подразбиране за откриване (прагове, закъснение при изключване, обработка на края) за този вид уред. Променяйте само ако устройството е настроено с грешен тип.", "label": "Тип устройство" }, "door_sensor_entity": { - "doc": "Допълнителен бинарен сензор на вратата. Използва се за откриване кога уредът е бил отворен/разтоварен след цикъл.", + "doc": "Незадължителен сензор за врата. Показва кога уредът е отворен за разтоварване след цикъл.", "label": "Обект на сензор за врата" }, "duration_tolerance": { - "doc": "Толерантност към оценките на оставащото време (обратна информация за обучение, несъвпадение). Ако действителната продължителност е в рамките на +/-X% от прогнозата, това се счита за добро съвпадение.", + "doc": "Само за обратната връзка при обучението: цикъл в рамките на +/-X% от оценената продължителност се брои за добра оценка. Не влияе на съпоставянето.", "label": "Толеранс на оценка" }, "enable_ml_models": { - "doc": "Докато цикълът се изпълнява, оставете моделите да прецизират резултатите на живо: по-стабилна оценка на оставащото време и енергия/разходи и защита срещу преждевременно спиране при откриване на край (може само да забави финала, никога да не приключи по-рано и е ограничен). Използва вашите фино настроени модели, когато са налични, в противен случай вградените. Изкл. = само класическата базирана на мощност логика (все още надеждна).", + "doc": "Използвайте модел, обучен на тази машина, за прогноза на енергията и цената на текущия цикъл и позволете на добре позната програма, спряла необичайно рано, да приключи без пълното закъснение при изключване. Изключено: само логика по мощност.", "label": "Прилагане на умни модели по време на цикъл" }, "end_energy_threshold": { - "doc": "По време на обратното отброяване на времето за изключване, натрупаната енергия (ватове х време) се сравнява с този праг. Ако бъде превишено, обратното броене се нулира - запазвайки въртящите се машини против намачкване и опашките за сушене в съдомиялна машина, прикрепени към цикъла, вместо да ги съкращават. Повишете го, ако циклите приключат твърде рано по време на охлаждане; намалете го, ако откриването е бавно.", + "doc": "Енергия, разрешена по време на закъснението при изключване. Над нея цикълът продължава, така въртенията и финалното сушене остават част от него. Намалете, ако циклите приключват по време на опашката; увеличете, ако завършени цикли продължават да чакат.", "label": "Крайна енергия" }, - "end_repeat_count": { - "doc": "Брой последователни отчитания под прага на спиране, необходими преди края на цикъла. 1 е добре за повечето щепсели. Увеличете до 2-3, ако интелигентният ви щепсел от време на време отчита проба с фалшива нула по средата на цикъла и вашите цикли приключват преждевременно.", - "label": "Брой повторения при край" - }, "energy_price_entity": { - "doc": "Сензор с текущата цена на електроенергията за kWh (например динамична тарифа). Трябва да е цена, а не собственият сензор за мощност или енергия на контакта: има предимство пред статичната цена по-долу, така че брояч на kWh тук остойностява всеки цикъл по показанието на брояча вместо по вашата тарифа. Когато „Цена, претеглена по време“ е включена, всеки цикъл се таксува по цената, валидна във всеки момент от работата му; в противен случай върху него се фиксира цената, валидна в момента на приключването му.", + "doc": "Сензор с текущата цена за kWh (напр. динамична тарифа). Има предимство пред статичната цена по-долу, затова трябва да е цена, а не сензорът за мощност или енергия на контакта.", "label": "Обект на цена на енергия" }, "energy_price_static": { - "doc": "Фиксирана цена за kWh, използвана за стойности на разходите, когато по-горе не е зададен обект на реална цена.", + "doc": "Фиксирана цена за kWh, използва се, когато не е зададен обект за цена.", "label": "Статична цена на енергия (на kWh)" }, "energy_sensor": { - "doc": "Незадължителен натрупващ брояч на енергия (total_increasing kWh/Wh, напр. собственият брояч за общото потребление на контакта). Когато е зададен, енергията, отчитана за всеки цикъл, се взема от разликата в показанията на този брояч между началото и края, което избягва подценяването, което се получава при интегриране на бавно докладващ сензор за мощност. Връща се към интегрираната стойност, ако показанието липсва, мерната му единица е неизвестна или разликата не е положителна. Оставете празно, за да продължите да интегрирате сензора за мощност.", + "doc": "Незадължителен брояч на kWh или Wh (total_increasing, напр. собственият електромер на контакта). Енергията на цикъла тогава се чете от него, което е по-точно от изчисляването по бавен сензор за мощност. Ако стойността му е неизползваема, се ползва изчислената.", "label": "Обект на измервателен уред за енергия" }, "expose_debug_entities": { - "doc": "Публикувайте допълнителни диагностични HA обекти (доверие на съвпадението, неяснота, вътрешно състояние). Off поддържа списъка с обекти чист за нормална употреба.", + "doc": "Добавете диагностични обекти (увереност на съвпадение, нееднозначност, вътрешно състояние). Изключено: списъкът с обекти остава чист.", "label": "Изложи обекти за отстраняване на грешки" }, "external_end_trigger": { - "doc": "Двоичен сензор, чиято промяна на състоянието маркира края на цикъла (напр. „завършен“ контакт на уреда или придружаваща интеграция).", + "doc": "Двоичен сензор, чиято промяна отбелязва края на цикъла (напр. контакт „готово“ или друга интеграция).", "label": "Обект на външен тригер" }, "external_end_trigger_enabled": { - "doc": "Нека външен бинарен сензор сигнализира за края на цикъл, в допълнение към вграденото откриване, базирано на мощност.", + "doc": "Позволете на двоичен сензор да сигнализира края на цикъла, в допълнение към откриването по мощност.", "label": "Активиране на външен краен тригер" }, "external_end_trigger_inverted": { - "doc": "Отнасяйте се към ИЗКЛЮЧВАНЕ (а не ВКЛЮЧВАНЕ) на сензора за задействане като сигнал за край на цикъла.", + "doc": "Приемайте ИЗКЛЮЧВАНЕТО на сензора (а не включването) за сигнал за край.", "label": "Инвертиране на zewn. тригер (задействане при ИЗКЛ.)" }, "learning_confidence": { - "doc": "Ако резултатът за съвпадение е между тази стойност и „Доверие за автоматично маркиране“, известие за обратна връзка ви моли да потвърдите разпознатата програма. Под този резултат съвпадението е твърде несигурно, за да бъде показано. Трябва да се поддържа под „Доверие за автоматично маркиране“.", + "doc": "Най-ниската оценка на съвпадение за маркиране на завършен цикъл. Явният победител се маркира; при близък резултат цикълът отива в опашката за преглед в „Цикли“. Под тази стойност нищо не се маркира и не се пита. Дръжте я под Автоматично маркиране доверие.", "label": "Доверие в обучението" }, "linked_device": { - "doc": "По желание вмъкнете това WashData устройство под друго устройство (напр. интелигентния щепсел) в регистъра на HA устройството, показано като „Свързано чрез ...“.", + "doc": "Незадължително. Показвайте това устройство WashData под друго устройство (напр. смарт контакта) в Home Assistant като „Свързано чрез ...“.", "label": "Групиране под устройство" }, "min_off_gap": { - "doc": "Ако машината се изключи за по-малко от това време, последователността включване/изключване/включване се третира като един непрекъснат цикъл. Предотвратява разделянето на програмите за накисване (машината се изключва за няколко минути по време на прането) на два отделни цикъла. Задайте го по-кратък от интервала между вашите последователни натоварвания, ако искате те да се броят като отделни цикли. Стандартните настройки за тип устройство защитават типичната пауза в рамките на цикъла за всеки уред.", + "doc": "Изключване, по-кратко от това, не разделя цикъла, затова паузите за накисване остават в едно пране. Дръжте по-кратко от паузата между последователни зареждания, иначе те се сливат в един цикъл.", "label": "Минимална пауза при изключване" }, "min_power": { - "doc": "Абсолютна минимална мощност, считана за активна. Показанията под това се третират като 0 W (в режим на готовност), филтрирайки фантомното натоварване на интелигентните щепсели и светодиодите в режим на готовност.", + "doc": "Стойности под тази се броят за 0 W (готовност). Филтрира консумацията на празен ход на смарт контактите и светодиодите в готовност.", "label": "Минимална мощност" }, "ml_training_enabled": { - "doc": "Периодично изучавайте вашите прегледани цикли за една нощ и настройвайте фино моделите за тази конкретна машина. Промяната се запазва само когато наистина има по-добри резултати при задържани цикли, така че това може само да помогне или да остане същото - никога да не регресира.", + "doc": "От време на време преобучавайте модела за енергия върху завършените цикли на тази машина. Новият модел се запазва само ако на тестовите цикли е по-добър от простата оценка.", "label": "Учене от тази машина" }, "ml_training_hour": { - "doc": "Местен час от деня (0-23), за да извършите фината настройка през нощта. Изберете тих час, например 2 (02:00).", + "doc": "Час от денонощието (0-23) за нощното обучение. Изберете тих час, напр. 2.", "label": "Учене в час" }, "ml_training_interval_days": { - "doc": "Проверете отново за подобрения най-много веднъж за тези много дни.", + "doc": "Преобучавайте най-много веднъж на толкова дни.", "label": "Проверявай не по-рядко от" }, "ml_training_min_cycles": { - "doc": "Изчакайте поне толкова много цикли да бъдат записани преди фина настройка, така че има достатъчно за поука.", + "doc": "Преди обучение изчакайте поне толкова записани цикъла.", "label": "Необходими цикли първо" }, "name": { - "doc": "Показвано име, показано в списъка с интеграции на HA и регистъра на устройствата.", + "doc": "Име на това устройство в Home Assistant.", "label": "Име на устройство" }, "no_update_active_timeout": { - "doc": "Ако не пристигнат актуализации на захранването за толкова дълго време, докато работи, приемете, че щепселът е изпуснат офлайн и спрете принудително, за да избегнете цикъл на зомбиране. По подразбиране 600 s позволява забавяне в облак или мрежа.", + "doc": "Ако по време на цикъл толкова време не пристигне актуализация на мощността, се приема, че контактът е офлайн, и цикълът се спира принудително. 600 с по подразбиране покриват закъснения на облака или mesh мрежата.", "label": "Таймаут без актуализация" }, "notify_before_end_minutes": { @@ -1346,35 +1315,35 @@ "label": "Предупреждение преди края" }, "notify_channel": { - "doc": "Android notification channel name for start/live messages (controls per-channel sound and priority on the mobile app). Blank използва придружаващото приложение по подразбиране.", + "doc": "Канал на Android за съобщенията за старт и на живо; каналът задава звука и приоритета. Празно: каналът на приложението по подразбиране.", "label": "Android канал (старт/на живо)" }, "notify_cycle_timers": { - "doc": "Известия в определени минути в цикъл (напр. за добавяне на омекотител). Съобщението поддържа {device}, {program}, {minutes}. Активирайте автоматична пауза, за да направите пауза в този момент и да получите интерактивно известие с бутон Възобновяване; възобновяване чрез панела, услугата за пауза/възобновяване или действието за известяване.", + "doc": "Известия в зададени минути от цикъла (напр. за добавяне на омекотител). Съобщението поддържа {device}, {program}, {minutes}. С автоматична пауза цикълът също спира там, а известието получава бутон „Възобновяване“.", "label": "Циклови таймери" }, "notify_finish_channel": { - "doc": "Име на канала за известяване на Android за крайното съобщение. Blank използва повторно началния/на живо канал.", + "doc": "Канал на Android за съобщението за завършване. Празно: използва се каналът за старт и на живо.", "label": "Android канал (завършване)" }, "notify_finish_message": { - "doc": "Текст, изпращан при завършване на цикъл. Променливи на шаблона: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}. {time_finished} и {vs_typical} са най-полезни тук.", + "doc": "Текст при завършване на цикъл. Променливи: {device}, {program}, {duration} (минути), {duration_hm} (1 h 05 min), {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}, {status} (completed или force_stopped; прекъснатите цикли не изпращат нищо). При всяка друга променлива се използва текстът по подразбиране.", "label": "Съобщение при завършване" }, "notify_finish_services": { - "doc": "notify.* услуги, извиквани, когато цикълът приключи. Добавете по една на цел; оставете празно, за да няма известие за приключване.", + "doc": "Услуги notify.*, извиквани при завършване на цикъл. Оставете празно за никакви.", "label": "Услуги при завършване" }, "notify_fire_events": { - "doc": "Също така задействайте ha_washdata_* събития при начало/завършване на цикъл, за да можете да изградите свои собствени автоматизации.", + "doc": "Генерирайте и събития ha_washdata_* при старт и завършване на цикъл за собствени автоматизации.", "label": "Задействане на HA събитие за известия" }, "notify_icon": { - "doc": "Малката икона, показвана в известието (напр. mdi:washing-machine). Android я изчертава в лентата на състоянието; iOS я показва вместо иконата на приложението и изисква придружаващото приложение 2026.8 или по-ново. Оставете празно за иконата по подразбиране на платформата.", + "doc": "Малка икона в известието (напр. mdi:washing-machine). Изпраща се само до получатели mobile_app, не до групи notify. На iOS замества иконата на приложението и изисква мобилно приложение 2026.8 или по-ново. Празно: иконата на платформата по подразбиране.", "label": "Икона на известие" }, "notify_live_chronometer": { - "doc": "Показвайте актуализиращ се на живо таймер за обратно отброяване в известието (на платформи, които го поддържат) вместо статична оценка.", + "doc": "Показвайте в известието обратно броене на живо (където се поддържа) вместо фиксирана оценка.", "label": "Използване на хронометър на живо" }, "notify_live_interval_seconds": { @@ -1382,139 +1351,131 @@ "label": "Интервал на актуализация на живо" }, "notify_live_overrun_percent": { - "doc": "Ако даден цикъл надхвърли оценката си с повече от този процент, изпратете предупреждение за превишаване.", + "doc": "Актуализациите на живо спират, когато цикълът надхвърли очакваната си продължителност с този процент (20 = до 1,2 пъти очакваното време).", "label": "Живо преминаване % преди предупреждение" }, "notify_live_services": { - "doc": "notify.* услуги, извикани за актуализации на напредъка на живо, докато тече цикъл. Оставете празно, за да деактивирате известията за напредъка на живо.", + "doc": "Услуги notify.* за актуализации на напредъка на живо по време на цикъл. Оставете празно, за да ги изключите.", "label": "Услуги за напредък на живо" }, "notify_only_when_home": { - "doc": "Изпращайте известия само когато поне един от свързаните хора (по-горе) е у дома.", + "doc": "Известявайте само когато поне един от хората по-горе е вкъщи.", "label": "Известяване само когато е вкъщи" }, "notify_people": { - "doc": "person.* обекти, използвани от „Уведомяване само при вкъщи“, за да решат дали някой е у дома.", + "doc": "Обекти person.*, които проверява „Известяване само когато е вкъщи“.", "label": "Хора (само когато са вкъщи)" }, "notify_pre_complete_message": { - "doc": "Текст на предупреждението преди край / почти готово. Променливи на шаблона: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Текст на всяка актуализация на напредъка на живо. Променливи на шаблона: {device}, {program}, {minutes} (оставащи минути). При всяка друга променлива се използва текстът по подразбиране.", "label": "Съобщение преди завършване" }, "notify_reminder_message": { - "doc": "Текст на напомнянето за все още непразния уред. Празно използва вградената стойност по подразбиране. Променливи на шаблона: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Текст на еднократното Предупреждение преди края. Променливи на шаблона: {device}, {program}, {minutes} (настройката Предупреждение преди края). При всяка друга променлива се използва текстът по подразбиране.", "label": "Съобщение за напомняне" }, "notify_start_message": { - "doc": "Текст, изпращан при стартиране на цикъл. Променливи на шаблона: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Текст при стартиране на цикъл. Променливи на шаблона: {device}, {program}. При всяка друга променлива се използва текстът по подразбиране.", "label": "Съобщение при старт" }, "notify_start_services": { - "doc": "notify.* услуги, извиквани при започване на цикъл. Добавете по един за цел (телефон, табло и т.н.); оставете празно, за да няма известие за начало.", + "doc": "Услуги notify.*, извиквани при старт на цикъл (телефон, табло, ...). Оставете празно за никакви.", "label": "Услуги при старт" }, "notify_timeout_seconds": { - "doc": "Автоматично отхвърляне на известието след толкова секунди (на платформи, които го поддържат). 0 го запазва, докато не бъде отхвърлен ръчно.", + "doc": "Затворете известието след толкова секунди (където се поддържа). 0 го оставя, докато не го затворите.", "label": "Автоматично отхвърляне след" }, "notify_title": { - "doc": "Заглавие на известието. Променливи на шаблона: {device}, {program}, {duration}, {energy}, {cost}, {date}, {time}, {minutes}.", + "doc": "Заглавие на известието. Замества се само {device}; при всяка друга променлива се използва заглавието по подразбиране.", "label": "Заглавие на известие" }, "notify_unload_delay_minutes": { - "doc": "Минути след края на цикъла преди изпращане на все още чакащото напомняне за „разтоварване на машината“. Задайте 0, за да деактивирате напомнянето.", + "doc": "Минути след края на цикъла преди напомнянето „разтоварете машината“. 0 го изключва.", "label": "Закъснение на напомнянето за разтоварване" }, "notify_unload_repeat": { - "doc": "Продължава да изпраща напомнянето за разтоварване на всеки \"Закъснение на напомнянето за разтоварване\" минути, докато разтоварването не бъде потвърдено или не докоснете \"Спри напомнянията\" в известието. Необходим е начин за потвърждаване на разтоварването: Обект на сензор за врата, Обект за потвърждение на разтоварване или опцията \"Ръчно потвърждаване на разтоварването\" по-долу. Бутонът за отхвърляне работи в известията на мобилното приложение на Home Assistant (companion).", + "doc": "Изпращайте отново напомнянето за разтоварване на всеки „Закъснение на напомнянето за разтоварване“ минути, докато разтоварването не бъде потвърдено или не докоснете „Спри напомнянията“ (мобилно приложение). Нужен е Обект на сензор за врата, Обект за потвърждение на разтоварване или „Ръчно потвърждаване на разтоварването“.", "label": "Повтаряне до разтоварване" }, "off_delay": { - "doc": "Време за изчакване след падане на захранването, преди да се обяви цикълът за завършен. Ако захранването се възобнови в рамките на този прозорец, цикълът продължава безпроблемно - това свързва паузите между етапите на пране. Съдомиялните машини имат дълги фази на сушене (изключени за 20-60 минути), така че времето за изключване трябва да надвишава това, за да поддържа цялото измиване+сушене като един цикъл.", + "doc": "Колко дълго мощността трябва да остане ниска, преди цикълът да приключи. Ако мощността се върне в това време, цикълът продължава. Съдомиялните може да са неактивни 20-60 мин по време на сушене, затова задайте повече.", "label": "Закъснение при изключване" }, "pause_cuts_power": { - "doc": "Когато даден цикъл е поставен на пауза, изключете и обекта за превключване по-долу. Само за уреди, чийто щепсел може безопасно да се изреже по средата на цикъла.", + "doc": "При пауза на цикъла изключвайте и Обекта на превключвател по-долу. Само за уреди, които могат безопасно да загубят захранване по средата на цикъла.", "label": "Паузата изрязва и мощността (чрез превключвател)" }, "power_off_delay": { - "doc": "Колко дълго мощността трябва да остане под Праг за изключване на захранването, след като цикълът приключи, преди състоянието да се върне на „Изкл“. Използва се само когато Праг за изключване на захранването е над 0. Проверява се на фонов ритъм, така че ефективното закъснение се закръгля нагоре до следващия тик за изтичане на състоянието.", + "doc": "Колко дълго мощността след цикъла трябва да остане под Прага за изключване на захранването, преди състоянието да се върне на „Изключено“. Използва се само когато този праг е над 0. Проверява се периодично, затова реалното чакане може да е малко по-дълго.", "label": "Закъснение при изключване на захранването" }, "power_off_threshold_w": { - "doc": "Опционално откриване на изключено състояние въз основа на мощността. Когато е над 0, след като цикълът приключи и мощността остане под това ниво в продължение на Закъснение при изключване на захранването, машината се третира като изключена и състоянието се връща на „Изкл“. Оставете на 0, за да го деактивирате (по подразбиране). Задайте го над реалното ниво на изключено захранване и под Праг на спиране и под потреблението в режим на готовност на машината ви (завършила, но все още включена); ако не е под Праг на спиране, се игнорира. Когато е активирано, то замества Закъснение при нулиране на напредъка за връщане на „Изкл“, така че завършила машина остава на „Готово/Чисто“, докато действително не бъде изключена.", + "doc": "След цикъла мощност под тази стойност за Закъснението при изключване на захранването задава състояние „Изключено“ вместо Закъснението при нулиране на напредъка. Задайте над консумацията на изключения уред, но под готовността и Прага на спиране. 0 го изключва (по подразбиране).", "label": "Праг за изключване на захранването" }, "power_sensor": { - "doc": "Сензорният обект, отчитащ жива мощност във ватове за този уред (напр. sensor.washer_power). Откриването на всички цикли се основава на този сигнал.", + "doc": "Сензорът, който отчита текущата мощност на този уред във ватове (напр. sensor.washer_power). Цялото откриване се основава на него.", "label": "Сензор за мощност" }, - "profile_duration_tolerance": { - "doc": "+/- лентата около средната продължителност на профил, използвана по време на съвпадение. 0,25 означава, че профил от 60 минути съответства на 45-75 минути цикли.", - "label": "Толеранс на продължителност на профил" - }, "profile_match_interval": { - "doc": "Колко често да правите опити за съвпадение на профили по време на цикъл на работа. Стандартните 300 s (5 минути) балансират скоростта на откриване и процесора.", + "doc": "Колко често текущият цикъл се съпоставя с профилите. По подразбиране 300 с (5 мин).", "label": "Интервал на съвпадение" }, "profile_match_max_duration_ratio": { - "doc": "Максимална дължина на цикъла спрямо профила. 1.3 означава, че цикълът трябва да е под 130% от продължителността на профила, за да съответства.", + "doc": "Програмата отпада, когато цикълът продължи по-дълго от това кратно на обичайната ѝ продължителност. 1,8: цикъл с 80% по-дълъг от програмата вече не се съпоставя с нея.", "label": "Макс. съотношение на продължителност" }, "profile_match_min_duration_ratio": { - "doc": "Минимална дължина на цикъла спрямо профила. 0,9 означава, че цикълът трябва да бъде поне 90% от продължителността на профила, за да съответства.", + "doc": "Програмата се разглежда едва след като цикълът е изминал този дял от обичайната ѝ продължителност. 0,10: 2-часова програма става кандидат след 12 минути. Не важи в първите 15 минути на цикъла.", "label": "Мин. съотношение на продължителност" }, "profile_match_threshold": { - "doc": "Минимален резултат за сходство (0-1), необходим в края на цикъла, за да се приеме идентификация на програмата. Повишете го, за да намалите грешните идентификации; намалете го, ако програмите на вашата машина не се съпоставят. По подразбиране 0.4 е консервативна отправна точка.", + "doc": "Оценка на съвпадението на живо, която цикълът трябва да има, преди Интелигентното прекратяване или завършването против бръчки да могат да го спрат по-рано. Не променя показаната програма. Повишете, ако слаби съвпадения спират циклите рано; понижете, ако ясни съвпадения чакат закъснението при изключване.", "label": "Праг на съвпадение" }, "profile_unmatch_threshold": { - "doc": "Ако съвпадението на живо в средата на цикъла падне под този резултат, предварителното разпознаване се изчиства. Дръжте го малко под „Праг на съвпадение“, за да не може кратък спад в сходството да върне показанието обратно към „без съвпадение“.", + "doc": "Ако оценката на съвпадението на живо падне под тази стойност, текущото предположение за програмата се изчиства. Дръжте малко под Прага на съвпадение, за да не го нулира кратък спад.", "label": "Праг на несъвпадение" }, "progress_reset_delay": { - "doc": "След като приключите, задръжте напредъка на 100% за това време, така че Completed да се вижда на таблата за управление, преди да се върнете към неактивен.", + "doc": "След цикъла дръжте напредъка на 100% толкова време, преди да се нулира на „Празен ход“.", "label": "Закъснение при нулиране на напредъка" }, "pump_stuck_duration": { - "doc": "Секунди помпата може да работи непрекъснато, преди да бъде маркирана като евентуално блокирана (задейства събитието блокирана помпа).", + "doc": "Колко дълго помпата може да работи без прекъсване, преди да се задейства събитието за заседнала помпа.", "label": "Продължителност при заседнала помпа" }, "sampling_interval": { - "doc": "Очаквано време между показанията на сензора - използва се за оразмеряване на прозореца за изглаждане и правилно стартиране на отстраняването на отскок. Всяка актуализация на сензора се улавя независимо от тази стойност; той само калибрира изчисленията надолу по веригата. Механизмът за предложения измерва действителния каданс на вашия сензор от минали цикли и го задава автоматично.", + "doc": "Минимално време между обработваните стойности. Стойности, пристигнали по-рано, се пропускат (ниските стойности по време на цикъл винаги се пазят). Задайте интервала на отчитане на контакта или по-малко; по-висока стойност само изхвърля данни.", "label": "Интервал на вземане на проби" }, "save_debug_traces": { - "doc": "Съхранявайте пълното проследяване на мощността и съответстващите данни за отстраняване на грешки за всеки цикъл. Полезно за отстраняване на проблеми, но увеличава размера на хранилището.", + "doc": "Запазвайте с всеки цикъл и данните за отстраняване на грешки на съпоставянето (оценките на всички кандидати). Добавя около 20 KB на цикъл.", "label": "Запазване на следи за отстраняване на грешки" }, - "smoothing_window": { - "doc": "Колко е изгладен суровият сигнал за мощност. Ниска (2) е отзивчива, но шумна; високо (5) изглажда пиковете, но добавя забавяне.", - "label": "Прозорец за изглаждане" - }, "start_duration_threshold": { - "doc": "Мощността трябва да остане над стартовия праг толкова дълго, за да се потвърди истински старт, предотвратявайки превключвателите за включване/изключване за част от секундата да започнат цикъл.", + "doc": "Мощността трябва да остане толкова време над Прага на стартиране, преди цикълът да започне, за да се игнорират кратките включвания и изключвания.", "label": "Продължителност на стартиране" }, "start_energy_threshold": { - "doc": "Енергия (мощност х време), която уредът трябва да консумира, преди да ПУСКА. Кратък пик с висока мощност има много ниска енергия и се игнорира, предотвратявайки фалшиви стартове.", + "doc": "Енергия, която уредът трябва да изразходва, преди цикълът да се брои за работещ. Кратък скок на мощността изразходва малко енергия, затова не стартира цикъл.", "label": "Начална енергия" }, "start_threshold_w": { - "doc": "Мощността трябва да се повиши над това ниво, за да се потвърди, че цикълът е започнал. Задаването му на твърде ниско ниво причинява фалшиви стартове от мощността в режим на готовност; пропускат се прекалено високи и бавно стартиращи програми (студено пълнене). Механизмът за предложения задава това точно над наблюдаваната най-ниска активна мощност на машината.", + "doc": "Мощността трябва да се покачи над тази стойност, за да започне цикъл. Твърде ниско: консумацията в готовност води до фалшиви стартове. Твърде високо: бавно стартиращи програми (студено пълнене) се пропускат.", "label": "Праг на стартиране" }, "stop_threshold_w": { - "doc": "Мощността трябва да падне под това ниво, преди да започне отброяването на времето за изключване. Задайте го под началния праг - празнината между тях е хистерезисната лента, която предотвратява трептенето. Ако са зададени твърде високо, фазите с ниска мощност (задържане на изплакване, против намачкване) погрешно задействат крайната последователност.", + "doc": "Мощността трябва да падне под тази стойност, преди да започне отброяването на закъснението при изключване. Дръжте под Прага на стартиране. Ако е твърде високо, фазите с ниска мощност (паузи при изплакване, против бръчки) по грешка пускат отброяването на края.", "label": "Праг на спиране" }, "switch_entity": { - "doc": "Допълнителен превключвател, който се изключва при пауза и се включва отново при възобновяване, използва се заедно с „Паузата също прекъсва захранването“.", + "doc": "Незадължителен превключвател, който се изключва при пауза и се включва отново при възобновяване, заедно с „Паузата изрязва и мощността“.", "label": "Обект на превключвател" }, "watchdog_interval": { - "doc": "Колко често фоновият наблюдател проверява за блокирали сензори и изминали времена на изчакване. По подразбиране 30 s.", + "doc": "Колко често фоновата проверка търси блокирали сензори и изтекли таймаути. По подразбиране 30 с.", "label": "Интервал на наблюдател" }, "notify_milestone_message": { @@ -1522,135 +1483,90 @@ "label": "Съобщение за етап" }, "notify_milestones": { - "doc": "Разделени със запетая бройки цикли, които при достигане задействат еднократно поздравително известие (напр. 50, 100, 500, 1000). Празно изключва известията за етапи.", + "doc": "Брой цикли, при които идва еднократно поздравително известие, разделени със запетаи (напр. 50, 100, 500, 1000). Празно ги изключва.", "label": "Етапи на циклите" }, "notify_quiet_end_hour": { - "doc": "Край на прозореца „не безпокойте“ (0-23). Задържаните известия се доставят в този час. Оставете празно, за да изключите.", + "doc": "Край на прозореца „не безпокойте“ (0-23); задържаните известия се изпращат тогава. Празно го изключва.", "label": "Край на тихите часове" }, "notify_quiet_start_hour": { - "doc": "Начало на прозорец „не безпокойте“ (0-23). Известията за завършване, напомнянията и известията за чисто пране, които биха се задействали през тихите часове, се задържат и доставят при края на прозореца. Оставете празно, за да изключите. Поддържа прозорци, преминаващи през полунощ (напр. начало 22, край 7).", + "doc": "Начало на прозореца „не безпокойте“ (0-23). Известията за завършване, напомнянията и известията за чисто пране в този прозорец се задържат до края му. Може да минава през полунощ (напр. от 22 до 7). Празно го изключва.", "label": "Начало на тихите часове" }, "peak_rate_message": { - "doc": "Незадължителен персонализиран текст за съвета за пикова тарифа, добавен към известието при старт. Променливи на шаблона: {device}, {price}. Празно използва вградената стойност по подразбиране.", + "doc": "Собствен текст на съвета за пикова тарифа. Променливи на шаблона: {device}, {price}. Празно: текстът по подразбиране.", "label": "Съобщение за пикова тарифа" }, "peak_rate_threshold": { - "doc": "Когато цикъл започне и текущата цена за kWh е на или над тази стойност, добавя съвет за пикова тарифа към известието при старт. 0 или празно изключва съвета.", + "doc": "Ако цената за kWh при старта на цикъла е равна на тази стойност или по-висока, добавете съвет за пикова тарифа в известието за старт. 0 или празно го изключва.", "label": "Праг на пикова тарифа (за kWh)" }, - "dtw_bandwidth": { - "label": "Толеранс на съпоставянето по форма", - "doc": "Колко модулът за съпоставяне може да разтегне цикъла във времето, за да го напасне към формата на запаметена програма. По-висока стойност допуска повече вариации в скоростта; твърде висока рискува да съпостави грешната програма." - }, - "corr_weight": { - "label": "Тегло форма спрямо ниво", - "doc": "Баланс между формата на кривата и абсолютното ниво на мощността при оценяване на съпоставянето. По-високата стойност предпочита формата; по-ниската предпочита съвпадението на нивото на мощността." - }, - "duration_weight": { - "label": "Тегло на продължителността", - "doc": "Колко силно се предпочита програма, чиято типична продължителност съвпада с този цикъл." - }, - "energy_weight": { - "label": "Тегло на енергията", - "doc": "Колко силно се предпочита програма, чието типично потребление на енергия съвпада с този цикъл." - }, "store_brand": { "label": "Марка на уреда", - "doc": "По желание. Марката на уреда, избрана от каталога на общността. Използва се за намиране и споделяне на подходящи еталонни записи. Оставете празно, ако не използвате онлайн функциите." + "doc": "Незадължително. Марката на уреда ви от каталога на общността, използва се за намиране и споделяне на еталонни записи. Оставете празно, ако не ползвате онлайн функции." }, "store_model": { "label": "Модел на уреда", - "doc": "По желание. Моделът на уреда, избран от каталога на общността, след като е зададена марка. Ако вашият модел не е в списъка, можете да го добавите в каталога." + "doc": "Незадължително. Моделът на уреда ви от каталога на общността (първо изберете марка). Ако го няма в списъка, можете да го добавите." }, "show_contributor": { "doc": "Покажи името на сътрудника в профилите, изтеглени от магазина на общността" }, "smart_termination_duration_ratio": { - "doc": "Колко навътре в очакваната продължителност на съпоставената програма трябва да е навлязъл цикълът, преди Интелигентното прекратяване да може да го завърши предсрочно при спадане на мощността. Очакваната продължителност е средната за програмата, затова при уреди със силно променливо време на работа - перални при студена зимна и топла лятна входяща вода, сушилни с датчик за влажност, програми, зависещи от натоварването - около половината от всички пускания завършват по-кратко от тази средна стойност и никога не получават бързото завършване, приключвайки само чрез резервния таймаут с минути закъснение. Намалете тази стойност (напр. 0.85) при такива машини, за да се задейства все пак ранното завършване; повишете я към 1.0 за по-предпазливо поведение. Оставете празно за стойността по подразбиране (0.98 или 0.99 за съдомиялни). Може само да завърши цикъл по-рано, никога по-късно, и никога не се задейства при нееднозначно или несигурно съпоставяне.", + "doc": "Дял от средната продължителност на програмата, който цикълът трябва да достигне, преди Интелигентното прекратяване да може да го спре по-рано. Ако времената варират (температура на водата, сушене по сензор, количество пране), намалете го (напр. 0,85). Оставете празно за 0,98 (0,99 при съдомиялни).", "label": "Коефициент на интелигентно прекратяване" }, - "enable_phase_matching": { - "label": "Оставащо време съобразено с фазите", - "doc": "Разделя всеки активен цикъл на фази (нагряване, пране, центрофугиране) и разпределя оставащото време по фази, съчетано с класическата оценка - като разчита на разпределението по фази в началото на цикъла и на класическата оценка към края. Това персонализира обратното броене спрямо това колко всъщност загрява и работи вашата машина, което е най-осезаемо през първата половина на цикъла. Изключено = само класическата оценка. Засяга се само показването на оставащото време; съпоставянето на програми и откриването на цикли остават непроменени." - }, - "keep_min_score": { - "label": "Мин. резултат на съвпадение", - "doc": "Минимален резултат на сходство, необходим на кандидата, за да остане в надпреварата. Стойността по подразбиране 0,1 е умишлено щедра - допуска дори слаби кандидати и разчита на по-късните етапи за намиране на най-доброто съвпадение. Увеличете за по-ранно отхвърляне на малко вероятни профили; намалете за разширяване на началния набор от кандидати." - }, - "dtw_blend": { - "label": "Смесване на деформация", - "doc": "Колко DTW резултатът от изравняването замества основния резултат от Етап 2. 0 = използвай само резултата от Етап 2, 1 = използвай само DTW резултата, 0,5 (по подразбиране) = равно смесване. Увеличете за по-голямо разчитане на warp изравняване, когато програмите имат подобни нива на мощност, но различни времеви модели." - }, - "dtw_ensemble_w": { - "label": "Микс на комбинация DTW", - "doc": "В режим на ансамбъл DTW (по подразбиране), тежест на мащабиран L1 срещу производен DTW (DDTW). 1,0 = само мащабиран L1, 0 = само DDTW, 0,7 = по подразбиране. Мащабираният вариант L1 сравнява нивата на мощност; производният вариант реагира на промените на мощността във времето. Ансамблът обединява и двете." - }, - "dtw_ddtw_scale": { - "label": "Скала на производна деформация", - "doc": "Разстояние на полунасищане за оценяване с производен DTW. Резултатът се намалява наполовина, когато разстоянието DDTW е равно на тази стойност. По-малко = по-чувствително към разлики в формата. Стойността по подразбиране 30 е калибрирана за типични следи на мощност на домакински уреди. Засяга само режимите ensemble и ddtw на DTW." - }, - "dtw_refine_top_n": { - "label": "Брой прецизирани кандидати", - "doc": "Колко от водещите кандидати от Етап 2 DTW преоценява. DTW е по-скъп, затова се прилага само за N-те най-добри кандидати. По подразбиране 5. Увеличете (до 7-9), ако правилният профил понякога достига само 4-то или 5-то място след Етап 2 - DTW може да го спаси. Намаляването леко ускорява съпоставянето." - }, - "duration_scale": { - "label": "Острота на продължителност", - "doc": "Острота на санкцията за несъответствие на продължителността в Етап 4. Това е лог-съотношение, при което резултатът на съответствие се намалява наполовина. По-малко = по-строго: несъответствието на продължителността снижава резултата повече. Стойността по подразбиране 0,175 съответства на толеранс на продължителността от около 18% при половинна тежест. Комбинирайте с Тежестта на продължителността." - }, - "energy_scale": { - "label": "Острота на енергия", - "doc": "Острота на санкцията за несъответствие на енергията в Етап 4. По-малко = по-строго: несъответствието на енергията снижава резултата повече. Стойността по подразбиране 0,25 е умишлено по-щедра от Остротата на продължителността, тъй като енергията варира с натоварването. Комбинирайте с Тежестта на енергията." - }, "power_profile_interval_min": { "label": "Интервал на профила на мощността", - "doc": "Размер на кофата за атрибута на сензора power_profile за всеки профил (плоски средни стойности в Вт за слот, използвани от външни планировчици като EMHASS и tibber_prices). По-малките кофи запазват кратките пикове на мощността остри; по-голямата кофа изглажда формата. По подразбиране 15 мин. Само за четене; не засяга откриването." + "doc": "Размер на интервала на атрибута power_profile (средни ватове на слот), използван от планировчици като EMHASS и tibber_prices. По-малкият пази кратките пикове; по-големият изглажда кривата. По подразбиране 15 мин. Не влияе на откриването." }, "notify_live_silent": { "label": "Тихи актуализации на живо", - "doc": "Само за iOS. Опреснявайте напредъка на живо безшумно: всяка актуализация идва без звук и вибрация, с по-нисък приоритет, който пести батерия и който iOS може за кратко да групира. Първата актуализация в цикъла (тази, която стартира Live Activity), както и известията за начало и край, продължават да сигнализират както обикновено. Изключете това, за да получавате сигнал при всяка актуализация." + "doc": "Само iOS. Обновявайте напредъка на живо безшумно, с пестящ батерията приоритет, който iOS може да групира. Първата актуализация и известията за старт и завършване все още звучат. Изключено: всяка актуализация звучи." }, "notify_live_sticky": { "label": "Запазване на живото известие при докосване", - "doc": "Само за Android. Направете известието за ход на живо постоянно (закачено), за да не го откажете при докосване. Изключено запазва поведението по подразбиране, при което докосването го откажва." + "doc": "Само Android. Известието за напредък на живо остава след докосване. Изключено: докосването го затваря." }, "notify_live_click_action": { "label": "Цел при докосване на известието", - "doc": "Накъде отваря докосването на известие от този уред, на Android и iOS. Празно отваря панела WashData за този уред. Въведете път към табло (напр. /lovelace/laundry) или пълен URL адрес, за да го насочите другаде, или „none“ за никаква цел при докосване." + "doc": "Какво отваря докосването на известието на този уред. Празно: панелът на WashData за този уред. Въведете път до табло (напр. /lovelace/laundry) или URL, или „none“, за да не отваря нищо." }, "door_opens_at_end": { "label": "Вратата се отваря автоматично в края", - "doc": "За съдомиялни, при които вратата се отваря в края на цикъла за сушене (AirDry и подобни). При включено, отварянето на вратата по време на работещ цикъл вече не го поставя на пауза завинаги; вместо това, ако вратата остане отворена за времето за изчакване по-долу, WashData третира цикъла като завършен. Краткото отваряне (добавяне на артикул) се игнорира. Изисква се обект на сензор за врата." + "doc": "За съдомиялни, които отварят вратата в края, за да сушат (напр. AirDry). Врата, отворена за времето по-долу, приключва цикъла, вместо да го спира на пауза; кратко отваряне се игнорира. Нужен е Обект на сензор за врата." }, "door_end_dwell_seconds": { "label": "Изчакване при отворена врата", - "doc": "Колко дълго трябва вратата да остане отворена, преди WashData да приключи цикъла, когато \"Вратата се отваря автоматично в края\" е включено. Достатъчно дълго, за да игнорира бързото добавяне на съд (по подразбиране 60 с), достатъчно кратко, за да приключи бързо, когато машината отвори вратата." + "doc": "При включено „Вратата се отваря автоматично в края“: колко дълго вратата трябва да остане отворена, преди цикълът да приключи. Достатъчно дълго, за да се игнорира добавянето на чиния (по подразбиране 60 с)." }, "profile_evidence_sources": { "label": "Цикли, които оформят програмата", - "doc": "Кои цикли се използват за изграждане на кривата на мощността на всяка програма и за съпоставяне на завършен цикъл с нея. Ако махнете отметката на даден вид, той спира да оформя програмите ви, без нищо да се изтрива - циклите остават в списъка Цикли и все още могат да бъдат маркирани или премахнати. Полезно, ако не се доверявате на импортираните данни. Статистиката не се влияе: в нея винаги се броят само циклите, които тази машина наистина е изпълнила. Махането на всички отметки се игнорира, защото програма без нито един цикъл никога не би могла да съвпадне." + "doc": "Кои видове цикли оформят кривата и съпоставянето на всяка програма. Махането на отметка не изтрива нищо; полезно, ако не вярвате на импортирани данни. Статистиката винаги брои само циклите на тази машина. Махането на всички отметки се игнорира." }, "energy_price_dynamic": { "label": "Цена, претеглена по време", - "doc": "Таксува всеки цикъл по цената, валидна във всеки момент от работата му, вместо по единствената цена в момента на приключването му. Работи само с обект за цена (статичната цена не се променя). Не изисква допълнителна настройка: цената се следи на живо, докато цикълът върви, а „Обработка на историята“ в Диагностика преизчислява разходите на по-старите цикли от recorder. Изключено = класичното поведение, една цена, фиксирана накрая." + "doc": "Таксувайте всеки цикъл по цената във всеки момент от работата му, а не по цената при завършване. Нужен е обект за цена. „Обработка на историята“ (Диагностика) преизчислява по-старите цикли." }, "curve_preroll_seconds": { - "doc": "Колко секунди отчитания от прекъснати опити за стартиране WashData може да пренесе в началото на кривата на цикъла. Кривата на цикъла обикновено започва от опита за стартиране, който в крайна сметка е успял, затова при машина, която опитва няколко пъти, преди да потегли - избор на програма, заключване на вратата, първо пълнене - в началото на всяка крива липсват първите минути реална работа. Задайте стойност малко над най-дългия такъв разбег на вашата машина (300 с стигат с излишък за повечето). 0 изключва функцията. Имайте предвид, че включването ѝ измества записаното начало по-рано, така че циклите, записани преди и след промяната, носят различна продължителност за една и съща програма; научените средни стойности ще се изместват, докато по-старите цикли не отпаднат от историята.", + "doc": "Добавя в началото на цикъла стойностите от неуспешни опити за старт, за машини, които пробват преди старта (заключване на вратата, първо пълнене). 300 с стигат за повечето; 0 = изключено. Записаните начала се изместват по-рано, затова научените продължителности се променят известно време.", "label": "Предзапис на кривата" }, "unload_confirm_entity": { "label": "Обект за потвърждение на разтоварване", - "doc": "Незадължителен обект, чието активиране означава, че съдържанието е извадено, за машини, които не могат да имат сензор на вратата. Работи всякакъв вид: бутон Zigbee или NFC, помощник input_button, сензор за движение пред машината, сцена. Всяка промяна на състоянието се брои за потвърждение, с изключение на изключване и преминаване в неналично състояние. Задаването на това включва напомнянето за разтоварване и на устройство без Обект на сензор за врата." + "doc": "Незадължително. Всеки обект, който потвърждава, че прането е извадено, за машини без сензор за врата (бутон, input_button, сензор за движение, сцена). Брои се всяка промяна на състоянието, освен изключване и недостъпност. Включва и напомнянето за разтоварване." }, "unload_track_without_door": { "label": "Ръчно потвърждаване на разтоварването", - "doc": "Изпълнявайте напомнянето за разтоварване на устройство без Обект на сензор за врата и без Обект за потвърждение на разтоварване, а разтоварването потвърждавайте от собствена автоматизация: натиснете бутонния обект \"Маркирай като разтоварено\" или извикайте услугата ha_washdata.mark_unloaded. Игнорира се, когато е зададен Обект на сензор за врата, който вече дава този сигнал." + "doc": "Пускайте напомнянето за разтоварване без сензор за врата или обект за потвърждение. Потвърждавайте разтоварването от своя автоматизация с бутона „Маркирай като разтоварено“ или услугата ha_washdata.mark_unloaded. Игнорира се, когато е зададен Обект на сензор за врата." }, "notify_icon_color": { "label": "Цвят на известие", - "doc": "Акцентен цвят за известията на този уред, за да се различават пералнята, сушилнята и съдомиялната машина от пръв поглед. iOS оцветява иконата и лентата за напредък на Live Activity на заключения екран и в Dynamic Island. Android получава същия цвят, но какво прави с него зависи от телефона: някои оцветяват иконата, а много от тях (особено Samsung One UI) го пренебрегват напълно. И двете платформи разбират Hex (напр. #03A9F4). Оставете празно за цвета по подразбиране на платформата." + "doc": "Акцентен цвят на известията на този уред, за да се различават лесно пералня, сушилня и съдомиялна. На iOS запълва кръга зад иконата и оцветява лентата на Live Activity. Много телефони с Android (особено Samsung) го игнорират. Използвайте hex, напр. #03A9F4. Празно: цветът на платформата по подразбиране." + }, + "share_name": { + "doc": "Показвайте името на акаунта си като автор на циклите, които споделяте. Изключено: споделя се без име." } }, "setting_group": { @@ -1678,9 +1594,6 @@ "door_pause": { "label": "Врата и пауза" }, - "dtw_refinement": { - "label": "DTW прецизиране" - }, "duration_gates": { "label": "Врати на продължителност" }, @@ -1757,13 +1670,18 @@ "rebuilding": "Повторно изграждане…", "analyzing": "Анализиране…", "resetting": "Нулиране…", - "reverting": "Връщане…", "trimming": "Изрязване…", "splitting": "Разделяне…", + "stalled": "Заседнал", "deleting": "Изтриване…", "imported": "Импортирано", "preparing": "Подготовка…", - "unlabelling": "Премахване на етикета…" + "unlabelling": "Премахване на етикета…", + "working": "Обработва се…", + "match_uncertain": "Несигурно: {top} или {runner}", + "match_uncertain_single": "Несигурно: може би {top}", + "match_sure": "~{pct}% сигурност", + "match_uncertain_tip": "WashData още не е избрал програма. Числото показва колко често водещото предположение, {top}, се е оказвало вярно в минали цикли при такава преднина. Само за информация." }, "tab": { "advanced": "Разширено", @@ -1794,13 +1712,17 @@ }, "toast": { "catalog_refreshed": "Каталогът на общността е опреснен", + "task_name_required": "Дайте име на задачата", + "task_added": "Задачата е добавена", + "task_save_failed": "Задачата не можа да бъде запазена: {error}", + "task_removed": "Задачата е премахната", + "maint_logged": "Отбелязано като изпълнено: {task}", "access_saved": "Контролът на достъпа е запазен", "all_wiped": "Всички данни са изтрити", "analysis_complete_none": "Анализът е завършен: няма нови предложения", "auto_label_complete": "Автоматичното етикетиране е завършено", "automation_deleted": "Автоматизацията е изтрита", "correction_submitted": "Корекцията е изпратена", - "cycle_deleted": "Цикълът е изтрит", "cycle_force_stopped": "Цикълът е принудително спрян", "cycle_labelled": "Цикъл с етикет", "cycle_paused": "Цикълът на пауза", @@ -1816,11 +1738,9 @@ "group_deleted": "Групата е изтрита", "group_name_required": "Името на групата е задължително", "group_saved": "Групата е запазена", - "import_successful": "Импортирането е успешно; презареждане на интеграцията", "json_required": "Необходими са JSON данни", "legacy_removed": "Наследените действия са премахнати", "logs_exported": "Изнесени трупи", - "matching_reverted": "Съответстващите тегла са върнати към стойностите по подразбиране", "min_2_profiles": "Изберете поне 2 профила за група", "ml_training_no_improvement": "Обучението е завършено: изходното ниво е запазено (без подобрение)", "models_reverted": "Моделите на устройството са върнати към базовата линия", @@ -1830,8 +1750,8 @@ "pg_preset_loaded": "Конфигурацията „{name}“ е заредена", "pg_preset_saved": "Конфигурацията „{name}“ е запазена", "pg_sugg_loaded": "Заредени {n} предложени стойности - стартирайте тестовата среда за сравнение", - "pg_sugg_ml_loaded": "Заредени {n} ML-калибрирани стойности - стартирайте тестовата среда за сравнение", "phase_name_required": "Изисква се име на фаза", + "phase_reset": "Фазата „{name}“ е върната към вградената версия", "phase_updated": "Фазата е актуализирана", "phases_saved": "Фазите са запазени", "preferences_saved": "Предпочитанията са запазени", @@ -1878,15 +1798,11 @@ "store_name_required": "Въведете име на профил", "store_import_failed": "Импортирането е неуспешно: {error}", "store_imported": "Импортирано в {profile}", - "store_program_required": "Въведете име на програма", "store_no_appliance": "Първо задайте марката и модела на уреда в настройките.", "store_share_failed": "Споделянето е неуспешно: {error}", "store_shared": "Споделено в магазина на общността, изчаква преглед.", "contrib_device_changed": "Добавено в магазина на общността, но избраният уред се смени, докато формулярът беше отворен. Изберете го в настройките.", "appliance_added": "Уредът е добавен, изчаква одобрение", - "thanks_confirming": "Благодарим за потвърждението", - "device_approved": "Одобрено от общността", - "rating_saved": "Оценката за качество е запазена", "brand_added": "Марката е добавена, изчаква одобрение", "profile_added": "Профилът е добавен, изчаква одобрение", "saved_except_conflicts": "Запазено. Поправете маркираните конфликти, за да запазите останалото.", @@ -1909,11 +1825,50 @@ "odometer_saved": "Общият брой цикли е запазен", "odometer_save_failed": "Общият брой не може да бъде запазен: {error}", "store_pick_profile_share": "Изберете профил, в който да споделите", - "remove_failed": "Премахването е неуспешно: {error}" + "remove_failed": "Премахването е неуспешно: {error}", + "analysis_complete": "Анализът е завършен: предложения: {count}", + "analysis_failed": "Анализът е неуспешен: {error}", + "auto_detect_failed": "Автоматичното откриване е неуспешно: {error}", + "auto_label_failed": "Автоматичното етикетиране е неуспешно: {error}", + "could_not_load_cycle": "Цикълът не можа да бъде зареден: {error}", + "create_failed": "Създаването е неуспешно: {error}", + "cycles_deleted": "Изтрити цикли: {count}", + "debug_cleared": "Изчистени следи за отстраняване на грешки: {count}", + "discard_failed": "Отхвърлянето е неуспешно: {error}", + "error": "Грешка: {error}", + "export_failed": "Експортирането е неуспешно: {error}", + "import_failed": "Импортирането е неуспешно: {error}", + "invalid_json": "„{key}“ не е валиден JSON - поправете го или изчистете полето преди запазване.", + "label_failed": "Етикетирането е неуспешно: {error}", + "ml_training_promoted": "Обучението е завършено: активирани модели: {count}", + "pause_failed": "Паузирането е неуспешно: {error}", + "phase_created": "Фазата „{name}“ е създадена", + "phase_deleted": "Фазата „{name}“ е изтрита", + "profile_created": "Профилът „{name}“ е създаден", + "rename_failed": "Преименуването е неуспешно: {error}", + "save_failed": "Запазването е неуспешно: {error}", + "split_complete": "Разделено на цикли: {count}", + "start_failed": "Стартирането е неуспешно: {error}", + "stop_failed": "Спирането е неуспешно: {error}", + "update_failed": "Актуализирането е неуспешно: {error}", + "store_download_skipped": "Пропуснати записи: {n} (твърде кратки, с твърде много пропуски или вече са на вашето устройство).", + "store_import_low_quality": "Този запис е твърде кратък, с твърде много пропуски или неправдоподобен, за да бъде импортиран.", + "store_import_duplicate": "Този запис вече е на вашето устройство.", + "device_approved": "Одобрено от общността", + "thanks_confirming": "Благодарим за потвърждението", + "rating_saved": "Оценката за качество е запазена", + "store_download_partial": "Неуспешно изтеглени програми: {n}; опитайте отново по-късно.", + "import_no_restore_point": "Импортирането е извършено, но не можа да се запише точка за възстановяване, така че то не може да бъде отменено", + "import_undo_failed": "Импортирането не можа да бъде отменено: {error}", + "import_undone": "Импортирането е отменено: възстановени са {profiles} профил(а) и {cycles} цикъл(а)", + "auto_label_cancelled": "Автоматичното етикетиране е отменено след етикетиране на {count} цикъла.", + "auto_label_cancelled_one": "Автоматичното етикетиране е отменено след етикетиране на {count} цикъл.", + "auto_label_cancelled_other": "Автоматичното етикетиране е отменено след етикетиране на {count} цикъла.", + "auto_label_done": "Автоматичното етикетиране е завършено: етикетирани са {count} цикъла, без етикет остават: {skipped}.", + "auto_label_done_one": "Автоматичното етикетиране е завършено: етикетиран е {count} цикъл, без етикет остават: {skipped}.", + "auto_label_done_other": "Автоматичното етикетиране е завършено: етикетирани са {count} цикъла, без етикет остават: {skipped}." }, "suggestion": { - "both_agree": "WashData препоръчва", - "calibrated_label": "Калибрирано", "observed_label": "Наблюдавано", "impact": { "auto_label_confidence": { @@ -1928,10 +1883,6 @@ "higher": "Затваря само цикли, използвали значителна енергия", "lower": "Затваря и по-кратки или по-малко енергийни цикли" }, - "end_repeat_count": { - "higher": "Изисква сигналът за край да се повтори повече пъти – по-надеждно, малко по-бавно", - "lower": "Завършва след по-малко повторения – по-бързо откриване, по-висок риск от фалшиво завършване" - }, "learning_confidence": { "higher": "Учи само от много уверени съвпадения – по-бавно, но по-надеждно", "lower": "Учи от повече цикли; моделът може да е по-зашумен" @@ -1970,40 +1921,25 @@ } }, "reason": { - "auto_label_confidence": "Достоверност на 15-ия персентил за {cycles} автоматични етикета, които потребителят никога не е коригирал ({p15}).", "completion_min_seconds": "Половината от продължителността на чист цикъл p05 ({minutes} min) за {cycles} цикъла; филтрира фалшивите цикли.{excl}", - "dead_zone_batch": "Задържано кратко (медиана на прозореца на нестабилност при стартиране за {cycles} цикъла = {s}s), за да се разпознае бързо истинският край.", - "dead_zone_from_dip": "Въз основа на ранен спад на мощността, открит при {s}s.", - "end_energy_default": "Препоръчана стойност по подразбиране за прага на шума в края на цикъла.", - "end_energy_false": "Енергия при фалшив край p95 ({p95}Wh) за {cycles} цикъла, с долна граница 0,2 % от медианната енергия на цикъла ({floor}Wh).", - "end_energy_no_false": "Няма фалшиви краища за {cycles} цикъла; с долна граница 0,2 % от медианната енергия на цикъла ({floor}Wh).", - "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), така че минимумът не добавя натоварване.", + "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": "Задържано възможно най-ниско, за да се разпознае програмата рано в цикъла; праговете на достоверност и нееднозначност предотвратяват преждевременни потвърждения.", "min_off_gap": "Въз основа на {gaps} наблюдавани интервала между циклите (p05={p05}s). Долна граница на уреда: {floor}s.", "min_off_gap_bridge": "Оразмерено да покрие най-дългия тих участък в рамките на цикъла: {span}s + 60s буфер, от {spans} покрити интервала за {cycles} чисти цикъла (остава под вашия най-кратък интервал между две последователни стартирания, който е {ceiling}s).", "min_power": "40 % от най-ниската работна мощност p05 ({p05}W) за {cycles} чисти цикъла, за да остане прагът на изключване под реалната консумация.{excl}", - "ml_auto_label": "Най-ниската достоверност, която моделът за качество все още е оценил като чиста (p10 от {cycles} чисти цикъла = {p10}).", - "ml_end_repeat": "{false}/{total} цикъла ({pct}%) са имали пауза, оценена от детектора за край над 50 %; изисквай {val} потвърждение(я) за край.", - "ml_off_delay": "Паузи, потвърдени от детектора за край: p95 {p95}s + 60s буфер, от {pauses} проверени от модела паузи за {cycles} цикъла (долна граница {floor}s).", "no_update_timeout": "Въз основа на наблюдаваната честота на обновяване (p95={p95}s) * 20 (мин. 60s).", "off_delay_cadence": "Въз основа на наблюдаваната честота на обновяване (p95={p95}s) * 5", "off_delay_device_floor": "Използван е безопасен минимум, специфичен за уреда {device} ({floor}s).", "off_delay_generic_floor": "Използван е общ безопасен минимум ({floor}s).", "off_delay_pauses": "Оразмерено да надживее реалните паузи: пауза в рамките на цикъла p95 {p95}s + 60s буфер, от {pauses} паузи за {cycles} чисти цикъла (долна граница {floor}s).", - "profile_match_threshold": "Достоверност p10 на {cycles} правилни автоматични етикета ({p10}); безопасна долна граница за потвърждение в реално време.", - "sampling_interval": "Медиана на интервала на обновяване, наблюдавана за {cycles} чисти цикъла ({si}s).{excl}", - "smoothing_window": "Оразмерено да изглажда ~30s отчитания при семплиране от {si}s ({samples} проби).", - "start_duration": "Задържано кратко (~един интервал на семплиране от {si}s), за да започне разпознаването възможно най-рано, като все пак се игнорират единични скокове.", - "thr_batch": "Задържано точно над най-ниската работна мощност p05 за {cycles} цикъла ({p05}W), за да се улови стартът възможно най-рано и прагът на спиране да остане под най-ниската работна мощност на машината.", - "tol_per_profile": "p75 на дисперсията на продължителността по профил за {profiles} профила ({cycles} цикъла); стриктните профили не се санкционират.", - "tol_pooled": "Въз основа на обединената дисперсия на продължителността на {cycles} скорошни етикетирани цикъла (p95 отклонение={dev}).", - "watchdog": "Задържано възможно най-ниско при запазена безопасност (малко над интервала на обновяване p95 от {p95}s и поне 2x интервала на вземане на проби от {median}s, мин. 30s), за да се улавят бързо блокиранията без фалшиви спирания." + "standby_floor": "Всеки скорошен цикъл е консумирал {idle}W при завършване, затова праг под тази стойност никога не може да засече изключването на уреда. Задържан е над нея; записаните {cycles} чисти цикъла в историята не са спадали под него за повече от {pause}с.", + "thr_batch": "Малко над най-ниската работна мощност (p05) за {cycles} цикъла ({p05}W), за да се хващат стартовете рано, а прагът на спиране да остане под най-ниската работна мощност.", + "watchdog": "Задържано възможно най-ниско при запазена безопасност (малко над интервала на обновяване p95 от {p95}s и поне 2x интервала на вземане на проби от {median}s, мин. 30s), за да се улавят бързо блокиранията без фалшиви спирания.", + "end_energy_floor": "Повишено до минимума, който следва от прага на спиране ({stop} Вт) за времето на закъснението при изключване ({off_delay} с); под него цикълът може да завърши само по резервен път.", + "max_duration_ratio_default": "Върнато към стойността по подразбиране: по-нисък таван отхвърля правилната програма всеки път, когато цикълът е по-дълъг от обикновено.", + "min_duration_ratio_default": "Върнато към стойността по подразбиране: по-висок долен праг пречи текущият цикъл да бъде разпознат, докато не е напреднал значително." }, "exclusions": { "summary": "Изключени са {total} грешно открити цикъла: {parts}.", @@ -2021,7 +1957,8 @@ "no_power": "без мощност", "no_active_power": "без активна мощност" } - } + }, + "now_value": "(сега {v})" }, "trend": { "down": "Тенденция надолу", @@ -2029,6 +1966,10 @@ }, "maint": { "bearing_service": "Сервиз на лагерите", + "salt": "Допълване на сол", + "rinse_aid": "Допълване на препарат за изплакване", + "lint_filter": "Почистване на филтъра за мъх", + "condenser_clean": "Почистване на кондензатора", "descale": "Премахване на котлен камък", "drum_clean": "Почистване на барабана", "filter_clean": "Почистване на филтъра", @@ -2036,34 +1977,22 @@ }, "pg_desc": { "dishwasher_end_spike_quiet_release": "Съдомиялна машина: секунди неактивност след очакваната продължителност, преди да се освободи изчакването за източване в края на цикъла", - "smart_termination_duration_ratio": "Дял от очакваната продължителност на съпоставената програма, който цикълът трябва да достигне, преди Интелигентното прекратяване да може да го завърши предсрочно; намалете го за машини, зависещи от натоварването или температурата", + "smart_termination_duration_ratio": "Дял от средната продължителност на програмата, който цикълът трябва да достигне, преди Интелигентното прекратяване да може да го спре по-рано; намалете, ако времената варират", "completion_min_seconds": "Най-краткото изпълнение, което се счита за истински цикъл", - "end_repeat_count": "Ниски отчитания поред преди приключване", "interrupted_min_seconds": "Кратки цикли се отбелязват като прекъснати", "min_off_gap": "Интервал, необходим за разделяне на два цикъла", "off_delay": "Секунди ниска мощност преди край на цикъла", "start_duration_threshold": "Секунди над прага за потвърждаване на старта", "start_threshold_w": "Минимални ватове, за да се счита за стартирал", "stop_threshold_w": "Под тази стойност машината се счита за изключена", - "anti_crease_finalize_ratio": "Дял от очакваната продължителност на съпоставената програма, който цикълът трябва да достигне, преди опашката от въртене против бръчки да може да бъде завършена; намалете го при сушилни с дълга опашка от въртене, при перални оставете стойността по подразбиране", + "anti_crease_finalize_ratio": "Дял от средната продължителност на програмата, който цикълът трябва да достигне, преди опашката против бръчки да може да го завърши; намалете при сушилни, при перални оставете по подразбиране", "anti_wrinkle_enabled": "Поглъщайте импулсите на въртене на барабана след основната фаза, вместо да ги четете като нови цикли", - "anti_wrinkle_exit_power": "Мощността между импулсите трябва да падне под това, за да остане активен режимът против бръчки", + "anti_wrinkle_exit_power": "Ниво на покой между импулсите на въртене; има ефект само над Прага на спиране", "anti_wrinkle_idle_timeout": "Разрешено спокойствие между два импулса на въртене на барабана, преди режимът против бръчки да приключи", "anti_wrinkle_max_duration": "Импулс, по-дълъг от това, приключва режима против бръчки и отваря нов цикъл", "anti_wrinkle_max_power": "Импулс над това приключва режима против бръчки и отваря нов цикъл", - "dtw_bandwidth": "Етап 3: лента за деформация на Sakoe-Chiba (0 = DTW изключено; по подразбиране 0,2 = 20% от дължината на цикъла)", - "corr_weight": "Етап 2: баланс между формата на кривата (корелация) и нивото на мощността (MAE); по подразбиране 0,45", - "duration_weight": "Етап 4: колко силно съответствието на продължителността влияе върху крайния резултат (по подразбиране 0,22)", - "energy_weight": "Етап 4: колко силно съответствието на енергията влияе върху крайния резултат (по подразбиране 0,22)", - "profile_match_min_duration_ratio": "Етап 1: минимална дължина на цикъла като дял от продължителността на профила; по подразбиране 0,1 (10%)", - "profile_match_max_duration_ratio": "Етап 1: максимална дължина на цикъла като дял от продължителността на профила; по подразбиране 1,8 (180%)", - "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)", - "dtw_refine_top_n": "Етап 3: кандидати, преоценени от DTW; увеличете до 7-9, ако правилният профил заема 4.-5. място (по подразбиране 5)", - "duration_scale": "Етап 4: лог-съотношение, при което съответствието на продължителността се намалява наполовина; по-малко = по-строга санкция (по подразбиране 0,175)", - "energy_scale": "Етап 4: лог-съотношение, при което съответствието на енергията се намалява наполовина; по-малко = по-строга санкция (по подразбиране 0,25)", + "profile_match_min_duration_ratio": "Минимална дължина на цикъла като дял от продължителността на профила; по подразбиране 0,1 (10%)", + "profile_match_max_duration_ratio": "Максимална дължина на цикъла като дял от продължителността на профила; по подразбиране 1,8 (180%)", "curve_preroll_seconds": "Секунди отчитания от прекъснати опити за стартиране, пренасяни в началото на кривата на цикъла; 0 = изключено" }, "col": { @@ -2071,12 +2000,14 @@ "status_tip": "Резултат от цикъла: Завършен (естествен край), Прекъснат (рязък спад на мощността), Принудително спрян (ръчно) или Има нужда от преглед (чака се обратна връзка).", "date_tip": "Дата и час на започване на цикъла.", "duration_tip": "Общо време на изпълнение на цикъла от началото до края.", - "energy_tip": "Общо консумирана енергия (kWh). Изчислява се чрез интегриране на мощността във времето.", - "cost_tip": "Разход за енергия за този цикъл, фиксиран при приключването му. Когато „Цена, претеглена по време“ е включена, кривата на мощността се таксува по цената, валидна във всеки момент; в противен случай енергия x единствената цена, валидна накрая. Задайте цена в Настройки, за да се попълни.", + "energy_tip": "Общо изразходвана енергия (kWh), изчислена от мощността във времето.", + "cost_tip": "Разход за енергия за този цикъл, фиксиран при завършването му. При включена „Цена, претеглена по време“ всеки момент се таксува по своята цена; иначе по цената в края. Задайте цена в Настройки, за да се попълни.", "confidence_tip": "Увереност на съвпадението на профила (0–100%). Колко точно кривата на мощността на цикъла съвпада с определената програма.", - "health_tip": "ML здраве на цикъла (по-високо = по-добре). Щракнете върху цикъл, за да го инспектирате и прегледате.", "flags_tip": "Маркери на общността и индикатори за качество на данните", - "cost_dynamic_tip": "Претеглено по време: таксувано по цената, валидна по време на цикъла (ефективно {price} {cur}/kWh)." + "cost_dynamic_tip": "Претеглено по време: таксувано по цената, валидна по време на цикъла (ефективно {price} {cur}/kWh).", + "dbg_conf": "Увер.", + "dbg_mae": "MAE", + "dbg_corr": "Корел." }, "stat": { "avg": "средно", @@ -2100,7 +2031,10 @@ "notify_finish": "Би било изпратено известие за завършване.", "notify_milestone": "Би било изпратено известие за важен етап.", "notify_held": "Известие беше задържано заради тихите часове.", - "finished": "Цикълът достигна крайно състояние и приключи." + "finished": "Цикълът достигна крайно състояние и приключи.", + "match_reverted": "Програмата се върна към откриване: резултатът ѝ падна далеч под собствения ѝ пик.", + "verified_pause": "Подравняването в реално време потвърди (или отмени) пауза в рамките на програмата. Докато паузата е в сила, цикълът не се приключва.", + "stalled": "Цикълът остана на консумация в готовност по-дълго, отколкото тази програма някога прави пауза там, а работата не е свършена. Показва се като На пауза (Заседнал); цикълът остава отворен." }, "store": { "search_ph2": "Търсене по марка или модел…", @@ -2109,11 +2043,7 @@ "programs_count": "Програми: {n}", "browse": "Разглеждане", "device": "Уред", - "favorites": "Любими", - "search_ph": "Търсене по марка или модел…", "no_results": "Няма намерени съответстващи уреди. Опитайте друго търсене.", - "n_programs": "{n} програми", - "n_cycles": "{n} цикъла", "no_programs": "Все още няма споделени програми за този уред.", "no_cycles": "Все още няма споделени еталонни цикли за тази програма.", "anon": "анонимен", @@ -2125,19 +2055,21 @@ "merge_target": "Обединяване в профил", "description": "Описание (по желание)", "contributed_by": "от {name}", + "website": "Уебсайт на магазина ↗", + "website_tip": "Отворете уебсайта на магазина на общността", + "favorites": "Любими", "rating_summary": "★ {avg} ({n})", "no_ratings": "Все още няма оценки", - "website": "Уебсайт на магазина ↗", - "website_tip": "Отворете уебсайта на магазина на общността" + "unreachable": "Няма връзка с магазина на общността. Проверете връзката си и опитайте отново." }, "settings": { "gear": { - "title": "Настройки" + "title": "Предпочитания" } }, "link": { - "manual": "Ръководство ↗", - "add_appliance": "Добавете вашия уред" + "maintenance_supporter": "Maintenance Supporter в GitHub ↗", + "manual": "Ръководство ↗" }, "tip": { "add_brand": "Марката я няма в списъка? Добавете я в каталога на общността", @@ -2171,7 +2103,13 @@ "rebuild": { "envelopes": "Преизчисляване на обвивките" }, - "cancelling": "Отмяна..." + "cancelling": "Отмяна...", + "auto_label": { + "running": "Автоматично етикетиране на цикли" + }, + "store_download": { + "running": "Изтегляне от магазина" + } }, "setup": { "hdr": { @@ -2180,26 +2118,24 @@ }, "phase0": { "washer": "WashData вече открива вашите цикли. Запишете първия цикъл, за да активирате имена на програми и прогнози за времето.", - "dishwasher": "WashData наблюдава. Съдомиялните машини имат сложни цикли – записването на първия цикъл е настоятелно препоръчително. Ако открит цикъл тече прекалено дълго, използвайте редактора на цикли, за да го изрежете преди запазване като профил.", + "dishwasher": "WashData наблюдава. Циклите на съдомиялната са сложни, затова запишете първия цикъл. Ако открит цикъл е твърде дълъг, изрежете го в редактора на цикъла, преди да го запазите като профил.", "generic": "WashData наблюдава. Запишете или маркирайте открит цикъл, за да започнете да изграждате профили." }, "phase1a": { - "labelled": "Добро начало – вашата първа програма е запазена. За най-чисти данни обмислете записването на следващия цикъл с джаджата за запис." + "labelled": "Добро начало: първата ви програма е запазена. За най-чисти данни запишете следващия цикъл с ръчен запис." }, "phase1b": { - "recorded": "Вашият запис е запазен като {profile_name}. Сега запишете или маркирайте другите чести програми, за да разширите покритието." + "recorded": "Записът ви е запазен като {profile_name}. Сега запишете или маркирайте другите си чести програми." }, "phase1c": { - "verify": "Имате {count} програми от общността. Стартирайте цикъл, за да проверите дали WashData ги разпознава правилно – съвпадението ще се подобрява с натрупването на собствена история от устройството." + "verify": "Имате програми от общността ({count}). Пуснете цикъл, за да проверите дали WashData го разпознава; съпоставянето се подобрява, докато устройството трупа собствена история." }, "phase2": { - "cluster": "WashData е видял {count} цикъла, които не съответстват на нито една запазена програма – те си приличат. Искате ли да създадете нов профил за тях?", + "cluster": "WashData видя подобни цикли ({count}), които не съвпадат с нито една запазена програма. Да се създаде ли нов профил за тях?", "unmatched": "Последният ви цикъл не съответства на нито една запазена програма. Това нова програма ли е?" }, "phase3": { - "suggestions": "WashData има препоръки за настройки въз основа на историята на вашите цикли – прегледайте ги за подобряване на точността на разпознаване.", - "groups": "Някои от профилите ви изглеждат като една и съща програма при различни температури. Организирайте ги в група за по-добро съвпадение.", - "phases": "Добавете фази на програмата към {profile_name} за по-точни прогнози за оставащото време." + "suggestions": "WashData има предложения за настройки от историята на циклите ви. Прегледайте ги за по-добро откриване." }, "phase4": { "healthy": "Това устройство е напълно настроено ({profile_count} профила)." @@ -2212,12 +2148,58 @@ "create_from_cluster": "Създай профил от тези цикли", "create_profile": "Създай за него профил", "review_suggestions": "Прегледай препоръките", - "organise_profiles": "Организирай профилите", - "configure_phases": "Конфигурирай фазите", "skip_step": "Пропусни тази стъпка", "skip_forever": "Не показвай отново", "hide_guidance": "Скрий насоките", "show_guidance": "Покажи насоките" } + }, + "access": { + "edit": "Редактиране", + "full": "Пълен", + "inherit": "Наследяване", + "none": "Няма (скрито)", + "read": "Четене" + }, + "pg_label_reason": { + "no_winner": "Няма подходяща програма", + "below_floor": "Под прага „Доверие в обучението“", + "ambiguous": "Отбелязано като двусмислено", + "margin": "Твърде близо до втората най-добра програма", + "unknown_profile": "Програмата вече не съществува", + "not_evaluated": "Не е оценено" + }, + "diagram": { + "accum": "натруп.", + "auto": "авто", + "below_off": "под = изкл.", + "confirmed": "потвърдено", + "energy_counts": "енергията се отчита", + "feedback": "обратна връзка", + "gap_below_min": "пауза под мин. = един цикъл", + "heat_phase": "фаза на нагряване", + "held_at_100": "остава на 100%", + "ignored": "игнорира се", + "kept": "запазен", + "max": "макс.", + "min": "мин.", + "minus_tol": "-тол", + "no_match": "няма съвпадение", + "no_updates": "няма актуализации", + "off_delay_wait": "закъснение при изключване", + "off_gap": "пауза изкл.", + "plus_tol": "+тол", + "profile": "профил", + "reading_interval": "типичен интервал на отчитане", + "sensor_offline": "сензор офлайн: принуд. спиране", + "si": "ИП", + "spike_ignored": "пикът се игнорира", + "spike_ignored_short": "пик: игнорира се", + "start": "старт", + "stop": "стоп", + "tail_energy_resets": "енергия в края над прага: таймерът се нулира", + "thr": "праг", + "too_short": "твърде кратък", + "tumble_kept": "импулсите на барабана се запазват" } } diff --git a/custom_components/ha_washdata/translations/panel/bs.json b/custom_components/ha_washdata/translations/panel/bs.json index 268a9e37..4626f90c 100644 --- a/custom_components/ha_washdata/translations/panel/bs.json +++ b/custom_components/ha_washdata/translations/panel/bs.json @@ -3,6 +3,7 @@ "min": "min", "entity_not_found": "Entitet nije pronađen", "running_long": "Traje predugo", + "stalled": "Zaglavljeno", "remaining": "preostalo", "progress": "napredak", "show_time_remaining": "Prikaži preostalo vrijeme", @@ -48,16 +49,17 @@ } }, "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_tip": "Pri kraju ovaj program utihne na oko {mins} min, a zatim povuče oko {watts} W tokom {secs} s. Uočeno u {seen} od {measured} izmjerenih ciklusa.", "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", + "artifact_tip": "Otkriveno je {n} anomalija (npr. vrata otvorena sredinom ciklusa) – otvorite da ih vidite na grafikonu", + "artifact_tip_one": "Otkrivena je {n} anomalija (npr. vrata otvorena sredinom ciklusa) – otvorite da je vidite na grafikonu", + "artifact_tip_few": "Otkrivene su {n} anomalije (npr. vrata otvorena sredinom ciklusa) – otvorite da ih vidite na grafikonu", + "artifact_tip_other": "Otkriveno je {n} anomalija (npr. vrata otvorena sredinom ciklusa) – otvorite da ih vidite na grafikonu", "auto": "(automatski otkriveno)", "built_in_tag": "ugrađen", "declining": "↘ u opadanju", "energy_low": "Niža potrošnja energije nego inače", "energy_spike": "Viša potrošnja energije nego inače", - "fair_fit": "prihvatljiva kvaliteta", - "fair_fit_tip": "Umjerena konzistentnost podudaranja – neki ciklusi dodijeljeni ovom profilu imaju niže rezultate pouzdanosti. Označite više ciklusa ili ponovo snimite profil da biste poboljšali preciznost.", "feedback_requested": "Zatražena je povratna informacija", "golden_cycle": "Snimljeni referentni ciklus", "duration_outlier": "Ciklusi neobične dužine: {n}", @@ -67,26 +69,37 @@ "overrun": "Trajao duže nego inače", "underrun": "Završio brže nego inače", "poor_fit": "loša kvaliteta", - "poor_fit_tip": "Nedosljedna historija podudaranja - razmislite o ponovnoj izgradnji ovog profila", - "restart_gap_tip": "{n} HA restart praznina: trag snage ima rupu", + "restart_gap_tip": "{n} HA restart praznina: trag snage ima rupe", + "restart_gap_tip_one": "{n} HA restart praznina: trag snage ima rupu", + "restart_gap_tip_few": "{n} HA restart praznine: trag snage ima rupe", + "restart_gap_tip_other": "{n} HA restart praznina: trag snage ima rupe", "reviewed": "Recenzirano", "steady": "→ stabilan", - "using_defaults": "Korištenje isporučenih zadanih postavki", - "using_tuned": "Korištenje podešenih težina", "overrun_ratio": "({x}x očekivanog)", "underrun_ratio": "({pct}% očekivanog)", - "awaiting_tip": "Čeka odobrenje zajednice", - "awaiting_n": "Čeka odobrenje · potvrđeno {n}", - "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.", - "armed": "(primjenjuje se na sljedeći ciklus)" + "armed": "(primjenjuje se na sljedeći ciklus)", + "awaiting_tip": "Čeka odobrenje zajednice", + "awaiting_n": "Čeka odobrenje · potvrđeno {n}", + "awaiting": "Čeka odobrenje", + "approved": "Odobreno", + "matcher_unused": "Još bez podudaranja s vašim ciklusima", + "matcher_used": "Podudaranje u {n} od vaših ciklusa", + "matcher_used_one": "Podudaranje u {n} od vaših ciklusa", + "matcher_used_few": "Podudaranje u {n} od vaših ciklusa", + "matcher_used_other": "Podudaranje u {n} od vaših ciklusa", + "matcher_used_tip": "Ciklusi na ovom uređaju koje je WashData sam podudario s ovim uvezenim programom. Uvoz koji se nikada ne podudari možda ne odgovara vašem uređaju; možete ga ukloniti. Broji se lokalno; ništa se ne šalje u trgovinu." }, "btn": { + "pg_preset_delete": "Izbriši konfiguraciju", + "pg_preset_load": "Učitaj konfiguraciju", + "pg_preset_load_tip": "Zamijeni vrijednosti u testnom okruženju ovom konfiguracijom (aktivne postavke ostaju netaknute)", + "pg_preset_save": "Sačuvaj kao konfiguraciju", + "pg_preset_save_tip": "Sačuvaj sve vrijednosti ispod kao imenovanu konfiguraciju za ovaj uređaj", "reset_zoom": "Poništi zumiranje", "set_brand_model": "Postavi marku i model", "refresh_catalog": "Osvježi katalog", @@ -97,12 +110,11 @@ "add_timer": "+ Dodajte tajmer", "apply_all": "Primijeni sve", "apply_label": "Primijeni oznaku", - "apply_set_b": "Primijeni skup B", "apply_split": "Primijeni podjelu", "apply_trim": "Primijeni skraćivanje", "auto_detect_split": "Automatsko otkrivanje", "auto_label_cycles": "Ciklusi automatskog označavanja", - "auto_label_cycles_tip": "Automatski dodjeljuje nazive profila neoznačenim ciklusima čija pouzdanost podudaranja prelazi prag", + "auto_label_cycles_tip": "Imenujte neoznačene cikluse čija pouzdanost podudaranja prelazi prag.", "cancel": "Otkaži", "clear_debug": "Obrišite podatke za otklanjanje grešaka", "clear_splits": "Očisti", @@ -112,8 +124,6 @@ "convert_to_automation": "Pretvorite u automatizaciju", "correct": "Ispravi…", "create": "Kreiraj", - "create_from_cluster": "Kreirajte profil od {count} ciklusa", - "create_profile": "+ Kreirajte profil", "delete": "Izbriši", "delete_group": "Izbriši grupu", "delete_group_tip": "Izbrišite samo ovu grupu - profili članova se čuvaju", @@ -122,39 +132,30 @@ "discard_tip": "Odbaci snimljeni trag bez pohrane", "dismiss": "Odbaci", "mute_suggestion": "Prestani predlagati ovu postavku", - "pg_load_calibrated": "Učitaj kalibrirano (ML) ({n})", - "pg_load_calibrated_tip": "Učitava ML kalibrirane postavke kao zamjene u testnom okruženju", "pg_load_live": "Učitaj aktivne postavke", "pg_load_live_tip": "Ponovo pročitaj trenutne postavke integracije i odbaci sve izmjene u testnom okruženju", "pg_load_suggested": "Učitaj prijedloge ({n})", "pg_load_suggested_tip": "Učitava trenutne prijedloge automatskog podešavača kao zamjene u testnom okruženju", - "pg_preset_delete": "Izbriši konfiguraciju", - "pg_preset_load": "Učitaj konfiguraciju", - "pg_preset_load_tip": "Zamijeni vrijednosti u testnom okruženju ovom konfiguracijom (aktivne postavke ostaju netaknute)", - "pg_preset_save": "Sačuvaj kao konfiguraciju", - "pg_preset_save_tip": "Sačuvaj sve vrijednosti ispod kao imenovanu konfiguraciju za ovaj uređaj", "pg_publish_all": "Primijeni {n} na integraciju", "pg_publish_one": "Primijeni {label} na integraciju", "reset_muted": "Poništi utišane", + "reset_phase_tip": "Poništite svoju izmjenu i vratite ugrađenu fazu", "done": "Gotovo", "edit": "Uredi", "export": "Izvoz", - "export_json": "Izvezi u JSON", "force_stop": "Prisilno zaustavi", "force_stop_tip": "Odmah završi trenutni ciklus i označi ga kao prisilno zaustavljen", "from_template": "Iz šablona ▾", "ignore": "Ignoriraj", "import_json": "Uvoz iz JSON-a", - "import_overwrite": "Uvoz (prepisuje podatke)", "inspect": "Pregledaj", "label": "Označi", - "load": "Učitaj", "load_more": "Učitaj još", "manage": "Upravljaj", "merge": "Spoji", "new_automation": "+ Nova automatizacija", "new_group": "+ Nova grupa", - "new_group_tip": "Grupirajte skoro identične profile (isti oblik/trajanje, različita temperatura ili centrifuga) da uparivanje pouzdano bira između njih", + "new_group_tip": "Grupišite gotovo iste profile (isti oblik, različita temperatura ili centrifuga) kako bi ih podudaranje moglo razlikovati.", "new_phase": "+ Nova faza", "new_profile": "+ Novi profil", "new_profile_tip": "Kreirajte novi profil programa iz postojećeg označenog ciklusa ili snimanja", @@ -162,23 +163,19 @@ "on_cycle_started": "Započeo ciklus", "pause_cycle": "Pauza", "pause_cycle_tip": "Pauzirajte aktivni ciklus – uređaj će se nastaviti od mjesta na kojem je stao", - "pg_apply_device": "Primijeni na uređaj", - "pg_autofill": "Automatsko popunjavanje", - "play": "Reproduciraj", "process": "Obradi", "process_history": "Obradi sada", "process_recording": "Sačuvaj snimanje", "process_tip": "Sačuvajte snimljeni trag kao novi ili postojeći profil", "rebuild": "Obnovi koverte", "rebuild_envelope": "Obnovi kovertu", - "rebuild_tip": "Preračunajte očekivanu kovertu snage (min/maks pojas) za sve profile iz njihovih označenih ciklusa – pokrenite nakon označavanja novih ciklusa ili ispravljanja starih", + "rebuild_tip": "Ponovo izgradite očekivani pojas snage svakog profila iz njegovih označenih ciklusa. Pokrenite nakon označavanja ili ispravljanja ciklusa.", "rec_start_tip": "Počnite snimanje traga snage uređaja – pokrenite neposredno prije pokretanja ciklusa", "rec_stop": "Zaustavi", "rec_stop_tip": "Zaustavite snimanje i zadržite snimljeni trag radi pregleda", "record": "Započni snimanje", "refresh": "Osvježi", "refresh_diag_tip": "Ponovo učitajte statistike pohrane", - "refresh_logs_tip": "Ponovo učitajte najnovije zapise dnevnika", "refresh_settings_tip": "Ponovo učitajte postavke sa servera", "relabel": "Ponovo označi ({count})", "remove": "Ukloni", @@ -186,7 +183,6 @@ "reset": "Vrati na zadano", "reset_ml_models_tip": "Odbacite fino podešene modele i vratite se na one ugrađene. WashData ih može ponovo naučiti kasnije.", "reset_to_builtin": "Vratite se na ugrađene modele", - "reset_to_defaults": "Vratite na zadane postavke", "resume_cycle": "Nastavi", "resume_cycle_tip": "Nastavite pauzirani ciklus", "revert_settings": "Odbaci izmjene", @@ -199,8 +195,6 @@ "run_analysis": "🔍 Pokrenite analizu prijedloga", "run_analysis_tip": "Analizirajte svoje snimljene cikluse sada i osvježite prijedloge za podešavanje", "run_auto_label": "Pokreni Auto-Label", - "run_both": "Pokreni A vs B", - "run_simulation": "Pokreni simulaciju", "save": "Sačuvaj", "save_access_control": "Sačuvajte kontrolu pristupa", "save_group": "Sačuvaj grupu", @@ -212,10 +206,8 @@ "save_settings": "Sačuvaj postavke", "select": "Odaberite", "select_all": "Odaberi sve", - "select_last20": "Zadnjih 20", "select_none": "Očisti", "show_only": "Pokaži samo", - "skip_setup": "Preskoči podešavanje", "split": "Podijeli", "stop": "Zaustavi", "submit_correction": "Pošalji ispravku", @@ -224,30 +216,24 @@ "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.", + "unlabel_selected_tip": "Uklonite oznaku s odabranih ciklusa. Ostaju u historiji, ali prestaju oblikovati ovaj profil.", "use": "Koristi", "wipe_all": "Obriši sve podatke", "delete_selected": "Izbriši odabrano ({n})", - "group_suggest": "Grupa {n}: {members}", "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", "import": "Uvezi", "disconnect": "Prekini vezu", "connect_github": "Poveži GitHub", - "save_appliance": "Sačuvaj uređaj", "share_to_store": "Podijeli u trgovinu", "share": "Podijeli", - "confirm_appliance": "Potvrdi ovaj uređaj", - "confirmed": "Potvrđeno", "browse_community_setups": "Pregledaj postavke zajednice", "download_device": "Preuzmi postavke uređaja", "share_device": "Podijeli postavke uređaja", - "share_device_tip": "Podijelite programe i postavke ovog uređaja sa zajednicom", + "share_device_tip": "Podijelite ovaj uređaj i njegove referentne cikluse u trgovini zajednice kako bi ih vlasnici iste mašine mogli preuzeti.", "share_n": "Podijeli cikluse ({n})", "export_selected": "Izvoz (odaberi podatke)", "export_all": "Brzi izvoz svega", - "import_raw": "Napredno: zamijeni sve iz JSON datoteke", "download_export": "Preuzmi izvoz", "analyze_import": "Analiziraj datoteku", "import_selected": "Uvezi odabrano", @@ -260,7 +246,15 @@ "save_total_cycles": "Sačuvaj ukupan broj", "clear": "Očisti", "add_brand": "Dodaj marku", - "add_appliance": "Dodaj uređaj" + "log_done_task": "Označi kao obavljeno: {task}", + "add_task": "Dodaj zadatak", + "add_appliance": "Dodaj uređaj", + "retry": "Pokušaj ponovo", + "confirm_appliance": "Potvrdi ovaj uređaj", + "confirmed": "Potvrđeno", + "create_profile_from_gap": "Kreirajte profil", + "undo_import": "Poništi posljednji uvoz", + "pg_rerun": "Ažuriraj simulaciju" }, "note": { "off_delay": { @@ -268,10 +262,6 @@ } }, "conflict": { - "anti_wrinkle_exit": { - "exit": "Mora biti ispod Praga zaustavljanja ({stop} W); inače se izlazna snaga anti-gužvanja zanemaruje", - "stop": "Mora biti iznad Izlazne snage anti-gužvanja ({exit} W)" - }, "anti_wrinkle_max": { "max": "Mora biti iznad Praga pokretanja ({start} W); inače se vremensko ograničenje anti-gužvanja zaobilazi", "start": "Mora biti ispod Maks. snage anti-gužvanja ({max} W)" @@ -309,14 +299,14 @@ "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." + "power_sensor": "Ovo je Senzor snage uređaja, a ne cijena. Ciklusi bi se obračunavali u vatima po kWh; očistite ovo polje.", + "energy_sensor": "Ovo je Brojilo energije uređaja, a ne cijena. Ciklusi bi se obračunavali po očitanju brojila po kWh; očistite ovo polje.", + "device_class": "Ovaj senzor mjeri {dc}, a ne cijenu po kWh. Očistite ovo polje ili odaberite senzor tarife.", + "unit": "Ovaj senzor očitava u {unit}, a ne cijenu po kWh. Očistite ovo polje ili odaberite 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" + "duration": "Mora biti manje od Timeouta bez ažuriranja ({to} s) kako bi se alarm zaglavljivanja aktivirao prvi", + "timeout": "Mora premašiti Trajanje zaglavljivanja pumpe ({ps} s) kako bi se alarm zaglavljivanja aktivirao prvi" }, "start_dur": { "sampling": "Interval uzorkovanja premašuje Trajanje pokretanja ({sdt} s); skokovi s jednim uzorkom mogu otvoriti ciklus", @@ -334,10 +324,15 @@ "sampling": "Interval uzorkovanja treba biti najviše polovina Intervala čuvara ({wi} s)" }, "settings_banner": "Konflikti postavki: {n}. Provjerite istaknute sekcije i ispravite ih prije pohrane.", - "settings_banner_btn": "Idi na prvi" + "settings_banner_btn": "Idi na prvi", + "end_energy": { + "energy": "Prestrogo za Prag zaustavljanja ({w} W) i Kašnjenje isključivanja ({d} s); ciklus se može završiti samo rezervnim putem", + "stop": "Završna energija ({e} Wh tokom {d} s) dopušta samo {p} W" + } }, "hdr": { - "ab_comparison": "Poređenje postavki A/B", + "custom_tasks": "Vlastiti zadaci", + "maintenance_supporter": "Trebate više?", "access_control": "Kontrola pristupa", "add_maintenance": "Dodaj događaj održavanja", "automation_delete": "Izbrišite ovu automatizaciju", @@ -346,28 +341,22 @@ "automation_review_this_cycle": "Pregledajte ovaj ciklus", "automations": "Automatizacija", "clear_debug": "Obrišite tragove za otklanjanje grešaka", - "cycle_simulator": "Simulator ciklusa", "diagnostics_pref": "Dijagnostika", "display": "Prikaz", - "dtw_inspector": "DTW inspektor", "export_import": "Izvoz/uvoz", - "getting_started": "Početak rada", "live_power": "Snaga uživo", "logs": "Dnevnici", - "logs_diagnostics": "Dijagnostika", "maintenance": "Radnje održavanja", "maintenance_log": "Dnevnik održavanja", "maintenance_reminders": "Podsjetnici za servis", "manual_recording": "Ručno snimanje", - "manual_recording_tip": "Namjerno pokrenite ciklus dok WashData snima trag snage. Pokrenite neposredno prije nego što uređaj krene, zaustavite kada završi, zatim obradite za pohranu kao imenovani profil.", + "manual_recording_tip": "Namjerno snimite ciklus: pritisnite Započni snimanje neposredno prije pokretanja uređaja i Zaustavi kad završi, zatim Obradi da ga sačuvate kao profil.", "ml_learned": "Šta je WashData naučio", - "ml_matching_tuning": "Fino podešavanje usklađenosti programa", "ml_settings_card": "Postavke", "ml_smart_learning": "Pametno učenje", "my_preferences": "Moje postavke", "no_automations": "Još nijedna automatizacija ne spominje ovaj uređaj.", "panel_settings": "Postavke panela", - "param_sweep": "Ispitivanje parametra", "pg_settings_source": "Izvor postavki", "phase_catalog": "Katalog faza", "playground": "Testno okruženje", @@ -376,11 +365,8 @@ "status": "Status", "status_graph": "Graf statusa", "storage_stats": "Statistike pohrane", - "test_history": "Provjera na historiji", "toggle_sidebar": "Uključite bočnu traku Home Assistant", - "tools_and_data": "Alati i podaci", "wipe_history": "Obriši historiju", - "recommendations": "Preporuke ({n})", "pg_detection_params": "Postavke detekcije", "pg_outcome": "Ishod simulacije", "pg_across_cycles": "Kroz sve vaše cikluse", @@ -388,7 +374,8 @@ "online_account": "Zajednička trgovina i online funkcije", "import_power_history": "Uvoz historije snage", "service_status": "Status servisa", - "online_account_tab": "Online i zajednica" + "online_account_tab": "Online i zajednica", + "live_match_debug": "Dijagnostika podudaranja uživo" }, "health": { "fair": "Prihvatljiva kvaliteta podudaranja", @@ -396,11 +383,16 @@ "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_tip": "Položaj na snimljenoj krivulji podudarenog programa: {pct}%. Određuje se poravnavanjem ciklusa s profilom, pa ostaje tačan kad je rad duži ili kraći. Osvježava se dok je uređaj tih, pa 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", + "task_name": "Zadatak", + "every_cycles": "Interval (ciklusi)", + "every_days": "Interval (dani)", + "new_task_name": "Novi zadatak", + "days_since_service": "{since} / {total} dana", "actions": "Akcije", "activity": "Aktivnost", "administrators": "Administratori", @@ -408,10 +400,8 @@ "aria_cycle_chart": "Kriva snage ciklusa", "aria_envelope_chart": "Grafikon anvelope snage profila", "aria_phase_chart": "Grafikon uređivača faza", - "aria_playground_chart": "Kriva snage ciklusa sa pomjerljivim pragovima detekcije", "aria_power_chart": "Grafikon potrošnje snage", "aria_spaghetti_chart": "Preklopljene krive snage ciklusa", - "aria_sweep_chart": "Grafikon rezultata pretrage parametara", "artifact_anomaly": "Anomalija", "artifact_high_power": "Visoka snaga", "artifact_interruption": "Prekid", @@ -425,151 +415,102 @@ "cohesion_good": "kohezija {pct}%", "cohesion_low": "⚠ niska kohezija {pct}%", "compare_profiles": "Uporedite sa profilima", - "concurrency": "Paralelnost", "confidence": "Samopouzdanje", "confidence_threshold": "Prag povjerenja", "correct_duration": "Ispravno trajanje (min, opcionalno)", "correct_profile": "Ispravan profil", - "correlation": "Korelacija", "cost": "Troškovi", "create_new_profile": "+ Kreirajte novi profil…", "current_phase": "Trenutna faza", - "curves": "Krive", "cycle": "Ciklus", "cycle_date_display": "Prikaz datuma ciklusa", - "cycle_health": "Zdravlje ciklusa", - "cycle_index": "indeks ciklusa →", - "cycles": "Ciklusi", "cycles_count": "Ciklusi", "cycles_lc": "ciklusa", - "dashed": "isprekidano", "date": "Datum", "debug_traces": "Tragovi otklanjanja grešaka", - "default": "Zadano", "default_access_level": "Zadani nivo za korisnike koji nisu navedeni ispod", "default_other": "Zadano (ostali uređaji)", "default_tab": "Podrazumevana kartica prilikom otvaranja panela", "font_size": "Veličina fonta ploče", - "delta": "Δ", "description": "Opis", - "dtw_blend_w": "Težina miješanja", - "dtw_blended": "Miješani rezultat", - "dtw_ddtw": "DDTW rezultat", - "dtw_l1": "L1 rezultat", - "dtw_matched_profile": "(profil podudaran ciklusu)", "duration": "Trajanje", - "duration_agreement": "Slaganje trajanja", - "emphasis": "Naglasak", "enable_access_control": "Omogućite kontrolu pristupa po korisniku", "energy": "Energija", - "energy_agreement": "Slaganje energije", - "event_timeline": "Vremenska linija događaja", "event_type": "Tip događaja", "expected": "Očekivano", + "suspected_profile": "{name} (pretpostavka)", + "runner_up_profile": "{name} (drugi najbolji)", "expected_duration": "Očekivano trajanje (min)", "file_kb": "Fajl (kB)", - "final_score": "Konačni rezultat", "from": "Od", "gap_s": "jaz (s)", "group_name": "Naziv grupe", "head_trim": "Skraćivanje početka (s)", - "health": "Zdravlje", "hide_tabs": "Sakrij kartice za neadministratore", - "in_use": "U upotrebi", "json_data": "JSON podaci", "label": "Podudarajući program", "last": "Posljednjih", "last_checked": "Zadnja provjera:", "load_from_file": "Učitaj iz datoteke", - "mae_score": "MAE rezultat", "manual_duration": "Ručno trajanje (min, opciono)", "match": "Podudaranje", "members": "Članovi", - "metric": "Metrika", "mode_existing_profile": "Dodaj postojećem profilu", "mode_new_profile": "Kreirajte novi profil", "models_fine_tuned": "(fino podešeni modeli: {count})", - "n_classic_suggestions": "{n} klasičan", - "n_ml_suggestions": "{n} ML", "n_selected": "{n} odabrano", - "n_tuning_suggestions": "{n} prijedlog podešavanja", - "na": "n/a", + "n_tuning_suggestions": "{n} prijedloga podešavanja", + "n_tuning_suggestions_one": "{n} prijedlog podešavanja", + "n_tuning_suggestions_few": "{n} prijedloga podešavanja", + "n_tuning_suggestions_other": "{n} prijedloga podešavanja", "new_profile_name": "Novo ime profila", "notes": "Napomene", - "of": "od", - "outcome": "Ishod", "panel_default_tab": "Zadana kartica", "panel_language": "Jezik ploče", "per_cycle_short": "ciklus", - "pg_autofill_tip": "Automatski popuni opseg iz trenutne vrijednosti ({lo}–{hi})", "pg_best_value": "Pronađena najbolja vrijednost", "pg_current_value": "Trenutna vrijednost:", "pg_drag_hint": "Povucite liniju na grafikonu", "pg_anti_wrinkle": "Anti-gužvanje", - "pg_dtw": "DTW", "pg_ev_ending": "Završava", "pg_ev_running": "U toku", "pg_group_advanced": "Granični slučajevi", "pg_group_detection": "Okidači detekcije", "pg_group_timing": "Pravila vremena", "pg_idle": "Neaktivan", - "pg_last_run": "Posljednje pokretanje: {date}", - "pg_match_score": "Ocjena podudaranja", - "pg_pct_of": "{p}% od {total}", "pg_poor_match": "Loše podudaranje", "pg_preset": "Konfiguracija testnog okruženja", "pg_preset_name": "Naziv nove konfiguracije", "pg_preset_none": "Odaberite konfiguraciju…", "pg_strong_match": "Snažno podudaranje", - "pg_tile_ambiguous": "Dvosmisleno", - "pg_tile_ambiguous_tip": "Ciklusi s gotovo izjednačenim podudaranjem između dva programa", - "pg_tile_detected": "Detektovano", - "pg_tile_detected_tip": "Ciklusi kod kojih je snaga prešla prag pokretanja dovoljno dugo da se registruje kao rad", - "pg_tile_matched": "Podudareno", - "pg_tile_matched_tip": "Ciklusi ispravno pripisani poznatom programu", - "pg_time_left": "Preostalo vrijeme", "pg_weak_match": "Slabo podudaranje", "phase_name": "Ime faze", "name": "Ime", "phase_timeline": "Vremenska linija faza", "power": "Snaga", "profile": "Profil", - "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. Odaberite program prije pokretanja uređaja i primijenit će se čim počne sljedeći ciklus.", + "program_tip": "Odaberite s kojim se programom podudara trenutni ciklus. Automatsko otkrivanje bira najbolje podudaranje. Prikvačite program kad automatsko otkrivanje griješi; program odabran prije pokretanja važi za sljedeći ciklus.", "progress": "Napredak", "raw_socket": "Sirova utičnica", - "read_only": "Samo za čitanje", "ref_cycle": "Referentni ciklus (opciono)", - "refresh_interval": "Interval osvježavanja (s)", "remaining": "Preostalo", "reminder_every": "Podsjeti svakih (ciklusa)", "remove_label": "- Ukloni etiketu -", "rename_profile": "Preimenuj profil", - "replay_duration": "Reprodukcija", "resulting_profile": "Rezultirajući profil", "save_mode": "Režim spremanja", - "score": "Rezultat", - "score_breakdown": "Raščlamba rezultata", "seconds_from_start": "Sekunde od početka", "select_profile": "Odaberite Profil", "setting": "Postavka", - "settings_a": "Postavke A (trenutne)", - "settings_a_short": "A", "settings_advanced": "Napredno", - "settings_b": "Postavke B (test)", - "settings_b_short": "B", "settings_basic": "Osnovno", "settings_detail_level": "Nivo detalja postavki", - "settings_overrides": "Zamjene postavki", "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", - "stage2": "Faza 2 – osnovna sličnost", - "stage3": "Faza 3 – DTW", - "stage4": "Faza 4 – slaganje", "status": "Status", "tail_trim": "Skraćivanje kraja (s)", "tap_to_dismiss": "dodirnite grafikon da zatvorite", @@ -580,7 +521,6 @@ "ungrouped": "Negrupisano", "unlabelled": "Neoznačeno", "unlabelled_paren": "(neoznačeno)", - "warp_path": "Warp putanja", "input": "Ulaz:", "start": "Početak", "end": "Kraj", @@ -605,9 +545,8 @@ "n_shown": ", prikazano {n}", "cycles_title": "Ciklusi ({n})", "pg_detecting": "Otkrivanje", - "pg_best_marker": "Najbolji: {v}", "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", + "pg_time_left_model_tip": "Procijenjeno prema fazama programa, a ne fiksno odbrojavanje", "phase": "Faza", "pg_mode_history": "Provjeri na historiji", "pg_ev_detected": "Detektovano", @@ -647,18 +586,8 @@ "pg_mode_optimize": "Optimiziraj", "pg_group_matching": "Podudaranje programa", "pg_zoom_hint": "dvostruki klik poništava zumiranje", - "aria_playground_chart2": "Interaktivni grafikon snage ciklusa: pređite mišem za prikaz vremena/snage, pomjerajte točkić za zumiranje, povucite za pomjeranje", "pg_alert_timeout_end": "Završeno zbog isteka vremena, a ne na osnovu predviđanja", "pg_alert_indefinite": "Radio bi beskonačno", - "pg_alert_stress_cap": "Dostignut sigurnosni limit", - "pg_alert_stress_ok": "Otkrivanje mirovanja: ciklus zaustavljen", - "pg_alert_stress_warn": "Potrošnja u mirovanju iznad praga zaustavljanja", - "pg_stress_group": "Test otkrivanja mirovanja", - "pg_stress_idle_auto": "Auto", - "pg_stress_idle_w": "Razina mirovanja (W)", - "pg_stress_idle_w_desc": "Zamijeni automatski otkrivenu donju granicu u stanju pripravnosti (ostavi prazno za automatsko otkrivanje).", - "pg_stress_toggle": "Testiraj otkrivanje mirovanja", - "pg_stress_toggle_desc": "Simulira da uređaj ostaje na potrošnji u stanju pripravnosti nakon završetka snimanja -- prikazuje ako i kada WashData zaustavi ciklus.", "task_pg_history": "Test na historiji", "task_pg_sweep": "Optimiziraj", "task_reprocess": "Ponovna obrada", @@ -668,16 +597,9 @@ "pg_recent_runs": "Nedavna pokretanja", "pg_load_run": "Ponovo učitaj ovo pokretanje", "pg_match_confidence": "Pouzdanost podudaranja", - "pg_envelope_fit": "Uklapanje u omotač", - "pg_envelope_fit_tip": "Koliko dobro ciklus ostaje unutar sačuvanog omotača snage ovog profila, drugačiji pogled od pouzdanosti podudaranja.", "n_imported_note": ", {n} uvezeno", "enable_online": "Omogući online funkcije", - "store_brand": "Marka uređaja", - "store_model": "Model uređaja", "anonymous": "Anonimno", - "rate_quality": "Ocijeni kvalitet", - "rate_n": "{n} zvjezdica", - "approved_only": "Samo odobreno", "adopt_settings": "Prihvati postavke", "flags": "Oznake", "include_phase_map": "Uključi mapu faza", @@ -694,7 +616,6 @@ "cat_custom_phases": "Prilagođene faze", "cat_profile_groups": "Grupe profila", "cat_settings": "Postavke otkrivanja i prepoznavanja", - "cat_matching_config": "Podešavanje prepoznavanja", "cat_ml_models": "ML modeli", "cat_feedback": "Oznake povratnih informacija i pregleda", "cat_suggestions": "Prijedlozi", @@ -714,7 +635,6 @@ "conflict_overwrite": "Prepiši", "hist_csv_data": "CSV podaci", "hist_from_recorder": "Ili je pročitajte iz Home Assistant", - "days": "dana", "hist_keep": "Zadrži ovaj ciklus", "hist_looks_complete": "završen", "peak_power_short": "Vrh", @@ -735,7 +655,42 @@ "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" + "correct_total_cycles": "Ispravi ukupan broj", + "hist_reason_already_recorded": "preklapa se s ciklusom koji je WashData već zabilježio", + "share_consent": "Potvrđujem da su ovi ciklusi normalno završeni bez prekida", + "show_internal": "Prikaži interne postavke", + "show_internal_tip": "Postavke koje WashData podešava umjesto vas (uzorkovanje, čuvar, učestalost podudaranja, pragovi). Prikazuju se kad ovaj uređaj ima vlastitu vrijednost, konflikt ili prijedlog.", + "rate_n": "{n} zvjezdica", + "rate_quality": "Ocijeni kvalitet", + "pg_autolabel": "Automatsko označavanje", + "pg_label_review": "Traži pregled", + "pg_ev_match_reverted": "Podudaranje poništeno", + "pg_ev_verified_pause": "Potvrđena pauza", + "n_samples": "{n} uzoraka", + "n_samples_one": "{n} uzorak", + "n_samples_few": "{n} uzorka", + "n_samples_other": "{n} uzoraka", + "n_cycles": "{n} ciklusa", + "n_cycles_one": "{n} ciklus", + "n_cycles_few": "{n} ciklusa", + "n_cycles_other": "{n} ciklusa", + "energy_per_cycle": "{v}/ciklus", + "energy_total": "ukupno {v}", + "automation_off": "(isključeno)", + "ref_cycle_none": "Nijedan", + "share_name": "Prikaži moje ime uz ono što dijelim", + "cat_backfill_cycles": "Ciklusi pronađeni u uvezenoj historiji snage", + "pg_compare_profile": "Uporedi s profilom", + "pg_obj_early": "Stopa ranih završetaka", + "pg_obj_endlag": "Kašnjenje završetka (medijan)", + "pg_obj_split": "Stopa podijeljenih ciklusa", + "pg_sweep_cycles": "Nedavni ciklusi", + "cycle_context": "Historija recordera oko ciklusa", + "cycle_context_off": "Isključeno", + "cycle_context_minutes": "{min} min", + "cycle_context_series": "Historija recordera", + "before_start": "Prije početka", + "after_end": "Nakon kraja" }, "log": { "all_levels": "Svi nivoi", @@ -745,11 +700,9 @@ }, "ml": { "builtin_models": "● Korišćenje ugrađenih modela", - "fit_fair": "Prihvatljivo", "fit_good": "Dobro", "fit_slight": "Lagano", "fit_strong": "Jaka", - "fit_weak": "Slabo", "fit_word": "kvaliteta", "personalized": "● Personalizovano za ovu mašinu", "better_than_baseline": "{pct}% bolje od osnovne procjene ({metric})", @@ -757,21 +710,31 @@ "trend_declining_tip": "Prilagodba ovog modela se pogoršala u nedavnim ponovnim provjerama, pregled više ciklusa može mu pomoći da ponovo nauči.", "trend_steady_tip": "Prilagodba ovog modela je ostala otprilike stabilna u nedavnim ponovnim provjerama.", "cap_blurb": { - "end": "Prepoznavanje kada je ciklus zaista završen", - "live_match": "Brže prepoznavanje programa koji je u toku", - "quality": "Uočavanje pogrešno prepoznatih ili oštećenih ciklusa", - "remaining_time": "Predviđanje preostalog vremena", "total_energy": "Predviđanje ukupne potrošnje energije i troškova" }, "cap_label": { - "end": "Prepoznavanje kraja ciklusa", - "live_match": "Prepoznavanje programa", - "quality": "Provjera kvaliteta ciklusa", - "remaining_time": "Procjena preostalog vremena", "total_energy": "Procjena potrošnje energije" }, - "metric_auc": "AUC {auc} na izdvojenim podacima", - "metric_mae": "greška {model} u odnosu na osnovnu vrijednost {baseline}" + "metric_mae": "greška {model} u odnosu na osnovnu vrijednost {baseline}", + "fine_tuned_at": "fino podešeno {when}", + "held_out_count": "izmjereno na {count} izdvojenih ciklusa", + "held_out_count_one": "izmjereno na {count} izdvojenom ciklusu", + "held_out_count_few": "izmjereno na {count} izdvojena ciklusa", + "held_out_count_other": "izmjereno na {count} izdvojenih ciklusa", + "last_run_not_promoted": "Zadnja provjera ({when}): {reason}", + "never": "nikad", + "skip_reason": { + "fit_failed": "Treniranje nije uspjelo: {error}", + "holdout_too_small": "Premalo ciklusa za pošteno testiranje novog modela: za testiranje se moglo izdvojiti {held_out}, a potrebno je {min}.", + "insufficient_rows": "Još nema dovoljno upotrebljivih ciklusa: zasad {count} označenih, čistih ciklusa.", + "insufficient_rows_one": "Još nema dovoljno upotrebljivih ciklusa: zasad {count} označen, čist ciklus.", + "insufficient_rows_few": "Još nema dovoljno upotrebljivih ciklusa: zasad {count} označena, čista ciklusa.", + "insufficient_rows_other": "Još nema dovoljno upotrebljivih ciklusa: zasad {count} označenih, čistih ciklusa.", + "not_better_than_incumbent": "Novi model nije bio bolji od onog koji se koristi (greška {model} u odnosu na {incumbent}), pa je zadržan trenutni.", + "not_better_than_naive": "Novi model nije bio jasno bolji od jednostavne procjene (greška {model} u odnosu na {naive}).", + "unknown": "Ništa novo nije naučeno." + }, + "trained_unknown": "u nepoznato vrijeme" }, "modal": { "auto_label": "Ciklusi automatskog označavanja", @@ -780,7 +743,6 @@ "cycle": "Ciklus", "edit_group": "Uredi grupu profila", "edit_phase": "Uredi fazu", - "import_config": "Uvoz konfiguracije", "label_cycle": "Označi ciklus", "merge_cycles": "Spoji {n} ciklusa", "new_group": "Nova grupa profila", @@ -795,6 +757,8 @@ "delete_automation_title": "Izbriši automatizaciju", "delete_automation_msg": "Izbrisati automatizaciju \"{name}\" iz Home Assistant? Ovo se ne može poništiti.", "remove_legacy_title": "Ukloni zastarjele akcije", + "reset_phase_msg": "Poništiti vašu izmjenu faze \"{name}\" i vratiti ugrađenu fazu? Programi zadržavaju svoje opsege.", + "reset_phase_title": "Vrati fazu na zadano", "remove_legacy_msg": "Ukloniti zastarjele prilagođene akcije? Prestat će se aktivirati na događajima ciklusa. Ovo se ne može poništiti iz panela.", "discard_recording_title": "Odbaci snimak", "discard_recording_msg": "Odbaciti sačuvani snimak? Ovo se ne može poništiti.", @@ -815,32 +779,45 @@ "store_share_device": "Podijeli postavke uređaja", "export_select": "Izvoz - odaberi podatke", "import_wizard": "Uvoz - odaberi podatke", - "history_import": "Uvoz historije snage" + "history_import": "Uvoz historije snage", + "apply_suggestions_title": "Primijeni prijedloge", + "remove_task_title": "Ukloni zadatak", + "remove_task_msg": "Ukloniti podsjetnik \"{task}\"? Njegovi unosi u dnevniku održavanja ostaju sačuvani.", + "apply_suggestions_msg": "Promijeniti ove postavke ({n})? Integracija se ponovo učitava nakon spremanja.", + "discard_settings_title": "Odbaciti nesačuvane promjene?", + "discard_settings_msg": "Nesačuvane promjene postavki: {n}. Napustiti Postavke i odbaciti ih?", + "profile_title": "Profil · {name}", + "undo_import_msg": "Podaci i postavke ovog uređaja vraćaju se na stanje prije posljednjeg uvoza ({when}). Ciklusi zabilježeni i promjene napravljene od tada bit će izgubljeni.", + "undo_import_ok": "Poništi uvoz", + "undo_import_title": "Poništi posljednji uvoz" }, "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.", + "pg_preset_delete_confirm": "Izbrisati konfiguraciju \"{name}\"?", + "pg_preset_limit": "Dostignut limit konfiguracija ({n})", + "pg_preset_overwrite": "Prepisati konfiguraciju \"{name}\"?", "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.", - "refresh_catalog_hint": "Liste marki i uređaja zajednice privremeno se čuvaju kako bi zajednička trgovina ostala u okviru svog dnevnog ograničenja. Osvježite da preuzmete unose koje su drugi dodali ili odobrili.", + "store_sibling_hint": "Ništa nije podijeljeno za vaš model? Srodan model iste marke obično je dobar početak.", + "store_declare_appliance": "Recite WashData koji uređaj imate da vidite postavke koje su drugi podijelili za njega. Ili upišite marku iznad i razgledajte.", + "refresh_catalog_hint": "Liste marki i uređaja zajednice privremeno se čuvaju zbog dnevnog ograničenja trgovine. Osvježite da vidite unose koje su drugi dodali ili odobrili.", "head_trim_hint": "Uklonite ovoliko sekundi sa početka", "appliance_monitor": "Monitor uređaja", "artifact_dip_detail": "Pao ispod uobičajenog pojasa snage za ~{n}s.", "artifact_footer": "Istaknuto na grafikonu iznad. To su prolazni artefakti (npr. vrata se otvaraju usred ciklusa), ne nužno problemi.", "artifact_header": "{n} anomalija otkrivenih tokom ovog ciklusa", - "artifact_pause_detail": "Snaga je pala na gotovo nulu za ~{n}s, a zatim se nastavila – vjerovatno su vrata otvorena sredinom ciklusa ili je ciklus pauziran.", + "artifact_header_one": "{n} anomalija otkrivena tokom ovog ciklusa", + "artifact_header_few": "{n} anomalije otkrivene tokom ovog ciklusa", + "artifact_header_other": "{n} anomalija otkrivenih tokom ovog ciklusa", + "artifact_pause_detail": "Snaga je pala gotovo na nulu na ~{n}s, a zatim se vratila. Vjerovatno su vrata otvorena ili je ciklus pauziran.", "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": "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.", + "cleanup_intro": "Svi označeni ciklusi jedan preko drugog. Označite odstupajuće, zatim im uklonite oznaku (ostaju u historiji, ali više ne oblikuju 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)", - "compare_profiles_tip": "Prekrijte druge koverte profila na gornjoj tabeli da vidite koja najbolje odgovara ovom ciklusu.", + "compare_profiles_tip": "Prekrijte grafikon drugim profilima da vidite koji najbolje odgovara ovom ciklusu.", "compare_selected_cycles": "Odabrani ciklusi (puni) – prikaži / sakrij", - "consider_new_profile": "Razmislite o kreiranju novog profila.", - "coverage_gap": "nedavni ciklusi nemaju odgovarajući profil ({pct}% od posljednjih 30).", - "coverage_gap_similar_cycles": "Pronađeno {count} sličnih neoznačenih ciklusa – kreirajte profil da počnete s njihovim uparivanjem.", "cycles_deleted": "Izbrisano ciklusa: {count}", "enough_data": "Dovoljno podataka za učenje ({current}/{min} ciklusa).", "export_description": "Odaberite tačno koje profile, cikluse, postavke i ostalo želite izvesti u JSON datoteku ili analizirajte datoteku i uvezite samo željene dijelove.", @@ -848,30 +825,27 @@ "feedback_prompt": "Potvrdite da je ispravno, ispravite program ili zanemarite.", "feedback_relabel_hint": "Ponovno označavanje ovog ciklusa takođe ga rješava.", "filter_by_profile": "Filtriraj po profilu…", - "group_modal_help": "Grupni programi istog oblika koji se razlikuju po temperaturi/centru (trajanja mogu varirati). Uparivanje boduje grupu kao jednog kandidata, a zatim bira najprikladnijeg člana. Odaberite najmanje 2; prekrivanje pokazuje koliko su slični.", + "group_modal_help": "Grupišite programe istog oblika koji se razlikuju po temperaturi ili centrifugi. Podudaranje ocjenjuje grupu kao cjelinu, a zatim bira člana koji najbolje odgovara. Odaberite barem 2; preklapanje pokazuje koliko su slični.", "group_not_cohesive": "Ovi profili nisu dovoljno slični da bi se pouzdano grupirali, tako da ih uparivanje tretira pojedinačno sve dok ne uklonite izuzetak ili podijelite grupu.", "group_preview_hint": "Označite 2 ili više članova za pregled i usporedbu krivulja snage.", - "import_intro": "Učitajte izvezeni fajl ili zalijepite JSON korisni učitavanje ispod.", - "legacy_actions_title": "{count} naslijeđena prilagođena radnja još uvijek radi", + "legacy_actions_title": "{count} naslijeđenih prilagođenih radnji još uvijek radi", + "legacy_actions_title_one": "{count} naslijeđena prilagođena radnja još uvijek radi", + "legacy_actions_title_few": "{count} naslijeđene prilagođene radnje još uvijek rade", + "legacy_actions_title_other": "{count} naslijeđenih prilagođenih radnji još uvijek radi", "live_chart_loading": "Dijagram snage uživo se pojavljuje kako stignu očitanja.", "loading": "Učitavanje…", "loading_curve": "Učitavanje krivulje…", "loading_settings": "Učitavanje postavki…", "log_buffer_hint": "Najnovije prvo · baferuje poslednjih 500 ha_washdata zapisa od ponovnog pokretanja · prevucite donju ivicu da promenite veličinu.", - "maintenance_advisory": "Povećanje trajanja/energije može ukazivati da je potrebno održavanje uređaja (npr. uklanjanje kamenca, čišćenje filtera).", "maintenance_due": "Održavanje dospijeva: {items}", - "maintenance_intro": "Zabilježite servis koji obavljate na ovom uređaju i budite podsjećeni kada svaki zadatak ponovo dospije.", + "maintenance_intro": "Bilježite servisiranje ovog uređaja i dobijte podsjetnik kad svaki zadatak ponovo dođe na red.", "maintenance_load_error": "Nije moguće učitati podatke o održavanju: {error}", "maintenance_requires_access": "Održavanje i izvoz/uvoz zahtijevaju pun pristup.", - "manual_duration_hint": "Prosječna/očekivana dužina ciklusa profila, koja se koristi za procjene preostalog vremena. Uredite da biste ga postavili; ostavljanje nepromijenjenog zadržava trenutnu vrijednost.", - "matching_tuning_intro": "Prilikom učenja, WashData također prilagođava koliko usklađivanje programa teži obliku u odnosu na trajanje i energiju.", + "manual_duration_hint": "Očekivana dužina ciklusa profila, koristi se za procjenu preostalog vremena. Ostavite nepromijenjeno da zadržite trenutnu vrijednost.", "merge_intro": "Odabrani ciklusi se kombinuju u jedan (hronološki red; praznine popunjene sa 0 W). Odaberite rezultirajući profil.", "ml_intro": "WashData se isporučuje sa pametnim modelima koji rade bez upotrebe.", "ml_learned_intro": "Modeli fino prilagođeni ovoj mašini.", - "ml_loading": "učitavanje ML-a…", "ml_settings_intro": "Dva nezavisna prekidača: jedan primenjuje modele dok ciklus traje, drugi omogućava WashData da ih fino podesi prema vašoj mašini tokom vremena.", - "name_first_program": "Imate dovoljno ciklusa – imenujte svoj prvi program da započnete podudaranje.", - "near_duplicate_cluster": "otkriven skoro duplirani klaster profila. Grupisanje omogućava pouzdano uparivanje biranja između sličnih (npr. isti program na različitoj temperaturi/centrifikaciji).", "no_cycles_match": "Nijedan ciklus ne odgovara trenutnom filteru.", "no_cycles_profile": "Nema ciklusa za ovaj profil.", "no_cycles_selected": "Prvo odaberite barem jedan ciklus.", @@ -896,79 +870,63 @@ "no_suggestions": "Nema aktivnih prijedloga.", "notify_services_hint": "Koristite ID-ove servisa {entity} (odvojene zarezima za više). Varijable predloška: {vars}.", "old_actions_warning": "Konfigurisano sa starim uređivačem akcija (sada uklonjeno). I dalje se aktiviraju na događaje ciklusa, ali se više ne mogu uređivati ovdje. Pretvorite ih u normalnu automatizaciju ili ih uklonite.", - "onboarding_progress": "{n} / 3 ciklusa promatrano", - "onboarding_watching": "Pustite uređaj da radi normalno – WashData prati. Nakon 3 ciklusa počinje podudaranje programa.", "pending_feedback": "Čeka se povratna informacija o detekciji", "performance_trend": "Trend performansi ({n} ciklusa)", - "pg_analysis_empty": "Učitajte ciklus da vidite analizu podudaranja.", "pg_apply_confirm": "Primijeniti najbolju vrijednost: {label} = {value}?", - "pg_canvas_empty": "Odaberite ciklus iznad, zatim pritisnite „Učitaj“ da vidite njegovu krivu snage.", - "pg_chart_caption": "Stopa detekcije/podudaranja po vrijednostima {param}", "pg_ctrl_intro": "Vrijednosti polaze od aktivnih postavki integracije za ovaj uređaj. Izmjene ostaju u testnom okruženju dok ih ne primijenite.", "pg_live_unavailable": "Aktivne postavke nisu dostupne, prikazane su zadane vrijednosti.", "pg_load_live_confirm": "Odbaciti izmjene u testnom okruženju ({n}) i ponovo učitati trenutne postavke integracije?", - "pg_match_rate": "{pct}% podudaranja na {total} ciklusa", "pg_matches_live": "Odgovara aktivnim postavkama", "pg_n_changed": "Promjene u odnosu na aktivne postavke: {n}", "pg_sugg_none": "Svi prijedlozi već odgovaraju trenutnim postavkama", - "pg_preset_delete_confirm": "Izbrisati konfiguraciju \"{name}\"?", - "pg_preset_limit": "Dostignut limit konfiguracija ({n})", - "pg_preset_overwrite": "Prepisati konfiguraciju \"{name}\"?", "pg_publish_one_confirm": "Sačuvati {label} = {value} u postavke ovog uređaja?", "pg_restart_note": "Ponovo pokrenite Home Assistant da omogućite alate za simulaciju.", - "pg_sim_progress": "{done} / {total} ciklusa", - "pg_starting": "Pokretanje…", - "pg_stress_synthetic": "simulacija mirovanja počinje ovdje", - "pg_sweep_intro": "Šta ako bi {param} bio drugačiji? Testirajte {steps} vrijednosti na vaših posljednjih {cycles} ciklusa da pronađete postavku pri kojoj se najviše ciklusa ispravno podudara.", - "pg_sweep_step": "Korak {done} / {total}", - "pg_verdict_bad": "Zahtijeva pažnju: mnogi ciklusi ostaju neotkriveni.", - "pg_verdict_good": "Dobro podešeno: većina ciklusa se ispravno prepoznaje i podudara.", - "pg_verdict_ok": "Prihvatljivo: neki ciklusi su propušteni. Pokušajte smanjiti prag pokretanja.", "phase_catalog_intro": "Imenovani segmenti ciklusa (predpranje, grijanje, centrifuga…). Dodijelite ih profilu sa njegove kontrolne ploče.", "phase_ranges_intro": "Opsezi faza (minuti od početka ciklusa) preklapaju se na krivulju prosjeka. Uredite vrijednosti za pregled uživo.", - "playground_intro": "Sigurno okruženje za ponovnu reprodukciju prošlih ciklusa s različitim postavkama, poređenje dvije konfiguracije jednu pored druge i pregled kako se ciklus poravnava s profilom. Ništa ovdje ne mijenja vaše aktivne podatke dok to izričito ne primijenite.", - "playground_needs_restart": "Ponovo pokrenite Home Assistant da omogućite alate Testnog okruženja.", - "preferences_admin": "Preference, panel i kontrola pristupa", - "preferences_adv": "Postavke", + "playground_intro": "Isprobajte postavke na stvarnim podacima svojih ciklusa. Ništa ovdje ne mijenja aktivne postavke dok ih ne primijenite.", "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 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.", + "process_history_hint": "Ponovo pokrenite podudaranje na svim sačuvanim ciklusima, osvježite prijedloge, ponovo trenirajte ML modele (ako su omogućeni), preračunajte troškove ciklusa prema zabilježenim cijenama i ponovo izračunajte zdravlje ciklusa. Pokrenite 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.", - "rbac_hint": "Kada je isključeno, svaki korisnik Home Assistant-a ima puni pristup (podrazumevano). Administratori uvijek imaju pun pristup i mogu upravljati svima.", - "recent_logs": "Nedavni zapisi ha_washdata", + "rbac_hint": "Isključeno: svaki korisnik Home Assistanta ima puni pristup (zadano). Administratori uvijek imaju puni pristup i mogu upravljati svima.", "recording_in_progress": "Snimanje je u toku", "reminders_intro": "Prikaži podsjetnik u panelu toliko ciklusa nakon posljednjeg servisa. Ostavite prazno ili 0 da isključite podsjetnik.", "restart_gap_footer": "Istaknuto na grafikonu. Podaci o snazi nedostaju za ove intervale; uparivanje je koristilo samo stvarne očitavanja.", "restart_gap_header": "{n} HA restart praznina tokom ovog ciklusa", + "restart_gap_header_one": "{n} HA restart praznina tokom ovog ciklusa", + "restart_gap_header_few": "{n} HA restart praznine tokom ovog ciklusa", + "restart_gap_header_other": "{n} HA restart praznina tokom ovog ciklusa", "restart_gap_item": "{dur} praznina", - "review_confirm_help": "Potvrdite da li je ovaj ciklus ispravno detektovan. Vaše recenzije obučavaju model na vašoj mašini - što više ciklusa potvrdite, bolje se podudaraju i ocjenjuju zdravlje. Dovoljno je brzo dobro/loše.", + "review_confirm_help": "Je li ovaj ciklus ispravno otkriven? Potvrđivanje ciklusa poboljšava podudaranje za vašu mašinu. Dovoljno je brzo Dobro/Loše.", "review_in_settings": "Pregledajte u Postavkama", "review_notes_placeholder": "Bilješke (opcionalno)", "review_notes_tip": "Bilješke u slobodnom tekstu za vlastitu referencu. Ne koristi se za uparivanje ili obuku.", - "review_profile_tip": "Program ovog ciklusa je označen kao. Ako je program koji je automatski detektovan bio pogrešan, ispravite to ovdje - označavanje uči podudaranje za buduće cikluse.", - "review_quality_tip": "Koliko je čist ovaj ciklus. Dobar = primjer iz udžbenika ovog programa; Loše = otkriveno, ali bučno ili netipično; Neupotrebljivo = pogrešno otkriveno (spojeno, skraćeno ili lažno). Upravlja ocjenu zdravlja i koji ciklusi su dozvoljeni za obuku modela.", - "review_recorded_tip": "Označite ovo kao ručno odabran referentni ciklus za njegov program - ista uloga kao i ručno snimljeni ciklus. Referentni ciklusi se uvijek čuvaju, postavljaju podudarni predložak i nikada se ne ispuštaju čišćenjem. (Ovo je \"zlatna\"/snimljena zastava; obje su ista stvar.)", - "review_tags_tip": "Opcione zastavice koje opisuju šta je pošlo po zlu s ovim ciklusom, tako da obuka i čišćenje mogu to objasniti.", + "review_profile_tip": "Program kojim je ovaj ciklus označen. Ako je pogrešan, ispravite ga ovdje; oznake uče podudaranje.", + "review_quality_tip": "Koliko je ovaj ciklus čist. Dobro: školski rad ovog programa. Loše: otkriven, ali bučan ili neobičan. Neupotrebljivo: pogrešno otkriven (spojen, skraćen ili lažan). Utiče na ocjenu zdravlja i na to koji ciklusi treniraju model.", + "review_recorded_tip": "Označite kao ručno odabran referentni ciklus njegovog programa, kao ručno snimljeni ciklus. Referentni ciklusi se uvijek čuvaju, čine predložak podudaranja i čišćenje ih nikada ne uklanja.", + "review_tags_tip": "Opcione oznake o tome šta je pošlo po zlu, kako bi ih treniranje i čišćenje mogli uzeti u obzir.", "review_to_cycles": "Otvorite red za pregled ciklusa", "samples_decimated": "Prikazano {shown} od {total} uzoraka (prorijeđeno za prikaz; vrhovi zadržani). Široki razmak ovdje znači prorjeđivanje, a ne podatke koji nedostaju.", - "saving_triggers_reload": "Spremanje pokreće ponovno učitavanje integracije. HA entiteti se mogu nakratko prikazati kao nedostupni.", + "saving_triggers_reload": "Spremanje ponovo učitava integraciju. Entiteti mogu nakratko biti nedostupni.", "search_placeholder": "Postavke pretraživanja…", "see_recorder": "Pogledajte widget snimača ispod", "setting_changed": "Promijenjeno sa {old} na {new} dana {date}", "settings_basic_note": "Prikazuju se osnovne postavke. Prebacite na Napredno za potpunu listu.", - "shape_drift_advisory": "⚠ Drift oblika", - "shape_drift_detail": "Obrazac snage za ovaj profil je promijenjen tokom vremena – mogući habanje uređaja ili potrebno održavanje (npr. uklanjanje kamenca, čišćenje filtera).", "show_all_settings": "Prikaži sve postavke", - "showing_suggestions": "Prikazana je postavka {count} s prijedlozima.", - "split_intro": "Kliknite na grafikon da biste dodali ili uklonili tačku razdvajanja, ili automatsko otkrivanje po prazninama u mirovanju. Svaki rezultujući segment može dobiti svoj profil.", - "storage_diagnostics": "Statistika pohrane, održavanje, izvoz/uvoz", + "showing_suggestions": "Prikazano je {count} postavki s prijedlozima.", + "showing_suggestions_one": "Prikazana je {count} postavka s prijedlozima.", + "showing_suggestions_few": "Prikazane su {count} postavke s prijedlozima.", + "showing_suggestions_other": "Prikazano je {count} postavki s prijedlozima.", + "split_intro": "Kliknite grafikon da dodate ili uklonite tačku razdvajanja, ili je automatski pronađite po prazninama mirovanja. Svaki dio može dobiti vlastiti profil.", "sug_staged": "Postavljeno {key} = {val}. Sačuvajte za primjenu.", "sug_muted": "Ova postavka više neće biti predlagana", "sug_mute_failed": "Nije moguće utišati prijedlog", "sug_unmuted_all": "Utišani prijedlozi poništeni", - "n_suggestions_muted": "{count} utišano; automatski podešavač neće predlagati ove postavke.", + "n_suggestions_muted": "{count} prijedloga je utišano; automatski podešavač ih neće predlagati.", + "n_suggestions_muted_one": "{count} prijedlog je utišan; automatski podešavač ga neće predlagati.", + "n_suggestions_muted_few": "{count} prijedloga su utišana; automatski podešavač ih neće predlagati.", + "n_suggestions_muted_other": "{count} prijedloga je utišano; automatski podešavač ih neće predlagati.", "toast_auto_detect_enabled": "Automatsko prepoznavanje omogućeno", "toast_auto_label_complete": "Automatsko označavanje završeno", "toast_auto_label_failed": "Automatsko označavanje neuspješno: {error}", @@ -984,7 +942,6 @@ "toast_no_automation": "Ovdje nije moguće kreirati automatizaciju", "toast_profile_name_required": "Naziv profila je obavezan", "toast_program_set": "Program postavljen: {program}", - "toast_rebuild_failed": "Obnova neuspješna: {error}", "toast_resume_failed": "Nastavak neuspješan: {error}", "toast_revert_failed": "Povrat neuspješan: {error}", "toast_reverted": "{key} vraćeno", @@ -995,95 +952,81 @@ "trend_energy_up": "Energetski trend porasta ({pct}%/ciklus) – nedavni prosjek {avg}", "trim_destructive_confirm": "Ovo zadržava samo {pct}% ciklusa i ne može se poništiti. Nastaviti?", "trim_intro": "Prevucite crvene ručke ili unesite vrijednosti. Sve van prozora je uklonjeno.", - "tuning_suggestions_available": "{count} prijedlog podešavanja dostupan je iz posmatranih ciklusa. Pojavljuju se pored relevantnih polja.", + "tuning_suggestions_available": "{count} prijedloga podešavanja dostupno je na osnovu posmatranih ciklusa. Pojavljuju se pored relevantnih polja.", + "tuning_suggestions_available_one": "{count} prijedlog podešavanja dostupan je na osnovu posmatranih ciklusa. Pojavljuje se pored relevantnog polja.", + "tuning_suggestions_available_few": "{count} prijedloga podešavanja dostupna su na osnovu posmatranih ciklusa. Pojavljuju se pored relevantnih polja.", + "tuning_suggestions_available_other": "{count} prijedloga podešavanja dostupno je na osnovu posmatranih ciklusa. Pojavljuju se pored relevantnih polja.", "unsure_detected_prefix": "WashData nije siguran da je otkriven", - "updated": "Ažurirano", "warmup_badge": "Još uvijek uči ({done}/{needed} ciklusa)", - "warmup_detail": "Ovom profilu je potrebno {needed} označenih ciklusa prije nego što počne automatsko uparivanje. Svaki potvrđeni ciklus pomaže učenju.", + "warmup_detail": "Podudaranje već radi. WashData traži da potvrdite prvih {needed} ciklusa ovog profila prije nego što počne sam označavati.", "wipe_history_warning": "Trajno obrišite sve cikluse i profile. Ne može se poništiti.", "compare_overlay_tip": "Preklopite naučene koverte profila da vidite na koji program podsjeća svaki ciklus.", - "duration_trend_up_tip": "Trajanje raste ({pct}%/ciklus)", - "duration_trend_down_tip": "Trajanje pada ({pct}%/ciklus)", - "energy_trend_up_tip": "Energija raste ({pct}%/ciklus)", - "energy_trend_down_tip": "Energija pada ({pct}%/ciklus)", - "performance_trending": "Trend performansi", - "pg_select_cycle_hint": "Odaberite ciklus i kliknite ↺ za učitavanje", - "pg_sweep_progress": "Korak {done} / {total}", - "pg_analysis_hint": "Odaberite ciklus i kliknite ↺ za učitavanje analize.", "diagnostics_load_failed": "Nije moguće učitati dijagnostiku: {error}", - "manual_duration_ref_hint": "Koristi se samo kada nije odabran referentni ciklus. Referentni ciklus postavlja trajanje prema vlastitoj dužini.", + "manual_duration_ref_hint": "Koristi se samo bez referentnog ciklusa; referentni ciklus postavlja trajanje prema svojoj dužini.", + "duration_learned_hint": "Izračunato iz ciklusa ovog programa (bez odstupanja) i ažurira se nakon svakog novog. Da biste ga promijenili, skratite ili uklonite cikluse koji odstupaju.", "toast_convert_rolled_back": "Konverzija nije uspjela i poništena je (nije ostala nijedna automatizacija): {error}", "toast_convert_orphan": "Automatizacija je kreirana, ali brisanje starih radnji nije uspjelo. Nemojte pokušavati ponovo: ručno uklonite stare radnje kako biste izbjegli dupliranu automatizaciju.", - "advisory_duration_trend_up": "Ciklusi programa '{name}' postaju sve duži (otprilike +{pct}% po ciklusu). Ako se ponašanje uređaja promijenilo, ponovo snimite ili obnovite ovaj profil.", - "advisory_energy_trend_up": "Program '{name}' troši sve više energije (otprilike +{pct}% po ciklusu) - vrijedi provjeriti uređaj ako je to neočekivano.", - "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.", + "advisory_duration_trend_up": "Ciklusi programa '{name}' postaju sve duži (oko +{pct}% po ciklusu). Ako se uređaj promijenio, ponovo snimite ili obnovite ovaj profil.", + "advisory_energy_trend_up": "Program '{name}' troši više energije sa svakim ciklusom (oko +{pct}% po ciklusu). Provjerite uređaj ako je to neočekivano.", + "advisory_poor_health": "Profil '{name}' loše odgovara: njegovi noviji ciklusi jako variraju ili se slabo podudaraju. Pregledajte njegove cikluse ili ga ponovo snimite kako bi podudaranje i procjene ostali tačni.", + "advisory_shape_drift": "Profil '{name}' odstupio je od svog izvornog oblika snage, možda zbog kamenca ili habanja. Razmislite o ponovnom snimanju s novijim ciklusima.", + "advisory_shape_drift_corr": "Profil '{name}' odstupio je od svog izvornog oblika snage (korelacija {corr}), možda zbog kamenca ili habanja. Razmislite o ponovnom snimanju s novijim ciklusima.", + "advisory_unmatchable": "Profil '{name}' nikada se ne može podudariti: iza njega ne stoji nijedan ciklus s podacima o snazi. Označite ili snimite ciklus ovog programa da to popravite.", + "advisory_unmatchable_no_duration": "Profil '{name}' nikada se ne može podudariti: njegov ciklus nema upotrebljivo trajanje. Ponovo snimite ovaj program ili postavite njegovo očekivano trajanje.", "pg_no_alerts": "U ovom pokretanju nisu otkriveni problemi.", "pg_override_active": "Koriste se vaše izmijenjene postavke umjesto trenutnih", "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.", "pg_sweep_intro2": "Pronađite postavku koja najbolje ispunjava cilj za vaše nedavne cikluse. Ništa se neće promijeniti dok je ne primijenite.", - "pg_running_history": "Ponovno izvođenje vaših ciklusa…", - "pg_running_sweep": "Ispitivanje vrijednosti kroz sve vaše cikluse…", "pg_row_load_hint": "Učitaj ovaj ciklus u grafikon iznad", - "pg_history_intro2": "Ponovo izvedite svoje nedavne cikluse kroz stvarni detektor i modul za podudaranje s postavkama iznad. Kliknite bilo koji red da učitate taj ciklus u grafikon; izmijenite postavku da vidite poređenje prije i poslije.", + "pg_history_intro2": "Ponovo provedite nedavne cikluse kroz stvarni detektor i podudaranje s postavkama iznad. Kliknite red da ga učitate u grafikon; promijenite postavku da uporedite prije i poslije.", "pg_simulating": "Simuliranje ciklusa…", - "pg_canvas_empty2": "Odaberite ciklus iznad i pritisnite Pokreni da ga simulirate. Zatim pređite mišem za prikaz vrijednosti, pomjerajte točkić za zumiranje i povucite za pomjeranje.", "pg_apply_settings_confirm": "Sačuvati ovih {n} postavki na ovaj uređaj? {list}", - "pg_stress_terminated_detail": "Ustalilo se na ~{idle}W mirovanja -- ciklus je završio {h} h {m} min kasnije putem {reason}.", - "pg_stress_above_threshold_detail": "Potrošnja u mirovanju ~{idle}W jednaka je ili viša od stvarnog praga zaustavljanja ({stop}W) -- ciklus nikad nije zabilježen kao miran. Za ispravku povećajte stop_threshold_w.", - "pg_stress_hit_cap_detail": "Ciklus je radio {h} h {m} min bez zaustavljanja -- prisilno zaustavljen sigurnosnim limitom. Potrošnja u mirovanju {idle}W u odnosu na prag zaustavljanja {stop}W.", "no_logs_match": "Nijedan zapis dnevnika ne odgovara ovim filterima.", "pg_analysis_hint2": "Odaberite ciklus i pritisnite Pokreni za učitavanje analize podudaranja.", - "pg_analysis_empty2": "Pritisnite Pokreni za prikaz analize podudaranja.", - "imported_readonly": "Uvezeno iz zajedničke trgovine. Prikazano za referencu i podudaranje. Ne broji se u vašu statistiku i ne može se uređivati.", + "imported_readonly": "Uvezeno iz trgovine zajednice. Služi kao referenca i za podudaranje, ne broji se u statistiku i ne može se uređivati.", "merge_no_imports": "Uvezene snimke ne mogu se spojiti sa stvarnim ciklusom. Poništite njihov odabir da biste ih spojili.", - "store_enable_hint": "Omogućite online funkcije u Postavkama da biste mogli pregledavati i uvoziti referentne cikluse zajednice.", - "advisory_duration_outlier": "Ciklusi označeni kao '{name}' ({n}) previše odstupaju od njegove uobičajene dužine da bi se ikada mogli podudariti s njim (najveće odstupanje: {ratio}x). Vjerovatno su pogrešno označeni ili je jedno snimanje obuhvatilo dva pokretanja. Ponovo ih označite ili ih podijelite kako bi prestali iskrivljavati procjenu vremena za ovaj program.", + "store_enable_hint": "Uključite online funkcije u Preferencije > Online i zajednica da biste pregledali i uvezli referentne cikluse zajednice.", + "advisory_duration_outlier": "Ciklusi označeni kao ‘{name}’ ({n}) previše odstupaju od njegove uobičajene dužine da bi se ikada podudarili (najgori: {ratio}x). Vjerovatno su pogrešno označeni ili sadrže dva rada. Ponovo ih označite ili podijelite kako ne bi iskrivljavali procjenu vremena.", "standby_above_stop_title": "Još uvijek povlači {idle} W kada je ciklus završio", "standby_above_stop_sub": "{n} od posljednjih {total} ciklusa. Kašnjenje isključivanja počinje tek kada snaga padne ispod Praga zaustavljanja ({stop} W), pa se ciklusi završavaju sa zakašnjenjem ili ih treba zaustaviti ručno.", - "online_intro": "Dijelite i preuzimajte referentne cikluse s drugim korisnicima WashData. Sve online funkcije su dobrovoljne i podrazumijevano isključene.", "store_share_intro": "Otpremite ovaj referentni ciklus kako bi ga mogli koristiti drugi s istim uređajem. Pregleda se prije nego što postane javno dostupan.", - "online_intro_global": "Pregledavajte i dijelite referentne snimke s drugim korisnicima WashData te potvrđujte unose uređaja. Jedna veza vrijedi za cijelu vašu WashData integraciju; marka i model uređaja postavljaju se za svaki uređaj u odjeljku Osnovno. Sve online funkcije su dobrovoljne i podrazumijevano isključene.", + "online_intro_global": "Pregledajte i dijelite referentne snimke s drugim korisnicima WashData. Jedna veza važi za sve vaše WashData uređaje; marka i model postavljaju se za svaki uređaj pod Osnovno. Online funkcije su isključene dok ih ne uključite.", "online_unavailable": "Online funkcije nisu dostupne na ovom serveru.", "store_picker_offline": "Omogućite online funkcije u zupčaniku postavki da biste birali iz kataloga zajednice.", "pick_brand_first": "Prvo odaberite marku uređaja.", - "model_not_found": "Još nije u katalogu.", - "connect_to_confirm": "Povežite se u zupčaniku postavki da biste potvrdili ili ocijenili.", + "custom_task_limit": "Dostigli ste najveći broj zadataka ({max}).", + "custom_tasks_intro": "Dodajte sve ostalo na šta želite biti podsjećeni. Zadatak dospijeva kada se dostigne bilo koji od intervala; ostavite jedan prazan ili 0 da se koristi samo drugi.", + "maintenance_due_sensor_hint": "Binarni senzor Dospjelo održavanje uključen je dok je neki zadatak dospio; koristite ga u vlastitim automatizacijama obavještenja.", + "maintenance_supporter_intro": "Maintenance Supporter je zasebna integracija koja može mnogo više: bilo koji uređaj ili zadatak, rasporedi prema vremenu, korištenju ili senzoru, obavještenja i historija. Može koristiti brojač ciklusa WashData.", "adopt_settings_hint": "Primijeni postavke otkrivanja i uspoređivanja dijeljenog uređaja na ovaj uređaj", - "include_settings_hint": "Uključi pragove otkrivanja, težine uspoređivanja i ostale parametre u dijeljeni paket", + "include_settings_hint": "Podijelite pragove detekcije i podudaranja ovog uređaja (ne obavještenja, entitete ni cijenu energije). Drugi biraju hoće li ih primijeniti.", "onboard_download": "Preuzmi gotove postavke iz prodavnice zajednice", - "share_consent": "Dijelite stvarne podatke sa svog uređaja. Ne dijelite ako su vaši obrasci korištenja privatni.", "share_device_none": "Još nema postavljenih uređaja. Prvo dodajte uređaj.", "share_guideline_naming": "Koristite jasna imena programa (npr. 'Cotton 40', 'Eco 60') kako bi ih drugi mogli prepoznati", - "share_guideline_quality": "Dijelite samo cikluse koji su normalno završili -- bez prekida usred ciklusa, otvaranja vrata ili skokova napajanja.", + "share_guideline_quality": "Dijelite samo cikluse koji su normalno završili: bez prekida, otvaranja vrata i nestanaka struje.", "share_guideline_review": "Pregledajte profile prije dijeljenja -- uklonite one koji izgledaju pogrešno", "share_guidelines_title": "Prije dijeljenja", - "store_download_device_intro": "Preuzmite postavke uređaja iz zajednice i primijenite ih na novi ili postojeći uređaj", + "store_download_device_intro": "Dodajte svom uređaju sve podijeljene programe i njihove referentne cikluse. Vaši vlastiti ciklusi i statistika ostaju netaknuti.", "store_share_device_intro": "Otpremite {brand} {model} s referentnim ciklusima koje odaberete. Drugi s istim uređajem mogu preuzeti vaše programe. Unosi se pregledaju prije nego što postanu javni.", "share_profile_no_cycles": "Nema referentnih ciklusa - označite ciklus sa ⭐ u kartici Ciklusi da uključite ovaj profil", - "advisory_phase_inconsistent": "Čini se da '{name}' miješa različite programe ili temperature - njegovi ciklusi se griju vrlo različito dugo. Podjela na zasebne profile (npr. po temperaturi) poboljšat će podudaranje i procjene vremena.", - "advisory_phase_inconsistent_title": "⚠ Možda pomiješani programi", - "export_select_intro": "Označite tačno šta treba uključiti. Odabir profila bez njihovih ciklusa i dalje izvozi prepoznatljiv program (njegov naučeni oblik prenosi se s njim).", - "import_analyze_hint": "Učitajte izvezenu datoteku (ili zalijepite njen JSON). WashData je analizira i tačno prikazuje šta se može uvesti prije nego što se išta promijeni.", + "export_select_intro": "Označite šta uključiti. Profili bez svojih ciklusa i dalje izvoze prepoznatljiv program.", + "import_analyze_hint": "Učitajte izvezenu datoteku (ili zalijepite njen JSON). WashData pokazuje šta se može uvesti prije nego što se išta promijeni.", "analyzing": "Analiziranje…", - "device_type_mismatch_warn": "Ovaj izvoz je s druge vrste uređaja ({src} vs {local}). Programe i cikluse i dalje je moguće uvesti kao referentne podatke, ali postavke specifične za uređaj i uvoz stvarne historije su onemogućeni.", + "device_type_mismatch_warn": "Ovaj izvoz je s druge vrste uređaja ({src} vs {local}). Programe i cikluse i dalje možete uvesti kao referentne podatke, ali ne i postavke uređaja ni stvarnu historiju.", "merge_hint": "Uvezene stavke se dodaju; ništa lokalno se ne gubi. Sukobi naziva rješavaju se u nastavku.", "replace_warn": "Svaka označena kategorija briše se i zamjenjuje podacima iz datoteke. Neoznačene kategorije ostaju nepromijenjene.", "dest_reference_hint": "Uvezeni ciklusi samo poboljšavaju prepoznavanje programa i nikada ne utiču na statistiku potrošnje/energije.", - "dest_real_history_hint": "Uvezeni ciklusi računaju se kao vlastita historija ovog uređaja i doprinose statistici energije/potrošnje. Koristite pri premještanju jednog uređaja na novu instalaciju.", - "import_history_description": "Imali ste pametni utikač i prije WashData? Učitajte izvoz historije njegovog senzora snage ili je pročitajte direktno iz Home Assistant, a obično otkrivanje proći će kroz te podatke, pa će se stari ciklusi pojaviti na vašoj listi Ciklusi, spremni za imenovanje.", - "hist_input_hint": "Učitajte CSV preuzet s panela Historija (entitet, stanje, posljednja promjena) ili pustite WashData da direktno pročita historiju senzora. Otkrivanje se onda izvodi isto kao i uživo, a vi birate koje od pronađenih ciklusa ćete zadržati.", - "hist_recorder_hint": "Čita podatke od izabranog datuma do sada. Home Assistant standardno čuva detaljnu historiju 10 dana, a nakon toga samo satne prosjeke, koji su previše grubi za otkrivanje ciklusa - raniji datum birajte samo ako je vaš recorder postavljen na duže čuvanje.", - "hist_scanning": "Vašu historiju ponovo provodimo kroz detektor. Ovo se izvodi u pozadini - možete zatvoriti ovaj dijalog i vratiti se kasnije.", + "dest_real_history_hint": "Uvezeni ciklusi računaju se kao vlastita historija i statistika ovog uređaja. Koristite pri premještanju uređaja na novu instalaciju.", + "import_history_description": "Imali ste pametni utikač prije WashData? Učitajte njegovu historiju snage ili je pročitajte iz Home Assistanta. Detekcija prolazi kroz te podatke, a stari ciklusi pojavljuju se na listi Ciklusi spremni za imenovanje.", + "hist_input_hint": "Učitajte CSV s panela Historija (entitet, stanje, posljednja promjena) ili pustite WashData da direktno pročita historiju senzora. Detekcija uživo prolazi kroz te podatke, a vi birate koje cikluse zadržati.", + "hist_recorder_hint": "Čita od odabranog datuma do sada. Home Assistant standardno čuva detaljnu historiju 10 dana; stariji satni prosjeci pregrubi su za detekciju ciklusa.", + "hist_scanning": "Vaša historija prolazi kroz detektor u pozadini. Možete zatvoriti ovaj dijalog i vratiti se kasnije.", "hist_imported_count": "Uvezeni ciklusi: {n}.", "hist_duplicates": "Već ranije uvezeno i preskočeno: {n}.", "hist_capped": "Dostignuto je ograničenje uvezenih ciklusa po uređaju; ostatak nije sačuvan.", - "hist_next_step": "Nalaze se na vašoj listi Ciklusi, obilježeni kao uvezena historija. Otvorite jedan i tasterom Označi mu dodijelite naziv programa kojem pripada.", + "hist_next_step": "Nalaze se na listi Ciklusi, označeni kao uvezena historija. Otvorite jedan i dugmetom Označi imenujte njegov program.", "hist_rows_read": "Pročitana očitanja: {n}", "hist_breaks": "Praznine u kojima senzor nije bio dostupan: {n}", "hist_other_entity": "Zanemarena očitanja drugih entiteta: {n}", @@ -1095,25 +1038,74 @@ "hist_scan_capped": "Prikazani su samo prvi kandidati (pronađeno je {n}).", "hist_recorder_empty": "Home Assistant nema detaljnu historiju za ovaj senzor u tom periodu.", "hist_scan_failed": "Pretraživanje nije uspjelo.", - "hist_scan_expired": "To pretraživanje više nije dostupno. Pokrenite ga ponovo.", + "hist_scan_expired": "To skeniranje više nije dostupno. Skenirajte 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.", - "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.", + "imported_history_readonly": "Pronađeno u uvezenoj historiji snage. Pomaže podudaranju, ali se ne broji u statistiku i ne može se skratiti ni podijeliti. Označite ga da imenujete program.", + "odometer_intro": "Ovaj zbir samo raste. Brisanje ciklusa ga ne smanjuje, pa podsjetnici za servis ostaju tačni.", + "odometer_correct_hint": "Postavite ovo ako je WashData izbrojao ciklus koji se nije dogodio ili je uređaj radio cikluse prije instalacije WashData.", "no_reminders_set": "Još nema postavljenih podsjetnika za servis.", "toast_program_armed": "Program postavljen za sljedeći ciklus: {program}", "toast_range_required": "Postavite ispravan raspon Od/Do", - "no_profiles_option": "(nema profila, dodajte novi)" + "no_profiles_option": "(nema profila, dodajte novi)", + "compare_cycles_title": "Uporedi cikluse ({count})", + "edit_builtin_phase": "Ovo je ugrađena faza. Spremanjem se kreira vlastita kopija; izbrišite kopiju da vratite original.", + "fetch_error": "Učitavanje podataka nije uspjelo.", + "connection_lost": "Veza je prekinuta - prikazano je stanje u {time}.", + "pg_notify_partial": "Oznake prikazuju samo obavještenja o početku, skorom kraju, završetku, prekretnicama i tihim satima. Ažuriranja uživo, podsjetnici za istovar i upozorenja o prekoračenju ne simuliraju se.", + "connect_to_confirm": "Povežite se u zupčaniku postavki da biste potvrdili ili ocijenili.", + "coverage_gap_cluster": "Nedavni neoznačeni ciklusi ({count}) od oko {min} min izgledaju kao jedan program koji još niste kreirali.", + "pg_label_yes": "Na kraju ciklusa bio bi označen kao \"{name}\"", + "pg_label_no": "Bio bi poslan na pregled: {reason}", + "live_match_debug_tip": "Pouzdanost: koliko tačno trenutna kriva snage odgovara najboljem profilu kandidatu (0-100%). Dvosmisleno: rezultati dva najbolja kandidata razlikuju se za manje od 5%, pa je etiketa nesigurna dok se ciklus ne završi.", + "automation_alias_started": "{device}: ciklus je pokrenut", + "automation_alias_finished": "{device}: ciklus je završen", + "automation_desc_started": "Pokreće se kada započne ciklus uređaja {device}. Dodajte svoje akcije (obavještenja, svjetla, ...).", + "automation_desc_finished": "Pokreće se kada se završi ciklus uređaja {device}. Dodajte svoje akcije (obavještenja, svjetla, ...).", + "feedback_detected": "WashData je prepoznao: {name}", + "import_undo_hint": "Čuva se tačka vraćanja od prije posljednjeg uvoza ({when}): {profiles} profila i {cycles} ciklusa.", + "replace_undo_hint": "Prvo se sprema tačka vraćanja, pa ovaj uvoz možete poništiti u odjeljku Izvoz/uvoz.", + "pg_in_sample_note": "Ovi ciklusi su pomogli izgraditi profile s kojima se upoređuju, pa su rezultati optimistični: na novim ciklusima očekujte nešto manje tačnih podudaranja.", + "hist_tail_trimmed": "Odrezano je {min} min čekanja nakon završetka ciklusa, kao što to radi integracija tokom rada", + "hist_truncated": "zaustavljeno na ograničenju očitanja ({n}), pa ostatak historije nije pregledan", + "hist_warn_kw": "Najveće očitanje je samo {w}. Izgleda da ovaj senzor javlja kilovate; WashData treba vate, pa se ciklusi ne mogu otkriti.", + "hist_warn_naive_time": "{n} vremenskih oznaka nema vremensku zonu i pročitano je kao UTC. Ako datoteka koristi lokalno vrijeme, ciklusi su pomjereni za vaš pomak od UTC-a, a očitanja oko promjene vremena mogu se izgubiti.", + "hist_warn_naive_time_one": "{n} vremenska oznaka nema vremensku zonu i pročitana je kao UTC. Ako datoteka koristi lokalno vrijeme, ciklusi su pomjereni za vaš pomak od UTC-a, a očitanja oko promjene vremena mogu se izgubiti.", + "hist_warn_naive_time_few": "{n} vremenske oznake nemaju vremensku zonu i pročitane su kao UTC. Ako datoteka koristi lokalno vrijeme, ciklusi su pomjereni za vaš pomak od UTC-a, a očitanja oko promjene vremena mogu se izgubiti.", + "hist_warn_naive_time_other": "{n} vremenskih oznaka nema vremensku zonu i pročitano je kao UTC. Ako datoteka koristi lokalno vrijeme, ciklusi su pomjereni za vaš pomak od UTC-a, a očitanja oko promjene vremena mogu se izgubiti.", + "pg_compare_profile_tip": "Bira samo profil iscrtan preko grafikona. Simulacija uvijek sama traži odgovarajući program za ciklus, kao i integracija tokom rada.", + "pg_outcome_stale": "Postavke su se promijenile nakon pokretanja ove simulacije. I dalje prikazuje stari rezultat.", + "pg_sweep_guarded": "Prerano završava više ciklusa, dijeli više njih ili propušta cikluse koje trenutna postavka hvata, pa se nikada ne preporučuje.", + "pg_sweep_keep_current": "Zadrži trenutnu vrijednost ({value}, {metric}): nijedna vrijednost nije bila bolja za barem jedan ciklus.", + "pg_sweep_n_cycles": "na {count} ciklusa", + "pg_sweep_n_cycles_one": "na {count} ciklusu", + "pg_sweep_n_cycles_few": "na {count} ciklusa", + "pg_sweep_n_cycles_other": "na {count} ciklusa", + "pg_task_busy": "Za ovaj uređaj već je u toku „Provjeri na historiji” ili „Optimiziraj”. Sačekajte da završi ili ga otkažite.", + "cycle_context_loading": "Učitavanje historije recordera…", + "cycle_context_note": "Sivo: historija senzora snage iz recordera oko ovog ciklusa. Nije dio ciklusa i ne utiče na trajanje, energiju ni podudaranje.", + "cycle_context_none": "Recorder nema očitanja oko ovog ciklusa (Home Assistant standardno čuva historiju 10 dana).", + "cycle_context_tip": "Samo za prikaz: pokazuje historiju senzora snage iz recordera oko ovog ciklusa, ako je još sačuvana. Da biste neuspjele pokušaje pokretanja dodali budućim ciklusima, koristite Uvodne sekunde krivulje (Postavke > Detekcija > Početak ciklusa), što mijenja sačuvana trajanja.", + "invalid_threshold_pair": "Prag zaustavljanja ({stop} W) mora biti ispod Praga pokretanja ({start} W). Ništa nije sačuvano; promijenite obje vrijednosti zajedno." }, "phase_desc": { "anti_crease": "Povremena kratka spuštanja nakon završetka za smanjenje bora.", "anti_wrinkle": "Periodično prevrtanje nakon ciklusa za smanjenje bora.", + "baking": "Grijač na visokoj temperaturi stvara koru i sredinu hljeba.", + "cooking": "Glavna faza pripreme s uključenim grijačem i ventilatorom.", "cool_down": "Vrtanje bez grijanja blizu kraja ciklusa.", + "cool_down_af": "Hlađenje samo ventilatorom nakon grijanja.", "drain_and_switch": "Period prijelaza sa pranja na način sušenja.", "dry": "Faza sušenja pomoću grijača i/ili preostale topline.", "drying": "Glavni period grijanja.", "heat_up": "Početno zagrijavanje grijača prije početka potpunog sušenja.", + "keep_warm": "Lagano grijanje da hrana ostane topla.", + "keep_warm_bm": "Lagano grijanje da hljeb ostane topao nakon pečenja.", + "kneading": "Motor miješa i mijesi tijesto. Velika potrošnja snage.", + "pause_af": "Kratka pauza za protresanje ili provjeru.", + "pre_heat": "Početno zagrijavanje komore prije pune pripreme.", "pre_rinse": "Prvo prskanje prije pranja deterdžentom.", + "proving": "Narastanje na niskoj toplini: kvasac fermentira i tijesto se širi.", + "resting": "Kratka pauza male snage između miješenja da se gluten opusti.", "pre_wash": "Prvo namakanje ili predtretman prije glavnog pranja.", "rinse": "Faza ispiranja čistom vodom. Ova faza se može ponoviti više puta.", "sanitize": "Faza čišćenja na visokim temperaturama za programe dezinfekcije.", @@ -1126,6 +1118,7 @@ "wash_dw": "Glavni deterdžent za pranje sa grijanjem." }, "placeholder": { + "custom_task": "npr. Očisti brtvu vrata", "add": "dodaj…", "group_name": "npr. Pamuk 2:47", "json_buttons": "[{\"action\":\"ID\",\"title\":\"Label\"}]", @@ -1153,10 +1146,10 @@ "section": { "dishwasher": { "label": "Mašina za pranje posuđa", - "intro": "Obrada kraja ciklusa za mašine za pranje posuđa, koje obično završavaju dugom, gotovo tihom fazom sušenja prije kratkog završnog ispumpavanja vode." + "intro": "Obrada kraja za mašine za pranje posuđa, koje često dugo i tiho suše prije kratkog završnog ispumpavanja." }, "anti_wrinkle": { - "intro": "Mod anti-gužvanja/anti-nabora otkriva impulse bubnjevanja niske snage nakon glavne faze i drži ih vezane za završeni ciklus.", + "intro": "Zadržava kratke impulse prevrtanja niske snage nakon glavne faze sušilice uz završeni ciklus.", "label": "Anti-gužvanje" }, "basic": { @@ -1164,7 +1157,7 @@ "label": "Osnovno" }, "delay": { - "intro": "Detekcija odloženog starta prepoznaje kada je uređaj uključen ali još nije počeo ciklus.", + "intro": "Otkriva uređaj koji je uključen i čeka odgođeni početak.", "label": "Odloženi start" }, "detection": { @@ -1188,161 +1181,153 @@ "label": "Tajming i čuvar" }, "triggers": { - "intro": "Opcioni vanjski signali: okidač kraja, senzor vrata, prekidač za pauzu i podsjetnik za istovar.", + "intro": "Opcioni dodaci: okidač završetka, senzor vrata, prekidač za pauzu i podsjetnik za istovar.", "label": "Okidači i vrata" - }, - "phase_eta": { - "label": "Preostalo vrijeme", - "intro": "Preostalo vrijeme koje uzima u obzir faze, za uređaje čija dužina ciklusa ovisi o temperaturi ili centrifugi." } }, "setting": { "dishwasher_end_spike_quiet_release": { "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." + "doc": "Nakon očekivanog trajanja: koliko dugo mašina za suđe mora ostati ispod Praga zaustavljanja prije nego što WashData prestane čekati završno ispumpavanje. Povećajte ako se kasno ispumpavanje nakon dugog sušenja propusti. Čekanje nikada ne prelazi 30 minuta." }, "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.", + "doc": "Udio prosječnog trajanja programa koji ciklus mora dostići prije nego što se prevrtanje niske snage računa kao rep anti-gužvanja. Smanjite ga (npr. 0,75) kod sušilica koje se prilagođavaju količini veša. Kod mašina za veš ostavite zadano (0,98), inače pranje može završiti na pola.", "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.", + "doc": "Zadržite kratke impulse prevrtanja niske snage nakon glavne faze grijanja sušilice uz završeni ciklus, umjesto da se čitaju kao novi ciklusi.", "label": "Omogućiti detekciju anti-gužvanja" }, "anti_wrinkle_exit_power": { - "doc": "Snaga mora pasti ispod ove između impulsa da bi režim protiv bora ostao aktivan.", + "doc": "U režimu anti-gužvanja snaga ispod ove vrijednosti smatra se mirovanjem između impulsa okretanja, a mirovanje duže od Maksimalnog razmaka između impulsa završava režim. Ako je Prag zaustavljanja viši, koristi se on, pa ova postavka nešto mijenja samo iznad Praga zaustavljanja.", "label": "Prag izlazne snage" }, "anti_wrinkle_idle_timeout": { - "doc": "Koliko dugo mašina smije ostati mirna između dva impulsa bubnja prije nego što se režim anti-gužvanja završi. Postavite ga iznad najdužeg razmaka koji vaša sušilica ostavlja između impulsa, inače se svaki naredni impuls čita kao lažni početak.", + "doc": "Koliko dugo sušilica smije mirovati između impulsa prevrtanja prije nego što režim anti-gužvanja završi. Postavite iznad najdužeg razmaka vaše sušilice, inače se kasniji impulsi čitaju kao lažni počeci.", "label": "Maksimalni razmak između impulsa" }, "anti_wrinkle_max_duration": { - "doc": "Pulsevi duži od ovog tretiraju se kao pravi ciklus, a ne kao nalet protiv bora.", + "doc": "Impuls duži od ovoga računa se kao pravi ciklus, a ne kao prevrtanje protiv gužvanja.", "label": "Maksimalno trajanje" }, "anti_wrinkle_max_power": { - "doc": "Puls iznad ove snage tretira se kao pravi novi ciklus, a ne kao protiv bora. Postavite tik iznad snage impulsa prevrtanja.", + "doc": "Impuls iznad ove snage računa se kao pravi novi ciklus, a ne kao prevrtanje. Postavite malo iznad snage impulsa prevrtanja.", "label": "Maks snaga anti-gužvanja" }, "auto_label_confidence": { - "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.", + "doc": "Rezultat podudaranja na kraju ciklusa od kojeg se program označava bez pitanja. Povećajte za veću sigurnost; smanjite da se automatski označi više ciklusa. Držite iznad Pouzdanosti učenja.", "label": "Pouzdanost automatskog označavanja" }, "auto_maintenance": { - "doc": "Pokrenite noćno održavanje: obnovite omote profila, ponovo izračunajte zdravlje ciklusa, smanjite tragove otklanjanja grešaka i zadržite najnovije cikluse.", + "doc": "Noćno čišćenje: označava starije cikluse koji sada jasno odgovaraju programu, ponovo gradi krivulje programa, osvježava oznake anomalija i zdravlje ciklusa te uklanja tragove otklanjanja grešaka ako je Sačuvati tragove otklanjanja grešaka isključeno. Ciklusi i njihovi zapisi snage uvijek se čuvaju.", "label": "Automatsko održavanje (noćno čišćenje)" }, "completion_min_seconds": { - "doc": "Ciklusi kraći od ovoga se odbacuju kao ciklusi duhova (probni radovi, otvaranje vrata da se doda čarapa).", + "doc": "Ciklusi kraći od ovoga (probni radovi, otvaranje vrata radi dodavanja čarape) spremaju se kao prekinuti i ne dobijaju obavještenje o završetku.", "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.", + "doc": "Dodaje očitanja iz neuspjelih pokušaja pokretanja na početak ciklusa, za mašine koje se isprobavaju prije pokretanja (zaključavanje vrata, prvo punjenje). 300 s je dovoljno za većinu; 0 = isključeno. Zabilježeni počeci pomjeraju se ranije, pa se naučena trajanja neko vrijeme mijenjaju.", "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.", + "doc": "Snaga mora ostati u pojasu pripravnosti ovoliko dugo prije nego što se uređaj računa kao da čeka početak.", "label": "Sekunde potvrde" }, "delay_start_detect_enabled": { - "doc": "Otkrijte kada je uređaj uključen i čeka (odgođeni početak/pripravnost), ali nije započeo svoj ciklus, tako da se povlačenje u pripravno stanje ne smatra greškom u radu.", + "doc": "Otkrijte uređaj koji je uključen i čeka odgođeni početak, kako se njegova potrošnja u pripravnosti ne bi smatrala radom ciklusa.", "label": "Omogućiti detekciju odloženog starta" }, "delay_timeout_hours": { - "doc": "Prestanite čekati u režimu odgođenog pokretanja nakon ovoliko sati i vratite se u stanje mirovanja, tako da mašina koja je ostala uključena, ali nikada nije pokrenuta, ne čeka zauvijek.", + "doc": "Prestanite čekati odgođeni početak nakon ovoliko sati i vratite se u mirovanje.", "label": "Sati timeouta" }, "device_type": { - "doc": "Klasa aparata. Postavlja razumne zadane vrijednosti detekcije (pragovi, kašnjenje isključenja, rukovanje na kraju) podešene za taj tip uređaja; promijenite ga samo ako je uređaj prvobitno podešen kao pogrešan tip.", + "doc": "Postavlja zadane vrijednosti detekcije (pragovi, kašnjenje isključivanja, obrada kraja) za ovu vrstu uređaja. Mijenjajte samo ako je uređaj postavljen kao pogrešan tip.", "label": "Tip uređaja" }, "door_sensor_entity": { - "doc": "Opcioni binarni senzor vrata. Koristi se za otkrivanje kada je uređaj otvoren/istovaren nakon ciklusa.", + "doc": "Opcioni senzor vrata. Pokazuje kad je uređaj otvoren radi istovara nakon ciklusa.", "label": "Entitet senzora vrata" }, "duration_tolerance": { - "doc": "Tolerancija za procjene preostalog vremena (povratne informacije učenja, ne podudaranje). Ako je stvarno trajanje unutar +/-X% procjene, to se računa kao dobro podudaranje.", + "doc": "Samo za povratne informacije učenja: ciklus unutar +/-X% procijenjenog trajanja računa se kao dobra procjena. Ne utiče na podudaranje.", "label": "Tolerancija procjene" }, "enable_ml_models": { - "doc": "Dok ciklus traje, pustite modele da preciziraju rezultate uživo: stabilniju procjenu preostalog vremena i energije/troškova, i zaštitu od preranog zaustavljanja pri detekciji kraja (može samo odgoditi završetak, nikada ne završiti ranije, i ograničen je). Koristi vaše fino podešene modele kada su dostupni, inače ugrađene. Isključeno = samo klasična logika zasnovana na napajanju (i dalje pouzdana).", + "doc": "Koristite model treniran na ovoj mašini za predviđanje energije i troška ciklusa koji traje, i dozvolite dobro poznatom programu koji neobično rano stane da završi bez punog kašnjenja isključivanja. Isključeno: samo logika zasnovana na snazi.", "label": "Primijeniti pametne modele tokom ciklusa" }, "end_energy_threshold": { - "doc": "Tokom odbrojavanja sa kašnjenjem, akumulirana energija (vati x vrijeme) se upoređuje sa ovim pragom. Ako se prekorači, odbrojavanje se poništava - držeći klupe protiv gužvanja i repove za sušenje u mašini za pranje sudova pričvršćene za ciklus umesto da ih skrate. Podignite ga ako se ciklusi završe prerano tokom hlađenja; snizite ako je detekcija spora.", + "doc": "Energija dozvoljena tokom kašnjenja isključivanja. Iznad toga ciklus se nastavlja, pa prevrtanja i završno sušenje ostaju dio ciklusa. Smanjite ako ciklusi završavaju tokom repa; povećajte ako završeni ciklusi i dalje čekaju.", "label": "Završna energija" }, - "end_repeat_count": { - "doc": "Broj uzastopnih očitavanja ispod praga zaustavljanja potrebnih prije završetka ciklusa. 1 je u redu za većinu utikača. Povećajte na 2-3 ako vaš pametni utikač povremeno prijavi uzorak lažne nule usred ciklusa i vaši ciklusi se završavaju prerano.", - "label": "Broj ponavljanja kraja" - }, "energy_price_entity": { - "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.", + "doc": "Senzor s trenutnom cijenom po kWh (npr. dinamička tarifa). Ima prednost nad statičnom cijenom ispod, pa mora biti cijena, a ne senzor snage ili energije utikača.", "label": "Entitet cijene energije" }, "energy_price_static": { - "doc": "Fiksna cijena po kWh koja se koristi za brojke troškova kada nije postavljen nijedan entitet cijene uživo iznad.", + "doc": "Fiksna cijena po kWh, koristi se kad nije postavljen entitet cijene.", "label": "Statična cijena energije (po kWh)" }, "energy_sensor": { - "doc": "Opcionalno kumulativno brojilo energije (total_increasing kWh/Wh, npr. vlastito brojilo ukupne potrošnje utičnice). Kada je postavljeno, energija prijavljena za svaki ciklus uzima se iz razlike očitanja ovog brojila između početka i kraja, čime se izbjegava podbrojavanje koje nastaje integriranjem sporo izvještavajućeg senzora snage. Vraća se na integriranu vrijednost ako očitanje nedostaje, njegova jedinica je nepoznata ili razlika nije pozitivna. Ostavite prazno da nastavite integrirati senzor snage.", + "doc": "Opciono brojilo kWh ili Wh (total_increasing, npr. vlastito brojilo utikača). Energija ciklusa tada se čita iz njega, što je tačnije od izračuna iz sporog senzora snage. Ako je njegovo očitanje neupotrebljivo, koristi se izračunata vrijednost.", "label": "Entitet brojila energije" }, "expose_debug_entities": { - "doc": "Objavite dodatne dijagnostičke HA entitete (pouzdanost podudaranja, dvosmislenost, unutrašnje karakteristike stanja). Isključeno održava listu entiteta čistom za normalnu upotrebu.", + "doc": "Dodajte dijagnostičke entitete (pouzdanost podudaranja, dvosmislenost, unutrašnje stanje). Isključeno: lista entiteta ostaje uredna.", "label": "Izložiti entitete za otklanjanje grešaka" }, "external_end_trigger": { - "doc": "Binarni senzor čija promjena stanja označava kraj ciklusa (npr. \"završen\" kontakt uređaja ili integracija pratioca).", + "doc": "Binarni senzor čija promjena označava kraj ciklusa (npr. kontakt \"završeno\" ili druga integracija).", "label": "Entitet vanjskog okidača" }, "external_end_trigger_enabled": { - "doc": "Neka vanjski binarni senzor signalizira kraj ciklusa, uz ugrađenu detekciju zasnovanu na snazi.", + "doc": "Dozvolite binarnom senzoru da signalizira kraj ciklusa, uz detekciju zasnovanu na snazi.", "label": "Omogućiti vanjski okidač kraja" }, "external_end_trigger_inverted": { - "doc": "Tretirajte isključivanje senzora okidača (a ne UKLJUČENO) kao signal za kraj ciklusa.", + "doc": "Smatrajte ISKLJUČIVANJE senzora (a ne uključivanje) signalom kraja.", "label": "Invertirati vanjski okidač (okidač pri ISKLJUČIVANJU)" }, "learning_confidence": { - "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.", + "doc": "Najniži rezultat podudaranja za označavanje završenog ciklusa. Jasan pobjednik se označava; tijesan rezultat ide u red za pregled u Ciklusima. Ispod ove vrijednosti ništa se ne označava niti pita. Držite je ispod Pouzdanosti automatskog označavanja.", "label": "Pouzdanost učenja" }, "linked_device": { - "doc": "Opcionalno ugniježdite ovaj WashData uređaj ispod drugog uređaja (npr. pametnog priključka) u registar HA uređaja, prikazan kao \"Povezan putem ...\".", + "doc": "Opciono. Prikažite ovaj WashData uređaj ispod drugog uređaja (npr. pametnog utikača) u Home Assistantu, kao \"Povezano putem ...\".", "label": "Grupisati pod uređaj" }, "min_off_gap": { - "doc": "Ako se mašina isključi na kraće od ovog vremena, sekvenca uključivanja/isključivanja/uključivanja se tretira kao jedan kontinuirani ciklus. Sprečava podjelu programa namakanja (mašina se isključuje nekoliko minuta usred pranja) u dva odvojena ciklusa. Postavite ga kraćim od razmaka između vaših uzastopnih opterećenja ako želite da se oni računaju kao zasebni ciklusi. Zadane postavke tipa uređaja štite tipičnu pauzu unutar ciklusa za svaki uređaj.", + "doc": "Isključenje kraće od ovoga ne dijeli ciklus, pa pauze namakanja ostaju u jednom pranju. Držite kraće od razmaka između uzastopnih punjenja, inače se ona spajaju u jedan ciklus.", "label": "Min razmak isključivanja" }, "min_power": { - "doc": "Apsolutna minimalna snaga smatra se 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.", + "doc": "Očitanja ispod ove vrijednosti računaju se kao 0 W (pripravnost). Filtrira potrošnju u mirovanju pametnih utikača i LED lampica pripravnosti.", "label": "Minimalna snaga" }, "ml_training_enabled": { - "doc": "Periodično proučavajte svoje recenzirane cikluse preko noći i fino prilagodite modele ovoj specifičnoj mašini. Promjena se zadržava samo kada zaista ima bolje rezultate u ciklusima koji se zadržavaju, tako da ovo može samo pomoći ili ostati ista - nikada ne nazadovati.", + "doc": "Povremeno ponovo trenirajte model energije na završenim ciklusima ove mašine. Novi model se zadržava samo ako na testnim ciklusima nadmaši jednostavnu procjenu.", "label": "Učiti od ove mašine" }, "ml_training_hour": { - "doc": "Lokalni sat u danu (0-23) za fino podešavanje preko noći. Odaberite mirni sat kao što je 2 (02:00).", + "doc": "Sat u danu (0-23) za noćno treniranje. Odaberite miran sat, npr. 2.", "label": "Učiti u satu" }, "ml_training_interval_days": { - "doc": "Ponovo provjerite ima li poboljšanja najviše jednom u ovoliko dana.", + "doc": "Ponovo trenirajte najviše jednom u ovoliko dana.", "label": "Provjeravati najvjše svakih" }, "ml_training_min_cycles": { - "doc": "Pričekajte dok se barem ovoliko ciklusa ne snimi prije finog podešavanja, tako da ima dovoljno za učiti.", + "doc": "Prije treniranja sačekajte barem ovoliko zabilježenih ciklusa.", "label": "Ciklusi potrebni prvo" }, "name": { - "doc": "Ime za prikaz prikazano na listi HA integracija i registru uređaja.", + "doc": "Naziv ovog uređaja prikazan u Home Assistantu.", "label": "Naziv uređaja" }, "no_update_active_timeout": { - "doc": "Ako ne stigne ažuriranje napajanja ovoliko dugo dok radi, pretpostavite da je utikač ispao van mreže i prisilno se zaustavite kako biste izbjegli zombi ciklus. Zadanih 600 s dozvoljava kašnjenje u oblaku ili mreži.", + "doc": "Ako tokom ciklusa ovoliko dugo ne stigne nijedno ažuriranje snage, pretpostavlja se da je utikač van mreže i ciklus se prisilno zaustavlja. Zadanih 600 s pokriva kašnjenja oblaka ili mesh mreže.", "label": "Timeout bez ažuriranja" }, "notify_before_end_minutes": { @@ -1350,35 +1335,35 @@ "label": "Upozorenje pred kraj" }, "notify_channel": { - "doc": "Naziv kanala Android obavijesti za početne/uživo poruke (kontrolira zvuk po kanalu i prioritet na mobilnoj aplikaciji). Prazno koristi zadanu prateću aplikaciju.", + "doc": "Android kanal za poruke o početku i uživo; kanal određuje zvuk i prioritet. Prazno: zadani kanal aplikacije.", "label": "Android kanal (pokretanje/uživo)" }, "notify_cycle_timers": { - "doc": "Obavještenja u određenim minutama u ciklusu (npr. za dodavanje omekšivača). Poruka podržava {device}, {program}, {minutes}. Omogućite automatsku pauzu da biste pauzirali u tom trenutku i primili interaktivno obavještenje pomoću gumba Nastavi; nastaviti putem panela, usluge pauze/nastavljanja ili radnje obavještenja.", + "doc": "Obavještenja u zadanim minutama ciklusa (npr. za dodavanje omekšivača). Poruka podržava {device}, {program}, {minutes}. Uz automatsku pauzu ciklus se tu i pauzira, a obavještenje dobija dugme Nastavi.", "label": "Tajmeri ciklusa" }, "notify_finish_channel": { - "doc": "Naziv kanala Android obavještenja za završnu poruku. Blank ponovo koristi početni/uživo kanal.", + "doc": "Android kanal za poruku o završetku. Prazno: koristi se kanal za početak i uživo.", "label": "Android kanal (završetak)" }, "notify_finish_message": { - "doc": "Tekst poslan kada se ciklus završi. Varijable predloška: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}. {time_finished} i {vs_typical} su ovdje najkorisniji.", + "doc": "Tekst koji se šalje kada ciklus završi. Varijable: {device}, {program}, {duration} (minute), {duration_hm} (1 h 05 min), {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}, {status} (completed ili force_stopped; prekinuti ciklusi ne šalju ništa). Za svaku drugu varijablu koristi se zadani tekst.", "label": "Poruka završetka" }, "notify_finish_services": { - "doc": "notify.* usluge se pozivaju kada se ciklus završi. Dodajte jednu po meti; ostavite prazno da nema obavještenja o završetku.", + "doc": "notify.* usluge koje se pozivaju kad ciklus završi. Ostavite prazno za nijednu.", "label": "Usluge završetka" }, "notify_fire_events": { - "doc": "Također aktivirajte ha_washdata_* događaje na početku/završetku ciklusa kako biste mogli izgraditi vlastite automatizacije.", + "doc": "Pokrenite i događaje ha_washdata_* pri početku i završetku ciklusa za vlastite automatizacije.", "label": "Pokrenuti HA događaje za obavijesti" }, "notify_icon": { - "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.", + "doc": "Mala ikona na obavještenju (npr. mdi:washing-machine). Šalje se samo na mobile_app ciljeve, ne na notify grupe. Na iOS-u zamjenjuje ikonu aplikacije i zahtijeva mobilnu aplikaciju 2026.8 ili noviju. Prazno: zadana ikona platforme.", "label": "Ikona obavijesti" }, "notify_live_chronometer": { - "doc": "Prikaži tajmer za odbrojavanje koji se ažurira uživo u obavještenju (na platformama koje ga podržavaju) umjesto statične procjene.", + "doc": "Prikažite odbrojavanje uživo u obavještenju (gdje je podržano) umjesto fiksne procjene.", "label": "Koristiti hronometar uživo" }, "notify_live_interval_seconds": { @@ -1386,143 +1371,135 @@ "label": "Interval ažuriranja uživo" }, "notify_live_overrun_percent": { - "doc": "Ako ciklus prijeđe svoju procjenu za više od ovog postotka, pošaljite upozorenje o prekoračenju.", + "doc": "Ažuriranja uživo prestaju kada ciklus premaši očekivano trajanje za ovaj procenat (20 = do 1,2x očekivanog vremena).", "label": "Prekoračenje uživo % prije upozorenja" }, "notify_live_services": { - "doc": "notify.* usluge pozivaju na ažuriranja napretka uživo dok ciklus traje. Ostavite prazno da onemogućite obavještenja o napretku uživo.", + "doc": "notify.* usluge za ažuriranja napretka uživo dok ciklus traje. Ostavite prazno da ih isključite.", "label": "Usluge napretka uživo" }, "notify_only_when_home": { - "doc": "Obavještenja šaljite samo kada je barem jedna od povezanih osoba (gore) kod kuće.", + "doc": "Obavještavajte samo kad je barem jedna od osoba iznad kod kuće.", "label": "Obavijesti samo dok su kod kuće" }, "notify_people": { - "doc": "osoba.* entiteti koje koristi \"Notify Only When Home\" da odluči da li je neko kod kuće.", + "doc": "person.* entiteti koje provjerava \"Obavijesti samo dok su kod kuće\".", "label": "Osobe (za samo dok su kod kuće)" }, "notify_pre_complete_message": { - "doc": "Tekst upozorenja prije kraja / skoro gotovo. Varijable predloška: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Tekst svakog ažuriranja napretka uživo. Varijable predloška: {device}, {program}, {minutes} (preostale minute). Za svaku drugu varijablu koristi se zadani tekst.", "label": "Poruka pred završetak" }, "notify_reminder_message": { - "doc": "Tekst podsjetnika za još neizvađeno rublje. Prazno koristi ugrađenu zadanu vrijednost. Varijable predloška: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Tekst jednokratnog Upozorenja pred kraj. Varijable predloška: {device}, {program}, {minutes} (postavka Upozorenje pred kraj). Za svaku drugu varijablu koristi se zadani tekst.", "label": "Poruka podsjetnika" }, "notify_start_message": { - "doc": "Tekst poslan kada ciklus započne. Varijable predloška: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Tekst koji se šalje kada ciklus počne. Varijable predloška: {device}, {program}. Za svaku drugu varijablu koristi se zadani tekst.", "label": "Poruka pokretanja" }, "notify_start_services": { - "doc": "notify.* usluge koje se pozivaju kada ciklus započne. Dodajte jedan po meti (telefon, kontrolna tabla, itd.); ostavite prazno da nema obaveštenja o početku.", + "doc": "notify.* usluge koje se pozivaju kad ciklus počne (telefon, kontrolna tabla, ...). Ostavite prazno za nijednu.", "label": "Usluge pokretanja" }, "notify_timeout_seconds": { - "doc": "Automatski odbacite obavještenje nakon ovoliko sekundi (na platformama koje ga podržavaju). 0 ga čuva dok se ne odbaci ručno.", + "doc": "Zatvorite obavještenje nakon ovoliko sekundi (gdje je podržano). 0 ga zadržava dok ga ne zatvorite.", "label": "Automatski odbaci nakon" }, "notify_title": { - "doc": "Naslov obavještenja. Varijable šablona: {device}, {program}, {duration}, {energy}, {cost}, {date}, {time}, {minutes}.", + "doc": "Naslov obavještenja. Zamjenjuje se samo {device}; za svaku drugu varijablu koristi se zadani naslov.", "label": "Naslov obavijesti" }, "notify_unload_delay_minutes": { - "doc": "Nekoliko minuta nakon završetka ciklusa prije slanja podsjetnika \"istovarite mašinu\" koji još uvijek čeka. Postavite 0 da onemogućite podsjetnik.", + "doc": "Minute nakon završetka ciklusa prije podsjetnika \"istovarite mašinu\". 0 ga isključuje.", "label": "Kašnjenje podsjetnika za istovar" }, "notify_unload_repeat": { - "doc": "Ponovo šalje podsjetnik za istovar svakih \"Kašnjenje podsjetnika za istovar\" minuta dok se istovar ne potvrdi ili dok ne dodirnete \"Prestani podsjećati\" na obavijesti. Potreban je način za potvrdu istovara: Entitet senzora vrata, Entitet potvrde istovara ili opcija \"Ručno potvrdi istovar\" ispod. Dugme za odbacivanje radi na obavijestima mobilne aplikacije Home Assistant (companion app).", + "doc": "Ponovo šaljite podsjetnik za istovar svakih \"Kašnjenje podsjetnika za istovar\" minuta dok se istovar ne potvrdi ili ne dodirnete \"Prestani podsjećati\" (mobilna aplikacija). Zahtijeva Entitet senzora vrata, Entitet potvrde istovara ili \"Ručno potvrdi istovar\".", "label": "Ponavljaj do istovara" }, "off_delay": { - "doc": "Vrijeme čekanja nakon pada struje prije nego što se ciklus proglasi završenim. Ako se napajanje nastavi unutar ovog prozora, ciklus se nastavlja neometano - ovo premošćuje pauze između faza pranja. Mašine za pranje posuđa imaju duge faze sušenja (isključenje na 20-60 min) tako da kašnjenje isključenja mora premašiti to da bi se cijelo pranje+sušilo kao jedan ciklus.", + "doc": "Koliko dugo snaga mora ostati niska prije nego što ciklus završi. Povratak snage u tom vremenu nastavlja ciklus. Mašine za suđe mogu mirovati 20-60 min tokom sušenja, pa postavite duže od toga.", "label": "Kašnjenje isključivanja" }, "pause_cuts_power": { - "doc": "Kada je ciklus pauziran, također isključite Switch Entity ispod. Samo za uređaje čiji se utikač može bezbedno iseći usred ciklusa.", + "doc": "Kad je ciklus pauziran, isključite i Entitet prekidača ispod. Samo za uređaje koji sigurno mogu izgubiti napajanje usred ciklusa.", "label": "Pauza također isključuje napajanje (putem prekidača)" }, "power_off_delay": { - "doc": "Koliko dugo snaga mora ostati ispod Praga isključivanja napajanja nakon što se ciklus završi prije nego što se stanje vrati na „Isključeno“. Koristi se samo kada je Prag isključivanja napajanja iznad 0. Provjerava se na pozadinskom ritmu, tako da se efektivno kašnjenje zaokružuje naviše na sljedeći otkucaj isteka stanja.", + "doc": "Koliko dugo snaga mora ostati ispod Praga isključivanja napajanja nakon ciklusa prije nego što se stanje vrati na Isključeno. Koristi se samo kad je taj prag iznad 0. Provjerava se povremeno, pa stvarno čekanje može biti malo duže.", "label": "Kašnjenje isključivanja napajanja" }, "power_off_threshold_w": { - "doc": "Opciono otkrivanje isključenog stanja na osnovu snage. Kada je iznad 0, nakon što se ciklus završi i snaga ostane ispod ovog nivoa tokom Kašnjenja isključivanja napajanja, mašina se tretira kao isključena i stanje se vraća na „Isključeno“. Ostavite na 0 da onemogućite (zadano). Postavite ga iznad stvarnog nivoa isključenog napajanja, a ispod Praga zaustavljanja i ispod potrošnje vaše mašine u stanju pripravnosti (završeno, ali još uključeno); ako nije ispod Praga zaustavljanja, zanemaruje se. Kada je omogućeno, zamjenjuje Kašnjenje poništavanja napretka za povratak na „Isključeno“, tako da završena mašina ostaje na „Završeno/Čisto“ dok se stvarno ne isključi.", + "doc": "Nakon ciklusa snaga ispod ove vrijednosti tokom Kašnjenja isključivanja napajanja postavlja stanje na Isključeno, umjesto Kašnjenja poništavanja napretka. Postavite iznad potrošnje isključenog uređaja, a ispod pripravnosti i Praga zaustavljanja. 0 ga isključuje (zadano).", "label": "Prag isključivanja napajanja" }, "power_sensor": { - "doc": "Entitet senzora koji prijavljuje živu snagu u vatima za ovaj uređaj (npr. sensor.washer_power). Sve detekcije ciklusa se zasnivaju na ovom signalu.", + "doc": "Senzor koji javlja trenutnu snagu ovog uređaja u vatima (npr. sensor.washer_power). Cijela detekcija se zasniva na njemu.", "label": "Senzor snage" }, - "profile_duration_tolerance": { - "doc": "Opseg +/- oko prosečnog trajanja profila koji se koristi tokom uparivanja. 0,25 znači da profil od 60 minuta odgovara ciklusima od 45-75 minuta.", - "label": "Tolerancija trajanja profila" - }, "profile_match_interval": { - "doc": "Koliko često pokušavati uparivanje profila tokom ciklusa rada. Podrazumevano 300 s (5 minuta) balansira brzinu detekcije i CPU.", + "doc": "Koliko često podudarati ciklus u toku s profilima. Zadano 300 s (5 min).", "label": "Interval podudaranja" }, "profile_match_max_duration_ratio": { - "doc": "Maksimalna dužina ciklusa u odnosu na profil. 1.3 znači da ciklus mora biti manji od 130% trajanja profila da bi se podudarao.", + "doc": "Program se odbacuje kad ciklus traje duže od ovog višekratnika njegovog uobičajenog trajanja. 1,8: ciklus 80% duži od programa više mu ne odgovara.", "label": "Maks omjer trajanja" }, "profile_match_min_duration_ratio": { - "doc": "Minimalna dužina ciklusa u odnosu na profil. 0,9 znači da ciklus mora biti najmanje 90% trajanja profila da bi se podudarao.", + "doc": "Program se razmatra tek kad ciklus prođe ovaj udio njegovog uobičajenog trajanja. 0,10: program od 2 sata postaje kandidat nakon 12 minuta. Ne primjenjuje se u prvih 15 minuta ciklusa.", "label": "Min omjer trajanja" }, "profile_match_threshold": { - "doc": "Minimalni rezultat sličnosti (0-1) potreban na kraju ciklusa da bi se prihvatila identifikacija programa. Podignite ga da smanjite pogrešne identifikacije; snizite ga ako se programi vaše mašine ne poklapaju. Zadana vrijednost 0,4 je konzervativna polazna tačka.", + "doc": "Rezultat podudaranja uživo koji ciklus treba prije nego što ga Pametni završetak ili završetak anti-gužvanja smije ranije završiti. Ne mijenja prikazani program. Povećajte ako slaba podudaranja prerano završavaju cikluse; smanjite ako jasna podudaranja čekaju kašnjenje isključivanja.", "label": "Prag podudaranja" }, "profile_unmatch_threshold": { - "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.", + "doc": "Ako rezultat podudaranja uživo padne ispod ove vrijednosti, trenutna pretpostavka programa se briše. Držite malo ispod Praga podudaranja kako je kratak pad ne bi poništio.", "label": "Prag nepodudaranja" }, "progress_reset_delay": { - "doc": "Nakon završetka, držite napredak na 100% ovoliko dugo kako bi Završeno bilo vidljivo na kontrolnoj tabli prije nego što se vratite na stanje mirovanja.", + "doc": "Nakon ciklusa držite napredak na 100% ovoliko dugo prije vraćanja na Mirovanje.", "label": "Kašnjenje poništavanja napretka" }, "pump_stuck_duration": { - "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).", + "doc": "Koliko dugo pumpa smije raditi bez prekida prije nego što se pokrene događaj zaglavljene pumpe.", "label": "Trajanje zaglavljivanja pumpe" }, "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.", + "doc": "Najkraće vrijeme između obrađenih očitanja. Očitanja koja stignu ranije se preskaču (niska očitanja tokom ciklusa se uvijek zadržavaju). Postavite na interval javljanja utikača ili manje; veća vrijednost samo odbacuje očitanja.", "label": "Interval uzorkovanja" }, "save_debug_traces": { - "doc": "Sačuvajte puni trag napajanja i odgovarajuće podatke za otklanjanje grešaka za svaki ciklus. Korisno za rješavanje problema, ali povećava veličinu skladišta.", + "doc": "Uz svaki ciklus čuvajte i podatke otklanjanja grešaka podudaranja (rezultate svih kandidata), za rješavanje problema. Dodaje oko 20 KB po ciklusu.", "label": "Sačuvati tragove otklanjanja grešaka" }, "smart_termination_duration_ratio": { - "doc": "Koliko daleko u očekivanom trajanju podudarajućeg programa ciklus mora biti prije nego što ga Pametni završetak može ranije završiti nakon pada snage. Očekivano trajanje je prosjek programa, pa kod uređaja čije vrijeme rada jako varira - mašine za pranje rublja s hladnom dovodnom vodom zimi u odnosu na toplu ljeti, sušilice sa senzorskim sušenjem, programi ovisni o količini rublja - otprilike polovina svih pokretanja završi kraće od tog prosjeka i nikada ne dobije brzo završavanje, već završi tek putem rezervnog vremenskog ograničenja, nekoliko minuta kasnije. Kod takvih mašina smanjite ovu vrijednost (npr. 0,85) kako bi se ranije završavanje ipak pokrenulo; povećajte je prema 1,0 za oprezniji pristup. Ostavite prazno za zadanu vrijednost (0,98, ili 0,99 za mašine za pranje posuđa). Ciklus može uvijek samo ranije završiti, nikada kasnije, i nikada se ne pokreće kod nejasnog podudaranja ili podudaranja niske pouzdanosti.", + "doc": "Udio prosječnog trajanja programa koji ciklus mora dostići prije nego što ga Pametni završetak smije ranije završiti. Ako vremena rada variraju (temperatura vode, sušenje po senzoru, količina veša), smanjite ga (npr. 0,85). Ostavite prazno za 0,98 (0,99 kod mašina za suđe).", "label": "Omjer pametnog završetka" }, - "smoothing_window": { - "doc": "Koliko je signal sirove snage izglađen. Niska (2) je odzivna, ali bučna; high (5) izglađuje šiljke, ali dodaje zaostajanje.", - "label": "Prozor izglađivanja" - }, "start_duration_threshold": { - "doc": "Snaga mora ostati iznad početne granice ovoliko dugo kako bi se potvrdio pravi početak, sprečavajući prekidače za uključivanje/isključivanje u djeliću sekunde da započnu ciklus.", + "doc": "Snaga mora ostati iznad Praga pokretanja ovoliko dugo prije nego što ciklus počne, kako bi se kratka uključivanja i isključivanja zanemarila.", "label": "Trajanje pokretanja" }, "start_energy_threshold": { - "doc": "Energija (snaga x vrijeme) koju uređaj mora utrošiti prije POKRETA. Kratak skok velike snage ima vrlo nisku energiju i zanemaruje se, sprječavajući lažne startove.", + "doc": "Energija koju uređaj mora potrošiti prije nego što se ciklus računa kao pokrenut. Kratak skok snage troši malo energije, pa ne pokreće ciklus.", "label": "Početna energija" }, "start_threshold_w": { - "doc": "Snaga mora porasti iznad ovog nivoa da potvrdi da je ciklus započeo. Ako ga postavite prenisko, uzrokuje lažno pokretanje iz stanja pripravnosti; propušteni su previsoki i spori programi (hladno punjenje). Mehanizam za prijedloge postavlja ovo tik iznad uočene najniže aktivne snage mašine.", + "doc": "Snaga mora porasti iznad ove vrijednosti da ciklus počne. Prenisko: potrošnja u pripravnosti uzrokuje lažne početke. Previsoko: programi koji sporo kreću (hladno punjenje) se propuštaju.", "label": "Prag pokretanja" }, "stop_threshold_w": { - "doc": "Snaga mora pasti ispod ovog nivoa prije nego što počne odbrojavanje odgode isključenja. Postavite ga ispod početnog praga - jaz između njih je histerezni pojas koji sprečava treperenje. Ako su postavljene previsoko, faze male snage (zadržavanje ispiranja, zaštita od gužvanja) lažno pokreću krajnju sekvencu.", + "doc": "Snaga mora pasti ispod ove vrijednosti prije nego što kašnjenje isključivanja počne odbrojavati. Držite ispod Praga pokretanja. Ako je previsoko, faze niske snage (pauze ispiranja, anti-gužvanje) greškom pokreću odbrojavanje kraja.", "label": "Prag zaustavljanja" }, "switch_entity": { - "doc": "Opcioni prekidač se isključuje u pauzi i ponovo uključuje kada se nastavlja, koristi se zajedno sa \"Pauza takođe prekida napajanje\".", + "doc": "Opcioni prekidač koji se isključuje pri pauzi i ponovo uključuje pri nastavku, uz \"Pauza također isključuje napajanje\".", "label": "Entitet prekidača" }, "watchdog_interval": { - "doc": "Koliko često pozadinski nadzornik provjerava zastoje senzora i protekla vremena. Standardno 30 s.", + "doc": "Koliko često pozadinska provjera traži zastale senzore i istekla vremenska ograničenja. Zadano 30 s.", "label": "Interval čuvara" }, "notify_milestone_message": { @@ -1530,127 +1507,82 @@ "label": "Poruka o prekretnici" }, "notify_milestones": { - "doc": "Zarezima odvojeni brojevi ciklusa koji pokreću jednokratnu obavijest o slavlju kada se dosegnu (npr. 50, 100, 500, 1000). Prazno onemogućava obavijesti o prekretnicama.", + "doc": "Brojevi ciklusa koji pokreću jednokratnu čestitku, odvojeni zarezima (npr. 50, 100, 500, 1000). Prazno ih isključuje.", "label": "Prekretnice ciklusa" }, "notify_quiet_end_hour": { - "doc": "Kraj prozora ne ometaj (0-23). Zadržane obavijesti se dostavljaju u ovaj sat. Ostavite prazno da onemogućite.", + "doc": "Kraj perioda ne ometaj (0-23); zadržana obavještenja tada se šalju. Prazno ga isključuje.", "label": "Kraj tihih sati" }, "notify_quiet_start_hour": { - "doc": "Početak prozora ne ometaj (0-23). Obavijesti o završetku, podsjetnici i obavijesti o čistom rublju koje bi se aktivirale tokom tihih sati zadržavaju se i dostavljaju kada prozor završi. Ostavite prazno da onemogućite. Podržava prozore koji prelaze ponoć (npr. početak 22, kraj 7).", + "doc": "Početak perioda ne ometaj (0-23). Obavještenja o završetku, podsjetnici i obavještenja o čistom vešu u tom periodu zadržavaju se do njegovog kraja. Može prelaziti ponoć (npr. 22 do 7). Prazno ga isključuje.", "label": "Početak tihih sati" }, "peak_rate_message": { - "doc": "Opcionalni prilagođeni tekst za savjet o vršnoj tarifi dodan uz obavijest o pokretanju. Varijable predloška: {device}, {price}. Prazno koristi ugrađenu zadanu vrijednost.", + "doc": "Vlastiti tekst savjeta o vršnoj tarifi. Varijable predloška: {device}, {price}. Prazno: zadani tekst.", "label": "Poruka o vršnoj tarifi" }, "peak_rate_threshold": { - "doc": "Kada ciklus započne i trenutna cijena po kWh je na ovoj vrijednosti ili iznad nje, dodaje savjet o vršnoj tarifi uz obavijest o pokretanju. 0 ili prazno onemogućava savjet.", + "doc": "Ako je cijena po kWh na početku ciklusa jednaka ili veća od ove, dodajte savjet o vršnoj tarifi obavještenju o početku. 0 ili prazno ga isključuje.", "label": "Prag vršne tarife (po kWh)" }, - "dtw_bandwidth": { - "label": "Tolerancija podudaranja oblika", - "doc": "Koliko modul za podudaranje smije vremenski izobličiti ciklus da odgovara obliku sačuvanog programa. Viša vrijednost podnosi veće varijacije brzine; previsoka riskira podudaranje s pogrešnim programom." - }, - "corr_weight": { - "label": "Težina oblika u odnosu na nivo", - "doc": "Ravnoteža između oblika krive i apsolutnog nivoa snage pri ocjenjivanju podudaranja. Viša vrijednost daje prednost obliku; niža daje prednost slaganju nivoa snage." - }, - "duration_weight": { - "label": "Težina trajanja", - "doc": "Koliko snažno se daje prednost programu čije se tipično trajanje podudara s ovim ciklusom." - }, - "energy_weight": { - "label": "Težina energije", - "doc": "Koliko snažno se daje prednost programu čija se tipična potrošnja energije podudara s ovim ciklusom." - }, "store_brand": { "label": "Marka uređaja", - "doc": "Opcionalno. Marka uređaja odabrana iz kataloga zajednice. Koristi se za pronalaženje i dijeljenje odgovarajućih referentnih snimki. Ostavite prazno ako ne koristite online funkcije." + "doc": "Opciono. Marka vašeg uređaja iz kataloga zajednice, koristi se za pronalaženje i dijeljenje referentnih snimaka. Ostavite prazno ako ne koristite online funkcije." }, "store_model": { "label": "Model uređaja", - "doc": "Opcionalno. Model uređaja odabran iz kataloga zajednice nakon što je marka postavljena. Ako vaš model nije na spisku, možete ga dodati u katalog." + "doc": "Opciono. Model vašeg uređaja iz kataloga zajednice (prvo odaberite marku). Ako nije na listi, možete ga dodati." }, "show_contributor": { "doc": "Prikaži ime doprinositelja na profilima preuzetim iz prodavnice zajednice" }, - "enable_phase_matching": { - "label": "Preostalo vrijeme prema fazama", - "doc": "Svaki aktivni ciklus dijeli na faze (grijanje, pranje, centrifuga) i raspoređuje preostalo vrijeme po fazama, spojeno s klasičnom procjenom - na početku ciklusa oslanja se na budžet faza, a pri kraju na klasičnu procjenu. To prilagođava odbrojavanje tome koliko se vaš uređaj stvarno grije i radi, što je najuočljivije u prvoj polovini ciklusa. Isključeno = samo klasična procjena. Utječe samo na prikaz preostalog vremena; podudaranje programa i detekcija ciklusa ostaju nepromijenjeni." - }, - "keep_min_score": { - "label": "Min. ocjena podudaranja", - "doc": "Minimalna ocjena sličnosti potrebna kandidatu za ostanak u utrci podudaranja. Zadana vrijednost 0,1 je namjerno blaga - dopušta č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." - }, - "dtw_blend": { - "label": "Miješanje deformacije", - "doc": "Koliko DTW ocjena poravnanja zamjenjuje osnovnu ocjenu Faze 2. 0 = koristi samo ocjenu Faze 2, 1 = koristi samo DTW ocjenu, 0,5 (zadano) = jednako miješanje. Povećajte za veće oslanjanje na warp poravnanje kada programi imaju slične nivoe snage ali različite obrasce tajminga." - }, - "dtw_ensemble_w": { - "label": "Miks kombinacije DTW", - "doc": "U kombiniranom DTW načinu (zadanom), težina skaliranog L1 nasuprot derivatnog DTW (DDTW). 1,0 = sve skalirano L1, 0 = sve DDTW, 0,7 = zadano. Skalirana L1 varijanta uspoređuje nivoe snage; derivatna varijanta reagira na promjene snage kroz vrijeme. Kombinirani način spaja oboje." - }, - "dtw_ddtw_scale": { - "label": "Skala derivatne deformacije", - "doc": "Udaljenost polu-zasićenja za ocjenjivanje derivatnim DTW. Ocjena se prepolovi kada je DDTW udaljenost jednaka ovoj vrijednosti. Manje = osjetljivije na razlike u obliku. Zadana vrijednost 30 je kalibrirana za tipične tragove snage kućanskih uređaja. Utječe samo na kombinirani i ddtw DTW način." - }, - "dtw_refine_top_n": { - "label": "Broj preciziranih kandidata", - "doc": "Koliko prvih kandidata Faze 2 DTW ponovo ocjenjuje. DTW je skuplji pa se primjenjuje samo na N najboljih kandidata. Zadano 5. Povećajte (na 7-9) ako ispravni profil ponekad dostigne samo 4. ili 5. mjesto nakon Faze 2 - DTW ga može spasiti. Smanjenje malo ubrzava podudaranje." - }, - "duration_scale": { - "label": "Ostrina trajanja", - "doc": "Oštrina kazne za nepodudaranje trajanja u Fazi 4. Ovo je omjer logaritma pri kojemu se ocjena slaganja prepolovi. Manje = strože: nepodudaranje trajanja više snižava ocjenu. Zadana vrijednost 0,175 odgovara toleranciji trajanja od oko 18% pri polu-težini. Koristite zajedno s Težinom trajanja." - }, - "energy_scale": { - "label": "Ostrina energije", - "doc": "Oštrina kazne za nepodudaranje energije u Fazi 4. Manje = strože: nepodudaranje energije više snižava ocjenu. Zadana vrijednost 0,25 je namjerno blaža od Oštrine trajanja jer energija varira s opterećenjem. Koristite zajedno s Težinom energije." - }, "power_profile_interval_min": { "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." + "doc": "Veličina segmenta atributa power_profile (prosječni vati po slotu) koji koriste planeri poput EMHASS i tibber_prices. Manji čuva kratke skokove; veći izglađuje krivulju. Zadano 15 min. Ne utiče na detekciju." }, "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." + "doc": "Samo iOS. Ažurirajte napredak uživo tiho, uz prioritet koji štedi bateriju i koji iOS može grupisati. Prvo ažuriranje te obavještenja o početku i završetku i dalje upozoravaju. Isključeno: svako ažuriranje upozorava." }, "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." + "doc": "Samo Android. Zadržite obavještenje o napretku uživo nakon dodira. Isključeno: dodir ga zatvara." }, "notify_live_click_action": { "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." + "doc": "Šta otvara dodir obavještenja ovog uređaja. Prazno: WashData panel ovog uređaja. Upišite putanju kontrolne table (npr. /lovelace/laundry) ili URL, ili \"none\" da dodir ništa ne otvara." }, "door_opens_at_end": { "label": "Vrata se automatski otvaraju na kraju", - "doc": "Za mašine za pranje sudova koje na kraju ciklusa otvaraju vrata radi sušenja (AirDry i slično). Kada je uključeno, otvaranje vrata za vrijeme pokrenuta ciklusa više ne zaustavlja ciklus zauvijek; ako vrata ostanu otvorena koliko je navedeno ispod, WashData smatra ciklus završenim. Kratko otvaranje (dodavanje predmeta) se zanemaruje. Zahtijeva entitet senzora vrata." + "doc": "Za mašine za suđe koje na kraju otvaraju vrata radi sušenja (npr. AirDry). Vrata otvorena tokom vremena zadržavanja ispod završavaju ciklus umjesto da ga pauziraju; kratko otvaranje se zanemaruje. Zahtijeva Entitet senzora vrata." }, "door_end_dwell_seconds": { "label": "Zadržavanje otvorenih vrata za završetak", - "doc": "Koliko dugo vrata moraju ostati otvorena prije nego WashData završi ciklus, kada je uključena postavka \"Vrata se automatski otvaraju na kraju\". Dovoljno dugo da se ignorira brzo dodavanje posuđa (zadano 60 s), dovoljno kratko za brz završetak kad mašina otvori vrata." + "doc": "Uz uključeno \"Vrata se automatski otvaraju na kraju\": koliko dugo vrata moraju ostati otvorena prije nego što ciklus završi. Dovoljno dugo da se zanemari dodavanje posuđa (zadano 60 s)." }, "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." + "doc": "Koje vrste ciklusa oblikuju krivulju i podudaranje svakog programa. Odznačavanje vrste ništa ne briše; korisno ako ne vjerujete uvezenim podacima. Statistika uvijek broji samo cikluse ove mašine. Odznačavanje svih se zanemaruje." }, "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." + "doc": "Naplatite svaki ciklus po cijeni u svakom trenutku njegovog rada, umjesto po cijeni kad je završio. Zahtijeva entitet cijene. Obrada historije (Dijagnostika) preračunava starije cikluse." }, "unload_confirm_entity": { "label": "Entitet potvrde istovara", - "doc": "Opcioni entitet čije aktiviranje znači da je sadržaj izvađen, za mašine koje ne mogu imati senzor vrata. Radi bilo koja vrsta: Zigbee ili NFC dugme, input_button pomoćnik, senzor pokreta ispred mašine, scena. Svaka promjena stanja broji se kao potvrda, osim isključivanja i prelaska u nedostupno stanje. Postavljanje ovoga ujedno omogućava podsjetnik za istovar na uređaju bez Entiteta senzora vrata." + "doc": "Opciono. Bilo koji entitet koji potvrđuje da je sadržaj izvađen, za mašine bez senzora vrata (dugme, input_button, senzor pokreta, scena). Broji se svaka promjena stanja osim isključivanja i nedostupnosti. Uključuje i podsjetnik za istovar." }, "unload_track_without_door": { "label": "Ručno potvrdi istovar", - "doc": "Pokrenite podsjetnik za istovar na uređaju bez Entiteta senzora vrata i bez Entiteta potvrde istovara, a istovar potvrdite iz vlastite automatizacije: pritisnite entitet dugmeta \"Označi kao istovareno\" ili pozovite uslugu ha_washdata.mark_unloaded. Zanemaruje se kada je postavljen Entitet senzora vrata, koji već daje taj signal." + "doc": "Pokrenite podsjetnik za istovar bez senzora vrata ili entiteta potvrde. Potvrdite istovar iz vlastite automatizacije dugmetom \"Označi kao istovareno\" ili uslugom ha_washdata.mark_unloaded. Zanemaruje se kad je postavljen Entitet senzora vrata." }, "notify_icon_color": { "label": "Boja obavijesti", - "doc": "Akcentna boja obavještenja ovog uređaja, kako bi se mašina za pranje rublja, sušilica i mašina za pranje posuđa razlikovale na prvi pogled. iOS boji ikonu i traku napretka Live Activity na zaključanom ekranu i u Dynamic Island. Android dobiva istu boju, ali šta će s njom uraditi ovisi o telefonu: neki oboje ikonu, a mnogi (posebno Samsung One UI) je potpuno zanemaruju. Obje platforme razumiju Hex (npr. #03A9F4). Ostavite prazno za zadanu boju platforme." + "doc": "Boja naglaska obavještenja ovog uređaja, kako bi se mašina za veš, sušilica i mašina za suđe lako razlikovale. Na iOS-u ispunjava krug iza ikone i boji traku Live Activity. Mnogi Android telefoni (posebno Samsung) je zanemaruju. Koristite hex, npr. #03A9F4. Prazno: zadana boja platforme." + }, + "share_name": { + "doc": "Prikažite naziv svog računa kao autora ciklusa koje dijelite. Isključeno: dijeli se bez imena." } }, "setting_group": { @@ -1678,9 +1610,6 @@ "door_pause": { "label": "Vrata i pauza" }, - "dtw_refinement": { - "label": "DTW preciziranje" - }, "duration_gates": { "label": "Vrata trajanja" }, @@ -1757,17 +1686,20 @@ "rebuilding": "Ponovna izgradnja…", "analyzing": "Analiziranje…", "resetting": "Resetovanje…", - "reverting": "Vraćanje…", "trimming": "Skraćivanje…", "splitting": "Dijeljenje…", + "stalled": "Zaglavljeno", "deleting": "Brisanje…", "imported": "Uvezeno", "preparing": "Priprema…", - "unlabelling": "Uklanjanje oznake…" + "unlabelling": "Uklanjanje oznake…", + "working": "Obrada…", + "match_uncertain": "Nesigurno: {top} ili {runner}", + "match_uncertain_single": "Nesigurno: možda {top}", + "match_sure": "~{pct}% sigurnosti", + "match_uncertain_tip": "WashData se još nije odlučio za program. Broj pokazuje koliko je često vodeća pretpostavka, {top}, bila tačna u prošlim ciklusima uz ovakvu prednost. Samo informativno." }, "suggestion": { - "both_agree": "WashData preporučuje", - "calibrated_label": "Kalibrisano", "observed_label": "Izmjereno", "impact": { "auto_label_confidence": { @@ -1782,10 +1714,6 @@ "higher": "Zatvara samo cikluse sa značajnom potrošnjom energije", "lower": "Zatvara i kraće ili manje energetske cikluse" }, - "end_repeat_count": { - "higher": "Zahtijeva više ponavljanja signala kraja – pouzdanije, malo sporije", - "lower": "Završava nakon manje ponavljanja – brže otkrivanje, veći rizik lažnog završetka" - }, "learning_confidence": { "higher": "Uči samo iz visoko pouzdanih podudaranja – sporije, ali pouzdanije", "lower": "Uči iz više ciklusa; model može biti manje precizan" @@ -1824,40 +1752,25 @@ } }, "reason": { - "auto_label_confidence": "Pouzdanost 15. percentila za {cycles} automatskih oznaka koje korisnik nikada nije ispravio ({p15}).", "completion_min_seconds": "Polovina trajanja čistog ciklusa p05 ({minutes} min) kroz {cycles} ciklusa; filtrira lažne cikluse.{excl}", - "dead_zone_batch": "Zadržano kratkim (medijana prozora nestabilnosti pri pokretanju kroz {cycles} ciklusa = {s}s) kako bi se stvarni kraj brzo prepoznao.", - "dead_zone_from_dip": "Na osnovu ranog pada snage prepoznatog na {s}s.", - "end_energy_default": "Zadana preporučena osnovna vrijednost za prag šuma na kraju ciklusa.", - "end_energy_false": "Energija lažnih završetaka p95 ({p95}Wh) kroz {cycles} ciklusa, sa donjom granicom od 0,2 % medijane energije ciklusa ({floor}Wh).", - "end_energy_no_false": "Nema lažnih završetaka kroz {cycles} ciklusa; sa donjom granicom od 0,2 % medijane energije ciklusa ({floor}Wh).", - "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.", + "match_interval_floored": "Najkraći program ({shortest}s) ograničio bi ovo na {cap}s, ispod minimuma od {minimum}s, pa se koristi minimum: {persistence} uzastopnih podudaranja traje {budget}s, više od {pct}% tog programa. Podudaranje se izvodi samo kad stigne očitanje (medijan={median}s), pa to ništa ne košta.", "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.", "min_off_gap": "Na osnovu {gaps} uočenih razmaka između ciklusa (p05={p05}s). Donja granica uređaja: {floor}s.", "min_off_gap_bridge": "Postavljeno tako da premosti najduži tihi odsječak unutar ciklusa: {span}s + 60s rezerve, iz {spans} premoštenih razmaka kroz {cycles} čistih ciklusa (ostaje ispod vašeg najkraćeg razmaka između dva uzastopna pokretanja, koji iznosi {ceiling}s).", "min_power": "40 % najniže radne snage p05 ({p05}W) kroz {cycles} čistih ciklusa, čime prag isključenja ostaje ispod stvarne potrošnje.{excl}", - "ml_auto_label": "Najniža pouzdanost koju je model kvaliteta još ocijenio čistom (p10 od {cycles} čistih ciklusa = {p10}).", - "ml_end_repeat": "{false}/{total} ciklusa ({pct}%) imalo je pauzu koju je detektor kraja ocijenio sa >50 %; zahtijevaj {val} potvrdu(e) kraja.", - "ml_off_delay": "Pauze potvrđene detektorom kraja: p95 {p95}s + 60s rezerve, iz {pauses} modelom provjerenih pauza kroz {cycles} ciklusa (donja granica {floor}s).", "no_update_timeout": "Na osnovu uočene učestalosti ažuriranja (p95={p95}s) * 20 (najmanje 60s).", "off_delay_cadence": "Na osnovu uočene učestalosti ažuriranja (p95={p95}s) * 5", "off_delay_device_floor": "Korišten siguran minimum specifičan za uređaj {device} ({floor}s).", "off_delay_generic_floor": "Korišten opći siguran minimum ({floor}s).", "off_delay_pauses": "Postavljeno tako da nadmaši stvarne pauze: pauza unutar ciklusa p95 {p95}s + 60s rezerve, iz {pauses} pauza kroz {cycles} čistih ciklusa (donja granica {floor}s).", - "profile_match_threshold": "Pouzdanost p10 za {cycles} tačnih automatskih oznaka ({p10}); sigurna donja granica za potvrdu tokom rada.", - "sampling_interval": "Medijana intervala ažuriranja uočena kroz {cycles} čistih ciklusa ({si}s).{excl}", - "smoothing_window": "Postavljeno za izglađivanje ~30s očitavanja pri uzorkovanju od {si}s ({samples} uzoraka).", - "start_duration": "Zadržano kratkim (~jedan interval uzorkovanja od {si}s) kako bi prepoznavanje počelo što ranije, uz zanemarivanje pojedinačnih skokova.", - "thr_batch": "Zadržano tik iznad najniže radne snage p05 kroz {cycles} ciklusa ({p05}W) kako bi se početak uhvatio što ranije, a prag zaustavljanja ostao ispod najniže radne snage mašine.", - "tol_per_profile": "p75 varijanse trajanja po profilu kroz {profiles} profila ({cycles} ciklusa); dosljedni profili nisu kažnjeni.", - "tol_pooled": "Na osnovu objedinjene varijanse trajanja {cycles} novijih označenih ciklusa (p95 odstupanje={dev}).", - "watchdog": "Zadržano što nižim uz sigurnost (tik iznad razmaka ažuriranja p95 od {p95}s i najmanje 2× intervala uzorkovanja od {median}s, najmanje 30s) kako bi se zastoji brzo uhvatili bez lažnih zaustavljanja." + "standby_floor": "Svaki nedavni ciklus je i dalje trošio {idle}W kada je završio, pa prag ispod toga nikada ne može registrirati isključivanje uređaja. Zadržan je iznad te vrijednosti; {cycles} čistih ciklusa u evidenciji nikada nije palo ispod njega na duže od {pause}s.", + "thr_batch": "Malo iznad najniže aktivne snage (p05) u {cycles} ciklusa ({p05}W), kako bi se počeci rano uhvatili, a prag zaustavljanja ostao ispod najniže radne snage.", + "watchdog": "Zadržano što nižim uz sigurnost (tik iznad razmaka ažuriranja p95 od {p95}s i najmanje 2× intervala uzorkovanja od {median}s, najmanje 30s) kako bi se zastoji brzo uhvatili bez lažnih zaustavljanja.", + "end_energy_floor": "Povećano na minimum koji proizlazi iz praga zaustavljanja ({stop} W) tokom kašnjenja isključivanja ({off_delay} s); ispod toga ciklus se može završiti samo rezervnim putem.", + "max_duration_ratio_default": "Vraćeno na zadanu vrijednost: niža gornja granica odbacuje pravi program kad god ciklus traje duže nego obično.", + "min_duration_ratio_default": "Vraćeno na zadanu vrijednost: viša donja granica sprečava da se ciklus u toku prepozna sve dok dobro ne odmakne." }, "exclusions": { "summary": "Isključeno {total} pogrešno otkrivenih ciklusa: {parts}.", @@ -1875,7 +1788,8 @@ "no_power": "bez snage", "no_active_power": "bez aktivne snage" } - } + }, + "now_value": "(trenutno {v})" }, "tab": { "advanced": "Napredno", @@ -1906,13 +1820,17 @@ }, "toast": { "catalog_refreshed": "Katalog zajednice je osvježen", + "task_name_required": "Dajte zadatku naziv", + "task_added": "Zadatak dodan", + "task_save_failed": "Nije moguće sačuvati zadatak: {error}", + "task_removed": "Zadatak uklonjen", + "maint_logged": "Označeno kao obavljeno: {task}", "access_saved": "Kontrola pristupa je sačuvana", "all_wiped": "Svi podaci su izbrisani", "analysis_complete_none": "Analiza završena: nema novih prijedloga", "auto_label_complete": "Automatsko označavanje je završeno", "automation_deleted": "Automatizacija izbrisana", "correction_submitted": "Ispravka dostavljena", - "cycle_deleted": "Ciklus je obrisan", "cycle_force_stopped": "Ciklus prisilno zaustavljen", "cycle_labelled": "Ciklus označen", "cycle_paused": "Ciklus je pauziran", @@ -1928,11 +1846,9 @@ "group_deleted": "Grupa je izbrisana", "group_name_required": "Naziv grupe je obavezan", "group_saved": "Grupa je sačuvana", - "import_successful": "Uvoz uspješan; ponovno učitavanje integracije", "json_required": "Potrebni su JSON podaci", "legacy_removed": "Naslijeđene radnje su uklonjene", "logs_exported": "Dnevnici su izvezeni", - "matching_reverted": "Odgovarajuće težine vraćene na zadane vrijednosti", "min_2_profiles": "Odaberite najmanje 2 profila za grupu", "ml_training_no_improvement": "Obuka je završena: osnovna linija je zadržana (bez poboljšanja)", "models_reverted": "Modeli na uređaju vraćeni su na osnovnu liniju", @@ -1942,8 +1858,8 @@ "pg_preset_loaded": "Konfiguracija \"{name}\" učitana", "pg_preset_saved": "Konfiguracija \"{name}\" sačuvana", "pg_sugg_loaded": "Učitano {n} predloženih vrijednosti - pokrenite testno okruženje za usporedbu", - "pg_sugg_ml_loaded": "Učitano {n} ML kalibriranih vrijednosti - pokrenite testno okruženje za usporedbu", "phase_name_required": "Naziv faze je obavezan", + "phase_reset": "Faza \"{name}\" je vraćena na ugrađenu verziju", "phase_updated": "Faza ažurirana", "phases_saved": "Faze su sačuvane", "preferences_saved": "Postavke su sačuvane", @@ -1990,15 +1906,11 @@ "store_name_required": "Unesite naziv profila", "store_import_failed": "Uvoz nije uspio: {error}", "store_imported": "Uvezeno u {profile}", - "store_program_required": "Unesite naziv programa", "store_no_appliance": "Prvo postavite marku i model uređaja u Postavkama.", "store_share_failed": "Dijeljenje nije uspjelo: {error}", "store_shared": "Podijeljeno u zajedničku trgovinu, čeka pregled.", "contrib_device_changed": "Dodano u zajedničku trgovinu, ali se odabrani uređaj promijenio dok je obrazac bio otvoren. Odaberite ga u Postavkama.", "appliance_added": "Uređaj dodan, čeka odobrenje", - "thanks_confirming": "Hvala na potvrdi", - "device_approved": "Odobreno od strane zajednice", - "rating_saved": "Ocjena kvaliteta sačuvana", "brand_added": "Marka dodana, čeka odobrenje", "profile_added": "Profil dodan, čeka odobrenje", "saved_except_conflicts": "Sačuvano. Ispravite istaknute konflikte da sačuvate ostalo.", @@ -2021,7 +1933,50 @@ "odometer_saved": "Ukupan broj ciklusa je sačuvan", "odometer_save_failed": "Nije moguće sačuvati ukupan broj: {error}", "store_pick_profile_share": "Odaberite profil za dijeljenje", - "remove_failed": "Uklanjanje nije uspjelo: {error}" + "remove_failed": "Uklanjanje nije uspjelo: {error}", + "analysis_complete": "Analiza završena: prijedloga: {count}", + "analysis_failed": "Analiza nije uspjela: {error}", + "auto_detect_failed": "Automatsko otkrivanje nije uspjelo: {error}", + "auto_label_failed": "Automatsko označavanje nije uspjelo: {error}", + "could_not_load_cycle": "Nije moguće učitati ciklus: {error}", + "create_failed": "Kreiranje nije uspjelo: {error}", + "cycles_deleted": "Obrisano ciklusa: {count}", + "debug_cleared": "Obrisano tragova otklanjanja grešaka: {count}", + "discard_failed": "Odbacivanje nije uspjelo: {error}", + "error": "Greška: {error}", + "export_failed": "Izvoz nije uspio: {error}", + "import_failed": "Uvoz nije uspio: {error}", + "invalid_json": "\"{key}\" nije ispravan JSON - ispravite ga ili ispraznite polje prije čuvanja.", + "label_failed": "Označavanje nije uspjelo: {error}", + "ml_training_promoted": "Treniranje završeno: aktivirano modela: {count}", + "pause_failed": "Pauziranje nije uspjelo: {error}", + "phase_created": "Faza \"{name}\" je kreirana", + "phase_deleted": "Faza \"{name}\" je obrisana", + "profile_created": "Profil \"{name}\" je kreiran", + "rename_failed": "Preimenovanje nije uspjelo: {error}", + "save_failed": "Čuvanje nije uspjelo: {error}", + "split_complete": "Podijeljeno na cikluse: {count}", + "start_failed": "Pokretanje nije uspjelo: {error}", + "stop_failed": "Zaustavljanje nije uspjelo: {error}", + "update_failed": "Ažuriranje nije uspjelo: {error}", + "store_download_skipped": "Preskočeni snimci: {n} (prekratki, s previše praznina ili već na vašem uređaju).", + "store_import_low_quality": "Ovaj snimak je prekratak, s previše praznina ili nevjerodostojan da bi se uvezao.", + "store_import_duplicate": "Ovaj snimak je već na vašem uređaju.", + "device_approved": "Odobreno od strane zajednice", + "thanks_confirming": "Hvala na potvrdi", + "rating_saved": "Ocjena kvaliteta sačuvana", + "store_download_partial": "Programi koji se nisu mogli preuzeti: {n}; pokušajte ponovo kasnije.", + "import_no_restore_point": "Uvezeno, ali tačka vraćanja nije mogla biti spremljena, pa se ovaj uvoz ne može poništiti", + "import_undo_failed": "Uvoz nije moguće poništiti: {error}", + "import_undone": "Uvoz poništen: vraćeno {profiles} profila i {cycles} ciklusa", + "auto_label_cancelled": "Automatsko označavanje otkazano nakon označavanja {count} ciklusa.", + "auto_label_cancelled_one": "Automatsko označavanje otkazano nakon označavanja {count} ciklusa.", + "auto_label_cancelled_few": "Automatsko označavanje otkazano nakon označavanja {count} ciklusa.", + "auto_label_cancelled_other": "Automatsko označavanje otkazano nakon označavanja {count} ciklusa.", + "auto_label_done": "Automatsko označavanje završeno: označeno je {count} ciklusa, bez oznake: {skipped}.", + "auto_label_done_one": "Automatsko označavanje završeno: označen je {count} ciklus, bez oznake: {skipped}.", + "auto_label_done_few": "Automatsko označavanje završeno: označena su {count} ciklusa, bez oznake: {skipped}.", + "auto_label_done_other": "Automatsko označavanje završeno: označeno je {count} ciklusa, bez oznake: {skipped}." }, "trend": { "down": "Opadajući trend", @@ -2029,6 +1984,10 @@ }, "maint": { "bearing_service": "Servis ležajeva", + "salt": "Dopuni so", + "rinse_aid": "Dopuni sredstvo za ispiranje", + "lint_filter": "Očisti filter za dlačice", + "condenser_clean": "Očisti kondenzator", "descale": "Uklanjanje kamenca", "drum_clean": "Očisti bubanj", "filter_clean": "Očisti filter", @@ -2036,34 +1995,22 @@ }, "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", + "smart_termination_duration_ratio": "Udio prosječnog trajanja programa koji ciklus mora dostići prije nego što ga Pametni završetak smije ranije završiti; smanjite ako vremena rada variraju", + "anti_crease_finalize_ratio": "Udio prosječnog trajanja programa koji ciklus mora dostići prije nego što ga rep anti-gužvanja smije dovršiti; smanjite kod sušilica, kod mašina za veš ostavite zadano", "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_exit_power": "Nivo mirovanja između impulsa okretanja; djeluje samo iznad Praga zaustavljanja", "anti_wrinkle_idle_timeout": "Dozvoljeno vrijeme mirovanja između dva impulsa bubnja prije nego što se anti-gužvanje završi", "anti_wrinkle_max_duration": "Duži impuls završava anti-gužvanje i otvara novi ciklus", "anti_wrinkle_max_power": "Impuls iznad ove vrijednosti završava anti-gužvanje i otvara novi ciklus", "completion_min_seconds": "Najkraće pokretanje koje se računa kao pravi ciklus", - "end_repeat_count": "Niska očitanja zaredom prije završetka", "interrupted_min_seconds": "Kratki ciklusi označeni kao prekinuti", "min_off_gap": "Razmak potreban za razdvajanje dva ciklusa", "off_delay": "Sekundi niske snage prije završetka ciklusa", "start_duration_threshold": "Sekundi iznad praga za potvrdu pokretanja", "start_threshold_w": "Minimum vati da bi se računalo kao pokrenuto", "stop_threshold_w": "Ispod ovoga mašina se računa kao isključena", - "dtw_bandwidth": "Faza 3: warp opseg Sakoe-Chiba (0 = DTW isključen; zadano 0,2 = 20% dužine ciklusa)", - "corr_weight": "Faza 2: ravnoteža između oblika krivulje (korelacija) i nivoa snage (MAE); zadano 0,45", - "duration_weight": "Faza 4: koliko jako slaganje trajanja utječe na konačnu ocjenu (zadano 0,22)", - "energy_weight": "Faza 4: koliko jako slaganje energije utječe na konačnu ocjenu (zadano 0,22)", - "profile_match_min_duration_ratio": "Faza 1: minimalna dužina ciklusa kao udio trajanja profila; zadano 0,1 (10%)", - "profile_match_max_duration_ratio": "Faza 1: maksimalna dužina ciklusa kao udio trajanja profila; zadano 1,8 (180%)", - "keep_min_score": "Faza 2: minimalna ocjena za ostanak u utrci; zadano 0,1 dopušta slabe podudarnosti, kasniji koraci biraju najbolje", - "dtw_blend": "Faza 3: 0 = samo osnovna ocjena, 1 = samo DTW ocjena, 0,5 = jednako miješanje (zadano)", - "dtw_ensemble_w": "Faza 3 (kombinirani način): težina skaliranog L1 nasuprot derivatnog DTW; zadano 0,7 favorizira nivo", - "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)", + "profile_match_min_duration_ratio": "Minimalna dužina ciklusa kao udio trajanja profila; zadano 0,1 (10%)", + "profile_match_max_duration_ratio": "Maksimalna dužina ciklusa kao udio trajanja profila; zadano 1,8 (180%)", "curve_preroll_seconds": "Sekunde očitanja iz prekinutih pokušaja pokretanja prenesene na početak krivulje ciklusa; 0 = isključeno" }, "col": { @@ -2071,12 +2018,14 @@ "status_tip": "Ishod ciklusa: Završeno (prirodni kraj), Prekinuto (nagli pad snage), Prisilno zaustavljeno (ručno) ili Potrebna provjera (čeka se povratna informacija).", "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. 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.", + "energy_tip": "Ukupna potrošena energija (kWh), izračunata iz snage kroz vrijeme.", + "cost_tip": "Trošak energije ovog ciklusa, utvrđen kad je završio. Uz uključen Vremenski ponderisan trošak svaki trenutak se naplaćuje po svojoj cijeni; inače po cijeni na kraju. Postavite cijenu u Postavkama da se ovo 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", - "cost_dynamic_tip": "Vremenski ponderisano: naplaćeno po cijeni koja je važila tokom ciklusa (efektivno {price} {cur}/kWh)." + "cost_dynamic_tip": "Vremenski ponderisano: naplaćeno po cijeni koja je važila tokom ciklusa (efektivno {price} {cur}/kWh).", + "dbg_conf": "Pouzd.", + "dbg_mae": "MAE", + "dbg_corr": "Kor." }, "stat": { "avg": "pros.", @@ -2100,7 +2049,10 @@ "notify_finish": "Bila bi poslana obavijest o završetku.", "notify_milestone": "Bila bi poslana obavijest o prekretnici.", "notify_held": "Obavijest je zadržana zbog tihih sati.", - "finished": "Ciklus je dostigao završno stanje i završio." + "finished": "Ciklus je dostigao završno stanje i završio.", + "match_reverted": "Program se vratio na otkrivanje: njegov rezultat je pao znatno ispod vlastitog vrha.", + "verified_pause": "Poravnanje uživo je potvrdilo (ili ukinulo) pauzu unutar programa. Dok pauza traje, ciklus se ne završava.", + "stalled": "Ciklus je ostao na potrošnji u pripravnosti duže nego što ovaj program ikada tu pauzira, a posao nije gotov. Prikazano kao Pauzirano (Zaglavljeno); ciklus ostaje otvoren." }, "store": { "search_ph2": "Pretraži po marki ili modelu…", @@ -2109,11 +2061,7 @@ "programs_count": "Programi: {n}", "browse": "Pregledaj", "device": "Uređaj", - "favorites": "Favoriti", - "search_ph": "Pretraži po marki ili modelu…", "no_results": "Nisu pronađeni odgovarajući uređaji. Pokušajte s drugom pretragom.", - "n_programs": "{n} programa", - "n_cycles": "{n} ciklusa", "no_programs": "Za ovaj uređaj još nema podijeljenih programa.", "no_cycles": "Za ovaj program još nema podijeljenih referentnih ciklusa.", "anon": "anonimno", @@ -2125,19 +2073,21 @@ "merge_target": "Spoji u profil", "description": "Opis (opcionalno)", "contributed_by": "od {name}", + "website": "Web stranica trgovine ↗", + "website_tip": "Otvorite web stranicu zajedničke trgovine", + "favorites": "Favoriti", "rating_summary": "★ {avg} ({n})", "no_ratings": "Još nema ocjena", - "website": "Web stranica trgovine ↗", - "website_tip": "Otvorite web stranicu zajedničke trgovine" + "unreachable": "Nije moguće povezati se sa zajedničkom trgovinom. Provjerite vezu i pokušajte ponovo." }, "settings": { "gear": { - "title": "Postavke" + "title": "Preferencije" } }, "link": { - "manual": "Priručnik ↗", - "add_appliance": "Dodajte svoj uređaj" + "maintenance_supporter": "Maintenance Supporter na GitHubu ↗", + "manual": "Priručnik ↗" }, "tip": { "add_brand": "Marke nema na spisku? Dodajte je u katalog zajednice", @@ -2171,6 +2121,12 @@ }, "rebuild": { "envelopes": "Ponovna izgradnja omotača" + }, + "auto_label": { + "running": "Automatsko označavanje ciklusa" + }, + "store_download": { + "running": "Preuzimanje iz trgovine" } }, "setup": { @@ -2180,26 +2136,24 @@ }, "phase0": { "washer": "WashData već otkriva vaše cikluse. Snimite prvi ciklus kako biste omogućili nazive programa i procjene vremena.", - "dishwasher": "WashData prati. Mašine za pranje posuđa imaju složene cikluse – snimanje prvog ciklusa je izrazito preporučeno. Ako otkriveni ciklus traje predugo, koristite uređivač ciklusa za rezanje prije pohrane kao profila.", + "dishwasher": "WashData prati. Ciklusi mašine za suđe su složeni, pa snimite prvi ciklus. Ako otkriveni ciklus traje predugo, skratite ga u uređivaču ciklusa prije spremanja kao profila.", "generic": "WashData prati. Snimite ili označite otkriveni ciklus kako biste počeli graditi profile." }, "phase1a": { - "labelled": "Dobar početak – vaš prvi program je pohranjen. Za najčišće podatke razmislite o snimanju sljedećeg ciklusa pomoću widgeta snimača." + "labelled": "Dobar početak: vaš prvi program je sačuvan. Za najčišće podatke snimite sljedeći ciklus snimačem." }, "phase1b": { - "recorded": "Vaša snimka pohranjena je kao {profile_name}. Sada snimite ili označite ostale uobičajene programe za izgradnju pokrivenosti." + "recorded": "Vaš snimak je sačuvan kao {profile_name}. Sada snimite ili označite ostale česte programe." }, "phase1c": { - "verify": "Imate {count} programa od zajednice. Pokrenite ciklus kako biste provjerili prepoznaje li ih WashData ispravno – podudaranje će se poboljšati kako vaš uređaj gradi vlastitu historiju." + "verify": "Imate programe iz zajednice ({count}). Pokrenite ciklus da provjerite prepoznaje li ga WashData; podudaranje se poboljšava kako uređaj gradi vlastitu historiju." }, "phase2": { - "cluster": "WashData je vidio {count} ciklusa koji ne odgovaraju nijednom pohranjenome programu – nalikuju jedni drugima. Želite li za njih stvoriti novi profil?", + "cluster": "WashData je uočio slične cikluse ({count}) koji ne odgovaraju nijednom sačuvanom programu. Kreirati novi profil za njih?", "unmatched": "Vaš zadnji ciklus nije odgovarao nijednom pohranjenome programu. Je li to novi program?" }, "phase3": { - "suggestions": "WashData ima preporuke postavki temeljene na vašoj historiji ciklusa – pregledajte ih kako biste poboljšali tačnost otkrivanja.", - "groups": "Neki vaši profili izgledaju kao isti program pri različitim temperaturama. Organizujte ih u grupu za bolje podudaranje.", - "phases": "Dodajte faze programa u {profile_name} za tačnije procjene preostalog vremena." + "suggestions": "WashData ima prijedloge postavki iz historije vaših ciklusa. Pregledajte ih za bolju detekciju." }, "phase4": { "healthy": "Ovaj uređaj je u potpunosti postavljen ({profile_count} profila)." @@ -2212,12 +2166,58 @@ "create_from_cluster": "Napravi profil od ovih ciklusa", "create_profile": "Napravi za njega profil", "review_suggestions": "Preglej prijedloge", - "organise_profiles": "Organizuj profile", - "configure_phases": "Konfiguriraj faze", "skip_step": "Preskoči ovaj korak", "skip_forever": "Ne prikazuj više", "hide_guidance": "Sakrij upute", "show_guidance": "Prikaži upute" } + }, + "access": { + "edit": "Uređivanje", + "full": "Potpuno", + "inherit": "Naslijedi", + "none": "Nema (skriveno)", + "read": "Čitanje" + }, + "pg_label_reason": { + "no_winner": "Nijedan program ne odgovara", + "below_floor": "Ispod praga \"Pouzdanost učenja\"", + "ambiguous": "Označeno kao dvosmisleno", + "margin": "Preblizu drugom najboljem programu", + "unknown_profile": "Program više ne postoji", + "not_evaluated": "Nije procijenjeno" + }, + "diagram": { + "accum": "akum.", + "auto": "auto", + "below_off": "ispod = isklj.", + "confirmed": "potvrđeno", + "energy_counts": "energija se broji", + "feedback": "povratna inf.", + "gap_below_min": "razmak ispod min. = jedan ciklus", + "heat_phase": "faza grijanja", + "held_at_100": "ostaje na 100%", + "ignored": "zanemareno", + "kept": "zadržano", + "max": "maks.", + "min": "min.", + "minus_tol": "-tol", + "no_match": "nema podudaranja", + "no_updates": "nema ažuriranja", + "off_delay_wait": "kašnjenje isključivanja", + "off_gap": "razmak isklj.", + "plus_tol": "+tol", + "profile": "profil", + "reading_interval": "tipični interval očitanja", + "sensor_offline": "senzor van mreže: prisilno zaustavljanje", + "si": "IU", + "spike_ignored": "skok zanemaren", + "spike_ignored_short": "skok: zanemaren", + "start": "pokretanje", + "stop": "zaustavljanje", + "tail_energy_resets": "energija kraja iznad praga: odbrojavanje se resetuje", + "thr": "prag", + "too_short": "prekratak", + "tumble_kept": "impulsi bubnja zadržani" } } diff --git a/custom_components/ha_washdata/translations/panel/cs.json b/custom_components/ha_washdata/translations/panel/cs.json index 725e4f04..67b4e396 100644 --- a/custom_components/ha_washdata/translations/panel/cs.json +++ b/custom_components/ha_washdata/translations/panel/cs.json @@ -3,6 +3,7 @@ "min": "min", "entity_not_found": "Entita nenalezena", "running_long": "Běží dlouho", + "stalled": "Zaseknuto", "remaining": "zbývá", "progress": "postup", "show_time_remaining": "Zobrazit zbývající čas", @@ -48,16 +49,18 @@ } }, "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_tip": "Ke konci tento program ztichne asi na {mins} min a pak odebírá asi {watts} W po dobu {secs} s. Zaznamenáno u {seen} z {measured} změřených cyklů.", "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", + "artifact_tip_one": "Zjištěna {n} anomálie (např. dveře otevřené uprostřed cyklu) – otevřete, abyste ji viděli na grafu", + "artifact_tip_few": "Zjištěny {n} anomálie (např. dveře otevřené uprostřed cyklu) – otevřete, abyste je viděli na grafu", + "artifact_tip_many": "Zjištěno {n} anomálie (např. dveře otevřené uprostřed cyklu) – otevřete, abyste je viděli na grafu", + "artifact_tip_other": "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ý", "declining": "↘ klesající", "energy_low": "Nižší spotřeba energie než obvykle", "energy_spike": "Vyšší spotřeba energie než obvykle", - "fair_fit": "přijatelná shoda", - "fair_fit_tip": "Střední konzistence shody – některé cykly přiřazené k tomuto profilu mají nižší skóre spolehlivosti. Označte více cyklů nebo znovu zaznamenejte profil, abyste zvýšili přesnost.", "feedback_requested": "Vyžádána zpětná vazba", "golden_cycle": "Zaznamenaný referenční cyklus", "duration_outlier": "Cykly s neobvyklou délkou: {n}", @@ -67,26 +70,39 @@ "overrun": "Běžel déle než obvykle", "underrun": "Dokončil rychleji než obvykle", "poor_fit": "špatná shoda", - "poor_fit_tip": "Nekonzistentní historie shod - zvažte přebudování tohoto profilu", - "restart_gap_tip": "{n} mezera při restartu HA: průběh výkonu má díru", + "restart_gap_tip": "{n} mezer při restartu HA: průběh výkonu má díry", + "restart_gap_tip_one": "{n} mezera při restartu HA: průběh výkonu má díru", + "restart_gap_tip_few": "{n} mezery při restartu HA: průběh výkonu má díry", + "restart_gap_tip_many": "{n} mezery při restartu HA: průběh výkonu má díry", + "restart_gap_tip_other": "{n} mezer při restartu HA: průběh výkonu má díry", "reviewed": "Zkontrolováno", "steady": "→ stabilní", - "using_defaults": "Pomocí dodaných výchozích nastavení", - "using_tuned": "Použití vyladěných vah", "overrun_ratio": "({x}x očekávaného)", "underrun_ratio": "({pct}% očekávaného)", - "awaiting_tip": "Čeká na schválení komunitou", - "awaiting_n": "Čeká na schválení · potvrzeno {n}", - "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.", - "armed": "(použije se na další cyklus)" + "armed": "(použije se na další cyklus)", + "awaiting_tip": "Čeká na schválení komunitou", + "awaiting_n": "Čeká na schválení · potvrzeno {n}", + "awaiting": "Čeká na schválení", + "approved": "Schváleno", + "matcher_unused": "Zatím nepřiřazeno žádnému z vašich cyklů", + "matcher_used": "Přiřazeno k {n} vašim cyklům", + "matcher_used_one": "Přiřazeno k {n} vašemu cyklu", + "matcher_used_few": "Přiřazeno k {n} vašim cyklům", + "matcher_used_many": "Přiřazeno k {n} cyklu", + "matcher_used_other": "Přiřazeno k {n} vašim cyklům", + "matcher_used_tip": "Cykly na tomto spotřebiči, které WashData sám přiřadil k tomuto importovanému programu. Import, který se nikdy nepřiřadí, nemusí odpovídat vašemu spotřebiči; můžete jej odebrat. Počítáno lokálně; do obchodu se nic neodesílá." }, "btn": { + "pg_preset_delete": "Smazat přednastavení", + "pg_preset_load": "Načíst přednastavení", + "pg_preset_load_tip": "Nahradit hodnoty v testovacím prostředí tímto přednastavením (aktivní nastavení zůstane beze změny)", + "pg_preset_save": "Uložit jako přednastavení", + "pg_preset_save_tip": "Uložit všechny hodnoty níže jako pojmenované přednastavení pro toto zařízení", "reset_zoom": "Obnovit přiblížení", "set_brand_model": "Nastavit značku a model", "refresh_catalog": "Obnovit katalog", @@ -97,12 +113,11 @@ "add_timer": "+ Přidat časovač", "apply_all": "Použít vše", "apply_label": "Použít štítek", - "apply_set_b": "Použít sadu B", "apply_split": "Použít rozdělení", "apply_trim": "Použít oříznutí", "auto_detect_split": "Automatická detekce", "auto_label_cycles": "Cykly automatického označování", - "auto_label_cycles_tip": "Automaticky přiřadit názvy profilů neoznačeným cyklům, jejichž spolehlivost shody překračuje prahovou hodnotu", + "auto_label_cycles_tip": "Pojmenovat neoznačené cykly, jejichž spolehlivost shody překračuje práh.", "cancel": "Zrušit", "clear_debug": "Vymazat data ladění", "clear_splits": "Smazat body", @@ -112,8 +127,6 @@ "convert_to_automation": "Převést na automatizaci", "correct": "Opravit…", "create": "Vytvořit", - "create_from_cluster": "Vytvořit profil z {count} cyklů", - "create_profile": "+ Vytvořit profil", "delete": "Smazat", "delete_group": "Smazat skupinu", "delete_group_tip": "Smazat pouze tuto skupinu - profily členů zůstanou zachovány", @@ -122,39 +135,30 @@ "discard_tip": "Vyřadit zaznamenaný průběh bez uložení", "dismiss": "Propustit", "mute_suggestion": "Přestat navrhovat toto nastavení", - "pg_load_calibrated": "Načíst kalibrované (ML) ({n})", - "pg_load_calibrated_tip": "Načíst nastavení kalibrovaná ML jako přepsání v testovacím prostředí", "pg_load_live": "Načíst aktivní nastavení", "pg_load_live_tip": "Znovu načíst aktuální nastavení integrace a zahodit všechny úpravy v testovacím prostředí", "pg_load_suggested": "Načíst navrhované ({n})", "pg_load_suggested_tip": "Načíst aktuální návrhy automatického ladění jako přepsání v testovacím prostředí", - "pg_preset_delete": "Smazat přednastavení", - "pg_preset_load": "Načíst přednastavení", - "pg_preset_load_tip": "Nahradit hodnoty v testovacím prostředí tímto přednastavením (aktivní nastavení zůstane beze změny)", - "pg_preset_save": "Uložit jako přednastavení", - "pg_preset_save_tip": "Uložit všechny hodnoty níže jako pojmenované přednastavení pro toto zařízení", "pg_publish_all": "Uložit do integrace ({n})", "pg_publish_one": "Uložit {label} do integrace", "reset_muted": "Obnovit skryté", + "reset_phase_tip": "Vrátit vaši úpravu a obnovit vestavěnou fázi", "done": "Hotovo", "edit": "Upravit", "export": "Exportovat", - "export_json": "Export do JSON", "force_stop": "Vynutit zastavení", "force_stop_tip": "Okamžitě ukončit aktuální cyklus a označit ho jako vynuceně zastavený", "from_template": "Ze šablony ▾", "ignore": "Ignorovat", "import_json": "Import z JSON", - "import_overwrite": "Importovat (přepíše data)", "inspect": "Kontrolovat", "label": "Označit", - "load": "Načíst", "load_more": "Načíst více", "manage": "Spravovat", "merge": "Spojit", "new_automation": "+ Nová automatizace", "new_group": "+ Nová skupina", - "new_group_tip": "Seskupit téměř identické profily (stejný tvar/doba trvání, různá teplota nebo odstřeďování), aby párování spolehlivě vybíralo mezi nimi", + "new_group_tip": "Seskupit téměř stejné profily (stejný tvar, jiná teplota nebo odstřeďování), aby je párování dokázalo rozlišit.", "new_phase": "+ Nová fáze", "new_profile": "+ Nový profil", "new_profile_tip": "Vytvořit nový profil programu z existujícího označeného cyklu nebo záznamu", @@ -162,23 +166,19 @@ "on_cycle_started": "Na začátku cyklu", "pause_cycle": "Pauza", "pause_cycle_tip": "Pozastavit běžící cyklus – spotřebič pokračuje tam, kde skončil", - "pg_apply_device": "Použít na zařízení", - "pg_autofill": "Automaticky vyplnit", - "play": "Přehrát", "process": "Zpracovat", "process_history": "Zpracovat nyní", "process_recording": "Uložit záznam", "process_tip": "Uložit zaznamenaný průběh jako nový nebo existující profil", "rebuild": "Obnovit obálky", "rebuild_envelope": "Znovu sestavit obálku", - "rebuild_tip": "Přepočítat očekávanou výkonovou obálku (pásmo min/max) pro všechny profily z jejich označených cyklů – spustit po označení nových cyklů nebo opravě starých", + "rebuild_tip": "Přepočítat očekávané výkonové pásmo každého profilu z jeho označených cyklů. Spusťte po označení nebo opravě cyklů.", "rec_start_tip": "Zahájit záznam průběhu výkonu spotřebiče – spustit těsně před spuštěním cyklu", "rec_stop": "Zastavit", "rec_stop_tip": "Zastavit nahrávání a podržet zachycený průběh ke kontrole", "record": "Spustit nahrávání", "refresh": "Obnovit", "refresh_diag_tip": "Znovu načíst statistiky úložiště", - "refresh_logs_tip": "Znovu načíst nejnovější záznamy protokolu", "refresh_settings_tip": "Znovu načíst nastavení ze serveru", "relabel": "Přeznačit ({count})", "remove": "Odstranit", @@ -186,7 +186,6 @@ "reset": "Resetovat", "reset_ml_models_tip": "Zahoďte doladěné modely a vraťte se k těm vestavěným. WashData se je může později znovu naučit.", "reset_to_builtin": "Obnovit vestavěné modely", - "reset_to_defaults": "Obnovit výchozí nastavení", "resume_cycle": "Pokračovat v cyklu", "resume_cycle_tip": "Obnovit pozastavený cyklus", "revert_settings": "Zahodit změny", @@ -199,8 +198,6 @@ "run_analysis": "🔍 Spustit analýzu návrhů", "run_analysis_tip": "Nyní analyzujte své zaznamenané cykly a obnovte návrhy ladění", "run_auto_label": "Spusťte Auto-Label", - "run_both": "Spustit A vs B", - "run_simulation": "Spustit simulaci", "save": "Uložit", "save_access_control": "Uložit řízení přístupu", "save_group": "Uložit skupinu", @@ -212,10 +209,8 @@ "save_settings": "Uložit nastavení", "select": "Vybrat", "select_all": "Vybrat vše", - "select_last20": "Posledních 20", "select_none": "Zrušit výběr", "show_only": "Zobrazit pouze", - "skip_setup": "Přeskočit nastavení", "split": "Rozdělit", "stop": "Zastavit", "submit_correction": "Odeslat opravu", @@ -224,30 +219,24 @@ "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.", + "unlabel_selected_tip": "Odebrat štítek z vybraných cyklů. Zůstanou v historii, ale přestanou tvořit tento profil.", "use": "Použít", "wipe_all": "Vymazat všechna data", "delete_selected": "Smazat vybrané ({n})", - "group_suggest": "Skupina {n}: {members}", "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í", "import": "Importovat", "disconnect": "Odpojit", "connect_github": "Připojit GitHub", - "save_appliance": "Uložit spotřebič", "share_to_store": "Sdílet do obchodu", "share": "Sdílet", - "confirm_appliance": "Potvrdit tento spotřebič", - "confirmed": "Potvrzeno", "browse_community_setups": "Procházet nastavení komunity", "download_device": "Stáhnout nastavení zařízení", "share_device": "Sdílet nastavení zařízení", - "share_device_tip": "Sdílet programy a nastavení tohoto zařízení s komunitou", + "share_device_tip": "Sdílet tento spotřebič a jeho referenční cykly v komunitním obchodě, aby je mohli převzít majitelé stejného stroje.", "share_n": "Sdílet cykly: {n}", "export_selected": "Export (vybrat data)", "export_all": "Rychlý export všeho", - "import_raw": "Pokročilé: nahradit vše ze souboru JSON", "download_export": "Stáhnout export", "analyze_import": "Analyzovat soubor", "import_selected": "Importovat vybrané", @@ -260,7 +249,15 @@ "save_total_cycles": "Uložit celkový počet", "clear": "Vymazat", "add_brand": "Přidat značku", - "add_appliance": "Přidat spotřebič" + "log_done_task": "Zapsat jako hotové: {task}", + "add_task": "Přidat úkol", + "add_appliance": "Přidat spotřebič", + "retry": "Zkusit znovu", + "confirm_appliance": "Potvrdit tento spotřebič", + "confirmed": "Potvrzeno", + "create_profile_from_gap": "Vytvořit profil", + "undo_import": "Vrátit zpět poslední import", + "pg_rerun": "Aktualizovat simulaci" }, "note": { "off_delay": { @@ -268,10 +265,6 @@ } }, "conflict": { - "anti_wrinkle_exit": { - "exit": "Musí být pod Prahem zastavení ({stop} W); jinak je výkon ukončení ochrany před zmačkáním ignorován", - "stop": "Musí být nad Výkonem pro ukončení ochrany před zmačkáním ({exit} W)" - }, "anti_wrinkle_max": { "max": "Musí být nad Prahem spuštění ({start} W); jinak je limit délky ochrany před zmačkáním přeskočen", "start": "Musí být pod Max. výkonem ochrany před zmačkáním ({max} W)" @@ -309,14 +302,14 @@ "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." + "power_sensor": "Toto je Snímač výkonu zařízení, ne cena. Cykly by se účtovaly ve wattech za kWh; vymažte toto pole.", + "energy_sensor": "Toto je Měřič energie zařízení, ne cena. Cykly by se účtovaly podle stavu měřiče za kWh; vymažte toto pole.", + "device_class": "Tento senzor měří {dc}, ne cenu za kWh. Vymažte toto pole nebo vyberte senzor tarifu.", + "unit": "Tento senzor měří v {unit}, ne cenu za kWh. Vymažte toto pole nebo vyberte 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" + "duration": "Musí být menší než Timeout bez aktualizace ({to} s), aby alarm zaseknutí zazněl dřív", + "timeout": "Musí překročit Délku zaseknutého čerpadla ({ps} s), aby alarm zaseknutí zazněl dřív" }, "start_dur": { "sampling": "Interval vzorkování překračuje Dobu trvání spuštění ({sdt} s); impulz z jednoho vzorku může otevřít cyklus", @@ -334,10 +327,15 @@ "sampling": "Interval vzorkování by měl být nejvýše polovina Intervalu hlídače ({wi} s)" }, "settings_banner": "Konflikty nastavení: {n}. Zkontrolujte zvýrazněné sekce a opravte je před uložením.", - "settings_banner_btn": "Přejít na první" + "settings_banner_btn": "Přejít na první", + "end_energy": { + "energy": "Příliš přísné pro Práh zastavení ({w} W) po dobu Zpoždění vypnutí ({d} s); cyklus může skončit jen záložní cestou", + "stop": "Energie konce ({e} Wh za {d} s) povoluje jen {p} W" + } }, "hdr": { - "ab_comparison": "Porovnání nastavení A/B", + "custom_tasks": "Vlastní úkoly", + "maintenance_supporter": "Potřebujete víc?", "access_control": "Řízení přístupu", "add_maintenance": "Přidat údržbovou událost", "automation_delete": "Smazat tuto automatizaci", @@ -346,28 +344,22 @@ "automation_review_this_cycle": "Zopakujte si tento cyklus", "automations": "Automatizace", "clear_debug": "Vymazat trasování ladění", - "cycle_simulator": "Simulátor cyklů", "diagnostics_pref": "Diagnostika", "display": "Zobrazit", - "dtw_inspector": "Inspektor DTW", "export_import": "Export / Import", - "getting_started": "Začínáme", "live_power": "Živá síla", "logs": "Záznamy", - "logs_diagnostics": "Diagnostika", "maintenance": "Údržbové akce", "maintenance_log": "Protokol údržby", "maintenance_reminders": "Připomínky servisu", "manual_recording": "Ruční nahrávání", - "manual_recording_tip": "Spusťte cyklus záměrně, zatímco WashData zaznamenává průběh výkonu. Stiskněte Start těsně před spuštěním spotřebiče, Stop po dokončení, pak Zpracovat pro uložení jako pojmenovaný profil.", + "manual_recording_tip": "Nahrajte cyklus záměrně: stiskněte Spustit nahrávání těsně před spuštěním spotřebiče a Zastavit po dokončení, pak Zpracovat pro uložení jako profil.", "ml_learned": "Co se WashData naučila", - "ml_matching_tuning": "Jemné doladění programového přizpůsobení", "ml_settings_card": "Nastavení", "ml_smart_learning": "Chytré učení", "my_preferences": "Moje preference", "no_automations": "Na toto zařízení zatím neodkazuje žádná automatizace.", "panel_settings": "Nastavení panelu", - "param_sweep": "Prohledávání parametru", "pg_settings_source": "Zdroj nastavení", "phase_catalog": "Katalog fází", "playground": "Testovací prostředí", @@ -376,11 +368,8 @@ "status": "Stav", "status_graph": "Stavový graf", "storage_stats": "Statistiky úložiště", - "test_history": "Otestovat na historii", "toggle_sidebar": "Přepnout postranní panel Home Assistant", - "tools_and_data": "Nástroje a data", "wipe_history": "Vymazat historii", - "recommendations": "Doporučení ({n})", "pg_detection_params": "Nastavení detekce", "pg_outcome": "Výsledek simulace", "pg_across_cycles": "Napříč vašimi cykly", @@ -388,7 +377,8 @@ "online_account": "Komunitní obchod a online funkce", "import_power_history": "Import historie výkonu", "service_status": "Stav servisu", - "online_account_tab": "Online a komunita" + "online_account_tab": "Online a komunita", + "live_match_debug": "Ladění shody v reálném čase" }, "health": { "fair": "Přijatelná shoda", @@ -396,11 +386,16 @@ "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_tip": "Pozice na zaznamenané křivce přiřazeného programu: {pct}%. Určuje se zarovnáním cyklu s profilem, takže sedí i při delším nebo kratším běhu. Obnovuje se, když je spotřebič v klidu, takže 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", + "task_name": "Úkol", + "every_cycles": "Interval (cykly)", + "every_days": "Interval (dny)", + "new_task_name": "Nový úkol", + "days_since_service": "{since} / {total} dní", "actions": "Akce", "activity": "Aktivita", "administrators": "Správci", @@ -408,10 +403,8 @@ "aria_cycle_chart": "Křivka výkonu cyklu", "aria_envelope_chart": "Obálka výkonu profilu", "aria_phase_chart": "Graf editoru fází", - "aria_playground_chart": "Křivka výkonu cyklu s přetažitelnými prahy detekce", "aria_power_chart": "Graf výkonu", "aria_spaghetti_chart": "Překryté křivky výkonu cyklů", - "aria_sweep_chart": "Graf výsledků přeběhu parametru", "artifact_anomaly": "Anomálie", "artifact_high_power": "Vysoký výkon", "artifact_interruption": "Přerušení", @@ -425,151 +418,103 @@ "cohesion_good": "soudržnost {pct} %", "cohesion_low": "⚠ nízká soudržnost {pct} %", "compare_profiles": "Porovnejte s profily", - "concurrency": "Souběžnost", "confidence": "Spolehlivost", "confidence_threshold": "Práh důvěry", "correct_duration": "Správné trvání (min, volitelné)", "correct_profile": "Správný profil", - "correlation": "Korelace", "cost": "Náklady", "create_new_profile": "+ Vytvořit nový profil…", "current_phase": "Aktuální fáze", - "curves": "Křivky", "cycle": "Cyklus", "cycle_date_display": "Zobrazení data cyklu", - "cycle_health": "Cyklujte zdraví", - "cycle_index": "index cyklu →", - "cycles": "Cykly", "cycles_count": "Cykly", "cycles_lc": "cyklů", - "dashed": "přerušovaná", "date": "Datum", "debug_traces": "Trasování ladění", - "default": "Výchozí", "default_access_level": "Výchozí úroveň pro uživatele, kteří nejsou uvedeni níže", "default_other": "Výchozí (ostatní zařízení)", "default_tab": "Výchozí karta při otevření panelu", "font_size": "Velikost písma panelu", - "delta": "Δ", "description": "Popis", - "dtw_blend_w": "Váha směsi", - "dtw_blended": "Smíšené skóre", - "dtw_ddtw": "Skóre DDTW", - "dtw_l1": "Skóre L1", - "dtw_matched_profile": "(profil přiřazený cyklu)", "duration": "Trvání", - "duration_agreement": "Shoda trvání", - "emphasis": "Důraz", "enable_access_control": "Povolit řízení přístupu pro jednotlivé uživatele", "energy": "Energie", - "energy_agreement": "Shoda energie", - "event_timeline": "Časová osa událostí", "event_type": "Typ události", "expected": "Očekávaný", + "suspected_profile": "{name} (odhad)", + "runner_up_profile": "{name} (druhý nejlepší)", "expected_duration": "Očekávaná doba trvání (min)", "file_kb": "Soubor (kB)", - "final_score": "Konečné skóre", "from": "Od", "gap_s": "mezera (y)", "group_name": "Název skupiny", "head_trim": "Oříznutí začátku (s)", - "health": "Zdraví", "hide_tabs": "Skrýt karty pro neadministrátory", - "in_use": "Používá se", "json_data": "Data JSON", "label": "Shoda", "last": "Poslední", "last_checked": "Naposledy zkontrolováno:", "load_from_file": "Načíst ze souboru", - "mae_score": "Skóre MAE", "manual_duration": "Manuální trvání (min, volitelné)", "match": "Shoda", "members": "Členové", - "metric": "Metrika", "mode_existing_profile": "Přidat do stávajícího profilu", "mode_new_profile": "Vytvořit nový profil", "models_fine_tuned": "(doladěné modely: {count})", - "n_classic_suggestions": "{n} klasický", - "n_ml_suggestions": "{n} ML", "n_selected": "{n} vybráno", - "n_tuning_suggestions": "{n} návrh ladění", - "na": "n/a", + "n_tuning_suggestions": "{n} návrhů ladění", + "n_tuning_suggestions_one": "{n} návrh ladění", + "n_tuning_suggestions_few": "{n} návrhy ladění", + "n_tuning_suggestions_many": "{n} návrhu ladění", + "n_tuning_suggestions_other": "{n} návrhů ladění", "new_profile_name": "Nový název profilu", "notes": "Poznámky", - "of": "z", - "outcome": "Výsledek", "panel_default_tab": "Výchozí karta", "panel_language": "Jazyk panelu", "per_cycle_short": "cyklus", - "pg_autofill_tip": "Automaticky vyplnit rozsah z aktuální hodnoty ({lo}–{hi})", "pg_best_value": "Nalezena nejlepší hodnota", "pg_current_value": "Aktuální hodnota:", "pg_drag_hint": "Táhněte čáru v grafu", "pg_anti_wrinkle": "Ochrana před zmačkáním", - "pg_dtw": "DTW", "pg_ev_ending": "Končí", "pg_ev_running": "Běží", "pg_group_advanced": "Hraniční případy", "pg_group_detection": "Spouštěče detekce", "pg_group_timing": "Pravidla časování", "pg_idle": "Nečinný", - "pg_last_run": "Poslední běh: {date}", - "pg_match_score": "Skóre shody", - "pg_pct_of": "{p}% z {total}", "pg_poor_match": "Špatná shoda", "pg_preset": "Přednastavení testovacího prostředí", "pg_preset_name": "Název nového přednastavení", "pg_preset_none": "Vyberte přednastavení…", "pg_strong_match": "Silná shoda", - "pg_tile_ambiguous": "Nejednoznačné", - "pg_tile_ambiguous_tip": "Cykly s téměř shodným výsledkem mezi dvěma programy", - "pg_tile_detected": "Detekováno", - "pg_tile_detected_tip": "Cykly, kde výkon překročil práh spuštění dostatečně dlouho, aby byl zaznamenán jako běžící", - "pg_tile_matched": "Spárováno", - "pg_tile_matched_tip": "Cykly správně přiřazené ke známému programu", - "pg_time_left": "Zbývající čas", "pg_weak_match": "Slabá shoda", "phase_name": "Název fáze", "name": "Jméno", "phase_timeline": "Časová osa fází", "power": "Výkon", "profile": "Profil", - "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ěží. Vyberte program před spuštěním spotřebiče a použije se, jakmile začne další cyklus.", + "program_tip": "Vyberte, ke kterému programu se aktuální cyklus páruje. Automatická detekce vybere nejlepší shodu. Připněte program, když se automatická detekce mýlí; program vybraný před spuštěním platí pro další cyklus.", "progress": "Pokrok", "raw_socket": "Surový signál", - "read_only": "Pouze pro čtení", "ref_cycle": "Referenční cyklus (volitelné)", - "refresh_interval": "Interval obnovení (s)", "remaining": "Zbývající", "reminder_every": "Připomenout každých (cyklů)", "remove_label": "- Odebrat štítek -", "rename_profile": "Přejmenovat profil", - "replay_duration": "Přehrávání", "resulting_profile": "Výsledný profil", "save_mode": "Uložit režim", - "score": "Skóre", - "score_breakdown": "Rozpis skóre", "seconds_from_start": "Sekundy od začátku", "select_profile": "Vyberte Profil", "setting": "Nastavení", - "settings_a": "Nastavení A (aktuální)", - "settings_a_short": "A", "settings_advanced": "Pokročilé", - "settings_b": "Nastavení B (test)", - "settings_b_short": "B", "settings_basic": "Základní", "settings_detail_level": "Úroveň podrobností nastavení", - "settings_overrides": "Přepsání nastavení", "show_debug": "Zobrazit živou kartu ladění shody na stránce Přehled (spolehlivost, nejednoznačnost, nejlepší kandidáti)", "show_expected": "Zobrazit očekávané překrytí křivky (shodný profil, oranžová)", "show_raw": "Zobrazit přepínač surových dat zásuvky v živém grafu výkonu", "sparkline": "Trend délky posledních cyklů", - "stage2": "Fáze 2 – jádrová podobnost", - "stage3": "Fáze 3 – DTW", - "stage4": "Fáze 4 – shoda", "status": "Stav", "tail_trim": "Oříznutí konce (s)", "tap_to_dismiss": "klepnutím na graf zavřete", @@ -580,7 +525,6 @@ "ungrouped": "Neseskupeno", "unlabelled": "Neoznačené", "unlabelled_paren": "(neoznačeno)", - "warp_path": "Cesta warpingu", "input": "Vstup:", "start": "Začátek", "end": "Konec", @@ -605,9 +549,8 @@ "n_shown": ", zobrazeno {n}", "cycles_title": "Cykly ({n})", "pg_detecting": "Rozpoznávání", - "pg_best_marker": "Nejlepší: {v}", "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í", + "pg_time_left_model_tip": "Odhad podle fází programu, ne pevné odpočítávání", "phase": "Fáze", "pg_mode_history": "Otestovat na historii", "pg_ev_detected": "Detekováno", @@ -647,18 +590,8 @@ "pg_mode_optimize": "Optimalizovat", "pg_group_matching": "Přiřazování programů", "pg_zoom_hint": "dvojklikem obnovíte přiblížení", - "aria_playground_chart2": "Interaktivní graf výkonu cyklu: najetím zobrazíte čas/výkon, kolečkem přiblížíte, tažením posunete", "pg_alert_timeout_end": "Ukončeno časovým limitem, ne predikcí", "pg_alert_indefinite": "Běželo by donekonečna", - "pg_alert_stress_cap": "Dosažen bezpečnostní limit", - "pg_alert_stress_ok": "Detekce nečinnosti: cyklus zastaven", - "pg_alert_stress_warn": "Příkon v nečinnosti nad prahem zastavení", - "pg_stress_group": "Test detekce nečinnosti", - "pg_stress_idle_auto": "Auto", - "pg_stress_idle_w": "Úroveň nečinnosti (W)", - "pg_stress_idle_w_desc": "Přepsat automaticky zjištěnou spodní hranici pohotovostního příkonu (ponechte prázdné pro automatické zjištění).", - "pg_stress_toggle": "Testovat detekci nečinnosti", - "pg_stress_toggle_desc": "Simuluje, že spotřebič zůstane při svém pohotovostním příkonu po skončení záznamu -- zobrazí, zda a kdy WashData cyklus zastaví.", "task_pg_history": "Test na historii", "task_pg_sweep": "Optimalizovat", "task_reprocess": "Opětovné zpracování", @@ -668,16 +601,9 @@ "pg_recent_runs": "Nedávné běhy", "pg_load_run": "Znovu načíst tento běh", "pg_match_confidence": "Spolehlivost shody", - "pg_envelope_fit": "Soulad s obálkou", - "pg_envelope_fit_tip": "Jak přesně cyklus zapadá do uložené výkonové obálky tohoto profilu, jiný pohled než spolehlivost shody.", "n_imported_note": ", {n} importováno", "enable_online": "Povolit online funkce", - "store_brand": "Značka spotřebiče", - "store_model": "Model spotřebiče", "anonymous": "Anonymní", - "rate_quality": "Ohodnotit kvalitu", - "rate_n": "{n} hvězdiček", - "approved_only": "Pouze schválené", "adopt_settings": "Přijmout nastavení", "flags": "Příznaky", "include_phase_map": "Zahrnout mapu fází", @@ -694,7 +620,6 @@ "cat_custom_phases": "Vlastní fáze", "cat_profile_groups": "Skupiny profilů", "cat_settings": "Nastavení detekce a rozpoznávání", - "cat_matching_config": "Ladění rozpoznávání", "cat_ml_models": "Modely ML", "cat_feedback": "Zpětná vazba a hodnoticí štítky", "cat_suggestions": "Návrhy", @@ -714,7 +639,6 @@ "conflict_overwrite": "Přepsat", "hist_csv_data": "Data CSV", "hist_from_recorder": "Nebo ji načtěte z Home Assistant", - "days": "dní", "hist_keep": "Zachovat tento cyklus", "hist_looks_complete": "dokončený", "peak_power_short": "Špička", @@ -735,7 +659,44 @@ "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" + "correct_total_cycles": "Opravit celkový počet", + "hist_reason_already_recorded": "překrývá se s cyklem, který WashData již zaznamenal", + "share_consent": "Potvrzuji, že tyto cykly proběhly normálně až do konce bez přerušení", + "show_internal": "Zobrazit interní nastavení", + "show_internal_tip": "Nastavení, která WashData ladí za vás (vzorkování, hlídač, frekvence párování, prahy). Zobrazí se, když má toto zařízení vlastní hodnotu, konflikt nebo návrh.", + "rate_n": "{n} hvězdiček", + "rate_quality": "Ohodnotit kvalitu", + "pg_autolabel": "Automatické označení", + "pg_label_review": "Vyžádá revizi", + "pg_ev_match_reverted": "Shoda zrušena", + "pg_ev_verified_pause": "Ověřená pauza", + "n_samples": "{n} vzorků", + "n_samples_one": "{n} vzorek", + "n_samples_few": "{n} vzorky", + "n_samples_many": "{n} vzorku", + "n_samples_other": "{n} vzorků", + "n_cycles": "{n} cyklů", + "n_cycles_one": "{n} cyklus", + "n_cycles_few": "{n} cykly", + "n_cycles_many": "{n} cyklu", + "n_cycles_other": "{n} cyklů", + "energy_per_cycle": "{v}/cyklus", + "energy_total": "celkem {v}", + "automation_off": "(vypnuto)", + "ref_cycle_none": "Žádný", + "share_name": "Zobrazovat mé jméno u sdíleného obsahu", + "cat_backfill_cycles": "Cykly nalezené v importované historii výkonu", + "pg_compare_profile": "Porovnat s profilem", + "pg_obj_early": "Míra předčasných konců", + "pg_obj_endlag": "Zpoždění konce (medián)", + "pg_obj_split": "Míra rozdělených cyklů", + "pg_sweep_cycles": "Nedávné cykly", + "cycle_context": "Historie z recorderu kolem cyklu", + "cycle_context_off": "Vypnuto", + "cycle_context_minutes": "{min} min", + "cycle_context_series": "Historie z recorderu", + "before_start": "Před začátkem", + "after_end": "Po konci" }, "log": { "all_levels": "Všechny úrovně", @@ -745,11 +706,9 @@ }, "ml": { "builtin_models": "● Použití vestavěných modelů", - "fit_fair": "Přijatelný", "fit_good": "Dobrý", "fit_slight": "Mírný", "fit_strong": "Silný", - "fit_weak": "Slabý", "fit_word": "shoda", "personalized": "● Přizpůsobeno pro tento stroj", "better_than_baseline": "{pct}% lepší než základní odhad ({metric})", @@ -757,21 +716,33 @@ "trend_declining_tip": "Přizpůsobení tohoto modelu se v posledních opakovaných kontrolách zhoršilo, kontrola více cyklů mu může pomoci znovu se naučit.", "trend_steady_tip": "Přizpůsobení tohoto modelu zůstalo v posledních opakovaných kontrolách zhruba stabilní.", "cap_blurb": { - "end": "Rozpoznání, kdy cyklus skutečně skončil", - "live_match": "Dřívější rozpoznání běžícího programu", - "quality": "Odhalování chybně rozpoznaných nebo poškozených cyklů", - "remaining_time": "Předpověď zbývajícího času", "total_energy": "Předpověď celkové spotřeby energie a nákladů" }, "cap_label": { - "end": "Rozpoznání konce cyklu", - "live_match": "Rozpoznávání programu", - "quality": "Kontrola kvality cyklu", - "remaining_time": "Odhad zbývajícího času", "total_energy": "Odhad spotřeby energie" }, - "metric_auc": "AUC {auc} na validačních datech", - "metric_mae": "chyba {model} oproti základnímu modelu {baseline}" + "metric_mae": "chyba {model} oproti základnímu modelu {baseline}", + "fine_tuned_at": "doladěno {when}", + "held_out_count": "měřeno na {count} validačních cyklech", + "held_out_count_one": "měřeno na {count} validačním cyklu", + "held_out_count_few": "měřeno na {count} validačních cyklech", + "held_out_count_many": "měřeno na {count} validačního cyklu", + "held_out_count_other": "měřeno na {count} validačních cyklech", + "last_run_not_promoted": "Poslední kontrola ({when}): {reason}", + "never": "nikdy", + "skip_reason": { + "fit_failed": "Trénování selhalo: {error}", + "holdout_too_small": "Příliš málo cyklů na férové otestování nového modelu: k testování šlo vyčlenit {held_out}, potřeba je {min}.", + "insufficient_rows": "Zatím málo použitelných cyklů: dosud {count} označených a čistých cyklů.", + "insufficient_rows_one": "Zatím málo použitelných cyklů: dosud {count} označený a čistý cyklus.", + "insufficient_rows_few": "Zatím málo použitelných cyklů: dosud {count} označené a čisté cykly.", + "insufficient_rows_many": "Zatím málo použitelných cyklů: dosud {count} označeného a čistého cyklu.", + "insufficient_rows_other": "Zatím málo použitelných cyklů: dosud {count} označených a čistých cyklů.", + "not_better_than_incumbent": "Nový model nebyl lepší než ten používaný (chyba {model} oproti {incumbent}), proto zůstal stávající.", + "not_better_than_naive": "Nový model nebyl zřetelně lepší než jednoduchý odhad (chyba {model} oproti {naive}).", + "unknown": "Nic nového se nepodařilo naučit." + }, + "trained_unknown": "v neznámou dobu" }, "modal": { "auto_label": "Cykly automatického označování", @@ -780,7 +751,6 @@ "cycle": "Cyklus", "edit_group": "Upravit skupinu profilů", "edit_phase": "Upravit fázi", - "import_config": "Importovat konfiguraci", "label_cycle": "Cyklus štítků", "merge_cycles": "Sloučit {n} cyklů", "new_group": "Nová skupina profilů", @@ -795,6 +765,8 @@ "delete_automation_title": "Smazat automatizaci", "delete_automation_msg": "Smazat automatizaci \"{name}\" z Home Assistant? Tuto akci nelze vrátit zpět.", "remove_legacy_title": "Odebrat starší akce", + "reset_phase_msg": "Vrátit vaši úpravu fáze \"{name}\" a obnovit vestavěnou fázi? Programy si zachovají své rozsahy.", + "reset_phase_title": "Resetovat fázi", "remove_legacy_msg": "Odebrat starší vlastní akce? Přestanou se spouštět při událostech cyklu. Z panelu to nelze vrátit zpět.", "discard_recording_title": "Zahodit záznam", "discard_recording_msg": "Zahodit uložený záznam? Tuto akci nelze vrátit zpět.", @@ -815,32 +787,46 @@ "store_share_device": "Sdílet nastavení zařízení", "export_select": "Export - vybrat data", "import_wizard": "Import - vybrat data", - "history_import": "Import historie výkonu" + "history_import": "Import historie výkonu", + "apply_suggestions_title": "Použít návrhy", + "remove_task_title": "Odebrat úkol", + "remove_task_msg": "Odebrat připomínku \"{task}\"? Její záznamy v protokolu údržby zůstanou zachovány.", + "apply_suggestions_msg": "Změnit tato nastavení ({n})? Integrace se po uložení znovu načte.", + "discard_settings_title": "Zahodit neuložené změny?", + "discard_settings_msg": "Neuložené změny nastavení: {n}. Opustit Nastavení a změny zahodit?", + "profile_title": "Profil · {name}", + "undo_import_msg": "Data a nastavení tohoto zařízení se vrátí do stavu před posledním importem ({when}). Cykly zaznamenané a změny provedené od té doby budou ztraceny.", + "undo_import_ok": "Vrátit import zpět", + "undo_import_title": "Vrátit zpět poslední import" }, "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.", + "pg_preset_delete_confirm": "Smazat přednastavení \"{name}\"?", + "pg_preset_limit": "Dosažen limit přednastavení ({n})", + "pg_preset_overwrite": "Přepsat přednastavení \"{name}\"?", "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.", - "refresh_catalog_hint": "Seznamy značek a spotřebičů z komunity se ukládají do mezipaměti, aby sdílený obchod nepřekročil svůj denní limit dotazů. Obnovením načtete položky, které ostatní přidali nebo schválili.", + "store_sibling_hint": "Pro váš model není nic sdíleno? Příbuzný model stejné značky je obvykle dobrý začátek.", + "store_declare_appliance": "Řekněte WashData, jaký spotřebič máte, a uvidíte nastavení, která pro něj sdíleli ostatní. Nebo zadejte značku výše a rozhlédněte se.", + "refresh_catalog_hint": "Seznamy značek a spotřebičů z komunity se ukládají do mezipaměti kvůli dennímu limitu obchodu. Obnovením uvidíte položky, které ostatní přidali nebo schválili.", "head_trim_hint": "Kolik sekund odstranit ze začátku", "appliance_monitor": "Monitor spotřebiče", "artifact_dip_detail": "Klesl pod obvyklé pásmo výkonu po dobu ~{n} s.", "artifact_footer": "Zvýrazněno na grafu výše. Jedná se o přechodné artefakty (např. dveře otevřené uprostřed cyklu), ne nutně problémy.", "artifact_header": "Během tohoto cyklu bylo zjištěno {n} anomálií", - "artifact_pause_detail": "Výkon klesl téměř na nulu po dobu ~{n} s a poté se obnovil – pravděpodobně byla otevřena dvířka nebo byl cyklus pozastaven.", + "artifact_header_one": "Během tohoto cyklu byla zjištěna {n} anomálie", + "artifact_header_few": "Během tohoto cyklu byly zjištěny {n} anomálie", + "artifact_header_many": "Během tohoto cyklu bylo zjištěno {n} anomálie", + "artifact_header_other": "Během tohoto cyklu bylo zjištěno {n} anomálií", + "artifact_pause_detail": "Výkon klesl téměř na nulu na ~{n}s a pak se obnovil. Nejspíš byla otevřena dvířka nebo byl cyklus pozastaven.", "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": "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.", + "cleanup_intro": "Všechny označené cykly přes sebe. Zaškrtněte odlehlé a pak jim odeberte štítek (zůstanou v historii, ale přestanou tvořit tento profil) nebo je 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é)", - "compare_profiles_tip": "Překryjte další obálky profilu na výše uvedené tabulce a zjistěte, která z nich nejlépe vyhovuje tomuto cyklu.", + "compare_profiles_tip": "Překryjte graf dalšími profily a zjistěte, který k tomuto cyklu sedí nejlépe.", "compare_selected_cycles": "Vybrané cykly (plné) – zobrazit / skrýt", - "consider_new_profile": "Zvažte vytvoření nového profilu.", - "coverage_gap": "nedávné cykly nemají žádný odpovídající profil ({pct} % z posledních 30).", - "coverage_gap_similar_cycles": "Nalezeno {count} podobných neoznačených cyklů – vytvořte profil, abyste je začali párovat.", "cycles_deleted": "Smazáno cyklů: {count}", "enough_data": "Dostatek dat k učení ({current}/{min} cyklů).", "export_description": "Vyberte přesně, které profily, cykly, nastavení a další data exportovat do souboru JSON, nebo analyzujte soubor a importujte jen ty části, které chcete.", @@ -848,30 +834,28 @@ "feedback_prompt": "Potvrďte, že to bylo správné, opravte program nebo jej ignorujte.", "feedback_relabel_hint": "Přeznačení tohoto cyklu jej také vyřeší.", "filter_by_profile": "Filtrovat podle profilu…", - "group_modal_help": "Skupinové programy se stejným tvarem, které se liší teplotou/odstřeďováním (doba trvání se může lišit). Shoda hodnotí skupinu jako jednoho kandidáta a poté vybere nejlépe padnoucího člena. Vyberte alespoň 2; překrytí ukazuje, jak jsou si podobné.", + "group_modal_help": "Seskupte programy se stejným tvarem, které se liší teplotou nebo odstřeďováním. Párování hodnotí skupinu jako celek a pak vybere nejlépe sedícího člena. Vyberte alespoň 2; překrytí ukazuje, jak jsou si podobné.", "group_not_cohesive": "Tyto profily nejsou dostatečně podobné, aby se daly spolehlivě seskupit, takže shoda s nimi zachází individuálně, dokud neodstraníte odlehlou hodnotu nebo nerozdělíte skupinu.", "group_preview_hint": "Zaškrtněte 2 nebo více členů pro náhled a porovnání jejich výkonových křivek.", - "import_intro": "Načtěte exportovaný soubor nebo níže vložte datovou část JSON.", - "legacy_actions_title": "Stále běží starší vlastní akce {count}", + "legacy_actions_title": "Stále běží {count} starších vlastních akcí", + "legacy_actions_title_one": "Stále běží {count} starší vlastní akce", + "legacy_actions_title_few": "Stále běží {count} starší vlastní akce", + "legacy_actions_title_many": "Stále běží {count} starší vlastní akce", + "legacy_actions_title_other": "Stále běží {count} starších vlastních akcí", "live_chart_loading": "Při příchodu naměřených hodnot se zobrazí graf živého výkonu.", "loading": "Načítání…", "loading_curve": "Načítání křivky…", "loading_settings": "Načítání nastavení…", "log_buffer_hint": "Od nejnovějších · ukládá do vyrovnávací paměti posledních 500 záznamů ha_washdata od restartu · přetažením spodního okraje změníte velikost.", - "maintenance_advisory": "Zvyšující se doba trvání/energie může znamenat nutnost údržby spotřebiče (např. odstranění vodního kamene, čištění filtru).", "maintenance_due": "Údržba je na řadě: {items}", - "maintenance_intro": "Zaznamenávejte servis, který na tomto spotřebiči provádíte, a nechte se upozornit, až bude každý úkol znovu na řadě.", + "maintenance_intro": "Zaznamenávejte servis tohoto spotřebiče a nechte si připomenout, až bude každý úkol znovu na řadě.", "maintenance_load_error": "Nepodařilo se načíst data údržby: {error}", "maintenance_requires_access": "Údržba a export/import vyžadují plný přístup.", - "manual_duration_hint": "Průměrná/očekávaná délka cyklu profilu, použitá pro odhady zbývajícího času. Upravit pro nastavení; ponechání beze změny zachová aktuální hodnotu.", - "matching_tuning_intro": "Při učení WashData také upravuje, jak moc váží přizpůsobení programu tvar oproti délce trvání a energii.", + "manual_duration_hint": "Očekávaná délka cyklu profilu, použitá pro odhad zbývajícího času. Ponechte beze změny pro zachování aktuální hodnoty.", "merge_intro": "Vybrané cykly se spojí do jednoho (chronologické pořadí; mezery vyplněné 0 W). Vyberte výsledný profil.", "ml_intro": "WashData se dodává s chytrými modely, které fungují hned po vybalení.", "ml_learned_intro": "Modely vyladěné na tento stroj.", - "ml_loading": "načítání ML…", "ml_settings_intro": "Dva nezávislé přepínače: jeden použije modely během cyklu, druhý umožňuje programu WashData je v průběhu času jemně doladit na váš stroj.", - "name_first_program": "Máte dostatek cyklů – pojmenujte svůj první program a zahajte párování.", - "near_duplicate_cluster": "zjištěn téměř duplicitní shluk profilů. Seskupování umožňuje spárování spolehlivě vybrat mezi podobnými (např. stejný program při různé teplotě/odstřeďování).", "no_cycles_match": "Žádné cykly neodpovídají aktuálnímu filtru.", "no_cycles_profile": "Žádné cykly pro tento profil.", "no_cycles_selected": "Nejprve vyberte alespoň jeden cyklus.", @@ -896,79 +880,66 @@ "no_suggestions": "Žádné aktivní návrhy.", "notify_services_hint": "Zadejte ID služeb {entity} (více oddělete čárkou). Proměnné šablony: {vars}.", "old_actions_warning": "Nakonfigurováno pomocí starého editoru akcí (nyní odstraněno). Stále se spouštějí při událostech cyklu, ale zde je již nelze upravovat. Převeďte je na normální automatizaci nebo je odstraňte.", - "onboarding_progress": "{n} / 3 sledovaných cyklů", - "onboarding_watching": "Nechte spotřebič běžet normálně – WashData sleduje. Po 3 cyklech začne párování programů.", "pending_feedback": "Čeká na zpětnou vazbu detekce", "performance_trend": "Trend výkonu ({n} cyklů)", - "pg_analysis_empty": "Načtěte cyklus pro zobrazení analýzy shody.", "pg_apply_confirm": "Použít nejlepší hodnotu: {label} = {value}?", - "pg_canvas_empty": "Vyberte cyklus výše a poté stiskněte Načíst pro zobrazení jeho průběhu výkonu.", - "pg_chart_caption": "Míra detekce/shody v závislosti na hodnotách {param}", "pg_ctrl_intro": "Hodnoty vycházejí z aktivního nastavení integrace pro toto zařízení. Úpravy zůstávají v testovacím prostředí, dokud je neuložíte do integrace.", "pg_live_unavailable": "Aktivní nastavení není k dispozici, zobrazují se výchozí hodnoty.", "pg_load_live_confirm": "Zahodit úpravy v testovacím prostředí ({n}) a znovu načíst aktuální nastavení integrace?", - "pg_match_rate": "{pct}% míra shody na {total} cyklech", "pg_matches_live": "Odpovídá aktivnímu nastavení", "pg_n_changed": "Změněno oproti aktivnímu nastavení: {n}", "pg_sugg_none": "Všechny návrhy již odpovídají aktuálnímu nastavení", - "pg_preset_delete_confirm": "Smazat přednastavení \"{name}\"?", - "pg_preset_limit": "Dosažen limit přednastavení ({n})", - "pg_preset_overwrite": "Přepsat přednastavení \"{name}\"?", "pg_publish_one_confirm": "Uložit {label} = {value} do nastavení tohoto zařízení?", "pg_restart_note": "Restartujte Home Assistant, abyste povolili nástroje simulace.", - "pg_sim_progress": "{done} / {total} cyklů", - "pg_starting": "Spouštění…", - "pg_stress_synthetic": "simulace nečinnosti začíná zde", - "pg_sweep_intro": "Co kdyby {param} byl jiný? Otestujte {steps} hodnot na vašich posledních {cycles} cyklech a najděte nastavení, při kterém je správně spárováno nejvíce cyklů.", - "pg_sweep_step": "Krok {done} / {total}", - "pg_verdict_bad": "Vyžaduje pozornost: mnoho cyklů zůstává nedetekováno.", - "pg_verdict_good": "Dobře vyladěno: většina cyklů je správně rozpoznána a spárována.", - "pg_verdict_ok": "Přijatelné: některé cykly nebyly zachyceny. Zkuste snížit práh spuštění.", "phase_catalog_intro": "Pojmenované segmenty cyklu (Předpírka, Ohřev, Odstřeďování…). Přiřaďte je k profilu z jeho ovládacího panelu.", "phase_ranges_intro": "Fázové rozsahy (minuty od začátku cyklu) překrývající průměrnou křivku. Upravte hodnoty pro živý náhled.", - "playground_intro": "Bezpečné prostředí pro přehrávání minulých cyklů s jiným nastavením, porovnání dvou konfigurací vedle sebe a prozkoumání, jak se cyklus zarovná k profilu. Nic zde nezmění vaše aktivní data, dokud to výslovně nepoužijete.", - "playground_needs_restart": "Restartujte Home Assistant, abyste povolili nástroje Testovacího prostředí.", - "preferences_admin": "Předvolby, panel a řízení přístupu", - "preferences_adv": "Předvolby", + "playground_intro": "Vyzkoušejte nastavení na skutečných datech svých cyklů. Nic zde nezmění živá nastavení, dokud je nepoužijete.", "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í 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.", + "process_history_hint": "Znovu spustí párování u všech uložených cyklů, obnoví návrhy, přetrénuje modely ML (je-li povoleno), přepočítá náklady cyklů podle zaznamenaných cen 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.", - "rbac_hint": "Když je vypnutá, každý uživatel Home Assistant má plný přístup (výchozí). Správci mají vždy plný přístup a mohou spravovat každého.", - "recent_logs": "Nedávné záznamy ha_washdata", + "rbac_hint": "Vypnuto: každý uživatel Home Assistant má plný přístup (výchozí). Správci mají vždy plný přístup a mohou spravovat každého.", "recording_in_progress": "Probíhá nahrávání", "reminders_intro": "Zobrazit připomínku v panelu tolik cyklů po posledním servisu. Ponechte prázdné nebo 0 pro vypnutí připomínky.", "restart_gap_footer": "Zvýrazněno v grafu. Data o výkonu pro tyto intervaly chybí; párování použilo pouze skutečná měření.", - "restart_gap_header": "{n} mezera při restartu HA během tohoto cyklu", + "restart_gap_header": "{n} mezer při restartu HA během tohoto cyklu", + "restart_gap_header_one": "{n} mezera při restartu HA během tohoto cyklu", + "restart_gap_header_few": "{n} mezery při restartu HA během tohoto cyklu", + "restart_gap_header_many": "{n} mezery při restartu HA během tohoto cyklu", + "restart_gap_header_other": "{n} mezer při restartu HA během tohoto cyklu", "restart_gap_item": "{dur} mezera", - "review_confirm_help": "Potvrďte, zda byl tento cyklus správně detekován. Vaše recenze trénují model na vašem stroji – čím více cyklů potvrdíte, tím lepší spárování a hodnocení zdraví získáte. Rychlé dobré/špatné stačí.", + "review_confirm_help": "Byl tento cyklus detekován správně? Potvrzování cyklů zlepšuje párování pro váš stroj. Stačí rychlé Dobrý/Špatný.", "review_in_settings": "Zkontrolujte v Nastavení", "review_notes_placeholder": "Poznámky (volitelné)", "review_notes_tip": "Poznámky s volným textem pro vaši vlastní potřebu. Nepoužívá se při párování nebo školení.", - "review_profile_tip": "Program, jako je tento cyklus označen. Pokud byl automaticky zjištěný program nesprávný, opravte jej zde – označení učí párování pro budoucí cykly.", - "review_quality_tip": "Jak čistý je tento cyklus. Dobrý = učebnicový příklad tohoto programu; Špatný = detekovaný, ale hlučný nebo atypický; Nepoužitelný = chybně zjištěný (sloučený, zkrácený nebo falešný). Řídí skóre zdraví a které cykly mohou model trénovat.", - "review_recorded_tip": "Označte to jako ručně vybraný referenční cyklus pro svůj program – stejná role jako ručně zaznamenaný cyklus. Referenční cykly jsou vždy zachovány, nasazují odpovídající šablonu a při čištění se nikdy nevynechají. (Toto je \"zlatá\" / zaznamenaná vlajka; obě jsou totéž.)", - "review_tags_tip": "Volitelné příznaky popisující, co se v tomto cyklu pokazilo, takže za to může školení a čištění.", + "review_profile_tip": "Program, kterým je tento cyklus označen. Pokud je špatně, opravte jej zde; štítky učí párování.", + "review_quality_tip": "Jak čistý je tento cyklus. Dobrý: učebnicový běh tohoto programu. Špatný: detekovaný, ale zašuměný nebo neobvyklý. Nepoužitelný: chybně detekovaný (sloučený, zkrácený nebo falešný). Ovlivňuje skóre zdraví a které cykly trénují model.", + "review_recorded_tip": "Označit jako ručně vybraný referenční cyklus jeho programu, stejně jako ručně nahraný cyklus. Referenční cykly se vždy zachovají, tvoří šablonu párování a čištění je nikdy neodstraní.", + "review_tags_tip": "Volitelné příznaky, co se pokazilo, aby s tím trénování a čištění mohly počítat.", "review_to_cycles": "Otevřete frontu kontroly cyklů", "samples_decimated": "Zobrazeno {shown} z {total} vzorků (prořídnuto pro zobrazení; špičky zachovány). Široká mezera zde znamená prořídnutí, nikoli chybějící data.", - "saving_triggers_reload": "Uložení spustí opětovné načtení integrace. Entity HA se mohou krátce zobrazit jako nedostupné.", + "saving_triggers_reload": "Uložení znovu načte integraci. Entity se mohou krátce zobrazit jako nedostupné.", "search_placeholder": "Vyhledávání nastavení…", "see_recorder": "Viz widget rekordéru níže", "setting_changed": "Změněno z {old} na {new} dne {date}", "settings_basic_note": "Zobrazují se základní nastavení. Přepněte na Pokročilé pro úplný seznam.", - "shape_drift_advisory": "⚠ Drift tvaru", - "shape_drift_detail": "Vzor příkonu pro tento profil se časem posunul – možné opotřebení spotřebiče nebo potřebná údržba (např. odvápnění, čištění filtru).", "show_all_settings": "Zobrazit všechna nastavení", - "showing_suggestions": "Zobrazuje se nastavení {count} s návrhy.", - "split_intro": "Kliknutím na graf přidáte nebo odstraníte dělicí bod nebo automaticky rozpoznáte mezery v nečinnosti. Každý výsledný segment může získat svůj vlastní profil.", - "storage_diagnostics": "Statistiky úložiště, údržba, export/import", + "showing_suggestions": "Zobrazuje se {count} nastavení s návrhy.", + "showing_suggestions_one": "Zobrazuje se {count} nastavení s návrhy.", + "showing_suggestions_few": "Zobrazují se {count} nastavení s návrhy.", + "showing_suggestions_many": "Zobrazuje se {count} nastavení s návrhy.", + "showing_suggestions_other": "Zobrazuje se {count} nastavení s návrhy.", + "split_intro": "Kliknutím na graf přidáte nebo odeberete bod rozdělení, nebo jej najděte automaticky podle mezer nečinnosti. Každá část může dostat vlastní profil.", "sug_staged": "Nastaveno {key} = {val}. Uložte pro použití.", "sug_muted": "Toto nastavení již nebude navrhováno", "sug_mute_failed": "Skrytí návrhu se nezdařilo", "sug_unmuted_all": "Skryté návrhy obnoveny", - "n_suggestions_muted": "{count} skrytých; automatický ladicí nástroj tato nastavení nebude navrhovat.", + "n_suggestions_muted": "{count} návrhů je skryto; automatický ladicí nástroj je nebude navrhovat.", + "n_suggestions_muted_one": "{count} návrh je skrytý; automatický ladicí nástroj ho nebude navrhovat.", + "n_suggestions_muted_few": "{count} návrhy jsou skryté; automatický ladicí nástroj je nebude navrhovat.", + "n_suggestions_muted_many": "{count} návrhu je skryto; automatický ladicí nástroj je nebude navrhovat.", + "n_suggestions_muted_other": "{count} návrhů je skryto; automatický ladicí nástroj je nebude navrhovat.", "toast_auto_detect_enabled": "Automatické rozpoznávání povoleno", "toast_auto_label_complete": "Automatické označování dokončeno", "toast_auto_label_failed": "Automatické označování selhalo: {error}", @@ -984,7 +955,6 @@ "toast_no_automation": "Zde nelze vytvořit automatizaci", "toast_profile_name_required": "Název profilu je povinný", "toast_program_set": "Program nastaven: {program}", - "toast_rebuild_failed": "Přestavba selhala: {error}", "toast_resume_failed": "Obnovení selhalo: {error}", "toast_revert_failed": "Vrácení selhalo: {error}", "toast_reverted": "{key} vráceno zpět", @@ -995,95 +965,82 @@ "trend_energy_up": "Trend energie stoupá ({pct} %/cyklus) – poslední průměr {avg}", "trim_destructive_confirm": "Tímto zůstane jen {pct}% cyklu a akci nelze vrátit zpět. Pokračovat?", "trim_intro": "Přetáhněte červené úchyty nebo zadejte hodnoty. Vše mimo okno je odstraněno.", - "tuning_suggestions_available": "{count} návrh ladění dostupný z pozorovaných cyklů. Zobrazují se vedle příslušných polí.", + "tuning_suggestions_available": "{count} návrhů ladění je k dispozici na základě pozorovaných cyklů. Zobrazují se vedle příslušných polí.", + "tuning_suggestions_available_one": "{count} návrh ladění je k dispozici na základě pozorovaných cyklů. Zobrazuje se vedle příslušného pole.", + "tuning_suggestions_available_few": "{count} návrhy ladění jsou k dispozici na základě pozorovaných cyklů. Zobrazují se vedle příslušných polí.", + "tuning_suggestions_available_many": "{count} návrhu ladění je k dispozici na základě pozorovaných cyklů. Zobrazují se vedle příslušných polí.", + "tuning_suggestions_available_other": "{count} návrhů ladění je k dispozici na základě pozorovaných cyklů. Zobrazují se vedle příslušných polí.", "unsure_detected_prefix": "WashData si není jistý, zda jej detekoval", - "updated": "Aktualizováno", "warmup_badge": "Stále se učí ({done}/{needed} cyklů)", - "warmup_detail": "Tento profil potřebuje {needed} označených cyklů, než začne automatické párování. Každý potvrzený cyklus pomáhá učení.", + "warmup_detail": "Párování už funguje. WashData vás žádá o potvrzení prvních {needed} cyklů tohoto profilu, než začne označovat samo.", "wipe_history_warning": "Trvale smažte všechny cykly a profily. Nelze vrátit zpět.", "compare_overlay_tip": "Překryjte naučené obálky profilů, abyste viděli, který program každý cyklus připomíná.", - "duration_trend_up_tip": "Doba trvání roste ({pct}%/cyklus)", - "duration_trend_down_tip": "Doba trvání klesá ({pct}%/cyklus)", - "energy_trend_up_tip": "Energie roste ({pct}%/cyklus)", - "energy_trend_down_tip": "Energie klesá ({pct}%/cyklus)", - "performance_trending": "Trend výkonu", - "pg_select_cycle_hint": "Vyberte cyklus a kliknutím na ↺ jej načtěte", - "pg_sweep_progress": "Krok {done} / {total}", - "pg_analysis_hint": "Vyberte cyklus a kliknutím na ↺ načtěte analýzu.", "diagnostics_load_failed": "Nepodařilo se načíst diagnostiku: {error}", - "manual_duration_ref_hint": "Použije se pouze tehdy, když není vybrán žádný referenční cyklus. Referenční cyklus nastaví trvání podle vlastní délky.", + "manual_duration_ref_hint": "Použije se jen bez referenčního cyklu; referenční cyklus nastaví trvání podle své délky.", + "duration_learned_hint": "Vypočteno z cyklů tohoto programu (bez odlehlých hodnot) a aktualizuje se po každém novém. Chcete-li hodnotu změnit, ořízněte nebo odstraňte cykly, které nesedí.", "toast_convert_rolled_back": "Převod selhal a byl vrácen zpět (žádná automatizace nezůstala): {error}", "toast_convert_orphan": "Automatizace byla vytvořena, ale vymazání starých akcí selhalo. Nezkoušejte to znovu: staré akce odstraňte ručně, abyste předešli duplicitní automatizaci.", - "advisory_duration_trend_up": "Cykly '{name}' trvají stále déle (přibližně +{pct}% na cyklus). Pokud se chování spotřebiče změnilo, nahrajte nebo přebudujte tento profil znovu.", - "advisory_energy_trend_up": "'{name}' odebírá stále více energie (přibližně +{pct}% na cyklus) - pokud je to nečekané, vyplatí se spotřebič zkontrolovat.", - "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í.", + "advisory_duration_trend_up": "Cykly '{name}' trvají stále déle (asi +{pct}% na cyklus). Pokud se spotřebič změnil, nahrajte nebo přepočítejte tento profil znovu.", + "advisory_energy_trend_up": "'{name}' spotřebuje s každým cyklem více energie (asi +{pct}% na cyklus). Pokud je to nečekané, zkontrolujte spotřebič.", + "advisory_poor_health": "'{name}' sedí špatně: jeho poslední cykly se hodně liší nebo se slabě shodují. Projděte jeho cykly nebo jej nahrajte znovu, aby párování a odhady zůstaly přesné.", + "advisory_shape_drift": "'{name}' se odchýlil od původního tvaru křivky výkonu, možná kvůli vodnímu kameni nebo opotřebení. Zvažte nové nahrání z posledních cyklů.", + "advisory_shape_drift_corr": "'{name}' se odchýlil od původního tvaru křivky výkonu (korelace {corr}), možná kvůli vodnímu kameni nebo opotřebení. Zvažte nové nahrání z posledních cyklů.", + "advisory_unmatchable": "'{name}' se nikdy nepřiřadí: nestojí za ním žádný cyklus s daty o výkonu. Opravíte to označením nebo nahráním cyklu tohoto programu.", + "advisory_unmatchable_no_duration": "'{name}' se nikdy nepřiřadí: jeho cyklus nemá použitelnou dobu trvání. Nahrajte tento program znovu nebo nastavte jeho očekávanou dobu trvá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_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.", "pg_sweep_intro2": "Najděte nastavení, které nejlépe splňuje cíl napříč vašimi nedávnými cykly. Dokud jej nepoužijete, nic se nezmění.", - "pg_running_history": "Přehrávání vašich cyklů…", - "pg_running_sweep": "Testování hodnot napříč vašimi cykly…", "pg_row_load_hint": "Načíst tento cyklus do grafu výše", - "pg_history_intro2": "Přehrajte své nedávné cykly skutečným detektorem a nástrojem pro přiřazování programů s nastavením výše. Kliknutím na libovolný řádek načtete daný cyklus do grafu; úpravou nastavení zobrazíte srovnání před úpravou a po ní.", + "pg_history_intro2": "Přehrajte nedávné cykly skutečným detektorem a párováním s nastavením výše. Kliknutím na řádek jej načtete do grafu; změnou nastavení porovnáte před a po.", "pg_simulating": "Simulace cyklu…", - "pg_canvas_empty2": "Vyberte cyklus výše a stisknutím Spustit jej nasimulujte. Poté najetím zobrazíte hodnoty, kolečkem přiblížíte a tažením posunete.", "pg_apply_settings_confirm": "Uložit těchto {n} nastavení do tohoto zařízení? {list}", - "pg_stress_terminated_detail": "Ustálil se na ~{idle}W v nečinnosti -- cyklus skončil o {h} h {m} min později přes {reason}.", - "pg_stress_above_threshold_detail": "Příkon v nečinnosti ~{idle}W je roven nebo vyšší než efektivní práh zastavení ({stop}W) -- cyklus se nikdy nezaregistroval jako klidový. Opravte to zvýšením stop_threshold_w.", - "pg_stress_hit_cap_detail": "Cyklus běžel {h} h {m} min bez zastavení -- vynuceně zastaven bezpečnostním limitem. Příkon v nečinnosti {idle}W oproti prahu zastavení {stop}W.", "no_logs_match": "Těmto filtrům neodpovídají žádné záznamy protokolu.", "pg_analysis_hint2": "Vyberte cyklus a stiskněte Spustit pro načtení analýzy shody.", - "pg_analysis_empty2": "Stiskněte Spustit pro zobrazení analýzy shody.", - "imported_readonly": "Importováno z komunitního obchodu. Zobrazuje se pro referenci a porovnávání. Nezapočítává se do vašich statistik a nelze jej upravovat.", + "imported_readonly": "Importováno z komunitního obchodu. Slouží jako reference a pro párování, nezapočítává se do statistik a nelze jej upravovat.", "merge_no_imports": "Importované záznamy nelze sloučit se skutečným cyklem. Zrušte jejich výběr pro sloučení.", - "store_enable_hint": "Povolte online funkce v Nastavení, abyste mohli procházet a importovat referenční cykly komunity.", - "advisory_duration_outlier": "Cykly označené jako '{name}' ({n}) jsou příliš daleko od jeho obvyklé délky, než aby se s ním kdy mohly shodovat (nejhorší: {ratio}x). Pravděpodobně jsou špatně označené, nebo jeden záznam zachytil dva běhy. Označte je znovu nebo je rozdělte, aby přestaly zkreslovat odhad času tohoto programu.", + "store_enable_hint": "Zapněte online funkce v sekci Předvolby > Online a komunita, abyste mohli procházet a importovat referenční cykly komunity.", + "advisory_duration_outlier": "Cykly označené jako ‘{name}’ ({n}) jsou příliš daleko od jeho obvyklé délky, aby se kdy přiřadily (nejhorší: {ratio}x). Nejspíš jsou špatně označené nebo obsahují dva běhy. Označte je znovu nebo rozdělte, aby přestaly zkreslovat odhad času.", "standby_above_stop_title": "Při ukončení cyklu stále odebírá {idle} W", "standby_above_stop_sub": "{n} z posledních {total} cyklů. Zpoždění vypnutí se začne počítat až ve chvíli, kdy příkon klesne pod Práh zastavení ({stop} W), takže cykly končí pozdě nebo je nutné je zastavit ručně.", - "online_intro": "Sdílejte a stahujte referenční cykly s ostatními uživateli WashData. Všechny online funkce jsou dobrovolné a ve výchozím nastavení vypnuté.", "store_share_intro": "Nahrajte tento referenční cyklus, aby jej mohli používat ostatní se stejným spotřebičem. Před zveřejněním bude zkontrolován.", - "online_intro_global": "Procházejte a sdílejte referenční záznamy s ostatními uživateli WashData a potvrzujte záznamy spotřebičů. Jedno připojení platí pro celou vaši integraci WashData; značka a model spotřebiče se nastavují pro každé zařízení v sekci Základní. Všechny online funkce jsou dobrovolné a ve výchozím nastavení vypnuté.", + "online_intro_global": "Procházejte a sdílejte referenční záznamy s ostatními uživateli WashData. Jedno připojení platí pro všechna vaše zařízení WashData; značka a model se nastavují pro každé zařízení v sekci Základní. Online funkce jsou vypnuté, dokud je nezapnete.", "online_unavailable": "Online funkce nejsou na tomto serveru dostupné.", "store_picker_offline": "Povolte online funkce v ozubeném kolečku nastavení, abyste mohli vybírat z katalogu komunity.", "pick_brand_first": "Nejprve vyberte značku spotřebiče.", - "model_not_found": "Zatím není v katalogu.", - "connect_to_confirm": "Připojte se v ozubeném kolečku nastavení pro potvrzení nebo hodnocení.", + "custom_task_limit": "Dosáhli jste maximálního počtu úkolů ({max}).", + "custom_tasks_intro": "Přidejte cokoli dalšího, na co chcete být upozorňováni. Úkol je na řadě, jakmile uplyne kterýkoli z intervalů; jeden nechte prázdný nebo 0, aby platil jen ten druhý.", + "maintenance_due_sensor_hint": "Binární senzor Údržba na řadě je zapnutý, dokud je některý úkol na řadě; použijte jej ve vlastních automatizacích oznámení.", + "maintenance_supporter_intro": "Maintenance Supporter je samostatná integrace, která umí mnohem víc: libovolný spotřebič nebo úkol, plánování podle času, používání nebo senzoru, oznámení a historii. Může využívat počítadlo cyklů WashData.", "adopt_settings_hint": "Použít nastavení detekce a porovnávání sdíleného zařízení na toto zařízení", - "include_settings_hint": "Zahrnout prahy detekce, váhy porovnávání a další parametry do sdíleného balíčku", + "include_settings_hint": "Sdílet prahy detekce a párování tohoto zařízení (ne oznámení, entity ani cenu energie). Ostatní se rozhodnou, zda je použijí.", "onboard_download": "Stáhnout hotové nastavení z komunitního obchodu", - "share_consent": "Sdílíte skutečná data ze svého spotřebiče. Nesdílejte, pokud jsou vaše vzorce používání soukromé.", "share_device_none": "Zatím nejsou nastavena žádná zařízení. Nejprve přidejte zařízení.", "share_guideline_naming": "Používejte srozumitelné názvy programů (např. 'Cotton 40', 'Eco 60'), aby je ostatní mohli identifikovat", - "share_guideline_quality": "Sdílejte pouze cykly, které proběhly normálně: bez přerušení uprostřed cyklu, otevření dvířek nebo krátkých výpadků napájení.", + "share_guideline_quality": "Sdílejte jen cykly, které skončily normálně: bez přerušení, otevření dvířek a výpadků napájení.", "share_guideline_review": "Před sdílením zkontrolujte své profily -- odstraňte ty, které vypadají špatně", "share_guidelines_title": "Před sdílením", - "store_download_device_intro": "Stáhnout nastavení zařízení komunity a použít ho na nové nebo stávající zařízení", + "store_download_device_intro": "Přidat do zařízení všechny sdílené programy a jejich referenční cykly. Vaše vlastní cykly a statistiky se nezmění.", "store_share_device_intro": "Nahrajte {brand} {model} s referenčními cykly, které vyberete. Ostatní se stejným spotřebičem mohou vaše programy přijmout. Záznamy jsou před zveřejněním zkontrolovány.", "share_profile_no_cycles": "Žádné referenční cykly. Chcete-li tento profil zahrnout, označte cyklus jako ⭐ na kartě Cykly", - "advisory_phase_inconsistent": "Zdá se, že '{name}' míchá různé programy nebo teploty - jeho cykly se ohřívají po velmi různě dlouhou dobu. Rozdělení na samostatné profily (např. podle teploty) zlepší shodu i odhady času.", - "advisory_phase_inconsistent_title": "⚠ Možná smíšené programy", - "export_select_intro": "Zaškrtněte přesně to, co má být zahrnuto. Výběr profilů bez jejich cyklů stále exportuje rozpoznatelný program (jeho naučená křivka se přenese s ním).", - "import_analyze_hint": "Načtěte exportovaný soubor (nebo vložte jeho JSON). WashData jej analyzuje a přesně ukáže, co lze importovat, dříve než dojde k jakékoli změně.", + "export_select_intro": "Zaškrtněte, co zahrnout. Profily bez svých cyklů stále exportují rozpoznatelný program.", + "import_analyze_hint": "Načtěte exportovaný soubor (nebo vložte jeho JSON). WashData ukáže, co lze importovat, dřív než se cokoli změní.", "analyzing": "Analyzuji…", - "device_type_mismatch_warn": "Tento export pochází z jiného typu spotřebiče ({src} vs {local}). Programy a cykly lze stále importovat jako referenční data, ale nastavení specifická pro zařízení a import skutečné historie jsou zakázány.", + "device_type_mismatch_warn": "Tento export pochází z jiného typu spotřebiče ({src} vs {local}). Programy a cykly lze stále importovat jako referenční data, ale ne nastavení zařízení ani skutečnou historii.", "merge_hint": "Importované položky se přidají; nic místního se neztratí. Konflikty názvů se řeší níže.", "replace_warn": "Každá zaškrtnutá kategorie se vymaže a nahradí daty ze souboru. Nezaškrtnuté kategorie zůstanou beze změny.", "dest_reference_hint": "Importované cykly pouze zlepšují rozpoznávání programů a nikdy neovlivňují statistiky spotřeby/energie.", - "dest_real_history_hint": "Importované cykly se počítají jako vlastní historie tohoto zařízení a přispívají do statistik energie/spotřeby. Použijte při přesunu jednoho spotřebiče do nové instalace.", - "import_history_description": "Měli jste chytrou zástrčku už před WashData? Nahrajte export historie jejího snímače výkonu nebo ji načtěte přímo z Home Assistant a proběhne nad ní běžná detekce, takže se dřívější cykly objeví v seznamu Cykly připravené k pojmenování.", - "hist_input_hint": "Nahrajte CSV stažené z panelu Historie (entita, stav, poslední změna), nebo nechte WashData načíst historii snímače přímo. Detekce pak proběhne úplně stejně jako naživo a vy vyberete, které z nalezených cyklů zachovat.", - "hist_recorder_hint": "Načte data od zvoleného data až do teď. Home Assistant ve výchozím nastavení uchovává podrobnou historii 10 dní a poté už jen hodinové průměry, které jsou pro detekci cyklů příliš hrubé - dřívější datum volte jen tehdy, pokud máte recorder nastavený na delší uchovávání.", - "hist_scanning": "Přehráváme vaši historii přes detektor. Běží to na pozadí - tento dialog můžete zavřít a vrátit se k němu později.", + "dest_real_history_hint": "Importované cykly se počítají jako vlastní historie a statistiky tohoto zařízení. Použijte při přesunu spotřebiče do nové instalace.", + "import_history_description": "Měli jste chytrou zásuvku už před WashData? Nahrajte její historii výkonu nebo ji načtěte z Home Assistant. Proběhne nad ní detekce a dřívější cykly se objeví v seznamu Cykly připravené k pojmenování.", + "hist_input_hint": "Nahrajte CSV z panelu Historie (entita, stav, poslední změna), nebo nechte WashData načíst historii senzoru přímo. Proběhne nad ní živá detekce a vy vyberete, které cykly zachovat.", + "hist_recorder_hint": "Načte data od zvoleného data do teď. Home Assistant ve výchozím nastavení uchovává podrobnou historii 10 dní; starší hodinové průměry jsou pro detekci cyklů příliš hrubé.", + "hist_scanning": "Vaše historie prochází detektorem na pozadí. Tento dialog můžete zavřít a vrátit se později.", "hist_imported_count": "Importované cykly: {n}.", "hist_duplicates": "Již dříve importováno a přeskočeno: {n}.", "hist_capped": "Byl dosažen limit importovaných cyklů na zařízení; zbytek nebyl uložen.", - "hist_next_step": "Najdete je v seznamu Cykly, označené jako importovaná historie. Otevřete některý z nich a tlačítkem Označit pojmenujte program, ke kterému patří.", + "hist_next_step": "Najdete je v seznamu Cykly, označené jako importovaná historie. Otevřete některý a tlačítkem Označit pojmenujte jeho program.", "hist_rows_read": "Přečtená měření: {n}", "hist_breaks": "Mezery, kdy byl snímač nedostupný: {n}", "hist_other_entity": "Ignorovaná měření jiných entit: {n}", @@ -1095,25 +1052,76 @@ "hist_scan_capped": "Zobrazují se jen první kandidáti (nalezeno bylo {n}).", "hist_recorder_empty": "Home Assistant nemá pro tento snímač v daném období žádnou podrobnou historii.", "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_scan_expired": "Toto skenování už není dostupné. Skenujte 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.", - "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.", + "imported_history_readonly": "Nalezeno v importované historii výkonu. Pomáhá párování, ale nezapočítává se do statistik a nelze jej oříznout ani rozdělit. Označením pojmenujte program.", + "odometer_intro": "Tento součet jen roste. Smazání cyklu jej nesníží, takže připomínky servisu zůstávají správné.", + "odometer_correct_hint": "Nastavte, pokud WashData započítala cyklus, který neproběhl, nebo spotřebič 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}", "toast_range_required": "Nastavte platný rozsah Od/Do", - "no_profiles_option": "(zatím žádné profily, přidejte nový)" + "no_profiles_option": "(zatím žádné profily, přidejte nový)", + "compare_cycles_title": "Porovnat cykly ({count})", + "edit_builtin_phase": "Toto je vestavěná fáze. Uložením vznikne vlastní kopie; smazáním kopie obnovíte originál.", + "fetch_error": "Načtení dat se nezdařilo.", + "connection_lost": "Spojení ztraceno - zobrazují se data k {time}.", + "pg_notify_partial": "Značky ukazují jen oznámení o startu, téměř hotovo, dokončení, milnících a tichých hodinách. Živé aktualizace, připomínky vyložení a upozornění na překročení se nesimulují.", + "connect_to_confirm": "Připojte se v ozubeném kolečku nastavení pro potvrzení nebo hodnocení.", + "coverage_gap_cluster": "Nedávné neoznačené cykly ({count}) o délce přibližně {min} min vypadají jako jeden program, který jste ještě nevytvořili.", + "pg_label_yes": "Na konci cyklu by byl označen jako \"{name}\"", + "pg_label_no": "Byla by vyžádána revize: {reason}", + "live_match_debug_tip": "Spolehlivost: jak přesně aktuální křivka příkonu odpovídá nejlepšímu kandidátnímu profilu (0-100 %). Nejednoznačné: dva nejlepší kandidáti mají skóre do 5 % od sebe, takže štítek je nejistý až do konce cyklu.", + "automation_alias_started": "{device}: cyklus spuštěn", + "automation_alias_finished": "{device}: cyklus dokončen", + "automation_desc_started": "Spustí se, když začne cyklus zařízení {device}. Přidejte své akce (oznámení, světla, ...).", + "automation_desc_finished": "Spustí se, když skončí cyklus zařízení {device}. Přidejte své akce (oznámení, světla, ...).", + "feedback_detected": "WashData rozpoznal: {name}", + "import_undo_hint": "Je uchován bod obnovení z doby před posledním importem ({when}): {profiles} profilů a {cycles} cyklů.", + "replace_undo_hint": "Nejprve se uloží bod obnovení, takže tento import můžete vrátit zpět v části Export / Import.", + "pg_in_sample_note": "Tyto cykly pomohly vytvořit profily, se kterými se porovnávají, takže výsledky jsou optimistické: u nových cyklů čekejte o něco méně správných přiřazení.", + "hist_tail_trimmed": "Oříznuto {min} min čekání po skončení cyklu, stejně jako to dělá běžící integrace", + "hist_truncated": "zastaveno na limitu počtu měření ({n}), zbytek historie proto nebyl prohledán", + "hist_warn_kw": "Nejvyšší hodnota je jen {w}. Tento senzor zřejmě hlásí kilowatty; WashData potřebuje watty, takže nelze detekovat žádné cykly.", + "hist_warn_naive_time": "{n} časových značek nemá časové pásmo a bylo načteno jako UTC. Pokud soubor používá místní čas, jsou cykly posunuté o váš posun vůči UTC a hodnoty kolem změny času mohou být vynechány.", + "hist_warn_naive_time_one": "{n} časová značka nemá časové pásmo a byla načtena jako UTC. Pokud soubor používá místní čas, jsou cykly posunuté o váš posun vůči UTC a hodnoty kolem změny času mohou být vynechány.", + "hist_warn_naive_time_few": "{n} časové značky nemají časové pásmo a byly načteny jako UTC. Pokud soubor používá místní čas, jsou cykly posunuté o váš posun vůči UTC a hodnoty kolem změny času mohou být vynechány.", + "hist_warn_naive_time_many": "{n} časové značky nemá časové pásmo a bylo načteno jako UTC. Pokud soubor používá místní čas, jsou cykly posunuté o váš posun vůči UTC a hodnoty kolem změny času mohou být vynechány.", + "hist_warn_naive_time_other": "{n} časových značek nemá časové pásmo a bylo načteno jako UTC. Pokud soubor používá místní čas, jsou cykly posunuté o váš posun vůči UTC a hodnoty kolem změny času mohou být vynechány.", + "pg_compare_profile_tip": "Vybírá jen profil vykreslený přes graf. Simulace vždy přiřazuje program k cyklu sama, stejně jako běžící integrace.", + "pg_outcome_stale": "Od spuštění této simulace se nastavení změnilo. Stále zobrazuje starý výsledek.", + "pg_sweep_guarded": "Ukončí více cyklů předčasně, více jich rozdělí nebo vynechá cykly, které současné nastavení zachytí, proto se nikdy nedoporučuje.", + "pg_sweep_keep_current": "Ponechat současnou hodnotu ({value}, {metric}): žádná hodnota nebyla lepší alespoň o jeden cyklus.", + "pg_sweep_n_cycles": "na {count} cyklech", + "pg_sweep_n_cycles_one": "na {count} cyklu", + "pg_sweep_n_cycles_few": "na {count} cyklech", + "pg_sweep_n_cycles_many": "na {count} cyklu", + "pg_sweep_n_cycles_other": "na {count} cyklech", + "pg_task_busy": "Pro toto zařízení už běží „Otestovat na historii“ nebo „Optimalizovat“. Počkejte na dokončení, nebo běh zrušte.", + "cycle_context_loading": "Načítání historie z recorderu…", + "cycle_context_note": "Šedě: historie snímače výkonu z recorderu kolem tohoto cyklu. Není součástí cyklu a neovlivňuje trvání, energii ani párování.", + "cycle_context_none": "Recorder kolem tohoto cyklu neuchovává žádné odečty (Home Assistant ve výchozím nastavení uchovává historii 10 dní).", + "cycle_context_tip": "Jen pro zobrazení: ukazuje historii snímače výkonu z recorderu kolem tohoto cyklu, pokud je ještě uložená. Chcete-li přidávat neúspěšné pokusy o spuštění do budoucích cyklů, použijte Náběh křivky (Nastavení > Detekce > Start cyklu), který mění uložené doby trvání.", + "invalid_threshold_pair": "Práh zastavení ({stop} W) musí být nižší než Práh spuštění ({start} W). Nic nebylo uloženo; změňte obě hodnoty společně." }, "phase_desc": { "anti_crease": "Občasné krátké otáčení bubnu po dokončení cyklu pro prevenci mačkání.", "anti_wrinkle": "Pravidelné otáčení bubnu po cyklu pro prevenci mačkání.", + "baking": "Topné těleso na vysoké teplotě vytváří kůrku a střídu.", + "cooking": "Hlavní fáze přípravy se zapnutým topením a ventilátorem.", "cool_down": "Otáčení bubnu bez tepla ke konci cyklu.", + "cool_down_af": "Chlazení jen ventilátorem po ohřevu.", "drain_and_switch": "Přechod z režimu praní do režimu sušení.", "dry": "Fáze sušení pomocí ohřívače a/nebo zbytkového tepla.", "drying": "Hlavní vyhřívaná perioda omílání.", "heat_up": "Počáteční zahřátí ohřívače před úplným sušením.", + "keep_warm": "Mírný ohřev, aby jídlo zůstalo teplé.", + "keep_warm_bm": "Mírný ohřev, aby chléb po upečení zůstal teplý.", + "kneading": "Motor míchá a hněte těsto. Vysoký příkon.", + "pause_af": "Krátká pauza na protřepání nebo kontrolu.", + "pre_heat": "Úvodní ohřev komory před samotnou přípravou.", "pre_rinse": "Počáteční ostřik před mytím detergentem.", + "proving": "Kynutí v mírném teple: droždí kvasí a těsto nabývá na objemu.", + "resting": "Krátká pauza s nízkým příkonem mezi hnětením, aby se lepek uvolnil.", "pre_wash": "Počáteční namáčení nebo předúprava před hlavním praním.", "rinse": "Fáze oplachu čistou vodou. Tato fáze se může opakovat několikrát.", "sanitize": "Vysokoteplotní čisticí stupeň pro sanitační programy.", @@ -1126,6 +1134,7 @@ "wash_dw": "Hlavní mycí prostředek s ohřevem." }, "placeholder": { + "custom_task": "např. Vyčistit těsnění dvířek", "add": "přidat…", "group_name": "např. Bavlna 2:47", "json_buttons": "[{\"action\":\"ID\",\"title\":\"Popisek\"}]", @@ -1153,10 +1162,10 @@ "section": { "dishwasher": { "label": "Myčka nádobí", - "intro": "Zpracování konce cyklu u myček nádobí, které obvykle končí dlouhou téměř tichou fází sušení před krátkým závěrečným odčerpáním vody." + "intro": "Zpracování konce u myček nádobí, které často dlouho a tiše suší před krátkým závěrečným odčerpáním." }, "anti_wrinkle": { - "intro": "Režim ochrany před zmačkáním detekuje krátké pulzy nízkého výkonu po hlavní fázi a připojuje je k dokončenému cyklu.", + "intro": "Ponechá krátké pulzy bubnu s nízkým výkonem po hlavní fázi sušičky u dokončeného cyklu.", "label": "Ochrana před zmačkáním" }, "basic": { @@ -1164,7 +1173,7 @@ "label": "Základní" }, "delay": { - "intro": "Detekce zpožděného startu identifikuje, kdy je spotřebič zapnut, ale cyklus ještě nezačal.", + "intro": "Detekuje spotřebič, který je zapnutý a čeká na odložený start.", "label": "Zpožděný start" }, "detection": { @@ -1188,157 +1197,149 @@ "label": "Časování a hlídač" }, "triggers": { - "intro": "Volitelné externí signály: spouštěč konce, senzor dveří, přepínač pauzy a připomínka vyložení.", + "intro": "Volitelné doplňky: spouštěč konce, senzor dveří, přepínač pauzy a připomínka vyložení.", "label": "Spouštěče a dveře" - }, - "phase_eta": { - "label": "Zbývající čas", - "intro": "Zbývající čas zohledňující fáze, pro spotřebiče, jejichž délka cyklu závisí na teplotě nebo odstřeďování." } }, "setting": { "dishwasher_end_spike_quiet_release": { "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." + "doc": "Po očekávané době trvání: jak dlouho musí myčka zůstat pod Prahem zastavení, než WashData přestane čekat na závěrečné odčerpání. Zvyšte, pokud se pozdní odčerpání po dlouhém sušení nezachytí. Čekání nikdy nepřekročí 30 minut." }, "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í.", + "doc": "Zlomek průměrné doby trvání programu, kterého musí cyklus dosáhnout, než se otáčení bubnu s nízkým výkonem počítá jako ocas ochrany před zmačkáním. U sušiček řízených náplní jej snižte (např. 0,75). U praček ponechte výchozí hodnotu (0,98), jinak může praní skončit v polovině.", "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.", + "doc": "Ponechat krátké pulzy bubnu s nízkým výkonem po hlavní fázi ohřevu sušičky u dokončeného cyklu, místo aby se četly jako nové cykly.", "label": "Povolit detekci ochrany před zmačkáním" }, "anti_wrinkle_exit_power": { - "doc": "Příkon musí klesnout pod tuto hodnotu mezi impulzy, aby režim ochrany před mačkáním zůstal aktivní.", + "doc": "V režimu ochrany před zmačkáním se příkon pod touto hodnotou počítá jako klid mezi impulzy otáčení a klid delší než Max. mezera mezi impulzy režim ukončí. Pokud je Práh zastavení vyšší, použije se on, takže nastavení má vliv jen nad Prahem zastavení.", "label": "Práh výkonu pro ukončení" }, "anti_wrinkle_idle_timeout": { - "doc": "Jak dlouho může stroj mezi dvěma impulzy bubnu zůstat v klidu, než skončí režim ochrany před zmačkáním. Nastavte ji nad nejdelší mezeru, kterou vaše sušička mezi impulzy nechává, jinak se každý další impulz vyhodnotí jako falešné spuštění.", + "doc": "Jak dlouho může sušička mezi pulzy bubnu zůstat v klidu, než režim ochrany před zmačkáním skončí. Nastavte nad nejdelší mezeru vaší sušičky, jinak se další pulzy vyhodnotí jako falešné starty.", "label": "Max. mezera mezi impulzy" }, "anti_wrinkle_max_duration": { - "doc": "Impulzy delší než toto jsou považovány za skutečný cyklus spíše než za otočení ochrany před mačkáním.", + "doc": "Pulz delší než tato doba se počítá jako skutečný cyklus, ne jako otáčení ochrany před zmačkáním.", "label": "Max. délka trvání" }, "anti_wrinkle_max_power": { - "doc": "Impulz nad tímto příkonem je považován za skutečný nový cyklus, ne za otočení ochrany před mačkáním. Nastavte těsně nad příkon impulzu ochrany.", + "doc": "Pulz nad tímto výkonem se počítá jako skutečný nový cyklus, ne jako otáčení bubnu. Nastavte těsně nad výkon pulzů bubnu.", "label": "Max. výkon ochrany před zmačkáním" }, "auto_label_confidence": { - "doc": "Pokud je skóre shody na konci cyklu rovno nebo vyšší než toto, program je automaticky označen bez jakéhokoliv potvrzení. Zvyšte pro vyšší jistotu před automatickým označením; snižte pro více automatizace. Funguje ve spojení s Důvěrou učení níže.", + "doc": "Skóre shody na konci cyklu, od kterého se program označí bez dotazu. Zvyšte pro větší jistotu; snižte, aby se automaticky označovalo více cyklů. Držte nad Důvěrou učení.", "label": "Důvěra auto-označení" }, "auto_maintenance": { - "doc": "Spusťte noční údržbu: obnověte obálky profilů, přepočítejte zdraví cyklů, ořízněte ladicí stopy a zachovejte nejnovější cykly.", + "doc": "Noční úklid: označí starší cykly, které nyní jasně odpovídají programu, přepočítá křivky programů, obnoví značky anomálií a zdraví cyklů a odstraní ladicí stopy, pokud je vypnuto Uložit ladící stopy. Cykly a jejich záznamy výkonu se vždy zachovají.", "label": "Automatická údržba (noční čištění)" }, "completion_min_seconds": { - "doc": "Cykly kratší než toto jsou zahozeny jako duchové cykly (zkušební chody, otevření dveří pro přidání ponožky).", + "doc": "Cykly kratší než tato doba (zkušební chody, otevření dvířek kvůli přidání ponožky) se uloží jako přerušené a nedostanou oznámení o dokončení.", "label": "Min. délka cyklu" }, "delay_confirm_seconds": { - "doc": "Příkon musí zůstat v pohotovostním pásmu po tuto dobu, než bude spotřebič považován za čekající na start spíše než za běžící.", + "doc": "Výkon musí zůstat v pohotovostním pásmu po tuto dobu, než se spotřebič počítá jako čekající na start.", "label": "Sekundy potvrzení" }, "delay_start_detect_enabled": { - "doc": "Detekuje, když je spotřebič zapnut a čeká (zpozděný start / pohotovostní režim), ale nezačal svůj cyklus, aby pohotovostní odběr nebyl zaměnen za běžící cyklus.", + "doc": "Detekovat spotřebič, který je zapnutý a čeká na odložený start, aby se jeho pohotovostní odběr nepovažoval za běžící cyklus.", "label": "Povolit detekci zpožděného startu" }, "delay_timeout_hours": { - "doc": "Přestaňte čekat v režimu zpozděného startu po tomto počtu hodin a vraťte se do nečinného stavu, aby stroj ponechaný zapnutý, ale nikdy nespouštěný, nečekal donekonečna.", + "doc": "Po tomto počtu hodin přestat čekat na odložený start a vrátit se do nečinnosti.", "label": "Hodiny timeoutu" }, "device_type": { - "doc": "Třída spotřebiče. Nastaví rozumné výchozí hodnoty detekce (prahy, zpozdění vypnutí, ukončení) optimalizované pro daný typ spotřebiče; měňte jen tehdy, pokud bylo zařízení původně nastaveno jako špatný typ.", + "doc": "Nastaví výchozí hodnoty detekce (prahy, zpoždění vypnutí, zpracování konce) pro tento druh spotřebiče. Měňte jen tehdy, když bylo zařízení nastaveno jako špatný typ.", "label": "Typ zařízení" }, "door_sensor_entity": { - "doc": "Volitelný binární senzor dveří. Používá se k detekci, kdy byl spotřebič otevřen/vyloučen po cyklu.", + "doc": "Volitelný senzor dveří. Ukazuje, kdy byl spotřebič po cyklu otevřen kvůli vyložení.", "label": "Entita senzoru dveří" }, "duration_tolerance": { - "doc": "Tolerance pro odhady zbývajícího času (zpětná vazba učení, ne párování). Pokud je skutečná doba trvání v rozmeží +/−X % od odhadu, počítá se jako dobrá shoda.", + "doc": "Jen pro zpětnou vazbu učení: cyklus v rozmezí +/-X % odhadované doby se počítá jako dobrý odhad. Neovlivňuje párování.", "label": "Tolerance odhadu" }, "enable_ml_models": { - "doc": "Během cyklu nechte modely vylepšovat živé výsledky: stabilnější odhad zbývajícího času a energie/nákladů a ochrana před předčasným ukončením detekce (může pouze zdržet dokončení, nikdy ho urychlit, a je ohraničena). Používá vaše doladěné modely, pokud jsou k dispozici, jinak vestavbě modely. Vypnuto = pouze klasická logika založená na příkonu (stále spolehlivost).", + "doc": "Použít model natrénovaný na tomto stroji k předpovědi energie a nákladů běžícího cyklu a nechat dobře známý program, který skončí neobvykle brzy, dokončit bez celého zpoždění vypnutí. Vypnuto: jen logika založená na výkonu.", "label": "Použít chytré modely během cyklu" }, "end_energy_threshold": { - "doc": "Během odpočtu zpozdění vypnutí je akumulovaná energie (watty × čas) porovávána s tímto prahem. Pokud je překročena, odpočet se resetuje – zachovává otočení ochrany před mačkáním a doby sušení myčky připojené k cyklu místo jejich předčasného přerušení. Zvyšte, pokud cykly končí příliš brzy během chladnutí; snižte, pokud je detekce pomalá.", + "doc": "Energie povolená během zpoždění vypnutí. Při vyšší spotřebě cyklus pokračuje, takže otáčení bubnu a závěrečné sušení zůstanou jeho součástí. Snižte, pokud cykly končí během ocasu; zvyšte, pokud dokončené cykly dál čekají.", "label": "Energie konce" }, - "end_repeat_count": { - "doc": "Počet po sobě jdoucích čtení pod prahem zastavení, potřebných před ukončením cyklu. 1 je v pořádku pro většinu zástrček. Zvyšte na 2–3, pokud vaše chytrá zástrčka občas hlásí falešně nulový vzorek během cyklu a vaše cykly předčasně končí.", - "label": "Počet opakování konce" - }, "energy_price_entity": { - "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í.", + "doc": "Senzor s aktuální cenou za kWh (např. dynamický tarif). Má přednost před statickou cenou níže, takže musí jít o cenu, ne o senzor výkonu nebo energie zásuvky.", "label": "Entita ceny energie" }, "energy_price_static": { - "doc": "Pevná cena za kWh používaná pro ukazatele nákladů, pokud není nastavena živá entita ceny výše.", + "doc": "Pevná cena za kWh, použitá, když není nastavena entita ceny.", "label": "Statická cena energie (za kWh)" }, "energy_sensor": { - "doc": "Volitelné kumulativní počítadlo energie (total_increasing kWh/Wh, např. vlastní celoživotní měřič zásuvky). Když je nastaveno, energie hlášená za každý cyklus se odvozuje z rozdílu tohoto počítadla mezi začátkem a koncem, což zabraňuje podhodnocení, které vzniká integrací pomalu hlásícího senzoru výkonu. Vrátí se k integrované hodnotě, pokud údaj chybí, jeho jednotka je neznámá nebo rozdíl není kladný. Ponechte prázdné, chcete-li dále integrovat senzor výkonu.", + "doc": "Volitelné počítadlo kWh nebo Wh (total_increasing, např. vlastní měřič zásuvky). Energie cyklu se pak čte z něj, což je přesnější než výpočet z pomalého senzoru výkonu. Je-li jeho hodnota nepoužitelná, použije se vypočtená.", "label": "Entita měřiče energie" }, "expose_debug_entities": { - "doc": "Publikujte extra diagnostické entity HA (spolehlivost shody, nejednoznačnost, interní stav). Vypnuto udržuje seznam entit čistý pro normální použití.", + "doc": "Přidat diagnostické entity (spolehlivost shody, nejednoznačnost, interní stav). Vypnuto: seznam entit zůstane přehledný.", "label": "Zobrazit ladící entity" }, "external_end_trigger": { - "doc": "Binární senzor, jehož změna stavu označuje konec cyklu (např. kontakt \"dokončeno\" spotřebiče nebo doprovodná integrace).", + "doc": "Binární senzor, jehož změna označuje konec cyklu (např. kontakt „dokončeno“ nebo jiná integrace).", "label": "Entita vnějšího spouštěče" }, "external_end_trigger_enabled": { - "doc": "Nechte externí binární senzor signalizovat konec cyklu, navíc k vestavbě detekci založené na příkonu.", + "doc": "Nechat binární senzor signalizovat konec cyklu, navíc k detekci podle výkonu.", "label": "Povolit vnější spouštěč konce" }, "external_end_trigger_inverted": { - "doc": "Berte vypnutí (místo zapnutí) senzoru spouštěče jako signál konce cyklu.", + "doc": "Brát VYPNUTÍ senzoru (ne zapnutí) jako signál konce.", "label": "Invertovat vnější spouštěč (spustit při VYP)" }, "learning_confidence": { - "doc": "Pokud skóre shody klesá mezi touto hodnotou a Důvěrou automatického označení, oznámení o zpětné vazbě vás požádá o ověření identifikovaného programu. Pod touto hodnotou je shoda příliš nejistá k zobrazení. Musí být udržována pod Důvěrou automatického označení.", + "doc": "Nejnižší skóre shody pro označení dokončeného cyklu. Jasný vítěz se označí; těsný výsledek jde do fronty kontroly v Cyklech. Pod touto hodnotou se nic neoznačí ani se neptá. Držte ji pod Důvěrou auto-označení.", "label": "Důvěra učení" }, "linked_device": { - "doc": "Volitelně vnořte toto zařízení WashData pod jiné zařízení (např. chytrou zástrčku) v registru zařízení HA – zobrazí se jako \"Připojeno přes ...\".", + "doc": "Volitelné. Zobrazit toto zařízení WashData pod jiným zařízením (např. chytrou zásuvkou) v Home Assistant jako „Připojeno přes ...“.", "label": "Seskupit pod zařízení" }, "min_off_gap": { - "doc": "Pokud je stroj vypnut po dobu kratší než tato hodnota, je sekvence zap/vyp/zap považována za jeden nepřetržitý cyklus. Brání programům s namáčením (stroj se na několik minut vypne během praní) v rozdělení na dva samostatné cykly. Nastavte kratší než mezera mezi vašimi po sobě jdoucími náklady, pokud chcete, aby byly počítány jako oddělené cykly. Výchozí hodnoty pro typ zařízení chrání typickou intra-cyklickou pauzu pro každý spotřebič.", + "doc": "Vypnutí kratší než tato doba cyklus nerozdělí, takže pauzy namáčení zůstanou v jednom praní. Držte kratší než mezera mezi po sobě jdoucími náplněmi, jinak se sloučí do jednoho cyklu.", "label": "Min. mezera vypnutí" }, "min_power": { - "doc": "Absolutní minimální příkon považovaný za aktivní. Hodnoty pod touto úrovní jsou považovány za 0 W (pohotovostní režim) a filtruje se tak fantomový příkon chytrých zástrček a pohotovostních LED diod.", + "doc": "Hodnoty pod touto mezí se počítají jako 0 W (pohotovost). Odfiltruje klidový odběr chytrých zásuvek a pohotovostních LED.", "label": "Minimální výkon" }, "ml_training_enabled": { - "doc": "Pravidelně studujte vaše recenzované cykly přes noc a doladěte modely pro tento konkrétní stroj. Změna je zachována pouze tehdy, když skutečně dosáhuje lepších výsledků na nezaznamenaných cyklech, takže to může pouze pomóci nebo zůstat stejné – nikdy se nezhorší.", + "doc": "Občas přetrénovat model energie na dokončených cyklech tohoto stroje. Nový model se ponechá jen tehdy, když na testovacích cyklech překoná jednoduchý odhad.", "label": "Učit se z tohoto zařízení" }, "ml_training_hour": { - "doc": "Místní hodina dne (0–23) pro noční doladění. Zvolte tichou hodinu, jako je 2 (02:00).", + "doc": "Hodina dne (0-23) pro noční trénování. Zvolte klidnou hodinu, např. 2.", "label": "Učit v hodinu" }, "ml_training_interval_days": { - "doc": "Znovu zkontrolujte vylepšení nejvýše jednou za tento počet dní.", + "doc": "Přetrénovat nejvýše jednou za tento počet dní.", "label": "Kontrolovat nejvýše každých" }, "ml_training_min_cycles": { - "doc": "Počkejte, dokud nebude zaznamenán alespoň tento počet cyklů před doladěním, aby bylo dostatečné množství dat k učení.", + "doc": "Před trénováním počkat alespoň na tento počet zaznamenaných cyklů.", "label": "Nejprve potřebné cykly" }, "name": { - "doc": "Zobrazované jméno v seznamu integrací HA a v registru zařízení.", + "doc": "Název tohoto zařízení zobrazený v Home Assistant.", "label": "Název zařízení" }, "no_update_active_timeout": { - "doc": "Pokud po tuto dobu nepřijedou žádné aktualizace příkonu během běhu, předpokládáme, že zástrčka se odpojila, a zastavíme cyklus, abychom se vyhnuli zombie cyklu. Výchozí 600 s umožňuje prodlevy v cloudu nebo síti.", + "doc": "Pokud během cyklu po tuto dobu nepřijde žádná aktualizace výkonu, předpokládá se, že zásuvka je offline, a cyklus se vynuceně zastaví. Výchozích 600 s počítá se zpožděním cloudu nebo mesh sítě.", "label": "Timeout bez aktualizace" }, "notify_before_end_minutes": { @@ -1346,35 +1347,35 @@ "label": "Upozornění před koncem" }, "notify_channel": { - "doc": "Název kanálu oznámení Android pro zprávy o spouštění/živém průběhu (řídí zvuk a prioritu pro daný kanál v mobilní aplikaci). Prázdné používá výchozí nastavení doprovodné aplikace.", + "doc": "Kanál Android pro zprávy o startu a živém průběhu; kanál určuje zvuk a prioritu. Prázdné: výchozí kanál aplikace.", "label": "Kanál Android (start/živě)" }, "notify_cycle_timers": { - "doc": "Oznámení v konkrétní minuty do cyklu (např. pro přidání aviváže). Zpráva podporuje {device}, {program}, {minutes}. Povolte Automatickou pauzu pro pozastavení v tomto bodě a získání interaktivního oznámení s tlačítkem Obnovit; obnovit přes panel, službu pozastavení/obnovení nebo akci oznámení.", + "doc": "Oznámení v nastavených minutách cyklu (např. pro přidání aviváže). Zpráva podporuje {device}, {program}, {minutes}. S Automatickou pauzou se cyklus zde také pozastaví a oznámení dostane tlačítko Pokračovat.", "label": "Časovače cyklu" }, "notify_finish_channel": { - "doc": "Název kanálu oznámení Android pro zprávu o dokončení. Prázdné znovu používá kanál spouštění/živého průběhu.", + "doc": "Kanál Android pro zprávu o dokončení. Prázdné: použije se kanál startu a živého průběhu.", "label": "Kanál Android (dokončení)" }, "notify_finish_message": { - "doc": "Text odeslaný při dokončení cyklu. Proměnné šablony: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}. {time_finished} a {vs_typical} jsou zde nejužitečnější.", + "doc": "Text odeslaný při dokončení cyklu. Proměnné: {device}, {program}, {duration} (minuty), {duration_hm} (1 h 05 min), {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}, {status} (completed nebo force_stopped; přerušené cykly nic neposílají). U jakékoli jiné proměnné se použije výchozí text.", "label": "Zpráva o dokončení" }, "notify_finish_services": { - "doc": "Služby notify.* volané při dokončení cyklu. Přidejte jednu pro každý cíl; nechte prázdné pro žádné oznámení o dokončení.", + "doc": "Služby notify.* volané při dokončení cyklu. Ponechte prázdné pro žádné.", "label": "Dokončovací služby" }, "notify_fire_events": { - "doc": "Také spustit události ha_washdata_* při spouštění/dokončení cyklu, abyste mohli sestavit vlastní automatizace.", + "doc": "Také vyvolat události ha_washdata_* při startu a dokončení cyklu pro vlastní automatizace.", "label": "Spustit HA události pro oznámení" }, "notify_icon": { - "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.", + "doc": "Malá ikona v oznámení (např. mdi:washing-machine). Posílá se jen na cíle mobile_app, ne na skupiny notify. V iOS nahradí ikonu aplikace a vyžaduje mobilní aplikaci 2026.8 nebo novější. Prázdné: výchozí ikona platformy.", "label": "Ikona oznámení" }, "notify_live_chronometer": { - "doc": "Zobrazit živě aktualizovaný odpočítávací časovač v oznámení (na platfomách, které to podporují) místo statického odhadu.", + "doc": "Zobrazit v oznámení živý odpočet (kde je podporován) místo pevného odhadu.", "label": "Použít živý chronometr" }, "notify_live_interval_seconds": { @@ -1382,143 +1383,135 @@ "label": "Interval živé aktualizace" }, "notify_live_overrun_percent": { - "doc": "Pokud cyklus překročí svůj odhad o více než toto procento, odeslat upozornění na překročení.", + "doc": "Živé aktualizace se zastaví, jakmile cyklus překročí očekávanou délku o toto procento (20 = až 1,2násobek očekávané doby).", "label": "Procento překročení živého oznámení" }, "notify_live_services": { - "doc": "Služby notify.* volané pro aktualizace živého průběhu během běžícího cyklu. Nechte prázdné pro vypnutí oznámení živého průběhu.", + "doc": "Služby notify.* pro živé aktualizace průběhu během cyklu. Ponechte prázdné pro vypnutí.", "label": "Služby živého průběhu" }, "notify_only_when_home": { - "doc": "Odesílat oznámení pouze tehdy, když je alespoň jedna z propojených osob (výše) doma.", + "doc": "Oznamovat jen tehdy, když je alespoň jedna z osob výše doma.", "label": "Oznamovat pouze když doma" }, "notify_people": { - "doc": "Entity person.* používané \"Oznamovat pouze když doma\" pro rozhodnutí, zda je někdo doma.", + "doc": "Entity person.*, které kontroluje „Oznamovat pouze když doma“.", "label": "Osoby (pro Pouze když doma)" }, "notify_pre_complete_message": { - "doc": "Text upozornění před koncem / téměř hotovo. Proměnné šablony: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Text každé živé aktualizace průběhu. Proměnné šablony: {device}, {program}, {minutes} (zbývající minuty). U jakékoli jiné proměnné se použije výchozí text.", "label": "Zpráva před dokončením" }, "notify_reminder_message": { - "doc": "Text připomínky stále čekajícího vyložení. Prázdné použije vestavěnou výchozí hodnotu. Proměnné šablony: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Text jednorázového Upozornění před koncem. Proměnné šablony: {device}, {program}, {minutes} (nastavení Upozornění před koncem). U jakékoli jiné proměnné se použije výchozí text.", "label": "Zpráva připomínky" }, "notify_start_message": { - "doc": "Text odeslaný při spuštění cyklu. Proměnné šablony: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Text odeslaný při spuštění cyklu. Proměnné šablony: {device}, {program}. U jakékoli jiné proměnné se použije výchozí text.", "label": "Startovní zpráva" }, "notify_start_services": { - "doc": "Služby notify.* volané při spouštění cyklu. Přidejte jednu pro každý cíl (telefon, panel atd.); nechte prázdné pro žádné oznámení o spouštění.", + "doc": "Služby notify.* volané při startu cyklu (telefon, nástěnka, ...). Ponechte prázdné pro žádné.", "label": "Startovní služby" }, "notify_timeout_seconds": { - "doc": "Automaticky zavřít oznámení po tomto počtu sekund (na platfomách, které to podporují). 0 ho ponechá, dokud není ručně zavřeno.", + "doc": "Zavřít oznámení po tomto počtu sekund (kde je podporováno). 0 jej ponechá, dokud jej nezavřete.", "label": "Automaticky zavřít po" }, "notify_title": { - "doc": "Nadpis oznámení. Šabonové proměnné: {device}, {program}, {duration}, {energy}, {cost}, {date}, {time}, {minutes}.", + "doc": "Nadpis oznámení. Dosazuje se jen {device}; u jakékoli jiné proměnné se použije výchozí nadpis.", "label": "Nadpis oznámení" }, "notify_unload_delay_minutes": { - "doc": "Minuty po ukončení cyklu před odesláním stále čekající připomínky \"vyloučit stroj\". Nastavte 0 pro vypnutí připomínky.", + "doc": "Minuty po skončení cyklu před připomínkou „vyložte stroj“. 0 ji vypne.", "label": "Zpoždění připomínky vyložení" }, "notify_unload_repeat": { - "doc": "Znovu odesílá připomínku vyložení každých „Zpoždění připomínky vyložení“ minut, dokud není vyložení potvrzeno nebo v oznámení neklepnete na „Přestat připomínat“. Vyžaduje způsob, jak vyložení potvrdit: Entitu senzoru dveří, Entitu potvrzení vyložení nebo níže uvedenou možnost „Ruční potvrzení vyložení“. Tlačítko pro zrušení funguje u oznámení mobilní aplikace Home Assistant (companion app).", + "doc": "Opakovat připomínku vyložení každých „Zpoždění připomínky vyložení“ minut, dokud není vyložení potvrzeno nebo neklepnete na „Přestat připomínat“ (mobilní aplikace). Vyžaduje Entitu senzoru dveří, Entitu potvrzení vyložení nebo „Ruční potvrzení vyložení“.", "label": "Opakovat do vyložení" }, "off_delay": { - "doc": "Čas čekání po poklesu příkonu před vyhlášením ukončení cyklu. Pokud se příkon obnoví v tomto okně, cyklus plynule pokračuje – to přemostí pauzy mezi fázemi praní. Myčky nádobí mají dlouhé fáze sušení (příkon vypnut po dobu 20–60 minut), takže zpozdění vypnutí musí tuto dobu překročit, aby byl celkový cyklus mytí+sušení zachován jako jeden.", + "doc": "Jak dlouho musí výkon zůstat nízký, než je cyklus dokončen. Návrat výkonu během této doby cyklus prodlouží. Myčky mohou být při sušení 20-60 min nečinné, takže nastavte delší dobu.", "label": "Zpoždění vypnutí" }, "pause_cuts_power": { - "doc": "Pokud je cyklus pozastaven, WashData také vypne entitu spínače nastavenou níže. Povolte pouze u spotřebičů, u nichž lze bezpečně přerušit napájení uprostřed cyklu.", + "doc": "Při pozastavení cyklu vypnout také Entitu spínače níže. Jen pro spotřebiče, které mohou bezpečně ztratit napájení uprostřed cyklu.", "label": "Odpojit napájení při pauze (přes entitu spínače)" }, "power_off_delay": { - "doc": "Jak dlouho musí příkon zůstat pod hodnotou „Práh vypnutí napájení“ po dokončení cyklu, než se stav vrátí na „Vypnuto“. Používá se pouze tehdy, když je „Práh vypnutí napájení“ větší než 0. Kontroluje se v pravidelném intervalu na pozadí, takže se skutečné zpoždění zaokrouhluje nahoru na nejbližší tik vypršení stavu.", + "doc": "Jak dlouho musí výkon po cyklu zůstat pod Prahem vypnutí napájení, než se stav vrátí na Vypnuto. Použije se jen tehdy, když je tento práh nad 0. Kontroluje se pravidelně, takže skutečné čekání může být o něco delší.", "label": "Zpoždění vypnutí napájení" }, "power_off_threshold_w": { - "doc": "Volitelná detekce stavu „Vypnuto“ podle příkonu. Když je hodnota větší než 0, po dokončení cyklu, pokud příkon zůstane pod touto úrovní po dobu „Zpoždění vypnutí napájení“, je spotřebič považován za vypnutý a stav se vrátí na „Vypnuto“. Ponechte 0 pro vypnutí tohohle nastavení (výchozí). Nastavte ji nad skutečnou úroveň vypnutého spotřebiče a pod „Práh zastavení“ i pod pohotovostní odběr dokončeného, ale stále zapnutého spotřebiče; pokud není nižší než „Práh zastavení“, je ignorována. Když je zapnuta, nahrazuje „Zpoždění resetu průběhu“ pro návrat na „Vypnuto“, takže dokončený spotřebič zůstane ve stavu „Dokončeno“/„Čistý“, dokud není skutečně vypnut.", + "doc": "Po cyklu výkon pod touto hodnotou po dobu Zpoždění vypnutí napájení nastaví stav na Vypnuto, místo Zpoždění resetu průběhu. Nastavte nad odběr vypnutého spotřebiče, pod pohotovost a Práh zastavení. 0 vypne (výchozí).", "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.washer_power). Veškerá detekce cyklu je založena na tomto signálu.", + "doc": "Senzor, který hlásí aktuální výkon tohoto spotřebiče ve wattech (např. sensor.washer_power). Veškerá detekce vychází z něj.", "label": "Snímač výkonu" }, - "profile_duration_tolerance": { - "doc": "Pásmo +/− kolem průměrné doby trvání profilu používané během párování. 0,25 znamená, že profil 60 min odpovídá cyklům 45–75 min.", - "label": "Tolerance délky profilu" - }, "profile_match_interval": { - "doc": "Jak často se pokušet o párování profilu během běžícího cyklu. Výchozí 300 s (5 minut) vyvažuje rychlost detekce a nároky na CPU.", + "doc": "Jak často párovat běžící cyklus s profily. Výchozí 300 s (5 min).", "label": "Interval shody" }, "profile_match_max_duration_ratio": { - "doc": "Maximální délka cyklu relativně k profilu. 1,3 znamená, že cyklus musí být kratší než 130 % doby trvání profilu, aby se pároval.", + "doc": "Program se vyřadí, jakmile cyklus běží déle než tento násobek jeho typické doby trvání. 1,8: cyklus o 80 % delší než program mu už neodpovídá.", "label": "Max. poměr délky" }, "profile_match_min_duration_ratio": { - "doc": "Minimální délka cyklu relativně k profilu. 0,9 znamená, že cyklus musí trvat alespoň 90 % doby trvání profilu, aby se pároval.", + "doc": "Program se zvažuje až poté, co cyklus proběhl tuto část jeho typické doby trvání. 0,10: dvouhodinový program se stane kandidátem po 12 minutách. Neplatí během prvních 15 minut cyklu.", "label": "Min. poměr délky" }, "profile_match_threshold": { - "doc": "Minimální skóre podobnosti (0–1) požadované na konci cyklu pro přijetí identifikace programu. Zvyšte pro snížení nesprávných identifikací; snižte, pokud programy vašeho stroje nejsou párované. Výchozí hodnota 0,4 je konzervativní výchozí bod.", + "doc": "Živé skóre shody, které cyklus potřebuje, než jej Chytré ukončení nebo dokončení ochrany před zmačkáním smí ukončit dříve. Nemění zobrazený program. Zvyšte, pokud slabé shody ukončují cykly předčasně; snižte, pokud jasné shody čekají celé zpoždění vypnutí.", "label": "Práh shody" }, "profile_unmatch_threshold": { - "doc": "Pokud živé párování během cyklu klesne pod toto skóre, prozatímní identifikace se vymaže. Udržujte trochu pod prahem shody, aby krátký pokles podobnosti nepřepnul zobrazení zpět na nepárované.", + "doc": "Pokud živé skóre shody klesne pod tuto hodnotu, aktuální odhad programu se vymaže. Držte trochu pod Prahem shody, aby jej krátký pokles nevynuloval.", "label": "Práh neshody" }, "progress_reset_delay": { - "doc": "Po ukončení držte průběh na 100 % po tuto dobu, aby bylo Dokončeno viditelné na panelech před resetem na Nečinný.", + "doc": "Po cyklu držet průběh na 100 % po tuto dobu, než se resetuje na Nečinný.", "label": "Zpoždění resetu průběhu" }, "pump_stuck_duration": { - "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).", + "doc": "Jak dlouho smí čerpadlo běžet bez přestávky, než se spustí událost zaseknutého čerpadla.", "label": "Délka zaseknutého čerpadla" }, "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.", + "doc": "Minimální doba mezi zpracovanými hodnotami. Hodnoty, které přijdou dříve, se přeskočí (nízké hodnoty během cyklu se vždy ponechají). Nastavte interval hlášení vaší zásuvky nebo méně; vyšší hodnota jen zahazuje data.", "label": "Interval vzorkování" }, "save_debug_traces": { - "doc": "Ukládejte plný záznam příkonu a ladicí data párování pro každý cyklus. Užitečné pro odstraňování problémů, ale zvyšuje velikost úložiště.", + "doc": "Ukládat ke každému cyklu i ladicí data párování (skóre všech kandidátů) pro řešení problémů. Přidává asi 20 KB na cyklus.", "label": "Uložit ladící stopy" }, "smart_termination_duration_ratio": { - "doc": "Jak daleko v očekávané době trvání přiřazeného programu musí cyklus být, než jej Chytré ukončení může po poklesu příkonu ukončit dříve. Očekávaná doba trvání je průměr programu, takže u spotřebičů, jejichž doba běhu hodně kolísá - pračky se studenou přiváděnou vodou v zimě oproti teplé v létě, sušičky se senzorovým sušením, programy závislé na náplni - skončí zhruba polovina všech běhů dříve než tento průměr a nikdy se rychlého ukončení nedočká, takže skončí až o několik minut později přes záložní časový limit. U takových strojů tuto hodnotu snižte (např. 0,85), aby se dřívější ukončení přesto spustilo; pro opatrnější chování ji zvyšte směrem k 1,0. Ponechte prázdné pro výchozí hodnotu (0,98, u myček nádobí 0,99). Cyklus může vždy jen ukončit dříve, nikdy později, a nikdy se nespustí u nejednoznačné shody nebo shody s nízkou spolehlivostí.", + "doc": "Zlomek průměrné doby trvání programu, kterého musí cyklus dosáhnout, než jej Chytré ukončení smí ukončit dříve. Pokud doby běhu kolísají (teplota vody, senzorové sušení, velikost náplně), snižte jej (např. 0,85). Ponechte prázdné pro 0,98 (0,99 u myček).", "label": "Poměr chytrého ukončení" }, - "smoothing_window": { - "doc": "Míra vyhlazení nezpracovaného signálu příkonu. Nízká hodnota (2) je citlivá, ale hlučná; vysoká (5) vyhlazuje špičky, ale přidává zpozdění.", - "label": "Okno vyhlazení" - }, "start_duration_threshold": { - "doc": "Příkon musí zůstat nad prahem spouštění po tuto dobu pro potvrzení skutečného startu, čímž se brání okamžitým zap/vyp přepnutím v zahájení cyklu.", + "doc": "Výkon musí zůstat nad Prahem spuštění po tuto dobu, než cyklus začne, aby se krátké zapnutí a vypnutí ignorovaly.", "label": "Doba trvání spuštění" }, "start_energy_threshold": { - "doc": "Energie (příkon × čas), kterou musí spotřebič spotřebovat před stavem BĚHU. Krátký spike s vysokým příkonem má velmi nízkou energii a je ignorován, čímž se brání falešným startům.", + "doc": "Energie, kterou musí spotřebič spotřebovat, než se cyklus počítá jako běžící. Krátká špička výkonu spotřebuje málo energie, takže cyklus nespustí.", "label": "Energie spuštění" }, "start_threshold_w": { - "doc": "Příkon musí vzrůst nad tuto úroveň pro potvrzení zahájení cyklu. Příliš nízké nastavení způsobuje falešné starty z pohotovostního příkonu; příliš vysoké a pomalu startující programy (studené plnění) jsou vynechány. Engine návrhů nastavuje tuto hodnotu těsně nad nejnižší pozorovanou aktivní spotřebu stroje.", + "doc": "Výkon musí stoupnout nad tuto hodnotu, aby cyklus začal. Příliš nízko: pohotovostní odběr způsobí falešné starty. Příliš vysoko: pomalu startující programy (studené napouštění) se přehlédnou.", "label": "Práh spuštění" }, "stop_threshold_w": { - "doc": "Příkon musí klesnout pod tuto úroveň před zahájením odpočtu zpozdění vypnutí. Nastavte pod Práh spouštění – mezera mezi nimi je hysterezní pásmo bránící mihoťání. Příliš vysoké nastavení způsobí, že nízkoenergetické fáze (přidržení oplachu, ochrana před mačkáním) falešně spustí sekvenci ukončení.", + "doc": "Výkon musí klesnout pod tuto hodnotu, než začne odpočet zpoždění vypnutí. Držte pod Prahem spuštění. Je-li příliš vysoko, fáze s nízkým výkonem (pauzy máchání, ochrana před zmačkáním) omylem spustí odpočet konce.", "label": "Práh zastavení" }, "switch_entity": { - "doc": "Volitelný spínač vypnutý při pozastavení a znovu zapnutý při obnovení, používaný společně s \"Pauza také přeruší napájení\".", + "doc": "Volitelný spínač, který se při pauze vypne a při pokračování znovu zapne, spolu s „Odpojit napájení při pauze“.", "label": "Entita spínače" }, "watchdog_interval": { - "doc": "Jak často hlídáč na pozadí kontroluje zastavené senzory a uplynulé časové limity. Výchozí 30 s.", + "doc": "Jak často kontrola na pozadí hledá zaseknuté senzory a vypršené časové limity. Výchozí 30 s.", "label": "Interval hlídače" }, "notify_milestone_message": { @@ -1526,131 +1519,86 @@ "label": "Zpráva o milníku" }, "notify_milestones": { - "doc": "Čárkami oddělené počty cyklů, které po dosažení spustí jednorázové oslavné oznámení (např. 50, 100, 500, 1000). Prázdné vypne oznámení o milnících.", + "doc": "Počty cyklů, které spustí jednorázové oslavné oznámení, oddělené čárkami (např. 50, 100, 500, 1000). Prázdné je vypne.", "label": "Milníky cyklů" }, "notify_quiet_end_hour": { - "doc": "Konec okna nerušit (0-23). Zadržená oznámení jsou doručena v tuto hodinu. Ponechte prázdné pro vypnutí.", + "doc": "Konec okna nerušit (0-23); zadržená oznámení se odešlou v tuto dobu. Prázdné jej vypne.", "label": "Konec tichých hodin" }, "notify_quiet_start_hour": { - "doc": "Začátek okna nerušit (0-23). Oznámení o dokončení, připomínky a upozornění na čisté prádlo, která by se spustila během tichých hodin, jsou zadržena a doručena po skončení okna. Ponechte prázdné pro vypnutí. Podporuje okna přes půlnoc (např. začátek 22, konec 7).", + "doc": "Začátek okna nerušit (0-23). Oznámení o dokončení, připomínky a oznámení o čistém prádle v tomto okně se zadrží až do jeho konce. Může přecházet přes půlnoc (např. 22 až 7). Prázdné jej vypne.", "label": "Začátek tichých hodin" }, "peak_rate_message": { - "doc": "Volitelný vlastní text tipu na špičkovou sazbu připojený ke spouštěcímu oznámení. Proměnné šablony: {device}, {price}. Prázdné použije vestavěnou výchozí hodnotu.", + "doc": "Vlastní text tipu na špičkovou sazbu. Proměnné šablony: {device}, {price}. Prázdné: výchozí text.", "label": "Zpráva o špičkové sazbě" }, "peak_rate_threshold": { - "doc": "Když cyklus začne a aktuální cena za kWh je na této hodnotě nebo vyšší, připojí ke spouštěcímu oznámení tip na špičkovou sazbu. 0 nebo prázdné tip vypne.", + "doc": "Pokud je cena za kWh při startu cyklu na této hodnotě nebo vyšší, přidat do oznámení o startu tip na špičkovou sazbu. 0 nebo prázdné jej vypne.", "label": "Práh špičkové sazby (za kWh)" }, - "dtw_bandwidth": { - "label": "Tolerance shody tvaru", - "doc": "Jak moc smí nástroj pro přiřazování časově deformovat cyklus, aby odpovídal tvaru uloženého programu. Vyšší hodnota toleruje větší kolísání rychlosti; příliš vysoká riskuje přiřazení nesprávného programu." - }, - "corr_weight": { - "label": "Váha tvaru vs. úrovně", - "doc": "Rovnováha mezi tvarem křivky a absolutní úrovní výkonu při hodnocení shody. Vyšší upřednostňuje tvar; nižší upřednostňuje shodu úrovně výkonu." - }, - "duration_weight": { - "label": "Váha délky trvání", - "doc": "Jak silně je upřednostněn program, jehož typická délka trvání odpovídá tomuto cyklu." - }, - "energy_weight": { - "label": "Váha spotřeby energie", - "doc": "Jak silně je upřednostněn program, jehož typická spotřeba energie odpovídá tomuto cyklu." - }, "store_brand": { "label": "Značka spotřebiče", - "doc": "Volitelné. Značka spotřebiče vybraná z katalogu komunity. Používá se k vyhledávání a sdílení odpovídajících referenčních záznamů. Ponechte prázdné, pokud nepoužíváte online funkce." + "doc": "Volitelné. Značka spotřebiče z katalogu komunity, používaná k hledání a sdílení referenčních záznamů. Ponechte prázdné, pokud online funkce nepoužíváte." }, "store_model": { "label": "Model spotřebiče", - "doc": "Volitelné. Model spotřebiče vybraný z katalogu komunity po nastavení značky. Pokud váš model není uveden, můžete jej přidat do katalogu." + "doc": "Volitelné. Model spotřebiče z katalogu komunity (nejdřív vyberte značku). Pokud v seznamu není, můžete jej přidat." }, "show_contributor": { "doc": "Zobrazit jméno přispěvatele u profilů stažených z komunitního obchodu" }, - "enable_phase_matching": { - "label": "Zbývající čas zohledňující fáze", - "doc": "Rozdělí každý běžící cyklus do fází (ohřev, praní, odstřeďování) a rozpočítá zbývající čas na jednotlivé fáze, smíchaný s klasickým odhadem - na začátku cyklu se opírá o rozpočet fází a ke konci o klasický odhad. Přizpůsobí odpočet tomu, jak dlouho se váš spotřebič skutečně ohřívá a běží, což je nejvíce patrné v první polovině cyklu. Vypnuto = pouze klasický odhad. Ovlivněno je jen zobrazení zbývajícího času; shoda programů a detekce cyklu se nemění." - }, - "keep_min_score": { - "label": "Min. skore shody", - "doc": "Minimální skóre podobnosti, které musí kandidát mít, aby zůstal v závodu o shodu. Výchozí hodnota 0,1 je záměrně benevolentní - připouští i slabé kandidáty a spoléhá na pozdější fáze při výběru nejlepší shody. Zvyšte ji pro dřívější vyřazení nepravděpodobných profilů; snižte ji pro rozšíření počáteční skupiny kandidátů." - }, - "dtw_blend": { - "label": "Mísení deformace", - "doc": "Jak moc skóre zarovnání DTW nahrazuje základní skóre Fáze 2. 0 = použij pouze skóre Fáze 2, 1 = použij pouze skóre DTW, 0,5 (výchozí) = rovnoměrné míchání. Zvyšte, chcete-li se více spoléhat na zarovnání, když mají programy podobné úrovně výkonu, ale různé časové vzory." - }, - "dtw_ensemble_w": { - "label": "Mix kombinace DTW", - "doc": "V režimu kombinace DTW (výchozím) je to váha škálovaného L1 oproti derivátnímu DTW (DDTW). 1,0 = vše škálované L1, 0 = vše DDTW, 0,7 = výchozí. Škálovaná varianta L1 porovnává úrovně výkonu; derivátní varianta reaguje na změny výkonu v čase. Kombinace spojuje obě." - }, - "dtw_ddtw_scale": { - "label": "Škála derivátní deformace", - "doc": "Vzdálenost polosaturace pro hodnocení derivátním DTW. Skóre se sníží na polovinu, když vzdálenost DDTW rovná se této hodnotě. Menší = citlivější na rozdíly ve tvaru. Výchozí hodnota 30 je zkalibrována pro typické průběhy výkonu spotřebičů. Ovlivňuje pouze kombinační a ddtw režimy DTW." - }, - "dtw_refine_top_n": { - "label": "Pocet zpresnenych kandidatu", - "doc": "Kolik nejlepších kandidátů z Fáze 2 je přehodnoceno DTW. DTW je nákladnější, takže se používá pouze pro N nejlepších kandidátů. Výchozí 5. Zvyšte (na 7-9), pokud správný profil po Fázi 2 někdy dosáhne pouze 4. nebo 5. místa - DTW ho může zachránit. Snížení mírně urychlí porovnávání." - }, - "duration_scale": { - "label": "Ostrost délky trvání", - "doc": "Ostrost penalizace za neshodu délky trvání ve Fázi 4. Jedná se o logaritmický poměr, při kterém se skóre shody sníží na polovinu. Menší = přísnější: neshoda délky trvání více snižuje skóre. Výchozí hodnota 0,175 odpovídá přibližně 18% toleranci délky trvání při poloviční váze. Kombinujte s Váhou délky trvání." - }, - "energy_scale": { - "label": "Ostrost energie", - "doc": "Ostrost penalizace za neshodu energie ve Fázi 4. Menší = přísnější: neshoda energie více snižuje skóre. Výchozí hodnota 0,25 je záměrně benevolentnější než Ostrost délky trvání, protože energie se liší podle zátěže. Kombinujte s Váhou energie." - }, "power_profile_interval_min": { "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." + "doc": "Velikost úseku atributu power_profile (průměrné watty na slot), který používají plánovače jako EMHASS a tibber_prices. Menší zachová krátké špičky; větší vyhladí křivku. Výchozí 15 min. 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." + "doc": "Pouze iOS. Aktualizovat živý průběh potichu, s prioritou šetřící baterii, kterou iOS může seskupovat. První aktualizace a oznámení o startu a dokončení stále upozorní. Vypnuto: upozorní každá aktualizace." }, "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." + "doc": "Pouze Android. Ponechat oznámení živého průběhu i po klepnutí. Vypnuto: klepnutí jej zavře." }, "notify_live_click_action": { "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." + "doc": "Co otevře klepnutí na oznámení tohoto spotřebiče. Prázdné: panel WashData pro tento spotřebič. Zadejte cestu k nástěnce (např. /lovelace/laundry) nebo URL, nebo „none“, aby klepnutí nic neotevřelo." }, "door_opens_at_end": { "label": "Dveře se na konci automaticky otevřou", - "doc": "Pro myčky nádobí, které na konci cyklu otevřou dveře za účelem sušení (AirDry a podobné). Při zapnuté funkci otevření dveří za běhu cyklu již nezastaví cyklus natrvalo; pokud dveře zůstanou otevřené po dobu čekání níže, WashData cyklus ukončí. Krátké otevření (přidání předmětu) je ignorováno. Vyžaduje entitu senzoru dveří." + "doc": "Pro myčky, které na konci otevřou dveře kvůli sušení (např. AirDry). Dveře otevřené po dobu níže cyklus ukončí místo pozastavení; krátké otevření se ignoruje. Vyžaduje Entitu senzoru dveří." }, "door_end_dwell_seconds": { "label": "Čekání na otevření dveří pro ukončení", - "doc": "Jak dlouho musí dveře zůstat otevřené, než WashData ukončí cyklus, pokud je zapnuta funkce \"Dveře se na konci automaticky otevřou\". Dostatečně dlouhé k ignorování rychlého přidání nádobí (výchozí 60 s), dostatečně krátké pro rychlé ukončení po otevření dveří spotřebičem." + "doc": "Při zapnutém „Dveře se na konci automaticky otevřou“: jak dlouho musí dveře zůstat otevřené, než cyklus skončí. Dost dlouho, aby se ignorovalo přidání nádobí (výchozí 60 s)." }, "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." + "doc": "Které druhy cyklů tvoří křivku a párování každého programu. Odškrtnutí druhu nic nesmaže; hodí se, pokud nedůvěřujete importovaným datům. Statistiky vždy počítají jen cykly tohoto stroje. Odškrtnutí všech se ignoruje." }, "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." + "doc": "Účtovat každý cyklus cenou platnou v každém okamžiku jeho běhu, ne cenou při dokončení. Vyžaduje entitu ceny. Zpracování historie (Diagnostika) přepočítá starší cykly." }, "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.", + "doc": "Přidá na začátek cyklu hodnoty z neúspěšných pokusů o spuštění, pro stroje, které se před startem zkoušejí rozběhnout (zámek dveří, první napouštění). 300 s stačí většině; 0 = vypnuto. Zaznamenaný začátek se posune dříve, takže naučené doby trvání se nějakou dobu posouvají.", "label": "Náběh křivky" }, "unload_confirm_entity": { "label": "Entita potvrzení vyložení", - "doc": "Volitelná entita, jejíž aktivace znamená, že náplň byla vyjmuta, pro spotřebiče, na které nelze umístit senzor dveří. Funguje jakýkoli typ: tlačítko Zigbee nebo NFC, pomocník input_button, senzor pohybu před spotřebičem, scéna. Za potvrzení se počítá každá změna stavu kromě vypnutí a přechodu do stavu nedostupný. Nastavení této možnosti také zapne připomínku vyložení na zařízení bez Entity senzoru dveří." + "doc": "Volitelné. Jakákoli entita, která potvrdí vyjmutí náplně, pro stroje bez senzoru dveří (tlačítko, input_button, senzor pohybu, scéna). Počítá se každá změna stavu kromě vypnutí a nedostupnosti. Zapne také připomínku vyložení." }, "unload_track_without_door": { "label": "Ruční potvrzení vyložení", - "doc": "Spustí připomínku vyložení na zařízení bez Entity senzoru dveří a bez Entity potvrzení vyložení, přičemž vyložení potvrdíte z vlastní automatizace: stiskněte entitu tlačítka „Označit jako vyložené“ nebo zavolejte službu ha_washdata.mark_unloaded. Ignorováno, pokud je nastavena Entita senzoru dveří, která tento signál již poskytuje." + "doc": "Spouštět připomínku vyložení bez senzoru dveří nebo entity potvrzení. Vyložení potvrďte z vlastní automatizace tlačítkem „Označit jako vyložené“ nebo službou ha_washdata.mark_unloaded. Ignoruje se, když je nastavena Entita senzoru dveří." }, "notify_icon_color": { "label": "Barva oznámení", - "doc": "Barva zvýraznění oznámení tohoto spotřebiče, aby se pračka, sušička a myčka nádobí daly na první pohled rozeznat. iOS obarví ikonu a ukazatel průběhu Live Activity na zamčené obrazovce a v Dynamic Island. Android dostane stejnou barvu, ale co s ní udělá, záleží na telefonu: některé obarví ikonu a mnohé (zejména Samsung One UI) ji zcela ignorují. Obě platformy rozumí formátu Hex (např. #03A9F4). Nechte prázdné pro výchozí barvu platformy." + "doc": "Barva zvýraznění oznámení tohoto spotřebiče, aby šlo snadno rozlišit pračku, sušičku a myčku. V iOS vyplní kruh za ikonou a obarví pruh Live Activity. Mnoho telefonů s Androidem (hlavně Samsung) ji ignoruje. Použijte hex, např. #03A9F4. Prázdné: výchozí barva platformy." + }, + "share_name": { + "doc": "Zobrazit název vašeho účtu jako autora sdílených cyklů. Vypnuto: sdílí se bez jména." } }, "setting_group": { @@ -1678,9 +1626,6 @@ "door_pause": { "label": "Dveře a pauza" }, - "dtw_refinement": { - "label": "Zpřesnění DTW" - }, "duration_gates": { "label": "Brány délky" }, @@ -1757,17 +1702,20 @@ "rebuilding": "Obnovování…", "analyzing": "Analýza…", "resetting": "Resetování…", - "reverting": "Vracení zpět…", "trimming": "Ořezávání…", "splitting": "Rozdělování…", + "stalled": "Zaseknuto", "deleting": "Mazání…", "imported": "Importováno", "preparing": "Příprava…", - "unlabelling": "Odznačování…" + "unlabelling": "Odznačování…", + "working": "Pracuji…", + "match_uncertain": "Nejisté: {top} nebo {runner}", + "match_uncertain_single": "Nejisté: možná {top}", + "match_sure": "jistota ~{pct} %", + "match_uncertain_tip": "WashData se zatím nerozhodl pro program. Číslo udává, jak často měl vedoucí odhad, {top}, v minulých cyklech pravdu při takovém náskoku. Jen pro informaci." }, "suggestion": { - "both_agree": "WashData doporučuje", - "calibrated_label": "Kalibrováno", "observed_label": "Naměřeno", "impact": { "auto_label_confidence": { @@ -1782,10 +1730,6 @@ "higher": "Uzavírá pouze cykly se značnou spotřebou energie", "lower": "Uzavírá i kratší nebo méně energetické cykly" }, - "end_repeat_count": { - "higher": "Vyžaduje více opakování signálu konce – spolehlivější, mírně pomalejší", - "lower": "Ukončí po méně opakováních – rychlejší detekce, větší riziko předčasného ukončení" - }, "learning_confidence": { "higher": "Učí se pouze z velmi jistých shod – pomalejší, ale spolehlivější", "lower": "Učí se z více cyklů; model může být méně přesný" @@ -1824,40 +1768,25 @@ } }, "reason": { - "auto_label_confidence": "Jistota na 15. percentilu z {cycles} automatických štítků, které uživatel nikdy neopravil ({p15}).", "completion_min_seconds": "Polovina délky p05 čistého cyklu ({minutes} min) napříč {cycles} cykly; odfiltruje falešné cykly.{excl}", - "dead_zone_batch": "Ponecháno krátké (medián okna nestability při spuštění napříč {cycles} cykly = {s}s), aby byl skutečný konec rozpoznán rychle.", - "dead_zone_from_dip": "Na základě raného poklesu výkonu zjištěného v {s}s.", - "end_energy_default": "Výchozí doporučená hodnota pro práh šumu na konci cyklu.", - "end_energy_false": "Energie p95 falešných konců ({p95}Wh) napříč {cycles} cykly, s dolní hranicí 0.2% mediánové energie cyklu ({floor}Wh).", - "end_energy_no_false": "Žádné falešné konce napříč {cycles} cykly; dolní hranice 0.2% mediánové energie cyklu ({floor}Wh).", - "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.", + "match_interval_floored": "Nejkratší program ({shortest}s) by hodnotu omezil na {cap}s, pod minimum {minimum}s, takže se použije minimum: {persistence} po sobě jdoucích shod trvá {budget}s, přes {pct}% tohoto programu. Párování běží jen při příchodu hodnoty (medián={median}s), takže to nic nestojí.", "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í.", "min_off_gap": "Na základě {gaps} pozorovaných mezer mezi cykly (p05={p05}s). Minimum zařízení: {floor}s.", "min_off_gap_bridge": "Nastaveno tak, aby překlenulo nejdelší tichý úsek uvnitř cyklu: {span}s + rezerva 60s, z {spans} překlenutých mezer napříč {cycles} čistými cykly (zůstává pod {ceiling}s, nejkratší mezerou, kterou necháváte mezi dvěma dávkami).", "min_power": "40% nejnižšího činného výkonu p05 ({p05}W) napříč {cycles} čistými cykly, aby práh vypnutí zůstal pod skutečným odběrem.{excl}", - "ml_auto_label": "Nejnižší jistota, při které model kvality stále hodnotil cyklus jako čistý (p10 z {cycles} čistých cyklů = {p10}).", - "ml_end_repeat": "{false}/{total} cyklů ({pct}%) mělo pauzu, kterou detektor konce ohodnotil nad 50%; vyžadovat {val} potvrzení konce.", - "ml_off_delay": "Pauzy potvrzené detektorem konce: p95 {p95}s + rezerva 60s, z {pauses} pauz ověřených modelem napříč {cycles} cykly (minimum {floor}s).", "no_update_timeout": "Na základě pozorované frekvence aktualizací (p95={p95}s) * 20 (min. 60s).", "off_delay_cadence": "Na základě pozorované frekvence aktualizací (p95={p95}s) * 5", "off_delay_device_floor": "Použito bezpečné minimum specifické pro zařízení {device} ({floor}s).", "off_delay_generic_floor": "Použito obecné bezpečné minimum ({floor}s).", "off_delay_pauses": "Nastaveno tak, aby přečkalo skutečné pauzy: pauza uvnitř cyklu p95 {p95}s + rezerva 60s, z {pauses} pauz napříč {cycles} čistými cykly (minimum {floor}s).", - "profile_match_threshold": "Jistota p10 z {cycles} správných automatických štítků ({p10}); bezpečná hranice pro potvrzení za běhu.", - "sampling_interval": "Mediánový interval aktualizace pozorovaný napříč {cycles} čistými cykly ({si}s).{excl}", - "smoothing_window": "Nastaveno pro vyhlazení ~30s odečtů při vzorkování {si}s ({samples} vzorků).", - "start_duration": "Ponecháno krátké (~jeden interval vzorkování {si}s), aby detekce začínala co nejdříve a přitom ignorovala jednotlivé špičky.", - "thr_batch": "Ponecháno těsně nad nejnižším činným výkonem p05 napříč {cycles} cykly ({p05}W), aby byl start zachycen co nejdříve a práh zastavení zůstal pod nejnižším provozním výkonem stroje.", - "tol_per_profile": "p75 rozptylu délky podle profilu napříč {profiles} profily ({cycles} cyklů); konzistentní profily nejsou penalizovány.", - "tol_pooled": "Na základě sloučeného rozptylu délky {cycles} nedávných označených cyklů (odchylka p95={dev}).", - "watchdog": "Ponecháno co nejnižší při zachování bezpečnosti (těsně nad intervalem aktualizace p95 ve výši {p95}s a alespoň 2x nad intervalem vzorkování {median}s, min. 30s), aby byla zaseknutí rychle odhalena bez falešných zastavení." + "standby_floor": "Každý nedávný cyklus stále odebíral {idle}W v okamžiku ukončení, takže práh pod touto hodnotou nikdy nezaznamená vypnutí spotřebiče. Byl ponechán nad ní; {cycles} čistých cyklů v záznamu nikdy neklesly pod něj na déle než {pause}s.", + "thr_batch": "Těsně nad nejnižším činným výkonem (p05) napříč {cycles} cykly ({p05}W), aby se starty zachytily brzy a práh zastavení zůstal pod nejnižším provozním výkonem.", + "watchdog": "Ponecháno co nejnižší při zachování bezpečnosti (těsně nad intervalem aktualizace p95 ve výši {p95}s a alespoň 2x nad intervalem vzorkování {median}s, min. 30s), aby byla zaseknutí rychle odhalena bez falešných zastavení.", + "end_energy_floor": "Zvýšeno na minimum, které vyplývá z prahu zastavení ({stop} W) po dobu zpoždění vypnutí ({off_delay} s); pod ním může cyklus skončit jen záložní cestou.", + "max_duration_ratio_default": "Vráceno na výchozí hodnotu: nižší horní mez odmítne správný program pokaždé, když běh trvá déle než obvykle.", + "min_duration_ratio_default": "Vráceno na výchozí hodnotu: vyšší dolní mez brání rozpoznání běžícího cyklu, dokud není v plném proudu." }, "exclusions": { "summary": "Vyloučeno {total} chybně detekovaných cyklů: {parts}.", @@ -1875,7 +1804,8 @@ "no_power": "bez výkonu", "no_active_power": "bez aktivního výkonu" } - } + }, + "now_value": "(aktuálně {v})" }, "tab": { "advanced": "Pokročilé", @@ -1906,13 +1836,17 @@ }, "toast": { "catalog_refreshed": "Katalog komunity obnoven", + "task_name_required": "Zadejte název úkolu", + "task_added": "Úkol přidán", + "task_save_failed": "Úkol se nepodařilo uložit: {error}", + "task_removed": "Úkol odebrán", + "maint_logged": "Zapsáno jako hotové: {task}", "access_saved": "Řízení přístupu bylo uloženo", "all_wiped": "Všechna data vymazána", "analysis_complete_none": "Analýza dokončena: žádné nové návrhy", "auto_label_complete": "Automatické označení dokončeno", "automation_deleted": "Automatizace odstraněna", "correction_submitted": "Oprava odeslána", - "cycle_deleted": "Cyklus smazán", "cycle_force_stopped": "Cyklus nuceně zastaven", "cycle_labelled": "Cyklus označen", "cycle_paused": "Cyklus pozastaven", @@ -1928,11 +1862,9 @@ "group_deleted": "Skupina smazána", "group_name_required": "Název skupiny je povinný", "group_saved": "Skupina uložena", - "import_successful": "Import byl úspěšný; znovunačtení integrace", "json_required": "Jsou vyžadována data JSON", "legacy_removed": "Starší akce byly odstraněny", "logs_exported": "Protokoly byly exportovány", - "matching_reverted": "Odpovídající váhy se vrátily na výchozí hodnoty", "min_2_profiles": "Vyberte alespoň 2 profily pro skupinu", "ml_training_no_improvement": "Školení dokončeno: základní linie zachována (žádné zlepšení)", "models_reverted": "Modely na zařízení se vrátily k výchozímu stavu", @@ -1942,8 +1874,8 @@ "pg_preset_loaded": "Přednastavení \"{name}\" načteno", "pg_preset_saved": "Přednastavení \"{name}\" uloženo", "pg_sugg_loaded": "Načteno {n} navrhovaných hodnot - spusťte testovací prostředí pro porovnání", - "pg_sugg_ml_loaded": "Načteno {n} hodnot kalibrovaných ML - spusťte testovací prostředí pro porovnání", "phase_name_required": "Název fáze je povinný", + "phase_reset": "Fáze \"{name}\" obnovena na vestavěnou verzi", "phase_updated": "Fáze aktualizována", "phases_saved": "Fáze uloženy", "preferences_saved": "Předvolby uloženy", @@ -1990,15 +1922,11 @@ "store_name_required": "Zadejte název profilu", "store_import_failed": "Import selhal: {error}", "store_imported": "Importováno do {profile}", - "store_program_required": "Zadejte název programu", "store_no_appliance": "Nejprve nastavte značku a model spotřebiče v Nastavení.", "store_share_failed": "Sdílení selhalo: {error}", "store_shared": "Sdíleno do komunitního obchodu, čeká na kontrolu.", "contrib_device_changed": "Přidáno do komunitního obchodu, ale vybraný spotřebič se změnil, zatímco byl formulář otevřený. Vyberte jej v Nastavení.", "appliance_added": "Spotřebič přidán, čeká na schválení", - "thanks_confirming": "Děkujeme za potvrzení", - "device_approved": "Schváleno komunitou", - "rating_saved": "Hodnocení kvality uloženo", "brand_added": "Značka přidána, čeká na schválení", "profile_added": "Profil přidán, čeká na schválení", "saved_except_conflicts": "Uloženo. Opravte zvýrazněné konflikty, aby se uložil zbytek.", @@ -2021,7 +1949,52 @@ "odometer_saved": "Celkový počet cyklů uložen", "odometer_save_failed": "Celkový počet nelze uložit: {error}", "store_pick_profile_share": "Vyberte profil pro sdílení", - "remove_failed": "Odstranění selhalo: {error}" + "remove_failed": "Odstranění selhalo: {error}", + "analysis_complete": "Analýza dokončena: návrhy: {count}", + "analysis_failed": "Analýza selhala: {error}", + "auto_detect_failed": "Automatická detekce selhala: {error}", + "auto_label_failed": "Automatické označení selhalo: {error}", + "could_not_load_cycle": "Cyklus nelze načíst: {error}", + "create_failed": "Vytvoření selhalo: {error}", + "cycles_deleted": "Smazané cykly: {count}", + "debug_cleared": "Vymazaná trasování ladění: {count}", + "discard_failed": "Vyřazení selhalo: {error}", + "error": "Chyba: {error}", + "export_failed": "Export selhal: {error}", + "import_failed": "Import selhal: {error}", + "invalid_json": "\"{key}\" není platný JSON - opravte jej nebo před uložením pole vymažte.", + "label_failed": "Označení selhalo: {error}", + "ml_training_promoted": "Trénování dokončeno: nasazené modely: {count}", + "pause_failed": "Pozastavení selhalo: {error}", + "phase_created": "Fáze \"{name}\" vytvořena", + "phase_deleted": "Fáze \"{name}\" smazána", + "profile_created": "Profil \"{name}\" vytvořen", + "rename_failed": "Přejmenování selhalo: {error}", + "save_failed": "Uložení selhalo: {error}", + "split_complete": "Rozděleno na cykly: {count}", + "start_failed": "Spuštění selhalo: {error}", + "stop_failed": "Zastavení selhalo: {error}", + "update_failed": "Aktualizace selhala: {error}", + "store_download_skipped": "Přeskočeno záznamů: {n} (příliš krátké, s příliš mnoha mezerami nebo na vašem zařízení již existují).", + "store_import_low_quality": "Tento záznam je příliš krátký, má příliš mnoho mezer nebo je nevěrohodný, proto jej nelze importovat.", + "store_import_duplicate": "Tento záznam na vašem zařízení již existuje.", + "device_approved": "Schváleno komunitou", + "thanks_confirming": "Děkujeme za potvrzení", + "rating_saved": "Hodnocení kvality uloženo", + "store_download_partial": "Programy, které se nepodařilo stáhnout: {n}; zkuste to později.", + "import_no_restore_point": "Importováno, ale bod obnovení se nepodařilo uložit, takže tento import nelze vrátit zpět", + "import_undo_failed": "Import se nepodařilo vrátit zpět: {error}", + "import_undone": "Import vrácen zpět: obnoveno {profiles} profilů a {cycles} cyklů", + "auto_label_cancelled": "Automatické označování zrušeno po označení {count} cyklů.", + "auto_label_cancelled_one": "Automatické označování zrušeno po označení {count} cyklu.", + "auto_label_cancelled_few": "Automatické označování zrušeno po označení {count} cyklů.", + "auto_label_cancelled_many": "Automatické označování zrušeno po označení {count} cyklu.", + "auto_label_cancelled_other": "Automatické označování zrušeno po označení {count} cyklů.", + "auto_label_done": "Automatické označování dokončeno: označeno {count} cyklů, bez označení zůstalo: {skipped}.", + "auto_label_done_one": "Automatické označování dokončeno: označen {count} cyklus, bez označení zůstalo: {skipped}.", + "auto_label_done_few": "Automatické označování dokončeno: označeny {count} cykly, bez označení zůstalo: {skipped}.", + "auto_label_done_many": "Automatické označování dokončeno: označeno {count} cyklu, bez označení zůstalo: {skipped}.", + "auto_label_done_other": "Automatické označování dokončeno: označeno {count} cyklů, bez označení zůstalo: {skipped}." }, "trend": { "down": "Klesající trend", @@ -2029,6 +2002,10 @@ }, "maint": { "bearing_service": "Servis ložisek", + "salt": "Doplnit sůl", + "rinse_aid": "Doplnit leštidlo", + "lint_filter": "Vyčistit filtr na vlákna", + "condenser_clean": "Vyčistit kondenzátor", "descale": "Odvápnění", "drum_clean": "Vyčistit buben", "filter_clean": "Vyčistit filtr", @@ -2036,34 +2013,22 @@ }, "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", + "smart_termination_duration_ratio": "Zlomek průměrné doby trvání programu, kterého musí cyklus dosáhnout, než jej Chytré ukončení smí ukončit dříve; snižte, pokud doby běhu kolísají", + "anti_crease_finalize_ratio": "Zlomek průměrné doby trvání programu, kterého musí cyklus dosáhnout, než jej ocas ochrany před zmačkáním smí dokončit; u sušiček snižte, u praček ponechte výchozí", "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_exit_power": "Úroveň klidu mezi impulzy otáčení; projeví se jen nad Prahem zastavení", "anti_wrinkle_idle_timeout": "Povolený klid mezi dvěma impulzy bubnu, než ochrana před zmačkáním skončí", "anti_wrinkle_max_duration": "Delší impulz ukončí ochranu před zmačkáním a otevře nový cyklus", "anti_wrinkle_max_power": "Impulz nad touto hodnotou ukončí ochranu před zmačkáním a otevře nový cyklus", "completion_min_seconds": "Nejkratší běh, který se počítá jako skutečný cyklus", - "end_repeat_count": "Nízká čtení za sebou před ukončením", "interrupted_min_seconds": "Krátké cykly označené jako přerušené", "min_off_gap": "Mezera nutná k oddělení dvou cyklů", "off_delay": "Sekundy nízkého výkonu před koncem cyklu", "start_duration_threshold": "Sekundy nad prahem pro potvrzení spuštění", "start_threshold_w": "Minimální výkon ve wattech pro započtení spuštění", "stop_threshold_w": "Pod touto hodnotou se stroj počítá jako vypnutý", - "dtw_bandwidth": "Fáze 3: pásmo deformace Sakoe-Chiba (0 = DTW vypnuto; výchozí 0,2 = 20 % délky cyklu)", - "corr_weight": "Fáze 2: rovnováha mezi tvarem křivky (korelace) a úrovní výkonu (MAE); výchozí 0,45", - "duration_weight": "Fáze 4: jak silně shoda délky trvání ovlivňuje výsledné skóre (výchozí 0,22)", - "energy_weight": "Fáze 4: jak silně shoda energie ovlivňuje výsledné skóre (výchozí 0,22)", - "profile_match_min_duration_ratio": "Fáze 1: minimální délka cyklu jako zlomek doby trvání profilu; výchozí 0,1 (10 %)", - "profile_match_max_duration_ratio": "Fáze 1: maximální délka cyklu jako zlomek doby trvání profilu; výchozí 1,8 (180 %)", - "keep_min_score": "Fáze 2: minimální skóre pro setrvání v závodu; výchozí 0,1 připouští slabé shody, pozdější fáze vyberou nejlepší", - "dtw_blend": "Fáze 3: 0 = pouze základní skóre, 1 = pouze skóre DTW, 0,5 = rovnoměrné míchání (výchozí)", - "dtw_ensemble_w": "Fáze 3 (režim kombinace): váha škálovaného L1 oproti derivátnímu DTW; výchozí 0,7 upřednostňuje úroveň", - "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)", + "profile_match_min_duration_ratio": "Minimální délka cyklu jako zlomek doby trvání profilu; výchozí 0,1 (10 %)", + "profile_match_max_duration_ratio": "Maximální délka cyklu jako zlomek doby trvání profilu; výchozí 1,8 (180 %)", "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": { @@ -2071,12 +2036,14 @@ "status_tip": "Výsledek cyklu: Dokončeno (přirozený konec), Přerušeno (náhlý pokles výkonu), Vynuceně zastaveno (ručně) nebo Vyžaduje kontrolu (čeká zpětná vazba).", "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 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.", + "energy_tip": "Celková spotřebovaná energie (kWh), vypočtená z výkonu v čase.", + "cost_tip": "Náklady na energii za tento cyklus, pevně dané při dokončení. Se zapnutými Časově váženými náklady se každý okamžik účtuje vlastní cenou; jinak cenou na konci. Pro vyplnění nastavte cenu v Nastavení.", "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", - "cost_dynamic_tip": "Časově vážené: účtováno cenou platnou v průběhu cyklu (efektivně {price} {cur}/kWh)." + "cost_dynamic_tip": "Časově vážené: účtováno cenou platnou v průběhu cyklu (efektivně {price} {cur}/kWh).", + "dbg_conf": "Spol.", + "dbg_mae": "MAE", + "dbg_corr": "Kor." }, "stat": { "avg": "prům.", @@ -2100,7 +2067,10 @@ "notify_finish": "Bylo by odesláno oznámení o dokončení.", "notify_milestone": "Bylo by odesláno oznámení o milníku.", "notify_held": "Oznámení bylo zadrženo kvůli tichým hodinám.", - "finished": "Cyklus dosáhl koncového stavu a skončil." + "finished": "Cyklus dosáhl koncového stavu a skončil.", + "match_reverted": "Program se vrátil do rozpoznávání: jeho skóre kleslo výrazně pod jeho vlastní špičku.", + "verified_pause": "Živé zarovnání potvrdilo (nebo uvolnilo) pauzu uvnitř programu. Dokud pauza trvá, cyklus se neukončí.", + "stalled": "Cyklus setrval na pohotovostním odběru déle, než u tohoto programu kdy trvá pauza, a práce ještě zbývá. Zobrazeno jako Pozastaveno (Zaseknuto); cyklus zůstává otevřený." }, "store": { "search_ph2": "Hledat podle značky nebo modelu…", @@ -2109,11 +2079,7 @@ "programs_count": "Programy: {n}", "browse": "Procházet", "device": "Zařízení", - "favorites": "Oblíbené", - "search_ph": "Hledat podle značky nebo modelu…", "no_results": "Nebyly nalezeny žádné odpovídající spotřebiče. Zkuste jiné vyhledávání.", - "n_programs": "{n} programů", - "n_cycles": "{n} cyklů", "no_programs": "Pro tento spotřebič zatím nejsou sdíleny žádné programy.", "no_cycles": "Pro tento program zatím nejsou sdíleny žádné referenční cykly.", "anon": "anonymní", @@ -2125,19 +2091,21 @@ "merge_target": "Sloučit do profilu", "description": "Popis (volitelné)", "contributed_by": "od {name}", + "website": "Web obchodu ↗", + "website_tip": "Otevřít web komunitního obchodu", + "favorites": "Oblíbené", "rating_summary": "★ {avg} ({n})", "no_ratings": "Zatím žádná hodnocení", - "website": "Web obchodu ↗", - "website_tip": "Otevřít web komunitního obchodu" + "unreachable": "Nepodařilo se připojit ke komunitnímu obchodu. Zkontrolujte připojení a zkuste to znovu." }, "settings": { "gear": { - "title": "Nastavení" + "title": "Předvolby" } }, "link": { - "manual": "Příručka ↗", - "add_appliance": "Přidejte svůj spotřebič" + "maintenance_supporter": "Maintenance Supporter na GitHubu ↗", + "manual": "Příručka ↗" }, "tip": { "add_brand": "Značka není uvedena? Přidejte ji do katalogu komunity", @@ -2171,7 +2139,13 @@ "rebuild": { "envelopes": "Přepočítávání obálek" }, - "cancelling": "Ruším..." + "cancelling": "Ruším...", + "auto_label": { + "running": "Automatické označování cyklů" + }, + "store_download": { + "running": "Stahování z obchodu" + } }, "setup": { "hdr": { @@ -2180,26 +2154,24 @@ }, "phase0": { "washer": "WashData již detekuje vaše cykly. Zaznamenejte první cyklus, abyste povolili názvy programů a odhady doby trvání.", - "dishwasher": "WashData sleduje. Myčky nádobí mají složité cykly – nahrání prvního cyklu je silně doporučeno. Pokud detekovaný cyklus běží příliš dlouho, ořízněte ho v editoru cyklů před uložením jako profil.", + "dishwasher": "WashData sleduje. Cykly myčky jsou složité, proto první cyklus nahrajte. Pokud detekovaný cyklus běží příliš dlouho, ořízněte jej v editoru cyklu před uložením jako profil.", "generic": "WashData sleduje. Zaznamenejte nebo označte detekovaný cyklus a začněte vytvářet profily." }, "phase1a": { - "labelled": "Dobrý začátek – váš první program je uložen. Pro nejčistší data zvažte nahrání dalšího cyklu pomocí widgetu rekordéru." + "labelled": "Dobrý začátek: váš první program je uložen. Pro nejčistší data nahrajte další cyklus rekordérem." }, "phase1b": { - "recorded": "Váš záznam byl uložen jako {profile_name}. Nyní nahrajte nebo označte další běžné programy, abyste rozšířili pokrytí." + "recorded": "Váš záznam byl uložen jako {profile_name}. Nyní nahrajte nebo označte další běžné programy." }, "phase1c": { - "verify": "Máte {count} programů od komunity. Spusťte cyklus, abyste ověřili, že je WashData správně rozpoznává – párování se bude zlepšovat, jak zařízení buduje vlastní historii." + "verify": "Máte programy od komunity ({count}). Spusťte cyklus a ověřte, že jej WashData rozpozná; párování se zlepšuje, jak si zařízení buduje vlastní historii." }, "phase2": { - "cluster": "WashData zaznamenal {count} cyklů, které neodpovídají žádnému uloženému programu – vypadají si navzájem podobně. Chcete pro ně vytvořit nový profil?", + "cluster": "WashData zaznamenal podobné cykly ({count}), které neodpovídají žádnému uloženému programu. Vytvořit pro ně nový profil?", "unmatched": "Váš poslední cyklus neodpovídal žádnému uloženému programu. Je to nový program?" }, "phase3": { - "suggestions": "WashData má doporučení k nastavení na základě vaší historie cyklů – zkontrolujte je pro zlepšení přesnosti detekce.", - "groups": "Některé vaše profily vypadají jako stejný program při různých teplotách. Uspořádejte je do skupiny pro lepší párování.", - "phases": "Přidejte fáze programu k {profile_name} pro přesnější odhady zbývajícího času." + "suggestions": "WashData má návrhy nastavení z vaší historie cyklů. Projděte je pro lepší detekci." }, "phase4": { "healthy": "Toto zařízení je plně nastaveno ({profile_count} profilů)." @@ -2212,12 +2184,58 @@ "create_from_cluster": "Vytvořit profil z těchto cyklů", "create_profile": "Vytvořit pro něj profil", "review_suggestions": "Zkontrolovat návrhy", - "organise_profiles": "Uspořádat profily", - "configure_phases": "Konfigurovat fáze", "skip_step": "Přeskočit tento krok", "skip_forever": "Nezobrazovat znovu", "hide_guidance": "Skrýt pokyny", "show_guidance": "Zobrazit pokyny" } + }, + "access": { + "edit": "Úpravy", + "full": "Plný", + "inherit": "Zdědit", + "none": "Žádný (skryto)", + "read": "Čtení" + }, + "pg_label_reason": { + "no_winner": "Žádný program neodpovídá", + "below_floor": "Pod prahem \"Důvěra učení\"", + "ambiguous": "Označeno jako nejednoznačné", + "margin": "Příliš blízko druhému nejlepšímu programu", + "unknown_profile": "Program již neexistuje", + "not_evaluated": "Nevyhodnoceno" + }, + "diagram": { + "accum": "akum.", + "auto": "auto", + "below_off": "pod = vyp.", + "confirmed": "potvrzeno", + "energy_counts": "energie se počítá", + "feedback": "zpětná vazba", + "gap_below_min": "mezera pod min. = jeden cyklus", + "heat_phase": "fáze ohřevu", + "held_at_100": "zůstává na 100 %", + "ignored": "ignorováno", + "kept": "ponecháno", + "max": "max", + "min": "min", + "minus_tol": "-tol", + "no_match": "bez shody", + "no_updates": "bez aktualizací", + "off_delay_wait": "zpoždění vypnutí", + "off_gap": "mezera vyp.", + "plus_tol": "+tol", + "profile": "profil", + "reading_interval": "typický interval měření", + "sensor_offline": "senzor offline: vynucené zastavení", + "si": "IV", + "spike_ignored": "špička ignorována", + "spike_ignored_short": "špička: ignorována", + "start": "spuštění", + "stop": "zastavení", + "tail_energy_resets": "energie konce nad prahem: časovač se vynuluje", + "thr": "práh", + "too_short": "příliš krátký", + "tumble_kept": "impulzy bubnu ponechány" } } diff --git a/custom_components/ha_washdata/translations/panel/da.json b/custom_components/ha_washdata/translations/panel/da.json index c08bc90c..562d16c7 100644 --- a/custom_components/ha_washdata/translations/panel/da.json +++ b/custom_components/ha_washdata/translations/panel/da.json @@ -3,6 +3,7 @@ "min": "min", "entity_not_found": "Entitet ikke fundet", "running_long": "Kører længere", + "stalled": "Gået i stå", "remaining": "tilbage", "progress": "fremgang", "show_time_remaining": "Vis resterende tid", @@ -48,16 +49,16 @@ } }, "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_tip": "Mod slutningen bliver dette program stille i ca. {mins} min og trækker derefter ca. {watts} W i {secs} s. Set i {seen} af {measured} målte cyklusser.", "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", + "artifact_tip": "{n} registrerede anomalier (f.eks. dør åbnet midt i cyklussen) - åbn for at se dem på grafen", + "artifact_tip_one": "{n} registreret anomali (f.eks. dør åbnet midt i cyklussen) - åbn for at se den på grafen", + "artifact_tip_other": "{n} registrerede anomalier (f.eks. dør åbnet midt i cyklussen) - åbn for at se dem på grafen", "auto": "(auto-detekteret)", "built_in_tag": "indbygget", "declining": "↘ faldende", "energy_low": "Lavere energiforbrug end normalt", "energy_spike": "Højere energiforbrug end normalt", - "fair_fit": "acceptabel kvalitet", - "fair_fit_tip": "Moderat matchkonsistens - nogle cyklusser, der er tildelt denne profil, har lavere konfidensscore. Mærk flere cyklusser, eller optag profilen igen for at forbedre nøjagtigheden.", "feedback_requested": "Feedback anmodet", "golden_cycle": "Registreret referencecyklus", "duration_outlier": "{n} cyklus(ser) med afvigende længde", @@ -67,26 +68,37 @@ "overrun": "Løb længere end normalt", "underrun": "Færdig hurtigere end normalt", "poor_fit": "dårlig kvalitet", - "poor_fit_tip": "Inkonsekvent matchhistorik: overvej at genopbygge denne profil", - "restart_gap_tip": "{n} HA-genstartsgab: strømsporet har et hul", + "restart_gap_tip": "{n} HA-genstartsgab: strømsporet har huller", + "restart_gap_tip_one": "{n} HA-genstartsgab: strømsporet har et hul", + "restart_gap_tip_other": "{n} HA-genstartsgab: strømsporet har huller", "reviewed": "Gennemgået", "steady": "→ stabil", - "using_defaults": "Brug af afsendte standardindstillinger", - "using_tuned": "Brug af indstillede vægte", "overrun_ratio": "({x}x forventet)", "underrun_ratio": "({pct}% af forventet)", - "awaiting_tip": "Afventer godkendelse fra fællesskabet", - "awaiting_n": "Afventer godkendelse · {n} bekræftet", - "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.", - "armed": "(gælder for næste cyklus)" + "armed": "(gælder for næste cyklus)", + "awaiting_tip": "Afventer godkendelse fra fællesskabet", + "awaiting_n": "Afventer godkendelse · {n} bekræftet", + "awaiting": "Afventer godkendelse", + "approved": "Godkendt", + "matcher_unused": "Endnu ikke genkendt i dine cyklusser", + "matcher_used": "Genkendt i {n} af dine cyklusser", + "matcher_used_one": "Genkendt i {n} af dine cyklusser", + "matcher_used_other": "Genkendt i {n} af dine cyklusser", + "matcher_used_tip": "Cyklusser på dette apparat, som WashData selv har matchet med dette importerede program. En import, der aldrig matcher, passer måske ikke til dit apparat; du kan fjerne den. Tælles lokalt; intet sendes til fællesskabsbutikken." }, "btn": { + "log_done_task": "Log som udført: {task}", + "add_task": "Tilføj opgave", + "pg_preset_delete": "Slet forudindstilling", + "pg_preset_load": "Indlæs forudindstilling", + "pg_preset_load_tip": "Erstat værdierne i legepladsen med denne forudindstilling (de aktive indstillinger røres ikke)", + "pg_preset_save": "Gem som forudindstilling", + "pg_preset_save_tip": "Gem alle værdier nedenfor som en navngiven forudindstilling for denne enhed", "reset_zoom": "Nulstil zoom", "set_brand_model": "Angiv mærke og model", "refresh_catalog": "Opdater katalog", @@ -97,12 +109,11 @@ "add_timer": "+ Tilføj timer", "apply_all": "Anvend alle", "apply_label": "Påfør etiket", - "apply_set_b": "Anvend sæt B", "apply_split": "Påfør Split", "apply_trim": "Anvend beskæring", "auto_detect_split": "Automatisk registrering", "auto_label_cycles": "Auto-label cyklusser", - "auto_label_cycles_tip": "Tildel automatisk profilnavne til umærkede cyklusser, hvis matchkonfidens overstiger tærsklen", + "auto_label_cycles_tip": "Navngiv umærkede cyklusser, hvis matchkonfidens når tærsklen.", "cancel": "Annuller", "clear_debug": "Ryd fejlretningsdata", "clear_splits": "Ryd", @@ -112,8 +123,6 @@ "convert_to_automation": "Konverter til automatisering", "correct": "Korriger…", "create": "Opret", - "create_from_cluster": "Opret profil fra {count} cyklusser", - "create_profile": "+ Opret profil", "delete": "Slet", "delete_group": "Slet gruppe", "delete_group_tip": "Slet kun denne gruppe - medlemsprofilerne bevares", @@ -124,22 +133,19 @@ "done": "Færdig", "edit": "Rediger", "export": "Eksportér", - "export_json": "Eksporter til JSON", "force_stop": "Tving stop", "force_stop_tip": "Afslut den aktuelle cyklus øjeblikkeligt og markér den som tvangsstoppet", "from_template": "Fra skabelon ▾", "ignore": "Ignorer", "import_json": "Importer fra JSON", - "import_overwrite": "Importer (overskriver data)", "inspect": "Inspicer", "label": "Mærk", - "load": "Indlæs", "load_more": "Indlæs flere", "manage": "Administrer", "merge": "Flet sammen", "new_automation": "+ Ny automatisering", "new_group": "+ Ny gruppe", - "new_group_tip": "Gruppér næsten identiske profiler (samme form/varighed, forskellig temperatur eller centrifugering) så matcheren pålideligt vælger mellem dem", + "new_group_tip": "Gruppér næsten identiske profiler (samme form, anden temperatur eller centrifugering), så matchningen kan skelne dem.", "new_phase": "+ Ny fase", "new_profile": "+ Ny profil", "new_profile_tip": "Opret en ny programprofil fra en eksisterende mærket cyklus eller optagelse", @@ -147,36 +153,25 @@ "on_cycle_started": "På cyklus startet", "pause_cycle": "Pause", "pause_cycle_tip": "Sæt den kørende cyklus på pause – apparatet genoptager, hvor det slap", - "pg_apply_device": "Anvend på enhed", - "pg_autofill": "Autoudfyld", - "pg_load_calibrated": "Indlæs kalibrerede (ML) ({n})", - "pg_load_calibrated_tip": "Indlæser ML-kalibrerede indstillinger som legeplads-tilsidesættelser", "pg_load_live": "Indlæs aktive indstillinger", "pg_load_live_tip": "Genindlæs integrationens aktuelle indstillinger, og kassér alle ændringer i legepladsen", "pg_load_suggested": "Indlæs forslag ({n})", "pg_load_suggested_tip": "Indlæser auto-tunerens aktuelle forslag som legeplads-tilsidesættelser", - "pg_preset_delete": "Slet forudindstilling", - "pg_preset_load": "Indlæs forudindstilling", - "pg_preset_load_tip": "Erstat værdierne i legepladsen med denne forudindstilling (de aktive indstillinger røres ikke)", - "pg_preset_save": "Gem som forudindstilling", - "pg_preset_save_tip": "Gem alle værdier nedenfor som en navngiven forudindstilling for denne enhed", "pg_publish_all": "Gem {n} i integrationen", "pg_publish_one": "Gem {label} i integrationen", - "play": "Afspil", "process": "Behandl", "process_history": "Behandl nu", "process_recording": "Gem optagelse", "process_tip": "Gem det optagede spor som en ny eller eksisterende profil", "rebuild": "Genopbyg konvolutter", "rebuild_envelope": "Genopbyg konvolut", - "rebuild_tip": "Genberegn den forventede effektkonvolut (min/max-bånd) for alle profiler fra deres mærkede cyklusser – kør efter mærkning af nye cyklusser eller korrigering af gamle", + "rebuild_tip": "Genopbyg hver profils forventede effektbånd ud fra dens mærkede cyklusser. Kør efter mærkning eller rettelse af cyklusser.", "rec_start_tip": "Begynd at optage apparatets effektspor – start umiddelbart inden du kører en cyklus", "rec_stop": "Stop", "rec_stop_tip": "Stop optagelsen og behold det indfangede spor til gennemgang", "record": "Start optagelse", "refresh": "Opdater", "refresh_diag_tip": "Genindlæs lagerstatistik", - "refresh_logs_tip": "Genindlæs de nyeste logposter", "refresh_settings_tip": "Genindlæs indstillinger fra serveren", "relabel": "Ommærk ({count})", "remove": "Fjern", @@ -184,7 +179,6 @@ "reset": "Nulstil", "reset_ml_models_tip": "Kassér de finjusterede modeller og gå tilbage til de indbyggede. WashData kan lære dem igen senere.", "reset_to_builtin": "Nulstil til indbyggede modeller", - "reset_to_defaults": "Nulstil til standardindstillinger", "resume_cycle": "Fortsæt", "resume_cycle_tip": "Genoptag den satte cyklus", "revert_settings": "Annuller ændringer", @@ -197,8 +191,6 @@ "run_analysis": "🔍 Kør forslagsanalyse", "run_analysis_tip": "Analyser dine optagede cyklusser nu, og opdater indstillingsforslag", "run_auto_label": "Kør Auto-label", - "run_both": "Kør A mod B", - "run_simulation": "Kør simulering", "save": "Gem", "save_access_control": "Gem adgangskontrol", "save_group": "Gem gruppe", @@ -210,10 +202,8 @@ "save_settings": "Gem indstillinger", "select": "Vælg", "select_all": "Vælg alle", - "select_last20": "Seneste 20", "select_none": "Ryd", "show_only": "Vis kun", - "skip_setup": "Spring opsætning over", "split": "Opdel", "stop": "Stop", "submit_correction": "Send rettelse", @@ -222,36 +212,32 @@ "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.", + "unlabel_selected_tip": "Fjern mærkningen fra de valgte cyklusser. De bliver i din historik, men former ikke længere denne profil.", "use": "Brug", "wipe_all": "Slet alle data", + "pg_rerun": "Opdater simulering", "delete_selected": "Slet valgte ({n})", - "group_suggest": "Gruppe {n}: {members}", "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", "import": "Importér", "disconnect": "Afbryd forbindelse", "connect_github": "Forbind GitHub", - "save_appliance": "Gem apparat", "share_to_store": "Del i butikken", "share": "Del", - "confirm_appliance": "Bekræft dette apparat", - "confirmed": "Bekræftet", "browse_community_setups": "Gennemse fællesskabsopsætninger", "download_device": "Download denne opsætning", "share_device": "⬆ Del dette apparat", - "share_device_tip": "Del dette apparat og dets optagede referencecyklusser til fællesskabsbutikken, så andre med samme maskine kan overtage dem", + "share_device_tip": "Del dette apparat og dets referencecyklusser til fællesskabsbutikken, så ejere af samme maskine kan overtage dem.", "share_n": "Del {n} cyklus/cyklusser", "export_selected": "Eksportér (vælg data)", "export_all": "Hurtig eksport af alt", - "import_raw": "Avanceret: erstat alt fra JSON", "download_export": "Download eksport", "analyze_import": "Analysér fil", "import_selected": "Importér valgte", "back": "Tilbage", "mute_suggestion": "Stop med at foreslå denne indstilling", "reset_muted": "Nulstil undertrykte forslag", + "reset_phase_tip": "Fortryd din ændring og gendan den indbyggede fase", "import_power_history": "Importér effekthistorik", "hist_read_recorder": "Læs fra Home Assistant", "hist_scan": "Scan efter cyklusser", @@ -260,7 +246,12 @@ "save_total_cycles": "Gem total", "clear": "Ryd", "add_brand": "Tilføj mærke", - "add_appliance": "Tilføj apparat" + "add_appliance": "Tilføj apparat", + "retry": "Prøv igen", + "confirm_appliance": "Bekræft dette apparat", + "confirmed": "Bekræftet", + "create_profile_from_gap": "Opret profil", + "undo_import": "Fortryd seneste import" }, "note": { "off_delay": { @@ -268,10 +259,6 @@ } }, "conflict": { - "anti_wrinkle_exit": { - "exit": "Skal være under stoptærsklen ({stop} W); ellers ignoreres antikrøl-afslutningseffekten", - "stop": "Skal være over antikrøl-afslutningseffekten ({exit} W)" - }, "anti_wrinkle_max": { "max": "Skal være over starttærsklen ({start} W); ellers omgås antikrøl-varighedsgrænsen", "start": "Skal være under antikrøl-maksimumeffekten ({max} W)" @@ -309,14 +296,14 @@ "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." + "power_sensor": "Dette er apparatets Effektsensor, ikke en pris. Cyklusser ville blive beregnet i watt pr. kWh; ryd dette felt.", + "energy_sensor": "Dette er apparatets Energimåler, ikke en pris. 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. Ryd dette felt, eller vælg en tarifsensor.", + "unit": "Denne sensor måler i {unit}, ikke en pris pr. kWh. Ryd dette felt, eller vælg 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" + "duration": "Skal være under Timeout uden opdatering ({to} s), så alarmen for fastklemt pumpe kan udløses først", + "timeout": "Skal være over Varighed for fastklemt pumpe ({ps} s), så alarmen for fastklemt pumpe kan udløses først" }, "start_dur": { "sampling": "Samplingsinterval overskrider startvarigheden ({sdt} s); enkeltmålingsspidser kan åbne en cyklus", @@ -334,10 +321,15 @@ "sampling": "Samplingsinterval bør højst være halvdelen af watchdog-intervallet ({wi} s)" }, "settings_banner": "Indstillingskonflikter: {n}. Tjek de fremhævede sektioner og ret dem, inden du gemmer.", - "settings_banner_btn": "Gå til første" + "settings_banner_btn": "Gå til første", + "end_energy": { + "energy": "For streng til Stoptærskel ({w} W) i løbet af Slukkeforsinkelse ({d} s); cyklussen kan kun afsluttes via en reservevej", + "stop": "Slutenergi ({e} Wh på {d} s) tillader kun {p} W" + } }, "hdr": { - "ab_comparison": "A/B-indstillingssammenligning", + "custom_tasks": "Dine egne opgaver", + "maintenance_supporter": "Brug for mere?", "access_control": "Adgangskontrol", "add_maintenance": "Tilføj vedligeholdelseshændelse", "automation_delete": "Slet denne automatisering", @@ -346,28 +338,22 @@ "automation_review_this_cycle": "Gennemgå denne cyklus", "automations": "Automatiseringer", "clear_debug": "Ryd fejlretningsspor", - "cycle_simulator": "Cyklussimulator", "diagnostics_pref": "Diagnostik", "display": "Vise", - "dtw_inspector": "DTW-inspektor", "export_import": "Eksport/import", - "getting_started": "Kom godt i gang", "live_power": "Live effekt", "logs": "Logfiler", - "logs_diagnostics": "Diagnostik", "maintenance": "Vedligeholdelseshandlinger", "maintenance_log": "Vedligeholdelseslog", "maintenance_reminders": "Servicepåmindelser", "manual_recording": "Manuel optagelse", - "manual_recording_tip": "Kør en cyklus bevidst mens WashData optager effektsporet. Klik på Start optagelse umiddelbart inden apparatet starter, Stop når det er færdigt, og derefter Behandle for at gemme det som en navngivet profil.", + "manual_recording_tip": "Optag en cyklus bevidst: tryk på Start lige før apparatet starter og Stop, når det er færdigt, og derefter Behandl for at gemme den som en profil.", "ml_learned": "Hvad WashData har lært", - "ml_matching_tuning": "Program-matchende finjustering", "ml_settings_card": "Indstillinger", "ml_smart_learning": "Smart læring", "my_preferences": "Mine præferencer", "no_automations": "Ingen automatiseringer refererer til denne enhed endnu.", "panel_settings": "Panelindstillinger", - "param_sweep": "Parametersweep", "pg_settings_source": "Indstillingernes kilde", "phase_catalog": "Fasekatalog", "playground": "Legeplads", @@ -376,11 +362,8 @@ "status": "Status", "status_graph": "Statusgraf", "storage_stats": "Opbevaringsstatistik", - "test_history": "Test på historik", "toggle_sidebar": "Skift Home Assistant-sidebjælken", - "tools_and_data": "Værktøjer og data", "wipe_history": "Tør historie", - "recommendations": "Anbefalinger ({n})", "pg_detection_params": "Registreringsindstillinger", "pg_outcome": "Simuleringsresultat", "pg_across_cycles": "På tværs af dine cyklusser", @@ -388,7 +371,8 @@ "online_account": "Fællesskabsbutik og onlinefunktioner", "import_power_history": "Importér effekthistorik", "service_status": "Servicestatus", - "online_account_tab": "Online & fællesskab" + "online_account_tab": "Online & fællesskab", + "live_match_debug": "Live-match-fejlretning" }, "health": { "fair": "Acceptabel matchkvalitet", @@ -396,7 +380,12 @@ "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.", + "task_name": "Opgave", + "every_cycles": "Hver (cyklusser)", + "every_days": "Hver (dage)", + "new_task_name": "Ny opgave", + "days_since_service": "{since} / {total} dage", + "envelope_position_tip": "Position på det matchede programs optagede kurve: {pct} %. Findes ved at justere cyklussen efter profilen, så den passer, selv når en kørsel er længere eller kortere. Opdateres, mens apparatet er stille, så den kan halte bagefter.", "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})", @@ -408,10 +397,8 @@ "aria_cycle_chart": "Cyklussens effektkurve", "aria_envelope_chart": "Diagram over profilens effektindhylning", "aria_phase_chart": "Diagram for faseredigering", - "aria_playground_chart": "Cyklussens effektkurve med trækbare detektionstærskler", "aria_power_chart": "Diagram over effektforbrug", "aria_spaghetti_chart": "Overlejrede effektkurver for cyklusser", - "aria_sweep_chart": "Diagram over resultater fra parametersweep", "artifact_anomaly": "Anomali", "artifact_high_power": "Høj effekt", "artifact_interruption": "Afbrydelse", @@ -425,155 +412,105 @@ "cohesion_good": "sammenhæng {pct} %", "cohesion_low": "⚠ lav sammenhæng {pct} %", "compare_profiles": "Sammenlign med profiler", - "concurrency": "Samtidighed", "confidence": "Tillid", "confidence_threshold": "Tillidsgrænse", "correct_duration": "Korrekt varighed (min, valgfrit)", "correct_profile": "Korrekt profil", - "correlation": "Korrelation", "cost": "Omkostning", "create_new_profile": "+ Opret ny profil...", "current_phase": "Aktuel fase", - "curves": "Kurver", "cycle": "Cyklus", "cycle_date_display": "Visning af cyklusdato", - "cycle_health": "Cyklussundhed", - "cycle_index": "cyklusindeks →", - "cycles": "Cyklusser", + "cycle_context": "Recorder-historik omkring cyklussen", + "cycle_context_off": "Fra", + "cycle_context_minutes": "{min} min", + "cycle_context_series": "Recorder-historik", + "before_start": "Før start", + "after_end": "Efter slut", "cycles_count": "Cykler", "cycles_lc": "cyklusser", - "dashed": "stiplet", "date": "Dato", "debug_traces": "Debug Spor", - "default": "Standard", "default_access_level": "Standardniveau for brugere, der ikke er angivet nedenfor", "default_other": "Standard (andre enheder)", "default_tab": "Standardfane ved åbning af panelet", - "delta": "Δ", "description": "Beskrivelse", - "dtw_blend_w": "Blandingsvægt", - "dtw_blended": "Blandet score", - "dtw_ddtw": "DDTW-score", - "dtw_l1": "L1-score", - "dtw_matched_profile": "(cyklussens matchede profil)", "duration": "Varighed", - "duration_agreement": "Varighedsoverensstemmelse", - "emphasis": "Vægt", "enable_access_control": "Aktiver adgangskontrol pr. bruger", "energy": "Energi", - "energy_agreement": "Energioverensstemmelse", - "event_timeline": "Hændelsestidslinje", "event_type": "Hændelsestype", "expected": "Forventet", + "suspected_profile": "{name} (formodet)", + "runner_up_profile": "{name} (nummer to)", "expected_duration": "Forventet varighed (min)", "file_kb": "Fil (kB)", - "final_score": "Endelig score", "from": "Fra", "gap_s": "Mellemrum (s)", "group_name": "Gruppenavn", "head_trim": "Beskær start (s)", - "health": "Sundhed", "hide_tabs": "Skjul faner for ikke-administratorer", - "in_use": "I brug", "json_data": "JSON-data", "label": "Matchet profil", "last": "Seneste", "last_checked": "Sidst tjekket:", "load_from_file": "Indlæs fra fil", - "mae_score": "MAE-score", "manual_duration": "Manuel varighed (min, valgfri)", "match": "Match", "members": "Medlemmer", - "metric": "Metrik", "mode_existing_profile": "Føj til eksisterende profil", "mode_new_profile": "Opret ny profil", "models_fine_tuned": "(finjusterede modeller: {count})", - "n_classic_suggestions": "{n} klassisk", - "n_ml_suggestions": "{n} ML", "n_selected": "{n} valgt", "n_tuning_suggestions": "{n} justeringsforslag", - "na": "n/a", + "n_tuning_suggestions_one": "{n} justeringsforslag", + "n_tuning_suggestions_other": "{n} justeringsforslag", "new_profile_name": "Nyt profilnavn", "notes": "Noter", - "of": "af", - "outcome": "Resultat", "panel_default_tab": "Standardfane", "panel_language": "Panelsprog", "per_cycle_short": "cyklus", - "pg_autofill_tip": "Autoudfyld interval fra aktuel værdi ({lo}–{hi})", "pg_best_value": "Bedste værdi fundet", "pg_current_value": "Aktuel værdi:", "pg_drag_hint": "Træk linjen på grafen", - "pg_dtw": "DTW", "pg_ev_ending": "Afslutter", "pg_ev_running": "Kører", "pg_group_advanced": "Særtilfælde", "pg_group_detection": "Registreringsudløsere", "pg_group_timing": "Tidsregler", "pg_idle": "Inaktiv", + "pg_poor_match": "Dårligt match", "pg_preset": "Forudindstilling til legepladsen", "pg_preset_name": "Navn på ny forudindstilling", "pg_preset_none": "Vælg en forudindstilling…", - "pg_stress_group": "Test af inaktiv afslutning", - "pg_stress_idle_auto": "Auto", - "pg_stress_idle_w": "Inaktivt niveau (W)", - "pg_stress_idle_w_desc": "Tilsidesæt den automatisk registrerede standby-minimumsgrænse (lad feltet være tomt for automatisk).", - "pg_stress_toggle": "Test inaktiv afslutning", - "pg_stress_toggle_desc": "Simulerer at apparatet forbliver ved sit standbyforbrug efter optagelsens afslutning - viser om og hvornår WashData stopper cyklussen.", - "pg_last_run": "Seneste kørsel: {date}", - "pg_match_score": "Matchscore", - "pg_pct_of": "{p}% af {total}", - "pg_poor_match": "Dårligt match", "pg_strong_match": "Stærkt match", - "pg_tile_ambiguous": "Tvetydig", - "pg_tile_ambiguous_tip": "Cyklusser med næsten uafgjort mellem to programmer", - "pg_tile_detected": "Registreret", - "pg_tile_detected_tip": "Cyklusser hvor effekten krydsede starttærsklen længe nok til at blive registreret som kørende", - "pg_tile_matched": "Matchet", - "pg_tile_matched_tip": "Cyklusser der er korrekt tilskrevet et kendt program", - "pg_time_left": "Tid tilbage", "pg_weak_match": "Svagt match", "phase_name": "Fasenavn", "name": "Navn", "phase_timeline": "Fasetidslinje", "power": "Effekt", "profile": "Profil", - "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. Vælg et, før du starter apparatet, så anvendes det, så snart den næste cyklus begynder.", + "program_tip": "Vælg, hvilket program den aktuelle cyklus matches med. Automatisk registrering vælger det bedste match. Fastgør et program, når automatisk registrering tager fejl; vælges det før start, gælder det for næste cyklus.", "progress": "Fremskridt", "raw_socket": "Rå stikkontakt", - "read_only": "Skrivebeskyttet", "ref_cycle": "Referencecyklus (valgfrit)", - "refresh_interval": "Opdateringsinterval (s)", "remaining": "Tilbage", "reminder_every": "Påmind hver (cyklusser)", "remove_label": "- Fjern etiket -", "rename_profile": "Omdøb profil", - "replay_duration": "Afspilning", "resulting_profile": "Resulterende profil", "save_mode": "Gem tilstand", - "score": "Score", - "score_breakdown": "Scoreopdeling", "seconds_from_start": "Sekunder fra start", "select_profile": "Vælg Profil", "setting": "Indstilling", - "settings_a": "Indstillinger A (nuværende)", - "settings_a_short": "A", "settings_advanced": "Avanceret", - "settings_b": "Indstillinger B (test)", - "settings_b_short": "B", "settings_basic": "Grundlæggende", "settings_detail_level": "Detaljeniveau for indstillinger", - "settings_overrides": "Tilsidesættelser af indstillinger", "show_debug": "Vis live-match-fejlretningskort på statussiden (tillid, tvetydighed, topkandidater)", "show_expected": "Vis forventet kurveoverlejring (matchet profil, orange)", "show_raw": "Vis kontakt for råe stikkontaktværdier i live strømgraf", "sparkline": "Tendens for seneste cyklusvarigheder", - "stage2": "Trin 2 – kerneligning", - "stage3": "Trin 3 – DTW", - "stage4": "Trin 4 – overensstemmelse", "status": "Status", "tail_trim": "Beskær slut (s)", "tap_to_dismiss": "tryk på grafen for at lukke", @@ -584,7 +521,6 @@ "ungrouped": "Ugrupperet", "unlabelled": "Umærket", "unlabelled_paren": "(umærket)", - "warp_path": "Warp-sti", "input": "Input:", "start": "Start", "end": "Slut", @@ -609,9 +545,8 @@ "n_shown": ", {n} vist", "cycles_title": "Cyklusser ({n})", "pg_detecting": "Registrerer", - "pg_best_marker": "Bedste: {v}", "pg_time_left_model": "Tid tilbage (model)", - "pg_time_left_model_tip": "Modelberegnet resterende tid (faseberegner + ML-blanding), ikke en statisk nedtælling", + "pg_time_left_model_tip": "Beregnet ud fra programmets faser, ikke en fast nedtælling", "phase": "Fase", "pg_mode_history": "Test på historik", "pg_ev_detected": "Registreret", @@ -651,12 +586,8 @@ "pg_mode_optimize": "Optimér", "pg_group_matching": "Programgenkendelse", "pg_zoom_hint": "dobbeltklik for at nulstille zoom", - "aria_playground_chart2": "Interaktiv effektgraf for cyklussen: hold markøren over for at aflæse tid/effekt, rul for at zoome, træk for at panorere", "pg_alert_timeout_end": "Afsluttet ved timeout, ikke ved forudsigelse", "pg_alert_indefinite": "Ville køre i det uendelige", - "pg_alert_stress_cap": "Sikkerhedsgrænse nået", - "pg_alert_stress_ok": "Inaktiv afslutning: cyklus stoppet", - "pg_alert_stress_warn": "Standbyforbrug over stoptærskel", "task_pg_history": "Test på historik", "task_pg_sweep": "Optimér", "task_reprocess": "Genbehandling", @@ -666,16 +597,9 @@ "pg_recent_runs": "Seneste kørsler", "pg_load_run": "Genindlæs denne kørsel", "pg_match_confidence": "Matchsikkerhed", - "pg_envelope_fit": "Envelope-tilpasning", - "pg_envelope_fit_tip": "Hvor tæt cyklussen holder sig inden for denne profils gemte effekt-envelope, en anden vinkel end matchsikkerhed.", "n_imported_note": ", {n} importeret", "enable_online": "Aktivér onlinefunktioner", - "store_brand": "Apparatets mærke", - "store_model": "Apparatets model", "anonymous": "Anonym", - "rate_quality": "Bedøm kvalitet", - "rate_n": "{n} stjerner", - "approved_only": "Kun godkendte", "adopt_settings": "Overtag også indstillinger", "flags": "Flags", "include_phase_map": "Inkludér fasekort", @@ -692,7 +616,6 @@ "cat_custom_phases": "Brugerdefinerede faser", "cat_profile_groups": "Profilgrupper", "cat_settings": "Registrerings- & genkendelsesindstillinger", - "cat_matching_config": "Finjustering af genkendelse", "cat_ml_models": "ML-modeller", "cat_feedback": "Feedback & gennemgangsetiketter", "cat_suggestions": "Forslag", @@ -715,7 +638,6 @@ "hist_csv_data": "CSV-data", "hist_from_recorder": "Eller læs den fra Home Assistant", "hist_since": "Siden", - "days": "dage", "hist_keep": "Behold denne cyklus", "hist_looks_complete": "komplet", "peak_power_short": "Spids", @@ -735,7 +657,34 @@ "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" + "correct_total_cycles": "Ret totalen", + "hist_reason_already_recorded": "overlapper en cyklus, som WashData allerede har registreret", + "share_consent": "Jeg bekræfter, at disse cyklusser er gennemført normalt uden afbrydelse", + "show_internal": "Vis interne indstillinger", + "show_internal_tip": "Indstillinger, som WashData justerer for dig (sampling, watchdog, matchfrekvens, tærskler). Vises, når denne enhed har sin egen værdi, en konflikt eller et forslag.", + "rate_n": "{n} stjerner", + "rate_quality": "Bedøm kvalitet", + "pg_autolabel": "Auto-mærkning", + "pg_label_review": "Beder om gennemgang", + "pg_ev_match_reverted": "Match trukket tilbage", + "pg_ev_verified_pause": "Bekræftet pause", + "n_samples": "{n} målinger", + "n_samples_one": "{n} måling", + "n_samples_other": "{n} målinger", + "n_cycles": "{n} cyklusser", + "n_cycles_one": "{n} cyklus", + "n_cycles_other": "{n} cyklusser", + "energy_per_cycle": "{v}/cyklus", + "energy_total": "{v} i alt", + "automation_off": "(slået fra)", + "ref_cycle_none": "Ingen", + "share_name": "Vis mit navn på det, jeg deler", + "pg_compare_profile": "Sammenlign med profil", + "pg_obj_endlag": "Slutforsinkelse (median)", + "pg_obj_early": "Rate for tidlig afslutning", + "pg_obj_split": "Opdelingsrate", + "pg_sweep_cycles": "Seneste cyklusser", + "cat_backfill_cycles": "Cyklusser fundet i importeret effekthistorik" }, "log": { "all_levels": "Alle niveauer", @@ -745,11 +694,9 @@ }, "ml": { "builtin_models": "● Brug af indbyggede modeller", - "fit_fair": "Acceptabel", "fit_good": "God", "fit_slight": "Let", "fit_strong": "Stærk", - "fit_weak": "Svag", "fit_word": "tilpasning", "personalized": "● Personliggjort til denne maskine", "better_than_baseline": "{pct}% bedre end grundlinjeestimatet ({metric})", @@ -757,30 +704,39 @@ "trend_declining_tip": "Denne models tilpasning er blevet dårligere ved de seneste gentjek. At gennemgå flere cyklusser kan hjælpe den med at lære igen.", "trend_steady_tip": "Denne models tilpasning har holdt sig nogenlunde stabil ved de seneste gentjek.", "cap_blurb": { - "end": "At vide, hvornår en cyklus reelt er færdig", - "live_match": "At identificere det kørende program hurtigere", - "quality": "At opdage fejldetekterede eller beskadigede cyklusser", - "remaining_time": "At forudsige, hvor lang tid der er tilbage", "total_energy": "At forudsige samlet energi og omkostning" }, "cap_label": { - "end": "Registrering af cyklusslut", - "live_match": "Programgenkendelse", - "quality": "Kvalitetstjek af cyklus", - "remaining_time": "Estimat af resterende tid", "total_energy": "Energiestimat" }, - "metric_auc": "AUC {auc} på valideringsdata", - "metric_mae": "fejl {model} mod baseline {baseline}" + "metric_mae": "fejl {model} mod baseline {baseline}", + "fine_tuned_at": "finjusteret {when}", + "never": "aldrig", + "trained_unknown": "på et ukendt tidspunkt", + "held_out_count": "målt på {count} tilbageholdte valideringscyklusser", + "held_out_count_one": "målt på {count} tilbageholdt valideringscyklus", + "held_out_count_other": "målt på {count} tilbageholdte valideringscyklusser", + "last_run_not_promoted": "Sidste tjek ({when}): {reason}", + "skip_reason": { + "insufficient_rows": "Endnu ikke nok brugbare cyklusser: {count} mærkede, rene cyklusser indtil videre.", + "insufficient_rows_one": "Endnu ikke nok brugbare cyklusser: {count} mærket, ren cyklus indtil videre.", + "insufficient_rows_other": "Endnu ikke nok brugbare cyklusser: {count} mærkede, rene cyklusser indtil videre.", + "fit_failed": "Træningen mislykkedes: {error}", + "unknown": "Intet nyt blev lært.", + "holdout_too_small": "For få cyklusser til at teste en ny model retvisende: {held_out} kunne holdes tilbage til test, der kræves {min}.", + "not_better_than_naive": "Den nye model var ikke tydeligt bedre end det simple skøn (fejl {model} mod {naive}).", + "not_better_than_incumbent": "Den nye model var ikke bedre end den, der er i brug (fejl {model} mod {incumbent}), så den nuværende blev beholdt." + } }, "modal": { + "remove_task_title": "Fjern opgave", + "remove_task_msg": "Fjern påmindelsen \"{task}\"? Dens poster i vedligeholdelsesloggen bevares.", "auto_label": "Auto-label cyklusser", "correct_feedback": "Korriger feedback", "create_profile": "Opret profil", "cycle": "Cyklus", "edit_group": "Rediger profilgruppe", "edit_phase": "Rediger fase", - "import_config": "Importer konfiguration", "label_cycle": "Mærk cyklus", "merge_cycles": "Flet {n} cyklusser", "new_group": "Ny profilgruppe", @@ -795,6 +751,8 @@ "delete_automation_title": "Slet automatisering", "delete_automation_msg": "Slet automatiseringen \"{name}\" fra Home Assistant? Dette kan ikke fortrydes.", "remove_legacy_title": "Fjern ældre handlinger", + "reset_phase_msg": "Fortryd din ændring af \"{name}\" og gendan den indbyggede fase? Programmer beholder deres områder.", + "reset_phase_title": "Nulstil fase", "remove_legacy_msg": "Fjern de ældre brugerdefinerede handlinger? De vil ikke længere blive udløst ved cyklushændelser. Dette kan ikke fortrydes fra panelet.", "discard_recording_title": "Kassér optagelse", "discard_recording_msg": "Kassér den gemte optagelse? Dette kan ikke fortrydes.", @@ -815,63 +773,77 @@ "store_share_device": "Del dette apparat", "export_select": "Eksport - vælg data", "import_wizard": "Import - vælg data", - "history_import": "Importér effekthistorik" + "history_import": "Importér effekthistorik", + "apply_suggestions_title": "Anvend forslag", + "apply_suggestions_msg": "Ændre disse {n} indstillinger? Integrationen genindlæses efter gemning.", + "discard_settings_title": "Kassér ikke-gemte ændringer?", + "discard_settings_msg": "Ikke-gemte ændringer af indstillinger: {n}. Vil du forlade Indstillinger og kassere dem?", + "profile_title": "Profil · {name}", + "undo_import_msg": "Denne enheds data og indstillinger vender tilbage til, hvordan de var før den seneste import ({when}). Cyklusser og ændringer siden da går tabt.", + "undo_import_ok": "Fortryd import", + "undo_import_title": "Fortryd seneste import" }, "msg": { + "custom_task_limit": "Du har nået grænsen på {max} opgaver.", + "custom_tasks_intro": "Tilføj alt andet, du vil mindes om. En opgave forfalder, når et af intervallerne er nået; lad det ene være tomt eller 0 for kun at bruge det andet.", + "maintenance_due_sensor_hint": "Den binære sensor Vedligeholdelse forfalder er tændt, mens en opgave er forfalden, til dine egne notifikationsautomatiseringer.", + "maintenance_supporter_intro": "Maintenance Supporter er en separat integration, der kan meget mere: ethvert apparat eller enhver opgave, intervaller efter tid, brug eller en sensor, notifikationer og en historik. Den kan bruge WashDatas cyklustæller.", "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.", + "pg_preset_delete_confirm": "Slet forudindstillingen \"{name}\"?", + "pg_preset_limit": "Grænsen for forudindstillinger er nået ({n})", + "pg_preset_overwrite": "Overskriv forudindstillingen \"{name}\"?", "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.", - "refresh_catalog_hint": "Fællesskabets lister over mærker og apparater gemmes i cachen, så fællesskabsbutikken holder sig inden for sin daglige grænse. Opdater for at hente bidrag, som andre har tilføjet eller godkendt.", + "store_sibling_hint": "Intet delt til din model? En beslægtet model fra samme mærke er normalt en god start.", + "store_declare_appliance": "Fortæl WashData, hvilket apparat du har, for at se opsætninger, andre har delt til det. Eller skriv et mærke ovenfor for at se dig omkring.", + "refresh_catalog_hint": "Fællesskabets lister over mærker og apparater caches for at spare butikkens daglige budget. Opdater for at se poster, som andre har tilføjet eller godkendt.", "head_trim_hint": "Fjern så mange sekunder fra starten", "appliance_monitor": "Apparat monitor", "artifact_dip_detail": "Faldt under det normale effektbånd i ~{n}s.", "artifact_footer": "Fremhævet på grafen ovenfor. Disse er forbigående artefakter (f.eks. døren åbnet midt i cyklussen), ikke nødvendigvis problemer.", "artifact_header": "{n} uregelmæssigheder opdaget i denne cyklus", - "artifact_pause_detail": "Effekten faldt til næsten nul i ~{n}s og genoptog derefter – sandsynligvis blev døren åbnet midt i cyklussen, eller cyklussen blev sat på pause.", + "artifact_header_one": "{n} uregelmæssighed opdaget i denne cyklus", + "artifact_header_other": "{n} uregelmæssigheder opdaget i denne cyklus", + "artifact_pause_detail": "Effekten faldt til næsten nul i ~{n}s og vendte derefter tilbage. Døren blev sandsynligvis åbnet, eller cyklussen blev sat på pause.", "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 fjern derefter deres mærkning (de bliver i historikken, men påvirker ikke længere denne profil), eller slet dem helt.", + "cleanup_intro": "Alle mærkede cyklusser overlejret. Sæt kryds ved afvigere, og fjern derefter deres mærkning (de bliver i historikken, men former ikke længere denne profil), eller slet dem.", "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)", - "compare_profiles_tip": "Overlæg andre profilkonvolutter på skemaet ovenfor for at se, hvilken der passer bedst til denne cyklus.", + "compare_profiles_tip": "Overlejr andre profiler på grafen for at se, hvilken der passer bedst til denne cyklus.", "compare_selected_cycles": "Valgte cyklusser (fast) – vis/skjul", - "consider_new_profile": "Overvej at oprette en ny profil.", - "coverage_gap": "seneste cyklusser har ingen matchende profil ({pct}% af sidste 30).", - "coverage_gap_similar_cycles": "{count} lignende umærkede cyklusser fundet – opret en profil for at begynde at matche dem.", "cycles_deleted": "{count} cyklus(ser) slettet", + "cycle_context_loading": "Indlæser recorder-historik…", + "cycle_context_note": "Grå: effektsensorens recorder-historik omkring denne cyklus. Den er ikke en del af cyklussen og påvirker ikke varighed, energi eller matchning.", + "cycle_context_none": "Recorderen har ingen målinger omkring denne cyklus (Home Assistant gemmer som standard 10 dages historik).", + "cycle_context_tip": "Kun visning: viser effektsensorens recorder-historik omkring denne cyklus, hvis den stadig findes. For at tilføje mislykkede startforsøg til fremtidige cyklusser skal du bruge Kurveoptakt (Indstillinger > Registrering > Cyklusstart), som ændrer gemte varigheder.", "enough_data": "Nok data til at lære af ({current}/{min} cyklusser).", "export_description": "Vælg præcis hvilke profiler, cyklusser, indstillinger og mere der skal eksporteres til JSON, eller analysér en fil og importér kun de dele, du vil have.", "feedback_cycles_pending": "Til gennemgang: {n}", "feedback_prompt": "Bekræft, at det var rigtigt, ret programmet, eller ignorer.", "feedback_relabel_hint": "At mærke denne cyklus om løser også gennemgangen.", "filter_by_profile": "Filtrer efter profil…", - "group_modal_help": "Gruppér programmer med samme form, der adskiller sig i temperatur/centrifugering (varigheden kan variere). Matching scorer gruppen som én kandidat, og vælger derefter det bedst passende medlem. Vælg mindst 2; overlejringen viser, hvor ens de er.", + "group_modal_help": "Gruppér programmer med samme form, der adskiller sig i temperatur eller centrifugering. Matchningen scorer gruppen som én og vælger derefter det bedst passende medlem. Vælg mindst 2; overlejringen viser, hvor ens de er.", "group_not_cohesive": "Disse profiler er ikke ens nok til at gruppere pålideligt, så matching behandler dem individuelt, indtil du fjerner afvigelsen eller opdeler gruppen.", "group_preview_hint": "Markér 2 eller flere medlemmer for at forhåndsvise og sammenligne deres effektkurver.", - "import_intro": "Indlæs en eksporteret fil, eller indsæt en JSON-nyttelast nedenfor.", - "legacy_actions_title": "{count} ældre tilpasset handling kører stadig", + "legacy_actions_title": "{count} ældre tilpassede handlinger kører stadig", + "legacy_actions_title_one": "{count} ældre tilpasset handling kører stadig", + "legacy_actions_title_other": "{count} ældre tilpassede handlinger kører stadig", "live_chart_loading": "Direkte strømdiagram vises, når målinger ankommer.", "loading": "Indlæser...", "loading_curve": "Indlæser kurve...", "loading_settings": "Indlæser indstillinger...", "log_buffer_hint": "Nyeste først · buffer de sidste 500 ha_washdata-poster siden genstart · træk i den nederste kant for at ændre størrelsen.", - "maintenance_advisory": "Øget varighed/energi kan indikere nødvendig vedligeholdelse af apparatet (f.eks. afkalkning, filterrensning).", "maintenance_due": "Vedligeholdelse forfalder: {items}", - "maintenance_intro": "Log den service, du udfører på dette apparat, og bliv mindet om det, når hver opgave forfalder igen.", + "maintenance_intro": "Log service på dette apparat, og få en påmindelse, når hver opgave forfalder igen.", "maintenance_load_error": "Kunne ikke indlæse vedligeholdelsesdata: {error}", "maintenance_requires_access": "Vedligeholdelse og eksport/import kræver fuld adgang.", - "manual_duration_hint": "Profilens gennemsnitlige/forventede cykluslængde, brugt til resterende tidsberegninger. Rediger for at indstille det; ved at lade den være uændret beholder den aktuelle værdi.", - "matching_tuning_intro": "Ved læring justerer WashData også, hvor meget programtilpasning vejer form kontra varighed og energi.", + "manual_duration_hint": "Profilens forventede cykluslængde, brugt til estimater af resterende tid. Lad den være uændret for at beholde den aktuelle værdi.", "merge_intro": "De valgte cyklusser kombineres til én (kronologisk rækkefølge; huller udfyldt med 0 W). Vælg den resulterende profil.", "ml_intro": "WashData leveres med smarte modeller, der fungerer ud af kassen.", "ml_learned_intro": "Modeller finjusteret til denne maskine.", - "ml_loading": "indlæser ML...", "ml_settings_intro": "To uafhængige kontakter: Den ene anvender modellerne, mens en cyklus kører, den anden lader WashData finjustere dem til din maskine over tid.", - "name_first_program": "Du har nok cyklusser – navngiv dit første program for at begynde matchning.", - "near_duplicate_cluster": "næsten dublet profilklynge fundet. Gruppering gør det muligt for matchning pålideligt at vælge mellem look-alikes (f.eks. samme program ved forskellig temperatur/spin).", "no_cycles_match": "Ingen cyklusser matcher det aktuelle filter.", "no_cycles_profile": "Ingen cyklusser for denne profil.", "no_cycles_selected": "Vælg mindst én cyklus først.", @@ -896,73 +868,52 @@ "no_suggestions": "Ingen aktive forslag.", "notify_services_hint": "Brug {entity}-service-ID'er (kommaseparerede for flere). Skabelonvariabler: {vars}.", "old_actions_warning": "Konfigureret med den gamle handlingseditor (nu fjernet). De skyder stadig på cyklusbegivenheder, men kan ikke længere redigeres her. Konverter dem til en normal automatisering, eller fjern dem.", - "onboarding_progress": "{n} / 3 cyklusser observeret", - "onboarding_watching": "Lad dit apparat køre normalt – WashData holder øje. Efter 3 cyklusser begynder programmatchning.", "pending_feedback": "Afventer detektionsfeedback", "performance_trend": "Præstationstendens ({n} cyklusser)", - "pg_analysis_empty": "Indlæs en cyklus for at se matchanalyse.", "pg_apply_confirm": "Anvend bedste værdi: {label} = {value}?", - "pg_canvas_empty": "Vælg en cyklus ovenfor, og tryk derefter på Indlæs for at se dens effektkurve.", - "pg_chart_caption": "Registrerings-/matchrate på tværs af værdier for {param}", "pg_ctrl_intro": "Værdierne tager udgangspunkt i denne enheds aktive indstillinger i integrationen. Ændringer bliver i legepladsen, indtil du gemmer dem i integrationen.", "pg_live_unavailable": "Aktive indstillinger er ikke tilgængelige, viser standardværdier.", "pg_load_live_confirm": "Kassér {n} ændring(er) i legepladsen, og genindlæs integrationens aktuelle indstillinger?", - "pg_match_rate": "{pct}% matchrate på {total} cyklusser", "pg_matches_live": "Svarer til de aktive indstillinger", "pg_n_changed": "{n} ændret i forhold til de aktive indstillinger", "pg_sugg_none": "Alle forslag stemmer allerede overens med de aktive indstillinger", - "pg_preset_delete_confirm": "Slet forudindstillingen \"{name}\"?", - "pg_preset_limit": "Grænsen for forudindstillinger er nået ({n})", - "pg_preset_overwrite": "Overskriv forudindstillingen \"{name}\"?", "pg_publish_one_confirm": "Gem {label} = {value} i denne enheds indstillinger?", "pg_restart_note": "Genstart Home Assistant for at aktivere simuleringsværktøjerne.", - "pg_sim_progress": "{done} / {total} cyklusser", - "pg_starting": "Starter…", - "pg_stress_synthetic": "inaktiv simulering begynder her", - "pg_sweep_intro": "Hvad hvis {param} var anderledes? Test {steps} værdier på tværs af dine seneste {cycles} cyklusser for at finde indstillingen, hvor flest cyklusser matches korrekt.", - "pg_sweep_step": "Trin {done} / {total}", - "pg_verdict_bad": "Kræver opmærksomhed: mange cyklusser bliver ikke registreret.", - "pg_verdict_good": "Godt indstillet: de fleste cyklusser identificeres og matches korrekt.", - "pg_verdict_ok": "Acceptabelt: nogle cyklusser blev overset. Prøv at sænke starttærsklen.", "phase_catalog_intro": "Navngivne segmenter af en cyklus (Forvask, Opvarmning, Centrifugering…). Tildel dem til en profil fra dens kontrolpanel.", "phase_ranges_intro": "Faseområder (minutter fra cyklusstart) overlejret på gennemsnitskurven. Rediger værdier for at få vist live.", - "playground_intro": "En sikker sandkasse til at afspille tidligere cyklusser med andre indstillinger, sammenligne to opsætninger side om side og undersøge, hvordan en cyklus justeres til en profil. Intet her ændrer dine aktive data, før du udtrykkeligt anvender det.", - "playground_needs_restart": "Genstart Home Assistant for at aktivere Legeplads-værktøjerne.", - "preferences_admin": "Præferencer, panel og adgangskontrol", - "preferences_adv": "Præferencer", + "playground_intro": "Afprøv indstillinger på dine rigtige cyklusdata. Intet her ændrer dine aktive indstillinger, før du anvender det.", "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), genberegn cyklussernes omkostninger ud fra dine registrerede energipriser, og genberegn cyklernes tilstand. Kør dette efter en batch af gennemgange.", + "process_history_hint": "Kør matchning igen på alle gemte cyklusser, opdater forslag, gentræn ML-modeller (hvis aktiveret), genberegn omkostninger ud fra registrerede priser, og genberegn cyklussundhed. Kør det efter en række 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.", - "rbac_hint": "Når den er slået fra, har alle Home Assistant-brugere fuld adgang (standard). Administratorer har altid fuld adgang og kan administrere alle.", - "recent_logs": "Seneste ha_washdata-poster", + "rbac_hint": "Fra: alle Home Assistant-brugere har fuld adgang (standard). Administratorer har altid fuld adgang og kan administrere alle.", "recording_in_progress": "Optagelse i gang", "reminders_intro": "Vis en påmindelse i panelet så mange cyklusser efter sidste service. Lad feltet være tomt eller 0 for at slå en påmindelse fra.", "restart_gap_footer": "Fremhævet på grafen. Strømdata mangler for disse intervaller; matching brugte kun rigtige aflæsninger.", "restart_gap_header": "{n} HA-genstartsgab under denne cyklus", + "restart_gap_header_one": "{n} HA-genstartsgab under denne cyklus", + "restart_gap_header_other": "{n} HA-genstartsgab under denne cyklus", "restart_gap_item": "{dur} gab", - "review_confirm_help": "Bekræft, om denne cyklus blev registreret korrekt. Dine anmeldelser træner modellen på din maskine - jo flere cyklusser du bekræfter, jo bedre matchning og sundhedsscore får du. En hurtig god/dårlig er nok.", + "review_confirm_help": "Blev denne cyklus registreret korrekt? Bekræftede cyklusser forbedrer matchningen for din maskine. En hurtig God/Dårlig er nok.", "review_in_settings": "Gennemgå i Indstillinger", "review_notes_placeholder": "Noter (valgfrit)", "review_notes_tip": "Fritekstnoter til din egen reference. Bruges ikke til match eller træning.", - "review_profile_tip": "Programmet denne cyklus er mærket som. Hvis det auto-detekterede program var forkert, skal du rette det her - mærkning lærer matchning til fremtidige cyklusser.", - "review_quality_tip": "Hvor ren er denne cyklus. Godt = et lærebogseksempel på dette program; Dårlig = opdaget, men støjende eller atypisk; Ubrugelig = fejlopdaget (fusioneret, trunkeret eller falsk). Driver sundhedsscore og hvilke cyklusser der må træne modellen.", - "review_recorded_tip": "Marker dette som en håndplukket referencecyklus for dets program - samme rolle som en manuelt optaget cyklus. Referencecyklusser opbevares altid, danner grundlag for matchskabelonen og droppes aldrig ved oprydning. (Dette er det \"gyldne\"/optagede flag; begge er det samme.)", - "review_tags_tip": "Valgfrie flag, der beskriver, hvad der gik galt med denne cyklus, så træning og oprydning kan forklare det.", - "review_to_cycles": "Åbn køen Cycles review", + "review_profile_tip": "Det program, denne cyklus er mærket som. Hvis det er forkert, så ret det her; mærkninger lærer matchningen.", + "review_quality_tip": "Hvor ren denne cyklus er. God: et lærebogseksempel på dette program. Dårlig: registreret, men støjende eller usædvanlig. Ubrugelig: fejlregistreret (sammenlagt, afkortet eller falsk). Påvirker sundhedsscoren og hvilke cyklusser der træner modellen.", + "review_recorded_tip": "Markér dette som en håndplukket referencecyklus for dets program, som en manuelt optaget cyklus. Referencecyklusser beholdes altid, danner grundlag for matchskabelonen og fjernes aldrig ved oprydning.", + "review_tags_tip": "Valgfrie flag for, hvad der gik galt, så træning og oprydning kan tage højde for det.", + "review_to_cycles": "Åbn gennemgangskøen under Cykler", "samples_decimated": "Viser {shown} af {total} prøver (udtyndet til visning; spidser bevares). Et bredt hul her er udtynding, ikke manglende data.", - "saving_triggers_reload": "Gemning udløser en genindlæsning af integration. HA-enheder kan kortvarigt vises som utilgængelige.", + "saving_triggers_reload": "Når du gemmer, genindlæses integrationen. Entiteter kan kortvarigt vises som utilgængelige.", "search_placeholder": "Søgeindstillinger...", "see_recorder": "Se optager-widget nedenfor", "setting_changed": "Ændret fra {old} til {new} den {date}", "settings_basic_note": "Viser væsentlige indstillinger. Skift til Avanceret for hele listen.", - "shape_drift_advisory": "⚠ Formdrift", - "shape_drift_detail": "Effektmønsteret for denne profil har ændret sig over tid – mulig slid på apparatet eller vedligeholdelse nødvendig (f.eks. afkalkning, filterrensning).", "show_all_settings": "Vis alle indstillinger", - "showing_suggestions": "Viser indstillingen {count} med forslag.", - "split_intro": "Klik på grafen for at tilføje eller fjerne et opdelingspunkt, eller automatisk detektere ved inaktive huller. Hvert resulterende segment kan få sin egen profil.", - "storage_diagnostics": "Lagerstatistik, vedligeholdelse, eksport/import", + "showing_suggestions": "Viser {count} indstillinger med forslag.", + "showing_suggestions_one": "Viser {count} indstilling med forslag.", + "showing_suggestions_other": "Viser {count} indstillinger med forslag.", + "split_intro": "Klik på grafen for at tilføje eller fjerne et opdelingspunkt, eller registrer automatisk ud fra pauser i tomgang. Hver del kan få sin egen profil.", "sug_staged": "Indstillet {key} = {val}. Gem for at anvende.", "toast_auto_detect_enabled": "Automatisk registrering aktiveret", "toast_auto_label_complete": "Automatisk mærkning fuldført", @@ -979,7 +930,6 @@ "toast_no_automation": "Kan ikke oprette automatisering her", "toast_profile_name_required": "Profilnavn er påkrævet", "toast_program_set": "Program valgt: {program}", - "toast_rebuild_failed": "Genopbygning mislykkedes: {error}", "toast_resume_failed": "Genoptagelse mislykkedes: {error}", "toast_revert_failed": "Tilbagerulning mislykkedes: {error}", "toast_reverted": "{key} tilbageført", @@ -990,100 +940,83 @@ "trend_energy_up": "Energitrends op ({pct} %/cyklus) – seneste gennemsnit {avg}", "trim_destructive_confirm": "Dermed bevares kun {pct}% af cyklussen. Dette kan ikke fortrydes. Fortsæt?", "trim_intro": "Træk i de røde håndtag, eller indtast værdier. Alt uden for vinduet fjernes.", - "tuning_suggestions_available": "{count} indstillingsforslag tilgængeligt fra observerede cyklusser. De vises ved siden af de relevante felter.", + "tuning_suggestions_available": "{count} indstillingsforslag tilgængelige fra observerede cyklusser. De vises ved siden af de relevante felter.", + "tuning_suggestions_available_one": "{count} indstillingsforslag tilgængeligt fra observerede cyklusser. Det vises ved siden af det relevante felt.", + "tuning_suggestions_available_other": "{count} indstillingsforslag tilgængelige fra observerede cyklusser. De vises ved siden af de relevante felter.", "unsure_detected_prefix": "WashData er usikker på, om det er registreret", - "updated": "Opdateret", "warmup_badge": "Lærer stadig ({done}/{needed} cyklusser)", - "warmup_detail": "Denne profil har brug for {needed} mærkede cyklusser, før automatisk matchning begynder. Hver bekræftet cyklus hjælper den med at lære.", + "warmup_detail": "Matchning virker allerede. WashData beder dig bekræfte de første {needed} cyklusser for denne profil, før den mærker på egen hånd.", "wipe_history_warning": "Slet alle cyklusser og profiler permanent. Kan ikke fortrydes.", "compare_overlay_tip": "Læg indlærte profilindhylninger oven på hinanden for at se, hvilket program hver cyklus ligner.", - "duration_trend_up_tip": "Varighed stiger ({pct}%/cyklus)", - "duration_trend_down_tip": "Varighed falder ({pct}%/cyklus)", - "energy_trend_up_tip": "Energi stiger ({pct}%/cyklus)", - "energy_trend_down_tip": "Energi falder ({pct}%/cyklus)", - "performance_trending": "Ydelsestendens", - "pg_select_cycle_hint": "Vælg en cyklus og klik på ↺ for at indlæse", - "pg_sweep_progress": "Trin {done} / {total}", - "pg_analysis_hint": "Vælg en cyklus og klik på ↺ for at indlæse analysen.", "diagnostics_load_failed": "Kunne ikke indlæse diagnostik: {error}", - "manual_duration_ref_hint": "Bruges kun, når der ikke er valgt en referencecyklus. En referencecyklus fastsætter varigheden ud fra sin egen længde.", + "manual_duration_ref_hint": "Bruges kun uden en referencecyklus; en referencecyklus fastsætter varigheden ud fra sin egen længde.", + "duration_learned_hint": "Beregnet ud fra programmets cyklusser (uden afvigere) og opdateret efter hver ny cyklus. Vil du ændre den, kan du beskære eller fjerne de cyklusser, der afviger.", "toast_convert_rolled_back": "Konverteringen mislykkedes og blev rullet tilbage (ingen automatisering blev efterladt): {error}", "toast_convert_orphan": "Automatiseringen blev oprettet, men rydningen af de gamle handlinger mislykkedes. Prøv ikke igen: fjern de gamle handlinger manuelt for at undgå en dubleret automatisering.", - "advisory_duration_trend_up": "Cyklusserne for '{name}' bliver gradvist længere (ca. +{pct}% pr. cyklus). Hvis apparatets adfærd har ændret sig, så optag eller genopbyg denne profil.", - "advisory_energy_trend_up": "'{name}' trækker gradvist mere energi (ca. +{pct}% pr. cyklus) - værd at tjekke apparatet, hvis det er uventet.", - "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.", + "advisory_duration_trend_up": "Cyklusserne for '{name}' bliver længere og længere (ca. +{pct}% pr. cyklus). Hvis apparatet har ændret sig, så optag eller genopbyg denne profil.", + "advisory_energy_trend_up": "'{name}' bruger mere energi for hver cyklus (ca. +{pct}% pr. cyklus). Tjek apparatet, hvis det er uventet.", + "advisory_poor_health": "'{name}' passer dårligt: dens seneste cyklusser varierer meget eller matcher svagt. Gennemgå dens cyklusser, eller optag den igen, så matchning og estimater forbliver præcise.", + "advisory_shape_drift": "'{name}' har fjernet sig fra sin oprindelige effektkurve, måske på grund af kalk eller slid. Overvej at optage den igen med nye cyklusser.", + "advisory_shape_drift_corr": "'{name}' har fjernet sig fra sin oprindelige effektkurve (korrelation {corr}), måske på grund af kalk eller slid. Overvej at optage den igen med nye cyklusser.", + "advisory_unmatchable": "'{name}' kan aldrig matches: der ligger ingen cyklus med effektdata bag den. Mærk eller optag en cyklus af dette program for at løse det.", + "advisory_unmatchable_no_duration": "'{name}' kan aldrig matches: dens cyklus har ingen brugbar varighed. Optag dette program igen, eller angiv dets forventede varighed.", "pg_no_alerts": "Ingen problemer registreret i denne kørsel.", "pg_override_active": "Bruger dine redigerede indstillinger mod de nuværende", "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.", "pg_sweep_intro2": "Find den indstilling, der bedst opfylder et mål på tværs af dine seneste cyklusser. Intet ændres, før du anvender det.", - "pg_running_history": "Dine cyklusser afspilles igen…", - "pg_running_sweep": "Værdier testes på tværs af dine cyklusser…", "pg_row_load_hint": "Indlæs denne cyklus i grafen ovenfor", - "pg_history_intro2": "Afspil dine seneste cyklusser igennem den rigtige detektor og genkendelse med indstillingerne ovenfor. Klik på en række for at indlæse den cyklus i grafen; rediger en indstilling for at se en før/efter-sammenligning.", + "pg_history_intro2": "Afspil seneste cyklusser gennem den rigtige detektor og matcher med indstillingerne ovenfor. Klik på en række for at indlæse den i grafen; skift en indstilling for at sammenligne før og efter.", "pg_simulating": "Simulerer cyklus…", - "pg_canvas_empty2": "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 for at zoome, og træk for at panorere.", "pg_apply_settings_confirm": "Gem disse {n} indstilling(er) på denne enhed? {list}", - "pg_stress_terminated_detail": "Stabiliserede sig på ~{idle}W inaktiv -- cyklussen sluttede {h} t {m} min senere via {reason}.", - "pg_stress_above_threshold_detail": "Standbyforbruget på ~{idle}W er på eller over den effektive stoptærskel ({stop}W) -- cyklussen blev aldrig registreret som inaktiv. Hæv stop_threshold_w for at rette dette.", - "pg_stress_hit_cap_detail": "Cyklussen kørte {h} t {m} min uden at stoppe -- tvangsstoppet af sikkerhedsgrænsen. Standbyforbrug {idle}W mod stoptærskel {stop}W.", "no_logs_match": "Ingen logposter matcher disse filtre.", "pg_analysis_hint2": "Vælg en cyklus, og tryk på Kør for at indlæse matchanalysen.", - "pg_analysis_empty2": "Tryk på Kør for at se matchanalysen.", - "imported_readonly": "Importeret fra fællesskabsbutikken. Vises til reference og matchning. Tælles ikke med i din statistik og kan ikke redigeres.", + "imported_readonly": "Importeret fra fællesskabsbutikken. Bruges til reference og matchning, tælles ikke med i din statistik og kan ikke redigeres.", "merge_no_imports": "Importerede optagelser kan ikke flettes ind i en rigtig cyklus. Fravælg dem for at flette.", - "store_enable_hint": "Aktivér onlinefunktioner i Indstillinger for at gennemse og importere fællesskabets referencecyklusser.", - "advisory_duration_outlier": "{n} cyklus(ser) mærket '{name}' ligger for langt fra programmets sædvanlige længde til nogensinde at kunne matche det (værst: {ratio}x). De er sandsynligvis mærket forkert, eller én optagelse har fanget to kørsler. Mærk dem om, eller del dem op, så de ikke længere skævvrider tidsestimatet for dette program.", + "store_enable_hint": "Slå onlinefunktioner til under Præferencer > Online & fællesskab for at gennemse og importere fællesskabets referencecyklusser.", + "advisory_duration_outlier": "{n} cyklus(ser) mærket '{name}' ligger for langt fra dens sædvanlige længde til nogensinde at matche (værst: {ratio}x). De er sandsynligvis fejlmærket eller indeholder to kørsler. Mærk dem igen, eller opdel dem, så de ikke længere skævvrider tidsestimatet.", "standby_above_stop_title": "Trak stadig {idle} W, da cyklussen sluttede", "standby_above_stop_sub": "{n} af de sidste {total} cyklusser. Slukkeforsinkelsen starter først, når effekten er under Stoptærskel ({stop} W), så cyklusser slutter for sent eller skal stoppes manuelt.", - "online_intro": "Del referencecyklusser med andre WashData-brugere, og download dem. Alle onlinefunktioner er valgfrie og slået fra som standard.", "store_share_intro": "Upload denne referencecyklus, så andre med samme apparat kan bruge den. Den gennemgås, før den vises offentligt.", - "online_intro_global": "Gennemse og del referenceoptagelser med andre WashData-brugere, og bekræft apparatposter. Én forbindelse gælder for hele din WashData-integration; apparatets mærke og model angives pr. enhed under Grundlæggende. Alle onlinefunktioner er valgfrie og slået fra som standard.", + "online_intro_global": "Gennemse og del referenceoptagelser med andre WashData-brugere. Én forbindelse dækker alle dine WashData-enheder; mærke og model angives pr. enhed under Grundlæggende. Onlinefunktioner er slået fra, indtil du slår dem til.", "online_unavailable": "Onlinefunktioner er ikke tilgængelige på denne server.", "store_picker_offline": "Aktivér onlinefunktioner i indstillingstandhjulet for at vælge fra fællesskabskataloget.", "pick_brand_first": "Vælg først et apparatmærke.", - "model_not_found": "Ikke i kataloget endnu.", - "connect_to_confirm": "Opret forbindelse i indstillingstandhjulet for at bekræfte eller bedømme.", "adopt_settings_hint": "Overskriver dette apparats detektions- og matchgrænser med de delte. Dine notifikationer, entiteter og energipris ændres aldrig.", - "include_settings_hint": "Deler dette apparats detektions- og matchgrænser (ikke dine notifikationer, entiteter eller energipris). Brugerne vælger selv, om de vil anvende dem.", + "include_settings_hint": "Del denne enheds registrerings- og matchtærskler (ikke notifikationer, entiteter eller energipris). Andre vælger selv, om de vil anvende dem.", "onboard_download": "Nyt apparat? Overtag en klar opsætning (programmer, referencecyklusser og faser) fra en anden WashData-bruger med samme apparat.", - "share_consent": "Jeg bekræfter, at disse cyklusser er gennemført normalt uden afbrydelse", "share_device_none": "Ingen delbare cyklusser endnu. Markér først en optaget eller håndplukket cyklus som referencecyklus (⭐) under fanen Cyklusser.", "share_guideline_naming": "Navngiv hver profil præcis som vist på apparatets drejeknap eller display (f.eks. 'Cotton 40', 'Eco 60').", - "share_guideline_quality": "Del kun cyklusser, der er gennemført normalt -- ingen afbrydelser midt i cyklen, åbning af dør eller strømudfald.", + "share_guideline_quality": "Del kun cyklusser, der sluttede normalt: ingen afbrydelser, døråbninger eller strømudfald.", "share_guideline_review": "Din upload starter som afventende og vises offentligt, når nok fællesskabsmedlemmer bekræfter den.", "share_guidelines_title": "Inden du deler", - "store_download_device_intro": "Overtag alle delte programmer og deres referencecyklusser til dit apparat. Dine egne optagede cyklusser og statistik påvirkes ikke.", + "store_download_device_intro": "Tilføj alle delte programmer og deres referencecyklusser til din enhed. Dine egne cyklusser og din statistik påvirkes ikke.", "store_share_device_intro": "Upload {brand} {model} med de referencecyklusser, du vælger. Andre med samme apparat kan overtage dine programmer. Bidrag gennemgås inden offentliggørelse.", "share_profile_no_cycles": "Ingen referencecyklusser -- markér en cyklus som ⭐ under fanen Cyklusser for at inkludere denne profil", - "advisory_phase_inconsistent": "'{name}' ser ud til at blande forskellige programmer eller temperaturer - dens cyklusser varmer op i meget forskellige tidsrum. Hvis du deler den op i separate profiler (f.eks. pr. temperatur), forbedres matchningen og tidsestimaterne.", - "advisory_phase_inconsistent_title": "⚠ Muligvis blandede programmer", - "export_select_intro": "Markér præcis hvad der skal inkluderes. At vælge profiler uden deres cyklusser eksporterer stadig et genkendeligt program (den lærte form følger med).", - "import_analyze_hint": "Indlæs en eksporteret fil (eller indsæt dens JSON). WashData analyserer den og viser præcis hvad der kan importeres, før noget ændres.", + "export_select_intro": "Sæt kryds ved det, der skal med. Profiler uden deres cyklusser eksporterer stadig et program, der kan matches.", + "import_analyze_hint": "Indlæs en eksporteret fil (eller indsæt dens JSON). WashData viser, hvad der kan importeres, før noget ændres.", "analyzing": "Analyserer…", - "device_type_mismatch_warn": "Denne eksport er fra en anden apparattype ({src} vs {local}). Programmer og cyklusser kan stadig importeres som referencedata, men apparatspecifikke indstillinger og import af rigtig historik er deaktiveret.", + "device_type_mismatch_warn": "Denne eksport er fra en anden apparattype ({src} vs {local}). Programmer og cyklusser kan stadig importeres som referencedata, men ikke enhedsindstillinger eller rigtig historik.", "merge_hint": "Importerede elementer tilføjes; intet lokalt går tabt. Navnekonflikter løses nedenfor.", "replace_warn": "Hver markeret kategori slettes og erstattes fra filen. Ikke-markerede kategorier forbliver uændrede.", "dest_reference_hint": "Importerede cyklusser forbedrer kun programgenkendelse og påvirker aldrig forbrugs-/energistatistik.", - "dest_real_history_hint": "Importerede cyklusser tæller som dette apparats egen historik og indgår i energi-/forbrugsstatistik. Brug dette til at flytte ét apparat til en ny installation.", + "dest_real_history_hint": "Importerede cyklusser tæller som dette apparats egen historik og statistik. Brug dette til at flytte et apparat til en ny installation.", "sug_muted": "Vil ikke foreslå denne indstilling igen", "sug_mute_failed": "Kunne ikke undertrykke forslaget", "sug_unmuted_all": "Undertrykte forslag nulstillet", - "n_suggestions_muted": "{count} undertrykt; autotuner vil ikke foreslå disse.", + "n_suggestions_muted": "{count} forslag undertrykt; autotuneren vil ikke foreslå disse.", + "n_suggestions_muted_one": "{count} forslag undertrykt; autotuneren vil ikke foreslå det.", + "n_suggestions_muted_other": "{count} forslag undertrykt; autotuneren vil ikke foreslå disse.", "font_size_hint": "Gør alt i dette panel større eller mindre. Gælder for din konto på denne enhed.", - "import_history_description": "Havde du allerede et smartstik, før du fik WashData? Upload en historikeksport fra dets effektsensor, eller lad den blive læst direkte fra Home Assistant, og den normale registrering kører hen over den, så tidligere cyklusser dukker op i din Cykler-liste, klar til at få navn.", - "hist_input_hint": "Upload en CSV-fil hentet fra Historik-panelet (entitet, tilstand, sidst ændret), eller lad WashData læse sensorens historik direkte. Registreringen kører derefter hen over den præcis som live, og du vælger, hvilke af de fundne cyklusser du vil beholde.", - "hist_recorder_hint": "Læser fra den dato, du vælger, og frem til nu. Home Assistant gemmer som standard detaljeret historik i 10 dage og derefter kun timegennemsnit, som er for grove at registrere cyklusser ud fra - vælg kun en dato længere tilbage, hvis din recorder er sat til at gemme mere.", - "hist_scanning": "Din historik spilles igennem registreringen. Dette kører i baggrunden - du kan lukke denne dialog og vende tilbage til den senere.", + "import_history_description": "Havde du et smartstik før WashData? Upload dets effekthistorik, eller læs den fra Home Assistant. Registreringen kører hen over den, og tidligere cyklusser vises i din Cykler-liste, klar til at blive navngivet.", + "hist_input_hint": "Upload en CSV fra Historik-panelet (entitet, tilstand, sidst ændret), eller lad WashData læse sensorens historik direkte. Live-registreringen kører hen over den, og du vælger, hvilke cyklusser der skal beholdes.", + "hist_recorder_hint": "Læser fra den valgte dato og frem til nu. Home Assistant gemmer som standard detaljeret historik i 10 dage; ældre timegennemsnit er for grove til cyklusregistrering.", + "hist_scanning": "Din historik køres gennem detektoren i baggrunden. Du kan lukke denne dialog og vende tilbage senere.", "hist_imported_count": "{n} cyklusser importeret.", "hist_duplicates": "{n} var allerede importeret og blev sprunget over.", "hist_capped": "Grænsen pr. enhed for importerede cyklusser blev nået; resten blev ikke gemt.", - "hist_next_step": "De ligger i din Cykler-liste, mærket som importeret historik. Åbn en, og brug Mærk til at give det tilhørende program navn.", + "hist_next_step": "De ligger i din Cykler-liste, mærket som importeret historik. Åbn en, og brug Mærk til at navngive dens program.", "hist_rows_read": "{n} aflæsninger læst", "hist_entity_substituted": "læste {used} (denne enhed er konfigureret til {wanted})", "hist_breaks": "{n} huller hvor sensoren var utilgængelig", @@ -1097,55 +1030,86 @@ "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.", - "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.", + "imported_history_readonly": "Fundet i importeret effekthistorik. Den hjælper matchningen, men tælles ikke med i din statistik og kan ikke beskæres eller opdeles. Mærk den for at navngive programmet.", + "odometer_intro": "Denne total stiger kun. Sletning af en cyklus sænker den ikke, så servicepåmindelser forbliver korrekte.", + "odometer_correct_hint": "Angiv dette, hvis WashData har talt en kørsel, der aldrig fandt sted, eller apparatet kørte 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}", "toast_range_required": "Angiv et gyldigt Fra/Til-interval.", - "no_profiles_option": "(ingen profiler endnu, tilføj en)" + "no_profiles_option": "(ingen profiler endnu, tilføj en)", + "compare_cycles_title": "Sammenlign {count} cyklusser", + "edit_builtin_phase": "Dette er en indbygget fase. Når du gemmer, oprettes en brugerdefineret kopi; slet kopien for at gendanne originalen.", + "fetch_error": "Kunne ikke indlæse data.", + "connection_lost": "Forbindelsen blev afbrudt - viser data fra {time}.", + "pg_notify_partial": "Markører viser kun notifikationer for start, næsten færdig, færdig, milepæle og stilletimer. Live-opdateringer, aflæsningspåmindelser og advarsler om overskridelse simuleres ikke.", + "connect_to_confirm": "Opret forbindelse i indstillingstandhjulet for at bekræfte eller bedømme.", + "coverage_gap_cluster": "{count} nylige umærkede cyklusser på omkring {min} min ligner ét program, som du endnu ikke har oprettet.", + "pg_label_yes": "Ville blive mærket \"{name}\" ved cyklussens afslutning", + "pg_label_no": "Ville bede om gennemgang: {reason}", + "live_match_debug_tip": "Tillid: hvor tæt den aktuelle effektkurve matcher den bedste kandidatprofil (0-100%). Tvetydig: de to bedste kandidater scorer inden for 5% af hinanden - etiketten er usikker, indtil cyklussen er færdig.", + "automation_alias_started": "{device}: cyklus startet", + "automation_alias_finished": "{device}: cyklus færdig", + "automation_desc_started": "Kører, når en {device}-cyklus starter. Tilføj dine handlinger (notifikation, lys, ...).", + "automation_desc_finished": "Kører, når en {device}-cyklus er færdig. Tilføj dine handlinger (notifikation, lys, ...).", + "feedback_detected": "WashData registrerede: {name}", + "pg_outcome_stale": "Indstillingerne er ændret, siden denne simulering kørte. Den viser stadig det gamle resultat.", + "pg_compare_profile_tip": "Vælger kun den profil, der tegnes oven på grafen. Simuleringen genkender altid cyklussen på egen hånd, ligesom integrationen gør under drift.", + "pg_task_busy": "En kørsel af Test på historik eller Optimér er allerede i gang for denne enhed. Vent på den, eller annuller den.", + "pg_sweep_guarded": "Afslutter flere cyklusser for tidligt, opdeler flere af dem eller overser cyklusser, som den nuværende indstilling fanger, så den anbefales aldrig.", + "pg_sweep_n_cycles": "over {count} cyklusser", + "pg_sweep_n_cycles_one": "over {count} cyklus", + "pg_sweep_n_cycles_other": "over {count} cyklusser", + "pg_sweep_keep_current": "Behold den nuværende værdi ({value}, {metric}): ingen værdi klarede sig bedre med mindst én cyklus.", + "hist_truncated": "stoppede ved grænsen på {n} aflæsninger, så resten af historikken blev ikke gennemgået", + "hist_warn_kw": "Den højeste måling er kun {w}. Denne sensor ser ud til at rapportere kilowatt; WashData har brug for watt, så ingen cyklusser kan registreres.", + "hist_warn_naive_time": "{n} tidsstempler har ingen tidszone og blev læst som UTC. Hvis filen bruger lokal tid, er cyklusser forskudt med din UTC-forskydning, og målinger omkring et sommertidsskift kan gå tabt.", + "hist_warn_naive_time_one": "{n} tidsstempel har ingen tidszone og blev læst som UTC. Hvis filen bruger lokal tid, er cyklusser forskudt med din UTC-forskydning, og målinger omkring et sommertidsskift kan gå tabt.", + "hist_warn_naive_time_other": "{n} tidsstempler har ingen tidszone og blev læst som UTC. Hvis filen bruger lokal tid, er cyklusser forskudt med din UTC-forskydning, og målinger omkring et sommertidsskift kan gå tabt.", + "hist_tail_trimmed": "{min} min ventetid efter cyklussens afslutning blev skåret fra, ligesom integrationen gør under drift", + "invalid_threshold_pair": "Stoptærskel ({stop} W) skal være under Starttærskel ({start} W). Intet blev gemt; ændr begge sammen.", + "import_undo_hint": "Et gendannelsespunkt fra før den seneste import ({when}) er bevaret: {profiles} profil(er) og {cycles} cyklus(ser).", + "replace_undo_hint": "Der gemmes først et gendannelsespunkt, så du kan fortryde denne import under Eksport/import.", + "pg_in_sample_note": "Disse cyklusser var med til at bygge de profiler, de matches mod, så resultaterne er optimistiske: forvent lidt færre korrekte match på nye cyklusser." }, "pg_desc": { "dishwasher_end_spike_quiet_release": "Opvaskemaskine: stille sekunder efter den forventede varighed, før ventetiden på udpumpning ved cyklusslut frigives", - "smart_termination_duration_ratio": "Andel af det genkendte programs forventede varighed, som en cyklus skal nå, før smart afslutning må afslutte den tidligt; sænk den for belastnings- eller temperaturafhængige maskiner", + "smart_termination_duration_ratio": "Andel af programmets gennemsnitlige varighed, som en cyklus skal nå, før smart afslutning må afslutte den tidligt; sænk den, hvis køretiderne varierer", "completion_min_seconds": "Korteste kørsel, der tæller som en rigtig cyklus", - "end_repeat_count": "Lave målinger i træk før afslutning", "interrupted_min_seconds": "Korte cyklusser markeres som afbrudt", "min_off_gap": "Mellemrum, der kræves for at adskille to cyklusser", "off_delay": "Sekunder med lav effekt, før cyklussen slutter", "start_duration_threshold": "Sekunder over tærsklen for at bekræfte start", "start_threshold_w": "Minimum watt for at tælle som startet", "stop_threshold_w": "Under dette tælles maskinen som slukket", - "dtw_bandwidth": "Trin 3: Sakoe-Chiba-tidsforvrængningsband (0 = DTW slukket; standard 0,2 = 20% af cykellængde)", - "corr_weight": "Trin 2: balance mellem kurveform (korrelation) og effektniveau (MAE); standard 0,45", - "duration_weight": "Trin 4: hvor stærkt varighedsoverensstemmelse påvirker slutscoren (standard 0,22)", - "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,8 (180%)", - "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)", - "dtw_refine_top_n": "Trin 3: kandidater DTW gen-scorer; hæv til 7-9 hvis korrekt profil placerer sig 4.-5. (standard 5)", - "duration_scale": "Trin 4: log-forhold der varighedsoverensstemmelse halveres; lavere = strengere straf (standard 0,175)", - "energy_scale": "Trin 4: log-forhold der energioverensstemmelse halveres; lavere = strengere straf (standard 0,25)", + "profile_match_min_duration_ratio": "Korteste cykluslængde som andel af profilens varighed; standard 0,1 (10%)", + "profile_match_max_duration_ratio": "Længste cykluslængde som andel af profilens varighed; standard 1,8 (180%)", "anti_wrinkle_idle_timeout": "Tilladt stilletid mellem to tumle-pulser, før anti-rynke slutter", "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_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", + "anti_wrinkle_exit_power": "Stille niveau mellem tumle-pulser; virker kun over Stoptærsklen", + "anti_crease_finalize_ratio": "Andel af programmets gennemsnitlige varighed, som en cyklus skal nå, før anti-rynke-efterløbet må afslutte den; sænk den på tørretumblere, lad vaskemaskiner stå på standard", "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.", "anti_wrinkle": "Periodisk tumbling efter cyklus for at reducere rynker.", + "baking": "Varmelegeme ved høj temperatur aktivt for at danne skorpe og krumme.", + "cooking": "Hovedtilberedning med varmelegeme og blæser aktive.", "cool_down": "Tumling uden varme nær cyklusslut.", + "cool_down_af": "Afkøling kun med blæser efter opvarmningen.", "drain_and_switch": "Overgangsperiode fra vask til tørretilstand.", "dry": "Tørretrin med varmelegeme og/eller restvarme.", "drying": "Hovedopvarmet tumleperiode.", "heat_up": "Indledende opvarmning af varmeapparatet, før fuld tørring begynder.", + "keep_warm": "Lav varme for at holde maden varm.", + "keep_warm_bm": "Lav varme for at holde brødet varmt efter bagningen.", + "kneading": "Motoren blander og ælter dejen. Højt strømforbrug.", + "pause_af": "Kort pause til at ryste eller tjekke maden.", + "pre_heat": "Indledende opvarmning af kammeret før selve tilberedningen.", "pre_rinse": "Indledende spray ned før vaskemiddel vask.", + "proving": "Hævning ved lav varme, så gæren kan arbejde og dejen hæve.", + "resting": "Kort pause ved lav effekt mellem æltningerne, så glutenen slapper af.", "pre_wash": "Indledende iblødsætning eller forbehandling før hovedvasken.", "rinse": "Renvandsskylningstrin. Denne fase kan gentages flere gange.", "sanitize": "Rengøringstrin ved høj temperatur til desinfektionsprogrammer.", @@ -1158,6 +1122,7 @@ "wash_dw": "Hovedvaskemiddel med varme." }, "placeholder": { + "custom_task": "f.eks. rengør dørpakning", "add": "tilføj…", "group_name": "f.eks. Bomuld 2:47", "json_buttons": "[{\"action\":\"ID\",\"title\":\"Label\"}]", @@ -1185,10 +1150,10 @@ "section": { "dishwasher": { "label": "Opvaskemaskine", - "intro": "Håndtering af cyklusafslutning for opvaskemaskiner, der typisk slutter med en lang, næsten lydløs tørrefase før en kort sidste udpumpning." + "intro": "Sluthåndtering for opvaskemaskiner, som ofte tørrer stille længe før en kort sidste udpumpning." }, "anti_wrinkle": { - "intro": "Anti-rynke-tilstand registrerer tumble-pulser med lav effekt efter den vigtigste fase og holder dem tilknyttet den afsluttede cyklus.", + "intro": "Holder en tørretumblers tumlepulser med lav effekt efter hovedfasen sammen med den færdige cyklus.", "label": "Anti-rynke" }, "basic": { @@ -1196,7 +1161,7 @@ "label": "Grundlæggende" }, "delay": { - "intro": "Registrering af forsinket start identificerer, hvornår et apparat er tændt, men endnu ikke har påbegyndt sin cyklus.", + "intro": "Registrerer et apparat, der er tændt og venter på en forsinket start.", "label": "Forsinket start" }, "detection": { @@ -1220,161 +1185,153 @@ "label": "Timing og Watchdog" }, "triggers": { - "intro": "Valgfrie eksterne signaler: en slutudløser, en dørsensor, en pausekontakt og aflæsningspåmindelsen.", + "intro": "Valgfrie ekstrafunktioner: slutudløser, dørsensor, pausekontakt og aflæsningspåmindelse.", "label": "Udløsere og døre" - }, - "phase_eta": { - "label": "Resterende tid", - "intro": "Fasebevidst resterende tid, til maskiner hvis cykluslængde afhænger af temperatur eller centrifugering." } }, "setting": { "dishwasher_end_spike_quiet_release": { "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." + "doc": "Efter den forventede varighed: hvor længe opvaskemaskinen skal blive under Stoptærskel, før WashData holder op med at vente på den sidste udpumpning. Hæv den, hvis en sen udpumpning efter lang tørring overses. Ventetiden er aldrig over 30 minutter." }, "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." + "doc": "Andel af programmets gennemsnitlige varighed, som en cyklus skal nå, før dens tumling med lav effekt tæller som anti-rynke-efterløbet. Sænk den (f.eks. 0,75) på tørretumblere med fugtsensor. Lad vaskemaskiner stå på standard (0,98), ellers kan en vask slutte halvvejs." }, "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.", + "doc": "Hold en tørretumblers korte tumlepulser med lav effekt efter hovedvarmefasen sammen med den færdige cyklus i stedet for at læse dem som nye cyklusser.", "label": "Aktivér anti-rynke registrering" }, "anti_wrinkle_exit_power": { - "doc": "Strømmen skal falde under dette mellem pulserne for at anti-rynketilstand forbliver aktiv.", + "doc": "I anti-rynke-tilstand tæller effekt under dette som stille mellem tumle-pulser, og stilhed længere end Maks. pulspause afslutter tilstanden. Stoptærsklen bruges, når den er højere, så dette ændrer kun noget, når det sættes over Stoptærsklen.", "label": "Udgangstærskel for effekt" }, "anti_wrinkle_idle_timeout": { - "doc": "Hvor længe maskinen må forblive stille mellem to tumle-pulser, før anti-rynketilstand slutter. Sæt den højere end den længste pause, din tørretumbler efterlader mellem to pulser, ellers læses hver efterfølgende puls som en falsk start.", + "doc": "Hvor længe tørretumbleren må være stille mellem tumlepulser, før anti-rynke-tilstand slutter. Sæt den over din tørretumblers længste pause, ellers læses senere pulser som falske starter.", "label": "Maks. pulspause" }, "anti_wrinkle_max_duration": { - "doc": "Pulser længere end dette behandles som en ægte cyklus snarere end en anti-rynke tumbling.", + "doc": "En længere puls tæller som en rigtig cyklus, ikke som anti-rynke-tumling.", "label": "Maks. varighed" }, "anti_wrinkle_max_power": { - "doc": "En puls over denne effekt behandles som en rigtig ny cyklus, ikke en anti-rynke tumbling. Indstil lige over tumle-pulseffekten.", + "doc": "En puls over denne effekt tæller som en rigtig ny cyklus, ikke som tumling. Sæt den lige over effekten af dine tumlepulser.", "label": "Maks. anti-rynkeeffekt" }, "auto_label_confidence": { - "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.", + "doc": "Matchscore ved cyklusslut, ved eller over hvilken programmet mærkes uden at spørge. Hæv den for mere sikkerhed; sænk den for at mærke flere cyklusser automatisk. Hold den over Læringskonfidens.", "label": "Auto-mærkning konfidens" }, "auto_maintenance": { - "doc": "Kør natlig rengøring: genopbyg profilkonvolutter, genberegn cyklus-sundhed, beskær fejlretningsspor og behold de seneste cyklusser.", + "doc": "Natlig oprydning: mærk gamle cyklusser, der nu tydeligt matcher et program, genopbyg programkurver, opdater anomalimarkeringer og cyklussundhed, og fjern fejlretningsspor, hvis Gem fejlretningsspor er slået fra. Cyklusser og deres effektspor beholdes altid.", "label": "Automatisk vedligeholdelse (natlig oprydning)" }, "completion_min_seconds": { - "doc": "Cykler, der er kortere end dette, kasseres som spøgelsescyklusser (testkørsler, åbning af døren for at tilføje en sok).", + "doc": "Cyklusser, der er kortere end dette (testkørsler, åbning af døren for at lægge en sok i), gemmes som Afbrudt og får ingen underretning om afslutning.", "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." + "doc": "Tilføjer målinger fra mislykkede startforsøg forrest i en cyklus, til maskiner der prøver sig frem før start (dørlås, første vandpåfyldning). 300 s dækker de fleste; 0 = fra. Registrerede starter rykker tidligere, så lærte varigheder glider en tid." }, "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.", + "doc": "Effekten skal blive i standby-båndet så længe, før apparatet tæller som ventende på start.", "label": "Bekræftelsessekunder" }, "delay_start_detect_enabled": { - "doc": "Registrer, når apparatet er tændt og venter (forsinket start/standby), men ikke har startet sin cyklus, så standby-tegning forveksles ikke med en kørende cyklus.", + "doc": "Registrer et apparat, der er tændt og venter på en forsinket start, så dets standbyforbrug ikke forveksles med en kørende cyklus.", "label": "Aktivér registrering af forsinket start" }, "delay_timeout_hours": { - "doc": "Stop med at vente i forsinket starttilstand efter disse mange timer, og vend tilbage til tomgang, så en maskine, der forbliver tændt, men aldrig startet, ikke venter for evigt.", + "doc": "Opgiv at vente på en forsinket start efter så mange timer, og vend tilbage til tomgang.", "label": "Timeout-timer" }, "device_type": { - "doc": "Apparat klasse. Indstiller fornuftige detekteringsstandarder (tærskelværdier, off-forsinkelse, sluthåndtering) indstillet til den pågældende apparattype; ændre det kun, hvis enheden oprindeligt var konfigureret som den forkerte type.", + "doc": "Indstiller registreringsstandarder (tærskler, slukkeforsinkelse, sluthåndtering) for denne type apparat. Skift den kun, hvis enheden blev opsat som den forkerte type.", "label": "Enhedstype" }, "door_sensor_entity": { - "doc": "Valgfri dør binær sensor. Bruges til at registrere, når apparatet er blevet åbnet/tømt efter en cyklus.", + "doc": "Valgfri dørsensor. Viser, hvornår apparatet blev åbnet for at blive tømt efter en cyklus.", "label": "Dørsensorenhed" }, "duration_tolerance": { - "doc": "Tolerance for resterende tidsberegninger (indlæringsfeedback, ikke matchende). Hvis den faktiske varighed er inden for +/-X% af estimatet, tæller det som et godt match.", + "doc": "Kun læringsfeedback: en cyklus inden for +/-X% af sin estimerede varighed tæller som et godt estimat. Påvirker ikke matchning.", "label": "Estimeringstolerance" }, "enable_ml_models": { - "doc": "Mens en cyklus kører, lad modellerne forfine resultaterne i realtid: et mere stabilt estimat for tilbageværende tid og energi/omkostningsestimat og en anti-prematur-stop-vagt ved slutdetektering (det kan kun forsinke en finish, aldrig afslutte en tidligt og er afgrænset). Bruger dine finjusterede modeller, når de er tilgængelige, ellers de indbyggede. Off = kun den klassiske strømbaserede logik (stadig pålidelig).", + "doc": "Brug en model trænet på denne maskine til at forudsige en kørende cyklus' energi og omkostning, og lad et velkendt program, der stopper usædvanligt tidligt, slutte uden den fulde slukkeforsinkelse. Fra: kun effektbaseret logik.", "label": "Anvend smarte modeller under en cyklus" }, "end_energy_threshold": { - "doc": "Under nedtællingen af slukket forsinkelse sammenlignes akkumuleret energi (watt x tid) med denne tærskel. Hvis det overskrides, nulstilles nedtællingen - ved at holde anti-krøl tumblere og opvaskemaskinens tørrehaler fastgjort til cyklussen i stedet for at afkorte dem. Hæv den, hvis cyklusser slutter for tidligt under nedkøling; sænk den, hvis detektionen er træg.", + "doc": "Energi, der tillades under slukkeforsinkelsen. Mere end dette, og cyklussen fortsætter, så tumlinger og tørrehaler forbliver en del af den. Sænk den, hvis cyklusser slutter under en hale; hæv den, hvis færdige cyklusser bliver ved med at vente.", "label": "Slutenergi" }, - "end_repeat_count": { - "doc": "Antallet af på hinanden følgende aflæsninger under stop-tærskel, der kræves, før cyklussen slutter. 1 er fint til de fleste stik. Hæv til 2-3, hvis dit smartstik lejlighedsvis rapporterer en falsk nul-prøve midt i cyklussen, og dine cyklusser slutter for tidligt.", - "label": "Slutgentagelsesantal" - }, "energy_price_entity": { - "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.", + "doc": "Sensor med den aktuelle pris pr. kWh (f.eks. en dynamisk tarif). Den har forrang over den statiske pris nedenfor, så den skal være en pris, ikke stikkets effekt- eller energisensor.", "label": "Energiprisenhed" }, "energy_price_static": { - "doc": "Fast pris pr. kWh brugt til omkostningstal, når der ikke er angivet en levende prisenhed ovenfor.", + "doc": "Fast pris pr. kWh, brugt når der ikke er angivet en prisenhed.", "label": "Statisk energipris (per kWh)" }, "energy_sensor": { - "doc": "Valgfri kumulativ energitæller (total_increasing i kWh/Wh, f.eks. stikkets egen levetidsmåler). Når den er angivet, tages hver cyklus' rapporterede energi fra denne tællers forskel mellem start og slut, hvilket undgår den undertælling, du får ved at integrere en langsomt rapporterende effektsensor. Falder tilbage til den integrerede værdi, hvis aflæsningen mangler, dens enhed er ukendt, eller forskellen ikke er positiv. Lad feltet stå tomt for at fortsætte med at integrere effektsensoren.", + "doc": "Valgfri kWh- eller Wh-tæller (total_increasing, f.eks. stikkets egen måler). Cyklussens energi læses så fra den, hvilket er mere præcist end at beregne den ud fra en langsom effektsensor. Hvis dens måling er ubrugelig, bruges den beregnede værdi.", "label": "Energimålerenhed" }, "expose_debug_entities": { - "doc": "Udgiv ekstra diagnostiske HA-enheder (match konfidens, tvetydighed, interne tilstande). Fra holder enhedslisten ren til normal brug.", + "doc": "Tilføj diagnostiske entiteter (matchkonfidens, tvetydighed, intern tilstand). Fra holder entitetslisten ren.", "label": "Vis fejlretningsenheder" }, "external_end_trigger": { - "doc": "Binær sensor, hvis tilstandsændring markerer cyklussens afslutning (f.eks. en apparat \"færdig\" kontakt eller en ledsagende integration).", + "doc": "Binær sensor, hvis ændring markerer cyklussens afslutning (f.eks. en \"færdig\"-kontakt eller en anden integration).", "label": "Ekstern udløserenhed" }, "external_end_trigger_enabled": { - "doc": "Lad en ekstern binær sensor signalere slutningen af en cyklus, ud over den indbyggede strømbaserede detektering.", + "doc": "Lad en binær sensor signalere en cyklus' afslutning ud over den effektbaserede registrering.", "label": "Aktivér ekstern slutudløser" }, "external_end_trigger_inverted": { - "doc": "Behandl triggersensoren, der slukker (i stedet for ON), som slutsignalet.", + "doc": "Behandl, at sensoren slukker (ikke tænder), som slutsignalet.", "label": "Inverter ekstern udløser (udløs ved SLUK)" }, "learning_confidence": { - "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.", + "doc": "Laveste matchscore for at mærke en færdig cyklus. En klar vinder mærkes; et tæt løb går til gennemgangskøen under Cykler. Under denne værdi mærkes eller spørges der ikke. Hold den under Auto-mærkning konfidens.", "label": "Læringskonfidens" }, "linked_device": { - "doc": "Indlejr eventuelt denne WashData-enhed under en anden enhed (f.eks. smartstikket) i HA-enhedsregistret, vist som \"Forbundet via ...\".", + "doc": "Valgfri. Vis denne WashData-enhed under en anden enhed (f.eks. smartstikket) i Home Assistant som \"Forbundet via ...\".", "label": "Gruppe under enhed" }, "min_off_gap": { - "doc": "Hvis maskinen slukker i mindre end denne tid, behandles tænd/sluk/tænd-sekvensen som én kontinuerlig cyklus. Forhindrer iblødsætningsprogrammer (maskinen slukker i flere minutter midt i vask) i at blive opdelt i to separate cyklusser. Indstil den kortere end afstanden mellem dine ryg-til-ryg-belastninger, hvis du vil have dem, der tælles som separate cyklusser. Standardindstillinger for enhedstype beskytter den typiske pause i cyklussen for hvert apparat.", + "doc": "En slukket periode kortere end dette deler ikke en cyklus, så iblødsætningspauser forbliver i én vask. Hold den kortere end pausen mellem vaske lige efter hinanden, ellers flettes de til én cyklus.", "label": "Minimum slukket mellemrum" }, "min_power": { - "doc": "Absolut minimumseffekt anses for aktiv. Aflæsninger under dette behandles som 0 W (standby), hvilket frafiltrerer fantombelastningen af smarte stik og standby-LED'er.", + "doc": "Målinger under dette tæller som 0 W (standby). Filtrerer tomgangsforbruget fra smartstik og standby-LED'er fra.", "label": "Minimumseffekt" }, "ml_training_enabled": { - "doc": "Studér med jævne mellemrum dine gennemgåede cyklusser natten over, og finjuster modellerne til denne specifikke maskine. En ændring bevares kun, når den virkelig scorer bedre på udholdte cyklusser, så dette kan kun hjælpe eller forblive det samme - aldrig gå tilbage.", + "doc": "Gentræn energimodellen på denne maskines færdige cyklusser med jævne mellemrum. En ny model beholdes kun, hvis den slår det simple estimat på testcyklusser.", "label": "Lær fra denne maskine" }, "ml_training_hour": { - "doc": "Lokal tid på dagen (0-23) for at foretage finjusteringen natten over. Vælg en stille time såsom 2 (02:00).", + "doc": "Time på dagen (0-23) for natlig træning. Vælg en stille time, f.eks. 2.", "label": "Lær på tidspunktet" }, "ml_training_interval_days": { - "doc": "Tjek igen for forbedringer højst én gang pr. så mange dage.", + "doc": "Gentræn højst én gang hver så mange dage.", "label": "Tjek højst én gang hver" }, "ml_training_min_cycles": { - "doc": "Vent til mindst så mange cyklusser er blevet optaget, før du finjusterer, så der er nok at lære af.", + "doc": "Vent på mindst så mange optagede cyklusser før træning.", "label": "Cykler nødvendige først" }, "name": { - "doc": "Vist navn vist i HA-integrationslisten og enhedsregistret.", + "doc": "Navnet, der vises i Home Assistant for denne enhed.", "label": "Enhedsnavn" }, "no_update_active_timeout": { - "doc": "Hvis der ikke ankommer strømopdateringer i så lang tid, mens du kører, antag, at stikket faldt offline og tvinges til at stoppe for at undgå en zombiecyklus. Standard 600 s giver mulighed for sky eller mesh lag.", + "doc": "Hvis ingen effektopdatering kommer så længe under en cyklus, antages stikket at være offline, og cyklussen tvangsstoppes. Standarden på 600 s giver plads til forsinkelser i sky eller mesh.", "label": "Timeout uden opdatering" }, "notify_before_end_minutes": { @@ -1382,35 +1339,35 @@ "label": "Forhåndsadvarsel om afslutning" }, "notify_channel": { - "doc": "Android-notifikationskanalnavn for start-/livebeskeder (styrer lyd pr. kanal og prioritet på mobilappen). Blank bruger standardappen for den ledsagende app.", + "doc": "Android-kanal for start- og livebeskeder; kanalen bestemmer lyd og prioritet. Tom: appens standard.", "label": "Android-kanal (start/live)" }, "notify_cycle_timers": { - "doc": "Meddelelser på bestemte minutter i en cyklus (f.eks. for at tilføje blødgøringsmiddel). Besked understøtter {device}, {program}, {minutes}. Aktiver Auto-pause for at holde pause på det tidspunkt og modtage en interaktiv meddelelse med en Genoptag-knap; genoptage via panelet, pause/genoptag-tjenesten eller meddelelseshandlingen.", + "doc": "Notifikationer ved bestemte minutter inde i en cyklus (f.eks. for at tilføje skyllemiddel). Beskeden understøtter {device}, {program}, {minutes}. Med Auto-pause sættes cyklussen også på pause dér, og notifikationen får en Genoptag-knap.", "label": "Cyklustimere" }, "notify_finish_channel": { - "doc": "Navn på Android-underretningskanal for slutbeskeden. Blank genbruger start/live-kanalen.", + "doc": "Android-kanal for slutbeskeden. Tom: start-/livekanalen genbruges.", "label": "Android-kanal (slut)" }, "notify_finish_message": { - "doc": "Tekst sendt, når en cyklus afsluttes. Skabelonvariabler: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}. {time_finished} og {vs_typical} er mest nyttige her.", + "doc": "Tekst, der sendes, når en cyklus slutter. Variabler: {device}, {program}, {duration} (minutter), {duration_hm} (1 h 05 min), {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}, {status} (completed eller force_stopped; afbrudte cyklusser sender intet). Med enhver anden variabel bruges standardteksten.", "label": "Slutbesked" }, "notify_finish_services": { - "doc": "notify.* tjenester kaldet, når en cyklus afsluttes. Tilføj en pr. mål; efterlad tom for ingen færdigmelding.", + "doc": "notify.*-tjenester, der kaldes, når en cyklus slutter. Lad være tomt for ingen.", "label": "Sluttjenester" }, "notify_fire_events": { - "doc": "Affyr også ha_washdata_* begivenheder på cyklus start/afslutning, så du kan bygge dine egne automatiseringer.", + "doc": "Udløs også ha_washdata_*-hændelser ved cyklusstart og -slut til dine egne automatiseringer.", "label": "Send HA-hændelser til notifikationer" }, "notify_icon": { - "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.", + "doc": "Lille ikon på notifikationen (f.eks. mdi:washing-machine). Sendes kun til mobile_app-mål, ikke til notify-grupper. På iOS erstatter det app-ikonet og kræver companion-appen 2026.8 eller nyere. Tom: platformens standard.", "label": "Notifikationsikon" }, "notify_live_chronometer": { - "doc": "Vis en live-opdatering nedtællingstimer i notifikationen (på platforme, der understøtter det) i stedet for et statisk estimat.", + "doc": "Vis en kørende nedtælling i notifikationen (hvor det understøttes) i stedet for et fast estimat.", "label": "Brug live kronometer" }, "notify_live_interval_seconds": { @@ -1418,143 +1375,135 @@ "label": "Live opdateringsinterval" }, "notify_live_overrun_percent": { - "doc": "Hvis en cyklus løber forbi sit estimat med mere end denne procentdel, skal du sende en advarsel om overløb.", + "doc": "Live-opdateringer stopper, når en cyklus kører denne procentdel over sin forventede varighed (20 = op til 1,2x den forventede tid).", "label": "Live overskridelsesprocent før advarsel" }, "notify_live_services": { - "doc": "notify.*-tjenester opfordrede til live statusopdateringer, mens en cyklus kører. Lad stå tomt for at deaktivere notifikationer om live-fremskridt.", + "doc": "notify.*-tjenester for live-statusopdateringer, mens en cyklus kører. Lad være tomt for at slå dem fra.", "label": "Live fremdriftstjenester" }, "notify_only_when_home": { - "doc": "Send kun notifikationer, når mindst én af de tilknyttede personer (ovenfor) er hjemme.", + "doc": "Giv kun besked, når mindst én af personerne ovenfor er hjemme.", "label": "Underret kun når hjemme" }, "notify_people": { - "doc": "person.* enheder, der bruges af \"Meddel kun når hjemme\" til at afgøre, om nogen er hjemme.", + "doc": "person.*-entiteter, som \"Underret kun når hjemme\" tjekker.", "label": "Personer (for kun når hjemme)" }, "notify_pre_complete_message": { - "doc": "Tekst i før-slut / næsten-færdig-advarslen. Skabelonvariabler: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Tekst i hver live-statusopdatering. Skabelonvariabler: {device}, {program}, {minutes} (minutter tilbage). Med enhver anden variabel bruges standardteksten.", "label": "Besked om næsten færdig" }, "notify_reminder_message": { - "doc": "Tekst i påmindelsen om stadig ventende tømning. Tom bruger den indbyggede standard. Skabelonvariabler: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Tekst i Forhåndsadvarsel om afslutning, der sendes én gang. Skabelonvariabler: {device}, {program}, {minutes} (indstillingen Forhåndsadvarsel om afslutning). Med enhver anden variabel bruges standardteksten.", "label": "Påmindelsesbesked" }, "notify_start_message": { - "doc": "Tekst sendt, når en cyklus starter. Skabelonvariabler: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Tekst, der sendes, når en cyklus starter. Skabelonvariabler: {device}, {program}. Med enhver anden variabel bruges standardteksten.", "label": "Startbesked" }, "notify_start_services": { - "doc": "notify.* tjenester kaldet, når en cyklus starter. Tilføj en pr. mål (telefon, dashboard osv.); efterlad tom for ingen startmeddelelse.", + "doc": "notify.*-tjenester, der kaldes, når en cyklus starter (telefon, dashboard, ...). Lad være tomt for ingen.", "label": "Starttjenester" }, "notify_timeout_seconds": { - "doc": "Afvis automatisk meddelelsen efter så mange sekunder (på platforme, der understøtter den). 0 beholder den, indtil den afvises manuelt.", + "doc": "Luk notifikationen efter så mange sekunder (hvor det understøttes). 0 beholder den, til du lukker den.", "label": "Auto-afvis efter" }, "notify_title": { - "doc": "Underretningstitel. Skabelonvariabler: {device}, {program}, {duration}, {energy}, {cost}, {date}, {time}, {minutes}.", + "doc": "Underretningstitel. Kun {device} udskiftes; med enhver anden variabel bruges standardtitlen.", "label": "Notifikationstitel" }, "notify_unload_delay_minutes": { - "doc": "Minutter efter en cyklus er afsluttet, før du sender den stadig ventende påmindelse om \"aflæs maskinen\". Indstil 0 for at deaktivere påmindelsen.", + "doc": "Minutter efter en cyklus' afslutning før påmindelsen \"tøm maskinen\". 0 slår den fra.", "label": "Forsinkelse for aflæsningspåmindelse" }, "notify_unload_repeat": { - "doc": "Bliv ved med at gensende aflæsningspåmindelsen hvert \"Forsinkelse for aflæsningspåmindelse\" minutter, indtil aflæsningen er bekræftet, eller du trykker på \"Stop påmindelser\" i notifikationen. Kræver en måde at bekræfte aflæsningen på: en Dørsensorenhed, en Aflæsningsbekræftelsesenhed eller indstillingen \"Bekræft aflæsning manuelt\" nedenfor. Knappen til at afvise virker på notifikationer fra Home Assistant companion-appen (mobil).", + "doc": "Send aflæsningspåmindelsen igen hvert \"Forsinkelse for aflæsningspåmindelse\" minut, indtil aflæsningen er bekræftet, eller du trykker \"Stop påmindelser\" (mobilapp). Kræver en Dørsensorenhed, en Aflæsningsbekræftelsesenhed eller \"Bekræft aflæsning manuelt\".", "label": "Gentag indtil aflæsset" }, "off_delay": { - "doc": "Tid til at vente efter strømfald, før cyklussen erklæres afsluttet. Hvis strømmen genoptages inden for dette vindue, fortsætter cyklussen problemfrit - dette slår bro mellem pauser mellem vasketrinene. Opvaskemaskiner har lange tørrefaser (strøm fra i 20-60 min), så slukningsforsinkelsen skal overstige det for at holde hele vask+tør som én cyklus.", + "doc": "Hvor længe effekten skal forblive lav, før cyklussen er færdig. Kommer effekten tilbage inden for denne tid, fortsætter cyklussen. Opvaskemaskiner kan være stille i 20-60 min under tørring, så sæt den længere end det.", "label": "Slukkeforsinkelse" }, "pause_cuts_power": { - "doc": "Når en cyklus er sat på pause, skal du også slukke for Switch Entity nedenfor. Kun til apparater, hvis stik sikkert kan afbrydes midt i cyklussen.", + "doc": "Når en cyklus sættes på pause, slukkes også Kontaktenheden nedenfor. Kun til apparater, der sikkert kan miste strøm midt i en cyklus.", "label": "Pause afbryder også strøm (via kontakt)" }, "power_off_delay": { - "doc": "Hvor længe effekten skal forblive under Slukketærskel, efter at en cyklus er afsluttet, før tilstanden vender tilbage til Slukket. Bruges kun, når Slukketærskel er over 0. Kontrolleres i baggrundstakten, så den effektive forsinkelse rundes op til næste tilstandsudløbstakt.", + "doc": "Hvor længe effekten skal blive under Slukketærskel efter en cyklus, før tilstanden går tilbage til Fra. Bruges kun, når den tærskel er over 0. Tjekkes periodisk, så den reelle ventetid kan være lidt længere.", "label": "Slukkeforsinkelse" }, "power_off_threshold_w": { - "doc": "Valgfri effektbaseret registrering af slukning. Når værdien er over 0, betragtes maskinen som slukket, og tilstanden vender tilbage til Slukket, når en cyklus er afsluttet, og effekten forbliver under dette niveau i hele Slukkeforsinkelsen. Lad den stå på 0 for at deaktivere (standard). Sæt den over det reelle slukkede nulniveau og under Stoptærskel samt under maskinens standbyforbrug i tilstanden færdig-men-tændt; hvis den ikke er under Stoptærskel, ignoreres den. Når den er aktiveret, erstatter den Forsinkelse ved nulstilling af fremskridt for at vende tilbage til Slukket, så en færdig maskine forbliver i Færdig/Ren, indtil den faktisk slukkes.", + "doc": "Efter en cyklus sætter effekt under denne i Slukkeforsinkelse tilstanden til Fra, i stedet for at Forsinkelse ved nulstilling af fremskridt gør det. Sæt den over forbruget i slukket tilstand, under standby og Stoptærskel. 0 slår den fra (standard).", "label": "Slukketærskel" }, "power_sensor": { - "doc": "Sensorenheden rapporterer strøm i watt for dette apparat (f.eks. sensor.washer_power). Al cyklusdetektion er baseret på dette signal.", + "doc": "Sensoren, der rapporterer dette apparats aktuelle effekt i watt (f.eks. sensor.washer_power). Al registrering bygger på den.", "label": "Effektsensor" }, - "profile_duration_tolerance": { - "doc": "+/- båndet omkring en profilgennemsnitsvarighed brugt under matchning. 0,25 betyder, at en 60 min profil matcher 45-75 min cyklusser.", - "label": "Profilvarighedstolerance" - }, "profile_match_interval": { - "doc": "Hvor ofte skal man forsøge profilmatching under en løbecyklus. Standard 300 s (5 minutter) balancerer registreringshastighed og CPU.", + "doc": "Hvor ofte en kørende cyklus matches mod profilerne. Standard 300 s (5 min).", "label": "Matchinterval" }, "profile_match_max_duration_ratio": { - "doc": "Maksimal cykluslængde i forhold til profilen. 1.3 betyder, at en cyklus skal være under 130 % af profilens varighed for at matche.", + "doc": "Et program droppes, når cyklussen har kørt længere end dette multiplum af dets typiske varighed. 1,8: en cyklus 80% længere end programmet matcher det ikke længere.", "label": "Maksimum varighed ratio" }, "profile_match_min_duration_ratio": { - "doc": "Minimum cykluslængde i forhold til profilen. 0,9 betyder, at en cyklus skal være mindst 90 % af profilens varighed for at matche.", + "doc": "Et program overvejes først, når cyklussen har kørt denne andel af dets typiske varighed. 0,10: et program på 2 timer bliver kandidat efter 12 minutter. Anvendes ikke i en cyklus' første 15 minutter.", "label": "Minimum varighed ratio" }, "profile_match_threshold": { - "doc": "Minimum lighedsscore (0-1) kræves ved cyklusslut for at acceptere en programidentifikation. Hæv den for at reducere forkerte identifikationer; sænk den, hvis din maskines programmer ikke bliver matchet. Standard 0,4 er et konservativt udgangspunkt.", + "doc": "Live-matchscore, en cyklus skal have, før smart afslutning eller anti-rynke-afslutningen må afslutte den tidligt. Ændrer ikke det viste program. Hæv den, hvis svage match afslutter cyklusser for tidligt; sænk den, hvis klare match venter hele slukkeforsinkelsen.", "label": "Matchtærskel" }, "profile_unmatch_threshold": { - "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.", + "doc": "Hvis live-matchscoren falder under denne, ryddes det aktuelle programgæt. Hold den lidt under Matchtærskel, så et kort dyk ikke nulstiller det.", "label": "Fejlmatchtærskel" }, "progress_reset_delay": { - "doc": "Når du er færdig, skal du holde status på 100 % så længe, så fuldført er synlig på dashboards, før du nulstiller til inaktiv.", + "doc": "Hold fremskridt på 100 % så længe efter en cyklus, før der nulstilles til Inaktiv.", "label": "Forsinkelse ved nulstilling af fremskridt" }, "pump_stuck_duration": { - "doc": "Sekunder kan en pumpe køre uafbrudt, før den er markeret som muligvis sidder fast (udløser hændelsen stuck-pumpe).", + "doc": "Hvor længe en pumpe må køre uafbrudt, før hændelsen for fastklemt pumpe udløses.", "label": "Varighed for fastklemt pumpe" }, "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.", + "doc": "Mindste tid mellem behandlede målinger. Målinger, der kommer hurtigere, springes over (lave målinger under en cyklus beholdes altid). Sæt den til dit stiks rapporteringsinterval eller lavere; en højere værdi smider kun målinger væk.", "label": "Samplingsinterval" }, "save_debug_traces": { - "doc": "Gem fuld effektsporing og matchende fejlretningsdata for hver cyklus. Nyttig til fejlfinding, men øger lagerstørrelsen.", + "doc": "Gem også matcherens fejlretningsdata (alle kandidaters scorer) med hver cyklus til fejlfinding. Fylder ca. 20 KB pr. cyklus.", "label": "Gem fejlretningsspor" }, "smart_termination_duration_ratio": { - "doc": "Hvor langt en cyklus skal være inde i det genkendte programs forventede varighed, før smart afslutning må afslutte den tidligt, når effekten falder. Den forventede varighed er programmets gennemsnit, så på apparater med stærkt varierende køretid - vaskemaskiner med koldt vinter- kontra varmt sommerindløbsvand, tørretumblere med fugtsensor, belastningsafhængige programmer - slutter omkring halvdelen af alle kørsler kortere end dette gennemsnit og får aldrig den hurtige afslutning, men slutter først minutter senere via reserve-timeoutet. Sænk denne værdi (f.eks. 0,85) på sådanne maskiner, så den tidlige afslutning stadig udløses; hæv den mod 1,0 for at være mere forsigtig. Lad feltet stå tomt for standarden (0,98 eller 0,99 for opvaskemaskiner). Den kan kun afslutte en cyklus tidligere, aldrig senere, og udløses aldrig ved en tvetydig eller usikker genkendelse.", + "doc": "Andel af programmets gennemsnitlige varighed, som en cyklus skal nå, før smart afslutning må afslutte den tidligt. Hvis køretiderne varierer (vandtemperatur, fugtsensor, mængde), så sænk den (f.eks. 0,85). Lad være tom for 0,98 (0,99 på opvaskemaskiner).", "label": "Forhold for smart afslutning" }, - "smoothing_window": { - "doc": "Hvor meget råeffektsignalet udjævnes. Lav (2) er lydhør, men støjende; høj (5) udglatter pigge, men tilføjer forsinkelse.", - "label": "Glatningsvindue" - }, "start_duration_threshold": { - "doc": "Strømmen skal forblive over starttærsklen så længe for at bekræfte en reel start, hvilket forhindrer, at tænd/sluk-skift på et splitsekund starter en cyklus.", + "doc": "Effekten skal blive over Starttærskel så længe, før en cyklus starter, så korte tænd/sluk-udsving ignoreres.", "label": "Startvarighed" }, "start_energy_threshold": { - "doc": "Energi (strøm x tid), apparatet skal forbruge, før det KØRES. En kort højeffektspids har meget lav energi og ignoreres, hvilket forhindrer falske starter.", + "doc": "Energi, apparatet skal bruge, før cyklussen tæller som kørende. En kort effektspids bruger lidt energi, så den starter ikke en cyklus.", "label": "Startenergi" }, "start_threshold_w": { - "doc": "Strømmen skal stige over dette niveau for at bekræfte, at en cyklus er startet. Hvis den indstilles for lavt, forårsager det falske starter fra standby-strøm; for høje og langsomt startende programmer (koldfyldning) savnes. Forslagsmotoren indstiller dette lige over maskinens observerede laveste aktive effekt.", + "doc": "Effekten skal stige over denne for at starte en cyklus. For lav: standbyeffekt giver falske starter. For høj: langsomt startende programmer (koldt vandindtag) overses.", "label": "Starttærskel" }, "stop_threshold_w": { - "doc": "Strømmen skal falde til under dette niveau, før nedtællingen af slukket forsinkelse begynder. Indstil den under starttærsklen - mellemrummet mellem dem er hysteresebåndet, der forhindrer flimmer. Hvis den er indstillet for højt, vil laveffektfaser (skyllestop, anti-krøl) fejlagtigt udløse slutsekvensen.", + "doc": "Effekten skal falde under denne, før slukkeforsinkelsen begynder at tælle. Hold den under Starttærskel. For høj, og faser med lav effekt (skyllepauser, anti-rynke) starter slutnedtællingen ved en fejl.", "label": "Stoptærskel" }, "switch_entity": { - "doc": "Valgfri kontakt slås fra ved pause og tændes igen ved genoptagelse, bruges sammen med \"Pause afbryder også strømmen\".", + "doc": "Valgfri kontakt, der slukkes ved pause og tændes igen ved genoptagelse, med \"Pause afbryder også strøm\".", "label": "Kontaktenhed" }, "watchdog_interval": { - "doc": "Hvor ofte vagthunden i baggrunden tjekker for fastholdte sensorer og forløbne timeouts. Standard 30 s.", + "doc": "Hvor ofte baggrundstjekket leder efter gået i stå-sensorer og timeouts. Standard 30 s.", "label": "Watchdog-interval" }, "notify_milestone_message": { @@ -1562,127 +1511,82 @@ "label": "Milepælsbesked" }, "notify_milestones": { - "doc": "Kommaseparerede cyklustællinger, der udløser en engangs-fejringsnotifikation, når de nås (f.eks. 50, 100, 500, 1000). Tom deaktiverer milepælsnotifikationer.", + "doc": "Cyklusantal, der udløser en engangsnotifikation til fejring, kommasepareret (f.eks. 50, 100, 500, 1000). Tom slår dem fra.", "label": "Cyklusmilepæle" }, "notify_quiet_end_hour": { - "doc": "Afslutning på forstyr-ikke-vinduet (0-23). Tilbageholdte notifikationer leveres på denne time. Lad feltet være tomt for at deaktivere.", + "doc": "Slutningen af forstyr ikke-vinduet (0-23); tilbageholdte notifikationer sendes da. Tom slår det fra.", "label": "Stilletimer slut" }, "notify_quiet_start_hour": { - "doc": "Start på et forstyr-ikke-vindue (0-23). Notifikationer om afslutning, påmindelser og ren-vasketøj, der ville blive udløst i stilletimerne, tilbageholdes og leveres, når vinduet slutter. Lad feltet være tomt for at deaktivere. Understøtter vinduer, der krydser midnat (f.eks. start 22, slut 7).", + "doc": "Start på et forstyr ikke-vindue (0-23). Slut-, påmindelses- og ren vask-notifikationer i dette vindue holdes tilbage, til det slutter. Kan gå hen over midnat (f.eks. 22 til 7). Tom slår det fra.", "label": "Stilletimer start" }, "peak_rate_message": { - "doc": "Valgfri brugerdefineret tekst til spidsbelastningstippet, der føjes til startnotifikationen. Skabelonvariabler: {device}, {price}. Tom bruger den indbyggede standard.", + "doc": "Egen tekst til spidstarif-tippet. Skabelonvariabler: {device}, {price}. Tom bruger standarden.", "label": "Spidsbelastningsbesked" }, "peak_rate_threshold": { - "doc": "Når en cyklus starter, og den aktuelle pris pr. kWh er på eller over denne værdi, føjes et spidsbelastningstip til startnotifikationen. 0 eller tom deaktiverer tippet.", + "doc": "Hvis prisen pr. kWh er på eller over denne, når en cyklus starter, tilføjes et spidstarif-tip til startnotifikationen. 0 eller tom slår det fra.", "label": "Spidsbelastningstærskel (pr. kWh)" }, - "dtw_bandwidth": { - "label": "Tolerance for formgenkendelse", - "doc": "Hvor meget genkendelsen må forvrænge en cyklus i tid for at passe til formen på et gemt program. Højere tillader mere hastighedsvariation; for højt risikerer at genkende det forkerte program." - }, - "corr_weight": { - "label": "Vægt: form kontra niveau", - "doc": "Balance mellem kurveform og absolut effektniveau, når en genkendelse vurderes. Højere foretrækker formen; lavere foretrækker at ramme effektniveauet." - }, - "duration_weight": { - "label": "Vægt på varighed", - "doc": "Hvor stærkt et program, hvis typiske køretid passer til denne cyklus, foretrækkes." - }, - "energy_weight": { - "label": "Vægt på energi", - "doc": "Hvor stærkt et program, hvis typiske energiforbrug passer til denne cyklus, foretrækkes." - }, "store_brand": { "label": "Apparatets mærke", - "doc": "Valgfrit. Apparatets mærke, valgt fra fællesskabskataloget. Bruges til at finde og dele matchende referenceoptagelser. Lad stå tomt, hvis du ikke bruger onlinefunktioner." + "doc": "Valgfri. Dit apparats mærke fra fællesskabskataloget, bruges til at finde og dele referenceoptagelser. Lad være tom, hvis du ikke bruger onlinefunktioner." }, "store_model": { "label": "Apparatets model", - "doc": "Valgfrit. Apparatets model, valgt fra fællesskabskataloget, når et mærke er angivet. Hvis din model ikke er på listen, kan du tilføje den til kataloget." + "doc": "Valgfri. Dit apparats model fra fællesskabskataloget (vælg først et mærke). Hvis den ikke er på listen, kan du tilføje den." }, "show_contributor": { "doc": "Viser tilskrivningen 'af ' på fællesskabsapparater og referencecyklusser." }, - "enable_phase_matching": { - "label": "Fasebevidst resterende tid", - "doc": "Opdel hver kørende cyklus i faser (opvarmning, vask, centrifugering) og budgettér den resterende tid pr. fase, blandet med det klassiske estimat - tidligt i cyklussen læner den sig op ad fasebudgettet og mod slutningen op ad det klassiske estimat. Dette tilpasser nedtællingen til, hvor længe din maskine faktisk varmer op og kører, hvilket er mest mærkbart i den første halvdel af en cyklus. Fra = kun det klassiske estimat. Kun visningen af resterende tid påvirkes; programmatchning og cyklusregistrering er uændrede." - }, - "keep_min_score": { - "label": "Min. matchingsscore", - "doc": "Laveste lighedsscore en kandidat behøver for at forblive i matcheprocessen. Standard 0,1 er bevidst tillåtende – det tillader selv svage kandidater og stoler på at senere trin finder den bedste match. Hæv for at skære usandsynlige profiler fra tidligere; sænk for at udvide den indledende kandidatpulje." - }, - "dtw_blend": { - "label": "DTW-blanding", - "doc": "Hvor meget DTW-justeringsscoren erstatter kerneresultatet fra Trin 2. 0 = brug kun Trin 2-scoren, 1 = brug kun DTW-scoren, 0,5 (standard) = ligelig blanding. Hæv for at stole mere på tidsjustering når programmer har ensartede effektniveauer men forskellige timingmønstre." - }, - "dtw_ensemble_w": { - "label": "DTW-ensemblemiks", - "doc": "I ensemble-DTW-tilstand (standard) er dette vægten på skaleret L1 kontra derivativ DTW (DDTW). 1,0 = kun skaleret L1, 0 = kun DDTW, 0,7 = standard. Den skalerede variant sammenligner effektniveauer; den derivative reagerer på hvordan effekten ændres over tid. Ensemble blander begge." - }, - "dtw_ddtw_scale": { - "label": "Derivativ DTW-skala", - "doc": "Halvmætningsafstand for derivativ DTW-scoring. Scoren halveres når DDTW-afstanden er lig denne værdi. Lavere = mere følsom for formforskelle. Standard 30 er kalibreret til typiske husholdningsapparaters effektprofiler. Påvirker kun ensemble- og ddtw-DTW-tilstande." - }, - "dtw_refine_top_n": { - "label": "DTW-forfiningsnummer", - "doc": "Hvor mange topkandidater fra Trin 2 der gen-scores af DTW. DTW er mere kostbar så det anvendes kun på de N bedste. Standard 5. Hæv (til 7-9) hvis den korrekte profil sommetider kun når 4. eller 5. plads efter Trin 2 – DTW kan redde den. Lavere giver lidt hurtigere matching." - }, - "duration_scale": { - "label": "Varighedsskarphed", - "doc": "Skarphed i Trin 4's straf for varighedsoverensstemmelse. Det er log-forholdet ved hvilket overensstemmelses-scoren halveres. Lavere = strengere: et varighedsafvigelse skader mere. Standard 0,175 svarer til omtrent 18% varighedstolerens ved halvvægt. Par med Varighedsvægt." - }, - "energy_scale": { - "label": "Energiskarphed", - "doc": "Skarphed i Trin 4's straf for energioverensstemmelse. Lavere = strengere: et energiafvigelse skader mere. Standard 0,25 er bevidst mere tillåtende end varighedsskarphed fordi energi varierer med belastning. Par med Energivægt." - }, "power_profile_interval_min": { "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." + "doc": "Bucketstørrelse for attributten power_profile (gennemsnitlig watt pr. slot), som planlæggere som EMHASS og tibber_prices bruger. Mindre bevarer korte spidser; større udglatter kurven. Standard 15 min. Påvirker ikke registreringen." }, "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." + "doc": "Kun iOS. Opdater live-status lydløst med en batterivenlig prioritet, som iOS kan samle. Den første opdatering og start-/slutnotifikationerne giver stadig besked. Fra: hver opdatering giver besked." }, "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." + "doc": "Kun Android. Behold live-statusnotifikationen ved tryk. Fra: et tryk lukker den." }, "notify_live_click_action": { "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." + "doc": "Hvad et tryk på dette apparats notifikation åbner. Tom: WashData-panelet for dette apparat. Angiv en dashboard-sti (f.eks. /lovelace/laundry) eller URL, eller \"none\" for ingen trykhandling." }, "door_opens_at_end": { "label": "Dør åbner automatisk ved afslutning", - "doc": "Til opvaskemaskiner, der springer døren op ved afslutningen af cyklussen for at tørre (AirDry og lignende). Når dette er aktiveret, sætter en døråbning under en aktiv cyklus den ikke på pause for evigt; hvis døren forbliver åben i den konfigurerede opholdsperiode, betragter WashData cyklussen som afsluttet. En kort åbning (tilføjelse af en tallerken) ignoreres. Kræver en dørsensorenhed." + "doc": "Til opvaskemaskiner, der åbner døren ved slutningen for at tørre (f.eks. AirDry). En dør, der står åben i ventetiden nedenfor, afslutter cyklussen i stedet for at sætte den på pause; en kort åbning ignoreres. Kræver en Dørsensorenhed." }, "door_end_dwell_seconds": { "label": "Dørens åbningstid ved afslutning", - "doc": "Hvor længe døren skal forblive åben, før WashData afslutter cyklussen, når \"Dør åbner automatisk ved afslutning\" er aktiveret. Lang nok til at ignorere hurtig tilføjelse af en tallerken (standard 60 s), kort nok til at afslutte hurtigt, når maskinen springer døren op." + "doc": "Med \"Dør åbner automatisk ved afslutning\" slået til: hvor længe døren skal stå åben, før cyklussen slutter. Længe nok til at ignorere, at en tallerken sættes ind (standard 60 s)." }, "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." + "doc": "Hvilke typer cyklusser der former hvert programs kurve og matchning. At fjerne krydset ved en type sletter intet; nyttigt, hvis du ikke stoler på importerede data. Statistik tæller altid kun denne maskines cyklusser. At fjerne alle ignoreres." }, "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." + "doc": "Beregn hver cyklus til prisen i hvert øjeblik, den kørte, i stedet for prisen, da den sluttede. Kræver en prisenhed. Behandl historik (Diagnostik) genberegner ældre cyklusser." }, "unload_confirm_entity": { "label": "Aflæsningsbekræftelsesenhed", - "doc": "Valgfri enhed, hvis aktivering betyder, at vasken er taget ud, til maskiner der ikke kan have en dørsensor. Alle typer virker: en Zigbee- eller NFC-knap, en input_button-hjælper, en bevægelsessensor foran maskinen, en scene. Enhver tilstandsændring tæller som en bekræftelse, bortset fra at blive slukket og at blive utilgængelig. Når denne indstilles, aktiveres aflæsningspåmindelsen også på en enhed uden Dørsensorenhed." + "doc": "Valgfri. En hvilken som helst entitet, der bekræfter, at vasken er taget ud, til maskiner uden dørsensor (en knap, input_button, bevægelsessensor, scene). Hver tilstandsændring tæller, undtagen at slukke eller blive utilgængelig. Aktiverer også aflæsningspåmindelsen." }, "unload_track_without_door": { "label": "Bekræft aflæsning manuelt", - "doc": "Kør aflæsningspåmindelsen på en enhed uden Dørsensorenhed og uden Aflæsningsbekræftelsesenhed, og bekræft i stedet aflæsningen fra din egen automatisering: tryk på knapenheden \"Markér som aflæsset\", eller kald handlingen ha_washdata.mark_unloaded. Ignoreres, når en Dørsensorenhed er angivet, da den allerede leverer signalet." + "doc": "Kør aflæsningspåmindelsen uden dørsensor eller bekræftelsesentitet. Bekræft aflæsningen fra din egen automatisering med knappen \"Markér som aflæsset\" eller tjenesten ha_washdata.mark_unloaded. Ignoreres, når en Dørsensorenhed er angivet." }, "notify_icon_color": { "label": "Notifikationsfarve", - "doc": "Accentfarve til denne enheds notifikationer, så en vaskemaskine, en tørretumbler og en opvaskemaskine kan skelnes fra hinanden på et øjeblik. iOS farver ikonet og Liveaktivitetens fremdriftslinje på den låste skærm og i Dynamic Island. Android får den samme farve sendt, men hvad telefonen bruger den til, er op til den: nogle farver ikonet, og mange (især Samsung One UI) ignorerer den helt. Begge forstår hex-værdier (f.eks. #03A9F4). Lad stå tomt for platformens standard." + "doc": "Accentfarve for dette apparats notifikationer, så vaskemaskine, tørretumbler og opvaskemaskine er lette at skelne. På iOS fylder den cirklen bag ikonet og farver Live Activity-bjælken. Mange Android-telefoner (især Samsung) ignorerer den. Brug hex, f.eks. #03A9F4. Tom: platformens standard." + }, + "share_name": { + "doc": "Vis dit kontonavn som uploader af cyklusser, du deler. Fra: delt uden navn." } }, "setting_group": { @@ -1710,9 +1614,6 @@ "door_pause": { "label": "Dør og pause" }, - "dtw_refinement": { - "label": "DTW-raffinering" - }, "duration_gates": { "label": "Varighedsporte" }, @@ -1789,17 +1690,20 @@ "rebuilding": "Genopbygger…", "analyzing": "Analyserer…", "resetting": "Nulstiller…", - "reverting": "Tilbagefører…", "trimming": "Beskærer…", "splitting": "Opdeler…", + "stalled": "Gået i stå", "deleting": "Sletter…", "imported": "Importeret", "preparing": "Forbereder…", - "unlabelling": "Fjerner mærkning…" + "unlabelling": "Fjerner mærkning…", + "working": "Arbejder…", + "match_uncertain": "Usikker: {top} eller {runner}", + "match_uncertain_single": "Usikker: måske {top}", + "match_sure": "~{pct}% sikker", + "match_uncertain_tip": "WashData har endnu ikke lagt sig fast på et program. Tallet viser, hvor ofte det førende gæt, {top}, var rigtigt i tidligere cyklusser, når det førte med så meget. Kun til information." }, "suggestion": { - "both_agree": "WashData anbefaler", - "calibrated_label": "Kalibreret", "observed_label": "Observeret", "impact": { "auto_label_confidence": { @@ -1814,10 +1718,6 @@ "higher": "Afslutter kun cyklusser med væsentligt energiforbrug", "lower": "Afslutter også kortere eller lavenergi-cyklusser" }, - "end_repeat_count": { - "higher": "Slutsignal skal gentages flere gange – mere robust, lidt langsommere", - "lower": "Slutter efter færre gentagelser – hurtigere detektering, mere risiko for falsk afslutning" - }, "learning_confidence": { "higher": "Lærer kun fra meget sikre matches – langsommere men mere pålidelig", "lower": "Lærer fra flere cyklusser – modellen kan blive mere upræcis" @@ -1856,40 +1756,25 @@ } }, "reason": { - "auto_label_confidence": "Konfidens ved 15. percentil for {cycles} automatiske etiketter, som brugeren aldrig rettede ({p15}).", "completion_min_seconds": "Halvdelen af p05-varigheden for rene cyklusser ({minutes} min) på tværs af {cycles} cyklusser; filtrerer spøgelsescyklusser fra.{excl}", - "dead_zone_batch": "Holdt kort (median for opstarts-ustabilitetsvindue på tværs af {cycles} cyklusser = {s}s), så et reelt slut registreres hurtigt.", - "dead_zone_from_dip": "Baseret på et tidligt effektfald registreret ved {s}s.", - "end_energy_default": "Standardanbefalet baseline for støjgrænsen ved cyklusslut.", - "end_energy_false": "p95 for energi ved falske slut ({p95}Wh) på tværs af {cycles} cyklusser, med gulv på 0.2% af medianens cyklusenergi ({floor}Wh).", - "end_energy_no_false": "Ingen falske slut på tværs af {cycles} cyklusser; med gulv på 0.2% af medianens cyklusenergi ({floor}Wh).", - "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.", + "match_interval_floored": "Det korteste program ({shortest}s) ville begrænse dette til {cap}s, under minimummet på {minimum}s, så minimummet bruges: {persistence} match i træk tager {budget}s, over {pct}% af det program. Matchning kører kun, når en måling kommer (median={median}s), så det koster intet.", "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.", "min_off_gap": "Baseret på {gaps} observerede mellemrum mellem cyklusser (p05={p05}s). Enhedsgulv: {floor}s.", "min_off_gap_bridge": "Dimensioneret til at bygge bro over den længste stille periode i en cyklus: {span}s + 60s buffer, fra {spans} dækkede mellemrum på tværs af {cycles} rene cyklusser (holder sig under dit korteste mellemrum på {ceiling}s mellem to kørsler).", "min_power": "40% af den laveste aktive effekt ved p05 ({p05}W) på tværs af {cycles} rene cyklusser, hvilket holder sluk-tærsklen under reelt forbrug.{excl}", - "ml_auto_label": "Laveste konfidens, som kvalitetsmodellen stadig vurderede som ren (p10 af {cycles} rene cyklusser = {p10}).", - "ml_end_repeat": "{false}/{total} cyklusser ({pct}%) havde en pause, som slutdetektoren vurderede til >50%; kræver {val} slutbekræftelse(r).", - "ml_off_delay": "Pauser bekræftet af slutdetektoren: p95 {p95}s + 60s buffer, fra {pauses} modelverificerede pauser på tværs af {cycles} cyklusser (gulv {floor}s).", "no_update_timeout": "Baseret på observeret opdateringskadence (p95={p95}s) * 20 (min 60s).", "off_delay_cadence": "Baseret på observeret opdateringskadence (p95={p95}s) * 5", "off_delay_device_floor": "Brugte et enhedsspecifikt sikkert minimum for {device} ({floor}s).", "off_delay_generic_floor": "Brugte et generisk sikkert minimum ({floor}s).", "off_delay_pauses": "Dimensioneret til at vare længere end reelle pauser: p95 for pause i cyklussen {p95}s + 60s buffer, fra {pauses} pauser på tværs af {cycles} rene cyklusser (gulv {floor}s).", - "profile_match_threshold": "p10-konfidens for {cycles} korrekte automatiske etiketter ({p10}); sikkert gulv for direkte matchning.", - "sampling_interval": "Median for opdateringsintervallet observeret på tværs af {cycles} rene cyklusser ({si}s).{excl}", - "smoothing_window": "Dimensioneret til at udjævne ~30s aflæsninger ved {si}s sampling ({samples} prøver).", - "start_duration": "Holdt kort (~ét samplingsinterval på {si}s), så detekteringen starter så tidligt som muligt, men stadig ignorerer enkelte spidser.", - "thr_batch": "Holdt lige over den laveste aktive effekt ved p05 på tværs af {cycles} cyklusser ({p05}W), så en start fanges så tidligt som muligt, og stoptærsklen forbliver under maskinens laveste driftseffekt.", - "tol_per_profile": "p75 for varighedsvariansen pr. profil på tværs af {profiles} profiler ({cycles} cyklusser); stramme profiler straffes ikke.", - "tol_pooled": "Baseret på samlet varighedsvarians for {cycles} seneste mærkede cyklusser (p95 afvigelse={dev}).", - "watchdog": "Holdt så lavt som forsvarligt (lige over p95-opdateringsmellemrummet på {p95}s og mindst 2x samplingintervallet på {median}s, min. 30s), så stilstande fanges hurtigt uden falske stop." + "standby_floor": "Alle nylige cyklusser trak stadig {idle}W, da de sluttede, så en tærskel derunder aldrig vil kunne registrere, at apparatet slukker. Holdt over denne grænse; de {cycles} rene cyklusser i oversigten pauserede aldrig under den i mere end {pause}s.", + "thr_batch": "Lige over den laveste aktive effekt (p05) over {cycles} cyklusser ({p05}W), så starter fanges tidligt, og stoptærsklen holdes under den laveste køreeffekt.", + "watchdog": "Holdt så lavt som forsvarligt (lige over p95-opdateringsmellemrummet på {p95}s og mindst 2x samplingintervallet på {median}s, min. 30s), så stilstande fanges hurtigt uden falske stop.", + "end_energy_floor": "Hævet til den nedre grænse, som stoptærsklen ({stop} W) i løbet af slukkeforsinkelsen ({off_delay} s) giver; under den kan cyklussen kun afsluttes via en reservevej.", + "max_duration_ratio_default": "Tilbage til standardværdien: et lavere loft afviser det rigtige program, når en kørsel varer længere end normalt.", + "min_duration_ratio_default": "Tilbage til standardværdien: en højere bundgrænse forhindrer en igangværende cyklus i at blive genkendt, før den er godt i gang." }, "exclusions": { "summary": "{total} fejldetekterede cyklus(ser) udelukket: {parts}.", @@ -1907,7 +1792,8 @@ "no_power": "ingen effekt", "no_active_power": "ingen aktiv effekt" } - } + }, + "now_value": "(nu {v})" }, "tab": { "advanced": "Avanceret", @@ -1937,6 +1823,11 @@ "wrong_profile": "Forkert profil" }, "toast": { + "task_name_required": "Giv opgaven et navn", + "task_added": "Opgave tilføjet", + "task_save_failed": "Kunne ikke gemme opgaven: {error}", + "task_removed": "Opgave fjernet", + "maint_logged": "Logget som udført: {task}", "catalog_refreshed": "Fællesskabskataloget er opdateret", "access_saved": "Adgangskontrol gemt", "all_wiped": "Alle data slettet", @@ -1944,7 +1835,6 @@ "auto_label_complete": "Auto-etiket fuldført", "automation_deleted": "Automatisering slettet", "correction_submitted": "Rettelse indsendt", - "cycle_deleted": "Cyklus slettet", "cycle_force_stopped": "Cyklus tvunget standset", "cycle_labelled": "Cyklus mærket", "cycle_paused": "Cyklus sat på pause", @@ -1960,11 +1850,9 @@ "group_deleted": "Gruppe slettet", "group_name_required": "Gruppenavn er påkrævet", "group_saved": "Gruppen er gemt", - "import_successful": "Importen lykkedes; integration genindlæsning", "json_required": "JSON-data er påkrævet", "legacy_removed": "Ældre handlinger blev fjernet", "logs_exported": "Logfiler eksporteret", - "matching_reverted": "Matchende vægte vendt tilbage til standardindstillingerne", "min_2_profiles": "Vælg mindst 2 profiler til en gruppe", "ml_training_no_improvement": "Træning gennemført: baseline bibeholdt (ingen forbedring)", "models_reverted": "Modeller på enheden er vendt tilbage til baseline", @@ -1974,8 +1862,8 @@ "pg_preset_loaded": "Forudindstillingen \"{name}\" er indlæst", "pg_preset_saved": "Forudindstillingen \"{name}\" er gemt", "pg_sugg_loaded": "{n} foreslåede(t) værdi(er) indlæst - kør legepladsen for at sammenligne", - "pg_sugg_ml_loaded": "{n} ML-kalibrerede(t) værdi(er) indlæst - kør legepladsen for at sammenligne", "phase_name_required": "Fasenavn er påkrævet", + "phase_reset": "\"{name}\" gendannet til den indbyggede fase", "phase_updated": "Fase opdateret", "phases_saved": "Faser gemt", "preferences_saved": "Præferencer gemt", @@ -2022,14 +1910,10 @@ "store_name_required": "Indtast et profilnavn", "store_import_failed": "Import mislykkedes: {error}", "store_imported": "Importeret til {profile}", - "store_program_required": "Indtast et programnavn", "store_no_appliance": "Angiv først dit apparats mærke og model i Indstillinger.", "store_share_failed": "Deling mislykkedes: {error}", "store_shared": "Delt i fællesskabsbutikken, afventer gennemgang.", "appliance_added": "Apparat tilføjet, afventer godkendelse", - "thanks_confirming": "Tak for bekræftelsen", - "device_approved": "Godkendt af fællesskabet", - "rating_saved": "Kvalitetsbedømmelse gemt", "brand_added": "Mærke tilføjet, afventer godkendelse", "profile_added": "Profil tilføjet, afventer godkendelse", "saved_except_conflicts": "Gemt. Løs de markerede konflikter for at gemme resten.", @@ -2053,13 +1937,58 @@ "odometer_save_failed": "Kunne ikke gemme totalen: {error}", "contrib_device_changed": "Tilføjet til fællesskabsbutikken, men apparatet blev skiftet, mens formularen var åben. Vælg det i Indstillinger.", "store_pick_profile_share": "Vælg en profil at dele til", - "remove_failed": "Fjernelse mislykkedes: {error}" + "remove_failed": "Fjernelse mislykkedes: {error}", + "analysis_complete": "Analyse færdig: {count} forslag", + "analysis_failed": "Analyse mislykkedes: {error}", + "auto_detect_failed": "Automatisk registrering mislykkedes: {error}", + "auto_label_failed": "Automatisk mærkning mislykkedes: {error}", + "auto_label_done": "Auto-mærkning fuldført: {count} cyklusser mærket, {skipped} ikke mærket.", + "auto_label_done_one": "Auto-mærkning fuldført: {count} cyklus mærket, {skipped} ikke mærket.", + "auto_label_done_other": "Auto-mærkning fuldført: {count} cyklusser mærket, {skipped} ikke mærket.", + "auto_label_cancelled": "Auto-mærkning annulleret efter mærkning af {count} cyklusser.", + "auto_label_cancelled_one": "Auto-mærkning annulleret efter mærkning af {count} cyklus.", + "auto_label_cancelled_other": "Auto-mærkning annulleret efter mærkning af {count} cyklusser.", + "could_not_load_cycle": "Kunne ikke indlæse cyklus: {error}", + "create_failed": "Oprettelse mislykkedes: {error}", + "cycles_deleted": "{count} cyklus(ser) slettet", + "debug_cleared": "{count} debug-spor ryddet", + "discard_failed": "Kassering mislykkedes: {error}", + "error": "Fejl: {error}", + "export_failed": "Eksport mislykkedes: {error}", + "import_failed": "Import mislykkedes: {error}", + "invalid_json": "\"{key}\" er ikke gyldig JSON - ret det, eller ryd feltet, før du gemmer.", + "label_failed": "Mærkning mislykkedes: {error}", + "ml_training_promoted": "Træning gennemført: {count} model(ler) aktiveret", + "pause_failed": "Pause mislykkedes: {error}", + "phase_created": "Fase \"{name}\" oprettet", + "phase_deleted": "Fase \"{name}\" slettet", + "profile_created": "Profil \"{name}\" oprettet", + "rename_failed": "Omdøbning mislykkedes: {error}", + "save_failed": "Lagring mislykkedes: {error}", + "split_complete": "Opdelt i {count} cyklusser", + "start_failed": "Start mislykkedes: {error}", + "stop_failed": "Stop mislykkedes: {error}", + "update_failed": "Opdatering mislykkedes: {error}", + "store_download_skipped": "{n} optagelse(r) sprunget over: for korte, med for mange huller eller allerede på dit apparat.", + "store_import_low_quality": "Denne optagelse er for kort, har for mange huller eller er urealistisk og kan ikke importeres.", + "store_import_duplicate": "Denne optagelse er allerede på dit apparat.", + "device_approved": "Godkendt af fællesskabet", + "thanks_confirming": "Tak for bekræftelsen", + "rating_saved": "Kvalitetsbedømmelse gemt", + "store_download_partial": "{n} program(mer) kunne ikke hentes; prøv igen senere.", + "import_no_restore_point": "Importeret, men der kunne ikke gemmes et gendannelsespunkt, så denne import kan ikke fortrydes", + "import_undo_failed": "Importen kunne ikke fortrydes: {error}", + "import_undone": "Import fortrudt: {profiles} profil(er) og {cycles} cyklus(ser) gendannet" }, "trend": { "down": "Trend nedad", "up": "Opadgående tendens" }, "maint": { + "salt": "Påfyld salt", + "rinse_aid": "Påfyld afspændingsmiddel", + "lint_filter": "Rengør fnugfilter", + "condenser_clean": "Rengør kondensator", "bearing_service": "Lejeservice", "descale": "Afkalk", "drum_clean": "Rengør tromle", @@ -2071,12 +2000,14 @@ "status_tip": "Cyklusresultat: Fuldført (naturlig afslutning), Afbrudt (pludseligt effektfald), Tvunget stoppet (manuelt) eller Kræver gennemgang (feedback afventer).", "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": "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.", + "energy_tip": "Samlet energiforbrug (kWh), beregnet ud fra effekt over tid.", + "cost_tip": "Energiomkostning for denne cyklus, fastlåst ved afslutning. Med Tidsvægtede omkostninger slået til beregnes hvert øjeblik til sin egen pris; ellers til prisen ved slutningen. Angiv en pris i Indstillinger for at udfylde dette.", "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.", - "cost_dynamic_tip": "Tidsvægtet: beregnet til den pris, der gjaldt under cyklussen (effektivt {price} {cur}/kWh)." + "cost_dynamic_tip": "Tidsvægtet: beregnet til den pris, der gjaldt under cyklussen (effektivt {price} {cur}/kWh).", + "dbg_conf": "Tillid", + "dbg_mae": "MAE", + "dbg_corr": "Korr." }, "stat": { "avg": "gns.", @@ -2100,7 +2031,10 @@ "notify_finish": "Der ville blive sendt en færdignotifikation.", "notify_milestone": "Der ville blive sendt en milepælsnotifikation.", "notify_held": "En notifikation blev holdt tilbage på grund af stilletimer.", - "finished": "Cyklussen nåede en sluttilstand og stoppede." + "finished": "Cyklussen nåede en sluttilstand og stoppede.", + "match_reverted": "Programmet faldt tilbage til registrering: dets score faldt et godt stykke under sit eget højdepunkt.", + "verified_pause": "Live-justeringen bekræftede (eller frigav) en pause inde i programmet. Så længe den gælder, afsluttes cyklussen ikke.", + "stalled": "Cyklussen lå på standbyeffekt længere, end dette program nogensinde holder pause dér, med arbejde tilbage. Vises som Pause (Gået i stå); cyklussen forbliver åben." }, "store": { "search_ph2": "Søg efter mærke eller model…", @@ -2109,11 +2043,7 @@ "programs_count": "Programmer: {n}", "browse": "Gennemse", "device": "Enhed", - "favorites": "Favoritter", - "search_ph": "Søg efter mærke eller model…", "no_results": "Ingen matchende apparater fundet. Prøv en anden søgning.", - "n_programs": "{n} programmer", - "n_cycles": "{n} cyklusser", "no_programs": "Ingen delte programmer for dette apparat endnu.", "no_cycles": "Ingen referencecyklusser delt for dette program endnu.", "anon": "anonym", @@ -2125,19 +2055,21 @@ "merge_target": "Flet ind i profil", "description": "Beskrivelse (valgfrit)", "contributed_by": "af {name}", + "website": "Fællesskabsbutikkens websted ↗", + "website_tip": "Åbn fællesskabsbutikkens websted", + "favorites": "Favoritter", "rating_summary": "★ {avg} ({n})", "no_ratings": "Ingen bedømmelser endnu", - "website": "Fællesskabsbutikkens websted ↗", - "website_tip": "Åbn fællesskabsbutikkens websted" + "unreachable": "Kunne ikke få forbindelse til fællesskabsbutikken. Tjek din forbindelse, og prøv igen." }, "settings": { "gear": { - "title": "Indstillinger" + "title": "Præferencer" } }, "link": { - "manual": "Manual ↗", - "add_appliance": "Tilføj dit apparat" + "maintenance_supporter": "Maintenance Supporter på GitHub ↗", + "manual": "Manual ↗" }, "tip": { "add_brand": "Mærke ikke på listen? Tilføj det til fællesskabskataloget", @@ -2171,7 +2103,13 @@ "rebuild": { "envelopes": "Genopbygger envelopes" }, - "cancelling": "Annullerer..." + "cancelling": "Annullerer...", + "auto_label": { + "running": "Mærker cyklusser automatisk" + }, + "store_download": { + "running": "Henter fra butikken" + } }, "setup": { "hdr": { @@ -2180,26 +2118,24 @@ }, "phase0": { "washer": "WashData registrerer allerede dine cyklusser. Optag din første cyklus for at aktivere programnavne og tidsestimater.", - "dishwasher": "WashData holder øje. Opvaskemaskiner har komplekse cyklusser – det anbefales stærkt at optage din første cyklus. Hvis en registreret cyklus kører for længe, brug cyklus-editoren til at beskære den, inden du gemmer den som en profil.", + "dishwasher": "WashData holder øje. Opvaskecyklusser er komplekse, så optag din første cyklus. Hvis en registreret cyklus kører for længe, så beskær den i cykluseditoren, før du gemmer den som en profil.", "generic": "WashData holder øje. Optag eller mærk en registreret cyklus for at begynde at opbygge profiler." }, "phase1a": { - "labelled": "Godt gået – dit første program er gemt. For de reneste data kan du overveje at optage din næste cyklus med optagelses-widgetten." + "labelled": "God start: dit første program er gemt. For de reneste data skal du optage din næste cyklus med optageren." }, "phase1b": { - "recorded": "Din optagelse blev gemt som {profile_name}. Optag nu eller mærk dine andre hyppigt brugte programmer for at opbygge dækning." + "recorded": "Din optagelse blev gemt som {profile_name}. Optag eller mærk nu dine andre almindelige programmer." }, "phase1c": { - "verify": "Du har {count} programmer fra fællesskabet. Kør en cyklus for at bekræfte, at WashData genkender den korrekt – matchningen forbedres, efterhånden som din enhed opbygger sin egen historik." + "verify": "Du har {count} programmer fra fællesskabet. Kør en cyklus for at tjekke, at WashData genkender den; matchningen bliver bedre, efterhånden som din enhed opbygger sin egen historik." }, "phase2": { - "cluster": "WashData har set {count} cyklusser, der ikke matcher noget gemt program – de ligner hinanden. Vil du oprette en ny profil til dem?", + "cluster": "WashData har set {count} ens cyklusser, der ikke matcher noget gemt program. Opret en ny profil til dem?", "unmatched": "Din seneste cyklus matchede ikke noget gemt program. Er det et nyt program?" }, "phase3": { - "suggestions": "WashData har indstillingsanbefalinger baseret på din cyklushistorik – gennemgå dem for at forbedre registreringens nøjagtighed.", - "groups": "Nogle af dine profiler ligner det samme program ved forskellige temperaturer. Organiser dem i en gruppe for bedre matchning.", - "phases": "Tilføj programfaser til {profile_name} for mere præcise estimater af den resterende tid." + "suggestions": "WashData har indstillingsforslag ud fra din cyklushistorik. Gennemgå dem for at forbedre registreringen." }, "phase4": { "healthy": "Dette apparat er fuldt opsat ({profile_count} profiler)." @@ -2212,12 +2148,58 @@ "create_from_cluster": "Opret profil fra disse cyklusser", "create_profile": "Opret en profil til den", "review_suggestions": "Gennemgå anbefalinger", - "organise_profiles": "Organiser profiler", - "configure_phases": "Konfigurer faser", "skip_step": "Spring dette trin over", "skip_forever": "Vis ikke igen", "hide_guidance": "Skjul vejledning", "show_guidance": "Vis vejledning" } + }, + "access": { + "edit": "Rediger", + "full": "Fuld", + "inherit": "Arv", + "none": "Ingen (skjult)", + "read": "Læs" + }, + "pg_label_reason": { + "no_winner": "Intet program passede", + "below_floor": "Under læringskonfidensen", + "ambiguous": "Markeret som tvetydig", + "margin": "For tæt på nummer to", + "unknown_profile": "Programmet findes ikke længere", + "not_evaluated": "Ikke evalueret" + }, + "diagram": { + "accum": "opsamlet", + "auto": "auto", + "below_off": "under = slukket", + "confirmed": "bekræftet", + "energy_counts": "energi tæller", + "feedback": "feedback", + "gap_below_min": "mellemrum under min = én cyklus", + "heat_phase": "varmefase", + "held_at_100": "holdt på 100%", + "ignored": "ignoreret", + "kept": "beholdt", + "max": "maks", + "min": "min", + "minus_tol": "-tol", + "no_match": "intet match", + "no_updates": "ingen opdateringer", + "off_delay_wait": "slukkeforsinkelse", + "off_gap": "slukket mellemrum", + "plus_tol": "+tol", + "profile": "profil", + "reading_interval": "typisk målingsinterval", + "sensor_offline": "sensor offline: tving stop", + "si": "SI", + "spike_ignored": "spids ignoreret", + "spike_ignored_short": "spids: ignoreret", + "start": "start", + "stop": "stop", + "tail_energy_resets": "restenergi > tærskel: timer nulstilles", + "thr": "tærsk.", + "too_short": "for kort", + "tumble_kept": "tumlepulser beholdt" } } diff --git a/custom_components/ha_washdata/translations/panel/de.json b/custom_components/ha_washdata/translations/panel/de.json index c9215687..8c35d9a6 100644 --- a/custom_components/ha_washdata/translations/panel/de.json +++ b/custom_components/ha_washdata/translations/panel/de.json @@ -3,6 +3,7 @@ "min": "Min.", "entity_not_found": "Entität nicht gefunden", "running_long": "Läuft länger", + "stalled": "Stillstand", "remaining": "verbleibend", "progress": "Fortschritt", "show_time_remaining": "Verbleibende Zeit anzeigen", @@ -48,16 +49,16 @@ } }, "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_tip": "Gegen Ende wird dieses Programm etwa {mins} Min. still und zieht dann etwa {watts} W für {secs} s. In {seen} von {measured} gemessenen Zyklen beobachtet.", "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", + "artifact_tip": "{n} Anomalien erkannt (z. B. Tür mitten im Zyklus geöffnet) - öffnen, um sie in der Grafik anzuzeigen", + "artifact_tip_one": "{n} Anomalie erkannt (z. B. Tür mitten im Zyklus geöffnet) - öffnen, um sie in der Grafik anzuzeigen", + "artifact_tip_other": "{n} Anomalien erkannt (z. B. Tür mitten im Zyklus geöffnet) - öffnen, um sie in der Grafik anzuzeigen", "auto": "(automatisch erkannt)", "built_in_tag": "eingebaut", "declining": "↘ rückläufig", "energy_low": "Niedrigerer Energieverbrauch als gewöhnlich", "energy_spike": "Höherer Energieverbrauch als gewöhnlich", - "fair_fit": "akzeptable Qualität", - "fair_fit_tip": "Mäßige Übereinstimmungskonsistenz – einige diesem Profil zugewiesene Zyklen weisen niedrigere Konfidenzwerte auf. Beschriften Sie weitere Zyklen oder zeichnen Sie das Profil neu auf, um die Genauigkeit zu verbessern.", "feedback_requested": "Feedback erbeten", "golden_cycle": "Aufgezeichneter Referenzzyklus", "duration_outlier": "{n} Zyklus/Zyklen mit untypischer Dauer", @@ -67,26 +68,37 @@ "overrun": "Bin länger gelaufen als gewöhnlich", "underrun": "Schneller als gewöhnlich abgeschlossen", "poor_fit": "schlechte Qualität", - "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", + "restart_gap_tip": "{n} HA-Neustart-Lücken: Leistungsverlauf hat Lücken", + "restart_gap_tip_one": "{n} HA-Neustart-Lücke: Leistungsverlauf hat eine Lücke", + "restart_gap_tip_other": "{n} HA-Neustart-Lücken: Leistungsverlauf hat Lücken", "reviewed": "Bewertet", "steady": "→ stetig", - "using_defaults": "Verwendung der Standardeinstellungen", - "using_tuned": "Verwendung abgestimmter Gewichte", "overrun_ratio": "({x}x erwartet)", "underrun_ratio": "({pct}% des Erwarteten)", - "awaiting_tip": "Warten auf Freigabe durch die Community", - "awaiting_n": "Warten auf Freigabe · {n} bestätigt", - "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.", - "armed": "(gilt für den nächsten Zyklus)" + "armed": "(gilt für den nächsten Zyklus)", + "awaiting_tip": "Warten auf Freigabe durch die Community", + "awaiting_n": "Warten auf Freigabe · {n} bestätigt", + "awaiting": "Warten auf Freigabe", + "approved": "Freigegeben", + "matcher_unused": "Noch in keinem Ihrer Zyklen erkannt", + "matcher_used": "In {n} Ihrer Zyklen erkannt", + "matcher_used_one": "In {n} Ihrer Zyklen erkannt", + "matcher_used_other": "In {n} Ihrer Zyklen erkannt", + "matcher_used_tip": "Zyklen auf diesem Gerät, die WashData selbstständig diesem importierten Programm zugeordnet hat. Ein Import, der nie zugeordnet wird, passt vielleicht nicht zu Ihrem Gerät; Sie können ihn entfernen. Lokal gezählt; nichts wird an den Community-Store gesendet." }, "btn": { + "log_done_task": "Als erledigt protokollieren: {task}", + "add_task": "Aufgabe hinzufügen", + "pg_preset_delete": "Voreinstellung löschen", + "pg_preset_load": "Voreinstellung laden", + "pg_preset_load_tip": "Die Werte im Testbereich durch diese Voreinstellung ersetzen (die aktiven Einstellungen bleiben unverändert)", + "pg_preset_save": "Als Voreinstellung speichern", + "pg_preset_save_tip": "Alle Werte unten als benannte Voreinstellung für dieses Gerät speichern", "reset_zoom": "Zoom zurücksetzen", "set_brand_model": "Marke und Modell festlegen", "refresh_catalog": "Katalog aktualisieren", @@ -97,12 +109,11 @@ "add_timer": "+ Timer hinzufügen", "apply_all": "Alles anwenden", "apply_label": "Etikett anwenden", - "apply_set_b": "Set B anwenden", "apply_split": "Teilung anwenden", "apply_trim": "Zuschnitt anwenden", "auto_detect_split": "Automatische Erkennung", "auto_label_cycles": "Auto-Label-Zyklen", - "auto_label_cycles_tip": "Weist unbeschrifteten Zyklen, deren Übereinstimmungskonfidens den Schwellenwert überschreitet, automatisch Profilnamen zu", + "auto_label_cycles_tip": "Unbeschriftete Zyklen benennen, deren Übereinstimmungskonfidenz den Schwellenwert erreicht.", "cancel": "Abbrechen", "clear_debug": "Debug-Daten löschen", "clear_splits": "Leeren", @@ -112,8 +123,6 @@ "convert_to_automation": "Auf Automatisierung umstellen", "correct": "Richtig…", "create": "Erstellen", - "create_from_cluster": "Profil aus {count} Zyklen erstellen", - "create_profile": "+ Profil erstellen", "delete": "Löschen", "delete_group": "Gruppe löschen", "delete_group_tip": "Nur diese Gruppe löschen – die Mitgliederprofile bleiben erhalten", @@ -124,22 +133,19 @@ "done": "Erledigt", "edit": "Bearbeiten", "export": "Exportieren", - "export_json": "Export nach JSON", "force_stop": "Stopp erzwingen", "force_stop_tip": "Aktuellen Zyklus sofort beenden und als erzwungen gestoppt markieren", "from_template": "Aus Vorlage ▾", "ignore": "Ignorieren", "import_json": "Import aus JSON", - "import_overwrite": "Importieren (Daten überschreiben)", "inspect": "Überprüfen", "label": "Beschriften", - "load": "Laden", "load_more": "Mehr laden", "manage": "Verwalten", "merge": "Verschmelzen", "new_automation": "+ Neue Automatisierung", "new_group": "+ Neue Gruppe", - "new_group_tip": "Nahezu identische Profile gruppieren (gleiche Form/Dauer, unterschiedliche Temperatur oder Schleuderstufe), damit das Matching zuverlässig zwischen ihnen wählt", + "new_group_tip": "Nahezu identische Profile gruppieren (gleiche Form, andere Temperatur oder Schleuderstufe), damit der Abgleich sie unterscheiden kann.", "new_phase": "+ Neue Phase", "new_profile": "+ Neues Profil", "new_profile_tip": "Neues Programmprofil aus einem vorhandenen beschrifteten Zyklus oder einer Aufnahme erstellen", @@ -147,36 +153,25 @@ "on_cycle_started": "Ein Zyklus gestartet", "pause_cycle": "Pause", "pause_cycle_tip": "Laufenden Zyklus anhalten – das Gerät setzt dort fort, wo es aufgehört hat", - "pg_apply_device": "Auf Gerät anwenden", - "pg_autofill": "Automatisch ausfüllen", - "pg_load_calibrated": "Kalibrierte (ML) laden ({n})", - "pg_load_calibrated_tip": "Lädt ML-kalibrierte Einstellungen als Testbereich-Überschreibungen", "pg_load_live": "Aktive Einstellungen laden", "pg_load_live_tip": "Die aktuellen Einstellungen der Integration neu einlesen und alle Änderungen im Testbereich verwerfen", "pg_load_suggested": "Vorschläge laden ({n})", "pg_load_suggested_tip": "Lädt die aktuellen Vorschläge des Auto-Tuners als Testbereich-Überschreibungen", - "pg_preset_delete": "Voreinstellung löschen", - "pg_preset_load": "Voreinstellung laden", - "pg_preset_load_tip": "Die Werte im Testbereich durch diese Voreinstellung ersetzen (die aktiven Einstellungen bleiben unverändert)", - "pg_preset_save": "Als Voreinstellung speichern", - "pg_preset_save_tip": "Alle Werte unten als benannte Voreinstellung für dieses Gerät speichern", "pg_publish_all": "{n} in Integration übernehmen", "pg_publish_one": "{label} in die Integration übernehmen", - "play": "Abspielen", "process": "Verarbeiten", "process_history": "Jetzt verarbeiten", "process_recording": "Aufnahme speichern", "process_tip": "Aufgezeichnete Datenspur als neues oder vorhandenes Profil speichern", "rebuild": "Umschläge neu erstellen", "rebuild_envelope": "Umschlag neu erstellen", - "rebuild_tip": "Erwarteten Leistungsumschlag (Min./Max.-Band) für alle Profile aus deren beschrifteten Zyklen neu berechnen – nach dem Beschriften neuer oder Korrigieren alter Zyklen ausführen", + "rebuild_tip": "Das erwartete Leistungsband jedes Profils aus seinen beschrifteten Zyklen neu berechnen. Nach dem Beschriften oder Korrigieren von Zyklen ausführen.", "rec_start_tip": "Leistungsaufzeichnung des Geräts starten – kurz vor dem Starten eines Zyklus beginnen", "rec_stop": "Stoppen", "rec_stop_tip": "Aufnahme stoppen und die erfasste Datenspur zur Überprüfung bereitstellen", "record": "Aufnahme starten", "refresh": "Aktualisieren", "refresh_diag_tip": "Speicherstatistiken neu laden", - "refresh_logs_tip": "Aktuelle Protokolleinträge neu laden", "refresh_settings_tip": "Einstellungen vom Server neu laden", "relabel": "Neu beschriften ({count})", "remove": "Entfernen", @@ -184,7 +179,6 @@ "reset": "Zurücksetzen", "reset_ml_models_tip": "Verwerfen Sie die fein abgestimmten Modelle und kehren Sie zu den eingebauten zurück. WashData kann sie später wieder erlernen.", "reset_to_builtin": "Zurücksetzen auf integrierte Modelle", - "reset_to_defaults": "Auf Standardeinstellungen zurücksetzen", "resume_cycle": "Fortsetzen", "resume_cycle_tip": "Angehaltenen Zyklus fortsetzen", "revert_settings": "Änderungen verwerfen", @@ -197,8 +191,6 @@ "run_analysis": "🔍 Vorschlagsanalyse ausführen", "run_analysis_tip": "Analysieren Sie jetzt Ihre aufgezeichneten Zyklen und aktualisieren Sie Tuning-Vorschläge", "run_auto_label": "Auto-Label ausführen", - "run_both": "A gegen B ausführen", - "run_simulation": "Simulation ausführen", "save": "Speichern", "save_access_control": "Zugriffskontrolle speichern", "save_group": "Gruppe speichern", @@ -210,10 +202,8 @@ "save_settings": "Einstellungen speichern", "select": "Wählen", "select_all": "Alle auswählen", - "select_last20": "Letzte 20", "select_none": "Leeren", "show_only": "nur Anzeigen", - "skip_setup": "Einrichtung überspringen", "split": "Teilen", "stop": "Stopp", "submit_correction": "Korrektur einreichen", @@ -222,36 +212,32 @@ "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.", + "unlabel_selected_tip": "Die Beschriftung der ausgewählten Zyklen entfernen. Sie bleiben in Ihrem Verlauf, prägen dieses Profil aber nicht mehr.", "use": "Verwenden", "wipe_all": "Alle Daten löschen", + "pg_rerun": "Simulation aktualisieren", "delete_selected": "Auswahl löschen ({n})", - "group_suggest": "Gruppe {n}: {members}", "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", "import": "Importieren", "disconnect": "Trennen", "connect_github": "Mit GitHub verbinden", - "save_appliance": "Gerät speichern", "share_to_store": "Im Store teilen", "share": "Teilen", - "confirm_appliance": "Dieses Gerät bestätigen", - "confirmed": "Bestätigt", "browse_community_setups": "Community-Setups durchsuchen", "download_device": "Dieses Setup herunterladen", "share_device": "⬆ Dieses Gerät teilen", - "share_device_tip": "Dieses Gerät und seine aufgezeichneten Referenzzyklen im Community-Store teilen, damit andere Nutzer mit demselben Gerät sie übernehmen können", + "share_device_tip": "Dieses Gerät und seine Referenzzyklen im Community-Store teilen, damit Besitzer desselben Geräts sie übernehmen können.", "share_n": "{n} Zyklen teilen", "export_selected": "Exportieren (Daten wählen)", "export_all": "Alles schnell exportieren", - "import_raw": "Erweitert: alles aus JSON ersetzen", "download_export": "Export herunterladen", "analyze_import": "Datei analysieren", "import_selected": "Auswahl importieren", "back": "Zurück", "mute_suggestion": "Diese Einstellung nicht mehr vorschlagen", "reset_muted": "Stummschaltungen zurücksetzen", + "reset_phase_tip": "Ihre Änderung rückgängig machen und die eingebaute Phase wiederherstellen", "import_power_history": "Leistungsverlauf importieren", "hist_read_recorder": "Aus Home Assistant lesen", "hist_scan": "Nach Zyklen suchen", @@ -260,7 +246,12 @@ "save_total_cycles": "Gesamtzahl speichern", "clear": "Leeren", "add_brand": "Marke hinzufügen", - "add_appliance": "Gerät hinzufügen" + "add_appliance": "Gerät hinzufügen", + "retry": "Erneut versuchen", + "confirm_appliance": "Dieses Gerät bestätigen", + "confirmed": "Bestätigt", + "create_profile_from_gap": "Profil erstellen", + "undo_import": "Letzten Import rückgängig machen" }, "note": { "off_delay": { @@ -268,10 +259,6 @@ } }, "conflict": { - "anti_wrinkle_exit": { - "exit": "Muss unter dem Stop-Schwellenwert ({stop} W) liegen; andernfalls wird die Knitterschutz-Abschaltleistung ignoriert", - "stop": "Muss über der Knitterschutz-Abschaltleistung ({exit} W) liegen" - }, "anti_wrinkle_max": { "max": "Muss über dem Start-Schwellenwert ({start} W) liegen; andernfalls wird das Knitterschutz-Dauerlimit umgangen", "start": "Muss unter der maximalen Knitterschutz-Leistung ({max} W) liegen" @@ -309,14 +296,14 @@ "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." + "power_sensor": "Das ist der Leistungssensor des Geräts, kein Preis. Zyklen würden mit Watt pro kWh berechnet; leeren Sie dieses Feld.", + "energy_sensor": "Das ist der Energiezähler des Geräts, kein Preis. Zyklen würden mit dem Zählerstand pro kWh berechnet; leeren Sie dieses Feld.", + "device_class": "Dieser Sensor misst {dc}, keinen Preis pro kWh. Leeren Sie dieses Feld oder wählen Sie einen Tarifsensor.", + "unit": "Dieser Sensor liefert Werte in {unit}, keinen Preis pro kWh. Leeren Sie dieses Feld oder wählen Sie einen Tarifsensor." }, "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" + "duration": "Muss kleiner als das Timeout ohne Aktualisierung ({to} s) sein, damit der Festsitzen-Alarm zuerst auslösen kann", + "timeout": "Muss größer als die Dauer der feststeckenden Pumpe ({ps} s) sein, damit der Festsitzen-Alarm zuerst auslösen kann" }, "start_dur": { "sampling": "Abtastintervall überschreitet die Startdauer ({sdt} s); einzelne Messwertspitzen können einen Zyklus öffnen", @@ -334,10 +321,15 @@ "sampling": "Abtastintervall sollte höchstens die Hälfte des Watchdog-Intervalls ({wi} s) betragen" }, "settings_banner": "Einstellungskonflikte: {n}. Prüfen Sie die markierten Abschnitte und beheben Sie sie vor dem Speichern.", - "settings_banner_btn": "Zum ersten" + "settings_banner_btn": "Zum ersten", + "end_energy": { + "energy": "Zu streng für Stoppschwellenwert ({w} W) während der Abschaltverzögerung ({d} s); der Zyklus kann nur über einen Ausweichpfad enden", + "stop": "Endenergie ({e} Wh in {d} s) lässt nur {p} W zu" + } }, "hdr": { - "ab_comparison": "A/B-Einstellungsvergleich", + "custom_tasks": "Ihre eigenen Aufgaben", + "maintenance_supporter": "Benötigen Sie mehr?", "access_control": "Zugangskontrolle", "add_maintenance": "Wartungsereignis hinzufügen", "automation_delete": "Löschen Sie diese Automatisierung", @@ -346,28 +338,22 @@ "automation_review_this_cycle": "Sehen Sie sich diesen Zyklus noch einmal an", "automations": "Automatisierungen", "clear_debug": "Debug-Traces löschen", - "cycle_simulator": "Zyklussimulator", "diagnostics_pref": "Diagnose", "display": "Anzeige", - "dtw_inspector": "DTW-Inspektor", "export_import": "Exportieren / Importieren", - "getting_started": "Erste Schritte", "live_power": "Live-Power", "logs": "Protokolle", - "logs_diagnostics": "Diagnose", "maintenance": "Wartungsmaßnahmen", "maintenance_log": "Wartungsprotokoll", "maintenance_reminders": "Service-Erinnerungen", "manual_recording": "Manuelle Aufnahme", - "manual_recording_tip": "Starten Sie einen Zyklus absichtlich, während WashData die Leistungskurve aufzeichnet. Klicken Sie auf Starten kurz bevor das Gerät beginnt, auf Stoppen wenn es fertig ist, und dann auf Verarbeiten, um es als benanntes Profil zu speichern.", + "manual_recording_tip": "Einen Zyklus gezielt aufnehmen: Drücken Sie kurz bevor das Gerät startet auf Start, nach dem Ende auf Stoppen und dann auf Verarbeiten, um ihn als Profil zu speichern.", "ml_learned": "Was WashData gelernt hat", - "ml_matching_tuning": "Feinabstimmung der Programmanpassung", "ml_settings_card": "Einstellungen", "ml_smart_learning": "Intelligentes Lernen", "my_preferences": "Meine Vorlieben", "no_automations": "Noch keine Automatisierungen verweisen auf dieses Gerät.", "panel_settings": "Panel-Einstellungen", - "param_sweep": "Parameter-Sweep", "pg_settings_source": "Herkunft der Einstellungen", "phase_catalog": "Phasenkatalog", "playground": "Testbereich", @@ -376,11 +362,8 @@ "status": "Status", "status_graph": "Statusdiagramm", "storage_stats": "Speicherstatistiken", - "test_history": "Auf Verlauf testen", "toggle_sidebar": "Schalten Sie die Seitenleiste von Home Assistant um", - "tools_and_data": "Tools & Daten", "wipe_history": "Verlauf löschen", - "recommendations": "Empfehlungen ({n})", "pg_detection_params": "Erkennungseinstellungen", "pg_outcome": "Simulationsergebnis", "pg_across_cycles": "Über deine Zyklen hinweg", @@ -388,7 +371,8 @@ "online_account": "Community-Store und Onlinefunktionen", "import_power_history": "Leistungsverlauf importieren", "service_status": "Wartungsstatus", - "online_account_tab": "Online & Community" + "online_account_tab": "Online & Community", + "live_match_debug": "Live-Match-Debug" }, "health": { "fair": "Akzeptable Übereinstimmungsqualität", @@ -396,7 +380,12 @@ "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.", + "task_name": "Aufgabe", + "every_cycles": "Alle (Zyklen)", + "every_days": "Alle (Tage)", + "new_task_name": "Neue Aufgabe", + "days_since_service": "{since} / {total} Tage", + "envelope_position_tip": "Position auf der aufgezeichneten Kurve des zugeordneten Programms: {pct} %. Ermittelt durch Abgleich des Zyklus mit dem Profil, daher stimmt sie auch bei längeren oder kürzeren Läufen. Wird aktualisiert, während das Gerät still ist, und kann daher 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})", @@ -408,10 +397,8 @@ "aria_cycle_chart": "Leistungsverlauf des Zyklus", "aria_envelope_chart": "Leistungshüllkurve des Profils", "aria_phase_chart": "Diagramm des Phaseneditors", - "aria_playground_chart": "Leistungsverlauf des Zyklus mit verschiebbaren Erkennungsschwellen", "aria_power_chart": "Leistungsdiagramm", "aria_spaghetti_chart": "Überlagerte Leistungsverläufe der Zyklen", - "aria_sweep_chart": "Ergebnisdiagramm des Parameterdurchlaufs", "artifact_anomaly": "Anomalie", "artifact_high_power": "Hohe Leistung", "artifact_interruption": "Unterbrechung", @@ -425,155 +412,105 @@ "cohesion_good": "Kohäsion {pct} %", "cohesion_low": "⚠ niedrige Kohäsion {pct} %", "compare_profiles": "Mit Profilen vergleichen", - "concurrency": "Parallelität", "confidence": "Vertrauen", "confidence_threshold": "Vertrauensschwelle", "correct_duration": "Richtige Dauer (Min., optional)", "correct_profile": "Richtiges Profil", - "correlation": "Korrelation", "cost": "Kosten", "create_new_profile": "+ Neues Profil erstellen…", "current_phase": "Aktuelle Phase", - "curves": "Kurven", "cycle": "Zyklus", "cycle_date_display": "Anzeige des Zyklusdatums", - "cycle_health": "Zyklusgesundheit", - "cycle_index": "Zyklusindex →", - "cycles": "Zyklen", + "cycle_context": "Recorder-Verlauf rund um den Zyklus", + "cycle_context_off": "Aus", + "cycle_context_minutes": "{min} Min.", + "cycle_context_series": "Recorder-Verlauf", + "before_start": "Vor dem Start", + "after_end": "Nach dem Ende", "cycles_count": "Zyklen", "cycles_lc": "Zyklen", - "dashed": "gestrichelt", "date": "Datum", "debug_traces": "Debug-Traces", - "default": "Standard", "default_access_level": "Standardebene für Benutzer, die unten nicht aufgeführt sind", "default_other": "Standard (andere Geräte)", "default_tab": "Standardregisterkarte beim Öffnen des Panels", - "delta": "Δ", "description": "Beschreibung", - "dtw_blend_w": "Mischgewicht", - "dtw_blended": "Gemischter Score", - "dtw_ddtw": "DDTW-Score", - "dtw_l1": "L1-Score", - "dtw_matched_profile": "(zugeordnetes Profil des Zyklus)", "duration": "Dauer", - "duration_agreement": "Dauerübereinstimmung", - "emphasis": "Schwerpunkt", "enable_access_control": "Aktivieren Sie die Zugriffskontrolle pro Benutzer", "energy": "Energie", - "energy_agreement": "Energieübereinstimmung", - "event_timeline": "Ereignis-Zeitachse", "event_type": "Ereignistyp", "expected": "Erwartet", + "suspected_profile": "{name} (vermutet)", + "runner_up_profile": "{name} (Zweitplatzierter)", "expected_duration": "Erwartete Dauer (Min.)", "file_kb": "Datei (kB)", - "final_score": "Endscore", "from": "Von", "gap_s": "Lücke (s)", "group_name": "Gruppenname", "head_trim": "Anfang kürzen (s)", - "health": "Gesundheit", "hide_tabs": "Tabs für Nicht-Administratoren ausblenden", - "in_use": "Im Einsatz", "json_data": "JSON-Daten", "label": "Zugeordnetes Programm", "last": "Letzte", "last_checked": "Zuletzt überprüft:", "load_from_file": "Aus Datei laden", - "mae_score": "MAE-Score", "manual_duration": "Manuelle Dauer (min., optional)", "match": "Übereinstimmung", "members": "Mitglieder", - "metric": "Metrik", "mode_existing_profile": "Zum vorhandenen Profil hinzufügen", "mode_new_profile": "Neues Profil erstellen", "models_fine_tuned": "(feinabgestimmte Modelle: {count})", - "n_classic_suggestions": "{n} klassisch", - "n_ml_suggestions": "{n} ML", "n_selected": "{n} ausgewählt", - "n_tuning_suggestions": "{n} Abstimmungsvorschlag", - "na": "n/a", + "n_tuning_suggestions": "{n} Abstimmungsvorschläge", + "n_tuning_suggestions_one": "{n} Abstimmungsvorschlag", + "n_tuning_suggestions_other": "{n} Abstimmungsvorschläge", "new_profile_name": "Neuer Profilname", "notes": "Notizen", - "of": "von", - "outcome": "Ergebnis", "panel_default_tab": "Standard-Tab", "panel_language": "Panelsprache", "per_cycle_short": "Zyklus", - "pg_autofill_tip": "Bereich aus aktuellem Wert automatisch ausfüllen ({lo}–{hi})", "pg_best_value": "Bester gefundener Wert", "pg_current_value": "Aktueller Wert:", "pg_drag_hint": "Linie im Diagramm ziehen", - "pg_dtw": "DTW", "pg_ev_ending": "Endet", "pg_ev_running": "Läuft", "pg_group_advanced": "Sonderfälle", "pg_group_detection": "Erkennungsauslöser", "pg_group_timing": "Zeitregeln", "pg_idle": "Leerlauf", + "pg_poor_match": "Schlechte Übereinstimmung", "pg_preset": "Testbereich-Voreinstellung", "pg_preset_name": "Name der neuen Voreinstellung", "pg_preset_none": "Voreinstellung auswählen…", - "pg_stress_group": "Leerlauferkennungstest", - "pg_stress_idle_auto": "Auto", - "pg_stress_idle_w": "Leerlaufpegel (W)", - "pg_stress_idle_w_desc": "Automatisch erkannten Bereitschafts-Basiswert überschreiben (leer lassen für automatisch).", - "pg_stress_toggle": "Leerlauferkennung testen", - "pg_stress_toggle_desc": "Simuliert, dass das Gerät nach dem Ende der Aufnahme weiterhin Bereitschaftsstrom verbraucht - zeigt, ob und wann WashData den Zyklus beendet.", - "pg_last_run": "Letzter Lauf: {date}", - "pg_match_score": "Übereinstimmungswert", - "pg_pct_of": "{p}% von {total}", - "pg_poor_match": "Schlechte Übereinstimmung", "pg_strong_match": "Starke Übereinstimmung", - "pg_tile_ambiguous": "Mehrdeutig", - "pg_tile_ambiguous_tip": "Zyklen mit einem knappen Gleichstand zwischen zwei Programmen", - "pg_tile_detected": "Erkannt", - "pg_tile_detected_tip": "Zyklen, bei denen die Leistung lange genug über dem Startschwellenwert lag, um als laufend zu gelten", - "pg_tile_matched": "Zugeordnet", - "pg_tile_matched_tip": "Zyklen, die korrekt einem bekannten Programm zugeordnet wurden", - "pg_time_left": "Verbleibende Zeit", "pg_weak_match": "Schwache Übereinstimmung", "phase_name": "Phasenname", "name": "Name", "phase_timeline": "Phasen-Zeitachse", "power": "Leistung", "profile": "Profil", - "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. Wählen Sie eines aus, bevor Sie das Gerät starten, dann wird es angewendet, sobald der nächste Zyklus beginnt.", + "program_tip": "Wählen Sie, welchem Programm der aktuelle Zyklus zugeordnet wird. Automatisch erkennen wählt die beste Übereinstimmung. Heften Sie ein Programm an, wenn die automatische Erkennung falsch liegt; vor dem Start gewählt, gilt es für den nächsten Zyklus.", "progress": "Fortschritt", "raw_socket": "Rohsignal", - "read_only": "Schreibgeschützt", "ref_cycle": "Referenzzyklus (optional)", - "refresh_interval": "Aktualisierungsintervall (s)", "remaining": "Übrig", "reminder_every": "Erinnern alle (Zyklen)", "remove_label": "- Etikett entfernen -", "rename_profile": "Profil umbenennen", - "replay_duration": "Wiedergabe", "resulting_profile": "Resultierendes Profil", "save_mode": "Speichermodus", - "score": "Score", - "score_breakdown": "Score-Aufschlüsselung", "seconds_from_start": "Sekunden vom Start", "select_profile": "Profil auswählen", "setting": "Einstellung", - "settings_a": "Einstellungen A (aktuell)", - "settings_a_short": "A", "settings_advanced": "Erweitert", - "settings_b": "Einstellungen B (Test)", - "settings_b_short": "B", "settings_basic": "Grundlegend", "settings_detail_level": "Detailgrad der Einstellungen", - "settings_overrides": "Einstellungsüberschreibungen", "show_debug": "Live-Match-Debug-Karte auf der Statusseite anzeigen (Konfidenz, Unklarheit, Spitzenkandidaten)", "show_expected": "Erwartete Kurvenüberlagerung anzeigen (angepasstes Profil, orange)", "show_raw": "Umschalter für rohe Steckdosenwerte im Live-Leistungsdiagramm anzeigen", "sparkline": "Trend der letzten Zyklusdauern", - "stage2": "Stufe 2 – Kernähnlichkeit", - "stage3": "Stufe 3 – DTW", - "stage4": "Stufe 4 – Übereinstimmung", "status": "Status", "tail_trim": "Ende kürzen (s)", "tap_to_dismiss": "zum Ausblenden auf das Diagramm tippen", @@ -584,7 +521,6 @@ "ungrouped": "Nicht gruppiert", "unlabelled": "Unbeschriftet", "unlabelled_paren": "(unbeschriftet)", - "warp_path": "Warp-Pfad", "input": "Eingabe:", "start": "Start", "end": "Ende", @@ -609,9 +545,8 @@ "n_shown": ", {n} angezeigt", "cycles_title": "Zyklen ({n})", "pg_detecting": "Erkennung", - "pg_best_marker": "Beste: {v}", "pg_time_left_model": "Restzeit (Modell)", - "pg_time_left_model_tip": "Modellgeschätzte verbleibende Zeit (Phasenschätzer + ML-Mix), kein statischer Countdown", + "pg_time_left_model_tip": "Aus den Phasen des Programms geschätzt, kein fester Countdown", "phase": "Phase", "pg_mode_history": "An Verlauf testen", "pg_ev_detected": "Erkannt", @@ -651,12 +586,8 @@ "pg_mode_optimize": "Optimieren", "pg_group_matching": "Programmzuordnung", "pg_zoom_hint": "Doppelklick zum Zurücksetzen des Zooms", - "aria_playground_chart2": "Interaktives Leistungsdiagramm des Zyklus: zum Ablesen von Zeit/Leistung mit dem Zeiger darüberfahren, zum Zoomen scrollen, zum Verschieben ziehen", "pg_alert_timeout_end": "Durch Zeitüberschreitung beendet, nicht durch Vorhersage", "pg_alert_indefinite": "Würde unbegrenzt weiterlaufen", - "pg_alert_stress_cap": "Sicherheitsstopp erreicht", - "pg_alert_stress_ok": "Ruhezustandserkennung: Zyklus gestoppt", - "pg_alert_stress_warn": "Ruhestromverbrauch über Stoppgrenze", "task_pg_history": "Mit Verlauf testen", "task_pg_sweep": "Optimieren", "task_reprocess": "Neuverarbeitung", @@ -666,16 +597,9 @@ "pg_recent_runs": "Letzte Durchläufe", "pg_load_run": "Diesen Durchlauf neu laden", "pg_match_confidence": "Zuordnungssicherheit", - "pg_envelope_fit": "Hüllkurven-Passung", - "pg_envelope_fit_tip": "Wie genau der Zyklus innerhalb der gespeicherten Leistungshüllkurve dieses Profils liegt, eine andere Sicht als die Zuordnungssicherheit.", "n_imported_note": ", {n} importiert", "enable_online": "Onlinefunktionen aktivieren", - "store_brand": "Gerätemarke", - "store_model": "Gerätemodell", "anonymous": "Anonym", - "rate_quality": "Qualität bewerten", - "rate_n": "{n} Sterne", - "approved_only": "Nur freigegebene", "adopt_settings": "Einstellungen auch übernehmen", "flags": "Flags", "include_phase_map": "Phasenkarte einbeziehen", @@ -692,7 +616,6 @@ "cat_custom_phases": "Benutzerdefinierte Phasen", "cat_profile_groups": "Profilgruppen", "cat_settings": "Erkennungs- & Zuordnungseinstellungen", - "cat_matching_config": "Feinabstimmung der Zuordnung", "cat_ml_models": "ML-Modelle", "cat_feedback": "Feedback & Prüf-Labels", "cat_suggestions": "Vorschläge", @@ -715,7 +638,6 @@ "hist_csv_data": "CSV-Daten", "hist_from_recorder": "Oder aus Home Assistant lesen", "hist_since": "Seit", - "days": "Tage", "hist_keep": "Diesen Zyklus behalten", "hist_looks_complete": "vollständig", "peak_power_short": "Spitze", @@ -735,7 +657,34 @@ "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" + "correct_total_cycles": "Gesamtzahl korrigieren", + "hist_reason_already_recorded": "überschneidet sich mit einem Zyklus, den WashData bereits aufgezeichnet hat", + "share_consent": "Ich bestätige, dass diese Zyklen normal und ohne Unterbrechung abgeschlossen wurden", + "show_internal": "Interne Einstellungen anzeigen", + "show_internal_tip": "Einstellungen, die WashData für Sie abstimmt (Abtastung, Watchdog, Abgleichrhythmus, Schwellenwerte). Werden angezeigt, wenn dieses Gerät einen eigenen Wert, einen Konflikt oder einen Vorschlag hat.", + "rate_n": "{n} Sterne", + "rate_quality": "Qualität bewerten", + "pg_autolabel": "Automatische Beschriftung", + "pg_label_review": "Bittet um Überprüfung", + "pg_ev_match_reverted": "Übereinstimmung zurückgenommen", + "pg_ev_verified_pause": "Bestätigte Pause", + "n_samples": "{n} Messwerte", + "n_samples_one": "{n} Messwert", + "n_samples_other": "{n} Messwerte", + "n_cycles": "{n} Zyklen", + "n_cycles_one": "{n} Zyklus", + "n_cycles_other": "{n} Zyklen", + "energy_per_cycle": "{v}/Zyklus", + "energy_total": "{v} gesamt", + "automation_off": "(aus)", + "ref_cycle_none": "Keiner", + "share_name": "Meinen Namen bei geteilten Inhalten anzeigen", + "pg_compare_profile": "Mit Profil vergleichen", + "pg_obj_endlag": "Endverzögerung (Median)", + "pg_obj_early": "Anteil vorzeitiger Enden", + "pg_obj_split": "Aufteilungsrate", + "pg_sweep_cycles": "Letzte Zyklen", + "cat_backfill_cycles": "Im importierten Leistungsverlauf gefundene Zyklen" }, "log": { "all_levels": "Alle Ebenen", @@ -745,11 +694,9 @@ }, "ml": { "builtin_models": "● Verwendung integrierter Modelle", - "fit_fair": "Akzeptabel", "fit_good": "Gut", "fit_slight": "Leicht", "fit_strong": "Stark", - "fit_weak": "Schwach", "fit_word": "Passung", "personalized": "● Personalisiert für diese Maschine", "better_than_baseline": "{pct}% besser als die Basisschätzung ({metric})", @@ -757,30 +704,39 @@ "trend_declining_tip": "Die Passung dieses Modells hat sich bei den letzten Überprüfungen verschlechtert. Mehr Zyklen zu überprüfen kann ihm beim erneuten Lernen helfen.", "trend_steady_tip": "Die Passung dieses Modells ist bei den letzten Überprüfungen weitgehend stabil geblieben.", "cap_blurb": { - "end": "Erkennen, wann ein Zyklus wirklich beendet ist", - "live_match": "Das laufende Programm früher erkennen", - "quality": "Fehlerkannte oder fehlerhafte Zyklen erkennen", - "remaining_time": "Vorhersagen, wie lange es noch dauert", "total_energy": "Gesamtverbrauch und Kosten vorhersagen" }, "cap_label": { - "end": "Zyklusende-Erkennung", - "live_match": "Programmerkennung", - "quality": "Zyklusqualitätsprüfung", - "remaining_time": "Schätzung der Restzeit", "total_energy": "Energieschätzung" }, - "metric_auc": "AUC {auc} auf Validierungsdaten", - "metric_mae": "Fehler {model} vs. Baseline {baseline}" + "metric_mae": "Fehler {model} vs. Baseline {baseline}", + "fine_tuned_at": "feinabgestimmt: {when}", + "never": "nie", + "trained_unknown": "zu einem unbekannten Zeitpunkt", + "held_out_count": "gemessen an {count} zurückgehaltenen Validierungszyklen", + "held_out_count_one": "gemessen an {count} zurückgehaltenen Validierungszyklus", + "held_out_count_other": "gemessen an {count} zurückgehaltenen Validierungszyklen", + "last_run_not_promoted": "Letzte Prüfung ({when}): {reason}", + "skip_reason": { + "insufficient_rows": "Noch nicht genug verwertbare Zyklen: bisher {count} beschriftete, saubere Zyklen.", + "insufficient_rows_one": "Noch nicht genug verwertbare Zyklen: bisher {count} beschrifteter, sauberer Zyklus.", + "insufficient_rows_other": "Noch nicht genug verwertbare Zyklen: bisher {count} beschriftete, saubere Zyklen.", + "fit_failed": "Training fehlgeschlagen: {error}", + "unknown": "Es wurde nichts Neues gelernt.", + "holdout_too_small": "Zu wenige Zyklen, um ein neues Modell fair zu testen: {held_out} konnten zum Testen zurückgehalten werden, {min} sind nötig.", + "not_better_than_naive": "Das neue Modell war nicht deutlich besser als die einfache Schätzung (Fehler {model} vs. {naive}).", + "not_better_than_incumbent": "Das neue Modell war nicht besser als das verwendete (Fehler {model} vs. {incumbent}), daher wurde das aktuelle beibehalten." + } }, "modal": { + "remove_task_title": "Aufgabe entfernen", + "remove_task_msg": "Die Erinnerung \"{task}\" entfernen? Die zugehörigen Einträge im Wartungsprotokoll bleiben erhalten.", "auto_label": "Auto-Label-Zyklen", "correct_feedback": "Feedback korrigieren", "create_profile": "Profil erstellen", "cycle": "Zyklus", "edit_group": "Profilgruppe bearbeiten", "edit_phase": "Phase bearbeiten", - "import_config": "Konfiguration importieren", "label_cycle": "Zyklus beschriften", "merge_cycles": "{n} Zyklen zusammenführen", "new_group": "Neue Profilgruppe", @@ -795,6 +751,8 @@ "delete_automation_title": "Automatisierung löschen", "delete_automation_msg": "Die Automatisierung \"{name}\" aus Home Assistant löschen? Dies kann nicht rückgängig gemacht werden.", "remove_legacy_title": "Alte Aktionen entfernen", + "reset_phase_msg": "Ihre Änderung an \"{name}\" rückgängig machen und die eingebaute Phase wiederherstellen? Programme behalten ihre Bereiche.", + "reset_phase_title": "Phase zurücksetzen", "remove_legacy_msg": "Die alten benutzerdefinierten Aktionen entfernen? Sie werden bei Zyklusereignissen nicht mehr ausgelöst. Dies kann über das Panel nicht rückgängig gemacht werden.", "discard_recording_title": "Aufnahme verwerfen", "discard_recording_msg": "Die gespeicherte Aufnahme verwerfen? Dies kann nicht rückgängig gemacht werden.", @@ -815,63 +773,77 @@ "store_share_device": "Dieses Gerät teilen", "export_select": "Export - Daten wählen", "import_wizard": "Import - Daten wählen", - "history_import": "Leistungsverlauf importieren" + "history_import": "Leistungsverlauf importieren", + "apply_suggestions_title": "Vorschläge anwenden", + "apply_suggestions_msg": "Diese {n} Einstellungen ändern? Die Integration wird nach dem Speichern neu geladen.", + "discard_settings_title": "Ungespeicherte Änderungen verwerfen?", + "discard_settings_msg": "Ungespeicherte Einstellungsänderungen: {n}. Einstellungen verlassen und Änderungen verwerfen?", + "profile_title": "Profil · {name}", + "undo_import_msg": "Daten und Einstellungen dieses Geräts werden auf den Stand vor dem letzten Import ({when}) zurückgesetzt. Seitdem aufgezeichnete Zyklen und vorgenommene Änderungen gehen verloren.", + "undo_import_ok": "Import rückgängig machen", + "undo_import_title": "Letzten Import rückgängig machen" }, "msg": { + "custom_task_limit": "Sie haben das Limit von {max} Aufgaben erreicht.", + "custom_tasks_intro": "Fügen Sie alles hinzu, woran Sie sonst noch erinnert werden möchten. Eine Aufgabe ist fällig, sobald eines der beiden Intervalle erreicht ist; lassen Sie eines leer oder auf 0, um nur das andere zu verwenden.", + "maintenance_due_sensor_hint": "Der binäre Sensor „Wartung fällig“ ist eingeschaltet, solange eine Aufgabe fällig ist, für Ihre eigenen Benachrichtigungsautomatisierungen.", + "maintenance_supporter_intro": "Maintenance Supporter ist eine separate Integration, die viel mehr kann: beliebige Geräte oder Aufgaben, Intervalle nach Zeit, Nutzung oder einem Sensor, Benachrichtigungen und einen Verlauf. Sie kann den Zykluszähler von WashData nutzen.", "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.", + "pg_preset_delete_confirm": "Die Voreinstellung \"{name}\" löschen?", + "pg_preset_limit": "Maximale Anzahl an Voreinstellungen erreicht ({n})", + "pg_preset_overwrite": "Die Voreinstellung \"{name}\" überschreiben?", "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.", - "refresh_catalog_hint": "Die Marken- und Gerätelisten der Community werden zwischengespeichert, damit der Community-Store sein Tageslimit einhält. Aktualisieren Sie, um Einträge zu übernehmen, die andere hinzugefügt oder freigegeben haben.", + "store_sibling_hint": "Nichts für Ihr Modell geteilt? Ein verwandtes Modell derselben Marke ist meist ein guter Anfang.", + "store_declare_appliance": "Teilen Sie WashData mit, welches Gerät Sie besitzen, um Setups zu sehen, die andere dafür geteilt haben. Oder geben Sie oben eine Marke ein, um sich umzusehen.", + "refresh_catalog_hint": "Marken- und Gerätelisten der Community werden zwischengespeichert, um das Tageslimit des Community-Stores zu schonen. Aktualisieren Sie, um Einträge zu sehen, die andere hinzugefügt oder freigegeben haben.", "head_trim_hint": "So viele Sekunden vom Anfang entfernen", "appliance_monitor": "Gerätemonitor", "artifact_dip_detail": "Sank unter das übliche Leistungsband für ~{n}s.", "artifact_footer": "In der Grafik oben hervorgehoben. Hierbei handelt es sich um vorübergehende Artefakte (z. B. das Öffnen der Tür mitten im Zyklus), nicht unbedingt um Probleme.", "artifact_header": "{n} Anomalien wurden während dieses Zyklus erkannt", - "artifact_pause_detail": "Die Leistung fiel für ~{n}s nahezu auf null und wurde dann wieder aufgenommen – wahrscheinlich wurde die Tür während des Zyklus geöffnet oder der Zyklus wurde angehalten.", + "artifact_header_one": "{n} Anomalie wurde während dieses Zyklus erkannt", + "artifact_header_other": "{n} Anomalien wurden während dieses Zyklus erkannt", + "artifact_pause_detail": "Die Leistung fiel für ~{n}s fast auf null und stieg dann wieder an. Wahrscheinlich wurde die Tür geöffnet oder der Zyklus angehalten.", "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 entfernen Sie dann deren Beschriftung (bleiben im Verlauf, beeinflussen dieses Profil aber nicht mehr) oder löschen Sie sie ganz.", + "cleanup_intro": "Alle beschrifteten Zyklen überlagert. Markieren Sie Ausreißer und entfernen Sie dann ihre Beschriftung (sie bleiben im Verlauf, prägen dieses Profil aber nicht mehr) oder löschen Sie sie.", "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)", - "compare_profiles_tip": "Überlagern Sie andere Profilumschläge in der Tabelle oben, um zu sehen, welcher am besten zu diesem Zyklus passt.", + "compare_profiles_tip": "Andere Profile im Diagramm überlagern, um zu sehen, welches am besten zu diesem Zyklus passt.", "compare_selected_cycles": "Ausgewählte Zyklen (durchgehend) – ein-/ausblenden", - "consider_new_profile": "Erwägen Sie die Erstellung eines neuen Profils.", - "coverage_gap": "Die letzten Zyklen haben kein passendes Profil ({pct} % der letzten 30).", - "coverage_gap_similar_cycles": "{count} ähnliche unbeschriftete Zyklen gefunden – erstellen Sie ein Profil, um diese zu erkennen.", "cycles_deleted": "{count} Zyklen gelöscht", + "cycle_context_loading": "Recorder-Verlauf wird geladen…", + "cycle_context_note": "Grau: der Recorder-Verlauf des Leistungssensors rund um diesen Zyklus. Er gehört nicht zum Zyklus und beeinflusst weder Dauer noch Energie oder Zuordnung.", + "cycle_context_none": "Der Recorder enthält keine Messwerte rund um diesen Zyklus (Home Assistant speichert standardmäßig 10 Tage Verlauf).", + "cycle_context_tip": "Nur Anzeige: zeigt den Recorder-Verlauf des Leistungssensors rund um diesen Zyklus, falls noch vorhanden. Um abgebrochene Startversuche in künftige Zyklen aufzunehmen, verwenden Sie Kurven-Vorlauf (Einstellungen > Erkennung > Zyklusstart); das ändert die gespeicherten Dauern.", "enough_data": "Genügend Daten zum Lernen ({current}/{min} Zyklen).", "export_description": "Wählen Sie genau aus, welche Profile, Zyklen, Einstellungen und mehr nach JSON exportiert werden sollen, oder analysieren Sie eine Datei und importieren Sie nur die gewünschten Teile.", "feedback_cycles_pending": "Zu überprüfen: {n}", "feedback_prompt": "Bestätigen Sie, dass es richtig war, korrigieren Sie das Programm oder ignorieren Sie es.", "feedback_relabel_hint": "Das erneute Beschriften dieses Zyklus erledigt die Überprüfung ebenfalls.", "filter_by_profile": "Nach Profil filtern…", - "group_modal_help": "Gruppieren Sie Programme mit derselben Form, die sich in Temperatur/Schleudergang unterscheiden (Dauer kann variieren). Matching bewertet die Gruppe als einen Kandidaten und wählt dann das am besten passende Mitglied aus. Wählen Sie mindestens 2; Die Überlagerung zeigt, wie ähnlich sie sind.", + "group_modal_help": "Gruppieren Sie Programme mit gleicher Form, die sich in Temperatur oder Schleudern unterscheiden. Der Abgleich bewertet die Gruppe als Einheit und wählt dann das am besten passende Mitglied. Wählen Sie mindestens 2; die Überlagerung zeigt, wie ähnlich sie sind.", "group_not_cohesive": "Diese Profile sind nicht ähnlich genug, um zuverlässig gruppiert zu werden. Beim Matching werden sie daher einzeln behandelt, bis Sie den Ausreißer entfernen oder die Gruppe aufteilen.", "group_preview_hint": "Mindestens 2 Mitglieder auswählen, um deren Leistungskurven in der Vorschau zu vergleichen.", - "import_intro": "Laden Sie eine exportierte Datei oder fügen Sie unten eine JSON-Nutzlast ein.", - "legacy_actions_title": "{count} alte benutzerdefinierte Aktion wird noch ausgeführt", + "legacy_actions_title": "{count} alte benutzerdefinierte Aktionen werden noch ausgeführt", + "legacy_actions_title_one": "{count} alte benutzerdefinierte Aktion wird noch ausgeführt", + "legacy_actions_title_other": "{count} alte benutzerdefinierte Aktionen werden noch ausgeführt", "live_chart_loading": "Sobald die Messwerte eintreffen, wird ein Live-Leistungsdiagramm angezeigt.", "loading": "Laden…", "loading_curve": "Kurve wird geladen…", "loading_settings": "Einstellungen werden geladen…", "log_buffer_hint": "Das Neueste zuerst · Puffert die letzten 500 ha_washdata-Datensätze seit dem Neustart · Ziehen Sie den unteren Rand, um die Größe zu ändern.", - "maintenance_advisory": "Eine zunehmende Dauer/Energie kann ein Hinweis darauf sein, dass eine Wartung des Geräts erforderlich ist (z. B. Entkalken, Filterreinigung).", "maintenance_due": "Wartung fällig: {items}", - "maintenance_intro": "Protokollieren Sie die an diesem Gerät durchgeführten Wartungen und lassen Sie sich erinnern, wenn eine Aufgabe wieder ansteht.", + "maintenance_intro": "Protokollieren Sie Wartungen an diesem Gerät und lassen Sie sich erinnern, wenn eine Aufgabe wieder fällig ist.", "maintenance_load_error": "Wartungsdaten konnten nicht geladen werden: {error}", "maintenance_requires_access": "Für Wartung und Export/Import ist Vollzugriff erforderlich.", - "manual_duration_hint": "Die durchschnittliche/erwartete Zykluslänge des Profils, die für Schätzungen der verbleibenden Zeit verwendet wird. Bearbeiten, um es festzulegen; Wenn Sie ihn unverändert lassen, bleibt der aktuelle Wert erhalten.", - "matching_tuning_intro": "Beim Lernen passt WashData auch an, wie stark die Programmanpassung Form gegenüber Dauer und Energie gewichtet.", + "manual_duration_hint": "Die erwartete Zykluslänge des Profils, verwendet für die Schätzung der Restzeit. Unverändert lassen, um den aktuellen Wert zu behalten.", "merge_intro": "Die ausgewählten Zyklen werden zu einem zusammengefasst (chronologische Reihenfolge; Lücken mit 0 W gefüllt). Wählen Sie das resultierende Profil aus.", "ml_intro": "WashData wird mit intelligenten Modellen geliefert, die sofort einsatzbereit sind.", "ml_learned_intro": "Auf diese Maschine abgestimmte Modelle.", - "ml_loading": "ML wird geladen…", "ml_settings_intro": "Zwei unabhängige Schalter: Einer wendet die Modelle an, während ein Zyklus läuft, der andere ermöglicht es WashData, sie im Laufe der Zeit an Ihre Maschine anzupassen.", - "name_first_program": "Sie haben genügend Zyklen – benennen Sie Ihr erstes Programm, um den Abgleich zu starten.", - "near_duplicate_cluster": "Beinahe doppelter Profilcluster erkannt. Durch die Gruppierung kann beim Matching zuverlässig zwischen Doppelgängern ausgewählt werden (z. B. dasselbe Programm bei unterschiedlicher Temperatur/Schleuder).", "no_cycles_match": "Keine Zyklen entsprechen dem aktuellen Filter.", "no_cycles_profile": "Keine Zyklen für dieses Profil.", "no_cycles_selected": "Wählen Sie zuerst mindestens einen Zyklus aus.", @@ -896,73 +868,52 @@ "no_suggestions": "Keine aktiven Vorschläge.", "notify_services_hint": "Verwenden Sie {entity}-Dienst-IDs (kommagetrennt für mehrere). Template-Variablen: {vars}.", "old_actions_warning": "Konfiguriert mit dem alten Aktionseditor (jetzt entfernt). Sie werden weiterhin auf Zyklusereignisse ausgelöst, können hier jedoch nicht mehr bearbeitet werden. Wandeln Sie in eine normale Automatisierung um oder entfernen Sie sie.", - "onboarding_progress": "{n} / 3 Zyklen beobachtet", - "onboarding_watching": "Lassen Sie Ihr Gerät normal laufen – WashData beobachtet. Nach 3 Zyklen beginnt der Programmabgleich.", "pending_feedback": "Ausstehende Rückmeldung zur Erkennung", "performance_trend": "Leistungstrend ({n} Zyklen)", - "pg_analysis_empty": "Laden Sie einen Zyklus, um die Zuordnungsanalyse anzuzeigen.", "pg_apply_confirm": "Besten Wert anwenden: {label} = {value}?", - "pg_canvas_empty": "Wählen Sie oben einen Zyklus aus und drücken Sie dann auf Laden, um seine Leistungskurve anzuzeigen.", - "pg_chart_caption": "Erkennungs-/Zuordnungsrate über die Werte von {param}", "pg_ctrl_intro": "Die Werte stammen zunächst aus den aktiven Einstellungen dieses Geräts in der Integration. Änderungen bleiben im Testbereich, bis sie in die Integration übernommen werden.", "pg_live_unavailable": "Aktive Einstellungen nicht verfügbar, es werden Standardwerte angezeigt.", "pg_load_live_confirm": "{n} Änderung(en) im Testbereich verwerfen und die aktuellen Einstellungen der Integration neu laden?", - "pg_match_rate": "{pct}% Zuordnungsrate bei {total} Zyklen", "pg_matches_live": "Stimmt mit den aktiven Einstellungen überein", "pg_n_changed": "{n} gegenüber den aktiven Einstellungen geändert", "pg_sugg_none": "Alle Vorschläge stimmen bereits mit den aktuellen Einstellungen überein", - "pg_preset_delete_confirm": "Die Voreinstellung \"{name}\" löschen?", - "pg_preset_limit": "Maximale Anzahl an Voreinstellungen erreicht ({n})", - "pg_preset_overwrite": "Die Voreinstellung \"{name}\" überschreiben?", "pg_publish_one_confirm": "{label} = {value} in den Einstellungen dieses Geräts speichern?", "pg_restart_note": "Starten Sie Home Assistant neu, um die Simulationstools zu aktivieren.", - "pg_sim_progress": "{done} / {total} Zyklen", - "pg_starting": "Wird gestartet…", - "pg_stress_synthetic": "Leerlaufsimulation beginnt hier", - "pg_sweep_intro": "Was wäre, wenn {param} anders wäre? Testen Sie {steps} Werte über Ihre letzten {cycles} Zyklen, um die Einstellung zu finden, bei der die meisten Zyklen korrekt zugeordnet werden.", - "pg_sweep_step": "Schritt {done} / {total}", - "pg_verdict_bad": "Erfordert Aufmerksamkeit: viele Zyklen bleiben unerkannt.", - "pg_verdict_good": "Gut abgestimmt: die meisten Zyklen werden korrekt erkannt und zugeordnet.", - "pg_verdict_ok": "Akzeptabel: einige Zyklen wurden verpasst. Versuchen Sie, den Startschwellenwert zu senken.", "phase_catalog_intro": "Benannte Segmente eines Zyklus (Vorwäsche, Erhitzen, Schleudern…). Weisen Sie über das Bedienfeld einem Profil zu.", "phase_ranges_intro": "Phasenbereiche (Minuten ab Zyklusbeginn), überlagert mit der Durchschnittskurve. Bearbeiten Sie die Werte, um eine Live-Vorschau anzuzeigen.", - "playground_intro": "Eine sichere Sandbox, um vergangene Zyklen mit anderen Einstellungen abzuspielen, zwei Konfigurationen nebeneinander zu vergleichen und zu untersuchen, wie ein Zyklus zu einem Profil passt. Nichts hier verändert Ihre Live-Daten, bis Sie es ausdrücklich anwenden.", - "playground_needs_restart": "Starten Sie Home Assistant neu, um die Testbereich-Tools zu aktivieren.", - "preferences_admin": "Einstellungen, Bedienfeld und Zugriffskontrolle", - "preferences_adv": "Präferenzen", + "playground_intro": "Testen Sie Einstellungen an Ihren echten Zyklusdaten. Nichts hier ändert Ihre Live-Einstellungen, bis Sie es anwenden.", "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), berechne die Zykluskosten anhand deiner erfassten Energiepreise neu und ermittle den Zykluszustand neu. Nach einer Stapelüberprüfung ausführen.", + "process_history_hint": "Gleicht alle gespeicherten Zyklen erneut ab, aktualisiert Vorschläge, trainiert ML-Modelle neu (falls aktiviert), berechnet Zykluskosten aus erfassten Preisen neu und ermittelt die Zyklusgesundheit neu. Nach einer Reihe von Bewertungen 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.", - "rbac_hint": "Wenn diese Option deaktiviert ist, hat jeder Home Assistant-Benutzer vollen Zugriff (Standardeinstellung). Administratoren haben immer vollen Zugriff und können alle verwalten.", - "recent_logs": "Aktuelle ha_washdata-Datensätze", + "rbac_hint": "Aus: Jeder Home Assistant-Benutzer hat vollen Zugriff (Standard). Administratoren haben immer vollen Zugriff und können alle verwalten.", "recording_in_progress": "Aufnahme läuft", "reminders_intro": "Eine Erinnerung im Panel so viele Zyklen nach der letzten Wartung anzeigen. Leer lassen oder 0, um eine Erinnerung auszuschalten.", "restart_gap_footer": "Im Diagramm hervorgehoben. Leistungsdaten fehlen für diese Intervalle; die Erkennung verwendete nur echte Messwerte.", - "restart_gap_header": "{n} HA-Neustart-Lücke während dieses Zyklus", + "restart_gap_header": "{n} HA-Neustart-Lücken während dieses Zyklus", + "restart_gap_header_one": "{n} HA-Neustart-Lücke während dieses Zyklus", + "restart_gap_header_other": "{n} HA-Neustart-Lücken während dieses Zyklus", "restart_gap_item": "{dur} Lücke", - "review_confirm_help": "Bestätigen Sie, ob dieser Zyklus korrekt erkannt wurde. Ihre Bewertungen trainieren das Modell auf Ihrer Maschine – je mehr Zyklen Sie bestätigen, desto besser werden die Übereinstimmung und die Gesundheitsbewertung. Ein kurzes Gut/Schlecht genügt.", + "review_confirm_help": "Wurde dieser Zyklus richtig erkannt? Bestätigte Zyklen verbessern den Abgleich für Ihre Maschine. Ein kurzes Gut/Schlecht genügt.", "review_in_settings": "Überprüfen Sie in den Einstellungen", "review_notes_placeholder": "Notizen (optional)", "review_notes_tip": "Freitextnotizen für Ihre eigene Referenz. Wird nicht durch Matching oder Training verwendet.", - "review_profile_tip": "Das Programm, als das dieser Zyklus bezeichnet wird. Wenn das automatisch erkannte Programm falsch war, korrigieren Sie es hier – die Beschriftung lehrt den Abgleich für zukünftige Zyklen.", - "review_quality_tip": "Wie sauber dieser Zyklus ist. Gut = ein Lehrbuchbeispiel für dieses Programm; Schlecht = erkannt, aber verrauscht oder untypisch; Unbrauchbar = falsch erkannt (zusammengeführt, abgeschnitten oder falsch). Steuert die Integritätsbewertung und welche Zyklen zum Trainieren des Modells zulässig sind.", - "review_recorded_tip": "Markieren Sie dies als handverlesenen Referenzzyklus für sein Programm – die gleiche Rolle wie ein manuell aufgezeichneter Zyklus. Referenzzyklen bleiben immer erhalten, setzen die passende Vorlage als Seed und werden bei der Bereinigung niemals gelöscht. (Dies ist die „goldene“/aufgezeichnete Flagge; beide sind dasselbe.)", - "review_tags_tip": "Optionale Flags, die beschreiben, was bei diesem Zyklus schief gelaufen ist, damit Training und Bereinigung dafür verantwortlich sein können.", - "review_to_cycles": "Öffnen Sie die Cycles-Bewertungswarteschlange", + "review_profile_tip": "Das Programm, mit dem dieser Zyklus beschriftet ist. Wenn es falsch ist, korrigieren Sie es hier; Beschriftungen lehren den Abgleich.", + "review_quality_tip": "Wie sauber dieser Zyklus ist. Gut: ein Lehrbuchlauf dieses Programms. Schlecht: erkannt, aber verrauscht oder ungewöhnlich. Unbrauchbar: falsch erkannt (zusammengeführt, abgeschnitten oder fehlerhaft). Beeinflusst die Gesundheitsbewertung und welche Zyklen das Modell trainieren.", + "review_recorded_tip": "Markieren Sie dies als handverlesenen Referenzzyklus für sein Programm, wie einen manuell aufgezeichneten Zyklus. Referenzzyklen bleiben immer erhalten, bilden die Grundlage der Abgleichvorlage und werden bei der Bereinigung nie entfernt.", + "review_tags_tip": "Optionale Markierungen dafür, was schiefging, damit Training und Bereinigung es berücksichtigen können.", + "review_to_cycles": "Zyklen-Bewertungswarteschlange öffnen", "samples_decimated": "Es werden {shown} von {total} Messwerten angezeigt (für die Anzeige ausgedünnt; Spitzen bleiben erhalten). Eine breite Lücke hier ist Ausdünnung, keine fehlenden Daten.", - "saving_triggers_reload": "Durch das Speichern wird ein Neuladen der Integration ausgelöst. HA-Entitäten werden möglicherweise kurzzeitig als nicht verfügbar angezeigt.", + "saving_triggers_reload": "Speichern lädt die Integration neu. Entitäten können kurz als nicht verfügbar erscheinen.", "search_placeholder": "Einstellungen durchsuchen…", "see_recorder": "Siehe Rekorder-Widget unten", "setting_changed": "Geändert von {old} zu {new} am {date}", "settings_basic_note": "Es werden die wesentlichen Einstellungen angezeigt. Wechseln Sie zu Erweitert für die vollständige Liste.", - "shape_drift_advisory": "⚠ Musterdrift", - "shape_drift_detail": "Das Leistungsmuster dieses Profils hat sich im Laufe der Zeit verändert – möglicher Geräteverschleiß oder Wartung erforderlich (z. B. Entkalken, Filterreinigung).", "show_all_settings": "Alle Einstellungen anzeigen", - "showing_suggestions": "{count} Einstellung mit Vorschlägen wird angezeigt.", - "split_intro": "Klicken Sie auf das Diagramm, um einen Teilungspunkt hinzuzufügen oder zu entfernen oder eine automatische Erkennung anhand von Leerlauflücken durchzuführen. Jedes resultierende Segment kann ein eigenes Profil erhalten.", - "storage_diagnostics": "Speicherstatistiken, Wartung, Export/Import", + "showing_suggestions": "{count} Einstellungen mit Vorschlägen werden angezeigt.", + "showing_suggestions_one": "{count} Einstellung mit Vorschlägen wird angezeigt.", + "showing_suggestions_other": "{count} Einstellungen mit Vorschlägen werden angezeigt.", + "split_intro": "Klicken Sie in das Diagramm, um einen Teilungspunkt hinzuzufügen oder zu entfernen, oder erkennen Sie automatisch anhand von Leerlauflücken. Jeder Teil kann ein eigenes Profil erhalten.", "sug_staged": "{key} = {val} gesetzt. Speichern zum Anwenden.", "toast_auto_detect_enabled": "Automatische Erkennung aktiviert", "toast_auto_label_complete": "Automatische Beschriftung abgeschlossen", @@ -979,7 +930,6 @@ "toast_no_automation": "Hier kann keine Automatisierung erstellt werden", "toast_profile_name_required": "Profilname ist erforderlich", "toast_program_set": "Programm gesetzt: {program}", - "toast_rebuild_failed": "Neuaufbau fehlgeschlagen: {error}", "toast_resume_failed": "Fortsetzen fehlgeschlagen: {error}", "toast_revert_failed": "Zurücksetzen fehlgeschlagen: {error}", "toast_reverted": "{key} zurückgesetzt", @@ -990,100 +940,83 @@ "trend_energy_up": "Energietrend steigt ({pct} %/Zyklus) – aktueller Durchschnitt {avg}", "trim_destructive_confirm": "Dadurch werden nur {pct}% des Zyklus behalten. Dies kann nicht rückgängig gemacht werden. Fortfahren?", "trim_intro": "Ziehen Sie die roten Ziehpunkte oder geben Sie Werte ein. Alles außerhalb des Fensters wird entfernt.", - "tuning_suggestions_available": "{count} Optimierungsvorschlag aus beobachteten Zyklen verfügbar. Sie erscheinen neben den relevanten Feldern.", + "tuning_suggestions_available": "{count} Optimierungsvorschläge aus beobachteten Zyklen verfügbar. Sie erscheinen neben den relevanten Feldern.", + "tuning_suggestions_available_one": "{count} Optimierungsvorschlag aus beobachteten Zyklen verfügbar. Er erscheint neben dem relevanten Feld.", + "tuning_suggestions_available_other": "{count} Optimierungsvorschläge aus beobachteten Zyklen verfügbar. Sie erscheinen neben den relevanten Feldern.", "unsure_detected_prefix": "WashData ist sich nicht sicher, ob es erkannt wurde", - "updated": "Aktualisiert", "warmup_badge": "Noch in der Lernphase ({done}/{needed} Zyklen)", - "warmup_detail": "Dieses Profil benötigt {needed} beschriftete Zyklen, bevor die automatische Erkennung beginnt. Jeder bestätigte Zyklus hilft beim Lernen.", + "warmup_detail": "Der Abgleich funktioniert bereits. WashData bittet Sie, die ersten {needed} Zyklen dieses Profils zu bestätigen, bevor es selbstständig beschriftet.", "wipe_history_warning": "Alle Zyklen und Profile dauerhaft löschen. Kann nicht rückgängig gemacht werden.", "compare_overlay_tip": "Legen Sie gelernte Profilhüllkurven übereinander, um zu sehen, welchem Programm jeder Zyklus ähnelt.", - "duration_trend_up_tip": "Dauer steigend ({pct}%/Zyklus)", - "duration_trend_down_tip": "Dauer fallend ({pct}%/Zyklus)", - "energy_trend_up_tip": "Energie steigend ({pct}%/Zyklus)", - "energy_trend_down_tip": "Energie fallend ({pct}%/Zyklus)", - "performance_trending": "Leistungstrend", - "pg_select_cycle_hint": "Wählen Sie einen Zyklus und klicken Sie auf ↺, um ihn zu laden", - "pg_sweep_progress": "Schritt {done} / {total}", - "pg_analysis_hint": "Wählen Sie einen Zyklus und klicken Sie auf ↺, um die Analyse zu laden.", "diagnostics_load_failed": "Diagnose konnte nicht geladen werden: {error}", - "manual_duration_ref_hint": "Wird nur verwendet, wenn kein Referenzzyklus ausgewählt ist. Ein Referenzzyklus legt die Dauer anhand seiner eigenen Länge fest.", + "manual_duration_ref_hint": "Wird nur ohne Referenzzyklus verwendet; ein Referenzzyklus legt die Dauer anhand seiner eigenen Länge fest.", + "duration_learned_hint": "Aus den Zyklen dieses Programms berechnet (ohne Ausreißer) und nach jedem neuen Zyklus aktualisiert. Um sie zu ändern, schneiden Sie die abweichenden Zyklen zu oder entfernen Sie sie.", "toast_convert_rolled_back": "Konvertierung fehlgeschlagen und zurückgesetzt (keine Automatisierung zurückgelassen): {error}", "toast_convert_orphan": "Die Automatisierung wurde erstellt, aber das Entfernen der alten Aktionen ist fehlgeschlagen. Nicht erneut versuchen: Entfernen Sie die alten Aktionen manuell, um eine doppelte Automatisierung zu vermeiden.", - "advisory_duration_trend_up": "Die Zyklen von '{name}' werden zunehmend länger (etwa +{pct}% pro Zyklus). Falls sich das Verhalten des Geräts geändert hat, zeichnen Sie dieses Profil neu auf oder erstellen Sie es neu.", - "advisory_energy_trend_up": "'{name}' verbraucht zunehmend mehr Energie (etwa +{pct}% pro Zyklus) - prüfen Sie das Gerät, falls das unerwartet ist.", - "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.", + "advisory_duration_trend_up": "Die Zyklen von '{name}' werden immer länger (etwa +{pct}% pro Zyklus). Falls sich das Gerät verändert hat, zeichnen Sie dieses Profil neu auf oder erstellen Sie es neu.", + "advisory_energy_trend_up": "'{name}' verbraucht mit jedem Zyklus mehr Energie (etwa +{pct}% pro Zyklus). Prüfen Sie das Gerät, falls das unerwartet ist.", + "advisory_poor_health": "'{name}' passt schlecht: Die letzten Zyklen schwanken stark oder passen nur schwach. Überprüfen Sie seine Zyklen oder zeichnen Sie es neu auf, damit Abgleich und Schätzungen genau bleiben.", + "advisory_shape_drift": "'{name}' weicht von seinem ursprünglichen Leistungsverlauf ab, etwa durch Verkalkung oder Verschleiß. Erwägen Sie, es mit aktuellen Zyklen neu aufzuzeichnen.", + "advisory_shape_drift_corr": "'{name}' weicht von seinem ursprünglichen Leistungsverlauf ab (Korrelation {corr}), etwa durch Verkalkung oder Verschleiß. Erwägen Sie, es mit aktuellen Zyklen neu aufzuzeichnen.", + "advisory_unmatchable": "'{name}' kann nie zugeordnet werden: Dahinter steht kein Zyklus mit Leistungsdaten. Beschriften Sie einen Zyklus dieses Programms oder nehmen Sie einen auf, um das zu beheben.", + "advisory_unmatchable_no_duration": "'{name}' kann nie zugeordnet werden: Sein Zyklus hat keine brauchbare Dauer. Nehmen Sie dieses Programm neu auf oder legen Sie seine erwartete Dauer fest.", "pg_no_alerts": "In diesem Lauf wurden keine Probleme erkannt.", "pg_override_active": "Verwendet Ihre bearbeiteten Einstellungen ggü. den aktuellen", "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.", "pg_sweep_intro2": "Finden Sie die Einstellung, die ein Ziel über Ihre letzten Zyklen am besten erfüllt. Es ändert sich nichts, bis Sie es anwenden.", - "pg_running_history": "Deine Zyklen werden erneut abgespielt…", - "pg_running_sweep": "Werte werden über deine Zyklen hinweg getestet…", "pg_row_load_hint": "Diesen Zyklus im Diagramm oben laden", - "pg_history_intro2": "Lass deine letzten Zyklen mit den obigen Einstellungen erneut durch die echte Erkennung und Zuordnung laufen. Klicke auf eine Zeile, um diesen Zyklus im Diagramm zu laden; ändere eine Einstellung, um einen Vorher-Nachher-Vergleich zu sehen.", + "pg_history_intro2": "Spielen Sie aktuelle Zyklen mit den obigen Einstellungen erneut durch die echte Erkennung und Zuordnung. Klicken Sie auf eine Zeile, um sie im Diagramm zu laden; ändern Sie eine Einstellung, um vorher und nachher zu vergleichen.", "pg_simulating": "Zyklus wird simuliert…", - "pg_canvas_empty2": "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 zum Zoomen und ziehe zum Verschieben.", "pg_apply_settings_confirm": "Diese {n} Einstellung(en) für dieses Gerät speichern? {list}", - "pg_stress_terminated_detail": "Auf ~{idle}W Leerlauf eingependelt -- Zyklus endete {h} Std. {m} Min. später über {reason}.", - "pg_stress_above_threshold_detail": "Der Ruhestromverbrauch von ~{idle}W liegt auf oder über der effektiven Stoppgrenze ({stop}W) -- der Zyklus wurde nie als ruhig erkannt. Erhöhe stop_threshold_w zur Behebung.", - "pg_stress_hit_cap_detail": "Der Zyklus lief {h} Std. {m} Min. ohne anzuhalten -- durch den Sicherheitsstopp zwangsweise beendet. Ruhestromverbrauch {idle}W gegenüber Stoppgrenze {stop}W.", "no_logs_match": "Keine Protokolleinträge entsprechen diesen Filtern.", "pg_analysis_hint2": "Wählen Sie einen Zyklus und klicken Sie auf Ausführen, um die Zuordnungsanalyse zu laden.", - "pg_analysis_empty2": "Klicken Sie auf Ausführen, um die Zuordnungsanalyse anzuzeigen.", - "imported_readonly": "Aus dem Community-Store importiert. Wird zur Referenz und zum Abgleich angezeigt. Zählt nicht in Ihre Statistik und kann nicht bearbeitet werden.", + "imported_readonly": "Aus dem Community-Store importiert. Dient als Referenz und zum Abgleich, zählt nicht in Ihre Statistik und kann nicht bearbeitet werden.", "merge_no_imports": "Importierte Aufnahmen können nicht mit einem echten Zyklus zusammengeführt werden. Heben Sie die Auswahl auf, um zusammenzuführen.", - "store_enable_hint": "Aktivieren Sie die Onlinefunktionen in den Einstellungen, um Referenzzyklen der Community zu durchsuchen und zu importieren.", - "advisory_duration_outlier": "{n} Zyklus/Zyklen mit der Bezeichnung '{name}' weichen zu stark von dessen üblicher Dauer ab, um jemals zugeordnet werden zu können (schlimmster Fall: {ratio}x). Wahrscheinlich sind sie falsch beschriftet, oder eine Aufzeichnung hat zwei Durchläufe erfasst. Beschriften Sie sie neu oder teilen Sie sie auf, damit sie die Zeitschätzung dieses Programms nicht weiter verzerren.", + "store_enable_hint": "Aktivieren Sie die Onlinefunktionen unter Präferenzen > Online & Community, um Referenzzyklen der Community zu durchsuchen und zu importieren.", + "advisory_duration_outlier": "{n} Zyklus/Zyklen mit der Beschriftung '{name}' weichen zu stark von seiner üblichen Länge ab, um je zugeordnet zu werden (schlimmster: {ratio}x). Wahrscheinlich sind sie falsch beschriftet oder enthalten zwei Läufe. Beschriften Sie sie neu oder teilen Sie sie auf, damit sie seine Zeitschätzung nicht verzerren.", "standby_above_stop_title": "Zieht beim Zyklusende noch {idle} W", "standby_above_stop_sub": "{n} der letzten {total} Zyklen. Die Abschaltverzögerung beginnt erst, wenn die Leistung unter den Stoppschwellenwert ({stop} W) fällt, daher enden Zyklen zu spät oder müssen von Hand gestoppt werden.", - "online_intro": "Teilen Sie Referenzzyklen mit anderen WashData-Nutzern und laden Sie welche herunter. Alle Onlinefunktionen sind optional und standardmäßig deaktiviert.", "store_share_intro": "Laden Sie diesen Referenzzyklus hoch, damit andere mit demselben Gerät ihn nutzen können. Er wird geprüft, bevor er öffentlich erscheint.", - "online_intro_global": "Durchsuchen und teilen Sie Referenzaufnahmen mit anderen WashData-Nutzern und bestätigen Sie Geräteeinträge. Eine Verbindung gilt für Ihre gesamte WashData-Integration; Gerätemarke und -modell werden pro Gerät unter Grundlegend festgelegt. Alle Onlinefunktionen sind optional und standardmäßig deaktiviert.", + "online_intro_global": "Durchsuchen und teilen Sie Referenzaufnahmen mit anderen WashData-Nutzern. Eine Verbindung gilt für alle Ihre WashData-Geräte; Marke und Modell werden pro Gerät unter Grundlegend festgelegt. Onlinefunktionen sind aus, bis Sie sie einschalten.", "online_unavailable": "Onlinefunktionen sind auf diesem Server nicht verfügbar.", "store_picker_offline": "Aktivieren Sie die Onlinefunktionen im Einstellungszahnrad, um aus dem Community-Katalog auszuwählen.", "pick_brand_first": "Wählen Sie zuerst eine Gerätemarke.", - "model_not_found": "Noch nicht im Katalog.", - "connect_to_confirm": "Verbinden Sie sich im Einstellungszahnrad, um zu bestätigen oder zu bewerten.", "adopt_settings_hint": "Überschreibt die Erkennungs- und Abgleichschwellenwerte dieses Geräts mit den geteilten. Benachrichtigungen, Entitäten und der Energiepreis werden nie geändert.", - "include_settings_hint": "Teilt die Erkennungs- und Abgleichschwellenwerte dieses Geräts (nicht Benachrichtigungen, Entitäten oder Energiepreis). Übernehmende wählen selbst, ob sie diese anwenden.", + "include_settings_hint": "Teilt die Erkennungs- und Abgleichschwellenwerte dieses Geräts (keine Benachrichtigungen, Entitäten oder Energiepreise). Andere entscheiden selbst, ob sie sie anwenden.", "onboard_download": "Neues Gerät? Übernehmen Sie ein fertiges Setup (Programme, Referenzzyklen und Phasen) von einem anderen WashData-Nutzer mit demselben Gerät.", - "share_consent": "Ich bestätige, dass diese Zyklen normal und ohne Unterbrechung abgeschlossen wurden", "share_device_none": "Noch keine teilbaren Zyklen. Markieren Sie zuerst einen aufgezeichneten oder ausgewählten Zyklus im Tab Zyklen als Referenzzyklus (⭐).", "share_guideline_naming": "Benennen Sie jedes Profil genau so, wie es auf dem Gerätewähler oder Display angezeigt wird (z. B. 'Cotton 40', 'Eco 60').", - "share_guideline_quality": "Teilen Sie nur Zyklen, die normal abgeschlossen wurden -- keine Unterbrechungen mitten im Zyklus, Türöffnungen oder Leistungsausfälle.", + "share_guideline_quality": "Teilen Sie nur normal abgeschlossene Zyklen: keine Unterbrechungen, Türöffnungen oder Stromaussetzer.", "share_guideline_review": "Ihr Upload startet als ausstehend und erscheint öffentlich, sobald genug Community-Mitglieder ihn bestätigt haben.", "share_guidelines_title": "Vor dem Teilen", - "store_download_device_intro": "Übernehmen Sie alle geteilten Programme und deren Referenzzyklen auf Ihr Gerät. Ihre eigenen aufgezeichneten Zyklen und Statistiken bleiben unberührt.", + "store_download_device_intro": "Alle geteilten Programme und ihre Referenzzyklen zu Ihrem Gerät hinzufügen. Ihre eigenen Zyklen und Statistiken bleiben unberührt.", "store_share_device_intro": "Laden Sie {brand} {model} mit den von Ihnen ausgewählten Referenzzyklen hoch. Andere mit demselben Gerät können Ihre Programme übernehmen. Einträge werden vor der öffentlichen Erscheinung geprüft.", "share_profile_no_cycles": "Keine Referenzzyklen -- markieren Sie einen Zyklus als ⭐ im Tab Zyklen, um dieses Profil einzubeziehen", - "advisory_phase_inconsistent": "'{name}' scheint verschiedene Programme oder Temperaturen zu vermischen - seine Zyklen heizen unterschiedlich lange. Wenn Sie es in separate Profile aufteilen (z. B. pro Temperatur), verbessern sich Abgleich und Zeitschätzungen.", - "advisory_phase_inconsistent_title": "⚠ Möglicherweise gemischte Programme", - "export_select_intro": "Kreuzen Sie genau an, was enthalten sein soll. Wenn Sie Profile ohne ihre Zyklen auswählen, wird trotzdem ein erkennbares Programm exportiert (seine gelernte Kurve wird mit übertragen).", - "import_analyze_hint": "Laden Sie eine exportierte Datei (oder fügen Sie ihr JSON ein). WashData analysiert sie und zeigt genau, was importiert werden kann, bevor sich etwas ändert.", + "export_select_intro": "Kreuzen Sie an, was enthalten sein soll. Profile ohne ihre Zyklen werden trotzdem als zuordenbares Programm exportiert.", + "import_analyze_hint": "Laden Sie eine exportierte Datei (oder fügen Sie ihr JSON ein). WashData zeigt, was importiert werden kann, bevor sich etwas ändert.", "analyzing": "Analysiere…", - "device_type_mismatch_warn": "Dieser Export stammt von einem anderen Gerätetyp ({src} vs. {local}). Programme und Zyklen können weiterhin als Referenzdaten importiert werden, aber gerätespezifische Einstellungen und der Import echter Verläufe sind deaktiviert.", + "device_type_mismatch_warn": "Dieser Export stammt von einem anderen Gerätetyp ({src} vs. {local}). Programme und Zyklen lassen sich weiterhin als Referenzdaten importieren, aber keine Geräteeinstellungen und kein echter Verlauf.", "merge_hint": "Importierte Elemente werden hinzugefügt; nichts Lokales geht verloren. Namenskonflikte werden unten aufgelöst.", "replace_warn": "Jede angekreuzte Kategorie wird gelöscht und aus der Datei ersetzt. Nicht angekreuzte Kategorien bleiben unverändert.", "dest_reference_hint": "Importierte Zyklen verbessern nur die Programmerkennung und beeinflussen niemals die Nutzungs-/Energiestatistik.", - "dest_real_history_hint": "Importierte Zyklen zählen als eigener Verlauf dieses Geräts und fließen in die Energie-/Nutzungsstatistik ein. Verwenden Sie dies, um ein Gerät in eine neue Installation zu übertragen.", + "dest_real_history_hint": "Importierte Zyklen zählen als eigener Verlauf und eigene Statistik dieses Geräts. Verwenden Sie dies, um ein Gerät in eine neue Installation zu übertragen.", "sug_muted": "Diese Einstellung wird nicht mehr vorgeschlagen", "sug_mute_failed": "Einstellung konnte nicht stummgeschaltet werden", "sug_unmuted_all": "Stummgeschaltete Vorschläge zurückgesetzt", - "n_suggestions_muted": "{count} stummgeschaltet; der Autotuner schlägt diese nicht mehr vor.", + "n_suggestions_muted": "{count} Vorschläge stummgeschaltet; der Autotuner schlägt diese nicht mehr vor.", + "n_suggestions_muted_one": "{count} Vorschlag stummgeschaltet; der Autotuner schlägt ihn nicht mehr vor.", + "n_suggestions_muted_other": "{count} Vorschläge stummgeschaltet; der Autotuner schlägt diese nicht mehr vor.", "font_size_hint": "Alles in diesem Panel vergrößern oder verkleinern. Gilt für Ihr Konto auf diesem Gerät.", - "import_history_description": "Hatten Sie schon vor WashData einen Smart Plug? Laden Sie einen Verlaufsexport seines Leistungssensors hoch oder lassen Sie ihn direkt aus Home Assistant lesen: Die normale Erkennung läuft darüber, sodass vergangene Zyklen in Ihrer Zyklusliste auftauchen und dort benannt werden können.", - "hist_input_hint": "Laden Sie eine CSV-Datei aus dem Verlaufs-Panel hoch (Entität, Status, Letzte Änderung), oder lassen Sie WashData den Verlauf des Sensors direkt lesen. Die Erkennung läuft dann genau so darüber wie im Livebetrieb, und Sie wählen aus, welche der gefundenen Zyklen behalten werden.", - "hist_recorder_hint": "Liest ab dem gewählten Datum bis jetzt. Home Assistant speichert detaillierte Verlaufsdaten standardmäßig 10 Tage lang und danach nur noch Stundenmittelwerte, die für die Zykluserkennung zu grob sind - wählen Sie nur dann ein weiter zurückliegendes Datum, wenn Ihr Recorder auf eine längere Aufbewahrung eingestellt ist.", - "hist_scanning": "Ihr Verlauf wird durch die Erkennung abgespielt. Das läuft im Hintergrund - Sie können diesen Dialog schließen und später zurückkommen.", + "import_history_description": "Hatten Sie schon vor WashData einen Smart Plug? Laden Sie seinen Leistungsverlauf hoch oder lesen Sie ihn aus Home Assistant. Die Erkennung läuft darüber, und vergangene Zyklen erscheinen in Ihrer Zyklusliste, bereit zum Benennen.", + "hist_input_hint": "Laden Sie eine CSV aus dem Verlaufs-Panel hoch (Entität, Status, Letzte Änderung) oder lassen Sie WashData den Verlauf des Sensors direkt lesen. Die Live-Erkennung läuft darüber, und Sie wählen, welche Zyklen behalten werden.", + "hist_recorder_hint": "Liest ab dem gewählten Datum bis jetzt. Home Assistant speichert detaillierten Verlauf standardmäßig 10 Tage; ältere Stundenmittelwerte sind für die Zykluserkennung zu grob.", + "hist_scanning": "Ihr Verlauf läuft im Hintergrund durch die Erkennung. Sie können diesen Dialog schließen und später zurückkommen.", "hist_imported_count": "{n} Zyklen importiert.", "hist_duplicates": "{n} waren bereits importiert und wurden übersprungen.", "hist_capped": "Das Limit pro Gerät für importierte Zyklen wurde erreicht; der Rest wurde nicht gespeichert.", - "hist_next_step": "Sie liegen in Ihrer Zyklusliste, gekennzeichnet als importierter Verlauf. Öffnen Sie einen und benennen Sie über \"Beschriften\" das zugehörige Programm.", + "hist_next_step": "Sie stehen in Ihrer Zyklusliste, gekennzeichnet als importierter Verlauf. Öffnen Sie einen und benennen Sie über „Beschriften“ sein Programm.", "hist_rows_read": "{n} Messwerte gelesen", "hist_entity_substituted": "{used} gelesen (dieses Gerät ist für {wanted} konfiguriert)", "hist_breaks": "{n} Lücken, in denen der Sensor nicht verfügbar war", @@ -1095,57 +1028,88 @@ "hist_scan_capped": "Es werden nur die ersten Kandidaten angezeigt ({n} wurden gefunden).", "hist_recorder_empty": "Home Assistant hat für diesen Sensor in diesem Zeitraum keine detaillierten Verlaufsdaten.", "hist_scan_failed": "Suche fehlgeschlagen.", - "hist_scan_expired": "Diese Suche ist nicht mehr verfügbar. Bitte erneut suchen.", + "hist_scan_expired": "Diese Suche ist nicht mehr verfügbar. Suchen Sie erneut.", "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.", - "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.", + "imported_history_readonly": "Im importierten Leistungsverlauf gefunden. Er hilft beim Abgleich, zählt aber nicht in Ihre Statistik und kann nicht gekürzt oder aufgeteilt werden. Beschriften Sie ihn, um das Programm zu benennen.", + "odometer_intro": "Diese Gesamtzahl steigt nur. Das Löschen eines Zyklus verringert sie nicht, damit Wartungserinnerungen korrekt bleiben.", + "odometer_correct_hint": "Setzen Sie diesen Wert, wenn WashData einen Lauf gezählt hat, der nie stattfand, 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}", "toast_range_required": "Legen Sie einen gültigen Von/Bis-Bereich fest.", - "no_profiles_option": "(noch keine Profile, eines hinzufügen)" + "no_profiles_option": "(noch keine Profile, eines hinzufügen)", + "compare_cycles_title": "{count} Zyklen vergleichen", + "edit_builtin_phase": "Dies ist eine integrierte Phase. Beim Speichern entsteht eine eigene Kopie; löschen Sie die Kopie, um das Original wiederherzustellen.", + "fetch_error": "Daten konnten nicht geladen werden.", + "connection_lost": "Verbindung unterbrochen - Daten von {time} werden angezeigt.", + "pg_notify_partial": "Marker zeigen nur Benachrichtigungen für Start, Fast fertig, Ende, Meilensteine und Ruhezeiten. Live-Updates, Entladeerinnerungen und Überlänge-Warnungen werden nicht simuliert.", + "connect_to_confirm": "Verbinden Sie sich im Einstellungszahnrad, um zu bestätigen oder zu bewerten.", + "coverage_gap_cluster": "{count} unbeschriftete Zyklen der letzten Zeit mit etwa {min} Min. Laufzeit sehen nach einem Programm aus, das Sie noch nicht erstellt haben.", + "pg_label_yes": "Würde am Zyklusende als \"{name}\" beschriftet", + "pg_label_no": "Würde um Überprüfung bitten: {reason}", + "live_match_debug_tip": "Vertrauen: wie genau die aktuelle Leistungskurve zum besten Kandidatenprofil passt (0-100%). Mehrdeutig: Die beiden besten Kandidaten liegen weniger als 5% auseinander - das Label ist unsicher, bis der Zyklus endet.", + "automation_alias_started": "{device}: Zyklus gestartet", + "automation_alias_finished": "{device}: Zyklus beendet", + "automation_desc_started": "Wird ausgeführt, wenn ein {device}-Zyklus startet. Fügen Sie Ihre Aktionen hinzu (Benachrichtigung, Licht, ...).", + "automation_desc_finished": "Wird ausgeführt, wenn ein {device}-Zyklus endet. Fügen Sie Ihre Aktionen hinzu (Benachrichtigung, Licht, ...).", + "feedback_detected": "Von WashData erkannt: {name}", + "pg_outcome_stale": "Die Einstellungen wurden geändert, seit diese Simulation lief. Sie zeigt noch das alte Ergebnis.", + "pg_compare_profile_tip": "Wählt nur das Profil, das über das Diagramm gelegt wird. Die Simulation ordnet den Zyklus immer selbstständig zu, so wie die Integration im laufenden Betrieb.", + "pg_task_busy": "Für dieses Gerät läuft bereits ein Test am Verlauf oder eine Optimierung. Warten Sie, bis der Vorgang fertig ist, oder brechen Sie ihn ab.", + "pg_sweep_guarded": "Beendet mehr Zyklen zu früh, teilt mehr davon auf oder verpasst Zyklen, die die aktuelle Einstellung erkennt, und wird daher nie empfohlen.", + "pg_sweep_n_cycles": "über {count} Zyklen", + "pg_sweep_n_cycles_one": "über {count} Zyklus", + "pg_sweep_n_cycles_other": "über {count} Zyklen", + "pg_sweep_keep_current": "Behalten Sie den aktuellen Wert ({value}, {metric}): Kein Wert war um mindestens einen Zyklus besser.", + "hist_truncated": "beim Limit von {n} Messwerten angehalten, daher wurde der restliche Verlauf nicht durchsucht", + "hist_warn_kw": "Der höchste Messwert beträgt nur {w}. Dieser Sensor scheint Kilowatt zu melden; WashData braucht Watt, daher können keine Zyklen erkannt werden.", + "hist_warn_naive_time": "{n} Zeitstempel haben keine Zeitzone und wurden als UTC gelesen. Wenn die Datei Ortszeit verwendet, sind die Zyklen um Ihren UTC-Versatz verschoben, und Messwerte nahe einer Sommerzeitumstellung können verloren gehen.", + "hist_warn_naive_time_one": "{n} Zeitstempel hat keine Zeitzone und wurde als UTC gelesen. Wenn die Datei Ortszeit verwendet, sind die Zyklen um Ihren UTC-Versatz verschoben, und Messwerte nahe einer Sommerzeitumstellung können verloren gehen.", + "hist_warn_naive_time_other": "{n} Zeitstempel haben keine Zeitzone und wurden als UTC gelesen. Wenn die Datei Ortszeit verwendet, sind die Zyklen um Ihren UTC-Versatz verschoben, und Messwerte nahe einer Sommerzeitumstellung können verloren gehen.", + "hist_tail_trimmed": "{min} Min. Wartezeit nach dem Zyklusende wurden abgeschnitten, wie es auch die Integration im laufenden Betrieb tut", + "invalid_threshold_pair": "Der Stoppschwellenwert ({stop} W) muss unter dem Startschwellenwert ({start} W) liegen. Es wurde nichts gespeichert; ändern Sie beide Werte gemeinsam.", + "import_undo_hint": "Ein Wiederherstellungspunkt von vor dem letzten Import ({when}) wird aufbewahrt: {profiles} Profil(e) und {cycles} Zyklus/Zyklen.", + "replace_undo_hint": "Zuvor wird ein Wiederherstellungspunkt gespeichert, sodass Sie diesen Import unter „Exportieren / Importieren“ rückgängig machen können.", + "pg_in_sample_note": "Diese Zyklen haben die Profile mitgebildet, mit denen sie abgeglichen werden, daher sind die Ergebnisse optimistisch: Bei neuen Zyklen ist mit etwas weniger richtigen Zuordnungen zu rechnen." }, "pg_desc": { "dishwasher_end_spike_quiet_release": "Geschirrspüler: Ruhesekunden nach der erwarteten Dauer, bevor die Wartezeit auf das Abpumpen am Zyklusende freigegeben wird", - "smart_termination_duration_ratio": "Anteil der erwarteten Dauer des zugeordneten Programms, den ein Zyklus erreichen muss, bevor die intelligente Beendigung ihn vorzeitig beenden darf; bei last- oder temperaturabhängigen Geräten senken", + "smart_termination_duration_ratio": "Anteil der durchschnittlichen Programmdauer, den ein Zyklus erreichen muss, bevor die intelligente Beendigung ihn vorzeitig beenden darf; senken, wenn die Laufzeiten schwanken", "completion_min_seconds": "Kürzester Lauf, der als echter Zyklus zählt", - "end_repeat_count": "Aufeinanderfolgende niedrige Messwerte vor dem Ende", "interrupted_min_seconds": "Kurze Zyklen werden als unterbrochen markiert", "min_off_gap": "Erforderliche Lücke, um zwei Zyklen zu trennen", "off_delay": "Sekunden mit geringer Leistung, bevor der Zyklus endet", "start_duration_threshold": "Sekunden über dem Schwellenwert, um den Start zu bestätigen", "start_threshold_w": "Mindestwattzahl, um als gestartet zu gelten", "stop_threshold_w": "Darunter gilt die Maschine als aus", - "dtw_bandwidth": "Stufe 3: Sakoe-Chiba-Verzerrungsband (0 = DTW aus; Standard 0,2 = 20% der Zykluslänge)", - "corr_weight": "Stufe 2: Balance zwischen Kurvenform (Korrelation) und Leistungspegel (MAE); Standard 0,45", - "duration_weight": "Stufe 4: wie stark die Dauerübereinstimmung den Endscore beeinflusst (Standard 0,22)", - "energy_weight": "Stufe 4: wie stark die Energieübereinstimmung den Endscore beeinflusst (Standard 0,22)", - "profile_match_min_duration_ratio": "Stufe 1: minimale Zyklusdauer als Bruchteil der Profildauer; Standard 0,1 (10%)", - "profile_match_max_duration_ratio": "Stufe 1: maximale Zyklusdauer als Bruchteil der Profildauer; Standard 1,8 (180%)", - "keep_min_score": "Stufe 2: Mindest-Score, um im Auswahlverfahren zu bleiben; Standard 0,1 lässt schwache Kandidaten zu, spätere Stufen wählen den besten", - "dtw_blend": "Stufe 3: 0 = nur Kernscore, 1 = nur DTW-Score, 0,5 = gleiche Mischung (Standard)", - "dtw_ensemble_w": "Stufe 3 (Ensemble-Modus): Gewicht auf skaliertem L1 gegenüber Ableitungs-DTW; Standard 0,7 bevorzugt pegelabhängige Erkennung", - "dtw_ddtw_scale": "Stufe 3: DDTW-Halbsättigungsabstand; kleiner = formempfindlicher (Standard 30)", - "dtw_refine_top_n": "Stufe 3: per DTW neu bewertete Kandidaten; auf 7-9 erhöhen, wenn das richtige Profil auf Platz 4-5 liegt (Standard 5)", - "duration_scale": "Stufe 4: Log-Verhältnis, bei dem die Dauerübereinstimmung halbiert wird; kleiner = strengere Strafe (Standard 0,175)", - "energy_scale": "Stufe 4: Log-Verhältnis, bei dem die Energieübereinstimmung halbiert wird; kleiner = strengere Strafe (Standard 0,25)", + "profile_match_min_duration_ratio": "Minimale Zykluslänge als Anteil der Profildauer; Standard 0,1 (10%)", + "profile_match_max_duration_ratio": "Maximale Zykluslänge als Anteil der Profildauer; Standard 1,8 (180%)", "anti_wrinkle_idle_timeout": "Erlaubte Stille zwischen zwei Trommelimpulsen, bevor der Knitterschutz endet", "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_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", + "anti_wrinkle_exit_power": "Pegel, der zwischen Trommelimpulsen als Stille gilt; wirkt nur über dem Stoppschwellenwert", + "anti_crease_finalize_ratio": "Anteil der durchschnittlichen Programmdauer, den ein Zyklus erreichen muss, bevor der Knitterschutz-Nachlauf ihn abschließen darf; bei Trocknern senken, Waschmaschinen beim Standard lassen", "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.", "anti_wrinkle": "Regelmäßige Trommelumwälzungen nach dem Zyklus, um Knitterung zu vermeiden.", + "baking": "Heizelement bei hoher Temperatur aktiv, für Kruste und Krume.", + "cooking": "Hauptgarphase mit aktiver Heizung und aktivem Lüfter.", "cool_down": "Taumeln ohne Hitze gegen Ende des Zyklus.", + "cool_down_af": "Abkühlen nur mit dem Lüfter nach dem Heizen.", "drain_and_switch": "Übergangszeit vom Wasch- zum Trocknungsmodus.", "dry": "Trocknungsphase mittels Heizung und/oder Restwärme.", "drying": "Hauptheizperiode.", "heat_up": "Erstes Aufwärmen der Heizung, bevor die vollständige Trocknung beginnt.", + "keep_warm": "Warmhaltephase mit geringer Hitze, damit das Essen warm bleibt.", + "keep_warm_bm": "Warmhaltephase mit geringer Hitze, damit das Brot nach dem Backen warm bleibt.", + "kneading": "Der Motor mischt und knetet den Teig. Hohe Leistungsaufnahme.", + "pause_af": "Kurze Pause zum Schütteln oder Nachsehen.", + "pre_heat": "Erstes Aufheizen des Garraums vor dem eigentlichen Garen.", "pre_rinse": "Erstes Aufsprühen vor dem Waschen mit Reinigungsmittel.", + "proving": "Gehzeit bei geringer Wärme, damit die Hefe arbeitet und der Teig aufgeht.", + "resting": "Kurze Pause mit geringer Leistung zwischen den Knetphasen, damit sich das Gluten entspannt.", "pre_wash": "Erstes Einweichen oder Vorbehandeln vor dem Hauptwaschgang.", "rinse": "Spülphase mit klarem Wasser. Diese Phase kann sich mehrmals wiederholen.", "sanitize": "Hochtemperatur-Reinigungsstufe für Desinfektionsprogramme.", @@ -1158,6 +1122,7 @@ "wash_dw": "Hauptwaschmittelwäsche mit Erhitzen." }, "placeholder": { + "custom_task": "z. B. Türdichtung reinigen", "add": "hinzufügen…", "group_name": "z. B. Baumwolle 2:47", "json_buttons": "[{\"action\":\"ID\",\"title\":\"Label\"}]", @@ -1185,10 +1150,10 @@ "section": { "dishwasher": { "label": "Geschirrspüler", - "intro": "Behandlung des Zyklusendes für Geschirrspüler, die typischerweise mit einer langen, nahezu stillen Trocknungsphase vor einem kurzen letzten Abpumpen enden." + "intro": "Endbehandlung für Geschirrspüler, die oft lange still trocknen, bevor sie kurz abpumpen." }, "anti_wrinkle": { - "intro": "Knitterschutz-Modus erkennt Tumble-Impulse mit geringer Leistung nach der Hauptphase und hält sie dem abgeschlossenen Zyklus zugeordnet.", + "intro": "Hält die Trommelimpulse eines Trockners mit geringer Leistung nach der Hauptphase beim abgeschlossenen Zyklus.", "label": "Knitterschutz" }, "basic": { @@ -1196,7 +1161,7 @@ "label": "Grundlegend" }, "delay": { - "intro": "Die Verzögerungsstarterkennung erkennt, wenn ein Gerät eingeschaltet ist, aber seinen Zyklus noch nicht begonnen hat.", + "intro": "Erkennt ein Gerät, das eingeschaltet ist und auf einen verzögerten Start wartet.", "label": "Verzögerter Start" }, "detection": { @@ -1220,157 +1185,149 @@ "label": "Zeitsteuerung & Watchdog" }, "triggers": { - "intro": "Optionale externe Signale: ein Endauslöser, ein Türsensor, ein Pause-Schalter und die Entladeerinnerung.", + "intro": "Optionale Extras: Endauslöser, Türsensor, Pause-Schalter und Entladeerinnerung.", "label": "Auslöser & Türen" - }, - "phase_eta": { - "label": "Verbleibende Zeit", - "intro": "Phasenbewusste Restzeit, für Maschinen, deren Zyklusdauer von Temperatur oder Schleuderdrehzahl abhängt." } }, "setting": { "dishwasher_end_spike_quiet_release": { "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." + "doc": "Nach der erwarteten Dauer: wie lange der Geschirrspüler unter dem Stoppschwellenwert bleiben muss, bevor WashData nicht mehr auf das letzte Abpumpen wartet. Erhöhen, wenn ein spätes Abpumpen nach langem Trocknen verpasst wird. Die Wartezeit überschreitet nie 30 Minuten." }, "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." + "doc": "Anteil der durchschnittlichen Programmdauer, den ein Zyklus erreichen muss, bevor sein Trommeln mit geringer Leistung als Knitterschutz-Nachlauf gilt. Senken Sie den Wert (z. B. 0,75) bei Trocknern mit Beladungserkennung. Lassen Sie Waschmaschinen beim Standard (0,98), sonst kann eine Wäsche mittendrin enden." }, "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.", + "doc": "Die kurzen Trommelimpulse eines Trockners mit geringer Leistung nach der Hauptheizphase beim abgeschlossenen Zyklus behalten, statt sie als neue Zyklen zu werten.", "label": "Knitterschutzerkennung aktivieren" }, "anti_wrinkle_exit_power": { - "doc": "Die Leistung muss zwischen den Impulsen unter diesen Wert fallen, damit der Anti-Falten-Modus aktiv bleibt.", + "doc": "Im Knitterschutz gilt Leistung unter diesem Wert als Stille zwischen Trommelimpulsen, und Stille länger als die Maximale Impulspause beendet den Modus. Ist der Stoppschwellenwert höher, wird er verwendet; dieser Wert wirkt also nur, wenn er über dem Stoppschwellenwert liegt.", "label": "Ausgangsleistungsschwellenwert" }, "anti_wrinkle_max_duration": { - "doc": "Längere Impulse werden als echter Zyklus und nicht als Anti-Falten-Wäsche behandelt.", + "doc": "Ein längerer Impuls gilt als echter Zyklus, nicht als Knitterschutz-Trommeln.", "label": "Maximale Dauer" }, "anti_wrinkle_max_power": { - "doc": "Ein Puls über dieser Leistung wird als echter neuer Zyklus behandelt, nicht als Anti-Falten-Sturz. Knapp oberhalb der Taumelimpulsleistung einstellen.", + "doc": "Ein Impuls über dieser Leistung gilt als echter neuer Zyklus, nicht als Trommeln. Knapp über der Leistung Ihrer Trommelimpulse einstellen.", "label": "Max. Knitterschutzleistung" }, "auto_label_confidence": { - "doc": "Liegt der Match-Score am Zyklusende bei oder darüber, wird das Programm automatisch ohne Sicherheitsabfrage gekennzeichnet. Erhöhen Sie den Wert, um vor der automatischen Kennzeichnung eine höhere Sicherheit zu erfordern. Reduzieren Sie es, um mehr zu automatisieren. Funktioniert in Verbindung mit der darunter liegenden Lernsicherheit.", + "doc": "Übereinstimmungswert am Zyklusende, ab dem das Programm ohne Rückfrage beschriftet wird. Erhöhen für mehr Sicherheit; senken, um mehr Zyklen automatisch zu beschriften. Über der Lernkonfidenz halten.", "label": "Automatische Beschriftungskonfidenz" }, "auto_maintenance": { - "doc": "Führen Sie die nächtliche Verwaltung durch: Erstellen Sie Profilumschläge neu, berechnen Sie den Zykluszustand neu, bereinigen Sie Debug-Traces und behalten Sie die neuesten Zyklen bei.", + "doc": "Nächtliche Bereinigung: alte Zyklen beschriften, die jetzt eindeutig zu einem Programm passen, Programmkurven neu erstellen, Anomalie-Markierungen und Zyklusgesundheit aktualisieren und Debug-Traces entfernen, wenn „Debug-Traces speichern“ aus ist. Zyklen und ihre Leistungsverläufe bleiben immer erhalten.", "label": "Automatische Wartung (nächtliche Bereinigung)" }, "completion_min_seconds": { - "doc": "Zyklen, die kürzer sind, werden als Geisterzyklen (Testläufe, Öffnen der Tür zum Hinzufügen einer Socke) verworfen.", + "doc": "Zyklen, die kürzer sind als dieser Wert (Testläufe, Tür öffnen, um eine Socke nachzulegen), werden als Unterbrochen gespeichert und erhalten keine Fertig-Benachrichtigung.", "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." + "doc": "Fügt Messwerte abgebrochener Startversuche am Anfang eines Zyklus hinzu, für Maschinen, die vor dem Start antasten (Türverriegelung, erstes Befüllen). 300 s reichen meist; 0 = aus. Aufgezeichnete Starts rücken nach vorn, daher driften gelernte Dauern eine Weile." }, "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.", + "doc": "Die Leistung muss so lange im Standby-Bereich bleiben, bevor das Gerät als auf den Start wartend gilt.", "label": "Bestätigungssekunden" }, "delay_start_detect_enabled": { - "doc": "Erkennen Sie, wenn das Gerät eingeschaltet ist und wartet (verzögerter Start/Standby), aber seinen Zyklus noch nicht begonnen hat, sodass die Standby-Entnahme nicht mit einem laufenden Zyklus verwechselt wird.", + "doc": "Ein Gerät erkennen, das eingeschaltet ist und auf einen verzögerten Start wartet, damit seine Standby-Leistung nicht als laufender Zyklus gilt.", "label": "Verzögerungsstarterkennung aktivieren" }, "delay_timeout_hours": { - "doc": "Hören Sie nach so vielen Stunden auf, im verzögerten Startmodus zu warten, und kehren Sie in den Leerlauf zurück, sodass eine Maschine, die zwar eingeschaltet bleibt, aber nie gestartet wird, nicht ewig wartet.", + "doc": "Nach so vielen Stunden das Warten auf einen verzögerten Start aufgeben und in den Leerlauf zurückkehren.", "label": "Timeout-Stunden" }, "device_type": { - "doc": "Geräteklasse. Legt sinnvolle Erkennungsstandardwerte (Schwellenwerte, Ausschaltverzögerung, Endbehandlung) fest, die auf diesen Appliance-Typ abgestimmt sind. Ändern Sie es nur, wenn das Gerät ursprünglich als falscher Typ eingerichtet wurde.", + "doc": "Legt Erkennungsstandards (Schwellenwerte, Abschaltverzögerung, Endbehandlung) für diese Geräteart fest. Nur ändern, wenn das Gerät als falscher Typ eingerichtet wurde.", "label": "Gerätetyp" }, "door_sensor_entity": { - "doc": "Optionaler binärer Türsensor. Wird verwendet, um zu erkennen, wann das Gerät nach einem Zyklus geöffnet/entladen wurde.", + "doc": "Optionaler Türsensor. Zeigt, wann das Gerät nach einem Zyklus zum Entladen geöffnet wurde.", "label": "Türsensor-Entität" }, "duration_tolerance": { - "doc": "Toleranz für Schätzungen der verbleibenden Zeit (Lernfeedback, kein Matching). Wenn die tatsächliche Dauer innerhalb von +/-X % der Schätzung liegt, gilt dies als gute Übereinstimmung.", + "doc": "Nur Lernfeedback: Ein Zyklus innerhalb von +/-X % seiner geschätzten Dauer gilt als gute Schätzung. Beeinflusst den Abgleich nicht.", "label": "Schätzungstoleranz" }, "enable_ml_models": { - "doc": "Während ein Zyklus läuft, können die Modelle die Live-Ergebnisse verfeinern: eine gleichmäßigere Schätzung der verbleibenden Zeit und der Energie/Kosten sowie einen Schutz vor vorzeitigem Stopp bei der Enderkennung (er kann ein Ende immer nur verzögern, niemals vorzeitig beenden und ist begrenzt). Verwendet Ihre fein abgestimmten Modelle, sofern verfügbar, ansonsten die integrierten. Aus = nur die klassische leistungsbasierte Logik (immer noch zuverlässig).", + "doc": "Ein auf dieser Maschine trainiertes Modell sagt Energie und Kosten eines laufenden Zyklus voraus, und ein gut bekanntes Programm, das ungewöhnlich früh stoppt, darf ohne die volle Abschaltverzögerung enden. Aus: nur leistungsbasierte Logik.", "label": "Intelligente Modelle während eines Zyklus anwenden" }, "end_energy_threshold": { - "doc": "Während des Countdowns zur Ausschaltverzögerung wird die kumulierte Energie (Watt x Zeit) mit diesem Schwellenwert verglichen. Bei Überschreitung wird der Countdown zurückgesetzt – so bleiben Knitterschutz-Wäschetrockner und Spülmaschinentrocknungsschwänze im Zyklus, anstatt sie abzubrechen. Erhöhen Sie den Wert, wenn die Zyklen während des Abkühlens zu früh enden. Verringern Sie den Wert, wenn die Erkennung langsam ist.", + "doc": "Während der Abschaltverzögerung erlaubte Energie. Wird mehr verbraucht, läuft der Zyklus weiter, sodass Trommelbewegungen und Trocknungsphasen dazugehören. Senken, wenn Zyklen während eines Nachlaufs enden; erhöhen, wenn abgeschlossene Zyklen weiter warten.", "label": "Endenergie" }, - "end_repeat_count": { - "doc": "Anzahl der aufeinanderfolgenden Messwerte unterhalb des Stoppschwellenwerts, die erforderlich sind, bevor der Zyklus endet. 1 reicht für die meisten Stecker aus. Erhöhen Sie den Wert auf 2–3, wenn Ihr Smart Plug mitten im Zyklus gelegentlich eine falsche Nullprobe meldet und Ihre Zyklen vorzeitig enden.", - "label": "End-Wiederholungsanzahl" - }, "energy_price_entity": { - "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.", + "doc": "Sensor mit dem aktuellen Preis pro kWh (z. B. ein dynamischer Tarif). Er hat Vorrang vor dem statischen Preis unten, muss also ein Preis sein, nicht der Leistungs- oder Energiesensor der Steckdose.", "label": "Energiepreis-Entität" }, "energy_price_static": { - "doc": "Fester Preis pro kWh, der für Kostenzahlen verwendet wird, wenn oben keine Live-Preiseinheit festgelegt ist.", + "doc": "Fester Preis pro kWh, verwendet, wenn keine Preis-Entität festgelegt ist.", "label": "Statischer Energiepreis (pro kWh)" }, "energy_sensor": { - "doc": "Optionaler kumulativer Energiezähler (total_increasing in kWh/Wh, z. B. der eigene Lebensdauerzähler der Steckdose). Wenn gesetzt, wird die gemeldete Energie jedes Zyklus aus der Differenz dieses Zählers zwischen Anfang und Ende ermittelt, was die Untererfassung vermeidet, die beim Integrieren eines langsam meldenden Leistungssensors entsteht. Greift auf den integrierten Wert zurück, wenn der Messwert fehlt, seine Einheit unbekannt ist oder die Differenz nicht positiv ausfällt. Leer lassen, um weiterhin den Leistungssensor zu integrieren.", + "doc": "Optionaler kWh- oder Wh-Zähler (total_increasing, z. B. der eigene Zähler der Steckdose). Die Zyklusenergie wird dann daraus gelesen, was genauer ist als die Berechnung aus einem langsamen Leistungssensor. Ist sein Wert unbrauchbar, wird der berechnete Wert verwendet.", "label": "Energiezähler-Entität" }, "expose_debug_entities": { - "doc": "Veröffentlichen Sie zusätzliche diagnostische HA-Entitäten (Übereinstimmungskonfidenz, Mehrdeutigkeit, Zustandsinterna). Aus sorgt dafür, dass die Entitätsliste für den normalen Gebrauch sauber bleibt.", + "doc": "Diagnose-Entitäten hinzufügen (Übereinstimmungskonfidenz, Mehrdeutigkeit, interner Zustand). Aus hält die Entitätsliste übersichtlich.", "label": "Debug-Entitäten anzeigen" }, "external_end_trigger": { - "doc": "Binärer Sensor, dessen Zustandsänderung das Zyklusende markiert (z. B. ein „Fertig“-Kontakt eines Geräts oder eine Companion-Integration).", + "doc": "Binärsensor, dessen Änderung das Zyklusende markiert (z. B. ein „Fertig“-Kontakt oder eine andere Integration).", "label": "Externe Auslöser-Entität" }, "external_end_trigger_enabled": { - "doc": "Zusätzlich zur integrierten strombasierten Erkennung kann ein externer Binärsensor das Ende eines Zyklus signalisieren.", + "doc": "Einen Binärsensor das Zyklusende melden lassen, zusätzlich zur leistungsbasierten Erkennung.", "label": "Externen Endauslöser aktivieren" }, "external_end_trigger_inverted": { - "doc": "Behandeln Sie das Ausschalten des Auslösesensors (und nicht das Einschalten) als Signal für das Ende des Zyklus.", + "doc": "Das Ausschalten (statt Einschalten) des Sensors als Endsignal werten.", "label": "Externen Auslöser umkehren (bei AUS auslösen)" }, "learning_confidence": { - "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.", + "doc": "Niedrigster Übereinstimmungswert, um einen abgeschlossenen Zyklus zu beschriften. Ein klarer Sieger wird beschriftet; ein knapper Fall kommt in die Bewertungswarteschlange unter Zyklen. Darunter wird nichts beschriftet oder nachgefragt. Unter der Automatischen Beschriftungskonfidenz halten.", "label": "Lernkonfidenz" }, "linked_device": { - "doc": "Verschachteln Sie dieses WashData-Gerät optional unter einem anderen Gerät (z. B. dem Smart Plug) in der HA-Geräteregistrierung, angezeigt als „Verbunden über ...“.", + "doc": "Optional. Dieses WashData-Gerät in Home Assistant unter einem anderen Gerät (z. B. dem Smart Plug) anzeigen, als „Verbunden über ...“.", "label": "Unter Gerät gruppieren" }, "min_off_gap": { - "doc": "Wenn die Maschine kürzer als diese Zeit ausgeschaltet ist, wird die Ein-/Aus-/Ein-Sequenz als ein kontinuierlicher Zyklus behandelt. Verhindert, dass Einweichprogramme (Maschine schaltet sich während des Waschens für einige Minuten aus) in zwei separate Zyklen aufgeteilt werden. Wenn Sie möchten, dass diese als separate Zyklen gezählt werden, stellen Sie den Wert kürzer ein als die Lücke zwischen Ihren aufeinanderfolgenden Ladungen. Gerätetyp-Standardeinstellungen schützen die typische Intra-Zyklus-Pause für jedes Gerät.", + "doc": "Eine kürzere Aus-Phase teilt keinen Zyklus, sodass Einweichpausen in einer Wäsche bleiben. Kürzer halten als die Pause zwischen aufeinanderfolgenden Ladungen, sonst verschmelzen diese zu einem Zyklus.", "label": "Mindest-Aus-Lücke" }, "min_power": { - "doc": "Absolute Mindestleistung gilt als aktiv. Werte darunter werden als 0 W (Standby) behandelt, wobei die Phantomlast von Smart Plugs und Standby-LEDs herausgefiltert wird.", + "doc": "Werte darunter gelten als 0 W (Standby). Filtert die Leerlaufleistung von Smart Plugs und Standby-LEDs heraus.", "label": "Mindestleistung" }, "ml_training_enabled": { - "doc": "Studieren Sie Ihre überprüften Zyklen regelmäßig über Nacht und stimmen Sie die Modelle genau auf diese spezielle Maschine ab. Eine Änderung wird nur dann beibehalten, wenn sie in durchgehaltenen Zyklen tatsächlich besser abschneidet. Dies kann also nur helfen oder gleich bleiben – niemals rückläufig sein.", + "doc": "Das Energiemodell regelmäßig mit den abgeschlossenen Zyklen dieser Maschine neu trainieren. Ein neues Modell wird nur behalten, wenn es auf Testzyklen die einfache Schätzung schlägt.", "label": "Von dieser Maschine lernen" }, "ml_training_hour": { - "doc": "Lokale Tageszeit (0-23), um die Feinabstimmung über Nacht vorzunehmen. Wählen Sie eine ruhige Stunde, z. B. 2 Uhr (02:00 Uhr).", + "doc": "Uhrzeit (0-23) für das nächtliche Training. Wählen Sie eine ruhige Stunde, z. B. 2.", "label": "Lernen zur Stunde" }, "ml_training_interval_days": { - "doc": "Überprüfen Sie alle diese Tage höchstens einmal erneut, ob sich Verbesserungen ergeben.", + "doc": "Höchstens einmal alle so vielen Tage neu trainieren.", "label": "Höchstens alle prüfen" }, "ml_training_min_cycles": { - "doc": "Warten Sie mit der Feinabstimmung, bis mindestens so viele Zyklen aufgezeichnet wurden, damit Sie genug lernen können.", + "doc": "Mit dem Training warten, bis mindestens so viele Zyklen aufgezeichnet sind.", "label": "Zuerst benötigte Zyklen" }, "name": { - "doc": "Anzeigename, der in der HA-Integrationsliste und der Geräteregistrierung angezeigt wird.", + "doc": "Name dieses Geräts in Home Assistant.", "label": "Gerätename" }, "no_update_active_timeout": { - "doc": "Wenn während der Ausführung so lange keine Stromaktualisierungen eintreffen, gehen Sie davon aus, dass der Stecker offline gegangen ist, und erzwingen Sie den Stopp, um einen Zombie-Zyklus zu vermeiden. Der Standardwert von 600 s ermöglicht Cloud- oder Mesh-Verzögerungen.", + "doc": "Kommt während eines Zyklus so lange keine Leistungsaktualisierung, gilt die Steckdose als offline und der Zyklus wird zwangsweise gestoppt. Der Standard von 600 s lässt Spielraum für Cloud- oder Mesh-Verzögerungen.", "label": "Timeout ohne Aktualisierung" }, "notify_before_end_minutes": { @@ -1378,35 +1335,35 @@ "label": "Vorend-Alarm" }, "notify_channel": { - "doc": "Name des Android-Benachrichtigungskanals für Start-/Live-Nachrichten (steuert den Ton pro Kanal und die Priorität in der mobilen App). Blank verwendet die Standardeinstellung der Begleit-App.", + "doc": "Android-Kanal für Start- und Live-Nachrichten; der Kanal legt Ton und Priorität fest. Leer: App-Standard.", "label": "Android-Kanal (Start/Live)" }, "notify_cycle_timers": { - "doc": "Benachrichtigungen zu bestimmten Minuten in einem Zyklus (z. B. um Weichspüler hinzuzufügen). Die Nachricht unterstützt {device}, {program}, {minutes}. Aktivieren Sie die automatische Pause, um an diesem Punkt zu pausieren und eine interaktive Benachrichtigung mit der Schaltfläche „Fortsetzen“ zu erhalten. Fortsetzen über das Bedienfeld, den Dienst „Pause/Fortsetzen“ oder die Benachrichtigungsaktion.", + "doc": "Benachrichtigungen nach festgelegten Minuten in einem Zyklus (z. B. um Weichspüler hinzuzufügen). Die Nachricht unterstützt {device}, {program}, {minutes}. Mit Automatische Pause wird der Zyklus dort auch angehalten und die Benachrichtigung erhält eine Schaltfläche „Fortsetzen“.", "label": "Zyklustimer" }, "notify_finish_channel": { - "doc": "Name des Android-Benachrichtigungskanals für die Zielnachricht. Blank verwendet den Start-/Live-Kanal wieder.", + "doc": "Android-Kanal für die Endnachricht. Leer: Start-/Live-Kanal wird verwendet.", "label": "Android-Kanal (Ende)" }, "notify_finish_message": { - "doc": "Text, der gesendet wird, wenn ein Zyklus endet. Vorlagenvariablen: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}. {time_finished} und {vs_typical} sind hier am nützlichsten.", + "doc": "Text, der beim Zyklusende gesendet wird. Variablen: {device}, {program}, {duration} (Minuten), {duration_hm} (1 h 05 min), {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}, {status} (completed oder force_stopped; unterbrochene Zyklen senden nichts). Bei jeder anderen Variable wird der Standardtext verwendet.", "label": "Endnachricht" }, "notify_finish_services": { - "doc": "notify.*-Dienste, die aufgerufen werden, wenn ein Zyklus beendet ist. Fügen Sie eins pro Ziel hinzu; Leer lassen, um keine Abschlussbenachrichtigung zu erhalten.", + "doc": "notify.*-Dienste, die beim Zyklusende aufgerufen werden. Leer lassen für keine.", "label": "Enddienste" }, "notify_fire_events": { - "doc": "Lösen Sie außerdem ha_washdata_*-Ereignisse bei Zyklusstart/-ende aus, damit Sie Ihre eigenen Automatisierungen erstellen können.", + "doc": "Zusätzlich ha_washdata_*-Ereignisse bei Zyklusstart und -ende auslösen, für Ihre eigenen Automatisierungen.", "label": "HA-Ereignisse für Benachrichtigungen auslösen" }, "notify_icon": { - "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.", + "doc": "Kleines Symbol auf der Benachrichtigung (z. B. mdi:washing-machine). Wird nur an mobile_app-Ziele gesendet, nicht an notify-Gruppen. Unter iOS ersetzt es das App-Symbol und erfordert die Companion-App 2026.8 oder neuer. Leer: Plattformstandard.", "label": "Benachrichtigungssymbol" }, "notify_live_chronometer": { - "doc": "Zeigen Sie in der Benachrichtigung einen Countdown-Timer für die Live-Aktualisierung an (auf Plattformen, die dies unterstützen) anstelle einer statischen Schätzung.", + "doc": "Einen laufenden Countdown in der Benachrichtigung anzeigen (wo unterstützt) statt einer festen Schätzung.", "label": "Live-Chronometer verwenden" }, "notify_live_interval_seconds": { @@ -1414,143 +1371,135 @@ "label": "Live-Aktualisierungsintervall" }, "notify_live_overrun_percent": { - "doc": "Wenn ein Zyklus seine Schätzung um mehr als diesen Prozentsatz überschreitet, wird eine Überschreitungswarnung gesendet.", + "doc": "Live-Updates enden, sobald ein Zyklus seine erwartete Dauer um diesen Prozentsatz überschreitet (20 = bis zum 1,2-Fachen der erwarteten Zeit).", "label": "Live-Überlauf-% vor Alarm" }, "notify_live_services": { - "doc": "notify.*-Dienste forderten Live-Fortschrittsaktualisierungen während der Ausführung eines Zyklus. Lassen Sie das Feld leer, um Live-Fortschrittsbenachrichtigungen zu deaktivieren.", + "doc": "notify.*-Dienste für Live-Fortschrittsupdates während eines Zyklus. Leer lassen, um sie zu deaktivieren.", "label": "Live-Fortschrittsdienste" }, "notify_only_when_home": { - "doc": "Senden Sie Benachrichtigungen nur, wenn mindestens eine der verknüpften Personen (oben) zu Hause ist.", + "doc": "Nur benachrichtigen, wenn mindestens eine der oben genannten Personen zu Hause ist.", "label": "Nur benachrichtigen wenn zu Hause" }, "notify_people": { - "doc": "person.*-Entitäten, die von „Notify Only When Home“ verwendet werden, um zu entscheiden, ob jemand zu Hause ist.", + "doc": "person.*-Entitäten, die „Nur benachrichtigen wenn zu Hause“ prüft.", "label": "Personen (für Nur wenn zu Hause)" }, "notify_pre_complete_message": { - "doc": "Text der Vorab-/Fast-fertig-Benachrichtigung. Vorlagenvariablen: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Text jedes Live-Fortschrittsupdates. Vorlagenvariablen: {device}, {program}, {minutes} (verbleibende Minuten). Bei jeder anderen Variable wird der Standardtext verwendet.", "label": "Vorend-Nachricht" }, "notify_reminder_message": { - "doc": "Text der Ausräum-Erinnerung (noch ausstehend). Leer verwendet die eingebaute Vorgabe. Vorlagenvariablen: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Text des einmaligen Vorend-Alarms. Vorlagenvariablen: {device}, {program}, {minutes} (die Einstellung Vorend-Alarm). Bei jeder anderen Variable wird der Standardtext verwendet.", "label": "Erinnerungsnachricht" }, "notify_start_message": { - "doc": "Text, der gesendet wird, wenn ein Zyklus startet. Vorlagenvariablen: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Text, der beim Start eines Zyklus gesendet wird. Vorlagenvariablen: {device}, {program}. Bei jeder anderen Variable wird der Standardtext verwendet.", "label": "Startnachricht" }, "notify_start_services": { - "doc": "notify.*-Dienste, die aufgerufen werden, wenn ein Zyklus beginnt. Fügen Sie eine pro Ziel hinzu (Telefon, Dashboard usw.); Für keine Startbenachrichtigung leer lassen.", + "doc": "notify.*-Dienste, die beim Zyklusstart aufgerufen werden (Telefon, Dashboard, ...). Leer lassen für keine.", "label": "Startdienste" }, "notify_timeout_seconds": { - "doc": "Die Benachrichtigung wird nach dieser Anzahl von Sekunden automatisch verworfen (auf Plattformen, die dies unterstützen). 0 behält es bei, bis es manuell verworfen wird.", + "doc": "Die Benachrichtigung nach so vielen Sekunden schließen (wo unterstützt). 0 behält sie, bis Sie sie schließen.", "label": "Automatisch schließen nach" }, "notify_title": { - "doc": "Titel der Benachrichtigung. Vorlagenvariablen: {device}, {program}, {duration}, {energy}, {cost}, {date}, {time}, {minutes}.", + "doc": "Titel der Benachrichtigung. Nur {device} wird ersetzt; bei jeder anderen Variable wird der Standardtitel verwendet.", "label": "Benachrichtigungstitel" }, "notify_unload_delay_minutes": { - "doc": "Minuten nach dem Ende eines Zyklus, bevor die noch wartende Erinnerung „Maschine entladen“ gesendet wird. Stellen Sie 0 ein, um die Erinnerung zu deaktivieren.", + "doc": "Minuten nach dem Zyklusende bis zur Erinnerung „Maschine entladen“. 0 deaktiviert sie.", "label": "Verzögerung der Entladeerinnerung" }, "notify_unload_repeat": { - "doc": "Die Entladeerinnerung wird alle \"Verzögerung der Entladeerinnerung\" Minuten erneut gesendet, bis die Entladung bestätigt wird oder Sie in der Benachrichtigung auf \"Nicht mehr erinnern\" tippen. Erfordert eine Möglichkeit, die Entladung zu bestätigen: eine Türsensor-Entität, eine Entladebestätigungs-Entität oder die Option \"Entladung manuell bestätigen\" weiter unten. Die Schaltfläche zum Verwerfen funktioniert bei Benachrichtigungen der Home Assistant Companion-App (mobil).", + "doc": "Die Entladeerinnerung alle „Verzögerung der Entladeerinnerung“ Minuten erneut senden, bis die Entladung bestätigt ist oder Sie auf „Nicht mehr erinnern“ tippen (mobile App). Erfordert eine Türsensor-Entität, eine Entladebestätigungs-Entität oder „Entladung manuell bestätigen“.", "label": "Wiederholen, bis entladen" }, "off_delay": { - "doc": "Zeit, die nach einem Stromausfall gewartet werden muss, bevor der Zyklus als beendet erklärt wird. Wenn die Stromversorgung innerhalb dieses Fensters wiederhergestellt wird, läuft der Zyklus nahtlos weiter – dies überbrückt Pausen zwischen den Waschphasen. Geschirrspüler haben lange Trocknungsphasen (Ausschalten für 20–60 Minuten), daher muss die Ausschaltverzögerung länger sein, damit der gesamte Spül- und Trockenvorgang als ein Zyklus erfolgt.", + "doc": "Wie lange die Leistung niedrig bleiben muss, bevor der Zyklus beendet ist. Kehrt die Leistung in dieser Zeit zurück, läuft der Zyklus weiter. Geschirrspüler können beim Trocknen 20-60 Min. still sein, also länger einstellen.", "label": "Abschaltverzögerung" }, "pause_cuts_power": { - "doc": "Wenn ein Zyklus angehalten wird, schalten Sie auch die Switch-Entität unten aus. Nur für Geräte, deren Stecker während des Zyklus sicher durchtrennt werden kann.", + "doc": "Beim Anhalten eines Zyklus auch die Schalter-Entität unten ausschalten. Nur für Geräte, die mitten im Zyklus sicher stromlos werden dürfen.", "label": "Pause unterbricht auch Strom (über Schalter)" }, "power_off_delay": { - "doc": "Wie lange die Leistung nach dem Ende eines Zyklus unter dem Abschaltschwellenwert bleiben muss, bevor der Status zurück zu Aus wechselt. Wird nur verwendet, wenn der Abschaltschwellenwert über 0 liegt. Die Prüfung erfolgt im Hintergrundtakt, daher wird die tatsächliche Verzögerung auf den nächsten Statusablauf-Takt aufgerundet.", + "doc": "Wie lange die Leistung nach einem Zyklus unter dem Abschaltschwellenwert bleiben muss, bevor der Status zu Aus wechselt. Nur aktiv, wenn dieser Schwellenwert über 0 liegt. Wird periodisch geprüft, daher kann die echte Wartezeit etwas länger sein.", "label": "Abschaltverzögerung" }, "power_off_threshold_w": { - "doc": "Optionale leistungsbasierte Erkennung des Ausschaltens. Bei einem Wert über 0 gilt die Maschine als ausgeschaltet und der Status wechselt zurück zu Aus, sobald ein Zyklus beendet ist und die Leistung für die Dauer der Abschaltverzögerung unter diesem Wert bleibt. Bei 0 belassen, um die Funktion zu deaktivieren (Standard). Setzen Sie den Wert über den echten Abschalt-Nullpegel und unter den Stoppschwellenwert sowie unter die Standby-Leistung Ihrer Maschine im Zustand fertig-aber-eingeschaltet; liegt er nicht unter dem Stoppschwellenwert, wird er ignoriert. Wenn aktiviert, ersetzt dieser Wert die Fortschritts-Reset-Verzögerung für die Rückkehr zu Aus, sodass eine fertige Maschine in Fertig/Sauber bleibt, bis sie tatsächlich ausgeschaltet wird.", + "doc": "Bleibt die Leistung nach einem Zyklus für die Abschaltverzögerung unter diesem Wert, wechselt der Status zu Aus, statt über die Fortschritts-Reset-Verzögerung. Über der Leistung im ausgeschalteten Zustand einstellen, unter Standby und Stoppschwellenwert. 0 deaktiviert es (Standard).", "label": "Abschaltschwellenwert" }, "power_sensor": { - "doc": "Die Sensoreinheit, die Live-Leistung in Watt für dieses Gerät meldet (z. B. sensor.washer_power). Die gesamte Zykluserkennung basiert auf diesem Signal.", + "doc": "Der Sensor, der die aktuelle Leistung dieses Geräts in Watt meldet (z. B. sensor.washer_power). Die gesamte Erkennung basiert darauf.", "label": "Leistungssensor" }, - "profile_duration_tolerance": { - "doc": "Das +/--Band um die durchschnittliche Dauer eines Profils, das beim Abgleich verwendet wird. 0,25 bedeutet, dass ein 60-Minuten-Profil 45-75-Minuten-Zyklen entspricht.", - "label": "Profildauertoleranz" - }, "profile_match_interval": { - "doc": "Wie oft soll während eines laufenden Zyklus versucht werden, einen Profilabgleich durchzuführen? Der Standardwert von 300 s (5 Minuten) gleicht Erkennungsgeschwindigkeit und CPU aus.", + "doc": "Wie oft ein laufender Zyklus mit den Profilen abgeglichen wird. Standard 300 s (5 Min.).", "label": "Abgleichintervall" }, "profile_match_max_duration_ratio": { - "doc": "Maximale Zykluslänge relativ zum Profil. 1,3 bedeutet, dass ein Zyklus weniger als 130 % der Profildauer betragen muss, um eine Übereinstimmung zu erzielen.", + "doc": "Ein Programm wird verworfen, sobald der Zyklus länger als dieses Vielfache seiner typischen Dauer läuft. 1,8: Ein Zyklus, der 80 % länger als das Programm läuft, wird ihm nicht mehr zugeordnet.", "label": "Maximaldauerverhältnis" }, "profile_match_min_duration_ratio": { - "doc": "Mindestzykluslänge relativ zum Profil. 0,9 bedeutet, dass ein Zyklus mindestens 90 % der Profildauer betragen muss, um eine Übereinstimmung zu erzielen.", + "doc": "Ein Programm wird erst berücksichtigt, wenn der Zyklus diesen Anteil seiner typischen Dauer erreicht hat. 0,10: Ein 2-Stunden-Programm wird nach 12 Minuten zum Kandidaten. Gilt nicht in den ersten 15 Minuten eines Zyklus.", "label": "Mindestdauerverhältnis" }, "profile_match_threshold": { - "doc": "Am Ende des Zyklus ist ein minimaler Ähnlichkeitswert (0-1) erforderlich, um eine Programmidentifikation zu akzeptieren. Erhöhen Sie den Wert, um falsche Identifikationen zu reduzieren. Verringern Sie den Wert, wenn die Programme Ihrer Maschine nicht übereinstimmen. Der Standardwert 0,4 ist ein konservativer Ausgangspunkt.", + "doc": "Live-Übereinstimmungswert, den ein Zyklus braucht, bevor die intelligente Beendigung oder der Knitterschutz-Abschluss ihn vorzeitig beenden darf. Ändert nicht das angezeigte Programm. Erhöhen, wenn schwache Zuordnungen Zyklen zu früh beenden; senken, wenn eindeutige Zuordnungen die Abschaltverzögerung abwarten.", "label": "Abgleichschwellenwert" }, "profile_unmatch_threshold": { - "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.", + "doc": "Fällt der Live-Übereinstimmungswert darunter, wird die aktuelle Programmvermutung gelöscht. Etwas unter dem Abgleichschwellenwert halten, damit ein kurzer Einbruch sie nicht zurücksetzt.", "label": "Nichtübereinstimmungsschwellenwert" }, "progress_reset_delay": { - "doc": "Halten Sie den Fortschritt nach Abschluss so lange auf 100 %, damit „Abgeschlossen“ auf den Dashboards angezeigt wird, bevor Sie auf „Leerlauf“ zurücksetzen.", + "doc": "Nach einem Zyklus den Fortschritt so lange auf 100 % halten, bevor auf Leerlauf zurückgesetzt wird.", "label": "Fortschritts-Reset-Verzögerung" }, "pump_stuck_duration": { - "doc": "Sekunden lang kann eine Pumpe ununterbrochen laufen, bevor sie als möglicherweise steckengeblieben gekennzeichnet wird (löst das Ereignis „Pumpe steckengeblieben“ aus).", + "doc": "Wie lange eine Pumpe ununterbrochen laufen darf, bevor das Ereignis „Pumpe steckengeblieben“ ausgelöst wird.", "label": "Dauer der feststeckenden Pumpe" }, "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.", + "doc": "Mindestzeit zwischen verarbeiteten Messwerten. Früher eintreffende Messwerte werden übersprungen (niedrige Werte während eines Zyklus bleiben immer erhalten). Auf das Meldeintervall Ihrer Steckdose oder niedriger einstellen; ein höherer Wert verwirft nur Messwerte.", "label": "Abtastintervall" }, "save_debug_traces": { - "doc": "Speichern Sie für jeden Zyklus die vollständige Leistungsverfolgung und die entsprechenden Debug-Daten. Nützlich zur Fehlerbehebung, erhöht jedoch die Speichergröße.", + "doc": "Zusätzlich die Debug-Daten des Abgleichs (die Bewertungen aller Kandidaten) mit jedem Zyklus speichern, zur Fehlerbehebung. Belegt etwa 20 KB pro Zyklus.", "label": "Debug-Traces speichern" }, "smart_termination_duration_ratio": { - "doc": "Wie weit ein Zyklus in die erwartete Dauer des zugeordneten Programms fortgeschritten sein muss, bevor die intelligente Beendigung ihn bei einem Leistungsabfall vorzeitig beenden darf. Die erwartete Dauer ist der Durchschnitt des Programms; bei Geräten mit stark schwankender Laufzeit - Waschmaschinen bei kaltem Winter- gegenüber warmem Sommer-Zulaufwasser, Trockner mit Feuchtesensor, lastabhängige Programme - endet daher etwa die Hälfte aller Läufe kürzer als dieser Durchschnitt und erhält nie die schnelle Beendigung, sondern endet erst Minuten später über den Rückfall-Timeout. Senken Sie diesen Wert (z. B. 0,85) bei solchen Geräten, damit die vorzeitige Beendigung dennoch auslöst; erhöhen Sie ihn Richtung 1,0, um vorsichtiger zu sein. Leer lassen für den Standard (0,98 bzw. 0,99 für Geschirrspüler). Er kann einen Zyklus nur früher beenden, niemals später, und löst nie bei einer mehrdeutigen oder unsicheren Zuordnung aus.", + "doc": "Anteil der durchschnittlichen Programmdauer, den ein Zyklus erreichen muss, bevor die intelligente Beendigung ihn vorzeitig beenden darf. Wenn die Laufzeiten schwanken (Wassertemperatur, Feuchtesensor, Beladung), senken Sie den Wert (z. B. 0,85). Leer lassen für 0,98 (0,99 bei Geschirrspülern).", "label": "Verhältnis für intelligente Beendigung" }, - "smoothing_window": { - "doc": "Wie stark wird das Rohleistungssignal geglättet? Niedrig (2) reagiert, ist aber laut; Hoch (5) glättet Spitzen, fügt aber Verzögerung hinzu.", - "label": "Glättungsfenster" - }, "start_duration_threshold": { - "doc": "Die Leistung muss so lange über dem Startschwellenwert bleiben, um einen echten Start zu bestätigen, wodurch verhindert wird, dass sekundenschnelle Ein-/Ausschaltvorgänge einen Zyklus starten.", + "doc": "Die Leistung muss so lange über dem Startschwellenwert bleiben, bevor ein Zyklus startet, damit kurze Ein-/Aus-Spitzen ignoriert werden.", "label": "Startdauer" }, "start_energy_threshold": { - "doc": "Energie (Leistung x Zeit), die das Gerät verbrauchen muss, bevor es in Betrieb geht. Eine kurze Hochleistungsspitze hat eine sehr geringe Energie und wird ignoriert, wodurch Fehlstarts vermieden werden.", + "doc": "Energie, die das Gerät verbrauchen muss, bevor der Zyklus als laufend gilt. Eine kurze Leistungsspitze verbraucht wenig Energie und startet daher keinen Zyklus.", "label": "Startenergie" }, "start_threshold_w": { - "doc": "Die Leistung muss über diesen Wert ansteigen, um zu bestätigen, dass ein Zyklus gestartet wurde. Eine zu niedrige Einstellung führt zu Fehlstarts aus dem Standby-Modus. zu hoch und langsam startende Programme (Kaltfüllung) werden verpasst. Die Vorschlags-Engine legt diesen Wert knapp über der beobachteten niedrigsten Wirkleistung der Maschine fest.", + "doc": "Die Leistung muss über diesen Wert steigen, damit ein Zyklus startet. Zu niedrig: Standby-Leistung verursacht Fehlstarts. Zu hoch: langsam startende Programme (Kaltwasserbefüllung) werden verpasst.", "label": "Startschwellenwert" }, "stop_threshold_w": { - "doc": "Die Leistung muss unter diesen Wert fallen, bevor der Countdown für die Ausschaltverzögerung beginnt. Stellen Sie ihn unterhalb des Startschwellenwerts ein – die Lücke dazwischen ist das Hystereseband, das ein Flackern verhindert. Bei einer zu hohen Einstellung lösen Phasen mit geringer Leistung (Spülstopps, Knitterschutz) fälschlicherweise die Endsequenz aus.", + "doc": "Die Leistung muss unter diesen Wert fallen, bevor die Abschaltverzögerung zu zählen beginnt. Unter dem Startschwellenwert halten. Ist er zu hoch, starten Phasen mit geringer Leistung (Spülstopps, Knitterschutz) fälschlich den End-Countdown.", "label": "Stoppschwellenwert" }, "switch_entity": { - "doc": "Optionaler Schalter, der bei Pause aus- und beim Fortsetzen wieder einschaltet und zusammen mit „Pause unterbricht auch die Stromversorgung“ verwendet wird.", + "doc": "Optionaler Schalter, der bei Pause aus- und beim Fortsetzen wieder eingeschaltet wird, zusammen mit „Pause unterbricht auch Strom“.", "label": "Schalter-Entität" }, "watchdog_interval": { - "doc": "Wie oft prüft der Hintergrund-Watchdog auf blockierte Sensoren und abgelaufene Zeitüberschreitungen? Standard 30 s.", + "doc": "Wie oft die Hintergrundprüfung nach hängenden Sensoren und Zeitüberschreitungen sucht. Standard 30 s.", "label": "Watchdog-Intervall" }, "notify_milestone_message": { @@ -1558,131 +1507,86 @@ "label": "Meilenstein-Nachricht" }, "notify_milestones": { - "doc": "Kommagetrennte Zykluszahlen, die beim Erreichen eine einmalige Glückwunsch-Benachrichtigung auslösen (z. B. 50, 100, 500, 1000). Leer deaktiviert Meilenstein-Benachrichtigungen.", + "doc": "Zykluszahlen, die eine einmalige Glückwunsch-Benachrichtigung auslösen, kommagetrennt (z. B. 50, 100, 500, 1000). Leer deaktiviert sie.", "label": "Zyklus-Meilensteine" }, "notify_quiet_end_hour": { - "doc": "Ende des Ruhezeitfensters (0-23). Zurückgehaltene Benachrichtigungen werden zu dieser Stunde zugestellt. Leer lassen zum Deaktivieren.", + "doc": "Ende des Ruhezeitfensters (0-23); zurückgehaltene Benachrichtigungen werden dann gesendet. Leer deaktiviert es.", "label": "Ende der Ruhezeiten" }, "notify_quiet_start_hour": { - "doc": "Beginn eines Ruhezeitfensters (0-23). Abschluss-, Erinnerungs- und Wäsche-fertig-Benachrichtigungen, die während der Ruhezeiten ausgelöst würden, werden zurückgehalten und bei Fensterende zugestellt. Leer lassen zum Deaktivieren. Unterstützt Fenster über Mitternacht (z. B. Start 22, Ende 7).", + "doc": "Beginn eines Ruhezeitfensters (0-23). End-, Erinnerungs- und Wäsche-fertig-Benachrichtigungen in diesem Fenster werden bis zu seinem Ende zurückgehalten. Kann über Mitternacht gehen (z. B. 22 bis 7). Leer deaktiviert es.", "label": "Beginn der Ruhezeiten" }, "peak_rate_message": { - "doc": "Optionaler benutzerdefinierter Text für den Spitzentarif-Hinweis, der an die Start-Benachrichtigung angehängt wird. Vorlagenvariablen: {device}, {price}. Leer verwendet die eingebaute Vorgabe.", + "doc": "Eigener Text für den Spitzentarif-Hinweis. Vorlagenvariablen: {device}, {price}. Leer: Standardtext.", "label": "Spitzentarif-Nachricht" }, "peak_rate_threshold": { - "doc": "Wenn ein Zyklus startet und der aktuelle Preis pro kWh bei oder über diesem Wert liegt, wird ein Spitzentarif-Hinweis an die Start-Benachrichtigung angehängt. 0 oder leer deaktiviert den Hinweis.", + "doc": "Liegt der Preis pro kWh beim Zyklusstart bei oder über diesem Wert, erhält die Start-Benachrichtigung einen Spitzentarif-Hinweis. 0 oder leer deaktiviert ihn.", "label": "Spitzentarif-Schwelle (pro kWh)" }, - "dtw_bandwidth": { - "label": "Toleranz beim Formabgleich", - "doc": "Wie stark die Zuordnung einen Zyklus zeitlich verzerren darf, damit er zur Form eines gespeicherten Programms passt. Höhere Werte lassen mehr Geschwindigkeitsschwankungen zu; zu hohe Werte bergen das Risiko, das falsche Programm zuzuordnen." - }, - "corr_weight": { - "label": "Gewichtung Form gegenüber Pegel", - "doc": "Balance zwischen Kurvenform und absolutem Leistungspegel bei der Bewertung einer Zuordnung. Höhere Werte bevorzugen die Form; niedrigere Werte bevorzugen die Übereinstimmung des Leistungspegels." - }, - "duration_weight": { - "label": "Gewichtung der Dauer", - "doc": "Wie stark ein Programm bevorzugt wird, dessen typische Laufzeit zu diesem Zyklus passt." - }, - "energy_weight": { - "label": "Gewichtung der Energie", - "doc": "Wie stark ein Programm bevorzugt wird, dessen typischer Energieverbrauch zu diesem Zyklus passt." - }, "store_brand": { "label": "Gerätemarke", - "doc": "Optional. Die Gerätemarke, ausgewählt aus dem Community-Katalog. Wird verwendet, um passende Referenzaufnahmen zu finden und zu teilen. Leer lassen, wenn Sie keine Onlinefunktionen nutzen." + "doc": "Optional. Ihre Gerätemarke aus dem Community-Katalog, zum Finden und Teilen von Referenzaufnahmen. Leer lassen, wenn Sie keine Onlinefunktionen nutzen." }, "store_model": { "label": "Gerätemodell", - "doc": "Optional. Das Gerätemodell, ausgewählt aus dem Community-Katalog, sobald eine Marke festgelegt ist. Wenn Ihr Modell nicht aufgeführt ist, können Sie es zum Katalog hinzufügen." + "doc": "Optional. Ihr Gerätemodell aus dem Community-Katalog (zuerst eine Marke wählen). Ist es nicht aufgeführt, können Sie es hinzufügen." }, "show_contributor": { "doc": "Zeigt die Nennung 'von ' bei Community-Geräten und Referenzzyklen an." }, - "enable_phase_matching": { - "label": "Phasenbewusste Restzeit", - "doc": "Teilt jeden laufenden Zyklus in Phasen auf (Heizen, Waschen, Schleudern) und veranschlagt die verbleibende Zeit pro Phase, gemischt mit der klassischen Schätzung - zu Beginn des Zyklus stützt sie sich auf das Phasenbudget, gegen Ende auf die klassische Schätzung. Dadurch wird der Countdown darauf abgestimmt, wie lange Ihre Maschine tatsächlich heizt und läuft, was in der ersten Hälfte eines Zyklus am deutlichsten spürbar ist. Aus = nur die klassische Schätzung. Betroffen ist ausschließlich die Anzeige der verbleibenden Zeit; Programmabgleich und Zykluserkennung bleiben unverändert." - }, - "keep_min_score": { - "label": "Min. Match-Score", - "doc": "Mindest-Ähnlichkeitsscore, den ein Kandidat benötigt, um im Auswahlverfahren zu bleiben. Der Standard 0,1 ist absichtlich permissiv - er lässt auch schwache Kandidaten zu und verlässt sich auf spätere Stufen, um die beste Übereinstimmung zu finden. Erhöhen, um unwahrscheinliche Profile früher auszuschließen; verringern, um den anfänglichen Kandidatenpool zu erweitern." - }, - "dtw_blend": { - "label": "Warp-Mischgewicht", - "doc": "Wie stark der Zeitverzerrungsscore (DTW) den Kernscore der Stufe 2 ersetzt. 0 = nur Stufe-2-Score, 1 = nur DTW-Score, 0,5 (Standard) = gleiche Mischung. Erhöhen, um sich mehr auf die Warp-Ausrichtung zu verlassen, wenn Programme ähnliche Leistungspegel, aber unterschiedliche Zeitmuster aufweisen." - }, - "dtw_ensemble_w": { - "label": "Warp-Ensemble-Anteil", - "doc": "Im Ensemble-DTW-Modus (Standard) das Gewicht auf skaliertem L1 gegenüber Ableitungs-DTW (DDTW). 1,0 = nur skaliertes L1, 0 = nur DDTW, 0,7 = Standard. Die skalierte L1-Variante vergleicht Leistungspegel; die Ableitungsvariante reagiert auf Leistungsänderungen im Zeitverlauf. Ensemble kombiniert beide." - }, - "dtw_ddtw_scale": { - "label": "Ableitungs-Warp-Skalierung", - "doc": "Halbsättigungsabstand für die Ableitungs-DTW-Bewertung. Der Score halbiert sich, wenn die DDTW-Distanz diesen Wert erreicht. Kleiner = empfindlicher gegenüber Formunterschieden. Standard 30 ist auf typische Geräteleistungsverläufe kalibriert. Betrifft nur Ensemble- und DDTW-DTW-Modi." - }, - "dtw_refine_top_n": { - "label": "Warp-Verfeinerungsanzahl", - "doc": "Anzahl der besten Stufe-2-Kandidaten, die per DTW neu bewertet werden. DTW ist aufwendiger, daher wird es nur auf die besten N Kandidaten angewendet. Standard 5. Erhöhen (auf 7-9), wenn das richtige Profil nach Stufe 2 manchmal nur auf Platz 4 oder 5 landet - DTW kann es noch retten. Verringern beschleunigt das Matching leicht." - }, - "duration_scale": { - "label": "Laufzeit-Trennschärfe", - "doc": "Schärfe der Dauerübereinstimmungsstrafe in Stufe 4. Dies ist das Log-Verhältnis, bei dem der Übereinstimmungsscore halbiert wird. Kleiner = strenger: eine Dauerabweichung schadet mehr. Standard 0,175 entspricht etwa 18% Dauertoleranz beim halben Gewicht. Gemeinsam mit der Dauergewichtung anpassen." - }, - "energy_scale": { - "label": "Energie-Trennschärfe", - "doc": "Schärfe der Energieübereinstimmungsstrafe. Kleiner = strenger: eine Energieabweichung schadet mehr. Standard 0,25 ist absichtlich nachsichtiger als die Dauerschärfe, da Energie lastabhängig schwankt. Gemeinsam mit der Energiegewichtung anpassen." - }, "anti_wrinkle_idle_timeout": { "label": "Maximale Impulspause", - "doc": "Wie lange das Gerät zwischen zwei Trommelimpulsen still bleiben darf, bevor der Knitterschutz-Modus endet. Höher setzen als die längste Pause, die Ihr Trockner zwischen zwei Impulsen lässt – sonst wird jeder weitere Impuls als Fehlstart gewertet." + "doc": "Wie lange der Trockner zwischen Trommelimpulsen still bleiben darf, bevor der Knitterschutz-Modus endet. Höher als die längste Pause Ihres Trockners einstellen, sonst werden spätere Impulse als Fehlstarts gewertet." }, "power_profile_interval_min": { "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." + "doc": "Bucket-Größe des Attributs power_profile (durchschnittliche Watt pro Slot), das Planer wie EMHASS und tibber_prices nutzen. Kleiner erhält kurze Spitzen; größer glättet die Kurve. Standard 15 Min. Beeinflusst die Erkennung nicht." }, "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." + "doc": "Nur iOS. Live-Fortschritt lautlos aktualisieren, mit einer akkuschonenden Priorität, die iOS bündeln kann. Die erste Aktualisierung und die Start-/Endbenachrichtigungen melden sich weiterhin. Aus: Jede Aktualisierung meldet sich." }, "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." + "doc": "Nur Android. Die Live-Fortschrittsbenachrichtigung beim Antippen behalten. Aus: Ein Tippen schließt sie." }, "notify_live_click_action": { "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." + "doc": "Was ein Tippen auf die Benachrichtigung dieses Geräts öffnet. Leer: das WashData-Panel bei diesem Gerät. Geben Sie einen Dashboard-Pfad (z. B. /lovelace/laundry) oder eine URL ein, oder „none“ für keine Aktion." }, "door_opens_at_end": { "label": "Tür öffnet sich am Ende automatisch", - "doc": "Für Geschirrspüler, die die Tür am Ende des Zyklus zum Trocknen aufspringen lassen (AirDry und ähnliche). Wenn aktiviert, unterbricht ein Türöffnen bei einem laufenden Zyklus diesen nicht dauerhaft; wenn die Tür für die eingestellte Verweildauer geöffnet bleibt, wertet WashData den Zyklus als abgeschlossen. Ein kurzes Öffnen (z. B. zum Nachlegen eines Geschirrstücks) wird ignoriert. Erfordert einen Türsensor." + "doc": "Für Geschirrspüler, die am Ende zum Trocknen die Tür öffnen (z. B. AirDry). Bleibt die Tür für die unten eingestellte Verweildauer offen, endet der Zyklus, statt zu pausieren; ein kurzes Öffnen wird ignoriert. Erfordert eine Türsensor-Entität." }, "door_end_dwell_seconds": { "label": "Türöffnungs-Endverweildauer", - "doc": "Wie lange die Tür geöffnet bleiben muss, bevor WashData den Zyklus beendet, wenn \"Tür öffnet sich am Ende automatisch\" aktiv ist. Lang genug, um ein kurzes Öffnen zum Nachlegen eines Geschirrteils zu ignorieren (Standard 60 s), kurz genug, um den Zyklus zügig zu beenden, sobald die Maschine die Tür aufspringt." + "doc": "Bei aktivem „Tür öffnet sich am Ende automatisch“: wie lange die Tür offen bleiben muss, bevor der Zyklus endet. Lang genug, um das Nachlegen eines Geschirrteils zu ignorieren (Standard 60 s)." }, "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." + "doc": "Welche Zyklusarten die Kurve und den Abgleich jedes Programms prägen. Das Abwählen einer Art löscht nichts; nützlich, wenn Sie importierten Daten nicht vertrauen. Die Statistik zählt immer nur die Zyklen dieser Maschine. Alles abzuwählen wird ignoriert." }, "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." + "doc": "Jeden Zyklus zu dem Preis berechnen, der in jedem Moment seines Laufs galt, statt zum Preis bei Zyklusende. Erfordert eine Preis-Entität. „Verlauf verarbeiten“ (Diagnose) berechnet ältere Zyklen neu." }, "unload_confirm_entity": { "label": "Entladebestätigungs-Entität", - "doc": "Optionale Entität, deren Auslösung bedeutet, dass die Ladung entnommen wurde, für Maschinen ohne Türsensor. Jede Art funktioniert: ein Zigbee- oder NFC-Taster, ein input_button-Helfer, ein Bewegungsmelder vor der Maschine, eine Szene. Jede Zustandsänderung zählt als Bestätigung, außer dem Ausschalten und dem Wechsel auf nicht verfügbar. Diese Einstellung aktiviert die Entladeerinnerung auch auf einem Gerät ohne Türsensor-Entität." + "doc": "Optional. Eine beliebige Entität, die bestätigt, dass die Ladung entnommen wurde, für Maschinen ohne Türsensor (Taster, input_button, Bewegungsmelder, Szene). Jede Zustandsänderung zählt, außer Ausschalten oder nicht verfügbar. Aktiviert auch die Entladeerinnerung." }, "unload_track_without_door": { "label": "Entladung manuell bestätigen", - "doc": "Führt die Entladeerinnerung auf einem Gerät ohne Türsensor-Entität und ohne Entladebestätigungs-Entität aus; die Entladung bestätigen Sie stattdessen aus Ihrer eigenen Automatisierung: die Schaltflächen-Entität \"Als entladen markieren\" drücken oder die Aktion ha_washdata.mark_unloaded aufrufen. Wird ignoriert, wenn eine Türsensor-Entität festgelegt ist, die das Signal bereits liefert." + "doc": "Die Entladeerinnerung ohne Türsensor oder Bestätigungs-Entität ausführen. Bestätigen Sie die Entladung aus Ihrer eigenen Automatisierung mit der Schaltfläche „Als entladen markieren“ oder dem Dienst ha_washdata.mark_unloaded. Wird ignoriert, wenn eine Türsensor-Entität festgelegt ist." }, "notify_icon_color": { "label": "Benachrichtigungsfarbe", - "doc": "Akzentfarbe für die Benachrichtigungen dieses Geräts, damit Waschmaschine, Trockner und Geschirrspüler auf einen Blick unterscheidbar sind. iOS färbt das Symbol und den Fortschrittsbalken der Live-Aktivität auf dem Sperrbildschirm und in der Dynamic Island. An Android wird dieselbe Farbe gesendet, aber was damit geschieht, entscheidet das Telefon: manche färben das Symbol ein, viele (insbesondere Samsung One UI) ignorieren sie ganz. Beide verstehen Hex-Werte (z. B. #03A9F4). Für den Plattformstandard lassen Sie das Feld leer." + "doc": "Akzentfarbe für die Benachrichtigungen dieses Geräts, damit Waschmaschine, Trockner und Geschirrspüler leicht zu unterscheiden sind. Unter iOS füllt sie den Kreis hinter dem Symbol und färbt den Balken der Live-Aktivität. Viele Android-Telefone (vor allem Samsung) ignorieren sie. Hex-Wert verwenden, z. B. #03A9F4. Leer: Plattformstandard." + }, + "share_name": { + "doc": "Ihren Kontonamen als Uploader geteilter Zyklen anzeigen. Aus: ohne Namen geteilt." } }, "setting_group": { @@ -1710,9 +1614,6 @@ "door_pause": { "label": "Tür & Pause" }, - "dtw_refinement": { - "label": "DTW-Verfeinerung" - }, "duration_gates": { "label": "Dauertore" }, @@ -1789,17 +1690,20 @@ "rebuilding": "Wird neu aufgebaut…", "analyzing": "Wird analysiert…", "resetting": "Wird zurückgesetzt…", - "reverting": "Wird zurückgeführt…", "trimming": "Wird zugeschnitten…", "splitting": "Wird geteilt…", + "stalled": "Stillstand", "deleting": "Wird gelöscht…", "imported": "Importiert", "preparing": "Wird vorbereitet…", - "unlabelling": "Beschriftung wird entfernt…" + "unlabelling": "Beschriftung wird entfernt…", + "working": "Wird ausgeführt…", + "match_uncertain": "Unsicher: {top} oder {runner}", + "match_uncertain_single": "Unsicher: vielleicht {top}", + "match_sure": "~{pct}% sicher", + "match_uncertain_tip": "WashData hat sich noch auf kein Programm festgelegt. Der Wert zeigt, wie oft der führende Kandidat, {top}, in früheren Zyklen richtig lag, wenn er so weit vorne lag. Nur zur Information." }, "suggestion": { - "both_agree": "WashData empfiehlt", - "calibrated_label": "Kalibriert", "observed_label": "Gemessen", "impact": { "auto_label_confidence": { @@ -1814,10 +1718,6 @@ "higher": "Beendet nur Zyklen mit wesentlichem Energieverbrauch", "lower": "Beendet auch kürzere oder energieärmere Zyklen" }, - "end_repeat_count": { - "higher": "Endmeldung muss sich öfter wiederholen – robuster, minimal langsamer", - "lower": "Endet nach weniger Wiederholungen – schnellere Erkennung, höheres Fehlendrisiko" - }, "learning_confidence": { "higher": "Lernt nur aus sehr sicheren Übereinstimmungen – langsamer, aber zuverlässiger", "lower": "Lernt aus mehr Zyklen – Modell kann ungenauer werden" @@ -1856,40 +1756,25 @@ } }, "reason": { - "auto_label_confidence": "15. Perzentil der Konfidenz von {cycles} automatischen Labels, die der Benutzer nie korrigiert hat ({p15}).", "completion_min_seconds": "Die Hälfte der p05-Dauer sauberer Zyklen ({minutes} min) über {cycles} Zyklen; filtert Geisterzyklen heraus.{excl}", - "dead_zone_batch": "Kurz gehalten (mittleres Startinstabilitätsfenster über {cycles} Zyklen = {s}s), damit ein echtes Ende zügig erkannt wird.", - "dead_zone_from_dip": "Basierend auf einem frühen Leistungseinbruch, der bei {s}s erkannt wurde.", - "end_energy_default": "Empfohlener Standardwert für die Rauschschwelle am Zyklusende.", - "end_energy_false": "p95 der Energie bei falschen Enden ({p95}Wh) über {cycles} Zyklen, begrenzt auf mindestens 0.2% der medianen Zyklusenergie ({floor}Wh).", - "end_energy_no_false": "Keine falschen Enden über {cycles} Zyklen; begrenzt auf mindestens 0.2% der medianen Zyklusenergie ({floor}Wh).", - "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.", + "match_interval_floored": "Das kürzeste Programm ({shortest}s) würde dies auf {cap}s begrenzen, unter dem Minimum von {minimum}s, daher gilt das Minimum: {persistence} aufeinanderfolgende Zuordnungen dauern {budget}s, über {pct}% dieses Programms. Der Abgleich läuft nur, wenn ein Messwert eintrifft (Median={median}s), das 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.", "min_off_gap": "Basierend auf {gaps} beobachteten Pausen zwischen Zyklen (p05={p05}s). Geräte-Untergrenze: {floor}s.", "min_off_gap_bridge": "So bemessen, dass die längste Ruhephase innerhalb eines Zyklus überbrückt wird: {span}s + 60s Puffer, aus {spans} überbrückten Lücken über {cycles} saubere Zyklen (bleibt unter Ihrem kürzesten Abstand von {ceiling}s zwischen zwei Ladungen).", "min_power": "40% der p05-niedrigsten aktiven Leistung ({p05}W) über {cycles} saubere Zyklen, sodass die Aus-Schwelle unter der tatsächlichen Leistungsaufnahme bleibt.{excl}", - "ml_auto_label": "Niedrigste Konfidenz, die das Qualitätsmodell noch als sauber bewertet hat (p10 von {cycles} sauberen Zyklen = {p10}).", - "ml_end_repeat": "{false}/{total} Zyklen ({pct}%) hatten eine Pause, die der Endedetektor mit >50% bewertete; {val} Endbestätigung(en) erforderlich.", - "ml_off_delay": "Vom Endedetektor bestätigte Pausen: p95 {p95}s + 60s Puffer, aus {pauses} modellgeprüften Pausen über {cycles} Zyklen (Untergrenze {floor}s).", "no_update_timeout": "Basierend auf dem beobachteten Aktualisierungsrhythmus (p95={p95}s) * 20 (min 60s).", "off_delay_cadence": "Basierend auf dem beobachteten Aktualisierungsrhythmus (p95={p95}s) * 5", "off_delay_device_floor": "Gerätespezifisches sicheres Minimum für {device} verwendet ({floor}s).", "off_delay_generic_floor": "Generisches sicheres Minimum verwendet ({floor}s).", "off_delay_pauses": "So bemessen, dass echte Pausen überbrückt werden: p95 der Pause innerhalb eines Zyklus {p95}s + 60s Puffer, aus {pauses} Pausen über {cycles} saubere Zyklen (Untergrenze {floor}s).", - "profile_match_threshold": "p10-Konfidenz von {cycles} korrekten automatischen Labels ({p10}); sichere Untergrenze für Live-Festlegungen.", - "sampling_interval": "Median-Aktualisierungsintervall, beobachtet über {cycles} saubere Zyklen ({si}s).{excl}", - "smoothing_window": "So bemessen, dass ~30s an Messwerten bei {si}s-Abtastung geglättet werden ({samples} Messwerte).", - "start_duration": "Kurz gehalten (~ein {si}s-Abtastintervall), damit die Erkennung so früh wie möglich beginnt, einzelne Messwertspitzen aber weiterhin ignoriert werden.", - "thr_batch": "Knapp über der p05-niedrigsten aktiven Leistung über {cycles} Zyklen ({p05}W) gehalten, damit ein Start so früh wie möglich erkannt wird und die Stopp-Schwelle unter der niedrigsten Betriebsleistung der Maschine bleibt.", - "tol_per_profile": "p75 der Dauervarianz pro Profil über {profiles} Profile ({cycles} Zyklen); enge Profile werden nicht benachteiligt.", - "tol_pooled": "Basierend auf der zusammengefassten Dauervarianz von {cycles} kürzlich gelabelten Zyklen (p95-Abweichung={dev}).", - "watchdog": "So niedrig wie sicher gehalten (knapp über der p95-Aktualisierungslücke von {p95}s und mindestens 2x dem Abtastintervall von {median}s, min. 30s), damit Hänger schnell erkannt werden, ohne Fehl-Stopps auszulösen." + "standby_floor": "Jeder aktuelle Zyklus zog beim Ende noch {idle}W, sodass ein Schwellenwert darunter niemals das Ausschalten des Geräts erkennen kann. Über diesem Wert gehalten; die {cycles} sauberen Zyklen im Verlauf haben darunter nie länger als {pause}s pausiert.", + "thr_batch": "Knapp über der niedrigsten aktiven Leistung (p05) über {cycles} Zyklen ({p05}W), damit Starts früh erkannt werden und der Stoppschwellenwert unter der niedrigsten Betriebsleistung bleibt.", + "watchdog": "So niedrig wie sicher gehalten (knapp über der p95-Aktualisierungslücke von {p95}s und mindestens 2x dem Abtastintervall von {median}s, min. 30s), damit Hänger schnell erkannt werden, ohne Fehl-Stopps auszulösen.", + "end_energy_floor": "Auf die Untergrenze angehoben, die sich aus dem Stoppschwellenwert ({stop} W) während der Abschaltverzögerung ({off_delay} s) ergibt; darunter kann der Zyklus nur über einen Ausweichpfad enden.", + "max_duration_ratio_default": "Zurück auf den Standardwert: Eine niedrigere Obergrenze verwirft das richtige Programm, sobald ein Durchlauf länger als üblich dauert.", + "min_duration_ratio_default": "Zurück auf den Standardwert: Eine höhere Untergrenze verhindert, dass ein laufender Zyklus erkannt wird, bevor er weit fortgeschritten ist." }, "exclusions": { "summary": "{total} falsch erkannte Zyklen ausgeschlossen: {parts}.", @@ -1907,7 +1792,8 @@ "no_power": "ohne Leistung", "no_active_power": "ohne aktive Leistung" } - } + }, + "now_value": "(aktuell {v})" }, "tab": { "advanced": "Erweitert", @@ -1937,6 +1823,11 @@ "wrong_profile": "Falsches Profil" }, "toast": { + "task_name_required": "Geben Sie der Aufgabe einen Namen", + "task_added": "Aufgabe hinzugefügt", + "task_save_failed": "Aufgabe konnte nicht gespeichert werden: {error}", + "task_removed": "Aufgabe entfernt", + "maint_logged": "Als erledigt protokolliert: {task}", "catalog_refreshed": "Community-Katalog aktualisiert", "access_saved": "Zugangskontrolle gespeichert", "all_wiped": "Alle Daten gelöscht", @@ -1944,7 +1835,6 @@ "auto_label_complete": "Automatische Beschriftung abgeschlossen", "automation_deleted": "Automatisierung gelöscht", "correction_submitted": "Korrektur eingereicht", - "cycle_deleted": "Zyklus gelöscht", "cycle_force_stopped": "Zyklus zwangsweise gestoppt", "cycle_labelled": "Zyklus beschriftet", "cycle_paused": "Zyklus pausiert", @@ -1960,11 +1850,9 @@ "group_deleted": "Gruppe gelöscht", "group_name_required": "Gruppenname ist erforderlich", "group_saved": "Gruppe gespeichert", - "import_successful": "Import erfolgreich; Neuladen der Integration", "json_required": "JSON-Daten sind erforderlich", "legacy_removed": "Legacy-Aktionen entfernt", "logs_exported": "Protokolle exportiert", - "matching_reverted": "Die passenden Gewichtungen wurden auf die Standardwerte zurückgesetzt", "min_2_profiles": "Wählen Sie mindestens 2 Profile für eine Gruppe aus", "ml_training_no_improvement": "Training abgeschlossen: Grundlinie beibehalten (keine Verbesserung)", "models_reverted": "Die Gerätemodelle wurden auf die Grundlinie zurückgesetzt", @@ -1974,8 +1862,8 @@ "pg_preset_loaded": "Voreinstellung \"{name}\" geladen", "pg_preset_saved": "Voreinstellung \"{name}\" gespeichert", "pg_sugg_loaded": "{n} Vorschlagswert(e) in den Testbereich geladen - Testbereich ausführen, um zu vergleichen", - "pg_sugg_ml_loaded": "{n} ML-kalibrierten Wert(e) in den Testbereich geladen - Testbereich ausführen, um zu vergleichen", "phase_name_required": "Phasenname ist erforderlich", + "phase_reset": "\"{name}\" auf die eingebaute Phase zurückgesetzt", "phase_updated": "Phase aktualisiert", "phases_saved": "Phasen gespeichert", "preferences_saved": "Einstellungen gespeichert", @@ -2022,14 +1910,10 @@ "store_name_required": "Geben Sie einen Profilnamen ein", "store_import_failed": "Import fehlgeschlagen: {error}", "store_imported": "In {profile} importiert", - "store_program_required": "Geben Sie einen Programmnamen ein", "store_no_appliance": "Legen Sie zuerst Marke und Modell Ihres Geräts in den Einstellungen fest.", "store_share_failed": "Teilen fehlgeschlagen: {error}", "store_shared": "Im Community-Store geteilt, Prüfung ausstehend.", "appliance_added": "Gerät hinzugefügt, Freigabe ausstehend", - "thanks_confirming": "Danke fürs Bestätigen", - "device_approved": "Von der Community freigegeben", - "rating_saved": "Qualitätsbewertung gespeichert", "brand_added": "Marke hinzugefügt, Freigabe ausstehend", "profile_added": "Profil hinzugefügt, Freigabe ausstehend", "saved_except_conflicts": "Gespeichert. Beheben Sie die markierten Konflikte, um den Rest zu speichern.", @@ -2053,13 +1937,58 @@ "odometer_save_failed": "Gesamtzahl konnte nicht gespeichert werden: {error}", "contrib_device_changed": "Zum Community-Store hinzugefügt, aber das Gerät wurde gewechselt, während das Formular offen war. Wählen Sie es in den Einstellungen aus.", "store_pick_profile_share": "Wählen Sie ein Profil zum Teilen aus", - "remove_failed": "Entfernen fehlgeschlagen: {error}" + "remove_failed": "Entfernen fehlgeschlagen: {error}", + "analysis_complete": "Analyse abgeschlossen: {count} Vorschlag/Vorschläge", + "analysis_failed": "Analyse fehlgeschlagen: {error}", + "auto_detect_failed": "Automatische Erkennung fehlgeschlagen: {error}", + "auto_label_failed": "Automatische Beschriftung fehlgeschlagen: {error}", + "auto_label_done": "Automatische Beschriftung abgeschlossen: {count} Zyklen beschriftet, {skipped} unbeschriftet gelassen.", + "auto_label_done_one": "Automatische Beschriftung abgeschlossen: {count} Zyklus beschriftet, {skipped} unbeschriftet gelassen.", + "auto_label_done_other": "Automatische Beschriftung abgeschlossen: {count} Zyklen beschriftet, {skipped} unbeschriftet gelassen.", + "auto_label_cancelled": "Automatische Beschriftung nach {count} beschrifteten Zyklen abgebrochen.", + "auto_label_cancelled_one": "Automatische Beschriftung nach {count} beschriftetem Zyklus abgebrochen.", + "auto_label_cancelled_other": "Automatische Beschriftung nach {count} beschrifteten Zyklen abgebrochen.", + "could_not_load_cycle": "Zyklus konnte nicht geladen werden: {error}", + "create_failed": "Erstellen fehlgeschlagen: {error}", + "cycles_deleted": "Gelöschte Zyklen: {count}", + "debug_cleared": "Gelöschte Debug-Traces: {count}", + "discard_failed": "Verwerfen fehlgeschlagen: {error}", + "error": "Fehler: {error}", + "export_failed": "Export fehlgeschlagen: {error}", + "import_failed": "Import fehlgeschlagen: {error}", + "invalid_json": "\"{key}\" ist kein gültiges JSON - korrigieren Sie es oder leeren Sie das Feld vor dem Speichern.", + "label_failed": "Beschriften fehlgeschlagen: {error}", + "ml_training_promoted": "Training abgeschlossen: {count} Modell(e) aktiviert", + "pause_failed": "Pausieren fehlgeschlagen: {error}", + "phase_created": "Phase \"{name}\" erstellt", + "phase_deleted": "Phase \"{name}\" gelöscht", + "profile_created": "Profil \"{name}\" erstellt", + "rename_failed": "Umbenennen fehlgeschlagen: {error}", + "save_failed": "Speichern fehlgeschlagen: {error}", + "split_complete": "In {count} Zyklen aufgeteilt", + "start_failed": "Starten fehlgeschlagen: {error}", + "stop_failed": "Stoppen fehlgeschlagen: {error}", + "update_failed": "Aktualisieren fehlgeschlagen: {error}", + "store_download_skipped": "{n} Aufnahme(n) übersprungen: zu kurz, zu lückenhaft oder bereits auf Ihrem Gerät.", + "store_import_low_quality": "Diese Aufnahme ist zu kurz, zu lückenhaft oder unplausibel und kann nicht importiert werden.", + "store_import_duplicate": "Diese Aufnahme ist bereits auf Ihrem Gerät.", + "device_approved": "Von der Community freigegeben", + "thanks_confirming": "Danke fürs Bestätigen", + "rating_saved": "Qualitätsbewertung gespeichert", + "store_download_partial": "{n} Programm(e) konnte(n) nicht heruntergeladen werden; versuchen Sie es später erneut.", + "import_no_restore_point": "Importiert, aber es konnte kein Wiederherstellungspunkt gespeichert werden, daher kann dieser Import nicht rückgängig gemacht werden", + "import_undo_failed": "Import konnte nicht rückgängig gemacht werden: {error}", + "import_undone": "Import rückgängig gemacht: {profiles} Profil(e) und {cycles} Zyklus/Zyklen wiederhergestellt" }, "trend": { "down": "Abwärtstrend", "up": "Aufwärtstrend" }, "maint": { + "salt": "Salz nachfüllen", + "rinse_aid": "Klarspüler nachfüllen", + "lint_filter": "Flusensieb reinigen", + "condenser_clean": "Kondensator reinigen", "bearing_service": "Lagerwartung", "descale": "Entkalken", "drum_clean": "Trommel reinigen", @@ -2071,12 +2000,14 @@ "status_tip": "Zyklusergebnis: Abgeschlossen (natürliches Ende), Unterbrochen (plötzlicher Leistungsabfall), Zwangsweise gestoppt (manuell) oder Überprüfung nötig (Feedback ausstehend).", "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 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.", + "energy_tip": "Insgesamt verbrauchte Energie (kWh), berechnet aus der Leistung über die Zeit.", + "cost_tip": "Energiekosten dieses Zyklus, beim Abschluss festgelegt. Mit Zeitgewichtete Kosten wird jeder Moment zu seinem eigenen Preis berechnet, sonst zum Preis am Ende. 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.", - "cost_dynamic_tip": "Zeitgewichtet: zu dem während des Zyklus jeweils gültigen Preis berechnet (effektiv {price} {cur}/kWh)." + "cost_dynamic_tip": "Zeitgewichtet: zu dem während des Zyklus jeweils gültigen Preis berechnet (effektiv {price} {cur}/kWh).", + "dbg_conf": "Konf.", + "dbg_mae": "MAE", + "dbg_corr": "Korr." }, "stat": { "avg": "Ø", @@ -2100,7 +2031,10 @@ "notify_finish": "Eine Abschlussbenachrichtigung würde gesendet werden.", "notify_milestone": "Eine Meilenstein-Benachrichtigung würde gesendet werden.", "notify_held": "Eine Benachrichtigung wurde wegen der Ruhezeiten zurückgehalten.", - "finished": "Der Zyklus hat einen Endzustand erreicht und wurde beendet." + "finished": "Der Zyklus hat einen Endzustand erreicht und wurde beendet.", + "match_reverted": "Das Programm ist auf die Erkennung zurückgefallen: Sein Wert ist deutlich unter seinen eigenen Höchstwert gesunken.", + "verified_pause": "Die laufende Ausrichtung hat eine Pause innerhalb des Programms bestätigt (oder aufgehoben). Solange sie gilt, wird der Zyklus nicht beendet.", + "stalled": "Der Zyklus blieb länger auf Standby-Leistung, als dieses Programm dort je pausiert, obwohl noch Arbeit ausstand. Angezeigt als Angehalten (Stillstand); der Zyklus bleibt offen." }, "store": { "search_ph2": "Nach Marke oder Modell suchen…", @@ -2109,11 +2043,7 @@ "programs_count": "Programme: {n}", "browse": "Durchsuchen", "device": "Gerät", - "favorites": "Favoriten", - "search_ph": "Nach Marke oder Modell suchen…", "no_results": "Keine passenden Geräte gefunden. Versuchen Sie eine andere Suche.", - "n_programs": "{n} Programme", - "n_cycles": "{n} Zyklen", "no_programs": "Noch keine geteilten Programme für dieses Gerät.", "no_cycles": "Noch keine Referenzzyklen für dieses Programm geteilt.", "anon": "anonym", @@ -2125,19 +2055,21 @@ "merge_target": "In Profil zusammenführen", "description": "Beschreibung (optional)", "contributed_by": "von {name}", + "website": "Community-Store-Website ↗", + "website_tip": "Community-Store-Website öffnen", + "favorites": "Favoriten", "rating_summary": "★ {avg} ({n})", "no_ratings": "Noch keine Bewertungen", - "website": "Community-Store-Website ↗", - "website_tip": "Community-Store-Website öffnen" + "unreachable": "Der Community-Store ist nicht erreichbar. Prüfen Sie Ihre Verbindung und versuchen Sie es erneut." }, "settings": { "gear": { - "title": "Einstellungen" + "title": "Präferenzen" } }, "link": { - "manual": "Handbuch ↗", - "add_appliance": "Ihr Gerät hinzufügen" + "maintenance_supporter": "Maintenance Supporter auf GitHub ↗", + "manual": "Handbuch ↗" }, "tip": { "add_brand": "Marke nicht aufgeführt? Fügen Sie sie zum Community-Katalog hinzu", @@ -2171,7 +2103,13 @@ "rebuild": { "envelopes": "Hüllkurven werden neu aufgebaut" }, - "cancelling": "Wird abgebrochen..." + "cancelling": "Wird abgebrochen...", + "auto_label": { + "running": "Zyklen werden automatisch beschriftet" + }, + "store_download": { + "running": "Wird aus dem Store heruntergeladen" + } }, "setup": { "hdr": { @@ -2180,26 +2118,24 @@ }, "phase0": { "washer": "WashData erkennt bereits Ihre Zyklen. Nehmen Sie Ihren ersten Zyklus auf, um Programmnamen und Zeitschätzungen zu aktivieren.", - "dishwasher": "WashData beobachtet. Geschirrspüler haben komplexe Zyklen – es wird dringend empfohlen, Ihren ersten Zyklus aufzunehmen. Falls ein erkannter Zyklus zu lang läuft, kürzen Sie ihn im Zyklus-Editor, bevor Sie ihn als Profil speichern.", + "dishwasher": "WashData beobachtet. Geschirrspülerzyklen sind komplex, nehmen Sie daher Ihren ersten Zyklus auf. Läuft ein erkannter Zyklus zu lang, kürzen Sie ihn im Zyklus-Editor, bevor Sie ihn als Profil speichern.", "generic": "WashData beobachtet. Nehmen Sie einen Zyklus auf oder beschriften Sie einen erkannten Zyklus, um mit dem Aufbau von Profilen zu beginnen." }, "phase1a": { - "labelled": "Guter Start – Ihr erstes Programm ist gespeichert. Für die saubersten Daten empfiehlt es sich, den nächsten Zyklus mit dem Aufnahme-Widget aufzuzeichnen." + "labelled": "Guter Start: Ihr erstes Programm ist gespeichert. Für die saubersten Daten nehmen Sie Ihren nächsten Zyklus mit der Aufnahmefunktion auf." }, "phase1b": { - "recorded": "Ihre Aufnahme wurde als {profile_name} gespeichert. Nehmen Sie nun Ihre anderen häufig verwendeten Programme auf oder beschriften Sie sie, um die Abdeckung zu verbessern." + "recorded": "Ihre Aufnahme wurde als {profile_name} gespeichert. Nehmen Sie jetzt Ihre anderen häufigen Programme auf oder beschriften Sie sie." }, "phase1c": { - "verify": "Sie haben {count} Programme aus der Community. Führen Sie einen Zyklus durch, um zu überprüfen, ob WashData ihn korrekt erkennt – die Erkennung verbessert sich, je mehr Verlauf Ihr Gerät aufbaut." + "verify": "Sie haben {count} Programme aus der Community. Lassen Sie einen Zyklus laufen, um zu prüfen, ob WashData ihn erkennt; der Abgleich wird besser, je mehr eigenen Verlauf Ihr Gerät aufbaut." }, "phase2": { - "cluster": "WashData hat {count} Zyklen gesehen, die keinem gespeicherten Programm entsprechen – sie sehen einander ähnlich aus. Möchten Sie ein neues Profil dafür erstellen?", + "cluster": "WashData hat {count} ähnliche Zyklen gesehen, die zu keinem gespeicherten Programm passen. Ein neues Profil dafür erstellen?", "unmatched": "Ihr letzter Zyklus hat keinem gespeicherten Programm entsprochen. Ist es ein neues Programm?" }, "phase3": { - "suggestions": "WashData hat Einstellungsempfehlungen basierend auf Ihrem Zyklusverlauf – überprüfen Sie sie, um die Erkennungsgenauigkeit zu verbessern.", - "groups": "Einige Ihrer Profile sehen aus wie dasselbe Programm bei unterschiedlichen Temperaturen. Organisieren Sie sie in einer Gruppe für eine bessere Erkennung.", - "phases": "Fügen Sie {profile_name} Programmphasen hinzu, um genauere Schätzungen der verbleibenden Zeit zu erhalten." + "suggestions": "WashData hat Einstellungsvorschläge aus Ihrem Zyklusverlauf. Prüfen Sie sie, um die Erkennung zu verbessern." }, "phase4": { "healthy": "Dieses Gerät ist vollständig eingerichtet ({profile_count} Profile)." @@ -2212,12 +2148,58 @@ "create_from_cluster": "Profil aus diesen Zyklen erstellen", "create_profile": "Ein Profil dafür erstellen", "review_suggestions": "Empfehlungen überprüfen", - "organise_profiles": "Profile organisieren", - "configure_phases": "Phasen konfigurieren", "skip_step": "Diesen Schritt überspringen", "skip_forever": "Nicht mehr anzeigen", "hide_guidance": "Anleitung ausblenden", "show_guidance": "Anleitung einblenden" } + }, + "access": { + "edit": "Bearbeiten", + "full": "Vollzugriff", + "inherit": "Erben", + "none": "Keine (ausgeblendet)", + "read": "Lesen" + }, + "pg_label_reason": { + "no_winner": "Kein Programm passte", + "below_floor": "Unter der Lernkonfidenz", + "ambiguous": "Als mehrdeutig markiert", + "margin": "Zu nah am Zweitplatzierten", + "unknown_profile": "Programm existiert nicht mehr", + "not_evaluated": "Nicht ausgewertet" + }, + "diagram": { + "accum": "kumuliert", + "auto": "auto", + "below_off": "darunter = aus", + "confirmed": "bestätigt", + "energy_counts": "Energie zählt", + "feedback": "Feedback", + "gap_below_min": "Lücke unter Min. = ein Zyklus", + "heat_phase": "Heizphase", + "held_at_100": "bei 100% gehalten", + "ignored": "ignoriert", + "kept": "behalten", + "max": "max", + "min": "min", + "minus_tol": "-Tol", + "no_match": "kein Treffer", + "no_updates": "keine Updates", + "off_delay_wait": "Abschaltverzögerung", + "off_gap": "Aus-Lücke", + "plus_tol": "+Tol", + "profile": "Profil", + "reading_interval": "typisches Messintervall", + "sensor_offline": "Sensor offline: Zwangsstopp", + "si": "AI", + "spike_ignored": "Spitze ignoriert", + "spike_ignored_short": "Spitze: ignoriert", + "start": "Start", + "stop": "Stopp", + "tail_energy_resets": "Restenergie über Schwelle: Timer-Reset", + "thr": "Schw.", + "too_short": "zu kurz", + "tumble_kept": "Trommelimpulse behalten" } } diff --git a/custom_components/ha_washdata/translations/panel/el.json b/custom_components/ha_washdata/translations/panel/el.json index d1e3e15c..f9c4db10 100644 --- a/custom_components/ha_washdata/translations/panel/el.json +++ b/custom_components/ha_washdata/translations/panel/el.json @@ -3,6 +3,7 @@ "min": "λεπτ.", "entity_not_found": "Η οντότητα δεν βρέθηκε", "running_long": "Καθυστερεί", + "stalled": "Κόλλησε", "remaining": "απομένουν", "progress": "πρόοδος", "show_time_remaining": "Εμφάνιση χρόνου που απομένει", @@ -48,16 +49,16 @@ } }, "badge": { - "quiet_tail_tip": "Κοντά στο τέλος αυτό το πρόγραμμα ησυχάζει για περίπου {mins} λεπτ., έπειτα τραβά περίπου {watts} W για {secs} δευτ. και ολοκληρώνεται. Παρατηρήθηκε σε {seen}/{measured} μετρημένους κύκλους: η συσκευή δεν το κάνει σε κάθε εκτέλεση.", + "quiet_tail_tip": "Κοντά στο τέλος αυτό το πρόγραμμα ησυχάζει για περίπου {mins} λεπτ. και μετά τραβά περίπου {watts} W για {secs} δευτ. Παρατηρήθηκε σε {seen} από {measured} μετρημένους κύκλους.", "quiet_tail": "~{mins} λεπτ. ησυχίας στο τέλος", - "artifact_tip": "Εντοπίστηκαν {n} ανωμαλίες (π.χ. η πόρτα άνοιξε στα μέσα του κύκλου) – ανοίξτε για να τις δείτε στο γράφημα", + "artifact_tip": "Εντοπίστηκαν {n} ανωμαλίες (π.χ. η πόρτα άνοιξε στα μέσα του κύκλου) - ανοίξτε για να τις δείτε στο γράφημα", + "artifact_tip_one": "Εντοπίστηκε {n} ανωμαλία (π.χ. η πόρτα άνοιξε στα μέσα του κύκλου) - ανοίξτε για να τη δείτε στο γράφημα", + "artifact_tip_other": "Εντοπίστηκαν {n} ανωμαλίες (π.χ. η πόρτα άνοιξε στα μέσα του κύκλου) - ανοίξτε για να τις δείτε στο γράφημα", "auto": "(αυτόματη ανίχνευση)", "built_in_tag": "ενσωματωμένο", "declining": "↘ φθίνουσα", "energy_low": "Χαμηλότερη κατανάλωση ενέργειας από το συνηθισμένο", "energy_spike": "Υψηλότερη κατανάλωση ενέργειας από το συνηθισμένο", - "fair_fit": "αποδεκτή ποιότητα", - "fair_fit_tip": "Μέτρια συνέπεια αντιστοίχισης – ορισμένοι κύκλοι που έχουν εκχωρηθεί σε αυτό το προφίλ έχουν χαμηλότερους βαθμούς εμπιστοσύνης. Επισημάνετε περισσότερους κύκλους ή εγγράψτε ξανά το προφίλ για να βελτιώσετε την ακρίβεια.", "feedback_requested": "Ζητήθηκαν σχόλια", "golden_cycle": "Καταγεγραμμένος κύκλος αναφοράς", "improving": "↗ βελτίωση", @@ -66,27 +67,36 @@ "overrun": "Λειτούργησε περισσότερο από το συνηθισμένο", "underrun": "Ολοκληρώθηκε γρηγορότερα από το συνηθισμένο", "poor_fit": "κακή ποιότητα", - "poor_fit_tip": "Ασυνεπές ιστορικό αντιστοίχισης – σκεφτείτε να δημιουργήσετε ξανά αυτό το προφίλ", - "restart_gap_tip": "{n} κενό επανεκκίνησης HA: το ίχνος ισχύος έχει ένα κενό", + "restart_gap_tip": "{n} κενά επανεκκίνησης HA: το ίχνος ισχύος έχει κενά", + "restart_gap_tip_one": "{n} κενό επανεκκίνησης HA: το ίχνος ισχύος έχει ένα κενό", + "restart_gap_tip_other": "{n} κενά επανεκκίνησης HA: το ίχνος ισχύος έχει κενά", "reviewed": "Αξιολογήθηκε", "steady": "→ σταθερό", - "using_defaults": "Χρήση των ενσωματωμένων προεπιλογών", - "using_tuned": "Χρησιμοποιώντας συντονισμένα βάρη", "overrun_ratio": "({x}x του αναμενόμενου)", "underrun_ratio": "({pct}% του αναμενόμενου)", - "awaiting_tip": "Αναμονή έγκρισης από την κοινότητα", - "awaiting_n": "Αναμονή έγκρισης · {n} επιβεβαιώσεις", - "approved": "Εγκρίθηκε", - "awaiting": "Αναμονή έγκρισης", "imported_tip": "Εισαγωγή από το κοινοτικό κατάστημα. Χρησιμοποιείται μόνο για αντιστοίχιση, δεν προσμετράται στα στατιστικά.", "unmatchable": "δεν αντιστοιχίζεται", "not_importable": "μη διαθέσιμο εδώ", "exists": "υπάρχει", "backfilled_tip": "Ανιχνεύθηκε σε εισαγόμενο ιστορικό ισχύος. Διαμορφώνει μόνο την αντιστοίχιση προγραμμάτων, δεν προσμετράται στα στατιστικά.", "armed": "(εφαρμόζεται στον επόμενο κύκλο)", - "duration_outlier": "{n} κύκλοι με ασυνήθιστη διάρκεια" + "duration_outlier": "{n} κύκλοι με ασυνήθιστη διάρκεια", + "awaiting_tip": "Αναμονή έγκρισης από την κοινότητα", + "awaiting_n": "Αναμονή έγκρισης · {n} επιβεβαιώσεις", + "awaiting": "Αναμονή έγκρισης", + "approved": "Εγκρίθηκε", + "matcher_unused": "Δεν έχει αντιστοιχιστεί ακόμα σε κανέναν από τους κύκλους σας", + "matcher_used": "Αντιστοιχίστηκε σε {n} κύκλους σας", + "matcher_used_one": "Αντιστοιχίστηκε σε {n} κύκλο σας", + "matcher_used_other": "Αντιστοιχίστηκε σε {n} κύκλους σας", + "matcher_used_tip": "Κύκλοι αυτής της συσκευής που το WashData αντιστοίχισε μόνο του σε αυτό το εισαγόμενο πρόγραμμα. Μια εισαγωγή που δεν αντιστοιχίζεται ποτέ ίσως δεν ταιριάζει στη συσκευή σας· μπορείτε να την αφαιρέσετε. Μετράται τοπικά· τίποτα δεν αποστέλλεται στο κατάστημα." }, "btn": { + "pg_preset_delete": "Διαγραφή προρύθμισης", + "pg_preset_load": "Φόρτωση προρύθμισης", + "pg_preset_load_tip": "Αντικατάσταση των τιμών του Χώρου δοκιμών με αυτήν την προρύθμιση (οι ενεργές ρυθμίσεις δεν αλλάζουν)", + "pg_preset_save": "Αποθήκευση ως προρύθμιση", + "pg_preset_save_tip": "Αποθήκευση όλων των παρακάτω τιμών ως επώνυμη προρύθμιση για αυτήν τη συσκευή", "reset_zoom": "Επαναφορά ζουμ", "set_brand_model": "Ορισμός μάρκας και μοντέλου", "refresh_catalog": "Ανανέωση καταλόγου", @@ -97,12 +107,11 @@ "add_timer": "+ Προσθήκη χρονοδιακόπτη", "apply_all": "Εφαρμόστε όλα", "apply_label": "Εφαρμογή ετικέτας", - "apply_set_b": "Εφαρμογή συνόλου B", "apply_split": "Εφαρμογή διαχωρισμού", "apply_trim": "Εφαρμογή περικοπής", "auto_detect_split": "Αυτόματος εντοπισμός", "auto_label_cycles": "Αυτόματη επισήμανση κύκλων", - "auto_label_cycles_tip": "Αυτόματη εκχώρηση ονομάτων προφίλ σε κύκλους χωρίς ετικέτα, των οποίων η εμπιστοσύνη αντιστοίχισης ξεπερνά το κατώφλι", + "auto_label_cycles_tip": "Ονομάζει τους κύκλους χωρίς ετικέτα των οποίων η εμπιστοσύνη αντιστοίχισης ξεπερνά το κατώφλι.", "cancel": "Ακύρωση", "clear_debug": "Εκκαθάριση δεδομένων εντοπισμού σφαλμάτων", "clear_splits": "Καθαρισμός", @@ -112,8 +121,6 @@ "convert_to_automation": "Μετατροπή σε αυτοματισμό", "correct": "Διόρθωση…", "create": "Δημιουργία", - "create_from_cluster": "Δημιουργία προφίλ από {count} κύκλους", - "create_profile": "+ Δημιουργία προφίλ", "delete": "Διαγραφή", "delete_group": "Διαγραφή ομάδας", "delete_group_tip": "Διαγραφή μόνο αυτής της ομάδας - τα προφίλ μελών διατηρούνται", @@ -124,22 +131,19 @@ "done": "Τέλος", "edit": "Επεξεργασία", "export": "Εξαγωγή", - "export_json": "Εξαγωγή σε JSON", "force_stop": "Αναγκαστική διακοπή", "force_stop_tip": "Άμεσος τερματισμός του τρέχοντος κύκλου και χαρακτηρισμός του ως αναγκαστικά τερματισμένου", "from_template": "Από το πρότυπο ▾", "ignore": "Αγνόηση", "import_json": "Εισαγωγή από JSON", - "import_overwrite": "Εισαγωγή (αντικαθιστά δεδομένα)", "inspect": "Επιθεώρηση", "label": "Επισήμανση", - "load": "Φόρτωση", "load_more": "Φόρτωση περισσότερων", "manage": "Διαχείριση", "merge": "Συγχώνευση", "new_automation": "+ Νέος Αυτοματισμός", "new_group": "+ Νέα ομάδα", - "new_group_tip": "Ομαδοποιήστε σχεδόν πανομοιότυπα προφίλ (ίδιο σχήμα/διάρκεια, διαφορετική θερμοκρασία ή στύψιμο) ώστε η αντιστοίχιση να επιλέγει αξιόπιστα μεταξύ τους", + "new_group_tip": "Ομαδοποιήστε σχεδόν πανομοιότυπα προφίλ (ίδιο σχήμα, άλλη θερμοκρασία ή στύψιμο) ώστε η αντιστοίχιση να τα ξεχωρίζει.", "new_phase": "+ Νέα Φάση", "new_profile": "+ Νέο προφίλ", "new_profile_tip": "Δημιουργία νέου προφίλ προγράμματος από έναν υπάρχοντα επισημασμένο κύκλο ή εγγραφή", @@ -147,36 +151,25 @@ "on_cycle_started": "Ο κύκλος ξεκίνησε", "pause_cycle": "Παύση", "pause_cycle_tip": "Παύση του εν λειτουργία κύκλου – η συσκευή θα συνεχίσει από εκεί που σταμάτησε", - "pg_apply_device": "Εφαρμογή στη συσκευή", - "pg_autofill": "Αυτόματη συμπλήρωση", - "pg_load_calibrated": "Φόρτωση βαθμονομημένων (ML) ({n})", - "pg_load_calibrated_tip": "Φορτώνει τις ρυθμίσεις βαθμονόμησης ML ως παρακαμπτήριες ρυθμίσεις στον Χώρο δοκιμών", "pg_load_live": "Φόρτωση ενεργών ρυθμίσεων", "pg_load_live_tip": "Επαναφόρτωση των τρεχουσών ρυθμίσεων της ενσωμάτωσης και απόρριψη κάθε αλλαγής στον Χώρο δοκιμών", "pg_load_suggested": "Φόρτωση προτάσεων ({n})", "pg_load_suggested_tip": "Φορτώνει τις τρέχουσες προτάσεις του αυτόματου βελτιστοποιητή ως παρακαμπτήριες ρυθμίσεις στον Χώρο δοκιμών", - "pg_preset_delete": "Διαγραφή προρύθμισης", - "pg_preset_load": "Φόρτωση προρύθμισης", - "pg_preset_load_tip": "Αντικατάσταση των τιμών του Χώρου δοκιμών με αυτήν την προρύθμιση (οι ενεργές ρυθμίσεις δεν αλλάζουν)", - "pg_preset_save": "Αποθήκευση ως προρύθμιση", - "pg_preset_save_tip": "Αποθήκευση όλων των παρακάτω τιμών ως επώνυμη προρύθμιση για αυτήν τη συσκευή", "pg_publish_all": "Εφαρμογή στην ενσωμάτωση ({n})", "pg_publish_one": "Εφαρμογή {label} στην ενσωμάτωση", - "play": "Αναπαραγωγή", "process": "Επεξεργασία", "process_history": "Επεξεργασία τώρα", "process_recording": "Αποθήκευση εγγραφής", "process_tip": "Αποθήκευση του καταγεγραμμένου ίχνους ως νέο ή υπάρχον προφίλ", "rebuild": "Ανακατασκευή περιβαλλουσών", "rebuild_envelope": "Ανακατασκευή περιβάλλουσας", - "rebuild_tip": "Επανυπολογισμός της αναμενόμενης περιβάλλουσας ισχύος (ελάχιστο/μέγιστο εύρος) για όλα τα προφίλ από τους επισημασμένους τους κύκλους – εκτελέστε μετά από επισήμανση νέων κύκλων ή διόρθωση παλιών", + "rebuild_tip": "Επανυπολογίζει την αναμενόμενη ζώνη ισχύος κάθε προφίλ από τους επισημασμένους κύκλους του. Εκτελέστε το μετά από επισήμανση ή διόρθωση κύκλων.", "rec_start_tip": "Έναρξη καταγραφής του ίχνους ισχύος της συσκευής – ξεκινήστε λίγο πριν τη λειτουργία του κύκλου", "rec_stop": "Διακοπή", "rec_stop_tip": "Διακοπή εγγραφής και διατήρηση του καταγεγραμμένου ίχνους για αναθεώρηση", "record": "Ξεκινήστε την εγγραφή", "refresh": "Ανανέωση", "refresh_diag_tip": "Επαναφόρτωση στατιστικών αποθήκευσης", - "refresh_logs_tip": "Επαναφόρτωση των πιο πρόσφατων εγγραφών καταγραφής", "refresh_settings_tip": "Επαναφόρτωση ρυθμίσεων από τον διακομιστή", "relabel": "Επανεπισήμανση ({count})", "remove": "Αφαίρεση", @@ -184,7 +177,6 @@ "reset": "Επαναφορά", "reset_ml_models_tip": "Απορρίψτε τα βελτιωμένα μοντέλα και επιστρέψτε στα ενσωματωμένα. Το WashData μπορεί να τα μάθει ξανά αργότερα.", "reset_to_builtin": "Επαναφορά σε ενσωματωμένα μοντέλα", - "reset_to_defaults": "Επαναφορά στις προεπιλογές", "resume_cycle": "Συνέχιση κύκλου", "resume_cycle_tip": "Συνέχιση του παυσμένου κύκλου", "revert_settings": "Απόρριψη αλλαγών", @@ -197,8 +189,6 @@ "run_analysis": "🔍 Εκτέλεση ανάλυσης προτάσεων", "run_analysis_tip": "Αναλύστε τους καταγεγραμμένους κύκλους σας τώρα και ανανεώστε τις προτάσεις συντονισμού", "run_auto_label": "Εκτέλεση αυτόματης επισήμανσης", - "run_both": "Εκτέλεση A έναντι B", - "run_simulation": "Εκτέλεση προσομοίωσης", "save": "Αποθήκευση", "save_access_control": "Αποθήκευση ελέγχου πρόσβασης", "save_group": "Αποθήκευση ομάδας", @@ -210,10 +200,8 @@ "save_settings": "Αποθήκευση ρυθμίσεων", "select": "Επιλογή", "select_all": "Επιλογή όλων", - "select_last20": "Τελευταία 20", "select_none": "Καθαρισμός", "show_only": "Εμφάνιση μόνο", - "skip_setup": "Παράλειψη ρύθμισης", "split": "Διαχωρισμός", "stop": "Διακοπή", "submit_correction": "Υποβολή Διόρθωσης", @@ -222,36 +210,31 @@ "trim_split": "Περικοπή / Διαχωρισμός", "undo": "Αναίρεση", "unlabel_selected": "Αφαίρεση επισήμανσης επιλεγμένων ({n})", - "unlabel_selected_tip": "Αφαιρεί την ετικέτα προφίλ από τους επιλεγμένους κύκλους. Παραμένουν στο ιστορικό σας και μπορούν να επισημανθούν ξανά αργότερα - απλώς παύουν να διαμορφώνουν αυτό το προφίλ.", + "unlabel_selected_tip": "Αφαιρεί την ετικέτα από τους επιλεγμένους κύκλους. Μένουν στο ιστορικό σας αλλά παύουν να διαμορφώνουν αυτό το προφίλ.", "use": "Χρήση", "wipe_all": "Διαγραφή όλων των δεδομένων", "delete_selected": "Διαγραφή επιλεγμένων ({n})", - "group_suggest": "Ομάδα {n}: {members}", "pg_apply_best": "Εφαρμογή καλύτερου", - "pg_apply_to_settings": "Αποθήκευση στις ρυθμίσεις", "pg_apply_to_settings_tip": "Αντιγραφή των τιμών που επεξεργαστήκατε εδώ στις ενεργές ρυθμίσεις αυτής της συσκευής", "import": "Εισαγωγή", "disconnect": "Αποσύνδεση", "connect_github": "Σύνδεση GitHub", - "save_appliance": "Αποθήκευση συσκευής", "share_to_store": "Κοινοποίηση στο κατάστημα", "share": "Κοινοποίηση", - "confirm_appliance": "Επιβεβαίωση αυτής της συσκευής", - "confirmed": "Επιβεβαιώθηκε", "browse_community_setups": "Περιήγηση σε ρυθμίσεις κοινότητας", "download_device": "Λήψη αυτής της ρύθμισης", "share_device": "⬆ Κοινοποίηση αυτής της συσκευής", - "share_device_tip": "Κοινοποιήστε αυτή τη συσκευή και τους καταγεγραμμένους κύκλους αναφοράς της στο κοινοτικό κατάστημα, ώστε άλλοι με το ίδιο μηχάνημα να μπορούν να τα υιοθετήσουν", + "share_device_tip": "Κοινοποιήστε αυτή τη συσκευή και τους κύκλους αναφοράς της στο κοινοτικό κατάστημα, ώστε όσοι έχουν το ίδιο μηχάνημα να τα υιοθετήσουν.", "share_n": "Κοινοποίηση {n} κύκλου(ων)", "export_selected": "Εξαγωγή (επιλογή δεδομένων)", "export_all": "Γρήγορη εξαγωγή όλων", - "import_raw": "Για προχωρημένους: αντικατάσταση όλων από JSON", "download_export": "Λήψη εξαγωγής", "analyze_import": "Ανάλυση αρχείου", "import_selected": "Εισαγωγή επιλεγμένων", "back": "Πίσω", "mute_suggestion": "Διακοπή πρότασης αυτής της ρύθμισης", "reset_muted": "Επαναφορά αποσιωπημένων", + "reset_phase_tip": "Αναίρεση της επεξεργασίας σας και επαναφορά της ενσωματωμένης φάσης", "import_power_history": "Εισαγωγή ιστορικού ισχύος", "hist_read_recorder": "Ανάγνωση από το Home Assistant", "hist_scan": "Σάρωση για κύκλους", @@ -260,13 +243,17 @@ "save_total_cycles": "Αποθήκευση συνόλου", "clear": "Καθαρισμός", "add_brand": "Προσθήκη μάρκας", - "add_appliance": "Προσθήκη συσκευής" + "log_done_task": "Καταγραφή ολοκλήρωσης: {task}", + "add_task": "Προσθήκη εργασίας", + "add_appliance": "Προσθήκη συσκευής", + "retry": "Επανάληψη", + "confirm_appliance": "Επιβεβαίωση αυτής της συσκευής", + "confirmed": "Επιβεβαιώθηκε", + "create_profile_from_gap": "Δημιουργία προφίλ", + "undo_import": "Αναίρεση τελευταίας εισαγωγής", + "pg_rerun": "Ενημέρωση προσομοίωσης" }, "conflict": { - "anti_wrinkle_exit": { - "exit": "Πρέπει να είναι κάτω από το Κατώφλι Διακοπής ({stop} W)· διαφορετικά η ισχύς εξόδου αντί-τσακίσματος αγνοείται", - "stop": "Πρέπει να είναι πάνω από την Ισχύ Εξόδου Αντί-Τσακίσματος ({exit} W)" - }, "anti_wrinkle_max": { "max": "Πρέπει να είναι πάνω από το Κατώφλι Εκκίνησης ({start} W)· διαφορετικά το όριο διάρκειας αντί-τσακίσματος παρακάμπτεται", "start": "Πρέπει να είναι κάτω από τη Μέγιστη Ισχύ Αντί-Τσακίσματος ({max} W)" @@ -274,10 +261,10 @@ "attn_sub": "Διορθώστε τις συγκρούσεις πριν αποθηκεύσετε", "attn_title": "Συγκρούσεις ρυθμίσεων: {n}", "price_entity": { - "power_sensor": "Αυτός είναι ο Αισθητήρας Ισχύος της συσκευής, όχι τιμή. Μια οντότητα τιμής υπερισχύει της στατικής τιμής παρακάτω, οπότε οι κύκλοι θα κοστολογούνταν με την ένδειξη σε watt ως τιμή ανά kWh - καθαρίστε αυτό το πεδίο.", - "energy_sensor": "Αυτός είναι ο Μετρητής Ενέργειας της συσκευής, όχι τιμή. Μια οντότητα τιμής υπερισχύει της στατικής τιμής παρακάτω, οπότε οι κύκλοι θα κοστολογούνταν με την ένδειξη του μετρητή ως τιμή ανά kWh - καθαρίστε αυτό το πεδίο.", - "device_class": "Αυτός ο αισθητήρας μετρά {dc}, όχι τιμή ανά kWh. Μια οντότητα τιμής υπερισχύει της στατικής τιμής παρακάτω - καθαρίστε αυτό το πεδίο ή επιλέξτε έναν αισθητήρα τιμολογίου.", - "unit": "Αυτός ο αισθητήρας δίνει ένδειξη σε {unit}, που δεν είναι τιμή ανά kWh. Μια οντότητα τιμής υπερισχύει της στατικής τιμής παρακάτω - καθαρίστε αυτό το πεδίο ή επιλέξτε έναν αισθητήρα τιμολογίου." + "power_sensor": "Αυτός είναι ο Αισθητήρας Ισχύος της συσκευής, όχι τιμή. Οι κύκλοι θα κοστολογούνταν σε watt ανά kWh· καθαρίστε αυτό το πεδίο.", + "energy_sensor": "Αυτός είναι ο Μετρητής Ενέργειας της συσκευής, όχι τιμή. Οι κύκλοι θα κοστολογούνταν με την ένδειξη του μετρητή ανά kWh· καθαρίστε αυτό το πεδίο.", + "device_class": "Αυτός ο αισθητήρας μετρά {dc}, όχι τιμή ανά kWh. Καθαρίστε αυτό το πεδίο ή επιλέξτε αισθητήρα τιμολογίου.", + "unit": "Αυτός ο αισθητήρας μετρά σε {unit}, όχι τιμή ανά kWh. Καθαρίστε αυτό το πεδίο ή επιλέξτε αισθητήρα τιμολογίου." }, "settings_banner": "Συγκρούσεις ρυθμίσεων: {n}. Ελέγξτε τα επισημασμένα τμήματα και διορθώστε τα πριν την αποθήκευση.", "settings_banner_btn": "Μετάβαση στο πρώτο", @@ -312,8 +299,8 @@ "threshold": "Πρέπει να είναι κάτω από το Κατώφλι Διακοπής ({stop} W) για να τεθεί σε ισχύ" }, "pump_stuck": { - "duration": "Πρέπει να είναι μικρότερο από το Χρονικό Όριο Χωρίς Ενημέρωση ({to} s) ώστε ο συναγερμός αγκίστρωσης να ενεργοποιείται πριν το watchdog τερματίσει τον κύκλο", - "timeout": "Πρέπει να υπερβαίνει τη Διάρκεια Αγκίστρωσης Αντλίας ({ps} s) ώστε ο συναγερμός να ενεργοποιείται πριν τον αναγκαστικό τερματισμό του κύκλου" + "duration": "Πρέπει να είναι μικρότερο από τη Λήξη Χρόνου Χωρίς Ενημέρωση ({to} δευτ.), ώστε να ενεργοποιείται πρώτα ο συναγερμός κολλημένης αντλίας", + "timeout": "Πρέπει να υπερβαίνει τη Διάρκεια Κολλημένης Αντλίας ({ps} δευτ.), ώστε να ενεργοποιείται πρώτα ο συναγερμός κολλημένης αντλίας" }, "start_dur": { "sampling": "Το Διάστημα Δειγματοληψίας υπερβαίνει τη Διάρκεια Εκκίνησης ({sdt} s)· ακίδες μονού δείγματος μπορούν να ανοίξουν κύκλο", @@ -329,10 +316,15 @@ "watchdog": { "interval": "Πρέπει να είναι τουλάχιστον 2× το Διάστημα Δειγματοληψίας ({si} s)", "sampling": "Το Διάστημα Δειγματοληψίας πρέπει να είναι το πολύ το μισό του Διαστήματος Watchdog ({wi} s)" + }, + "end_energy": { + "energy": "Πολύ αυστηρό για το Κατώφλι Τερματισμού ({w} W) στη διάρκεια της Καθυστέρησης Απενεργοποίησης ({d} s)· ο κύκλος μπορεί να τερματιστεί μόνο μέσω εναλλακτικής διαδρομής", + "stop": "Η Ενέργεια Λήξης ({e} Wh σε {d} s) επιτρέπει μόνο {p} W" } }, "hdr": { - "ab_comparison": "Σύγκριση ρυθμίσεων A/B", + "custom_tasks": "Οι δικές σας εργασίες", + "maintenance_supporter": "Χρειάζεστε περισσότερα;", "access_control": "Έλεγχος πρόσβασης", "add_maintenance": "Προσθήκη συμβάντος συντήρησης", "automation_delete": "Διαγράψτε αυτόν τον αυτοματισμό", @@ -341,28 +333,22 @@ "automation_review_this_cycle": "Ελέγξτε αυτόν τον κύκλο", "automations": "Αυτοματισμοί", "clear_debug": "Εκκαθάριση ιχνών εντοπισμού σφαλμάτων", - "cycle_simulator": "Προσομοιωτής κύκλων", "diagnostics_pref": "Διαγνωστικά", "display": "Εμφάνιση", - "dtw_inspector": "Επιθεωρητής DTW", "export_import": "Εξαγωγή / Εισαγωγή", - "getting_started": "Ξεκινώντας", "live_power": "Ζωντανή ισχύς", "logs": "Αρχεία καταγραφής", - "logs_diagnostics": "Διαγνωστικά", "maintenance": "Ενέργειες Συντήρησης", "maintenance_log": "Αρχείο συντήρησης", "maintenance_reminders": "Υπενθυμίσεις σέρβις", "manual_recording": "Χειροκίνητη εγγραφή", - "manual_recording_tip": "Εκτελέστε έναν κύκλο σκόπιμα ενώ το WashData καταγράφει το ίχνος ισχύος. Ξεκινήστε αμέσως πριν ανοίξει η συσκευή, σταματήστε όταν τελειώσει, και Επεξεργαστείτε για να το αποθηκεύσετε ως κατονομαζόμενο προφίλ.", + "manual_recording_tip": "Καταγράψτε έναν κύκλο σκόπιμα: πατήστε Έναρξη λίγο πριν ξεκινήσει η συσκευή και Διακοπή όταν τελειώσει, μετά Επεξεργασία για να τον αποθηκεύσετε ως προφίλ.", "ml_learned": "Τι έμαθε η WashData", - "ml_matching_tuning": "Βελτιστοποίηση αντιστοίχισης προγράμματος", "ml_settings_card": "Ρυθμίσεις", "ml_smart_learning": "Έξυπνη μάθηση", "my_preferences": "Οι Προτιμήσεις μου", "no_automations": "Δεν αναφέρονται ακόμη αυτοματισμοί σε αυτήν τη συσκευή.", "panel_settings": "Ρυθμίσεις πίνακα", - "param_sweep": "Σάρωση παραμέτρων", "pg_settings_source": "Προέλευση ρυθμίσεων", "phase_catalog": "Κατάλογος Φάσης", "playground": "Χώρος δοκιμών", @@ -371,11 +357,8 @@ "status": "Κατάσταση", "status_graph": "Γράφημα κατάστασης", "storage_stats": "Στατιστικά αποθήκευσης", - "test_history": "Δοκιμή στο ιστορικό", "toggle_sidebar": "Εναλλαγή πλευρικής γραμμής Home Assistant", - "tools_and_data": "Εργαλεία & Δεδομένα", "wipe_history": "Διαγραφή ιστορικού", - "recommendations": "Συστάσεις ({n})", "pg_detection_params": "Ρυθμίσεις ανίχνευσης", "pg_outcome": "Αποτέλεσμα προσομοίωσης", "pg_across_cycles": "Σε όλους τους κύκλους σας", @@ -383,7 +366,8 @@ "online_account": "Κοινοτικό Κατάστημα & διαδικτυακές λειτουργίες", "import_power_history": "Εισαγωγή ιστορικού ισχύος", "service_status": "Κατάσταση συντήρησης", - "online_account_tab": "Διαδικτυακά και κοινότητα" + "online_account_tab": "Διαδικτυακά και κοινότητα", + "live_match_debug": "Εντοπισμός σφαλμάτων ζωντανής αντιστοίχισης" }, "health": { "fair": "Αποδεκτή ποιότητα αντιστοίχισης", @@ -391,11 +375,16 @@ "poor": "⚠ Κακή ποιότητα αντιστοίχισης" }, "lbl": { - "envelope_position_tip": "Θέση πάνω στην καταγεγραμμένη καμπύλη του ίδιου του αντιστοιχισμένου προγράμματος, {pct}%. Μετριέται με ευθυγράμμιση αυτού του κύκλου με το προφίλ αντί για μέτρηση του χρόνου, γι' αυτό παραμένει σωστή όταν μια εκτέλεση είναι μεγαλύτερη ή μικρότερη από τη συνηθισμένη. Ανανεώνεται όσο η συσκευή είναι ήσυχη, γι' αυτό μπορεί να καθυστερεί κατά τη διάρκεια μιας ενεργής φάσης.", + "envelope_position_tip": "Θέση πάνω στην καταγεγραμμένη καμπύλη του αντιστοιχισμένου προγράμματος: {pct}%. Βρίσκεται με ευθυγράμμιση του κύκλου με το προφίλ, οπότε μένει σωστή όταν μια εκτέλεση κρατά περισσότερο ή λιγότερο. Ανανεώνεται όσο η συσκευή είναι ήσυχη, οπότε μπορεί να καθυστερεί.", "envelope_position": "καμπύλη {pct}%", "aria_playground_chart3": "Διαδραστικό γράφημα ισχύος κύκλου: τοποθετήστε τον δείκτη για ανάγνωση χρόνου/ισχύος, κυλήστε ή κάντε ζουμ με δύο δάχτυλα, σύρετε για μετακίνηση", "device_unresolved": "Μη διαθέσιμη συσκευή ({id})", "drag_to_resize": "Σύρετε για αλλαγή μεγέθους", + "task_name": "Εργασία", + "every_cycles": "Κάθε (κύκλους)", + "every_days": "Κάθε (ημέρες)", + "new_task_name": "Νέα εργασία", + "days_since_service": "{since} / {total} ημέρες", "actions": "Ενέργειες", "activity": "Δραστηριότητα", "administrators": "Διαχειριστές", @@ -403,10 +392,8 @@ "aria_cycle_chart": "Καμπύλη ισχύος κύκλου", "aria_envelope_chart": "Γράφημα περιβάλλουσας ισχύος προφίλ", "aria_phase_chart": "Γράφημα επεξεργασίας φάσεων", - "aria_playground_chart": "Καμπύλη ισχύος κύκλου με συρόμενα όρια ανίχνευσης", "aria_power_chart": "Γράφημα κατανάλωσης ισχύος", "aria_spaghetti_chart": "Επικαλυπτόμενες καμπύλες ισχύος κύκλων", - "aria_sweep_chart": "Γράφημα αποτελεσμάτων σάρωσης παραμέτρων", "artifact_anomaly": "Ανωμαλία", "artifact_high_power": "Υψηλή ισχύς", "artifact_interruption": "Διακοπή", @@ -420,150 +407,100 @@ "cohesion_good": "συνοχή {pct}%", "cohesion_low": "⚠ χαμηλή συνοχή {pct}%", "compare_profiles": "Σύγκριση με προφίλ", - "concurrency": "Ταυτοχρονισμός", "confidence": "Εμπιστοσύνη", "confidence_threshold": "Όριο εμπιστοσύνης", "correct_duration": "Σωστή διάρκεια (λεπτά, προαιρετικό)", "correct_profile": "Σωστό Προφίλ", - "correlation": "Συσχέτιση", "cost": "Κόστος", "create_new_profile": "+ Δημιουργία νέου προφίλ…", "current_phase": "Τρέχουσα φάση", - "curves": "Καμπύλες", "cycle": "Κύκλος", "cycle_date_display": "Εμφάνιση ημερομηνίας κύκλου", - "cycle_health": "Υγεία του κύκλου", - "cycle_index": "δείκτης κύκλου →", - "cycles": "Κύκλοι", "cycles_count": "Κύκλοι", "cycles_lc": "κύκλοι", - "dashed": "διακεκομμένη", "date": "Ημερομηνία", "debug_traces": "Ίχνη εντοπισμού σφαλμάτων", - "default": "Προεπιλογή", "default_access_level": "Προεπιλεγμένο επίπεδο για χρήστες που δεν αναφέρονται παρακάτω", "default_other": "Προεπιλογή (άλλες συσκευές)", "default_tab": "Προεπιλεγμένη καρτέλα κατά το άνοιγμα του πίνακα", - "delta": "Δ", "description": "Περιγραφή", - "dtw_blend_w": "Βάρος ανάμειξης", - "dtw_blended": "Αναμεμειγμένη βαθμολογία", - "dtw_ddtw": "Βαθμολογία DDTW", - "dtw_l1": "Βαθμολογία L1", - "dtw_matched_profile": "(αντιστοιχισμένο προφίλ του κύκλου)", "duration": "Διάρκεια", - "duration_agreement": "Συμφωνία διάρκειας", - "emphasis": "Εμφαση", "enable_access_control": "Ενεργοποίηση ελέγχου πρόσβασης ανά χρήστη", "energy": "Ενέργεια", - "energy_agreement": "Συμφωνία ενέργειας", - "event_timeline": "Χρονολόγιο συμβάντων", "event_type": "Τύπος συμβάντος", "expected": "Αναμενόμενο", + "suspected_profile": "{name} (πιθανό)", + "runner_up_profile": "{name} (δεύτερο καλύτερο)", "expected_duration": "Αναμενόμενη διάρκεια (λεπτά)", "file_kb": "Αρχείο (kB)", - "final_score": "Τελική βαθμολογία", "from": "Από", "gap_s": "Κενό (δευτ.)", "group_name": "Όνομα ομάδας", "head_trim": "Περικοπή αρχής (δευτ.)", - "health": "Υγεία", "hide_tabs": "Απόκρυψη καρτελών για μη διαχειριστές", - "in_use": "Σε χρήση", "json_data": "Δεδομένα JSON", "label": "Αντιστοιχισμένο πρόγραμμα", "last": "Τελευταία", "last_checked": "Τελευταίος έλεγχος:", "load_from_file": "Φόρτωση από αρχείο", - "mae_score": "Βαθμολογία MAE", "manual_duration": "Μη αυτόματη διάρκεια (λεπτά, προαιρετικό)", "match": "Αντιστοίχιση", "members": "Μέλη", - "metric": "Μετρική", "mode_existing_profile": "Προσθήκη στο υπάρχον προφίλ", "mode_new_profile": "Δημιουργία Νέου Προφίλ", "models_fine_tuned": "(βελτιστοποιημένα μοντέλα: {count})", - "n_classic_suggestions": "{n} κλασικά", - "n_ml_suggestions": "{n} ML", "n_selected": "{n} επιλεγμένα", - "n_tuning_suggestions": "{n} πρόταση συντονισμού", - "na": "n/a", + "n_tuning_suggestions": "{n} προτάσεις συντονισμού", + "n_tuning_suggestions_one": "{n} πρόταση συντονισμού", + "n_tuning_suggestions_other": "{n} προτάσεις συντονισμού", "new_profile_name": "Νέο όνομα προφίλ", "notes": "Σημειώσεις", - "of": "από", - "outcome": "Έκβαση", "panel_default_tab": "Προεπιλεγμένη καρτέλα", "panel_language": "Γλώσσα πίνακα", "per_cycle_short": "κύκλος", "pg_anti_wrinkle": "Κατά τσακίσματος", - "pg_autofill_tip": "Αυτόματη συμπλήρωση εύρους από την τρέχουσα τιμή ({lo}–{hi})", "pg_best_value": "Καλύτερη τιμή που βρέθηκε", "pg_current_value": "Τρέχουσα τιμή:", "pg_drag_hint": "Σύρετε τη γραμμή στο γράφημα", - "pg_dtw": "DTW", "pg_ev_ending": "Λήγει", "pg_ev_running": "Εκτελείται", "pg_group_advanced": "Ακραίες περιπτώσεις", "pg_group_detection": "Ενεργοποιητές ανίχνευσης", "pg_group_timing": "Κανόνες χρονισμού", "pg_idle": "Αδρανής", - "pg_last_run": "Τελευταία εκτέλεση: {date}", - "pg_match_score": "Βαθμολογία αντιστοίχισης", - "pg_pct_of": "{p}% των {total}", "pg_poor_match": "Κακή αντιστοίχιση", "pg_preset": "Προρύθμιση Χώρου δοκιμών", "pg_preset_name": "Όνομα νέας προρύθμισης", "pg_preset_none": "Επιλέξτε προρύθμιση…", "pg_strong_match": "Ισχυρή αντιστοίχιση", - "pg_tile_ambiguous": "Ασαφείς", - "pg_tile_ambiguous_tip": "Κύκλοι με σχεδόν ισοπαλία μεταξύ δύο προγραμμάτων", - "pg_tile_detected": "Εντοπισμένοι", - "pg_tile_detected_tip": "Κύκλοι όπου η ισχύς ξεπέρασε το κατώφλι εκκίνησης αρκετά ώστε να καταγραφεί ως σε λειτουργία", - "pg_tile_matched": "Αντιστοιχισμένοι", - "pg_tile_matched_tip": "Κύκλοι που αποδόθηκαν σωστά σε γνωστό πρόγραμμα", - "pg_time_left": "Χρόνος που απομένει", "pg_weak_match": "Ασθενής αντιστοίχιση", "phase_name": "Όνομα φάσης", "name": "Ονομα", "phase_timeline": "Χρονολόγιο φάσεων", "power": "Ισχύς", "profile": "Προφίλ", - "profile_index": "δείκτης προφίλ →", "profile_name": "Όνομα προφίλ", "program": "Πρόγραμμα", - "program_tip": "Παρακάμψτε ποιο προφίλ αντιστοιχίζεται στον τρέχοντα κύκλο. Η αυτόματη ανίχνευση επιτρέπει στην ενσωμάτωση να επιλέξει αυτόματα την καλύτερη αντιστοίχιση. Καρφιτσώστε ένα συγκεκριμένο πρόγραμμα για αναγκαστική αντιστοίχιση όταν η αυτόματη ανίχνευση είναι λανθασμένη ή γνωρίζετε τι εκτελείται. Επιλέξτε ένα πρόγραμμα πριν θέσετε σε λειτουργία τη συσκευή και θα εφαρμοστεί μόλις ξεκινήσει ο επόμενος κύκλος.", + "program_tip": "Επιλέξτε σε ποιο πρόγραμμα αντιστοιχίζεται ο τρέχων κύκλος. Ο Αυτόματος εντοπισμός διαλέγει την καλύτερη αντιστοίχιση. Καρφιτσώστε ένα πρόγραμμα όταν ο αυτόματος εντοπισμός κάνει λάθος· αν το επιλέξετε πριν την εκκίνηση, ισχύει για τον επόμενο κύκλο.", "progress": "Πρόοδος", "raw_socket": "Ακατέργαστο σήμα πρίζας", - "read_only": "Μόνο για ανάγνωση", "ref_cycle": "Κύκλος αναφοράς (προαιρετικό)", - "refresh_interval": "Διάστημα ανανέωσης (δευτ.)", "remaining": "Απομένει", "reminder_every": "Υπενθύμιση κάθε (κύκλους)", "remove_label": "- Αφαιρέστε την ετικέτα -", "rename_profile": "Μετονομασία προφίλ", - "replay_duration": "Αναπαραγωγή", "resulting_profile": "Προκύπτον προφίλ", "save_mode": "Λειτουργία αποθήκευσης", - "score": "Βαθμολογία", - "score_breakdown": "Ανάλυση βαθμολογίας", "seconds_from_start": "Δευτερόλεπτα από την αρχή", "select_profile": "Επιλέξτε Προφίλ", "setting": "Ρύθμιση", - "settings_a": "Ρυθμίσεις A (τρέχουσες)", - "settings_a_short": "A", "settings_advanced": "Για προχωρημένους", - "settings_b": "Ρυθμίσεις B (δοκιμή)", - "settings_b_short": "B", "settings_basic": "Βασικά", "settings_detail_level": "Επίπεδο λεπτομέρειας ρυθμίσεων", - "settings_overrides": "Παρακάμψεις ρυθμίσεων", "show_debug": "Εμφάνιση της κάρτας εντοπισμού σφαλμάτων ζωντανής αντιστοίχισης στη σελίδα Κατάσταση (εμπιστοσύνη, ασάφεια, κορυφαίοι υποψήφιοι)", "show_expected": "Εμφάνιση αναμενόμενης επικάλυψης καμπύλης (αντιστοιχισμένο προφίλ, πορτοκαλί)", "show_raw": "Εμφάνιση εναλλακτή ακατέργαστης πρίζας στο γράφημα ζωντανής ισχύος", "sparkline": "Τάση διάρκειας πρόσφατων κύκλων", - "stage2": "Στάδιο 2 – βασική ομοιότητα", - "stage3": "Στάδιο 3 – DTW", - "stage4": "Στάδιο 4 – συμφωνία", "status": "Κατάσταση", "tail_trim": "Περικοπή τέλους (δευτ.)", "tap_to_dismiss": "πατήστε το γράφημα για κλείσιμο", @@ -574,7 +511,6 @@ "ungrouped": "Μη ομαδοποιημένος", "unlabelled": "Χωρίς ετικέτα", "unlabelled_paren": "(χωρίς ετικέτα)", - "warp_path": "Διαδρομή warp", "input": "Είσοδος:", "start": "Έναρξη", "end": "Τέλος", @@ -599,9 +535,8 @@ "n_shown": ", {n} εμφανίζονται", "cycles_title": "Κύκλοι ({n})", "pg_detecting": "Εντοπισμός", - "pg_best_marker": "Βέλτιστο: {v}", "pg_time_left_model": "Χρόνος που απομένει (μοντέλο)", - "pg_time_left_model_tip": "Εκτιμώμενος από το μοντέλο χρόνος που απομένει (εκτιμητής φάσης + μείγμα ML), όχι στατική αντίστροφη μέτρηση", + "pg_time_left_model_tip": "Εκτίμηση από τις φάσεις του προγράμματος, όχι σταθερή αντίστροφη μέτρηση", "phase": "Φάση", "pg_mode_history": "Δοκιμή στο ιστορικό", "pg_ev_detected": "Ανιχνεύθηκε", @@ -633,12 +568,6 @@ "pg_sweep_param": "Παράμετρος", "pg_objective": "Στόχος", "pg_steps": "Βήματα", - "pg_stress_group": "Δοκιμή τερματισμού αδράνειας", - "pg_stress_idle_auto": "Auto", - "pg_stress_idle_w": "Επίπεδο αδράνειας (W)", - "pg_stress_idle_w_desc": "Παρακάμψτε το αυτόματα ανιχνευθέν κατώφλι κατάστασης αναμονής (αφήστε κενό για αυτόματο).", - "pg_stress_toggle": "Δοκιμή τερματισμού αδράνειας", - "pg_stress_toggle_desc": "Προσομοιώνει τη συσκευή που παραμένει στο επίπεδο κατανάλωσης αναμονής μετά το τέλος της εγγραφής - δείχνει αν και πότε το WashData σταματά τον κύκλο.", "pg_obj_match": "Ακρίβεια αντιστοίχισης", "pg_obj_endtiming": "Ακρίβεια χρόνου λήξης", "pg_obj_falseend": "Ποσοστό ψευδών λήξεων", @@ -647,12 +576,8 @@ "pg_mode_optimize": "Βελτιστοποίηση", "pg_group_matching": "Αντιστοίχιση προγράμματος", "pg_zoom_hint": "διπλό κλικ για επαναφορά ζουμ", - "aria_playground_chart2": "Διαδραστικό γράφημα ισχύος κύκλου: τοποθετήστε τον δείκτη για ανάγνωση χρόνου/ισχύος, κυλήστε για ζουμ, σύρετε για μετακίνηση", "pg_alert_timeout_end": "Τερματίστηκε λόγω λήξης χρόνου, όχι από πρόβλεψη", "pg_alert_indefinite": "Θα εκτελούνταν επ' αόριστον", - "pg_alert_stress_cap": "Επιτεύχθηκε το όριο ασφαλείας", - "pg_alert_stress_ok": "Τερματισμός λόγω αδράνειας: ο κύκλος σταμάτησε", - "pg_alert_stress_warn": "Κατανάλωση αδράνειας πάνω από το κατώφλι διακοπής", "task_pg_history": "Δοκιμή στο ιστορικό", "task_pg_sweep": "Βελτιστοποίηση", "task_reprocess": "Επανεπεξεργασία", @@ -662,16 +587,9 @@ "pg_recent_runs": "Πρόσφατες εκτελέσεις", "pg_load_run": "Επαναφόρτωση αυτής της εκτέλεσης", "pg_match_confidence": "Βεβαιότητα αντιστοίχισης", - "pg_envelope_fit": "Προσαρμογή στην περιβάλλουσα", - "pg_envelope_fit_tip": "Πόσο καλά ο κύκλος παραμένει μέσα στην αποθηκευμένη περιβάλλουσα ισχύος αυτού του προφίλ, μια διαφορετική οπτική από τη βεβαιότητα αντιστοίχισης.", "n_imported_note": ", {n} εισηγμένοι", "enable_online": "Ενεργοποίηση διαδικτυακών λειτουργιών", - "store_brand": "Μάρκα συσκευής", - "store_model": "Μοντέλο συσκευής", "anonymous": "Ανώνυμος", - "rate_quality": "Βαθμολόγηση ποιότητας", - "rate_n": "{n} αστέρια", - "approved_only": "Μόνο εγκεκριμένα", "adopt_settings": "Υιοθέτηση ρυθμίσεων επίσης", "flags": "Σημαίες", "include_phase_map": "Συμπερίληψη χάρτη φάσεων", @@ -688,7 +606,6 @@ "cat_custom_phases": "Προσαρμοσμένες φάσεις", "cat_profile_groups": "Ομάδες προφίλ", "cat_settings": "Ρυθμίσεις ανίχνευσης & αναγνώρισης", - "cat_matching_config": "Ρύθμιση αναγνώρισης", "cat_ml_models": "Μοντέλα ML", "cat_feedback": "Ετικέτες σχολίων & αξιολόγησης", "cat_suggestions": "Προτάσεις", @@ -710,7 +627,6 @@ "hist_csv_data": "Δεδομένα CSV", "hist_from_recorder": "Ή διαβάστε το από το Home Assistant", "hist_since": "Από", - "days": "ημέρες", "hist_keep": "Διατήρηση αυτού του κύκλου", "hist_looks_complete": "πλήρης", "peak_power_short": "Αιχμή", @@ -730,7 +646,40 @@ "zoom_hint_touch_zoomed": "ζουμ με δύο δάχτυλα · διπλό πάτημα για επαναφορά", "cycles_since_service": "{since} / {total} κύκλοι", "total_cycles_run": "κύκλοι συνολικά", - "correct_total_cycles": "Διόρθωση συνόλου" + "correct_total_cycles": "Διόρθωση συνόλου", + "hist_reason_already_recorded": "επικαλύπτει έναν κύκλο που το WashData έχει ήδη καταγράψει", + "share_consent": "Επιβεβαιώνω ότι αυτοί οι κύκλοι ολοκληρώθηκαν κανονικά, χωρίς διακοπή", + "show_internal": "Εμφάνιση εσωτερικών ρυθμίσεων", + "show_internal_tip": "Ρυθμίσεις που προσαρμόζει το WashData για εσάς (δειγματοληψία, watchdog, συχνότητα αντιστοίχισης, κατώφλια). Εμφανίζονται όταν η συσκευή έχει δική της τιμή, σύγκρουση ή πρόταση.", + "rate_n": "{n} αστέρια", + "rate_quality": "Βαθμολόγηση ποιότητας", + "pg_autolabel": "Αυτόματη επισήμανση", + "pg_label_review": "Ζητά αναθεώρηση", + "pg_ev_match_reverted": "Αντιστοίχιση αναιρέθηκε", + "pg_ev_verified_pause": "Επιβεβαιωμένη παύση", + "n_samples": "{n} δείγματα", + "n_samples_one": "{n} δείγμα", + "n_samples_other": "{n} δείγματα", + "n_cycles": "{n} κύκλοι", + "n_cycles_one": "{n} κύκλος", + "n_cycles_other": "{n} κύκλοι", + "energy_per_cycle": "{v}/κύκλο", + "energy_total": "{v} συνολικά", + "automation_off": "(ανενεργός)", + "ref_cycle_none": "Κανένας", + "share_name": "Εμφάνιση του ονόματός μου σε ό,τι κοινοποιώ", + "cat_backfill_cycles": "Κύκλοι που βρέθηκαν σε εισαγόμενο ιστορικό ισχύος", + "pg_compare_profile": "Σύγκριση με προφίλ", + "pg_obj_early": "Ποσοστό πρόωρου τέλους", + "pg_obj_endlag": "Καθυστέρηση τέλους (διάμεσος)", + "pg_obj_split": "Ποσοστό διαχωρισμού", + "pg_sweep_cycles": "Πρόσφατοι κύκλοι", + "cycle_context": "Ιστορικό καταγραφέα γύρω από τον κύκλο", + "cycle_context_off": "Ανενεργό", + "cycle_context_minutes": "{min} λεπτ.", + "cycle_context_series": "Ιστορικό καταγραφέα", + "before_start": "Πριν την έναρξη", + "after_end": "Μετά το τέλος" }, "log": { "all_levels": "Όλα τα επίπεδα", @@ -740,11 +689,9 @@ }, "ml": { "builtin_models": "● Χρήση ενσωματωμένων μοντέλων", - "fit_fair": "Αποδεκτός", "fit_good": "Καλός", "fit_slight": "Μικρός", "fit_strong": "Ισχυρός", - "fit_weak": "Αδύναμος", "fit_word": "κατάλληλος", "personalized": "● Εξατομικευμένο σε αυτό το μηχάνημα", "better_than_baseline": "{pct}% καλύτερα από τη βασική εκτίμηση ({metric})", @@ -752,21 +699,29 @@ "trend_declining_tip": "Η καταλληλότητα αυτού του μοντέλου έχει υποχωρήσει στους πρόσφατους επανελέγχους· η αξιολόγηση περισσότερων κύκλων μπορεί να το βοηθήσει να μάθει ξανά.", "trend_steady_tip": "Η καταλληλότητα αυτού του μοντέλου έχει παραμείνει σχεδόν σταθερή στους πρόσφατους επανελέγχους.", "cap_blurb": { - "end": "Να γνωρίζει πότε ένας κύκλος έχει πράγματι ολοκληρωθεί", - "live_match": "Ταχύτερη αναγνώριση του προγράμματος που εκτελείται", - "quality": "Εντοπισμός κύκλων που ανιχνεύτηκαν λανθασμένα ή αλλοιώθηκαν", - "remaining_time": "Πρόβλεψη του χρόνου που απομένει", "total_energy": "Πρόβλεψη της συνολικής ενέργειας και του κόστους" }, "cap_label": { - "end": "Ανίχνευση τέλους κύκλου", - "live_match": "Αναγνώριση προγράμματος", - "quality": "Έλεγχος ποιότητας κύκλου", - "remaining_time": "Εκτίμηση χρόνου που απομένει", "total_energy": "Εκτίμηση ενέργειας" }, - "metric_auc": "AUC {auc} σε δεδομένα ελέγχου", - "metric_mae": "σφάλμα: {model} έναντι της βάσης {baseline}" + "metric_mae": "σφάλμα: {model} έναντι της βάσης {baseline}", + "fine_tuned_at": "βελτιστοποιήθηκε στις {when}", + "last_run_not_promoted": "Τελευταίος έλεγχος ({when}): {reason}", + "never": "ποτέ", + "skip_reason": { + "fit_failed": "Η εκπαίδευση απέτυχε: {error}", + "holdout_too_small": "Πολύ λίγοι κύκλοι για να δοκιμαστεί δίκαια ένα νέο μοντέλο: για δοκιμή μπορούσαν να κρατηθούν {held_out}, ενώ χρειάζονται {min}.", + "not_better_than_incumbent": "Το νέο μοντέλο δεν ήταν καλύτερο από αυτό που χρησιμοποιείται (σφάλμα {model} έναντι {incumbent}), οπότε διατηρήθηκε το τρέχον.", + "not_better_than_naive": "Το νέο μοντέλο δεν ήταν σαφώς καλύτερο από την απλή εκτίμηση (σφάλμα {model} έναντι {naive}).", + "unknown": "Δεν μάθηκε τίποτα νέο.", + "insufficient_rows": "Δεν υπάρχουν ακόμα αρκετοί αξιοποιήσιμοι κύκλοι: μέχρι στιγμής υπάρχουν {count} καθαροί κύκλοι με ετικέτα.", + "insufficient_rows_one": "Δεν υπάρχουν ακόμα αρκετοί αξιοποιήσιμοι κύκλοι: μέχρι στιγμής υπάρχει {count} καθαρός κύκλος με ετικέτα.", + "insufficient_rows_other": "Δεν υπάρχουν ακόμα αρκετοί αξιοποιήσιμοι κύκλοι: μέχρι στιγμής υπάρχουν {count} καθαροί κύκλοι με ετικέτα." + }, + "trained_unknown": "άγνωστη ημερομηνία", + "held_out_count": "μετρήθηκε σε {count} κύκλους ελέγχου", + "held_out_count_one": "μετρήθηκε σε {count} κύκλο ελέγχου", + "held_out_count_other": "μετρήθηκε σε {count} κύκλους ελέγχου" }, "modal": { "auto_label": "Κύκλοι αυτόματης ετικέτας", @@ -775,7 +730,6 @@ "cycle": "Κύκλος", "edit_group": "Επεξεργασία ομάδας προφίλ", "edit_phase": "Επεξεργασία Φάσης", - "import_config": "Εισαγωγή διαμόρφωσης", "label_cycle": "Κύκλος ετικέτας", "merge_cycles": "Συγχώνευση {n} Κύκλων", "new_group": "Νέα ομάδα προφίλ", @@ -790,6 +744,8 @@ "delete_automation_title": "Διαγραφή αυτοματισμού", "delete_automation_msg": "Διαγραφή του αυτοματισμού \"{name}\" από το Home Assistant; Αυτό δεν μπορεί να αναιρεθεί.", "remove_legacy_title": "Αφαίρεση ενεργειών παλαιού τύπου", + "reset_phase_msg": "Αναίρεση της επεξεργασίας σας στη φάση \"{name}\" και επαναφορά της ενσωματωμένης φάσης; Τα προγράμματα διατηρούν τα εύρη τους.", + "reset_phase_title": "Επαναφορά φάσης", "remove_legacy_msg": "Αφαίρεση των προσαρμοσμένων ενεργειών παλαιού τύπου; Θα σταματήσουν να ενεργοποιούνται σε συμβάντα κύκλου. Αυτό δεν μπορεί να αναιρεθεί από τον πίνακα.", "discard_recording_title": "Απόρριψη εγγραφής", "discard_recording_msg": "Απόρριψη της αποθηκευμένης εγγραφής; Αυτό δεν μπορεί να αναιρεθεί.", @@ -810,32 +766,44 @@ "store_share_device": "Κοινοποίηση αυτής της συσκευής", "export_select": "Εξαγωγή - επιλογή δεδομένων", "import_wizard": "Εισαγωγή - επιλογή δεδομένων", - "history_import": "Εισαγωγή ιστορικού ισχύος" + "history_import": "Εισαγωγή ιστορικού ισχύος", + "apply_suggestions_title": "Εφαρμογή προτάσεων", + "remove_task_title": "Αφαίρεση εργασίας", + "remove_task_msg": "Αφαίρεση της υπενθύμισης \"{task}\"; Οι καταχωρίσεις της στο αρχείο συντήρησης διατηρούνται.", + "apply_suggestions_msg": "Αλλαγή αυτών των ρυθμίσεων ({n}); Η ενσωμάτωση επαναφορτώνεται μετά την αποθήκευση.", + "discard_settings_title": "Απόρριψη μη αποθηκευμένων αλλαγών;", + "discard_settings_msg": "Μη αποθηκευμένες αλλαγές ρυθμίσεων: {n}. Έξοδος από τις Ρυθμίσεις και απόρριψή τους;", + "profile_title": "Προφίλ · {name}", + "undo_import_msg": "Τα δεδομένα και οι ρυθμίσεις αυτής της συσκευής επιστρέφουν όπως ήταν πριν την τελευταία εισαγωγή ({when}). Οι κύκλοι που καταγράφηκαν και οι αλλαγές που έγιναν από τότε χάνονται.", + "undo_import_ok": "Αναίρεση εισαγωγής", + "undo_import_title": "Αναίρεση τελευταίας εισαγωγής" }, "msg": { "pg_canvas_empty3": "Επιλέξτε έναν κύκλο παραπάνω και πατήστε Εκτέλεση για να τον προσομοιώσετε. Στη συνέχεια τοποθετήστε τον δείκτη για ανάγνωση τιμών, κυλήστε ή κάντε ζουμ με δύο δάχτυλα και σύρετε για μετακίνηση.", + "pg_preset_delete_confirm": "Διαγραφή της προρύθμισης \"{name}\";", + "pg_preset_limit": "Συμπληρώθηκε το όριο προρυθμίσεων ({n})", + "pg_preset_overwrite": "Αντικατάσταση της προρύθμισης \"{name}\";", "tail_trim_hint": "Αφαιρέστε τόσα δευτερόλεπτα από το τέλος", - "store_sibling_hint": "Δεν υπάρχει τίποτα κοινοποιημένο για το ακριβές μοντέλο σας; Ένα στενά συγγενικό μοντέλο της ίδιας μάρκας είναι συνήθως καλή αφετηρία.", - "store_declare_appliance": "Δηλώστε στο WashData ποια συσκευή έχετε και αυτή η καρτέλα θα εμφανίσει τις διαμορφώσεις που έχουν κοινοποιήσει άλλοι για αυτήν. Μπορείτε επίσης να γράψετε μια μάρκα παραπάνω για να δείτε τον κατάλογο.", - "refresh_catalog_hint": "Οι λίστες μαρκών και συσκευών της κοινότητας αποθηκεύονται προσωρινά, ώστε το κοινοτικό κατάστημα να παραμένει μέσα στο ημερήσιο όριό του. Ανανεώστε για να εμφανιστούν καταχωρίσεις που πρόσθεσαν ή ενέκριναν άλλοι.", + "store_sibling_hint": "Τίποτα κοινοποιημένο για το μοντέλο σας; Ένα συγγενικό μοντέλο της ίδιας μάρκας είναι συνήθως καλή αρχή.", + "store_declare_appliance": "Πείτε στο WashData ποια συσκευή έχετε για να δείτε τις διαμορφώσεις που μοιράστηκαν άλλοι γι' αυτήν. Ή γράψτε μια μάρκα παραπάνω για να ρίξετε μια ματιά.", + "refresh_catalog_hint": "Οι λίστες μαρκών και συσκευών της κοινότητας αποθηκεύονται προσωρινά, για να μην ξοδεύεται το ημερήσιο όριο του καταστήματος. Ανανεώστε για να δείτε καταχωρίσεις που πρόσθεσαν ή ενέκριναν άλλοι.", "head_trim_hint": "Αφαιρέστε τόσα δευτερόλεπτα από την αρχή", "appliance_monitor": "Οθόνη συσκευής", "artifact_dip_detail": "Έπεσε κάτω από το συνηθισμένο εύρος ισχύος για ~{n}s.", "artifact_footer": "Επισημαίνεται στο παραπάνω γράφημα. Αυτά είναι παροδικά τεχνουργήματα (π.χ. η πόρτα άνοιξε στα μέσα του κύκλου), όχι απαραίτητα προβλήματα.", - "artifact_header": "{n} ανωμαλίες που εντοπίστηκαν κατά τη διάρκεια αυτού του κύκλου", - "artifact_pause_detail": "Η ισχύς έπεσε σχεδόν στο μηδέν για ~{n}s και μετά ανέκαμψε – πιθανώς η πόρτα άνοιξε στα μέσα του κύκλου ή ο κύκλος τέθηκε σε παύση.", + "artifact_header": "Εντοπίστηκαν {n} ανωμαλίες κατά τη διάρκεια αυτού του κύκλου", + "artifact_header_one": "Εντοπίστηκε {n} ανωμαλία κατά τη διάρκεια αυτού του κύκλου", + "artifact_header_other": "Εντοπίστηκαν {n} ανωμαλίες κατά τη διάρκεια αυτού του κύκλου", + "artifact_pause_detail": "Η ισχύς έπεσε σχεδόν στο μηδέν για ~{n} δευτ. και μετά επανήλθε. Πιθανώς άνοιξε η πόρτα ή ο κύκλος τέθηκε σε παύση.", "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": "Προφίλ επικάλυψης (ασθενώς)", - "compare_profiles_tip": "Επικαλύψτε άλλους φακέλους προφίλ στο παραπάνω γράφημα για να δείτε ποιος ταιριάζει καλύτερα σε αυτόν τον κύκλο.", + "compare_profiles_tip": "Επικαλύψτε άλλα προφίλ στο γράφημα για να δείτε ποιο ταιριάζει καλύτερα σε αυτόν τον κύκλο.", "compare_selected_cycles": "Επιλεγμένοι κύκλοι (συμπαγής) – εμφάνιση / απόκρυψη", - "consider_new_profile": "Σκεφτείτε να δημιουργήσετε ένα νέο προφίλ.", - "coverage_gap": "οι πρόσφατοι κύκλοι δεν έχουν αντίστοιχο προφίλ ({pct}% των τελευταίων 30).", - "coverage_gap_similar_cycles": "Βρέθηκαν {count} παρόμοιοι κύκλοι χωρίς ετικέτα – δημιουργήστε ένα προφίλ για να αρχίσετε την αντιστοίχισή τους.", "cycles_deleted": "Διαγράφηκαν {count} κύκλοι", "enough_data": "Αρκετά δεδομένα για εκμάθηση ({current}/{min} κύκλοι).", "export_description": "Επιλέξτε ακριβώς ποια προφίλ, κύκλους, ρυθμίσεις και άλλα θα εξαχθούν σε JSON, ή αναλύστε ένα αρχείο και εισάγετε μόνο τα μέρη που θέλετε.", @@ -843,30 +811,26 @@ "feedback_prompt": "Επιβεβαιώστε ότι ήταν σωστό, διορθώστε το πρόγραμμα ή αγνοήστε.", "feedback_relabel_hint": "Η επανεπισήμανση αυτού του κύκλου τον επιλύει επίσης.", "filter_by_profile": "Φιλτράρισμα κατά προφίλ…", - "group_modal_help": "Ομαδοποιήστε προγράμματα με το ίδιο σχήμα που διαφέρουν σε θερμοκρασία/περιστροφή (οι διάρκειες μπορεί να διαφέρουν). Η αντιστοίχιση βαθμολογεί την ομάδα ως έναν υποψήφιο και, στη συνέχεια, επιλέγει το μέλος που ταιριάζει καλύτερα. Διαλέξτε τουλάχιστον 2. η επικάλυψη δείχνει πόσο μοιάζουν.", + "group_modal_help": "Ομαδοποιήστε προγράμματα με το ίδιο σχήμα που διαφέρουν σε θερμοκρασία ή στύψιμο. Η αντιστοίχιση βαθμολογεί την ομάδα σαν ένα πρόγραμμα και μετά διαλέγει το μέλος που ταιριάζει καλύτερα. Διαλέξτε τουλάχιστον 2· η επικάλυψη δείχνει πόσο μοιάζουν.", "group_not_cohesive": "Αυτά τα προφίλ δεν είναι αρκετά παρόμοια ώστε να ομαδοποιούνται αξιόπιστα, επομένως η αντιστοίχιση τα αντιμετωπίζει μεμονωμένα μέχρι να αφαιρέσετε το ακραίο στοιχείο ή να χωρίσετε την ομάδα.", "group_preview_hint": "Επιλέξτε 2+ μέλη για προεπισκόπηση και σύγκριση των καμπυλών ισχύος τους.", - "import_intro": "Φορτώστε ένα εξαγόμενο αρχείο ή επικολλήστε ένα ωφέλιμο φορτίο JSON παρακάτω.", - "legacy_actions_title": "{count} προσαρμοσμένη ενέργεια παλαιού τύπου εξακολουθεί να εκτελείται", + "legacy_actions_title": "{count} προσαρμοσμένες ενέργειες παλαιού τύπου εξακολουθούν να εκτελούνται", + "legacy_actions_title_one": "{count} προσαρμοσμένη ενέργεια παλαιού τύπου εξακολουθεί να εκτελείται", + "legacy_actions_title_other": "{count} προσαρμοσμένες ενέργειες παλαιού τύπου εξακολουθούν να εκτελούνται", "live_chart_loading": "Το γράφημα ζωντανής ισχύος εμφανίζεται καθώς φτάνουν οι ενδείξεις.", "loading": "Φόρτωση…", "loading_curve": "Φόρτωση καμπύλης…", "loading_settings": "Φόρτωση ρυθμίσεων…", "log_buffer_hint": "Πρώτα τα νεότερα · αποθηκεύει προσωρινά τις τελευταίες 500 εγγραφές ha_washdata από την επανεκκίνηση · σύρετε το κάτω άκρο για αλλαγή μεγέθους.", - "maintenance_advisory": "Η αύξηση της διάρκειας/ενέργειας μπορεί να υποδηλώνει ότι απαιτείται συντήρηση της συσκευής (π.χ. αφαλάτωση, καθαρισμός φίλτρου).", "maintenance_due": "Οφειλόμενη συντήρηση: {items}", - "maintenance_intro": "Καταγράψτε το σέρβις που πραγματοποιείτε σε αυτή τη συσκευή και λάβετε υπενθύμιση όταν κάθε εργασία οφείλεται ξανά.", + "maintenance_intro": "Καταγράψτε το σέρβις αυτής της συσκευής και λάβετε υπενθύμιση όταν κάθε εργασία οφείλεται ξανά.", "maintenance_load_error": "Δεν ήταν δυνατή η φόρτωση των δεδομένων συντήρησης: {error}", "maintenance_requires_access": "Η συντήρηση και η εξαγωγή/εισαγωγή απαιτούν πλήρη πρόσβαση.", - "manual_duration_hint": "Η μέση/αναμενόμενη διάρκεια κύκλου του προφίλ, που χρησιμοποιείται για εκτιμήσεις του υπολειπόμενου χρόνου. Επεξεργαστείτε για να το ορίσετε. αφήνοντάς το αμετάβλητο διατηρεί την τρέχουσα τιμή.", - "matching_tuning_intro": "Κατά την εκμάθηση, το WashData προσαρμόζει επίσης το βάρος της αντιστοίχισης προγράμματος σε σχέση με τη διάρκεια και την ενέργεια.", + "manual_duration_hint": "Η αναμενόμενη διάρκεια κύκλου του προφίλ, για τις εκτιμήσεις υπολειπόμενου χρόνου. Αφήστε την ως έχει για να κρατήσετε την τρέχουσα τιμή.", "merge_intro": "Οι επιλεγμένοι κύκλοι συνδυάζονται σε έναν (χρονολογική σειρά, κενά γεμάτα με 0 W). Επιλέξτε το προφίλ που προκύπτει.", "ml_intro": "Το WashData συνοδεύεται από έξυπνα μοντέλα που λειτουργούν εκτός συσκευασίας.", "ml_learned_intro": "Μοντέλα προσαρμοσμένα σε αυτό το μηχάνημα.", - "ml_loading": "φόρτωση ML…", "ml_settings_intro": "Δύο ανεξάρτητοι διακόπτες: ο ένας εφαρμόζει τα μοντέλα ενώ εκτελείται ένας κύκλος, ο άλλος επιτρέπει στην WashData να τα ρυθμίσει με ακρίβεια στο μηχάνημά σας με την πάροδο του χρόνου.", - "name_first_program": "Έχετε αρκετούς κύκλους – ονομάστε το πρώτο σας πρόγραμμα για να ξεκινήσει η αντιστοίχιση.", - "near_duplicate_cluster": "Εντοπίστηκε σχεδόν διπλό σύμπλεγμα προφίλ. Η ομαδοποίηση επιτρέπει την αξιόπιστη επιλογή αντιστοίχισης μεταξύ ομοίων (π.χ. ίδιο πρόγραμμα σε διαφορετική θερμοκρασία/περιστροφή).", "no_cycles_match": "Κανένας κύκλος δεν ταιριάζει με το τρέχον φίλτρο.", "no_cycles_profile": "Δεν υπάρχουν κύκλοι για αυτό το προφίλ.", "no_cycles_selected": "Επιλέξτε πρώτα τουλάχιστον έναν κύκλο.", @@ -891,73 +855,52 @@ "no_suggestions": "Δεν υπάρχουν ενεργές προτάσεις.", "notify_services_hint": "Χρησιμοποιήστε αναγνωριστικά υπηρεσιών {entity} (χωρισμένα με κόμμα για πολλαπλά). Μεταβλητές προτύπου: {vars}.", "old_actions_warning": "Διαμορφώθηκε με το παλιό πρόγραμμα επεξεργασίας ενεργειών (τώρα καταργήθηκε). Εξακολουθούν να ενεργοποιούνται σε συμβάντα κύκλου, αλλά δεν είναι πλέον δυνατή η επεξεργασία τους εδώ. Μετατρέψτε τα σε κανονικό αυτοματισμό ή αφαιρέστε τα.", - "onboarding_progress": "{n} / 3 κύκλοι παρατηρήθηκαν", - "onboarding_watching": "Αφήστε τη συσκευή σας να λειτουργεί κανονικά – το WashData παρακολουθεί. Μετά από 3 κύκλους, θα ξεκινήσει η αντιστοίχιση προγραμμάτων.", "pending_feedback": "Εκκρεμεί σχόλια ανίχνευσης", "performance_trend": "Τάση απόδοσης ({n} κύκλοι)", - "pg_analysis_empty": "Φορτώστε έναν κύκλο για να δείτε την ανάλυση αντιστοίχισης.", "pg_apply_confirm": "Εφαρμογή της καλύτερης τιμής: {label} = {value};", - "pg_canvas_empty": "Επιλέξτε έναν κύκλο παραπάνω και, στη συνέχεια, πατήστε Φόρτωση για να δείτε την καμπύλη ισχύος του.", - "pg_chart_caption": "Ποσοστό εντοπισμού/αντιστοίχισης ανά τιμές του {param}", "pg_ctrl_intro": "Οι τιμές ξεκινούν από τις ενεργές ρυθμίσεις ενσωμάτωσης αυτής της συσκευής. Οι αλλαγές παραμένουν στον Χώρο δοκιμών μέχρι να τις εφαρμόσετε.", "pg_live_unavailable": "Οι ενεργές ρυθμίσεις δεν είναι διαθέσιμες, εμφανίζονται οι προεπιλογές.", "pg_load_live_confirm": "Να απορριφθούν οι αλλαγές του Χώρου δοκιμών ({n}) και να επαναφορτωθούν οι τρέχουσες ρυθμίσεις της ενσωμάτωσης;", - "pg_match_rate": "{pct}% ποσοστό αντιστοίχισης σε {total} κύκλους", "pg_matches_live": "Ταυτίζεται με τις ενεργές ρυθμίσεις", "pg_n_changed": "Διαφορές από τις ενεργές ρυθμίσεις: {n}", "pg_sugg_none": "Όλες οι προτάσεις αντιστοιχούν ήδη στις τρέχουσες ρυθμίσεις", - "pg_preset_delete_confirm": "Διαγραφή της προρύθμισης \"{name}\";", - "pg_preset_limit": "Συμπληρώθηκε το όριο προρυθμίσεων ({n})", - "pg_preset_overwrite": "Αντικατάσταση της προρύθμισης \"{name}\";", "pg_publish_one_confirm": "Αποθήκευση {label} = {value} στις ρυθμίσεις αυτής της συσκευής;", "pg_restart_note": "Επανεκκινήστε το Home Assistant για να ενεργοποιήσετε τα εργαλεία προσομοίωσης.", - "pg_sim_progress": "{done} / {total} κύκλοι", - "pg_starting": "Εκκίνηση…", - "pg_stress_synthetic": "η προσομοίωση αδράνειας ξεκινά εδώ", - "pg_sweep_intro": "Τι θα γινόταν αν το {param} ήταν διαφορετικό; Δοκιμάστε {steps} τιμές στους τελευταίους {cycles} κύκλους για να βρείτε τη ρύθμιση όπου αντιστοιχίζονται σωστά οι περισσότεροι κύκλοι.", - "pg_sweep_step": "Βήμα {done} / {total}", - "pg_verdict_bad": "Χρειάζεται προσοχή: πολλοί κύκλοι δεν εντοπίζονται.", - "pg_verdict_good": "Καλά ρυθμισμένο: οι περισσότεροι κύκλοι αναγνωρίζονται και αντιστοιχίζονται σωστά.", - "pg_verdict_ok": "Αποδεκτό: ορισμένοι κύκλοι χάθηκαν. Δοκιμάστε να μειώσετε το κατώφλι εκκίνησης.", "phase_catalog_intro": "Ονομασμένα τμήματα ενός κύκλου (Πρόπλυση, Θέρμανση, Στύψιμο…). Αντιστοιχίστε τα σε ένα προφίλ από τον πίνακα ελέγχου του.", "phase_ranges_intro": "Εύρος φάσεων (λεπτά από την έναρξη του κύκλου) που επικαλύπτονται στη μέση καμπύλη. Επεξεργαστείτε τις τιμές για ζωντανή προεπισκόπηση.", - "playground_intro": "Ένας ασφαλής χώρος δοκιμών για την αναπαραγωγή παλαιότερων κύκλων με διαφορετικές ρυθμίσεις, τη σύγκριση δύο διαμορφώσεων δίπλα δίπλα και την εξέταση του πώς ένας κύκλος ευθυγραμμίζεται με ένα προφίλ. Τίποτα εδώ δεν αλλάζει τα ενεργά σας δεδομένα μέχρι να το εφαρμόσετε ρητά.", - "playground_needs_restart": "Επανεκκινήστε το Home Assistant για να ενεργοποιήσετε τα εργαλεία του Χώρου δοκιμών.", - "preferences_admin": "Προτιμήσεις, πίνακας & έλεγχος πρόσβασης", - "preferences_adv": "Προτιμήσεις", + "playground_intro": "Δοκιμάστε ρυθμίσεις στα πραγματικά δεδομένα των κύκλων σας. Τίποτα εδώ δεν αλλάζει τις ενεργές ρυθμίσεις σας μέχρι να το εφαρμόσετε.", "prefs_personal": "Αυτά ισχύουν μόνο για τον λογαριασμό σας Home Assistant.", - "process_history_hint": "Εκτέλεση αντιστοίχισης σε όλους τους αποθηκευμένους κύκλους, ανανέωση προτάσεων ρύθμισης, επανεκπαίδευση μοντέλων ML (αν είναι ενεργοποιημένα), επανυπολογισμός κόστους των κύκλων με βάση τις καταγεγραμμένες τιμές ενέργειας και επανυπολογισμός υγείας κύκλων. Εκτελέστε το μετά από μια δέσμη αξιολογήσεων.", + "process_history_hint": "Ξανατρέχει την αντιστοίχιση σε όλους τους αποθηκευμένους κύκλους, ανανεώνει τις προτάσεις, επανεκπαιδεύει τα μοντέλα ML (αν είναι ενεργά), επανυπολογίζει το κόστος των κύκλων από τις καταγεγραμμένες τιμές και την υγεία των κύκλων. Εκτελέστε το μετά από μια σειρά αξιολογήσεων.", "profile_deleted": "Το προφίλ διαγράφηκε", - "profile_poor_health_detail": "Οι κύκλοι που έχουν εκχωρηθεί σε αυτό το προφίλ έχουν ασυνεπή σχήματα ή χαμηλή εμπιστοσύνη. Εξετάστε το ενδεχόμενο να φτιάξετε ξανά το φάκελο ή να αναθεωρήσετε τους κύκλους με ετικέτα.", "profiles_intro": "Κάντε κλικ σε ένα προφίλ για στατιστικά στοιχεία, φάσεις και εκκαθάριση. Ομαδοποιήστε σχεδόν πανομοιότυπα προγράμματα (ίδιο σχήμα/διάρκεια, διαφορετική θερμοκρασία ή στύψιμο) ώστε να ταιριάζουν αξιόπιστα οι επιλογές μεταξύ τους.", - "rbac_hint": "Όταν είναι απενεργοποιημένο, κάθε χρήστης του Home Assistant έχει πλήρη πρόσβαση (η προεπιλογή). Οι διαχειριστές έχουν πάντα πλήρη πρόσβαση και μπορούν να διαχειρίζονται όλους.", - "recent_logs": "Πρόσφατες εγγραφές ha_washdata", + "rbac_hint": "Ανενεργό: κάθε χρήστης του Home Assistant έχει πλήρη πρόσβαση (προεπιλογή). Οι διαχειριστές έχουν πάντα πλήρη πρόσβαση και διαχειρίζονται όλους.", "recording_in_progress": "Η εγγραφή σε εξέλιξη", "reminders_intro": "Εμφάνιση υπενθύμισης στον πίνακα τόσους κύκλους μετά το τελευταίο σέρβις. Αφήστε κενό ή 0 για απενεργοποίηση μιας υπενθύμισης.", "restart_gap_footer": "Επισημασμένο στο γράφημα. Τα δεδομένα ισχύος λείπουν για αυτά τα διαστήματα· η αντιστοίχιση χρησιμοποίησε μόνο πραγματικές μετρήσεις.", - "restart_gap_header": "{n} κενό επανεκκίνησης HA κατά τη διάρκεια αυτού του κύκλου", + "restart_gap_header": "{n} κενά επανεκκίνησης HA κατά τη διάρκεια αυτού του κύκλου", + "restart_gap_header_one": "{n} κενό επανεκκίνησης HA κατά τη διάρκεια αυτού του κύκλου", + "restart_gap_header_other": "{n} κενά επανεκκίνησης HA κατά τη διάρκεια αυτού του κύκλου", "restart_gap_item": "{dur} κενό", - "review_confirm_help": "Επιβεβαιώστε εάν αυτός ο κύκλος εντοπίστηκε σωστά. Οι κριτικές σας εκπαιδεύουν το μοντέλο στο μηχάνημά σας - όσο περισσότερους κύκλους επιβεβαιώσετε, τόσο καλύτερη αντιστοίχιση και βαθμολογία υγείας. Ένα γρήγορο Καλό/Κακό είναι αρκετό.", + "review_confirm_help": "Ανιχνεύθηκε σωστά αυτός ο κύκλος; Οι επιβεβαιώσεις βελτιώνουν την αντιστοίχιση για το μηχάνημά σας. Αρκεί ένα γρήγορο Καλός/Κακός.", "review_in_settings": "Έλεγχος στις Ρυθμίσεις", "review_notes_placeholder": "Σημειώσεις (προαιρετικό)", "review_notes_tip": "Σημειώσεις ελεύθερου κειμένου για δική σας αναφορά. Δεν χρησιμοποιείται με ταίριασμα ή προπόνηση.", - "review_profile_tip": "Το πρόγραμμα αυτού του κύκλου χαρακτηρίζεται ως. Εάν το πρόγραμμα που εντοπίστηκε αυτόματα ήταν λάθος, διορθώστε το εδώ - η επισήμανση διδάσκει αντιστοίχιση για μελλοντικούς κύκλους.", - "review_quality_tip": "Πόσο καθαρός είναι αυτός ο κύκλος. Καλό = παράδειγμα βιβλίου αυτού του προγράμματος. Κακό = ανιχνεύθηκε αλλά θορυβώδες ή άτυπο. Μη χρησιμοποιήσιμο = εσφαλμένα εντοπισμένο (συγχωνευμένο, περικομμένο ή ψευδές). Οδηγεί το δείκτη υγείας και ποιοι κύκλοι επιτρέπονται για την εκπαίδευση του μοντέλου.", - "review_recorded_tip": "Επισημάνετε αυτό ως έναν επιλεγμένο από το χέρι κύκλο αναφοράς για το πρόγραμμά του - τον ίδιο ρόλο με έναν μη αυτόματα εγγεγραμμένο κύκλο. Οι κύκλοι αναφοράς διατηρούνται πάντα, εισάγονται το αντίστοιχο πρότυπο και δεν απορρίπτονται ποτέ με εκκαθάριση. (Αυτή είναι η \"χρυσή\"/ηχογραφημένη σημαία· και τα δύο είναι το ίδιο πράγμα.)", - "review_tags_tip": "Προαιρετικές σημαίες που περιγράφουν τι πήγε στραβά με αυτόν τον κύκλο, οπότε η εκπαίδευση και ο καθαρισμός μπορούν να το εξηγήσουν.", + "review_profile_tip": "Το πρόγραμμα με το οποίο έχει επισημανθεί αυτός ο κύκλος. Αν είναι λάθος, διορθώστε το εδώ· οι ετικέτες διδάσκουν την αντιστοίχιση.", + "review_quality_tip": "Πόσο καθαρός είναι αυτός ο κύκλος. Καλός: κλασική εκτέλεση αυτού του προγράμματος. Κακός: ανιχνεύθηκε, αλλά με θόρυβο ή ασυνήθιστος. Αχρησιμοποίητο: λάθος ανίχνευση (συγχωνευμένος, κομμένος ή ψευδής). Επηρεάζει τη βαθμολογία υγείας και ποιοι κύκλοι εκπαιδεύουν το μοντέλο.", + "review_recorded_tip": "Σημειώστε τον ως επιλεγμένο κύκλο αναφοράς για το πρόγραμμά του, όπως έναν χειροκίνητα καταγεγραμμένο κύκλο. Οι κύκλοι αναφοράς διατηρούνται πάντα, θεμελιώνουν το πρότυπο αντιστοίχισης και δεν αφαιρούνται ποτέ από την εκκαθάριση.", + "review_tags_tip": "Προαιρετικές σημάνσεις για το τι πήγε στραβά, ώστε η εκπαίδευση και η εκκαθάριση να το λαμβάνουν υπόψη.", "review_to_cycles": "Ανοίξτε την ουρά ελέγχου Κύκλων", "samples_decimated": "Εμφάνιση {shown} από {total} δείγματα (αραιωμένα για προβολή· διατηρούνται οι αιχμές). Ένα μεγάλο κενό εδώ είναι αραίωση, όχι δεδομένα που λείπουν.", - "saving_triggers_reload": "Η αποθήκευση ενεργοποιεί μια εκ νέου φόρτωση ενσωμάτωσης. Οι οντότητες HA ενδέχεται να εμφανίζονται για λίγο ως μη διαθέσιμες.", + "saving_triggers_reload": "Η αποθήκευση επαναφορτώνει την ενσωμάτωση. Οι οντότητες μπορεί για λίγο να εμφανίζονται μη διαθέσιμες.", "search_placeholder": "Ρυθμίσεις αναζήτησης…", "see_recorder": "Δείτε παρακάτω το γραφικό στοιχείο εγγραφής", "setting_changed": "Άλλαξε από {old} σε {new} στις {date}", "settings_basic_note": "Εμφανίζονται οι βασικές ρυθμίσεις. Μεταβείτε στο Για προχωρημένους για την πλήρη λίστα.", - "shape_drift_advisory": "⚠ Απόκλιση σχήματος", - "shape_drift_detail": "Το μοτίβο ισχύος αυτού του προφίλ έχει αλλάξει με την πάροδο του χρόνου – πιθανή φθορά της συσκευής ή απαιτείται συντήρηση (π.χ. αφαλάτωση, καθαρισμός φίλτρου).", "show_all_settings": "Εμφάνιση όλων των ρυθμίσεων", - "showing_suggestions": "Εμφάνιση ρύθμισης {count} με προτάσεις.", - "split_intro": "Κάντε κλικ στο γράφημα για να προσθέσετε ή να αφαιρέσετε ένα σημείο διαίρεσης ή για αυτόματη ανίχνευση με κενά αδράνειας. Κάθε τμήμα που προκύπτει μπορεί να αποκτήσει το δικό του προφίλ.", - "storage_diagnostics": "Στατιστικά αποθήκευσης, συντήρηση, εξαγωγές/εισαγωγές", + "showing_suggestions": "Εμφάνιση {count} ρυθμίσεων με προτάσεις.", + "showing_suggestions_one": "Εμφάνιση {count} ρύθμισης με προτάσεις.", + "showing_suggestions_other": "Εμφάνιση {count} ρυθμίσεων με προτάσεις.", + "split_intro": "Κάντε κλικ στο γράφημα για να προσθέσετε ή να αφαιρέσετε σημείο διαίρεσης, ή εντοπίστε τα αυτόματα από τα κενά αδράνειας. Κάθε τμήμα μπορεί να πάρει δικό του προφίλ.", "sug_staged": "Ορίστηκε {key} = {val}. Αποθηκεύστε για εφαρμογή.", "toast_auto_detect_enabled": "Αυτόματη ανίχνευση ενεργοποιήθηκε", "toast_auto_label_complete": "Αυτόματη επισήμανση ολοκληρώθηκε", @@ -974,7 +917,6 @@ "toast_no_automation": "Δεν είναι δυνατή η δημιουργία αυτοματισμού εδώ", "toast_profile_name_required": "Απαιτείται όνομα προφίλ", "toast_program_set": "Πρόγραμμα ορίστηκε: {program}", - "toast_rebuild_failed": "Η ανακατασκευή απέτυχε: {error}", "toast_resume_failed": "Η επανέναρξη απέτυχε: {error}", "toast_revert_failed": "Η επαναφορά απέτυχε: {error}", "toast_reverted": "Έγινε επαναφορά: {key}", @@ -985,97 +927,84 @@ "trend_energy_up": "Ανοδικές τάσεις ενέργειας ({pct}%/κύκλος) – πρόσφατος μέσος όρος {avg}", "trim_destructive_confirm": "Αυτό διατηρεί μόνο το {pct}% του κύκλου και δεν μπορεί να αναιρεθεί. Συνέχεια;", "trim_intro": "Σύρετε τις κόκκινες λαβές ή εισαγάγετε τιμές. Όλα έξω από το παράθυρο αφαιρούνται.", - "tuning_suggestions_available": "{count} πρόταση συντονισμού διαθέσιμη από παρατηρούμενους κύκλους. Εμφανίζονται δίπλα στα σχετικά πεδία.", + "tuning_suggestions_available": "{count} προτάσεις συντονισμού διαθέσιμες από παρατηρούμενους κύκλους. Εμφανίζονται δίπλα στα σχετικά πεδία.", + "tuning_suggestions_available_one": "{count} πρόταση συντονισμού διαθέσιμη από παρατηρούμενους κύκλους. Εμφανίζεται δίπλα στο σχετικό πεδίο.", + "tuning_suggestions_available_other": "{count} προτάσεις συντονισμού διαθέσιμες από παρατηρούμενους κύκλους. Εμφανίζονται δίπλα στα σχετικά πεδία.", "unsure_detected_prefix": "Το WashData δεν είναι βέβαιο ότι εντόπισε", - "updated": "Ενημερώθηκε", "warmup_badge": "Συνεχίζει να μαθαίνει ({done}/{needed} κύκλοι)", - "warmup_detail": "Αυτό το προφίλ χρειάζεται {needed} επισημασμένους κύκλους πριν ξεκινήσει η αυτόματη αντιστοίχιση. Κάθε επιβεβαιωμένος κύκλος βοηθά στη μάθηση.", + "warmup_detail": "Η αντιστοίχιση λειτουργεί ήδη. Το WashData σας ζητά να επιβεβαιώσετε τους πρώτους {needed} κύκλους αυτού του προφίλ πριν αρχίσει να τους επισημαίνει μόνο του.", "wipe_history_warning": "Διαγράψτε οριστικά όλους τους κύκλους και τα προφίλ. Δεν είναι δυνατή η αναίρεση.", "compare_overlay_tip": "Επικαλύψτε τους εκμαθημένους φακέλους προφίλ για να δείτε με ποιο πρόγραμμα μοιάζει κάθε κύκλος.", - "duration_trend_up_tip": "Αύξηση διάρκειας ({pct}%/κύκλο)", - "duration_trend_down_tip": "Μείωση διάρκειας ({pct}%/κύκλο)", - "energy_trend_up_tip": "Αύξηση ενέργειας ({pct}%/κύκλο)", - "energy_trend_down_tip": "Μείωση ενέργειας ({pct}%/κύκλο)", - "performance_trending": "Τάση απόδοσης", - "pg_select_cycle_hint": "Επιλέξτε έναν κύκλο και κάντε κλικ στο ↺ για φόρτωση", - "pg_sweep_progress": "Βήμα {done} / {total}", - "pg_analysis_hint": "Επιλέξτε έναν κύκλο και κάντε κλικ στο ↺ για φόρτωση της ανάλυσης.", "diagnostics_load_failed": "Δεν ήταν δυνατή η φόρτωση των διαγνωστικών: {error}", - "manual_duration_ref_hint": "Χρησιμοποιείται μόνο όταν δεν έχει επιλεγεί κύκλος αναφοράς. Ένας κύκλος αναφοράς ορίζει τη διάρκεια από το δικό του μήκος.", + "manual_duration_ref_hint": "Χρησιμοποιείται μόνο χωρίς κύκλο αναφοράς· ένας κύκλος αναφοράς ορίζει τη διάρκεια από το δικό του μήκος.", + "duration_learned_hint": "Υπολογίζεται από τους κύκλους αυτού του προγράμματος (χωρίς τους ακραίους) και ενημερώνεται μετά από κάθε νέο. Για να την αλλάξετε, περικόψτε ή αφαιρέστε τους κύκλους που αποκλίνουν.", "toast_convert_rolled_back": "Η μετατροπή απέτυχε και αναιρέθηκε (δεν έμεινε κανένας αυτοματισμός): {error}", "toast_convert_orphan": "Ο αυτοματισμός δημιουργήθηκε, αλλά η εκκαθάριση των παλιών ενεργειών απέτυχε. Μην προσπαθήσετε ξανά· αφαιρέστε τις παλιές ενέργειες χειροκίνητα για να αποφύγετε έναν διπλότυπο αυτοματισμό.", - "advisory_duration_trend_up": "Οι κύκλοι '{name}' διαρκούν όλο και περισσότερο (περίπου +{pct}% ανά κύκλο). Αν άλλαξε η συμπεριφορά της συσκευής, καταγράψτε ξανά ή ανακατασκευάστε αυτό το προφίλ.", - "advisory_energy_trend_up": "Το '{name}' καταναλώνει όλο και περισσότερη ενέργεια (περίπου +{pct}% ανά κύκλο) - αξίζει να ελέγξετε τη συσκευή αν αυτό είναι απροσδόκητο.", - "advisory_poor_health": "Το '{name}' έχει χαμηλή βαθμολογία ταιριάσματος - οι πρόσφατοι κύκλοι του διαφέρουν πολύ ή ταιριάζουν ασθενώς. Ελέγξτε τους κύκλους του ή καταγράψτε ξανά το προφίλ ώστε η αναγνώριση και οι εκτιμήσεις χρόνου να παραμείνουν ακριβείς.", - "advisory_shape_drift": "Το '{name}' έχει αποκλίνει σημαντικά από την αρχική του καμπύλη ισχύος. Η συμπεριφορά της συσκευής μπορεί να έχει αλλάξει με την πάροδο του χρόνου (π.χ. άλατα, φθορά). Εξετάστε το ενδεχόμενο νέας καταγραφής αυτού του προφίλ με πρόσφατους κύκλους.", - "advisory_shape_drift_corr": "Το '{name}' έχει αποκλίνει σημαντικά από την αρχική του καμπύλη ισχύος (συσχέτιση {corr}). Η συμπεριφορά της συσκευής μπορεί να έχει αλλάξει με την πάροδο του χρόνου (π.χ. άλατα, φθορά). Εξετάστε το ενδεχόμενο νέας καταγραφής αυτού του προφίλ με πρόσφατους κύκλους.", - "advisory_unmatchable": "Το '{name}' δεν μπορεί ποτέ να αντιστοιχιστεί: δεν υπάρχει πίσω του κανένας κύκλος με δεδομένα ισχύος, επομένως το WashData δεν έχει με τι να συγκρίνει τον κύκλο που εκτελείται. Επισημάνετε έναν κύκλο αυτού του προγράμματος (ή εγγράψτε έναν) για να επανέλθει στην αντιστοίχιση.", - "advisory_unmatchable_no_duration": "Το '{name}' δεν μπορεί ποτέ να αντιστοιχιστεί: έχει έναν κύκλο με δεδομένα ισχύος αλλά χωρίς αξιοποιήσιμη διάρκεια, επομένως το WashData δεν έχει καμία διάρκεια αναφοράς πάνω στην οποία να στηρίξει μια αντιστοίχιση. Καταγράψτε ξανά αυτό το πρόγραμμα ή ορίστε την αναμενόμενη διάρκειά του, για να επανέλθει στην αντιστοίχιση.", + "advisory_duration_trend_up": "Οι κύκλοι '{name}' διαρκούν όλο και περισσότερο (περίπου +{pct}% ανά κύκλο). Αν άλλαξε η συσκευή, καταγράψτε ξανά ή ανακατασκευάστε αυτό το προφίλ.", + "advisory_energy_trend_up": "Το '{name}' καταναλώνει περισσότερη ενέργεια σε κάθε κύκλο (περίπου +{pct}% ανά κύκλο). Ελέγξτε τη συσκευή αν αυτό είναι απροσδόκητο.", + "advisory_poor_health": "Το '{name}' ταιριάζει άσχημα: οι πρόσφατοι κύκλοι του διαφέρουν πολύ ή ταιριάζουν ασθενώς. Ελέγξτε τους κύκλους του ή καταγράψτε το ξανά, ώστε η αντιστοίχιση και οι εκτιμήσεις να μένουν ακριβείς.", + "advisory_shape_drift": "Το '{name}' έχει απομακρυνθεί από την αρχική καμπύλη ισχύος του, ίσως λόγω αλάτων ή φθοράς. Σκεφτείτε να το καταγράψετε ξανά με πρόσφατους κύκλους.", + "advisory_shape_drift_corr": "Το '{name}' έχει απομακρυνθεί από την αρχική καμπύλη ισχύος του (συσχέτιση {corr}), ίσως λόγω αλάτων ή φθοράς. Σκεφτείτε να το καταγράψετε ξανά με πρόσφατους κύκλους.", + "advisory_unmatchable": "Το '{name}' δεν μπορεί ποτέ να αντιστοιχιστεί: δεν υπάρχει πίσω του κύκλος με δεδομένα ισχύος. Επισημάνετε ή καταγράψτε έναν κύκλο αυτού του προγράμματος για να διορθωθεί.", + "advisory_unmatchable_no_duration": "Το '{name}' δεν μπορεί ποτέ να αντιστοιχιστεί: ο κύκλος του δεν έχει αξιοποιήσιμη διάρκεια. Καταγράψτε ξανά αυτό το πρόγραμμα ή ορίστε την αναμενόμενη διάρκειά του.", "pg_no_alerts": "Δεν εντοπίστηκαν προβλήματα σε αυτή την εκτέλεση.", "pg_override_active": "Χρησιμοποιούνται οι επεξεργασμένες ρυθμίσεις σας έναντι των τρεχουσών", "pg_history_empty": "Πατήστε Εκτέλεση για να αναπαράγετε τους κύκλους σας.", "pg_history_summary": "{detected}/{total} ανιχνεύθηκαν · {correct} αντιστοιχίστηκαν σωστά", "pg_sweep_no_metric": "Δεν υπάρχουν αρκετά δεδομένα για τη βαθμολόγηση αυτού του στόχου.", "pg_sweep_intro2": "Βρείτε τη ρύθμιση που ικανοποιεί καλύτερα έναν στόχο στους πρόσφατους κύκλους σας. Τίποτα δεν αλλάζει μέχρι να την εφαρμόσετε.", - "pg_running_history": "Αναπαραγωγή των κύκλων σας…", - "pg_running_sweep": "Δοκιμή τιμών σε όλους τους κύκλους σας…", "pg_row_load_hint": "Φόρτωση αυτού του κύκλου στο παραπάνω γράφημα", - "pg_history_intro2": "Αναπαραγάγετε τους πρόσφατους κύκλους σας μέσω του πραγματικού ανιχνευτή και μηχανισμού αντιστοίχισης με τις παραπάνω ρυθμίσεις. Κάντε κλικ σε οποιαδήποτε γραμμή για να φορτώσετε αυτόν τον κύκλο στο γράφημα. Επεξεργαστείτε μια ρύθμιση για να δείτε μια σύγκριση πριν/μετά.", + "pg_history_intro2": "Αναπαράγετε πρόσφατους κύκλους μέσα από τον πραγματικό ανιχνευτή και μηχανισμό αντιστοίχισης με τις παραπάνω ρυθμίσεις. Κάντε κλικ σε μια γραμμή για να τη φορτώσετε στο γράφημα· αλλάξτε μια ρύθμιση για να συγκρίνετε πριν και μετά.", "pg_simulating": "Προσομοίωση κύκλου…", - "pg_canvas_empty2": "Επιλέξτε έναν κύκλο παραπάνω και πατήστε Εκτέλεση για να τον προσομοιώσετε. Στη συνέχεια τοποθετήστε τον δείκτη για ανάγνωση τιμών, κυλήστε για ζουμ και σύρετε για μετακίνηση.", "pg_apply_settings_confirm": "Αποθήκευση αυτών των {n} ρυθμίσεων σε αυτήν τη συσκευή; {list}", - "pg_stress_terminated_detail": "Σταθεροποιήθηκε σε περίπου {idle} W αδράνειας -- ο κύκλος τερμάτισε {h} ώρ. {m} λεπ. αργότερα μέσω {reason}.", - "pg_stress_above_threshold_detail": "Η κατανάλωση αδράνειας (~{idle} W) είναι ίση ή μεγαλύτερη από το πραγματικό κατώφλι διακοπής ({stop} W) -- ο κύκλος δεν καταγράφηκε ποτέ ως ήσυχος. Για διόρθωση, αυξήστε το stop_threshold_w.", - "pg_stress_hit_cap_detail": "Ο κύκλος έτρεξε {h} ώρ. {m} λεπ. χωρίς να σταματήσει -- διακόπηκε αναγκαστικά από το όριο ασφαλείας. Κατανάλωση αδράνειας {idle} W έναντι κατωφλίου διακοπής {stop} W.", "no_logs_match": "Καμία εγγραφή καταγραφής δεν ταιριάζει με αυτά τα φίλτρα.", "pg_analysis_hint2": "Επιλέξτε έναν κύκλο και πατήστε Εκτέλεση για να φορτώσετε την ανάλυση αντιστοίχισης.", - "pg_analysis_empty2": "Πατήστε Εκτέλεση για να δείτε την ανάλυση αντιστοίχισης.", - "imported_readonly": "Εισαγωγή από το κοινοτικό κατάστημα. Εμφανίζεται για αναφορά και αντιστοίχιση. Δεν προσμετράται στα στατιστικά σας και δεν μπορεί να επεξεργαστεί.", + "imported_readonly": "Εισαγωγή από το κοινοτικό κατάστημα. Χρησιμοποιείται για αναφορά και αντιστοίχιση, δεν μετρά στα στατιστικά σας και δεν μπορεί να τροποποιηθεί.", "merge_no_imports": "Οι εισηγμένες καταγραφές δεν μπορούν να συγχωνευθούν σε πραγματικό κύκλο. Αποεπιλέξτε τες για συγχώνευση.", - "store_enable_hint": "Ενεργοποιήστε τις διαδικτυακές λειτουργίες στις Ρυθμίσεις για να περιηγηθείτε και να εισαγάγετε κύκλους αναφοράς της κοινότητας.", - "online_intro": "Κοινοποιήστε και κατεβάστε κύκλους αναφοράς με άλλους χρήστες του WashData. Όλες οι διαδικτυακές λειτουργίες είναι προαιρετικές και ανενεργές από προεπιλογή.", + "store_enable_hint": "Ενεργοποιήστε τις διαδικτυακές λειτουργίες στις Προτιμήσεις > Διαδικτυακά και κοινότητα για να περιηγηθείτε και να εισαγάγετε κύκλους αναφοράς της κοινότητας.", "store_share_intro": "Ανεβάστε αυτόν τον κύκλο αναφοράς ώστε να μπορούν να τον χρησιμοποιήσουν άλλοι με την ίδια συσκευή. Ελέγχεται πριν εμφανιστεί δημόσια.", - "online_intro_global": "Περιηγηθείτε και κοινοποιήστε καταγραφές αναφοράς με άλλους χρήστες του WashData και επιβεβαιώστε καταχωρίσεις συσκευών. Μία σύνδεση ισχύει για ολόκληρη την ενσωμάτωση WashData· η μάρκα και το μοντέλο της συσκευής ορίζονται ανά συσκευή στην ενότητα Βασικά. Όλες οι διαδικτυακές λειτουργίες είναι προαιρετικές και ανενεργές από προεπιλογή.", + "online_intro_global": "Περιηγηθείτε και κοινοποιήστε καταγραφές αναφοράς με άλλους χρήστες του WashData. Μία σύνδεση καλύπτει όλες τις συσκευές WashData σας· η μάρκα και το μοντέλο ορίζονται ανά συσκευή στα Βασικά. Οι διαδικτυακές λειτουργίες μένουν ανενεργές μέχρι να τις ενεργοποιήσετε.", "online_unavailable": "Οι διαδικτυακές λειτουργίες δεν είναι διαθέσιμες σε αυτόν τον διακομιστή.", "store_picker_offline": "Ενεργοποιήστε τις διαδικτυακές λειτουργίες στο γρανάζι ρυθμίσεων για να επιλέξετε από τον κατάλογο της κοινότητας.", "pick_brand_first": "Επιλέξτε πρώτα μια μάρκα συσκευής.", - "model_not_found": "Δεν υπάρχει ακόμη στον κατάλογο.", - "connect_to_confirm": "Συνδεθείτε στο γρανάζι ρυθμίσεων για επιβεβαίωση ή βαθμολόγηση.", + "custom_task_limit": "Φτάσατε το όριο των {max} εργασιών.", + "custom_tasks_intro": "Προσθέστε οτιδήποτε άλλο θέλετε να σας υπενθυμίζεται. Μια εργασία οφείλεται όταν συμπληρωθεί οποιοδήποτε από τα δύο διαστήματα· αφήστε το ένα κενό ή 0 για να χρησιμοποιείται μόνο το άλλο.", + "maintenance_due_sensor_hint": "Ο δυαδικός αισθητήρας \"Οφειλόμενη συντήρηση\" είναι ενεργός όσο οφείλεται κάποια εργασία, για τους δικούς σας αυτοματισμούς ειδοποιήσεων.", + "maintenance_supporter_intro": "Το Maintenance Supporter είναι μια ξεχωριστή ενσωμάτωση που κάνει πολύ περισσότερα: οποιαδήποτε συσκευή ή εργασία, προγραμματισμός βάσει χρόνου, χρήσης ή αισθητήρα, ειδοποιήσεις και ιστορικό. Μπορεί να χρησιμοποιεί τον μετρητή κύκλων του WashData.", "adopt_settings_hint": "Αντικατάσταση των ορίων ανίχνευσης & αντιστοίχισης αυτής της συσκευής με τα κοινοποιημένα. Οι ειδοποιήσεις, οι οντότητες και η τιμή ενέργειάς σας δεν αλλάζουν ποτέ.", - "include_settings_hint": "Κοινοποίηση των ορίων αναγνώρισης και αντιστοίχισης αυτής της συσκευής (όχι ειδοποιήσεων, οντοτήτων ή τιμής ενέργειας). Οι υιοθετούντες επιλέγουν εάν θα τα εφαρμόσουν.", + "include_settings_hint": "Κοινοποιήστε τα κατώφλια ανίχνευσης και αντιστοίχισης αυτής της συσκευής (όχι ειδοποιήσεις, οντότητες ή τιμή ενέργειας). Οι άλλοι επιλέγουν αν θα τα εφαρμόσουν.", "onboard_download": "Νέα συσκευή; Υιοθετήστε μια έτοιμη ρύθμιση (προγράμματα, κύκλους αναφοράς και φάσεις) από έναν άλλο χρήστη WashData με την ίδια συσκευή.", - "share_consent": "Επιβεβαιώνω ότι αυτοί οι κύκλοι ολοκληρώθηκαν κανονικά χωρίς διακοπή", "share_device_none": "Δεν υπάρχουν ακόμα κύκλοι για κοινοποίηση. Επισημάνετε πρώτα έναν καταγεγραμμένο ή χειροκίνητα επιλεγμένο κύκλο ως κύκλο αναφοράς (⭐) στην καρτέλα Κύκλοι.", "share_guideline_naming": "Ονομάστε κάθε προφίλ ακριβώς όπως εμφανίζεται στον επιλογέα ή την οθόνη της συσκευής (π.χ. 'Cotton 40', 'Eco 60').", - "share_guideline_quality": "Κοινοποιήστε μόνο κύκλους που ολοκληρώθηκαν κανονικά -- χωρίς διακοπές στη μέση του κύκλου, ανοίγματα πόρτας ή διακυμάνσεις ισχύος.", + "share_guideline_quality": "Κοινοποιήστε μόνο κύκλους που ολοκληρώθηκαν κανονικά: χωρίς διακοπές, ανοίγματα πόρτας ή διακυμάνσεις ρεύματος.", "share_guideline_review": "Η μεταφόρτωσή σας ξεκινά ως εκκρεμής και εμφανίζεται δημόσια μόλις αρκετά μέλη της κοινότητας την επιβεβαιώσουν.", "share_guidelines_title": "Πριν κοινοποιήσετε", - "store_download_device_intro": "Υιοθετήστε κάθε κοινοποιημένο πρόγραμμα και τους κύκλους αναφοράς του στη συσκευή σας. Οι δικοί σας καταγεγραμμένοι κύκλοι και στατιστικά δεν επηρεάζονται.", + "store_download_device_intro": "Προσθέστε κάθε κοινοποιημένο πρόγραμμα και τους κύκλους αναφοράς του στη συσκευή σας. Οι δικοί σας κύκλοι και τα στατιστικά δεν επηρεάζονται.", "store_share_device_intro": "Ανεβάστε {brand} {model} με τους κύκλους αναφοράς που επιλέγετε. Άλλοι με την ίδια συσκευή μπορούν να υιοθετήσουν τα προγράμματά σας. Οι καταχωρίσεις ελέγχονται πριν εμφανιστούν δημόσια.", "share_profile_no_cycles": "Δεν υπάρχουν κύκλοι αναφοράς -- επισημάνετε έναν κύκλο ως ⭐ στην καρτέλα Κύκλοι για να συμπεριλάβετε αυτό το προφίλ", - "advisory_phase_inconsistent": "Το '{name}' φαίνεται να αναμειγνύει διαφορετικά προγράμματα ή θερμοκρασίες - οι κύκλοι του θερμαίνονται για πολύ διαφορετικά χρονικά διαστήματα. Ο διαχωρισμός του σε ξεχωριστά προφίλ (π.χ. ανά θερμοκρασία) θα βελτιώσει την αντιστοίχιση και τις εκτιμήσεις χρόνου.", - "advisory_phase_inconsistent_title": "⚠ Πιθανώς μεικτά προγράμματα", - "export_select_intro": "Τσεκάρετε ακριβώς τι θα συμπεριληφθεί. Η επιλογή προφίλ χωρίς τους κύκλους τους εξάγει και πάλι ένα αναγνωρίσιμο πρόγραμμα (η μαθημένη του καμπύλη μεταφέρεται μαζί).", - "import_analyze_hint": "Φορτώστε ένα εξαγόμενο αρχείο (ή επικολλήστε το JSON του). Το WashData το αναλύει και δείχνει ακριβώς τι μπορεί να εισαχθεί πριν αλλάξει οτιδήποτε.", + "export_select_intro": "Τσεκάρετε τι θα συμπεριληφθεί. Τα προφίλ χωρίς τους κύκλους τους εξάγουν και πάλι ένα αναγνωρίσιμο πρόγραμμα.", + "import_analyze_hint": "Φορτώστε ένα εξαγόμενο αρχείο (ή επικολλήστε το JSON του). Το WashData δείχνει τι μπορεί να εισαχθεί πριν αλλάξει οτιδήποτε.", "analyzing": "Ανάλυση…", - "device_type_mismatch_warn": "Αυτή η εξαγωγή προέρχεται από διαφορετικό τύπο συσκευής ({src} έναντι {local}). Τα προγράμματα και οι κύκλοι μπορούν και πάλι να εισαχθούν ως δεδομένα αναφοράς, αλλά οι ρυθμίσεις που αφορούν τη συσκευή και η εισαγωγή πραγματικού ιστορικού είναι απενεργοποιημένες.", + "device_type_mismatch_warn": "Αυτή η εξαγωγή είναι από διαφορετικό τύπο συσκευής ({src} έναντι {local}). Προγράμματα και κύκλοι μπορούν να εισαχθούν ως δεδομένα αναφοράς, αλλά όχι οι ρυθμίσεις συσκευής ή το πραγματικό ιστορικό.", "merge_hint": "Τα εισαγόμενα στοιχεία προστίθενται και τίποτα τοπικό δεν χάνεται. Οι συγκρούσεις ονομάτων επιλύονται παρακάτω.", "replace_warn": "Κάθε επιλεγμένη κατηγορία διαγράφεται και αντικαθίσταται από το αρχείο. Οι μη επιλεγμένες κατηγορίες παραμένουν ανέπαφες.", "dest_reference_hint": "Οι εισαγόμενοι κύκλοι βελτιώνουν μόνο την αναγνώριση προγράμματος και δεν επηρεάζουν ποτέ τα στατιστικά χρήσης/ενέργειας.", - "dest_real_history_hint": "Οι εισαγόμενοι κύκλοι υπολογίζονται ως ιστορικό της ίδιας της συσκευής και τροφοδοτούν τα στατιστικά ενέργειας/χρήσης. Χρησιμοποιήστε το για τη μεταφορά μιας συσκευής σε νέα εγκατάσταση.", + "dest_real_history_hint": "Οι εισαγόμενοι κύκλοι μετρούν ως ιστορικό και στατιστικά της ίδιας της συσκευής. Χρησιμοποιήστε το για να μεταφέρετε μια συσκευή σε νέα εγκατάσταση.", "sug_muted": "Δεν θα προτείνεται ξανά αυτή η ρύθμιση", "sug_mute_failed": "Αποτυχία σίγασης πρότασης", "sug_unmuted_all": "Οι αποσιωπημένες προτάσεις επαναφέρθηκαν", - "n_suggestions_muted": "{count} αποσιωπημένες· ο αυτόματος ρυθμιστής δεν θα τις προτείνει.", + "n_suggestions_muted": "{count} αποσιωπημένες προτάσεις· ο αυτόματος ρυθμιστής δεν θα τις προτείνει.", + "n_suggestions_muted_one": "{count} αποσιωπημένη πρόταση· ο αυτόματος ρυθμιστής δεν θα την προτείνει.", + "n_suggestions_muted_other": "{count} αποσιωπημένες προτάσεις· ο αυτόματος ρυθμιστής δεν θα τις προτείνει.", "font_size_hint": "Κάντε τα πάντα σε αυτόν τον πίνακα μεγαλύτερα ή μικρότερα. Ισχύει για τον λογαριασμό σας σε αυτήν τη συσκευή.", - "import_history_description": "Είχατε ήδη έξυπνο βύσμα πριν από το WashData; Ανεβάστε μια εξαγωγή ιστορικού του αισθητήρα ισχύος του, ή διαβάστε το απευθείας από το Home Assistant, και η κανονική ανίχνευση θα εκτελεστεί πάνω σε αυτό, ώστε οι παλιοί κύκλοι να εμφανιστούν στη λίστα Κύκλοι έτοιμοι για ονομασία.", - "hist_input_hint": "Ανεβάστε ένα CSV που κατεβάσατε από το πάνελ Ιστορικού (entity, state, last changed), ή αφήστε το WashData να διαβάσει απευθείας το ιστορικό του αισθητήρα. Στη συνέχεια η ανίχνευση εκτελείται πάνω σε αυτό ακριβώς όπως και σε ζωντανή λειτουργία, και εσείς επιλέγετε ποιους από τους κύκλους που βρέθηκαν θα διατηρήσετε.", - "hist_recorder_hint": "Διαβάζει από την ημερομηνία που επιλέγετε έως τώρα. Το Home Assistant διατηρεί από προεπιλογή αναλυτικό ιστορικό για 10 ημέρες και μόνο ωριαίους μέσους όρους μετά από αυτό, οι οποίοι είναι πολύ χονδροειδείς για την ανίχνευση κύκλων - επιλέξτε παλαιότερη ημερομηνία μόνο αν ο recorder είναι ρυθμισμένος να διατηρεί περισσότερα.", - "hist_scanning": "Το ιστορικό σας αναπαράγεται μέσα από τον ανιχνευτή. Αυτό εκτελείται στο παρασκήνιο - μπορείτε να κλείσετε αυτό το παράθυρο και να επιστρέψετε αργότερα.", + "import_history_description": "Είχατε έξυπνο βύσμα πριν από το WashData; Ανεβάστε το ιστορικό ισχύος του ή διαβάστε το από το Home Assistant. Η ανίχνευση τρέχει πάνω του και οι παλιοί κύκλοι εμφανίζονται στη λίστα Κύκλοι, έτοιμοι για ονομασία.", + "hist_input_hint": "Ανεβάστε ένα CSV από το πάνελ Ιστορικού (entity, state, last changed) ή αφήστε το WashData να διαβάσει απευθείας το ιστορικό του αισθητήρα. Η ζωντανή ανίχνευση τρέχει πάνω του και εσείς επιλέγετε ποιους κύκλους θα κρατήσετε.", + "hist_recorder_hint": "Διαβάζει από την ημερομηνία που επιλέγετε έως τώρα. Το Home Assistant κρατά από προεπιλογή αναλυτικό ιστορικό 10 ημερών· οι παλαιότεροι ωριαίοι μέσοι όροι είναι πολύ χονδροειδείς για ανίχνευση κύκλων.", + "hist_scanning": "Το ιστορικό σας περνά από τον ανιχνευτή στο παρασκήνιο. Μπορείτε να κλείσετε αυτό το παράθυρο και να επιστρέψετε.", "hist_imported_count": "Εισήχθησαν {n} κύκλοι.", "hist_duplicates": "{n} είχαν εισαχθεί ήδη και παραλείφθηκαν.", "hist_capped": "Συμπληρώθηκε το όριο εισαγόμενων κύκλων ανά συσκευή· οι υπόλοιποι δεν αποθηκεύτηκαν.", - "hist_next_step": "Βρίσκονται στη λίστα Κύκλοι, με σήμανση ως εισαγόμενο ιστορικό. Ανοίξτε έναν και χρησιμοποιήστε την Επισήμανση για να ορίσετε το πρόγραμμα στο οποίο ανήκει.", + "hist_next_step": "Βρίσκονται στη λίστα Κύκλοι, με σήμανση εισαγόμενου ιστορικού. Ανοίξτε έναν και πατήστε Επισήμανση για να ονομάσετε το πρόγραμμά του.", "hist_rows_read": "Διαβάστηκαν {n} μετρήσεις", "hist_entity_substituted": "διαβάστηκε το {used} (αυτή η συσκευή είναι ρυθμισμένη για {wanted})", "hist_breaks": "{n} κενά όπου ο αισθητήρας ήταν μη διαθέσιμος", @@ -1089,26 +1018,73 @@ "hist_scan_failed": "Η σάρωση απέτυχε.", "hist_scan_expired": "Αυτή η σάρωση δεν είναι πλέον διαθέσιμη. Κάντε ξανά σάρωση.", "hist_import_failed": "Η εισαγωγή απέτυχε.", - "imported_history_readonly": "Ανιχνεύθηκε σε εισαγόμενο ιστορικό ισχύος. Διαμορφώνει την αντιστοίχιση προγραμμάτων, αλλά δεν προσμετράται στα στατιστικά σας και δεν μπορεί να περικοπεί ή να διαχωριστεί. Επισημάνετέ τον για να ορίσετε το πρόγραμμα.", - "odometer_intro": "Αυτό το σύνολο μόνο αυξάνεται. Η διαγραφή της εγγραφής ενός κύκλου δεν το αλλάζει και συνεχίζει να μετρά πέρα από το όριο του αποθηκευμένου ιστορικού, ώστε οι υπενθυμίσεις σέρβις να παραμένουν σωστές.", - "odometer_correct_hint": "Ορίστε αυτή την τιμή αν το WashData κατέγραψε μια λειτουργία που δεν έγινε ποτέ, ή αν η συσκευή είχε εκτελέσει κύκλους πριν από την εγκατάσταση του WashData.", + "imported_history_readonly": "Βρέθηκε σε εισαγόμενο ιστορικό ισχύος. Βοηθά την αντιστοίχιση, αλλά δεν μετρά στα στατιστικά σας και δεν μπορεί να περικοπεί ή να διαχωριστεί. Επισημάνετέ τον για να ονομάσετε το πρόγραμμα.", + "odometer_intro": "Αυτό το σύνολο μόνο αυξάνεται. Η διαγραφή ενός κύκλου δεν το μειώνει, ώστε οι υπενθυμίσεις σέρβις να μένουν σωστές.", + "odometer_correct_hint": "Ορίστε το αν το WashData μέτρησε μια λειτουργία που δεν έγινε ποτέ ή αν η συσκευή εκτέλεσε κύκλους πριν εγκατασταθεί το WashData.", "no_reminders_set": "Δεν έχουν οριστεί ακόμη υπενθυμίσεις σέρβις.", "toast_program_armed": "Πρόγραμμα ορίστηκε για τον επόμενο κύκλο: {program}", "standby_above_stop_title": "Εξακολουθεί να καταναλώνει {idle} W όταν ο κύκλος τελείωσε", "standby_above_stop_sub": "{n} από τους τελευταίους {total} κύκλους. Η Καθυστέρηση Απενεργοποίησης ξεκινά μόνο όταν η ισχύς πέσει κάτω από το Κατώφλι Τερματισμού ({stop} W), οπότε οι κύκλοι τελειώνουν με καθυστέρηση ή πρέπει να τερματίζονται με το χέρι.", - "advisory_duration_outlier": "{n} κύκλοι με την ετικέτα '{name}' απέχουν υπερβολικά από τη συνήθη διάρκειά του για να αντιστοιχιστούν ποτέ με αυτό (χειρότερη περίπτωση: {ratio}x). Πιθανότατα έχουν λάθος ετικέτα ή μία καταγραφή κατέγραψε δύο εκτελέσεις. Αλλάξτε τους ετικέτα ή χωρίστε τους, ώστε να σταματήσουν να αλλοιώνουν την εκτίμηση χρόνου αυτού του προγράμματος.", + "advisory_duration_outlier": "{n} κύκλοι με ετικέτα '{name}' απέχουν πολύ από τη συνήθη διάρκειά του για να αντιστοιχιστούν ποτέ (χειρότερος: {ratio}x). Πιθανώς έχουν λάθος ετικέτα ή περιέχουν δύο εκτελέσεις. Αλλάξτε τους ετικέτα ή χωρίστε τους, ώστε να μην αλλοιώνουν την εκτίμηση χρόνου του.", "toast_range_required": "Ορίστε ένα έγκυρο εύρος Από/Έως", - "no_profiles_option": "(δεν υπάρχουν προφίλ, προσθέστε ένα)" + "no_profiles_option": "(δεν υπάρχουν προφίλ, προσθέστε ένα)", + "compare_cycles_title": "Σύγκριση {count} κύκλων", + "edit_builtin_phase": "Αυτή είναι ενσωματωμένη φάση. Η αποθήκευση δημιουργεί προσαρμοσμένο αντίγραφο· διαγράψτε το αντίγραφο για να επαναφέρετε το πρωτότυπο.", + "fetch_error": "Η φόρτωση των δεδομένων απέτυχε.", + "connection_lost": "Η σύνδεση χάθηκε - εμφανίζονται δεδομένα των {time}.", + "pg_notify_partial": "Οι δείκτες δείχνουν μόνο ειδοποιήσεις έναρξης, σχεδόν έτοιμου, ολοκλήρωσης, ορόσημων και ωρών ησυχίας. Οι ζωντανές ενημερώσεις, οι υπενθυμίσεις αποφόρτωσης και οι ειδοποιήσεις υπέρβασης δεν προσομοιώνονται.", + "connect_to_confirm": "Συνδεθείτε στο γρανάζι ρυθμίσεων για επιβεβαίωση ή βαθμολόγηση.", + "coverage_gap_cluster": "{count} πρόσφατοι κύκλοι χωρίς ετικέτα, διάρκειας περίπου {min} λεπτ., μοιάζουν με ένα πρόγραμμα που δεν έχετε δημιουργήσει ακόμη.", + "pg_label_yes": "Θα επισημαινόταν ως \"{name}\" στο τέλος του κύκλου", + "pg_label_no": "Θα ζητούσε αναθεώρηση: {reason}", + "live_match_debug_tip": "Εμπιστοσύνη: πόσο στενά ταιριάζει η τρέχουσα καμπύλη ισχύος με το κορυφαίο υποψήφιο προφίλ (0-100%). Ασαφής: οι δύο καλύτεροι υποψήφιοι έχουν βαθμολογίες που απέχουν λιγότερο από 5% μεταξύ τους - η ετικέτα παραμένει αβέβαιη μέχρι να ολοκληρωθεί ο κύκλος.", + "automation_alias_started": "{device}: ο κύκλος ξεκίνησε", + "automation_alias_finished": "{device}: ο κύκλος ολοκληρώθηκε", + "automation_desc_started": "Εκτελείται όταν ξεκινά ένας κύκλος της συσκευής {device}. Προσθέστε τις ενέργειές σας (ειδοποίηση, φώτα, ...).", + "automation_desc_finished": "Εκτελείται όταν ολοκληρώνεται ένας κύκλος της συσκευής {device}. Προσθέστε τις ενέργειές σας (ειδοποίηση, φώτα, ...).", + "feedback_detected": "Το WashData εντόπισε: {name}", + "import_undo_hint": "Διατηρείται ένα σημείο επαναφοράς από πριν την τελευταία εισαγωγή ({when}): {profiles} προφίλ και {cycles} κύκλοι.", + "replace_undo_hint": "Πρώτα αποθηκεύεται ένα σημείο επαναφοράς, ώστε να μπορείτε να αναιρέσετε αυτήν την εισαγωγή από την ενότητα Εξαγωγή / Εισαγωγή.", + "pg_in_sample_note": "Αυτοί οι κύκλοι βοήθησαν στη δημιουργία των προφίλ με τα οποία αντιστοιχίζονται, οπότε τα αποτελέσματα είναι αισιόδοξα: σε νέους κύκλους περιμένετε λίγο λιγότερες σωστές αντιστοιχίσεις.", + "hist_tail_trimmed": "Αφαιρέθηκαν {min} λεπτά αναμονής μετά το τέλος του κύκλου, όπως τα αφαιρεί και η ενεργή ενσωμάτωση", + "hist_truncated": "σταμάτησε στο όριο των {n} μετρήσεων, οπότε το υπόλοιπο ιστορικό δεν σαρώθηκε", + "hist_warn_kw": "Η υψηλότερη μέτρηση είναι μόλις {w}. Ο αισθητήρας φαίνεται να αναφέρει κιλοβάτ· το WashData χρειάζεται βατ, οπότε δεν μπορούν να ανιχνευθούν κύκλοι.", + "pg_compare_profile_tip": "Επιλέγει μόνο το προφίλ που σχεδιάζεται πάνω στο γράφημα. Η προσομοίωση αντιστοιχίζει πάντα τον κύκλο μόνη της, όπως κάνει και η ενεργή ενσωμάτωση.", + "pg_outcome_stale": "Οι ρυθμίσεις άλλαξαν από τότε που εκτελέστηκε αυτή η προσομοίωση. Εξακολουθεί να δείχνει το παλιό αποτέλεσμα.", + "pg_sweep_guarded": "Τερματίζει περισσότερους κύκλους πρόωρα, διαχωρίζει περισσότερους ή χάνει κύκλους που εντοπίζει η τρέχουσα ρύθμιση, οπότε δεν προτείνεται ποτέ.", + "pg_sweep_keep_current": "Διατηρήστε την τρέχουσα τιμή ({value}, {metric}): καμία τιμή δεν τα πήγε καλύτερα κατά τουλάχιστον έναν κύκλο.", + "pg_task_busy": "Μια εκτέλεση Δοκιμής στο ιστορικό ή Βελτιστοποίησης τρέχει ήδη για αυτή τη συσκευή. Περιμένετε ή ακυρώστε την.", + "hist_warn_naive_time": "{n} χρονοσφραγίδες δεν έχουν ζώνη ώρας και διαβάστηκαν ως UTC. Αν το αρχείο είναι σε τοπική ώρα, οι κύκλοι μετατοπίζονται κατά τη διαφορά σας από την UTC και μετρήσεις κοντά σε αλλαγή θερινής ώρας μπορεί να απορριφθούν.", + "hist_warn_naive_time_one": "{n} χρονοσφραγίδα δεν έχει ζώνη ώρας και διαβάστηκε ως UTC. Αν το αρχείο είναι σε τοπική ώρα, οι κύκλοι μετατοπίζονται κατά τη διαφορά σας από την UTC και μετρήσεις κοντά σε αλλαγή θερινής ώρας μπορεί να απορριφθούν.", + "hist_warn_naive_time_other": "{n} χρονοσφραγίδες δεν έχουν ζώνη ώρας και διαβάστηκαν ως UTC. Αν το αρχείο είναι σε τοπική ώρα, οι κύκλοι μετατοπίζονται κατά τη διαφορά σας από την UTC και μετρήσεις κοντά σε αλλαγή θερινής ώρας μπορεί να απορριφθούν.", + "pg_sweep_n_cycles": "σε {count} κύκλους", + "pg_sweep_n_cycles_one": "σε {count} κύκλο", + "pg_sweep_n_cycles_other": "σε {count} κύκλους", + "cycle_context_loading": "Φόρτωση ιστορικού καταγραφέα…", + "cycle_context_note": "Γκρι: το ιστορικό καταγραφέα του αισθητήρα ισχύος γύρω από αυτόν τον κύκλο. Δεν ανήκει στον κύκλο και δεν επηρεάζει τη διάρκεια, την ενέργεια ή την αντιστοίχιση.", + "cycle_context_none": "Ο καταγραφέας δεν έχει μετρήσεις γύρω από αυτόν τον κύκλο (το Home Assistant διατηρεί από προεπιλογή ιστορικό 10 ημερών).", + "cycle_context_tip": "Μόνο για προβολή: δείχνει το ιστορικό καταγραφέα του αισθητήρα ισχύος γύρω από αυτόν τον κύκλο, αν διατηρείται ακόμη. Για να προστεθούν αποτυχημένες απόπειρες εκκίνησης σε μελλοντικούς κύκλους, χρησιμοποιήστε την Προεγγραφή Καμπύλης (Ρυθμίσεις > Ανίχνευση > Έναρξη Κύκλου), που αλλάζει τις αποθηκευμένες διάρκειες.", + "invalid_threshold_pair": "Το Κατώφλι Τερματισμού ({stop} W) πρέπει να είναι κάτω από το Κατώφλι Εκκίνησης ({start} W). Δεν αποθηκεύτηκε τίποτα· αλλάξτε τα δύο μαζί." }, "phase_desc": { "anti_crease": "Περιστασιακά σύντομες ανατροπές μετά την ολοκλήρωση για μείωση των ρυτίδων.", "anti_wrinkle": "Περιοδική αναδίπλωση μετά τον κύκλο για μείωση των ρυτίδων.", + "baking": "Το θερμαντικό στοιχείο λειτουργεί σε υψηλή θερμοκρασία για να σχηματιστούν η κόρα και η ψίχα.", + "cooking": "Κύρια φάση μαγειρέματος με ενεργό θερμαντήρα και ανεμιστήρα.", "cool_down": "Ανατροπή χωρίς θερμότητα κοντά στο τέλος του κύκλου.", + "cool_down_af": "Ψύξη μόνο με τον ανεμιστήρα μετά τη θέρμανση.", "drain_and_switch": "Μεταβατική περίοδος από τη λειτουργία πλύσης στη λειτουργία στεγνώματος.", "dry": "Στάδιο στεγνώματος με χρήση θερμαντήρα ή/και υπολειπόμενης θερμότητας.", "drying": "Κύρια θερμαινόμενη περίοδος ανατροπής.", "heat_up": "Η αρχική προθέρμανση του θερμαντήρα πριν ξεκινήσει το πλήρες στέγνωμα.", + "keep_warm": "Διατήρηση σε χαμηλή θερμότητα ώστε το φαγητό να μένει ζεστό.", + "keep_warm_bm": "Διατήρηση σε χαμηλή θερμότητα ώστε το ψωμί να μένει ζεστό μετά το ψήσιμο.", + "kneading": "Το μοτέρ ανακατεύει και ζυμώνει τη ζύμη. Υψηλή κατανάλωση ισχύος.", + "pause_af": "Σύντομη παύση για ανακίνηση ή έλεγχο.", + "pre_heat": "Αρχική θέρμανση του θαλάμου πριν από το κανονικό μαγείρεμα.", "pre_rinse": "Αρχικό ψεκασμό πριν από την πλύση με απορρυπαντικό.", + "proving": "Φούσκωμα σε χαμηλή θερμότητα: η μαγιά δρα και η ζύμη διογκώνεται.", + "resting": "Σύντομη παύση χαμηλής ισχύος ανάμεσα στα ζυμώματα για να χαλαρώσει η γλουτένη.", "pre_wash": "Αρχικό εμποτισμό ή προεπεξεργασία πριν από την κύρια πλύση.", "rinse": "Στάδιο έκπλυσης με καθαρό νερό. Αυτή η φάση μπορεί να επαναληφθεί πολλές φορές.", "sanitize": "Στάδιο καθαρισμού υψηλής θερμοκρασίας για προγράμματα απολύμανσης.", @@ -1121,6 +1097,7 @@ "wash_dw": "Κύριο πλύσιμο με απορρυπαντικό με θέρμανση." }, "placeholder": { + "custom_task": "π.χ. Καθαρισμός λάστιχου πόρτας", "add": "προσθήκη…", "group_name": "π.χ. Βαμβάκι 2:47", "json_buttons": "[{\"action\":\"ID\",\"title\":\"Label\"}]", @@ -1147,7 +1124,7 @@ }, "section": { "anti_wrinkle": { - "intro": "Η λειτουργία κατά τσακίσματος εντοπίζει παλμούς χαμηλής ισχύος τύμβου μετά την κύρια φάση και τους διατηρεί συνδεδεμένους με τον ολοκληρωμένο κύκλο.", + "intro": "Κρατά τους παλμούς περιστροφής χαμηλής ισχύος ενός στεγνωτηρίου μετά την κύρια φάση συνδεδεμένους με τον ολοκληρωμένο κύκλο.", "label": "Αντί-τσάκισμα" }, "basic": { @@ -1155,7 +1132,7 @@ "label": "Βασικά" }, "delay": { - "intro": "Η ανίχνευση καθυστερημένης εκκίνησης αναγνωρίζει πότε μια συσκευή είναι ενεργοποιημένη αλλά δεν έχει ακόμα ξεκινήσει τον κύκλο της.", + "intro": "Εντοπίζει μια συσκευή που είναι αναμμένη και περιμένει καθυστερημένη εκκίνηση.", "label": "Καθυστερημένη Εκκίνηση" }, "detection": { @@ -1179,153 +1156,145 @@ "label": "Χρονισμός & Watchdog" }, "triggers": { - "intro": "Προαιρετικά εξωτερικά σήματα: ενεργοποιητής τέλους, αισθητήρας πόρτας, διακόπτης παύσης και υπενθύμιση αποφόρτωσης.", + "intro": "Προαιρετικά: ενεργοποιητής τέλους, αισθητήρας πόρτας, διακόπτης παύσης και υπενθύμιση αποφόρτωσης.", "label": "Ενεργοποιητές & Πόρτες" }, - "phase_eta": { - "label": "Υπολειπόμενος Χρόνος", - "intro": "Υπολειπόμενος χρόνος με επίγνωση φάσεων, για μηχανές των οποίων η διάρκεια κύκλου εξαρτάται από τη θερμοκρασία ή το στύψιμο." - }, "dishwasher": { "label": "Πλυντήριο πιάτων", - "intro": "Διαχείριση τέλους κύκλου για πλυντήρια πιάτων, τα οποία συνήθως ολοκληρώνονται με μια μακρά φάση στεγνώματος σχεδόν μηδενικής κατανάλωσης πριν από μια σύντομη τελική αποστράγγιση." + "intro": "Χειρισμός του τέλους για πλυντήρια πιάτων, που συχνά στεγνώνουν αθόρυβα για πολλή ώρα πριν από μια σύντομη τελική αποστράγγιση." } }, "setting": { "anti_wrinkle_enabled": { - "doc": "Αναγνωρίστε τους σύντομους παλμούς χαμηλής ισχύος που εκπέμπει ένα στεγνωτήριο μετά την κύρια φάση θερμότητας και κρατήστε τους συνδεδεμένους στον τελικό κύκλο αντί να τους διαβάζετε ως νέους κύκλους.", + "doc": "Κρατά τους σύντομους παλμούς περιστροφής χαμηλής ισχύος ενός στεγνωτηρίου μετά την κύρια φάση θέρμανσης μαζί με τον ολοκληρωμένο κύκλο, αντί να τους διαβάζει ως νέους κύκλους.", "label": "Ενεργοποίηση Ανίχνευσης Κατά Τσακίσματος" }, "anti_wrinkle_exit_power": { - "doc": "Η ισχύς πρέπει να πέσει κάτω από αυτό μεταξύ των παλμών για να παραμείνει ενεργή η λειτουργία κατά των ρυτίδων.", + "doc": "Στη λειτουργία κατά τσακίσματος, ισχύς κάτω από αυτό μετρά ως ησυχία ανάμεσα στους παλμούς ανατροπής, και ησυχία μεγαλύτερη από το Μέγιστο Κενό Παλμών τερματίζει τη λειτουργία. Αν το Κατώφλι Τερματισμού είναι υψηλότερο, χρησιμοποιείται εκείνο, οπότε αυτή η ρύθμιση αλλάζει κάτι μόνο όταν οριστεί πάνω από το Κατώφλι Τερματισμού.", "label": "Κατώφλι Ισχύος Εξόδου" }, "anti_wrinkle_idle_timeout": { "label": "Μέγιστο Κενό Παλμών", - "doc": "Πόση ώρα μπορεί να παραμείνει ήσυχη η συσκευή ανάμεσα σε δύο παλμούς ανατροπής προτού λήξει η λειτουργία κατά τσακίσματος. Ορίστε το πάνω από το μεγαλύτερο κενό που αφήνει το στεγνωτήριό σας ανάμεσα στους παλμούς, αλλιώς κάθε επόμενος παλμός διαβάζεται ως εσφαλμένη εκκίνηση." + "doc": "Πόση ώρα μπορεί το στεγνωτήριο να μείνει ήσυχο ανάμεσα σε παλμούς περιστροφής πριν λήξει η λειτουργία κατά τσακίσματος. Ορίστε το πάνω από το μεγαλύτερο κενό του στεγνωτηρίου σας, αλλιώς οι επόμενοι παλμοί διαβάζονται ως εσφαλμένες εκκινήσεις." }, "anti_wrinkle_max_duration": { - "doc": "Οι παλμοί που είναι μεγαλύτεροι από αυτό αντιμετωπίζονται ως πραγματικός κύκλος και όχι ως αντιρυτιδικό περιτύλιγμα.", + "doc": "Ένας παλμός μεγαλύτερος από αυτό μετρά ως πραγματικός κύκλος, όχι ως περιστροφή κατά τσακίσματος.", "label": "Μέγιστη Διάρκεια" }, "anti_wrinkle_max_power": { - "doc": "Ένας παλμός πάνω από αυτήν την ισχύ αντιμετωπίζεται ως ένας πραγματικός νέος κύκλος, όχι ως αντιρυτιδικό κύμα. Ρυθμίστε ακριβώς πάνω από την ισχύ του παλμού ανατροπής.", + "doc": "Ένας παλμός πάνω από αυτή την ισχύ μετρά ως πραγματικός νέος κύκλος, όχι ως περιστροφή. Ορίστε το λίγο πάνω από την ισχύ των παλμών περιστροφής.", "label": "Μέγιστη Ισχύς Κατά Τσακίσματος" }, "auto_label_confidence": { - "doc": "Εάν η βαθμολογία αντιστοίχισης στο τέλος του κύκλου είναι ίση ή μεγαλύτερη από αυτήν, το πρόγραμμα επισημαίνεται αυτόματα χωρίς καμία προτροπή επιβεβαίωσης. Αυξήστε την για να απαιτείται μεγαλύτερη βεβαιότητα πριν από την αυτόματη επισήμανση, μειώστε την για περισσότερη αυτοματοποίηση. Λειτουργεί σε συνδυασμό με την Εμπιστοσύνη Μάθησης ακριβώς από κάτω.", + "doc": "Βαθμολογία αντιστοίχισης στο τέλος του κύκλου από την οποία και πάνω το πρόγραμμα επισημαίνεται χωρίς ερώτηση. Αυξήστε την για περισσότερη βεβαιότητα· μειώστε την για να επισημαίνονται αυτόματα περισσότεροι κύκλοι. Κρατήστε την πάνω από την Εμπιστοσύνη Μάθησης.", "label": "Εμπιστοσύνη Αυτόματης Επισήμανσης" }, "auto_maintenance": { - "doc": "Εκτελέστε νυχτερινή καθαριότητα: ανακατασκευάστε φακέλους προφίλ, υπολογίστε ξανά την υγεία του κύκλου, κλαδέψτε τα ίχνη εντοπισμού σφαλμάτων και διατηρήστε τους πιο πρόσφατους κύκλους.", + "doc": "Νυχτερινή εκκαθάριση: επισημαίνει παλιούς κύκλους που πλέον αντιστοιχούν σαφώς σε ένα πρόγραμμα, ανακατασκευάζει τις καμπύλες των προγραμμάτων, ανανεώνει τις σημάνσεις ανωμαλιών και την υγεία των κύκλων και αφαιρεί τα ίχνη αποσφαλμάτωσης αν το \"Αποθήκευση Ιχνών Αποσφαλμάτωσης\" είναι ανενεργό. Οι κύκλοι και τα ίχνη ισχύος τους διατηρούνται πάντα.", "label": "Αυτόματη Συντήρηση (νυχτερινός καθαρισμός)" }, "completion_min_seconds": { - "doc": "Κύκλοι μικρότεροι από αυτό απορρίπτονται ως κύκλοι φάντασμα (δοκιμαστικές διαδρομές, άνοιγμα της πόρτας για προσθήκη κάλτσας).", + "doc": "Κύκλοι μικρότεροι από αυτό (δοκιμαστικές εκτελέσεις, άνοιγμα της πόρτας για να μπει μια κάλτσα) αποθηκεύονται ως Διακόπηκε και δεν στέλνουν ειδοποίηση ολοκλήρωσης.", "label": "Ελάχιστη Διάρκεια Κύκλου" }, "delay_confirm_seconds": { - "doc": "Η τροφοδοσία πρέπει να παραμείνει στη ζώνη αναμονής για τόσο μεγάλο χρονικό διάστημα, προτού η συσκευή αντιμετωπιστεί ως αναμονή για την εκκίνηση αντί να λειτουργεί.", + "doc": "Η ισχύς πρέπει να μείνει στη ζώνη αναμονής τόσο χρόνο πριν η συσκευή θεωρηθεί ότι περιμένει να ξεκινήσει.", "label": "Δευτερόλεπτα Επιβεβαίωσης" }, "delay_start_detect_enabled": { - "doc": "Ανιχνεύστε πότε η συσκευή είναι ενεργοποιημένη και περιμένει (καθυστερημένη εκκίνηση / αναμονή) αλλά δεν έχει ξεκινήσει τον κύκλο της, επομένως η σχεδίαση αναμονής δεν θεωρείται λανθασμένα με κύκλο λειτουργίας.", + "doc": "Εντοπίζει μια συσκευή που είναι αναμμένη και περιμένει καθυστερημένη εκκίνηση, ώστε η κατανάλωση αναμονής της να μη θεωρείται κύκλος σε λειτουργία.", "label": "Ενεργοποίηση Ανίχνευσης Καθυστερημένης Εκκίνησης" }, "delay_timeout_hours": { - "doc": "Σταματήστε την αναμονή στη λειτουργία καθυστερημένης εκκίνησης μετά από τόσες ώρες και επιστρέψτε στο ρελαντί, έτσι ώστε ένα μηχάνημα να μένει τροφοδοτημένο αλλά να μην ξεκινά ποτέ, να μην περιμένει για πάντα.", + "doc": "Σταματά την αναμονή καθυστερημένης εκκίνησης μετά από τόσες ώρες και επιστρέφει σε αδράνεια.", "label": "Ώρες Λήξης Χρόνου" }, "device_type": { - "doc": "Κατηγορία συσκευών. Ορίζει λογικές προεπιλογές ανίχνευσης (οριακά όρια, μη καθυστέρηση, τερματισμός χειρισμού) προσαρμοσμένες για αυτόν τον τύπο συσκευής. αλλάξτε το μόνο εάν η συσκευή είχε ρυθμιστεί αρχικά ως λάθος τύπος.", + "doc": "Ορίζει τις προεπιλογές ανίχνευσης (κατώφλια, καθυστέρηση απενεργοποίησης, χειρισμός τέλους) για αυτό το είδος συσκευής. Αλλάξτε το μόνο αν η συσκευή ρυθμίστηκε με λάθος τύπο.", "label": "Τύπος Συσκευής" }, "door_sensor_entity": { - "doc": "Προαιρετικός δυαδικός αισθητήρας πόρτας. Χρησιμοποιείται για τον εντοπισμό πότε η συσκευή έχει ανοίξει/εκφορτωθεί μετά από έναν κύκλο.", + "doc": "Προαιρετικός αισθητήρας πόρτας. Δείχνει πότε άνοιξε η συσκευή για αποφόρτωση μετά από έναν κύκλο.", "label": "Οντότητα Αισθητήρα Πόρτας" }, "duration_tolerance": { - "doc": "Ανοχή για εκτιμήσεις που απομένει (ανατροφοδότηση μάθησης, μη αντιστοίχιση). Εάν η πραγματική διάρκεια είναι εντός +/-X% της εκτίμησης, λογίζεται ως καλή αντιστοίχιση.", + "doc": "Μόνο για την ανατροφοδότηση μάθησης: ένας κύκλος εντός +/-X% της εκτιμώμενης διάρκειας μετρά ως καλή εκτίμηση. Δεν επηρεάζει την αντιστοίχιση.", "label": "Ανοχή Εκτίμησης" }, "enable_ml_models": { - "doc": "Ενώ εκτελείται ένας κύκλος, αφήστε τα μοντέλα να βελτιώσουν τα ζωντανά αποτελέσματα: μια πιο σταθερή εκτίμηση υπολειπόμενου χρόνου και ενέργειας/κόστους και ένα προστατευτικό κατά της πρόωρης διακοπής κατά την ανίχνευση τέλους (μπορεί μόνο να καθυστερήσει ένα φινίρισμα, να μην τελειώσει ποτέ νωρίτερα και να περιοριστεί). Χρησιμοποιεί τα βελτιωμένα μοντέλα σας όταν είναι διαθέσιμα, διαφορετικά τα ενσωματωμένα. Off = μόνο η κλασική λογική που βασίζεται στην εξουσία (ακόμα αξιόπιστη).", + "doc": "Χρησιμοποιεί ένα μοντέλο εκπαιδευμένο σε αυτό το μηχάνημα για να προβλέπει την ενέργεια και το κόστος του κύκλου σε εξέλιξη, και αφήνει ένα γνωστό πρόγραμμα που σταματά ασυνήθιστα νωρίς να ολοκληρωθεί χωρίς ολόκληρη την καθυστέρηση απενεργοποίησης. Ανενεργό: μόνο λογική βάσει ισχύος.", "label": "Εφαρμογή έξυπνων μοντέλων κατά τη διάρκεια κύκλου" }, "end_energy_threshold": { - "doc": "Κατά τη διάρκεια της αντίστροφης μέτρησης εκτός καθυστέρησης, η συσσωρευμένη ενέργεια (βατ x χρόνο) συγκρίνεται με αυτό το όριο. Σε περίπτωση υπέρβασης, η αντίστροφη μέτρηση επαναφέρεται - διατηρώντας τα αντιτσαλάκια και τις ουρές στεγνώματος στο πλυντήριο πιάτων συνδεδεμένα στον κύκλο αντί να τα κόβετε σύντομα. Αυξήστε το εάν οι κύκλοι τελειώνουν πολύ νωρίς κατά τη διάρκεια της ψύξης. χαμηλώστε το εάν η ανίχνευση είναι αργή.", + "doc": "Η ενέργεια που επιτρέπεται κατά την καθυστέρηση απενεργοποίησης. Πάνω από αυτήν ο κύκλος συνεχίζεται, ώστε οι περιστροφές και οι ουρές στεγνώματος να μένουν συνδεδεμένες. Μειώστε την αν οι κύκλοι τελειώνουν κατά τη διάρκεια μιας ουράς· αυξήστε την αν ολοκληρωμένοι κύκλοι συνεχίζουν να περιμένουν.", "label": "Ενέργεια Λήξης" }, - "end_repeat_count": { - "doc": "Αριθμός διαδοχικών μετρήσεων κάτω από το όριο τερματισμού που απαιτούνται πριν από το τέλος του κύκλου. Το 1 είναι καλό για τα περισσότερα βύσματα. Αυξήστε στο 2-3 εάν το έξυπνο βύσμα σας αναφέρει περιστασιακά ένα δείγμα ψευδούς μηδενισμού στο μέσο του κύκλου και οι κύκλοι σας τελειώνουν πρόωρα.", - "label": "Πλήθος Επαναλήψεων Λήξης" - }, "energy_price_entity": { - "doc": "Αισθητήρας με την τρέχουσα τιμή ηλεκτρικής ενέργειας ανά kWh (π.χ. ένα δυναμικό τιμολόγιο). Πρέπει να είναι τιμή, όχι η οντότητα ισχύος ή ενέργειας της ίδιας της πρίζας: προηγείται της στατικής τιμής παρακάτω, οπότε ένας μετρητής kWh εδώ κοστολογεί κάθε κύκλο με την ένδειξη του μετρητή αντί για το τιμολόγιό σας. Με ενεργό το Χρονικά Σταθμισμένο Κόστος, κάθε κύκλος χρεώνεται με την τιμή που ίσχυε σε κάθε στιγμή της λειτουργίας του· διαφορετικά παγιώνεται πάνω του η τιμή που ίσχυε όταν τελείωσε.", + "doc": "Αισθητήρας με την τρέχουσα τιμή ανά kWh (π.χ. δυναμικό τιμολόγιο). Υπερισχύει της στατικής τιμής παρακάτω, οπότε πρέπει να είναι τιμή, όχι ο αισθητήρας ισχύος ή ενέργειας της πρίζας.", "label": "Οντότητα Τιμής Ενέργειας" }, "energy_price_static": { - "doc": "Σταθερή τιμή ανά kWh που χρησιμοποιείται για τα στοιχεία κόστους όταν δεν έχει οριστεί παραπάνω οντότητα ζωντανής τιμής.", + "doc": "Σταθερή τιμή ανά kWh, όταν δεν έχει οριστεί οντότητα τιμής.", "label": "Στατική Τιμή Ενέργειας (ανά kWh)" }, "energy_sensor": { - "doc": "Προαιρετικός αθροιστικός μετρητής ενέργειας (total_increasing kWh/Wh, π.χ. ο ίδιος ο μετρητής συνολικής κατανάλωσης της πρίζας). Όταν οριστεί, η αναφερόμενη ενέργεια κάθε κύκλου λαμβάνεται από τη διαφορά αυτού του μετρητή από την αρχή έως το τέλος, γεγονός που αποφεύγει την υποκαταμέτρηση που προκύπτει από την ολοκλήρωση ενός αισθητήρα ισχύος που αναφέρει αργά. Επανέρχεται στην ολοκληρωμένη τιμή αν η ένδειξη λείπει, η μονάδα της είναι άγνωστη ή η διαφορά δεν είναι θετική. Αφήστε το κενό για να συνεχιστεί η ολοκλήρωση του αισθητήρα ισχύος.", + "doc": "Προαιρετικός μετρητής kWh ή Wh (total_increasing, π.χ. ο μετρητής της ίδιας της πρίζας). Η ενέργεια του κύκλου διαβάζεται τότε από αυτόν, κάτι πιο ακριβές από τον υπολογισμό μέσω ενός αργού αισθητήρα ισχύος. Αν η ένδειξή του δεν είναι αξιοποιήσιμη, χρησιμοποιείται η υπολογισμένη τιμή.", "label": "Οντότητα Μετρητή Ενέργειας" }, "expose_debug_entities": { - "doc": "Δημοσιεύστε επιπλέον διαγνωστικές οντότητες HA (εμπιστοσύνη αντιστοίχισης, ασάφεια, εσωτερικά στοιχεία κατάστασης). Το Off διατηρεί τη λίστα οντοτήτων καθαρή για κανονική χρήση.", + "doc": "Προσθέτει διαγνωστικές οντότητες (εμπιστοσύνη αντιστοίχισης, ασάφεια, εσωτερική κατάσταση). Ανενεργό: η λίστα οντοτήτων μένει καθαρή.", "label": "Εμφάνιση Οντοτήτων Αποσφαλμάτωσης" }, "external_end_trigger": { - "doc": "Δυαδικός αισθητήρας του οποίου η αλλαγή κατάστασης σηματοδοτεί το τέλος του κύκλου (π.χ. μια επαφή \"τελειωμένη\" συσκευής ή μια συνοδευτική ενσωμάτωση).", + "doc": "Δυαδικός αισθητήρας του οποίου η αλλαγή σηματοδοτεί το τέλος του κύκλου (π.χ. μια επαφή \"ολοκληρώθηκε\" ή άλλη ενσωμάτωση).", "label": "Οντότητα Εξωτερικού Ενεργοποιητή" }, "external_end_trigger_enabled": { - "doc": "Αφήστε έναν εξωτερικό δυαδικό αισθητήρα να σηματοδοτήσει το τέλος ενός κύκλου, επιπλέον της ενσωματωμένης ανίχνευσης που βασίζεται σε ενέργεια.", + "doc": "Αφήνει έναν δυαδικό αισθητήρα να σηματοδοτεί το τέλος ενός κύκλου, επιπλέον της ανίχνευσης βάσει ισχύος.", "label": "Ενεργοποίηση Εξωτερικού Ενεργοποιητή Λήξης" }, "external_end_trigger_inverted": { - "doc": "Αντιμετωπίστε το κλείσιμο του αισθητήρα σκανδάλης στο OFF (αντί στο ON) ως σήμα τέλους κύκλου.", + "doc": "Το πέρασμα του αισθητήρα σε OFF (όχι σε ON) θεωρείται σήμα τέλους.", "label": "Αντιστροφή Εξωτερικού Ενεργοποιητή (ενεργοποίηση στο ΑΠΕΝΕΡΓΟ)" }, "learning_confidence": { - "doc": "Εάν η βαθμολογία αντιστοίχισης βρίσκεται ανάμεσα σε αυτήν και την Εμπιστοσύνη Αυτόματης Επισήμανσης, μια ειδοποίηση ανατροφοδότησης σάς ζητά να επαληθεύσετε το πρόγραμμα που αναγνωρίστηκε. Κάτω από αυτήν τη βαθμολογία η αντιστοίχιση είναι πολύ αβέβαιη για να εμφανιστεί. Πρέπει να διατηρείται κάτω από την Εμπιστοσύνη Αυτόματης Επισήμανσης.", + "doc": "Η χαμηλότερη βαθμολογία αντιστοίχισης για να επισημανθεί ένας ολοκληρωμένος κύκλος. Ένας ξεκάθαρος νικητής επισημαίνεται· μια οριακή περίπτωση πηγαίνει στην ουρά ελέγχου στους Κύκλους. Κάτω από αυτήν, τίποτα δεν επισημαίνεται ούτε ερωτάται. Κρατήστε την κάτω από την Εμπιστοσύνη Αυτόματης Επισήμανσης.", "label": "Εμπιστοσύνη Μάθησης" }, "linked_device": { - "doc": "Προαιρετικά, τοποθετήστε αυτή τη συσκευή WashData κάτω από μια άλλη συσκευή (π.χ. το έξυπνο βύσμα) στο μητρώο συσκευών HA, που εμφανίζεται ως \"Σύνδεση μέσω ...\".", + "doc": "Προαιρετικό. Εμφανίζει αυτή τη συσκευή WashData κάτω από μια άλλη συσκευή (π.χ. το έξυπνο βύσμα) στο Home Assistant, ως \"Σύνδεση μέσω ...\".", "label": "Ομαδοποίηση κάτω από Συσκευή" }, "min_off_gap": { - "doc": "Εάν το μηχάνημα απενεργοποιηθεί για λιγότερο από αυτό το διάστημα, η σειρά ενεργοποίησης/απενεργοποίησης/ενεργοποίησης αντιμετωπίζεται ως ένας συνεχής κύκλος. Αποτρέπει τον διαχωρισμό των προγραμμάτων εμποτισμού (το μηχάνημα απενεργοποιείται για αρκετά λεπτά κατά τη διάρκεια της πλύσης) σε δύο ξεχωριστούς κύκλους. Ρυθμίστε το μικρότερο από το κενό μεταξύ των φορτίων σας πίσω με πλάτη, εάν θέλετε να υπολογίζονται ως ξεχωριστοί κύκλοι. Οι προεπιλογές τύπου συσκευής προστατεύουν την τυπική παύση εντός του κύκλου για κάθε συσκευή.", + "doc": "Μια περίοδος εκτός λειτουργίας μικρότερη από αυτό δεν χωρίζει τον κύκλο, ώστε οι παύσεις μουλιάσματος να μένουν σε μία πλύση. Κρατήστε το μικρότερο από το κενό ανάμεσα σε διαδοχικά φορτία, αλλιώς αυτά συγχωνεύονται σε έναν κύκλο.", "label": "Ελάχιστο Κενό Απενεργοποίησης" }, "min_power": { - "doc": "Η απόλυτη ελάχιστη ισχύς θεωρείται ενεργή. Οι ενδείξεις κάτω από αυτό αντιμετωπίζονται ως 0 W (αναμονή), φιλτράροντας το φανταστικό φορτίο των έξυπνων βυσμάτων και των LED αναμονής.", + "doc": "Οι μετρήσεις κάτω από αυτό μετρούν ως 0 W (αναμονή). Φιλτράρει την κατανάλωση αδράνειας των έξυπνων βυσμάτων και των LED αναμονής.", "label": "Ελάχιστη Ισχύς" }, "ml_training_enabled": { - "doc": "Μελετήστε περιοδικά τους κύκλους που έχετε εξετάσει κατά τη διάρκεια της νύχτας και προσαρμόστε τα μοντέλα σε αυτό το συγκεκριμένο μηχάνημα. Μια αλλαγή διατηρείται μόνο όταν έχει πραγματικά καλύτερη βαθμολογία σε κύκλους που δεν μένουν, οπότε αυτό μπορεί μόνο να βοηθήσει ή να παραμείνει το ίδιο - ποτέ μην υποχωρήσει.", + "doc": "Επανεκπαιδεύει κατά διαστήματα το μοντέλο ενέργειας με τους ολοκληρωμένους κύκλους αυτού του μηχανήματος. Ένα νέο μοντέλο κρατιέται μόνο αν ξεπερνά την απλή εκτίμηση σε κύκλους δοκιμής.", "label": "Μάθηση από αυτή τη συσκευή" }, "ml_training_hour": { - "doc": "Τοπική ώρα της ημέρας (0-23) για να κάνετε την ολονύκτια λεπτομέρεια. Επιλέξτε μια ώρα ησυχίας, όπως 2 (02:00).", + "doc": "Ώρα της ημέρας (0-23) για τη νυχτερινή εκπαίδευση. Επιλέξτε μια ήσυχη ώρα, π.χ. 2.", "label": "Μάθηση την ώρα" }, "ml_training_interval_days": { - "doc": "Ελέγξτε ξανά για βελτιώσεις το πολύ μία φορά ανά τόσες μέρες.", + "doc": "Επανεκπαίδευση το πολύ μία φορά ανά τόσες ημέρες.", "label": "Έλεγχος το πολύ κάθε" }, "ml_training_min_cycles": { - "doc": "Περιμένετε έως ότου έχουν καταγραφεί τουλάχιστον τόσοι πολλοί κύκλοι πριν από τη λεπτομέρεια, οπότε υπάρχουν αρκετά για να μάθετε από αυτά.", + "doc": "Αναμονή για τουλάχιστον τόσους καταγεγραμμένους κύκλους πριν από την εκπαίδευση.", "label": "Κύκλοι που χρειάζονται πρώτα" }, "name": { - "doc": "Εμφανιζόμενο όνομα που εμφανίζεται στη λίστα ενσωματώσεων HA και στο μητρώο συσκευών.", + "doc": "Το όνομα αυτής της συσκευής στο Home Assistant.", "label": "Όνομα Συσκευής" }, "no_update_active_timeout": { - "doc": "Εάν δεν υπάρχουν ενημερώσεις τροφοδοσίας για τόσο μεγάλο χρονικό διάστημα κατά τη λειτουργία, υποθέστε ότι το βύσμα έπεσε εκτός σύνδεσης και διακόψτε αναγκαστικά για να αποφύγετε έναν κύκλο ζόμπι. Τα προεπιλεγμένα 600 s επιτρέπουν υστέρηση σε σύννεφο ή πλέγμα.", + "doc": "Αν δεν έρθει ενημέρωση ισχύος για τόσο χρόνο κατά τη διάρκεια ενός κύκλου, θεωρείται ότι το βύσμα βγήκε εκτός σύνδεσης και ο κύκλος διακόπτεται αναγκαστικά. Τα προεπιλεγμένα 600 δευτ. καλύπτουν καθυστερήσεις cloud ή δικτύου mesh.", "label": "Λήξη Χρόνου Χωρίς Ενημέρωση" }, "notify_before_end_minutes": { @@ -1333,35 +1302,35 @@ "label": "Προειδοποίηση Πριν το Τέλος" }, "notify_channel": { - "doc": "Όνομα καναλιού ειδοποιήσεων Android για έναρξη/ζωντανά μηνύματα (ελέγχει τον ήχο και την προτεραιότητα ανά κανάλι στην εφαρμογή για κινητά). Το Blank χρησιμοποιεί την προεπιλογή της συνοδευτικής εφαρμογής.", + "doc": "Κανάλι Android για τα μηνύματα έναρξης και ζωντανής προόδου· το κανάλι ορίζει ήχο και προτεραιότητα. Κενό: προεπιλογή της εφαρμογής.", "label": "Κανάλι Android (εκκίνηση/ζωντανά)" }, "notify_cycle_timers": { - "doc": "Ειδοποιήσεις σε συγκεκριμένα λεπτά σε έναν κύκλο (π.χ. για προσθήκη μαλακτικού). Το μήνυμα υποστηρίζει {device}, {program}, {minutes}. Ενεργοποιήστε την αυτόματη παύση για παύση σε αυτό το σημείο και λάβετε μια διαδραστική ειδοποίηση με ένα κουμπί Συνέχιση. συνέχιση μέσω του πίνακα, της υπηρεσίας παύσης/συνέχισης ή της ενέργειας ειδοποίησης.", + "doc": "Ειδοποιήσεις σε ορισμένα λεπτά μέσα στον κύκλο (π.χ. για προσθήκη μαλακτικού). Το μήνυμα υποστηρίζει {device}, {program}, {minutes}. Με την αυτόματη παύση, ο κύκλος μπαίνει και σε παύση εκεί και η ειδοποίηση έχει κουμπί Συνέχεια.", "label": "Χρονόμετρα Κύκλου" }, "notify_finish_channel": { - "doc": "Όνομα καναλιού ειδοποιήσεων Android για το μήνυμα τερματισμού. Το Blank επαναχρησιμοποιεί το κανάλι έναρξης/ζωντανού.", + "doc": "Κανάλι Android για το μήνυμα ολοκλήρωσης. Κενό: χρησιμοποιείται το κανάλι έναρξης/ζωντανής προόδου.", "label": "Κανάλι Android (λήξη)" }, "notify_finish_message": { - "doc": "Κείμενο που αποστέλλεται όταν ολοκληρώνεται ένας κύκλος. Μεταβλητές προτύπου: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}. Τα {time_finished} και {vs_typical} είναι τα πιο χρήσιμα εδώ.", + "doc": "Κείμενο που στέλνεται όταν τελειώνει ένας κύκλος. Μεταβλητές: {device}, {program}, {duration} (λεπτά), {duration_hm} (1 h 05 min), {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}, {status} (completed ή force_stopped· οι κύκλοι που διακόπηκαν δεν στέλνουν τίποτα). Με άλλη μεταβλητή χρησιμοποιείται το προεπιλεγμένο κείμενο.", "label": "Μήνυμα Λήξης" }, "notify_finish_services": { - "doc": "ειδοποιήστε.* οι υπηρεσίες καλούνται όταν τελειώσει ένας κύκλος. Προσθέστε ένα ανά στόχο. αφήστε κενό για καμία ειδοποίηση τερματισμού.", + "doc": "Υπηρεσίες notify.* που καλούνται όταν τελειώνει ένας κύκλος. Αφήστε κενό για καμία.", "label": "Υπηρεσίες Λήξης" }, "notify_fire_events": { - "doc": "Επίσης ενεργοποιήστε τα γεγονότα ha_washdata_* κατά την έναρξη/τέρμα του κύκλου, ώστε να μπορείτε να δημιουργήσετε τους δικούς σας αυτοματισμούς.", + "doc": "Εκπέμπει επίσης γεγονότα ha_washdata_* στην έναρξη και στο τέλος του κύκλου, για τους δικούς σας αυτοματισμούς.", "label": "Ενεργοποίηση Συμβάντων HA για Ειδοποιήσεις" }, "notify_icon": { - "doc": "Το μικρό εικονίδιο που εμφανίζεται στην ειδοποίηση (π.χ. mdi:washing-machine). Το Android το σχεδιάζει στη γραμμή κατάστασης· το iOS το εμφανίζει στη θέση του εικονιδίου της εφαρμογής και απαιτεί τη συνοδευτική εφαρμογή 2026.8 ή νεότερη. Αφήστε κενό για την προεπιλογή της πλατφόρμας.", + "doc": "Μικρό εικονίδιο στην ειδοποίηση (π.χ. mdi:washing-machine). Στέλνεται μόνο σε προορισμούς mobile_app, όχι σε ομάδες notify. Στο iOS αντικαθιστά το εικονίδιο της εφαρμογής και απαιτεί συνοδευτική εφαρμογή 2026.8 ή νεότερη. Κενό: προεπιλογή της πλατφόρμας.", "label": "Εικονίδιο Ειδοποίησης" }, "notify_live_chronometer": { - "doc": "Εμφάνιση ενός χρονοδιακόπτη αντίστροφης μέτρησης ζωντανής ενημέρωσης στην ειδοποίηση (σε πλατφόρμες που το υποστηρίζουν) αντί για στατική εκτίμηση.", + "doc": "Δείχνει ζωντανή αντίστροφη μέτρηση στην ειδοποίηση (όπου υποστηρίζεται) αντί για σταθερή εκτίμηση.", "label": "Χρήση Ζωντανού Χρονόμετρου" }, "notify_live_interval_seconds": { @@ -1369,143 +1338,135 @@ "label": "Διάστημα Ζωντανής Ενημέρωσης" }, "notify_live_overrun_percent": { - "doc": "Εάν ένας κύκλος ξεπεράσει την εκτίμησή του κατά περισσότερο από αυτό το ποσοστό, στείλτε μια ειδοποίηση υπέρβασης.", + "doc": "Οι ζωντανές ενημερώσεις σταματούν όταν ένας κύκλος ξεπεράσει την αναμενόμενη διάρκειά του κατά αυτό το ποσοστό (20 = έως 1,2x του αναμενόμενου χρόνου).", "label": "% Υπέρβαση Ζωντανά Πριν την Ειδοποίηση" }, "notify_live_services": { - "doc": "ειδοποίηση.* οι υπηρεσίες ζητούν ζωντανές ενημερώσεις προόδου ενώ εκτελείται ένας κύκλος. Αφήστε το κενό για να απενεργοποιήσετε τις ειδοποιήσεις ζωντανής προόδου.", + "doc": "Υπηρεσίες notify.* για ζωντανές ενημερώσεις προόδου όσο τρέχει ένας κύκλος. Αφήστε κενό για να τις απενεργοποιήσετε.", "label": "Υπηρεσίες Ζωντανής Προόδου" }, "notify_only_when_home": { - "doc": "Στείλτε ειδοποιήσεις μόνο όταν τουλάχιστον ένα από τα συνδεδεμένα άτομα (παραπάνω) είναι στο σπίτι.", + "doc": "Ειδοποιεί μόνο όταν τουλάχιστον ένα από τα παραπάνω άτομα είναι στο σπίτι.", "label": "Ειδοποίηση Μόνο Όταν Είμαι Σπίτι" }, "notify_people": { - "doc": "πρόσωπο.* οντότητες που χρησιμοποιούνται από το \"Notify Only When Home\" για να αποφασίσουν εάν κάποιος είναι στο σπίτι.", + "doc": "Οντότητες person.* που ελέγχει το \"Ειδοποίηση Μόνο Όταν Είμαι Σπίτι\".", "label": "Άτομα (για Μόνο Όταν Είμαι Σπίτι)" }, "notify_pre_complete_message": { - "doc": "Κείμενο της ειδοποίησης πριν το τέλος / σχεδόν έτοιμο. Μεταβλητές προτύπου: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Κείμενο κάθε ζωντανής ενημέρωσης προόδου. Μεταβλητές προτύπου: {device}, {program}, {minutes} (λεπτά που απομένουν). Με άλλη μεταβλητή χρησιμοποιείται το προεπιλεγμένο κείμενο.", "label": "Μήνυμα Προ-Ολοκλήρωσης" }, "notify_reminder_message": { - "doc": "Κείμενο της υπενθύμισης για εκκρεμή εκφόρτωση. Το κενό χρησιμοποιεί την ενσωματωμένη προεπιλογή. Μεταβλητές προτύπου: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Κείμενο της μοναδικής Προειδοποίησης Πριν το Τέλος. Μεταβλητές προτύπου: {device}, {program}, {minutes} (η ρύθμιση Προειδοποίηση Πριν το Τέλος). Με άλλη μεταβλητή χρησιμοποιείται το προεπιλεγμένο κείμενο.", "label": "Μήνυμα Υπενθύμισης" }, "notify_start_message": { - "doc": "Κείμενο που αποστέλλεται όταν ξεκινά ένας κύκλος. Μεταβλητές προτύπου: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Κείμενο που στέλνεται όταν ξεκινά ένας κύκλος. Μεταβλητές προτύπου: {device}, {program}. Με άλλη μεταβλητή χρησιμοποιείται το προεπιλεγμένο κείμενο.", "label": "Μήνυμα Εκκίνησης" }, "notify_start_services": { - "doc": "ειδοποιήστε.* οι υπηρεσίες καλούνται όταν ξεκινά ένας κύκλος. Προσθέστε ένα ανά στόχο (τηλέφωνο, πίνακα ελέγχου κ.λπ.). αφήστε κενό για ειδοποίηση μη έναρξης.", + "doc": "Υπηρεσίες notify.* που καλούνται όταν ξεκινά ένας κύκλος (τηλέφωνο, πίνακας ελέγχου, ...). Αφήστε κενό για καμία.", "label": "Υπηρεσίες Εκκίνησης" }, "notify_timeout_seconds": { - "doc": "Αυτόματη απόρριψη της ειδοποίησης μετά από τόσα δευτερόλεπτα (σε πλατφόρμες που την υποστηρίζουν). Το 0 το διατηρεί μέχρι να απορριφθεί χειροκίνητα.", + "doc": "Κλείνει την ειδοποίηση μετά από τόσα δευτερόλεπτα (όπου υποστηρίζεται). Με 0 μένει μέχρι να την κλείσετε.", "label": "Αυτόματη Απόρριψη Μετά από" }, "notify_title": { - "doc": "Τίτλος ειδοποίησης. Μεταβλητές προτύπου: {device}, {program}, {duration}, {energy}, {cost}, {date}, {time}, {minutes}.", + "doc": "Τίτλος ειδοποίησης. Αντικαθίσταται μόνο το {device}· με άλλη μεταβλητή χρησιμοποιείται ο προεπιλεγμένος τίτλος.", "label": "Τίτλος Ειδοποίησης" }, "notify_unload_delay_minutes": { - "doc": "Λίγα λεπτά μετά τη λήξη ενός κύκλου προτού στείλετε την υπενθύμιση \"ξεφόρτωση του μηχανήματος\" σε αναμονή. Ορίστε το 0 για να απενεργοποιήσετε την υπενθύμιση.", + "doc": "Λεπτά μετά το τέλος ενός κύκλου πριν από την υπενθύμιση \"αδειάστε το μηχάνημα\". Το 0 την απενεργοποιεί.", "label": "Καθυστέρηση Υπενθύμισης Αποφόρτωσης" }, "notify_unload_repeat": { - "doc": "Στέλνει ξανά την υπενθύμιση αποφόρτωσης κάθε \"Καθυστέρηση Υπενθύμισης Αποφόρτωσης\" λεπτά, μέχρι να επιβεβαιωθεί η αποφόρτωση ή να πατήσετε \"Διακοπή υπενθυμίσεων\" στην ειδοποίηση. Απαιτεί έναν τρόπο επιβεβαίωσης της αποφόρτωσης: μια Οντότητα Αισθητήρα Πόρτας, μια Οντότητα Επιβεβαίωσης Αποφόρτωσης ή την επιλογή \"Χειροκίνητη Επιβεβαίωση Αποφόρτωσης\" παρακάτω. Το κουμπί απόρριψης λειτουργεί σε ειδοποιήσεις της εφαρμογής-συντρόφου (κινητό) του Home Assistant.", + "doc": "Στέλνει ξανά την υπενθύμιση αποφόρτωσης κάθε \"Καθυστέρηση Υπενθύμισης Αποφόρτωσης\" λεπτά, μέχρι να επιβεβαιωθεί η αποφόρτωση ή να πατήσετε \"Διακοπή υπενθυμίσεων\" (εφαρμογή κινητού). Απαιτεί Οντότητα Αισθητήρα Πόρτας, Οντότητα Επιβεβαίωσης Αποφόρτωσης ή \"Χειροκίνητη Επιβεβαίωση Αποφόρτωσης\".", "label": "Επανάληψη μέχρι την αποφόρτωση" }, "off_delay": { - "doc": "Χρόνος αναμονής μετά την πτώση του ρεύματος πριν δηλώσετε ότι ο κύκλος έχει ολοκληρωθεί. Εάν επανέλθει η τροφοδοσία σε αυτό το παράθυρο, ο κύκλος συνεχίζεται απρόσκοπτα - αυτό γεφυρώνει τις παύσεις μεταξύ των σταδίων πλύσης. Τα πλυντήρια πιάτων έχουν μεγάλες φάσεις στεγνώματος (απενεργοποίηση για 20-60 λεπτά), επομένως η καθυστέρηση εκτός λειτουργίας πρέπει να υπερβαίνει αυτήν για να διατηρηθεί ολόκληρο το πλύσιμο+στεγνό σε έναν κύκλο.", + "doc": "Πόση ώρα πρέπει η ισχύς να μείνει χαμηλή πριν ολοκληρωθεί ο κύκλος. Αν η ισχύς επανέλθει μέσα σε αυτόν τον χρόνο, ο κύκλος συνεχίζεται. Τα πλυντήρια πιάτων μπορεί να μένουν αδρανή 20-60 λεπτ. στο στέγνωμα, οπότε ορίστε το μεγαλύτερο από αυτό.", "label": "Καθυστέρηση Απενεργοποίησης" }, "pause_cuts_power": { - "doc": "Όταν ένας κύκλος τίθεται σε παύση, απενεργοποιήστε επίσης την οντότητα διακόπτη παρακάτω. Μόνο για συσκευές των οποίων το βύσμα μπορεί να κοπεί με ασφάλεια στη μέση του κύκλου.", + "doc": "Όταν ένας κύκλος μπαίνει σε παύση, απενεργοποιεί και την Οντότητα Διακόπτη παρακάτω. Μόνο για συσκευές που μπορούν με ασφάλεια να μείνουν χωρίς ρεύμα στη μέση του κύκλου.", "label": "Η Παύση Διακόπτει Επίσης Ρεύμα (μέσω διακόπτη)" }, "power_off_delay": { - "doc": "Για πόσο χρόνο πρέπει να παραμείνει η ισχύς κάτω από το Κατώφλι Απενεργοποίησης μετά την ολοκλήρωση ενός κύκλου, προτού η κατάσταση επιστρέψει σε Ανενεργό. Χρησιμοποιείται μόνο όταν το Κατώφλι Απενεργοποίησης είναι πάνω από 0. Ελέγχεται στον ρυθμό του παρασκηνίου, οπότε η πραγματική καθυστέρηση στρογγυλοποιείται προς τα πάνω στον επόμενο κύκλο λήξης κατάστασης.", + "doc": "Πόση ώρα πρέπει η ισχύς να μείνει κάτω από το Κατώφλι Απενεργοποίησης μετά από έναν κύκλο πριν η κατάσταση επιστρέψει σε Ανενεργό. Χρησιμοποιείται μόνο όταν αυτό το κατώφλι είναι πάνω από 0. Ελέγχεται περιοδικά, οπότε η πραγματική αναμονή μπορεί να είναι λίγο μεγαλύτερη.", "label": "Καθυστέρηση Απενεργοποίησης" }, "power_off_threshold_w": { - "doc": "Προαιρετική ανίχνευση κατάστασης Ανενεργό με βάση την ισχύ. Όταν είναι πάνω από 0, μόλις ολοκληρωθεί ένας κύκλος και η ισχύς παραμείνει κάτω από αυτό το επίπεδο για το διάστημα της Καθυστέρησης Απενεργοποίησης, η συσκευή θεωρείται ότι έχει σβήσει και η κατάσταση επιστρέφει σε Ανενεργό. Αφήστε το στο 0 για απενεργοποίηση (προεπιλογή). Ορίστε το πάνω από το πραγματικό κατώτατο όριο σβησμένης συσκευής και κάτω από το Κατώφλι Τερματισμού και την κατανάλωση αναμονής της συσκευής σας όταν είναι ολοκληρωμένη αλλά ακόμη αναμμένη· αν δεν είναι κάτω από το Κατώφλι Τερματισμού, αγνοείται. Όταν είναι ενεργό, αντικαθιστά την Καθυστέρηση Επαναφοράς Προόδου για την επιστροφή σε Ανενεργό, ώστε μια ολοκληρωμένη συσκευή να παραμένει σε Ολοκληρώθηκε/Καθαρός μέχρι να σβήσει πραγματικά.", + "doc": "Μετά από έναν κύκλο, ισχύς κάτω από αυτό για την Καθυστέρηση Απενεργοποίησης θέτει την κατάσταση σε Ανενεργό, αντί να το κάνει η Καθυστέρηση Επαναφοράς Προόδου. Ορίστε το πάνω από την κατανάλωση της σβηστής συσκευής, κάτω από την αναμονή και το Κατώφλι Τερματισμού. Το 0 το απενεργοποιεί (προεπιλογή).", "label": "Κατώφλι Απενεργοποίησης" }, "power_sensor": { - "doc": "Η οντότητα αισθητήρα που αναφέρει την ενεργή ισχύ σε watt για αυτήν τη συσκευή (π.χ. sensor.washer_power). Η ανίχνευση όλων των κύκλων βασίζεται σε αυτό το σήμα.", + "doc": "Ο αισθητήρας που αναφέρει τη ζωντανή ισχύ αυτής της συσκευής σε watt (π.χ. sensor.washer_power). Όλη η ανίχνευση βασίζεται σε αυτόν.", "label": "Αισθητήρας Ισχύος" }, - "profile_duration_tolerance": { - "doc": "Η ζώνη +/- γύρω από μια μέση διάρκεια προφίλ που χρησιμοποιείται κατά την αντιστοίχιση. 0,25 σημαίνει ότι ένα προφίλ 60 λεπτών ταιριάζει με κύκλους 45-75 λεπτών.", - "label": "Ανοχή Διάρκειας Προφίλ" - }, "profile_match_interval": { - "doc": "Πόσο συχνά επιχειρείτε αντιστοίχιση προφίλ κατά τη διάρκεια ενός κύκλου τρεξίματος. Τα προεπιλεγμένα 300 δευτερόλεπτα (5 λεπτά) εξισορροπούν την ταχύτητα ανίχνευσης και τη CPU.", + "doc": "Πόσο συχνά ένας κύκλος σε εξέλιξη συγκρίνεται με τα προφίλ. Προεπιλογή 300 δευτ. (5 λεπτ.).", "label": "Διάστημα Αντιστοίχισης" }, "profile_match_max_duration_ratio": { - "doc": "Μέγιστο μήκος κύκλου σε σχέση με το προφίλ. 1.3 σημαίνει ότι ένας κύκλος πρέπει να είναι κάτω από το 130% της διάρκειας του προφίλ για να ταιριάζει.", + "doc": "Ένα πρόγραμμα απορρίπτεται μόλις ο κύκλος διαρκέσει περισσότερο από αυτό το πολλαπλάσιο της τυπικής του διάρκειας. 1,8: ένας κύκλος 80% μεγαλύτερος από το πρόγραμμα δεν αντιστοιχίζεται πλέον σε αυτό.", "label": "Μέγιστη Αναλογία Διάρκειας" }, "profile_match_min_duration_ratio": { - "doc": "Ελάχιστη διάρκεια κύκλου σε σχέση με το προφίλ. 0,9 σημαίνει ότι ένας κύκλος πρέπει να είναι τουλάχιστον το 90% της διάρκειας του προφίλ για να ταιριάζει.", + "doc": "Ένα πρόγραμμα εξετάζεται μόνο αφού ο κύκλος διαρκέσει αυτό το κλάσμα της τυπικής του διάρκειας. 0,10: ένα πρόγραμμα 2 ωρών γίνεται υποψήφιο μετά από 12 λεπτά. Δεν εφαρμόζεται στα πρώτα 15 λεπτά ενός κύκλου.", "label": "Ελάχιστη Αναλογία Διάρκειας" }, "profile_match_threshold": { - "doc": "Απαιτείται ελάχιστος βαθμός ομοιότητας (0-1) στο τέλος του κύκλου για την αποδοχή μιας αναγνώρισης προγράμματος. Αυξήστε το για να μειώσετε τις λανθασμένες ταυτοποιήσεις. χαμηλώστε το εάν τα προγράμματα του μηχανήματός σας δεν ταιριάζουν. Το προεπιλεγμένο 0,4 είναι ένα συντηρητικό σημείο εκκίνησης.", + "doc": "Ζωντανή βαθμολογία αντιστοίχισης που χρειάζεται ένας κύκλος πριν ο Έξυπνος Τερματισμός ή η ολοκλήρωση κατά τσακίσματος μπορέσουν να τον τερματίσουν νωρίς. Δεν αλλάζει το πρόγραμμα που εμφανίζεται. Αυξήστε την αν αδύναμες αντιστοιχίσεις τερματίζουν κύκλους νωρίς· μειώστε την αν σαφείς αντιστοιχίσεις περιμένουν την καθυστέρηση απενεργοποίησης.", "label": "Κατώφλι Αντιστοίχισης" }, "profile_unmatch_threshold": { - "doc": "Εάν μια ζωντανή αντιστοίχιση στη μέση του κύκλου πέσει κάτω από αυτήν τη βαθμολογία, η προσωρινή αναγνώριση διαγράφεται. Διατηρήστε την λίγο κάτω από το Κατώφλι Αντιστοίχισης, ώστε μια σύντομη πτώση της ομοιότητας να μην επαναφέρει την ένδειξη σε Χωρίς αντιστοίχιση.", + "doc": "Αν η ζωντανή βαθμολογία αντιστοίχισης πέσει κάτω από αυτήν, η τρέχουσα εκτίμηση προγράμματος διαγράφεται. Κρατήστε την λίγο κάτω από το Κατώφλι Αντιστοίχισης, ώστε μια σύντομη πτώση να μην την επαναφέρει.", "label": "Κατώφλι Μη-Αντιστοίχισης" }, "progress_reset_delay": { - "doc": "Μετά την ολοκλήρωση, κρατήστε την πρόοδο στο 100% για τόσο μεγάλο χρονικό διάστημα, ώστε το Ολοκληρώθηκε να είναι ορατό στους πίνακες εργαλείων πριν επαναφέρετε την κατάσταση αδράνειας.", + "doc": "Μετά από έναν κύκλο, η πρόοδος μένει στο 100% για τόσο χρόνο πριν επανέλθει σε αδράνεια.", "label": "Καθυστέρηση Επαναφοράς Προόδου" }, "pump_stuck_duration": { - "doc": "Δευτερόλεπτα μια αντλία μπορεί να λειτουργεί συνεχώς πριν επισημανθεί ως πιθανώς κολλημένη (πυρίζει το συμβάν κολλημένης αντλίας).", + "doc": "Πόση ώρα μπορεί μια αντλία να λειτουργεί συνεχώς πριν ενεργοποιηθεί το συμβάν κολλημένης αντλίας.", "label": "Διάρκεια Κολλημένης Αντλίας" }, "sampling_interval": { - "doc": "Αναμενόμενος χρόνος μεταξύ των μετρήσεων του αισθητήρα - χρησιμοποιείται για το μέγεθος του παραθύρου εξομάλυνσης και τη σωστή έναρξη της αναπήδησης. Κάθε ενημέρωση αισθητήρα καταγράφεται ανεξάρτητα από αυτήν την τιμή. βαθμονομεί μόνο τους κατάντη υπολογισμούς. Η μηχανή προτάσεων μετρά τον πραγματικό ρυθμό του αισθητήρα σας από προηγούμενους κύκλους και το ρυθμίζει αυτόματα.", + "doc": "Ελάχιστος χρόνος μεταξύ επεξεργασμένων μετρήσεων. Μετρήσεις που φτάνουν νωρίτερα παραλείπονται (οι χαμηλές μετρήσεις κατά τη διάρκεια ενός κύκλου κρατιούνται πάντα). Ορίστε το στο διάστημα αναφοράς της πρίζας σας ή χαμηλότερα· μια υψηλότερη τιμή απλώς πετά μετρήσεις.", "label": "Διάστημα Δειγματοληψίας" }, "save_debug_traces": { - "doc": "Αποθηκεύστε το πλήρες ίχνος ισχύος και τα αντίστοιχα δεδομένα εντοπισμού σφαλμάτων για κάθε κύκλο. Χρήσιμο για την αντιμετώπιση προβλημάτων, αλλά αυξάνει το μέγεθος του αποθηκευτικού χώρου.", + "doc": "Αποθηκεύει επίσης σε κάθε κύκλο τα δεδομένα αποσφαλμάτωσης της αντιστοίχισης (τις βαθμολογίες κάθε υποψηφίου), για αντιμετώπιση προβλημάτων. Προσθέτει περίπου 20 KB ανά κύκλο.", "label": "Αποθήκευση Ιχνών Αποσφαλμάτωσης" }, "smart_termination_duration_ratio": { - "doc": "Πόσο μέσα στην αναμενόμενη διάρκεια του αντιστοιχισμένου προγράμματος πρέπει να έχει προχωρήσει ένας κύκλος προτού ο Έξυπνος Τερματισμός μπορέσει να τον τερματίσει νωρίς μόλις πέσει η ισχύς. Η αναμενόμενη διάρκεια είναι ο μέσος όρος του προγράμματος, οπότε σε συσκευές των οποίων ο χρόνος λειτουργίας ποικίλλει πολύ (πλυντήρια ρούχων με κρύο νερό παροχής τον χειμώνα έναντι ζεστού το καλοκαίρι, στεγνωτήρια με αισθητήρα στεγνώματος, προγράμματα που εξαρτώνται από το φορτίο) περίπου οι μισές από όλες τις εκτελέσεις τελειώνουν συντομότερα από αυτόν τον μέσο όρο και δεν λαμβάνουν ποτέ τον γρήγορο τερματισμό, τελειώνοντας μόνο μέσω του εφεδρικού χρονικού ορίου με καθυστέρηση λεπτών. Χαμηλώστε αυτή την τιμή (π.χ. 0,85) σε αυτά τα μηχανήματα ώστε ο πρόωρος τερματισμός να εξακολουθεί να ενεργοποιείται. Αυξήστε την προς το 1,0 για να είστε πιο συντηρητικοί. Αφήστε το κενό για την προεπιλογή (0,98, ή 0,99 για πλυντήρια πιάτων). Μπορεί μόνο να τερματίσει έναν κύκλο νωρίτερα, ποτέ αργότερα, και δεν ενεργοποιείται ποτέ σε μια ασαφή αντιστοίχιση ή αντιστοίχιση χαμηλής εμπιστοσύνης.", + "doc": "Κλάσμα της μέσης διάρκειας του προγράμματος που πρέπει να φτάσει ένας κύκλος πριν ο Έξυπνος Τερματισμός μπορέσει να τον τερματίσει νωρίς. Αν οι χρόνοι λειτουργίας διαφέρουν (θερμοκρασία νερού, στέγνωμα με αισθητήρα, μέγεθος φορτίου), χαμηλώστε το (π.χ. 0,85). Αφήστε κενό για 0,98 (0,99 στα πλυντήρια πιάτων).", "label": "Αναλογία Έξυπνου Τερματισμού" }, - "smoothing_window": { - "doc": "Πόσο εξομαλύνεται το πρωτογενές σήμα ισχύος. Το Low (2) ανταποκρίνεται αλλά είναι θορυβώδες. υψηλό (5) εξομαλύνει τις αιχμές αλλά προσθέτει υστέρηση.", - "label": "Παράθυρο Εξομάλυνσης" - }, "start_duration_threshold": { - "doc": "Η ισχύς πρέπει να παραμείνει πάνω από το όριο εκκίνησης για τόσο μεγάλο χρονικό διάστημα για να επιβεβαιωθεί μια πραγματική εκκίνηση, αποτρέποντας τις εναλλαγές ενεργοποίησης/απενεργοποίησης κλασματικού δευτερολέπτου από την έναρξη ενός κύκλου.", + "doc": "Η ισχύς πρέπει να μείνει πάνω από το Κατώφλι Εκκίνησης τόσο χρόνο πριν ξεκινήσει ένας κύκλος, ώστε να αγνοούνται σύντομες εναλλαγές.", "label": "Διάρκεια Εκκίνησης" }, "start_energy_threshold": { - "doc": "Ενέργεια (ισχύς x χρόνος) πρέπει να καταναλώσει η συσκευή πριν τη λειτουργία. Μια σύντομη ακίδα υψηλής ισχύος έχει πολύ χαμηλή ενέργεια και αγνοείται, αποτρέποντας τις λανθασμένες εκκινήσεις.", + "doc": "Η ενέργεια που πρέπει να καταναλώσει η συσκευή πριν ο κύκλος θεωρηθεί σε λειτουργία. Μια σύντομη αιχμή ισχύος καταναλώνει λίγη ενέργεια, οπότε δεν ξεκινά κύκλο.", "label": "Ενέργεια Εκκίνησης" }, "start_threshold_w": { - "doc": "Η ισχύς πρέπει να ανέβει πάνω από αυτό το επίπεδο για να επιβεβαιωθεί ότι ένας κύκλος έχει ξεκινήσει. Η ρύθμιση του σε πολύ χαμηλά επίπεδα προκαλεί λανθασμένες εκκινήσεις από την τροφοδοσία αναμονής. Τα προγράμματα πολύ υψηλά και αργής εκκίνησης (ψυχρή πλήρωση) χάνονται. Ο κινητήρας προτάσεων το θέτει ακριβώς πάνω από τη χαμηλότερη ενεργή ισχύ του μηχανήματος που έχει παρατηρηθεί.", + "doc": "Η ισχύς πρέπει να ανέβει πάνω από αυτό για να ξεκινήσει ένας κύκλος. Πολύ χαμηλό: η ισχύς αναμονής προκαλεί ψευδείς εκκινήσεις. Πολύ υψηλό: χάνονται προγράμματα με αργή εκκίνηση (κρύο γέμισμα).", "label": "Κατώφλι Εκκίνησης" }, "stop_threshold_w": { - "doc": "Η ισχύς πρέπει να πέσει κάτω από αυτό το επίπεδο προτού ξεκινήσει η αντίστροφη μέτρηση εκτός καθυστέρησης. Ρυθμίστε το κάτω από το όριο έναρξης - το κενό μεταξύ τους είναι η ζώνη υστέρησης που αποτρέπει το τρεμόπαιγμα. Εάν ρυθμιστεί πολύ ψηλά, οι φάσεις χαμηλής ισχύος (κρατήματα ξεβγάλματος, αντιτσαλάκωση) ενεργοποιούν λανθασμένα την τελική ακολουθία.", + "doc": "Η ισχύς πρέπει να πέσει κάτω από αυτό πριν αρχίσει να μετρά η καθυστέρηση απενεργοποίησης. Κρατήστε το κάτω από το Κατώφλι Εκκίνησης. Αν είναι πολύ υψηλό, φάσεις χαμηλής ισχύος (παύσεις ξεβγάλματος, κατά τσακίσματος) ξεκινούν κατά λάθος την αντίστροφη μέτρηση τέλους.", "label": "Κατώφλι Τερματισμού" }, "switch_entity": { - "doc": "Ο προαιρετικός διακόπτης απενεργοποιήθηκε κατά την παύση και ενεργοποιήθηκε ξανά κατά τη συνέχιση, που χρησιμοποιείται μαζί με το \"Η παύση διακόπτει επίσης το ρεύμα\".", + "doc": "Προαιρετικός διακόπτης που σβήνει στην παύση και ανάβει ξανά στη συνέχιση, με το \"Η Παύση Διακόπτει Επίσης Ρεύμα\".", "label": "Οντότητα Διακόπτη" }, "watchdog_interval": { - "doc": "Πόσο συχνά ελέγχει το παρασκήνιο παρακολούθησης για στάσιμους αισθητήρες και παρελθόντα χρονικά όρια. Προεπιλογή 30 δευτ.", + "doc": "Πόσο συχνά ο έλεγχος στο παρασκήνιο ψάχνει για αισθητήρες που κόλλησαν και λήξεις χρόνου. Προεπιλογή 30 δευτ.", "label": "Διάστημα Watchdog" }, "notify_milestone_message": { @@ -1513,139 +1474,94 @@ "label": "Μήνυμα ορόσημου" }, "notify_milestones": { - "doc": "Αριθμοί κύκλων χωρισμένοι με κόμμα που ενεργοποιούν μια εφάπαξ εορταστική ειδοποίηση όταν επιτευχθούν (π.χ. 50, 100, 500, 1000). Το κενό απενεργοποιεί τις ειδοποιήσεις ορόσημων.", + "doc": "Αριθμοί κύκλων που ενεργοποιούν μια εφάπαξ εορταστική ειδοποίηση, χωρισμένοι με κόμμα (π.χ. 50, 100, 500, 1000). Κενό: απενεργοποιημένο.", "label": "Ορόσημα κύκλων" }, "notify_quiet_end_hour": { - "doc": "Τέλος του παραθύρου μην ενοχλείτε (0-23). Οι ειδοποιήσεις που κρατήθηκαν παραδίδονται αυτή την ώρα. Αφήστε κενό για απενεργοποίηση.", + "doc": "Τέλος του παραθύρου μην ενοχλείτε (0-23)· τότε στέλνονται οι ειδοποιήσεις σε αναμονή. Κενό: απενεργοποιημένο.", "label": "Τέλος ωρών ησυχίας" }, "notify_quiet_start_hour": { - "doc": "Έναρξη ενός παραθύρου μην ενοχλείτε (0-23). Οι ειδοποιήσεις ολοκλήρωσης, υπενθύμισης και καθαρών ρούχων που θα ενεργοποιούνταν κατά τις ώρες ησυχίας κρατούνται και παραδίδονται όταν λήξει το παράθυρο. Αφήστε κενό για απενεργοποίηση. Υποστηρίζει παράθυρα που διασχίζουν τα μεσάνυχτα (π.χ. έναρξη 22, τέλος 7).", + "doc": "Έναρξη ενός παραθύρου μην ενοχλείτε (0-23). Οι ειδοποιήσεις ολοκλήρωσης, υπενθύμισης και καθαρών ρούχων σε αυτό το παράθυρο κρατούνται μέχρι να λήξει. Μπορεί να περνά τα μεσάνυχτα (π.χ. 22 έως 7). Κενό: απενεργοποιημένο.", "label": "Έναρξη ωρών ησυχίας" }, "peak_rate_message": { - "doc": "Προαιρετικό προσαρμοσμένο κείμενο για τη συμβουλή τιμής αιχμής που προσαρτάται στην ειδοποίηση έναρξης. Μεταβλητές προτύπου: {device}, {price}. Το κενό χρησιμοποιεί την ενσωματωμένη προεπιλογή.", + "doc": "Προσαρμοσμένο κείμενο για τη συμβουλή τιμής αιχμής. Μεταβλητές προτύπου: {device}, {price}. Κενό: η προεπιλογή.", "label": "Μήνυμα τιμής αιχμής" }, "peak_rate_threshold": { - "doc": "Όταν ξεκινά ένας κύκλος και η τρέχουσα τιμή ανά kWh είναι στην ή πάνω από αυτή την τιμή, προσαρτά μια συμβουλή τιμής αιχμής στην ειδοποίηση έναρξης. Το 0 ή κενό απενεργοποιεί τη συμβουλή.", + "doc": "Αν η τιμή ανά kWh είναι ίση ή πάνω από αυτήν όταν ξεκινά ένας κύκλος, προστίθεται συμβουλή τιμής αιχμής στην ειδοποίηση έναρξης. Το 0 ή κενό την απενεργοποιεί.", "label": "Κατώφλι τιμής αιχμής (ανά kWh)" }, - "dtw_bandwidth": { - "label": "Ανοχή αντιστοίχισης σχήματος", - "doc": "Πόσο μπορεί ο μηχανισμός αντιστοίχισης να παραμορφώσει χρονικά έναν κύκλο ώστε να ταιριάξει με το σχήμα ενός αποθηκευμένου προγράμματος. Υψηλότερη τιμή ανέχεται μεγαλύτερη διακύμανση ταχύτητας. Πολύ υψηλή κινδυνεύει να αντιστοιχίσει λάθος πρόγραμμα." - }, - "corr_weight": { - "label": "Βάρος σχήματος έναντι επιπέδου", - "doc": "Ισορροπία μεταξύ του σχήματος της καμπύλης και του απόλυτου επιπέδου ισχύος κατά τη βαθμολόγηση μιας αντιστοίχισης. Υψηλότερη τιμή ευνοεί το σχήμα. Χαμηλότερη ευνοεί την αντιστοίχιση του επιπέδου ισχύος." - }, - "duration_weight": { - "label": "Βάρος διάρκειας", - "doc": "Πόσο ισχυρά προτιμάται ένα πρόγραμμα του οποίου η τυπική διάρκεια ταιριάζει με αυτόν τον κύκλο." - }, - "energy_weight": { - "label": "Βάρος ενέργειας", - "doc": "Πόσο ισχυρά προτιμάται ένα πρόγραμμα του οποίου η τυπική κατανάλωση ενέργειας ταιριάζει με αυτόν τον κύκλο." - }, "store_brand": { "label": "Μάρκα Συσκευής", - "doc": "Προαιρετικό. Η μάρκα της συσκευής, επιλεγμένη από τον κατάλογο της κοινότητας. Χρησιμοποιείται για την εύρεση και κοινοποίηση αντίστοιχων καταγραφών αναφοράς. Αφήστε το κενό αν δεν χρησιμοποιείτε διαδικτυακές λειτουργίες." + "doc": "Προαιρετικό. Η μάρκα της συσκευής σας από τον κατάλογο της κοινότητας, για εύρεση και κοινοποίηση καταγραφών αναφοράς. Αφήστε κενό αν δεν χρησιμοποιείτε διαδικτυακές λειτουργίες." }, "store_model": { "label": "Μοντέλο Συσκευής", - "doc": "Προαιρετικό. Το μοντέλο της συσκευής, επιλεγμένο από τον κατάλογο της κοινότητας μόλις οριστεί μια μάρκα. Αν το μοντέλο σας δεν αναφέρεται, μπορείτε να το προσθέσετε στον κατάλογο." + "doc": "Προαιρετικό. Το μοντέλο της συσκευής σας από τον κατάλογο της κοινότητας (επιλέξτε πρώτα μάρκα). Αν δεν υπάρχει στη λίστα, μπορείτε να το προσθέσετε." }, "show_contributor": { "doc": "Εμφάνιση της ένδειξης \"από <συνεισφέρων>\" στις κοινοτικές συσκευές και τους κύκλους αναφοράς." }, - "enable_phase_matching": { - "label": "Υπολειπόμενος χρόνος με επίγνωση φάσεων", - "doc": "Χωρίστε κάθε κύκλο που εκτελείται σε φάσεις (θέρμανση, πλύση, στύψιμο) και κατανείμετε τον υπολειπόμενο χρόνο ανά φάση, συνδυασμένο με την κλασική εκτίμηση - στηριζόμενοι στην κατανομή ανά φάση στην αρχή του κύκλου και στην κλασική εκτίμηση κοντά στο τέλος. Αυτό προσαρμόζει την αντίστροφη μέτρηση στο πόση ώρα πραγματικά θερμαίνεται και λειτουργεί η μηχανή σας, κάτι που είναι πιο εμφανές στο πρώτο μισό ενός κύκλου. Off = μόνο η κλασική εκτίμηση. Επηρεάζεται μόνο η ένδειξη υπολειπόμενου χρόνου· η αντιστοίχιση προγραμμάτων και η ανίχνευση κύκλου παραμένουν αμετάβλητες." - }, - "keep_min_score": { - "label": "Ελάχ. Βαθμός Αντιστοίχισης", - "doc": "Ελάχιστη βαθμολογία ομοιότητας που χρειάζεται ένας υποψήφιος για να παραμείνει στη διεκδίκηση της αντιστοίχισης. Η προεπιλογή 0.1 είναι εκ προθέσεως επιεικής - επιτρέπει ακόμη και αδύναμους υποψηφίους, αφήνοντας τα επόμενα στάδια να βρουν την καλύτερη αντιστοίχιση. Αυξήστε την για να αποκλείονται νωρίτερα τα απίθανα προφίλ, μειώστε την για να διευρύνετε την αρχική ομάδα υποψηφίων." - }, - "dtw_blend": { - "label": "Ανάμειξη DTW", - "doc": "Σε ποιο βαθμό ο βαθμός ευθυγράμμισης DTW αντικαθιστά τον βαθμό πυρήνα Σταδίου 2. 0 = μόνο ο βαθμός Σταδίου 2, 1 = μόνο ο βαθμός DTW, 0.5 (προεπιλογή) = ισόρροπη ανάμειξη. Αυξήστε για μεγαλύτερη εξάρτηση από ευθυγράμμιση παραμόρφωσης όταν τα προγράμματα έχουν παρόμοια επίπεδα ισχύος αλλά διαφορετικά χρονικά πρότυπα." - }, - "dtw_ensemble_w": { - "label": "Σύνολο DTW", - "doc": "Στη λειτουργία συνόλου DTW (η προεπιλογή), το βάρος στο κλιμακωμένο L1 έναντι παραγώγου DTW (DDTW). 1.0 = πλήρως κλιμακωμένο-L1, 0 = πλήρως DDTW, 0.7 = προεπιλογή. Η παραλλαγή κλιμακωμένου-L1 συγκρίνει επίπεδα ισχύος; η παραγώγου παραλλαγή ανταποκρίνεται στο πώς μεταβάλλεται η ισχύς. Το σύνολο συνδυάζει και τα δύο." - }, - "dtw_ddtw_scale": { - "label": "Κλίμακα DDTW", - "doc": "Απόσταση ημίσεος κορεσμού για τη βαθμολόγηση παραγώγου DTW. Η βαθμολογία υποδιπλασιάζεται όταν η απόσταση DDTW ισούται με αυτήν την τιμή. Μικρότερη = μεγαλύτερη ευαισθησία σε διαφορές σχήματος. Η προεπιλογή 30 είναι βαθμονομημένη στα τυπικά ίχνη ισχύος οικιακών συσκευών. Επηρεάζει μόνο τις λειτουργίες DTW συνόλου και ddtw." - }, - "dtw_refine_top_n": { - "label": "Αριθμός Εκλέπτυνσης DTW", - "doc": "Πόσοι κορυφαίοι υποψήφιοι Σταδίου 2 επαναβαθμολογούνται από DTW. Η DTW είναι πιο ακριβή υπολογιστικά, οπότε εφαρμόζεται μόνο στους καλύτερους N υποψηφίους. Προεπιλογή 5. Αυξήστε (σε 7-9) αν το σωστό προφίλ φτάνει μερικές φορές μόνο στη 4η ή 5η θέση μετά το Στάδιο 2 - η DTW μπορεί να το σώσει. Η μείωση επιταχύνει ελαφρώς την αντιστοίχιση." - }, - "duration_scale": { - "label": "Ευαισθησία Διάρκειας", - "doc": "Αυστηρότητα της ποινής συμφωνίας διάρκειας Σταδίου 4. Αυτός είναι ο λόγος λογαρίθμου στον οποίο ο βαθμός συμφωνίας υποδιπλασιάζεται. Μικρότερος = αυστηρότερος: μια ασυμφωνία διάρκειας επηρεάζει περισσότερο. Η προεπιλογή 0.175 αντιστοιχεί σε ανοχή διάρκειας περίπου 18% στο μισό βάρος. Χρησιμοποιήστε μαζί με το Βάρος Διάρκειας." - }, - "energy_scale": { - "label": "Ευαισθησία Ενέργειας", - "doc": "Αυστηρότητα της ποινής συμφωνίας ενέργειας Σταδίου 4. Μικρότερος = αυστηρότερος: μια ασυμφωνία ενέργειας επηρεάζει περισσότερο. Η προεπιλογή 0.25 είναι εκ προθέσεως πιο επιεικής από την Ευαισθησία Διάρκειας επειδή η ενέργεια ποικίλλει με το φορτίο. Χρησιμοποιήστε μαζί με το Βάρος Ενέργειας." - }, "power_profile_interval_min": { "label": "Διάστημα προφίλ ισχύος", - "doc": "Το μέγεθος κουβά για το χαρακτηριστικό αισθητήρα power_profile ανά προφίλ (ο επίπεδος πίνακας μέσων βατ ανά θέση που καταναλώνουν εξωτερικοί σχεδιαστές όπως το EMHASS και το tibber_prices). Μικρότεροι κουβάδες διατηρούν τις σύντομες κορυφές ισχύος έντονες· μεγαλύτεροι κουβάδες εξομαλύνουν το σχήμα. Προεπιλογή 15 λεπτά. Μόνο κατά την ανάγνωση· δεν επηρεάζει την ανίχνευση." + "doc": "Μέγεθος διαστήματος ομαδοποίησης του χαρακτηριστικού power_profile (μέσα watt ανά διάστημα) που χρησιμοποιούν σχεδιαστές όπως το EMHASS και το tibber_prices. Μικρότερο κρατά τις σύντομες κορυφές· μεγαλύτερο εξομαλύνει την καμπύλη. Προεπιλογή 15 λεπτ. Δεν επηρεάζει την ανίχνευση." }, "notify_live_silent": { "label": "Αθόρυβες Ζωντανές Ενημερώσεις", - "doc": "Μόνο iOS. Ανανεώνει αθόρυβα τη ζωντανή πρόοδο: κάθε ενημέρωση φτάνει χωρίς ήχο ή δόνηση, με χαμηλότερη προτεραιότητα που εξοικονομεί μπαταρία και ενδέχεται να ομαδοποιηθεί για λίγο από το iOS. Η πρώτη ενημέρωση κάθε κύκλου (αυτή που ξεκινά το Live Activity) και οι ειδοποιήσεις έναρξης/ολοκλήρωσης εξακολουθούν να ειδοποιούν κανονικά. Απενεργοποιήστε το για να ειδοποιείστε σε κάθε ενημέρωση." + "doc": "Μόνο iOS. Ανανεώνει αθόρυβα τη ζωντανή πρόοδο, με προτεραιότητα εξοικονόμησης μπαταρίας που το iOS μπορεί να ομαδοποιεί. Η πρώτη ενημέρωση και οι ειδοποιήσεις έναρξης/ολοκλήρωσης ειδοποιούν κανονικά. Ανενεργό: κάθε ενημέρωση ειδοποιεί." }, "notify_live_sticky": { "label": "Διατήρηση ζωντανής ειδοποίησης μετά το πάτημα", - "doc": "Μόνο Android. Κάντε την ειδοποίηση ζωντανής προόδου μόνιμη (sticky) ώστε το πάτημά της να μη σβήνει το ενεργό νήμα. Απενεργοποιημένο διατηρεί την προεπιλεγμένη συμπεριφορά όπου το πάτημα σβήνει την ειδοποίηση." + "doc": "Μόνο Android. Η ειδοποίηση ζωντανής προόδου μένει όταν την πατάτε. Ανενεργό: το πάτημα την κλείνει." }, "notify_live_click_action": { "label": "Στόχος πατήματος ειδοποίησης", - "doc": "Πού οδηγεί το πάτημα μιας ειδοποίησης από αυτή τη συσκευή, σε Android και iOS. Αν μείνει κενό, ανοίγει ο πίνακας WashData σε αυτή τη συσκευή. Δώστε μια διαδρομή πίνακα ελέγχου (π.χ. /lovelace/laundry) ή ένα πλήρες URL για να το στείλετε αλλού, ή \"none\" για να μην υπάρχει καθόλου στόχος πατήματος." + "doc": "Τι ανοίγει το πάτημα στην ειδοποίηση αυτής της συσκευής. Κενό: ο πίνακας WashData σε αυτή τη συσκευή. Δώστε διαδρομή πίνακα ελέγχου (π.χ. /lovelace/laundry) ή URL, ή \"none\" για καμία ενέργεια πατήματος." }, "door_opens_at_end": { "label": "Η πόρτα ανοίγει αυτόματα στο τέλος", - "doc": "Για πλυντήρια πιάτων που ανοίγουν την πόρτα στο τέλος του κύκλου για στέγνωμα (AirDry και παρόμοια). Με αυτό ενεργό, το άνοιγμα της πόρτας κατά τη διάρκεια ενεργού κύκλου δεν τον παγώνει πλέον για πάντα· αντίθετα, εάν η πόρτα παραμείνει ανοιχτή για τη διάρκεια αναμονής παρακάτω, το WashData θεωρεί τον κύκλο ολοκληρωμένο. Μια σύντομη άνοιξη (προσθήκη σκεύους) αγνοείται. Απαιτείται οντότητα αισθητήρα πόρτας." + "doc": "Για πλυντήρια πιάτων που ανοίγουν την πόρτα στο τέλος για στέγνωμα (π.χ. AirDry). Αν η πόρτα μείνει ανοιχτή για τον χρόνο αναμονής παρακάτω, ο κύκλος τελειώνει αντί να μπει σε παύση· ένα σύντομο άνοιγμα αγνοείται. Απαιτεί Οντότητα Αισθητήρα Πόρτας." }, "door_end_dwell_seconds": { "label": "Διάρκεια αναμονής τέλους ανοίγματος πόρτας", - "doc": "Πόσο ώρα πρέπει να παραμείνει ανοιχτή η πόρτα πριν το WashData τερματίσει τον κύκλο, όταν είναι ενεργό το \"Η πόρτα ανοίγει αυτόματα στο τέλος\". Αρκετά μεγάλη ώστε να αγνοεί την γρήγορη προσθήκη πιάτου (προεπιλογή 60 s), αρκετά μικρή ώστε να τερματίζει γρήγορα μόλις ανοίξει η πόρτα." + "doc": "Με ενεργό το \"Η πόρτα ανοίγει αυτόματα στο τέλος\": πόση ώρα πρέπει η πόρτα να μείνει ανοιχτή πριν τελειώσει ο κύκλος. Αρκετή ώστε να αγνοείται η προσθήκη ενός πιάτου (προεπιλογή 60 δευτ.)." }, "dishwasher_end_spike_quiet_release": { "label": "Απελευθέρωση σε ήσυχο παθητικό στέγνωμα", - "doc": "Μόλις ο κύκλος ξεπεράσει την αναμενόμενη διάρκειά του, για πόσο χρόνο πρέπει το πλυντήριο πιάτων να παραμείνει ήσυχο (κάτω από το Κατώφλι Τερματισμού) πριν το WashData σταματήσει να περιμένει μια τελική αποστράγγιση και τερματίσει τον κύκλο. Αυξήστε το αν το μηχάνημά σας έχει μια μακρά, αθόρυβη φάση στεγνώματος πριν από μια καθυστερημένη τελική αποστράγγιση που δεν εντοπίζεται - ένα ευρύτερο παράθυρο επιτρέπει στη μαθημένη διάρκεια να ακολουθεί την εποχική μεταβολή (πιο κρύο νερό παροχής = μεγαλύτεροι κύκλοι) αντί να κλειδώνει στον παλιό μέσο όρο. Μπορεί μόνο να μειώσει την αναμονή σε σχέση με το εσωτερικό όριο των 30 λεπτών για την τελική αιχμή, ποτέ να την επεκτείνει." + "doc": "Μετά την αναμενόμενη διάρκεια, πόση ώρα πρέπει το πλυντήριο πιάτων να μείνει κάτω από το Κατώφλι Τερματισμού πριν το WashData σταματήσει να περιμένει την τελική αποστράγγιση. Αυξήστε το αν χάνεται μια καθυστερημένη αποστράγγιση μετά από μακρύ στέγνωμα. Η αναμονή δεν ξεπερνά ποτέ τα 30 λεπτά." }, "profile_evidence_sources": { "label": "Κύκλοι που διαμορφώνουν ένα πρόγραμμα", - "doc": "Ποιοι κύκλοι χρησιμοποιούνται για τη δημιουργία της καμπύλης ισχύος κάθε προγράμματος και για την αντιστοίχιση ενός ολοκληρωμένου κύκλου με αυτήν. Αν αφαιρέσετε την επιλογή ενός είδους, αυτό σταματά να διαμορφώνει τα προγράμματά σας χωρίς να διαγράφεται τίποτα - οι κύκλοι παραμένουν στη λίστα Κύκλοι και μπορείτε ακόμη να τους επισημάνετε ή να τους αφαιρέσετε. Χρήσιμο αν δεν εμπιστεύεστε τα εισαγόμενα δεδομένα. Τα στατιστικά δεν επηρεάζονται: μετρούν πάντα μόνο τους κύκλους που εκτέλεσε πραγματικά αυτό το μηχάνημα. Η αφαίρεση όλων των επιλογών αγνοείται, καθώς ένα πρόγραμμα χωρίς κύκλους πίσω του δεν θα μπορούσε ποτέ να αντιστοιχιστεί." + "doc": "Ποια είδη κύκλων διαμορφώνουν την καμπύλη και την αντιστοίχιση κάθε προγράμματος. Αν αποεπιλέξετε ένα είδος, δεν διαγράφεται τίποτα· χρήσιμο αν δεν εμπιστεύεστε τα εισαγόμενα δεδομένα. Τα στατιστικά μετρούν πάντα μόνο τους κύκλους αυτού του μηχανήματος. Η αποεπιλογή όλων αγνοείται." }, "energy_price_dynamic": { "label": "Χρονικά Σταθμισμένο Κόστος", - "doc": "Χρεώνει κάθε κύκλο με την τιμή που ίσχυε σε κάθε στιγμή της λειτουργίας του, αντί για τη μία τιμή που ίσχυε όταν τελείωσε. Ισχύει μόνο με οντότητα τιμής (μια στατική τιμή δεν μεταβάλλεται). Δεν χρειάζεται επιπλέον ρύθμιση: η τιμή παρακολουθείται ζωντανά όσο τρέχει ο κύκλος, ενώ η Επεξεργασία ιστορικού στα Διαγνωστικά επανυπολογίζει το κόστος παλαιότερων κύκλων από τον recorder. Ανενεργό = η κλασική συμπεριφορά, μία τιμή παγιωμένη στο τέλος." + "doc": "Χρεώνει κάθε κύκλο με την τιμή κάθε στιγμής της λειτουργίας του, αντί για την τιμή όταν τελείωσε. Απαιτεί οντότητα τιμής. Η Επεξεργασία ιστορικού (Διαγνωστικά) επανυπολογίζει το κόστος παλαιότερων κύκλων." }, "anti_crease_finalize_ratio": { "label": "Αναλογία Οριστικοποίησης Κατά Τσακίσματος", - "doc": "Πόσο μέσα στην αναμενόμενη διάρκεια του αντιστοιχισμένου προγράμματος πρέπει να έχει προχωρήσει ένας κύκλος προτού το WashData μπορέσει να κρίνει ότι η περιστροφή χαμηλής ισχύος που βλέπει είναι η ουρά κατά τσακίσματος και να τερματίσει εκεί τον κύκλο. Η αναμενόμενη διάρκεια είναι ο μέσος όρος του προγράμματος, οπότε σε ένα στεγνωτήριο του οποίου ο χρόνος στεγνώματος με αισθητήρα ακολουθεί το φορτίο, οι περισσότερες εκτελέσεις δεν φτάνουν ποτέ στο 98% της και η ουρά παραμένει μέχρι το εφεδρικό χρονικό όριο, συχνά 15 έως 40 λεπτά αφότου σταμάτησε πραγματικά το μηχάνημα. Σε τέτοιο στεγνωτήριο χαμηλώστε την τιμή (π.χ. 0,75). Αφήστε τα πλυντήρια ρούχων στην προεπιλογή: ένα πλυντήριο περνά το μεγαλύτερο μέρος του κύκλου του κάτω από τη Μέγιστη Ισχύ Κατά Τσακίσματος, άρα αυτό το κλάσμα είναι το μόνο που ξεχωρίζει μια ήσυχη ενδιάμεση φάση πλύσης από την ουρά, και αν το χαμηλώσετε μπορεί να κοπεί μια πλύση στη μέση. Αφήστε το κενό για την προεπιλογή (0,98). Πρόκειται για διαφορετική ρύθμιση από την Αναλογία Έξυπνου Τερματισμού, η οποία ελέγχει έναν ξεχωριστό έλεγχο πρόωρης ολοκλήρωσης." + "doc": "Κλάσμα της μέσης διάρκειας του προγράμματος που πρέπει να φτάσει ένας κύκλος πριν η περιστροφή χαμηλής ισχύος μετρήσει ως ουρά κατά τσακίσματος. Χαμηλώστε το (π.χ. 0,75) σε στεγνωτήρια με αισθητήρα φορτίου. Αφήστε τα πλυντήρια στην προεπιλογή (0,98), αλλιώς μια πλύση μπορεί να τελειώσει στη μέση." }, "curve_preroll_seconds": { "label": "Προεγγραφή Καμπύλης", - "doc": "Πόσα δευτερόλεπτα μετρήσεων από ματαιωμένες απόπειρες εκκίνησης μπορεί να μεταφέρει το WashData στην αρχή της καμπύλης ενός κύκλου. Η καμπύλη ενός κύκλου ξεκινά κανονικά από την απόπειρα εκκίνησης που τελικά πετυχαίνει, οπότε σε ένα μηχάνημα που δοκιμάζει μερικές φορές πριν σταθεροποιηθεί (επιλογή προγράμματος, κλείδωμα πόρτας, πρώτο γέμισμα) τα πρώτα λεπτά της πραγματικής δραστηριότητας λείπουν από κάθε καμπύλη. Ορίστε την τιμή λίγο πάνω από τη μεγαλύτερη τέτοια περίοδο προετοιμασίας που έχει το μηχάνημά σας (τα 300 δευτ. αρκούν για τα περισσότερα). Το 0 την απενεργοποιεί. Προσοχή: η ενεργοποίηση μετατοπίζει νωρίτερα την καταγεγραμμένη έναρξη, οπότε οι κύκλοι που καταγράφηκαν πριν και μετά την αλλαγή έχουν διαφορετικές διάρκειες για το ίδιο πρόγραμμα. Οι μέσοι όροι που έχουν μαθευτεί θα αποκλίνουν μέχρι να φύγουν οι παλαιότεροι κύκλοι." + "doc": "Προσθέτει μετρήσεις από αποτυχημένες απόπειρες εκκίνησης στην αρχή ενός κύκλου, για μηχανήματα που δοκιμάζουν πριν ξεκινήσουν (κλείδωμα πόρτας, πρώτο γέμισμα). Τα 300 δευτ. αρκούν για τα περισσότερα· 0 = ανενεργό. Οι καταγεγραμμένες εκκινήσεις μετακινούνται νωρίτερα, οπότε οι μαθημένες διάρκειες αποκλίνουν για λίγο καιρό." }, "unload_confirm_entity": { "label": "Οντότητα Επιβεβαίωσης Αποφόρτωσης", - "doc": "Προαιρετική οντότητα της οποίας η ενεργοποίηση σημαίνει ότι το φορτίο αφαιρέθηκε, για μηχανήματα που δεν μπορούν να έχουν αισθητήρα πόρτας. Λειτουργεί κάθε είδους: ένα κουμπί Zigbee ή NFC, ένας βοηθός input_button, ένας αισθητήρας κίνησης μπροστά από το μηχάνημα, μια σκηνή. Κάθε αλλαγή κατάστασης μετράει ως επιβεβαίωση, εκτός από την απενεργοποίηση και τη μετάβαση σε μη διαθέσιμη. Η ρύθμιση αυτή ενεργοποιεί επίσης την υπενθύμιση αποφόρτωσης σε συσκευή χωρίς Οντότητα Αισθητήρα Πόρτας." + "doc": "Προαιρετικό. Οποιαδήποτε οντότητα επιβεβαιώνει ότι το φορτίο αφαιρέθηκε, για μηχανήματα χωρίς αισθητήρα πόρτας (κουμπί, input_button, αισθητήρας κίνησης, σκηνή). Μετρά κάθε αλλαγή κατάστασης εκτός από απενεργοποίηση ή μη διαθέσιμο. Ενεργοποιεί επίσης την υπενθύμιση αποφόρτωσης." }, "unload_track_without_door": { "label": "Χειροκίνητη Επιβεβαίωση Αποφόρτωσης", - "doc": "Εκτελεί την υπενθύμιση αποφόρτωσης σε συσκευή χωρίς Οντότητα Αισθητήρα Πόρτας και χωρίς Οντότητα Επιβεβαίωσης Αποφόρτωσης, και η επιβεβαίωση γίνεται από τον δικό σας αυτοματισμό: πατήστε την οντότητα κουμπιού \"Σήμανση Αποφόρτωσης\" ή καλέστε την υπηρεσία ha_washdata.mark_unloaded. Αγνοείται όταν έχει οριστεί Οντότητα Αισθητήρα Πόρτας, η οποία παρέχει ήδη το σήμα." + "doc": "Εκτελεί την υπενθύμιση αποφόρτωσης χωρίς αισθητήρα πόρτας ή οντότητα επιβεβαίωσης. Επιβεβαιώστε την αποφόρτωση από δικό σας αυτοματισμό με το κουμπί \"Σήμανση Αποφόρτωσης\" ή την υπηρεσία ha_washdata.mark_unloaded. Αγνοείται όταν έχει οριστεί Οντότητα Αισθητήρα Πόρτας." }, "notify_icon_color": { "label": "Χρώμα Ειδοποίησης", - "doc": "Χρώμα τονισμού για τις ειδοποιήσεις αυτής της συσκευής, ώστε το πλυντήριο, το στεγνωτήριο και το πλυντήριο πιάτων να ξεχωρίζουν με μια ματιά. Το iOS χρωματίζει το εικονίδιο και τη γραμμή προόδου της Ζωντανής δραστηριότητας στην οθόνη κλειδώματος και στο Dynamic Island. Στο Android αποστέλλεται το ίδιο χρώμα, αλλά το τι θα το κάνει εξαρτάται από το τηλέφωνο: ορισμένα χρωματίζουν το εικονίδιο, ενώ πολλά (ιδίως το Samsung One UI) το αγνοούν εντελώς. Και τα δύο κατανοούν τον δεκαεξαδικό (Hex) κωδικό (π.χ. #03A9F4). Αφήστε κενό για την προεπιλογή της πλατφόρμας." + "doc": "Χρώμα τονισμού για τις ειδοποιήσεις αυτής της συσκευής, ώστε πλυντήριο, στεγνωτήριο και πλυντήριο πιάτων να ξεχωρίζουν εύκολα. Στο iOS γεμίζει τον κύκλο πίσω από το εικονίδιο και χρωματίζει τη γραμμή της Ζωντανής δραστηριότητας. Πολλά τηλέφωνα Android (ιδίως Samsung) το αγνοούν. Χρησιμοποιήστε hex, π.χ. #03A9F4. Κενό: προεπιλογή της πλατφόρμας." + }, + "share_name": { + "doc": "Εμφανίζει το όνομα του λογαριασμού σας ως αποστολέα των κύκλων που κοινοποιείτε. Ανενεργό: κοινοποιούνται χωρίς όνομα." } }, "setting_group": { @@ -1673,9 +1589,6 @@ "door_pause": { "label": "Πόρτα & Παύση" }, - "dtw_refinement": { - "label": "Βελτίωση DTW" - }, "duration_gates": { "label": "Πύλες Διάρκειας" }, @@ -1752,13 +1665,18 @@ "rebuilding": "Ανακατασκευή…", "analyzing": "Ανάλυση…", "resetting": "Επαναφορά…", - "reverting": "Αναίρεση…", "trimming": "Περικοπή…", "splitting": "Διαίρεση…", + "stalled": "Κόλλησε", "deleting": "Διαγραφή…", "imported": "Εισηγμένο", "preparing": "Προετοιμασία…", - "unlabelling": "Αφαίρεση επισήμανσης…" + "unlabelling": "Αφαίρεση επισήμανσης…", + "working": "Επεξεργασία…", + "match_uncertain": "Αβέβαιο: {top} ή {runner}", + "match_uncertain_single": "Αβέβαιο: ίσως {top}", + "match_sure": "~{pct}% βεβαιότητα", + "match_uncertain_tip": "Το WashData δεν έχει καταλήξει ακόμα σε πρόγραμμα. Το ποσοστό δείχνει πόσο συχνά η κορυφαία εκτίμηση, {top}, ήταν σωστή σε παλιούς κύκλους όταν προηγούνταν τόσο. Μόνο ενημερωτικό." }, "tab": { "advanced": "Για προχωρημένους", @@ -1790,13 +1708,17 @@ "toast": { "catalog_refreshed": "Ο κατάλογος της κοινότητας ανανεώθηκε", "contrib_device_changed": "Προστέθηκε στο κοινοτικό κατάστημα, αλλά η επιλεγμένη συσκευή άλλαξε όσο η φόρμα ήταν ανοιχτή. Επιλέξτε την στις Ρυθμίσεις.", + "task_name_required": "Δώστε ένα όνομα στην εργασία", + "task_added": "Η εργασία προστέθηκε", + "task_save_failed": "Δεν ήταν δυνατή η αποθήκευση της εργασίας: {error}", + "task_removed": "Η εργασία αφαιρέθηκε", + "maint_logged": "Καταγράφηκε ως ολοκληρωμένη: {task}", "access_saved": "Ο έλεγχος πρόσβασης αποθηκεύτηκε", "all_wiped": "Όλα τα δεδομένα διαγράφηκαν", "analysis_complete_none": "Η ανάλυση ολοκληρώθηκε: δεν υπάρχουν νέες προτάσεις", "auto_label_complete": "Η αυτόματη ετικέτα ολοκληρώθηκε", "automation_deleted": "Ο αυτοματισμός διαγράφηκε", "correction_submitted": "Υποβλήθηκε διόρθωση", - "cycle_deleted": "Ο κύκλος διαγράφηκε", "cycle_force_stopped": "Ο κύκλος σταμάτησε με δύναμη", "cycle_labelled": "Κύκλος με ετικέτα", "cycle_paused": "Ο κύκλος σταμάτησε", @@ -1812,11 +1734,9 @@ "group_deleted": "Η ομάδα διαγράφηκε", "group_name_required": "Απαιτείται όνομα ομάδας", "group_saved": "Η ομάδα αποθηκεύτηκε", - "import_successful": "Επιτυχής εισαγωγή. επαναφόρτωση ενσωμάτωσης", "json_required": "Απαιτούνται δεδομένα JSON", "legacy_removed": "Οι ενέργειες παλαιού τύπου καταργήθηκαν", "logs_exported": "Εξαγωγή αρχείων καταγραφής", - "matching_reverted": "Τα αντίστοιχα βάρη επανήλθαν στις προεπιλογές", "min_2_profiles": "Επιλέξτε τουλάχιστον 2 προφίλ για μια ομάδα", "ml_training_no_improvement": "Η εκπαίδευση ολοκληρώθηκε: διατηρήθηκε η βασική γραμμή (καμία βελτίωση)", "models_reverted": "Τα μοντέλα στη συσκευή επανήλθαν στη γραμμή βάσης", @@ -1826,8 +1746,8 @@ "pg_preset_loaded": "Η προρύθμιση \"{name}\" φορτώθηκε", "pg_preset_saved": "Η προρύθμιση \"{name}\" αποθηκεύτηκε", "pg_sugg_loaded": "Φορτώθηκαν {n} προτεινόμενες τιμές - εκτελέστε τον Χώρο δοκιμών για σύγκριση", - "pg_sugg_ml_loaded": "Φορτώθηκαν {n} τιμές βαθμονόμησης ML - εκτελέστε τον Χώρο δοκιμών για σύγκριση", "phase_name_required": "Απαιτείται όνομα φάσης", + "phase_reset": "Η φάση \"{name}\" επαναφέρθηκε στην ενσωματωμένη έκδοση", "phase_updated": "Η φάση ενημερώθηκε", "phases_saved": "Οι φάσεις αποθηκεύτηκαν", "preferences_saved": "Οι προτιμήσεις αποθηκεύτηκαν", @@ -1874,14 +1794,10 @@ "store_name_required": "Εισαγάγετε ένα όνομα προφίλ", "store_import_failed": "Η εισαγωγή απέτυχε: {error}", "store_imported": "Εισήχθη στο {profile}", - "store_program_required": "Εισαγάγετε ένα όνομα προγράμματος", "store_no_appliance": "Ορίστε πρώτα τη μάρκα και το μοντέλο της συσκευής σας στις Ρυθμίσεις.", "store_share_failed": "Η κοινοποίηση απέτυχε: {error}", "store_shared": "Κοινοποιήθηκε στο κοινοτικό κατάστημα, σε αναμονή ελέγχου.", "appliance_added": "Η συσκευή προστέθηκε, σε αναμονή έγκρισης", - "thanks_confirming": "Ευχαριστούμε για την επιβεβαίωση", - "device_approved": "Εγκρίθηκε από την κοινότητα", - "rating_saved": "Η βαθμολογία ποιότητας αποθηκεύτηκε", "brand_added": "Η μάρκα προστέθηκε, σε αναμονή έγκρισης", "profile_added": "Το προφίλ προστέθηκε, σε αναμονή έγκρισης", "saved_except_conflicts": "Αποθηκεύτηκε. Διορθώστε τις επισημασμένες συγκρούσεις για να αποθηκεύσετε τα υπόλοιπα.", @@ -1904,11 +1820,50 @@ "odometer_saved": "Το σύνολο κύκλων αποθηκεύτηκε", "odometer_save_failed": "Δεν ήταν δυνατή η αποθήκευση του συνόλου: {error}", "store_pick_profile_share": "Επιλέξτε ένα προφίλ για κοινοποίηση", - "remove_failed": "Η αφαίρεση απέτυχε: {error}" + "remove_failed": "Η αφαίρεση απέτυχε: {error}", + "analysis_complete": "Η ανάλυση ολοκληρώθηκε, προτάσεις: {count}", + "analysis_failed": "Η ανάλυση απέτυχε: {error}", + "auto_detect_failed": "Ο αυτόματος εντοπισμός απέτυχε: {error}", + "auto_label_failed": "Η αυτόματη επισήμανση απέτυχε: {error}", + "could_not_load_cycle": "Δεν ήταν δυνατή η φόρτωση του κύκλου: {error}", + "create_failed": "Η δημιουργία απέτυχε: {error}", + "cycles_deleted": "Κύκλοι που διαγράφηκαν: {count}", + "debug_cleared": "Ίχνη εντοπισμού σφαλμάτων που εκκαθαρίστηκαν: {count}", + "discard_failed": "Η απόρριψη απέτυχε: {error}", + "error": "Σφάλμα: {error}", + "export_failed": "Η εξαγωγή απέτυχε: {error}", + "import_failed": "Η εισαγωγή απέτυχε: {error}", + "invalid_json": "Το \"{key}\" δεν είναι έγκυρο JSON· διορθώστε το ή αδειάστε το πεδίο πριν από την αποθήκευση.", + "label_failed": "Η επισήμανση απέτυχε: {error}", + "ml_training_promoted": "Η εκπαίδευση ολοκληρώθηκε, μοντέλα που προήχθησαν: {count}", + "pause_failed": "Η παύση απέτυχε: {error}", + "phase_created": "Η φάση \"{name}\" δημιουργήθηκε", + "phase_deleted": "Η φάση \"{name}\" διαγράφηκε", + "profile_created": "Το προφίλ \"{name}\" δημιουργήθηκε", + "rename_failed": "Η μετονομασία απέτυχε: {error}", + "save_failed": "Η αποθήκευση απέτυχε: {error}", + "split_complete": "Διαχωρίστηκε σε {count} κύκλους", + "start_failed": "Η εκκίνηση απέτυχε: {error}", + "stop_failed": "Η διακοπή απέτυχε: {error}", + "update_failed": "Η ενημέρωση απέτυχε: {error}", + "store_download_skipped": "{n} εγγραφή(ές) παραλείφθηκαν: πολύ σύντομες, με πάρα πολλά κενά ή ήδη στη συσκευή σας.", + "store_import_low_quality": "Αυτή η εγγραφή είναι πολύ σύντομη, έχει πάρα πολλά κενά ή είναι απίθανη και δεν μπορεί να εισαχθεί.", + "store_import_duplicate": "Αυτή η εγγραφή βρίσκεται ήδη στη συσκευή σας.", + "device_approved": "Εγκρίθηκε από την κοινότητα", + "thanks_confirming": "Ευχαριστούμε για την επιβεβαίωση", + "rating_saved": "Η βαθμολογία ποιότητας αποθηκεύτηκε", + "store_download_partial": "Δεν ήταν δυνατή η λήψη {n} προγράμματος(-ων). Δοκιμάστε ξανά αργότερα.", + "import_no_restore_point": "Η εισαγωγή ολοκληρώθηκε, αλλά δεν ήταν δυνατή η αποθήκευση σημείου επαναφοράς, οπότε αυτή η εισαγωγή δεν μπορεί να αναιρεθεί", + "import_undo_failed": "Δεν ήταν δυνατή η αναίρεση της εισαγωγής: {error}", + "import_undone": "Η εισαγωγή αναιρέθηκε: επαναφέρθηκαν {profiles} προφίλ και {cycles} κύκλοι", + "auto_label_cancelled": "Η αυτόματη επισήμανση ακυρώθηκε αφού επισημάνθηκαν {count} κύκλοι.", + "auto_label_cancelled_one": "Η αυτόματη επισήμανση ακυρώθηκε αφού επισημάνθηκε {count} κύκλος.", + "auto_label_cancelled_other": "Η αυτόματη επισήμανση ακυρώθηκε αφού επισημάνθηκαν {count} κύκλοι.", + "auto_label_done": "Η αυτόματη επισήμανση ολοκληρώθηκε: επισημάνθηκαν {count} κύκλοι, χωρίς ετικέτα: {skipped}.", + "auto_label_done_one": "Η αυτόματη επισήμανση ολοκληρώθηκε: επισημάνθηκε {count} κύκλος, χωρίς ετικέτα: {skipped}.", + "auto_label_done_other": "Η αυτόματη επισήμανση ολοκληρώθηκε: επισημάνθηκαν {count} κύκλοι, χωρίς ετικέτα: {skipped}." }, "suggestion": { - "both_agree": "Το WashData προτείνει", - "calibrated_label": "Βαθμονομημένο", "observed_label": "Παρατηρήθηκε", "impact": { "auto_label_confidence": { @@ -1923,10 +1878,6 @@ "higher": "Κλείνει μόνο κύκλους που κατανάλωσαν σημαντική ενέργεια", "lower": "Κλείνει και συντομότερους ή χαμηλής ενέργειας κύκλους" }, - "end_repeat_count": { - "higher": "Απαιτεί επανάληψη του σήματος λήξης περισσότερες φορές – πιο αξιόπιστο, ελαφρώς πιο αργό", - "lower": "Τερματίζει μετά από λιγότερες επαναλήψεις – ταχύτερη ανίχνευση, μεγαλύτερος κίνδυνος ψευδούς λήξης" - }, "learning_confidence": { "higher": "Μαθαίνει μόνο από πολύ σίγουρες αντιστοιχίσεις – πιο αργό αλλά πιο αξιόπιστο", "lower": "Μαθαίνει από περισσότερους κύκλους; το μοντέλο μπορεί να έχει περισσότερο θόρυβο" @@ -1965,40 +1916,25 @@ } }, "reason": { - "auto_label_confidence": "Αξιοπιστία 15ου εκατοστημορίου των {cycles} αυτόματων ετικετών που ο χρήστης δεν διόρθωσε ποτέ ({p15}).", "completion_min_seconds": "Το μισό της διάρκειας p05 των καθαρών κύκλων ({minutes} min) σε {cycles} κύκλους· φιλτράρει τους ψευδείς κύκλους.{excl}", - "dead_zone_batch": "Διατηρείται σύντομο (διάμεσο παράθυρο αστάθειας εκκίνησης σε {cycles} κύκλους = {s}s) ώστε ένα πραγματικό τέλος να ανιχνεύεται άμεσα.", - "dead_zone_from_dip": "Βάσει πρώιμης πτώσης ισχύος που ανιχνεύτηκε στα {s}s.", - "end_energy_default": "Προεπιλεγμένη συνιστώμενη βάση για το κατώφλι θορύβου στο τέλος του κύκλου.", - "end_energy_false": "Ενέργεια ψευδούς τέλους p95 ({p95}Wh) σε {cycles} κύκλους, με κατώτατο όριο το 0.2% της διάμεσης ενέργειας κύκλου ({floor}Wh).", - "end_energy_no_false": "Κανένα ψευδές τέλος σε {cycles} κύκλους· με κατώτατο όριο το 0.2% της διάμεσης ενέργειας κύκλου ({floor}Wh).", - "end_repeat_count": "{false}/{total} καθαροί κύκλοι ({pct}%) είχαν εσωτερική παύση >60s που συνεχίστηκε· απαιτούνται {val} επιβεβαίωση(εις) τέλους.", - "learning_confidence": "Αξιοπιστία p05 των {cycles} κύκλων με ετικέτα χρήστη ({p05})· κάτω από αυτό, ζητείται επαλήθευση.", "match_interval": "Βάσει του παρατηρούμενου ρυθμού ενημέρωσης (διάμεσος={median}s) * 10.", "max_duration_ratio": "Βάσει των διαρκειών των κύκλων με ετικέτα (p95={p95}).", - "min_active": "Βάσει της ελάχιστης ενεργού ισχύος ({min}W) που παρατηρήθηκε στον τελευταίο κύκλο.", - "min_duration_ratio": "Διατηρείται όσο το δυνατόν χαμηλότερο ώστε ένα πρόγραμμα να αναγνωρίζεται νωρίς στον κύκλο· τα κατώφλια αξιοπιστίας και ασάφειας αποτρέπουν τις πρόωρες επιβεβαιώσεις.", "min_off_gap": "Βάσει {gaps} παρατηρούμενων διακυκλικών κενών (p05={p05}s). Κατώτατο όριο συσκευής: {floor}s.", "min_off_gap_bridge": "Διαστασιολογήθηκε ώστε να γεφυρώνει το μεγαλύτερο διάστημα ησυχίας μέσα στον κύκλο: {span}s + 60s περιθώριο, από {spans} γεφυρωμένα κενά σε {cycles} καθαρούς κύκλους (παραμένει κάτω από τα {ceiling}s, το συντομότερο διάλειμμα που αφήνετε ανάμεσα σε δύο διαδοχικές εκκινήσεις).", "min_power": "40% της χαμηλότερης ενεργού ισχύος p05 ({p05}W) σε {cycles} καθαρούς κύκλους, διατηρώντας το κατώφλι απενεργοποίησης κάτω από την πραγματική κατανάλωση.{excl}", - "ml_auto_label": "Η χαμηλότερη αξιοπιστία που το μοντέλο ποιότητας εξακολούθησε να αξιολογεί ως καθαρή (p10 των {cycles} καθαρών κύκλων = {p10}).", - "ml_end_repeat": "{false}/{total} κύκλοι ({pct}%) είχαν παύση που ο ανιχνευτής τέλους βαθμολόγησε >50%· απαιτούνται {val} επιβεβαίωση(εις) τέλους.", - "ml_off_delay": "Παύσεις επιβεβαιωμένες από τον ανιχνευτή τέλους: p95 {p95}s + 60s περιθώριο, από {pauses} παύσεις επαληθευμένες από το μοντέλο σε {cycles} κύκλους (κατώτατο όριο {floor}s).", "no_update_timeout": "Βάσει του παρατηρούμενου ρυθμού ενημέρωσης (p95={p95}s) * 20 (ελάχ. 60s).", "off_delay_cadence": "Βάσει του παρατηρούμενου ρυθμού ενημέρωσης (p95={p95}s) * 5", "off_delay_device_floor": "Χρησιμοποιήθηκε το ασφαλές ελάχιστο ειδικό για τη συσκευή {device} ({floor}s).", "off_delay_generic_floor": "Χρησιμοποιήθηκε γενικό ασφαλές ελάχιστο ({floor}s).", "off_delay_pauses": "Διαστασιολογήθηκε ώστε να ξεπερνά τις πραγματικές παύσεις: παύση εντός κύκλου p95 {p95}s + 60s περιθώριο, από {pauses} παύσεις σε {cycles} καθαρούς κύκλους (κατώτατο όριο {floor}s).", - "profile_match_threshold": "Αξιοπιστία p10 των {cycles} σωστών αυτόματων ετικετών ({p10})· ασφαλές κατώτατο όριο άμεσης επιβεβαίωσης.", - "sampling_interval": "Διάμεσο διάστημα ενημέρωσης που παρατηρήθηκε σε {cycles} καθαρούς κύκλους ({si}s).{excl}", - "smoothing_window": "Διαστασιολογήθηκε ώστε να εξομαλύνει ~30s μετρήσεων σε δειγματοληψία {si}s ({samples} δείγματα).", - "start_duration": "Διατηρείται σύντομο (~ένα διάστημα δειγματοληψίας {si}s) ώστε η ανίχνευση να ξεκινά όσο το δυνατόν νωρίτερα, αγνοώντας παράλληλα τις αιχμές ενός μόνο δείγματος.", - "thr_batch": "Διατηρείται λίγο πάνω από τη χαμηλότερη ενεργό ισχύ p05 σε {cycles} κύκλους ({p05}W) ώστε μια έναρξη να εντοπίζεται όσο το δυνατόν νωρίτερα και το κατώφλι διακοπής να παραμένει κάτω από τη χαμηλότερη ισχύ λειτουργίας της μηχανής.", - "tol_per_profile": "p75 της διακύμανσης διάρκειας ανά προφίλ σε {profiles} προφίλ ({cycles} κύκλοι)· τα σταθερά προφίλ δεν επιβαρύνονται.", - "tol_pooled": "Βάσει της συγκεντρωτικής διακύμανσης διάρκειας {cycles} πρόσφατων κύκλων με ετικέτα (απόκλιση p95={dev}).", + "standby_floor": "Κάθε πρόσφατος κύκλος εξακολουθούσε να καταναλώνει {idle}W όταν τελείωσε, οπότε ένα κατώφλι κάτω από αυτό δεν μπορεί ποτέ να εντοπίσει ότι η συσκευή σβήνει. Διατηρείται πάνω από αυτό· οι {cycles} καθαροί κύκλοι που έχουν καταγραφεί δεν έπεσαν ποτέ κάτω από αυτό για περισσότερο από {pause}s.", + "thr_batch": "Λίγο πάνω από τη χαμηλότερη ενεργή ισχύ (p05) σε {cycles} κύκλους ({p05}W), ώστε οι εκκινήσεις να εντοπίζονται νωρίς και το κατώφλι τερματισμού να μένει κάτω από τη χαμηλότερη ισχύ λειτουργίας.", "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), επομένως το ελάχιστο δεν κοστίζει τίποτα." + "match_interval_floored": "Το συντομότερο πρόγραμμα ({shortest} δευτ.) θα περιόριζε την τιμή στα {cap} δευτ., κάτω από το ελάχιστο των {minimum} δευτ., οπότε χρησιμοποιείται το ελάχιστο: {persistence} διαδοχικές αντιστοιχίσεις χρειάζονται {budget} δευτ., πάνω από το {pct}% αυτού του προγράμματος. Η αντιστοίχιση τρέχει μόνο όταν φτάνει μέτρηση (διάμεσος={median} δευτ.), οπότε αυτό δεν κοστίζει τίποτα.", + "end_energy_floor": "Αυξήθηκε στο ελάχιστο που προκύπτει από το κατώφλι τερματισμού ({stop} W) στη διάρκεια της καθυστέρησης απενεργοποίησης ({off_delay} s)· κάτω από αυτό, ο κύκλος μπορεί να τερματιστεί μόνο μέσω εναλλακτικής διαδρομής.", + "max_duration_ratio_default": "Επαναφορά στην προεπιλογή: ένα χαμηλότερο ανώτατο όριο απορρίπτει το σωστό πρόγραμμα κάθε φορά που ένας κύκλος διαρκεί περισσότερο από το συνηθισμένο.", + "min_duration_ratio_default": "Επαναφορά στην προεπιλογή: ένα υψηλότερο κατώτατο όριο εμποδίζει την αναγνώριση ενός κύκλου σε εξέλιξη μέχρι να έχει προχωρήσει αρκετά." }, "exclusions": { "summary": "Εξαιρέθηκαν {total} εσφαλμένα εντοπισμένοι κύκλοι: {parts}.", @@ -2016,7 +1952,8 @@ "no_power": "χωρίς ισχύ", "no_active_power": "χωρίς ενεργή ισχύ" } - } + }, + "now_value": "(τώρα {v})" }, "trend": { "down": "Πτωτικές τάσεις", @@ -2024,6 +1961,10 @@ }, "maint": { "bearing_service": "Σέρβις ρουλεμάν", + "salt": "Αναπλήρωση αλατιού", + "rinse_aid": "Αναπλήρωση λαμπρυντικού", + "lint_filter": "Καθαρισμός φίλτρου χνουδιού", + "condenser_clean": "Καθαρισμός συμπυκνωτή", "descale": "Αφαλάτωση", "drum_clean": "Καθαρισμός κάδου", "filter_clean": "Καθαρισμός φίλτρου", @@ -2031,7 +1972,6 @@ }, "pg_desc": { "completion_min_seconds": "Η συντομότερη εκτέλεση που μετρά ως πραγματικός κύκλος", - "end_repeat_count": "Χαμηλές μετρήσεις στη σειρά πριν τη λήξη", "interrupted_min_seconds": "Οι σύντομοι κύκλοι επισημαίνονται ως διακοπή", "min_off_gap": "Απαιτούμενο κενό για διαχωρισμό δύο κύκλων", "off_delay": "Δευτερόλεπτα χαμηλής ισχύος πριν λήξει ο κύκλος", @@ -2039,26 +1979,15 @@ "start_threshold_w": "Ελάχιστα watt για να θεωρηθεί ότι ξεκίνησε", "stop_threshold_w": "Κάτω από αυτό, το μηχάνημα θεωρείται εκτός λειτουργίας", "anti_wrinkle_enabled": "Απορροφά τους παλμούς ανατροπής μετά την κύρια φάση αντί να τους διαβάζει ως νέους κύκλους", - "anti_wrinkle_exit_power": "Η ισχύς πρέπει να πέσει κάτω από αυτό μεταξύ των παλμών για να παραμείνει ενεργή η λειτουργία κατά τσακίσματος", + "anti_wrinkle_exit_power": "Επίπεδο ησυχίας ανάμεσα στους παλμούς ανατροπής· ισχύει μόνο πάνω από το Κατώφλι Τερματισμού", "anti_wrinkle_idle_timeout": "Ο χρόνος ησυχίας που επιτρέπεται ανάμεσα σε δύο παλμούς ανατροπής προτού λήξει η λειτουργία κατά τσακίσματος", "anti_wrinkle_max_duration": "Ένας παλμός μεγαλύτερος από αυτό τερματίζει τη λειτουργία κατά τσακίσματος και ανοίγει νέο κύκλο", "anti_wrinkle_max_power": "Ένας παλμός πάνω από αυτό τερματίζει τη λειτουργία κατά τσακίσματος και ανοίγει νέο κύκλο", - "dtw_bandwidth": "Στάδιο 3: ζώνη παραμόρφωσης Sakoe-Chiba (0 = DTW ανενεργό; προεπιλογή 0.2 = 20% μήκους κύκλου)", - "corr_weight": "Στάδιο 2: ισορροπία μεταξύ σχήματος καμπύλης (συσχέτιση) και επιπέδου ισχύος (MAE); προεπιλογή 0.45", - "duration_weight": "Στάδιο 4: πόσο επηρεάζει η συμφωνία διάρκειας τον τελικό βαθμό (προεπιλογή 0.22)", - "energy_weight": "Στάδιο 4: πόσο επηρεάζει η συμφωνία ενέργειας τον τελικό βαθμό (προεπιλογή 0.22)", - "profile_match_min_duration_ratio": "Στάδιο 1: ελάχιστο μήκος κύκλου ως κλάσμα της διάρκειας προφίλ; προεπιλογή 0.1 (10%)", - "profile_match_max_duration_ratio": "Στάδιο 1: μέγιστο μήκος κύκλου ως κλάσμα της διάρκειας προφίλ; προεπιλογή 1.8 (180%)", - "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)", - "dtw_refine_top_n": "Στάδιο 3: υποψήφιοι που επαναβαθμολογεί η DTW; αυξήστε σε 7-9 αν το σωστό προφίλ βρίσκεται στη 4η-5η θέση (προεπιλογή 5)", - "duration_scale": "Στάδιο 4: λόγος λογαρίθμου όπου η συμφωνία διάρκειας υποδιπλασιάζεται; μικρότερος = αυστηρότερη ποινή (προεπιλογή 0.175)", - "energy_scale": "Στάδιο 4: λόγος λογαρίθμου όπου η συμφωνία ενέργειας υποδιπλασιάζεται; μικρότερος = αυστηρότερη ποινή (προεπιλογή 0.25)", + "profile_match_min_duration_ratio": "Ελάχιστο μήκος κύκλου ως κλάσμα της διάρκειας του προφίλ· προεπιλογή 0.1 (10%)", + "profile_match_max_duration_ratio": "Μέγιστο μήκος κύκλου ως κλάσμα της διάρκειας του προφίλ· προεπιλογή 1.8 (180%)", "dishwasher_end_spike_quiet_release": "Πλυντήριο πιάτων: δευτερόλεπτα ησυχίας μετά την αναμενόμενη διάρκεια πριν απελευθερωθεί η αναμονή αποστράγγισης τέλους κύκλου", - "smart_termination_duration_ratio": "Κλάσμα της αναμενόμενης διάρκειας του αντιστοιχισμένου προγράμματος που πρέπει να φτάσει ένας κύκλος προτού ο Έξυπνος Τερματισμός μπορέσει να τον τερματίσει νωρίς· χαμηλώστε το για μηχανήματα που εξαρτώνται από το φορτίο ή τη θερμοκρασία", - "anti_crease_finalize_ratio": "Κλάσμα της αναμενόμενης διάρκειας του αντιστοιχισμένου προγράμματος που πρέπει να φτάσει ένας κύκλος προτού οριστικοποιηθεί η ουρά περιστροφής κατά τσακίσματος· χαμηλώστε το σε στεγνωτήρια με μεγάλη ουρά, αφήστε τα πλυντήρια στην προεπιλογή", + "smart_termination_duration_ratio": "Κλάσμα της μέσης διάρκειας του προγράμματος που πρέπει να φτάσει ένας κύκλος πριν ο Έξυπνος Τερματισμός μπορέσει να τον τερματίσει νωρίς· χαμηλώστε το αν οι χρόνοι λειτουργίας διαφέρουν", + "anti_crease_finalize_ratio": "Κλάσμα της μέσης διάρκειας του προγράμματος που πρέπει να φτάσει ένας κύκλος πριν η ουρά κατά τσακίσματος μπορέσει να τον ολοκληρώσει· χαμηλώστε το στα στεγνωτήρια, αφήστε τα πλυντήρια στην προεπιλογή", "curve_preroll_seconds": "Δευτερόλεπτα μετρήσεων από ματαιωμένες απόπειρες εκκίνησης που μεταφέρονται στην αρχή της καμπύλης ενός κύκλου· 0 = ανενεργό" }, "col": { @@ -2066,12 +1995,14 @@ "status_tip": "Έκβαση κύκλου: Ολοκληρώθηκε (φυσικό τέλος), Διακόπηκε (απότομη πτώση ισχύος), Αναγκαστική διακοπή (μη αυτόματη) ή Χρειάζεται αναθεώρηση (εκκρεμούν σχόλια).", "date_tip": "Ημερομηνία και ώρα έναρξης του κύκλου.", "duration_tip": "Συνολικός χρόνος λειτουργίας του κύκλου από την αρχή έως το τέλος.", - "energy_tip": "Συνολική ενέργεια που καταναλώθηκε (kWh). Υπολογίζεται με ολοκλήρωση της ισχύος στον χρόνο.", - "cost_tip": "Κόστος ενέργειας για αυτόν τον κύκλο, παγιωμένο κατά την ολοκλήρωση. Με ενεργό το Χρονικά Σταθμισμένο Κόστος, η καμπύλη ισχύος χρεώνεται με την τιμή που ίσχυε σε κάθε στιγμή· διαφορετικά ενέργεια x η μία τιμή που ίσχυε στο τέλος. Ορίστε μια τιμή στις Ρυθμίσεις για να συμπληρωθεί.", + "energy_tip": "Συνολική ενέργεια που καταναλώθηκε (kWh), υπολογισμένη από την ισχύ στον χρόνο.", + "cost_tip": "Κόστος ενέργειας αυτού του κύκλου, παγιωμένο όταν τελείωσε. Με ενεργό το Χρονικά Σταθμισμένο Κόστος, κάθε στιγμή χρεώνεται με τη δική της τιμή· αλλιώς με την τιμή στο τέλος. Ορίστε μια τιμή στις Ρυθμίσεις για να συμπληρωθεί.", "confidence_tip": "Εμπιστοσύνη αντιστοίχισης προφίλ (0-100%). Πόσο στενά η καμπύλη ισχύος του κύκλου ταίριαξε με το αναγνωρισμένο πρόγραμμα.", - "health_tip": "Υγεία κύκλου ML (υψηλότερο = καλύτερο). Κάντε κλικ σε έναν κύκλο για να τον ελέγξετε και να τον αξιολογήσετε.", "flags_tip": "Σημαίες αναθεώρησης, ανωμαλίας και πηγής για τον κύκλο. Τοποθετήστε τον δείκτη πάνω σε ένα εικονίδιο για λεπτομέρειες.", - "cost_dynamic_tip": "Χρονικά σταθμισμένο: χρεώθηκε με την τιμή που ίσχυε κατά τη διάρκεια του κύκλου (πραγματική τιμή {price} {cur}/kWh)." + "cost_dynamic_tip": "Χρονικά σταθμισμένο: χρεώθηκε με την τιμή που ίσχυε κατά τη διάρκεια του κύκλου (πραγματική τιμή {price} {cur}/kWh).", + "dbg_conf": "Εμπ.", + "dbg_mae": "MAE", + "dbg_corr": "Συσχ." }, "stat": { "avg": "μ.ό.", @@ -2095,7 +2026,10 @@ "notify_finish": "Θα στελνόταν μια ειδοποίηση ολοκλήρωσης.", "notify_milestone": "Θα στελνόταν μια ειδοποίηση ορόσημου.", "notify_held": "Μια ειδοποίηση κρατήθηκε λόγω των ωρών ησυχίας.", - "finished": "Ο κύκλος έφτασε σε τελική κατάσταση και ολοκληρώθηκε." + "finished": "Ο κύκλος έφτασε σε τελική κατάσταση και ολοκληρώθηκε.", + "match_reverted": "Το πρόγραμμα επέστρεψε σε εντοπισμό: η βαθμολογία του έπεσε πολύ κάτω από το δικό της μέγιστο.", + "verified_pause": "Η ζωντανή ευθυγράμμιση επιβεβαίωσε (ή ήρε) μια παύση μέσα στο πρόγραμμα. Όσο ισχύει, ο κύκλος δεν τερματίζεται.", + "stalled": "Ο κύκλος έμεινε στην κατανάλωση αναμονής περισσότερο από όσο κάνει ποτέ παύση εκεί αυτό το πρόγραμμα, ενώ απέμενε εργασία. Εμφανίζεται ως Σε παύση (Κόλλησε)· ο κύκλος μένει ανοιχτός." }, "store": { "search_ph2": "Αναζήτηση με μάρκα ή μοντέλο…", @@ -2104,11 +2038,7 @@ "programs_count": "Προγράμματα: {n}", "browse": "Περιήγηση", "device": "Συσκευή", - "favorites": "Αγαπημένα", - "search_ph": "Αναζήτηση κατά μάρκα ή μοντέλο…", "no_results": "Δεν βρέθηκαν αντίστοιχες συσκευές. Δοκιμάστε διαφορετική αναζήτηση.", - "n_programs": "{n} προγράμματα", - "n_cycles": "{n} κύκλοι", "no_programs": "Δεν υπάρχουν ακόμη κοινοποιημένα προγράμματα για αυτή τη συσκευή.", "no_cycles": "Δεν έχουν κοινοποιηθεί ακόμη κύκλοι αναφοράς για αυτό το πρόγραμμα.", "anon": "ανώνυμος", @@ -2120,19 +2050,21 @@ "merge_target": "Συγχώνευση σε προφίλ", "description": "Περιγραφή (προαιρετικό)", "contributed_by": "από {name}", + "website": "Ιστότοπος καταστήματος ↗", + "website_tip": "Άνοιγμα ιστότοπου κοινοτικού καταστήματος", + "favorites": "Αγαπημένα", "rating_summary": "★ {avg} ({n})", "no_ratings": "Δεν υπάρχουν ακόμη βαθμολογίες", - "website": "Ιστότοπος καταστήματος ↗", - "website_tip": "Άνοιγμα ιστότοπου κοινοτικού καταστήματος" + "unreachable": "Δεν ήταν δυνατή η σύνδεση με το κοινοτικό κατάστημα. Ελέγξτε τη σύνδεσή σας και δοκιμάστε ξανά." }, "settings": { "gear": { - "title": "Ρυθμίσεις" + "title": "Προτιμήσεις" } }, "link": { - "manual": "Εγχειρίδιο ↗", - "add_appliance": "Προσθέστε τη συσκευή σας" + "maintenance_supporter": "Maintenance Supporter στο GitHub ↗", + "manual": "Εγχειρίδιο ↗" }, "tip": { "add_brand": "Η μάρκα δεν αναφέρεται; Προσθέστε την στον κατάλογο της κοινότητας", @@ -2166,7 +2098,13 @@ "rebuild": { "envelopes": "Ανακατασκευή φακέλων" }, - "cancelling": "Ακύρωση..." + "cancelling": "Ακύρωση...", + "auto_label": { + "running": "Αυτόματη επισήμανση κύκλων" + }, + "store_download": { + "running": "Λήψη από το κατάστημα" + } }, "setup": { "hdr": { @@ -2175,26 +2113,24 @@ }, "phase0": { "washer": "Το WashData ανιχνεύει ήδη τους κύκλους σας. Εγγράψτε τον πρώτο κύκλο για να ενεργοποιήσετε ονόματα προγραμμάτων και εκτιμήσεις χρόνου.", - "dishwasher": "Το WashData παρακολουθεί. Τα πλυντήρια πιάτων έχουν σύνθετους κύκλους – συνιστάται ανεπιφύλακτα η εγγραφή του πρώτου κύκλου. Αν ένας ανιχνευθείς κύκλος διαρκεί πολύ, χρησιμοποιήστε το πρόγραμμα επεξεργασίας κύκλων για να τον περικόψετε πριν αποθηκευτεί ως προφίλ.", + "dishwasher": "Το WashData παρακολουθεί. Οι κύκλοι των πλυντηρίων πιάτων είναι σύνθετοι, οπότε καταγράψτε τον πρώτο σας κύκλο. Αν ένας ανιχνευμένος κύκλος διαρκεί πολύ, περικόψτε τον στον επεξεργαστή κύκλων πριν τον αποθηκεύσετε ως προφίλ.", "generic": "Το WashData παρακολουθεί. Εγγράψτε ή επισημάνετε έναν ανιχνευθέντα κύκλο για να ξεκινήσετε να δημιουργείτε προφίλ." }, "phase1a": { - "labelled": "Καλή αρχή – το πρώτο πρόγραμμά σας αποθηκεύτηκε. Για τα πιο καθαρά δεδομένα, σκεφτείτε να εγγράψετε τον επόμενο κύκλο με το widget εγγραφής." + "labelled": "Καλή αρχή: το πρώτο σας πρόγραμμα αποθηκεύτηκε. Για τα πιο καθαρά δεδομένα, καταγράψτε τον επόμενο κύκλο με τη χειροκίνητη καταγραφή." }, "phase1b": { - "recorded": "Η εγγραφή σας αποθηκεύτηκε ως {profile_name}. Τώρα εγγράψτε ή επισημάνετε τα άλλα συνηθισμένα προγράμματά σας για να διευρύνετε την κάλυψη." + "recorded": "Η καταγραφή σας αποθηκεύτηκε ως {profile_name}. Τώρα καταγράψτε ή επισημάνετε τα άλλα συνηθισμένα προγράμματά σας." }, "phase1c": { - "verify": "Έχετε {count} προγράμματα από την κοινότητα. Εκτελέστε έναν κύκλο για να επαληθεύσετε ότι το WashData τα αναγνωρίζει σωστά – η αντιστοίχιση θα βελτιώνεται καθώς η συσκευή σας δημιουργεί το δικό της ιστορικό." + "verify": "Έχετε {count} προγράμματα από την κοινότητα. Εκτελέστε έναν κύκλο για να δείτε αν το WashData τον αναγνωρίζει· η αντιστοίχιση βελτιώνεται καθώς η συσκευή σας χτίζει δικό της ιστορικό." }, "phase2": { - "cluster": "Το WashData έχει δει {count} κύκλους που δεν αντιστοιχούν σε κανένα αποθηκευμένο πρόγραμμα – μοιάζουν μεταξύ τους. Θέλετε να δημιουργήσετε ένα νέο προφίλ για αυτούς;", + "cluster": "Το WashData έχει δει {count} παρόμοιους κύκλους που δεν αντιστοιχούν σε κανένα αποθηκευμένο πρόγραμμα. Να δημιουργηθεί νέο προφίλ γι' αυτούς;", "unmatched": "Ο τελευταίος κύκλος δεν αντιστοιχεί σε κανένα αποθηκευμένο πρόγραμμα. Είναι νέο πρόγραμμα;" }, "phase3": { - "suggestions": "Το WashData έχει προτάσεις ρυθμίσεων βάσει του ιστορικού κύκλων σας – ελέγξτε τις για βελτίωση της ακρίβειας ανίχνευσης.", - "groups": "Μερικά από τα προφίλ σας μοιάζουν με το ίδιο πρόγραμμα σε διαφορετικές θερμοκρασίες. Οργανώστε τα σε μια ομάδα για καλύτερη αντιστοίχιση.", - "phases": "Προσθέστε φάσεις προγράμματος στο {profile_name} για πιο ακριβείς εκτιμήσεις χρόνου που απομένει." + "suggestions": "Το WashData έχει προτάσεις ρυθμίσεων από το ιστορικό των κύκλων σας. Ελέγξτε τις για καλύτερη ανίχνευση." }, "phase4": { "healthy": "Αυτή η συσκευή έχει ρυθμιστεί πλήρως ({profile_count} προφίλ)." @@ -2207,8 +2143,6 @@ "create_from_cluster": "Δημιουργία προφίλ από αυτούς τους κύκλους", "create_profile": "Δημιουργία προφίλ για αυτόν", "review_suggestions": "Έλεγχος προτάσεων", - "organise_profiles": "Οργάνωση προφίλ", - "configure_phases": "Διαμόρφωση φάσεων", "skip_step": "Παράλειψη βήματος", "skip_forever": "Να μην εμφανιστεί ξανά", "hide_guidance": "Απόκρυψη οδηγιών", @@ -2219,5 +2153,53 @@ "off_delay": { "effective": "Ένας ολοκληρωμένος κύκλος περιμένει στην πραγματικότητα {eff} s, όχι {delay} s: το Ελάχιστο Κενό Απενεργοποίησης ({gap} s) είναι μεγαλύτερο και υπερισχύει." } + }, + "access": { + "edit": "Επεξεργασία", + "full": "Πλήρης", + "inherit": "Κληρονόμηση", + "none": "Καμία (κρυφό)", + "read": "Ανάγνωση" + }, + "pg_label_reason": { + "no_winner": "Κανένα πρόγραμμα δεν ταίριαξε", + "below_floor": "Κάτω από την Εμπιστοσύνη Μάθησης", + "ambiguous": "Χαρακτηρίστηκε ασαφής", + "margin": "Πολύ κοντά στο δεύτερο καλύτερο", + "unknown_profile": "Το πρόγραμμα δεν υπάρχει πλέον", + "not_evaluated": "Δεν αξιολογήθηκε" + }, + "diagram": { + "accum": "συσσ.", + "auto": "αυτόματα", + "below_off": "κάτω = σβηστό", + "confirmed": "επιβεβαιώθηκε", + "energy_counts": "μετράει η ενέργεια", + "feedback": "σχόλια", + "gap_below_min": "κενό κάτω από ελάχ. = ένας κύκλος", + "heat_phase": "φάση θέρμανσης", + "held_at_100": "σταθερό στο 100%", + "ignored": "αγνοείται", + "kept": "διατηρείται", + "max": "μέγ.", + "min": "ελάχ.", + "minus_tol": "-ανοχή", + "no_match": "χωρίς αντιστοίχιση", + "no_updates": "χωρίς ενημερώσεις", + "off_delay_wait": "αναμονή απενεργοποίησης", + "off_gap": "κενό απενεργ.", + "plus_tol": "+ανοχή", + "profile": "προφίλ", + "reading_interval": "τυπικό διάστημα μετρήσεων", + "sensor_offline": "αισθητήρας εκτός σύνδεσης: αναγκαστική διακοπή", + "si": "ΔΔ", + "spike_ignored": "η αιχμή αγνοείται", + "spike_ignored_short": "αιχμή: αγνοείται", + "start": "εκκίνηση", + "stop": "τερματισμός", + "tail_energy_resets": "ενέργεια ουράς πάνω από κατώφλι: μηδενισμός χρονομέτρου", + "thr": "κατ.", + "too_short": "πολύ σύντομο", + "tumble_kept": "οι παλμοί περιστροφής διατηρούνται" } } diff --git a/custom_components/ha_washdata/translations/panel/en.json b/custom_components/ha_washdata/translations/panel/en.json index 62404fcf..ba8e37ce 100644 --- a/custom_components/ha_washdata/translations/panel/en.json +++ b/custom_components/ha_washdata/translations/panel/en.json @@ -3,6 +3,7 @@ "min": "min", "entity_not_found": "Entity not found", "running_long": "Running long", + "stalled": "Stalled", "remaining": "remaining", "progress": "progress", "show_time_remaining": "Show time remaining", @@ -48,27 +49,28 @@ } }, "badge": { - "approved": "Approved", "artifact_tip": "{n} anomalies detected (e.g. door opened mid-cycle) – open to see them on the graph", + "artifact_tip_one": "{n} anomaly detected (e.g. door opened mid-cycle) - open to see it on the graph", + "artifact_tip_other": "{n} anomalies detected (e.g. door opened mid-cycle) - open to see them on the graph", "auto": "(auto-detected)", - "awaiting": "Awaiting approval", - "awaiting_n": "Awaiting approval · {n} confirmed", - "awaiting_tip": "Awaiting community approval", "backfilled_tip": "Detected in imported power history. Shapes program matching only, not counted in stats.", "built_in_tag": "built-in", "declining": "↘ declining", "energy_low": "Lower energy than usual", "energy_spike": "Higher energy than usual", "exists": "exists", - "fair_fit": "acceptable quality", - "fair_fit_tip": "Moderate match consistency – some cycles assigned to this profile have lower confidence scores. Label more cycles or re-record the profile to improve accuracy.", "feedback_requested": "Feedback requested", "golden_cycle": "Recorded reference cycle", "duration_outlier": "{n} odd-length cycle(s)", "imported_tip": "Imported from the community store. Used for matching only, not counted in stats.", + "matcher_unused": "Not matched to your cycles yet", + "matcher_used": "Matched {n} of your cycles", + "matcher_used_one": "Matched {n} of your cycles", + "matcher_used_other": "Matched {n} of your cycles", + "matcher_used_tip": "Cycles on this appliance that WashData matched to this imported program on its own. An import that never matches may not fit your appliance; you can remove it. Counted locally; nothing is sent to the store.", "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.", + "quiet_tail_tip": "Near the end this program goes quiet for about {mins} min, then draws about {watts} W for {secs} s. Seen in {seen} of {measured} measured cycles.", "improving": "↗ improving", "manual": "(manually selected)", "needs_review": "Needs review", @@ -76,17 +78,24 @@ "overrun": "Ran longer than usual", "overrun_ratio": "({x}x expected)", "poor_fit": "poor quality", - "poor_fit_tip": "Inconsistent match history – consider rebuilding this profile", "restart_gap_tip": "{n} HA restart gap: power trace has a hole", + "restart_gap_tip_one": "{n} HA restart gap: power trace has a hole", + "restart_gap_tip_other": "{n} HA restart gaps: power trace has holes", "reviewed": "Reviewed", "steady": "→ steady", "underrun": "Finished faster than usual", "underrun_ratio": "({pct}% of expected)", - "using_defaults": "Using shipped defaults", - "using_tuned": "Using tuned weights", - "armed": "(applies to the next cycle)" + "armed": "(applies to the next cycle)", + "awaiting_tip": "Awaiting community approval", + "awaiting_n": "Awaiting approval · {n} confirmed", + "awaiting": "Awaiting approval", + "approved": "Approved" }, "btn": { + "log_done_task": "Log done: {task}", + "add_task": "Add task", + "add_appliance": "Add appliance", + "add_brand": "Add brand", "add_device": "+ Add device", "add_device_tip": "Add another WashData device", "add_maintenance": "Add maintenance event", @@ -95,18 +104,15 @@ "analyze_import": "Analyze file", "apply_all": "Apply all", "apply_label": "Apply Label", - "apply_set_b": "Apply set B", "apply_split": "Apply Split", "apply_trim": "Apply Trim", "auto_detect_split": "Auto-detect", "auto_label_cycles": "Auto-label cycles", - "auto_label_cycles_tip": "Automatically assign profile names to unlabelled cycles whose match confidence clears the threshold", + "auto_label_cycles_tip": "Name unlabelled cycles whose match confidence clears the threshold.", "back": "Back", "browse_community_setups": "Browse community setups", "cancel": "Cancel", "clear": "Clear", - "add_brand": "Add brand", - "add_appliance": "Add appliance", "clear_debug": "Clear Debug Data", "clear_splits": "Clear", "close": "Close", @@ -118,8 +124,7 @@ "convert_to_automation": "Convert to automation", "correct": "Correct…", "create": "Create", - "create_from_cluster": "Create profile from {count} cycles", - "create_profile": "+ Create profile", + "create_profile_from_gap": "Create profile", "delete": "Delete", "delete_group": "Delete Group", "delete_group_tip": "Delete this group only - the member profiles are kept", @@ -135,12 +140,10 @@ "edit": "Edit", "export": "Export", "export_all": "Quick export everything", - "export_json": "Export to JSON", "export_selected": "Export (choose data)", "force_stop": "Force Stop", "force_stop_tip": "Immediately end the current cycle and mark it as force-stopped", "from_template": "From template ▾", - "group_suggest": "Group {n}: {members}", "hist_goto_cycles": "Show me the cycles", "hist_import_n": "Import {n} cycles", "hist_read_recorder": "Read from Home Assistant", @@ -148,20 +151,17 @@ "ignore": "Ignore", "import": "Import", "import_json": "Import from JSON", - "import_overwrite": "Import (overwrites data)", "import_power_history": "Import power history", - "import_raw": "Advanced: replace all from JSON", "import_selected": "Import selected", "inspect": "Inspect", "label": "Label", - "load": "Load", "load_more": "Load more", "manage": "Manage", "merge": "Merge", "mute_suggestion": "Stop suggesting this setting", "new_automation": "+ New Automation", "new_group": "+ New Group", - "new_group_tip": "Group near-identical profiles (same shape/duration, different temperature or spin) so the matcher reliably picks between them", + "new_group_tip": "Group near-identical profiles (same shape, different temperature or spin) so matching can tell them apart.", "new_phase": "+ New Phase", "new_profile": "+ New Profile", "new_profile_tip": "Create a new program profile from an existing labelled cycle or recording", @@ -170,12 +170,7 @@ "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", - "pg_autofill": "Auto-fill", - "pg_load_calibrated": "Load Calibrated (ML) ({n})", - "pg_load_calibrated_tip": "Stage ML-calibrated settings as Playground overrides", "pg_load_live": "Load live settings", "pg_load_live_tip": "Re-read the integration's current settings and drop every Playground edit", "pg_load_suggested": "Load suggested ({n})", @@ -187,14 +182,13 @@ "pg_preset_save_tip": "Save every value below as a named preset for this device", "pg_publish_all": "Publish {n} to integration", "pg_publish_one": "Publish {label} to the integration", - "play": "Play", "process": "Process", "process_history": "Process Now", "process_recording": "Save Recording", "process_tip": "Save the recorded trace as a new or existing profile", "rebuild": "Rebuild Envelopes", "rebuild_envelope": "Rebuild Envelope", - "rebuild_tip": "Recompute the expected power envelope (min/max band) for all profiles from their labelled cycles – run after labelling new cycles or correcting old ones", + "rebuild_tip": "Rebuild every profile's expected power band from its labelled cycles. Run after labelling or correcting cycles.", "rec_start_tip": "Begin recording the appliance's power trace – start just before running a cycle", "rec_stop": "Stop", "rec_stop_tip": "Stop recording and hold the captured trace for review", @@ -202,7 +196,6 @@ "refresh": "Refresh", "refresh_catalog": "Refresh catalog", "refresh_diag_tip": "Reload storage statistics", - "refresh_logs_tip": "Reload the latest log records", "refresh_settings_tip": "Reload settings from the server", "relabel": "Relabel ({count})", "remove": "Remove", @@ -210,11 +203,12 @@ "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_phase_tip": "Undo your edit and restore the built-in phase", "reset_to_builtin": "Reset to built-in models", - "reset_to_defaults": "Reset to defaults", + "reset_zoom": "Reset zoom", "resume_cycle": "Resume cycle", "resume_cycle_tip": "Resume the paused cycle", + "retry": "Retry", "revert": "Revert", "revert_settings": "Revert changes", "revert_settings_tip": "Restore settings from before your last save", @@ -225,11 +219,8 @@ "run_analysis": "🔍 Run suggestion analysis", "run_analysis_tip": "Analyze your recorded cycles now and refresh tuning suggestions", "run_auto_label": "Run Auto-Label", - "run_both": "Run A vs B", - "run_simulation": "Run simulation", "save": "Save", "save_access_control": "Save Access Control", - "save_appliance": "Save appliance", "save_group": "Save Group", "save_panel_settings": "Save Panel Settings", "save_phases": "Save phases", @@ -237,18 +228,17 @@ "save_reminders": "Save reminders", "save_review": "Save Review", "save_settings": "Save Settings", + "save_total_cycles": "Save total", "select": "Select", "select_all": "Select all", - "select_last20": "Last 20", "select_none": "Clear", "set_brand_model": "Set brand & model", "share": "Share", "share_device": "⬆ Share this device", - "share_device_tip": "Share this appliance and its recorded reference cycles to the community store so others with the same machine can adopt them", + "share_device_tip": "Share this appliance and its reference cycles to the community store so owners of the same machine can adopt them.", "share_n": "Share {n} cycle(s)", "share_to_store": "Share to store", "show_only": "Show only", - "skip_setup": "Skip setup", "split": "Split", "stop": "Stop", "submit_correction": "Submit Correction", @@ -256,11 +246,12 @@ "trim": "Trim", "trim_split": "Trim / Split", "undo": "Undo", + "undo_import": "Undo last import", "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.", + "unlabel_selected_tip": "Remove the label from the selected cycles. They stay in your history but stop shaping this profile.", "use": "Use", "wipe_all": "Wipe All Data", - "save_total_cycles": "Save total" + "pg_rerun": "Update simulation" }, "note": { "off_delay": { @@ -268,10 +259,6 @@ } }, "conflict": { - "anti_wrinkle_exit": { - "exit": "Must be below Stop Threshold ({stop} W); otherwise the anti-wrinkle exit power is ignored", - "stop": "Must be above Anti-Wrinkle Exit Power ({exit} W)" - }, "anti_wrinkle_max": { "max": "Must be above Start Threshold ({start} W); otherwise anti-wrinkle duration limit is bypassed", "start": "Must be below Anti-Wrinkle Max Power ({max} W)" @@ -279,10 +266,10 @@ "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." + "power_sensor": "This is the device's Power Sensor, not a price. Cycles would be costed in watts per kWh; clear this field.", + "energy_sensor": "This is the device's Energy Meter, not a price. Cycles would be costed at the meter reading per kWh; clear this field.", + "device_class": "This sensor measures {dc}, not a price per kWh. Clear this field or pick a tariff sensor.", + "unit": "This sensor reads in {unit}, not a price per kWh. Clear this field or pick a tariff sensor." }, "settings_banner": "Setting conflicts: {n}. Check the highlighted sections and fix them before saving.", "settings_banner_btn": "Go to first", @@ -317,8 +304,8 @@ "threshold": "Must be below Stop Threshold ({stop} W) to take effect" }, "pump_stuck": { - "duration": "Must be less than No-Update Timeout ({to} s) so the stuck alarm fires before the watchdog kills the cycle", - "timeout": "Must exceed Pump Stuck Duration ({ps} s) so the stuck alarm fires before the cycle is force-stopped" + "duration": "Must be below No-Update Timeout ({to} s) so the stuck alarm can fire first", + "timeout": "Must exceed Pump Stuck Duration ({ps} s) so the stuck alarm can fire first" }, "start_dur": { "sampling": "Sampling Interval exceeds Start Duration ({sdt} s); single-sample spikes can open a cycle", @@ -334,10 +321,15 @@ "watchdog": { "interval": "Should be at least 2x Sampling Interval ({si} s)", "sampling": "Sampling Interval should be at most half of Watchdog Interval ({wi} s)" + }, + "end_energy": { + "energy": "Too strict for Stop Threshold ({w} W) over Off Delay ({d} s); the cycle can only end through a fallback path", + "stop": "End Energy Threshold ({e} Wh over {d} s) only permits {p} W" } }, "hdr": { - "ab_comparison": "A/B Settings Comparison", + "custom_tasks": "Your own tasks", + "maintenance_supporter": "Need more?", "access_control": "Access Control", "add_maintenance": "Add Maintenance Event", "automation_delete": "Delete this automation", @@ -347,23 +339,18 @@ "automations": "Automations", "clear_debug": "Clear Debug Traces", "community_store": "Community Store", - "cycle_simulator": "Cycle Simulator", "diagnostics_pref": "Diagnostics", "display": "Display", - "dtw_inspector": "DTW Inspector", "export_import": "Export / Import", - "getting_started": "Getting started", "import_power_history": "Import power history", "live_power": "Live Power", "logs": "Logs", - "logs_diagnostics": "Diagnostics", "maintenance": "Maintenance Actions", "maintenance_log": "Maintenance Log", "maintenance_reminders": "Service Reminders", "manual_recording": "Manual Recording", - "manual_recording_tip": "Run a cycle intentionally while WashData records the power trace. Start just before the appliance starts, Stop when it finishes, then Process to save it as a named profile.", + "manual_recording_tip": "Record a cycle on purpose: press Start just before the appliance starts and Stop when it finishes, then Process to save it as a profile.", "ml_learned": "What WashData has learned", - "ml_matching_tuning": "Program-matching fine-tuning", "ml_settings_card": "Settings", "ml_smart_learning": "Smart Learning", "my_preferences": "My Preferences", @@ -371,7 +358,6 @@ "online_account": "Community Store & online features", "online_account_tab": "Online & Community", "panel_settings": "Panel Settings", - "param_sweep": "Parameter sweep", "pg_across_cycles": "Across your cycles", "pg_detection_params": "Detection settings", "pg_outcome": "Simulation outcome", @@ -379,16 +365,14 @@ "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", "storage_stats": "Storage Stats", - "test_history": "Test on history", "toggle_sidebar": "Toggle Home Assistant sidebar", - "tools_and_data": "Tools & Data", - "wipe_history": "Wipe History" + "wipe_history": "Wipe History", + "live_match_debug": "Live Match Debug" }, "health": { "fair": "Acceptable profile quality", @@ -396,22 +380,23 @@ "poor": "⚠ Poor profile quality" }, "lbl": { + "task_name": "Task", + "every_cycles": "Every (cycles)", + "every_days": "Every (days)", + "new_task_name": "New task", + "days_since_service": "{since} / {total} days", "actions": "Actions", "activity": "Activity", "administrators": "Administrators", "adopt_settings": "Also adopt settings", "anonymous": "Anonymous", - "approved_only": "Approved only", "aria_compare_chart": "Cycle comparison chart", "aria_cycle_chart": "Cycle power trace", "aria_envelope_chart": "Profile power envelope chart", "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", "artifact_anomaly": "Anomaly", "artifact_high_power": "High power", "artifact_interruption": "Interruption", @@ -421,12 +406,12 @@ "auto_fine_tune_off": "auto fine-tune off", "auto_fine_tune_on": "auto fine-tune on (around {hour}:00)", "avg_cost": "Avg cost", + "cat_backfill_cycles": "Cycles found in imported power history", "cat_custom_phases": "Custom phases", "cat_feedback": "Feedback & review labels", "cat_history_logs": "History & change logs", "cat_lifetime_stats": "Lifetime totals", "cat_maintenance_log": "Maintenance log", - "cat_matching_config": "Matcher tuning", "cat_ml_models": "ML models", "cat_profile_groups": "Profile groups", "cat_profiles": "Profiles (programs)", @@ -440,7 +425,6 @@ "cohesion_good": "cohesion {pct}%", "cohesion_low": "⚠ low cohesion {pct}%", "compare_profiles": "Compare with profiles", - "concurrency": "Concurrency", "confidence": "Confidence", "confidence_threshold": "Confidence threshold", "conflict_import_copy": "Import as copy", @@ -449,61 +433,50 @@ "conflict_resolution": "Name clashes", "correct_duration": "Correct Duration (min, optional)", "correct_profile": "Correct Profile", - "correlation": "Correlation", "cost": "Cost", "create_new_profile": "+ Create new profile…", "current_phase": "Current phase", - "curves": "Curves", "cycle": "Cycle", "cycle_date_display": "Cycle date display", + "cycle_context": "Recorder history around the cycle", + "cycle_context_off": "Off", + "cycle_context_minutes": "{min} min", + "cycle_context_series": "Recorder history", + "before_start": "Before start", + "after_end": "After end", "cycle_destination": "Where should imported cycles go?", - "cycle_health": "Cycle health", - "cycle_index": "cycle index →", - "cycles": "Cycles", "cycles_count": "Cycles", "cycles_lc": "cycles", "cycles_short": "cycles", "cycles_title": "Cycles ({n})", - "dashed": "dashed", "date": "Date", - "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", - "delta": "Δ", "description": "Description", "dest_real_history": "Real history (counts in stats)", "dest_reference": "Reference (shape only)", "drag_to_resize": "Drag to resize", - "dtw_blend_w": "Blend weight", - "dtw_blended": "Blended score", - "dtw_ddtw": "DDTW score", - "dtw_l1": "L1 score", - "dtw_matched_profile": "(cycle's matched profile)", "duration": "Duration", - "duration_agreement": "Duration agreement", "duration_avg": "~{v}m avg", - "emphasis": "Emphasis", "enable_access_control": "Enable per-user access control", "enable_online": "Enable online features", "end": "End", "energy": "Energy", - "energy_agreement": "Energy agreement", "envelope": "Envelope", "eta_mins": "~{n}m left", "eta_secs": "~{n}s left", - "event_timeline": "Event timeline", "event_type": "Event type", "evidence_backfill_cycles": "Found in imported power history", "evidence_real_cycles": "Cycles this machine ran", "evidence_reference_cycles": "Downloaded from the community store", "expected": "Expected", + "suspected_profile": "{name} (suspected)", + "runner_up_profile": "{name} (runner-up)", "expected_duration": "Expected Duration (min)", "file_kb": "File (kB)", - "final_score": "Final score", "flags": "Flags", "font_size": "Panel font size", "from": "From", @@ -512,7 +485,6 @@ "group_name": "Group name", "ha_restarted": "HA restarted", "head_trim": "Head Trim (s)", - "health": "Health", "hide_tabs": "Hide tabs for non-admins", "hist_csv_data": "CSV data", "hist_from_recorder": "Or read it from Home Assistant", @@ -526,7 +498,6 @@ "hist_skip_short": "too few readings", "hist_skip_sparse": "readings too far apart", "import_mode": "How to combine", - "in_use": "In use", "include_phase_map": "Include phase map", "include_settings": "Include detection & matching settings", "input": "Input:", @@ -536,29 +507,24 @@ "last_checked": "Last checked:", "load_from_file": "Load from file", "loading_paren": "(loading…)", - "mae_score": "MAE score", "manual_duration": "Manual Duration (min, optional)", "match": "Match", "members": "Members", - "metric": "Metric", "mode_existing_profile": "Add to Existing Profile", "mode_merge": "Merge (keep mine)", "mode_new_profile": "Create New Profile", "mode_replace": "Replace selected", "models_fine_tuned": "(fine-tuned models: {count})", - "n_classic_suggestions": "{n} classic", "n_imported_note": ", {n} imported", - "n_ml_suggestions": "{n} ML", "n_selected": "{n} selected", "n_shown": ", {n} shown", "n_tuning_suggestions": "{n} tuning suggestion", - "na": "n/a", + "n_tuning_suggestions_one": "{n} tuning suggestion", + "n_tuning_suggestions_other": "{n} tuning suggestions", "name_placeholder": "(name)", "new_profile_name": "New Profile Name", "no_duration": "no duration", "notes": "Notes", - "of": "of", - "outcome": "Outcome", "panel_default_tab": "Default tab", "panel_language": "Panel language", "pct_match_confidence": "{pct}% match confidence", @@ -570,25 +536,17 @@ "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", @@ -608,10 +566,8 @@ "pg_group_matching": "Program matching", "pg_group_timing": "Timing rules", "pg_idle": "Idle", - "pg_last_run": "Last run: {date}", "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", @@ -621,7 +577,6 @@ "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", @@ -630,23 +585,10 @@ "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)", - "pg_stress_idle_w_desc": "Override the auto-detected standby floor (leave blank for auto).", - "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", - "pg_tile_detected_tip": "Cycles where power crossed the start threshold long enough to register as running", - "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_time_left_model_tip": "Estimated from the program's phases, not a fixed countdown", "pg_vs_current": "vs current", "pg_weak_match": "Weak match", "pg_zoom_hint": "double-click to reset zoom", @@ -656,54 +598,36 @@ "phase_timeline": "Phase timeline", "power": "Power", "profile": "Profile", - "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. Pick one before starting the appliance and it is applied as soon as the next cycle begins.", + "program_tip": "Choose which program the current cycle is matched to. Auto-detect picks the best match. Pin a program when auto-detect is wrong; pick one before starting and it applies to the next cycle.", "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.", + "envelope_position_tip": "Position on the matched program's recorded curve: {pct}%. Found by aligning the cycle to the profile, so it stays right when a run is longer or shorter. Updates while the appliance is quiet, so it can lag.", "quality": "Quality", - "rate_n": "{n} stars", - "rate_quality": "Rate quality", "raw_socket": "Raw socket", - "read_only": "Read-only", "ref_cycle": "Reference Cycle (optional)", - "refresh_interval": "Refresh interval (s)", "remaining": "Remaining", "reminder_every": "Remind every (cycles)", "remove_label": "- Remove label -", "rename_profile": "Rename Profile", - "replay_duration": "Replay", "resulting_profile": "Resulting profile", "reviewed_on": "reviewed {date}", "save_mode": "Save Mode", - "score": "Score", - "score_breakdown": "Score breakdown", "seconds_from_start": "Seconds from start", "select_profile": "Select Profile", "setting": "Setting", - "settings_a": "Settings A (current)", - "settings_a_short": "A", "settings_advanced": "Advanced", - "settings_b": "Settings B (test)", - "settings_b_short": "B", "settings_basic": "Basic", "settings_detail_level": "Settings detail level", - "settings_overrides": "Settings overrides", "shape": "Shape", "show_contributor": "Show contributor names", "show_debug": "Show live match debug card on the Status page (confidence, ambiguity, top candidates)", "show_expected": "Show expected curve overlay (matched profile, orange)", "show_raw": "Show raw socket toggle in live power graph", "sparkline": "Recent cycle-duration trend", - "stage2": "Stage 2 – core similarity", - "stage3": "Stage 3 – DTW", - "stage4": "Stage 4 – agreement", "start": "Start", "status": "Status", - "store_brand": "Appliance brand", - "store_model": "Appliance model", "tags": "Tags", "tail_trim": "Tail Trim (s)", "task_history_import": "Scanning power history", @@ -728,14 +652,39 @@ "unknown": "Unknown", "unlabelled": "Unlabelled", "unlabelled_paren": "(unlabelled)", - "warp_path": "Warp path", "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" + "correct_total_cycles": "Correct the total", + "hist_reason_already_recorded": "overlaps a cycle WashData already recorded", + "share_consent": "I confirm these cycles ran to normal completion without interruption", + "show_internal": "Show internal settings", + "show_internal_tip": "Settings WashData tunes for you (sampling, watchdog, match cadence, thresholds). Shown when this device has its own value, a conflict or a suggestion.", + "rate_n": "{n} stars", + "rate_quality": "Rate quality", + "pg_autolabel": "Auto-label", + "pg_label_review": "Asks for review", + "pg_ev_match_reverted": "Match reverted", + "pg_ev_verified_pause": "Verified pause", + "n_samples": "{n} samples", + "n_samples_one": "{n} sample", + "n_samples_other": "{n} samples", + "n_cycles": "{n} cycles", + "n_cycles_one": "{n} cycle", + "n_cycles_other": "{n} cycles", + "energy_per_cycle": "{v}/cycle", + "energy_total": "{v} total", + "automation_off": "(off)", + "ref_cycle_none": "None", + "share_name": "Show my name on what I share", + "pg_compare_profile": "Compare with profile", + "pg_obj_endlag": "End delay (median)", + "pg_obj_early": "Early-end rate", + "pg_obj_split": "Split rate", + "pg_sweep_cycles": "Recent cycles" }, "log": { "all_levels": "All levels", @@ -744,6 +693,10 @@ "search_ph": "Search logs…" }, "maint": { + "salt": "Refill salt", + "rinse_aid": "Refill rinse aid", + "lint_filter": "Clean lint filter", + "condenser_clean": "Clean condenser", "descale": "Descale", "filter_clean": "Clean filter", "drum_clean": "Clean drum", @@ -752,11 +705,9 @@ }, "ml": { "builtin_models": "● Using built-in models", - "fit_fair": "Fair", "fit_good": "Good", "fit_slight": "Slight", "fit_strong": "Strong", - "fit_weak": "Weak", "fit_word": "fit", "personalized": "● Personalized to this machine", "better_than_baseline": "{pct}% better than the baseline estimate ({metric})", @@ -764,23 +715,35 @@ "trend_declining_tip": "This model's fit has slipped across recent re-checks – reviewing more cycles may help it re-learn.", "trend_steady_tip": "This model's fit has held roughly steady across recent re-checks.", "cap_label": { - "end": "Cycle-end detection", - "quality": "Cycle quality check", - "live_match": "Program matching", - "remaining_time": "Time-remaining estimate", "total_energy": "Energy estimate" }, "cap_blurb": { - "end": "Knowing when a cycle has truly finished", - "quality": "Spotting mis-detected or corrupted cycles", - "live_match": "Identifying the running program sooner", - "remaining_time": "Predicting how long is left", "total_energy": "Predicting total energy and cost" }, - "metric_auc": "AUC {auc} on held-out data", - "metric_mae": "error {model} vs {baseline} baseline" + "metric_mae": "error {model} vs {baseline} baseline", + "fine_tuned_at": "fine-tuned {when}", + "never": "never", + "trained_unknown": "at an unknown time", + "held_out_count": "measured on {count} held-out cycles", + "held_out_count_one": "measured on {count} held-out cycle", + "held_out_count_other": "measured on {count} held-out cycles", + "last_run_not_promoted": "Last check ({when}): {reason}", + "skip_reason": { + "insufficient_rows": "Not enough usable cycles yet: {count} labelled, clean cycles so far.", + "insufficient_rows_one": "Not enough usable cycles yet: {count} labelled, clean cycle so far.", + "insufficient_rows_other": "Not enough usable cycles yet: {count} labelled, clean cycles so far.", + "holdout_too_small": "Too few cycles to test a new model fairly: {held_out} could be set aside for testing, {min} are needed.", + "not_better_than_naive": "The new model was not clearly better than the simple estimate (error {model} vs {naive}).", + "not_better_than_incumbent": "The new model was not better than the one in use (error {model} vs {incumbent}), so the current one was kept.", + "fit_failed": "Training failed: {error}", + "unknown": "Nothing new was learnt." + } }, "modal": { + "remove_task_title": "Remove Task", + "remove_task_msg": "Remove the reminder \"{task}\"? Its entries in the maintenance log are kept.", + "apply_suggestions_msg": "Change these {n} settings? The integration reloads after saving.", + "apply_suggestions_title": "Apply suggestions", "auto_label": "Auto-Label Cycles", "clear_debug_msg": "Delete all stored debug traces?", "clear_debug_title": "Clear Debug Data", @@ -795,6 +758,8 @@ "delete_phase_title": "Delete Phase", "discard_recording_msg": "Discard the saved recording? This cannot be undone.", "discard_recording_title": "Discard Recording", + "discard_settings_msg": "{n} setting change(s) are not saved yet. Leave Settings and discard them?", + "discard_settings_title": "Discard unsaved changes?", "dismiss_all_msg": "Dismiss all {count} pending feedback requests?", "dismiss_all_ok": "Dismiss All", "dismiss_all_title": "Dismiss All Feedbacks", @@ -804,7 +769,6 @@ "force_stop_msg": "Force-stop the active cycle now? The cycle will be saved as interrupted.", "force_stop_title": "Force Stop Cycle", "history_import": "Import power history", - "import_config": "Import Configuration", "import_wizard": "Import - choose data", "label_cycle": "Label Cycle", "merge_cycles": "Merge {n} Cycles", @@ -817,62 +781,68 @@ "relabel_cycles": "Relabel {count} cycles", "remove_legacy_msg": "Remove the legacy custom actions? They will stop firing on cycle events. This cannot be undone from the panel.", "remove_legacy_title": "Remove Legacy Actions", + "reset_phase_msg": "Undo your edit of \"{name}\" and restore the built-in phase? Programs keep their ranges.", + "reset_phase_title": "Reset Phase", "store_import": "Import reference cycle", "store_share": "Share to community store", "store_share_device": "Share this device", + "undo_import_msg": "This device's data and settings go back to how they were before the last import ({when}). Cycles recorded and changes made since then are lost.", + "undo_import_ok": "Undo import", + "undo_import_title": "Undo last import", "wipe_all_msg": "⚠️ This permanently deletes ALL cycles and profiles. This cannot be undone.", "wipe_all_ok": "Wipe Everything", - "wipe_all_title": "Wipe All Data" + "wipe_all_title": "Wipe All Data", + "profile_title": "Profile · {name}" }, "msg": { + "custom_task_limit": "You have reached the limit of {max} tasks.", + "custom_tasks_intro": "Add anything else you want to be reminded of. A task is due when either interval is reached; leave one blank or 0 to use only the other.", + "maintenance_due_sensor_hint": "The Maintenance due binary sensor is on while any task is due, for your own notification automations.", + "maintenance_supporter_intro": "Maintenance Supporter is a separate integration that does much more: any appliance or task, schedules by time, usage or a sensor, notifications and a history. It can use the WashData cycle counter.", "adopt_settings_hint": "Overwrite this device's detection & matching thresholds with the shared ones. Your notifications, entities and energy price are never changed.", - "advisory_duration_trend_up": "'{name}' cycles are running progressively longer (about +{pct}% per cycle). If the appliance's behaviour changed, re-record or rebuild this profile.", - "advisory_energy_trend_up": "'{name}' is drawing progressively more energy (about +{pct}% per cycle) - worth checking the appliance if that is unexpected.", - "advisory_phase_inconsistent": "'{name}' looks like it mixes different programs or temperatures - its cycles heat for very different lengths of time. Splitting it into separate profiles (e.g. per temperature) will improve matching and time estimates.", - "advisory_phase_inconsistent_title": "⚠ Possibly mixed programs", - "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.", + "advisory_duration_trend_up": "'{name}' cycles keep getting longer (about +{pct}% per cycle). If the appliance changed, re-record or rebuild this profile.", + "advisory_energy_trend_up": "'{name}' uses more energy each cycle (about +{pct}% per cycle). Check the appliance if that is unexpected.", + "advisory_poor_health": "'{name}' fits poorly: its recent cycles vary a lot or match weakly. Review its cycles or re-record it to keep matching and estimates accurate.", + "advisory_shape_drift": "'{name}' has drifted from its original power shape, perhaps from limescale or wear. Consider re-recording it with recent cycles.", + "advisory_shape_drift_corr": "'{name}' has drifted from its original power shape (correlation {corr}), perhaps from limescale or wear. Consider re-recording it with recent cycles.", + "advisory_unmatchable": "'{name}' can never match: no cycle with power data is behind it. Label or record a cycle of this program to fix it.", + "advisory_unmatchable_no_duration": "'{name}' can never match: its cycle has no usable duration. Re-record this program or set its expected duration.", "analyzing": "Analyzing…", "appliance_monitor": "Appliance monitor", "artifact_dip_detail": "Dropped below the usual power band for ~{n}s.", "artifact_footer": "Highlighted on the graph above. These are transient artifacts (e.g. the door opened mid-cycle), not necessarily problems.", "artifact_header": "{n} anomalies detected during this cycle", - "artifact_pause_detail": "Power dropped to near zero for ~{n}s then resumed – likely the door was opened mid-cycle or the cycle was paused.", + "artifact_header_one": "{n} anomaly detected during this cycle", + "artifact_header_other": "{n} anomalies detected during this cycle", + "artifact_pause_detail": "Power dropped to near zero for ~{n}s, then resumed. The door was probably opened or the cycle paused.", "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, then unlabel them (kept in history, but no longer shaping this profile) or delete them outright.", + "cleanup_intro": "All labelled cycles overlaid. Tick outliers, then unlabel them (kept in history, no longer shaping this profile) or delete them.", "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)", "compare_overlay_tip": "Overlay learned profile envelopes to see which program each cycle resembles.", - "compare_profiles_tip": "Overlay other profile envelopes on the chart above to see which one best fits this cycle.", + "compare_profiles_tip": "Overlay other profiles on the chart to see which fits this cycle best.", "compare_selected_cycles": "Selected cycles (solid) – show / hide", - "connect_to_confirm": "Connect in the settings gear to confirm or rate.", - "consider_new_profile": "Consider creating a new profile.", - "coverage_gap": "recent cycles have no matching profile ({pct}% of last 30).", - "coverage_gap_similar_cycles": "{count} similar unlabelled cycles found – create a profile to start matching them.", "cycles_deleted": "{count} cycle(s) deleted", - "dest_real_history_hint": "Imported cycles count as this device's own history and feed energy/usage stats. Use for moving one appliance to a new install.", + "cycle_context_loading": "Loading recorder history…", + "cycle_context_note": "Grey: the power sensor's recorder history around this cycle. It is not part of the cycle and does not affect duration, energy or matching.", + "cycle_context_none": "The recorder holds no readings around this cycle (Home Assistant keeps 10 days of history by default).", + "cycle_context_tip": "Display only: shows the power sensor's recorder history around this cycle, if still kept. To add failed start attempts to future cycles, use Curve Pre-roll (Settings > Detection > Cycle Start), which changes stored durations.", + "dest_real_history_hint": "Imported cycles count as this device's own history and statistics. Use this to move an appliance to a new install.", "dest_reference_hint": "Imported cycles only improve program matching and never affect usage/energy statistics.", - "device_type_mismatch_warn": "This export is from a different appliance type ({src} vs {local}). Programs and cycles can still be imported as reference data, but device-specific settings and real-history import are disabled.", + "device_type_mismatch_warn": "This export is from a different appliance type ({src} vs {local}). Programs and cycles can still be imported as reference data, but not device settings or real history.", "diagnostics_load_failed": "Could not load diagnostics: {error}", - "duration_trend_down_tip": "Duration down ({pct}%/cycle)", - "duration_trend_up_tip": "Duration up ({pct}%/cycle)", - "energy_trend_down_tip": "Energy down ({pct}%/cycle)", - "energy_trend_up_tip": "Energy up ({pct}%/cycle)", "enough_data": "Enough data to learn from ({current}/{min} cycles).", "export_description": "Choose exactly which profiles, cycles, settings and more to export to JSON, or analyze a file and import only the parts you want.", - "export_select_intro": "Tick exactly what to include. Selecting profiles without their cycles still exports a matchable program (its learned shape travels along).", + "export_select_intro": "Tick what to include. Profiles without their cycles still export a matchable program.", "feedback_cycles_pending": "To review: {n}", "feedback_prompt": "Confirm it was right, correct the program, or ignore.", "feedback_relabel_hint": "Re-labelling this cycle resolves it too.", "filter_by_profile": "Filter by profile…", "font_size_hint": "Make everything in this panel larger or smaller. Applies to your account on this device.", - "group_modal_help": "Group programs with the same shape that differ in temperature/spin (durations may vary). Matching scores the group as one candidate, then picks the best-fitting member. Pick at least 2; the overlay shows how alike they are.", + "group_modal_help": "Group programs with the same shape that differ in temperature or spin. Matching scores the group as one, then picks the best-fitting member. Pick at least 2; the overlay shows how alike they are.", "group_not_cohesive": "These profiles aren't similar enough to group reliably, so matching treats them individually until you remove the outlier or split the group.", "group_preview_hint": "Tick 2+ members to preview and compare their power curves.", "head_trim_hint": "Remove this many seconds from the start", @@ -883,50 +853,49 @@ "hist_found": "Found {n} cycles. Untick anything that does not look like a real run - nothing is stored until you import.", "hist_import_failed": "Import failed.", "hist_imported_count": "{n} cycles imported.", - "hist_input_hint": "Upload a CSV downloaded from the History panel (entity, state, last changed), or let WashData read the sensor's history directly. Detection then runs over it exactly as it does live, and you choose which of the cycles it finds to keep.", - "hist_next_step": "They are in your Cycles list, tagged as imported history. Open one and use Label to name the program it belongs to.", + "hist_input_hint": "Upload a CSV from the History panel (entity, state, last changed), or let WashData read the sensor's history directly. Live detection runs over it and you choose which cycles to keep.", + "hist_next_step": "They are in your Cycles list, tagged as imported history. Open one and use Label to name its program.", "hist_none_found": "No cycles could be detected in that history.", "hist_other_entity": "{n} readings for other entities ignored", "hist_recorder_empty": "Home Assistant has no detailed history for this sensor in that window.", - "hist_recorder_hint": "Reads from the date you pick up to now. Home Assistant keeps detailed history for 10 days by default and only hourly averages after that, which are too coarse to detect cycles from - pick a date further back only if your recorder is set to keep more.", + "hist_recorder_hint": "Reads from the date you pick until now. Home Assistant keeps detailed history for 10 days by default; older hourly averages are too coarse for cycle detection.", "hist_rows_read": "{n} readings read", "hist_scan_capped": "Only the first candidates are shown ({n} were found).", - "hist_scan_expired": "That scan is no longer available. Please scan again.", + "hist_scan_expired": "That scan is no longer available. Scan again.", "hist_scan_failed": "Scanning failed.", - "hist_scanning": "Replaying your history through the detector. This runs in the background - you can close this dialog and come back to it.", + "hist_scanning": "Running your history through the detector in the background. You can close this dialog and come back.", "hist_settings_used": "Detected using this device's current settings (minimum power {w} W, off delay {s} s).", "hist_skipped_spans": "Skipped stretches", - "import_analyze_hint": "Load an exported file (or paste its JSON). WashData analyzes it and shows exactly what can be imported before anything changes.", - "import_history_description": "Already had a smart plug before WashData? Upload a history export of its power sensor, or read it straight from Home Assistant, and the normal detection runs over it so past cycles turn up in your Cycles list ready to name.", - "import_intro": "Load an exported file or paste a JSON payload below.", - "imported_history_readonly": "Detected in imported power history. It shapes program matching but is not counted in your statistics, and cannot be trimmed or split. Label it to name the program.", - "imported_readonly": "Imported from the community store. Shown for reference and matching. It is not counted in your stats and cannot be edited.", - "include_settings_hint": "Share this device's recognition and matching thresholds (not your notifications, entities or energy price). Adopters choose whether to apply them.", + "import_analyze_hint": "Load an exported file (or paste its JSON). WashData shows what can be imported before anything changes.", + "import_history_description": "Had a smart plug before WashData? Upload its power history or read it from Home Assistant. Detection runs over it, and past cycles appear in your Cycles list ready to name.", + "import_undo_hint": "A restore point from before the last import ({when}) is kept: {profiles} profile(s) and {cycles} cycle(s).", + "imported_history_readonly": "Found in imported power history. It helps matching but is not counted in your statistics and cannot be trimmed or split. Label it to name the program.", + "imported_readonly": "Imported from the community store. Used for reference and matching, not counted in your stats, and cannot be edited.", + "include_settings_hint": "Share this device's detection and matching thresholds (not notifications, entities or energy price). Others choose whether to apply them.", "legacy_actions_title": "{count} legacy custom action still running", + "legacy_actions_title_one": "{count} legacy custom action still running", + "legacy_actions_title_other": "{count} legacy custom actions still running", "live_chart_loading": "Live power chart appears as readings arrive.", "loading": "Loading…", "loading_curve": "Loading curve…", "loading_settings": "Loading settings…", "log_buffer_hint": "Newest first · buffers the last 500 ha_washdata records since restart · drag the bottom edge to resize.", - "maintenance_advisory": "Increasing duration/energy may indicate appliance maintenance needed (e.g. descaling, filter cleaning).", "maintenance_due": "Maintenance due: {items}", - "maintenance_intro": "Log servicing you perform on this appliance and get reminded when each task is due again.", + "maintenance_intro": "Log servicing on this appliance and get reminded when each task is due again.", "maintenance_load_error": "Could not load maintenance data: {error}", "maintenance_requires_access": "Maintenance and export/import require full access.", - "manual_duration_hint": "The profile's average/expected cycle length, used for time-remaining estimates. Edit to set it; leaving it unchanged keeps the current value.", - "manual_duration_ref_hint": "Only used when no reference cycle is selected – a reference cycle sets the duration from its own length.", - "matching_tuning_intro": "When learning, WashData also adjusts how much program matching weighs shape versus duration and energy.", + "manual_duration_hint": "The profile's expected cycle length, used for time-remaining estimates. Leave it unchanged to keep the current value.", + "manual_duration_ref_hint": "Only used without a reference cycle; a reference cycle sets the duration from its own length.", + "duration_learned_hint": "Worked out from this program's cycles (outliers left out) and updated after each new one. To change it, trim or remove the cycles that are off.", "merge_hint": "Imported items are added; nothing local is lost. Name clashes are resolved below.", "merge_intro": "The selected cycles are combined into one (chronological order; gaps filled with 0 W). Pick the resulting profile.", "merge_no_imports": "Imported recordings cannot be merged into a real cycle. Deselect them to merge.", "ml_intro": "WashData ships with smart models that work out of the box.", "ml_learned_intro": "Models fine-tuned to this machine.", - "ml_loading": "loading ML…", "ml_settings_intro": "Two independent switches: one applies the models while a cycle runs, the other lets WashData fine-tune them to your machine over time.", - "model_not_found": "Not in the catalog yet.", "n_suggestions_muted": "{count} muted; the auto-tuner will not propose these.", - "name_first_program": "You have enough cycles – name your first program to start matching.", - "near_duplicate_cluster": "near-duplicate profile cluster detected. Grouping lets matching reliably pick between look-alikes (e.g. same program at different temperature/spin).", + "n_suggestions_muted_one": "{count} suggestion muted; the auto-tuner will not propose it.", + "n_suggestions_muted_other": "{count} suggestions muted; the auto-tuner will not propose these.", "no_cycles_match": "No cycles match the current filter.", "no_cycles_profile": "No cycles for this profile.", "no_cycles_selected": "Select at least one cycle first.", @@ -953,35 +922,25 @@ "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.", + "odometer_correct_hint": "Set this if WashData counted a run that never happened, or the appliance ran cycles before WashData was installed.", + "odometer_intro": "This total only goes up. Deleting a cycle does not lower it, 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", - "onboarding_watching": "Run your appliance normally – WashData is watching. After 3 cycles, program matching will begin.", - "online_intro": "Share and download reference cycles with other WashData users. All online features are opt-in and off by default.", - "online_intro_global": "Browse and share reference recordings with other WashData users, and confirm appliance entries. One connection applies to your whole WashData integration; appliance brand and model are set per device under Basic. All online features are opt-in and off by default.", + "online_intro_global": "Browse and share reference recordings with other WashData users. One connection covers all your WashData devices; brand and model are set per device under Basic. Online features are off until you turn them on.", "online_unavailable": "Online features are not available on this server.", "pending_feedback": "Pending detection feedback", "performance_trend": "Performance trend ({n} cycles)", - "performance_trending": "Performance trending", - "pg_analysis_empty": "Load a cycle to see match analysis.", - "pg_analysis_empty2": "Press Run to see match analysis.", - "pg_analysis_hint": "Select a cycle and click ↺ to load analysis.", "pg_analysis_hint2": "Pick a cycle and press Run to load match analysis.", "pg_apply_confirm": "Apply best value: {label} = {value}?", "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_intro2": "Replay recent cycles through the real detector and matcher with the settings above. Click a row to load it in the graph; change a setting to compare before and after.", "pg_history_summary": "{detected}/{total} detected · {correct} matched correctly", + "pg_in_sample_note": "These cycles helped build the profiles they are matched against, so results are optimistic: expect slightly fewer correct matches on new cycles.", "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.", @@ -992,84 +951,65 @@ "pg_publish_one_confirm": "Save {label} = {value} to this device's settings?", "pg_restart_note": "Restart Home Assistant to enable simulation tools.", "pg_row_load_hint": "Load this cycle in the graph above", - "pg_running_history": "Replaying your cycles…", - "pg_running_sweep": "Testing values across your cycles…", - "pg_select_cycle_hint": "Select a cycle and click ↺ to load", - "pg_sim_progress": "{done} / {total} cycles", "pg_simulating": "Simulating cycle…", - "pg_starting": "Starting…", - "pg_stress_above_threshold_detail": "Idle draw ~{idle}W is at or above the effective stop threshold ({stop}W) -- the cycle never registered as quiet. Raise stop_threshold_w to fix.", - "pg_stress_hit_cap_detail": "Cycle ran {h}h {m}m without stopping -- force-stopped by the safety cap. Idle draw {idle}W vs stop threshold {stop}W.", - "pg_stress_synthetic": "idle simulation starts here", - "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.", - "pg_verdict_good": "Well tuned: most cycles are correctly identified and matched.", - "pg_verdict_ok": "Acceptable: some cycles missed. Try lowering the start threshold.", "phase_catalog_intro": "Named segments of a cycle (Pre-wash, Heating, Spin…). Assign them to a profile from its control panel.", "phase_ranges_intro": "Phase ranges (minutes from cycle start) overlaid on the average curve. Edit values to preview live.", "pick_brand_first": "Pick an appliance brand first.", - "playground_intro": "A safe sandbox to replay past cycles with different settings, compare two setups side by side, and inspect how a cycle aligns to a profile. Nothing here changes your live data until you explicitly apply it.", - "playground_needs_restart": "Restart Home Assistant to enable the Playground tools.", - "preferences_admin": "Preferences, panel & access control", - "preferences_adv": "Preferences", + "playground_intro": "Try settings on your real cycle data. Nothing here changes your live settings until you apply it.", "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), recost cycles against your recorded energy prices, and recompute cycle health. Run this after a batch of reviews.", + "process_history_hint": "Re-run matching on all stored cycles, refresh suggestions, retrain ML models (if enabled), recost cycles from recorded prices and recompute cycle health. Run it 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.", - "rbac_hint": "When off, every Home Assistant user has full access (the default). Administrators always have full access and can manage everyone.", - "recent_logs": "Recent ha_washdata records", + "rbac_hint": "Off: every Home Assistant user has full access (default). Administrators always have full access and can manage everyone.", "recording_in_progress": "Recording in progress", - "refresh_catalog_hint": "The community brand and appliance lists are cached to keep the shared store within its daily budget. Refresh to pick up entries added or approved by others.", + "refresh_catalog_hint": "Community brand and appliance lists are cached to save the store's daily budget. Refresh to see entries others added or approved.", "reminders_intro": "Show a reminder in the panel this many cycles after the last service. Leave blank or 0 to turn a reminder off.", + "replace_undo_hint": "A restore point is saved first, so you can undo this import from Export / Import.", "replace_warn": "Each ticked category is wiped and replaced from the file. Unticked categories are left untouched.", "restart_gap_footer": "Highlighted on the graph. Power data is missing for these intervals; matching used only real readings.", "restart_gap_header": "{n} HA restart gap during this cycle", + "restart_gap_header_one": "{n} HA restart gap during this cycle", + "restart_gap_header_other": "{n} HA restart gaps during this cycle", "restart_gap_item": "{dur} gap", - "review_confirm_help": "Confirm whether this cycle was detected correctly. Your reviews train the model on your machine - the more cycles you confirm, the better matching and health scoring get. A quick Good/Bad is enough.", + "review_confirm_help": "Was this cycle detected correctly? Confirming cycles improves matching for your machine. A quick Good/Bad is enough.", "review_in_settings": "Review in Settings", "review_notes_placeholder": "Notes (optional)", "review_notes_tip": "Free-text notes for your own reference. Not used by matching or training.", - "review_profile_tip": "The program this cycle is labelled as. If the auto-detected program was wrong, correct it here - labelling teaches matching for future cycles.", - "review_quality_tip": "How clean this cycle is. Good = a textbook example of this program; Bad = detected but noisy or atypical; Unusable = mis-detected (merged, truncated or spurious). Drives the health score and which cycles are allowed to train the model.", - "review_recorded_tip": "Mark this as a hand-picked reference cycle for its program - the same role as a manually recorded cycle. Reference cycles are always kept, seed the matching template, and are never dropped by cleanup. (This is the \"golden\"/recorded flag; both are the same thing.)", - "review_tags_tip": "Optional flags describing what went wrong with this cycle, so training and cleanup can account for it.", + "review_profile_tip": "The program this cycle is labelled as. If it is wrong, correct it here; labels teach matching.", + "review_quality_tip": "How clean this cycle is. Good: a textbook run of this program. Bad: detected, but noisy or unusual. Unusable: mis-detected (merged, cut short or spurious). Affects the health score and which cycles train the model.", + "review_recorded_tip": "Mark this as a hand-picked reference cycle for its program, like a manually recorded cycle. Reference cycles are always kept, seed the matching template, and are never removed by cleanup.", + "review_tags_tip": "Optional flags for what went wrong, so training and cleanup can allow for it.", "review_to_cycles": "Open the Cycles review queue", "samples_decimated": "Showing {shown} of {total} samples (thinned for display; peaks kept). A wide gap here is thinning, not missing data.", - "saving_triggers_reload": "Saving triggers an integration reload. HA entities may briefly show as unavailable.", + "saving_triggers_reload": "Saving reloads the integration. Entities may briefly show as unavailable.", "search_placeholder": "Search settings…", "see_recorder": "See recorder widget below", "setting_changed": "Changed from {old} to {new} on {date}", "settings_basic_note": "Showing essential settings. Switch to Advanced for the full list.", - "shape_drift_advisory": "⚠ Shape drifting", - "shape_drift_detail": "The power pattern for this profile has shifted over time – possible appliance wear or maintenance needed (e.g. descaling, filter cleaning).", - "share_consent": "I confirm these cycles ran to normal completion without interruption", "share_device_none": "No shareable cycles yet. Mark a recorded or hand-picked cycle as a reference cycle (⭐) in the Cycles tab first.", "share_guideline_naming": "Name each profile exactly as shown on the appliance dial or display (e.g. 'Cotton 40', 'Eco 60').", - "share_guideline_quality": "Only share cycles that completed normally -- no mid-cycle interruptions, door-open events, or power blips.", + "share_guideline_quality": "Only share cycles that finished normally: no interruptions, door openings or power blips.", "share_guideline_review": "Your upload starts as pending and appears publicly once enough community members confirm it.", "share_guidelines_title": "Before you share", "share_profile_no_cycles": "No reference cycles – mark a cycle as ⭐ in the Cycles tab to include this profile", "show_all_settings": "Show all settings", "showing_suggestions": "Showing {count} setting with suggestions.", - "split_intro": "Click the graph to add or remove a split point, or auto-detect by idle gaps. Each resulting segment can get its own profile.", - "storage_diagnostics": "Storage stats, maintenance, export/import", - "store_declare_appliance": "Tell WashData which appliance you own and this tab shows the setups other people have shared for it. You can also type a brand above to look around.", - "store_download_device_intro": "Adopt every shared program and its reference cycles onto your device. Your own recorded cycles and stats are not affected.", - "store_enable_hint": "Enable online features in Settings to browse and import community reference cycles.", - "advisory_duration_outlier": "{n} cycle(s) labelled ‘{name}’ are too far from its usual length to ever match it (worst: {ratio}x). They are probably mislabelled, or one recording captured two runs. Re-label or split them so they stop skewing this program’s time estimate.", + "showing_suggestions_one": "Showing {count} setting with suggestions.", + "showing_suggestions_other": "Showing {count} settings with suggestions.", + "split_intro": "Click the graph to add or remove a split point, or auto-detect by idle gaps. Each part can get its own profile.", + "store_declare_appliance": "Tell WashData which appliance you own to see setups others shared for it. Or type a brand above to look around.", + "store_download_device_intro": "Add every shared program and its reference cycles to your device. Your own cycles and stats are not affected.", + "store_enable_hint": "Turn on online features in Preferences > Online & Community to browse and import community reference cycles.", + "advisory_duration_outlier": "{n} cycle(s) labelled ‘{name}’ are too far from its usual length to ever match (worst: {ratio}x). They are probably mislabelled or hold two runs. Relabel or split them so they stop skewing its time estimate.", "standby_above_stop_title": "Still drawing {idle} W when the cycle ended", "standby_above_stop_sub": "{n} of the last {total} cycles. The off delay only starts once power is below the Stop Threshold ({stop} W), so cycles finish late or need stopping by hand.", "store_picker_offline": "Enable online features in the settings gear to pick from the community catalog.", "store_share_device_intro": "Upload {brand} {model} with the reference cycles you select. Others with the same appliance can adopt your programs. Entries are reviewed before appearing publicly.", "store_share_intro": "Upload this reference cycle so others with the same appliance can use it. It is reviewed before appearing publicly.", - "store_sibling_hint": "Nothing shared for your exact model? A closely-related model from the same brand is usually a good starting point.", + "store_sibling_hint": "Nothing shared for your model? A related model from the same brand is usually a good start.", "sug_mute_failed": "Could not mute suggestion", "sug_muted": "Won't suggest this setting again", "sug_staged": "Set {key} = {val}. Save to apply.", @@ -1094,7 +1034,6 @@ "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}", "toast_revert_failed": "Revert failed: {error}", "toast_reverted": "{key} reverted", @@ -1106,53 +1045,82 @@ "trim_destructive_confirm": "This keeps only {pct}% of the cycle and cannot be undone. Continue?", "trim_intro": "Drag the red handles, or enter values. Everything outside the window is removed.", "tuning_suggestions_available": "{count} tuning suggestion available from observed cycles. They appear beside the relevant fields.", + "tuning_suggestions_available_one": "{count} tuning suggestion available from observed cycles. It appears beside the relevant field.", + "tuning_suggestions_available_other": "{count} tuning suggestions available from observed cycles. They appear beside the relevant fields.", "unsure_detected_prefix": "WashData is unsure it detected", - "updated": "Updated", "warmup_badge": "Still learning ({done}/{needed} cycles)", - "warmup_detail": "This profile needs {needed} labelled cycles before auto-matching begins. Every confirmed cycle helps it learn.", - "wipe_history_warning": "Permanently delete all cycles and profiles. Cannot be undone." + "warmup_detail": "Matching already works. WashData asks you to confirm the first {needed} cycles of this profile before labelling on its own.", + "wipe_history_warning": "Permanently delete all cycles and profiles. Cannot be undone.", + "compare_cycles_title": "Compare {count} cycles", + "edit_builtin_phase": "This is a built-in phase. Saving creates a custom copy; delete the copy to restore the original.", + "fetch_error": "Failed to load data.", + "connection_lost": "Connection lost - showing data from {time}.", + "pg_notify_partial": "Markers show start, almost-done, finish, milestone and quiet-hours notifications only. Live updates, unload reminders and overrun alerts are not simulated.", + "connect_to_confirm": "Connect in the settings gear to confirm or rate.", + "coverage_gap_cluster": "{count} recent unlabelled cycles of about {min} min look like one program you have not created yet.", + "pg_label_yes": "Would be labelled \"{name}\" at cycle end", + "pg_label_no": "Would ask for review: {reason}", + "live_match_debug_tip": "Confidence: how closely the current power curve matches the top candidate profile (0-100%). Ambiguous: the two best candidates score within 5% of each other - the label is uncertain until the cycle finishes.", + "automation_alias_started": "{device}: cycle started", + "automation_alias_finished": "{device}: cycle finished", + "automation_desc_started": "Runs when a {device} cycle starts. Add your actions (notify, lights, ...).", + "automation_desc_finished": "Runs when a {device} cycle finishes. Add your actions (notify, lights, ...).", + "feedback_detected": "WashData detected: {name}", + "pg_outcome_stale": "Settings changed since this simulation ran. It still shows the old result.", + "pg_compare_profile_tip": "Only picks the profile drawn over the graph. The simulation always matches the cycle on its own, as the live integration does.", + "pg_task_busy": "A Test-on-history or Optimize run is already in progress for this device. Wait for it or cancel it.", + "pg_sweep_guarded": "Ends more cycles early, splits more of them, or misses cycles the current setting catches, so it is never recommended.", + "pg_sweep_n_cycles": "over {count} cycles", + "pg_sweep_n_cycles_one": "over {count} cycle", + "pg_sweep_n_cycles_other": "over {count} cycles", + "pg_sweep_keep_current": "Keep the current value ({value}, {metric}): no value did better by at least one cycle.", + "hist_truncated": "stopped at the {n}-reading limit, so the rest of the history was not scanned", + "hist_warn_kw": "The highest reading is only {w}. This sensor seems to report kilowatts; WashData needs watts, so no cycles can be detected.", + "hist_warn_naive_time": "{n} timestamps have no time zone and were read as UTC. If the file uses local time, cycles are shifted by your UTC offset and readings near a DST change may be dropped.", + "hist_warn_naive_time_one": "{n} timestamp has no time zone and was read as UTC. If the file uses local time, cycles are shifted by your UTC offset and readings near a DST change may be dropped.", + "hist_warn_naive_time_other": "{n} timestamps have no time zone and were read as UTC. If the file uses local time, cycles are shifted by your UTC offset and readings near a DST change may be dropped.", + "hist_tail_trimmed": "{min} min of waiting after the cycle ended were trimmed, as the live integration trims them", + "invalid_threshold_pair": "Stop Threshold ({stop} W) must be below Start Threshold ({start} W). Nothing was saved; change both together." }, "pg_desc": { "completion_min_seconds": "Shortest run that counts as a real cycle", - "end_repeat_count": "Low readings in a row before ending", "interrupted_min_seconds": "Short cycles flagged as interrupted", "min_off_gap": "Gap required to separate two cycles", "off_delay": "Seconds of low power before cycle ends", "start_duration_threshold": "Seconds above threshold to confirm start", "start_threshold_w": "Minimum watts to count as started", "stop_threshold_w": "Below this, machine counts as off", - "dtw_bandwidth": "Stage 3: Sakoe-Chiba warp band (0 = DTW off; default 0.2 = 20% of cycle length)", - "corr_weight": "Stage 2: balance between curve shape (correlation) and power level (MAE); default 0.45", - "duration_weight": "Stage 4: how strongly run-length agreement affects the final score (default 0.22)", - "energy_weight": "Stage 4: how strongly energy agreement affects the final score (default 0.22)", - "profile_match_min_duration_ratio": "Stage 1: minimum cycle length as a fraction of profile duration; default 0.1 (10%)", - "profile_match_max_duration_ratio": "Stage 1: maximum cycle length as a fraction of profile duration; default 1.8 (180%)", - "keep_min_score": "Stage 2: floor score to stay in the race; default 0.1 admits weak matches, later stages pick the best", - "dtw_blend": "Stage 3: 0 = core score only, 1 = DTW score only, 0.5 = equal blend (default)", - "dtw_ensemble_w": "Stage 3 (ensemble mode): weight on scaled-L1 vs derivative DTW; default 0.7 favours level-aware", - "dtw_ddtw_scale": "Stage 3: DDTW half-saturation distance; smaller = more shape-sensitive (default 30)", - "dtw_refine_top_n": "Stage 3: candidates DTW re-scores; raise to 7-9 if correct profile ranks 4th-5th (default 5)", - "duration_scale": "Stage 4: log-ratio where duration agreement halves; smaller = stricter penalty (default 0.175)", - "energy_scale": "Stage 4: log-ratio where energy agreement halves; smaller = stricter penalty (default 0.25)", + "profile_match_min_duration_ratio": "Minimum cycle length as a fraction of the profile's duration; default 0.1 (10%)", + "profile_match_max_duration_ratio": "Maximum cycle length as a fraction of the profile's duration; default 1.8 (180%)", "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", + "smart_termination_duration_ratio": "Fraction of the program's average duration a cycle must reach before Smart Termination may end it early; lower it if run times vary", + "anti_crease_finalize_ratio": "Fraction of the program's average duration a cycle must reach before the anti-crease tail may finish it; lower it on dryers, keep washers 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": "Quiet level between tumble pulses; only takes effect above the Stop Threshold", "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.", "anti_wrinkle": "Periodic post-cycle tumbling to reduce wrinkles.", + "baking": "High-temperature heating element active for crust and crumb formation.", + "cooking": "Main cooking phase with active heater and fan.", "cool_down": "Tumbling without heat near cycle end.", + "cool_down_af": "Fan-only cool-down stage after heating.", "drain_and_switch": "Transition period from washing to drying mode.", "dry": "Drying stage using heater and/or residual heat.", "drying": "Main heated tumbling period.", "heat_up": "Initial heater warm-up before full drying begins.", + "keep_warm": "Low-heat holding stage to keep food warm.", + "keep_warm_bm": "Low-heat holding stage to keep the loaf warm after baking.", + "kneading": "Motor-driven dough mixing and development. High power draw.", + "pause_af": "Short pause for shaking or inspection.", + "pre_heat": "Initial chamber heating before full cooking.", "pre_rinse": "Initial spray-down before detergent wash.", + "proving": "Low-heat rising period to allow yeast fermentation and dough expansion.", + "resting": "Short low-power pause between kneading stages for gluten relaxation.", "pre_wash": "Initial soak or pre-treatment before the main wash.", "rinse": "Clean-water rinse stage. This phase may repeat multiple times.", "sanitize": "High-temperature cleaning stage for sanitization programs.", @@ -1165,6 +1133,7 @@ "wash_dw": "Main detergent wash with heating." }, "placeholder": { + "custom_task": "e.g. Clean door seal", "add": "add…", "group_name": "e.g. Cotton 2:47", "json_buttons": "[{\"action\":\"ID\",\"title\":\"Label\"}]", @@ -1191,7 +1160,7 @@ }, "section": { "anti_wrinkle": { - "intro": "Anti-wrinkle / anti-crease mode detects low-power tumble pulses after the main phase and keeps them attached to the finished cycle.", + "intro": "Keeps a dryer's low-power tumble pulses after the main phase attached to the finished cycle.", "label": "Anti-Wrinkle" }, "basic": { @@ -1199,7 +1168,7 @@ "label": "Basic" }, "delay": { - "intro": "Delayed-start detection identifies when an appliance is powered but has not yet begun its cycle.", + "intro": "Detects an appliance that is on and waiting for a delayed start.", "label": "Delayed Start" }, "detection": { @@ -1207,7 +1176,7 @@ "label": "Detection" }, "dishwasher": { - "intro": "End-of-cycle handling for dishwashers, which typically finish with a long near-silent drying phase before a short final drain.", + "intro": "End handling for dishwashers, which often dry quietly for a long time before a short final drain.", "label": "Dishwasher" }, "matching": { @@ -1227,221 +1196,165 @@ "label": "Timing & Watchdog" }, "triggers": { - "intro": "Optional external signals: an end trigger, a door sensor, a pause switch, and the unload reminder.", + "intro": "Optional extras: end trigger, door sensor, pause switch and unload reminder.", "label": "Triggers & Doors" - }, - "phase_eta": { - "intro": "Phase-aware time-remaining, for machines whose cycle length depends on temperature or spin.", - "label": "Time Remaining" } }, "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.", + "doc": "Fraction of the program's average duration a cycle must reach before its low-power tumbling counts as the anti-crease tail. Lower it (e.g. 0.75) on load-sensing dryers. Keep washers at the default (0.98), or a wash can end halfway.", "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.", + "doc": "Keep a dryer's short low-power tumble pulses after the main heat phase with the finished cycle, instead of reading them as new cycles.", "label": "Enable Anti-Wrinkle Detection" }, "anti_wrinkle_exit_power": { - "doc": "Power must fall below this between pulses for anti-wrinkle mode to stay active.", + "doc": "In anti-wrinkle mode, power below this counts as quiet between tumble pulses, and quiet for longer than the Max Pulse Gap ends the mode. The Stop Threshold is used when it is higher, so this only changes anything when set above the Stop Threshold.", "label": "Exit Power Threshold" }, "anti_wrinkle_idle_timeout": { "label": "Max Pulse Gap", - "doc": "How long the machine may stay quiet between two tumble pulses before anti-wrinkle mode ends. Set it above the longest gap your dryer leaves between pulses, otherwise every later pulse is read as a false start." + "doc": "How long the dryer may stay quiet between tumble pulses before anti-wrinkle mode ends. Set it above your dryer's longest gap, or later pulses are read as false starts." }, "anti_wrinkle_max_duration": { - "doc": "Pulses longer than this are treated as a real cycle rather than an anti-wrinkle tumble.", + "doc": "A pulse longer than this counts as a real cycle, not an anti-wrinkle tumble.", "label": "Max Duration" }, "anti_wrinkle_max_power": { - "doc": "A pulse above this power is treated as a real new cycle, not an anti-wrinkle tumble. Set just above the tumble-pulse power.", + "doc": "A pulse above this power counts as a real new cycle, not a tumble. Set it just above your tumble-pulse power.", "label": "Max Anti-Wrinkle Power" }, "auto_label_confidence": { - "doc": "If the match score at cycle end is at or above this, the program is labeled automatically without any confirmation prompt. Raise it to require higher certainty before auto-labeling; lower it to automate more. Works in conjunction with Learning Confidence below it.", + "doc": "Match score at cycle end at or above which the program is labelled without asking. Raise it for more certainty; lower it to label more cycles automatically. Keep it above Learning Confidence.", "label": "Auto-Label Confidence" }, "auto_maintenance": { - "doc": "Run nightly housekeeping: rebuild profile envelopes, recompute cycle health, prune debug traces and retain the most recent cycles.", + "doc": "Nightly cleanup: label old cycles that now clearly match a program, rebuild program curves, refresh anomaly marks and cycle health, and remove debug traces if Save Debug Traces is off. Cycles and their power traces are always kept.", "label": "Auto Maintenance (nightly cleanup)" }, "completion_min_seconds": { - "doc": "Cycles shorter than this are discarded as ghost cycles (test runs, opening the door to add a sock).", + "doc": "Cycles shorter than this (test runs, opening the door to add a sock) are saved as Interrupted and get no finish notification.", "label": "Min Cycle Duration" }, - "corr_weight": { - "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.", + "doc": "Adds readings from failed start attempts to the front of a cycle, for machines that probe before starting (door lock, first fill). 300 s covers most; 0 = off. Recorded starts move earlier, so learned durations drift for a while.", "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.", + "doc": "Power must stay in the standby band this long before the appliance counts as waiting to start.", "label": "Confirm Seconds" }, "delay_start_detect_enabled": { - "doc": "Detect when the appliance is powered on and waiting (delayed start / standby) but has not begun its cycle, so standby draw is not mistaken for a running cycle.", + "doc": "Detect an appliance that is on and waiting for a delayed start, so its standby draw is not taken for a running cycle.", "label": "Enable Delay-Start Detection" }, "delay_timeout_hours": { - "doc": "Stop waiting in delayed-start mode after this many hours and return to idle, so a machine left powered but never started does not wait forever.", + "doc": "Give up waiting for a delayed start after this many hours and return to idle.", "label": "Timeout Hours" }, "device_type": { - "doc": "Appliance class. Sets sensible detection defaults (thresholds, off-delay, end handling) tuned for that appliance type; change it only if the device was originally set up as the wrong type.", + "doc": "Sets detection defaults (thresholds, off delay, end handling) for this kind of appliance. Change it only if the device was set up as the wrong type.", "label": "Device Type" }, "dishwasher_end_spike_quiet_release": { - "doc": "Once the cycle passes its expected duration, how long the dishwasher must stay quiet (below the Stop Threshold) before WashData stops waiting for a final drain and ends the cycle. Raise it if your machine has a long silent drying phase before a late final drain that is being missed - a wider window lets the learned duration follow seasonal drift (colder inlet water = longer cycles) instead of locking to the old average. It only ever shortens the wait relative to the internal 30-minute end-spike cap, never extends it.", + "doc": "After the expected duration, how long the dishwasher must stay below the Stop Threshold before WashData stops waiting for the final drain. Raise it if a late drain after a long dry is missed. The wait never exceeds 30 minutes.", "label": "Passive-Dry Quiet Release" }, "door_end_dwell_seconds": { "label": "Door-Open End Dwell", - "doc": "How long the door must stay open before WashData ends the cycle, when \"Door Opens Automatically At End\" is on. Long enough to ignore quickly adding a dish (default 60 s), short enough to end promptly once the machine pops the door." + "doc": "With \"Door Opens Automatically At End\" on: how long the door must stay open before the cycle ends. Long enough to ignore adding a dish (default 60 s)." }, "door_opens_at_end": { "label": "Door Opens Automatically At End", - "doc": "For dishwashers that pop the door open at the end of the cycle to dry (AirDry and similar). With this on, a door-open on a running cycle no longer pauses it forever; instead, if the door stays open for the dwell below, WashData treats the cycle as finished. A brief open (adding an item) is ignored. Requires a Door Sensor Entity." + "doc": "For dishwashers that open the door at the end to dry (e.g. AirDry). A door left open for the dwell time below ends the cycle instead of pausing it; a brief open is ignored. Needs a Door Sensor Entity." }, "door_sensor_entity": { - "doc": "Optional door binary sensor. Used to detect when the appliance has been opened/unloaded after a cycle.", + "doc": "Optional door sensor. Shows when the appliance was opened to unload after a cycle.", "label": "Door Sensor Entity" }, - "dtw_bandwidth": { - "label": "Shape Match Tolerance", - "doc": "How much the matcher may time-warp a cycle to fit a saved program's shape. Higher tolerates more speed variation; too high risks matching the wrong program." - }, - "dtw_blend": { - "label": "Warp Blend", - "doc": "How much the time-warp (DTW) alignment score replaces the Stage 2 core score. 0 = use only the Stage 2 score, 1 = use only the DTW score, 0.5 (default) = equal blend. Raise it to rely more on warp alignment when programs have similar power levels but different timing patterns." - }, - "dtw_ddtw_scale": { - "label": "Derivative Warp Scale", - "doc": "Half-saturation distance for derivative DTW scoring. The score halves when the DDTW distance equals this value. Smaller = more sensitive to shape differences. Default 30 is calibrated to typical appliance power traces. Only affects ensemble and ddtw DTW modes." - }, - "dtw_ensemble_w": { - "label": "Warp Ensemble Mix", - "doc": "In ensemble DTW mode (the default), the weight on scaled L1 versus derivative DTW (DDTW). 1.0 = all scaled-L1, 0 = all DDTW, 0.7 = default. The scaled-L1 variant compares power levels; the derivative variant reacts to how power changes over time. Ensemble blends both." - }, - "dtw_refine_top_n": { - "label": "Warp Refine Count", - "doc": "How many top Stage 2 candidates are re-scored by DTW. DTW is more expensive so it is only applied to the best N candidates. Default 5. Raise it (to 7-9) if the correct profile sometimes only reaches 4th or 5th place after Stage 2 - DTW can rescue it. Lowering speeds up matching slightly." - }, - "duration_scale": { - "label": "Duration Sharpness", - "doc": "Sharpness of the Stage 4 duration agreement penalty. This is the log-ratio at which the agreement score halves. Smaller = stricter: a duration mismatch hurts more. Default 0.175 corresponds to roughly 18% duration tolerance at half-weight. Pair with Duration Weight." - }, "duration_tolerance": { - "doc": "Tolerance for time-remaining estimates (learning feedback, not matching). If the actual duration is within +/-X% of the estimate it counts as a good match.", + "doc": "Learning feedback only: a cycle within +/-X% of its estimated duration counts as a good estimate. Does not affect matching.", "label": "Estimate Tolerance" }, - "duration_weight": { - "label": "Duration Weight", - "doc": "How strongly a program whose typical run-length matches this cycle is preferred." - }, "enable_ml_models": { - "doc": "While a cycle runs, let the models refine the live results: a steadier time-remaining and energy/cost estimate, and an anti-premature-stop guard on end detection (it can only ever delay a finish, never end one early, and is bounded). Uses your fine-tuned models when available, otherwise the built-in ones. Off = the classic power-based logic only (still reliable).", + "doc": "Use a model trained on this machine to predict a running cycle's energy and cost, and let a well-known program that stops unusually early finish without the full off delay. Off: power-based logic only.", "label": "Apply smart models during a cycle" }, - "enable_phase_matching": { - "doc": "Break each running cycle into phases (heating, wash, spin) and budget the time remaining per phase, blended with the classic estimate - leaning on the phase budget early in the cycle and the classic estimate near the end. This personalises the countdown to how long your machine actually heats and runs, which is most noticeable in the first half of a cycle. Off = the classic estimate only. Only the time-remaining display is affected; program matching and cycle detection are unchanged.", - "label": "Phase-aware time remaining" - }, "end_energy_threshold": { - "doc": "During the off-delay countdown, accumulated energy (watts x time) is compared to this threshold. If exceeded, the countdown resets - keeping anti-crease tumbles and dishwasher drying tails attached to the cycle instead of cutting them short. Raise it if cycles end too early during cool-down; lower it if detection is sluggish.", + "doc": "Energy allowed during the off delay. More than this and the cycle keeps going, so tumbles and drying tails stay attached. Lower it if cycles end during a tail; raise it if finished cycles keep waiting.", "label": "End Energy" }, - "end_repeat_count": { - "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.", + "doc": "Charge each cycle at the price at each moment it ran, instead of the price when it finished. Needs a price entity. Process History (Diagnostics) recosts older cycles.", "label": "Time-Weighted Cost" }, "energy_price_entity": { - "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.", + "doc": "Sensor with the current price per kWh (e.g. a dynamic tariff). It overrides the static price below, so it must be a price, not the plug's power or energy sensor.", "label": "Energy Price Entity" }, "energy_price_static": { - "doc": "Fixed price per kWh used for cost figures when no live price entity is set above.", + "doc": "Fixed price per kWh, used when no price entity is set.", "label": "Static Energy Price (per kWh)" }, - "energy_scale": { - "label": "Energy Sharpness", - "doc": "Sharpness of the Stage 4 energy agreement penalty. Smaller = stricter: an energy mismatch hurts more. Default 0.25 is intentionally more forgiving than Duration Sharpness because energy varies with load. Pair with Energy Weight." - }, "energy_sensor": { - "doc": "Optional cumulative energy counter (total_increasing kWh/Wh, e.g. the plug's own lifetime meter). When set, each cycle's reported energy is taken from this counter's start-to-end delta, which avoids the under-counting you get from integrating a slow-reporting power sensor. Falls back to the integrated value if the reading is missing, its unit is unknown, or the delta is not positive. Leave blank to keep integrating the power sensor.", + "doc": "Optional kWh or Wh counter (total_increasing, e.g. the plug's own meter). Cycle energy is then read from it, which is more accurate than computing it from a slow power sensor. If its reading is unusable, the computed value is used.", "label": "Energy Meter Entity" }, - "energy_weight": { - "label": "Energy Weight", - "doc": "How strongly a program whose typical energy use matches this cycle is preferred." - }, "expose_debug_entities": { - "doc": "Publish extra diagnostic HA entities (match confidence, ambiguity, state internals). Off keeps the entity list clean for normal use.", + "doc": "Add diagnostic entities (match confidence, ambiguity, internal state). Off keeps the entity list clean.", "label": "Expose Debug Entities" }, "external_end_trigger": { - "doc": "Binary sensor whose state change marks the cycle end (e.g. an appliance \"finished\" contact or a companion integration).", + "doc": "Binary sensor whose change marks the cycle end (e.g. a \"finished\" contact or another integration).", "label": "External Trigger Entity" }, "external_end_trigger_enabled": { - "doc": "Let an external binary sensor signal the end of a cycle, in addition to the built-in power-based detection.", + "doc": "Let a binary sensor signal the end of a cycle, in addition to power-based detection.", "label": "Enable External End Trigger" }, "external_end_trigger_inverted": { - "doc": "Treat the trigger sensor turning OFF (rather than ON) as the end-of-cycle signal.", + "doc": "Treat the sensor turning OFF (not ON) as the end signal.", "label": "Invert External Trigger (trigger on OFF)" }, - "keep_min_score": { - "label": "Min Match Score", - "doc": "Minimum similarity score a candidate needs to stay in the match race. The default 0.1 is deliberately permissive - it admits even weak candidates and relies on later stages to find the best match. Raise it to prune unlikely profiles earlier; lower it to widen the initial candidate pool." - }, "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. Must be kept below Auto-Label Confidence.", + "doc": "Lowest match score for labelling a finished cycle. A clear winner is labelled; a close call goes to the review queue in Cycles. Below this, nothing is labelled or asked. Keep it below Auto-Label Confidence.", "label": "Learning Confidence" }, "linked_device": { - "doc": "Optionally nest this WashData device under another device (e.g. the smart plug) in the HA device registry, shown as \"Connected via ...\".", + "doc": "Optional. Show this WashData device under another device (e.g. the smart plug) in Home Assistant, as \"Connected via ...\".", "label": "Group Under Device" }, "min_off_gap": { - "doc": "If the machine powers off for less than this time, the on/off/on sequence is treated as one continuous cycle. Prevents soak programs (machine powers off for several minutes mid-wash) from being split into two separate cycles. Set it shorter than the gap between your back-to-back loads if you want those counted as separate cycles. Device-type defaults protect the typical intra-cycle pause for each appliance.", + "doc": "An off period shorter than this does not split a cycle, so soak pauses stay in one wash. Keep it shorter than the gap between back-to-back loads, or those merge into one cycle.", "label": "Min Off Gap" }, "min_power": { - "doc": "Absolute minimum power considered active. Readings below this are treated as 0 W (standby), filtering out the phantom load of smart plugs and standby LEDs.", + "doc": "Readings below this count as 0 W (standby). Filters out the idle draw of smart plugs and standby LEDs.", "label": "Minimum Power" }, "ml_training_enabled": { - "doc": "Periodically study your reviewed cycles overnight and fine-tune the models to this specific machine. A change is only kept when it genuinely scores better on held-out cycles, so this can only help or stay the same - never regress.", + "doc": "Retrain the energy model on this machine's finished cycles from time to time. A new model is kept only if it beats the simple estimate on test cycles.", "label": "Learn from this machine" }, "ml_training_hour": { - "doc": "Local hour of day (0-23) to do the overnight fine-tuning. Pick a quiet hour such as 2 (02:00).", + "doc": "Hour of day (0-23) for overnight training. Pick a quiet hour, e.g. 2.", "label": "Learn at hour" }, "ml_training_interval_days": { - "doc": "Re-check for improvements at most once per this many days.", + "doc": "Retrain at most once every this many days.", "label": "Check at most every" }, "ml_training_min_cycles": { - "doc": "Wait until at least this many cycles have been recorded before fine-tuning, so there is enough to learn from.", + "doc": "Wait for at least this many recorded cycles before training.", "label": "Cycles needed first" }, "name": { - "doc": "Display name shown in the HA integrations list and device registry.", + "doc": "Name shown in Home Assistant for this device.", "label": "Device Name" }, "no_update_active_timeout": { - "doc": "If no power updates arrive for this long while running, assume the plug dropped offline and force-stop to avoid a zombie cycle. Default 600 s allows for cloud or mesh lag.", + "doc": "If no power update arrives for this long during a cycle, assume the plug went offline and force-stop the cycle. The default 600 s allows for cloud or mesh delays.", "label": "No-Update Timeout" }, "notify_before_end_minutes": { @@ -1449,247 +1362,242 @@ "label": "Pre-End Alert" }, "notify_channel": { - "doc": "Android notification channel name for start/live messages (controls per-channel sound and priority on the mobile app). Blank uses the companion app default.", + "doc": "Android channel for start and live messages; the channel sets sound and priority. Blank: app default.", "label": "Android Channel (start/live)" }, "notify_cycle_timers": { - "doc": "Notifications at specific minutes into a cycle (e.g. to add softener). Message supports {device}, {program}, {minutes}. Enable Auto-pause to pause at that point and receive an interactive notification with a Resume button; resume via the panel, the pause/resume service, or the notification action.", + "doc": "Notifications at set minutes into a cycle (e.g. to add softener). Message supports {device}, {program}, {minutes}. With Auto-pause, the cycle also pauses there and the notification gets a Resume button.", "label": "Cycle Timers" }, "notify_finish_channel": { - "doc": "Android notification channel name for the finish message. Blank reuses the start/live channel.", + "doc": "Android channel for the finish message. Blank reuses the start/live channel.", "label": "Android Channel (finish)" }, "notify_finish_message": { - "doc": "Body sent when a cycle finishes. Template variables: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}. {time_finished} and {vs_typical} are most useful here.", + "doc": "Body sent when a cycle finishes. Variables: {device}, {program}, {duration} (minutes), {duration_hm} (1 h 05 min), {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}, {status} (completed or force_stopped; interrupted cycles send nothing). Any other variable falls back to the default text.", "label": "Finish Message" }, "notify_finish_services": { - "doc": "notify.* services called when a cycle finishes. Add one per target; leave empty for no finish notification.", + "doc": "notify.* services to call when a cycle finishes. Leave empty for none.", "label": "Finish Services" }, "notify_fire_events": { - "doc": "Also fire ha_washdata_* events on cycle start/finish so you can build your own automations.", + "doc": "Also fire ha_washdata_* events on cycle start and finish for your own automations.", "label": "Fire HA Events for Notifications" }, "notify_icon": { - "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.", + "doc": "Small icon on the notification (e.g. mdi:washing-machine). Sent only to mobile_app targets, not notify groups. On iOS it replaces the app icon and needs companion app 2026.8 or newer. Blank: platform default.", "label": "Notification Icon" }, "notify_icon_color": { - "doc": "Accent colour for this appliance's notifications, so a washer, a dryer and a dishwasher are tellable apart at a glance. iOS tints the icon and the Live Activity progress bar on the Lock Screen and in the Dynamic Island. Android is sent the same colour, but what it does with it is up to the phone: some tint the icon, and many (Samsung One UI especially) ignore it entirely. Hex (e.g. #03A9F4) is understood by both. Leave blank for the platform default.", + "doc": "Accent colour for this appliance's notifications, so washer, dryer and dishwasher are easy to tell apart. On iOS it fills the circle behind the icon and colours the Live Activity bar. Many Android phones (Samsung especially) ignore it. Use hex, e.g. #03A9F4. Blank: platform default.", "label": "Notification Colour" }, "notify_live_chronometer": { - "doc": "Show a live-updating countdown timer in the notification (on platforms that support it) instead of a static estimate.", + "doc": "Show a live countdown timer in the notification (where supported) instead of a fixed estimate.", "label": "Use Live Chronometer" }, "notify_live_click_action": { "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." + "doc": "What a tap on this appliance's notification opens. Blank: the WashData panel on this appliance. Enter a dashboard path (e.g. /lovelace/laundry) or URL, or \"none\" for no tap action." }, "notify_live_interval_seconds": { "doc": "How often live-progress notifications are refreshed while a cycle runs.", "label": "Live Update Interval" }, "notify_live_overrun_percent": { - "doc": "If a cycle runs past its estimate by more than this percentage, send an overrun alert.", + "doc": "Live updates stop once a cycle runs this percent past its expected duration (20 = up to 1.2x the expected time).", "label": "Live Overrun % Before Alert" }, "notify_live_services": { - "doc": "notify.* services called for live progress updates while a cycle runs. Leave empty to disable live-progress notifications.", + "doc": "notify.* services for live progress updates while a cycle runs. Leave empty to turn them off.", "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." + "doc": "iOS only. Update live progress silently, at a battery-saving priority that iOS may batch. The first update and the start/finish notifications still alert. Off: every update alerts." }, "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." + "doc": "Android only. Keep the live progress notification when tapped. Off: a tap dismisses it." }, "notify_milestone_message": { "doc": "Message for the milestone notification. Template variables: {device}, {cycle_count}.", "label": "Milestone Message" }, "notify_milestones": { - "doc": "Comma-separated cycle counts that trigger a one-off celebration notification when reached (e.g. 50, 100, 500, 1000). Blank disables milestone notifications.", + "doc": "Cycle counts that trigger a one-off celebration notification, comma-separated (e.g. 50, 100, 500, 1000). Blank disables them.", "label": "Cycle Milestones" }, "notify_only_when_home": { - "doc": "Only send notifications when at least one of the linked people (above) is home.", + "doc": "Only notify when at least one of the people above is home.", "label": "Notify Only When Home" }, "notify_people": { - "doc": "person.* entities used by \"Notify Only When Home\" to decide whether anyone is home.", + "doc": "person.* entities that \"Notify Only When Home\" checks.", "label": "People (for Only When Home)" }, "notify_pre_complete_message": { - "doc": "Body of the pre-end / almost-done alert. Template variables: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Text of each live progress update. Template variables: {device}, {program}, {minutes} (minutes left). Any other variable falls back to the default text.", "label": "Pre-Complete Message" }, "notify_quiet_end_hour": { - "doc": "End of the do-not-disturb window (0-23). Held notifications are delivered at this hour. Leave blank to disable.", + "doc": "End of the do-not-disturb window (0-23); held notifications are sent then. Blank disables it.", "label": "Quiet Hours End" }, "notify_quiet_start_hour": { - "doc": "Start of a do-not-disturb window (0-23). Finish, reminder and clean-laundry notifications that would fire during quiet hours are held and delivered when the window ends. Leave blank to disable. Supports windows that cross midnight (e.g. start 22, end 7).", + "doc": "Start of a do-not-disturb window (0-23). Finish, reminder and clean-laundry notifications in this window are held until it ends. Can cross midnight (e.g. 22 to 7). Blank disables it.", "label": "Quiet Hours Start" }, "notify_reminder_message": { - "doc": "Body of the still-waiting unload reminder. Blank uses the built-in default. Template variables: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Text of the one-time Pre-End Alert. Template variables: {device}, {program}, {minutes} (the Pre-End Alert setting). Any other variable falls back to the default text.", "label": "Reminder Message" }, "notify_start_message": { - "doc": "Body sent when a cycle starts. Template variables: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Body sent when a cycle starts. Template variables: {device}, {program}. Any other variable falls back to the default text.", "label": "Start Message" }, "notify_start_services": { - "doc": "notify.* services called when a cycle starts. Add one per target (phone, dashboard, etc.); leave empty for no start notification.", + "doc": "notify.* services to call when a cycle starts (phone, dashboard, ...). Leave empty for none.", "label": "Start Services" }, "notify_timeout_seconds": { - "doc": "Automatically dismiss the notification after this many seconds (on platforms that support it). 0 keeps it until dismissed manually.", + "doc": "Dismiss the notification after this many seconds (where supported). 0 keeps it until you dismiss it.", "label": "Auto-Dismiss After" }, "notify_title": { - "doc": "Notification title. Template variables: {device}, {program}, {duration}, {energy}, {cost}, {date}, {time}, {minutes}.", + "doc": "Notification title. Only {device} is substituted; any other variable falls back to the default title.", "label": "Notification Title" }, "notify_unload_delay_minutes": { - "doc": "Minutes after a cycle ends before sending the still-waiting \"unload the machine\" reminder. Set 0 to disable the reminder.", + "doc": "Minutes after a cycle ends before the \"unload the machine\" reminder. 0 disables it.", "label": "Unload Nag Delay" }, "notify_unload_repeat": { - "doc": "Keep re-sending the unload reminder every \"Unload Nag Delay\" minutes until the unload is confirmed or you tap \"Stop reminding\" on the notification. Needs a way to confirm the unload: a Door Sensor Entity, an Unload Confirmation Entity, or the \"Confirm Unload Manually\" option below. The dismiss button works on Home Assistant companion-app (mobile) notifications.", + "doc": "Resend the unload reminder every \"Unload Nag Delay\" minutes until the unload is confirmed or you tap \"Stop reminding\" (mobile app). Needs a Door Sensor Entity, an Unload Confirmation Entity, or \"Confirm Unload Manually\".", "label": "Repeat Until Unloaded" }, "off_delay": { - "doc": "Time to wait after power drops before declaring the cycle finished. If power resumes within this window the cycle continues seamlessly - this bridges pauses between wash stages. Dishwashers have long drying phases (power off for 20-60 min) so the off-delay must exceed that to keep the whole wash+dry as one cycle.", + "doc": "How long power must stay low before the cycle is finished. Power coming back within this time continues the cycle. Dishwashers can be idle 20-60 min while drying, so set it longer than that.", "label": "Off Delay" }, "pause_cuts_power": { - "doc": "When a cycle is paused, also switch off the Switch Entity below. Only for appliances whose plug can safely be cut mid-cycle.", + "doc": "When a cycle is paused, also switch off the Switch Entity below. Only for appliances that can safely lose power mid-cycle.", "label": "Pause Also Cuts Power (via switch)" }, "peak_rate_message": { - "doc": "Optional custom text for the peak-rate tip appended to the start notification. Template variables: {device}, {price}. Blank uses the built-in default.", + "doc": "Custom text for the peak-rate tip. Template variables: {device}, {price}. Blank uses the default.", "label": "Peak-Rate Message" }, "peak_rate_threshold": { - "doc": "When a cycle starts and the current price per kWh is at or above this value, append a peak-rate tip to the start notification. 0 or blank disables the tip.", + "doc": "If the price per kWh is at or above this when a cycle starts, add a peak-rate tip to the start notification. 0 or blank disables it.", "label": "Peak-Rate Threshold (per kWh)" }, "power_off_delay": { - "doc": "How long power must stay below the Power Off Threshold after a cycle finishes before the state returns to Off. Only used when the Power Off Threshold is above 0. Checked on the background cadence, so the effective delay rounds up to the next state-expiry tick.", + "doc": "How long power must stay below the Power Off Threshold after a cycle before the state returns to Off. Only used when that threshold is above 0. Checked periodically, so the real wait can be slightly longer.", "label": "Power Off Delay" }, "power_off_threshold_w": { - "doc": "Optional power-based Off detection. When above 0, once a cycle has finished and power stays below this level for the Power Off Delay, the machine is treated as switched off and the state returns to Off. Leave at 0 to disable (the default). Set it above the true switched-off floor and below the Stop Threshold and your machine's finished-but-on standby draw; if it is not below the Stop Threshold it is ignored. When enabled it replaces the Progress Reset Delay for returning to Off, so a finished machine stays in Finished/Clean until it is actually powered off.", + "doc": "After a cycle, power below this for the Power Off Delay sets the state to Off, instead of the Progress Reset Delay doing it. Set it above the switched-off draw, below standby and the Stop Threshold. 0 disables it (default).", "label": "Power Off Threshold" }, "power_profile_interval_min": { "label": "Power Profile Interval", - "doc": "Bucket size for the per-profile power_profile sensor attribute (the flat per-slot average-watts array consumed by external planners such as EMHASS and tibber_prices). Smaller buckets keep short power spikes sharp; larger buckets smooth the shape. Default 15 min. Read-time only; does not affect detection." + "doc": "Bucket size of the power_profile attribute (average watts per slot) used by planners such as EMHASS and tibber_prices. Smaller keeps short spikes; larger smooths the curve. Default 15 min. Does not affect detection." }, "power_sensor": { - "doc": "The sensor entity reporting live power in watts for this appliance (e.g. sensor.washer_power). All cycle detection is based on this signal.", + "doc": "The sensor that reports this appliance's live power in watts (e.g. sensor.washer_power). All detection is based on it.", "label": "Power Sensor" }, - "profile_duration_tolerance": { - "doc": "The +/- band around a profile average duration used during matching. 0.25 means a 60 min profile matches 45-75 min cycles.", - "label": "Profile Duration Tolerance" - }, "profile_evidence_sources": { "label": "Cycles that shape a program", - "doc": "Which cycles are used to build each program's power curve, and to match a finished cycle against it. Unticking a kind stops it shaping your programs without deleting anything - the cycles stay in your Cycles list and can still be labelled or removed. Useful if you do not trust imported data. Statistics are unaffected: they always count only the cycles this machine actually ran. Unticking everything is ignored, since a program with no cycles behind it could never match." + "doc": "Which kinds of cycles shape each program's curve and matching. Unticking a kind deletes nothing; useful if you do not trust imported data. Statistics always count only this machine's cycles. Unticking all is ignored." }, "profile_match_interval": { - "doc": "How often to attempt profile matching during a running cycle. Default 300 s (5 minutes) balances detection speed and CPU.", + "doc": "How often to match a running cycle against profiles. Default 300 s (5 min).", "label": "Match Interval" }, "profile_match_max_duration_ratio": { - "doc": "Maximum cycle length relative to the profile. 1.3 means a cycle must be under 130% of the profile duration to match.", + "doc": "A program is dropped once the cycle has run longer than this multiple of its typical duration. 1.8: a cycle 80% longer than the program no longer matches it.", "label": "Max Duration Ratio" }, "profile_match_min_duration_ratio": { - "doc": "Minimum cycle length relative to the profile. 0.9 means a cycle must be at least 90% of the profile duration to match.", + "doc": "A program is only considered after the cycle has run this fraction of its typical duration. 0.10: a 2-hour program becomes a candidate after 12 minutes. Not applied in a cycle's first 15 minutes.", "label": "Min Duration Ratio" }, "profile_match_threshold": { - "doc": "Minimum similarity score (0-1) required at cycle end to accept a program identification. Raise it to reduce wrong identifications; lower it if your machine's programs are not being matched. Default 0.4 is a conservative starting point.", + "doc": "Live match score a cycle needs before Smart Termination or the anti-crease finish may end it early. It does not change the shown program. Raise it if weak matches end cycles early; lower it if clear matches wait the off delay.", "label": "Match Threshold" }, "profile_unmatch_threshold": { - "doc": "If a live mid-cycle match drops below this score, the tentative identification is cleared. Keep it a little below the Match Threshold so a brief dip in similarity does not flip the display back to unmatched.", + "doc": "If the live match score drops below this, the current program guess is cleared. Keep it a little below the Match Threshold so a brief dip does not reset it.", "label": "Unmatch Threshold" }, "progress_reset_delay": { - "doc": "After finishing, hold progress at 100% for this long so Completed is visible on dashboards before resetting to Idle.", + "doc": "After a cycle, keep progress at 100% this long before resetting to Idle.", "label": "Progress Reset Delay" }, "pump_stuck_duration": { - "doc": "Seconds a pump may run continuously before it is flagged as possibly stuck (fires the stuck-pump event).", + "doc": "How long a pump may run nonstop before the stuck-pump event fires.", "label": "Pump Stuck Duration" }, "sampling_interval": { - "doc": "Expected time between sensor readings - used to size the smoothing window and start debounce correctly. Every sensor update is captured regardless of this value; it only calibrates the downstream calculations. The suggestion engine measures your sensor's actual cadence from past cycles and sets this automatically.", + "doc": "Minimum time between processed readings. Readings that arrive sooner are skipped (low readings during a cycle are always kept). Set it to your plug's reporting interval or lower; a higher value only throws readings away.", "label": "Sampling Interval" }, "save_debug_traces": { - "doc": "Store the full power trace and matching debug data for each cycle. Useful for troubleshooting but increases storage size.", + "doc": "Also store the matcher's debug data (every candidate's scores) with each cycle, for troubleshooting. Adds about 20 KB per cycle.", "label": "Save Debug Traces" }, "show_contributor": { "doc": "Show the \"by \" attribution on community appliances and reference cycles." }, "smart_termination_duration_ratio": { - "doc": "How far into the matched program's expected duration a cycle must be before Smart Termination may end it early once power drops. The expected duration is the program's average, so on appliances whose runtime varies a lot - washers on cold winter vs warm summer inlet water, sensor-dry dryers, load-dependent programs - about half of all runs finish shorter than that average and never get the fast finish, ending only via the fallback timeout minutes late. Lower this (e.g. 0.85) on those machines so the early finish still fires; raise it toward 1.0 to be more conservative. Leave empty for the default (0.98, or 0.99 for dishwashers). It can only ever end a cycle earlier, never later, and never fires on an ambiguous or low-confidence match.", + "doc": "Fraction of the program's average duration a cycle must reach before Smart Termination may end it early. If run times vary (water temperature, sensor-dry, load size), lower it (e.g. 0.85). Leave empty for 0.98 (0.99 on dishwashers).", "label": "Smart Termination Ratio" }, - "smoothing_window": { - "doc": "How much the raw power signal is smoothed. Low (2) is responsive but noisy; high (5) smooths spikes but adds lag.", - "label": "Smoothing Window" - }, "start_duration_threshold": { - "doc": "Power must stay above the start threshold this long to confirm a real start, preventing split-second on/off toggles from starting a cycle.", + "doc": "Power must stay above the Start Threshold this long before a cycle starts, so brief on/off blips are ignored.", "label": "Start Duration" }, "start_energy_threshold": { - "doc": "Energy (power x time) the appliance must consume before RUNNING. A brief high-power spike has very low energy and is ignored, preventing false starts.", + "doc": "Energy the appliance must use before the cycle counts as running. A short power spike uses little energy, so it does not start a cycle.", "label": "Start Energy" }, "start_threshold_w": { - "doc": "Power must rise above this level to confirm a cycle has started. Setting it too low causes false starts from standby power; too high and slow-starting programs (cold fill) are missed. The suggestion engine sets this just above the machine's observed lowest active power.", + "doc": "Power must rise above this to start a cycle. Too low: standby power causes false starts. Too high: slow-starting programs (cold fill) are missed.", "label": "Start Threshold" }, "stop_threshold_w": { - "doc": "Power must fall below this level before the off-delay countdown begins. Set it below the Start Threshold - the gap between them is the hysteresis band that prevents flicker. If set too high, low-power phases (rinse holds, anti-crease) falsely trigger the end sequence.", + "doc": "Power must fall below this before the off delay starts counting. Keep it below the Start Threshold. Too high, and low-power phases (rinse holds, anti-crease) start the end countdown by mistake.", "label": "Stop Threshold" }, "store_brand": { "label": "Appliance Brand", - "doc": "Optional. The appliance brand, picked from the community catalog. Used to find and share matching reference recordings. Leave blank if you are not using online features." + "doc": "Optional. Your appliance brand from the community catalog, used to find and share reference recordings. Leave blank if you do not use online features." }, "store_model": { "label": "Appliance Model", - "doc": "Optional. The appliance model, picked from the community catalog once a brand is set. If your model is not listed you can add it to the catalog." + "doc": "Optional. Your appliance model from the community catalog (pick a brand first). If it is not listed, you can add it." }, "switch_entity": { - "doc": "Optional switch toggled off on pause and back on when resuming, used together with \"Pause also cuts power\".", + "doc": "Optional switch turned off on pause and back on when resuming, with \"Pause Also Cuts Power\".", "label": "Switch Entity" }, "unload_confirm_entity": { - "doc": "Optional entity whose activation means the load has been taken out, for machines that cannot have a door sensor. Any kind works: a Zigbee or NFC button, an input_button helper, a motion sensor in front of the machine, a scene. Every state change counts as a confirmation except switching off and going unavailable. Setting this also enables the unload reminder on a device with no Door Sensor Entity.", + "doc": "Optional. Any entity that confirms the load was taken out, for machines without a door sensor (a button, input_button, motion sensor, scene). Every state change counts except turning off or unavailable. Also enables the unload reminder.", "label": "Unload Confirmation Entity" }, "unload_track_without_door": { - "doc": "Run the unload reminder on a device with no Door Sensor Entity and no Unload Confirmation Entity, and confirm the unload from your own automation instead: press the \"Mark Unloaded\" button entity, or call the ha_washdata.mark_unloaded service. Ignored when a Door Sensor Entity is set, which already provides the signal.", + "doc": "Run the unload reminder without a door sensor or confirmation entity. Confirm the unload from your own automation with the \"Mark Unloaded\" button or the ha_washdata.mark_unloaded service. Ignored when a Door Sensor Entity is set.", "label": "Confirm Unload Manually" }, "watchdog_interval": { - "doc": "How often the background watchdog checks for stalled sensors and elapsed timeouts. Default 30 s.", + "doc": "How often the background check looks for stalled sensors and timeouts. Default 30 s.", "label": "Watchdog Interval" + }, + "share_name": { + "doc": "Show your account name as the uploader of cycles you share. Off: shared without a name." } }, "setting_group": { @@ -1723,9 +1631,6 @@ "door_pause": { "label": "Door & Pause" }, - "dtw_refinement": { - "label": "DTW Refinement" - }, "duration_gates": { "label": "Duration Gates" }, @@ -1792,21 +1697,24 @@ "idle": "Idle", "imported": "Imported", "interrupted": "Interrupted", + "match_sure": "~{pct}% sure", + "match_uncertain": "Uncertain: {top} or {runner}", + "match_uncertain_single": "Uncertain: maybe {top}", + "match_uncertain_tip": "WashData has not settled on a program yet. The figure is how often the leading guess, {top}, was right in past cycles when it led by this much. Information only.", "preparing": "Preparing…", "ready": "Ready to process", "rebuilding": "Rebuilding…", "recording": "Recording", "resetting": "Resetting…", - "reverting": "Reverting…", "saving": "Saving…", "splitting": "Splitting…", + "stalled": "Stalled", "training": "Training…", "trimming": "Trimming…", - "unlabelling": "Unlabelling…" + "unlabelling": "Unlabelling…", + "working": "Working…" }, "suggestion": { - "both_agree": "WashData recommends", - "calibrated_label": "Calibrated", "observed_label": "Observed", "impact": { "auto_label_confidence": { @@ -1821,10 +1729,6 @@ "higher": "Closes only cycles that used substantial energy", "lower": "Closes shorter or lower-energy cycles too" }, - "end_repeat_count": { - "higher": "Requires the end signal to repeat more times – more robust, slightly slower", - "lower": "Ends after fewer repetitions – faster detection, more false-end risk" - }, "learning_confidence": { "higher": "Learns only from very confident matches – slower but more reliable", "lower": "Learns from more cycles; model may be noisier" @@ -1871,32 +1775,17 @@ "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.", + "match_interval_floored": "The shortest program ({shortest}s) would cap this at {cap}s, below the {minimum}s minimum, so the minimum is used: {persistence} consecutive matches take {budget}s, over {pct}% of that program. Matching only runs when a reading arrives (median={median}s), so this costs nothing.", "max_duration_ratio": "Based on labeled cycle durations (p95={p95}).", - "sampling_interval": "Median update interval observed across {cycles} clean cycles ({si}s).{excl}", - "smoothing_window": "Sized to smooth ~30s of readings at {si}s sampling ({samples} samples).", - "start_duration": "Kept short (~one {si}s sample interval) so detection starts as early as possible while still ignoring single-sample spikes.", "min_power": "40% of the p05 lowest active power ({p05}W) across {cycles} clean cycles, keeping the off-gate below real draw.{excl}", "completion_min_seconds": "Half the p05 clean-cycle duration ({minutes} min) across {cycles} cycles; filters ghost cycles.{excl}", - "learning_confidence": "p05 confidence of {cycles} user-labeled cycles ({p05}); below this, request verification.", - "auto_label_confidence": "15th-percentile confidence of {cycles} auto-labels the user never corrected ({p15}).", - "profile_match_threshold": "p10 confidence of {cycles} correct auto-labels ({p10}); safe live-commit floor.", - "end_repeat_count": "{false}/{total} clean cycles ({pct}%) had a >60s internal pause that resumed; require {val} end confirmation(s).", "min_off_gap": "Based on {gaps} observed inter-cycle gaps (p05={p05}s). Device floor: {floor}s.", "min_off_gap_bridge": "Sized to bridge the longest quiet stretch inside a cycle: {span}s + 60s buffer, from {spans} bridged gaps across {cycles} clean cycles (stays under your {ceiling}s back-to-back headroom).", - "min_active": "Based on minimum active power ({min}W) observed in last cycle.", - "end_energy_default": "Default recommended baseline for end-of-cycle noise gate.", - "dead_zone_from_dip": "Based on early power dip detected at {s}s.", - "thr_batch": "Kept just above the p05 lowest active power across {cycles} cycles ({p05}W) so a start is caught as early as possible and the stop threshold stays below the machine's lowest running power.", - "end_energy_false": "p95 false-end energy ({p95}Wh) across {cycles} cycles, floored at 0.2% of median cycle energy ({floor}Wh).", - "end_energy_no_false": "No false ends across {cycles} cycles; floored at 0.2% of median cycle energy ({floor}Wh).", - "dead_zone_batch": "Kept short (median startup-instability window across {cycles} cycles = {s}s) so a real end is detected promptly.", - "ml_off_delay": "End-detector-confirmed pauses: p95 {p95}s + 60s buffer, from {pauses} model-verified pauses across {cycles} cycles (floor {floor}s).", - "ml_end_repeat": "{false}/{total} cycles ({pct}%) had a pause the end-detector scored >50%; require {val} end confirmation(s).", - "ml_auto_label": "Lowest confidence the quality model still rated clean (p10 of {cycles} clean cycles = {p10})." + "standby_floor": "Every recent cycle was still drawing {idle}W when it ended, so a threshold below that can never see the appliance switch off. Kept above it; the {cycles} clean cycles on record never paused under it for more than {pause}s.", + "thr_batch": "Just above the lowest active power (p05) across {cycles} cycles ({p05}W), so starts are caught early and the stop threshold stays below the lowest running power.", + "max_duration_ratio_default": "Back to the default: a lower ceiling rejects the right programme whenever a run is longer than usual.", + "min_duration_ratio_default": "Back to the default: a higher floor stops a running cycle from being recognised until it is well under way.", + "end_energy_floor": "Raised to the floor the stop threshold ({stop} W) over the off delay ({off_delay} s) implies; below it the cycle can only end through a fallback path." }, "exclusions": { "summary": "Excluded {total} mis-detected cycle(s): {parts}.", @@ -1914,7 +1803,8 @@ "no_power": "no power", "no_active_power": "no active power" } - } + }, + "now_value": "(now {v})" }, "tab": { "advanced": "Advanced", @@ -1944,31 +1834,51 @@ "wrong_profile": "Wrong profile" }, "toast": { + "task_name_required": "Give the task a name", + "task_added": "Task added", + "task_save_failed": "Could not save task: {error}", + "task_removed": "Task removed", + "maint_logged": "Logged as done: {task}", "access_saved": "Access control saved", "all_wiped": "All data wiped", + "analysis_complete": "Analysis complete: {count} suggestion(s)", "analysis_complete_none": "Analysis complete: no new suggestions", + "analysis_failed": "Analysis failed: {error}", "appliance_added": "Appliance added - awaiting approval", "apply_failed": "Apply failed: {error}", + "auto_detect_failed": "Auto-detect failed: {error}", "auto_label_complete": "Auto-label complete", + "auto_label_failed": "Auto-label failed: {error}", + "auto_label_done": "Auto-label complete: {count} cycles labelled, {skipped} left unlabelled.", + "auto_label_done_one": "Auto-label complete: {count} cycle labelled, {skipped} left unlabelled.", + "auto_label_done_other": "Auto-label complete: {count} cycles labelled, {skipped} left unlabelled.", + "auto_label_cancelled": "Auto-label cancelled after labelling {count} cycles.", + "auto_label_cancelled_one": "Auto-label cancelled after labelling {count} cycle.", + "auto_label_cancelled_other": "Auto-label cancelled after labelling {count} cycles.", "automation_deleted": "Automation deleted", "brand_added": "Brand added - awaiting approval", "catalog_refreshed": "Community catalog refreshed", "contrib_device_changed": "Added to the community store, but the device changed while the form was open. Select it in Settings.", "correction_submitted": "Correction submitted", - "cycle_deleted": "Cycle deleted", + "could_not_load_cycle": "Could not load cycle: {error}", + "create_failed": "Create failed: {error}", "cycle_force_stopped": "Cycle force-stopped", "cycle_labelled": "Cycle labelled", "cycle_paused": "Cycle paused", "cycle_resumed": "Cycle resumed", "cycle_trimmed": "Cycle trimmed", + "cycles_deleted": "Deleted {count} cycle(s)", "cycles_merged": "Cycles merged", + "debug_cleared": "Cleared {count} debug traces", "delete_failed": "Delete failed: {error}", - "remove_failed": "Remove failed: {error}", "delete_partial_failed": "Some items could not be deleted and were restored", "device_approved": "Approved by the community", + "discard_failed": "Discard failed: {error}", "envelope_rebuilt": "Envelope rebuilt", "envelopes_rebuilt": "Envelopes rebuilt", + "error": "Error: {error}", "export_downloaded": "Export downloaded", + "export_failed": "Export failed: {error}", "export_selective_done": "Export downloaded", "feedback_all_dismissed": "All feedbacks dismissed", "feedback_confirmed": "Feedback confirmed", @@ -1978,9 +1888,14 @@ "group_name_required": "Group name is required", "group_saved": "Group saved", "hist_csv_required": "Load a CSV file or paste its contents first", + "import_failed": "Import failed: {error}", + "import_no_restore_point": "Imported, but no restore point could be saved, so this import cannot be undone", "import_selective_done": "Imported {profiles} profile(s) and {cycles} cycle(s)", - "import_successful": "Import successful; integration reloading", + "import_undo_failed": "Could not undo the import: {error}", + "import_undone": "Import undone: {profiles} profile(s) and {cycles} cycle(s) restored", + "invalid_json": "\"{key}\" is not valid JSON - fix it or clear the field before saving.", "json_required": "JSON data is required", + "label_failed": "Label failed: {error}", "legacy_removed": "Legacy actions removed", "load_more_failed": "Could not load more: {error}", "logs_exported": "Logs exported", @@ -1988,22 +1903,25 @@ "maint_added": "Maintenance event added", "maint_delete_failed": "Could not delete event: {error}", "maint_deleted": "Maintenance event deleted", - "matching_reverted": "Matching weights reverted to defaults", "min_2_profiles": "Select at least 2 profiles for a group", "ml_training_no_improvement": "Training complete: baseline kept (no improvement)", + "ml_training_promoted": "Training complete: promoted {count} model(s)", "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_failed": "Pause failed: {error}", "pause_no_cycle": "No active cycle to pause", "pg_preset_loaded": "Preset \"{name}\" loaded", "pg_preset_saved": "Preset \"{name}\" saved", "pg_run_gone": "That run is no longer available.", "pg_sugg_loaded": "Staged {n} suggested value(s) - run the Playground to compare", - "pg_sugg_ml_loaded": "Staged {n} ML-calibrated value(s) - run the Playground to compare", + "phase_created": "Phase \"{name}\" created", + "phase_deleted": "Phase \"{name}\" deleted", "phase_name_required": "Phase name is required", + "phase_reset": "\"{name}\" restored to the built-in phase", "phase_updated": "Phase updated", "phases_saved": "Phases saved", "preferences_saved": "Preferences saved", @@ -2012,6 +1930,7 @@ "processed_models": "{n} model(s) promoted", "processed_suggestions": "{n} suggestion(s)", "profile_added": "Profile added - awaiting approval", + "profile_created": "Profile \"{name}\" created", "profile_name_required": "Profile name is required", "profile_renamed": "Profile renamed", "rating_saved": "Quality rating saved", @@ -2023,14 +1942,20 @@ "relabel_failed": "Relabel failed: {error}", "reminders_save_failed": "Could not save reminders: {error}", "reminders_saved": "Service reminders saved", + "remove_failed": "Remove failed: {error}", + "rename_failed": "Rename failed: {error}", "resume_no_cycle": "No paused cycle to resume", "review_saved": "Review saved", + "save_failed": "Save failed: {error}", "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", + "split_complete": "Split into {count} cycles", + "start_failed": "Start failed: {error}", + "stop_failed": "Stop failed: {error}", "store_connect_failed": "Connect failed: {error}", "store_connected": "Connected to the community store", "store_device_downloaded": "{p} program(s), {c} recording(s) added", @@ -2043,14 +1968,16 @@ "store_disconnected": "Disconnected from the community store", "store_download_failed": "Download failed: {error}", "store_download_nothing": "Nothing new to download - this setup is already on your device.", + "store_download_skipped": "{n} recording(s) skipped: too short, too gappy or already on your device.", "store_error": "Error: {error}", + "store_import_duplicate": "This recording is already on your device.", "store_import_failed": "Import failed: {error}", + "store_import_low_quality": "This recording is too short, too gappy or implausible to import.", "store_imported": "Imported into {profile}", "store_name_required": "Enter a profile name", "store_no_appliance": "Set your appliance brand and model in Settings first.", "store_pick_profile": "Pick a profile to merge into", "store_pick_profile_share": "Pick a profile to share into", - "store_program_required": "Enter a program name", "store_search_failed": "Search failed: {error}", "store_share_failed": "Share failed: {error}", "store_shared": "Shared to the community store - pending review.", @@ -2060,7 +1987,9 @@ "task_cancelled": "Cancelled.", "thanks_confirming": "Thanks for confirming", "unlabel_done": "Unlabelled {count} cycle(s)", - "unlabel_failed": "Unlabel failed: {error}" + "unlabel_failed": "Unlabel failed: {error}", + "update_failed": "Update failed: {error}", + "store_download_partial": "{n} program(s) could not be downloaded; try again later." }, "trend": { "down": "Trending down", @@ -2071,12 +2000,14 @@ "status_tip": "Cycle outcome: Completed (natural end), Interrupted (abrupt power drop), Force Stopped (manual), or Needs Review (feedback pending).", "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.", + "energy_tip": "Total energy used (kWh), computed from power over time.", "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.", + "cost_tip": "Energy cost of this cycle, fixed when it finished. With Time-Weighted Cost on, each moment is charged at its own price; otherwise at the price at the end. Set a price in Settings to fill this in.", "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." + "flags_tip": "Review, anomaly and source flags for the cycle. Hover an icon for detail.", + "dbg_conf": "Conf", + "dbg_mae": "MAE", + "dbg_corr": "Corr" }, "stat": { "avg": "avg", @@ -2100,20 +2031,19 @@ "notify_finish": "A finish notification would be sent.", "notify_milestone": "A milestone notification would be sent.", "notify_held": "A notification was held back for quiet hours.", - "finished": "The cycle reached a terminal state and ended." + "finished": "The cycle reached a terminal state and ended.", + "match_reverted": "The program fell back to detecting: its score dropped well below its own peak.", + "verified_pause": "The live alignment confirmed (or released) a pause inside the program. While it holds, the cycle is not ended.", + "stalled": "The cycle sat at standby draw longer than this program ever pauses there, with work left. Shown as Paused (Stalled); the cycle stays open." }, "store": { "browse": "Browse", "device": "Device", - "favorites": "Favourites", "programs_count": "Programs: {n}", "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", "no_results": "No matching appliances found. Try a different search.", - "n_programs": "{n} programs", - "n_cycles": "{n} cycles", "no_programs": "No shared programs for this appliance yet.", "no_cycles": "No reference cycles shared for this program yet.", "anon": "anonymous", @@ -2125,19 +2055,21 @@ "merge_target": "Merge into profile", "description": "Description (optional)", "contributed_by": "by {name}", + "website": "Store website ↗", + "website_tip": "Open the community store website", + "favorites": "Favourites", "rating_summary": "★ {avg} ({n})", "no_ratings": "No ratings yet", - "website": "Store website ↗", - "website_tip": "Open the community store website" + "unreachable": "Couldn't reach the community store. Check your connection and try again." }, "settings": { "gear": { - "title": "Settings" + "title": "Preferences" } }, "link": { - "manual": "Manual ↗", - "add_appliance": "Add your appliance" + "maintenance_supporter": "Maintenance Supporter on GitHub ↗", + "manual": "Manual ↗" }, "tip": { "add_brand": "Brand not listed? Add it to the community catalog", @@ -2171,7 +2103,13 @@ "rebuild": { "envelopes": "Rebuilding envelopes" }, - "cancelling": "Cancelling..." + "cancelling": "Cancelling...", + "auto_label": { + "running": "Auto-labelling cycles" + }, + "store_download": { + "running": "Downloading from the store" + } }, "setup": { "hdr": { @@ -2180,26 +2118,24 @@ }, "phase0": { "washer": "WashData is already detecting your cycles. Record your first cycle to enable program names and time estimates.", - "dishwasher": "WashData is watching. Dishwashers have complex cycles – recording your first cycle is strongly recommended. If a detected cycle runs too long, use the cycle editor to trim it before saving as a profile.", + "dishwasher": "WashData is watching. Dishwasher cycles are complex, so record your first cycle. If a detected cycle runs too long, trim it in the cycle editor before saving it as a profile.", "generic": "WashData is watching. Record or label a detected cycle to start building profiles." }, "phase1a": { - "labelled": "Good start – your first program is saved. For the cleanest data, consider recording your next cycle with the recorder widget." + "labelled": "Good start: your first program is saved. For the cleanest data, record your next cycle with the recorder." }, "phase1b": { - "recorded": "Your recording was saved as {profile_name}. Now record or label your other common programs to build coverage." + "recorded": "Your recording was saved as {profile_name}. Now record or label your other common programs." }, "phase1c": { - "verify": "You have {count} programs from the community. Run a cycle to verify WashData recognises it correctly – matching will improve as your device builds its own history." + "verify": "You have {count} programs from the community. Run a cycle to check WashData recognises it; matching improves as your device builds its own history." }, "phase2": { - "cluster": "WashData has seen {count} cycles that don't match any saved program – they look similar to each other. Want to create a new profile for them?", + "cluster": "WashData has seen {count} similar cycles that match no saved program. Create a new profile for them?", "unmatched": "Your last cycle didn't match any saved program. Is it a new program?" }, "phase3": { - "suggestions": "WashData has settings recommendations based on your cycle history – review them to improve detection accuracy.", - "groups": "Some of your profiles look like the same program at different temperatures. Organise them into a group for better matching.", - "phases": "Add program phases to {profile_name} for more accurate time-remaining estimates." + "suggestions": "WashData has setting suggestions from your cycle history. Review them to improve detection." }, "phase4": { "healthy": "This device is fully set up ({profile_count} profiles)." @@ -2212,12 +2148,58 @@ "create_from_cluster": "Create profile from these cycles", "create_profile": "Create a profile for it", "review_suggestions": "Review suggestions", - "organise_profiles": "Organise profiles", - "configure_phases": "Configure phases", "skip_step": "Skip this step", "skip_forever": "Don't show again", "hide_guidance": "Hide guidance", "show_guidance": "Show guidance" } + }, + "access": { + "edit": "Edit", + "full": "Full", + "inherit": "Inherit", + "none": "None (hidden)", + "read": "Read" + }, + "pg_label_reason": { + "no_winner": "No program fit", + "below_floor": "Below the learning confidence", + "ambiguous": "Flagged ambiguous", + "margin": "Too close to the runner-up", + "unknown_profile": "Program no longer exists", + "not_evaluated": "Not evaluated" + }, + "diagram": { + "accum": "accum", + "auto": "auto", + "below_off": "below = off", + "confirmed": "confirmed", + "energy_counts": "energy counts", + "feedback": "feedback", + "gap_below_min": "gap below min = one cycle", + "heat_phase": "heat phase", + "held_at_100": "held at 100%", + "ignored": "ignored", + "kept": "kept", + "max": "max", + "min": "min", + "minus_tol": "-tol", + "no_match": "no match", + "no_updates": "no updates", + "off_delay_wait": "off-delay wait", + "off_gap": "off-gap", + "plus_tol": "+tol", + "profile": "profile", + "reading_interval": "typical reading interval", + "sensor_offline": "sensor offline: force-stop", + "si": "SI", + "spike_ignored": "spike ignored", + "spike_ignored_short": "spike: ignored", + "start": "start", + "stop": "stop", + "tail_energy_resets": "tail energy above thresh: timer resets", + "thr": "thr", + "too_short": "too short", + "tumble_kept": "tumble pulses kept" } } diff --git a/custom_components/ha_washdata/translations/panel/es.json b/custom_components/ha_washdata/translations/panel/es.json index 0409bbd3..4ce1f069 100644 --- a/custom_components/ha_washdata/translations/panel/es.json +++ b/custom_components/ha_washdata/translations/panel/es.json @@ -1,15 +1,16 @@ { "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_tip": "Cerca del final, este programa queda en silencio unos {mins} min y luego consume unos {watts} W durante {secs} s. Visto en {seen} de {measured} ciclos medidos.", "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", + "artifact_tip_one": "{n} anomalía detectada (p. ej., puerta abierta a mitad del ciclo): ábrala para verla en el gráfico", + "artifact_tip_many": "{n} anomalías detectadas (p. ej., puerta abierta a mitad del ciclo): ábrala para verlas en el gráfico", + "artifact_tip_other": "{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", "declining": "↘ en declive", "energy_low": "Menor energía de lo habitual", "energy_spike": "Mayor energía de lo habitual", - "fair_fit": "calidad aceptable", - "fair_fit_tip": "Consistencia de coincidencia moderada: algunos ciclos asignados a este perfil tienen puntuaciones de confianza más bajas. Etiquete más ciclos o vuelva a registrar el perfil para mejorar la precisión.", "feedback_requested": "Comentarios solicitados", "golden_cycle": "Ciclo de referencia registrado", "improving": "↗ mejorando", @@ -17,28 +18,41 @@ "needs_review": "Necesita revisión", "overrun": "Corrió más de lo habitual", "poor_fit": "calidad deficiente", - "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", + "restart_gap_tip": "{n} brechas de reinicio de HA: el trazado de potencia tiene huecos", + "restart_gap_tip_one": "{n} brecha de reinicio de HA: el trazado de potencia tiene un hueco", + "restart_gap_tip_many": "{n} brechas de reinicio de HA: el trazado de potencia tiene huecos", + "restart_gap_tip_other": "{n} brechas de reinicio de HA: el trazado de potencia tiene huecos", "reviewed": "Revisado", "steady": "→ estable", "underrun": "Terminó más rápido de lo habitual", - "using_defaults": "Usando los valores predeterminados enviados", - "using_tuned": "Usando pesos sintonizados", "overrun_ratio": "({x}x lo esperado)", "underrun_ratio": "({pct}% de lo esperado)", - "awaiting_tip": "A la espera de la aprobación de la comunidad", - "awaiting_n": "A la espera de aprobación · {n} confirmados", - "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.", "armed": "(se aplica al siguiente ciclo)", - "duration_outlier": "{n} ciclo(s) de duración anómala" + "duration_outlier": "{n} ciclo(s) de duración anómala", + "awaiting_tip": "A la espera de la aprobación de la comunidad", + "awaiting_n": "A la espera de aprobación · {n} confirmados", + "awaiting": "A la espera de aprobación", + "approved": "Aprobado", + "matcher_unused": "Aún no coincide con ninguno de sus ciclos", + "matcher_used": "Coincide con {n} de sus ciclos", + "matcher_used_one": "Coincide con {n} de sus ciclos", + "matcher_used_many": "Coincide con {n} de sus ciclos", + "matcher_used_other": "Coincide con {n} de sus ciclos", + "matcher_used_tip": "Ciclos de este electrodoméstico que WashData asoció por sí solo con este programa importado. Una importación que nunca coincide puede no ajustarse a su electrodoméstico; puede eliminarla. Se cuenta localmente; no se envía nada a la tienda de la comunidad." }, "btn": { + "log_done_task": "Registrar como hecho: {task}", + "add_task": "Agregar tarea", + "pg_preset_delete": "Eliminar preajuste", + "pg_preset_load": "Cargar preajuste", + "pg_preset_load_tip": "Sustituir los valores de la Zona de pruebas por este preajuste (los ajustes activos no se modifican)", + "pg_preset_save": "Guardar como preajuste", + "pg_preset_save_tip": "Guardar todos los valores de abajo como un preajuste con nombre para este dispositivo", "reset_zoom": "Restablecer zoom", "set_brand_model": "Definir marca y modelo", "refresh_catalog": "Actualizar catálogo", @@ -49,12 +63,11 @@ "add_timer": "+ Agregar temporizador", "apply_all": "Aplicar todo", "apply_label": "Aplicar etiqueta", - "apply_set_b": "Aplicar conjunto B", "apply_split": "Aplicar división", "apply_trim": "Aplicar recorte", "auto_detect_split": "Detección automática", "auto_label_cycles": "Ciclos de etiquetado automático", - "auto_label_cycles_tip": "Asignar automáticamente nombres de perfil a ciclos sin etiquetar cuya confianza de coincidencia supera el umbral", + "auto_label_cycles_tip": "Poner nombre a los ciclos sin etiquetar cuya confianza de coincidencia supera el umbral.", "cancel": "Cancelar", "clear_debug": "Borrar datos de depuración", "clear_splits": "Borrar", @@ -64,8 +77,6 @@ "convert_to_automation": "Convertir a automatización", "correct": "Correcto…", "create": "Crear", - "create_from_cluster": "Crear perfil a partir de {count} ciclos", - "create_profile": "+ Crear perfil", "delete": "Borrar", "delete_group": "Eliminar grupo", "delete_group_tip": "Eliminar solo este grupo: los perfiles de los miembros se conservan", @@ -76,22 +87,19 @@ "done": "Hecho", "edit": "Editar", "export": "Exportar", - "export_json": "Exportar a JSON", "force_stop": "Forzar parada", "force_stop_tip": "Terminar inmediatamente el ciclo actual y marcarlo como detenido forzadamente", "from_template": "De plantilla ▾", "ignore": "Ignorar", "import_json": "Importar desde JSON", - "import_overwrite": "Importar (sobrescribe datos)", "inspect": "Inspeccionar", "label": "Etiquetar", - "load": "Cargar", "load_more": "Cargar más", "manage": "Administrar", "merge": "Unir", "new_automation": "+ Nueva automatización", "new_group": "+ Nuevo grupo", - "new_group_tip": "Agrupar perfiles casi idénticos (misma forma/duración, diferente temperatura o centrifugado) para que el comparador distinga entre ellos de forma fiable", + "new_group_tip": "Agrupar perfiles casi idénticos (misma forma, distinta temperatura o centrifugado) para que la coincidencia pueda distinguirlos.", "new_phase": "+ Nueva Fase", "new_profile": "+ Nuevo perfil", "new_profile_tip": "Crear un nuevo perfil de programa a partir de un ciclo etiquetado existente o una grabación", @@ -99,36 +107,25 @@ "on_cycle_started": "En ciclo iniciado", "pause_cycle": "Pausa", "pause_cycle_tip": "Pausar el ciclo en curso – el electrodoméstico reanudará donde lo dejó", - "pg_apply_device": "Aplicar al dispositivo", - "pg_autofill": "Autocompletar", - "pg_load_calibrated": "Cargar Calibrado (ML) ({n})", - "pg_load_calibrated_tip": "Cargar los ajustes calibrados por ML como anulaciones en la Zona de pruebas", "pg_load_live": "Cargar los ajustes activos", "pg_load_live_tip": "Volver a leer los ajustes actuales de la integración y descartar todas las ediciones de la Zona de pruebas", "pg_load_suggested": "Cargar sugerencias ({n})", "pg_load_suggested_tip": "Cargar las sugerencias actuales del ajustador automático como anulaciones en la Zona de pruebas", - "pg_preset_delete": "Eliminar preajuste", - "pg_preset_load": "Cargar preajuste", - "pg_preset_load_tip": "Sustituir los valores de la Zona de pruebas por este preajuste (los ajustes activos no se modifican)", - "pg_preset_save": "Guardar como preajuste", - "pg_preset_save_tip": "Guardar todos los valores de abajo como un preajuste con nombre para este dispositivo", "pg_publish_all": "Guardar {n} ajuste(s) en la integración", "pg_publish_one": "Guardar {label} en la integración", - "play": "Reproducir", "process": "Proceso", "process_history": "Procesar ahora", "process_recording": "Guardar grabación", "process_tip": "Guardar la grabación capturada como un perfil nuevo o existente", "rebuild": "Reconstruir sobres", "rebuild_envelope": "Reconstruir envolvente", - "rebuild_tip": "Recalcular la envolvente de potencia esperada (banda mín/máx) para todos los perfiles a partir de sus ciclos etiquetados – ejecutar después de etiquetar nuevos ciclos o corregir los existentes", + "rebuild_tip": "Reconstruir la banda de potencia esperada de cada perfil a partir de sus ciclos etiquetados. Ejecútelo después de etiquetar o corregir ciclos.", "rec_start_tip": "Comenzar a grabar la traza de potencia del electrodoméstico – iniciar justo antes de ejecutar un ciclo", "rec_stop": "Detener", "rec_stop_tip": "Detener la grabación y retener la traza capturada para revisión", "record": "Iniciar grabación", "refresh": "Refrescar", "refresh_diag_tip": "Recargar las estadísticas de almacenamiento", - "refresh_logs_tip": "Recargar los últimos registros de log", "refresh_settings_tip": "Recargar la configuración desde el servidor", "relabel": "Reetiquetar ({count})", "remove": "Eliminar", @@ -136,7 +133,6 @@ "reset": "Reiniciar", "reset_ml_models_tip": "Descarta los modelos ajustados y vuelve a los integrados. WashData puede volver a aprenderlos más tarde.", "reset_to_builtin": "Restablecer a modelos integrados", - "reset_to_defaults": "Restablecer los valores predeterminados", "resume_cycle": "Reanudar ciclo", "resume_cycle_tip": "Reanudar el ciclo en pausa", "revert_settings": "Descartar cambios", @@ -149,8 +145,6 @@ "run_analysis": "🔍 Ejecutar análisis de sugerencias", "run_analysis_tip": "Analice sus ciclos grabados ahora y actualice las sugerencias de ajuste", "run_auto_label": "Ejecutar etiqueta automática", - "run_both": "Ejecutar A vs B", - "run_simulation": "Ejecutar simulación", "save": "Guardar", "save_access_control": "Guardar control de acceso", "save_group": "Guardar grupo", @@ -162,10 +156,8 @@ "save_settings": "Guardar configuración", "select": "Seleccionar", "select_all": "Seleccionar todo", - "select_last20": "Últimos 20", "select_none": "Ninguno", "show_only": "Mostrar solo", - "skip_setup": "Omitir configuración", "split": "Dividir", "stop": "Detener", "submit_correction": "Enviar corrección", @@ -174,36 +166,31 @@ "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.", + "unlabel_selected_tip": "Quitar la etiqueta de los ciclos seleccionados. Siguen en su historial, pero dejan de dar forma a este perfil.", "use": "Usar", "wipe_all": "Borrar todos los datos", "delete_selected": "Eliminar seleccionados ({n})", - "group_suggest": "Grupo {n}: {members}", "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", "import": "Importar", "disconnect": "Desconectar", "connect_github": "Conectar GitHub", - "save_appliance": "Guardar electrodoméstico", "share_to_store": "Compartir en la tienda", "share": "Compartir", - "confirm_appliance": "Confirmar este electrodoméstico", - "confirmed": "Confirmado", "browse_community_setups": "Explorar configuraciones de la comunidad", "download_device": "Descargar esta configuración", "share_device": "⬆ Compartir este dispositivo", - "share_device_tip": "Comparte este electrodoméstico y sus ciclos de referencia grabados en la tienda de la comunidad para que otros con el mismo aparato puedan adoptarlos", + "share_device_tip": "Compartir este electrodoméstico y sus ciclos de referencia en la tienda de la comunidad para que los dueños de la misma máquina puedan adoptarlos.", "share_n": "Compartir {n} ciclo(s)", "export_selected": "Exportar (elegir datos)", "export_all": "Exportar todo rápido", - "import_raw": "Avanzado: reemplazar todo desde JSON", "download_export": "Descargar exportación", "analyze_import": "Analizar archivo", "import_selected": "Importar lo seleccionado", "back": "Atrás", "mute_suggestion": "Dejar de sugerir este ajuste", "reset_muted": "Restablecer silenciados", + "reset_phase_tip": "Deshacer la edición y restaurar la fase incorporada", "import_power_history": "Importar historial de potencia", "hist_read_recorder": "Leer desde Home Assistant", "hist_scan": "Buscar ciclos", @@ -212,13 +199,15 @@ "save_total_cycles": "Guardar total", "clear": "Borrar", "add_brand": "Añadir marca", - "add_appliance": "Añadir electrodoméstico" + "add_appliance": "Añadir electrodoméstico", + "retry": "Reintentar", + "confirm_appliance": "Confirmar este electrodoméstico", + "confirmed": "Confirmado", + "create_profile_from_gap": "Crear perfil", + "undo_import": "Deshacer la última importación", + "pg_rerun": "Actualizar simulación" }, "conflict": { - "anti_wrinkle_exit": { - "exit": "Debe ser inferior al Umbral de parada ({stop} W); de lo contrario, la potencia de salida antiarrugas se ignora", - "stop": "Debe ser superior a la Potencia de salida antiarrugas ({exit} W)" - }, "anti_wrinkle_max": { "max": "Debe ser superior al Umbral de inicio ({start} W); de lo contrario, el límite de duración antiarrugas se omite", "start": "Debe ser inferior a la Potencia máxima antiarrugas ({max} W)" @@ -226,10 +215,10 @@ "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." + "power_sensor": "Este es el Sensor de potencia del dispositivo, no un precio. Los ciclos se cobrarían en vatios por kWh; borre este campo.", + "energy_sensor": "Esta es la Entidad del medidor de energía del dispositivo, no un precio. 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. Borre este campo o elija un sensor de tarifa.", + "unit": "Este sensor mide en {unit}, no en precio por kWh. Borre este campo o elija 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", @@ -264,8 +253,8 @@ "threshold": "Debe ser inferior al Umbral de parada ({stop} W) para tener efecto" }, "pump_stuck": { - "duration": "Debe ser menor que el Tiempo de espera sin actualización ({to} s) para que la alarma de atasco se active antes de que el watchdog detenga el ciclo", - "timeout": "Debe superar la Duración de bomba atascada ({ps} s) para que la alarma se active antes de que el ciclo se detenga forzadamente" + "duration": "Debe ser menor que el Tiempo de espera sin actualización ({to} s) para que la alarma de bomba atascada pueda saltar primero", + "timeout": "Debe superar la Duración de bomba atascada ({ps} s) para que la alarma de bomba atascada pueda saltar primero" }, "start_dur": { "sampling": "El Intervalo de muestreo supera la Duración de inicio ({sdt} s); los picos de una sola muestra pueden abrir un ciclo", @@ -281,10 +270,15 @@ "watchdog": { "interval": "Debería ser al menos 2x el Intervalo de muestreo ({si} s)", "sampling": "El Intervalo de muestreo debería ser como máximo la mitad del Intervalo watchdog ({wi} s)" + }, + "end_energy": { + "energy": "Demasiado estricto para el Umbral de parada ({w} W) durante el Retardo de apagado ({d} s); el ciclo solo puede terminar mediante una vía de respaldo", + "stop": "La Energía de fin ({e} Wh en {d} s) solo permite {p} W" } }, "hdr": { - "ab_comparison": "Comparación de ajustes A/B", + "custom_tasks": "Sus propias tareas", + "maintenance_supporter": "¿Necesita más?", "access_control": "Control de acceso", "add_maintenance": "Agregar evento de mantenimiento", "automation_delete": "Eliminar esta automatización", @@ -293,28 +287,22 @@ "automation_review_this_cycle": "Revisa este ciclo", "automations": "Automatizaciones", "clear_debug": "Borrar rastros de depuración", - "cycle_simulator": "Simulador de ciclos", "diagnostics_pref": "Diagnóstico", "display": "Mostrar", - "dtw_inspector": "Inspector DTW", "export_import": "Exportar / Importar", - "getting_started": "Primeros pasos", "live_power": "Poder en vivo", "logs": "Registros", - "logs_diagnostics": "Diagnóstico", "maintenance": "Acciones de mantenimiento", "maintenance_log": "Registro de mantenimiento", "maintenance_reminders": "Recordatorios de servicio", "manual_recording": "Grabación manual", - "manual_recording_tip": "Ejecute un ciclo intencionalmente mientras WashData registra la traza de potencia. Inicie justo antes de que el electrodoméstico arranque, Detenga cuando termine, luego Procese para guardarlo como un perfil con nombre.", + "manual_recording_tip": "Grabe un ciclo a propósito: pulse Inicio justo antes de que arranque el electrodoméstico y Detener cuando termine, y luego Proceso para guardarlo como perfil.", "ml_learned": "Lo que WashData ha aprendido", - "ml_matching_tuning": "Ajuste fino de coincidencia de programas", "ml_settings_card": "Ajustes", "ml_smart_learning": "Aprendizaje inteligente", "my_preferences": "Mis preferencias", "no_automations": "Ninguna automatización hace referencia a este dispositivo todavía.", "panel_settings": "Configuración del panel", - "param_sweep": "Barrido de parámetros", "pg_settings_source": "Origen de los ajustes", "phase_catalog": "Catálogo de fases", "playground": "Zona de pruebas", @@ -323,11 +311,8 @@ "status": "Estado", "status_graph": "Gráfico de estado", "storage_stats": "Estadísticas de almacenamiento", - "test_history": "Probar con el historial", "toggle_sidebar": "Alternar barra lateral de Home Assistant", - "tools_and_data": "Herramientas y datos", "wipe_history": "Limpiar historial", - "recommendations": "Recomendaciones ({n})", "pg_detection_params": "Ajustes de detección", "pg_outcome": "Resultado de la simulación", "pg_across_cycles": "En todos tus ciclos", @@ -335,7 +320,8 @@ "online_account": "Tienda de la comunidad y funciones en línea", "import_power_history": "Importar historial de potencia", "service_status": "Estado del mantenimiento", - "online_account_tab": "En línea y comunidad" + "online_account_tab": "En línea y comunidad", + "live_match_debug": "Depuración de coincidencia en vivo" }, "health": { "fair": "Calidad de perfil aceptable", @@ -343,7 +329,12 @@ "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.", + "task_name": "Tarea", + "every_cycles": "Cada (ciclos)", + "every_days": "Cada (días)", + "new_task_name": "Nueva tarea", + "days_since_service": "{since} / {total} días", + "envelope_position_tip": "Posición en la curva grabada del programa coincidente: {pct}%. Se obtiene alineando el ciclo con el perfil, así que sigue siendo correcta aunque un ciclo dure más o menos. Se actualiza mientras el electrodoméstico está en silencio, por lo que puede ir con retraso.", "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})", @@ -355,10 +346,8 @@ "aria_cycle_chart": "Curva de potencia del ciclo", "aria_envelope_chart": "Envolvente de potencia del perfil", "aria_phase_chart": "Gráfico del editor de fases", - "aria_playground_chart": "Curva de potencia del ciclo con umbrales de detección arrastrables", "aria_power_chart": "Gráfico de potencia", "aria_spaghetti_chart": "Curvas de potencia de ciclos superpuestas", - "aria_sweep_chart": "Gráfico de resultados del barrido de parámetros", "artifact_anomaly": "Anomalía", "artifact_high_power": "Potencia alta", "artifact_interruption": "Interrupción", @@ -372,150 +361,107 @@ "cohesion_good": "cohesión {pct}%", "cohesion_low": "⚠ cohesión baja {pct}%", "compare_profiles": "Comparar con perfiles", - "concurrency": "Concurrencia", "confidence": "Confianza", "confidence_threshold": "Umbral de confianza", "correct_duration": "Duración correcta (min, opcional)", "correct_profile": "Perfil correcto", - "correlation": "Correlación", "cost": "Costo", "create_new_profile": "+ Crear nuevo perfil…", "current_phase": "Fase actual", - "curves": "Curvas", "cycle": "Ciclo", "cycle_date_display": "Visualización de la fecha del ciclo", - "cycle_health": "Salud del ciclo", - "cycle_index": "índice de ciclo →", - "cycles": "Ciclos", + "cycle_context": "Historial del recorder en torno al ciclo", + "cycle_context_off": "Desactivado", + "cycle_context_minutes": "{min} min", + "cycle_context_series": "Historial del recorder", + "before_start": "Antes del inicio", + "after_end": "Después del final", "cycles_count": "Ciclos", "cycles_lc": "ciclos", - "dashed": "discontinua", "date": "Fecha", "debug_traces": "Seguimientos de depuración", - "default": "Por defecto", "default_access_level": "Nivel predeterminado para usuarios que no figuran a continuación", "default_other": "Predeterminado (otros dispositivos)", "default_tab": "Pestaña predeterminada al abrir el panel", - "delta": "Δ", "description": "Descripción", - "dtw_blend_w": "Peso de mezcla", - "dtw_blended": "Puntuación combinada", - "dtw_ddtw": "Puntuación DDTW", - "dtw_l1": "Puntuación L1", - "dtw_matched_profile": "(perfil coincidente del ciclo)", "duration": "Duración", - "duration_agreement": "Concordancia de duración", - "emphasis": "Énfasis", "enable_access_control": "Habilitar el control de acceso por usuario", "energy": "Energía", - "energy_agreement": "Concordancia de energía", - "event_timeline": "Cronología de eventos", "event_type": "Tipo de evento", "expected": "Esperado", + "suspected_profile": "{name} (probable)", + "runner_up_profile": "{name} (segundo candidato)", "expected_duration": "Duración esperada (min)", "file_kb": "Archivo (KB)", - "final_score": "Puntuación final", "from": "Desde", "gap_s": "Brechas", "group_name": "Nombre del grupo", "head_trim": "Recorte de inicio (s)", - "health": "Salud", "hide_tabs": "Ocultar pestañas para no administradores", - "in_use": "En uso", "json_data": "Datos JSON", "label": "Programa coincidente", "last": "Últimos", "last_checked": "Última comprobación:", "load_from_file": "Cargar desde archivo", - "mae_score": "Puntuación MAE", "manual_duration": "Duración manual (min, opcional)", "match": "Coincidencia", "members": "Miembros", - "metric": "Métrica", "mode_existing_profile": "Agregar al perfil existente", "mode_new_profile": "Crear nuevo perfil", "models_fine_tuned": "(modelos ajustados: {count})", - "n_classic_suggestions": "{n} clásico", - "n_ml_suggestions": "{n} ML", "n_selected": "{n} seleccionados", - "n_tuning_suggestions": "{n} sugerencia de ajuste", - "na": "n/d", + "n_tuning_suggestions": "{n} sugerencias de ajuste", + "n_tuning_suggestions_one": "{n} sugerencia de ajuste", + "n_tuning_suggestions_many": "{n} sugerencias de ajuste", + "n_tuning_suggestions_other": "{n} sugerencias de ajuste", "new_profile_name": "Nuevo nombre de perfil", "notes": "Notas", - "of": "de", - "outcome": "Resultado", "panel_default_tab": "Pestaña predeterminada", "panel_language": "Idioma del panel", "per_cycle_short": "ciclo", "pg_anti_wrinkle": "Antiarrugas", - "pg_autofill_tip": "Autocompletar el rango a partir del valor actual ({lo}–{hi})", "pg_best_value": "Mejor valor encontrado", "pg_current_value": "Valor actual:", "pg_drag_hint": "Arrastre la línea en el gráfico", - "pg_dtw": "DTW", "pg_ev_ending": "Finalizando", "pg_ev_running": "En marcha", "pg_group_advanced": "Casos límite", "pg_group_detection": "Activadores de detección", "pg_group_timing": "Reglas de tiempo", "pg_idle": "Inactivo", - "pg_last_run": "Última ejecución: {date}", - "pg_match_score": "Puntuación de coincidencia", - "pg_pct_of": "{p}% de {total}", "pg_poor_match": "Coincidencia deficiente", "pg_preset": "Preajuste de la Zona de pruebas", "pg_preset_name": "Nombre del nuevo preajuste", "pg_preset_none": "Seleccione un preajuste…", "pg_strong_match": "Coincidencia fuerte", - "pg_tile_ambiguous": "Ambiguo", - "pg_tile_ambiguous_tip": "Ciclos con un empate ajustado entre dos programas", - "pg_tile_detected": "Detectado", - "pg_tile_detected_tip": "Ciclos en los que la potencia superó el umbral de inicio el tiempo suficiente para registrarse como en marcha", - "pg_tile_matched": "Reconocido", - "pg_tile_matched_tip": "Ciclos correctamente atribuidos a un programa conocido", - "pg_time_left": "Tiempo restante", "pg_weak_match": "Coincidencia débil", "phase_name": "Nombre de la fase", "name": "Nombre", "phase_timeline": "Cronología de fases", "power": "Potencia", "profile": "Perfil", - "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. Elige uno antes de poner en marcha el electrodoméstico y se aplicará en cuanto empiece el siguiente ciclo.", + "program_tip": "Elija con qué programa se asocia el ciclo actual. Detección automática elige la mejor coincidencia. Fije un programa cuando la detección automática se equivoque; si lo elige antes de empezar, se aplica al siguiente ciclo.", "progress": "Progreso", "raw_socket": "Señal bruta", - "read_only": "Sólo lectura", "ref_cycle": "Ciclo de referencia (opcional)", - "refresh_interval": "Intervalo(s) de actualización", "remaining": "Restante", "reminder_every": "Recordar cada (ciclos)", "remove_label": "- Quitar etiqueta -", "rename_profile": "Cambiar nombre de perfil", - "replay_duration": "Reproducción", "resulting_profile": "Perfil resultante", "save_mode": "Modo Guardar", - "score": "Puntuación", - "score_breakdown": "Desglose de puntuación", "seconds_from_start": "Segundos desde el inicio", "select_profile": "Seleccionar perfil", "setting": "Ajuste", - "settings_a": "Ajustes A (actual)", - "settings_a_short": "A", "settings_advanced": "Avanzado", - "settings_b": "Ajustes B (prueba)", - "settings_b_short": "B", "settings_basic": "Básico", "settings_detail_level": "Nivel de detalle de ajustes", - "settings_overrides": "Anulaciones de ajustes", "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", - "stage2": "Etapa 2 – similitud principal", - "stage3": "Etapa 3 – DTW", - "stage4": "Etapa 4 – concordancia", "status": "Estado", "tail_trim": "Recorte de fin (s)", "tap_to_dismiss": "toca el gráfico para cerrar", @@ -526,7 +472,6 @@ "ungrouped": "Desagrupados", "unlabelled": "Sin etiquetar", "unlabelled_paren": "(sin etiquetar)", - "warp_path": "Ruta de deformación", "input": "Entrada:", "start": "Inicio", "end": "Fin", @@ -551,9 +496,8 @@ "n_shown": ", {n} mostrados", "cycles_title": "Ciclos ({n})", "pg_detecting": "Detectando", - "pg_best_marker": "Mejor: {v}", "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", + "pg_time_left_model_tip": "Estimado a partir de las fases del programa, no una cuenta atrás fija", "phase": "Fase", "pg_mode_history": "Probar en el historial", "pg_ev_detected": "Detectado", @@ -576,9 +520,6 @@ "pg_alert_unmatched": "Sin coincidencia", "pg_alert_ambiguous": "Coincidencia ambigua", "pg_alert_energy": "Anomalía de energía", - "pg_alert_stress_cap": "Límite de seguridad alcanzado", - "pg_alert_stress_ok": "Terminación en reposo: ciclo detenido", - "pg_alert_stress_warn": "Consumo en reposo por encima del umbral de parada", "pg_newly_correct": "ahora correcto", "pg_regressed": "empeoró", "pg_end_timing_changed": "momento de fin cambiado", @@ -588,12 +529,6 @@ "pg_sweep_param": "Parámetro", "pg_objective": "Objetivo", "pg_steps": "Pasos", - "pg_stress_group": "Prueba de terminación en reposo", - "pg_stress_idle_auto": "Auto", - "pg_stress_idle_w": "Nivel en reposo (W)", - "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_obj_match": "Precisión de coincidencia", "pg_obj_endtiming": "Precisión del momento de fin", "pg_obj_falseend": "Tasa de falsos finales", @@ -602,7 +537,6 @@ "pg_mode_optimize": "Optimizar", "pg_group_matching": "Coincidencia de programa", "pg_zoom_hint": "haz doble clic para restablecer el zoom", - "aria_playground_chart2": "Gráfico interactivo de potencia del ciclo: pasa el cursor para leer el tiempo/la potencia, desplázate para hacer zoom, arrastra para desplazarte", "pg_alert_timeout_end": "Finalizado por tiempo de espera, no por predicción", "pg_alert_indefinite": "Se ejecutaría indefinidamente", "task_pg_history": "Probar con el historial", @@ -614,16 +548,9 @@ "pg_recent_runs": "Ejecuciones recientes", "pg_load_run": "Volver a cargar esta ejecución", "pg_match_confidence": "Confianza de coincidencia", - "pg_envelope_fit": "Ajuste a la envolvente", - "pg_envelope_fit_tip": "Qué tan bien el ciclo se mantiene dentro de la envolvente de potencia guardada de este perfil, una perspectiva distinta a la confianza de coincidencia.", "n_imported_note": ", {n} importados", "enable_online": "Activar funciones en línea", - "store_brand": "Marca del electrodoméstico", - "store_model": "Modelo del electrodoméstico", "anonymous": "Anónimo", - "rate_quality": "Valorar la calidad", - "rate_n": "{n} estrellas", - "approved_only": "Solo aprobados", "adopt_settings": "Adoptar también la configuración", "flags": "Indicadores", "include_phase_map": "Incluir mapa de fases", @@ -640,7 +567,6 @@ "cat_custom_phases": "Fases personalizadas", "cat_profile_groups": "Grupos de perfiles", "cat_settings": "Ajustes de detección y reconocimiento", - "cat_matching_config": "Ajuste del reconocimiento", "cat_ml_models": "Modelos de ML", "cat_feedback": "Comentarios y etiquetas de revisión", "cat_suggestions": "Sugerencias", @@ -662,7 +588,6 @@ "hist_csv_data": "Datos CSV", "hist_from_recorder": "O léalo desde Home Assistant", "hist_since": "Desde", - "days": "días", "hist_keep": "Conservar este ciclo", "hist_looks_complete": "completo", "peak_power_short": "Pico", @@ -682,7 +607,36 @@ "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" + "correct_total_cycles": "Corregir el total", + "hist_reason_already_recorded": "se solapa con un ciclo que WashData ya registró", + "share_consent": "Confirmo que estos ciclos terminaron con normalidad, sin interrupciones", + "show_internal": "Mostrar ajustes internos", + "show_internal_tip": "Ajustes que WashData afina por usted (muestreo, vigilancia, cadencia de coincidencia, umbrales). Se muestran cuando este dispositivo tiene un valor propio, un conflicto o una sugerencia.", + "rate_n": "{n} estrellas", + "rate_quality": "Valorar la calidad", + "pg_autolabel": "Etiquetado automático", + "pg_label_review": "Solicita revisión", + "pg_ev_match_reverted": "Coincidencia revertida", + "pg_ev_verified_pause": "Pausa verificada", + "n_samples": "{n} muestras", + "n_samples_one": "{n} muestra", + "n_samples_many": "{n} muestras", + "n_samples_other": "{n} muestras", + "n_cycles": "{n} ciclos", + "n_cycles_one": "{n} ciclo", + "n_cycles_many": "{n} ciclos", + "n_cycles_other": "{n} ciclos", + "energy_per_cycle": "{v}/ciclo", + "energy_total": "{v} en total", + "automation_off": "(desactivada)", + "ref_cycle_none": "Ninguno", + "share_name": "Mostrar mi nombre en lo que comparto", + "cat_backfill_cycles": "Ciclos encontrados en un historial de potencia importado", + "pg_compare_profile": "Comparar con perfil", + "pg_obj_early": "Tasa de finales anticipados", + "pg_obj_endlag": "Retraso del fin (mediana)", + "pg_obj_split": "Tasa de divisiones", + "pg_sweep_cycles": "Ciclos recientes" }, "log": { "all_levels": "Todos los niveles", @@ -692,11 +646,9 @@ }, "ml": { "builtin_models": "● Uso de modelos integrados", - "fit_fair": "Aceptable", "fit_good": "Bien", "fit_slight": "Leve", "fit_strong": "Fuerte", - "fit_weak": "Débil", "fit_word": "ajuste", "personalized": "● Personalizado para esta máquina", "better_than_baseline": "{pct}% mejor que la estimación de referencia ({metric})", @@ -704,30 +656,41 @@ "trend_declining_tip": "El ajuste de este modelo ha empeorado en las últimas comprobaciones: revisar más ciclos puede ayudarlo a reaprender.", "trend_steady_tip": "El ajuste de este modelo se ha mantenido más o menos estable en las últimas comprobaciones.", "cap_blurb": { - "end": "Saber cuándo un ciclo ha terminado realmente", - "live_match": "Identificar antes el programa en curso", - "quality": "Detectar ciclos mal detectados o dañados", - "remaining_time": "Predecir cuánto tiempo queda", "total_energy": "Predecir la energía total y el coste" }, "cap_label": { - "end": "Detección de fin de ciclo", - "live_match": "Reconocimiento del programa", - "quality": "Comprobación de calidad del ciclo", - "remaining_time": "Estimación del tiempo restante", "total_energy": "Estimación de la energía" }, - "metric_auc": "AUC {auc} en datos de validación", - "metric_mae": "error {model} frente a referencia {baseline}" + "metric_mae": "error {model} frente a referencia {baseline}", + "fine_tuned_at": "ajustado el {when}", + "held_out_count": "medido en {count} ciclos de validación", + "held_out_count_one": "medido en {count} ciclo de validación", + "held_out_count_many": "medido en {count} ciclos de validación", + "held_out_count_other": "medido en {count} ciclos de validación", + "last_run_not_promoted": "Última comprobación ({when}): {reason}", + "never": "nunca", + "skip_reason": { + "fit_failed": "El entrenamiento falló: {error}", + "holdout_too_small": "Muy pocos ciclos para probar un nuevo modelo de forma fiable: se pudieron reservar {held_out} para la prueba y se necesitan {min}.", + "insufficient_rows": "Aún no hay suficientes ciclos utilizables: hasta ahora, {count} ciclos etiquetados y limpios.", + "insufficient_rows_one": "Aún no hay suficientes ciclos utilizables: hasta ahora, {count} ciclo etiquetado y limpio.", + "insufficient_rows_many": "Aún no hay suficientes ciclos utilizables: hasta ahora, {count} ciclos etiquetados y limpios.", + "insufficient_rows_other": "Aún no hay suficientes ciclos utilizables: hasta ahora, {count} ciclos etiquetados y limpios.", + "not_better_than_incumbent": "El nuevo modelo no era mejor que el que está en uso (error {model} frente a {incumbent}), así que se mantuvo el actual.", + "not_better_than_naive": "El nuevo modelo no era claramente mejor que la estimación simple (error {model} frente a {naive}).", + "unknown": "No se aprendió nada nuevo." + }, + "trained_unknown": "en un momento desconocido" }, "modal": { + "remove_task_title": "Eliminar tarea", + "remove_task_msg": "¿Eliminar el recordatorio \"{task}\"? Sus entradas en el registro de mantenimiento se conservan.", "auto_label": "Ciclos de etiquetado automático", "correct_feedback": "Comentarios correctos", "create_profile": "Crear perfil", "cycle": "Ciclo", "edit_group": "Editar grupo de perfiles", "edit_phase": "Editar fase", - "import_config": "Importar configuración", "label_cycle": "Ciclo de etiquetas", "merge_cycles": "Fusionar {n} ciclos", "new_group": "Nuevo grupo de perfiles", @@ -742,6 +705,8 @@ "delete_automation_title": "Eliminar automatización", "delete_automation_msg": "¿Eliminar la automatización \"{name}\" de Home Assistant? Esto no se puede deshacer.", "remove_legacy_title": "Eliminar acciones heredadas", + "reset_phase_msg": "¿Deshacer la edición de \"{name}\" y restaurar la fase incorporada? Los programas conservan sus rangos.", + "reset_phase_title": "Restablecer fase", "remove_legacy_msg": "¿Eliminar las acciones personalizadas heredadas? Dejarán de dispararse en los eventos de ciclo. Esto no se puede deshacer desde el panel.", "discard_recording_title": "Descartar la grabación", "discard_recording_msg": "¿Descartar la grabación guardada? Esto no se puede deshacer.", @@ -762,63 +727,79 @@ "store_share_device": "Compartir este dispositivo", "export_select": "Exportar - elegir datos", "import_wizard": "Importar - elegir datos", - "history_import": "Importar historial de potencia" + "history_import": "Importar historial de potencia", + "apply_suggestions_title": "Aplicar sugerencias", + "apply_suggestions_msg": "¿Cambiar estos {n} ajustes? La integración se recarga después de guardar.", + "discard_settings_title": "¿Descartar los cambios sin guardar?", + "discard_settings_msg": "Cambios de ajustes sin guardar: {n}. ¿Salir de Ajustes y descartarlos?", + "profile_title": "Perfil · {name}", + "undo_import_msg": "Los datos y ajustes de este dispositivo vuelven a como estaban antes de la última importación ({when}). Se pierden los ciclos y cambios posteriores.", + "undo_import_ok": "Deshacer importación", + "undo_import_title": "Deshacer la última importación" }, "msg": { + "custom_task_limit": "Ha alcanzado el límite de {max} tareas.", + "custom_tasks_intro": "Agregue cualquier otra cosa que quiera que se le recuerde. Una tarea queda pendiente cuando se alcanza cualquiera de los dos intervalos; deje uno vacío o en 0 para usar solo el otro.", + "maintenance_due_sensor_hint": "El sensor binario Mantenimiento pendiente está activado mientras haya alguna tarea pendiente, para sus propias automatizaciones de notificación.", + "maintenance_supporter_intro": "Maintenance Supporter es una integración independiente que hace mucho más: cualquier electrodoméstico o tarea, plazos por tiempo, uso o un sensor, notificaciones e historial. Puede usar el contador de ciclos de WashData.", "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.", + "pg_preset_delete_confirm": "¿Eliminar el preajuste \"{name}\"?", + "pg_preset_limit": "Límite de preajustes alcanzado ({n})", + "pg_preset_overwrite": "¿Sobrescribir el preajuste \"{name}\"?", "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.", - "refresh_catalog_hint": "Las listas de marcas y electrodomésticos de la comunidad se guardan en caché para mantener la tienda compartida dentro de su cuota diaria. Actualice para incorporar las entradas añadidas o aprobadas por otros usuarios.", + "store_sibling_hint": "¿No hay nada compartido para su modelo? Un modelo parecido de la misma marca suele ser un buen comienzo.", + "store_declare_appliance": "Indique a WashData qué electrodoméstico tiene para ver las configuraciones que otros han compartido para él. O escriba una marca arriba para echar un vistazo.", + "refresh_catalog_hint": "Las listas de marcas y electrodomésticos de la comunidad se guardan en caché para ahorrar la cuota diaria de la tienda. Actualice para ver las entradas que otros han añadido o aprobado.", "head_trim_hint": "Número de segundos que se eliminarán del inicio", "appliance_monitor": "monitor de electrodomésticos", "artifact_dip_detail": "Cayó por debajo de la banda de potencia habitual durante ~{n}s.", "artifact_footer": "Destacado en el gráfico de arriba. Estos son artefactos transitorios (por ejemplo, la puerta se abrió a mitad del ciclo), no necesariamente problemas.", "artifact_header": "{n} anomalías detectadas durante este ciclo", - "artifact_pause_detail": "La potencia cayó a casi cero durante ~{n}s y luego se reanudó – probablemente la puerta se abrió a mitad del ciclo o el ciclo fue pausado.", + "artifact_header_one": "{n} anomalía detectada durante este ciclo", + "artifact_header_many": "{n} anomalías detectadas durante este ciclo", + "artifact_header_other": "{n} anomalías detectadas durante este ciclo", + "artifact_pause_detail": "La potencia cayó casi a cero durante ~{n}s y luego volvió. Probablemente se abrió la puerta o se pausó el ciclo.", "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 luego quíteles la etiqueta (se conservan en el historial, pero dejan de influir en este perfil) o elimínelos definitivamente.", + "cleanup_intro": "Todos los ciclos etiquetados superpuestos. Marque los atípicos y luego quíteles la etiqueta (se quedan en el historial, pero dejan de dar forma a este perfil) o elimínelos.", "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)", - "compare_profiles_tip": "Superponga otros sobres de perfil en el cuadro anterior para ver cuál se adapta mejor a este ciclo.", + "compare_profiles_tip": "Superponga otros perfiles en el gráfico para ver cuál encaja mejor con este ciclo.", "compare_selected_cycles": "Ciclos seleccionados (sólidos): mostrar/ocultar", - "consider_new_profile": "Considere la posibilidad de crear un nuevo perfil.", - "coverage_gap": "Los ciclos recientes no tienen ningún perfil coincidente ({pct}% de los últimos 30).", - "coverage_gap_similar_cycles": "{count} ciclos similares sin etiquetar encontrados – cree un perfil para comenzar a identificarlos.", "cycles_deleted": "{count} ciclo(s) eliminado(s)", + "cycle_context_loading": "Cargando historial del recorder…", + "cycle_context_note": "Gris: el historial del recorder del sensor de potencia alrededor de este ciclo. No forma parte del ciclo y no afecta a la duración, la energía ni la coincidencia.", + "cycle_context_none": "El recorder no conserva lecturas en torno a este ciclo (Home Assistant guarda 10 días de historial de forma predeterminada).", + "cycle_context_tip": "Solo visualización: muestra el historial del recorder del sensor de potencia alrededor de este ciclo, si aún se conserva. Para añadir intentos de arranque fallidos a futuros ciclos, use Preinicio de la curva (Ajustes > Detección > Inicio del ciclo), que cambia las duraciones guardadas.", "enough_data": "Suficientes datos para aprender ({current}/{min} ciclos).", "export_description": "Elige exactamente qué perfiles, ciclos, ajustes y más exportar a JSON, o analiza un archivo e importa solo las partes que quieras.", "feedback_cycles_pending": "Para revisar: {n}", "feedback_prompt": "Confirme que era correcto, corrija el programa o ignórelo.", "feedback_relabel_hint": "Volver a etiquetar este ciclo también lo resuelve.", "filter_by_profile": "Filtrar por perfil…", - "group_modal_help": "Programas grupales con la misma forma que difieren en temperatura/centrifugado (las duraciones pueden variar). El emparejamiento califica al grupo como un candidato y luego elige al miembro que mejor se adapta. Elija al menos 2; la superposición muestra cuán parecidos son.", + "group_modal_help": "Agrupe programas con la misma forma que difieren en temperatura o centrifugado. La coincidencia puntúa el grupo como uno solo y luego elige el miembro que mejor encaja. Elija al menos 2; la superposición muestra cuánto se parecen.", "group_not_cohesive": "Estos perfiles no son lo suficientemente similares como para agruparlos de manera confiable, por lo que la comparación los trata individualmente hasta que se elimina el valor atípico o se divide el grupo.", "group_preview_hint": "Marque 2 o más miembros para previsualizar y comparar sus curvas de potencia.", - "import_intro": "Cargue un archivo exportado o pegue una carga útil JSON a continuación.", - "legacy_actions_title": "{count} acción personalizada heredada aún en ejecución", + "legacy_actions_title": "{count} acciones personalizadas heredadas aún en ejecución", + "legacy_actions_title_one": "{count} acción personalizada heredada aún en ejecución", + "legacy_actions_title_many": "{count} acciones personalizadas heredadas aún en ejecución", + "legacy_actions_title_other": "{count} acciones personalizadas heredadas aún en ejecución", "live_chart_loading": "El gráfico de potencia en vivo aparece a medida que llegan las lecturas.", "loading": "Cargando…", "loading_curve": "Curva de carga…", "loading_settings": "Cargando configuración…", "log_buffer_hint": "El más nuevo primero · almacena en buffer los últimos 500 registros ha_washdata desde el reinicio · arrastra el borde inferior para cambiar el tamaño.", - "maintenance_advisory": "El aumento de la duración/energía puede indicar que se necesita mantenimiento del aparato (por ejemplo, descalcificación, limpieza del filtro).", "maintenance_due": "Mantenimiento pendiente: {items}", - "maintenance_intro": "Registre el mantenimiento que realiza en este electrodoméstico y reciba un recordatorio cuando cada tarea vuelva a estar pendiente.", + "maintenance_intro": "Registre el mantenimiento de este electrodoméstico y reciba un recordatorio cuando cada tarea vuelva a tocar.", "maintenance_load_error": "No se pudieron cargar los datos de mantenimiento: {error}", "maintenance_requires_access": "El mantenimiento y la exportación/importación requieren acceso completo.", - "manual_duration_hint": "La duración del ciclo promedio/esperada del perfil, utilizada para las estimaciones del tiempo restante. Edite para configurarlo; dejarlo sin cambios mantiene el valor actual.", - "matching_tuning_intro": "Al aprender, WashData también ajusta cuánto pesa la coincidencia de programas entre la forma y la duración y la energía.", + "manual_duration_hint": "La duración esperada del ciclo del perfil, usada para estimar el tiempo restante. Déjela sin cambios para mantener el valor actual.", "merge_intro": "Los ciclos seleccionados se combinan en uno (orden cronológico; los espacios se rellenan con 0 W). Elija el perfil resultante.", "ml_intro": "WashData se envía con modelos inteligentes que funcionan desde el primer momento.", "ml_learned_intro": "Modelos adaptados a esta máquina.", - "ml_loading": "cargando ML...", "ml_settings_intro": "Dos interruptores independientes: uno aplica los modelos mientras se ejecuta un ciclo, el otro permite que WashData los ajuste a su máquina con el tiempo.", - "name_first_program": "Ya tiene suficientes ciclos – nombre su primer programa para empezar a hacer coincidencias.", - "near_duplicate_cluster": "Se detectó un grupo de perfiles casi duplicados. La agrupación permite elegir de manera confiable entre elementos similares (por ejemplo, el mismo programa a diferente temperatura/centrifugado).", "no_cycles_match": "Ningún ciclo coincide con el filtro actual.", "no_cycles_profile": "No hay ciclos para este perfil.", "no_cycles_selected": "Seleccione al menos un ciclo primero.", @@ -843,73 +824,54 @@ "no_suggestions": "No hay sugerencias activas.", "notify_services_hint": "Use los IDs de servicio {entity} (separados por comas para varios). Variables de plantilla: {vars}.", "old_actions_warning": "Configurado con el antiguo editor de acciones (ahora eliminado). Todavía se activan en eventos de ciclo pero ya no se pueden editar aquí. Conviértalos en una automatización normal o elimínelos.", - "onboarding_progress": "{n} / 3 ciclos observados", - "onboarding_watching": "Use su electrodoméstico con normalidad – WashData está observando. Después de 3 ciclos, comenzará la coincidencia de programas.", "pending_feedback": "Comentarios de detección pendientes", "performance_trend": "Tendencia de rendimiento ({n} ciclos)", - "pg_analysis_empty": "Cargue un ciclo para ver el análisis de coincidencia.", "pg_apply_confirm": "¿Aplicar el mejor valor: {label} = {value}?", - "pg_canvas_empty": "Seleccione un ciclo arriba y luego pulse Cargar para ver su curva de potencia.", - "pg_chart_caption": "Tasa de detección/coincidencia según los valores de {param}", "pg_ctrl_intro": "Los valores parten de los ajustes activos de este dispositivo en la integración. Las ediciones permanecen en la Zona de pruebas hasta que las guarde en la integración.", "pg_live_unavailable": "Ajustes activos no disponibles, se muestran los valores predeterminados.", "pg_load_live_confirm": "¿Descartar {n} edición(es) de la Zona de pruebas y volver a cargar los ajustes actuales de la integración?", - "pg_match_rate": "{pct}% de tasa de coincidencia en {total} ciclos", "pg_matches_live": "Coincide con los ajustes activos", "pg_n_changed": "{n} valor(es) distinto(s) de los ajustes activos", "pg_sugg_none": "Todas las sugerencias ya coinciden con los ajustes actuales", - "pg_preset_delete_confirm": "¿Eliminar el preajuste \"{name}\"?", - "pg_preset_limit": "Límite de preajustes alcanzado ({n})", - "pg_preset_overwrite": "¿Sobrescribir el preajuste \"{name}\"?", "pg_publish_one_confirm": "¿Guardar {label} = {value} en los ajustes de este dispositivo?", "pg_restart_note": "Reinicie Home Assistant para habilitar las herramientas de simulación.", - "pg_sim_progress": "{done} / {total} ciclos", - "pg_starting": "Iniciando…", - "pg_stress_synthetic": "la simulación en reposo empieza aquí", - "pg_sweep_intro": "¿Y si {param} fuera diferente? Pruebe {steps} valores en sus últimos {cycles} ciclos para encontrar el ajuste con el que más ciclos se reconocen correctamente.", - "pg_sweep_step": "Paso {done} / {total}", - "pg_verdict_bad": "Necesita atención: muchos ciclos no se detectan.", - "pg_verdict_good": "Bien ajustado: la mayoría de los ciclos se identifican y reconocen correctamente.", - "pg_verdict_ok": "Aceptable: algunos ciclos no se detectaron. Pruebe a bajar el umbral de inicio.", "phase_catalog_intro": "Segmentos de un ciclo con nombre (Prelavado, Calentamiento, Centrifugado…). Asígnalos a un perfil desde su panel de control.", "phase_ranges_intro": "Rangos de fase (minutos desde el inicio del ciclo) superpuestos en la curva promedio. Edite valores para obtener una vista previa en vivo.", - "playground_intro": "Un entorno de pruebas seguro para reproducir ciclos anteriores con distintos ajustes, comparar dos configuraciones en paralelo e inspeccionar cómo se alinea un ciclo con un perfil. Nada de esto cambia sus datos en vivo hasta que lo aplique explícitamente.", - "playground_needs_restart": "Reinicie Home Assistant para habilitar las herramientas de la Zona de pruebas.", - "preferences_admin": "Preferencias, panel y control de acceso", - "preferences_adv": "Preferencias", + "playground_intro": "Pruebe ajustes con los datos reales de sus ciclos. Nada de esto cambia sus ajustes en vivo hasta que lo aplique.", "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), 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.", + "process_history_hint": "Vuelve a ejecutar la coincidencia en todos los ciclos guardados, actualiza las sugerencias, reentrena los modelos ML (si están activados), recalcula el costo con los precios registrados y recalcula la salud de los ciclos. Ejecútelo después de una tanda 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.", - "rbac_hint": "Cuando está desactivado, todos los usuarios de Home Assistant tienen acceso completo (valor predeterminado). Los administradores siempre tienen acceso completo y pueden administrar a todos.", - "recent_logs": "Registros recientes de ha_washdata", + "rbac_hint": "Desactivado: todos los usuarios de Home Assistant tienen acceso completo (predeterminado). Los administradores siempre tienen acceso completo y pueden gestionar a todos.", "recording_in_progress": "Grabación en curso", "reminders_intro": "Muestra un recordatorio en el panel tantos ciclos después del último servicio. Deje en blanco o 0 para desactivar un recordatorio.", "restart_gap_footer": "Resaltado en el gráfico. Faltan datos de potencia para estos intervalos; la correspondencia usó solo lecturas reales.", - "restart_gap_header": "{n} brecha de reinicio de HA durante este ciclo", + "restart_gap_header": "{n} brechas de reinicio de HA durante este ciclo", + "restart_gap_header_one": "{n} brecha de reinicio de HA durante este ciclo", + "restart_gap_header_many": "{n} brechas de reinicio de HA durante este ciclo", + "restart_gap_header_other": "{n} brechas de reinicio de HA durante este ciclo", "restart_gap_item": "{dur} brecha", - "review_confirm_help": "Confirme si este ciclo se detectó correctamente. Sus revisiones entrenan el modelo en su máquina: cuantos más ciclos confirme, mejor será la coincidencia y la puntuación de salud. Un rápido Bueno/Malo es suficiente.", + "review_confirm_help": "¿Se detectó bien este ciclo? Confirmar ciclos mejora la coincidencia para su máquina. Basta con un rápido Bien/Malo.", "review_in_settings": "Revisar en Configuración", "review_notes_placeholder": "Notas (opcional)", "review_notes_tip": "Notas de texto libre para su propia referencia. No se utiliza en emparejamientos ni en entrenamiento.", - "review_profile_tip": "El programa con el que está etiquetado este ciclo. Si el programa detectado automáticamente era incorrecto, corríjalo aquí: el etiquetado enseña la coincidencia para ciclos futuros.", - "review_quality_tip": "Qué limpio es este ciclo. Bueno = un ejemplo de libro de texto de este programa; Malo = detectado pero ruidoso o atípico; Inutilizable = mal detectado (fusionado, truncado o espurio). Determina la puntuación de salud y qué ciclos se permiten para entrenar el modelo.", - "review_recorded_tip": "Marque esto como un ciclo de referencia cuidadosamente seleccionado para su programa: la misma función que un ciclo registrado manualmente. Los ciclos de referencia siempre se mantienen, generan la plantilla coincidente y nunca se eliminan durante la limpieza. (Esta es la bandera \"dorada\"/grabada; ambas son lo mismo).", - "review_tags_tip": "Banderas opcionales que describen lo que salió mal en este ciclo, para que el entrenamiento y la limpieza puedan explicarlo.", + "review_profile_tip": "El programa con el que está etiquetado este ciclo. Si es incorrecto, corríjalo aquí; las etiquetas enseñan a la coincidencia.", + "review_quality_tip": "Lo limpio que es este ciclo. Bien: un ejemplo de manual de este programa. Malo: detectado, pero ruidoso o inusual. Inutilizable: mal detectado (fusionado, cortado o espurio). Afecta a la puntuación de salud y a qué ciclos entrenan el modelo.", + "review_recorded_tip": "Marque este ciclo como referencia elegida a mano para su programa, como un ciclo grabado manualmente. Los ciclos de referencia se conservan siempre, sirven de base a la plantilla de coincidencia y la limpieza nunca los elimina.", + "review_tags_tip": "Marcas opcionales sobre lo que salió mal, para que el entrenamiento y la limpieza lo tengan en cuenta.", "review_to_cycles": "Abrir la cola de revisión de Ciclos", "samples_decimated": "Mostrando {shown} de {total} muestras (reducidas para la visualización; se conservan los picos). Un hueco amplio aquí se debe a la reducción, no a datos faltantes.", - "saving_triggers_reload": "Al guardar se activa una recarga de integración. Las entidades HA pueden aparecer brevemente como no disponibles.", + "saving_triggers_reload": "Al guardar se recarga la integración. Las entidades pueden aparecer brevemente como no disponibles.", "search_placeholder": "Configuración de búsqueda…", "see_recorder": "Vea el widget de grabadora a continuación", "setting_changed": "Cambiado de {old} a {new} el {date}", "settings_basic_note": "Mostrando ajustes esenciales. Cambie a Avanzado para ver la lista completa.", - "shape_drift_advisory": "⚠ Forma a la deriva", - "shape_drift_detail": "El patrón de potencia de este perfil ha cambiado con el tiempo – posible desgaste del electrodoméstico o mantenimiento necesario (p. ej., descalcificación, limpieza del filtro).", "show_all_settings": "Mostrar todas las configuraciones", - "showing_suggestions": "Mostrando configuración {count} con sugerencias.", - "split_intro": "Haga clic en el gráfico para agregar o eliminar un punto de división, o detectar automáticamente por espacios inactivos. Cada segmento resultante puede obtener su propio perfil.", - "storage_diagnostics": "Estadísticas de almacenamiento, mantenimiento, exportación/importación.", + "showing_suggestions": "Mostrando {count} configuraciones con sugerencias.", + "showing_suggestions_one": "Mostrando {count} configuración con sugerencias.", + "showing_suggestions_many": "Mostrando {count} configuraciones con sugerencias.", + "showing_suggestions_other": "Mostrando {count} configuraciones con sugerencias.", + "split_intro": "Haga clic en el gráfico para añadir o quitar un punto de división, o detecte automáticamente por pausas inactivas. Cada parte puede tener su propio perfil.", "sug_staged": "Establecido {key} = {val}. Guarde para aplicar.", "toast_auto_detect_enabled": "Detección automática activada", "toast_auto_label_complete": "Etiquetado automático completado", @@ -926,7 +888,6 @@ "toast_no_automation": "No se puede crear automatización aquí", "toast_profile_name_required": "Se requiere nombre de perfil", "toast_program_set": "Programa establecido: {program}", - "toast_rebuild_failed": "Error al reconstruir: {error}", "toast_resume_failed": "Error al reanudar: {error}", "toast_revert_failed": "Error al revertir: {error}", "toast_reverted": "{key} revertido", @@ -937,97 +898,82 @@ "trend_energy_up": "Tendencia de energía al alza ({pct}%/ciclo): promedio reciente {avg}", "trim_destructive_confirm": "Esto conserva solo el {pct}% del ciclo y no se puede deshacer. ¿Continuar?", "trim_intro": "Arrastre los controles rojos o ingrese valores. Se elimina todo lo que está fuera de la ventana.", - "tuning_suggestions_available": "{count} sugerencia de ajuste disponible de los ciclos observados. Aparecen al lado de los campos relevantes.", + "tuning_suggestions_available": "{count} sugerencias de ajuste disponibles de los ciclos observados. Aparecen al lado de los campos relevantes.", + "tuning_suggestions_available_one": "{count} sugerencia de ajuste disponible de los ciclos observados. Aparece al lado del campo relevante.", + "tuning_suggestions_available_many": "{count} sugerencias de ajuste disponibles de los ciclos observados. Aparecen al lado de los campos relevantes.", + "tuning_suggestions_available_other": "{count} sugerencias de ajuste disponibles de los ciclos observados. Aparecen al lado de los campos relevantes.", "unsure_detected_prefix": "WashData no está seguro de haber detectado", - "updated": "Actualizado", "warmup_badge": "Aún aprendiendo ({done}/{needed} ciclos)", - "warmup_detail": "Este perfil necesita {needed} ciclos etiquetados antes de que comience la coincidencia automática. Cada ciclo confirmado le ayuda a aprender.", + "warmup_detail": "La coincidencia ya funciona. WashData le pide que confirme los primeros {needed} ciclos de este perfil antes de etiquetar por su cuenta.", "wipe_history_warning": "Eliminar permanentemente todos los ciclos y perfiles. No se puede deshacer.", "compare_overlay_tip": "Superponga los sobres de los perfiles aprendidos para ver a qué programa se parece cada ciclo.", - "duration_trend_up_tip": "Duración en aumento ({pct}%/ciclo)", - "duration_trend_down_tip": "Duración en disminución ({pct}%/ciclo)", - "energy_trend_up_tip": "Energía en aumento ({pct}%/ciclo)", - "energy_trend_down_tip": "Energía en disminución ({pct}%/ciclo)", - "performance_trending": "Tendencia del rendimiento", - "pg_select_cycle_hint": "Seleccione un ciclo y haga clic en ↺ para cargar", - "pg_sweep_progress": "Paso {done} / {total}", - "pg_analysis_hint": "Seleccione un ciclo y haga clic en ↺ para cargar el análisis.", "diagnostics_load_failed": "No se pudieron cargar los diagnósticos: {error}", - "manual_duration_ref_hint": "Solo se usa cuando no hay ningún ciclo de referencia seleccionado. Un ciclo de referencia establece la duración a partir de su propia longitud.", + "manual_duration_ref_hint": "Solo se usa sin ciclo de referencia; un ciclo de referencia fija la duración a partir de su propia longitud.", + "duration_learned_hint": "Calculada a partir de los ciclos de este programa (sin los atípicos) y actualizada tras cada ciclo nuevo. Para cambiarla, recorte o elimine los ciclos que no encajen.", "toast_convert_rolled_back": "La conversión falló y se revirtió (no se dejó ninguna automatización): {error}", "toast_convert_orphan": "La automatización se creó, pero no se pudieron borrar las acciones antiguas. No lo vuelva a intentar: elimine las acciones antiguas manualmente para evitar una automatización duplicada.", - "advisory_duration_trend_up": "Los ciclos de '{name}' cada vez duran más (unos +{pct}% por ciclo). Si el comportamiento del electrodoméstico ha cambiado, vuelve a grabar o reconstruye este perfil.", - "advisory_energy_trend_up": "'{name}' consume cada vez más energía (unos +{pct}% por ciclo) - conviene revisar el electrodoméstico si no es lo esperado.", - "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.", + "advisory_duration_trend_up": "Los ciclos de '{name}' duran cada vez más (unos +{pct}% por ciclo). Si el electrodoméstico ha cambiado, vuelva a grabar o reconstruya este perfil.", + "advisory_energy_trend_up": "'{name}' consume más energía en cada ciclo (unos +{pct}% por ciclo). Revise el electrodoméstico si no es lo esperado.", + "advisory_poor_health": "'{name}' encaja mal: sus ciclos recientes varían mucho o coinciden poco. Revise sus ciclos o vuelva a grabarlo para que la coincidencia y las estimaciones sigan siendo precisas.", + "advisory_shape_drift": "'{name}' se ha alejado de su curva de potencia original, quizá por cal o desgaste. Considere volver a grabarlo con ciclos recientes.", + "advisory_shape_drift_corr": "'{name}' se ha alejado de su curva de potencia original (correlación {corr}), quizá por cal o desgaste. Considere volver a grabarlo con ciclos recientes.", + "advisory_unmatchable": "'{name}' nunca podrá coincidir: no tiene detrás ningún ciclo con datos de potencia. Etiquete o grabe un ciclo de este programa para solucionarlo.", + "advisory_unmatchable_no_duration": "'{name}' nunca podrá coincidir: su ciclo no tiene una duración utilizable. Vuelva a grabar este programa o indique su duración esperada.", "pg_no_alerts": "No se detectaron problemas en esta ejecución.", "pg_override_active": "Usando sus ajustes editados vs. los actuales", "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.", "pg_sweep_intro2": "Encuentre el ajuste que mejor cumple un objetivo en sus ciclos recientes. Nada cambia hasta que lo aplique.", - "pg_running_history": "Reproduciendo tus ciclos…", - "pg_running_sweep": "Probando valores en todos tus ciclos…", "pg_row_load_hint": "Cargar este ciclo en el gráfico de arriba", - "pg_history_intro2": "Reproduce tus ciclos recientes a través del detector y el comparador reales con los ajustes de arriba. Haz clic en cualquier fila para cargar ese ciclo en el gráfico; edita un ajuste para ver una comparación antes/después.", + "pg_history_intro2": "Reproduzca ciclos recientes con el detector y el comparador reales usando los ajustes de arriba. Haga clic en una fila para cargarla en el gráfico; cambie un ajuste para comparar antes y después.", "pg_simulating": "Simulando el ciclo…", - "pg_canvas_empty2": "Elige un ciclo de arriba y pulsa Ejecutar para simularlo. Después, pasa el cursor para leer los valores, desplázate para hacer zoom y arrastra para desplazarte.", "pg_apply_settings_confirm": "¿Guardar estos {n} ajuste(s) en este dispositivo? {list}", - "pg_stress_terminated_detail": "Se estabilizó en ~{idle}W en reposo -- el ciclo terminó {h}h {m}m después mediante {reason}.", - "pg_stress_above_threshold_detail": "El consumo en reposo ~{idle}W es igual o superior al umbral de parada efectivo ({stop}W) -- el ciclo nunca se consideró inactivo. Aumente stop_threshold_w para corregirlo.", - "pg_stress_hit_cap_detail": "El ciclo duró {h}h {m}m sin detenerse -- detenido a la fuerza por el límite de seguridad. Consumo en reposo {idle}W frente al umbral de parada {stop}W.", "no_logs_match": "Ningún registro coincide con estos filtros.", "pg_analysis_hint2": "Seleccione un ciclo y pulse Ejecutar para cargar el análisis de coincidencia.", - "pg_analysis_empty2": "Pulse Ejecutar para ver el análisis de coincidencia.", - "imported_readonly": "Importado de la tienda de la comunidad. Se muestra como referencia y para la coincidencia. No cuenta en sus estadísticas y no se puede editar.", + "imported_readonly": "Importado de la tienda de la comunidad. Se usa como referencia y para la coincidencia, no cuenta en sus estadísticas y no se puede editar.", "merge_no_imports": "Las grabaciones importadas no se pueden combinar con un ciclo real. Deselecciónelas para combinar.", - "store_enable_hint": "Active las funciones en línea en Ajustes para explorar e importar ciclos de referencia de la comunidad.", - "online_intro": "Comparta y descargue ciclos de referencia con otros usuarios de WashData. Todas las funciones en línea son opcionales y están desactivadas de forma predeterminada.", + "store_enable_hint": "Active las funciones en línea en Preferencias > En línea y comunidad para explorar e importar ciclos de referencia de la comunidad.", "store_share_intro": "Suba este ciclo de referencia para que otras personas con el mismo electrodoméstico puedan usarlo. Se revisa antes de aparecer públicamente.", - "online_intro_global": "Explore y comparta grabaciones de referencia con otros usuarios de WashData, y confirme entradas de electrodomésticos. Una sola conexión se aplica a toda su integración de WashData; la marca y el modelo del electrodoméstico se configuran por dispositivo en Básico. Todas las funciones en línea son opcionales y están desactivadas de forma predeterminada.", + "online_intro_global": "Explore y comparta grabaciones de referencia con otros usuarios de WashData. Una conexión sirve para todos sus dispositivos WashData; la marca y el modelo se fijan por dispositivo en Básico. Las funciones en línea están desactivadas hasta que las active.", "online_unavailable": "Las funciones en línea no están disponibles en este servidor.", "store_picker_offline": "Active las funciones en línea en el icono de ajustes para elegir del catálogo de la comunidad.", "pick_brand_first": "Elija primero una marca de electrodoméstico.", - "model_not_found": "Todavía no está en el catálogo.", - "connect_to_confirm": "Conéctese en el icono de ajustes para confirmar o valorar.", "adopt_settings_hint": "Sobrescribir los umbrales de detección y reconocimiento de este dispositivo con los compartidos. Tus notificaciones, entidades y precio de energía no se modifican nunca.", - "include_settings_hint": "Comparte los umbrales de reconocimiento y coincidencia de este dispositivo (no tus notificaciones, entidades ni precio de energía). Los que adopten la configuración eligen si aplicarlos.", + "include_settings_hint": "Comparte los umbrales de detección y coincidencia de este dispositivo (no las notificaciones, entidades ni precio de la energía). Los demás deciden si los aplican.", "onboard_download": "¿Dispositivo nuevo? Adopta una configuración lista para usar (programas, ciclos de referencia y fases) de otro usuario de WashData con el mismo electrodoméstico.", - "share_consent": "Confirmo que estos ciclos se completaron con normalidad sin interrupciones", "share_device_none": "Aún no hay ciclos para compartir. Primero marca un ciclo grabado o seleccionado manualmente como ciclo de referencia (⭐) en la pestaña Ciclos.", "share_guideline_naming": "Nombra cada perfil exactamente como aparece en el selector o pantalla del electrodoméstico (p. ej. 'Cotton 40', 'Eco 60').", - "share_guideline_quality": "Comparte solo ciclos que se completaron con normalidad -- sin interrupciones a mitad de ciclo, eventos de puerta abierta ni cortes de corriente.", + "share_guideline_quality": "Comparta solo ciclos que terminaron con normalidad: sin interrupciones, aperturas de puerta ni cortes de corriente.", "share_guideline_review": "Tu subida empieza como pendiente y aparece públicamente cuando suficientes miembros de la comunidad la confirman.", "share_guidelines_title": "Antes de compartir", - "store_download_device_intro": "Adopta todos los programas compartidos y sus ciclos de referencia en tu dispositivo. Tus propios ciclos grabados y estadísticas no se ven afectados.", + "store_download_device_intro": "Añade todos los programas compartidos y sus ciclos de referencia a su dispositivo. Sus propios ciclos y estadísticas no se ven afectados.", "store_share_device_intro": "Sube {brand} {model} con los ciclos de referencia que selecciones. Otros con el mismo electrodoméstico podrán adoptar tus programas. Las entradas se revisan antes de aparecer públicamente.", "share_profile_no_cycles": "Sin ciclos de referencia -- marca un ciclo como ⭐ en la pestaña Ciclos para incluir este perfil", - "advisory_phase_inconsistent": "'{name}' parece mezclar diferentes programas o temperaturas - sus ciclos calientan durante tiempos muy distintos. Dividirlo en perfiles separados (p. ej. por temperatura) mejorará la coincidencia y las estimaciones de tiempo.", - "advisory_phase_inconsistent_title": "⚠ Posibles programas mezclados", - "export_select_intro": "Marca exactamente qué incluir. Seleccionar perfiles sin sus ciclos igualmente exporta un programa reconocible (su forma aprendida viaja con él).", - "import_analyze_hint": "Carga un archivo exportado (o pega su JSON). WashData lo analiza y muestra exactamente qué se puede importar antes de que nada cambie.", + "export_select_intro": "Marque lo que quiere incluir. Los perfiles sin sus ciclos exportan igualmente un programa reconocible.", + "import_analyze_hint": "Cargue un archivo exportado (o pegue su JSON). WashData muestra qué se puede importar antes de cambiar nada.", "analyzing": "Analizando…", - "device_type_mismatch_warn": "Esta exportación es de otro tipo de electrodoméstico ({src} en lugar de {local}). Los programas y ciclos aún pueden importarse como datos de referencia, pero los ajustes específicos del dispositivo y la importación al historial real están deshabilitados.", + "device_type_mismatch_warn": "Esta exportación es de otro tipo de electrodoméstico ({src} en lugar de {local}). Los programas y ciclos aún pueden importarse como datos de referencia, pero no los ajustes del dispositivo ni el historial real.", "merge_hint": "Los elementos importados se añaden; no se pierde nada local. Los conflictos de nombres se resuelven abajo.", "replace_warn": "Cada categoría marcada se borra y se reemplaza con el contenido del archivo. Las categorías sin marcar quedan intactas.", "dest_reference_hint": "Los ciclos importados solo mejoran el reconocimiento de programas y nunca afectan a las estadísticas de uso/energía.", - "dest_real_history_hint": "Los ciclos importados cuentan como el historial propio de este dispositivo y alimentan las estadísticas de energía/uso. Úsalo para mover un electrodoméstico a una instalación nueva.", + "dest_real_history_hint": "Los ciclos importados cuentan como historial y estadísticas propios de este dispositivo. Úselo para trasladar un electrodoméstico a una instalación nueva.", "sug_muted": "Este ajuste no volverá a sugerirse", "sug_mute_failed": "No se pudo silenciar la sugerencia", "sug_unmuted_all": "Sugerencias silenciadas restablecidas", - "n_suggestions_muted": "{count} silenciadas; el ajustador automático no propondrá estas.", + "n_suggestions_muted": "{count} sugerencias silenciadas; el ajustador automático no las propondrá.", + "n_suggestions_muted_one": "{count} sugerencia silenciada; el ajustador automático no la propondrá.", + "n_suggestions_muted_many": "{count} sugerencias silenciadas; el ajustador automático no las propondrá.", + "n_suggestions_muted_other": "{count} sugerencias silenciadas; el ajustador automático no las propondrá.", "font_size_hint": "Aumenta o reduce el tamaño de todo en este panel. Se aplica a tu cuenta en este dispositivo.", - "import_history_description": "¿Ya tenía un enchufe inteligente antes de WashData? Cargue una exportación del historial de su sensor de potencia, o léalo directamente desde Home Assistant: la detección normal se ejecuta sobre esos datos, así que los ciclos pasados aparecen en su lista de Ciclos listos para nombrar.", - "hist_input_hint": "Cargue un CSV descargado del panel Historial (entidad, estado, último cambio), o deje que WashData lea directamente el historial del sensor. Después la detección se ejecuta sobre él igual que en directo, y usted elige cuáles de los ciclos encontrados conservar.", - "hist_recorder_hint": "Lee desde la fecha que elija hasta ahora. Home Assistant guarda el historial detallado durante 10 días de forma predeterminada y, a partir de ahí, solo promedios por hora, demasiado imprecisos para detectar ciclos - elija una fecha más antigua solo si el recorder de Home Assistant está configurado para conservar más.", - "hist_scanning": "Se está reproduciendo su historial a través del detector. Esto se ejecuta en segundo plano: puede cerrar este diálogo y volver más tarde.", + "import_history_description": "¿Tenía un enchufe inteligente antes de WashData? Suba su historial de potencia o léalo desde Home Assistant. La detección lo recorre y los ciclos pasados aparecen en su lista de Ciclos, listos para ponerles nombre.", + "hist_input_hint": "Suba un CSV del panel Historial (entidad, estado, último cambio), o deje que WashData lea directamente el historial del sensor. La detección en vivo lo recorre y usted elige qué ciclos conservar.", + "hist_recorder_hint": "Lee desde la fecha elegida hasta ahora. Home Assistant guarda historial detallado durante 10 días por defecto; los promedios por hora más antiguos son demasiado imprecisos para detectar ciclos.", + "hist_scanning": "Su historial pasa por el detector en segundo plano. Puede cerrar este diálogo y volver más tarde.", "hist_imported_count": "{n} ciclos importados.", "hist_duplicates": "{n} ya se habían importado y se omitieron.", "hist_capped": "Se alcanzó el límite de ciclos importados por dispositivo; el resto no se guardó.", - "hist_next_step": "Están en su lista de Ciclos, marcados como historial importado. Abra uno y use Etiquetar para indicar el programa al que pertenece.", + "hist_next_step": "Están en su lista de Ciclos, marcados como historial importado. Abra uno y use Etiquetar para poner nombre a su programa.", "hist_rows_read": "{n} lecturas leídas", "hist_breaks": "{n} huecos en los que el sensor no estaba disponible", "hist_other_entity": "{n} lecturas de otras entidades ignoradas", @@ -1039,60 +985,93 @@ "hist_scan_capped": "Solo se muestran los primeros candidatos (se encontraron {n}).", "hist_recorder_empty": "Home Assistant no tiene historial detallado de este sensor en ese periodo.", "hist_scan_failed": "El análisis falló.", - "hist_scan_expired": "Ese análisis ya no está disponible. Vuelva a analizar el historial.", + "hist_scan_expired": "Ese análisis ya no está disponible. Vuelva a analizar.", "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.", - "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.", + "imported_history_readonly": "Encontrado en un historial de potencia importado. Ayuda a la coincidencia, pero no cuenta en sus estadísticas y no se puede recortar ni dividir. Etiquételo para poner nombre al programa.", + "odometer_intro": "Este total solo sube. Eliminar un ciclo no lo reduce, así que los recordatorios de servicio siguen siendo correctos.", + "odometer_correct_hint": "Ajuste este valor si WashData contó un ciclo que nunca ocurrió, o si el electrodoméstico ya hizo 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}", "standby_above_stop_title": "Sigue consumiendo {idle} W cuando el ciclo ha terminado", "standby_above_stop_sub": "{n} de los últimos {total} ciclos. El Retardo de apagado solo empieza cuando la potencia baja del Umbral de parada ({stop} W), así que los ciclos terminan tarde o hay que detenerlos a mano.", - "advisory_duration_outlier": "{n} ciclo(s) etiquetados como '{name}' se alejan demasiado de su duración habitual para poder coincidir alguna vez con él (el peor: {ratio}x). Probablemente estén mal etiquetados, o una grabación captó dos ejecuciones. Vuelve a etiquetarlos o divídelos para que dejen de distorsionar la estimación de tiempo de este programa.", + "advisory_duration_outlier": "{n} ciclo(s) etiquetados como '{name}' se alejan demasiado de su duración habitual para llegar a coincidir (el peor: {ratio}x). Probablemente estén mal etiquetados o contengan dos ciclos. Vuelva a etiquetarlos o divídalos para que dejen de distorsionar su estimación de tiempo.", "toast_range_required": "Establezca un rango Desde/Hasta válido", - "no_profiles_option": "(aún no hay perfiles, añade uno)" + "no_profiles_option": "(aún no hay perfiles, añade uno)", + "compare_cycles_title": "Comparar {count} ciclos", + "edit_builtin_phase": "Esta es una fase integrada. Al guardar se crea una copia personalizada; elimine la copia para restaurar el original.", + "fetch_error": "No se pudieron cargar los datos.", + "connection_lost": "Conexión perdida - mostrando datos de las {time}.", + "pg_notify_partial": "Los marcadores solo muestran notificaciones de inicio, casi terminado, fin, hitos y horas de silencio. No se simulan las actualizaciones en vivo, los recordatorios de descarga ni las alertas de exceso de tiempo.", + "connect_to_confirm": "Conéctese en el icono de ajustes para confirmar o valorar.", + "coverage_gap_cluster": "{count} ciclos recientes sin etiquetar de unos {min} min parecen un programa que aún no ha creado.", + "pg_label_yes": "Se etiquetaría como \"{name}\" al final del ciclo", + "pg_label_no": "Solicitaría revisión: {reason}", + "live_match_debug_tip": "Confianza: qué tan bien coincide la curva de potencia actual con el perfil candidato principal (0-100%). Ambiguo: los dos mejores candidatos puntúan a menos de un 5% entre sí; la etiqueta es incierta hasta que termine el ciclo.", + "automation_alias_started": "{device}: ciclo iniciado", + "automation_alias_finished": "{device}: ciclo terminado", + "automation_desc_started": "Se ejecuta cuando empieza un ciclo de {device}. Añada sus acciones (notificar, luces, ...).", + "automation_desc_finished": "Se ejecuta cuando termina un ciclo de {device}. Añada sus acciones (notificar, luces, ...).", + "feedback_detected": "WashData detectó: {name}", + "import_undo_hint": "Se conserva un punto de restauración de antes de la última importación ({when}): {profiles} perfil(es) y {cycles} ciclo(s).", + "replace_undo_hint": "Primero se guarda un punto de restauración, así que puedes deshacer esta importación desde Exportar / Importar.", + "pg_in_sample_note": "Estos ciclos ayudaron a construir los perfiles con los que se comparan, así que los resultados son optimistas: espere algunas coincidencias correctas menos en ciclos nuevos.", + "hist_tail_trimmed": "Se recortaron {min} min de espera tras el fin del ciclo, como hace la integración en vivo", + "invalid_threshold_pair": "El Umbral de parada ({stop} W) debe ser inferior al Umbral de inicio ({start} W). No se ha guardado nada; cambie ambos a la vez.", + "hist_truncated": "se detuvo en el límite de {n} lecturas, así que el resto del historial no se analizó", + "hist_warn_kw": "La lectura más alta es solo {w}. Parece que este sensor informa en kilovatios; WashData necesita vatios, así que no se puede detectar ningún ciclo.", + "hist_warn_naive_time": "{n} marcas de tiempo no tienen zona horaria y se leyeron como UTC. Si el archivo usa hora local, los ciclos quedan desplazados según su diferencia con UTC y pueden perderse lecturas cerca de un cambio de horario de verano.", + "hist_warn_naive_time_one": "{n} marca de tiempo no tiene zona horaria y se leyó como UTC. Si el archivo usa hora local, los ciclos quedan desplazados según su diferencia con UTC y pueden perderse lecturas cerca de un cambio de horario de verano.", + "hist_warn_naive_time_many": "{n} marcas de tiempo no tienen zona horaria y se leyeron como UTC. Si el archivo usa hora local, los ciclos quedan desplazados según su diferencia con UTC y pueden perderse lecturas cerca de un cambio de horario de verano.", + "hist_warn_naive_time_other": "{n} marcas de tiempo no tienen zona horaria y se leyeron como UTC. Si el archivo usa hora local, los ciclos quedan desplazados según su diferencia con UTC y pueden perderse lecturas cerca de un cambio de horario de verano.", + "pg_compare_profile_tip": "Solo elige el perfil que se dibuja sobre el gráfico. La simulación siempre busca por sí misma la coincidencia del ciclo, como hace la integración en vivo.", + "pg_outcome_stale": "Los ajustes cambiaron desde que se ejecutó esta simulación. Todavía muestra el resultado anterior.", + "pg_sweep_guarded": "Termina más ciclos antes de tiempo, divide más ciclos o pasa por alto ciclos que el ajuste actual detecta, así que este valor nunca se recomienda.", + "pg_sweep_keep_current": "Mantenga el valor actual ({value}, {metric}): ningún valor fue mejor por al menos un ciclo.", + "pg_sweep_n_cycles": "en {count} ciclos", + "pg_sweep_n_cycles_one": "en {count} ciclo", + "pg_sweep_n_cycles_many": "en {count} ciclos", + "pg_sweep_n_cycles_other": "en {count} ciclos", + "pg_task_busy": "Ya hay una ejecución de Probar en el historial u Optimizar en curso para este dispositivo. Espere a que termine o cancélela." }, "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", + "smart_termination_duration_ratio": "Fracción de la duración media del programa que un ciclo debe alcanzar antes de que la Terminación inteligente pueda finalizarlo antes de tiempo; redúzcala si la duración varía", + "anti_crease_finalize_ratio": "Fracción de la duración media del programa que un ciclo debe alcanzar antes de que la cola antiarrugas pueda terminarlo; redúzcala en secadoras, 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_exit_power": "Nivel que cuenta como silencio entre pulsos de volteo; solo actúa por encima del Umbral de parada", "anti_wrinkle_idle_timeout": "Tiempo de silencio permitido entre dos pulsos de volteo antes de que finalice el modo antiarrugas", "anti_wrinkle_max_duration": "Un pulso más largo que esto finaliza el modo antiarrugas y abre un nuevo ciclo", "anti_wrinkle_max_power": "Un pulso por encima de esto finaliza el modo antiarrugas y abre un nuevo ciclo", "completion_min_seconds": "Ejecución más corta que cuenta como ciclo real", - "end_repeat_count": "Lecturas bajas consecutivas antes de terminar", "interrupted_min_seconds": "Ciclos cortos marcados como interrumpidos", "min_off_gap": "Intervalo necesario para separar dos ciclos", "off_delay": "Segundos de baja potencia antes de que termine el ciclo", "start_duration_threshold": "Segundos por encima del umbral para confirmar el inicio", "start_threshold_w": "Vatios mínimos para contar como iniciado", "stop_threshold_w": "Por debajo de esto, la máquina cuenta como apagada", - "dtw_bandwidth": "Etapa 3: banda de deformación Sakoe-Chiba (0 = DTW desactivado; predeterminado 0,2 = 20% de la longitud del ciclo)", - "corr_weight": "Etapa 2: equilibrio entre la forma de la curva (correlación) y el nivel de potencia (MAE); predeterminado 0,45", - "duration_weight": "Etapa 4: cuánto influye la concordancia de duración en la puntuación final (predeterminado 0,22)", - "energy_weight": "Etapa 4: cuánto influye la concordancia de energía en la puntuación final (predeterminado 0,22)", - "profile_match_min_duration_ratio": "Etapa 1: duración mínima del ciclo como fracción de la duración del perfil; predeterminado 0,1 (10%)", - "profile_match_max_duration_ratio": "Etapa 1: duración máxima del ciclo como fracción de la duración del perfil; predeterminado 1,8 (180%)", - "keep_min_score": "Etapa 2: puntuación mínima para seguir en la carrera; el valor predeterminado 0,1 admite coincidencias débiles, las etapas posteriores seleccionan la mejor", - "dtw_blend": "Etapa 3: 0 = solo puntuación central, 1 = solo puntuación DTW, 0,5 = mezcla igual (predeterminado)", - "dtw_ensemble_w": "Etapa 3 (modo ensemble): peso en L1 escalado frente a DTW derivado; el predeterminado 0,7 favorece la sensibilidad al nivel", - "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)", + "profile_match_min_duration_ratio": "Duración mínima del ciclo como fracción de la duración del perfil; predeterminado 0,1 (10%)", + "profile_match_max_duration_ratio": "Duración máxima del ciclo como fracción de la duración del perfil; predeterminado 1,8 (180%)", "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.", "anti_wrinkle": "Volteos periódicos tras el ciclo para evitar que la ropa se arrugue.", + "baking": "Resistencia a alta temperatura activa para formar la corteza y la miga.", + "cooking": "Fase principal de cocción con resistencia y ventilador activos.", "cool_down": "Girar sin calor cerca del final del ciclo.", + "cool_down_af": "Enfriamiento solo con ventilador tras el calentamiento.", "drain_and_switch": "Periodo de transición del modo lavado al secado.", "dry": "Etapa de secado mediante calentador y/o calor residual.", "drying": "Período principal de volteo calentado.", "heat_up": "Calentamiento inicial del calentador antes de que comience el secado completo.", + "keep_warm": "Fase de calor bajo para mantener la comida caliente.", + "keep_warm_bm": "Fase de calor bajo para mantener el pan caliente tras el horneado.", + "kneading": "El motor mezcla y amasa la masa. Consumo elevado.", + "pause_af": "Pausa breve para agitar o revisar la comida.", + "pre_heat": "Calentamiento inicial de la cámara antes de la cocción completa.", "pre_rinse": "Pulverización inicial antes del lavado con detergente.", + "proving": "Levado con calor bajo para que la levadura fermente y la masa crezca.", + "resting": "Pausa breve de bajo consumo entre amasados para que el gluten se relaje.", "pre_wash": "Remojo inicial o pretratamiento antes del lavado principal.", "rinse": "Etapa de enjuague con agua limpia. Esta fase puede repetirse varias veces.", "sanitize": "Etapa de limpieza a alta temperatura para programas de sanitización.", @@ -1105,6 +1084,7 @@ "wash_dw": "Lavado con detergente principal con calefacción." }, "placeholder": { + "custom_task": "p. ej., limpiar la junta de la puerta", "add": "añadir…", "group_name": "ej. Algodón 2:47", "json_buttons": "[{\"action\":\"ID\",\"title\":\"Label\"}]", @@ -1132,10 +1112,10 @@ "section": { "dishwasher": { "label": "Lavavajillas", - "intro": "Gestión del final del ciclo para lavavajillas, que suelen terminar con una larga fase de secado casi silenciosa antes de un breve vaciado final." + "intro": "Gestión del final para lavavajillas, que a menudo secan en silencio durante mucho tiempo antes de un breve vaciado final." }, "anti_wrinkle": { - "intro": "El modo antiarrugas/antipliegues detecta pulsos de volteo de baja potencia después de la fase principal y los mantiene unidos al ciclo terminado.", + "intro": "Mantiene los pulsos de volteo de baja potencia de una secadora tras la fase principal dentro del ciclo terminado.", "label": "Antiarrugas" }, "basic": { @@ -1143,7 +1123,7 @@ "label": "Básico" }, "delay": { - "intro": "La detección de inicio diferido identifica cuándo el electrodoméstico está encendido pero aún no ha iniciado su ciclo.", + "intro": "Detecta un electrodoméstico encendido que espera un inicio diferido.", "label": "Inicio diferido" }, "detection": { @@ -1167,161 +1147,153 @@ "label": "Temporización y vigilancia" }, "triggers": { - "intro": "Señales externas opcionales: un disparador de fin, un sensor de puerta, un interruptor de pausa y el recordatorio de descarga.", + "intro": "Extras opcionales: disparador de fin, sensor de puerta, interruptor de pausa y recordatorio de descarga.", "label": "Disparadores y puertas" - }, - "phase_eta": { - "label": "Tiempo restante", - "intro": "Tiempo restante según las fases, para máquinas cuya duración de ciclo depende de la temperatura o el centrifugado." } }, "setting": { "dishwasher_end_spike_quiet_release": { "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." + "doc": "Tras la duración esperada: cuánto tiempo debe quedarse el lavavajillas por debajo del Umbral de parada antes de que WashData deje de esperar el vaciado final. Súbalo si se pierde un vaciado tardío después de un secado largo. La espera nunca supera los 30 minutos." }, "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." + "doc": "Fracción de la duración media del programa que un ciclo debe alcanzar antes de que su volteo de baja potencia cuente como cola antiarrugas. Redúzcala (p. ej. 0,75) en secadoras con sensor de carga. Deje las lavadoras en el valor predeterminado (0,98), o un lavado puede terminar a la mitad." }, "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.", + "doc": "Mantener los pulsos cortos de volteo de baja potencia de una secadora tras la fase principal de calor dentro del ciclo terminado, en lugar de leerlos como ciclos nuevos.", "label": "Habilitar detección antiarrugas" }, "anti_wrinkle_exit_power": { - "doc": "La energía debe caer por debajo de esto entre pulsos para que el modo antiarrugas permanezca activo.", + "doc": "En modo antiarrugas, una potencia por debajo de este valor cuenta como silencio entre pulsos de volteo, y un silencio más largo que el Intervalo máximo entre pulsos termina el modo. Si el Umbral de parada es más alto, se usa ese, así que esto solo cambia algo si está por encima del Umbral de parada.", "label": "Umbral de potencia de salida" }, "anti_wrinkle_idle_timeout": { - "doc": "Cuánto tiempo puede permanecer en silencio la máquina entre dos pulsos de volteo antes de que finalice el modo antiarrugas. Ajústelo por encima del intervalo más largo que su secadora deja entre pulsos; de lo contrario, cada pulso posterior se lee como un falso inicio.", + "doc": "Cuánto tiempo puede estar en silencio la secadora entre pulsos de volteo antes de que termine el modo antiarrugas. Ajústelo por encima de la pausa más larga de su secadora, o los pulsos posteriores se leerán como falsos arranques.", "label": "Intervalo máximo entre pulsos" }, "anti_wrinkle_max_duration": { - "doc": "Los pulsos más largos se tratan como un ciclo real en lugar de una caída antiarrugas.", + "doc": "Un pulso más largo que esto cuenta como un ciclo real, no como volteo antiarrugas.", "label": "Duración máxima" }, "anti_wrinkle_max_power": { - "doc": "Un pulso por encima de esta potencia se trata como un verdadero ciclo nuevo, no como una caída antiarrugas. Establezca justo encima de la potencia del pulso de caída.", + "doc": "Un pulso por encima de esta potencia cuenta como un ciclo nuevo real, no como volteo. Ajústelo justo por encima de la potencia de sus pulsos de volteo.", "label": "Potencia máxima antiarrugas" }, "auto_label_confidence": { - "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.", + "doc": "Puntuación de coincidencia al final del ciclo a partir de la cual el programa se etiqueta sin preguntar. Súbala para tener más certeza; bájela para etiquetar más ciclos automáticamente. Manténgala por encima de la Confianza de aprendizaje.", "label": "Confianza de etiquetado automático" }, "auto_maintenance": { - "doc": "Ejecute la limpieza nocturna: reconstruya los sobres del perfil, vuelva a calcular el estado del ciclo, elimine los rastros de depuración y conserve los ciclos más recientes.", + "doc": "Limpieza nocturna: etiqueta ciclos antiguos que ahora coinciden claramente con un programa, reconstruye las curvas de los programas, actualiza las marcas de anomalía y la salud de los ciclos, y elimina las trazas de depuración si Guardar trazas de depuración está desactivado. Los ciclos y sus trazas de potencia se conservan siempre.", "label": "Mantenimiento automático (limpieza nocturna)" }, "completion_min_seconds": { - "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).", + "doc": "Los ciclos más cortos que esto (pruebas, abrir la puerta para añadir un calcetín) se guardan como Interrumpido y no reciben notificación de fin.", "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." + "doc": "Añade al principio de un ciclo las lecturas de intentos de arranque fallidos, para máquinas que tantean antes de arrancar (bloqueo de puerta, primer llenado). 300 s bastan en la mayoría; 0 = desactivado. Los inicios registrados se adelantan, así que las duraciones aprendidas se desvían durante un tiempo." }, "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.", + "doc": "La potencia debe quedarse en la banda de espera este tiempo antes de que el electrodoméstico cuente como a la espera de empezar.", "label": "Segundos de confirmación" }, "delay_start_detect_enabled": { - "doc": "Detecta cuando el electrodoméstico está encendido y en espera (inicio retrasado/en espera) pero no ha comenzado su ciclo, para que el consumo en espera no se confunda con un ciclo en ejecución.", + "doc": "Detectar un electrodoméstico encendido que espera un inicio diferido, para que su consumo en espera no se tome por un ciclo en marcha.", "label": "Habilitar detección de inicio diferido" }, "delay_timeout_hours": { - "doc": "Deje de esperar en modo de inicio retrasado después de tantas horas y vuelva al estado inactivo, de modo que una máquina que se deja encendida pero nunca se enciende no espera para siempre.", + "doc": "Dejar de esperar un inicio diferido tras este número de horas y volver a inactivo.", "label": "Horas de tiempo de espera" }, "device_type": { - "doc": "Clase de electrodomésticos. Establece valores predeterminados de detección sensibles (umbrales, retardo de apagado, manejo final) ajustados para ese tipo de dispositivo; cámbielo solo si el dispositivo se configuró originalmente como del tipo incorrecto.", + "doc": "Define los valores de detección predeterminados (umbrales, retardo de apagado, gestión del final) para este tipo de electrodoméstico. Cámbielo solo si el dispositivo se configuró con un tipo equivocado.", "label": "Tipo de dispositivo" }, "door_sensor_entity": { - "doc": "Sensor binario de puerta opcional. Se utiliza para detectar cuando el aparato ha sido abierto/descargado después de un ciclo.", + "doc": "Sensor de puerta opcional. Muestra cuándo se abrió el electrodoméstico para descargarlo tras un ciclo.", "label": "Entidad de sensor de puerta" }, "duration_tolerance": { - "doc": "Tolerancia a las estimaciones de tiempo restante (retroalimentación de aprendizaje, no coincidencia). Si la duración real está dentro de +/-X% de la estimación, se considera una buena coincidencia.", + "doc": "Solo para el aprendizaje: un ciclo dentro de +/-X% de su duración estimada cuenta como una buena estimación. No afecta a la coincidencia.", "label": "Tolerancia de estimación" }, "enable_ml_models": { - "doc": "Mientras se ejecuta un ciclo, deje que los modelos refinen los resultados en vivo: una estimación de tiempo restante y energía/costo más estable, y una protección contra paradas prematuras en la detección final (solo puede retrasar una finalización, nunca finalizar una antes de tiempo y está limitada). Utiliza sus modelos ajustados cuando estén disponibles; de lo contrario, los integrados. Apagado = solo la lógica clásica basada en energía (aún confiable).", + "doc": "Usar un modelo entrenado en esta máquina para predecir la energía y el costo de un ciclo en marcha, y dejar que un programa bien conocido que se detiene inusualmente pronto termine sin todo el retardo de apagado. Desactivado: solo lógica basada en la potencia.", "label": "Aplicar modelos inteligentes durante un ciclo" }, "end_energy_threshold": { - "doc": "Durante la cuenta atrás del retardo de apagado, la energía acumulada (vatios x tiempo) se compara con este umbral. Si se excede, la cuenta regresiva se reinicia, manteniendo las secadoras antiarrugas y las colas de secado en lavavajillas adheridas al ciclo en lugar de acortarlas. Auméntelo si los ciclos terminan demasiado pronto durante el enfriamiento; bájelo si la detección es lenta.", + "doc": "Energía permitida durante el retardo de apagado. Si se supera, el ciclo continúa, de modo que los volteos y las colas de secado siguen formando parte de él. Bájela si los ciclos terminan durante una cola; súbala si los ciclos terminados siguen esperando.", "label": "Energía de fin" }, - "end_repeat_count": { - "doc": "Número de lecturas consecutivas por debajo del umbral requerido antes de que finalice el ciclo. 1 está bien para la mayoría de los enchufes. Aumente a 2-3 si su enchufe inteligente ocasionalmente informa una muestra de cero falso a mitad del ciclo y sus ciclos terminan prematuramente.", - "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). 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ó.", + "doc": "Sensor con el precio actual por kWh (p. ej. una tarifa dinámica). Tiene prioridad sobre el precio fijo de abajo, así que debe ser un precio, no el sensor de potencia o de energía del enchufe.", "label": "Entidad de precio de energía" }, "energy_price_static": { - "doc": "Precio fijo por kWh utilizado para cifras de costos cuando no se establece ninguna entidad de precio en vivo arriba.", + "doc": "Precio fijo por kWh, usado cuando no hay ninguna entidad de precio configurada.", "label": "Precio de energía estático (por kWh)" }, "energy_sensor": { - "doc": "Contador de energía acumulada opcional (total_increasing en kWh/Wh, p. ej. el propio medidor de consumo total del enchufe). Cuando se establece, la energía indicada de cada ciclo se toma de la diferencia de este contador entre el inicio y el final, lo que evita la subestimación que se produce al integrar un sensor de potencia que informa lentamente. Recurre al valor integrado si falta la lectura, si su unidad es desconocida o si el delta no es positivo. Deje en blanco para seguir integrando el sensor de potencia.", + "doc": "Contador opcional en kWh o Wh (total_increasing, p. ej. el propio medidor del enchufe). La energía del ciclo se lee entonces de él, lo que es más preciso que calcularla a partir de un sensor de potencia lento. Si su lectura no sirve, se usa el valor calculado.", "label": "Entidad del medidor de energía" }, "expose_debug_entities": { - "doc": "Publicar entidades HA de diagnóstico adicionales (confianza de coincidencia, ambigüedad, estados internos). Desactivado mantiene la lista de entidades limpia para un uso normal.", + "doc": "Añadir entidades de diagnóstico (confianza de coincidencia, ambigüedad, estado interno). Desactivado mantiene limpia la lista de entidades.", "label": "Exponer entidades de depuración" }, "external_end_trigger": { - "doc": "Sensor binario cuyo cambio de estado marca el final del ciclo (por ejemplo, un contacto \"terminado\" de un electrodoméstico o una integración complementaria).", + "doc": "Sensor binario cuyo cambio marca el final del ciclo (p. ej. un contacto de \"terminado\" u otra integración).", "label": "Entidad disparadora externa" }, "external_end_trigger_enabled": { - "doc": "Deje que un sensor binario externo indique el final de un ciclo, además de la detección basada en energía incorporada.", + "doc": "Dejar que un sensor binario indique el final de un ciclo, además de la detección basada en la potencia.", "label": "Habilitar disparador de fin externo" }, "external_end_trigger_inverted": { - "doc": "Trate el sensor de disparo que se apaga (en lugar de encender) como la señal de fin de ciclo.", + "doc": "Tomar como señal de fin el paso del sensor a apagado (no a encendido).", "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 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.", + "doc": "Puntuación de coincidencia mínima para etiquetar un ciclo terminado. Un ganador claro se etiqueta; un caso dudoso va a la cola de revisión de Ciclos. Por debajo, no se etiqueta ni se pregunta nada. Manténgala por debajo de la Confianza de etiquetado automático.", "label": "Confianza de aprendizaje" }, "linked_device": { - "doc": "Opcionalmente, anide este dispositivo WashData debajo de otro dispositivo (por ejemplo, el enchufe inteligente) en el registro de dispositivos HA, que se muestra como \"Conectado a través de ...\".", + "doc": "Opcional. Mostrar este dispositivo WashData bajo otro dispositivo (p. ej. el enchufe inteligente) en Home Assistant, como \"Conectado a través de ...\".", "label": "Agrupar bajo dispositivo" }, "min_off_gap": { - "doc": "Si la máquina se apaga durante menos de este tiempo, la secuencia de encendido/apagado/encendido se trata como un ciclo continuo. Evita que los programas de remojo (la máquina se apaga durante varios minutos a mitad del lavado) se dividan en dos ciclos separados. Configúrelo más corto que el espacio entre sus cargas consecutivas si desea que se cuenten como ciclos separados. Los valores predeterminados del tipo de dispositivo protegen la pausa típica dentro del ciclo para cada aparato.", + "doc": "Un periodo apagado más corto que esto no divide un ciclo, así que las pausas de remojo quedan dentro de un mismo lavado. Manténgalo por debajo de la pausa entre cargas seguidas, o se fusionarán en un solo ciclo.", "label": "Brecha mínima de apagado" }, "min_power": { - "doc": "Potencia mínima absoluta considerada activa. Las lecturas por debajo de este se tratan como 0 W (en espera), filtrando la carga fantasma de los enchufes inteligentes y los LED en espera.", + "doc": "Las lecturas por debajo de este valor cuentan como 0 W (en espera). Filtra el consumo en reposo de los enchufes inteligentes y los LED de espera.", "label": "Potencia mínima" }, "ml_training_enabled": { - "doc": "Estudie periódicamente los ciclos revisados durante la noche y ajuste los modelos a esta máquina específica. Un cambio sólo se mantiene cuando realmente obtiene una mejor puntuación en los ciclos prolongados, por lo que esto sólo puede ayudar o permanecer igual, nunca retroceder.", + "doc": "Reentrenar de vez en cuando el modelo de energía con los ciclos terminados de esta máquina. Un modelo nuevo solo se conserva si supera a la estimación simple en ciclos de prueba.", "label": "Aprender de esta máquina" }, "ml_training_hour": { - "doc": "Hora local del día (0-23) para realizar el ajuste durante la noche. Elija una hora tranquila como las 2 (02:00).", + "doc": "Hora del día (0-23) para el entrenamiento nocturno. Elija una hora tranquila, p. ej. 2.", "label": "Aprender a la hora" }, "ml_training_interval_days": { - "doc": "Vuelva a verificar las mejoras como máximo una vez cada tantos días.", + "doc": "Reentrenar como máximo una vez cada este número de días.", "label": "Comprobar como máximo cada" }, "ml_training_min_cycles": { - "doc": "Espere hasta que se hayan registrado al menos tantos ciclos antes de realizar el ajuste fino, para que haya suficiente de qué aprender.", + "doc": "Esperar a tener al menos este número de ciclos grabados antes de entrenar.", "label": "Ciclos necesarios primero" }, "name": { - "doc": "Nombre para mostrar que se muestra en la lista de integraciones de HA y en el registro de dispositivos.", + "doc": "Nombre que se muestra en Home Assistant para este dispositivo.", "label": "Nombre del dispositivo" }, "no_update_active_timeout": { - "doc": "Si no llegan actualizaciones de energía durante tanto tiempo mientras está en funcionamiento, suponga que el enchufe se desconectó y fuerce la detención para evitar un ciclo zombi. Los 600 s predeterminados permiten un retraso de nube o malla.", + "doc": "Si no llega ninguna actualización de potencia durante este tiempo en un ciclo, se supone que el enchufe se desconectó y se fuerza la detención del ciclo. El valor predeterminado de 600 s da margen a los retrasos de la nube o de la red mesh.", "label": "Tiempo de espera sin actualización" }, "notify_before_end_minutes": { @@ -1329,35 +1301,35 @@ "label": "Alerta previa al fin" }, "notify_channel": { - "doc": "Nombre del canal de notificación de Android para mensajes de inicio/en vivo (controla el sonido por canal y la prioridad en la aplicación móvil). Blank usa la aplicación complementaria predeterminada.", + "doc": "Canal de Android para los mensajes de inicio y en vivo; el canal define el sonido y la prioridad. En blanco: el predeterminado de la aplicación.", "label": "Canal Android (inicio/vivo)" }, "notify_cycle_timers": { - "doc": "Notificaciones en minutos específicos de un ciclo (por ejemplo, para agregar suavizante). El mensaje admite {device}, {program}, {minutes}. Habilite la pausa automática para hacer una pausa en ese punto y recibir una notificación interactiva con un botón Reanudar; reanudar a través del panel, el servicio de pausa/reanudación o la acción de notificación.", + "doc": "Notificaciones en minutos concretos de un ciclo (p. ej. para añadir suavizante). El mensaje admite {device}, {program}, {minutes}. Con Pausa automática, el ciclo también se pausa ahí y la notificación incluye un botón Reanudar.", "label": "Temporizadores de ciclo" }, "notify_finish_channel": { - "doc": "Nombre del canal de notificación de Android para el mensaje final. Blank reutiliza el canal de inicio/en vivo.", + "doc": "Canal de Android para el mensaje de fin. En blanco: reutiliza el canal de inicio/en vivo.", "label": "Canal Android (fin)" }, "notify_finish_message": { - "doc": "Cuerpo enviado cuando finaliza un ciclo. Variables de plantilla: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}. {time_finished} y {vs_typical} son los más útiles aquí.", + "doc": "Cuerpo enviado cuando termina un ciclo. Variables: {device}, {program}, {duration} (minutos), {duration_hm} (1 h 05 min), {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}, {status} (completed o force_stopped; los ciclos interrumpidos no envían nada). Cualquier otra variable hace que se use el texto predeterminado.", "label": "Mensaje de fin" }, "notify_finish_services": { - "doc": "notificar.* servicios llamados cuando finaliza un ciclo. Agregue uno por objetivo; déjelo vacío para que no haya notificación de finalización.", + "doc": "Servicios notify.* a los que llamar al terminar un ciclo. Déjelo vacío para ninguno.", "label": "Servicios de fin" }, "notify_fire_events": { - "doc": "También active eventos ha_washdata_* al inicio/final del ciclo para que pueda crear sus propias automatizaciones.", + "doc": "Lanzar también eventos ha_washdata_* al inicio y al final del ciclo, para sus propias automatizaciones.", "label": "Disparar eventos de HA para notificaciones" }, "notify_icon": { - "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.", + "doc": "Icono pequeño de la notificación (p. ej. mdi:washing-machine). Solo se envía a destinos mobile_app, no a grupos notify. En iOS sustituye al icono de la aplicación y requiere la aplicación complementaria 2026.8 o posterior. En blanco: el predeterminado de la plataforma.", "label": "Icono de notificación" }, "notify_live_chronometer": { - "doc": "Muestre un temporizador de cuenta regresiva de actualización en vivo en la notificación (en plataformas que lo admitan) en lugar de una estimación estática.", + "doc": "Mostrar una cuenta atrás en marcha en la notificación (donde se admita) en lugar de una estimación fija.", "label": "Usar cronómetro en vivo" }, "notify_live_interval_seconds": { @@ -1365,143 +1337,135 @@ "label": "Intervalo de actualización en vivo" }, "notify_live_overrun_percent": { - "doc": "Si un ciclo supera su estimación en más de este porcentaje, envíe una alerta de exceso.", + "doc": "Las actualizaciones en vivo se detienen cuando un ciclo supera su duración prevista en este porcentaje (20 = hasta 1,2x el tiempo previsto).", "label": "% de sobretiempo en vivo antes de alerta" }, "notify_live_services": { - "doc": "Los servicios notify.* solicitan actualizaciones de progreso en vivo mientras se ejecuta un ciclo. Déjelo vacío para desactivar las notificaciones de progreso en vivo.", + "doc": "Servicios notify.* para las actualizaciones de progreso en vivo mientras dura un ciclo. Déjelo vacío para desactivarlas.", "label": "Servicios de progreso en vivo" }, "notify_only_when_home": { - "doc": "Envíe notificaciones únicamente cuando al menos una de las personas vinculadas (arriba) esté en casa.", + "doc": "Notificar solo cuando al menos una de las personas de arriba esté en casa.", "label": "Notificar solo cuando esté en casa" }, "notify_people": { - "doc": "person.* entidades utilizadas por \"Notificar solo cuando esté en casa\" para decidir si hay alguien en casa.", + "doc": "Entidades person.* que comprueba \"Notificar solo cuando esté en casa\".", "label": "Personas (para solo cuando esté en casa)" }, "notify_pre_complete_message": { - "doc": "Cuerpo de la alerta previa al final/casi terminado. Variables de plantilla: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Texto de cada actualización de progreso en vivo. Variables de plantilla: {device}, {program}, {minutes} (minutos restantes). Cualquier otra variable hace que se use el texto predeterminado.", "label": "Mensaje de pre-finalización" }, "notify_reminder_message": { - "doc": "Cuerpo del recordatorio de descarga que aún está en espera. En blanco usa el valor predeterminado incorporado. Variables de plantilla: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Texto de la Alerta previa al fin, que se envía una sola vez. Variables de plantilla: {device}, {program}, {minutes} (el ajuste Alerta previa al fin). Cualquier otra variable hace que se use el texto predeterminado.", "label": "Mensaje de recordatorio" }, "notify_start_message": { - "doc": "Cuerpo enviado cuando comienza un ciclo. Variables de plantilla: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Cuerpo enviado cuando comienza un ciclo. Variables de plantilla: {device}, {program}. Cualquier otra variable hace que se use el texto predeterminado.", "label": "Mensaje de inicio" }, "notify_start_services": { - "doc": "notificar.* servicios llamados cuando comienza un ciclo. Agregue uno por objetivo (teléfono, tablero, etc.); déjelo vacío para que no haya notificación de inicio.", + "doc": "Servicios notify.* a los que llamar al empezar un ciclo (teléfono, panel, ...). Déjelo vacío para ninguno.", "label": "Servicios de inicio" }, "notify_timeout_seconds": { - "doc": "Descartar automáticamente la notificación después de tantos segundos (en plataformas que lo admitan). 0 lo mantiene hasta que se descarte manualmente.", + "doc": "Cerrar la notificación tras este número de segundos (donde se admita). 0 la mantiene hasta que usted la cierre.", "label": "Descartar automáticamente después de" }, "notify_title": { - "doc": "Título de la notificación. Variables de plantilla: {device}, {program}, {duration}, {energy}, {cost}, {date}, {time}, {minutes}.", + "doc": "Título de la notificación. Solo se sustituye {device}; cualquier otra variable hace que se use el título predeterminado.", "label": "Título de notificación" }, "notify_unload_delay_minutes": { - "doc": "Minutos después de que finaliza un ciclo antes de enviar el recordatorio de \"descargar la máquina\" que aún está en espera. Establezca 0 para desactivar el recordatorio.", + "doc": "Minutos tras el final de un ciclo antes del recordatorio \"descargar la máquina\". 0 lo desactiva.", "label": "Retardo del recordatorio de descarga" }, "notify_unload_repeat": { - "doc": "Sigue reenviando el recordatorio de descarga cada \"Retardo del recordatorio de descarga\" minutos hasta que se confirme la descarga o toques \"Dejar de recordar\" en la notificación. Necesita alguna forma de confirmar la descarga: una Entidad de sensor de puerta, una Entidad de confirmación de descarga o la opción \"Confirmar la descarga manualmente\" de abajo. El botón de descarte funciona en las notificaciones de la aplicación complementaria (móvil) de Home Assistant.", + "doc": "Reenviar el recordatorio de descarga cada \"Retardo del recordatorio de descarga\" minutos hasta que se confirme la descarga o pulse \"Dejar de recordar\" (aplicación móvil). Requiere una Entidad de sensor de puerta, una Entidad de confirmación de descarga o \"Confirmar la descarga manualmente\".", "label": "Repetir hasta confirmar la descarga" }, "off_delay": { - "doc": "Es tiempo de esperar después de que se corte la energía antes de declarar finalizado el ciclo. Si la energía se reanuda dentro de esta ventana, el ciclo continúa sin problemas; esto salva las pausas entre las etapas de lavado. Los lavavajillas tienen fases de secado largas (apagado durante 20 a 60 minutos), por lo que el retraso de apagado debe exceder eso para mantener todo el lavado y secado como un solo ciclo.", + "doc": "Cuánto tiempo debe mantenerse baja la potencia antes de dar el ciclo por terminado. Si la potencia vuelve en ese tiempo, el ciclo continúa. Los lavavajillas pueden estar en reposo 20-60 min mientras secan, así que póngalo más largo que eso.", "label": "Retardo de apagado" }, "pause_cuts_power": { - "doc": "Cuando se pausa un ciclo, apague también la entidad de cambio a continuación. Solo para electrodomésticos cuyo enchufe se pueda cortar de forma segura a mitad del ciclo.", + "doc": "Al pausar un ciclo, apagar también la Entidad interruptora de abajo. Solo para electrodomésticos que pueden quedarse sin corriente a mitad de ciclo sin riesgo.", "label": "La pausa también corta la energía (mediante interruptor)" }, "power_off_delay": { - "doc": "Cuánto tiempo debe mantenerse la potencia por debajo del Umbral de apagado tras finalizar un ciclo antes de que el estado vuelva a Apagado. Solo se usa cuando el Umbral de apagado es mayor que 0. Se comprueba según la cadencia en segundo plano, por lo que el retardo efectivo se redondea al alza hasta el siguiente tick de expiración de estado.", + "doc": "Cuánto tiempo debe quedarse la potencia por debajo del Umbral de apagado tras un ciclo antes de que el estado vuelva a Apagado. Solo se usa cuando ese umbral es mayor que 0. Se comprueba periódicamente, así que la espera real puede ser algo mayor.", "label": "Retardo de apagado" }, "power_off_threshold_w": { - "doc": "Detección opcional del estado Apagado basada en la potencia. Cuando es mayor que 0, una vez que un ciclo ha finalizado y la potencia se mantiene por debajo de este nivel durante el Retardo de apagado, se considera que la máquina está apagada y el estado vuelve a Apagado. Déjelo en 0 para desactivarlo (valor predeterminado). Ajústelo por encima del nivel real de consumo con la máquina apagada y por debajo del Umbral de parada y del consumo en espera de su máquina (finalizada pero encendida); si no es inferior al Umbral de parada, se ignora. Cuando está activado, sustituye al Retardo de reinicio de progreso para volver a Apagado, de modo que una máquina finalizada permanece en Finalizado/Limpio hasta que se apaga realmente.", + "doc": "Tras un ciclo, una potencia por debajo de este valor durante el Retardo de apagado pone el estado en Apagado, en lugar de hacerlo el Retardo de reinicio de progreso. Póngalo por encima del consumo apagado y por debajo del de espera y del Umbral de parada. 0 lo desactiva (predeterminado).", "label": "Umbral de apagado" }, "power_sensor": { - "doc": "La entidad del sensor que informa la potencia activa en vatios para este electrodoméstico (por ejemplo, sensor.washer_power). Toda la detección de ciclos se basa en esta señal.", + "doc": "El sensor que informa la potencia actual de este electrodoméstico en vatios (p. ej. sensor.washer_power). Toda la detección se basa en él.", "label": "Sensor de potencia" }, - "profile_duration_tolerance": { - "doc": "La banda +/- alrededor de la duración promedio de un perfil utilizada durante la coincidencia. 0,25 significa que un perfil de 60 minutos coincide con ciclos de 45 a 75 minutos.", - "label": "Tolerancia de duración del perfil" - }, "profile_match_interval": { - "doc": "Con qué frecuencia intentar hacer coincidir el perfil durante un ciclo de ejecución. El valor predeterminado de 300 s (5 minutos) equilibra la velocidad de detección y la CPU.", + "doc": "Cada cuánto se compara un ciclo en marcha con los perfiles. Predeterminado 300 s (5 min).", "label": "Intervalo de coincidencia" }, "profile_match_max_duration_ratio": { - "doc": "Longitud máxima del ciclo en relación con el perfil. 1.3 significa que un ciclo debe tener menos del 130 % de la duración del perfil para que coincida.", + "doc": "Un programa se descarta cuando el ciclo supera este múltiplo de su duración habitual. 1,8: un ciclo un 80 % más largo que el programa ya no coincide con él.", "label": "Proporción máxima de duración" }, "profile_match_min_duration_ratio": { - "doc": "Duración mínima del ciclo en relación con el perfil. 0,9 significa que un ciclo debe tener al menos el 90 % de la duración del perfil para coincidir.", + "doc": "Un programa solo se tiene en cuenta cuando el ciclo ha alcanzado esta fracción de su duración habitual. 0,10: un programa de 2 horas pasa a ser candidato a los 12 minutos. No se aplica en los primeros 15 minutos de un ciclo.", "label": "Proporción mínima de duración" }, "profile_match_threshold": { - "doc": "Puntuación de similitud mínima (0-1) requerida al final del ciclo para aceptar una identificación de programa. Auméntelo para reducir identificaciones erróneas; bájelo si los programas de su máquina no coinciden. El valor predeterminado 0,4 es un punto de partida conservador.", + "doc": "Puntuación de coincidencia en vivo que necesita un ciclo antes de que la Terminación inteligente o el final antiarrugas puedan terminarlo antes de tiempo. No cambia el programa mostrado. Súbala si coincidencias débiles terminan ciclos demasiado pronto; bájela si coincidencias claras esperan todo el retardo de apagado.", "label": "Umbral de coincidencia" }, "profile_unmatch_threshold": { - "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.", + "doc": "Si la puntuación de coincidencia en vivo cae por debajo de este valor, se borra la suposición actual de programa. Manténgala un poco por debajo del Umbral de coincidencia para que una caída breve no la reinicie.", "label": "Umbral de no coincidencia" }, "progress_reset_delay": { - "doc": "Después de terminar, mantenga el progreso al 100% durante este tiempo para que Completado sea visible en los paneles antes de restablecerlo a Inactivo.", + "doc": "Tras un ciclo, mantener el progreso al 100% durante este tiempo antes de volver a Inactivo.", "label": "Retardo de reinicio de progreso" }, "pump_stuck_duration": { - "doc": "Segundos que una bomba puede funcionar continuamente antes de que se marque como posiblemente atascada (activa el evento de bomba atascada).", + "doc": "Cuánto tiempo puede funcionar una bomba sin parar antes de que salte el evento de bomba atascada.", "label": "Duración de bomba atascada" }, "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.", + "doc": "Tiempo mínimo entre lecturas procesadas. Las lecturas que llegan antes se omiten (las lecturas bajas durante un ciclo siempre se conservan). Póngalo en el intervalo de informe de su enchufe o menos; un valor mayor solo descarta lecturas.", "label": "Intervalo de muestreo" }, "save_debug_traces": { - "doc": "Almacene el seguimiento de potencia total y los datos de depuración coincidentes para cada ciclo. Útil para solucionar problemas pero aumenta el tamaño de almacenamiento.", + "doc": "Guardar también con cada ciclo los datos de depuración del comparador (las puntuaciones de cada candidato), para solucionar problemas. Añade unos 20 KB por ciclo.", "label": "Guardar trazas de depuración" }, "smart_termination_duration_ratio": { - "doc": "Cuánto debe haber avanzado un ciclo dentro de la duración esperada del programa coincidente antes de que la Terminación inteligente pueda finalizarlo antes de tiempo una vez que la potencia cae. La duración esperada es el promedio del programa, por lo que en aparatos cuyo tiempo de funcionamiento varía mucho - lavadoras según el agua de entrada fría en invierno o templada en verano, secadoras con sensor de humedad, programas que dependen de la carga - cerca de la mitad de las ejecuciones terminan antes que ese promedio y nunca obtienen el final rápido, finalizando solo mediante el tiempo de espera de reserva con minutos de retraso. Baje este valor (p. ej. 0,85) en esas máquinas para que el final anticipado siga activándose; súbalo hacia 1,0 para ser más conservador. Déjelo vacío para el valor predeterminado (0,98, o 0,99 para lavavajillas). Solo puede finalizar un ciclo antes, nunca después, y nunca se activa en una coincidencia ambigua o de baja confianza.", + "doc": "Fracción de la duración media del programa que un ciclo debe alcanzar antes de que la Terminación inteligente pueda finalizarlo antes de tiempo. Si la duración varía (temperatura del agua, secado por sensor, tamaño de la carga), redúzcala (p. ej. 0,85). Déjela vacía para 0,98 (0,99 en lavavajillas).", "label": "Proporción de terminación inteligente" }, - "smoothing_window": { - "doc": "Cuánto se suaviza la señal de potencia bruta. Bajo (2) responde pero es ruidoso; alto (5) suaviza los picos pero agrega retraso.", - "label": "Ventana de suavizado" - }, "start_duration_threshold": { - "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.", + "doc": "La potencia debe mantenerse por encima del Umbral de inicio este tiempo antes de que empiece un ciclo, para ignorar parpadeos breves de encendido/apagado.", "label": "Duración de inicio" }, "start_energy_threshold": { - "doc": "Energía (potencia x tiempo) que debe consumir el aparato antes de FUNCIONAR. Un breve pico de alta potencia tiene muy poca energía y se ignora, lo que evita arranques en falso.", + "doc": "Energía que debe consumir el electrodoméstico antes de que el ciclo cuente como en marcha. Un pico breve de potencia consume poca energía, así que no inicia un ciclo.", "label": "Energía de inicio" }, "start_threshold_w": { - "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.", + "doc": "La potencia debe superar este valor para iniciar un ciclo. Demasiado bajo: el consumo en espera provoca falsos arranques. Demasiado alto: se pierden los programas de arranque lento (llenado en frío).", "label": "Umbral de inicio" }, "stop_threshold_w": { - "doc": "La energía debe caer por debajo de este nivel antes de que comience la cuenta atrás del retraso de apagado. Configúrelo por debajo del umbral de inicio; el espacio entre ellos es la banda de histéresis que evita el parpadeo. Si se ajusta demasiado alto, las fases de baja potencia (paradas de aclarado, antiarrugas) activan erróneamente la secuencia final.", + "doc": "La potencia debe caer por debajo de este valor antes de que empiece a contar el retardo de apagado. Manténgalo por debajo del Umbral de inicio. Si es demasiado alto, las fases de baja potencia (pausas de aclarado, antiarrugas) inician por error la cuenta atrás del final.", "label": "Umbral de parada" }, "switch_entity": { - "doc": "El interruptor opcional se apaga en pausa y se vuelve a encender al reanudar, se usa junto con \"Pausa también corta la energía\".", + "doc": "Interruptor opcional que se apaga al pausar y se vuelve a encender al reanudar, con \"La pausa también corta la energía\".", "label": "Entidad interruptora" }, "watchdog_interval": { - "doc": "Con qué frecuencia el organismo de control en segundo plano comprueba si hay sensores bloqueados y tiempos de espera transcurridos. Por defecto 30 s.", + "doc": "Cada cuánto busca la comprobación en segundo plano sensores bloqueados y tiempos de espera vencidos. Predeterminado 30 s.", "label": "Intervalo de vigilancia" }, "notify_milestone_message": { @@ -1509,127 +1473,82 @@ "label": "Mensaje de hito" }, "notify_milestones": { - "doc": "Recuentos de ciclos separados por comas que activan una notificación de celebración única al alcanzarse (p. ej., 50, 100, 500, 1000). En blanco desactiva las notificaciones de hitos.", + "doc": "Recuentos de ciclos que activan una única notificación de celebración, separados por comas (p. ej. 50, 100, 500, 1000). En blanco las desactiva.", "label": "Hitos de ciclos" }, "notify_quiet_end_hour": { - "doc": "Fin de la franja de no molestar (0-23). Las notificaciones retenidas se entregan a esta hora. Deje en blanco para desactivar.", + "doc": "Fin de la franja de no molestar (0-23); las notificaciones retenidas se envían entonces. En blanco la desactiva.", "label": "Fin de horas de silencio" }, "notify_quiet_start_hour": { - "doc": "Inicio de una franja de no molestar (0-23). Las notificaciones de finalización, recordatorio y ropa limpia que se activarían durante las horas de silencio se retienen y se entregan cuando termina la franja. Deje en blanco para desactivar. Admite franjas que cruzan la medianoche (p. ej., inicio 22, fin 7).", + "doc": "Inicio de una franja de no molestar (0-23). Las notificaciones de fin, recordatorio y ropa limpia en esta franja se retienen hasta que termina. Puede cruzar la medianoche (p. ej. de 22 a 7). En blanco la desactiva.", "label": "Inicio de horas de silencio" }, "peak_rate_message": { - "doc": "Texto personalizado opcional para el aviso de tarifa alta que se añade a la notificación de inicio. Variables de plantilla: {device}, {price}. En blanco usa el valor predeterminado incorporado.", + "doc": "Texto propio para el aviso de tarifa alta. Variables de plantilla: {device}, {price}. En blanco usa el predeterminado.", "label": "Mensaje de tarifa alta" }, "peak_rate_threshold": { - "doc": "Cuando comienza un ciclo y el precio actual por kWh es igual o superior a este valor, añade un aviso de tarifa alta a la notificación de inicio. 0 o en blanco desactiva el aviso.", + "doc": "Si el precio por kWh es igual o superior a este valor al empezar un ciclo, añade un aviso de tarifa alta a la notificación de inicio. 0 o en blanco lo desactiva.", "label": "Umbral de tarifa alta (por kWh)" }, - "dtw_bandwidth": { - "label": "Tolerancia de coincidencia de forma", - "doc": "Cuánta deformación temporal puede aplicar el comparador a un ciclo para ajustarlo a la forma de un programa guardado. Un valor más alto tolera más variación de velocidad; demasiado alto corre el riesgo de coincidir con el programa equivocado." - }, - "corr_weight": { - "label": "Peso de forma frente a nivel", - "doc": "Equilibrio entre la forma de la curva y el nivel de potencia absoluto al puntuar una coincidencia. Un valor más alto favorece la forma; uno más bajo favorece la coincidencia del nivel de potencia." - }, - "duration_weight": { - "label": "Peso de la duración", - "doc": "Con qué intensidad se prefiere un programa cuya duración habitual coincide con este ciclo." - }, - "energy_weight": { - "label": "Peso de la energía", - "doc": "Con qué intensidad se prefiere un programa cuyo consumo de energía habitual coincide con este ciclo." - }, "store_brand": { "label": "Marca del electrodoméstico", - "doc": "Opcional. La marca del electrodoméstico, elegida del catálogo de la comunidad. Se usa para encontrar y compartir grabaciones de referencia coincidentes. Déjelo en blanco si no usa las funciones en línea." + "doc": "Opcional. La marca de su electrodoméstico en el catálogo de la comunidad, para encontrar y compartir grabaciones de referencia. Déjela en blanco si no usa las funciones en línea." }, "store_model": { "label": "Modelo del electrodoméstico", - "doc": "Opcional. El modelo del electrodoméstico, elegido del catálogo de la comunidad una vez definida la marca. Si su modelo no aparece, puede añadirlo al catálogo." + "doc": "Opcional. El modelo de su electrodoméstico en el catálogo de la comunidad (elija antes una marca). Si no aparece, puede añadirlo." }, "show_contributor": { "doc": "Mostrar la atribución \"por \" en los electrodomésticos de la comunidad y los ciclos de referencia." }, - "enable_phase_matching": { - "label": "Tiempo restante según las fases", - "doc": "Divide cada ciclo en curso en fases (calentamiento, lavado, centrifugado) y reparte el tiempo restante por fase, combinado con la estimación clásica - apoyándose en el reparto por fases al principio del ciclo y en la estimación clásica cerca del final. Esto personaliza la cuenta atrás según cuánto calienta y funciona realmente tu máquina, lo que se nota más en la primera mitad del ciclo. Desactivado = solo la estimación clásica. Solo se ve afectada la visualización del tiempo restante; la coincidencia de programas y la detección de ciclos no cambian." - }, - "keep_min_score": { - "label": "Score mín de coincidencia", - "doc": "Puntuación de similitud mínima que necesita un candidato para seguir en la carrera de coincidencia. El valor predeterminado 0,1 es deliberadamente permisivo - admite incluso candidatos débiles y se basa en etapas posteriores para encontrar la mejor coincidencia. Auméntelo para eliminar perfiles improbables antes; redúzcalo para ampliar el pool de candidatos inicial." - }, - "dtw_blend": { - "label": "Fusión Warp", - "doc": "Cuánto reemplaza la puntuación de alineación por deformación temporal (DTW) a la puntuación central de la Etapa 2. 0 = usar solo la puntuación de la Etapa 2, 1 = usar solo la puntuación DTW, 0,5 (predeterminado) = mezcla igual. Auméntelo para confiar más en el alineamiento Warp cuando los programas tienen niveles de potencia similares pero diferentes patrones de temporización." - }, - "dtw_ensemble_w": { - "label": "Mix ensemble Warp", - "doc": "En el modo DTW ensemble (el predeterminado), el peso en L1 escalado frente a DTW derivado (DDTW). 1,0 = todo L1 escalado, 0 = todo DDTW, 0,7 = predeterminado. La variante L1 escalado compara niveles de potencia; la variante derivada reacciona a cómo cambia la potencia con el tiempo. Ensemble combina ambas." - }, - "dtw_ddtw_scale": { - "label": "Escala Warp derivada", - "doc": "Distancia de semisaturación para la puntuación DTW derivada. La puntuación se reduce a la mitad cuando la distancia DDTW es igual a este valor. Menor = más sensible a las diferencias de forma. El predeterminado 30 está calibrado para trazas de potencia típicas de electrodomésticos. Solo afecta a los modos DTW ensemble y ddtw." - }, - "dtw_refine_top_n": { - "label": "Contador de refinamiento Warp", - "doc": "Cuántos candidatos principales de la Etapa 2 son repuntuados por DTW. DTW es más costoso, por lo que solo se aplica a los mejores N candidatos. Predeterminado 5. Auméntelo (a 7-9) si el perfil correcto a veces solo alcanza el 4.º o 5.º lugar tras la Etapa 2 - DTW puede rescatarlo. Reducirlo acelera ligeramente la coincidencia." - }, - "duration_scale": { - "label": "Nitidez de duración", - "doc": "Precisión de la penalización por concordancia de duración en la Etapa 4. Este es el log-ratio en el que el score de concordancia se reduce a la mitad. Menor = más estricto: una discordancia de duración penaliza más. El predeterminado 0,175 corresponde a aproximadamente el 18% de tolerancia de duración a medio peso. Combinar con Peso de duración." - }, - "energy_scale": { - "label": "Nitidez de energía", - "doc": "Precisión de la penalización por concordancia de energía. Menor = más estricto: una discordancia de energía penaliza más. El predeterminado 0,25 es intencionalmente más indulgente que la Precisión de duración, porque la energía varía con la carga. Combinar con Peso de energía." - }, "power_profile_interval_min": { "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." + "doc": "Tamaño de intervalo del atributo power_profile (vatios medios por franja) que usan planificadores como EMHASS y tibber_prices. Más pequeño conserva los picos cortos; más grande suaviza la curva. Predeterminado 15 min. 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." + "doc": "Solo iOS. Actualizar el progreso en vivo en silencio, con una prioridad que ahorra batería y que iOS puede agrupar. La primera actualización y las notificaciones de inicio y fin siguen avisando. Desactivado: cada actualización avisa." }, "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." + "doc": "Solo Android. Mantener la notificación de progreso en vivo al tocarla. Desactivado: un toque la cierra." }, "notify_live_click_action": { "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." + "doc": "Qué abre un toque en la notificación de este electrodoméstico. En blanco: el panel de WashData en este electrodoméstico. Introduzca una ruta de panel (p. ej. /lovelace/laundry) o una URL, o \"none\" para no hacer nada al tocar." }, "door_opens_at_end": { "label": "La puerta se abre automáticamente al final", - "doc": "Para lavavajillas que abren la puerta al final del ciclo para secar (AirDry y similares). Con esta opción activada, abrir la puerta durante un ciclo en marcha ya no lo pausa indefinidamente; en cambio, si la puerta permanece abierta el tiempo mínimo indicado abajo, WashData considera el ciclo finalizado. Una apertura breve (para añadir un elemento) se ignora. Requiere una entidad de sensor de puerta." + "doc": "Para lavavajillas que abren la puerta al final para secar (p. ej. AirDry). Una puerta que queda abierta durante el tiempo de espera de abajo termina el ciclo en lugar de pausarlo; una apertura breve se ignora. Requiere una Entidad de sensor de puerta." }, "door_end_dwell_seconds": { "label": "Tiempo mínimo de puerta abierta para finalizar", - "doc": "Cuánto tiempo debe permanecer abierta la puerta antes de que WashData finalice el ciclo, cuando «La puerta se abre automáticamente al final» está activada. Lo suficientemente largo como para ignorar añadir un elemento rápidamente (predeterminado 60 s), y lo suficientemente corto como para terminar pronto una vez que la máquina abra la puerta." + "doc": "Con «La puerta se abre automáticamente al final» activado: cuánto tiempo debe quedar abierta la puerta antes de que termine el ciclo. Lo bastante para ignorar que se añade un plato (predeterminado 60 s)." }, "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." + "doc": "Qué tipos de ciclos dan forma a la curva y a la coincidencia de cada programa. Desmarcar un tipo no borra nada; útil si no se fía de los datos importados. Las estadísticas siempre cuentan solo los ciclos de esta máquina. Desmarcar todos se ignora." }, "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." + "doc": "Cobrar cada ciclo al precio de cada momento en que estuvo en marcha, en lugar del precio al terminar. Requiere una entidad de precio. Procesar historial (Diagnóstico) recalcula los ciclos anteriores." }, "unload_confirm_entity": { "label": "Entidad de confirmación de descarga", - "doc": "Entidad opcional cuya activación significa que se ha sacado la carga, para máquinas que no pueden tener un sensor de puerta. Sirve cualquier tipo: un botón Zigbee o NFC, un ayudante input_button, un sensor de movimiento delante de la máquina, una escena. Cualquier cambio de estado cuenta como confirmación, excepto apagarse o pasar a no disponible. Definir esto también activa el recordatorio de descarga en un dispositivo sin Entidad de sensor de puerta." + "doc": "Opcional. Cualquier entidad que confirme que se ha sacado la carga, para máquinas sin sensor de puerta (un botón, input_button, sensor de movimiento, escena). Cuenta cualquier cambio de estado excepto apagarse o quedar no disponible. También activa el recordatorio de descarga." }, "unload_track_without_door": { "label": "Confirmar la descarga manualmente", - "doc": "Ejecuta el recordatorio de descarga en un dispositivo sin Entidad de sensor de puerta ni Entidad de confirmación de descarga, y confirmas tú la descarga desde tu propia automatización: pulsa la entidad de botón \"Marcar como descargado\" o llama al servicio ha_washdata.mark_unloaded. Se ignora cuando hay una Entidad de sensor de puerta configurada, que ya proporciona la señal." + "doc": "Usar el recordatorio de descarga sin sensor de puerta ni entidad de confirmación. Confirme la descarga desde su propia automatización con el botón \"Marcar como descargado\" o el servicio ha_washdata.mark_unloaded. Se ignora si hay una Entidad de sensor de puerta configurada." }, "notify_icon_color": { "label": "Color de notificación", - "doc": "Color de acento para las notificaciones de este electrodoméstico, para distinguir de un vistazo una lavadora, una secadora y un lavavajillas. iOS tiñe el icono y la barra de progreso de la Actividad en vivo en la pantalla bloqueada y en la Dynamic Island. A Android se le envía el mismo color, pero lo que haga con él depende del teléfono: algunos tiñen el icono y muchos (Samsung One UI en especial) lo ignoran por completo. Ambos entienden el formato hexadecimal (por ejemplo, #03A9F4). Déjelo en blanco para el valor predeterminado de la plataforma." + "doc": "Color de acento para las notificaciones de este electrodoméstico, para distinguir fácilmente lavadora, secadora y lavavajillas. En iOS rellena el círculo detrás del icono y colorea la barra de la Live Activity. Muchos teléfonos Android (sobre todo Samsung) lo ignoran. Use hex, p. ej. #03A9F4. En blanco: el predeterminado de la plataforma." + }, + "share_name": { + "doc": "Mostrar el nombre de su cuenta como autor de los ciclos que comparte. Desactivado: se comparten sin nombre." } }, "setting_group": { @@ -1657,9 +1576,6 @@ "door_pause": { "label": "Puerta y pausa" }, - "dtw_refinement": { - "label": "Refinamiento DTW" - }, "duration_gates": { "label": "Puertas de duración" }, @@ -1736,13 +1652,18 @@ "rebuilding": "Reconstruyendo…", "analyzing": "Analizando…", "resetting": "Reiniciando…", - "reverting": "Revirtiendo…", "trimming": "Recortando…", "splitting": "Dividiendo…", + "stalled": "Atascado", "deleting": "Eliminando…", "imported": "Importado", "preparing": "Preparando…", - "unlabelling": "Quitando etiqueta…" + "unlabelling": "Quitando etiqueta…", + "working": "Procesando…", + "match_uncertain": "Incierto: {top} o {runner}", + "match_uncertain_single": "Incierto: quizá {top}", + "match_sure": "~{pct} % de certeza", + "match_uncertain_tip": "WashData aún no se ha decidido por un programa. La cifra indica con qué frecuencia acertó la opción principal, {top}, en ciclos anteriores cuando iba así de destacada. Solo informativo." }, "tab": { "advanced": "Avanzado", @@ -1772,6 +1693,11 @@ "wrong_profile": "perfil equivocado" }, "toast": { + "task_name_required": "Asigne un nombre a la tarea", + "task_added": "Tarea agregada", + "task_save_failed": "No se pudo guardar la tarea: {error}", + "task_removed": "Tarea eliminada", + "maint_logged": "Registrado como hecho: {task}", "catalog_refreshed": "Catálogo de la comunidad actualizado", "access_saved": "Control de acceso guardado", "all_wiped": "Todos los datos borrados", @@ -1779,7 +1705,6 @@ "auto_label_complete": "Etiqueta automática completa", "automation_deleted": "Automatización eliminada", "correction_submitted": "Corrección enviada", - "cycle_deleted": "Ciclo eliminado", "cycle_force_stopped": "Ciclo detenido a la fuerza", "cycle_labelled": "Ciclo etiquetado", "cycle_paused": "Ciclo en pausa", @@ -1795,11 +1720,9 @@ "group_deleted": "Grupo eliminado", "group_name_required": "El nombre del grupo es obligatorio.", "group_saved": "Grupo guardado", - "import_successful": "Importación exitosa; recarga de integración", "json_required": "Se requieren datos JSON", "legacy_removed": "Se eliminaron las acciones heredadas", "logs_exported": "Registros exportados", - "matching_reverted": "Los pesos coincidentes se revirtieron a los valores predeterminados", "min_2_profiles": "Seleccione al menos 2 perfiles para un grupo", "ml_training_no_improvement": "Entrenamiento completo: línea base mantenida (sin mejora)", "models_reverted": "Los modelos en el dispositivo volvieron a la línea base", @@ -1809,8 +1732,8 @@ "pg_preset_loaded": "Preajuste \"{name}\" cargado", "pg_preset_saved": "Preajuste \"{name}\" guardado", "pg_sugg_loaded": "{n} valor(es) sugerido(s) cargado(s) - ejecute la Zona de pruebas para comparar", - "pg_sugg_ml_loaded": "{n} valor(es) calibrado(s) por ML cargado(s) - ejecute la Zona de pruebas para comparar", "phase_name_required": "Se requiere el nombre de la fase", + "phase_reset": "Fase \"{name}\" restablecida a su versión incorporada", "phase_updated": "Fase actualizada", "phases_saved": "Fases guardadas", "preferences_saved": "Preferencias guardadas", @@ -1857,15 +1780,11 @@ "store_name_required": "Introduzca un nombre de perfil", "store_import_failed": "Error al importar: {error}", "store_imported": "Importado en {profile}", - "store_program_required": "Introduzca un nombre de programa", "store_no_appliance": "Primero configure la marca y el modelo de su electrodoméstico en Ajustes.", "store_share_failed": "Error al compartir: {error}", "store_shared": "Compartido en la tienda de la comunidad, pendiente de revisión.", "appliance_added": "Electrodoméstico añadido, a la espera de aprobación", "contrib_device_changed": "Añadido a la tienda de la comunidad, pero el dispositivo seleccionado cambió mientras el formulario estaba abierto. Selecciónelo en Ajustes.", - "thanks_confirming": "Gracias por confirmar", - "device_approved": "Aprobado por la comunidad", - "rating_saved": "Valoración de calidad guardada", "brand_added": "Marca añadida, a la espera de aprobación", "profile_added": "Perfil añadido, a la espera de aprobación", "saved_except_conflicts": "Guardado. Corrige los conflictos resaltados para guardar el resto.", @@ -1888,11 +1807,52 @@ "odometer_saved": "Total de ciclos guardado", "odometer_save_failed": "No se pudo guardar el total: {error}", "store_pick_profile_share": "Elija un perfil en el que compartir", - "remove_failed": "Error al quitar: {error}" + "remove_failed": "Error al quitar: {error}", + "analysis_complete": "Análisis completo: {count} sugerencia(s)", + "analysis_failed": "Error en el análisis: {error}", + "auto_detect_failed": "Error en la detección automática: {error}", + "auto_label_failed": "Error en el etiquetado automático: {error}", + "could_not_load_cycle": "No se pudo cargar el ciclo: {error}", + "create_failed": "Error al crear: {error}", + "cycles_deleted": "{count} ciclo(s) eliminado(s)", + "debug_cleared": "{count} rastros de depuración borrados", + "discard_failed": "Error al descartar: {error}", + "error": "Error: {error}", + "export_failed": "Error al exportar: {error}", + "import_failed": "Error al importar: {error}", + "invalid_json": "\"{key}\" no es un JSON válido; corríjalo o vacíe el campo antes de guardar.", + "label_failed": "Error al etiquetar: {error}", + "ml_training_promoted": "Entrenamiento completo: {count} modelo(s) promovido(s)", + "pause_failed": "Error al pausar: {error}", + "phase_created": "Fase \"{name}\" creada", + "phase_deleted": "Fase \"{name}\" eliminada", + "profile_created": "Perfil \"{name}\" creado", + "rename_failed": "Error al renombrar: {error}", + "save_failed": "Error al guardar: {error}", + "split_complete": "Dividido en {count} ciclos", + "start_failed": "Error al iniciar: {error}", + "stop_failed": "Error al detener: {error}", + "update_failed": "Error al actualizar: {error}", + "store_download_skipped": "Grabaciones omitidas: {n} (demasiado cortas, con demasiados huecos o ya presentes en tu dispositivo).", + "store_import_low_quality": "Esta grabación es demasiado corta, tiene demasiados huecos o es poco plausible y no se puede importar.", + "store_import_duplicate": "Esta grabación ya está en tu dispositivo.", + "device_approved": "Aprobado por la comunidad", + "thanks_confirming": "Gracias por confirmar", + "rating_saved": "Valoración de calidad guardada", + "store_download_partial": "No se pudieron descargar {n} programa(s); inténtelo de nuevo más tarde.", + "import_no_restore_point": "Importado, pero no se pudo guardar un punto de restauración, así que esta importación no se puede deshacer", + "import_undo_failed": "No se pudo deshacer la importación: {error}", + "import_undone": "Importación deshecha: se restauraron {profiles} perfil(es) y {cycles} ciclo(s)", + "auto_label_cancelled": "Etiquetado automático cancelado tras etiquetar {count} ciclos.", + "auto_label_cancelled_one": "Etiquetado automático cancelado tras etiquetar {count} ciclo.", + "auto_label_cancelled_many": "Etiquetado automático cancelado tras etiquetar {count} ciclos.", + "auto_label_cancelled_other": "Etiquetado automático cancelado tras etiquetar {count} ciclos.", + "auto_label_done": "Etiquetado automático completado: {count} ciclos etiquetados, {skipped} sin etiquetar.", + "auto_label_done_one": "Etiquetado automático completado: {count} ciclo etiquetado, {skipped} sin etiquetar.", + "auto_label_done_many": "Etiquetado automático completado: {count} ciclos etiquetados, {skipped} sin etiquetar.", + "auto_label_done_other": "Etiquetado automático completado: {count} ciclos etiquetados, {skipped} sin etiquetar." }, "suggestion": { - "both_agree": "WashData recomienda", - "calibrated_label": "Calibrado", "observed_label": "Observado", "impact": { "auto_label_confidence": { @@ -1907,10 +1867,6 @@ "higher": "Solo cierra ciclos que usaron energía significativa", "lower": "También cierra ciclos cortos o de baja energía" }, - "end_repeat_count": { - "higher": "Exige que la señal de fin se repita más veces – más robusto, ligeramente más lento", - "lower": "Termina tras menos repeticiones – detección más rápida, mayor riesgo de fin falso" - }, "learning_confidence": { "higher": "Aprende solo de coincidencias muy confiables – más lento pero más fiable", "lower": "Aprende de más ciclos; el modelo puede ser menos preciso" @@ -1949,40 +1905,25 @@ } }, "reason": { - "auto_label_confidence": "Confianza en el percentil 15 de {cycles} etiquetados automáticos que el usuario nunca corrigió ({p15}).", "completion_min_seconds": "La mitad de la duración p05 de los ciclos limpios ({minutes} min) en {cycles} ciclos; filtra los ciclos fantasma.{excl}", - "dead_zone_batch": "Se mantiene corto (ventana mediana de inestabilidad de arranque en {cycles} ciclos = {s}s) para detectar pronto un final real.", - "dead_zone_from_dip": "Según una caída de potencia temprana detectada a los {s}s.", - "end_energy_default": "Valor de referencia recomendado por defecto para el umbral de ruido de fin de ciclo.", - "end_energy_false": "Energía p95 de los finales falsos ({p95}Wh) en {cycles} ciclos, con un mínimo del 0.2% de la energía mediana del ciclo ({floor}Wh).", - "end_energy_no_false": "No hay finales falsos en {cycles} ciclos; con un mínimo del 0.2% de la energía mediana del ciclo ({floor}Wh).", - "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.", + "match_interval_floored": "El programa más corto ({shortest}s) limitaría este valor a {cap}s, por debajo del mínimo de {minimum}s, así que se usa el mínimo: {persistence} coincidencias seguidas tardan {budget}s, más del {pct}% de ese programa. La coincidencia solo se ejecuta cuando llega una lectura (mediana={median}s), así que 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.", "min_off_gap": "Según {gaps} intervalos entre ciclos observados (p05={p05}s). Mínimo del dispositivo: {floor}s.", "min_off_gap_bridge": "Dimensionado para cubrir el periodo de silencio más largo dentro de un ciclo: {span}s + 60s de margen, a partir de {spans} intervalos cubiertos en {cycles} ciclos limpios (se mantiene por debajo de su intervalo mínimo de {ceiling}s entre cargas consecutivas).", "min_power": "40% de la potencia activa más baja en el p05 ({p05}W) en {cycles} ciclos limpios, manteniendo el umbral de apagado por debajo del consumo real.{excl}", - "ml_auto_label": "Confianza más baja que el modelo de calidad aún consideró limpia (p10 de {cycles} ciclos limpios = {p10}).", - "ml_end_repeat": "{false}/{total} ciclos ({pct}%) tuvieron una pausa que el detector de fin puntuó >50%; se requieren {val} confirmación(es) de fin.", - "ml_off_delay": "Pausas confirmadas por el detector de fin: p95 {p95}s + 60s de margen, a partir de {pauses} pausas verificadas por el modelo en {cycles} ciclos (mínimo {floor}s).", "no_update_timeout": "Según la cadencia de actualización observada (p95={p95}s) * 20 (mín. 60s).", "off_delay_cadence": "Según la cadencia de actualización observada (p95={p95}s) * 5", "off_delay_device_floor": "Se ha usado el mínimo seguro específico del dispositivo para {device} ({floor}s).", "off_delay_generic_floor": "Se ha usado el mínimo seguro genérico ({floor}s).", "off_delay_pauses": "Dimensionado para superar las pausas reales: pausa intraciclo p95 {p95}s + 60s de margen, a partir de {pauses} pausas en {cycles} ciclos limpios (mínimo {floor}s).", - "profile_match_threshold": "Confianza p10 de {cycles} etiquetados automáticos correctos ({p10}); mínimo seguro para confirmar en directo.", - "sampling_interval": "Intervalo de actualización mediano observado en {cycles} ciclos limpios ({si}s).{excl}", - "smoothing_window": "Dimensionado para suavizar ~30s de lecturas con un muestreo de {si}s ({samples} muestras).", - "start_duration": "Se mantiene corto (~un intervalo de muestreo de {si}s) para que la detección empiece lo antes posible sin dejar de ignorar los picos de una sola muestra.", - "thr_batch": "Se mantiene justo por encima de la potencia activa más baja en el p05 en {cycles} ciclos ({p05}W) para captar un arranque lo antes posible y que el umbral de parada quede por debajo de la potencia de funcionamiento más baja de la máquina.", - "tol_per_profile": "p75 de la varianza de duración por perfil en {profiles} perfiles ({cycles} ciclos); los perfiles ajustados no se penalizan.", - "tol_pooled": "Según la varianza de duración agrupada de {cycles} ciclos etiquetados recientes (desviación p95={dev}).", - "watchdog": "Se mantiene lo más bajo que resulta seguro (justo por encima del intervalo de actualización p95 de {p95}s y al menos 2x el intervalo de muestreo de {median}s, mín. 30s) para detectar rápido los bloqueos sin paradas falsas." + "standby_floor": "Cada ciclo reciente seguía consumiendo {idle}W cuando terminó, por lo que un umbral por debajo de eso nunca podrá detectar que el electrodoméstico se apaga. Mantenido por encima; los {cycles} ciclos limpios registrados nunca bajaron por debajo durante más de {pause}s.", + "thr_batch": "Justo por encima de la potencia activa más baja (p05) en {cycles} ciclos ({p05}W), para detectar pronto los arranques y que el umbral de parada quede por debajo de la potencia de funcionamiento más baja.", + "watchdog": "Se mantiene lo más bajo que resulta seguro (justo por encima del intervalo de actualización p95 de {p95}s y al menos 2x el intervalo de muestreo de {median}s, mín. 30s) para detectar rápido los bloqueos sin paradas falsas.", + "end_energy_floor": "Elevado al mínimo que implican el umbral de parada ({stop} W) durante el retardo de apagado ({off_delay} s); por debajo, el ciclo solo puede terminar mediante una vía de respaldo.", + "max_duration_ratio_default": "Vuelta al valor predeterminado: un techo más bajo rechaza el programa correcto siempre que un ciclo dura más de lo habitual.", + "min_duration_ratio_default": "Vuelta al valor predeterminado: un mínimo más alto impide reconocer un ciclo en marcha hasta que está bien avanzado." }, "exclusions": { "summary": "Excluido(s) {total} ciclo(s) mal detectado(s): {parts}.", @@ -2000,13 +1941,18 @@ "no_power": "sin energía", "no_active_power": "sin energía activa" } - } + }, + "now_value": "(ahora {v})" }, "trend": { "down": "Tendencia a la baja", "up": "Tendencia ascendente" }, "maint": { + "salt": "Rellenar sal", + "rinse_aid": "Rellenar abrillantador", + "lint_filter": "Limpiar filtro de pelusa", + "condenser_clean": "Limpiar condensador", "bearing_service": "Servicio de rodamientos", "descale": "Descalcificar", "drum_clean": "Limpiar tambor", @@ -2018,12 +1964,14 @@ "status_tip": "Resultado del ciclo: Completado (fin natural), Interrumpido (caída brusca de potencia), Detenido forzadamente (manual) o Necesita revisión (comentario pendiente).", "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. 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.", + "energy_tip": "Energía total consumida (kWh), calculada a partir de la potencia a lo largo del tiempo.", + "cost_tip": "Costo de energía de este ciclo, fijado al terminar. Con Costo ponderado en el tiempo activado, cada momento se cobra a su propio precio; si no, al precio del final. Configure un precio en Ajustes para rellenar esto.", "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.", - "cost_dynamic_tip": "Ponderado en el tiempo: cobrado al precio vigente durante el ciclo (precio efectivo {price} {cur}/kWh)." + "cost_dynamic_tip": "Ponderado en el tiempo: cobrado al precio vigente durante el ciclo (precio efectivo {price} {cur}/kWh).", + "dbg_conf": "Conf.", + "dbg_mae": "MAE", + "dbg_corr": "Corr." }, "stat": { "avg": "prom.", @@ -2047,7 +1995,10 @@ "notify_finish": "Se enviaría una notificación de finalización.", "notify_milestone": "Se enviaría una notificación de hito.", "notify_held": "Se retuvo una notificación por las horas de silencio.", - "finished": "El ciclo alcanzó un estado final y terminó." + "finished": "El ciclo alcanzó un estado final y terminó.", + "match_reverted": "El programa volvió a la detección: su puntuación cayó muy por debajo de su propio máximo.", + "verified_pause": "La alineación en vivo confirmó (o liberó) una pausa dentro del programa. Mientras se mantiene, el ciclo no se da por terminado.", + "stalled": "El ciclo se quedó en consumo de espera más tiempo del que este programa pausa nunca en ese punto, con trabajo pendiente. Se muestra como En pausa (Atascado); el ciclo sigue abierto." }, "store": { "search_ph2": "Buscar por marca o modelo…", @@ -2056,11 +2007,7 @@ "programs_count": "Programas: {n}", "browse": "Explorar", "device": "Dispositivo", - "favorites": "Favoritos", - "search_ph": "Buscar por marca o modelo…", "no_results": "No se encontraron electrodomésticos coincidentes. Pruebe otra búsqueda.", - "n_programs": "{n} programas", - "n_cycles": "{n} ciclos", "no_programs": "Todavía no hay programas compartidos para este electrodoméstico.", "no_cycles": "Todavía no se han compartido ciclos de referencia para este programa.", "anon": "anónimo", @@ -2072,19 +2019,21 @@ "merge_target": "Combinar con el perfil", "description": "Descripción (opcional)", "contributed_by": "por {name}", + "website": "Sitio web de la tienda ↗", + "website_tip": "Abrir el sitio web de la tienda de la comunidad", + "favorites": "Favoritos", "rating_summary": "★ {avg} ({n})", "no_ratings": "Aún no hay valoraciones", - "website": "Sitio web de la tienda ↗", - "website_tip": "Abrir el sitio web de la tienda de la comunidad" + "unreachable": "No se pudo acceder a la tienda de la comunidad. Compruebe su conexión e inténtelo de nuevo." }, "settings": { "gear": { - "title": "Ajustes" + "title": "Preferencias" } }, "link": { - "manual": "Manual ↗", - "add_appliance": "Añadir su electrodoméstico" + "maintenance_supporter": "Maintenance Supporter en GitHub ↗", + "manual": "Manual ↗" }, "tip": { "add_brand": "¿La marca no aparece? Añádala al catálogo de la comunidad", @@ -2118,7 +2067,13 @@ "rebuild": { "envelopes": "Reconstruyendo envolventes" }, - "cancelling": "Cancelando..." + "cancelling": "Cancelando...", + "auto_label": { + "running": "Etiquetando ciclos automáticamente" + }, + "store_download": { + "running": "Descargando de la tienda" + } }, "setup": { "hdr": { @@ -2127,26 +2082,24 @@ }, "phase0": { "washer": "WashData ya está detectando tus ciclos. Graba tu primer ciclo para activar los nombres de programas y las estimaciones de tiempo.", - "dishwasher": "WashData está observando. Los lavavajillas tienen ciclos complejos – se recomienda encarecidamente grabar el primer ciclo. Si un ciclo detectado se alarga demasiado, usa el editor de ciclos para recortarlo antes de guardarlo como perfil.", + "dishwasher": "WashData está observando. Los ciclos de lavavajillas son complejos, así que grabe su primer ciclo. Si un ciclo detectado dura demasiado, recórtelo en el editor de ciclos antes de guardarlo como perfil.", "generic": "WashData está observando. Graba o etiqueta un ciclo detectado para empezar a crear perfiles." }, "phase1a": { - "labelled": "Buen comienzo – tu primer programa está guardado. Para datos más precisos, considera grabar tu próximo ciclo con el widget de grabación." + "labelled": "Buen comienzo: su primer programa está guardado. Para obtener los datos más limpios, grabe su siguiente ciclo con la grabadora." }, "phase1b": { - "recorded": "Tu grabación se guardó como {profile_name}. Ahora graba o etiqueta tus otros programas habituales para ampliar la cobertura." + "recorded": "Su grabación se guardó como {profile_name}. Ahora grabe o etiquete sus otros programas habituales." }, "phase1c": { - "verify": "Tienes {count} programas de la comunidad. Ejecuta un ciclo para verificar que WashData lo reconoce correctamente – la correspondencia mejorará a medida que tu dispositivo construya su propio historial." + "verify": "Tiene {count} programas de la comunidad. Ejecute un ciclo para comprobar que WashData lo reconoce; la coincidencia mejora a medida que su dispositivo crea su propio historial." }, "phase2": { - "cluster": "WashData ha observado {count} ciclos que no coinciden con ningún programa guardado – se parecen entre sí. ¿Deseas crear un nuevo perfil para ellos?", + "cluster": "WashData ha visto {count} ciclos parecidos que no coinciden con ningún programa guardado. ¿Crear un perfil nuevo para ellos?", "unmatched": "Tu último ciclo no coincidió con ningún programa guardado. ¿Es un nuevo programa?" }, "phase3": { - "suggestions": "WashData tiene recomendaciones de configuración basadas en tu historial de ciclos – revísalas para mejorar la precisión de detección.", - "groups": "Algunos de tus perfiles parecen el mismo programa a diferentes temperaturas. Organízalos en un grupo para una mejor correspondencia.", - "phases": "Añade fases de programa a {profile_name} para estimaciones más precisas del tiempo restante." + "suggestions": "WashData tiene sugerencias de ajustes basadas en su historial de ciclos. Revíselas para mejorar la detección." }, "phase4": { "healthy": "Este dispositivo está completamente configurado ({profile_count} perfiles)." @@ -2159,8 +2112,6 @@ "create_from_cluster": "Crear perfil a partir de estos ciclos", "create_profile": "Crear un perfil para este programa", "review_suggestions": "Revisar sugerencias", - "organise_profiles": "Organizar perfiles", - "configure_phases": "Configurar fases", "skip_step": "Omitir este paso", "skip_forever": "No volver a mostrar", "hide_guidance": "Ocultar guía", @@ -2171,6 +2122,7 @@ "min": "min", "entity_not_found": "Entidad no encontrada", "running_long": "Con retraso", + "stalled": "Atascado", "remaining": "restante", "progress": "progreso", "show_time_remaining": "Mostrar tiempo restante", @@ -2219,5 +2171,53 @@ "off_delay": { "effective": "Un ciclo terminado espera en realidad {eff} s, no {delay} s: la Brecha mínima de apagado ({gap} s) es mayor y tiene prioridad." } + }, + "access": { + "edit": "Edición", + "full": "Completo", + "inherit": "Heredar", + "none": "Ninguno (oculto)", + "read": "Lectura" + }, + "pg_label_reason": { + "no_winner": "Ningún programa coincide", + "below_floor": "Por debajo de la Confianza de aprendizaje", + "ambiguous": "Coincidencia marcada como ambigua", + "margin": "Demasiado cerca del segundo candidato", + "unknown_profile": "El programa ya no existe", + "not_evaluated": "No evaluado" + }, + "diagram": { + "accum": "acum.", + "auto": "auto", + "below_off": "debajo = apagado", + "confirmed": "confirmado", + "energy_counts": "cuenta la energía", + "feedback": "revisión", + "gap_below_min": "brecha bajo el mínimo = un ciclo", + "heat_phase": "fase de calentamiento", + "held_at_100": "mantenido en 100%", + "ignored": "ignorado", + "kept": "conservado", + "max": "máx.", + "min": "mín.", + "minus_tol": "-tol", + "no_match": "no coincide", + "no_updates": "sin actualizaciones", + "off_delay_wait": "retardo de apagado", + "off_gap": "brecha", + "plus_tol": "+tol", + "profile": "perfil", + "reading_interval": "intervalo típico de lectura", + "sensor_offline": "sensor caído: parada forzada", + "si": "IM", + "spike_ignored": "pico ignorado", + "spike_ignored_short": "pico: ignorado", + "start": "inicio", + "stop": "parada", + "tail_energy_resets": "cola > umbral: reinicia el temporizador", + "thr": "umbral", + "too_short": "demasiado corto", + "tumble_kept": "volteos conservados" } } diff --git a/custom_components/ha_washdata/translations/panel/et.json b/custom_components/ha_washdata/translations/panel/et.json index be112b79..de9e7fb0 100644 --- a/custom_components/ha_washdata/translations/panel/et.json +++ b/custom_components/ha_washdata/translations/panel/et.json @@ -3,6 +3,7 @@ "min": "min", "entity_not_found": "Olemit ei leitud", "running_long": "Töötab kaua", + "stalled": "Seiskunud", "remaining": "jäänud", "progress": "edenemine", "show_time_remaining": "Näita järelejäänud aega", @@ -48,16 +49,16 @@ } }, "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_tip": "Lõpu poole jääb see programm umbes {mins} minutiks vaikseks ja tarbib siis {secs} s jooksul umbes {watts} W. Nähtud {seen} korral {measured} mõõdetud tsüklist.", "quiet_tail": "vaikne lõpp ~{mins} min", - "artifact_tip": "Tuvastati {n} anomaaliat (nt uks avati tsükli keskel) – avage, et näha neid graafikul", + "artifact_tip": "Tuvastati {n} anomaaliat (nt uks avati tsükli keskel) - avage, et neid graafikul näha", + "artifact_tip_one": "Tuvastati {n} anomaalia (nt uks avati tsükli keskel) - avage, et seda graafikul näha", + "artifact_tip_other": "Tuvastati {n} anomaaliat (nt uks avati tsükli keskel) - avage, et neid graafikul näha", "auto": "(tuvastatud automaatselt)", "built_in_tag": "sisseehitatud", "declining": "↘ väheneb", "energy_low": "Tavalisest vähem energiat", "energy_spike": "Tavalisest rohkem energiat", - "fair_fit": "rahuldav kvaliteet", - "fair_fit_tip": "Mõõdukas sobivuse järjepidevus – mõnel sellele profiilile määratud tsüklil on madalamad usaldusskoorid. Täpsuse parandamiseks märkige rohkem tsükleid või salvestage profiil uuesti.", "feedback_requested": "Soovitakse tagasisidet", "golden_cycle": "Salvestatud võrdlustsükkel", "duration_outlier": "{n} ebatavalise pikkusega tsüklit", @@ -66,27 +67,36 @@ "needs_review": "Vajab ülevaatamist", "overrun": "Jooks kauem kui tavaliselt", "poor_fit": "kehv kvaliteet", - "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", + "restart_gap_tip": "{n} HA taaskäivituslünka: võimsusjäljes on katkestusi", + "restart_gap_tip_one": "{n} HA taaskäivituslünk: võimsusjälg on katkenud", + "restart_gap_tip_other": "{n} HA taaskäivituslünka: võimsusjäljes on katkestusi", "reviewed": "Üle vaadatud", "steady": "→ stabiilne", "underrun": "Lõppes tavalisest kiiremini", - "using_defaults": "Tarnitud vaikeseadete kasutamine", - "using_tuned": "Häälestatud raskuste kasutamine", "overrun_ratio": "({x}x oodatust)", "underrun_ratio": "({pct}% oodatust)", - "awaiting_tip": "Ootab kogukonna heakskiitu", - "awaiting_n": "Ootab heakskiitu · {n} kinnitust", - "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.", - "armed": "(kehtib järgmise tsükli kohta)" + "armed": "(kehtib järgmise tsükli kohta)", + "awaiting_tip": "Ootab kogukonna heakskiitu", + "awaiting_n": "Ootab heakskiitu · {n} kinnitust", + "awaiting": "Ootab heakskiitu", + "approved": "Heaks kiidetud", + "matcher_unused": "Pole veel ühegi teie tsükliga sobitatud", + "matcher_used": "Sobitatud {n} teie tsükliga", + "matcher_used_one": "Sobitatud {n} teie tsükliga", + "matcher_used_other": "Sobitatud {n} teie tsükliga", + "matcher_used_tip": "Selle seadme tsüklid, mille WashData sobitas iseseisvalt selle imporditud programmiga. Import, mis kunagi ei sobitu, ei pruugi teie seadmele sobida; selle võib eemaldada. Loendatakse kohalikult; poodi ei saadeta midagi." }, "btn": { + "pg_preset_delete": "Kustuta eelseadistus", + "pg_preset_load": "Laadi eelseadistus", + "pg_preset_load_tip": "Asenda katseala väärtused selle eelseadistusega (aktiivseid seadeid ei muudeta)", + "pg_preset_save": "Salvesta eelseadistusena", + "pg_preset_save_tip": "Salvesta kõik allolevad väärtused selle seadme nimelise eelseadistusena", "reset_zoom": "Lähtesta suum", "set_brand_model": "Määra bränd ja mudel", "refresh_catalog": "Värskenda kataloogi", @@ -97,12 +107,11 @@ "add_timer": "+ Lisa taimer", "apply_all": "Rakenda kõik", "apply_label": "Rakenda silt", - "apply_set_b": "Rakenda komplekt B", "apply_split": "Rakenda poolitus", "apply_trim": "Rakenda kärpimine", "auto_detect_split": "Automaatne tuvastamine", "auto_label_cycles": "Automaatsed märgistamise tsüklid", - "auto_label_cycles_tip": "Määra automaatselt profiilide nimed märgistamata tsüklitele, mille sobivuse usaldus ületab läve", + "auto_label_cycles_tip": "Anna nimi märgistamata tsüklitele, mille vaste usaldus ületab läve.", "cancel": "Tühista", "clear_debug": "Silumisandmete kustutamine", "clear_splits": "Tühjenda", @@ -112,8 +121,6 @@ "convert_to_automation": "Teisendage automatiseerimiseks", "correct": "Paranda…", "create": "Loo", - "create_from_cluster": "Loo profiil {count} tsükli põhjal", - "create_profile": "+ Loo profiil", "delete": "Kustuta", "delete_group": "Kustuta grupp", "delete_group_tip": "Kustuta ainult see grupp – liikmeprofiilid jäetakse alles", @@ -124,22 +131,19 @@ "done": "Valmis", "edit": "Muuda", "export": "Ekspordi", - "export_json": "Ekspordi JSON-i", "force_stop": "Sundpeatus", "force_stop_tip": "Lõpeta praegune tsükkel kohe ja märgi see sundpeatunuks", "from_template": "Mallist ▾", "ignore": "Ignoreeri", "import_json": "Import JSON-ist", - "import_overwrite": "Import (kirjutab andmed üle)", "inspect": "Kontrollige", "label": "Sildista", - "load": "Laadi", "load_more": "Laadi rohkem", "manage": "Halda", "merge": "Ühendage", "new_automation": "+ Uus automaatika", "new_group": "+ Uus grupp", - "new_group_tip": "Rühmita peaaegu identsed profiilid (sama kuju/kestus, erinev temperatuur või tsentrifuugimine), et sobitamine valib nende vahel usaldusväärselt", + "new_group_tip": "Rühmita peaaegu identsed profiilid (sama kuju, erinev temperatuur või tsentrifuugimine), et sobitamine eristaks neid.", "new_phase": "+ Uus etapp", "new_profile": "+ Uus profiil", "new_profile_tip": "Loo uus programmide profiil olemasolevast märgistatud tsüklist või salvestusest", @@ -147,36 +151,25 @@ "on_cycle_started": "Tsükkel algas", "pause_cycle": "Paus", "pause_cycle_tip": "Peata käimasolev tsükkel – seade jätkab sealt, kus pooleli jäi", - "pg_apply_device": "Rakenda seadmele", - "pg_autofill": "Täida automaatselt", - "pg_load_calibrated": "Laadi kalibreeritud (ML) ({n})", - "pg_load_calibrated_tip": "Laeb ML-kalibreeritud seaded katseala alistustena", "pg_load_live": "Laadi aktiivsed seaded", "pg_load_live_tip": "Loe integratsiooni praegused seaded uuesti ja tühista kõik katseala muudatused", "pg_load_suggested": "Laadi soovitused ({n})", "pg_load_suggested_tip": "Laeb automaatse häälestaja praegused soovitused katseala alistustena", - "pg_preset_delete": "Kustuta eelseadistus", - "pg_preset_load": "Laadi eelseadistus", - "pg_preset_load_tip": "Asenda katseala väärtused selle eelseadistusega (aktiivseid seadeid ei muudeta)", - "pg_preset_save": "Salvesta eelseadistusena", - "pg_preset_save_tip": "Salvesta kõik allolevad väärtused selle seadme nimelise eelseadistusena", "pg_publish_all": "Salvesta seadetesse ({n})", "pg_publish_one": "Salvesta \"{label}\" integratsiooni seadetesse", - "play": "Esita", "process": "Töötle", "process_history": "Töötle kohe", "process_recording": "Salvesta salvestus", "process_tip": "Salvesta salvestatud jälg uue või olemasoleva profiilina", "rebuild": "Ümbrikute ümberehitamine", "rebuild_envelope": "Ümberehitage ümbrik", - "rebuild_tip": "Arvuta uuesti kõigi profiilide eeldatav võimsusümbrik (min/max riba) nende märgistatud tsüklite põhjal – käivitage pärast uute tsüklite märgistamist või vanade parandamist", + "rebuild_tip": "Arvuta iga profiili eeldatav võimsusriba uuesti selle märgistatud tsüklite põhjal. Käivita pärast tsüklite märgistamist või parandamist.", "rec_start_tip": "Alusta seadme võimsusjälje salvestamist – käivita vahetult enne tsükli käivitamist", "rec_stop": "Peatus", "rec_stop_tip": "Peata salvestamine ja hoia jäädvustatud jälg ülevaatamiseks", "record": "Alustage salvestamist", "refresh": "Värskenda", "refresh_diag_tip": "Laadi salvestusstatistika uuesti", - "refresh_logs_tip": "Laadi uusimad logikirjed uuesti", "refresh_settings_tip": "Laadi seaded serverist uuesti", "relabel": "Märgista uuesti ({count})", "remove": "Eemalda", @@ -184,7 +177,6 @@ "reset": "Lähtesta", "reset_ml_models_tip": "Loobuge peenhäälestatud mudelitest ja minge tagasi sisseehitatud mudelite juurde. WashData saab neid hiljem uuesti õppida.", "reset_to_builtin": "Lähtestage sisseehitatud mudelitele", - "reset_to_defaults": "Lähtestage vaikeseadetele", "resume_cycle": "Jätka tsüklit", "resume_cycle_tip": "Jätka peatatud tsüklit", "revert_settings": "Hülga muudatused", @@ -197,8 +189,6 @@ "run_analysis": "🔍 Käivitage soovituste analüüs", "run_analysis_tip": "Analüüsige oma salvestatud tsükleid kohe ja värskendage häälestussoovitusi", "run_auto_label": "Käivitage automaatne silt", - "run_both": "Käivita A vs B", - "run_simulation": "Käivita simulatsioon", "save": "Salvesta", "save_access_control": "Salvesta juurdepääsukontroll", "save_group": "Salvesta grupp", @@ -210,10 +200,8 @@ "save_settings": "Salvesta sätted", "select": "Valige", "select_all": "Vali kõik", - "select_last20": "Viimased 20", "select_none": "Tühista", "show_only": "Näita ainult", - "skip_setup": "Jäta seadistus vahele", "split": "Poolita", "stop": "Peata", "submit_correction": "Esitage parandus", @@ -222,36 +210,31 @@ "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.", + "unlabel_selected_tip": "Eemaldab valitud tsüklitelt sildi. Need jäävad sinu ajalukku, kuid ei kujunda enam seda profiili.", "use": "Kasutage", "wipe_all": "Pühkige kõik andmed", "delete_selected": "Kustuta valitud ({n})", - "group_suggest": "Rühm {n}: {members}", "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", "import": "Impordi", "disconnect": "Katkesta ühendus", "connect_github": "Ühenda GitHub", - "save_appliance": "Salvesta seade", "share_to_store": "Jaga poodi", "share": "Jaga", - "confirm_appliance": "Kinnita see seade", - "confirmed": "Kinnitatud", "browse_community_setups": "Sirvi kogukonna seadistusi", "download_device": "Laadi see seadistus alla", "share_device": "⬆ Jaga seda seadet", - "share_device_tip": "Jaga seda kodumasinat ja selle salvestatud võrdlustsükleid kogukonna poodi, et teised sama masina omanikud saaksid neid kasutusele võtta", + "share_device_tip": "Jaga seda seadet ja selle võrdlustsükleid kogukonna poodi, et sama masina omanikud saaksid neid kasutada.", "share_n": "Jaga {n} tsüklit", "export_selected": "Ekspordi (vali andmed)", "export_all": "Kiirekspordi kõik", - "import_raw": "Täpsem: asenda kõik JSON-ist", "download_export": "Laadi eksport alla", "analyze_import": "Analüüsi faili", "import_selected": "Impordi valitud", "back": "Tagasi", "mute_suggestion": "Lõpeta selle seade soovitamine", "reset_muted": "Lähtesta vaigistatud", + "reset_phase_tip": "Võta muudatus tagasi ja taasta sisseehitatud faas", "import_power_history": "Impordi võimsuse ajalugu", "hist_read_recorder": "Loe Home Assistantist", "hist_scan": "Otsi tsükleid", @@ -260,7 +243,15 @@ "save_total_cycles": "Salvesta koguarv", "clear": "Tühjenda", "add_brand": "Lisa bränd", - "add_appliance": "Lisa seade" + "log_done_task": "Märgi tehtuks: {task}", + "add_task": "Lisa ülesanne", + "add_appliance": "Lisa seade", + "retry": "Proovi uuesti", + "confirm_appliance": "Kinnita see seade", + "confirmed": "Kinnitatud", + "create_profile_from_gap": "Loo profiil", + "undo_import": "Võta viimane import tagasi", + "pg_rerun": "Uuenda simulatsiooni" }, "note": { "off_delay": { @@ -268,10 +259,6 @@ } }, "conflict": { - "anti_wrinkle_exit": { - "exit": "Peab olema alla peatumise läve ({stop} W); muidu kortsudevastane väljumise võimsus eiratakse", - "stop": "Peab olema üle kortsudevastase väljumise võimsuse ({exit} W)" - }, "anti_wrinkle_max": { "max": "Peab olema üle käivitumise läve ({start} W); muidu kortsudevastane kestuse piirang minnakse mööda", "start": "Peab olema alla kortsudevastase maksimaalse võimsuse ({max} W)" @@ -279,10 +266,10 @@ "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." + "power_sensor": "See on seadme Võimsusandur, mitte hind. Tsüklite maksumus arvestataks vatinäidu järgi kWh kohta; tühjendage see väli.", + "energy_sensor": "See on seadme Energiaarvesti, mitte hind. Tsüklite maksumus arvestataks arvesti näidu järgi kWh kohta; tühjendage see väli.", + "device_class": "See andur mõõdab suurust {dc}, mitte hinda kWh kohta. Tühjendage see väli või valige tariifiandur.", + "unit": "Selle anduri ühik on {unit}, mitte hind kWh kohta. 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", @@ -317,8 +304,8 @@ "threshold": "Peab olema alla peatumise läve ({stop} W), et jõustuda" }, "pump_stuck": { - "duration": "Peab olema väiksem kui ilma uuendusteta aegumine ({to} s), et kinnijäämise alarm käivituks enne, kui valvekoer tsükli tühistab", - "timeout": "Peab ületama pumba kinnijäämise kestuse ({ps} s), et kinnijäämise alarm käivituks enne tsükli sundpeatust" + "duration": "Peab olema väiksem kui Ilma Uuenduseta Aegumine ({to} s), et kinnijäämise alarm käivituks esimesena", + "timeout": "Peab olema suurem kui Pumba Kinnijäämise Kestus ({ps} s), et kinnijäämise alarm käivituks esimesena" }, "start_dur": { "sampling": "Proovivõtu intervall ületab käivitumise kestuse ({sdt} s); ühe proovi hüpped võivad avada tsükli", @@ -334,10 +321,15 @@ "watchdog": { "interval": "Peaks olema vähemalt 2x proovivõtu intervall ({si} s)", "sampling": "Proovivõtu intervall peaks olema maksimaalselt pool valvekoera intervallist ({wi} s)" + }, + "end_energy": { + "energy": "Liiga range, kui Peatumise Lävi on {w} W ja Väljalülitumise Viivitus {d} s; tsükkel saab lõppeda ainult varumehhanismi kaudu", + "stop": "Lõpu Energia ({e} Wh {d} s jooksul) lubab ainult {p} W" } }, "hdr": { - "ab_comparison": "A/B seadete võrdlus", + "custom_tasks": "Sinu ülesanded", + "maintenance_supporter": "Vajad rohkem?", "access_control": "Juurdepääsu kontroll", "add_maintenance": "Lisa hooldussündmus", "automation_delete": "Kustutage see automatiseerimine", @@ -346,28 +338,22 @@ "automation_review_this_cycle": "Vaadake see tsükkel üle", "automations": "Automatiseerimised", "clear_debug": "Tühjenda silumisjäljed", - "cycle_simulator": "Tsükli simulaator", "diagnostics_pref": "Diagnostika", "display": "Ekraan", - "dtw_inspector": "DTW inspektor", "export_import": "Eksport / Import", - "getting_started": "Alustamine", "live_power": "Elav jõud", "logs": "Logid", - "logs_diagnostics": "Diagnostika", "maintenance": "Hooldustoimingud", "maintenance_log": "Hoolduslogi", "maintenance_reminders": "Hoolduse meeldetuletused", "manual_recording": "Käsitsi salvestamine", - "manual_recording_tip": "Käita tsükkel tahtlikult, samal ajal kui WashData salvestab võimsusjälje. Alusta vahetult enne seadme käivitamist, peata kui see lõpetab, seejärel töötle salvestamiseks nimega profiilina.", + "manual_recording_tip": "Salvesta tsükkel tahtlikult: vajuta Algus vahetult enne seadme käivitamist ja Peata, kui see lõpetab, seejärel Töötle, et salvestada see profiilina.", "ml_learned": "Mida WashData on õppinud", - "ml_matching_tuning": "Programmi sobitamise peenhäälestus", "ml_settings_card": "Seaded", "ml_smart_learning": "Nutikas õppimine", "my_preferences": "Minu eelistused", "no_automations": "Sellele seadmele ei viita veel ükski automaatika.", "panel_settings": "Paneeli sätted", - "param_sweep": "Parameetri pühkimine", "pg_settings_source": "Seadete allikas", "phase_catalog": "Faaside kataloog", "playground": "Katseala", @@ -376,11 +362,8 @@ "status": "Olek", "status_graph": "Olekugraafik", "storage_stats": "Salvestusstatistika", - "test_history": "Testi ajalool", "toggle_sidebar": "Koduabilise külgriba sisse- ja väljalülitamine", - "tools_and_data": "Tööriistad ja andmed", "wipe_history": "Pühkige ajalugu", - "recommendations": "Soovitused ({n})", "pg_detection_params": "Tuvastuse seaded", "pg_outcome": "Simulatsiooni tulemus", "pg_across_cycles": "Sinu tsüklite lõikes", @@ -388,7 +371,8 @@ "online_account": "Kogukonna pood ja veebifunktsioonid", "import_power_history": "Võimsuse ajaloo import", "service_status": "Hoolduse olek", - "online_account_tab": "Veeb ja kogukond" + "online_account_tab": "Veeb ja kogukond", + "live_match_debug": "Reaalajas vaste silumine" }, "health": { "fair": "Rahuldav profiili kvaliteet", @@ -396,12 +380,17 @@ "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_tip": "Asukoht sobitatud programmi salvestatud kõveral: {pct}%. Leitakse tsükli joondamisel profiiliga, seega püsib õige ka pikema või lühema käivituse korral. Värskeneb, kui seade on vaikne, seega 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", + "task_name": "Ülesanne", + "every_cycles": "Iga (tsüklit)", + "every_days": "Iga (päeva)", + "new_task_name": "Uus ülesanne", + "days_since_service": "{since} / {total} päeva", "actions": "Tegevused", "activity": "Tegevus", "administrators": "Administraatorid", @@ -409,10 +398,8 @@ "aria_cycle_chart": "Tsükli võimsuskõver", "aria_envelope_chart": "Profiili võimsusümbriku graafik", "aria_phase_chart": "Faaside redaktori graafik", - "aria_playground_chart": "Tsükli võimsuskõver lohistatavate tuvastuslävedega", "aria_power_chart": "Võimsustarbimise graafik", "aria_spaghetti_chart": "Kattuvad tsükli võimsuskõverad", - "aria_sweep_chart": "Parameetri pühkimise tulemuste graafik", "artifact_anomaly": "Anomaalia", "artifact_high_power": "Kõrge võimsus", "artifact_interruption": "Katkestus", @@ -426,149 +413,99 @@ "cohesion_good": "ühtesus {pct}%", "cohesion_low": "⚠ madal ühtesus {pct}%", "compare_profiles": "Võrrelge profiilidega", - "concurrency": "Samaaegsus", "confidence": "Usaldus", "confidence_threshold": "Usalduse lävi", "correct_duration": "Õige kestus (min, valikuline)", "correct_profile": "Õige profiil", - "correlation": "Korrelatsioon", "cost": "Maksumus", "create_new_profile": "+ Loo uus profiil…", "current_phase": "Praegune faas", - "curves": "Kõverad", "cycle": "Tsükkel", "cycle_date_display": "Tsükli kuupäeva kuva", - "cycle_health": "Tsükli tervis", - "cycle_index": "tsükli indeks →", - "cycles": "Tsüklid", "cycles_count": "Tsüklid", "cycles_lc": "tsüklit", - "dashed": "kriipsjoon", "date": "Kuupäev", "debug_traces": "Silumisjäljed", - "default": "Vaikimisi", "default_access_level": "Vaiketase kasutajatele, keda pole allpool loetletud", "default_other": "Vaikimisi (muud seadmed)", "default_tab": "Vaikimisi vahekaart paneeli avamisel", - "delta": "Δ", "description": "Kirjeldus", - "dtw_blend_w": "Segamise kaal", - "dtw_blended": "Segatud skoor", - "dtw_ddtw": "DDTW skoor", - "dtw_l1": "L1 skoor", - "dtw_matched_profile": "(tsükli sobitatud profiil)", "duration": "Kestus", - "duration_agreement": "Kestuse kokkulangevus", - "emphasis": "Rõhuasetus", "enable_access_control": "Luba kasutajapõhine juurdepääsu kontroll", "energy": "Energia", - "energy_agreement": "Energia kokkulangevus", - "event_timeline": "Sündmuste ajajoon", "event_type": "Sündmuse tüüp", "expected": "Oodatav", + "suspected_profile": "{name} (oletatav)", + "runner_up_profile": "{name} (teine kandidaat)", "expected_duration": "Eeldatav kestus (min)", "file_kb": "Fail (kB)", - "final_score": "Lõppskoor", "from": "Alates", "gap_s": "Vahe(d)", "group_name": "Rühma nimi", "head_trim": "Kärbe algusest (s)", - "health": "Tervis", "hide_tabs": "Peida vahekaardid mitteadministraatorite jaoks", - "in_use": "Kasutuses", "json_data": "JSON-andmed", "label": "Vastendatud programm", "last": "Viimased", "last_checked": "Viimati kontrollitud:", "load_from_file": "Laadi failist", - "mae_score": "MAE skoor", "manual_duration": "Käsitsi kestus (min, valikuline)", "match": "Vaste", "members": "Liikmed", - "metric": "Mõõdik", "mode_existing_profile": "Lisa olemasolevale profiilile", "mode_new_profile": "Loo uus profiil", "models_fine_tuned": "(peenhäälestatud mudelid: {count})", - "n_classic_suggestions": "{n} klassikaline", - "n_ml_suggestions": "{n} ML", "n_selected": "{n} valitud", - "n_tuning_suggestions": "{n} häälestussoovitus", - "na": "pole", + "n_tuning_suggestions": "{n} häälestussoovitust", + "n_tuning_suggestions_one": "{n} häälestussoovitus", + "n_tuning_suggestions_other": "{n} häälestussoovitust", "new_profile_name": "Uus profiilinimi", "notes": "Märkmed", - "of": "/", - "outcome": "Tulemus", "panel_default_tab": "Vaikevahekaart", "panel_language": "Paneeli keel", "per_cycle_short": "tsükkel", - "pg_autofill_tip": "Täida vahemik automaatselt praegusest väärtusest ({lo}–{hi})", "pg_best_value": "Parim leitud väärtus", "pg_current_value": "Praegune väärtus:", "pg_drag_hint": "Lohista joont graafikul", - "pg_dtw": "DTW", "pg_ev_ending": "Lõpetamine", "pg_ev_running": "Töötab", "pg_group_advanced": "Erijuhud", "pg_group_detection": "Tuvastamise päästikud", "pg_group_timing": "Ajastusreeglid", "pg_idle": "Jõude", - "pg_last_run": "Viimane käivitus: {date}", - "pg_match_score": "Vaste skoor", - "pg_pct_of": "{p}% {total}-st", "pg_poor_match": "Halb vaste", "pg_preset": "Katseala eelseadistus", "pg_preset_name": "Uue eelseadistuse nimi", "pg_preset_none": "Vali eelseadistus…", "pg_strong_match": "Tugev vaste", - "pg_tile_ambiguous": "Ebaselge", - "pg_tile_ambiguous_tip": "Tsüklid, kus kaks programmi on peaaegu võrdsed", - "pg_tile_detected": "Tuvastatud", - "pg_tile_detected_tip": "Tsüklid, kus võimsus ületas käivitusläve piisavalt kaua, et registreeruda töötavana", - "pg_tile_matched": "Sobitatud", - "pg_tile_matched_tip": "Tsüklid, mis on õigesti seostatud tuntud programmiga", - "pg_time_left": "Aega jäänud", "pg_weak_match": "Nõrk vaste", "phase_name": "Faasi nimi", "name": "Nimi", "phase_timeline": "Faaside ajajoon", "power": "Võimsus", "profile": "Profiil", - "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. Valige programm enne seadme käivitamist ja see rakendatakse kohe, kui algab järgmine tsükkel.", + "program_tip": "Valige, millise programmiga praegune tsükkel sobitatakse. Automaatne tuvastamine valib parima vaste. Lukustage programm, kui automaatne tuvastamine eksib; enne käivitamist valitud programm rakendub järgmisele tsüklile.", "progress": "Edusammud", "raw_socket": "Toores signaal", - "read_only": "Kirjutuskaitstud", "ref_cycle": "Võrdlustsükkel (valikuline)", - "refresh_interval": "Värskendusintervall (id)", "remaining": "Ülejäänud", "reminder_every": "Tuleta meelde iga (tsüklit)", "remove_label": "- Eemalda silt -", "rename_profile": "Nimeta profiil ümber", - "replay_duration": "Taasesitus", "resulting_profile": "Tulemuseks olev profiil", "save_mode": "Salvestamisrežiim", - "score": "Skoor", - "score_breakdown": "Skoori jaotus", "seconds_from_start": "Sekundid algusest", "select_profile": "Valige Profiil", "setting": "Säte", - "settings_a": "Seaded A (praegune)", - "settings_a_short": "A", "settings_advanced": "Täpsemalt", - "settings_b": "Seaded B (test)", - "settings_b_short": "B", "settings_basic": "Põhiline", "settings_detail_level": "Seadete detailsuse tase", - "settings_overrides": "Seadete alistamised", "show_debug": "Kuva reaalajas vaste silumiskaart lehel Olek (kindlus, ebaselgus, parimad kandidaadid)", "show_expected": "Kuva eeldatav kõvera ülekate (sobiv profiil, oranž)", "show_raw": "Kuva töötlemata pistikupesa lülitit reaalajas võimsusgraafikus", "sparkline": "Hiljutine tsükli kestuse trend", - "stage2": "Etapp 2 – põhisarnasus", - "stage3": "Etapp 3 – DTW", - "stage4": "Etapp 4 – kokkulangevus", "status": "Olek", "tail_trim": "Kärbe lõpust (s)", "tap_to_dismiss": "puuduta graafikut sulgemiseks", @@ -579,7 +516,6 @@ "ungrouped": "Grupeerimata", "unlabelled": "Märgistamata", "unlabelled_paren": "(sildistamata)", - "warp_path": "Moondamistee", "input": "Sisend:", "start": "Algus", "end": "Lõpp", @@ -604,9 +540,8 @@ "n_shown": ", {n} kuvatud", "cycles_title": "Tsüklid ({n})", "pg_detecting": "Tuvastamine", - "pg_best_marker": "Parim: {v}", "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", + "pg_time_left_model_tip": "Hinnatud programmi faaside järgi, mitte kindel loendur", "phase": "Faas", "pg_mode_history": "Testi ajalooga", "pg_ev_detected": "Tuvastatud", @@ -638,12 +573,6 @@ "pg_sweep_param": "Parameeter", "pg_objective": "Eesmärk", "pg_steps": "Sammud", - "pg_stress_group": "Tühikäigu lõpetamise test", - "pg_stress_idle_auto": "Auto", - "pg_stress_idle_w": "Tühikäigu tase (W)", - "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_obj_match": "Vaste täpsus", "pg_obj_endtiming": "Lõpuaja täpsus", "pg_obj_falseend": "Valelõppude määr", @@ -652,12 +581,8 @@ "pg_mode_optimize": "Optimeeri", "pg_group_matching": "Programmi sobitamine", "pg_zoom_hint": "topeltklõps suumi lähtestamiseks", - "aria_playground_chart2": "Interaktiivne tsükli võimsusgraafik: hõljuta aja/võimsuse lugemiseks, keri suumimiseks, lohista liigutamiseks", "pg_alert_timeout_end": "Lõpetati aegumisega, mitte ennustuse põhjal", "pg_alert_indefinite": "Töötaks lõputult", - "pg_alert_stress_cap": "Ohutuspiir saavutatud", - "pg_alert_stress_ok": "Tühikäigu lõpetamine: tsükkel peatatud", - "pg_alert_stress_warn": "Tühikäigu tarbimine ületab peatumisläve", "task_pg_history": "Testi ajaloos", "task_pg_sweep": "Optimeeri", "task_reprocess": "Uuesti töötlemine", @@ -667,16 +592,9 @@ "pg_recent_runs": "Hiljutised käitused", "pg_load_run": "Laadi see käitus uuesti", "pg_match_confidence": "Vaste kindlus", - "pg_envelope_fit": "Sobivus mähisjoonega", - "pg_envelope_fit_tip": "Kui täpselt tsükkel püsib selle profiili salvestatud võimsuse mähisjoone sees, teistsugune vaatenurk kui vaste kindlus.", "n_imported_note": ", {n} imporditud", "enable_online": "Luba veebifunktsioonid", - "store_brand": "Seadme bränd", - "store_model": "Seadme mudel", "anonymous": "Anonüümne", - "rate_quality": "Hinda kvaliteeti", - "rate_n": "{n} tähte", - "approved_only": "Ainult heaks kiidetud", "adopt_settings": "Võta ka seaded kasutusele", "flags": "Embleemid", "include_phase_map": "Kaasa faasikaart", @@ -693,7 +611,6 @@ "cat_custom_phases": "Kohandatud faasid", "cat_profile_groups": "Profiilirühmad", "cat_settings": "Tuvastus- ja sobitamisseaded", - "cat_matching_config": "Sobitamise häälestus", "cat_ml_models": "ML-mudelid", "cat_feedback": "Tagasiside- ja ülevaatussildid", "cat_suggestions": "Soovitused", @@ -715,7 +632,6 @@ "hist_csv_data": "CSV-andmed", "hist_from_recorder": "Või loe see Home Assistantist", "hist_since": "Alates", - "days": "päeva", "hist_keep": "Säilita see tsükkel", "hist_looks_complete": "lõpetatud", "peak_power_short": "Tipp", @@ -735,7 +651,40 @@ "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" + "correct_total_cycles": "Paranda koguarv", + "hist_reason_already_recorded": "kattub tsükliga, mille WashData on juba salvestanud", + "share_consent": "Kinnitan, et need tsüklid jõudsid tavapäraselt lõpuni ilma katkestusteta", + "show_internal": "Näita sisemisi seadeid", + "show_internal_tip": "Seaded, mida WashData teie eest häälestab (proovivõtt, valvekoer, sobitamise sagedus, läved). Kuvatakse, kui sellel seadmel on oma väärtus, konflikt või soovitus.", + "rate_n": "{n} tähte", + "rate_quality": "Hinda kvaliteeti", + "pg_autolabel": "Automaatne märgistus", + "pg_label_review": "Palub ülevaatamist", + "pg_ev_match_reverted": "Vaste tühistatud", + "pg_ev_verified_pause": "Kinnitatud paus", + "n_samples": "{n} proovi", + "n_samples_one": "{n} proov", + "n_samples_other": "{n} proovi", + "n_cycles": "{n} tsüklit", + "n_cycles_one": "{n} tsükkel", + "n_cycles_other": "{n} tsüklit", + "energy_per_cycle": "{v}/tsükkel", + "energy_total": "kokku {v}", + "automation_off": "(väljas)", + "ref_cycle_none": "Puudub", + "share_name": "Näita minu nime jagatud sisul", + "cat_backfill_cycles": "Imporditud võimsuse ajaloost leitud tsüklid", + "pg_compare_profile": "Võrdle profiiliga", + "pg_obj_early": "Varajase lõpu määr", + "pg_obj_endlag": "Lõpu viivitus (mediaan)", + "pg_obj_split": "Tükeldamise määr", + "pg_sweep_cycles": "Hiljutised tsüklid", + "cycle_context": "Salvesti ajalugu tsükli ümber", + "cycle_context_off": "Väljas", + "cycle_context_minutes": "{min} min", + "cycle_context_series": "Salvesti ajalugu", + "before_start": "Enne algust", + "after_end": "Pärast lõppu" }, "log": { "all_levels": "Kõik tasemed", @@ -745,11 +694,9 @@ }, "ml": { "builtin_models": "● Sisseehitatud mudelite kasutamine", - "fit_fair": "Rahuldav", "fit_good": "Hea", "fit_slight": "Kerge", "fit_strong": "Tugev", - "fit_weak": "Nõrk", "fit_word": "sobima", "personalized": "● Selle masina jaoks kohandatud", "better_than_baseline": "{pct}% parem kui lähtehinnang ({metric})", @@ -757,21 +704,29 @@ "trend_declining_tip": "Selle mudeli sobivus on viimaste ülekontrollide jooksul halvenenud, rohkemate tsüklite ülevaatamine võib aidata sel uuesti õppida.", "trend_steady_tip": "Selle mudeli sobivus on viimaste ülekontrollide jooksul jäänud enam-vähem stabiilseks.", "cap_blurb": { - "end": "Teadmine, millal tsükkel on tegelikult lõppenud", - "live_match": "Käimasoleva programmi kiirem tuvastamine", - "quality": "Valesti tuvastatud või rikutud tsüklite märkamine", - "remaining_time": "Järelejäänud aja ennustamine", "total_energy": "Koguenergia ja kulu ennustamine" }, "cap_label": { - "end": "Tsükli lõpu tuvastus", - "live_match": "Programmi tuvastus", - "quality": "Tsükli kvaliteedikontroll", - "remaining_time": "Järelejäänud aja hinnang", "total_energy": "Energiahinnang" }, - "metric_auc": "AUC {auc} valideerimisandmetel", - "metric_mae": "viga {model} vs. baasjoon {baseline}" + "metric_mae": "viga {model} vs. baasjoon {baseline}", + "fine_tuned_at": "peenhäälestatud {when}", + "last_run_not_promoted": "Viimane kontroll ({when}): {reason}", + "never": "mitte kunagi", + "skip_reason": { + "fit_failed": "Mudeli õpetamine ebaõnnestus: {error}", + "holdout_too_small": "Liiga vähe tsükleid, et uut mudelit õiglaselt testida: testimiseks sai kõrvale panna {held_out}, vaja on {min}.", + "not_better_than_incumbent": "Uus mudel ei olnud parem kui kasutusel olev (viga {model} vs. {incumbent}), seega jäi praegune alles.", + "not_better_than_naive": "Uus mudel ei olnud lihtsast hinnangust selgelt parem (viga {model} vs. {naive}).", + "unknown": "Midagi uut ei õpitud.", + "insufficient_rows": "Kasutatavaid tsükleid pole veel piisavalt: seni {count} märgistatud puhast tsüklit.", + "insufficient_rows_one": "Kasutatavaid tsükleid pole veel piisavalt: seni {count} märgistatud puhas tsükkel.", + "insufficient_rows_other": "Kasutatavaid tsükleid pole veel piisavalt: seni {count} märgistatud puhast tsüklit." + }, + "trained_unknown": "teadmata ajal", + "held_out_count": "mõõdetud {count} valideerimistsüklil", + "held_out_count_one": "mõõdetud {count} valideerimistsüklil", + "held_out_count_other": "mõõdetud {count} valideerimistsüklil" }, "modal": { "auto_label": "Automaatsed sildistamise tsüklid", @@ -780,7 +735,6 @@ "cycle": "Tsükkel", "edit_group": "Redigeeri profiilirühma", "edit_phase": "Redigeeri faasi", - "import_config": "Impordi konfiguratsioon", "label_cycle": "Sildi tsükkel", "merge_cycles": "Ühendage {n} tsüklit", "new_group": "Uus profiilirühm", @@ -795,6 +749,8 @@ "delete_automation_title": "Kustuta automaatika", "delete_automation_msg": "Kas kustutada automaatika \"{name}\" Home Assistantist? Seda ei saa tagasi võtta.", "remove_legacy_title": "Eemalda vananenud toimingud", + "reset_phase_msg": "Kas võtta faasi \"{name}\" muudatus tagasi ja taastada sisseehitatud faas? Programmid säilitavad oma vahemikud.", + "reset_phase_title": "Lähtesta faas", "remove_legacy_msg": "Kas eemaldada vananenud kohandatud toimingud? Need lõpetavad tsükli sündmustele reageerimise. Seda ei saa paneelilt tagasi võtta.", "discard_recording_title": "Visake salvestis ära", "discard_recording_msg": "Kas visata salvestatud salvestis ära? Seda ei saa tagasi võtta.", @@ -815,32 +771,44 @@ "store_share_device": "Jaga seda seadet", "export_select": "Eksport - vali andmed", "import_wizard": "Import - vali andmed", - "history_import": "Võimsuse ajaloo import" + "history_import": "Võimsuse ajaloo import", + "apply_suggestions_title": "Rakenda soovitused", + "remove_task_title": "Eemalda ülesanne", + "remove_task_msg": "Kas eemaldada meeldetuletus \"{task}\"? Selle kirjed hoolduslogis jäävad alles.", + "apply_suggestions_msg": "Kas muuta neid seadeid ({n})? Integratsioon laaditakse pärast salvestamist uuesti.", + "discard_settings_title": "Kas loobuda salvestamata muudatustest?", + "discard_settings_msg": "Salvestamata seadete muudatused: {n}. Kas lahkuda vahekaardilt Seaded ja muudatustest loobuda?", + "profile_title": "Profiil · {name}", + "undo_import_msg": "Selle seadme andmed ja seaded taastatakse viimase impordi ({when}) eelsesse olekusse. Pärast seda salvestatud tsüklid ja tehtud muudatused lähevad kaotsi.", + "undo_import_ok": "Võta import tagasi", + "undo_import_title": "Võta viimane import tagasi" }, "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.", + "pg_preset_delete_confirm": "Kas kustutada eelseadistus \"{name}\"?", + "pg_preset_limit": "Eelseadistuste piir on täis ({n})", + "pg_preset_overwrite": "Kas kirjutada eelseadistus \"{name}\" üle?", "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.", - "refresh_catalog_hint": "Kogukonna brändide ja seadmete loendid on vahemälus, et jagatud pood püsiks oma päevase päringulimiidi piires. Värskenda, et näha teiste lisatud või heaks kiidetud kirjeid.", + "store_sibling_hint": "Sinu mudelile pole midagi jagatud? Sama brändi sarnane mudel on tavaliselt hea algus.", + "store_declare_appliance": "Ütle WashDatale, milline seade sul on, et näha teiste selle jaoks jagatud seadistusi. Või kirjuta ülal bränd ja vaata ringi.", + "refresh_catalog_hint": "Kogukonna brändide ja seadmete loendid on vahemälus, et säästa poe päevast limiiti. Värskenda, et näha teiste lisatud või heaks kiidetud kirjeid.", "head_trim_hint": "Mitu sekundit algusest eemaldada", "appliance_monitor": "Seadmete monitor", "artifact_dip_detail": "Langes alla tavapärase võimsusriba umbes {n}s jooksul.", "artifact_footer": "Ülaltoodud graafikul esile tõstetud. Need on mööduvad artefaktid (nt uks avati tsükli keskel), mitte tingimata probleemid.", "artifact_header": "Selle tsükli jooksul tuvastati {n} anomaaliat", - "artifact_pause_detail": "Võimsus langes peaaegu nulli umbes {n}s jooksul ja taastus seejärel – tõenäoliselt avati uks tsükli keskel või tsükkel peatati.", + "artifact_header_one": "Selle tsükli jooksul tuvastati {n} anomaalia", + "artifact_header_other": "Selle tsükli jooksul tuvastati {n} anomaaliat", + "artifact_pause_detail": "Võimsus langes umbes {n}s ajaks peaaegu nulli ja taastus siis. Tõenäoliselt avati uks või tsükkel peatati.", "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": "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.", + "cleanup_intro": "Kõik märgistatud tsüklid on kohakuti. Märgi kõrvalekalded ja eemalda nende märgistus (jäävad ajalukku, kuid ei kujunda enam seda profiili) või kustuta need.", "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)", - "compare_profiles_tip": "Katke ülaltoodud diagrammil teised profiiliümbrikud, et näha, milline neist selle tsükliga kõige paremini sobib.", + "compare_profiles_tip": "Kuvage diagrammil teisi profiile, et näha, milline sobib selle tsükliga kõige paremini.", "compare_selected_cycles": "Valitud tsüklid (tahked) – näita/peida", - "consider_new_profile": "Kaaluge uue profiili loomist.", - "coverage_gap": "viimastel tsüklitel pole sobivat profiili ({pct}% viimasest 30-st).", - "coverage_gap_similar_cycles": "{count} sarnast märgistamata tsüklit leitud – loo profiil, et neid sobitama hakata.", "cycles_deleted": "{count} tsüklit kustutatud", "enough_data": "Piisavalt andmeid õppimiseks ({current}/{min} tsüklit).", "export_description": "Vali täpselt, millised profiilid, tsüklid, seaded ja muu JSON-i eksportida, või analüüsi fail ja impordi ainult soovitud osad.", @@ -848,30 +816,26 @@ "feedback_prompt": "Kinnitage, et see oli õige, parandage programmi või ignoreerige.", "feedback_relabel_hint": "Selle tsükli uuesti sildistamine lahendab ka selle.", "filter_by_profile": "Filtreeri profiili järgi…", - "group_modal_help": "Rühmitage sama kujuga programme, mis erinevad temperatuuri/tsentrifuugimise poolest (kestus võib erineda). Matching annab rühmale ühe kandidaadi ja valib seejärel kõige sobivama liikme. Valige vähemalt 2; ülekate näitab, kui sarnased need on.", + "group_modal_help": "Rühmitage sama kujuga programme, mis erinevad temperatuuri või tsentrifuugimise poolest. Sobitamine hindab rühma ühena ja valib siis kõige sobivama liikme. Valige vähemalt 2; ülekate näitab, kui sarnased need on.", "group_not_cohesive": "Need profiilid ei ole piisavalt sarnased, et neid usaldusväärselt rühmitada, nii et sobitamine käsitleb neid üksikult, kuni eemaldate kõrvalekalde või jagate rühma.", "group_preview_hint": "Märkige 2+ liiget nende võimenduskõverate eelvaatamiseks ja võrdlemiseks.", - "import_intro": "Laadige alla eksporditud fail või kleepige JSON-fail.", - "legacy_actions_title": "{count} pärand kohandatud toiming töötab endiselt", + "legacy_actions_title": "{count} pärand kohandatud toimingut töötavad endiselt", + "legacy_actions_title_one": "{count} pärand kohandatud toiming töötab endiselt", + "legacy_actions_title_other": "{count} pärand kohandatud toimingut töötavad endiselt", "live_chart_loading": "Reaalajas võimsuse diagramm kuvatakse näitude saabumisel.", "loading": "Laadimine…", "loading_curve": "Kõvera laadimine…", "loading_settings": "Seadete laadimine…", "log_buffer_hint": "Uusimad enne · puhverdab viimased 500 ha_washdata kirjet pärast taaskäivitamist · lohistage alumist serva suuruse muutmiseks.", - "maintenance_advisory": "Kestuse/energia suurenemine võib viidata seadme hooldusvajadusele (nt katlakivi eemaldamine, filtri puhastamine).", "maintenance_due": "Hooldus vajalik: {items}", "maintenance_intro": "Registreeri selle seadme hooldustööd ja saa meeldetuletus, kui iga ülesanne on taas vajalik.", "maintenance_load_error": "Hooldusandmete laadimine ebaõnnestus: {error}", "maintenance_requires_access": "Hooldus ja eksport/import nõuavad täielikku juurdepääsu.", - "manual_duration_hint": "Profiili keskmine/eeldatav tsükli pikkus, mida kasutatakse järelejäänud aja hinnangute andmiseks. Muuda selle määramiseks; muutmata jättes säilib praegune väärtus.", - "matching_tuning_intro": "Õppides reguleerib WashData ka seda, kui palju programmide sobitamine kaalub kuju versus kestus ja energia.", + "manual_duration_hint": "Profiili eeldatav tsükli pikkus, mida kasutatakse järelejäänud aja hinnangutes. Praeguse väärtuse säilitamiseks jäta see muutmata.", "merge_intro": "Valitud tsüklid ühendatakse üheks (kronoloogiline järjestus; lüngad täidetud 0 W-ga). Valige saadud profiil.", "ml_intro": "WashData tarnitakse nutikate mudelitega, mis töötavad karbist välja.", "ml_learned_intro": "Selle masina jaoks peenhäälestatud mudelid.", - "ml_loading": "ML laadimine…", "ml_settings_intro": "Kaks sõltumatut lülitit: üks rakendab mudeleid tsükli töötamise ajal, teine võimaldab WashDatal neid aja jooksul teie masinale täpselt häälestada.", - "name_first_program": "Sul on piisavalt tsükleid – pane oma esimesele programmile nimi, et alustada sobitamist.", - "near_duplicate_cluster": "Tuvastati peaaegu duplikaatprofiili klaster. Rühmitamine võimaldab sobitamisel usaldusväärselt valida sarnasuste vahel (nt sama programm erineval temperatuuril/tsentrifuugimisel).", "no_cycles_match": "Ükski tsükkel ei vasta praegusele filtrile.", "no_cycles_profile": "Sellel profiilil pole tsükleid.", "no_cycles_selected": "Vali esmalt vähemalt üks tsükkel.", @@ -896,73 +860,52 @@ "no_suggestions": "Aktiivseid soovitusi pole.", "notify_services_hint": "Kasutage {entity} teenuse ID-sid (mitme korral komadega eraldatult). Malli muutujad: {vars}.", "old_actions_warning": "Seadistatud vana toimingute redaktoriga (nüüd eemaldatud). Need käivituvad endiselt tsüklisündmustele, kuid neid ei saa siin enam redigeerida. Teisendage need tavaliseks automaatikaks või eemaldage need.", - "onboarding_progress": "{n} / 3 tsüklit jälgitud", - "onboarding_watching": "Kasuta oma seadet tavapäraselt – WashData jälgib. Pärast 3 tsüklit algab programmide sobitamine.", "pending_feedback": "Ootab tuvastamise tagasisidet", "performance_trend": "Toimivuse trend ({n} tsüklit)", - "pg_analysis_empty": "Vaste analüüsi nägemiseks laadi tsükkel.", "pg_apply_confirm": "Kas rakendada parim väärtus: {label} = {value}?", - "pg_canvas_empty": "Vali ülalt tsükkel ja vajuta seejärel Laadi, et näha selle võimsuskõverat.", - "pg_chart_caption": "Tuvastus-/vastemäär {param} väärtuste lõikes", "pg_ctrl_intro": "Väärtused lähtuvad selle seadme aktiivsetest integratsiooni seadetest. Muudatused jäävad katsealale, kuni salvestad need seadetesse.", "pg_live_unavailable": "Aktiivsed seaded pole saadaval, kuvatakse vaikeväärtused.", "pg_load_live_confirm": "Kas tühistada katseala muudatused ({n}) ja laadida integratsiooni praegused seaded uuesti?", - "pg_match_rate": "{pct}% vastemäär {total} tsükli puhul", "pg_matches_live": "Vastab aktiivsetele seadetele", "pg_n_changed": "Erineb aktiivsetest seadetest: {n}", "pg_sugg_none": "Kõik soovitused vastavad juba praegustele seadetele", - "pg_preset_delete_confirm": "Kas kustutada eelseadistus \"{name}\"?", - "pg_preset_limit": "Eelseadistuste piir on täis ({n})", - "pg_preset_overwrite": "Kas kirjutada eelseadistus \"{name}\" üle?", "pg_publish_one_confirm": "Kas salvestada {label} = {value} selle seadme seadetesse?", "pg_restart_note": "Simulatsioonitööriistade lubamiseks taaskäivita Home Assistant.", - "pg_sim_progress": "{done} / {total} tsüklit", - "pg_starting": "Alustamine…", - "pg_stress_synthetic": "tühikäigu simulatsioon algab siit", - "pg_sweep_intro": "Mis siis, kui {param} oleks teistsugune? Testi {steps} väärtust oma viimase {cycles} tsükli ulatuses, et leida seade, mille juures kõige rohkem tsükleid sobitatakse õigesti.", - "pg_sweep_step": "Samm {done} / {total}", - "pg_verdict_bad": "Vajab tähelepanu: paljud tsüklid jäävad tuvastamata.", - "pg_verdict_good": "Hästi häälestatud: enamik tsükleid tuvastatakse ja sobitatakse õigesti.", - "pg_verdict_ok": "Vastuvõetav: mõned tsüklid jäid vahele. Proovi käivitusläve alandada.", "phase_catalog_intro": "Nimetatud tsükli segmendid (eelpesu, kuumutamine, tsentrifuugimine…). Määrake need juhtpaneelilt profiilile.", "phase_ranges_intro": "Faasivahemikud (minutid alates tsükli algusest) kattuvad keskmise kõveraga. Reaalajas eelvaate kuvamiseks muutke väärtusi.", - "playground_intro": "Turvaline katseala varasemate tsüklite uuesti esitamiseks erinevate seadetega, kahe seadistuse kõrvuti võrdlemiseks ja selle uurimiseks, kuidas tsükkel profiiliga joondub. Miski siin ei muuda su otseandmeid enne, kui sa selle sõnaselgelt rakendad.", - "playground_needs_restart": "Katseala tööriistade lubamiseks taaskäivita Home Assistant.", - "preferences_admin": "Eelistused, paneel ja juurdepääsu kontroll", - "preferences_adv": "Eelistused", + "playground_intro": "Proovi seadeid oma tegelike tsüklite andmetel. Miski siin ei muuda kasutusel olevaid seadeid enne, kui need rakendad.", "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), arvutage tsüklite maksumus salvestatud energiahindade järgi ümber ja arvutage tsükli tervis ümber. Käivitage see pärast hulga ülevaatuste tegemist.", + "process_history_hint": "Sobitab kõik salvestatud tsüklid uuesti, värskendab soovitusi, treenib ML-mudeleid uuesti (kui lubatud), arvutab tsüklite maksumuse salvestatud hindade järgi ümber ja arvutab tsüklite tervise uuesti. Käivitage see pärast hulga ülevaatusi.", "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.", - "rbac_hint": "Kui see on välja lülitatud, on igal koduabilise kasutajal täielik juurdepääs (vaikeseade). Administraatoritel on alati täielik juurdepääs ja nad saavad kõiki hallata.", - "recent_logs": "Viimased ha_washdata kirjed", + "rbac_hint": "Väljas: igal Home Assistanti kasutajal on täielik juurdepääs (vaikimisi). Administraatoritel on alati täielik juurdepääs ja nad saavad kõiki hallata.", "recording_in_progress": "Salvestamine on pooleli", "reminders_intro": "Näita paneelil meeldetuletust nii mitu tsüklit pärast viimast hooldust. Meeldetuletuse väljalülitamiseks jäta tühjaks või pane 0.", "restart_gap_footer": "Esile tõstetud graafikul. Võimsusandmed puuduvad nende intervalide jaoks; sobitamine kasutas ainult tegelikke näite.", - "restart_gap_header": "{n} HA taaskäivituslünk selle tsükli ajal", + "restart_gap_header": "{n} HA taaskäivituslünka selle tsükli ajal", + "restart_gap_header_one": "{n} HA taaskäivituslünk selle tsükli ajal", + "restart_gap_header_other": "{n} HA taaskäivituslünka selle tsükli ajal", "restart_gap_item": "{dur} lünk", - "review_confirm_help": "Kontrollige, kas see tsükkel tuvastati õigesti. Teie arvustused treenivad mudelit teie masinas – mida rohkem tsükleid kinnitate, seda parem on sobivus ja seisukord. Piisab kiirest Heast/Halbusest.", + "review_confirm_help": "Kas see tsükkel tuvastati õigesti? Tsüklite kinnitamine parandab teie masina sobitamist. Piisab kiirest hinnangust Hea või Halb.", "review_in_settings": "Vaadake üle seadetes", "review_notes_placeholder": "Märkused (valikuline)", "review_notes_tip": "Vabateksti märkmed teie enda jaoks. Ei kasutata sobitamisel ega treenimisel.", - "review_profile_tip": "Programm see tsükkel on märgistatud kui. Kui automaatselt tuvastatud programm oli vale, parandage see siin – sildistamine õpetab tulevaste tsüklite jaoks sobitamist.", - "review_quality_tip": "Kui puhas see tsükkel on. Hea = selle programmi õpikunäide; Halb = tuvastatud, kuid mürarikas või ebatüüpiline; Kasutamatu = valesti tuvastatud (liidetud, kärbitud või võlts). Juhib terviseskoori ja seda, millised tsüklid on mudeli treenimiseks lubatud.", - "review_recorded_tip": "Märkige see oma programmi jaoks käsitsi valitud võrdlustsükliks – sama roll kui käsitsi salvestatud tsüklil. Võrdlustsüklid jäetakse alati alles, külvatakse sobiv mall ja neid ei jäeta kunagi puhastamise käigus maha. (See on \"kuldne\" / salvestatud lipp; mõlemad on samad.)", - "review_tags_tip": "Valikulised lipud, mis kirjeldavad, mis selle tsükliga valesti läks, nii et koolitus ja puhastamine võivad seda arvesse võtta.", + "review_profile_tip": "Programm, millega see tsükkel on märgistatud. Kui see on vale, parandage see siin; sildid õpetavad sobitamist.", + "review_quality_tip": "Kui puhas see tsükkel on. Hea: selle programmi õpikunäide. Halb: tuvastatud, kuid mürarikas või ebatavaline. Kasutuskõlbmatu: valesti tuvastatud (liidetud, liiga vara katkenud või võlts). Mõjutab terviseskoori ja seda, millised tsüklid mudelit treenivad.", + "review_recorded_tip": "Märkige see oma programmi käsitsi valitud võrdlustsükliks, nagu käsitsi salvestatud tsükkel. Võrdlustsüklid jäetakse alati alles, on sobitamise malli aluseks ja puhastus ei eemalda neid kunagi.", + "review_tags_tip": "Valikulised lipud selle kohta, mis valesti läks, et treenimine ja puhastus saaksid seda arvestada.", "review_to_cycles": "Avage tsüklite ülevaatuse järjekord", "samples_decimated": "Kuvatakse {shown}/{total} näidet (kuvamiseks hõrendatud; tipud säilitatud). Lai vahe siin on hõrendamine, mitte puuduvad andmed.", - "saving_triggers_reload": "Salvestamine käivitab integratsiooni uuesti laadimise. HA olemid võivad hetkeks kuvada kui kättesaamatud.", + "saving_triggers_reload": "Salvestamine laadib integratsiooni uuesti. Olemid võivad hetkeks kuvada kättesaamatuna.", "search_placeholder": "Otsinguseaded…", "see_recorder": "Vaadake salvesti vidinat allpool", "setting_changed": "Muudetud {old} → {new} kuupäeval {date}", "settings_basic_note": "Kuvatakse olulisi seadeid. Täieliku loendi nägemiseks vali Täpsemalt.", - "shape_drift_advisory": "⚠ Kuju triivib", - "shape_drift_detail": "Selle profiili võimsusstruktuur on aja jooksul muutunud – võimalik seadme kulumine või vajalik hooldus (nt katlakivi eemaldamine, filtri puhastamine).", "show_all_settings": "Kuva kõik seaded", - "showing_suggestions": "Kuvatakse seade {count} koos soovitustega.", - "split_intro": "Jaotuspunkti lisamiseks või eemaldamiseks või tühikäigu automaatseks tuvastamiseks klõpsake graafikul. Iga saadud segment võib saada oma profiili.", - "storage_diagnostics": "Salvestusstatistika, hooldus, eksport/import", + "showing_suggestions": "Kuvatakse {count} soovitustega seadet.", + "showing_suggestions_one": "Kuvatakse {count} soovitustega seade.", + "showing_suggestions_other": "Kuvatakse {count} soovitustega seadet.", + "split_intro": "Klõpsake graafikul, et lisada või eemaldada jaotuspunkt, või tuvastage need automaatselt tühikäigu pauside järgi. Iga osa võib saada oma profiili.", "sug_staged": "Määratud {key} = {val}. Salvestage rakendamiseks.", "toast_auto_detect_enabled": "Automaatne tuvastamine lubatud", "toast_auto_label_complete": "Automaatne märgistamine lõpetatud", @@ -979,7 +922,6 @@ "toast_no_automation": "Siia ei saa automatiseerimist luua", "toast_profile_name_required": "Profiili nimi on nõutav", "toast_program_set": "Programm seatud: {program}", - "toast_rebuild_failed": "Ülesehitamine ebaõnnestus: {error}", "toast_resume_failed": "Jätkamine ebaõnnestus: {error}", "toast_revert_failed": "Tagasipööramine ebaõnnestus: {error}", "toast_reverted": "{key} taastatud", @@ -990,96 +932,83 @@ "trend_energy_up": "Energiatrend tõuseb ({pct}%/tsükkel) – hiljutine keskmine {avg}", "trim_destructive_confirm": "See säilitab tsüklist ainult {pct}% ja seda ei saa tagasi võtta. Kas jätkata?", "trim_intro": "Lohistage punaseid käepidemeid või sisestage väärtused. Kõik väljaspool akent eemaldatakse.", - "tuning_suggestions_available": "Vaadeldud tsüklitest on saadaval {count} häälestamise soovitus. Need kuvatakse asjakohaste väljade kõrval.", + "tuning_suggestions_available": "Vaadeldud tsüklitest on saadaval {count} häälestussoovitust. Need kuvatakse asjakohaste väljade kõrval.", + "tuning_suggestions_available_one": "Vaadeldud tsüklitest on saadaval {count} häälestussoovitus. See kuvatakse asjakohase välja kõrval.", + "tuning_suggestions_available_other": "Vaadeldud tsüklitest on saadaval {count} häälestussoovitust. Need kuvatakse asjakohaste väljade kõrval.", "unsure_detected_prefix": "WashData pole kindel, et see tuvastas", - "updated": "Uuendatud", "warmup_badge": "Õpib veel ({done}/{needed} tsüklit)", - "warmup_detail": "See profiil vajab {needed} märgistatud tsüklit enne automaatse sobitamise algust. Iga kinnitatud tsükkel aitab sellel õppida.", + "warmup_detail": "Sobitamine juba töötab. WashData palub teil kinnitada selle profiili esimesed {needed} tsüklit, enne kui hakkab ise märgistama.", "wipe_history_warning": "Kustutage jäädavalt kõik tsüklid ja profiilid. Seda ei saa tagasi võtta.", "compare_overlay_tip": "Aseta õpitud profiilide võimsusvahemikud peale, et näha, millist programmi iga tsükkel meenutab.", - "duration_trend_up_tip": "Kestus tõuseb ({pct}%/tsükkel)", - "duration_trend_down_tip": "Kestus langeb ({pct}%/tsükkel)", - "energy_trend_up_tip": "Energia tõuseb ({pct}%/tsükkel)", - "energy_trend_down_tip": "Energia langeb ({pct}%/tsükkel)", - "performance_trending": "Jõudluse suundumus", - "pg_select_cycle_hint": "Vali tsükkel ja klõpsa ↺ laadimiseks", - "pg_sweep_progress": "Samm {done} / {total}", - "pg_analysis_hint": "Vali tsükkel ja klõpsa ↺ analüüsi laadimiseks.", "diagnostics_load_failed": "Diagnostikat ei õnnestunud laadida: {error}", - "manual_duration_ref_hint": "Kasutatakse ainult siis, kui võrdlustsüklit pole valitud. Võrdlustsükkel määrab kestuse oma pikkuse järgi.", + "manual_duration_ref_hint": "Kasutatakse ainult ilma võrdlustsüklita; võrdlustsükkel määrab kestuse oma pikkuse järgi.", + "duration_learned_hint": "Arvutatud selle programmi tsüklitest (kõrvalekalded välja jäetud) ja uueneb pärast iga uut tsüklit. Muutmiseks kärpige või eemaldage kõrvalekalduvad tsüklid.", "toast_convert_rolled_back": "Teisendamine ebaõnnestus ja võeti tagasi (ühtegi automatiseerimist ei jäänud): {error}", "toast_convert_orphan": "Automatiseerimine loodi, kuid vanade toimingute kustutamine ebaõnnestus. Ärge proovige uuesti: eemaldage vanad toimingud käsitsi, et vältida dubleerivat automatiseerimist.", - "advisory_duration_trend_up": "'{name}' tsüklid muutuvad järk-järgult pikemaks (umbes +{pct}% tsükli kohta). Kui seadme käitumine on muutunud, salvesta või ehita see profiil uuesti.", - "advisory_energy_trend_up": "'{name}' tarbib järk-järgult rohkem energiat (umbes +{pct}% tsükli kohta) - kui see on ootamatu, tasub seade üle kontrollida.", - "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.", + "advisory_duration_trend_up": "'{name}' tsüklid muutuvad järjest pikemaks (umbes +{pct}% tsükli kohta). Kui seade on muutunud, salvesta või ehita see profiil uuesti.", + "advisory_energy_trend_up": "'{name}' tarbib iga tsükliga rohkem energiat (umbes +{pct}% tsükli kohta). Kui see on ootamatu, kontrolli seadet.", + "advisory_poor_health": "'{name}' sobitub halvasti: selle hiljutised tsüklid varieeruvad palju või sobituvad nõrgalt. Vaata selle tsüklid üle või salvesta see uuesti, et sobitamine ja hinnangud püsiksid täpsed.", + "advisory_shape_drift": "'{name}' on oma algsest võimsuskõverast kõrvale kaldunud, võib-olla katlakivi või kulumise tõttu. Kaalu selle uuesti salvestamist hiljutiste tsüklitega.", + "advisory_shape_drift_corr": "'{name}' on oma algsest võimsuskõverast kõrvale kaldunud (korrelatsioon {corr}), võib-olla katlakivi või kulumise tõttu. Kaalu selle uuesti salvestamist hiljutiste tsüklitega.", + "advisory_unmatchable": "'{name}' ei saa kunagi sobituda: selle taga pole ühtegi võimsusandmetega tsüklit. Parandamiseks märgista või salvesta selle programmi tsükkel.", + "advisory_unmatchable_no_duration": "'{name}' ei saa kunagi sobituda: selle tsüklil pole kasutatavat kestust. Salvesta see programm uuesti või määra selle eeldatav kestus.", "pg_no_alerts": "Selles käivituses ei tuvastatud probleeme.", "pg_override_active": "Kasutatakse su muudetud seadeid vs praegused", "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.", "pg_sweep_intro2": "Leia seade, mis su hiljutistes tsüklites eesmärki kõige paremini täidab. Miski ei muutu enne, kui selle rakendad.", - "pg_running_history": "Tsüklite taasesitus…", - "pg_running_sweep": "Väärtuste testimine sinu tsüklites…", "pg_row_load_hint": "Laadi see tsükkel ülalolevale graafikule", - "pg_history_intro2": "Taasesita oma hiljutised tsüklid läbi tegeliku tuvastaja ja sobitaja ülaltoodud sätetega. Klõpsa mis tahes real, et laadida see tsükkel graafikule; muuda sätet, et näha enne/pärast võrdlust.", + "pg_history_intro2": "Taasesita hiljutised tsüklid läbi tegeliku tuvastaja ja sobitaja ülaltoodud sätetega. Klõpsa real, et laadida see graafikule; muuda sätet, et võrrelda enne ja pärast.", "pg_simulating": "Tsükli simuleerimine…", - "pg_canvas_empty2": "Vali ülalt tsükkel ja vajuta Käivita, et seda simuleerida. Seejärel hõljuta väärtuste lugemiseks, keri suumimiseks ja lohista liigutamiseks.", "pg_apply_settings_confirm": "Kas salvestada need {n} sätet sellesse seadmesse? {list}", - "pg_stress_terminated_detail": "Stabiliseerus umbes {idle} W tühikäigule -- tsükkel lõppes {h} h {m} min hiljem põhjusel {reason}.", - "pg_stress_above_threshold_detail": "Tühikäigu tarbimine umbes {idle} W on peatumislävel ({stop} W) või sellest kõrgemal -- tsüklit ei registreeritud kunagi vaikseks. Paranda, tõstes väärtust stop_threshold_w.", - "pg_stress_hit_cap_detail": "Tsükkel töötas {h} h {m} min peatumata -- ohutuspiir peatas selle sunniviisiliselt. Tühikäigu tarbimine {idle} W vs peatumislävi {stop} W.", "no_logs_match": "Ükski logikirje ei vasta nendele filtritele.", "pg_analysis_hint2": "Vali tsükkel ja vajuta Käivita, et laadida vaste analüüs.", - "pg_analysis_empty2": "Vajuta Käivita, et näha vaste analüüsi.", - "imported_readonly": "Imporditud kogukonna poest. Kuvatakse viitena ja sobitamiseks. Seda ei arvestata sinu statistikas ega saa muuta.", + "imported_readonly": "Imporditud kogukonna poest. Kasutatakse viiteks ja sobitamiseks, ei lähe sinu statistikasse ega ole muudetav.", "merge_no_imports": "Imporditud salvestisi ei saa päris tsükliga liita. Tühista nende valik, et liitmine õnnestuks.", - "store_enable_hint": "Luba seadetes veebifunktsioonid, et sirvida ja importida kogukonna võrdlustsükleid.", - "advisory_duration_outlier": "'{name}' sildiga on {n} tsüklit, mille pikkus on selle tavapärasest kestusest liiga kaugel, et sellega üldse sobituda (halvim: {ratio}x). Tõenäoliselt on need valesti sildistatud või hõlmab üks salvestus kahte tsüklit. Sildista need ümber või jaga pooleks, et need ei moonutaks selle programmi ajahinnangut.", + "store_enable_hint": "Kogukonna võrdlustsüklite sirvimiseks ja importimiseks lülitage veebifunktsioonid sisse jaotises Eelistused > Veeb ja kogukond.", + "advisory_duration_outlier": "{n} tsüklit sildiga '{name}' on selle tavapikkusest liiga kaugel, et kunagi sobituda (halvim: {ratio}x). Tõenäoliselt on need valesti sildistatud või sisaldavad kahte käivitust. Sildista need ümber või jaga pooleks, et need ei moonutaks selle ajahinnangut.", "standby_above_stop_title": "Tsükli lõppedes tarbis endiselt {idle} W", "standby_above_stop_sub": "{n} viimasest {total} tsüklist. Väljalülitumise viivitus hakkab kulgema alles siis, kui võimsus langeb alla Peatumise Läve ({stop} W), seega lõppevad tsüklid hiljaks jäädes või tuleb need käsitsi peatada.", - "online_intro": "Jaga ja laadi alla võrdlustsükleid teiste WashData kasutajatega. Kõik veebifunktsioonid on vabatahtlikud ja vaikimisi välja lülitatud.", "store_share_intro": "Laadi see võrdlustsükkel üles, et teised sama seadmega kasutajad saaksid seda kasutada. See vaadatakse üle enne avalikuks muutumist.", - "online_intro_global": "Sirvi ja jaga võrdlussalvestisi teiste WashData kasutajatega ning kinnita seadmekirjeid. Üks ühendus kehtib kogu su WashData integratsioonile; seadme bränd ja mudel määratakse seadmepõhiselt jaotises Põhiseaded. Kõik veebifunktsioonid on vabatahtlikud ja vaikimisi välja lülitatud.", + "online_intro_global": "Sirvi ja jaga võrdlussalvestisi teiste WashData kasutajatega. Üks ühendus kehtib kõigile sinu WashData seadmetele; bränd ja mudel määratakse iga seadme jaoks jaotises Põhiline. Veebifunktsioonid on välja lülitatud, kuni sa need sisse lülitad.", "online_unavailable": "Veebifunktsioonid ei ole selles serveris saadaval.", "store_picker_offline": "Luba veebifunktsioonid seadete hammasrattast, et valida kogukonna kataloogist.", "pick_brand_first": "Vali kõigepealt seadme bränd.", - "model_not_found": "Pole veel kataloogis.", - "connect_to_confirm": "Ühenda seadete hammasrattast, et kinnitada või hinnata.", + "custom_task_limit": "Oled jõudnud {max} ülesande piirini.", + "custom_tasks_intro": "Lisa kõik muu, mida soovid meelde tuletada. Ülesanne on vajalik, kui kumbki intervall täitub; jäta üks tühjaks või pane 0, et kasutada ainult teist.", + "maintenance_due_sensor_hint": "Binaarandur \"Hooldus vajalik\" on sees, kui mõni ülesanne on tähtaegne; kasuta seda oma teavitusautomaatikates.", + "maintenance_supporter_intro": "Maintenance Supporter on eraldi integratsioon, mis teeb palju rohkem: mis tahes seade või ülesanne, ajakavad aja, kasutuse või anduri järgi, teavitused ja ajalugu. See saab kasutada WashData tsükliloendurit.", "adopt_settings_hint": "Kirjuta üle selle seadme tuvastus- ja sobitamislävede ühiskasutuses olevad. Teie teatisi, olemeid ja energiahinda ei muudeta kunagi.", - "include_settings_hint": "Jaga selle seadme tuvastus- ja sobitamislävesid (mitte teie teatisi, olemeid ega energiahinda). Kasutusele võtjad otsustavad, kas neid rakendada.", + "include_settings_hint": "Jaga selle seadme tuvastus- ja sobitamislävesid (mitte teavitusi, olemeid ega energiahinda). Teised otsustavad, kas neid rakendada.", "onboard_download": "Uus seade? Võtke kasutusele valmis seadistus (programmid, võrdlustsüklid ja faasid) teiselt WashData kasutajalt, kellel on sama kodumasin.", - "share_consent": "Kinnitan, et need tsüklid lõpetati normaalselt ilma katkestusteta", "share_device_none": "Jagatavaid tsükleid pole veel. Märkige salvestatud või käsitsi valitud tsükkel esmalt Tsüklid-vahekaardil võrdlustsükliks (⭐).", "share_guideline_naming": "Nimetage iga profiil täpselt nii, nagu on näidatud kodumasina nupp- või ekraanivaatest (nt 'Cotton 40', 'Eco 60').", - "share_guideline_quality": "Jagage ainult tsükleid, mis lõpetati normaalselt -- ei mingeid katkestusi tsükli keskel, ukse avamise sündmusi ega võimsuslünki.", + "share_guideline_quality": "Jagage ainult tsükleid, mis lõppesid normaalselt: ilma katkestuste, ukse avamiste ja toitehäireteta.", "share_guideline_review": "Teie üleslaadimine algab ootelolekuna ja muutub avalikuks, kui piisavalt kogukonna liikmeid selle kinnitab.", "share_guidelines_title": "Enne jagamist", - "store_download_device_intro": "Võtke kasutusele iga jagatud programm ja selle võrdlustsüklid oma seadmel. Teie enda salvestatud tsükleid ja statistikat ei mõjutata.", + "store_download_device_intro": "Lisage oma seadmele kõik jagatud programmid ja nende võrdlustsüklid. Teie enda tsükleid ja statistikat see ei mõjuta.", "store_share_device_intro": "Laadige {brand} {model} üles valitud võrdlustsüklitega. Teised sama kodumasina kasutajad saavad teie programme kasutusele võtta. Kirjeid vaadatakse üle enne avalikustamist.", "share_profile_no_cycles": "Võrdlustsükleid pole -- märkige tsükkel ⭐-ks Tsüklid-vahekaardil, et lisada see profiil", - "advisory_phase_inconsistent": "'{name}' näib segavat erinevaid programme või temperatuure - selle tsüklid kuumutavad väga erineva aja jooksul. Selle jagamine eraldi profiilideks (nt temperatuuri järgi) parandab sobitamist ja ajahinnanguid.", - "advisory_phase_inconsistent_title": "⚠ Võimalik programmide segu", - "export_select_intro": "Märgi täpselt, mida kaasata. Profiilide valimine ilma nende tsükliteta ekspordib siiski tuvastatava programmi (selle õpitud kuju liigub kaasa).", - "import_analyze_hint": "Laadi eksporditud fail (või kleebi selle JSON). WashData analüüsib seda ja näitab täpselt, mida saab importida, enne kui midagi muutub.", + "export_select_intro": "Märgi, mida kaasata. Ka ilma tsükliteta profiilid ekspordivad sobitatava programmi.", + "import_analyze_hint": "Laadi eksporditud fail (või kleebi selle JSON). WashData näitab enne muudatusi, mida saab importida.", "analyzing": "Analüüsimine…", - "device_type_mismatch_warn": "See eksport on erinevast seadmetüübist ({src} vs {local}). Programme ja tsükleid saab siiski importida viiteandmetena, kuid seadmepõhised seaded ja tegeliku ajaloo import on keelatud.", + "device_type_mismatch_warn": "See eksport on teist tüüpi seadmest ({src} vs {local}). Programme ja tsükleid saab siiski importida viiteandmetena, kuid mitte seadme seadeid ega tegelikku ajalugu.", "merge_hint": "Imporditud üksused lisatakse; kohalikest andmetest ei lähe midagi kaotsi. Nimekonfliktid lahendatakse allpool.", "replace_warn": "Iga märgitud kategooria kustutatakse ja asendatakse failist. Märkimata kategooriaid ei puudutata.", "dest_reference_hint": "Imporditud tsüklid ainult parandavad programmide tuvastamist ega mõjuta kunagi kasutus- ega energiastatistikat.", - "dest_real_history_hint": "Imporditud tsüklid arvestatakse selle seadme enda ajalooks ja need toidavad energia- ja kasutusstatistikat. Kasuta ühe seadme uude paigaldusse teisaldamiseks.", + "dest_real_history_hint": "Imporditud tsüklid loetakse selle seadme enda ajalooks ja statistikaks. Kasuta seda seadme kolimiseks uude paigaldusse.", "sug_muted": "Seda seadet enam ei soovitata", "sug_mute_failed": "Soovituse vaigistamine ebaõnnestus", "sug_unmuted_all": "Vaigistatud soovitused lähtestatud", - "n_suggestions_muted": "{count} vaigistatud; automaatne häälestaja ei paku neid enam.", + "n_suggestions_muted": "{count} soovitust vaigistatud; automaatne häälestaja ei paku neid enam.", + "n_suggestions_muted_one": "{count} soovitus vaigistatud; automaatne häälestaja ei paku seda enam.", + "n_suggestions_muted_other": "{count} soovitust vaigistatud; automaatne häälestaja ei paku neid enam.", "font_size_hint": "Muuda kõik selles paneelil suuremaks või väiksemaks. Kehtib sinu kontole sellel seadmel.", - "import_history_description": "Kas nutipistik oli sul olemas juba enne WashDatat? Laadi üles selle võimsusanduri ajaloo eksport või loe see otse Home Assistantist, ja tavaline tuvastus töötab need andmed läbi, nii et möödunud tsüklid ilmuvad sinu Tsüklite loendisse ja neile saab nime anda.", - "hist_input_hint": "Laadi üles Ajaloo paneelilt alla laaditud CSV (olem, olek, viimane muutus) või lase WashDatal anduri ajalugu otse lugeda. Seejärel töötab tuvastus need andmed läbi täpselt nii nagu reaalajas ja sina valid, millised leitud tsüklid alles jäävad.", - "hist_recorder_hint": "Loeb valitud kuupäevast kuni praeguse hetkeni. Home Assistant hoiab üksikasjalikku ajalugu vaikimisi 10 päeva ja pärast seda ainult tunniseid keskmisi, mis on tsüklite tuvastamiseks liiga jämedad - vali varasem kuupäev ainult siis, kui sinu recorder on seatud rohkem säilitama.", - "hist_scanning": "Sinu ajalugu jooksutatakse detektorist läbi. See toimub taustal - võid selle akna sulgeda ja hiljem tagasi tulla.", + "import_history_description": "Kas nutipistik oli sul juba enne WashDatat? Laadi üles selle võimsuse ajalugu või loe see Home Assistantist. Tuvastus töötab selle läbi ja möödunud tsüklid ilmuvad sinu Tsüklite loendisse, valmis nime saama.", + "hist_input_hint": "Laadi üles CSV Ajaloo paneelilt (olem, olek, viimane muutus) või lase WashDatal anduri ajalugu otse lugeda. Reaalajas tuvastus töötab selle läbi ja sina valid, millised tsüklid alles jätta.", + "hist_recorder_hint": "Loeb valitud kuupäevast praeguseni. Home Assistant hoiab üksikasjalikku ajalugu vaikimisi 10 päeva; vanemad tunnikeskmised on tsüklite tuvastamiseks liiga jämedad.", + "hist_scanning": "Sinu ajalugu jooksutatakse taustal detektorist läbi. Võid selle akna sulgeda ja hiljem tagasi tulla.", "hist_imported_count": "Imporditud tsükleid: {n}.", "hist_duplicates": "Juba varem imporditud ja vahele jäetud: {n}.", "hist_capped": "Seadme imporditud tsüklite piirmäär sai täis; ülejäänuid ei salvestatud.", @@ -1095,57 +1024,92 @@ "hist_scan_capped": "Kuvatakse ainult esimesed kandidaadid (leiti: {n}).", "hist_recorder_empty": "Home Assistantil pole selle anduri kohta sellest ajavahemikust üksikasjalikku ajalugu.", "hist_scan_failed": "Skannimine ebaõnnestus.", - "hist_scan_expired": "See skannimistulemus pole enam saadaval. Palun skanni uuesti.", + "hist_scan_expired": "See skannimistulemus pole enam saadaval. 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.", - "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.", + "imported_history_readonly": "Leitud imporditud võimsuse ajaloost. Aitab sobitamisel, kuid ei lähe sinu statistikasse ning seda ei saa kärpida ega poolitada. Programmi nimetamiseks sildista see.", + "odometer_intro": "See koguarv ainult kasvab. Tsükli kustutamine seda ei vähenda, nii et hoolduse meeldetuletused jäävad õigeks.", + "odometer_correct_hint": "Määra see, kui WashData loendas käivituse, mida polnud, või kui seade läbis tsükleid juba enne WashData paigaldamist.", "no_reminders_set": "Hoolduse meeldetuletusi pole veel määratud.", "toast_program_armed": "Programm seatud järgmise tsükli jaoks: {program}", "toast_range_required": "Määra kehtiv Alates/Kuni vahemik.", - "no_profiles_option": "(profiile veel pole, lisa uus)" + "no_profiles_option": "(profiile veel pole, lisa uus)", + "compare_cycles_title": "Võrdle tsükleid ({count})", + "edit_builtin_phase": "See on sisseehitatud faas. Salvestamine loob kohandatud koopia; originaali taastamiseks kustutage koopia.", + "fetch_error": "Andmete laadimine ebaõnnestus.", + "connection_lost": "Ühendus katkes - kuvatakse andmeid seisuga {time}.", + "pg_notify_partial": "Markerid näitavad ainult algus-, peaaegu valmis-, lõpu-, verstaposti- ja vaikuse tundide teavitusi. Reaalajas uuendusi, mahalaadimise meeldetuletusi ja ületusaja hoiatusi ei simuleerita.", + "connect_to_confirm": "Ühenda seadete hammasrattast, et kinnitada või hinnata.", + "coverage_gap_cluster": "{count} hiljutist märgistamata tsüklit kestusega umbes {min} min näivad olevat üks programm, mida pole veel loodud.", + "pg_label_yes": "Märgistataks tsükli lõpus kui \"{name}\"", + "pg_label_no": "Paluks ülevaatamist: {reason}", + "live_match_debug_tip": "Usaldus: kui hästi praegune võimsuskõver vastab parimale kandidaatprofiilile (0-100%). Ebaselge: kahe parima kandidaadi skoorid erinevad teineteisest vähem kui 5% - silt jääb ebakindlaks, kuni tsükkel lõpeb.", + "automation_alias_started": "{device}: tsükkel algas", + "automation_alias_finished": "{device}: tsükkel lõppes", + "automation_desc_started": "Käivitub, kui seadme {device} tsükkel algab. Lisa oma toimingud (teavitus, tuled, ...).", + "automation_desc_finished": "Käivitub, kui seadme {device} tsükkel lõpeb. Lisa oma toimingud (teavitus, tuled, ...).", + "feedback_detected": "WashData tuvastas: {name}", + "import_undo_hint": "Säilitatakse viimase impordi ({when}) eelne taastepunkt: {profiles} profiili ja {cycles} tsüklit.", + "replace_undo_hint": "Esmalt salvestatakse taastepunkt, nii et saad selle impordi jaotises Eksport / Import tagasi võtta.", + "pg_in_sample_note": "Need tsüklid aitasid luua profiile, millega neid sobitatakse, seega on tulemused optimistlikud: uute tsüklite puhul oodake veidi vähem õigeid vasteid.", + "hist_tail_trimmed": "Pärast tsükli lõppu kärbiti {min} min ootamist, nagu seda teeb ka aktiivne integratsioon", + "hist_truncated": "peatus {n} näidu piiril, seega ülejäänud ajalugu ei skannitud", + "hist_warn_kw": "Kõrgeim näit on vaid {w}. See andur näib edastavat kilovatte; WashData vajab vatte, seega tsükleid ei saa tuvastada.", + "pg_compare_profile_tip": "Valib ainult profiili, mis kuvatakse graafiku peal. Simulatsioon sobitab tsükli alati iseseisvalt, nagu teeb ka aktiivne integratsioon.", + "pg_outcome_stale": "Seaded on pärast selle simulatsiooni käivitamist muutunud. See näitab endiselt vana tulemust.", + "pg_sweep_guarded": "Lõpetab rohkem tsükleid liiga vara, tükeldab neid rohkem või jätab vahele tsükleid, mille praegune seade tabab, seega seda ei soovitata kunagi.", + "pg_sweep_keep_current": "Jäta praegune väärtus ({value}, {metric}): ükski väärtus ei olnud vähemalt ühe tsükli võrra parem.", + "pg_task_busy": "Selle seadme jaoks käib juba „Testi ajalooga“ või „Optimeeri“. Oota, kuni see lõpeb, või tühista see.", + "hist_warn_naive_time": "{n} ajatemplil puudub ajavöönd ja need loeti UTC-ajana. Kui fail kasutab kohalikku aega, nihkuvad tsüklid teie UTC-nihke võrra ning suveajale ülemineku ümbruse näidud võivad kaduda.", + "hist_warn_naive_time_one": "{n} ajatemplil puudub ajavöönd ja see loeti UTC-ajana. Kui fail kasutab kohalikku aega, nihkuvad tsüklid teie UTC-nihke võrra ning suveajale ülemineku ümbruse näidud võivad kaduda.", + "hist_warn_naive_time_other": "{n} ajatemplil puudub ajavöönd ja need loeti UTC-ajana. Kui fail kasutab kohalikku aega, nihkuvad tsüklid teie UTC-nihke võrra ning suveajale ülemineku ümbruse näidud võivad kaduda.", + "pg_sweep_n_cycles": "{count} tsükli põhjal", + "pg_sweep_n_cycles_one": "{count} tsükli põhjal", + "pg_sweep_n_cycles_other": "{count} tsükli põhjal", + "cycle_context_loading": "Salvesti ajaloo laadimine…", + "cycle_context_note": "Hall: võimsusanduri salvesti ajalugu selle tsükli ümber. See ei ole tsükli osa ega mõjuta kestust, energiat ega sobitamist.", + "cycle_context_none": "Salvestis pole selle tsükli ümber ühtegi näitu (Home Assistant hoiab vaikimisi 10 päeva ajalugu).", + "cycle_context_tip": "Ainult kuvamiseks: näitab võimsusanduri salvesti ajalugu selle tsükli ümber, kui see on veel alles. Katkenud käivituskatsete lisamiseks tulevastesse tsüklitesse kasutage sätet Kõvera eeljooks (Seaded > Tuvastamine > Tsükli Algus), mis muudab salvestatud kestusi.", + "invalid_threshold_pair": "Peatumise Lävi ({stop} W) peab olema väiksem kui Käivitumise Lävi ({start} W). Midagi ei salvestatud; muutke mõlemat korraga." }, "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", - "smart_termination_duration_ratio": "Osa vastendatud programmi eeldatavast kestusest, milleni tsükkel peab jõudma, enne kui nutikas lõpetamine tohib selle ennetähtaegselt lõpetada; vähendage seda koormusest või temperatuurist sõltuvatel masinatel", + "smart_termination_duration_ratio": "Osa programmi keskmisest kestusest, milleni tsükkel peab jõudma, enne kui nutikas lõpetamine tohib selle varem lõpetada; vähendage, kui tööajad kõiguvad", "anti_wrinkle_enabled": "Neelab pärast põhifaasi trummelimpulsid, selle asemel et lugeda neid uute tsüklitena", - "anti_wrinkle_exit_power": "Kortsudevastase režiimi aktiivsena püsimiseks peab võimsus impulsside vahel sellest allapoole langema", + "anti_wrinkle_exit_power": "Vaikuse tase trummelimpulsside vahel; mõjub ainult üle Peatumise Läve", "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", + "anti_crease_finalize_ratio": "Osa programmi keskmisest kestusest, milleni tsükkel peab jõudma, enne kui kortsudevastane saba tohib selle lõpetada; kuivatitel vähendage, pesumasinatel jätke vaikeväärtus", "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", "min_off_gap": "Kahe tsükli eraldamiseks vajalik vahe", "off_delay": "Madala võimsuse sekundid enne tsükli lõppu", "start_duration_threshold": "Sekundid üle läve käivituse kinnitamiseks", "start_threshold_w": "Minimaalne vatt, et lugeda käivitunuks", "stop_threshold_w": "Sellest allpool loetakse masin väljalülitatuks", - "dtw_bandwidth": "Etapp 3: Sakoe-Chiba aja-nihkeriba (0 = DTW väljas; vaikimisi 0,2 = 20% tsükli pikkusest)", - "corr_weight": "Etapp 2: tasakaal kõvera kuju (korrelatsioon) ja võimsustaseme (MAE) vahel; vaikimisi 0,45", - "duration_weight": "Etapp 4: kui tugevalt kestuse vastavus mõjutab lõppskooris (vaikimisi 0,22)", - "energy_weight": "Etapp 4: kui tugevalt energia vastavus mõjutab lõppskooris (vaikimisi 0,22)", - "profile_match_min_duration_ratio": "Etapp 1: lühim tsükkel profiili kestuse murdosana; vaikimisi 0,1 (10%)", - "profile_match_max_duration_ratio": "Etapp 1: pikim tsükkel profiili kestuse murdosana; vaikimisi 1,8 (180%)", - "keep_min_score": "Etapp 2: madalaim skoor võistluses püsimiseks; vaikimisi 0,1 lubab nõrku kandidaate, hilisemad etapid valivad parima", - "dtw_blend": "Etapp 3: 0 = ainult põhiskoor, 1 = ainult DTW-skoor, 0,5 = võrdne segu (vaikimisi)", - "dtw_ensemble_w": "Etapp 3 (ensemble-režiim): skaleeritud L1 kaal vs tuletise DTW; vaikimisi 0,7 eelistab tasemeteadlikkust", - "dtw_ddtw_scale": "Etapp 3: DDTW pool-küllastumiskaugus; väiksem = tundlikum kuju erinevuste suhtes (vaikimisi 30)", - "dtw_refine_top_n": "Etapp 3: kandidaadid keda DTW hindab ümber; tõstke 7-9-ni kui õige profiil jõuab 4.-5. kohale (vaikimisi 5)", - "duration_scale": "Etapp 4: log-suhe kus kestuse vastavus poolitub; väiksem = rangem karistus (vaikimisi 0,175)", - "energy_scale": "Etapp 4: log-suhe kus energia vastavus poolitub; väiksem = rangem karistus (vaikimisi 0,25)" + "profile_match_min_duration_ratio": "Lühim tsükli pikkus profiili kestuse murdosana; vaikimisi 0,1 (10%)", + "profile_match_max_duration_ratio": "Pikim tsükli pikkus profiili kestuse murdosana; vaikimisi 1,8 (180%)" }, "phase_desc": { "anti_crease": "Kortsude vähendamiseks tehke aeg-ajalt lühikesi tõmblusi pärast lõpetamist.", "anti_wrinkle": "Perioodiline tsüklijärgne trummeldamine kortsude vähendamiseks.", + "baking": "Küttekeha kuumutab kõrgel temperatuuril, et moodustuksid koorik ja sisu.", + "cooking": "Peamine küpsetusfaas, küttekeha ja ventilaator töötavad.", "cool_down": "Kuumutamine ilma kuumuseta tsükli lõpu lähedal.", + "cool_down_af": "Jahutus ainult ventilaatoriga pärast kuumutamist.", "drain_and_switch": "Üleminekuperiood pesemiselt kuivatusrežiimile.", "dry": "Kuivatusaste küttekeha ja/või jääksoojuse abil.", "drying": "Peamine kuumutatud trummeldamisperiood.", "heat_up": "Küttekeha esialgne soojendamine enne täieliku kuivamise algust.", + "keep_warm": "Hoidmine madalal kuumusel, et toit püsiks soe.", + "keep_warm_bm": "Hoidmine madalal kuumusel, et leib püsiks pärast küpsetamist soe.", + "kneading": "Mootor segab ja sõtkub tainast. Suur võimsustarve.", + "pause_af": "Lühike paus raputamiseks või kontrollimiseks.", + "pre_heat": "Kambri esialgne kuumutamine enne täielikku küpsetamist.", "pre_rinse": "Esmane pihustamine enne pesuvahendiga pesemist.", + "proving": "Kerkimine madalal kuumusel: pärm käärib ja tainas paisub.", + "resting": "Lühike madala võimsusega paus sõtkumiste vahel, et gluteen lõõgastuks.", "pre_wash": "Esmane leotamine või eeltöötlus enne põhipesu.", "rinse": "Puhta veega loputamise etapp. Seda faasi võib korrata mitu korda.", "sanitize": "Kõrge temperatuuriga puhastamise etapp desinfitseerimisprogrammide jaoks.", @@ -1158,6 +1122,7 @@ "wash_dw": "Põhipesuvahendi pesu kuumutamisega." }, "placeholder": { + "custom_task": "nt puhasta ukse tihend", "add": "lisa…", "group_name": "nt. Puuvill 2:47", "json_buttons": "[{\"action\":\"ID\",\"title\":\"Label\"}]", @@ -1185,10 +1150,10 @@ "section": { "dishwasher": { "label": "Nõudepesumasin", - "intro": "Tsükli lõpu käsitlemine nõudepesumasinate jaoks, mis tavaliselt lõpevad pika, peaaegu hääletu kuivatusfaasiga enne lühikest lõplikku tühjendust." + "intro": "Lõpu käsitlemine nõudepesumasinatele, mis kuivatavad sageli kaua vaikselt enne lühikest lõplikku tühjendust." }, "anti_wrinkle": { - "intro": "Kortsudevastane/kroomievastane režiim tuvastab madalvõimsusega trummelpulsid pärast põhifaasi ja hoiab need lõppenud tsükliga seotud.", + "intro": "Hoiab kuivati madala võimsusega trummelimpulsid pärast põhifaasi lõppenud tsükli küljes.", "label": "Kortsudevastane" }, "basic": { @@ -1196,7 +1161,7 @@ "label": "Põhiline" }, "delay": { - "intro": "Viivitatud käivituse tuvastamine tuvastab, millal seade on sisse lülitatud, kuid pole veel tsüklit alustanud.", + "intro": "Tuvastab seadme, mis on sees ja ootab viivitusega käivitust.", "label": "Viivitatud Käivitus" }, "detection": { @@ -1220,161 +1185,153 @@ "label": "Ajastus ja Valvekoer" }, "triggers": { - "intro": "Valikulised välised signaalid: lõpu päästik, ukse andur, pausi lüliti ja mahalaadimisel meeldetuletaja.", + "intro": "Valikulised lisad: lõpu päästik, ukseandur, pausi lüliti ja mahalaadimise meeldetuletus.", "label": "Päästikud ja Uksed" - }, - "phase_eta": { - "label": "Järelejäänud aeg", - "intro": "Faasiteadlik järelejäänud aeg masinatele, mille tsükli pikkus sõltub temperatuurist või tsentrifuugimisest." } }, "setting": { "dishwasher_end_spike_quiet_release": { "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." + "doc": "Kui kaua peab nõudepesumasin pärast eeldatavat kestust püsima allpool seadet Peatumise Lävi, enne kui WashData lõpetab lõpliku tühjenduse ootamise. Suurendage seda, kui pika kuivatuse järel hiline tühjendus jääb märkamata. Ootamine ei ületa kunagi 30 minutit." }, "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." + "doc": "Osa programmi keskmisest kestusest, milleni tsükkel peab jõudma, enne kui selle madala võimsusega trummeldamine loetakse kortsudevastaseks sabaks. Koormust tuvastavatel kuivatitel vähendage seda (nt 0,75). Pesumasinatel jätke vaikeväärtus (0,98), muidu võib pesu poole peal lõppeda." }, "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.", + "doc": "Hoidke kuivati lühikesed madala võimsusega trummelimpulsid pärast peamist kuumutusfaasi lõppenud tsükliga koos, selle asemel et lugeda neid uuteks tsükliteks.", "label": "Luba Kortsudevastane Tuvastamine" }, "anti_wrinkle_exit_power": { - "doc": "Kortsudevastase režiimi aktiivseks püsimiseks peab võimsus impulsside vahel langema sellest allapoole.", + "doc": "Kortsudevastases režiimis loetakse sellest väiksem võimsus trummelimpulsside vaheliseks vaikuseks ja Maksimaalsest Impulsivahest pikem vaikus lõpetab režiimi. Kui Peatumise Lävi on kõrgem, kasutatakse seda, nii et see seade mõjub ainult siis, kui see on üle Peatumise Läve.", "label": "Väljumise Võimsuse Lävi" }, "anti_wrinkle_idle_timeout": { - "doc": "Kui kaua tohib masin kahe trummelimpulsi vahel vaikida, enne kui kortsudevastane režiim lõpeb. Seadke see kõrgemaks kui pikim paus, mille teie kuivati impulsside vahele jätab, muidu loetakse iga hilisem impulss valestardiks.", + "doc": "Kui kaua tohib kuivati trummelimpulsside vahel vaikida, enne kui kortsudevastane režiim lõpeb. Seadke see pikemaks kui teie kuivati pikim paus, muidu loetakse hilisemad impulsid valestartideks.", "label": "Maksimaalne Impulsivahe" }, "anti_wrinkle_max_duration": { - "doc": "Sellest pikemaid kaunvilju käsitletakse pigem tõelise tsüklina kui kortsudevastasena.", + "doc": "Sellest pikem impulss loetakse tõeliseks tsükliks, mitte kortsudevastaseks trummeldamiseks.", "label": "Maksimaalne Kestus" }, "anti_wrinkle_max_power": { - "doc": "Sellest võimsusest kõrgemat pulssi käsitletakse kui tõelist uut tsüklit, mitte kui kortsudevastast tumblerit. Seadke trummelimpulsi võimsusest veidi kõrgemale.", + "doc": "Sellest suurema võimsusega impulss loetakse tõeliseks uueks tsükliks, mitte trummeldamiseks. Seadke see veidi üle oma trummelimpulsi võimsuse.", "label": "Maks Kortsudevastane Võimsus" }, "auto_label_confidence": { - "doc": "Kui vaste skoor tsükli lõpus on sellel tasemel või sellest suurem, märgistatakse programm automaatselt ilma kinnitusviibata. Suurendage seda, et nõuda enne automaatset märgistamist suuremat kindlust; alandage seda, et rohkem automatiseerida. Töötab koos selle all oleva Learning Confidence'iga.", + "doc": "Vaste skoor tsükli lõpus, millest alates programm märgistatakse küsimata. Suurendage seda suurema kindluse jaoks; vähendage, et rohkem tsükleid automaatselt märgistada. Hoidke see suurem kui Õppimise Usaldus.", "label": "Automaatse Sildi Usaldus" }, "auto_maintenance": { - "doc": "Korraldage igaõhtust majapidamist: koostage profiiliümbrised, arvutage uuesti tsükli seisukord, pügake silumisjälgi ja säilitage uusimad tsüklid.", + "doc": "Öine puhastus: märgistab vanad tsüklid, mis nüüd selgelt sobivad mõne programmiga, ehitab programmide kõverad uuesti, värskendab anomaaliate tähiseid ja tsüklite tervist ning eemaldab silumisjäljed, kui \"Salvesta Silumise Jäljed\" on väljas. Tsüklid ja nende võimsusjäljed säilitatakse alati.", "label": "Automaatne Hooldus (öine puhastus)" }, "completion_min_seconds": { - "doc": "Sellest lühemad tsüklid jäetakse kummitustsüklitena kõrvale (testisõidud, soki lisamiseks ukse avamine).", + "doc": "Sellest lühemad tsüklid (testkäivitused, ukse avamine soki lisamiseks) salvestatakse olekuga Katkestatud ja nende kohta lõputeavitust ei saadeta.", "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." + "doc": "Lisab katkenud käivituskatsete näidud tsükli algusse masinatel, mis enne käivitumist kobavad (ukselukk, esimene veevõtt). 300 s katab enamiku; 0 = väljas. Salvestatud algused nihkuvad varasemaks, seega õpitud kestused triivivad mõnda aega." }, "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.", + "doc": "Võimsus peab nii kaua püsima ooterežiimi vahemikus, enne kui seade loetakse käivitust ootavaks.", "label": "Kinnitussekundid" }, "delay_start_detect_enabled": { - "doc": "Tuvastab, millal seade on sisse lülitatud ja ootab (viivitusega käivitus / ooterežiim), kuid ei ole oma tsüklit alustanud, seega ei peeta ooterežiimi tõmbamist töötsükliks.", + "doc": "Tuvastab seadme, mis on sees ja ootab viivitusega käivitust, et selle ooterežiimi tarbimist ei peetaks töötavaks tsükliks.", "label": "Luba Viivitatud Käivituse Tuvastamine" }, "delay_timeout_hours": { - "doc": "Lõpetage pärast seda mitu tundi viitkäivituse režiimis ootamist ja naaske tühikäigule, nii et masin, mis on jäetud toiteallikaks, kuid mitte käivitatud, ei oota igavesti.", + "doc": "Lõpetage viivitusega käivituse ootamine pärast nii mitut tundi ja naaske jõudeolekusse.", "label": "Aegumistunnid" }, "device_type": { - "doc": "Seadmeklass. Määrab selle seadmetüübi jaoks häälestatud mõistlikud tuvastamise vaikesätted (läved, väljalülitusviivitus, lõppkäsitsus); muutke seda ainult siis, kui seade oli algselt seadistatud valet tüüpi.", + "doc": "Määrab seda tüüpi seadme tuvastamise vaikesätted (läved, väljalülitumise viivitus, lõpu käsitlemine). Muutke seda ainult siis, kui seade seadistati vale tüübiga.", "label": "Seadme Tüüp" }, "door_sensor_entity": { - "doc": "Valikuline ukse binaarandur. Kasutatakse selleks, et tuvastada, millal seade on pärast tsüklit avatud/laaditud.", + "doc": "Valikuline ukseandur. Näitab, millal seade pärast tsüklit mahalaadimiseks avati.", "label": "Ukse Anduri Olem" }, "duration_tolerance": { - "doc": "Tolerants järelejäänud aja hinnangute suhtes (õppiv tagasiside, mitte sobitamine). Kui tegelik kestus jääb hinnangulisest +/-X% piiresse, loetakse see heaks vasteks.", + "doc": "Ainult õppimise tagasiside: tsükkel, mis jääb +/-X% piiresse oma hinnangulisest kestusest, loetakse heaks hinnanguks. Sobitamist ei mõjuta.", "label": "Hinnangu Tolerants" }, "enable_ml_models": { - "doc": "Tsükli kulgemise ajal laske mudelitel reaalajas tulemusi täpsustada: ühtlasem järelejäänud aja- ja energia/kuluhinnang ning enneaegse peatumise vastane kaitse lõpu tuvastamisel (see võib ainult finišit edasi lükata, mitte kunagi varem lõpetada ja on piiratud). Kasutab võimalusel teie peenhäälestatud mudeleid, muul juhul sisseehitatud mudeleid. Väljas = ainult klassikaline toitepõhine loogika (ikka usaldusväärne).", + "doc": "Kasutage selles masinas treenitud mudelit, et prognoosida käimasoleva tsükli energiat ja maksumust, ning laske hästi tuntud programmil, mis peatub ebatavaliselt vara, lõppeda ilma täit väljalülitumise viivitust ootamata. Väljas: ainult võimsuspõhine loogika.", "label": "Rakenda nutikaid mudeleid tsükli ajal" }, "end_energy_threshold": { - "doc": "Väljalülitusviivituse loenduse ajal võrreldakse kogunenud energiat (vatti x aeg) selle lävega. Ületamise korral pöördloendus lähtestub – kortsumisvastased trumlid ja nõudepesumasina kuivatussabad jäävad tsükli külge, selle asemel et neid lühikeseks lõigata. Tõstke seda, kui tsüklid lõpevad jahtumise ajal liiga vara; alandage seda, kui tuvastamine on aeglane.", + "doc": "Väljalülitumise viivituse ajal lubatud energia. Kui seda kulub rohkem, tsükkel jätkub, nii et trummeldamised ja kuivatuse lõpuosad jäävad tsükli külge. Vähendage seda, kui tsüklid lõpevad lõpuosa ajal; suurendage, kui lõppenud tsüklid jäävad ootama.", "label": "Lõpu Energia" }, - "end_repeat_count": { - "doc": "Enne tsükli lõppu on vajalik järjestikuste alla stopp-läve näitude arv. 1 sobib enamiku pistikute jaoks. Tõstke väärtuseni 2–3, kui teie nutipistik teatab aeg-ajalt valenull-näidist tsükli keskel ja teie tsüklid lõpevad enneaegselt.", - "label": "Lõpu Kordusloendur" - }, "energy_price_entity": { - "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.", + "doc": "Andur praeguse hinnaga kWh kohta (nt dünaamiline tariif). See on ülimuslik alloleva staatilise hinna suhtes, seega peab see olema hind, mitte pistiku võimsuse või energia andur.", "label": "Energia Hinna Olem" }, "energy_price_static": { - "doc": "Fikseeritud hind kWh kohta, mida kasutatakse kulunäitajate jaoks, kui ülal pole määratud reaalajas hinna olemit.", + "doc": "Fikseeritud hind kWh kohta, mida kasutatakse, kui hinna olemit pole määratud.", "label": "Staatiline Energia Hind (kWh kohta)" }, "energy_sensor": { - "doc": "Valikuline kumulatiivne energialoendur (total_increasing kWh/Wh, nt pistiku enda kogutarbimise arvesti). Kui see on määratud, võetakse iga tsükli energianäit selle loenduri algus- ja lõppnäidu vahest, mis väldib alamõõtmist, mida tekitab aeglaselt uueneva võimsusanduri integreerimine. Kui näit puudub, selle ühik on teadmata või vahe pole positiivne, kasutatakse tagavaraks integreeritud väärtust. Jätke tühjaks, et jätkata võimsusanduri integreerimist.", + "doc": "Valikuline kWh või Wh loendur (total_increasing, nt pistiku enda arvesti). Siis loetakse tsükli energia sellest, mis on täpsem kui aeglase võimsusanduri põhjal arvutamine. Kui selle näit on kasutuskõlbmatu, kasutatakse arvutatud väärtust.", "label": "Energiaarvesti Olem" }, "expose_debug_entities": { - "doc": "Avaldage täiendavad diagnostilised HA-üksused (vaste usaldus, mitmetähenduslikkus, oleku sisemised andmed). Väljas hoiab olemiloendi tavakasutuseks puhtana.", + "doc": "Lisage diagnostilised olemid (vaste usaldus, mitmetähenduslikkus, sisemine olek). Väljas hoiab olemiloendi puhtana.", "label": "Näita Silumise Olemeid" }, "external_end_trigger": { - "doc": "Binaarsensor, mille oleku muutus tähistab tsükli lõppu (nt seadme \"lõpetatud\" kontakt või kaasintegratsioon).", + "doc": "Binaarandur, mille muutus tähistab tsükli lõppu (nt \"lõpetatud\" kontakt või muu integratsioon).", "label": "Väline Päästiku Olem" }, "external_end_trigger_enabled": { - "doc": "Lisaks sisseehitatud toitepõhisele tuvastamisele laske tsükli lõpust signaali anda välisel binaarsel anduril.", + "doc": "Laske binaaranduril anda tsükli lõpu signaal lisaks võimsuspõhisele tuvastamisele.", "label": "Luba Väline Lõpu Päästik" }, "external_end_trigger_inverted": { - "doc": "Käsitlege päästikuanduri VÄLJA lülitumist (mitte SISSE) tsükli lõpu signaalina.", + "doc": "Käsitlege anduri VÄLJA lülitumist (mitte SISSE) lõpu signaalina.", "label": "Pööra Väline Päästik (käivita VÄLJAS)" }, "learning_confidence": { - "doc": "Kui vaste skoor jääb selle ja automaatse sildi usaldusväärsuse vahele, palutakse tagasiside teatises tuvastatud programmi kinnitada. Sellest punktist allpool on matš liiga ebakindel, et pinnale jõuda. Tuleb hoida allpool automaatse sildi usaldusväärsust.", + "doc": "Väikseim vasteskoor lõppenud tsükli sildistamiseks. Selge võitja saab sildi; napp tulemus läheb vahekaardi Tsüklid ülevaatusjärjekorda. Sellest allpool ei sildistata ega küsita midagi. Hoidke see väiksem kui Automaatse Sildi Usaldus.", "label": "Õppimise Usaldus" }, "linked_device": { - "doc": "Soovi korral pesake see WashData seade HA-seadmete registris mõne teise seadme (nt nutipistiku) alla, kuvatakse kui \"Ühendatud ... kaudu\".", + "doc": "Valikuline. Kuvage see WashData seade Home Assistantis teise seadme (nt nutipistiku) all kui \"Ühendatud ... kaudu\".", "label": "Grupeeri Seadme Alla" }, "min_off_gap": { - "doc": "Kui masin lülitub välja vähem kui see aeg, käsitletakse sisse/välja/sisse jada ühe pideva tsüklina. Takistab leotusprogrammide (masin lülitub keset pesu mitmeks minutiks välja) jagamist kaheks eraldi tsükliks. Kui soovite, et neid loetaks eraldi tsüklitena, määrake see lühemaks kui vahe oma vahelt tagasi. Seadmetüübi vaikeseaded kaitsevad iga seadme tüüpilist tsüklisisest pausi.", + "doc": "Sellest lühem väljasolek ei jaga tsüklit, nii et leotuspausid jäävad ühte pessu. Hoidke see lühem kui vahe järjestikuste pesude vahel, muidu liidetakse need üheks tsükliks.", "label": "Min Väljalülitumise Vahe" }, "min_power": { - "doc": "Aktiivseks loetav absoluutne minimaalne võimsus. Sellest allpool olevaid näitu käsitletakse kui 0 W (ooterežiim), filtreerides välja nutikate pistikute ja ooterežiimi LED-ide fantoomkoormuse.", + "doc": "Sellest väiksemaid näite loetakse 0 W-ks (ooterežiim). Filtreerib välja nutipistikute ja ooterežiimi LED-ide tühikäigutarbimise.", "label": "Minimaalne Võimsus" }, "ml_training_enabled": { - "doc": "Uurige perioodiliselt üle öö oma ülevaadatud tsükleid ja viimistlege mudelid selle konkreetse masina jaoks. Muudatust hoitakse alles siis, kui see saavutab tõepoolest paremaid tulemusi pikendatud tsüklitel, nii et see võib ainult aidata või jääda samaks – mitte kunagi taanduda.", + "doc": "Treenib energiamudelit aeg-ajalt uuesti selle masina lõppenud tsüklitel. Uus mudel jäetakse alles ainult siis, kui see ületab testtsüklitel lihtsa hinnangu.", "label": "Õpi sellest masinast" }, "ml_training_hour": { - "doc": "Kohalik kellaaeg (0–23), et teha üleöö peenhäälestust. Valige vaikne tund, näiteks 2 (02:00).", + "doc": "Kellaaeg (0-23) öiseks treenimiseks. Valige vaikne tund, nt 2.", "label": "Õpi tunnil" }, "ml_training_interval_days": { - "doc": "Kontrollige täiustusi uuesti kõige rohkem üks kord selle mitme päeva jooksul.", + "doc": "Treenige uuesti kõige rohkem kord nii mitme päeva tagant.", "label": "Kontrolli maksimaalselt iga" }, "ml_training_min_cycles": { - "doc": "Enne peenhäälestamist oodake, kuni vähemalt nii palju tsükleid on salvestatud, et oleks piisavalt õppida.", + "doc": "Enne treenimist oodake vähemalt nii palju salvestatud tsükleid.", "label": "Esmalt vajalikud tsüklid" }, "name": { - "doc": "HA integratsioonide loendis ja seadmeregistris kuvatav nimi.", + "doc": "Selle seadme nimi Home Assistantis.", "label": "Seadme Nimi" }, "no_update_active_timeout": { - "doc": "Kui töötamise ajal nii kaua toitevärskendusi ei saabu, eeldage, et pistik katkeb võrguühenduseta ja sundpeata, et vältida zombitsüklit. Vaikimisi 600 s lubab pilve- või võrguviivitust.", + "doc": "Kui tsükli ajal ei saabu nii kaua ühtegi võimsusvärskendust, eeldatakse, et pistik läks võrgust ära, ja tsükkel peatatakse sunniviisiliselt. Vaikimisi 600 s arvestab pilve- või mesh-võrgu viivitustega.", "label": "Ilma Uuenduseta Aegumine" }, "notify_before_end_minutes": { @@ -1382,35 +1339,35 @@ "label": "Eel-Lõpu Hoiatus" }, "notify_channel": { - "doc": "Androidi teavituskanali nimi algus-/reaalajas sõnumite jaoks (juhib kanalipõhist heli ja prioriteeti mobiilirakenduses). Blank kasutab kaasrakenduse vaikeseadet.", + "doc": "Androidi kanal algus- ja reaalajas sõnumitele; kanal määrab heli ja prioriteedi. Tühi: rakenduse vaikeseade.", "label": "Android Kanal (käivitus/reaalajas)" }, "notify_cycle_timers": { - "doc": "Teavitused tsükli teatud minutitel (nt pehmendaja lisamiseks). Sõnum toetab {device}, {program}, {minutes}. Lubage automaatne paus, et peatada sellel hetkel ja saada interaktiivne teatis nupuga Jätka; jätkake paneeli, peatamise/jätkamise teenuse või teavitustoimingu kaudu.", + "doc": "Teavitused tsükli kindlatel minutitel (nt pehmendaja lisamiseks). Sõnum toetab {device}, {program}, {minutes}. Automaatse pausiga peatub tsükkel ka seal ja teavitusel on nupp Jätka.", "label": "Tsükli Taimerid" }, "notify_finish_channel": { - "doc": "Androidi teavituskanali nimi lõpusõnumi jaoks. Tühi kasutab taaskäivitus-/reaalajas kanalit.", + "doc": "Androidi kanal lõpusõnumile. Tühi kasutab algus-/reaalajas kanalit.", "label": "Android Kanal (lõpetamine)" }, "notify_finish_message": { - "doc": "Keha, mis saadetakse tsükli lõppedes. Malli muutujad: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}. {time_finished} ja {vs_typical} on siin kõige kasulikumad.", + "doc": "Tsükli lõppedes saadetav tekst. Muutujad: {device}, {program}, {duration} (minutid), {duration_hm} (1 h 05 min), {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}, {status} (completed või force_stopped; katkestatud tsüklid ei saada midagi). Muu muutuja korral kasutatakse vaiketeksti.", "label": "Lõpetamise Sõnum" }, "notify_finish_services": { - "doc": "teata.* teenuseid kutsutakse tsükli lõppedes. Lisa üks sihtmärgi kohta; jätke tühjaks, et mitte lõpetada teatist.", + "doc": "notify.* teenused, mida tsükli lõppedes kutsuda. Jätke tühjaks, kui neid pole vaja.", "label": "Lõpetamise Teenused" }, "notify_fire_events": { - "doc": "Käivitage ka ha_washdata_* sündmused tsükli alguses/lõpetamisel, et saaksite luua oma automaatika.", + "doc": "Käivitage tsükli alguses ja lõpus ka ha_washdata_* sündmused oma automaatikate jaoks.", "label": "Käivita HA Sündmused Teavituste Jaoks" }, "notify_icon": { - "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.", + "doc": "Väike ikoon teavitusel (nt mdi:washing-machine). Saadetakse ainult mobile_app sihtmärkidele, mitte notify gruppidele. iOS-is asendab see rakenduse ikooni ja vajab kaasrakendust 2026.8 või uuemat. Tühi: platvormi vaikeseade.", "label": "Teavituse Ikoon" }, "notify_live_chronometer": { - "doc": "Staatilise hinnangu asemel kuvage teatises (seda toetavatel platvormidel) reaalajas taastuv taimer.", + "doc": "Kuvage teavituses (kus toetatud) fikseeritud hinnangu asemel reaalajas pöördloendur.", "label": "Kasuta Elavat Kronomeetrit" }, "notify_live_interval_seconds": { @@ -1418,143 +1375,135 @@ "label": "Reaalajas Uuenduse Intervall" }, "notify_live_overrun_percent": { - "doc": "Kui tsükkel möödub oma hinnangust rohkem kui see protsent, saatke ületamise hoiatus.", + "doc": "Reaalajas uuendused lõpevad, kui tsükkel ületab eeldatavat kestust selle protsendi võrra (20 = kuni 1,2 korda eeldatav aeg).", "label": "Reaalajas Ülekulu % Enne Hoiatust" }, "notify_live_services": { - "doc": "teata.* teenused nõudsid tsükli kulgemise ajal reaalajas edenemise värskendusi. Reaalajas edenemise märguannete keelamiseks jätke tühjaks.", + "doc": "notify.* teenused reaalajas edenemise uuendusteks tsükli ajal. Väljalülitamiseks jätke tühjaks.", "label": "Reaalajas Edenemise Teenused" }, "notify_only_when_home": { - "doc": "Saatke märguandeid ainult siis, kui vähemalt üks lingitud inimestest (ülal) on kodus.", + "doc": "Saatke teavitusi ainult siis, kui vähemalt üks ülalolevatest inimestest on kodus.", "label": "Teavita Ainult Kodus Olles" }, "notify_people": { - "doc": "isik.* olemid, mida funktsioon „Teata ainult kodus” otsustamaks, kas keegi on kodus.", + "doc": "person.* olemid, mida „Teavita Ainult Kodus Olles” kontrollib.", "label": "Inimesed (Ainult Kodus Olles)" }, "notify_pre_complete_message": { - "doc": "Eel-/peaaegu valmis hoiatuse sisu. Malli muutujad: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Iga reaalajas edenemisuuenduse tekst. Malli muutujad: {device}, {program}, {minutes} (järelejäänud minutid). Muu muutuja korral kasutatakse vaiketeksti.", "label": "Eel-Lõpetamise Sõnum" }, "notify_reminder_message": { - "doc": "Ikka ootava mahalaadimise meeldetuletuse sisu. Tühi kasutab sisseehitatud vaikeseadet. Malli muutujad: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Ühekordse Eel-Lõpu Hoiatuse tekst. Malli muutujad: {device}, {program}, {minutes} (seade Eel-Lõpu Hoiatus). Muu muutuja korral kasutatakse vaiketeksti.", "label": "Meeldetuletuse Sõnum" }, "notify_start_message": { - "doc": "Keha, mis saadetakse tsükli alguses. Malli muutujad: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Tsükli alguses saadetav tekst. Malli muutujad: {device}, {program}. Muu muutuja korral kasutatakse vaiketeksti.", "label": "Käivitamise Sõnum" }, "notify_start_services": { - "doc": "teata.* teenuseid kutsutakse tsükli alguses. Lisage üks iga sihtmärgi (telefon, armatuurlaud jne) kohta; jätke tühjaks, et mitte alustada märguannet.", + "doc": "notify.* teenused, mida tsükli alguses kutsuda (telefon, töölaud, ...). Jätke tühjaks, kui neid pole vaja.", "label": "Käivitamise Teenused" }, "notify_timeout_seconds": { - "doc": "Loobuge teatisest automaatselt pärast seda mitut sekundit (seda toetavatel platvormidel). 0 säilitab selle kuni käsitsi loobumiseni.", + "doc": "Sulgege teavitus pärast nii mitut sekundit (kus toetatud). 0 hoiab selle alles, kuni selle ise sulgete.", "label": "Automaatne Sulgemine Pärast" }, "notify_title": { - "doc": "Teatise pealkiri. Malli muutujad: {device}, {program}, {duration}, {energy}, {cost}, {date}, {time}, {minutes}.", + "doc": "Teavituse pealkiri. Asendatakse ainult {device}; muu muutuja korral kasutatakse vaikepealkirja.", "label": "Teavituse Pealkiri" }, "notify_unload_delay_minutes": { - "doc": "Minutid pärast tsükli lõppu enne veel ootava \"masina mahalaadimise\" meeldetuletuse saatmist. Meeldetuletuse keelamiseks määrake 0.", + "doc": "Minutid pärast tsükli lõppu enne \"masina mahalaadimise\" meeldetuletust. 0 keelab selle.", "label": "Mahalaadimise Meeldetuletuse Viivitus" }, "notify_unload_repeat": { - "doc": "Saadab mahalaadimise meeldetuletuse iga „Mahalaadimise Meeldetuletuse Viivitus“ minuti järel uuesti, kuni mahalaadimine on kinnitatud või puudutate teavituses nuppu „Lõpeta meeldetuletused“. Vajab viisi mahalaadimise kinnitamiseks: Ukse Anduri Olem, Mahalaadimise Kinnituse Olem või allpool olev valik „Kinnita Mahalaadimine Käsitsi“. Loobumisnupp toimib Home Assistant kaaslasrakenduse (mobiili) teavitustes.", + "doc": "Saadab mahalaadimise meeldetuletuse iga „Mahalaadimise Meeldetuletuse Viivitus“ minuti järel uuesti, kuni mahalaadimine on kinnitatud või puudutate „Lõpeta meeldetuletused“ (mobiilirakendus). Vajab üht neist: Ukse Anduri Olem, Mahalaadimise Kinnituse Olem või „Kinnita Mahalaadimine Käsitsi“.", "label": "Korda, kuni on maha laaditud" }, "off_delay": { - "doc": "Aeg oodata pärast võimsuse langemist, enne kui tsükkel lõppenuks kuulutatakse. Kui toide taastub selles aknas, jätkub tsükkel sujuvalt – see sillutab pause pesuetappide vahel. Nõudepesumasinal on pikad kuivatusfaasid (väljalülitamine 20-60 min), seega peab väljalülitamise viivitus ületama seda, et hoida kogu pesu+kuiv ühe tsüklina.", + "doc": "Kui kaua peab võimsus madalal püsima, enne kui tsükkel on lõppenud. Kui võimsus selle aja jooksul taastub, tsükkel jätkub. Nõudepesumasinad võivad kuivatamise ajal olla 20-60 min jõude, seega seadke see sellest pikemaks.", "label": "Väljalülitumise Viivitus" }, "pause_cuts_power": { - "doc": "Kui tsükkel on peatatud, lülitage välja ka allolev olem Switch Entity. Ainult seadmetele, mille pistikut saab ohutult katkestada tsükli keskel.", + "doc": "Kui tsükkel peatatakse, lülitage välja ka allolev Lüliti Olem. Ainult seadmetele, mis taluvad toite kadumist tsükli keskel.", "label": "Paus Lõikab Ka Võimsuse (lüliti kaudu)" }, "power_off_delay": { - "doc": "Kui kaua peab võimsus pärast tsükli lõppu püsima allpool Toite Väljalülitamise Läve, enne kui olek naaseb olekusse Väljas. Kasutatakse ainult siis, kui Toite Väljalülitamise Lävi on üle 0. Kontrollitakse taustatakti järgi, seega tegelik viivitus ümardatakse üles järgmise oleku aegumise sammuni.", + "doc": "Kui kaua peab võimsus pärast tsüklit püsima allpool Toite Väljalülitamise Läve, enne kui olek naaseb olekusse Väljas. Kasutatakse ainult siis, kui see lävi on üle 0. Kontrollitakse perioodiliselt, seega tegelik ootamine võib olla veidi pikem.", "label": "Toite Väljalülitamise Viivitus" }, "power_off_threshold_w": { - "doc": "Valikuline võimsuspõhine Väljas-oleku tuvastamine. Kui väärtus on üle 0 ja tsükkel on lõppenud ning võimsus püsib sellest tasemest allpool Toite Väljalülitamise Viivituse jooksul, loetakse masin väljalülitatuks ja olek naaseb olekusse Väljas. Keelamiseks jätke väärtuseks 0 (vaikimisi). Määrake see tegelikust väljalülitatud tasemest kõrgemale, kuid Peatumise Läve ja oma masina lõpetatud-kuid-sees ooterežiimi tarbimisest allapoole; kui see ei ole Peatumise Lävest allpool, seda eiratakse. Sisselülitatuna asendab see olekusse Väljas naasmisel Edenemise Lähtestamise Viivituse, nii et lõpetatud masin jääb olekusse Valmis/Puhas, kuni see tegelikult välja lülitatakse.", + "doc": "Kui võimsus on pärast tsüklit Toite Väljalülitamise Viivituse jooksul sellest väiksem, seatakse olek Väljas (Edenemise Lähtestamise Viivituse asemel). Seadke see väljalülitatud seadme tarbimisest kõrgemale, kuid ooterežiimist ja Peatumise Lävest madalamale. 0 keelab selle (vaikimisi).", "label": "Toite Väljalülitamise Lävi" }, "power_sensor": { - "doc": "Anduri olem, mis teatab selle seadme reaalajas võimsust vattides (nt sensor.washer_power). Kogu tsükli tuvastamine põhineb sellel signaalil.", + "doc": "Andur, mis teatab selle seadme reaalajas võimsust vattides (nt sensor.washer_power). Kogu tuvastus põhineb sellel.", "label": "Võimsusandur" }, - "profile_duration_tolerance": { - "doc": "+/- riba profiili keskmise kestuse ümber, mida kasutatakse sobitamise ajal. 0,25 tähendab, et 60-minutiline profiil vastab 45-75-minutistele tsüklitele.", - "label": "Profiili Kestuse Tolerants" - }, "profile_match_interval": { - "doc": "Kui sageli proovite jooksutsükli jooksul profiili sobitada. Vaikimisi 300 s (5 minutit) tasakaalustab tuvastamise kiiruse ja protsessori.", + "doc": "Kui sageli käimasolevat tsüklit profiilidega sobitatakse. Vaikimisi 300 s (5 min).", "label": "Vaste Intervall" }, "profile_match_max_duration_ratio": { - "doc": "Tsükli maksimaalne pikkus profiili suhtes. 1.3 tähendab, et tsükkel peab vastamiseks olema alla 130% profiili kestusest.", + "doc": "Programm jäetakse kõrvale, kui tsükkel on kestnud kauem kui programmi tüüpiline kestus, korrutatud selle väärtusega. 1,8: programmist 80% pikem tsükkel sellega enam ei sobitu.", "label": "Max Kestuse Suhe" }, "profile_match_min_duration_ratio": { - "doc": "Minimaalne tsükli pikkus profiili suhtes. 0,9 tähendab, et tsükkel peab vastamiseks moodustama vähemalt 90% profiili kestusest.", + "doc": "Programmi arvestatakse alles siis, kui tsükkel on kestnud selle osa programmi tüüpilisest kestusest. 0,10: 2-tunnisest programmist saab kandidaat 12 minuti pärast. Ei rakendata tsükli esimese 15 minuti jooksul.", "label": "Min Kestuse Suhe" }, "profile_match_threshold": { - "doc": "Programmi tuvastamise aktsepteerimiseks on tsükli lõpus nõutav minimaalne sarnasusskoor (0-1). Tõstke see üles, et vähendada vale tuvastamist; alandage seda, kui teie masina programme ei sobitata. Vaikimisi 0,4 on konservatiivne lähtepunkt.", + "doc": "Reaalajas vaste skoor, mida tsükkel vajab, enne kui nutikas lõpetamine või kortsudevastane lõpetamine tohib selle varem lõpetada. Kuvatavat programmi see ei muuda. Tõstke seda, kui nõrgad vasted lõpetavad tsükleid liiga vara; langetage, kui selged vasted ootavad väljalülitumise viivituse ära.", "label": "Vaste Lävi" }, "profile_unmatch_threshold": { - "doc": "Kui reaalajas tsükli keskel toimuv matš langeb sellest punktist allapoole, siis esialgne tuvastamine kustutatakse. Hoidke seda veidi allpool sobivusläve, nii et lühike sarnasuse langus ei muudaks ekraani uuesti võrreldamatuks.", + "doc": "Kui reaalajas vaste skoor langeb sellest allapoole, kustutatakse praegune programmi oletus. Hoidke see veidi allpool Vaste Läve, et lühike langus seda ei lähtestaks.", "label": "Mitte-Vaste Lävi" }, "progress_reset_delay": { - "doc": "Pärast lõpetamist hoidke edenemist nii kaua 100% juures, et enne jõudeolekule lähtestamist oleks armatuurlaudadel näha tekst Lõpetatud.", + "doc": "Hoidke pärast tsüklit edenemist nii kaua 100% juures, enne kui see lähtestub jõudeolekusse.", "label": "Edenemise Lähtestamise Viivitus" }, "pump_stuck_duration": { - "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).", + "doc": "Kui kaua võib pump vahetpidamata töötada, enne kui käivitub pumba kinnijäämise sündmus.", "label": "Pumba Kinnijäämise Kestus" }, "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.", + "doc": "Minimaalne aeg töödeldud näitude vahel. Varem saabuvad näidud jäetakse vahele (madalad näidud tsükli ajal säilitatakse alati). Seadke see oma pistiku teavitusintervallile või väiksemaks; suurem väärtus ainult viskab näite ära.", "label": "Proovivõtu Intervall" }, "save_debug_traces": { - "doc": "Salvestage iga tsükli täielik võimsusjälg ja vastavad silumisandmed. Kasulik tõrkeotsinguks, kuid suurendab salvestusruumi mahtu.", + "doc": "Salvestab iga tsükliga ka sobitaja silumisandmed (iga kandidaadi skoorid) tõrkeotsinguks. Lisab umbes 20 KB tsükli kohta.", "label": "Salvesta Silumise Jäljed" }, "smart_termination_duration_ratio": { - "doc": "Kui kaugele vastendatud programmi eeldatavast kestusest peab tsükkel jõudma, enne kui nutikas lõpetamine tohib selle võimsuse langedes ennetähtaegselt lõpetada. Eeldatav kestus on programmi keskmine, seega seadmetel, mille tööaeg kõigub palju - pesumasinad külma talvise vs sooja suvise sissevoolava veega, andurkuivatusega kuivatid, koormusest sõltuvad programmid - lõpeb umbes pool kõigist tsüklitest sellest keskmisest lühemalt ega saa kunagi kiiret lõpetamist, lõppedes alles tagavara-aegumise kaudu minuteid hiljem. Vähendage seda (nt 0,85) neil masinatel, et varajane lõpetamine ikkagi käivituks; tõstke seda 1,0 poole, et olla ettevaatlikum. Jätke tühjaks vaikeväärtuse jaoks (0,98 või nõudepesumasinatel 0,99). See saab tsükli üksnes varem lõpetada, mitte kunagi hiljem, ega käivitu kunagi ebaselge või madala kindlusega vaste korral.", + "doc": "Osa programmi keskmisest kestusest, milleni tsükkel peab jõudma, enne kui nutikas lõpetamine tohib selle varem lõpetada. Kui tööajad kõiguvad (vee temperatuur, andurkuivatus, pesukogus), vähendage seda (nt 0,85). Jätke tühjaks vaikeväärtuse 0,98 jaoks (nõudepesumasinatel 0,99).", "label": "Nutika lõpetamise suhe" }, - "smoothing_window": { - "doc": "Kui palju toorvõimsussignaali tasandatakse. Madal (2) on tundlik, kuid lärmakas; kõrge (5) silub naelu, kuid lisab mahajäämust.", - "label": "Silumisaken" - }, "start_duration_threshold": { - "doc": "Tõelise käivituse kinnitamiseks peab võimsus püsima nii kaua üle käivitusläve, vältides sekundi murdosa sisse-/väljalülituslülititel tsükli käivitamist.", + "doc": "Võimsus peab nii kaua püsima üle Käivitumise Läve, enne kui tsükkel algab, et lühikesi sisse-välja hüppeid eirataks.", "label": "Käivitumise Kestus" }, "start_energy_threshold": { - "doc": "Energiat (võimsus x aeg), mida seade peab tarbima enne TÖÖTAMIST. Lühiajaline suure võimsusega piisk on väga madala energiaga ja seda ignoreeritakse, vältides valekäivitusi.", + "doc": "Energia, mille seade peab tarbima, enne kui tsükkel loetakse töötavaks. Lühike võimsushüpe tarbib vähe energiat, seega see tsüklit ei käivita.", "label": "Käivitumise Energia" }, "start_threshold_w": { - "doc": "Tsükli käivitamise kinnitamiseks peab võimsus tõusma sellest tasemest kõrgemale. Selle liiga madalaks seadmine põhjustab valekäivitusi ooterežiimist; liiga kõrged ja aeglaselt käivituvad programmid (külmtäitmine) jäävad vahele. Soovitusmootor seab selle masina vaadeldud väikseima aktiivvõimsuse tasemest veidi kõrgemale.", + "doc": "Tsükli käivitamiseks peab võimsus tõusma sellest kõrgemale. Liiga madal: ooterežiimi võimsus põhjustab valekäivitusi. Liiga kõrge: aeglaselt käivituvad programmid (külm veevõtt) jäävad vahele.", "label": "Käivitumise Lävi" }, "stop_threshold_w": { - "doc": "Võimsus peab langema allapoole seda taset enne väljalülitusviivituse loenduse algust. Seadke see stardiläve alla – vahe nende vahel on hüstereesiriba, mis väldib virvendust. Kui see on liiga kõrgeks seatud, käivitavad väikese võimsusega faasid (loputusjäägid, kortsumisvastane) lõppjada ekslikult.", + "doc": "Võimsus peab langema sellest allapoole, enne kui väljalülitumise viivituse loendus algab. Hoidke see väiksem kui Käivitumise Lävi. Kui see on liiga kõrge, käivitavad madala võimsusega faasid (loputuse ootamine, kortsudevastane) ekslikult lõpu loenduse.", "label": "Peatumise Lävi" }, "switch_entity": { - "doc": "Valikuline lüliti lülitatakse pausi korral välja ja jätkamisel uuesti sisse, kasutatakse koos funktsiooniga \"Paus katkestab ka toite\".", + "doc": "Valikuline lüliti, mis lülitatakse pausi ajal välja ja jätkamisel uuesti sisse, kui \"Paus Lõikab Ka Võimsuse\" on sees.", "label": "Lüliti Olem" }, "watchdog_interval": { - "doc": "Kui sageli taustavalve kontrollib andureid seiskunud ja aegunud aegu. Vaikimisi 30 s.", + "doc": "Kui sageli taustakontroll otsib seiskunud andureid ja aegumisi. Vaikimisi 30 s.", "label": "Valvekoera Intervall" }, "notify_milestone_message": { @@ -1562,127 +1511,82 @@ "label": "Verstaposti sõnum" }, "notify_milestones": { - "doc": "Komadega eraldatud tsüklite arvud, mis saavutamisel käivitavad ühekordse tähistusteavituse (nt 50, 100, 500, 1000). Tühi keelab verstaposti teavitused.", + "doc": "Tsüklite arvud, mis käivitavad ühekordse tähistusteavituse, komadega eraldatud (nt 50, 100, 500, 1000). Tühi keelab need.", "label": "Tsükli verstapostid" }, "notify_quiet_end_hour": { - "doc": "Mittehäirimise akna lõpp (0-23). Kinnipeetud teavitused edastatakse sel tunnil. Keelamiseks jäta tühjaks.", + "doc": "Mittehäirimise akna lõpp (0-23); kinni peetud teavitused saadetakse siis. Tühi keelab selle.", "label": "Vaikuse tundide lõpp" }, "notify_quiet_start_hour": { - "doc": "Mittehäirimise akna algus (0-23). Lõpetamise, meeldetuletuse ja puhta pesu teavitused, mis vaikuse tundidel käivituksid, peetakse kinni ja edastatakse akna lõppedes. Keelamiseks jäta tühjaks. Toetab keskööd ületavaid aknaid (nt algus 22, lõpp 7).", + "doc": "Mittehäirimise akna algus (0-23). Selle akna ajal peetakse lõpu-, meeldetuletus- ja puhta pesu teavitused kinni kuni akna lõpuni. Võib ületada keskööd (nt 22 kuni 7). Tühi keelab selle.", "label": "Vaikuse tundide algus" }, "peak_rate_message": { - "doc": "Valikuline kohandatud tekst tipptunni tariifi vihjele, mis lisatakse käivitamise teavitusele. Malli muutujad: {device}, {price}. Tühi kasutab sisseehitatud vaikeseadet.", + "doc": "Kohandatud tekst tipptunni hinna vihjele. Malli muutujad: {device}, {price}. Tühi kasutab vaikeseadet.", "label": "Tipptunni tariifi sõnum" }, "peak_rate_threshold": { - "doc": "Kui tsükkel algab ja praegune hind kWh kohta on sellel väärtusel või kõrgem, lisatakse käivitamise teavitusele tipptunni tariifi vihje. 0 või tühi keelab vihje.", + "doc": "Kui hind kWh kohta on tsükli alguses selle väärtusega või kõrgem, lisatakse käivitusteavitusele tipptunni hinna vihje. 0 või tühi keelab selle.", "label": "Tipptunni tariifi lävi (kWh kohta)" }, - "dtw_bandwidth": { - "label": "Kuju sobitamise tolerants", - "doc": "Kui palju sobitaja võib tsüklit ajaliselt venitada, et see salvestatud programmi kujuga sobiks. Kõrgem lubab suuremat kiiruse varieerumist; liiga kõrge võib sobitada vale programmi." - }, - "corr_weight": { - "label": "Kuju vs taseme kaal", - "doc": "Tasakaal kõvera kuju ja absoluutse võimsustaseme vahel sobivuse hindamisel. Kõrgem eelistab kuju; madalam eelistab võimsustaseme sobitamist." - }, - "duration_weight": { - "label": "Kestuse kaal", - "doc": "Kui tugevalt eelistatakse programmi, mille tüüpiline kestus selle tsükliga sobib." - }, - "energy_weight": { - "label": "Energia kaal", - "doc": "Kui tugevalt eelistatakse programmi, mille tüüpiline energiakulu selle tsükliga sobib." - }, "store_brand": { "label": "Seadme bränd", - "doc": "Valikuline. Seadme bränd, mis valitakse kogukonna kataloogist. Kasutatakse sobivate võrdlussalvestiste leidmiseks ja jagamiseks. Jäta tühjaks, kui sa veebifunktsioone ei kasuta." + "doc": "Valikuline. Sinu seadme bränd kogukonna kataloogist, mida kasutatakse võrdlussalvestiste leidmiseks ja jagamiseks. Jäta tühjaks, kui sa veebifunktsioone ei kasuta." }, "store_model": { "label": "Seadme mudel", - "doc": "Valikuline. Seadme mudel, mis valitakse kogukonna kataloogist, kui bränd on määratud. Kui sinu mudelit pole loendis, saad selle kataloogi lisada." + "doc": "Valikuline. Sinu seadme mudel kogukonna kataloogist (vali kõigepealt bränd). Kui seda pole loendis, saad selle lisada." }, "show_contributor": { "doc": "Kuva \" poolt\" omistamine kogukonna kodumasinate ja võrdlustsüklite juures." }, - "enable_phase_matching": { - "label": "Faasiteadlik järelejäänud aeg", - "doc": "Jaga iga käimasolev tsükkel faasideks (kuumutamine, pesu, tsentrifuugimine) ja eelarvesta järelejäänud aeg faaside kaupa, segatuna klassikalise hinnanguga - toetudes tsükli alguses faasi eelarvele ja lõpu lähedal klassikalisele hinnangule. See kohandab loenduri sellele, kui kaua su masin tegelikult kuumutab ja töötab, mis on kõige märgatavam tsükli esimeses pooles. Väljas = ainult klassikaline hinnang. Mõjutab ainult järelejäänud aja kuva; programmi sobitamine ja tsükli tuvastamine jäävad muutmata." - }, - "keep_min_score": { - "label": "Min vastenduse skoor", - "doc": "Madalaim sarnasuskoor, mida kandidaat vajab sobitamisel püsimiseks. Vaikimisi 0,1 on tahtlikult lubav – see lubab isegi nõrku kandidaate ja toetub hilisematele etappidele parima vaste leidmiseks. Tõstke, et kärpida ebatõenäolisi profiile varem; langetage algse kandidaatide hulga laiendamiseks." - }, - "dtw_blend": { - "label": "DTW-segu", - "doc": "Kui palju DTW-joondamise skoor asendab 2. etapi põhiskoorit. 0 = kasuta ainult 2. etapi skoorit, 1 = kasuta ainult DTW-skoorit, 0,5 (vaikimisi) = võrdne segu. Tõstke, et rohkem toetuda aja-joondamisele, kui programmidel on sarnased võimsustasemed kuid erinevad ajastamismustrid." - }, - "dtw_ensemble_w": { - "label": "DTW ensemble-miks", - "doc": "Ensemble-DTW-režiimis (vaikimisi) on see kaal skaleeritud L1 versus tuletise DTW (DDTW). 1,0 = ainult skaleeritud L1, 0 = ainult DDTW, 0,7 = vaikimisi. Skaleeritud variant võrdleb võimsustasemeid; tuletise variant reageerib sellele, kuidas võimsus aja jooksul muutub. Ensemble ühendab mõlemad." - }, - "dtw_ddtw_scale": { - "label": "Tuletise DTW skaala", - "doc": "Pool-küllastumiskaugus tuletise DTW hindamisel. Skoor poolitub, kui DDTW-kaugus võrdub selle väärtusega. Väiksem = tundlikum kuju erinevuste suhtes. Vaikimisi 30 on kalibreeritud tüüpiliste seadmete võimsusjälgedele. Mõjutab ainult ensemble- ja ddtw-DTW-režiime." - }, - "dtw_refine_top_n": { - "label": "DTW täpsustusarv", - "doc": "Mitu parimat 2. etapi kandidaati DTW ümber hindab. DTW on kallim, seega rakendatakse seda ainult N parimale. Vaikimisi 5. Tõstke (7-9-ni), kui õige profiil jõuab mõnikord ainult 4. või 5. kohale pärast 2. etappi – DTW saab selle päästa. Madalama väärtusega sobitumine kiireneb veidi." - }, - "duration_scale": { - "label": "Kestuse teravus", - "doc": "4. etapi kestuse vastavuse karistuse teravus. See on log-suhe, mille juures vastavuse skoor poolitub. Väiksem = rangem: kestuse mittevastavus teeb rohkem kahju. Vaikimisi 0,175 vastab ligikaudu 18% kestuse tolerantsile poolkaalu juures. Kasutage koos Kestuse kaaluga." - }, - "energy_scale": { - "label": "Energia teravus", - "doc": "4. etapi energia vastavuse karistuse teravus. Väiksem = rangem: energia mittevastavus teeb rohkem kahju. Vaikimisi 0,25 on tahtlikult andelikum kui kestuse teravus, kuna energia varieerub koormuse järgi. Kasutage koos Energia kaaluga." - }, "power_profile_interval_min": { "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." + "doc": "Atribuudi power_profile ämbri suurus (keskmised vatid pesa kohta), mida kasutavad planeerijad nagu EMHASS ja tibber_prices. Väiksem säilitab lühikesed hüpped; suurem silub kõverat. Vaikimisi 15 min. Tuvastust ei mõjuta." }, "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." + "doc": "Ainult iOS. Värskenda reaalajas edenemist vaikselt, akut säästva prioriteediga, mida iOS võib kokku koguda. Esimene uuendus ning algus- ja lõputeavitused annavad endiselt märku. Väljas: iga uuendus annab märku." }, "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." + "doc": "Ainult Android. Jäta reaalajas edenemise teavitus puudutamisel alles. Väljas: puudutus sulgeb selle." }, "notify_live_click_action": { "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." + "doc": "Mis avaneb selle seadme teavituse puudutamisel. Tühi: selle seadme WashData paneel. Sisestage töölaua tee (nt /lovelace/laundry) või URL, või \"none\", kui puudutus ei tohi midagi teha." }, "door_opens_at_end": { "label": "Uks avaneb automaatselt lõpus", - "doc": "Nõudepesumasinatele, mis avavad tsükli lõpus ukse kuivatamiseks (AirDry ja sarnased). Selle sisselülitamisel ei peata ukse avamine käimasoleva tsükli ajal enam seda igaveseks; selle asemel, kui uks jääb alla sätestatud viibimisaja jooksul avatuks, loeb WashData tsükli lõppenuks. Lühike avamine (eseme lisamine) ignoreeritakse. Nõuab ukse anduri olendit." + "doc": "Nõudepesumasinatele, mis avavad lõpus kuivatamiseks ukse (nt AirDry). Kui uks jääb allpool määratud viibimisajaks avatuks, lõpetab see tsükli, mitte ei peata seda; lühikest avamist eiratakse. Vajab sätet Ukse Anduri Olem." }, "door_end_dwell_seconds": { "label": "Ukse avamise lõppviibimisaeg", - "doc": "Kui kaua uks peab jääma avatuks, enne kui WashData tsükli lõpetab, kui \"Uks avaneb automaatselt lõpus\" on sisse lülitatud. Piisavalt pikk, et kiire taldrikute lisamine ignoreeritaks (vaikimisi 60 s), piisavalt lühike, et lõpetada kiiresti, kui masin ukse avab." + "doc": "Kui \"Uks avaneb automaatselt lõpus\" on sees: kui kaua peab uks avatuks jääma, enne kui tsükkel lõpeb. Piisavalt kaua, et nõude lisamist eirata (vaikimisi 60 s)." }, "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." + "doc": "Millised tsüklid kujundavad iga programmi kõverat ja sobitamist. Liigi märke eemaldamine ei kustuta midagi; kasulik, kui sa imporditud andmeid ei usalda. Statistika arvestab alati ainult selle masina tsükleid. Kõigi märkide eemaldamist eiratakse." }, "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." + "doc": "Arvestab iga tsüklit hinnaga, mis kehtis selle töö igal hetkel, mitte lõpetamise hetke hinnaga. Vajab hinna olemit. Töötle ajalugu (Diagnostika) arvutab vanemate tsüklite maksumuse ümber." }, "unload_confirm_entity": { "label": "Mahalaadimise Kinnituse Olem", - "doc": "Valikuline olem, mille aktiveerumine tähendab, et sisu on välja võetud; mõeldud masinatele, millele ei saa ukseandurit paigaldada. Sobib igasugune: Zigbee või NFC nupp, input_button abiline, masina ees olev liikumisandur, stseen. Kinnituseks loetakse iga olekumuutust, välja arvatud väljalülitamine ja kättesaamatuks muutumine. Selle määramine lülitab mahalaadimise meeldetuletuse sisse ka seadmel, millel Ukse Anduri Olemit ei ole." + "doc": "Valikuline. Mis tahes olem, mis kinnitab, et sisu on välja võetud, masinatele ilma ukseandurita (nupp, input_button, liikumisandur, stseen). Kinnituseks loetakse iga olekumuutust, välja arvatud väljalülitumine või kättesaamatuks muutumine. Lülitab sisse ka mahalaadimise meeldetuletuse." }, "unload_track_without_door": { "label": "Kinnita Mahalaadimine Käsitsi", - "doc": "Kasutada mahalaadimise meeldetuletust seadmel, millel pole ei Ukse Anduri Olemit ega Mahalaadimise Kinnituse Olemit, ja kinnitada mahalaadimine oma automaatikast: vajutage nupuolemit „Märgi mahalaadituks“ või kutsuge välja teenus ha_washdata.mark_unloaded. Eiratakse, kui Ukse Anduri Olem on määratud, sest see annab signaali juba ise." + "doc": "Kasutage mahalaadimise meeldetuletust ilma ukseanduri või kinnitusolemita. Kinnitage mahalaadimine oma automaatikast nupuga „Märgi mahalaadituks“ või teenusega ha_washdata.mark_unloaded. Eiratakse, kui Ukse Anduri Olem on määratud." }, "notify_icon_color": { "label": "Teavituse Värv", - "doc": "Rõhuvärv selle seadme teavitustele, et pesumasin, kuivati ja nõudepesumasin oleksid ühe pilguga eristatavad. iOS värvib ikooni ja Live Activity edenemisriba lukustuskuval ning Dynamic Islandis. Androidile saadetakse sama värv, kuid mida sellega tehakse, sõltub telefonist: mõni värvib ikooni ja paljud (eriti Samsung One UI) eiravad seda täielikult. Mõlemad mõistavad heksakoodi (nt #03A9F4). Platvormi vaikeseade jaoks jätke tühjaks." + "doc": "Selle seadme teavituste rõhuvärv, et pesumasinat, kuivatit ja nõudepesumasinat oleks lihtne eristada. iOS-is täidab see ikooni taga oleva ringi ja värvib Live Activity riba. Paljud Android-telefonid (eriti Samsung) eiravad seda. Kasutage heksakoodi, nt #03A9F4. Tühi: platvormi vaikeseade." + }, + "share_name": { + "doc": "Näita oma konto nime jagatud tsüklite üleslaadijana. Väljas: jagatakse ilma nimeta." } }, "setting_group": { @@ -1710,9 +1614,6 @@ "door_pause": { "label": "Uks ja Paus" }, - "dtw_refinement": { - "label": "DTW Täpsustamine" - }, "duration_gates": { "label": "Kestuse Väravad" }, @@ -1789,13 +1690,18 @@ "rebuilding": "Taasehitamine…", "analyzing": "Analüüsimine…", "resetting": "Lähtestamine…", - "reverting": "Taastamine…", "trimming": "Kärpimine…", "splitting": "Lõhestamine…", + "stalled": "Seiskunud", "deleting": "Kustutamine…", "imported": "Imporditud", "preparing": "Ettevalmistamine…", - "unlabelling": "Märgistuse eemaldamine…" + "unlabelling": "Märgistuse eemaldamine…", + "working": "Töötlemine…", + "match_uncertain": "Ebakindel: {top} või {runner}", + "match_uncertain_single": "Ebakindel: võib-olla {top}", + "match_sure": "~{pct}% kindel", + "match_uncertain_tip": "WashData pole veel programmi valinud. Protsent näitab, kui sageli juhtiv oletus, {top}, oli varasemates tsüklites õige, kui see oli nii palju ees. Ainult teadmiseks." }, "tab": { "advanced": "Täpsemalt", @@ -1827,13 +1733,17 @@ "toast": { "catalog_refreshed": "Kogukonna kataloog on värskendatud", "contrib_device_changed": "Lisatud kogukonna poodi, kuid valitud seade muutus, kui vorm oli avatud. Vali see seadetes.", + "task_name_required": "Anna ülesandele nimi", + "task_added": "Ülesanne lisatud", + "task_save_failed": "Ülesande salvestamine ebaõnnestus: {error}", + "task_removed": "Ülesanne eemaldatud", + "maint_logged": "Märgitud tehtuks: {task}", "access_saved": "Juurdepääsukontroll on salvestatud", "all_wiped": "Kõik andmed on kustutatud", "analysis_complete_none": "Analüüs on lõpetatud: uusi ettepanekuid pole", "auto_label_complete": "Automaatne silt on lõpetatud", "automation_deleted": "Automaatika kustutatud", "correction_submitted": "Parandus esitatud", - "cycle_deleted": "Tsükkel kustutatud", "cycle_force_stopped": "Tsükkel sundpeatatud", "cycle_labelled": "Tsükkel märgistatud", "cycle_paused": "Tsükkel peatatud", @@ -1849,11 +1759,9 @@ "group_deleted": "Grupp kustutatud", "group_name_required": "Grupi nimi on kohustuslik", "group_saved": "Grupp salvestatud", - "import_successful": "Import õnnestus; integratsiooni uuesti laadimine", "json_required": "Vaja on JSON-i andmeid", "legacy_removed": "Pärandtoimingud eemaldati", "logs_exported": "Palgid eksporditud", - "matching_reverted": "Sobivad kaalud taastati vaikeväärtustele", "min_2_profiles": "Valige rühma jaoks vähemalt 2 profiili", "ml_training_no_improvement": "Treening on lõpetatud: lähtetase säilinud (paranemist pole)", "models_reverted": "Seadmes olevad mudelid taastati algtasemele", @@ -1863,8 +1771,8 @@ "pg_preset_loaded": "Eelseadistus \"{name}\" laaditud", "pg_preset_saved": "Eelseadistus \"{name}\" salvestatud", "pg_sugg_loaded": "{n} soovitatud väärtus laaditud - võrdlemiseks käivita katseala", - "pg_sugg_ml_loaded": "{n} ML-kalibreeritud väärtus laaditud - võrdlemiseks käivita katseala", "phase_name_required": "Etapi nimi on nõutav", + "phase_reset": "Faas \"{name}\" taastati sisseehitatud kujule", "phase_updated": "Faas värskendatud", "phases_saved": "Faasid salvestatud", "preferences_saved": "Eelistused salvestatud", @@ -1911,14 +1819,10 @@ "store_name_required": "Sisesta profiili nimi", "store_import_failed": "Import ebaõnnestus: {error}", "store_imported": "Imporditud profiili {profile}", - "store_program_required": "Sisesta programmi nimi", "store_no_appliance": "Määra kõigepealt seadetes seadme bränd ja mudel.", "store_share_failed": "Jagamine ebaõnnestus: {error}", "store_shared": "Jagatud kogukonna poodi - ootab ülevaatust.", "appliance_added": "Seade lisatud - ootab heakskiitu", - "thanks_confirming": "Aitäh kinnitamast", - "device_approved": "Kogukond kiitis heaks", - "rating_saved": "Kvaliteedihinnang salvestatud", "brand_added": "Bränd lisatud - ootab heakskiitu", "profile_added": "Profiil lisatud - ootab heakskiitu", "saved_except_conflicts": "Salvestatud. Parandage esiletõstetud konfliktid ülejäänud salvestamiseks.", @@ -1941,11 +1845,50 @@ "odometer_saved": "Tsüklite koguarv salvestatud", "odometer_save_failed": "Koguarvu ei õnnestunud salvestada: {error}", "store_pick_profile_share": "Vali profiil, kuhu jagada", - "remove_failed": "Eemaldamine ebaõnnestus: {error}" + "remove_failed": "Eemaldamine ebaõnnestus: {error}", + "analysis_complete": "Analüüs on lõpetatud: ettepanekuid: {count}", + "analysis_failed": "Analüüs ebaõnnestus: {error}", + "auto_detect_failed": "Automaatne tuvastamine ebaõnnestus: {error}", + "auto_label_failed": "Automaatne sildistamine ebaõnnestus: {error}", + "could_not_load_cycle": "Tsüklit ei õnnestunud laadida: {error}", + "create_failed": "Loomine ebaõnnestus: {error}", + "cycles_deleted": "Kustutatud tsükleid: {count}", + "debug_cleared": "Tühjendatud silumisjälgi: {count}", + "discard_failed": "Äraviskamine ebaõnnestus: {error}", + "error": "Viga: {error}", + "export_failed": "Eksport ebaõnnestus: {error}", + "import_failed": "Import ebaõnnestus: {error}", + "invalid_json": "\"{key}\" ei ole kehtiv JSON - parandage see või tühjendage väli enne salvestamist.", + "label_failed": "Sildistamine ebaõnnestus: {error}", + "ml_training_promoted": "Treening lõpetatud: aktiveeritud mudeleid: {count}", + "pause_failed": "Pausile panemine ebaõnnestus: {error}", + "phase_created": "Faas \"{name}\" loodud", + "phase_deleted": "Faas \"{name}\" kustutatud", + "profile_created": "Profiil \"{name}\" loodud", + "rename_failed": "Ümbernimetamine ebaõnnestus: {error}", + "save_failed": "Salvestamine ebaõnnestus: {error}", + "split_complete": "Jagatud tsükliteks: {count}", + "start_failed": "Käivitamine ebaõnnestus: {error}", + "stop_failed": "Peatamine ebaõnnestus: {error}", + "update_failed": "Värskendamine ebaõnnestus: {error}", + "store_download_skipped": "{n} salvestist vahele jäetud: liiga lühikesed, liiga lünklikud või juba teie seadmel.", + "store_import_low_quality": "See salvestis on importimiseks liiga lühike, liiga lünklik või ebausutav.", + "store_import_duplicate": "See salvestis on juba teie seadmel.", + "device_approved": "Kogukond kiitis heaks", + "thanks_confirming": "Aitäh kinnitamast", + "rating_saved": "Kvaliteedihinnang salvestatud", + "store_download_partial": "{n} programmi ei õnnestunud alla laadida; proovi hiljem uuesti.", + "import_no_restore_point": "Imporditud, kuid taastepunkti ei õnnestunud salvestada, seega ei saa seda importi tagasi võtta", + "import_undo_failed": "Importi ei õnnestunud tagasi võtta: {error}", + "import_undone": "Import tagasi võetud: taastati {profiles} profiili ja {cycles} tsüklit", + "auto_label_cancelled": "Automaatne märgistamine tühistati pärast {count} tsükli märgistamist.", + "auto_label_cancelled_one": "Automaatne märgistamine tühistati pärast {count} tsükli märgistamist.", + "auto_label_cancelled_other": "Automaatne märgistamine tühistati pärast {count} tsükli märgistamist.", + "auto_label_done": "Automaatne märgistamine on lõpetatud: märgistati {count} tsüklit, märgistamata jäi {skipped}.", + "auto_label_done_one": "Automaatne märgistamine on lõpetatud: märgistati {count} tsükkel, märgistamata jäi {skipped}.", + "auto_label_done_other": "Automaatne märgistamine on lõpetatud: märgistati {count} tsüklit, märgistamata jäi {skipped}." }, "suggestion": { - "both_agree": "WashData soovitab", - "calibrated_label": "Kalibreeritud", "observed_label": "Mõõdetud", "impact": { "auto_label_confidence": { @@ -1960,10 +1903,6 @@ "higher": "Sulgeb ainult tsüklid, mis kasutasid olulisel määral energiat", "lower": "Sulgeb ka lühemad või madalama energiatarbe tsüklid" }, - "end_repeat_count": { - "higher": "Nõuab lõpusignaali kordamist rohkem kordi – usaldusväärsem, pisut aeglasem", - "lower": "Lõpetab pärast vähemat kordust – kiirem tuvastamine, suurem vale lõpu oht" - }, "learning_confidence": { "higher": "Õpib ainult väga kindlatest vastetest – aeglasem, kuid usaldusväärsem", "lower": "Õpib rohkematest tsüklitest; mudel võib olla müraram" @@ -2002,40 +1941,25 @@ } }, "reason": { - "auto_label_confidence": "15. protsentiili usaldusväärsus {cycles} automaatse sildi puhul, mida kasutaja kunagi ei parandanud ({p15}).", "completion_min_seconds": "Pool puhaste tsüklite p05 kestusest ({minutes} min) {cycles} tsükli lõikes; filtreerib välja kummitustsüklid.{excl}", - "dead_zone_batch": "Hoitakse lühike (käivitusaegse ebastabiilsuse akna mediaan {cycles} tsükli lõikes = {s}s), et tegelik lõpp tuvastataks kiiresti.", - "dead_zone_from_dip": "Põhineb varasel võimsuse langusel, mis tuvastati hetkel {s}s.", - "end_energy_default": "Vaikimisi soovitatud baasjoon tsükli lõpu mürakünnisele.", - "end_energy_false": "p95 valelõppude energia ({p95}Wh) {cycles} tsükli lõikes, alampiiriga 0.2% tsükli mediaanenergiast ({floor}Wh).", - "end_energy_no_false": "Ei ühtki valelõppu {cycles} tsükli lõikes; alampiiriga 0.2% tsükli mediaanenergiast ({floor}Wh).", - "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.", + "match_interval_floored": "Lühim programm ({shortest}s) piiraks selle väärtusega {cap}s, mis jääb alla {minimum}s miinimumi, seega kasutatakse miinimumi: {persistence} järjestikust vastet võtab {budget}s, üle {pct}% sellest programmist. Sobitamine käib ainult näidu saabudes (mediaan={median}s), seega see 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.", "min_off_gap": "Põhineb {gaps} täheldatud tsüklitevahelisel pausil (p05={p05}s). Seadme alampiir: {floor}s.", "min_off_gap_bridge": "Mõõdetud nii, et katab tsükli pikima vaikse lõigu: {span}s + 60s puhver, {spans} sillatud vahest {cycles} puhta tsükli lõikes (jääb alla teie lühima järjestikuste käivituste vahelise pausi, mis on {ceiling}s).", "min_power": "40% madalaimast aktiivvõimsusest p05 juures ({p05}W) {cycles} puhta tsükli lõikes, hoides väljalülituse läve alla tegeliku tarbimise.{excl}", - "ml_auto_label": "Madalaim usaldusväärsus, mida kvaliteedimudel hindas veel puhtaks (p10 {cycles} puhtast tsüklist = {p10}).", - "ml_end_repeat": "{false}/{total} tsüklist ({pct}%) sisaldas pausi, millele lõputuvastaja andis >50%; nõuab {val} lõpukinnitust.", - "ml_off_delay": "Lõputuvastaja kinnitatud pausid: p95 {p95}s + 60s puhver, {pauses} mudeli kontrollitud pausist {cycles} tsükli lõikes (alampiir {floor}s).", "no_update_timeout": "Põhineb täheldatud uuendustempol (p95={p95}s) * 20 (min 60s).", "off_delay_cadence": "Põhineb täheldatud uuendustempol (p95={p95}s) * 5", "off_delay_device_floor": "Kasutati seadmepõhist ohutut miinimumi seadmele {device} ({floor}s).", "off_delay_generic_floor": "Kasutati üldist ohutut miinimumi ({floor}s).", "off_delay_pauses": "Mõõdetud kestma kauem kui tegelikud pausid: p95 tsüklisisene paus {p95}s + 60s puhver, {pauses} pausist {cycles} puhta tsükli lõikes (alampiir {floor}s).", - "profile_match_threshold": "p10 usaldusväärsus {cycles} õige automaatse sildi puhul ({p10}); ohutu alampiir reaalajas kinnitamiseks.", - "sampling_interval": "Uuendusintervalli mediaan, mis täheldati {cycles} puhta tsükli lõikes ({si}s).{excl}", - "smoothing_window": "Mõõdetud silumaks umbes 30s lugemeid {si}s diskreetimissammuga ({samples} näidet).", - "start_duration": "Hoitakse lühike (~üks {si}s diskreetimissamm), et tuvastus algaks võimalikult vara, kuid eiraks siiski üksiknäite piike.", - "thr_batch": "Hoitakse veidi üle madalaima aktiivvõimsuse p05 juures {cycles} tsükli lõikes ({p05}W), et käivitus tuvastataks võimalikult vara ja seiskamislävi jääks alla masina madalaima töövõimsuse.", - "tol_per_profile": "p75 profiilipõhisest kestuse dispersioonist {profiles} profiili lõikes ({cycles} tsüklit); kitsaid profiile ei karistata.", - "tol_pooled": "Põhineb {cycles} viimase märgistatud tsükli koondkestuse dispersioonil (p95 hälve={dev}).", - "watchdog": "Hoitakse nii madal kui ohutu (veidi üle p95 uuendusintervalli {p95}s ja vähemalt 2x proovivõtu intervall {median}s, min 30s), et seiskumised tuvastataks kiiresti ilma valeseisakuteta." + "standby_floor": "Iga viimane tsükkel tarbis lõppedes endiselt {idle} W, seega alla selle jääv lävi ei tuvasta kunagi seadme väljalülitumist. Hoitud sellest kõrgemal; salvestatud {cycles} puhast tsüklit ei ole kunagi peatunud selle all üle {pause} s.", + "thr_batch": "Veidi üle madalaima aktiivvõimsuse (p05) {cycles} tsükli lõikes ({p05}W), et käivitused tabataks vara ja seiskamislävi jääks alla madalaima töövõimsuse.", + "watchdog": "Hoitakse nii madal kui ohutu (veidi üle p95 uuendusintervalli {p95}s ja vähemalt 2x proovivõtu intervall {median}s, min 30s), et seiskumised tuvastataks kiiresti ilma valeseisakuteta.", + "end_energy_floor": "Tõstetud miinimumini, mis tuleneb peatumise lävest ({stop} W) väljalülitumise viivituse ({off_delay} s) jooksul; sellest madalamal saab tsükkel lõppeda ainult varumehhanismi kaudu.", + "max_duration_ratio_default": "Taastatud vaikeväärtus: madalam ülempiir lükkab õige programmi tagasi alati, kui tsükkel kestab tavalisest kauem.", + "min_duration_ratio_default": "Taastatud vaikeväärtus: kõrgem alampiir takistab käimasoleva tsükli tuvastamist, kuni see on juba hästi käima läinud." }, "exclusions": { "summary": "Välja jäetud {total} valesti tuvastatud tsüklit: {parts}.", @@ -2053,7 +1977,8 @@ "no_power": "võimsus puudub", "no_active_power": "aktiivne võimsus puudub" } - } + }, + "now_value": "(praegu {v})" }, "trend": { "down": "Trend langeb", @@ -2061,6 +1986,10 @@ }, "maint": { "bearing_service": "Laagrite hooldus", + "salt": "Lisa soola", + "rinse_aid": "Lisa loputusvahendit", + "lint_filter": "Puhasta ebemefilter", + "condenser_clean": "Puhasta kondensaator", "descale": "Katlakivi eemaldamine", "drum_clean": "Puhasta trummel", "filter_clean": "Puhasta filter", @@ -2071,12 +2000,14 @@ "status_tip": "Tsükli tulemus: Lõpetatud (loomulik lõpp), Katkestatud (järsk võimsuse langus), Jõuga peatatud (käsitsi) või Vajab ülevaatust (tagasiside ootel).", "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. 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.", + "energy_tip": "Kokku tarbitud energia (kWh), arvutatud võimsusest aja jooksul.", + "cost_tip": "Selle tsükli energiakulu, fikseeritud lõpetamisel. Kui Ajaliselt Kaalutud Maksumus on sees, arvestatakse iga hetke selle enda hinnaga; muidu lõpu hinnaga. 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.", - "cost_dynamic_tip": "Ajaliselt kaalutud: arvestatud tsükli ajal kehtinud hinna järgi (tegelik {price} {cur}/kWh)." + "cost_dynamic_tip": "Ajaliselt kaalutud: arvestatud tsükli ajal kehtinud hinna järgi (tegelik {price} {cur}/kWh).", + "dbg_conf": "Usald.", + "dbg_mae": "MAE", + "dbg_corr": "Korr." }, "stat": { "avg": "keskm.", @@ -2100,7 +2031,10 @@ "notify_finish": "Saadetaks lõpetamise teavitus.", "notify_milestone": "Saadetaks verstaposti teavitus.", "notify_held": "Teavitus peeti vaikse aja tõttu kinni.", - "finished": "Tsükkel jõudis lõppolekusse ja lõppes." + "finished": "Tsükkel jõudis lõppolekusse ja lõppes.", + "match_reverted": "Programm langes tagasi tuvastamise olekusse: selle tulemus langes tunduvalt alla oma tipptaseme.", + "verified_pause": "Reaalajas joondamine kinnitas (või vabastas) programmi sees oleva pausi. Kuni see kehtib, tsüklit ei lõpetata.", + "stalled": "Tsükkel püsis ooterežiimi tarbimisel kauem, kui see programm seal kunagi pausi teeb, kuigi tööd oli veel jäänud. Kuvatakse kui Peatatud (Seiskunud); tsükkel jääb avatuks." }, "store": { "search_ph2": "Otsi brändi või mudeli järgi…", @@ -2109,11 +2043,7 @@ "programs_count": "Programmid: {n}", "browse": "Sirvi", "device": "Seade", - "favorites": "Lemmikud", - "search_ph": "Otsi brändi või mudeli järgi…", "no_results": "Sobivaid seadmeid ei leitud. Proovi teistsugust otsingut.", - "n_programs": "{n} programmi", - "n_cycles": "{n} tsüklit", "no_programs": "Sellele seadmele pole veel programme jagatud.", "no_cycles": "Selle programmi jaoks pole veel võrdlustsükleid jagatud.", "anon": "anonüümne", @@ -2125,19 +2055,21 @@ "merge_target": "Liida profiiliga", "description": "Kirjeldus (valikuline)", "contributed_by": "autor {name}", + "website": "Poe veebisait ↗", + "website_tip": "Ava kogukonna poe veebisait", + "favorites": "Lemmikud", "rating_summary": "★ {avg} ({n})", "no_ratings": "Hinnanguid veel pole", - "website": "Poe veebisait ↗", - "website_tip": "Ava kogukonna poe veebisait" + "unreachable": "Kogukonna poega ei õnnestunud ühendust luua. Kontrolli ühendust ja proovi uuesti." }, "settings": { "gear": { - "title": "Seaded" + "title": "Eelistused" } }, "link": { - "manual": "Juhend ↗", - "add_appliance": "Lisa oma seade" + "maintenance_supporter": "Maintenance Supporter GitHubis ↗", + "manual": "Juhend ↗" }, "tip": { "add_brand": "Brändi pole loendis? Lisa see kogukonna kataloogi", @@ -2171,7 +2103,13 @@ "rebuild": { "envelopes": "Võimsusvahemike taasloomine" }, - "cancelling": "Katkestan..." + "cancelling": "Katkestan...", + "auto_label": { + "running": "Tsüklite automaatne sildistamine" + }, + "store_download": { + "running": "Poest allalaadimine" + } }, "setup": { "hdr": { @@ -2180,26 +2118,24 @@ }, "phase0": { "washer": "WashData tuvastab juba teie tsükleid. Salvestage esimene tsükkel, et lubada programmide nimed ja ajahinnangud.", - "dishwasher": "WashData jälgib. Nõudepesumasinatel on keerukad tsüklid – esimese tsükli salvestamine on tungivalt soovitatav. Kui tuvastatud tsükkel töötab liiga kaua, kasutage tsükli redaktorit selle kärpimiseks enne profiilina salvestamist.", + "dishwasher": "WashData jälgib. Nõudepesumasina tsüklid on keerukad, seega salvestage oma esimene tsükkel. Kui tuvastatud tsükkel kestab liiga kaua, kärpige seda tsükliredaktoris enne profiilina salvestamist.", "generic": "WashData jälgib. Salvestage või märgistage tuvastatud tsükkel profiilide loomise alustamiseks." }, "phase1a": { - "labelled": "Hea algus – teie esimene programm on salvestatud. Puhtaimate andmete saamiseks kaaluge järgmise tsükli salvestamist salvestusvidinaga." + "labelled": "Hea algus: teie esimene programm on salvestatud. Puhtaimate andmete jaoks salvestage järgmine tsükkel käsitsi salvestamisega." }, "phase1b": { - "recorded": "Teie salvestus on salvestatud kui {profile_name}. Nüüd salvestage või märgistage oma teised sagedased programmid katvuse suurendamiseks." + "recorded": "Teie salvestus salvestati nimega {profile_name}. Nüüd salvestage või märgistage oma teised sagedased programmid." }, "phase1c": { - "verify": "Teil on {count} programmi kogukonnast. Käivitage tsükkel, et kontrollida, kas WashData tunneb selle õigesti ära – sobitamine paraneb, kui teie seade koostab oma ajaloo." + "verify": "Teil on kogukonnast {count} programmi. Käivitage tsükkel, et kontrollida, kas WashData tunneb selle ära; sobitamine paraneb, kui teie seade kogub oma ajalugu." }, "phase2": { - "cluster": "WashData on näinud {count} tsüklit, mis ei vasta ühelegi salvestatud programmile – need näevad sarnased välja. Kas soovite neile luua uue profiili?", + "cluster": "WashData on näinud {count} sarnast tsüklit, mis ei sobi ühegi salvestatud programmiga. Kas luua neile uus profiil?", "unmatched": "Viimane tsükkel ei vastanud ühelegi salvestatud programmile. Kas see on uus programm?" }, "phase3": { - "suggestions": "WashDatal on teie tsüklite ajaloo põhjal seadete soovitusi – vaadake need läbi tuvastustäpsuse parandamiseks.", - "groups": "Mõned teie profiilid näevad välja nagu sama programm erinevatel temperatuuridel. Korraldage need rühma parema sobitamise jaoks.", - "phases": "Lisage programmifaasid profiilile {profile_name} täpsemate järelejäänud aja hinnangute saamiseks." + "suggestions": "WashDatal on teie tsüklite ajaloo põhjal seadete soovitusi. Vaadake need läbi, et tuvastust parandada." }, "phase4": { "healthy": "See seade on täielikult seadistatud ({profile_count} profiili)." @@ -2212,12 +2148,58 @@ "create_from_cluster": "Loo profiil nendest tsüklitest", "create_profile": "Loo sellele profiil", "review_suggestions": "Vaata soovitusi läbi", - "organise_profiles": "Korralda profiile", - "configure_phases": "Seadista faasid", "skip_step": "Jäta see samm vahele", "skip_forever": "Ära näita enam", "hide_guidance": "Peida juhised", "show_guidance": "Näita juhiseid" } + }, + "access": { + "edit": "Muutmine", + "full": "Täielik", + "inherit": "Päri", + "none": "Puudub (peidetud)", + "read": "Lugemine" + }, + "pg_label_reason": { + "no_winner": "Ükski programm ei sobinud", + "below_floor": "Madalam kui Õppimise Usaldus", + "ambiguous": "Märgitud ebaselgeks", + "margin": "Liiga lähedal teisele kandidaadile", + "unknown_profile": "Programmi pole enam olemas", + "not_evaluated": "Ei hinnatud" + }, + "diagram": { + "accum": "kogun.", + "auto": "auto", + "below_off": "all = väljas", + "confirmed": "kinnitatud", + "energy_counts": "loeb energia", + "feedback": "tagasiside", + "gap_below_min": "vahe alla min = üks tsükkel", + "heat_phase": "kuumutusfaas", + "held_at_100": "hoitakse 100% juures", + "ignored": "eiratud", + "kept": "säilitatud", + "max": "max", + "min": "min", + "minus_tol": "-tol", + "no_match": "vastet pole", + "no_updates": "uuendusi pole", + "off_delay_wait": "väljalülitusviivitus", + "off_gap": "väljalülitusvahe", + "plus_tol": "+tol", + "profile": "profiil", + "reading_interval": "tüüpiline lugemite intervall", + "sensor_offline": "andur võrguühenduseta: sundpeatus", + "si": "PI", + "spike_ignored": "hüpe eiratud", + "spike_ignored_short": "hüpe: eiratud", + "start": "start", + "stop": "stopp", + "tail_energy_resets": "lõpuosa energia üle läve: taimer lähtestub", + "thr": "lävi", + "too_short": "liiga lühike", + "tumble_kept": "trummelimpulsid säilitatud" } } diff --git a/custom_components/ha_washdata/translations/panel/fi.json b/custom_components/ha_washdata/translations/panel/fi.json index 10c53372..ba0b8ff1 100644 --- a/custom_components/ha_washdata/translations/panel/fi.json +++ b/custom_components/ha_washdata/translations/panel/fi.json @@ -3,6 +3,7 @@ "min": "min", "entity_not_found": "Entiteettiä ei löytynyt", "running_long": "Käy pitkään", + "stalled": "Jumissa", "remaining": "jäljellä", "progress": "edistyminen", "show_time_remaining": "Näytä jäljellä oleva aika", @@ -48,16 +49,16 @@ } }, "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_tip": "Loppua kohti tämä ohjelma on hiljaa noin {mins} min ja ottaa sitten noin {watts} W {secs} s ajan. Havaittu {seen}/{measured} mitatussa syklissä.", "quiet_tail": "~{mins} min hiljainen loppu", - "artifact_tip": "{n} poikkeavaa havaittu (esim. oven avautuminen kesken syklin) – avaa nähdäksesi ne kaaviossa", + "artifact_tip": "{n} poikkeamaa havaittu (esim. ovi avattu kesken syklin) - avaa nähdäksesi ne kaaviossa", + "artifact_tip_one": "{n} poikkeama havaittu (esim. ovi avattu kesken syklin) - avaa nähdäksesi sen kaaviossa", + "artifact_tip_other": "{n} poikkeamaa havaittu (esim. ovi avattu kesken syklin) - avaa nähdäksesi ne kaaviossa", "auto": "(tunnistettu automaattisesti)", "built_in_tag": "sisäänrakennettu", "declining": "↘ vähenemässä", "energy_low": "Tavallista vähemmän energiaa", "energy_spike": "Tavallista enemmän energiaa", - "fair_fit": "tyydyttävä laatu", - "fair_fit_tip": "Kohtalainen vastaavuus – joillakin tälle profiilille määritetyillä jaksoilla on alhaisemmat luottamuspisteet. Merkitse lisää jaksoja tai tallenna profiili uudelleen tarkkuuden parantamiseksi.", "feedback_requested": "Palautetta pyydetty", "golden_cycle": "Tallennettu vertailujakso", "duration_outlier": "{n} poikkeavan pituista jaksoa", @@ -66,27 +67,36 @@ "needs_review": "Vaatii tarkistuksen", "overrun": "Juoksi tavallista pidempään", "poor_fit": "heikko laatu", - "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", + "restart_gap_tip": "{n} HA-uudelleenkäynnistyskatkosta: tehoviivan datassa on aukkoja", + "restart_gap_tip_one": "{n} HA-uudelleenkäynnistyskatkos: tehoviivan datassa on aukko", + "restart_gap_tip_other": "{n} HA-uudelleenkäynnistyskatkosta: tehoviivan datassa on aukkoja", "reviewed": "Tarkistettu", "steady": "→ tasainen", "underrun": "Päättyi tavallista nopeammin", - "using_defaults": "Toimitettujen oletusarvojen käyttäminen", - "using_tuned": "Käyttämällä viritettyjä painoja", "overrun_ratio": "({x}x odotetusta)", "underrun_ratio": "({pct}% odotetusta)", - "awaiting_tip": "Odottaa yhteisön hyväksyntää", - "awaiting_n": "Odottaa hyväksyntää · {n} vahvistusta", - "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.", - "armed": "(koskee seuraavaa sykliä)" + "armed": "(koskee seuraavaa sykliä)", + "awaiting_tip": "Odottaa yhteisön hyväksyntää", + "awaiting_n": "Odottaa hyväksyntää · {n} vahvistusta", + "awaiting": "Odottaa hyväksyntää", + "approved": "Hyväksytty", + "matcher_unused": "Ei vielä täsmäytetty yhteenkään sykliisi", + "matcher_used": "Täsmäytetty {n} sykliisi", + "matcher_used_one": "Täsmäytetty {n} sykliisi", + "matcher_used_other": "Täsmäytetty {n} sykliisi", + "matcher_used_tip": "Tämän laitteen syklit, jotka WashData täsmäytti itse tähän tuotuun ohjelmaan. Jos tuonti ei koskaan täsmää, se ei ehkä sovi laitteellesi, ja voit poistaa sen. Lasketaan paikallisesti; mitään ei lähetetä kauppaan." }, "btn": { + "pg_preset_delete": "Poista esiasetus", + "pg_preset_load": "Lataa esiasetus", + "pg_preset_load_tip": "Korvaa testialueen arvot tällä esiasetuksella (aktiivisiin asetuksiin ei kosketa)", + "pg_preset_save": "Tallenna esiasetuksena", + "pg_preset_save_tip": "Tallenna kaikki alla olevat arvot nimettynä esiasetuksena tälle laitteelle", "reset_zoom": "Nollaa zoomaus", "set_brand_model": "Aseta merkki ja malli", "refresh_catalog": "Päivitä luettelo", @@ -97,12 +107,11 @@ "add_timer": "+ Lisää ajastin", "apply_all": "Käytä kaikkia", "apply_label": "Käytä etikettiä", - "apply_set_b": "Ota käyttöön joukko B", "apply_split": "Käytä jakoa", "apply_trim": "Käytä leikkausta", "auto_detect_split": "Automaattinen tunnistus", "auto_label_cycles": "Automaattinen merkintäjaksot", - "auto_label_cycles_tip": "Määritä profiilinimet automaattisesti merkitsemättömille sykleille, joiden osuvuusvarmuus ylittää kynnyksen", + "auto_label_cycles_tip": "Nimeä merkitsemättömät syklit, joiden täsmäytysvarmuus ylittää kynnyksen.", "cancel": "Peruuta", "clear_debug": "Tyhjennä virheenkorjaustiedot", "clear_splits": "Tyhjennä", @@ -112,8 +121,6 @@ "convert_to_automation": "Muunna automaatioon", "correct": "Korjaa…", "create": "Luo", - "create_from_cluster": "Luo profiili {count} sykliltä", - "create_profile": "+ Luo profiili", "delete": "Poista", "delete_group": "Poista ryhmä", "delete_group_tip": "Poista vain tämä ryhmä - jäsenprofiilit säilytetään", @@ -124,22 +131,19 @@ "done": "Tehty", "edit": "Muokkaa", "export": "Vie", - "export_json": "Vie JSONiin", "force_stop": "Pakota pysäytys", "force_stop_tip": "Lopeta nykyinen sykli välittömästi ja merkitse se pakotetusti pysäytetyksi", "from_template": "Mallista ▾", "ignore": "Sivuuta", "import_json": "Tuo JSONista", - "import_overwrite": "Tuo (korvaa tiedot)", "inspect": "Tarkasta", "label": "Merkitse", - "load": "Lataa", "load_more": "Lataa lisää", "manage": "Hallitse", "merge": "Yhdistä", "new_automation": "+ Uusi automaatio", "new_group": "+ Uusi ryhmä", - "new_group_tip": "Ryhmittele lähes identtiset profiilit (sama muoto/kesto, eri lämpötila tai linkous), jotta täsmäytys valitsee niiden välillä luotettavasti", + "new_group_tip": "Ryhmittele lähes samanlaiset profiilit (sama muoto, eri lämpötila tai linkous), jotta täsmäytys erottaa ne toisistaan.", "new_phase": "+ Uusi vaihe", "new_profile": "+ Uusi profiili", "new_profile_tip": "Luo uusi ohjelmaprofiili olemassa olevasta merkitystä syklistä tai tallennuksesta", @@ -147,36 +151,25 @@ "on_cycle_started": "Kierros alkoi", "pause_cycle": "Tauko", "pause_cycle_tip": "Keskeytä käynnissä oleva sykli – laite jatkaa siitä, mihin se jäi", - "pg_apply_device": "Käytä laitteeseen", - "pg_autofill": "Autotäyttö", - "pg_load_calibrated": "Lataa kalibroidut (ML) ({n})", - "pg_load_calibrated_tip": "Lataa ML-kalibroidut asetukset testialueen ohituksina", "pg_load_live": "Lataa aktiiviset asetukset", "pg_load_live_tip": "Lue integraation nykyiset asetukset uudelleen ja hylkää kaikki testialueen muokkaukset", "pg_load_suggested": "Lataa ehdotetut ({n})", "pg_load_suggested_tip": "Lataa autovirittimen nykyiset ehdotukset testialueen ohituksina", - "pg_preset_delete": "Poista esiasetus", - "pg_preset_load": "Lataa esiasetus", - "pg_preset_load_tip": "Korvaa testialueen arvot tällä esiasetuksella (aktiivisiin asetuksiin ei kosketa)", - "pg_preset_save": "Tallenna esiasetuksena", - "pg_preset_save_tip": "Tallenna kaikki alla olevat arvot nimettynä esiasetuksena tälle laitteelle", "pg_publish_all": "Tallenna asetuksiin ({n})", "pg_publish_one": "Tallenna \"{label}\" integraation asetuksiin", - "play": "Toista", "process": "Käsittele", "process_history": "Käsittele nyt", "process_recording": "Tallenna tallennus", "process_tip": "Tallenna tallennettu jälki uutena tai olemassa olevana profiilina", "rebuild": "Rakenna kirjekuoret uudelleen", "rebuild_envelope": "Rakenna kirjekuori uudelleen", - "rebuild_tip": "Laske uudelleen kaikkien profiilien odotettu tehokuori (min/maks-alue) merkityistä sykleistä – suorita uusien syklien merkitsemisen tai vanhojen korjaamisen jälkeen", + "rebuild_tip": "Rakenna jokaisen profiilin odotettu tehoalue uudelleen sen merkityistä sykleistä. Suorita syklien merkitsemisen tai korjaamisen jälkeen.", "rec_start_tip": "Aloita laitteen tehojäljen tallentaminen – käynnistä juuri ennen syklin suorittamista", "rec_stop": "Pysäytä", "rec_stop_tip": "Lopeta tallennus ja pidä tallennettu jälki tarkistettavana", "record": "Aloita tallennus", "refresh": "Päivitä", "refresh_diag_tip": "Lataa tallennustilastot uudelleen", - "refresh_logs_tip": "Lataa uusimmat lokitietueet uudelleen", "refresh_settings_tip": "Lataa asetukset uudelleen palvelimelta", "relabel": "Nimeä uudelleen ({count})", "remove": "Poista", @@ -184,7 +177,6 @@ "reset": "Nollaa", "reset_ml_models_tip": "Hävitä hienosäädetyt mallit ja palaa sisäänrakennettuun malliin. WashData voi oppia ne uudelleen myöhemmin.", "reset_to_builtin": "Palauta sisäänrakennetut mallit", - "reset_to_defaults": "Palauta oletusasetukset", "resume_cycle": "Jatka sykliä", "resume_cycle_tip": "Jatka pysäytettyä sykliä", "revert_settings": "Hylkää muutokset", @@ -197,8 +189,6 @@ "run_analysis": "🔍 Suorita ehdotusanalyysi", "run_analysis_tip": "Analysoi tallennetut jaksot nyt ja päivitä viritysehdotukset", "run_auto_label": "Suorita Auto-Label", - "run_both": "Aja A vs B", - "run_simulation": "Aja simulaatio", "save": "Tallenna", "save_access_control": "Tallenna kulunvalvonta", "save_group": "Tallenna ryhmä", @@ -210,10 +200,8 @@ "save_settings": "Tallenna asetukset", "select": "Valitse", "select_all": "Valitse kaikki", - "select_last20": "Viimeiset 20", "select_none": "Ei mitään", "show_only": "Näytä vain", - "skip_setup": "Ohita määritys", "split": "Jaa", "stop": "Pysäytä", "submit_correction": "Lähetä korjaus", @@ -222,36 +210,31 @@ "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.", + "unlabel_selected_tip": "Poista merkintä valituista sykleistä. Ne jäävät historiaan, mutta eivät enää muovaa tätä profiilia.", "use": "Käytä", "wipe_all": "Pyyhi kaikki tiedot", "delete_selected": "Poista valitut ({n})", - "group_suggest": "Ryhmä {n}: {members}", "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", "import": "Tuo", "disconnect": "Katkaise yhteys", "connect_github": "Yhdistä GitHub", - "save_appliance": "Tallenna laite", "share_to_store": "Jaa kauppaan", "share": "Jaa", - "confirm_appliance": "Vahvista tämä laite", - "confirmed": "Vahvistettu", "browse_community_setups": "Selaa yhteisön asetuksia", "download_device": "Lataa tämä asetus", "share_device": "⬆ Jaa tämä laite", - "share_device_tip": "Jaa tämä laite ja sen tallennetut viitesyklit yhteisökauppaan, jotta muut saman koneen omistajat voivat ottaa ne käyttöön", + "share_device_tip": "Jaa tämä laite ja sen viitesyklit yhteisökauppaan, jotta saman koneen omistajat voivat ottaa ne käyttöön.", "share_n": "Jaa {n} sykli(ä)", "export_selected": "Vie (valitse tiedot)", "export_all": "Pikavienti: vie kaikki", - "import_raw": "Lisäasetukset: korvaa kaikki JSONista", "download_export": "Lataa vienti", "analyze_import": "Analysoi tiedosto", "import_selected": "Tuo valitut", "back": "Takaisin", "mute_suggestion": "Lopeta tämän asetuksen ehdottaminen", "reset_muted": "Palauta vaiennetut", + "reset_phase_tip": "Kumoa muokkauksesi ja palauta sisäänrakennettu vaihe", "import_power_history": "Tuo tehohistoria", "hist_read_recorder": "Lue Home Assistantista", "hist_scan": "Etsi syklejä", @@ -260,7 +243,15 @@ "save_total_cycles": "Tallenna kokonaismäärä", "clear": "Tyhjennä", "add_brand": "Lisää merkki", - "add_appliance": "Lisää laite" + "log_done_task": "Kirjaa tehdyksi: {task}", + "add_task": "Lisää tehtävä", + "add_appliance": "Lisää laite", + "retry": "Yritä uudelleen", + "confirm_appliance": "Vahvista tämä laite", + "confirmed": "Vahvistettu", + "create_profile_from_gap": "Luo profiili", + "undo_import": "Kumoa viimeisin tuonti", + "pg_rerun": "Päivitä simulaatio" }, "note": { "off_delay": { @@ -268,10 +259,6 @@ } }, "conflict": { - "anti_wrinkle_exit": { - "exit": "Täytyy olla alle pysäytyskynnyksen ({stop} W); muuten ryppyjeneston poistumistehoa ei huomioida", - "stop": "Täytyy olla yli ryppyjeneston poistumistehon ({exit} W)" - }, "anti_wrinkle_max": { "max": "Täytyy olla yli käynnistyskynnyksen ({start} W); muuten ryppyjeneston kestoraja ohitetaan", "start": "Täytyy olla alle ryppyjeneston enimmäistehon ({max} W)" @@ -309,14 +296,14 @@ "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." + "power_sensor": "Tämä on laitteen Tehoanturi, ei hinta. Sykleistä laskettaisiin wattilukema kWh-hintana; tyhjennä tämä kenttä.", + "energy_sensor": "Tämä on laitteen Energiamittari, ei hinta. Sykleistä laskettaisiin mittarin lukema kWh-hintana; tyhjennä tämä kenttä.", + "device_class": "Tämä anturi mittaa suuretta {dc}, ei hintaa per kWh. Tyhjennä tämä kenttä tai valitse tariffianturi.", + "unit": "Tämän anturin yksikkö on {unit}, ei hinta per kWh. 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ä" + "duration": "Oltava pienempi kuin Päivityksettömyyden aikakatkaisuaika ({to} s), jotta jumihälytys ehtii laueta ensin", + "timeout": "Oltava suurempi kuin Pumpun jumiutumiskesto ({ps} s), jotta jumihälytys ehtii laueta ensin" }, "start_dur": { "sampling": "Näytteenottoväli ylittää käynnistyksen keston ({sdt} s); yksittäiset piikit voivat avata syklin", @@ -334,10 +321,15 @@ "sampling": "Näytteenottovälin tulee olla enintään puolet tarkkailijavälistä ({wi} s)" }, "settings_banner": "Asetusten ristiriitoja: {n}. Tarkista korostetut osiot ja korjaa ne ennen tallentamista.", - "settings_banner_btn": "Siirry ensimmäiseen" + "settings_banner_btn": "Siirry ensimmäiseen", + "end_energy": { + "energy": "Liian tiukka Pysäytyskynnykselle ({w} W) Sammutusviiveen ({d} s) aikana; sykli voi päättyä vain varapolun kautta", + "stop": "Lopetuksen energiakynnys ({e} Wh / {d} s) sallii vain {p} W" + } }, "hdr": { - "ab_comparison": "A/B-asetusten vertailu", + "custom_tasks": "Omat tehtävät", + "maintenance_supporter": "Tarvitsetko enemmän?", "access_control": "Kulunvalvonta", "add_maintenance": "Lisää huoltotapahtuma", "automation_delete": "Poista tämä automaatio", @@ -346,28 +338,22 @@ "automation_review_this_cycle": "Tarkista tämä sykli", "automations": "Automaatiot", "clear_debug": "Tyhjennä virheenkorjausjäljet", - "cycle_simulator": "Syklisimulaattori", "diagnostics_pref": "Diagnostiikka", "display": "Näyttö", - "dtw_inspector": "DTW-tarkastin", "export_import": "Vienti / tuonti", - "getting_started": "Aloittaminen", "live_power": "Reaaliaikainen teho", "logs": "Lokit", - "logs_diagnostics": "Diagnostiikka", "maintenance": "Ylläpitotoimet", "maintenance_log": "Huoltoloki", "maintenance_reminders": "Huoltomuistutukset", "manual_recording": "Manuaalinen tallennus", - "manual_recording_tip": "Suorita sykli tarkoituksella WashDatan tallentaessa tehojälkeä. Käynnistä juuri ennen laitteen käynnistymistä, pysäytä kun se päättyy, sitten käsittele tallentaaksesi se nimettynä profiilina.", + "manual_recording_tip": "Tallenna sykli tarkoituksella: paina Alku juuri ennen kuin laite käynnistyy ja Pysäytä, kun se päättyy. Tallenna sykli sitten profiiliksi Käsittele-painikkeella.", "ml_learned": "Mitä WashData on oppinut", - "ml_matching_tuning": "Ohjelmasovitus hienosäätö", "ml_settings_card": "Asetukset", "ml_smart_learning": "Älykäs oppiminen", "my_preferences": "Omat asetukset", "no_automations": "Mikään automaatio ei viittaa tähän laitteeseen vielä.", "panel_settings": "Paneelin asetukset", - "param_sweep": "Parametripyyhkäisy", "pg_settings_source": "Asetusten lähde", "phase_catalog": "Vaiheen katalogi", "playground": "Testialue", @@ -376,11 +362,8 @@ "status": "Tila", "status_graph": "Tilakaavio", "storage_stats": "Tallennustilastot", - "test_history": "Testaa historialla", "toggle_sidebar": "Vaihda Home Assistant -sivupalkki", - "tools_and_data": "Työkalut ja tiedot", "wipe_history": "Pyyhi historia", - "recommendations": "Suositukset ({n})", "pg_detection_params": "Tunnistusasetukset", "pg_outcome": "Simulaation tulos", "pg_across_cycles": "Kaikissa jaksoissasi", @@ -388,7 +371,8 @@ "online_account": "Yhteisökauppa ja verkkotoiminnot", "import_power_history": "Tuo tehohistoria", "service_status": "Huollon tila", - "online_account_tab": "Verkko ja yhteisö" + "online_account_tab": "Verkko ja yhteisö", + "live_match_debug": "Reaaliaikaisen täsmäytyksen virheenkorjaus" }, "health": { "fair": "Tyydyttävä profiililaatu", @@ -396,12 +380,17 @@ "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_tip": "Sijainti täsmätyn ohjelman tallennetulla käyrällä: {pct} %. Löydetään kohdistamalla sykli profiiliin, joten se pysyy oikeana, vaikka ajo olisi pidempi tai lyhyempi. Päivittyy laitteen ollessa hiljaa, joten se voi jäädä jälkeen.", "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", + "task_name": "Tehtävä", + "every_cycles": "Väli (sykleinä)", + "every_days": "Väli (päivinä)", + "new_task_name": "Uusi tehtävä", + "days_since_service": "{since} / {total} päivää", "actions": "Toiminnot", "activity": "Toiminta", "administrators": "Järjestelmänvalvojat", @@ -409,10 +398,8 @@ "aria_cycle_chart": "Syklin tehokäyrä", "aria_envelope_chart": "Profiilin tehon verhokäyrän kaavio", "aria_phase_chart": "Vaihe-editorin kaavio", - "aria_playground_chart": "Syklin tehokäyrä ja raahattavat tunnistuskynnykset", "aria_power_chart": "Tehonkulutuskaavio", "aria_spaghetti_chart": "Päällekkäiset syklien tehokäyrät", - "aria_sweep_chart": "Parametripyyhkäisyn tuloskaavio", "artifact_anomaly": "Anomalia", "artifact_high_power": "Korkea teho", "artifact_interruption": "Keskeytys", @@ -426,149 +413,99 @@ "cohesion_good": "koheesio {pct} %", "cohesion_low": "⚠ matala koheesio {pct} %", "compare_profiles": "Vertaa profiileihin", - "concurrency": "Samanaikaisuus", "confidence": "Luottamus", "confidence_threshold": "Luottamusraja", "correct_duration": "Oikea kesto (min, valinnainen)", "correct_profile": "Oikea profiili", - "correlation": "Korrelaatio", "cost": "Maksaa", "create_new_profile": "+ Luo uusi profiili…", "current_phase": "Nykyinen vaihe", - "curves": "Käyrät", "cycle": "Sykli", "cycle_date_display": "Jakson päivämäärän näyttö", - "cycle_health": "Syklin kunto", - "cycle_index": "syklin indeksi →", - "cycles": "Syklit", "cycles_count": "Syklit", "cycles_lc": "sykliä", - "dashed": "katkoviiva", "date": "Päivämäärä", "debug_traces": "Virheenkorjausjäljet", - "default": "Oletus", "default_access_level": "Oletustaso käyttäjille, joita ei ole lueteltu alla", "default_other": "Oletus (muut laitteet)", "default_tab": "Oletusvälilehti paneelia avattaessa", - "delta": "Δ", "description": "Kuvaus", - "dtw_blend_w": "Sekoituspaino", - "dtw_blended": "Yhdistetty pisteet", - "dtw_ddtw": "DDTW-pisteet", - "dtw_l1": "L1-pisteet", - "dtw_matched_profile": "(syklin täsmätty profiili)", "duration": "Kesto", - "duration_agreement": "Keston vastaavuus", - "emphasis": "Painopiste", "enable_access_control": "Ota käyttöön käyttäjäkohtainen pääsynhallinta", "energy": "Energiaa", - "energy_agreement": "Energian vastaavuus", - "event_timeline": "Tapahtumien aikajana", "event_type": "Tapahtuman tyyppi", "expected": "Odotettu", + "suspected_profile": "{name} (oletettu)", + "runner_up_profile": "{name} (toiseksi paras)", "expected_duration": "Odotettu kesto (min)", "file_kb": "Tiedosto (kB)", - "final_score": "Lopulliset pisteet", "from": "Mistä", "gap_s": "aukko (t)", "group_name": "Ryhmän nimi", "head_trim": "Leikkaus alusta (s)", - "health": "Terveys", "hide_tabs": "Piilota välilehdet muille kuin järjestelmänvalvojille", - "in_use": "Käytössä", "json_data": "JSON-tiedot", "label": "Vastaava ohjelma", "last": "Viimeiset", "last_checked": "Viimeksi tarkistettu:", "load_from_file": "Lataa tiedostosta", - "mae_score": "MAE-pisteet", "manual_duration": "Manuaalinen kesto (min, valinnainen)", "match": "Osuma", "members": "Jäsenet", - "metric": "Mittari", "mode_existing_profile": "Lisää olemassa olevaan profiiliin", "mode_new_profile": "Luo uusi profiili", "models_fine_tuned": "(hienosäädetyt mallit: {count})", - "n_classic_suggestions": "{n} klassinen", - "n_ml_suggestions": "{n} ML", "n_selected": "{n} valittu", - "n_tuning_suggestions": "{n} viritysehdotus", - "na": "ei saatavilla", + "n_tuning_suggestions": "{n} viritysehdotusta", + "n_tuning_suggestions_one": "{n} viritysehdotus", + "n_tuning_suggestions_other": "{n} viritysehdotusta", "new_profile_name": "Uusi profiilinimi", "notes": "Muistiinpanot", - "of": "/", - "outcome": "Tulos", "panel_default_tab": "Oletus-välilehti", "panel_language": "Paneelin kieli", "per_cycle_short": "sykli", - "pg_autofill_tip": "Täytä väli automaattisesti nykyisestä arvosta ({lo}–{hi})", "pg_best_value": "Paras löydetty arvo", "pg_current_value": "Nykyinen arvo:", "pg_drag_hint": "Vedä viivaa kaaviossa", - "pg_dtw": "DTW", "pg_ev_ending": "Päättyy", "pg_ev_running": "Käynnissä", "pg_group_advanced": "Erikoistapaukset", "pg_group_detection": "Tunnistuksen laukaisimet", "pg_group_timing": "Ajoitussäännöt", "pg_idle": "Vapaana", - "pg_last_run": "Viimeisin ajo: {date}", - "pg_match_score": "Osumapisteet", - "pg_pct_of": "{p} % / {total}", "pg_poor_match": "Huono osuma", "pg_preset": "Testialueen esiasetus", "pg_preset_name": "Uuden esiasetuksen nimi", "pg_preset_none": "Valitse esiasetus…", "pg_strong_match": "Vahva osuma", - "pg_tile_ambiguous": "Epäselvä", - "pg_tile_ambiguous_tip": "Syklit, joissa kaksi ohjelmaa ovat lähes tasoissa", - "pg_tile_detected": "Tunnistettu", - "pg_tile_detected_tip": "Syklit, joissa teho ylitti aloituskynnyksen riittävän pitkään rekisteröityäkseen käynnissä olevaksi", - "pg_tile_matched": "Täsmätty", - "pg_tile_matched_tip": "Syklit, jotka on liitetty oikein tunnettuun ohjelmaan", - "pg_time_left": "Aikaa jäljellä", "pg_weak_match": "Heikko osuma", "phase_name": "Vaiheen nimi", "name": "Nimi", "phase_timeline": "Vaiheiden aikajana", "power": "Teho", "profile": "Profiili", - "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ä. Valitse ohjelma ennen laitteen käynnistämistä, niin se otetaan käyttöön heti kun seuraava sykli alkaa.", + "program_tip": "Valitse, mihin ohjelmaan nykyinen sykli täsmäytetään. Automaattinen tunnistus valitsee parhaan vastaavuuden. Kiinnitä ohjelma, kun automaattinen tunnistus on väärässä; ennen käynnistystä valittu ohjelma koskee seuraavaa sykliä.", "progress": "Edistyminen", "raw_socket": "Raaka signaali", - "read_only": "Vain luku", "ref_cycle": "Viitesykli (valinnainen)", - "refresh_interval": "Päivitysväli (s)", "remaining": "Jäljellä", "reminder_every": "Muistuta joka (sykli)", "remove_label": "- Poista etiketti -", "rename_profile": "Nimeä profiili uudelleen", - "replay_duration": "Toisto", "resulting_profile": "Tuloksena oleva profiili", "save_mode": "Tallennustila", - "score": "Pisteet", - "score_breakdown": "Pisteiden erittely", "seconds_from_start": "Sekunteja alusta", "select_profile": "Valitse Profiili", "setting": "Asetus", - "settings_a": "Asetukset A (nykyiset)", - "settings_a_short": "A", "settings_advanced": "Edistynyt", - "settings_b": "Asetukset B (testi)", - "settings_b_short": "B", "settings_basic": "Perus", "settings_detail_level": "Asetusten yksityiskohtien taso", - "settings_overrides": "Asetusten ohitukset", "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", - "stage2": "Vaihe 2 – ydinsamankaltaisuus", - "stage3": "Vaihe 3 – DTW", - "stage4": "Vaihe 4 – vastaavuus", "status": "Tila", "tail_trim": "Leikkaus lopusta (s)", "tap_to_dismiss": "napauta kaaviota sulkeaksesi", @@ -579,7 +516,6 @@ "ungrouped": "Ryhmittelemätön", "unlabelled": "Merkitsemätön", "unlabelled_paren": "(merkitsemätön)", - "warp_path": "Vääntöpolku", "input": "Syöte:", "start": "Alku", "end": "Loppu", @@ -604,9 +540,8 @@ "n_shown": ", {n} näkyvissä", "cycles_title": "Syklit ({n})", "pg_detecting": "Havaitaan", - "pg_best_marker": "Paras: {v}", "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", + "pg_time_left_model_tip": "Arvioitu ohjelman vaiheiden perusteella, ei kiinteä lähtölaskenta", "phase": "Vaihe", "pg_mode_history": "Testaa historialla", "pg_ev_detected": "Havaittu", @@ -638,12 +573,6 @@ "pg_sweep_param": "Parametri", "pg_objective": "Tavoite", "pg_steps": "Askeleet", - "pg_stress_group": "Valmiustilan päätöstesti", - "pg_stress_idle_auto": "Auto", - "pg_stress_idle_w": "Valmiustila (W)", - "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_obj_match": "Osumatarkkuus", "pg_obj_endtiming": "Lopetusajan tarkkuus", "pg_obj_falseend": "Väärien lopetusten osuus", @@ -652,12 +581,8 @@ "pg_mode_optimize": "Optimoi", "pg_group_matching": "Ohjelman täsmäytys", "pg_zoom_hint": "kaksoisnapsauta nollataksesi zoomauksen", - "aria_playground_chart2": "Interaktiivinen jakson tehokaavio: vie osoitin päälle lukeaksesi ajan/tehon, vieritä zoomataksesi, vedä panoroidaksesi", "pg_alert_timeout_end": "Päättyi aikakatkaisuun, ei ennusteeseen", "pg_alert_indefinite": "Kävisi loputtomiin", - "pg_alert_stress_cap": "Turvaraja saavutettu", - "pg_alert_stress_ok": "Valmiustilan päätös: sykli pysäytetty", - "pg_alert_stress_warn": "Valmiustilan kulutus ylittää pysäytyskynnyksen", "task_pg_history": "Testaa historialla", "task_pg_sweep": "Optimoi", "task_reprocess": "Uudelleenkäsittely", @@ -667,16 +592,9 @@ "pg_recent_runs": "Viimeaikaiset ajot", "pg_load_run": "Lataa tämä ajo uudelleen", "pg_match_confidence": "Osuman varmuus", - "pg_envelope_fit": "Verhokäyrän sopivuus", - "pg_envelope_fit_tip": "Kuinka tarkasti sykli pysyy tämän profiilin tallennetun tehon verhokäyrän sisällä, eri näkökulma kuin osuman varmuus.", "n_imported_note": ", {n} tuotua", "enable_online": "Ota verkkotoiminnot käyttöön", - "store_brand": "Laitteen merkki", - "store_model": "Laitteen malli", "anonymous": "Anonyymi", - "rate_quality": "Arvioi laatu", - "rate_n": "{n} tähteä", - "approved_only": "Vain hyväksytyt", "adopt_settings": "Ota myös asetukset käyttöön", "flags": "Liput", "include_phase_map": "Sisällytä vaihdekartta", @@ -693,7 +611,6 @@ "cat_custom_phases": "Mukautetut vaiheet", "cat_profile_groups": "Profiiliryhmät", "cat_settings": "Havaitsemis- ja tunnistusasetukset", - "cat_matching_config": "Tunnistuksen hienosäätö", "cat_ml_models": "ML-mallit", "cat_feedback": "Palaute- ja tarkistusmerkinnät", "cat_suggestions": "Ehdotukset", @@ -715,7 +632,6 @@ "hist_csv_data": "CSV-data", "hist_from_recorder": "Tai lue se Home Assistantista", "hist_since": "Alkaen", - "days": "päivää", "hist_keep": "Säilytä tämä sykli", "hist_looks_complete": "kokonainen", "peak_power_short": "Huippu", @@ -735,7 +651,40 @@ "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ä" + "correct_total_cycles": "Korjaa kokonaismäärä", + "hist_reason_already_recorded": "on päällekkäin syklin kanssa, jonka WashData on jo tallentanut", + "share_consent": "Vahvistan, että nämä syklit suoritettiin normaalisti loppuun ilman keskeytystä", + "show_internal": "Näytä sisäiset asetukset", + "show_internal_tip": "Asetukset, jotka WashData säätää puolestasi (näytteenotto, tarkkailija, täsmäytystiheys, kynnykset). Näytetään, kun tällä laitteella on oma arvo, ristiriita tai ehdotus.", + "rate_n": "{n} tähteä", + "rate_quality": "Arvioi laatu", + "pg_autolabel": "Automaattimerkintä", + "pg_label_review": "Pyytää tarkistusta", + "pg_ev_match_reverted": "Osuma peruttu", + "pg_ev_verified_pause": "Vahvistettu tauko", + "n_samples": "{n} näytettä", + "n_samples_one": "{n} näyte", + "n_samples_other": "{n} näytettä", + "n_cycles": "{n} sykliä", + "n_cycles_one": "{n} sykli", + "n_cycles_other": "{n} sykliä", + "energy_per_cycle": "{v}/sykli", + "energy_total": "{v} yhteensä", + "automation_off": "(pois)", + "ref_cycle_none": "Ei mitään", + "share_name": "Näytä nimeni jakamassani sisällössä", + "cat_backfill_cycles": "Tuodusta tehohistoriasta löydetyt jaksot", + "pg_compare_profile": "Vertaa profiiliin", + "pg_obj_early": "Ennenaikaisten päättymisten osuus", + "pg_obj_endlag": "Päättymisviive (mediaani)", + "pg_obj_split": "Jakautumisten osuus", + "pg_sweep_cycles": "Viimeisimmät syklit", + "cycle_context": "Tallentimen historia syklin ympäriltä", + "cycle_context_off": "Pois", + "cycle_context_minutes": "{min} min", + "cycle_context_series": "Tallentimen historia", + "before_start": "Ennen alkua", + "after_end": "Lopun jälkeen" }, "log": { "all_levels": "Kaikki tasot", @@ -745,11 +694,9 @@ }, "ml": { "builtin_models": "● Sisäänrakennettujen mallien käyttö", - "fit_fair": "Tyydyttävä", "fit_good": "Hyvä", "fit_slight": "Lievä", "fit_strong": "Vahva", - "fit_weak": "Heikko", "fit_word": "sopivuus", "personalized": "● Mukautettu tälle koneelle", "better_than_baseline": "{pct}% parempi kuin perusarvio ({metric})", @@ -757,21 +704,29 @@ "trend_declining_tip": "Tämän mallin sopivuus on heikentynyt viimeisimpien tarkistusten aikana · useampien syklien tarkistaminen voi auttaa sitä oppimaan uudelleen.", "trend_steady_tip": "Tämän mallin sopivuus on pysynyt suunnilleen vakaana viimeisimpien tarkistusten aikana.", "cap_blurb": { - "end": "Sen tunnistaminen, milloin jakso on todella päättynyt", - "live_match": "Käynnissä olevan ohjelman tunnistaminen nopeammin", - "quality": "Väärin tunnistettujen tai vioittuneiden jaksojen havaitseminen", - "remaining_time": "Jäljellä olevan ajan ennustaminen", "total_energy": "Kokonaisenergian ja -kustannuksen ennustaminen" }, "cap_label": { - "end": "Jakson lopun tunnistus", - "live_match": "Ohjelman tunnistus", - "quality": "Jakson laatutarkistus", - "remaining_time": "Jäljellä olevan ajan arvio", "total_energy": "Energia-arvio" }, - "metric_auc": "AUC {auc} validointidatalla", - "metric_mae": "virhe {model} vs. perustaso {baseline}" + "metric_mae": "virhe {model} vs. perustaso {baseline}", + "fine_tuned_at": "hienosäädetty {when}", + "last_run_not_promoted": "Viimeisin tarkistus ({when}): {reason}", + "never": "ei koskaan", + "skip_reason": { + "fit_failed": "Koulutus epäonnistui: {error}", + "holdout_too_small": "Liian vähän syklejä uuden mallin reiluun testaamiseen: testaukseen voitiin varata {held_out}, tarvitaan {min}.", + "not_better_than_incumbent": "Uusi malli ei ollut parempi kuin käytössä oleva (virhe {model} vs. {incumbent}), joten nykyinen säilytettiin.", + "not_better_than_naive": "Uusi malli ei ollut selvästi parempi kuin yksinkertainen arvio (virhe {model} vs. {naive}).", + "unknown": "Mitään uutta ei opittu.", + "insufficient_rows": "Käyttökelpoisia syklejä ei ole vielä tarpeeksi: tähän mennessä {count} merkittyä, puhdasta sykliä.", + "insufficient_rows_one": "Käyttökelpoisia syklejä ei ole vielä tarpeeksi: tähän mennessä {count} merkitty, puhdas sykli.", + "insufficient_rows_other": "Käyttökelpoisia syklejä ei ole vielä tarpeeksi: tähän mennessä {count} merkittyä, puhdasta sykliä." + }, + "trained_unknown": "tuntemattomana ajankohtana", + "held_out_count": "mitattu {count} validointisyklillä", + "held_out_count_one": "mitattu {count} validointisyklillä", + "held_out_count_other": "mitattu {count} validointisyklillä" }, "modal": { "auto_label": "Automaattiset etikettisyklit", @@ -780,7 +735,6 @@ "cycle": "Kierrä", "edit_group": "Muokkaa profiiliryhmää", "edit_phase": "Muokkaa vaihetta", - "import_config": "Tuo kokoonpano", "label_cycle": "Merkitse sykli", "merge_cycles": "Yhdistä {n} sykliä", "new_group": "Uusi profiiliryhmä", @@ -795,6 +749,8 @@ "delete_automation_title": "Poista automaatio", "delete_automation_msg": "Poistetaanko automaatio \"{name}\" Home Assistantista? Tätä ei voi kumota.", "remove_legacy_title": "Poista vanhat toiminnot", + "reset_phase_msg": "Kumotaanko vaiheeseen \"{name}\" tekemäsi muokkaus ja palautetaan sisäänrakennettu vaihe? Ohjelmat säilyttävät alueensa.", + "reset_phase_title": "Palauta vaihe", "remove_legacy_msg": "Poistetaanko vanhat mukautetut toiminnot? Ne eivät enää laukea syklitapahtumista. Tätä ei voi kumota paneelista.", "discard_recording_title": "Hävitä nauhoitus", "discard_recording_msg": "Hävitetäänkö tallennettu nauhoitus? Tätä ei voi kumota.", @@ -815,32 +771,44 @@ "store_share_device": "Jaa tämä laite", "export_select": "Vienti - valitse tiedot", "import_wizard": "Tuonti - valitse tiedot", - "history_import": "Tuo tehohistoria" + "history_import": "Tuo tehohistoria", + "apply_suggestions_title": "Ota ehdotukset käyttöön", + "remove_task_title": "Poista tehtävä", + "remove_task_msg": "Poistetaanko muistutus \"{task}\"? Sen merkinnät huoltolokissa säilytetään.", + "apply_suggestions_msg": "Muutetaanko nämä asetukset ({n})? Integraatio ladataan uudelleen tallennuksen jälkeen.", + "discard_settings_title": "Hävitetäänkö tallentamattomat muutokset?", + "discard_settings_msg": "Tallentamattomat asetusmuutokset: {n}. Poistutaanko Asetuksista ja hävitetäänkö muutokset?", + "profile_title": "Profiili · {name}", + "undo_import_msg": "Tämän laitteen tiedot ja asetukset palautetaan viimeisintä tuontia ({when}) edeltävään tilaan. Sen jälkeen tallennetut syklit ja tehdyt muutokset menetetään.", + "undo_import_ok": "Kumoa tuonti", + "undo_import_title": "Kumoa viimeisin tuonti" }, "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.", + "pg_preset_delete_confirm": "Poistetaanko esiasetus \"{name}\"?", + "pg_preset_limit": "Esiasetusten enimmäismäärä täynnä ({n})", + "pg_preset_overwrite": "Korvataanko esiasetus \"{name}\"?", "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.", - "refresh_catalog_hint": "Yhteisön merkki- ja laiteluettelot on tallennettu välimuistiin, jotta jaettu kauppa pysyy päivittäisen kyselykuotansa rajoissa. Päivitä nähdäksesi muiden lisäämät tai hyväksymät merkinnät.", + "store_sibling_hint": "Eikö mallillesi ole jaettu mitään? Saman merkin läheinen malli on yleensä hyvä alku.", + "store_declare_appliance": "Kerro WashDatalle, mikä laite sinulla on, niin näet muiden sille jakamat kokoonpanot. Voit myös kirjoittaa merkin yllä olevaan kenttään ja katsella ympärillesi.", + "refresh_catalog_hint": "Yhteisön merkki- ja laiteluettelot ovat välimuistissa kaupan päivittäisen kiintiön säästämiseksi. Päivitä nähdäksesi muiden lisäämät tai hyväksymät merkinnät.", "head_trim_hint": "Kuinka monta sekuntia poistetaan alusta", "appliance_monitor": "Laitteen näyttö", "artifact_dip_detail": "Alitti tavallisen tehon alarajan noin {n} sekunnin ajan.", "artifact_footer": "Korostettu yllä olevassa kaaviossa. Nämä ovat ohimeneviä esineitä (esim. ovi avautui kesken syklin), eivät välttämättä ongelmia.", - "artifact_header": "{n} poikkeavaa havaittu tämän jakson aikana", - "artifact_pause_detail": "Teho laski lähes nollaan noin {n} sekunniksi ja jatkui sitten – todennäköisesti ovi avattiin syklin aikana tai sykli pysäytettiin.", + "artifact_header": "{n} poikkeamaa havaittu tämän syklin aikana", + "artifact_header_one": "{n} poikkeama havaittu tämän syklin aikana", + "artifact_header_other": "{n} poikkeamaa havaittu tämän syklin aikana", + "artifact_pause_detail": "Teho laski lähes nollaan noin {n} sekunniksi ja palasi sitten. Ovi avattiin todennäköisesti tai sykli keskeytettiin.", "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": "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.", + "cleanup_intro": "Kaikki merkityt syklit päällekkäin. Valitse poikkeamat ja poista niiden merkintä (ne jäävät historiaan, 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ä)", - "compare_profiles_tip": "Aseta muut profiilikuoret yllä olevaan kaavioon nähdäksesi, mikä sopii parhaiten tähän jaksoon.", + "compare_profiles_tip": "Lisää kaavioon muita profiileja nähdäksesi, mikä sopii tähän sykliin parhaiten.", "compare_selected_cycles": "Valitut jaksot (kiinteä) – näytä/piilota", - "consider_new_profile": "Harkitse uuden profiilin luomista.", - "coverage_gap": "viimeaikaisilla jaksoilla ei ole vastaavaa profiilia ({pct} % viimeisestä 30:stä).", - "coverage_gap_similar_cycles": "{count} samanlaista merkitsemätöntä sykliä löydetty – luo profiili, jotta voit aloittaa niiden täsmäämisen.", "cycles_deleted": "{count} sykli(ä) poistettu", "enough_data": "Tarpeeksi tietoja oppimiseen ({current}/{min} sykliä).", "export_description": "Valitse tarkalleen, mitkä profiilit, jaksot, asetukset ja muut viedään JSON-tiedostoon, tai analysoi tiedosto ja tuo vain haluamasi osat.", @@ -848,30 +816,26 @@ "feedback_prompt": "Vahvista, että se oli oikein, korjaa ohjelma tai jätä huomiotta.", "feedback_relabel_hint": "Tämän syklin uudelleenmerkintä ratkaisee sen myös.", "filter_by_profile": "Suodata profiilin mukaan…", - "group_modal_help": "Ryhmäohjelmat, joilla on sama muoto ja jotka eroavat lämpötilassa/linkouksessa (kesto voi vaihdella). Matching antaa ryhmän yhdeksi ehdokkaaksi ja valitsee sitten parhaiten sopivan jäsenen. Valitse vähintään 2; peittokuva osoittaa, kuinka samanlaisia ne ovat.", + "group_modal_help": "Ryhmittele ohjelmat, joilla on sama muoto mutta eri lämpötila tai linkous. Täsmäytys pisteyttää ryhmän yhtenä ja valitsee sitten parhaiten sopivan jäsenen. Valitse vähintään 2; peittokuva näyttää, kuinka samanlaisia ne ovat.", "group_not_cohesive": "Nämä profiilit eivät ole riittävän samankaltaisia ryhmittelemään niitä luotettavasti, joten vastaavuudessa käsitellään niitä yksitellen, kunnes poistat poikkeaman tai jaat ryhmän.", "group_preview_hint": "Valitse vähintään 2 jäsentä esikatsellaksesi ja vertaillaksesi tehokäyriä.", - "import_intro": "Lataa viety tiedosto tai liitä JSON-hyötykuorma alle.", - "legacy_actions_title": "{count} vanha muokattu toiminto on edelleen käynnissä", + "legacy_actions_title": "{count} vanhaa muokattua toimintoa on edelleen käynnissä", + "legacy_actions_title_one": "{count} vanha muokattu toiminto on edelleen käynnissä", + "legacy_actions_title_other": "{count} vanhaa muokattua toimintoa on edelleen käynnissä", "live_chart_loading": "Live-tehokaavio tulee näkyviin lukemien saapuessa.", "loading": "Ladataan…", "loading_curve": "Ladataan käyrää…", "loading_settings": "Ladataan asetuksia…", "log_buffer_hint": "Uusin ensin · puskuroi viimeiset 500 ha_washdata-tietuetta uudelleenkäynnistyksen jälkeen · Vedä alareunaa muuttaaksesi kokoa.", - "maintenance_advisory": "Keston/energian lisääntyminen voi viitata laitteen huoltotarpeeseen (esim. kalkinpoisto, suodattimen puhdistus).", "maintenance_due": "Huolto tarpeen: {items}", - "maintenance_intro": "Kirjaa tälle laitteelle tekemäsi huoltotoimet ja saat muistutuksen, kun kukin tehtävä on taas ajankohtainen.", + "maintenance_intro": "Kirjaa tämän laitteen huollot ja saat muistutuksen, kun kukin tehtävä on taas ajankohtainen.", "maintenance_load_error": "Huoltotietoja ei voitu ladata: {error}", "maintenance_requires_access": "Ylläpito ja vienti/tuonti vaativat täyden käyttöoikeuden.", - "manual_duration_hint": "Profiilin keskimääräinen/odotettu syklin pituus, jota käytetään jäljellä olevan ajan arvioihin. Muokkaa asettaaksesi sen; sen jättäminen ennalleen säilyttää nykyisen arvon.", - "matching_tuning_intro": "Oppiessaan WashData säätää myös, kuinka paljon ohjelman yhteensovittaminen painaa muotoa kestoon ja energiaan.", + "manual_duration_hint": "Profiilin odotettu syklin pituus, jota käytetään jäljellä olevan ajan arvioihin. Jätä ennalleen säilyttääksesi nykyisen arvon.", "merge_intro": "Valitut syklit yhdistetään yhdeksi (kronologinen järjestys; välit täytetty 0 W:lla). Valitse tuloksena oleva profiili.", "ml_intro": "WashData toimitetaan älykkäillä malleilla, jotka toimivat heti pakkauksesta.", "ml_learned_intro": "Mallit hienosäädetty tälle koneelle.", - "ml_loading": "ladataan ML…", "ml_settings_intro": "Kaksi erillistä kytkintä: toinen käyttää malleja syklin aikana, toinen antaa WashDatan hienosäätää ne koneellesi ajan myötä.", - "name_first_program": "Sinulla on tarpeeksi syklejä – nimeä ensimmäinen ohjelma aloittaaksesi täsmäyksen.", - "near_duplicate_cluster": "lähes kaksoiskappale profiiliklusteri havaittu. Ryhmittely mahdollistaa täsmäämisen luotettavasti valitsemaan samankaltaisten ohjelmien välillä (esim. sama ohjelma eri lämpötilassa/linkouksessa).", "no_cycles_match": "Yksikään jakso ei vastaa nykyistä suodatinta.", "no_cycles_profile": "Ei jaksoja tälle profiilille.", "no_cycles_selected": "Valitse ensin vähintään yksi sykli.", @@ -896,73 +860,52 @@ "no_suggestions": "Ei aktiivisia ehdotuksia.", "notify_services_hint": "Käytä {entity}-palvelun tunnuksia (pilkulla eroteltuna usealle). Mallimuuttujat: {vars}.", "old_actions_warning": "Määritetty vanhalla toimintoeditorilla (nyt poistettu). Ne käynnistyvät edelleen syklitapahtumissa, mutta niitä ei voi enää muokata täällä. Muunna ne normaaliksi automaatioksi tai poista ne.", - "onboarding_progress": "{n} / 3 sykliä havaittu", - "onboarding_watching": "Käytä laitettasi normaalisti – WashData tarkkailee. Kolmen syklin jälkeen ohjelmien täsmäys alkaa.", "pending_feedback": "Odottaa tunnistuspalautetta", "performance_trend": "Tehotrendi ({n} jaksoa)", - "pg_analysis_empty": "Lataa sykli nähdäksesi osuma-analyysin.", "pg_apply_confirm": "Otetaanko paras arvo käyttöön: {label} = {value}?", - "pg_canvas_empty": "Valitse sykli yltä ja paina sitten Lataa nähdäksesi sen tehokäyrän.", - "pg_chart_caption": "Tunnistus-/osumaprosentti {param}-arvojen mukaan", "pg_ctrl_intro": "Arvot pohjautuvat tämän laitteen aktiivisiin integraation asetuksiin. Muokkaukset jäävät testialueelle, kunnes tallennat ne asetuksiin.", "pg_live_unavailable": "Aktiiviset asetukset eivät ole käytettävissä, näytetään oletusarvot.", "pg_load_live_confirm": "Hylätäänkö testialueen muokkaukset ({n}) ja ladataanko integraation nykyiset asetukset uudelleen?", - "pg_match_rate": "{pct}% osumaprosentti {total} syklillä", "pg_matches_live": "Vastaa aktiivisia asetuksia", "pg_n_changed": "Poikkeaa aktiivisista asetuksista: {n}", "pg_sugg_none": "Kaikki ehdotukset vastaavat jo nykyisiä asetuksia", - "pg_preset_delete_confirm": "Poistetaanko esiasetus \"{name}\"?", - "pg_preset_limit": "Esiasetusten enimmäismäärä täynnä ({n})", - "pg_preset_overwrite": "Korvataanko esiasetus \"{name}\"?", "pg_publish_one_confirm": "Tallennetaanko {label} = {value} tämän laitteen asetuksiin?", "pg_restart_note": "Käynnistä Home Assistant uudelleen ottaaksesi simulointityökalut käyttöön.", - "pg_sim_progress": "{done} / {total} sykliä", - "pg_starting": "Aloitetaan…", - "pg_stress_synthetic": "valmiustilan simulaatio alkaa tästä", - "pg_sweep_intro": "Entä jos {param} olisi erilainen? Testaa {steps} arvoa viimeisten {cycles} syklin perusteella löytääksesi asetuksen, jolla useimmat syklit täsmätään oikein.", - "pg_sweep_step": "Vaihe {done} / {total}", - "pg_verdict_bad": "Vaatii huomiota: monet syklit jäävät tunnistamatta.", - "pg_verdict_good": "Hyvin viritetty: useimmat syklit tunnistetaan ja täsmätään oikein.", - "pg_verdict_ok": "Hyväksyttävä: osa sykleistä jäi huomaamatta. Kokeile laskea aloituskynnystä.", "phase_catalog_intro": "Jakson nimetyt osat (esipesu, lämmitys, linkous…). Määritä ne profiiliin sen ohjauspaneelista.", "phase_ranges_intro": "Vaihealueet (minuutit syklin alusta) peittyvät keskiarvokäyrällä. Muokkaa arvoja esikatsellaksesi livenä.", - "playground_intro": "Turvallinen testialue aiempien syklien toistamiseen eri asetuksilla, kahden kokoonpanon vertailuun rinnakkain ja sen tarkasteluun, miten sykli kohdistuu profiiliin. Mikään tässä ei muuta live-tietojasi ennen kuin otat sen nimenomaisesti käyttöön.", - "playground_needs_restart": "Käynnistä Home Assistant uudelleen ottaaksesi Testialueen työkalut käyttöön.", - "preferences_admin": "Asetukset, paneeli ja kulunvalvonta", - "preferences_adv": "Asetukset", + "playground_intro": "Kokeile asetuksia todellisilla syklitiedoillasi. Mikään tässä ei muuta käytössä olevia asetuksia, ennen kuin otat sen käyttöön.", "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ä), laske syklien kustannukset uudelleen tallennettujen energiahintojen mukaan ja laske syklien kunto uudelleen. Suorita tämä arviointierän jälkeen.", + "process_history_hint": "Suorita täsmäytys uudelleen kaikille tallennetuille sykleille, päivitä ehdotukset, kouluta ML-mallit uudelleen (jos käytössä), laske syklien kustannukset uudelleen tallennetuista hinnoista 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.", - "rbac_hint": "Kun se ei ole käytössä, jokaisella Home Assistant -käyttäjällä on täydet käyttöoikeudet (oletus). Järjestelmänvalvojilla on aina täydet käyttöoikeudet ja he voivat hallita kaikkia.", - "recent_logs": "Viimeisimmät ha_washdata-tietueet", + "rbac_hint": "Pois: jokaisella Home Assistant -käyttäjällä on täydet oikeudet (oletus). Järjestelmänvalvojilla on aina täydet oikeudet, ja he voivat hallita kaikkia.", "recording_in_progress": "Tallennus käynnissä", "reminders_intro": "Näytä paneelissa muistutus näin monta sykliä viimeisen huollon jälkeen. Poista muistutus käytöstä jättämällä tyhjäksi tai asettamalla 0.", "restart_gap_footer": "Korostettu kuvaajalla. Tehodata puuttuu näiltä väleiltä; tunnistus käytti vain todellisia lukemia.", - "restart_gap_header": "{n} HA-uudelleenkäynnistyskatkos tämän syklin aikana", + "restart_gap_header": "{n} HA-uudelleenkäynnistyskatkosta tämän syklin aikana", + "restart_gap_header_one": "{n} HA-uudelleenkäynnistyskatkos tämän syklin aikana", + "restart_gap_header_other": "{n} HA-uudelleenkäynnistyskatkosta tämän syklin aikana", "restart_gap_item": "{dur} katkos", - "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_confirm_help": "Tunnistettiinko tämä sykli oikein? Syklien vahvistaminen parantaa täsmäytystä koneellasi. Pelkkä Hyvä/Huono riittää.", "review_in_settings": "Tarkista asetuksista", "review_notes_placeholder": "Huomautuksia (valinnainen)", "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.)", - "review_tags_tip": "Valinnaiset liput, jotka kuvaavat, mikä meni pieleen tässä syklissä, joten koulutus ja siivous voivat selittää sen.", + "review_profile_tip": "Ohjelma, jolla tämä sykli on merkitty. Jos se on väärä, korjaa se tässä; merkinnät opettavat täsmäytystä.", + "review_quality_tip": "Kuinka puhdas tämä sykli on. Hyvä: tämän ohjelman malliesimerkki. Huono: tunnistettu, mutta häiriöinen tai epätavallinen. Käyttämätön: väärin tunnistettu (yhdistetty, katkaistu tai aiheeton). Vaikuttaa kuntopisteisiin ja siihen, millä sykleillä malli koulutetaan.", + "review_recorded_tip": "Merkitse tämä ohjelmansa käsin valituksi viitesykliksi, kuten käsin tallennettu sykli. Viitesyklit säilytetään aina, ne toimivat täsmäytysmallin pohjana, eikä siivous koskaan poista niitä.", + "review_tags_tip": "Valinnaiset liput siitä, mikä meni pieleen, jotta koulutus ja siivous voivat ottaa sen huomioon.", "review_to_cycles": "Avaa Cycles tarkistusjono", "samples_decimated": "Näytetään {shown}/{total} näytettä (harvennettu näyttöä varten; huiput säilytetty). Leveä väli tässä johtuu harvennuksesta, ei puuttuvasta datasta.", - "saving_triggers_reload": "Tallentaminen käynnistää integroinnin uudelleenlatauksen. HA-entiteetit voivat hetken näkyä poissa käytöstä.", + "saving_triggers_reload": "Tallentaminen lataa integraation uudelleen. Entiteetit voivat hetken näkyä tilassa Ei käytettävissä.", "search_placeholder": "Hakuasetukset…", "see_recorder": "Katso tallennin-widget alta", "setting_changed": "Muutettu {old} → {new} {date}", "settings_basic_note": "Näytetään olennaiset asetukset. Vaihda Edistynyt-tilaan nähdäksesi koko luettelon.", - "shape_drift_advisory": "⚠ Muoto ajautuu", - "shape_drift_detail": "Tämän profiilin tehomuoto on muuttunut ajan myötä – mahdollinen laitteen kuluminen tai huolto tarvitaan (esim. kalkinpoisto, suodattimen puhdistus).", "show_all_settings": "Näytä kaikki asetukset", - "showing_suggestions": "Näytetään asetus {count} ja ehdotuksia.", - "split_intro": "Napsauta kaaviota lisätäksesi tai poistaaksesi jakopisteen tai tunnistaa automaattisesti tyhjäkäynnin väliltä. Jokainen tuloksena oleva segmentti voi saada oman profiilinsa.", - "storage_diagnostics": "Varastointitilastot, ylläpito, vienti/tuonti", + "showing_suggestions": "Näytetään {count} asetusta, joilla on ehdotuksia.", + "showing_suggestions_one": "Näytetään {count} asetus, jolla on ehdotuksia.", + "showing_suggestions_other": "Näytetään {count} asetusta, joilla on ehdotuksia.", + "split_intro": "Lisää tai poista jakokohta napsauttamalla kaaviota, tai tunnista kohdat automaattisesti tyhjäkäyntivälien perusteella. Jokainen osa voi saada oman profiilinsa.", "sug_staged": "{key} = {val} asetettu. Tallenna ottaaksesi käyttöön.", "toast_auto_detect_enabled": "Automaattinen tunnistus käytössä", "toast_auto_label_complete": "Automaattinen merkintä valmis", @@ -979,7 +922,6 @@ "toast_no_automation": "Tähän ei voi luoda automaatiota", "toast_profile_name_required": "Profiilin nimi vaaditaan", "toast_program_set": "Ohjelma asetettu: {program}", - "toast_rebuild_failed": "Uudelleenrakennus epäonnistui: {error}", "toast_resume_failed": "Jatkaminen epäonnistui: {error}", "toast_revert_failed": "Palautus epäonnistui: {error}", "toast_reverted": "{key} palautettu", @@ -990,100 +932,87 @@ "trend_energy_up": "Energiatrendi nousussa ({pct} %/sykli) – viimeaikainen keskiarvo {avg}", "trim_destructive_confirm": "Tämä säilyttää vain {pct}% syklistä. Tätä ei voi kumota. Jatketaanko?", "trim_intro": "Vedä punaisia kahvoja tai syötä arvot. Kaikki ikkunan ulkopuolella oleva poistetaan.", - "tuning_suggestions_available": "{count} viritysehdotus saatavilla havaituista sykleistä. Ne näkyvät asianmukaisten kenttien vieressä.", + "tuning_suggestions_available": "{count} viritysehdotusta saatavilla havaituista sykleistä. Ne näkyvät asianmukaisten kenttien vieressä.", + "tuning_suggestions_available_one": "{count} viritysehdotus saatavilla havaituista sykleistä. Se näkyy asianmukaisen kentän vieressä.", + "tuning_suggestions_available_other": "{count} viritysehdotusta saatavilla havaituista sykleistä. Ne näkyvät asianmukaisten kenttien vieressä.", "unsure_detected_prefix": "WashData ei ole varma, onko se havaittu", - "updated": "Päivitetty", "warmup_badge": "Oppii vielä ({done}/{needed} sykliä)", - "warmup_detail": "Tämä profiili tarvitsee {needed} merkittyä sykliä ennen automaattisen täsmäämisen alkua. Jokainen vahvistettu sykli auttaa sitä oppimaan.", + "warmup_detail": "Täsmäytys toimii jo. WashData pyytää sinua vahvistamaan tämän profiilin ensimmäiset {needed} sykliä, ennen kuin se merkitsee syklejä itse.", "wipe_history_warning": "Poista pysyvästi kaikki syklit ja profiilit. Ei voi kumota.", "compare_overlay_tip": "Aseta opitut profiilien tehoalueet päällekkäin nähdäksesi, mitä ohjelmaa kukin sykli muistuttaa.", - "duration_trend_up_tip": "Kesto nousussa ({pct}%/sykli)", - "duration_trend_down_tip": "Kesto laskussa ({pct}%/sykli)", - "energy_trend_up_tip": "Energia nousussa ({pct}%/sykli)", - "energy_trend_down_tip": "Energia laskussa ({pct}%/sykli)", - "performance_trending": "Suorituskyvyn kehityssuunta", - "pg_select_cycle_hint": "Valitse sykli ja napsauta ↺ ladataksesi", - "pg_sweep_progress": "Askel {done} / {total}", - "pg_analysis_hint": "Valitse sykli ja napsauta ↺ ladataksesi analyysin.", "diagnostics_load_failed": "Diagnostiikkaa ei voitu ladata: {error}", - "manual_duration_ref_hint": "Käytetään vain, kun viitesykliä ei ole valittu. Viitesykli asettaa keston oman pituutensa mukaan.", + "manual_duration_ref_hint": "Käytetään vain ilman viitesykliä; viitesykli asettaa keston oman pituutensa mukaan.", + "duration_learned_hint": "Laskettu tämän ohjelman sykleistä (poikkeamat pois lukien) ja päivittyy jokaisen uuden syklin jälkeen. Muuttaaksesi sitä leikkaa tai poista poikkeavat syklit.", "toast_convert_rolled_back": "Muuntaminen epäonnistui ja peruttiin (automaatiota ei jäänyt): {error}", "toast_convert_orphan": "Automaatio luotiin, mutta vanhojen toimintojen poistaminen epäonnistui. Älä yritä uudelleen: poista vanhat toiminnot manuaalisesti, jotta vältät automaation kahdentumisen.", - "advisory_duration_trend_up": "'{name}'-jaksot kestävät vähitellen pidempään (noin +{pct}% per jakso). Jos laitteen toiminta on muuttunut, tallenna tai rakenna tämä profiili uudelleen.", - "advisory_energy_trend_up": "'{name}' kuluttaa vähitellen enemmän energiaa (noin +{pct}% per jakso) - laite kannattaa tarkistaa, jos tämä on odottamatonta.", - "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.", + "advisory_duration_trend_up": "'{name}'-syklit pitenevät jatkuvasti (noin +{pct}% per sykli). Jos laite on muuttunut, tallenna tai rakenna tämä profiili uudelleen.", + "advisory_energy_trend_up": "'{name}' kuluttaa joka syklillä enemmän energiaa (noin +{pct}% per sykli). Tarkista laite, jos tämä on odottamatonta.", + "advisory_poor_health": "'{name}' sopii huonosti: sen viimeaikaiset syklit vaihtelevat paljon tai täsmäävät heikosti. Käy sen syklit läpi tai tallenna se uudelleen, jotta täsmäytys ja arviot pysyvät tarkkoina.", + "advisory_shape_drift": "'{name}' on poikennut alkuperäisestä tehokäyrästään, ehkä kalkin tai kulumisen vuoksi. Harkitse sen tallentamista uudelleen tuoreilla sykleillä.", + "advisory_shape_drift_corr": "'{name}' on poikennut alkuperäisestä tehokäyrästään (korrelaatio {corr}), ehkä kalkin tai kulumisen vuoksi. Harkitse sen tallentamista uudelleen tuoreilla sykleillä.", + "advisory_unmatchable": "'{name}'-profiilia ei voi koskaan täsmäyttää: sen taustalla ei ole yhtään sykliä, jossa on tehotietoja. Korjaa merkitsemällä tai tallentamalla tämän ohjelman sykli.", + "advisory_unmatchable_no_duration": "'{name}'-profiilia ei voi koskaan täsmäyttää: sen syklillä ei ole käyttökelpoista kestoa. Tallenna ohjelma uudelleen tai aseta sen odotettu kesto.", "pg_no_alerts": "Tässä ajossa ei havaittu ongelmia.", "pg_override_active": "Käytetään muokattuja asetuksiasi vs. nykyiset", "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.", "pg_sweep_intro2": "Etsi asetus, joka parhaiten täyttää tavoitteen viimeisimmissä sykleissäsi. Mikään ei muutu ennen kuin otat sen käyttöön.", - "pg_running_history": "Toistetaan jaksojasi…", - "pg_running_sweep": "Testataan arvoja kaikissa jaksoissasi…", "pg_row_load_hint": "Lataa tämä jakso yllä olevaan kaavioon", - "pg_history_intro2": "Toista viimeisimmät jaksosi todellisen tunnistimen ja täsmäyttimen läpi yllä olevilla asetuksilla. Napsauta mitä tahansa riviä ladataksesi kyseisen jakson kaavioon; muokkaa asetusta nähdäksesi ennen/jälkeen-vertailun.", + "pg_history_intro2": "Toista viimeisimmät syklit todellisen tunnistimen ja täsmäyttimen läpi yllä olevilla asetuksilla. Napsauta riviä ladataksesi sen kaavioon; muuta asetusta vertaillaksesi ennen ja jälkeen.", "pg_simulating": "Simuloidaan jaksoa…", - "pg_canvas_empty2": "Valitse jakso yltä ja paina Suorita simuloidaksesi sen. Vie sitten osoitin päälle lukeaksesi arvoja, vieritä zoomataksesi ja vedä panoroidaksesi.", "pg_apply_settings_confirm": "Tallennetaanko nämä {n} asetusta tähän laitteeseen? {list}", - "pg_stress_terminated_detail": "Asettui noin {idle} W:n valmiustilaan -- sykli päättyi {h} h {m} min myöhemmin syystä {reason}.", - "pg_stress_above_threshold_detail": "Valmiustilan kulutus noin {idle} W on pysäytyskynnyksen ({stop} W) tasolla tai sen yläpuolella -- sykliä ei koskaan rekisteröity hiljaiseksi. Korjaa nostamalla arvoa stop_threshold_w.", - "pg_stress_hit_cap_detail": "Sykli kävi {h} h {m} min pysähtymättä -- turvaraja pakotti sen pysähtymään. Valmiustilan kulutus {idle} W vs. pysäytyskynnys {stop} W.", "no_logs_match": "Mikään lokitietue ei vastaa näitä suodattimia.", "pg_analysis_hint2": "Valitse sykli ja paina Aja-painiketta ladataksesi osuma-analyysin.", - "pg_analysis_empty2": "Paina Aja-painiketta nähdäksesi osuma-analyysin.", - "imported_readonly": "Tuotu yhteisökaupasta. Näytetään viitteeksi ja täsmäytystä varten. Sitä ei lasketa tilastoihisi eikä sitä voi muokata.", + "imported_readonly": "Tuotu yhteisökaupasta. Käytetään viitteenä ja täsmäytyksessä, ei lasketa tilastoihisi, eikä sitä voi muokata.", "merge_no_imports": "Tuotuja tallennuksia ei voi yhdistää oikeaan sykliin. Poista niiden valinta yhdistääksesi.", - "store_enable_hint": "Ota verkkotoiminnot käyttöön asetuksista selataksesi ja tuodaksesi yhteisön viitesyklejä.", - "advisory_duration_outlier": "{n} jaksoa on merkitty nimellä '{name}', mutta niiden kesto on liian kaukana sen tavallisesta pituudesta, jotta ne voisivat koskaan täsmätä siihen (pahin: {ratio}x). Ne on todennäköisesti merkitty väärin, tai yksi tallenne sisältää kaksi ajoa. Merkitse ne uudelleen tai jaa ne, jotta ne eivät vääristä tämän ohjelman aika-arviota.", + "store_enable_hint": "Ota verkkotoiminnot käyttöön kohdassa Mieltymykset > Verkko ja yhteisö selataksesi ja tuodaksesi yhteisön viitesyklejä.", + "advisory_duration_outlier": "Merkinnällä '{name}' on syklejä ({n}), jotka poikkeavat sen tavallisesta pituudesta liikaa täsmätäkseen koskaan (pahin: {ratio}x). Ne on todennäköisesti merkitty väärin tai sisältävät kaksi ajoa. Merkitse ne uudelleen tai jaa ne, jotta ne eivät vääristä aika-arviota.", "standby_above_stop_title": "Laite otti yhä {idle} W jakson päättyessä", "standby_above_stop_sub": "{n} viimeisestä {total} jaksosta. Sammutusviive alkaa vasta, kun teho on Pysäytyskynnyksen ({stop} W) alapuolella, joten jaksot päättyvät myöhässä tai ne on pysäytettävä käsin.", - "online_intro": "Jaa ja lataa viitesyklejä muiden WashData-käyttäjien kanssa. Kaikki verkkotoiminnot ovat vapaaehtoisia ja oletuksena pois päältä.", "store_share_intro": "Lataa tämä viitesykli, jotta muut samaa laitetta käyttävät voivat hyödyntää sitä. Se tarkistetaan ennen julkaisua.", - "online_intro_global": "Selaa ja jaa viitetallennuksia muiden WashData-käyttäjien kanssa ja vahvista laitemerkintöjä. Yksi yhteys koskee koko WashData-integraatiotasi; laitteen merkki ja malli asetetaan laitekohtaisesti Perusmääritykset-osiossa. Kaikki verkkotoiminnot ovat vapaaehtoisia ja oletuksena pois päältä.", + "online_intro_global": "Selaa ja jaa viitetallennuksia muiden WashData-käyttäjien kanssa. Yksi yhteys kattaa kaikki WashData-laitteesi; merkki ja malli asetetaan laitekohtaisesti Perusasetukset-osiossa. Verkkotoiminnot ovat pois päältä, kunnes otat ne käyttöön.", "online_unavailable": "Verkkotoiminnot eivät ole käytettävissä tällä palvelimella.", "store_picker_offline": "Ota verkkotoiminnot käyttöön asetusten rattaasta valitaksesi yhteisön luettelosta.", "pick_brand_first": "Valitse ensin laitteen merkki.", - "model_not_found": "Ei vielä luettelossa.", - "connect_to_confirm": "Yhdistä asetusten rattaasta vahvistaaksesi tai arvioidaksesi.", + "custom_task_limit": "Olet saavuttanut {max} tehtävän rajan.", + "custom_tasks_intro": "Lisää mitä tahansa muuta, mistä haluat muistutuksen. Tehtävä on ajankohtainen, kun jompikumpi väli täyttyy; jätä toinen tyhjäksi tai 0:ksi, jos haluat käyttää vain toista.", + "maintenance_due_sensor_hint": "Binäärianturi \"Huolto tarpeen\" on päällä, kun jokin tehtävä on ajankohtainen. Voit käyttää sitä omissa ilmoitusautomaatioissasi.", + "maintenance_supporter_intro": "Maintenance Supporter on erillinen integraatio, joka tekee paljon enemmän: mikä tahansa laite tai tehtävä, aikataulut ajan, käytön tai anturin perusteella, ilmoitukset ja historia. Se voi käyttää WashDatan syklilaskuria.", "adopt_settings_hint": "Korvaa tämän laitteen tunnistus- ja täsmäytyskynnykset jaetuilla. Ilmoituksiasi, entiteettejäsi tai energiahintaasi ei koskaan muuteta.", - "include_settings_hint": "Jaa tämän laitteen tunnistus- ja täsmäytyskynnykset (ei ilmoituksiasi, entiteettejäsi tai energiahintaasi). Käyttöönottajat päättävät, otetaanko ne käyttöön.", + "include_settings_hint": "Jaa tämän laitteen tunnistus- ja täsmäytyskynnykset (ei ilmoituksia, entiteettejä tai energiahintaa). Muut päättävät, ottavatko ne käyttöön.", "onboard_download": "Uusi laite? Ota käyttöön valmis asetus (ohjelmat, viitesyklit ja vaiheet) toiselta WashData-käyttäjältä, jolla on sama laite.", - "share_consent": "Vahvistan, että nämä syklit suoritettiin normaalisti loppuun ilman keskeytystä", "share_device_none": "Ei vielä jaettavia syklejä. Merkitse tallennettu tai käsin valittu sykli viitesykliksi (⭐) ensin Syklit-välilehdessä.", "share_guideline_naming": "Nimeä kukin profiili täsmälleen kuten laitteen säätimessä tai näytössä näkyy (esim. 'Cotton 40', 'Eco 60').", - "share_guideline_quality": "Jaa vain syklit, jotka suorittivat normaalisti -- ei keskeytyksiä kesken syklin, oven avaustapahtumia tai tehohäiriöitä.", + "share_guideline_quality": "Jaa vain normaalisti päättyneitä syklejä: ei keskeytyksiä, oven avaamisia tai tehohäiriöitä.", "share_guideline_review": "Latauksesi alkaa odottavana ja tulee julkiseksi, kun riittävästi yhteisön jäseniä vahvistaa sen.", "share_guidelines_title": "Ennen jakamista", - "store_download_device_intro": "Ota käyttöön jokainen jaettu ohjelma ja sen viitesyklit laitteellesi. Omat tallennetut syklisi ja tilastosi eivät vaikutu.", + "store_download_device_intro": "Lisää jokainen jaettu ohjelma ja sen viitesyklit laitteellesi. Omat syklisi ja tilastosi eivät muutu.", "store_share_device_intro": "Lataa {brand} {model} valitsemiesi viitesyklien kanssa. Muut saman laitteen omistajat voivat ottaa ohjelmiasi käyttöön. Merkinnät tarkistetaan ennen julkaisua.", "share_profile_no_cycles": "Ei viitesyklejä -- merkitse sykli ⭐:ksi Syklit-välilehdessä sisällyttääksesi tämän profiilin", - "advisory_phase_inconsistent": "'{name}' vaikuttaa sekoittavan eri ohjelmia tai lämpötiloja - sen syklit lämmittävät hyvin eripituisia aikoja. Sen jakaminen erillisiin profiileihin (esim. lämpötilan mukaan) parantaa täsmäytystä ja aika-arvioita.", - "advisory_phase_inconsistent_title": "⚠ Mahdollisesti sekoittuneet ohjelmat", - "export_select_intro": "Rastita tarkalleen, mitä sisällytetään. Kun valitset profiilit ilman niiden jaksoja, viedään silti tunnistettava ohjelma (sen opittu muoto kulkee mukana).", - "import_analyze_hint": "Lataa viety tiedosto (tai liitä sen JSON). WashData analysoi sen ja näyttää tarkalleen, mitä voidaan tuoda, ennen kuin mitään muuttuu.", + "export_select_intro": "Valitse, mitä sisällytetään. Profiilit ilman syklejään viedään silti täsmäytettävinä ohjelmina.", + "import_analyze_hint": "Lataa viety tiedosto (tai liitä sen JSON). WashData näyttää, mitä voidaan tuoda, ennen kuin mitään muuttuu.", "analyzing": "Analysoidaan…", - "device_type_mismatch_warn": "Tämä vienti on eri laitetyypistä ({src} vs {local}). Ohjelmat ja jaksot voidaan silti tuoda viitedataksi, mutta laitekohtaiset asetukset ja todellisen historian tuonti on poistettu käytöstä.", + "device_type_mismatch_warn": "Tämä vienti on eri laitetyypistä ({src} vs {local}). Ohjelmat ja syklit voi silti tuoda viitedataksi, mutta ei laiteasetuksia eikä todellista historiaa.", "merge_hint": "Tuodut kohteet lisätään; mitään paikallista ei menetetä. Nimien törmäykset ratkaistaan alla.", "replace_warn": "Jokainen rastitettu luokka tyhjennetään ja korvataan tiedostosta. Rastittamattomia luokkia ei kosketa.", "dest_reference_hint": "Tuodut jaksot vain parantavat ohjelmien tunnistusta eivätkä koskaan vaikuta käyttö- tai energiatilastoihin.", - "dest_real_history_hint": "Tuodut jaksot lasketaan tämän laitteen omaksi historiaksi ja syöttävät energia- ja käyttötilastoja. Käytä tätä, kun siirrät yhden laitteen uuteen asennukseen.", + "dest_real_history_hint": "Tuodut syklit lasketaan tämän laitteen omaksi historiaksi ja tilastoiksi. Käytä tätä, kun siirrät laitteen uuteen asennukseen.", "sug_muted": "Tätä asetusta ei enää ehdoteta", "sug_mute_failed": "Ehdotuksen vaimennus epäonnistui", "sug_unmuted_all": "Vaiennetut ehdotukset palautettu", - "n_suggestions_muted": "{count} vaimennettu; automaattinen virittäjä ei ehdota näitä.", + "n_suggestions_muted": "{count} ehdotusta vaimennettu; automaattinen virittäjä ei ehdota näitä.", + "n_suggestions_muted_one": "{count} ehdotus vaimennettu; automaattinen virittäjä ei ehdota sitä.", + "n_suggestions_muted_other": "{count} ehdotusta vaimennettu; automaattinen virittäjä ei ehdota näitä.", "font_size_hint": "Suurenna tai pienennä kaikkea tässä paneelissa. Koskee tiliäsi tällä laitteella.", - "import_history_description": "Oliko sinulla älypistoke jo ennen WashDataa? Lataa sen tehoanturin historiavienti tai lue historia suoraan Home Assistantista, niin tavallinen tunnistus ajetaan sen yli ja menneet syklit ilmestyvät Syklit-luetteloon valmiina nimettäviksi.", - "hist_input_hint": "Lataa Historia-paneelista ladattu CSV (entity, state, last changed) tai anna WashDatan lukea anturin historia suoraan. Tunnistus ajetaan sen yli täsmälleen kuten reaaliajassa, ja sinä valitset, mitkä löytyneistä sykleistä säilytetään.", - "hist_recorder_hint": "Lukee valitsemastasi päivästä nykyhetkeen. Home Assistant säilyttää tarkan historian oletuksena 10 päivää ja sen jälkeen vain tuntikeskiarvot, jotka ovat liian karkeita syklien tunnistamiseen - valitse kauempana menneisyydessä oleva päivä vain, jos recorder on asetettu säilyttämään enemmän.", - "hist_scanning": "Historiaasi ajetaan uudelleen tunnistimen läpi. Tämä tapahtuu taustalla - voit sulkea tämän ikkunan ja palata siihen myöhemmin.", + "import_history_description": "Oliko sinulla älypistoke jo ennen WashDataa? Lataa sen tehohistoria tai lue se Home Assistantista. Tunnistus ajetaan sen yli, ja menneet syklit ilmestyvät Syklit-luetteloon valmiina nimettäviksi.", + "hist_input_hint": "Lataa CSV Historia-paneelista (entity, state, last changed) tai anna WashDatan lukea anturin historia suoraan. Tunnistus ajetaan sen yli, ja sinä valitset, mitkä syklit säilytetään.", + "hist_recorder_hint": "Lukee valitsemastasi päivästä nykyhetkeen. Home Assistant säilyttää tarkan historian oletuksena 10 päivää; vanhemmat tuntikeskiarvot ovat liian karkeita syklien tunnistamiseen.", + "hist_scanning": "Historiaasi ajetaan tunnistimen läpi taustalla. Voit sulkea tämän ikkunan ja palata myöhemmin.", "hist_imported_count": "{n} sykliä tuotu.", "hist_duplicates": "{n} oli tuotu jo aiemmin ja ohitettiin.", "hist_capped": "Laitekohtainen tuotujen syklien raja täyttyi; loppuja ei tallennettu.", - "hist_next_step": "Ne ovat Syklit-luettelossasi merkittynä tuoduksi historiaksi. Avaa sykli ja nimeä sen ohjelma Merkitse-toiminnolla.", + "hist_next_step": "Ne ovat Syklit-luettelossa tuoduksi historiaksi merkittyinä. Avaa sykli ja nimeä sen ohjelma Merkitse-toiminnolla.", "hist_rows_read": "{n} lukemaa luettu", "hist_entity_substituted": "luettiin {used} (tälle laitteelle on määritetty {wanted})", "hist_breaks": "{n} katkoa, joissa anturi ei ollut saatavilla", @@ -1095,57 +1024,92 @@ "hist_scan_capped": "Vain ensimmäiset ehdokkaat näytetään ({n} löytyi).", "hist_recorder_empty": "Home Assistantilla ei ole tarkkaa historiaa tälle anturille kyseiseltä ajanjaksolta.", "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_scan_expired": "Tämä 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.", - "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.", + "imported_history_readonly": "Löydetty tuodusta tehohistoriasta. Auttaa täsmäytyksessä, mutta ei näy tilastoissasi, eikä sitä voi rajata tai jakaa. Nimeä ohjelma Merkitse-toiminnolla.", + "odometer_intro": "Tämä kokonaismäärä vain kasvaa. Syklin poistaminen ei pienennä sitä, joten huoltomuistutukset pysyvät oikeina.", + "odometer_correct_hint": "Aseta tämä, jos WashData laski ajon, jota ei tapahtunut, tai laitteella ajettiin syklejä ennen WashDatan asennusta.", "no_reminders_set": "Huoltomuistutuksia ei ole vielä asetettu.", "toast_program_armed": "Ohjelma asetettu seuraavaa sykliä varten: {program}", "toast_range_required": "Aseta kelvollinen Mistä/Mihin-alue.", - "no_profiles_option": "(ei vielä profiileja, lisää uusi)" + "no_profiles_option": "(ei vielä profiileja, lisää uusi)", + "compare_cycles_title": "Vertaa {count} sykliä", + "edit_builtin_phase": "Tämä on sisäänrakennettu vaihe. Tallentaminen luo mukautetun kopion; poista kopio palauttaaksesi alkuperäisen.", + "fetch_error": "Tietojen lataaminen epäonnistui.", + "connection_lost": "Yhteys katkesi - näytetään tilanne klo {time}.", + "pg_notify_partial": "Merkit näyttävät vain nämä ilmoitukset: aloitus, melkein valmis, valmistuminen, välitavoite ja hiljaiset tunnit. Reaaliaikaisia päivityksiä, purkamismuistutuksia ja ylitysvaroituksia ei simuloida.", + "connect_to_confirm": "Yhdistä asetusten rattaasta vahvistaaksesi tai arvioidaksesi.", + "coverage_gap_cluster": "{count} viimeaikaista merkitsemätöntä, noin {min} min pituista sykliä näyttää yhdeltä ohjelmalta, jota et ole vielä luonut.", + "pg_label_yes": "Merkittäisiin jakson lopussa nimellä \"{name}\"", + "pg_label_no": "Pyytäisi tarkistusta: {reason}", + "live_match_debug_tip": "Luottamus: kuinka hyvin nykyinen tehokäyrä vastaa parasta ehdokasprofiilia (0-100 %). Epäselvä: kahden parhaan ehdokkaan pisteet ovat 5 %:n sisällä toisistaan - merkintä on epävarma, kunnes sykli päättyy.", + "automation_alias_started": "{device}: sykli alkoi", + "automation_alias_finished": "{device}: sykli päättyi", + "automation_desc_started": "Suoritetaan, kun laitteen {device} sykli alkaa. Lisää omat toimintosi (ilmoitus, valot, ...).", + "automation_desc_finished": "Suoritetaan, kun laitteen {device} sykli päättyy. Lisää omat toimintosi (ilmoitus, valot, ...).", + "feedback_detected": "WashData tunnisti: {name}", + "import_undo_hint": "Viimeisintä tuontia ({when}) edeltävä palautuspiste säilytetään: {profiles} profiilia ja {cycles} jaksoa.", + "replace_undo_hint": "Ensin tallennetaan palautuspiste, joten voit kumota tämän tuonnin kohdassa Vienti / tuonti.", + "pg_in_sample_note": "Nämä syklit auttoivat rakentamaan profiilit, joihin niitä täsmäytetään, joten tulokset ovat optimistisia: uusilla sykleillä oikeita täsmäytyksiä on hieman vähemmän.", + "hist_tail_trimmed": "Syklin päättymisen jälkeistä odotusta leikattiin pois {min} min, kuten aktiivinen integraatio sen leikkaa", + "hist_truncated": "pysähtyi {n} lukeman rajaan, joten loppua historiasta ei käyty läpi", + "hist_warn_kw": "Suurin lukema on vain {w}. Anturi näyttää ilmoittavan kilowatteja; WashData tarvitsee watteja, joten syklejä ei voi tunnistaa.", + "pg_compare_profile_tip": "Valitsee vain profiilin, joka piirretään kaavion päälle. Simulaatio täsmäyttää syklin aina itse, kuten aktiivinen integraatio.", + "pg_outcome_stale": "Asetukset ovat muuttuneet tämän simulaation ajon jälkeen. Se näyttää yhä vanhan tuloksen.", + "pg_sweep_guarded": "Päättää useampia syklejä liian aikaisin, jakaa useampia tai ohittaa syklejä, jotka nykyinen asetus tunnistaa, joten sitä ei koskaan suositella.", + "pg_sweep_keep_current": "Säilytä nykyinen arvo ({value}, {metric}): mikään arvo ei ollut parempi vähintään yhden syklin verran.", + "pg_task_busy": "Tälle laitteelle on jo käynnissä Testaa historialla -ajo tai Optimoi-ajo. Odota sen valmistumista tai peruuta se.", + "hist_warn_naive_time": "{n} aikaleimalta puuttuu aikavyöhyke, ja ne luettiin UTC-aikana. Jos tiedosto käyttää paikallista aikaa, syklit siirtyvät UTC-erosi verran, ja kesäajan vaihdoksen lähellä olevia lukemia voi pudota pois.", + "hist_warn_naive_time_one": "{n} aikaleimalta puuttuu aikavyöhyke, ja se luettiin UTC-aikana. Jos tiedosto käyttää paikallista aikaa, syklit siirtyvät UTC-erosi verran, ja kesäajan vaihdoksen lähellä olevia lukemia voi pudota pois.", + "hist_warn_naive_time_other": "{n} aikaleimalta puuttuu aikavyöhyke, ja ne luettiin UTC-aikana. Jos tiedosto käyttää paikallista aikaa, syklit siirtyvät UTC-erosi verran, ja kesäajan vaihdoksen lähellä olevia lukemia voi pudota pois.", + "pg_sweep_n_cycles": "{count} syklin perusteella", + "pg_sweep_n_cycles_one": "{count} syklin perusteella", + "pg_sweep_n_cycles_other": "{count} syklin perusteella", + "cycle_context_loading": "Ladataan tallentimen historiaa…", + "cycle_context_note": "Harmaa: tehoanturin tallentimen historia tämän syklin ympäriltä. Se ei ole osa sykliä eikä vaikuta kestoon, energiaan tai täsmäytykseen.", + "cycle_context_none": "Tallentimessa ei ole lukemia tämän syklin ympäriltä (Home Assistant säilyttää oletuksena 10 päivän historian).", + "cycle_context_tip": "Vain näyttöä varten: näyttää tehoanturin tallentimen historian tämän syklin ympäriltä, jos se on vielä tallessa. Jos haluat liittää keskeytyneet käynnistysyritykset tuleviin sykleihin, käytä asetusta Käyrän esijakso (Asetukset > Tunnistus > Syklin alku). Se muuttaa tallennettuja kestoja.", + "invalid_threshold_pair": "Pysäytyskynnyksen ({stop} W) on oltava pienempi kuin Käynnistyskynnys ({start} W). Mitään ei tallennettu; muuta molempia yhdessä." }, "pg_desc": { "dishwasher_end_spike_quiet_release": "Astianpesukone: hiljaiset sekunnit odotetun keston jälkeen ennen kuin syklin lopun vedenpoiston odotus vapautetaan", - "smart_termination_duration_ratio": "Osuus täsmätyn ohjelman odotetusta kestosta, joka syklin on saavutettava ennen kuin älykäs lopetus voi päättää sen ennenaikaisesti; laske sitä kuormasta tai lämpötilasta riippuvilla koneilla", + "smart_termination_duration_ratio": "Osuus ohjelman keskimääräisestä kestosta, joka syklin on saavutettava ennen kuin älykäs lopetus voi päättää sen aikaisin; laske sitä, jos ajoajat vaihtelevat", "anti_wrinkle_enabled": "Sulauta päävaiheen jälkeiset rumpupulssit sen sijaan, että lukisit ne uusina sykleinä", - "anti_wrinkle_exit_power": "Tehon on laskettava tämän alle pulssien välillä, jotta ryppyjenesto pysyy aktiivisena", + "anti_wrinkle_exit_power": "Hiljainen taso rumpupulssien välillä; vaikuttaa vain Pysäytyskynnyksen yläpuolella", "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", + "anti_crease_finalize_ratio": "Osuus ohjelman keskimääräisestä kestosta, joka syklin on saavutettava ennen kuin ryppyjeneston loppuhäntä voi päättää sen; laske sitä kuivausrummuissa, pidä pesukoneet oletusarvossa", "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", "min_off_gap": "Kahden syklin erottamiseen vaadittava tauko", "off_delay": "Sekunteja matalaa tehoa ennen syklin päättymistä", "start_duration_threshold": "Sekunteja kynnyksen yläpuolella käynnistyksen vahvistamiseksi", "start_threshold_w": "Vähimmäiswatit, jotta lasketaan käynnistyneeksi", "stop_threshold_w": "Tämän alapuolella laite lasketaan pois päältä olevaksi", - "dtw_bandwidth": "Vaihe 3: Sakoe-Chiba-ajankäyristyskaista (0 = DTW pois; oletus 0,2 = 20% syklinpituudesta)", - "corr_weight": "Vaihe 2: tasapaino käyrän muodon (korrelaatio) ja tehotason (MAE) välillä; oletus 0,45", - "duration_weight": "Vaihe 4: kuinka vahvasti keston yhdenmukaisuus vaikuttaa lopullisiin pisteisiin (oletus 0,22)", - "energy_weight": "Vaihe 4: kuinka vahvasti energian yhdenmukaisuus vaikuttaa lopullisiin pisteisiin (oletus 0,22)", - "profile_match_min_duration_ratio": "Vaihe 1: lyhin sykli profiilinkeston murto-osana; oletus 0,1 (10%)", - "profile_match_max_duration_ratio": "Vaihe 1: pisin sykli profiilinkeston murto-osana; oletus 1,8 (180%)", - "keep_min_score": "Vaihe 2: alin pisteet pysyäkseen mukana; oletus 0,1 sallii heikot ehdokkaat, myöhemmät vaiheet valitsevat parhaan", - "dtw_blend": "Vaihe 3: 0 = vain ydinpisteet, 1 = vain DTW-pisteet, 0,5 = tasainen sekoitus (oletus)", - "dtw_ensemble_w": "Vaihe 3 (ensemble-tila): skaloidun L1 paino vs. derivatiivinen DTW; oletus 0,7 suosii tasonhavainnoivaa", - "dtw_ddtw_scale": "Vaihe 3: DDTW:n puolikyllästymisetäisyys; pienempi = herkempi muodon eroille (oletus 30)", - "dtw_refine_top_n": "Vaihe 3: ehdokkaat joille DTW laskee pisteet uudelleen; nosta 7-9:ään jos oikea profiili sijoittuu 4.-5. (oletus 5)", - "duration_scale": "Vaihe 4: log-suhde jossa keston yhdenmukaisuus puolittuu; pienempi = tiukempi rangaistus (oletus 0,175)", - "energy_scale": "Vaihe 4: log-suhde jossa energian yhdenmukaisuus puolittuu; pienempi = tiukempi rangaistus (oletus 0,25)" + "profile_match_min_duration_ratio": "Lyhin sykli profiilin keston osuutena; oletus 0,1 (10 %)", + "profile_match_max_duration_ratio": "Pisin sykli profiilin keston osuutena; oletus 1,8 (180 %)" }, "phase_desc": { "anti_crease": "Satunnaisia lyhyitä rumpujaksia valmistumisen jälkeen vaatteiden rypistyistä ehkäisemiseksi.", "anti_wrinkle": "Jaksoittainen rummun pyörittäminen syklin jälkeen vaatteiden rypistyistä ehkäisemiseksi.", + "baking": "Lämmitysvastus toimii korkeassa lämpötilassa, jotta kuori ja sisus muodostuvat.", + "cooking": "Pääkypsennysvaihe, lämmitin ja puhallin käynnissä.", "cool_down": "Pyöriminen ilman lämpöä syklin lopussa.", + "cool_down_af": "Jäähdytys pelkällä puhaltimella lämmityksen jälkeen.", "drain_and_switch": "Siirtymäaika pesusta kuivaustilaan.", "dry": "Kuivausvaihe lämmittimellä ja/tai jälkilämmöllä.", "drying": "Lämmitetty pääjakso.", "heat_up": "Lämmittimen ensimmäinen lämmitys ennen täyden kuivumisen alkamista.", + "keep_warm": "Pito matalalla lämmöllä, jotta ruoka pysyy lämpimänä.", + "keep_warm_bm": "Pito matalalla lämmöllä, jotta leipä pysyy paistamisen jälkeen lämpimänä.", + "kneading": "Moottori sekoittaa ja vaivaa taikinaa. Suuri tehonkulutus.", + "pause_af": "Lyhyt tauko ravistelua tai tarkistusta varten.", + "pre_heat": "Kammion alkulämmitys ennen varsinaista kypsennystä.", "pre_rinse": "Ensimmäinen suihkutus ennen pesuainepesua.", + "proving": "Kohotus matalalla lämmöllä: hiiva käy ja taikina kohoaa.", + "resting": "Lyhyt pienitehoinen tauko vaivausten välillä, jotta gluteeni rentoutuu.", "pre_wash": "Ensimmäinen liotus tai esikäsittely ennen pääpesua.", "rinse": "Puhdasvesihuuhteluvaihe. Tämä vaihe voi toistua useita kertoja.", "sanitize": "Korkean lämpötilan puhdistusvaihe desinfiointiohjelmia varten.", @@ -1158,6 +1122,7 @@ "wash_dw": "Pääpesuaine pesu lämmityksellä." }, "placeholder": { + "custom_task": "esim. Puhdista luukun tiiviste", "add": "lisää…", "group_name": "esim. Puuvilla 2:47", "json_buttons": "[{\"action\":\"ID\",\"title\":\"Label\"}]", @@ -1185,10 +1150,10 @@ "section": { "dishwasher": { "label": "Astianpesukone", - "intro": "Syklin lopun käsittely astianpesukoneille, jotka tyypillisesti päättyvät pitkään, lähes hiljaiseen kuivausvaiheeseen ennen lyhyttä lopullista vedenpoistoa." + "intro": "Syklin lopun käsittely astianpesukoneille, jotka kuivaavat usein pitkään hiljaa ennen lyhyttä lopullista vedenpoistoa." }, "anti_wrinkle": { - "intro": "Ryppyjenestotila tunnistaa lyhyet pienet pyörimisjaksot päävaiheen jälkeen ja pitää ne kiinnitettynä valmiiseen sykliin.", + "intro": "Pitää kuivausrummun päävaiheen jälkeiset vähätehoiset rummutuspulssit kiinni valmiissa syklissä.", "label": "Ryppyjenesto" }, "basic": { @@ -1196,7 +1161,7 @@ "label": "Perusasetukset" }, "delay": { - "intro": "Viivästetyn käynnistyksen tunnistus havaitsee, kun laite on päällä mutta sykliä ei ole vielä aloitettu.", + "intro": "Tunnistaa laitteen, joka on päällä ja odottaa viivästettyä käynnistystä.", "label": "Viivästetty käynnistys" }, "detection": { @@ -1220,161 +1185,153 @@ "label": "Ajoitus ja tarkkailija" }, "triggers": { - "intro": "Valinnaiset ulkoiset signaalit: päätyssignaali, ovianturi, taukokytkin ja purkamismuistutus.", + "intro": "Valinnaiset lisät: päättymissignaali, ovianturi, taukokytkin ja purkamismuistutus.", "label": "Käynnistysimpulssit ja ovet" - }, - "phase_eta": { - "label": "Jäljellä oleva aika", - "intro": "Vaihetietoinen jäljellä oleva aika koneille, joiden syklin pituus riippuu lämpötilasta tai linkouksesta." } }, "setting": { "dishwasher_end_spike_quiet_release": { "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ä." + "doc": "Kun odotettu kesto on ylitetty: kuinka kauan astianpesukoneen on pysyttävä Pysäytyskynnyksen alapuolella, ennen kuin WashData lakkaa odottamasta lopullista vedenpoistoa. Nosta arvoa, jos pitkän kuivauksen jälkeinen myöhäinen vedenpoisto jää huomaamatta. Odotus ei koskaan ylitä 30 minuuttia." }, "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." + "doc": "Osuus ohjelman keskimääräisestä kestosta, joka syklin on saavutettava, ennen kuin sen vähätehoinen rummutus lasketaan ryppyjeneston loppuhännäksi. Laske sitä (esim. 0,75) kuormaa tunnistavissa kuivausrummuissa. Pidä pesukoneet oletusarvossa (0,98), muuten pesu voi päättyä puolivälissä." }, "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.", + "doc": "Pidä kuivausrummun lyhyet vähätehoiset rummutuspulssit päälämpövaiheen jälkeen valmiissa syklissä sen sijaan, että ne luettaisiin uusiksi sykleiksi.", "label": "Ota ryppyjenesto käyttöön" }, "anti_wrinkle_exit_power": { - "doc": "Tehon on oltava tämän alapuolella pulssien välillä, jotta ryppyjä estävä tila pysyy aktiivisena.", + "doc": "Ryppyjenestotilassa tätä pienempi teho lasketaan hiljaiseksi rumpupulssien välillä, ja Pulssien enimmäisväliä pidempi hiljaisuus lopettaa tilan. Jos Pysäytyskynnys on suurempi, käytetään sitä, joten tällä on vaikutusta vain, kun se on Pysäytyskynnystä suurempi.", "label": "Poistumistehokynnys" }, "anti_wrinkle_idle_timeout": { - "doc": "Kuinka kauan kone saa pysyä hiljaa kahden rumpupulssin välillä ennen kuin ryppyjenesto-tila päättyy. Aseta se pidemmäksi kuin pisin väli, jonka kuivausrumpu jättää pulssien välille, muuten jokainen myöhempi pulssi luetaan vääräksi käynnistykseksi.", + "doc": "Kuinka kauan kuivausrumpu saa olla hiljaa rummutuspulssien välillä, ennen kuin ryppyjenestotila päättyy. Aseta se pidemmäksi kuin rumpusi pisin tauko, muuten myöhemmät pulssit luetaan vääriksi käynnistyksiksi.", "label": "Pulssien enimmäisväli" }, "anti_wrinkle_max_duration": { - "doc": "Tätä pidempiä pulsseja käsitellään pikemminkin todellisena syklinä kuin ryppyjä ehkäisevänä ryppynä.", + "doc": "Tätä pidempi pulssi lasketaan todelliseksi sykliksi, ei ryppyjeneston rummutukseksi.", "label": "Enimmäiskesto" }, "anti_wrinkle_max_power": { - "doc": "Tämän tehon yläpuolella olevaa pulssia pidetään todellisena uutena syklinä, ei ryppyjä vähentävänä ryppynä. Aseta juuri rumpupulssin tehon yläpuolelle.", + "doc": "Tämän tehon ylittävä pulssi lasketaan todelliseksi uudeksi sykliksi, ei rummutukseksi. Aseta se hieman rummutuspulssien tehon yläpuolelle.", "label": "Ryppyjeneston enimmäisteho" }, "auto_label_confidence": { - "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.", + "doc": "Jos täsmäytyspisteet syklin lopussa ovat tämä tai enemmän, ohjelma merkitään kysymättä. Nosta sitä suuremman varmuuden saamiseksi; laske sitä, jos haluat merkitä useampia syklejä automaattisesti. Pidä se suurempana kuin Oppimisen varmuus.", "label": "Automaattimerkinnän varmuus" }, "auto_maintenance": { - "doc": "Suorita iltasiivous: rakenna profiilikuoret uudelleen, laske syklin kunto uudelleen, karsi virheenkorjausjälkiä ja säilytä viimeisimmät jaksot.", + "doc": "Yöllinen siivous: merkitse vanhat syklit, jotka nyt selvästi täsmäävät ohjelmaan, rakenna ohjelmien käyrät uudelleen, päivitä poikkeamamerkinnät ja syklien kunto sekä poista virheenkorjausjäljet, jos \"Tallenna virheenkorjausjäljet\" on pois päältä. Syklit ja niiden tehokäyrät säilytetään aina.", "label": "Automaattinen huolto (yösiivous)" }, "completion_min_seconds": { - "doc": "Tätä lyhyemmät syklit hylätään haamujaksoina (koeajot, oven avaaminen sukan lisäämiseksi).", + "doc": "Tätä lyhyemmät syklit (koeajot, oven avaaminen sukan lisäämiseksi) tallennetaan tilaan Keskeytetty, eikä niistä lähetetä valmistumisilmoitusta.", "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." + "doc": "Liittää keskeytyneiden käynnistysyritysten lukemat syklin alkuun koneissa, jotka kokeilevat ennen käynnistymistä (luukun lukitus, ensimmäinen vedenotto). 300 s riittää useimmille; 0 = pois. Tallennetut alkuhetket siirtyvät aikaisemmiksi, joten opitut kestot ajautuvat jonkin aikaa." }, "delay_confirm_seconds": { - "doc": "Virran on pysyttävä valmiustilassa niin kauan, ennen kuin laitetta käsitellään käynnistystä odottavana eikä käynnissä.", + "doc": "Tehon on pysyttävä valmiustilan alueella näin kauan, ennen kuin laitteen katsotaan odottavan käynnistystä.", "label": "Vahvistusaika sekunteina" }, "delay_start_detect_enabled": { - "doc": "Havaitsee, kun laite on päällä ja odottaa (viivästetty käynnistys / valmiustila), mutta ei ole aloittanut jaksoaan, joten valmiustilan vetoa ei pidä sekoittaa käynnissä olevaan jaksoon.", + "doc": "Tunnistaa laitteen, joka on päällä ja odottaa viivästettyä käynnistystä, jotta sen valmiustilan kulutusta ei luulla käynnissä olevaksi sykliksi.", "label": "Ota viivästetyn käynnistyksen tunnistus käyttöön" }, "delay_timeout_hours": { - "doc": "Lopeta odottaminen viivästetty käynnistystilassa näin monen tunnin jälkeen ja palaa tyhjäkäynnille, jotta kone, joka on jätetty päälle mutta ei käynnistynyt, ei odota ikuisesti.", + "doc": "Lopeta viivästetyn käynnistyksen odottaminen näin monen tunnin jälkeen ja palaa tyhjäkäynnille.", "label": "Aikakatkaisutunnit" }, "device_type": { - "doc": "Kodinkoneluokka. Asettaa järkevät tunnistuksen oletusasetukset (kynnykset, viive, loppukäsittely), jotka on viritetty kyseiselle laitetyypille; muuta sitä vain, jos laite on alun perin määritetty vääräksi tyypiksi.", + "doc": "Asettaa tunnistuksen oletukset (kynnykset, sammutusviive, lopun käsittely) tälle laitetyypille. Muuta sitä vain, jos laite on määritetty väärän tyyppiseksi.", "label": "Laitteen tyyppi" }, "door_sensor_entity": { - "doc": "Valinnainen oven binaarisensori. Käytetään tunnistamaan, milloin laite on avattu/purettu syklin jälkeen.", + "doc": "Valinnainen ovianturi. Näyttää, milloin laite avattiin purkamista varten syklin jälkeen.", "label": "Ovianturin entiteetti" }, "duration_tolerance": { - "doc": "Jäljellä olevan ajan arvioiden toleranssi (oppimispalaute, ei täsmääminen). Jos todellinen kesto on +/-X prosentin sisällä arviosta, se lasketaan hyväksi osuudeksi.", + "doc": "Vain oppimispalaute: sykli, joka poikkeaa arvioidusta kestostaan enintään +/-X %, lasketaan hyväksi arvioksi. Ei vaikuta täsmäytykseen.", "label": "Arviotoleranssi" }, "enable_ml_models": { - "doc": "Kun sykli on käynnissä, anna mallien tarkentaa reaaliaikaisia tuloksia: vakaampi jäljellä oleva aika- ja energia-/kustannusarvio ja ennenaikaisen pysäytyksen eston varoitus päätteen havaitsemisessa (se voi vain viivästyttää maalia, ei koskaan lopettaa yhtä aikaisin ja on rajallinen). Käyttää hienosäädettyjä mallejasi, kun niitä on saatavilla, muuten sisäänrakennettuja. Off = vain klassinen tehopohjainen logiikka (vielä luotettava).", + "doc": "Käytä tällä koneella koulutettua mallia käynnissä olevan syklin energian ja kustannusten ennustamiseen, ja anna tutun ohjelman, joka pysähtyy epätavallisen aikaisin, päättyä odottamatta koko sammutusviivettä. Pois: vain tehoon perustuva logiikka.", "label": "Käytä älykkäitä malleja syklin aikana" }, "end_energy_threshold": { - "doc": "Off-viivelaskennan aikana kertynyttä energiaa (wattia x aika) verrataan tähän kynnykseen. Jos se ylittyy, lähtölaskenta nollautuu – rypistymisenestorummut ja astianpesukoneen kuivauspäät pysyvät kiinni syklissä sen sijaan, että niitä leikattaisiin lyhyiksi. Nosta sitä, jos jaksot päättyvät liian aikaisin jäähdytyksen aikana; alenna sitä, jos tunnistus on hidasta.", + "doc": "Sammutusviiveen aikana sallittu energia. Jos sitä kuluu enemmän, sykli jatkuu, joten rummutukset ja kuivauksen loppuvaiheet pysyvät syklissä. Laske sitä, jos syklit päättyvät loppuvaiheen aikana; nosta sitä, jos valmiit syklit jäävät odottamaan.", "label": "Lopetuksen energiakynnys" }, - "end_repeat_count": { - "doc": "Peräkkäisten pysäytyskynnyksen alapuolella olevien lukemien määrä, joka vaaditaan ennen syklin päättymistä. 1 sopii useimpiin pistokkeisiin. Nosta arvoon 2–3, jos älypistoke ilmoittaa toisinaan väärän nollan näytteen kesken jakson ja syklisi päättyvät ennenaikaisesti.", - "label": "Lopetuksen toistomäärä" - }, "energy_price_entity": { - "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ä.", + "doc": "Anturi, jossa on nykyinen hinta per kWh (esim. dynaaminen tariffi). Se ohittaa alla olevan kiinteän hinnan, joten sen on oltava hinta, ei pistokkeen teho- tai energia-anturi.", "label": "Energiahinnan entiteetti" }, "energy_price_static": { - "doc": "Kiinteää kWh-hintaa käytetään kustannusluvuissa, kun yllä ei ole asetettu reaaliaikaista hintakokonaisuutta.", + "doc": "Kiinteä hinta per kWh, jota käytetään, kun hintaentiteettiä ei ole asetettu.", "label": "Kiinteä energiahinta (per kWh)" }, "energy_sensor": { - "doc": "Valinnainen kumulatiivinen energialaskuri (total_increasing kWh/Wh, esim. pistokkeen oma kokonaismittari). Kun tämä on asetettu, kunkin syklin raportoitu energia otetaan tämän laskurin alku- ja loppuarvon erotuksesta, mikä välttää alilaskennan, jonka saat integroimalla hitaasti raportoivaa tehoanturia. Palaa integroituun arvoon, jos lukema puuttuu, sen yksikkö on tuntematon tai erotus ei ole positiivinen. Jätä tyhjäksi jatkaaksesi tehoanturin integrointia.", + "doc": "Valinnainen kWh- tai Wh-laskuri (total_increasing, esim. pistokkeen oma mittari). Syklin energia luetaan silloin siitä, mikä on tarkempaa kuin sen laskeminen hitaasta tehoanturista. Jos lukema on käyttökelvoton, käytetään laskettua arvoa.", "label": "Energiamittarin entiteetti" }, "expose_debug_entities": { - "doc": "Julkaise ylimääräisiä diagnostisia HA-kokonaisuuksia (osumavarmuus, moniselitteisyys, tilan sisäiset tiedot). Pois päältä pitää entiteettiluettelon puhtaana normaalia käyttöä varten.", + "doc": "Lisää diagnostiikkaentiteetit (täsmäytysvarmuus, epäselvyys, sisäinen tila). Pois päältä pitää entiteettiluettelon siistinä.", "label": "Näytä virheenkorjausentiteetit" }, "external_end_trigger": { - "doc": "Binaarisensori, jonka tilan muutos merkitsee syklin loppua (esim. laitteen \"valmis\" kosketin tai kumppaniintegraatio).", + "doc": "Binäärianturi, jonka muutos merkitsee syklin loppua (esim. \"valmis\"-kosketin tai toinen integraatio).", "label": "Ulkoisen signaalin entiteetti" }, "external_end_trigger_enabled": { - "doc": "Anna ulkoisen binaarisensorin signaloida jakson päättymisestä sisäänrakennetun tehopohjaisen tunnistuksen lisäksi.", + "doc": "Anna binäärianturin ilmoittaa syklin päättymisestä tehoon perustuvan tunnistuksen lisäksi.", "label": "Ota ulkoinen päätyssignaali käyttöön" }, "external_end_trigger_inverted": { - "doc": "Käsittele liipaisuanturin sammuttamista (eikä PÄÄLLÄ) syklin päättymissignaalina.", + "doc": "Käsittele anturin kytkeytymistä POIS (ei PÄÄLLE) päättymissignaalina.", "label": "Käännä ulkoinen signaali (laukaise POIS)" }, "learning_confidence": { - "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ä.", + "doc": "Pienin täsmäytyspistemäärä valmiin syklin merkitsemiseen. Selvä voittaja merkitään; tiukka tapaus menee Syklit-välilehden tarkistusjonoon. Tämän alapuolella mitään ei merkitä eikä kysytä. Pidä se pienempänä kuin Automaattimerkinnän varmuus.", "label": "Oppimisen varmuus" }, "linked_device": { - "doc": "Vaihtoehtoisesti voit sijoittaa tämän WashData-laitteen toisen laitteen (esim. älypistokkeen) alle HA-laiterekisterissä, joka näkyy muodossa \"Yhdistetty ...\".", + "doc": "Valinnainen. Näytä tämä WashData-laite Home Assistantissa toisen laitteen (esim. älypistokkeen) alla muodossa \"Yhdistetty ...\".", "label": "Ryhmittele laitteen alle" }, "min_off_gap": { - "doc": "Jos kone sammuu tämän ajan lyhyemmäksi ajaksi, päälle/pois/päälle-jaksoa käsitellään yhtenä jatkuvana jaksona. Estää liotusohjelmia (kone sammuu useiksi minuutiksi kesken pesun) jakamasta kahteen erilliseen jaksoon. Aseta se lyhyemmäksi kuin peräkkäisten kuormitustesi välinen ero, jos haluat, että ne lasketaan erillisiksi jaksoiksi. Laitetyyppiset oletusasetukset suojaavat kunkin laitteen tyypillistä jakson sisäistä taukoa.", + "doc": "Tätä lyhyempi sammuksissaolo ei jaa sykliä, joten liotustauot pysyvät samassa pesussa. Pidä se lyhyempänä kuin peräkkäisten koneellisten välinen tauko, muuten ne yhdistyvät yhdeksi sykliksi.", "label": "Pienin sammutusväli" }, "min_power": { - "doc": "Absoluuttinen minimiteho katsotaan aktiiviseksi. Tämän alapuolella olevia lukemia käsitellään 0 W:na (valmiustila), mikä suodattaa älypistokkeiden ja valmiustilan LEDien haamukuorman.", + "doc": "Tätä pienemmät lukemat lasketaan 0 W:ksi (valmiustila). Suodattaa pois älypistokkeiden ja valmiustilan LEDien tyhjäkäyntikulutuksen.", "label": "Minimiteho" }, "ml_training_enabled": { - "doc": "Tutki ajoittain tarkasteltuja jaksojasi yön yli ja hienosäädä mallit tälle koneelle. Muutos säilyy vain silloin, kun se todella saa paremman tuloksen pidennetyillä sykleillä, joten tämä voi vain auttaa tai pysyä samana - älä koskaan heikkene.", + "doc": "Kouluta energiamalli ajoittain uudelleen tämän koneen valmiilla sykleillä. Uusi malli otetaan käyttöön vain, jos se päihittää yksinkertaisen arvion testisykleillä.", "label": "Opi tästä koneesta" }, "ml_training_hour": { - "doc": "Paikallinen kellonaika (0-23) yön yli tapahtuvaa hienosäätöä varten. Valitse hiljainen tunti, kuten 2 (02:00).", + "doc": "Vuorokauden tunti (0-23) yökoulutukselle. Valitse hiljainen tunti, esim. 2.", "label": "Opi tunnilla" }, "ml_training_interval_days": { - "doc": "Tarkista parannukset uudelleen enintään kerran näin monen päivän välein.", + "doc": "Kouluta uudelleen enintään kerran näin monessa päivässä.", "label": "Tarkista enintään joka" }, "ml_training_min_cycles": { - "doc": "Odota, kunnes ainakin näin monta sykliä on tallennettu ennen hienosäätöä, jotta opittavaa riittää.", + "doc": "Odota vähintään näin montaa tallennettua sykliä ennen koulutusta.", "label": "Tarvittavat syklit ensin" }, "name": { - "doc": "Näyttönimi näkyy HA-integraatioluettelossa ja laiterekisterissä.", + "doc": "Tämän laitteen nimi Home Assistantissa.", "label": "Laitteen nimi" }, "no_update_active_timeout": { - "doc": "Jos virtapäivityksiä ei tule näin pitkään käynnissä ollessa, oleta, että pistoke putoaa offline-tilassa ja pakota pysäytys välttääksesi zombie-syklin. Oletusarvo 600 s mahdollistaa pilvi- tai mesh-viiveen.", + "doc": "Jos tehopäivitystä ei tule näin pitkään syklin aikana, oletetaan pistokkeen menneen offline-tilaan ja sykli pysäytetään pakolla. Oletus 600 s sallii pilvi- tai mesh-viiveet.", "label": "Päivityksettömyyden aikakatkaisuaika" }, "notify_before_end_minutes": { @@ -1382,35 +1339,35 @@ "label": "Lopetusennakkoilmoitus" }, "notify_channel": { - "doc": "Android-ilmoituskanavan nimi aloitus-/lähetysviesteille (ohjaa kanavakohtaista ääntä ja prioriteettia mobiilisovelluksessa). Tyhjä käyttää kumppanisovelluksen oletusarvoa.", + "doc": "Android-kanava aloitus- ja reaaliaikaviesteille; kanava määrää äänen ja prioriteetin. Tyhjä: sovelluksen oletus.", "label": "Android-kanava (käynnistys/suora)" }, "notify_cycle_timers": { - "doc": "Ilmoitukset tiettyinä minuutteina jakson aikana (esim. huuhteluaineen lisäämiseksi). Viesti tukee seuraavia: {device}, {program}, {minutes}. Ota automaattinen tauko käyttöön keskeyttääksesi kyseisessä kohdassa ja vastaanottaaksesi interaktiivisen ilmoituksen Jatka-painikkeella. Jatka paneelin, keskeytys/jatka-palvelun tai ilmoitustoiminnon kautta.", + "doc": "Ilmoitukset asetettuina minuutteina syklin alusta (esim. huuhteluaineen lisäämiseksi). Viesti tukee muuttujia {device}, {program}, {minutes}. Kun Automaattinen tauko on käytössä, sykli myös keskeytyy siinä kohdassa ja ilmoitukseen tulee Jatka-painike.", "label": "Sykliajastimet" }, "notify_finish_channel": { - "doc": "Android-ilmoituskanavan nimi viimeistelyviestille. Tyhjä käyttää uudelleen aloitus/live-kanavaa.", + "doc": "Android-kanava valmistumisviestille. Tyhjä käyttää aloitus- ja reaaliaikakanavaa.", "label": "Android-kanava (lopetus)" }, "notify_finish_message": { - "doc": "Ilmoituksen runko, joka lähetetään syklin päättyessä. Mallin muuttujat: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}. {time_finished} ja {vs_typical} ovat tässä hyödyllisimmät.", + "doc": "Syklin päättyessä lähetettävä teksti. Muuttujat: {device}, {program}, {duration} (minuutteina), {duration_hm} (1 h 05 min), {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}, {status} (completed tai force_stopped; keskeytetyt syklit eivät lähetä mitään). Muulla muuttujalla käytetään oletustekstiä.", "label": "Lopetusviesti" }, "notify_finish_services": { - "doc": "notify.* -palvelut kutsuvat, kun jakso päättyy. Lisää yksi kohdetta kohden; jätä tyhjäksi, jotta et halua lopettaa ilmoitusta.", + "doc": "notify.*-palvelut, joita kutsutaan syklin päättyessä. Jätä tyhjäksi, jos et halua ilmoitusta.", "label": "Lopetuspalvelut" }, "notify_fire_events": { - "doc": "Sammuta myös ha_washdata_*-tapahtumat syklin alussa/lopetuksessa, jotta voit rakentaa omia automaatioitasi.", + "doc": "Lähetä myös ha_washdata_*-tapahtumat syklin alussa ja lopussa omia automaatioitasi varten.", "label": "Käynnistä HA-tapahtumat ilmoituksille" }, "notify_icon": { - "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.", + "doc": "Pieni kuvake ilmoituksessa (esim. mdi:washing-machine). Lähetetään vain mobile_app-kohteisiin, ei notify-ryhmiin. iOS:ssä se korvaa sovelluksen kuvakkeen ja vaatii kumppanisovelluksen 2026.8 tai uudemman. Tyhjä: alustan oletus.", "label": "Ilmoituksen kuvake" }, "notify_live_chronometer": { - "doc": "Näytä reaaliaikaisesti päivittyvä ajastin ilmoituksessa (setä tukevilla alustoilla) staattisen arvion sijaan.", + "doc": "Näytä ilmoituksessa reaaliaikainen ajastin (jos tuettu) kiinteän arvion sijaan.", "label": "Käytä suoraa kronometriä" }, "notify_live_interval_seconds": { @@ -1418,143 +1375,135 @@ "label": "Suoran päivityksen väli" }, "notify_live_overrun_percent": { - "doc": "Jos sykli ylittää arvionsa enemmän kuin tämä prosenttiosuus, lähetä ylitysvaroitus.", + "doc": "Live-päivitykset loppuvat, kun sykli ylittää odotetun kestonsa tällä prosentilla (20 = enintään 1,2-kertainen odotettu aika).", "label": "Suoran yliajan % ennen hälytystä" }, "notify_live_services": { - "doc": "notify.* -palvelut vaativat reaaliaikaisia edistymispäivityksiä syklin aikana. Jätä tyhjäksi, jos haluat poistaa live-edistymisilmoitukset käytöstä.", + "doc": "notify.*-palvelut reaaliaikaisille edistymispäivityksille syklin aikana. Jätä tyhjäksi poistaaksesi ne käytöstä.", "label": "Suoran edistymisen palvelut" }, "notify_only_when_home": { - "doc": "Lähetä ilmoituksia vain, kun vähintään yksi linkitetyistä henkilöistä (yllä) on kotona.", + "doc": "Ilmoita vain, kun vähintään yksi yllä olevista henkilöistä on kotona.", "label": "Ilmoita vain kotona ollessa" }, "notify_people": { - "doc": "henkilö.* entiteetit, joita \"Ilmoita vain kotona\" käyttää päättääkseen, onko kukaan kotona.", + "doc": "person.*-entiteetit, jotka \"Ilmoita vain kotona ollessa\" tarkistaa.", "label": "Henkilöt (Ilmoita vain kotona ollessa)" }, "notify_pre_complete_message": { - "doc": "Loppua edeltävän / melkein valmis -ilmoituksen runko. Mallin muuttujat: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Jokaisen live-edistymispäivityksen teksti. Mallin muuttujat: {device}, {program}, {minutes} (jäljellä olevat minuutit). Muulla muuttujalla käytetään oletustekstiä.", "label": "Ennen valmistumista -viesti" }, "notify_reminder_message": { - "doc": "Vielä odottavan purkumuistutuksen runko. Tyhjä käyttää sisäänrakennettua oletusarvoa. Mallin muuttujat: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Kerran lähetettävän Lopetusennakkoilmoituksen teksti. Mallin muuttujat: {device}, {program}, {minutes} (Lopetusennakkoilmoitus-asetus). Muulla muuttujalla käytetään oletustekstiä.", "label": "Muistutusviesti" }, "notify_start_message": { - "doc": "Ilmoituksen runko, joka lähetetään syklin alkaessa. Mallin muuttujat: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Syklin alkaessa lähetettävä teksti. Mallin muuttujat: {device}, {program}. Muulla muuttujalla käytetään oletustekstiä.", "label": "Käynnistysviesti" }, "notify_start_services": { - "doc": "notify.* -palvelut kutsuvat, kun sykli alkaa. Lisää yksi kohdetta kohden (puhelin, kojelauta jne.); jätä tyhjäksi, jos et aloita ilmoitusta.", + "doc": "notify.*-palvelut, joita kutsutaan syklin alkaessa (puhelin, kojelauta, ...). Jätä tyhjäksi, jos et halua ilmoitusta.", "label": "Käynnistyspalvelut" }, "notify_timeout_seconds": { - "doc": "Hylkää ilmoitus automaattisesti tämän muutaman sekunnin kuluttua (käyttöympäristöissä, jotka tukevat sitä). 0 säilyttää sen, kunnes se hylätään manuaalisesti.", + "doc": "Sulje ilmoitus näin monen sekunnin jälkeen (jos tuettu). 0 pitää sen, kunnes suljet sen itse.", "label": "Piilota automaattisesti sen jälkeen" }, "notify_title": { - "doc": "Ilmoituksen otsikko. Mallin muuttujat: {device}, {program}, {duration}, {energy}, {cost}, {date}, {time}, {minutes}.", + "doc": "Ilmoituksen otsikko. Vain {device} korvataan; muulla muuttujalla käytetään oletusotsikkoa.", "label": "Ilmoituksen otsikko" }, "notify_unload_delay_minutes": { - "doc": "Minuutit syklin päättymisen jälkeen ennen edelleen odottavan koneen purkamisen muistutuksen lähettämistä. Aseta 0 poistaaksesi muistutuksen käytöstä.", + "doc": "Minuutit syklin päättymisestä \"pura kone\" -muistutukseen. 0 poistaa sen käytöstä.", "label": "Purkamismuistutuksen viive" }, "notify_unload_repeat": { - "doc": "Lähettää purkamismuistutuksen uudelleen ”Purkamismuistutuksen viive” minuutin välein, kunnes purkaminen on vahvistettu tai napautat ilmoituksen ”Lopeta muistuttaminen” -painiketta. Vaatii tavan vahvistaa purkaminen: ovianturin entiteetti, purkamisen vahvistusentiteetti tai alla oleva ”Vahvista purkaminen manuaalisesti” -asetus. Hylkäyspainike toimii Home Assistant -kumppanisovelluksen (mobiili) ilmoituksissa.", + "doc": "Lähetä purkamismuistutus uudelleen ”Purkamismuistutuksen viive” minuutin välein, kunnes purkaminen vahvistetaan tai napautat ”Lopeta muistuttaminen” (mobiilisovellus). Vaatii jonkin näistä: ”Ovianturin entiteetti”, ”Purkamisen vahvistusentiteetti” tai ”Vahvista purkaminen manuaalisesti”.", "label": "Toista kunnes purettu" }, "off_delay": { - "doc": "Aika odottaa tehon putoamisen jälkeen ennen kuin sykli julistetaan päättyneeksi. Jos virta palaa tämän ikkunan sisällä, jakso jatkuu saumattomasti - tämä siltaa taukoja pesuvaiheiden välillä. Astianpesukoneissa on pitkät kuivausvaiheet (virta pois päältä 20-60 min), joten sammutusviiveen on oltava tätä suurempi, jotta koko pesu+kuivaus pysyy yhtenä jaksona.", + "doc": "Kuinka kauan tehon on pysyttävä matalana, ennen kuin sykli on valmis. Jos teho palaa tänä aikana, sykli jatkuu. Astianpesukoneet voivat olla kuivauksen aikana hiljaa 20-60 min, joten aseta tämä sitä pidemmäksi.", "label": "Sammutusviive" }, "pause_cuts_power": { - "doc": "Kun sykli on keskeytetty, sammuta myös alla oleva Switch Entity. Vain laitteille, joiden pistoke voidaan katkaista turvallisesti kesken jakson.", + "doc": "Kun sykli keskeytetään, sammuta myös alla oleva Kytkimen entiteetti. Vain laitteille, jotka kestävät virrankatkaisun kesken syklin.", "label": "Tauko katkaisee myös virran (kytkimen kautta)" }, "power_off_delay": { - "doc": "Kuinka kauan tehon on pysyttävä Virran sammutuksen kynnyksen alapuolella jakson päätyttyä, ennen kuin tila palaa Pois päältä -tilaan. Käytetään vain, kun Virran sammutuksen kynnys on yli 0. Tarkistetaan taustasyklin mukaan, joten todellinen viive pyöristyy ylöspäin seuraavaan tilan vanhenemisajankohtaan.", + "doc": "Kuinka kauan tehon on pysyttävä Virran sammutuksen kynnyksen alapuolella syklin jälkeen, ennen kuin tila palaa Pois päältä -tilaan. Käytetään vain, kun kynnys on yli 0. Tarkistetaan ajoittain, joten todellinen odotus voi olla hieman pidempi.", "label": "Virran sammutuksen viive" }, "power_off_threshold_w": { - "doc": "Valinnainen tehoon perustuva Pois päältä -tilan tunnistus. Kun arvo on yli 0 ja jakso on päättynyt sekä teho pysyy tämän tason alapuolella Virran sammutuksen viiveen ajan, koneen katsotaan olevan sammutettu ja tila palaa Pois päältä -tilaan. Poista käytöstä jättämällä arvoon 0 (oletus). Aseta se todellisen sammutetun tason yläpuolelle mutta Pysäytyskynnyksen ja koneesi valmis-mutta-päällä-valmiustilan kulutuksen alapuolelle; jos se ei ole Pysäytyskynnyksen alapuolella, se ohitetaan. Käytössä ollessaan se korvaa Edistymisen nollausviiveen paluussa Pois päältä -tilaan, joten valmistunut kone pysyy Valmis/Puhdas-tilassa, kunnes se todella sammutetaan.", + "doc": "Kun teho on syklin jälkeen tämän alapuolella Virran sammutuksen viiveen ajan, tilaksi tulee Pois päältä Edistymisen nollausviiveen sijaan. Aseta se sammutetun laitteen kulutuksen yläpuolelle mutta valmiustilan ja Pysäytyskynnyksen alapuolelle. 0 poistaa sen käytöstä (oletus).", "label": "Virran sammutuksen kynnys" }, "power_sensor": { - "doc": "Anturientiteetti, joka raportoi tämän laitteen reaaliaikaisen tehon watteina (esim. sensor.washer_power). Kaikki syklin tunnistus perustuu tähän signaaliin.", + "doc": "Anturi, joka raportoi tämän laitteen reaaliaikaisen tehon watteina (esim. sensor.washer_power). Kaikki tunnistus perustuu siihen.", "label": "Tehoanturi" }, - "profile_duration_tolerance": { - "doc": "+/- nauha profiilin keskimääräisen keston ympärillä, jota käytetään täsmäämisen aikana. 0,25 tarkoittaa, että 60 minuutin profiili vastaa 45-75 minuutin jaksoja.", - "label": "Profiilin kestotoleranssi" - }, "profile_match_interval": { - "doc": "Kuinka usein profiilin täsmäämistä yritetään juoksujakson aikana. Oletusarvo 300 s (5 minuuttia) tasapainottaa tunnistusnopeuden ja suorittimen.", + "doc": "Kuinka usein käynnissä olevaa sykliä täsmäytetään profiileihin. Oletus 300 s (5 min).", "label": "Täsmäytysväli" }, "profile_match_max_duration_ratio": { - "doc": "Jakson enimmäispituus suhteessa profiiliin. 1.3 tarkoittaa, että syklin on oltava alle 130 % profiilin kestosta, jotta se täsmää.", + "doc": "Ohjelma hylätään, kun sykli on kestänyt pidempään kuin tämä kerroin ohjelman tyypillisestä kestosta. 1,8: ohjelmaa 80 % pidempi sykli ei enää täsmää siihen.", "label": "Enimmäiskestosuhde" }, "profile_match_min_duration_ratio": { - "doc": "Jakson vähimmäispituus suhteessa profiiliin. 0,9 tarkoittaa, että jakson on oltava vähintään 90 % profiilin kestosta, jotta se täsmää.", + "doc": "Ohjelma otetaan huomioon vasta, kun sykli on kestänyt tämän osuuden ohjelman tyypillisestä kestosta. 0,10: 2 tunnin ohjelmasta tulee ehdokas 12 minuutin jälkeen. Ei käytetä syklin ensimmäisten 15 minuutin aikana.", "label": "Vähimmäiskestosuhde" }, "profile_match_threshold": { - "doc": "Minimi samankaltaisuuspisteet (0-1) vaaditaan syklin lopussa ohjelman tunnisteen hyväksymiseksi. Nosta se vähentääksesi vääriä tunnistuksia; alenna sitä, jos koneesi ohjelmia ei täsmää. Oletusarvo 0,4 on konservatiivinen lähtökohta.", + "doc": "Reaaliaikaiset täsmäytyspisteet, jotka sykli tarvitsee, ennen kuin älykäs lopetus tai ryppyjeneston lopetus voi päättää sen aikaisin. Ei muuta näytettävää ohjelmaa. Nosta sitä, jos heikot täsmäytykset päättävät syklejä liian aikaisin; laske sitä, jos selvät täsmäytykset odottavat sammutusviiveen.", "label": "Täsmäytyskynnys" }, "profile_unmatch_threshold": { - "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.", + "doc": "Jos reaaliaikaiset täsmäytyspisteet putoavat tämän alle, nykyinen ohjelma-arvaus tyhjennetään. Pidä se hieman Täsmäytyskynnystä pienempänä, jotta lyhyt notkahdus ei nollaa sitä.", "label": "Eritäsmäytyskynnys" }, "progress_reset_delay": { - "doc": "Kun olet valmis, pidä edistymistä 100 %:ssa näin kauan, jotta Valmis näkyy kojelaudoissa, ennen kuin palautat sen tyhjäkäynnille.", + "doc": "Pidä edistyminen syklin jälkeen 100 %:ssa näin kauan ennen paluuta Tyhjäkäynti-tilaan.", "label": "Edistymisen nollausviive" }, "pump_stuck_duration": { - "doc": "Sekunteja pumppu voi käydä jatkuvasti ennen kuin se merkitään mahdollisesti jumiutuneeksi (käynnistää juuttunut pumppu -tapahtuman).", + "doc": "Kuinka kauan pumppu saa käydä yhtäjaksoisesti, ennen kuin jumiutuneen pumpun tapahtuma laukeaa.", "label": "Pumpun jumiutumiskesto" }, "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.", + "doc": "Käsiteltyjen lukemien välinen vähimmäisaika. Aiemmin saapuvat lukemat ohitetaan (syklin aikaiset matalat lukemat säilytetään aina). Aseta se pistokkeesi raportointiväliin tai pienemmäksi; suurempi arvo vain heittää lukemia pois.", "label": "Näytteenottoväli" }, "save_debug_traces": { - "doc": "Tallenna täyden tehon jäljitys ja vastaavat virheenkorjaustiedot jokaiselle jaksolle. Hyödyllinen vianetsinnässä, mutta lisää tallennustilaa.", + "doc": "Tallenna jokaisen syklin mukana myös täsmäyttimen virheenkorjaustiedot (jokaisen ehdokkaan pisteet) vianetsintää varten. Lisää noin 20 KB sykliä kohden.", "label": "Tallenna virheenkorjausjäljet" }, "smart_termination_duration_ratio": { - "doc": "Kuinka pitkälle täsmätyn ohjelman odotettuun kestoon syklin on edettävä, ennen kuin älykäs lopetus voi päättää sen ennenaikaisesti tehon pudottua. Odotettu kesto on ohjelman keskiarvo, joten laitteilla, joiden ajoaika vaihtelee paljon - pesukoneet kylmällä talven tulovedellä vs. lämpimällä kesän tulovedellä, anturikuivaavat kuivausrummut, kuormasta riippuvat ohjelmat - noin puolet kaikista ajoista päättyy tätä keskiarvoa lyhyempinä eivätkä koskaan saa nopeaa lopetusta, vaan päättyvät vasta vara-aikakatkaisun kautta minuutteja myöhässä. Laske tätä (esim. 0,85) näissä koneissa, jotta ennenaikainen lopetus silti laukeaa; nosta sitä kohti arvoa 1,0 ollaksesi varovaisempi. Jätä tyhjäksi käyttääksesi oletusta (0,98, tai astianpesukoneilla 0,99). Se voi ainoastaan päättää syklin aiemmin, ei koskaan myöhemmin, eikä koskaan laukea epäselvässä tai matalan varmuuden täsmäyksessä.", + "doc": "Osuus ohjelman keskimääräisestä kestosta, joka syklin on saavutettava, ennen kuin älykäs lopetus voi päättää sen aikaisin. Jos ajoajat vaihtelevat (veden lämpötila, anturikuivaus, kuorman koko), laske sitä (esim. 0,85). Jätä tyhjäksi käyttääksesi arvoa 0,98 (astianpesukoneilla 0,99).", "label": "Älykkään lopetuksen suhde" }, - "smoothing_window": { - "doc": "Kuinka paljon raakatehosignaali tasoittuu. Matala (2) on herkkä, mutta meluisa; korkea (5) tasoittaa piikkejä, mutta lisää viivettä.", - "label": "Tasoitusikkuna" - }, "start_duration_threshold": { - "doc": "Tehon on pysyttävä käynnistyskynnyksen yläpuolella näin kauan varmistaakseen todellisen käynnistyksen, estäen sekunnin osan päälle/pois-kytkimiä käynnistämästä sykliä.", + "doc": "Tehon on pysyttävä Käynnistyskynnyksen yläpuolella näin kauan, ennen kuin sykli alkaa, jotta lyhyet päälle/pois-piikit ohitetaan.", "label": "Käynnistyksen kesto" }, "start_energy_threshold": { - "doc": "Laitteen tulee kuluttaa energiaa (teho x aika) ennen KÄYTTÖÄ. Lyhyellä suuritehoisella piikkillä on erittäin vähän energiaa, ja se jätetään huomiotta, mikä estää väärät käynnistykset.", + "doc": "Energia, joka laitteen on kulutettava, ennen kuin sykli lasketaan käynnissä olevaksi. Lyhyt tehopiikki kuluttaa vähän energiaa, joten se ei käynnistä sykliä.", "label": "Käynnistyksen energia" }, "start_threshold_w": { - "doc": "Tehon on noustava tämän tason yläpuolelle varmistaakseen, että sykli on alkanut. Liian alhaiseksi asettaminen aiheuttaa vääriä käynnistyksiä valmiustilasta; liian korkea ja hitaasti käynnistyvät ohjelmat (kylmätäyttö) jäävät huomiotta. Ehdotusmoottori asettaa tämän juuri koneen havaitun alhaisimman aktiivisen tehon yläpuolelle.", + "doc": "Tehon on noustava tämän yli, jotta sykli alkaa. Liian matala: valmiustilan teho aiheuttaa vääriä käynnistyksiä. Liian korkea: hitaasti käynnistyvät ohjelmat (kylmä vedenotto) jäävät huomaamatta.", "label": "Käynnistyskynnys" }, "stop_threshold_w": { - "doc": "Tehon on laskettava tämän tason alapuolelle ennen kuin poiskytkentäviiveen laskenta alkaa. Aseta se aloituskynnyksen alapuolelle - niiden välinen rako on hystereesinauha, joka estää välkkymisen. Jos asetettu liian korkeaksi, vähän tehoa käyttävät vaiheet (huuhteluesteet, rypistymisen esto) laukaisevat virheellisesti loppujakson.", + "doc": "Tehon on laskettava tämän alle, ennen kuin sammutusviiveen laskenta alkaa. Pidä se Käynnistyskynnystä pienempänä. Liian korkealla matalatehoiset vaiheet (huuhtelutauot, ryppyjenesto) käynnistävät loppulaskennan virheellisesti.", "label": "Pysäytyskynnys" }, "switch_entity": { - "doc": "Valinnainen kytkin kytketään pois päältä taukotilanteessa ja takaisin päälle, kun jatkat, käytetään yhdessä \"Tauko katkaisee myös virran\" kanssa.", + "doc": "Valinnainen kytkin, joka sammutetaan tauon ajaksi ja kytketään takaisin päälle jatkettaessa, kun \"Tauko katkaisee myös virran\" on käytössä.", "label": "Kytkimen entiteetti" }, "watchdog_interval": { - "doc": "Kuinka usein taustavahti tarkistaa jumiutuneiden antureiden ja kuluneiden aikakatkaisujen varalta. Oletusarvo 30 s.", + "doc": "Kuinka usein taustatarkistus etsii jumiutuneita antureita ja aikakatkaisuja. Oletus 30 s.", "label": "Tarkkailijaväli" }, "notify_milestone_message": { @@ -1562,127 +1511,82 @@ "label": "Virstanpylväsviesti" }, "notify_milestones": { - "doc": "Pilkuin eroteltu määrä syklejä, jotka saavutettaessa laukaisevat kertaluontoisen juhlistusilmoituksen (esim. 50, 100, 500, 1000). Tyhjä poistaa virstanpylväsilmoitukset käytöstä.", + "doc": "Syklimäärät, jotka laukaisevat kertaluontoisen juhlailmoituksen, pilkuin eroteltuina (esim. 50, 100, 500, 1000). Tyhjä poistaa ne käytöstä.", "label": "Syklien virstanpylväät" }, "notify_quiet_end_hour": { - "doc": "Älä häiritse -ikkunan loppu (0-23). Pidätetyt ilmoitukset toimitetaan tällä tunnilla. Poista käytöstä jättämällä tyhjäksi.", + "doc": "Älä häiritse -ikkunan loppu (0-23); pidätetyt ilmoitukset lähetetään silloin. Tyhjä poistaa sen käytöstä.", "label": "Hiljaisten tuntien loppu" }, "notify_quiet_start_hour": { - "doc": "Älä häiritse -ikkunan alku (0-23). Valmistumis-, muistutus- ja puhtaat pyykit -ilmoitukset, jotka laukeaisivat hiljaisina tunteina, pidätetään ja toimitetaan ikkunan päättyessä. Poista käytöstä jättämällä tyhjäksi. Tukee keskiyön yli meneviä ikkunoita (esim. alku 22, loppu 7).", + "doc": "Älä häiritse -ikkunan alku (0-23). Ikkunan aikana valmistumis-, muistutus- ja puhtaat pyykit -ilmoitukset pidätetään sen loppuun asti. Voi ylittää keskiyön (esim. 22-7). Tyhjä poistaa sen käytöstä.", "label": "Hiljaisten tuntien alku" }, "peak_rate_message": { - "doc": "Valinnainen mukautettu teksti huippuhintavihjeelle, joka liitetään käynnistysilmoitukseen. Mallin muuttujat: {device}, {price}. Tyhjä käyttää sisäänrakennettua oletusarvoa.", + "doc": "Oma teksti huippuhintavihjeelle. Mallin muuttujat: {device}, {price}. Tyhjä käyttää oletusta.", "label": "Huippuhintaviesti" }, "peak_rate_threshold": { - "doc": "Kun sykli alkaa ja nykyinen hinta per kWh on tämä arvo tai suurempi, käynnistysilmoitukseen liitetään huippuhintavihje. 0 tai tyhjä poistaa vihjeen käytöstä.", + "doc": "Jos hinta per kWh on syklin alkaessa tämä tai enemmän, aloitusilmoitukseen lisätään huippuhintavihje. 0 tai tyhjä poistaa sen käytöstä.", "label": "Huippuhintakynnys (per kWh)" }, - "dtw_bandwidth": { - "label": "Muodon täsmäytyksen toleranssi", - "doc": "Kuinka paljon täsmäytin saa venyttää jaksoa ajassa, jotta se sopii tallennetun ohjelman muotoon. Suurempi sallii enemmän nopeusvaihtelua; liian suuri voi täsmätä väärän ohjelman." - }, - "corr_weight": { - "label": "Muodon ja tason painotus", - "doc": "Tasapaino käyrän muodon ja absoluuttisen tehotason välillä täsmäytystä pisteytettäessä. Suurempi suosii muotoa; pienempi suosii tehotason täsmäystä." - }, - "duration_weight": { - "label": "Keston painotus", - "doc": "Kuinka voimakkaasti suositaan ohjelmaa, jonka tyypillinen kesto vastaa tätä jaksoa." - }, - "energy_weight": { - "label": "Energian painotus", - "doc": "Kuinka voimakkaasti suositaan ohjelmaa, jonka tyypillinen energiankulutus vastaa tätä jaksoa." - }, "store_brand": { "label": "Laitteen merkki", - "doc": "Valinnainen. Laitteen merkki, joka valitaan yhteisön luettelosta. Käytetään vastaavien viitetallennusten löytämiseen ja jakamiseen. Jätä tyhjäksi, jos et käytä verkkotoimintoja." + "doc": "Valinnainen. Laitteesi merkki yhteisön luettelosta; käytetään viitetallennusten etsimiseen ja jakamiseen. Jätä tyhjäksi, jos et käytä verkkotoimintoja." }, "store_model": { "label": "Laitteen malli", - "doc": "Valinnainen. Laitteen malli, joka valitaan yhteisön luettelosta merkin asettamisen jälkeen. Jos malliasi ei ole luettelossa, voit lisätä sen luetteloon." + "doc": "Valinnainen. Laitteesi malli yhteisön luettelosta (valitse ensin merkki). Jos sitä ei ole luettelossa, voit lisätä sen." }, "show_contributor": { "doc": "Näytä \"tekijä \" -merkintä yhteisön laitteissa ja viitesykleissä." }, - "enable_phase_matching": { - "label": "Vaihetietoinen jäljellä oleva aika", - "doc": "Jaa jokainen käynnissä oleva sykli vaiheisiin (lämmitys, pesu, linkous) ja budjetoi jäljellä oleva aika vaihekohtaisesti, yhdistettynä klassiseen arvioon - nojaten syklin alkuvaiheessa vaihebudjettiin ja lopussa klassiseen arvioon. Tämä mukauttaa laskurin siihen, kuinka kauan koneesi todella lämmittää ja käy, mikä näkyy selvimmin syklin alkupuoliskolla. Pois = vain klassinen arvio. Vaikuttaa vain jäljellä olevan ajan näyttöön; ohjelman täsmäytys ja syklin tunnistus pysyvät ennallaan." - }, - "keep_min_score": { - "label": "Vähim. vastaavuuspisteet", - "doc": "Alin samankaltaisuuspisteet, jotka ehdokas tarvitsee pysyäkseen täsmäytyksessä mukana. Oletus 0,1 on tarkoituksellisesti salliva – se sallii jopa heikot ehdokkaat ja luottaa myöhempiin vaiheisiin parhaan löytämiseksi. Nosta leikkaamaan epätodennäköiset profiilit aikaisemmin; laske laajentamaan alkuperäistä ehdokaspoolia." - }, - "dtw_blend": { - "label": "DTW-sekoitus", - "doc": "Kuinka paljon DTW-kohdistuspisteet korvaavat Vaiheen 2 ydinpisteet. 0 = käytä vain Vaiheen 2 pisteitä, 1 = käytä vain DTW-pisteitä, 0,5 (oletus) = tasainen sekoitus. Nosta luottamaan enemmän aikakohdistukseen kun ohjelmilla on samanlaiset tehotasot mutta erilaiset ajoitusmustrit." - }, - "dtw_ensemble_w": { - "label": "DTW-ensemble-miksaus", - "doc": "Ensemble-DTW-tilassa (oletus) tämä on skaloidun L1:n paino verrattuna derivatiiviseen DTW:hen (DDTW). 1,0 = vain skaloitu L1, 0 = vain DDTW, 0,7 = oletus. Skaloitu muunnelma vertaa tehotasoja; derivatiivinen reagoi siihen miten teho muuttuu ajan kuluessa. Ensemble yhdistää molemmat." - }, - "dtw_ddtw_scale": { - "label": "Deriv. DTW-skala", - "doc": "Derivatiivisen DTW-pisteytyksen puolikyllästymisetäisyys. Pisteet puolittuvat kun DDTW-etäisyys on yhtä suuri kuin tämä arvo. Pienempi = herkempi muodon eroille. Oletus 30 on kalibroitu tyypillisten laitteiden tehokäyrille. Vaikuttaa vain ensemble- ja ddtw-DTW-tiloihin." - }, - "dtw_refine_top_n": { - "label": "DTW-tarkistusmäärä", - "doc": "Kuinka monelle Vaiheen 2 parhaalle ehdokkaalle DTW laskee pisteet uudelleen. DTW on kalliimpaa joten se sovelletaan vain N parhaaseen. Oletus 5. Nosta (7-9:ään) jos oikea profiili pääsee joskus vain 4. tai 5. sijalle Vaiheen 2 jälkeen – DTW voi pelastaa sen. Pienempi arvo nopeuttaa täsmäytystä hieman." - }, - "duration_scale": { - "label": "Keston tarkkuus", - "doc": "Vaiheen 4 keston yhdenmukaisuusrangaistuksen tarkkuus. Tämä on log-suhde jossa yhdenmukaisuuspisteet puolittuvat. Pienempi = tiukempi: keston poikkeama haittaa enemmän. Oletus 0,175 vastaa noin 18% kestontoleranssia puolipainolla. Yhdistä Kestopaino-asetukseen." - }, - "energy_scale": { - "label": "Energian tarkkuus", - "doc": "Vaiheen 4 energian yhdenmukaisuusrangaistuksen tarkkuus. Pienempi = tiukempi: energian poikkeama haittaa enemmän. Oletus 0,25 on tarkoituksellisesti sallivampi kuin kestotarkkuus koska energia vaihtelee kuorman mukaan. Yhdistä Energiapaino-asetukseen." - }, "power_profile_interval_min": { "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." + "doc": "power_profile-attribuutin aikavälin pituus (keskiteho watteina aikaväliä kohden), jota käyttävät suunnittelijat kuten EMHASS ja tibber_prices. Pienempi säilyttää lyhyet piikit; suurempi tasoittaa käyrää. Oletus 15 min. 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ä." + "doc": "Vain iOS. Päivitä reaaliaikaista edistymistä äänettömästi, akkua säästävällä prioriteetilla, jonka iOS voi niputtaa. Ensimmäinen päivitys sekä aloitus- ja valmistumisilmoitukset hälyttävät edelleen. Pois: jokainen päivitys hälyttää." }, "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." + "doc": "Vain Android. Pidä reaaliaikainen edistymisilmoitus näkyvissä napautuksen jälkeen. Pois: napautus sulkee sen." }, "notify_live_click_action": { "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." + "doc": "Mitä tämän laitteen ilmoituksen napautus avaa. Tyhjä: tämän laitteen WashData-paneeli. Anna kojelaudan polku (esim. /lovelace/laundry) tai URL, tai \"none\", jos napautus ei tee mitään." }, "door_opens_at_end": { "label": "Ovi avautuu automaattisesti lopussa", - "doc": "Astianpesukoneille, jotka avaavat oven syklin lopussa kuivauksen ajaksi (AirDry ja vastaavat). Tämän ollessa päällä oven avautuminen käynnissä olevan syklin aikana ei enää pysäytä sitä pysyvästi; sen sijaan, jos ovi pysyy auki alla olevan odotusajan verran, WashData pitää syklin valmiina. Lyhyt avaus (tavaran lisääminen) ohitetaan. Vaatii ovisensorientiteetin." + "doc": "Astianpesukoneille, jotka avaavat oven lopussa kuivausta varten (esim. AirDry). Jos ovi pysyy auki alla olevan odotusajan, sykli päättyy tauon sijaan; lyhyt avaus ohitetaan. Vaatii asetuksen Ovianturin entiteetti." }, "door_end_dwell_seconds": { "label": "Oven avauksen loppuviive", - "doc": "Kuinka kauan oven on pysyttävä auki ennen kuin WashData päättää syklin, kun \"Ovi avautuu automaattisesti lopussa\" on käytössä. Tarpeeksi kauan ohittamaan lautasen lisäämisen (oletus 60 s), tarpeeksi lyhyt päättämään nopeasti kun kone avaa oven." + "doc": "Kun \"Ovi avautuu automaattisesti lopussa\" on käytössä: kuinka kauan oven on pysyttävä auki, ennen kuin sykli päättyy. Tarpeeksi pitkä, jotta lautasen lisäämistä ei huomioida (oletus 60 s)." }, "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ä." + "doc": "Mitkä syklityypit muovaavat kunkin ohjelman käyrää ja täsmäytystä. Valinnan poistaminen ei poista mitään; hyödyllinen, jos et luota tuotuun dataan. Tilastot laskevat aina vain tämän koneen syklit. Kaikkien valintojen poistaminen ohitetaan." }, "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." + "doc": "Veloita kukin sykli kunkin käyntihetken hinnalla päättymishetken hinnan sijaan. Vaatii hintaentiteetin. Käsittele historia (Diagnostiikka) laskee vanhempien syklien kustannukset uudelleen." }, "unload_confirm_entity": { "label": "Purkamisen vahvistusentiteetti", - "doc": "Valinnainen entiteetti, jonka aktivoituminen tarkoittaa, että sisältö on otettu pois; tarkoitettu laitteille, joihin ei saa ovianturia. Mikä tahansa käy: Zigbee- tai NFC-painike, input_button-apuri, laitteen edessä oleva liiketunnistin, tilanne. Vahvistukseksi lasketaan jokainen tilan muutos paitsi sammuminen ja siirtyminen ei saatavissa -tilaan. Tämän asettaminen ottaa purkamismuistutuksen käyttöön myös laitteessa, jolla ei ole ovianturin entiteettiä." + "doc": "Valinnainen. Mikä tahansa entiteetti, joka vahvistaa, että kuorma on otettu pois, koneille ilman ovianturia (painike, input_button, liiketunnistin, tilanne). Jokainen tilan muutos lasketaan, paitsi sammuminen tai siirtyminen tilaan Ei käytettävissä. Ottaa myös purkamismuistutuksen käyttöön." }, "unload_track_without_door": { "label": "Vahvista purkaminen manuaalisesti", - "doc": "Käytä purkamismuistutusta laitteessa, jolla ei ole ovianturin entiteettiä eikä purkamisen vahvistusentiteettiä, ja vahvista purkaminen omalla automaatiollasi: paina ”Merkitse puretuksi” -painike-entiteettiä tai kutsu palvelua ha_washdata.mark_unloaded. Ohitetaan, kun ovianturin entiteetti on asetettu, koska se antaa signaalin jo valmiiksi." + "doc": "Käytä purkamismuistutusta ilman ovianturia tai vahvistusentiteettiä. Vahvista purkaminen omalla automaatiollasi ”Merkitse puretuksi” -painikkeella tai palvelulla ha_washdata.mark_unloaded. Ohitetaan, jos ”Ovianturin entiteetti” on asetettu." }, "notify_icon_color": { "label": "Ilmoituksen väri", - "doc": "Korostusväri tämän laitteen ilmoituksille, jotta pesukone, kuivausrumpu ja astianpesukone erottuvat toisistaan yhdellä silmäyksellä. iOS värittää kuvakkeen ja Liveseurannan edistymispalkin lukitulla näytöllä ja Dynamic Islandissa. Androidille lähetetään sama väri, mutta puhelin päättää, mitä se sillä tekee: jotkin värittävät kuvakkeen ja monet (erityisesti Samsung One UI) jättävät sen kokonaan huomiotta. Molemmat ymmärtävät heksakoodin (esim. #03A9F4). Jätä tyhjäksi alustan oletusta varten." + "doc": "Tämän laitteen ilmoitusten korostusväri, jotta pesukoneen, kuivausrummun ja astianpesukoneen erottaa helposti. iOS:ssä se täyttää kuvakkeen takana olevan ympyrän ja värittää Live Activity -palkin. Monet Android-puhelimet (etenkin Samsung) jättävät sen huomiotta. Käytä heksakoodia, esim. #03A9F4. Tyhjä: alustan oletus." + }, + "share_name": { + "doc": "Näytä tilisi nimi jakamiesi syklien lataajana. Pois: jaetaan ilman nimeä." } }, "setting_group": { @@ -1710,9 +1614,6 @@ "door_pause": { "label": "Ovi ja tauko" }, - "dtw_refinement": { - "label": "DTW-tarkennus" - }, "duration_gates": { "label": "Keston portit" }, @@ -1789,17 +1690,20 @@ "rebuilding": "Rakennetaan uudelleen…", "analyzing": "Analysoidaan…", "resetting": "Nollataan…", - "reverting": "Palautetaan…", "trimming": "Leikataan…", "splitting": "Jaetaan…", + "stalled": "Jumissa", "deleting": "Poistetaan…", "imported": "Tuotu", "preparing": "Valmistellaan…", - "unlabelling": "Poistetaan merkintää…" + "unlabelling": "Poistetaan merkintää…", + "working": "Käsitellään…", + "match_uncertain": "Epävarma: {top} tai {runner}", + "match_uncertain_single": "Epävarma: ehkä {top}", + "match_sure": "~{pct} % varma", + "match_uncertain_tip": "WashData ei ole vielä päättänyt ohjelmaa. Luku kertoo, kuinka usein johtava arvaus, {top}, osui oikeaan aiemmissa sykleissä, kun se johti näin paljon. Vain tiedoksi." }, "suggestion": { - "both_agree": "WashData suosittelee", - "calibrated_label": "Kalibroitu", "observed_label": "Havaittu", "impact": { "auto_label_confidence": { @@ -1814,10 +1718,6 @@ "higher": "Sulkee vain syklit, jotka kuluttivat merkittävästi energiaa", "lower": "Sulkee myös lyhyemmät tai vähän energiaa kuluttavat syklit" }, - "end_repeat_count": { - "higher": "Päätyssignaali täytyy toistua useammin – luotettavampi, hieman hitaampi", - "lower": "Päättyy vähemmän toistojen jälkeen – nopeampi tunnistus, enemmän riskiä väärästä lopetuksesta" - }, "learning_confidence": { "higher": "Oppii vain erittäin varmoista osumista – hitaampi mutta luotettavampi", "lower": "Oppii useammista sykleistä – malli voi olla epätarkempi" @@ -1856,40 +1756,25 @@ } }, "reason": { - "auto_label_confidence": "15. persentiilin luottamus {cycles} automaattiselle merkinnälle, joita käyttäjä ei koskaan korjannut ({p15}).", "completion_min_seconds": "Puolet puhtaiden jaksojen p05-kestosta ({minutes} min) {cycles} jakson joukossa; suodattaa haamujaksot pois.{excl}", - "dead_zone_batch": "Pidetään lyhyenä (käynnistyksen epävakausikkunan mediaani {cycles} jakson joukossa = {s}s), jotta todellinen loppu havaitaan nopeasti.", - "dead_zone_from_dip": "Perustuu varhaiseen tehonlaskuun, joka havaittiin kohdassa {s}s.", - "end_energy_default": "Suositeltu oletusarvo jakson lopun kohinakynnykselle.", - "end_energy_false": "p95 väärien loppujen energialle ({p95}Wh) {cycles} jakson joukossa, alarajana 0.2% jakson mediaanienergiasta ({floor}Wh).", - "end_energy_no_false": "Ei vääriä loppuja {cycles} jakson joukossa; alarajana 0.2% jakson mediaanienergiasta ({floor}Wh).", - "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.", + "match_interval_floored": "Lyhin ohjelma ({shortest}s) rajaisi tämän arvoon {cap}s, alle {minimum}s vähimmäisarvon, joten käytetään vähimmäisarvoa: {persistence} peräkkäistä täsmäytystä kestää {budget}s, yli {pct} % kyseisestä ohjelmasta. Täsmäytys tehdään vain lukeman saapuessa (mediaani={median}s), joten tämä 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.", "min_off_gap": "Perustuu {gaps} havaittuun jaksojen väliseen taukoon (p05={p05}s). Laitteen alaraja: {floor}s.", "min_off_gap_bridge": "Mitoitettu siltaamaan jakson pisin hiljainen vaihe: {span}s + 60s puskuri, {spans} sillatusta katkosta {cycles} puhtaan jakson joukossa (pysyy alle {ceiling}s, joka on lyhin peräkkäisten käynnistysten välinen taukosi).", "min_power": "40% pienimmästä aktiivisesta tehosta p05:ssä ({p05}W) {cycles} puhtaan jakson joukossa, mikä pitää sammutuskynnyksen todellisen kulutuksen alapuolella.{excl}", - "ml_auto_label": "Pienin luottamus, jonka laatumalli arvioi vielä puhtaaksi (p10 {cycles} puhtaasta jaksosta = {p10}).", - "ml_end_repeat": "{false}/{total} jaksosta ({pct}%) sisälsi tauon, jolle loppudetektori antoi >50%; vaatii {val} loppuvahvistusta.", - "ml_off_delay": "Loppudetektorin vahvistamat tauot: p95 {p95}s + 60s puskuri, {pauses} mallin varmistamasta tauosta {cycles} jakson joukossa (alaraja {floor}s).", "no_update_timeout": "Perustuu havaittuun päivitystahtiin (p95={p95}s) * 20 (väh. 60s).", "off_delay_cadence": "Perustuu havaittuun päivitystahtiin (p95={p95}s) * 5", "off_delay_device_floor": "Käytettiin laitekohtaista turvallista vähimmäisarvoa laitteelle {device} ({floor}s).", "off_delay_generic_floor": "Käytettiin yleistä turvallista vähimmäisarvoa ({floor}s).", "off_delay_pauses": "Mitoitettu kestämään todellisia taukoja pidempään: p95 jakson sisäiselle tauolle {p95}s + 60s puskuri, {pauses} tauosta {cycles} puhtaan jakson joukossa (alaraja {floor}s).", - "profile_match_threshold": "p10-luottamus {cycles} oikealle automaattiselle merkinnälle ({p10}); turvallinen alaraja reaaliaikaiselle vahvistukselle.", - "sampling_interval": "Päivitysvälin mediaani {cycles} puhtaan jakson joukossa ({si}s).{excl}", - "smoothing_window": "Mitoitettu tasoittamaan noin 30s lukemia {si}s näytteenottovälillä ({samples} näytettä).", - "start_duration": "Pidetään lyhyenä (~yksi {si}s näytteenottoväli), jotta tunnistus alkaa mahdollisimman aikaisin mutta jättää silti yksittäiset piikit huomiotta.", - "thr_batch": "Pidetään juuri pienimmän aktiivisen tehon yläpuolella p05:ssä {cycles} jakson joukossa ({p05}W), jotta käynnistys havaitaan mahdollisimman aikaisin ja pysäytyskynnys pysyy koneen pienimmän käyntitehon alapuolella.", - "tol_per_profile": "p75 profiilikohtaisesta keston varianssista {profiles} profiilin joukossa ({cycles} jaksoa); tiiviitä profiileja ei rangaista.", - "tol_pooled": "Perustuu {cycles} viimeisimmän merkityn jakson yhdistettyyn keston varianssiin (p95 poikkeama={dev}).", - "watchdog": "Pidetään niin matalana kuin on turvallista (hieman yli p95-päivitysvälin {p95}s ja vähintään 2x näytteenottoväli {median}s, väh. 30s), jotta pysähdykset havaitaan nopeasti ilman vääriä pysäytyksiä." + "standby_floor": "Jokainen viimeinen sykli otti yhä {idle} W loppuessaan, joten sen alapuolella oleva kynnys ei koskaan näe laitteen sammuvan. Pidetään sen yläpuolella; tallennetuista {cycles} puhtaista sykleistä mikään ei koskaan pysähtynyt sen alle yli {pause} sekunniksi.", + "thr_batch": "Juuri pienimmän aktiivisen tehon (p05) yläpuolella {cycles} syklin joukossa ({p05}W), jotta käynnistykset havaitaan aikaisin ja pysäytyskynnys pysyy pienimmän käyntitehon alapuolella.", + "watchdog": "Pidetään niin matalana kuin on turvallista (hieman yli p95-päivitysvälin {p95}s ja vähintään 2x näytteenottoväli {median}s, väh. 30s), jotta pysähdykset havaitaan nopeasti ilman vääriä pysäytyksiä.", + "end_energy_floor": "Nostettu alarajaan, jonka pysäytyskynnys ({stop} W) sammutusviiveen ({off_delay} s) aikana edellyttää; sen alapuolella sykli voi päättyä vain varapolun kautta.", + "max_duration_ratio_default": "Takaisin oletusarvoon: matalampi yläraja hylkää oikean ohjelman aina, kun ajo kestää tavallista pidempään.", + "min_duration_ratio_default": "Takaisin oletusarvoon: korkeampi alaraja estää käynnissä olevan syklin tunnistamisen, kunnes se on jo pitkällä." }, "exclusions": { "summary": "Suljettu pois {total} väärin tunnistettua sykliä: {parts}.", @@ -1907,7 +1792,8 @@ "no_power": "ilman tehoa", "no_active_power": "ilman aktiivista tehoa" } - } + }, + "now_value": "(nyt {v})" }, "tab": { "advanced": "Edistynyt", @@ -1939,13 +1825,17 @@ "toast": { "catalog_refreshed": "Yhteisön luettelo päivitetty", "contrib_device_changed": "Lisätty yhteisökauppaan, mutta valittu laite vaihtui lomakkeen ollessa auki. Valitse se asetuksista.", + "task_name_required": "Anna tehtävälle nimi", + "task_added": "Tehtävä lisätty", + "task_save_failed": "Tehtävää ei voitu tallentaa: {error}", + "task_removed": "Tehtävä poistettu", + "maint_logged": "Kirjattu tehdyksi: {task}", "access_saved": "Kulunvalvonta tallennettu", "all_wiped": "Kaikki tiedot pyyhitty", "analysis_complete_none": "Analyysi valmis: ei uusia ehdotuksia", "auto_label_complete": "Automaattinen tarra valmis", "automation_deleted": "Automaatio poistettu", "correction_submitted": "Oikaisu lähetetty", - "cycle_deleted": "Jakso poistettu", "cycle_force_stopped": "Jakso pysäytetty", "cycle_labelled": "Sykli merkitty", "cycle_paused": "Jakso keskeytetty", @@ -1961,11 +1851,9 @@ "group_deleted": "Ryhmä poistettu", "group_name_required": "Ryhmän nimi vaaditaan", "group_saved": "Ryhmä tallennettu", - "import_successful": "Tuonti onnistui; integroinnin uudelleenlataus", "json_required": "JSON-tiedot vaaditaan", "legacy_removed": "Vanhat toiminnot poistettu", "logs_exported": "Lokit viety", - "matching_reverted": "Vastaavat painot palautettiin oletusarvoihin", "min_2_profiles": "Valitse vähintään 2 profiilia ryhmälle", "ml_training_no_improvement": "Harjoittelu suoritettu: lähtötaso pidetty (ei parannusta)", "models_reverted": "Laitteessa olevat mallit palautettiin lähtötasolle", @@ -1975,8 +1863,8 @@ "pg_preset_loaded": "Esiasetus \"{name}\" ladattu", "pg_preset_saved": "Esiasetus \"{name}\" tallennettu", "pg_sugg_loaded": "{n} ehdotettu arvo ladattu testialueelle - aja testialue vertailua varten", - "pg_sugg_ml_loaded": "{n} ML-kalibroitua arvoa ladattu testialueelle - aja testialue vertailua varten", "phase_name_required": "Vaiheen nimi vaaditaan", + "phase_reset": "Vaihe \"{name}\" palautettu sisäänrakennettuun muotoon", "phase_updated": "Vaihe päivitetty", "phases_saved": "Vaiheet tallennettu", "preferences_saved": "Asetukset tallennettu", @@ -2023,14 +1911,10 @@ "store_name_required": "Anna profiilin nimi", "store_import_failed": "Tuonti epäonnistui: {error}", "store_imported": "Tuotu profiiliin {profile}", - "store_program_required": "Anna ohjelman nimi", "store_no_appliance": "Aseta ensin laitteen merkki ja malli asetuksista.", "store_share_failed": "Jakaminen epäonnistui: {error}", "store_shared": "Jaettu yhteisökauppaan - odottaa tarkistusta.", "appliance_added": "Laite lisätty - odottaa hyväksyntää", - "thanks_confirming": "Kiitos vahvistuksesta", - "device_approved": "Yhteisö hyväksyi", - "rating_saved": "Laatuarvio tallennettu", "brand_added": "Merkki lisätty - odottaa hyväksyntää", "profile_added": "Profiili lisätty - odottaa hyväksyntää", "saved_except_conflicts": "Tallennettu. Korjaa korostetut ristiriidat tallentaaksesi loput.", @@ -2053,7 +1937,48 @@ "odometer_saved": "Syklien kokonaismäärä tallennettu", "odometer_save_failed": "Kokonaismäärän tallennus epäonnistui: {error}", "store_pick_profile_share": "Valitse profiili, johon jaetaan", - "remove_failed": "Poisto epäonnistui: {error}" + "remove_failed": "Poisto epäonnistui: {error}", + "analysis_complete": "Analyysi valmis: ehdotuksia {count}", + "analysis_failed": "Analyysi epäonnistui: {error}", + "auto_detect_failed": "Automaattinen tunnistus epäonnistui: {error}", + "auto_label_failed": "Automaattinen merkintä epäonnistui: {error}", + "could_not_load_cycle": "Syklin lataaminen epäonnistui: {error}", + "create_failed": "Luominen epäonnistui: {error}", + "cycles_deleted": "Syklejä poistettu: {count}", + "debug_cleared": "Virheenkorjausjälkiä tyhjennetty: {count}", + "discard_failed": "Hylkääminen epäonnistui: {error}", + "error": "Virhe: {error}", + "export_failed": "Vienti epäonnistui: {error}", + "import_failed": "Tuonti epäonnistui: {error}", + "invalid_json": "\"{key}\" ei ole kelvollista JSONia - korjaa se tai tyhjennä kenttä ennen tallentamista.", + "label_failed": "Merkintä epäonnistui: {error}", + "ml_training_promoted": "Koulutus valmis: malleja otettu käyttöön {count}", + "pause_failed": "Tauolle asettaminen epäonnistui: {error}", + "phase_created": "Vaihe \"{name}\" luotu", + "phase_deleted": "Vaihe \"{name}\" poistettu", + "profile_created": "Profiili \"{name}\" luotu", + "rename_failed": "Uudelleennimeäminen epäonnistui: {error}", + "save_failed": "Tallennus epäonnistui: {error}", + "split_complete": "Jaettu {count} sykliin", + "start_failed": "Käynnistys epäonnistui: {error}", + "stop_failed": "Pysäytys epäonnistui: {error}", + "update_failed": "Päivitys epäonnistui: {error}", + "store_download_skipped": "{n} tallennus(ta) ohitettu: liian lyhyitä, liian katkonaisia tai jo laitteellasi.", + "store_import_low_quality": "Tämä tallennus on liian lyhyt, liian katkonainen tai epäuskottava tuotavaksi.", + "store_import_duplicate": "Tämä tallennus on jo laitteellasi.", + "device_approved": "Yhteisö hyväksyi", + "thanks_confirming": "Kiitos vahvistuksesta", + "rating_saved": "Laatuarvio tallennettu", + "store_download_partial": "Ohjelmia ei voitu ladata: {n}. Yritä myöhemmin uudelleen.", + "import_no_restore_point": "Tuotu, mutta palautuspistettä ei voitu tallentaa, joten tätä tuontia ei voi kumota", + "import_undo_failed": "Tuontia ei voitu kumota: {error}", + "import_undone": "Tuonti kumottu: {profiles} profiilia ja {cycles} jaksoa palautettu", + "auto_label_cancelled": "Automaattinen merkintä peruttiin, kun {count} sykliä oli merkitty.", + "auto_label_cancelled_one": "Automaattinen merkintä peruttiin, kun {count} sykli oli merkitty.", + "auto_label_cancelled_other": "Automaattinen merkintä peruttiin, kun {count} sykliä oli merkitty.", + "auto_label_done": "Automaattinen merkintä valmis: {count} sykliä merkitty, {skipped} jäi merkitsemättä.", + "auto_label_done_one": "Automaattinen merkintä valmis: {count} sykli merkitty, {skipped} jäi merkitsemättä.", + "auto_label_done_other": "Automaattinen merkintä valmis: {count} sykliä merkitty, {skipped} jäi merkitsemättä." }, "trend": { "down": "Trendikäs alaspäin", @@ -2061,6 +1986,10 @@ }, "maint": { "bearing_service": "Laakerihuolto", + "salt": "Lisää suolaa", + "rinse_aid": "Lisää huuhtelukirkastetta", + "lint_filter": "Puhdista nukkasihti", + "condenser_clean": "Puhdista lauhdutin", "descale": "Kalkinpoisto", "drum_clean": "Puhdista rumpu", "filter_clean": "Puhdista suodatin", @@ -2071,12 +2000,14 @@ "status_tip": "Syklin lopputulos: Valmistunut (luonnollinen loppu), Keskeytynyt (äkillinen tehon lasku), Pakotettu pysäytys (manuaalinen) tai Vaatii tarkistuksen (palaute odottaa).", "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. 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.", + "energy_tip": "Kulutettu kokonaisenergia (kWh), laskettu tehosta ajan yli.", + "cost_tip": "Tämän syklin energiakustannus, lukittu valmistuessa. Kun Aikapainotettu kustannus on käytössä, kukin hetki veloitetaan omalla hinnallaan; muuten lopun hinnalla. Aseta hinta Asetuksissa, niin tämä täyttyy.", "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.", - "cost_dynamic_tip": "Aikapainotettu: veloitettu syklin aikana voimassa olleella hinnalla (toteutunut {price} {cur}/kWh)." + "cost_dynamic_tip": "Aikapainotettu: veloitettu syklin aikana voimassa olleella hinnalla (toteutunut {price} {cur}/kWh).", + "dbg_conf": "Luott.", + "dbg_mae": "MAE", + "dbg_corr": "Korr." }, "stat": { "avg": "ka.", @@ -2100,7 +2031,10 @@ "notify_finish": "Lähetettäisiin valmistumisilmoitus.", "notify_milestone": "Lähetettäisiin välitavoiteilmoitus.", "notify_held": "Ilmoitus pidätettiin hiljaisten tuntien vuoksi.", - "finished": "Jakso saavutti lopputilan ja päättyi." + "finished": "Jakso saavutti lopputilan ja päättyi.", + "match_reverted": "Ohjelma palasi havaitsemistilaan: sen pisteet putosivat selvästi oman huippunsa alapuolelle.", + "verified_pause": "Reaaliaikainen kohdistus vahvisti (tai vapautti) tauon ohjelman sisällä. Niin kauan kuin se on voimassa, jaksoa ei päätetä.", + "stalled": "Sykli pysyi valmiustilan kulutuksessa pidempään kuin tämä ohjelma koskaan pitää siinä taukoa, vaikka työtä oli jäljellä. Näytetään tilana Keskeytetty (Jumissa); sykli pysyy avoimena." }, "store": { "search_ph2": "Hae merkin tai mallin mukaan…", @@ -2109,11 +2043,7 @@ "programs_count": "Ohjelmat: {n}", "browse": "Selaa", "device": "Laite", - "favorites": "Suosikit", - "search_ph": "Hae merkin tai mallin mukaan…", "no_results": "Vastaavia laitteita ei löytynyt. Kokeile toista hakua.", - "n_programs": "{n} ohjelmaa", - "n_cycles": "{n} sykliä", "no_programs": "Tälle laitteelle ei ole vielä jaettuja ohjelmia.", "no_cycles": "Tälle ohjelmalle ei ole vielä jaettu viitesyklejä.", "anon": "anonyymi", @@ -2125,19 +2055,21 @@ "merge_target": "Yhdistä profiiliin", "description": "Kuvaus (valinnainen)", "contributed_by": "tekijä {name}", + "website": "Kaupan sivusto ↗", + "website_tip": "Avaa yhteisökaupan verkkosivusto", + "favorites": "Suosikit", "rating_summary": "★ {avg} ({n})", "no_ratings": "Ei vielä arvioita", - "website": "Kaupan sivusto ↗", - "website_tip": "Avaa yhteisökaupan verkkosivusto" + "unreachable": "Yhteisökauppaan ei saatu yhteyttä. Tarkista yhteytesi ja yritä uudelleen." }, "settings": { "gear": { - "title": "Asetukset" + "title": "Mieltymykset" } }, "link": { - "manual": "Käyttöohje ↗", - "add_appliance": "Lisää laitteesi" + "maintenance_supporter": "Maintenance Supporter GitHubissa ↗", + "manual": "Käyttöohje ↗" }, "tip": { "add_brand": "Merkkiä ei ole listalla? Lisää se yhteisön luetteloon", @@ -2171,7 +2103,13 @@ "rebuild": { "envelopes": "Rakennetaan tehoalueita uudelleen" }, - "cancelling": "Perutaan..." + "cancelling": "Perutaan...", + "auto_label": { + "running": "Merkitään syklejä automaattisesti" + }, + "store_download": { + "running": "Ladataan kaupasta" + } }, "setup": { "hdr": { @@ -2180,26 +2118,24 @@ }, "phase0": { "washer": "WashData tunnistaa jo syklisi. Tallenna ensimmäinen sykli ottaaksesi käyttöön ohjelmanimet ja aikaarviot.", - "dishwasher": "WashData tarkkailee. Astianpesukoneilla on monimutkaisia syklejä – ensimmäisen syklin tallentaminen on erittäin suositeltavaa. Jos tunnistettu sykli kestää liian kauan, käytä syklin editoria sen leikkaamiseen ennen profiilina tallentamista.", + "dishwasher": "WashData tarkkailee. Astianpesukoneen syklit ovat monimutkaisia, joten tallenna ensimmäinen syklisi. Jos tunnistettu sykli kestää liian kauan, rajaa se syklieditorissa ennen kuin tallennat sen profiiliksi.", "generic": "WashData tarkkailee. Tallenna tai merkitse tunnistettu sykli aloittaaksesi profiilien luomisen." }, "phase1a": { - "labelled": "Hyvä alku – ensimmäinen ohjelmasi on tallennettu. Puhtaimman datan saamiseksi harkitse seuraavan syklin tallentamista tallennuswidgetillä." + "labelled": "Hyvä alku: ensimmäinen ohjelmasi on tallennettu. Puhtaimman datan saat tallentamalla seuraavan syklin tallentimella." }, "phase1b": { - "recorded": "Tallennuksesi on tallennettu nimellä {profile_name}. Tallenna tai merkitse nyt muut yleisimmät ohjelmasi kattavuuden rakentamiseksi." + "recorded": "Tallennuksesi tallennettiin nimellä {profile_name}. Tallenna tai merkitse nyt muut yleiset ohjelmasi." }, "phase1c": { - "verify": "Sinulla on {count} ohjelmaa yhteisöltä. Suorita sykli tarkistaaksesi, tunnistaako WashData sen oikein – täsmäytys paranee laitteen kerätessä omaa historiaansa." + "verify": "Sinulla on {count} ohjelmaa yhteisöltä. Aja sykli ja tarkista, että WashData tunnistaa sen; täsmäytys paranee, kun laitteesi kerää omaa historiaa." }, "phase2": { - "cluster": "WashData on havainnut {count} sykliä, jotka eivät täsmää mihinkään tallennettuun ohjelmaan – ne näyttävät samanlaisilta keskenään. Haluatko luoda niille uuden profiilin?", + "cluster": "WashData on havainnut {count} samankaltaista sykliä, jotka eivät täsmää mihinkään tallennettuun ohjelmaan. Luodaanko niille uusi profiili?", "unmatched": "Viimeisin sykli ei täsmännyt mihinkään tallennettuun ohjelmaan. Onko se uusi ohjelma?" }, "phase3": { - "suggestions": "WashDatalla on asetussuosituksia syklihistoriasi perusteella – tarkista ne tunnistustarkkuuden parantamiseksi.", - "groups": "Jotkut profiileistasi näyttävät samalta ohjelmalta eri lämpötiloissa. Järjestä ne ryhmään parempaa täsmäytystä varten.", - "phases": "Lisää ohjelman vaiheet profiiliin {profile_name} tarkempien jäljellä olevan ajan arvioiden saamiseksi." + "suggestions": "WashDatalla on asetusehdotuksia syklihistoriasi perusteella. Tarkista ne tunnistuksen parantamiseksi." }, "phase4": { "healthy": "Tämä laite on täysin asennettu ({profile_count} profiilia)." @@ -2212,12 +2148,58 @@ "create_from_cluster": "Luo profiili näistä sykleistä", "create_profile": "Luo sille profiili", "review_suggestions": "Tarkista ehdotukset", - "organise_profiles": "Järjestä profiilit", - "configure_phases": "Määritä vaiheet", "skip_step": "Ohita tämä vaihe", "skip_forever": "Älä näytä enää", "hide_guidance": "Piilota opastus", "show_guidance": "Näytä opastus" } + }, + "access": { + "edit": "Muokkaus", + "full": "Täysi", + "inherit": "Peri", + "none": "Ei mitään (piilotettu)", + "read": "Luku" + }, + "pg_label_reason": { + "no_winner": "Mikään ohjelma ei sopinut", + "below_floor": "Oppimisen varmuuden alapuolella", + "ambiguous": "Merkitty epäselväksi", + "margin": "Liian lähellä toiseksi parasta", + "unknown_profile": "Ohjelmaa ei ole enää olemassa", + "not_evaluated": "Ei arvioitu" + }, + "diagram": { + "accum": "kertynyt", + "auto": "auto", + "below_off": "alle = pois", + "confirmed": "vahvistettu", + "energy_counts": "energia ratkaisee", + "feedback": "palaute", + "gap_below_min": "tauko alle min = yksi sykli", + "heat_phase": "lämmitysvaihe", + "held_at_100": "pysyy 100 %:ssa", + "ignored": "ohitettu", + "kept": "säilytetty", + "max": "maks.", + "min": "min.", + "minus_tol": "-tol.", + "no_match": "ei osumaa", + "no_updates": "ei päivityksiä", + "off_delay_wait": "sammutusviive", + "off_gap": "sammutusväli", + "plus_tol": "+tol.", + "profile": "profiili", + "reading_interval": "tyypillinen lukemaväli", + "sensor_offline": "anturi offline: pakotettu pysäytys", + "si": "NV", + "spike_ignored": "piikki ohitettu", + "spike_ignored_short": "piikki: ohitettu", + "start": "käynnistys", + "stop": "pysäytys", + "tail_energy_resets": "loppuenergia yli kynnyksen: ajastin nollautuu", + "thr": "kynnys", + "too_short": "liian lyhyt", + "tumble_kept": "rumpupulssit säilytetty" } } diff --git a/custom_components/ha_washdata/translations/panel/fr.json b/custom_components/ha_washdata/translations/panel/fr.json index c7baa965..ef4bdc54 100644 --- a/custom_components/ha_washdata/translations/panel/fr.json +++ b/custom_components/ha_washdata/translations/panel/fr.json @@ -1,15 +1,16 @@ { "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_tip": "Vers la fin, ce programme reste silencieux environ {mins} min, puis consomme environ {watts} W pendant {secs} s. Observé sur {seen} des {measured} cycles mesurés.", "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", + "artifact_tip": "{n} anomalies détectées (par exemple, porte ouverte en cours de cycle) : ouvrez pour les voir sur le graphique", + "artifact_tip_one": "{n} anomalie détectée (par exemple, porte ouverte en cours de cycle) : ouvrez pour la voir sur le graphique", + "artifact_tip_many": "{n} anomalies détectées (par exemple, porte ouverte en cours de cycle) : ouvrez pour les voir sur le graphique", + "artifact_tip_other": "{n} anomalies détectées (par exemple, porte ouverte en cours de cycle) : ouvrez pour les voir sur le graphique", "auto": "(détecté automatiquement)", "built_in_tag": "intégré", "declining": "↘ en déclin", "energy_low": "Énergie inférieure à la normale", "energy_spike": "Énergie supérieure à la normale", - "fair_fit": "qualité acceptable", - "fair_fit_tip": "Cohérence des correspondances modérée : certains cycles attribués à ce profil ont des scores de confiance inférieurs. Étiquetez plus de cycles ou réenregistrez le profil pour améliorer la précision.", "feedback_requested": "Commentaires demandés", "golden_cycle": "Cycle de référence enregistré", "improving": "↗ amélioration", @@ -17,28 +18,41 @@ "needs_review": "Besoin d'un examen", "overrun": "A couru plus longtemps que d'habitude", "poor_fit": "qualité médiocre", - "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", + "restart_gap_tip": "{n} coupures redémarrage HA : la courbe de puissance a des trous", + "restart_gap_tip_one": "{n} coupure redémarrage HA : la courbe de puissance a un trou", + "restart_gap_tip_many": "{n} coupures redémarrage HA : la courbe de puissance a des trous", + "restart_gap_tip_other": "{n} coupures redémarrage HA : la courbe de puissance a des trous", "reviewed": "Révisé", "steady": "→ stable", "underrun": "Terminé plus vite que d'habitude", - "using_defaults": "Utilisation des valeurs par défaut expédiées", - "using_tuned": "Utiliser des poids adaptés", "overrun_ratio": "({x}x le temps prévu)", "underrun_ratio": "({pct} % du temps prévu)", - "awaiting_tip": "En attente de l'approbation de la communauté", - "awaiting_n": "En attente d'approbation · {n} confirmés", - "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.", "armed": "(s'applique au prochain cycle)", - "duration_outlier": "{n} cycle(s) de durée anormale" + "duration_outlier": "{n} cycle(s) de durée anormale", + "awaiting_tip": "En attente de l'approbation de la communauté", + "awaiting_n": "En attente d'approbation · {n} confirmés", + "awaiting": "En attente d'approbation", + "approved": "Approuvé", + "matcher_unused": "Ne correspond encore à aucun de vos cycles", + "matcher_used": "Correspond à {n} de vos cycles", + "matcher_used_one": "Correspond à {n} de vos cycles", + "matcher_used_many": "Correspond à {n} de vos cycles", + "matcher_used_other": "Correspond à {n} de vos cycles", + "matcher_used_tip": "Cycles de cet appareil que WashData a associés de lui-même à ce programme importé. Un import qui ne correspond jamais ne convient peut-être pas à votre appareil ; vous pouvez le supprimer. Compté localement ; rien n'est envoyé à la boutique communautaire." }, "btn": { + "log_done_task": "Marquer comme fait : {task}", + "add_task": "Ajouter une tâche", + "pg_preset_delete": "Supprimer le préréglage", + "pg_preset_load": "Charger le préréglage", + "pg_preset_load_tip": "Remplacer les valeurs du Bac à sable par ce préréglage (les réglages actifs ne sont pas modifiés)", + "pg_preset_save": "Enregistrer comme préréglage", + "pg_preset_save_tip": "Enregistrer toutes les valeurs ci-dessous comme préréglage nommé pour cet appareil", "reset_zoom": "Réinitialiser le zoom", "set_brand_model": "Définir la marque et le modèle", "refresh_catalog": "Rafraîchir le catalogue", @@ -49,12 +63,11 @@ "add_timer": "+ Ajouter une minuterie", "apply_all": "Appliquer tout", "apply_label": "Appliquer l'étiquette", - "apply_set_b": "Appliquer le jeu B", "apply_split": "Appliquer le fractionnement", "apply_trim": "Appliquer le rognage", "auto_detect_split": "Détection automatique", "auto_label_cycles": "Cycles d'étiquetage automatique", - "auto_label_cycles_tip": "Attribuer automatiquement des noms de profil aux cycles sans étiquette dont la confiance de correspondance dépasse le seuil", + "auto_label_cycles_tip": "Nommer les cycles sans étiquette dont la confiance de correspondance dépasse le seuil.", "cancel": "Annuler", "clear_debug": "Effacer les données de débogage", "clear_splits": "Effacer", @@ -64,8 +77,6 @@ "convert_to_automation": "Convertir en automatisation", "correct": "Corriger…", "create": "Créer", - "create_from_cluster": "Créer un profil à partir de {count} cycles", - "create_profile": "+ Créer un profil", "delete": "Supprimer", "delete_group": "Supprimer le groupe", "delete_group_tip": "Supprimer ce groupe uniquement - les profils des membres sont conservés", @@ -76,22 +87,19 @@ "done": "Fait", "edit": "Modifier", "export": "Exporter", - "export_json": "Exporter vers JSON", "force_stop": "Forcer l'arrêt", "force_stop_tip": "Terminer immédiatement le cycle en cours et le marquer comme arrêté de force", "from_template": "À partir du modèle ▾", "ignore": "Ignorer", "import_json": "Importer depuis JSON", - "import_overwrite": "Importer (écrase les données)", "inspect": "Inspecter", "label": "Étiqueter", - "load": "Charger", "load_more": "Charger plus", "manage": "Gérer", "merge": "Fusionner", "new_automation": "+ Nouvelle automatisation", "new_group": "+ Nouveau groupe", - "new_group_tip": "Regrouper les profils quasi-identiques (même forme/durée, température ou essorage différents) pour que la correspondance les distingue de façon fiable", + "new_group_tip": "Regrouper les profils quasi identiques (même forme, température ou essorage différents) pour que la correspondance puisse les distinguer.", "new_phase": "+ Nouvelle phase", "new_profile": "+ Nouveau profil", "new_profile_tip": "Créer un nouveau profil de programme à partir d'un cycle étiqueté existant ou d'un enregistrement", @@ -99,36 +107,25 @@ "on_cycle_started": "Le cycle a démarré", "pause_cycle": "Pause", "pause_cycle_tip": "Mettre en pause le cycle en cours – l'appareil reprendra là où il s'est arrêté", - "pg_apply_device": "Appliquer à l'appareil", - "pg_autofill": "Remplissage automatique", - "pg_load_calibrated": "Charger Calibré (ML) ({n})", - "pg_load_calibrated_tip": "Charger les paramètres calibrés par ML comme valeurs de remplacement dans le Bac à sable", "pg_load_live": "Charger les réglages actifs", "pg_load_live_tip": "Relire les réglages actuels de l'intégration et abandonner toutes les modifications faites dans le Bac à sable", "pg_load_suggested": "Charger les suggestions ({n})", "pg_load_suggested_tip": "Charger les suggestions actuelles du réglage automatique comme valeurs de remplacement dans le Bac à sable", - "pg_preset_delete": "Supprimer le préréglage", - "pg_preset_load": "Charger le préréglage", - "pg_preset_load_tip": "Remplacer les valeurs du Bac à sable par ce préréglage (les réglages actifs ne sont pas modifiés)", - "pg_preset_save": "Enregistrer comme préréglage", - "pg_preset_save_tip": "Enregistrer toutes les valeurs ci-dessous comme préréglage nommé pour cet appareil", "pg_publish_all": "Enregistrer {n} réglage(s) dans l'intégration", "pg_publish_one": "Enregistrer {label} dans l'intégration", - "play": "Lire", "process": "Processus", "process_history": "Traiter maintenant", "process_recording": "Enregistrer l'enregistrement", "process_tip": "Enregistrer la trace capturée en tant que profil nouveau ou existant", "rebuild": "Reconstruire les enveloppes", "rebuild_envelope": "Reconstruire l'enveloppe", - "rebuild_tip": "Recalculer l'enveloppe de puissance attendue (bande min/max) pour tous les profils à partir de leurs cycles étiquetés – à exécuter après l'étiquetage de nouveaux cycles ou la correction d'anciens", + "rebuild_tip": "Reconstruire la bande de puissance attendue de chaque profil à partir de ses cycles étiquetés. À lancer après avoir étiqueté ou corrigé des cycles.", "rec_start_tip": "Commencer l'enregistrement de la trace de puissance de l'appareil – démarrer juste avant de lancer un cycle", "rec_stop": "Arrêt", "rec_stop_tip": "Arrêter l'enregistrement et conserver la trace capturée pour révision", "record": "Commencer l'enregistrement", "refresh": "Rafraîchir", "refresh_diag_tip": "Recharger les statistiques de stockage", - "refresh_logs_tip": "Recharger les derniers enregistrements de journaux", "refresh_settings_tip": "Recharger les paramètres depuis le serveur", "relabel": "Réétiqueter ({count})", "remove": "Retirer", @@ -136,7 +133,6 @@ "reset": "Réinitialiser", "reset_ml_models_tip": "Jetez les modèles affinés et revenez aux modèles intégrés. WashData peut les réapprendre plus tard.", "reset_to_builtin": "Réinitialiser aux modèles intégrés", - "reset_to_defaults": "Réinitialiser aux valeurs par défaut", "resume_cycle": "Reprendre le cycle", "resume_cycle_tip": "Reprendre le cycle mis en pause", "revert_settings": "Annuler les modifications", @@ -149,8 +145,6 @@ "run_analysis": "🔍 Exécutez l'analyse des suggestions", "run_analysis_tip": "Analysez vos cycles enregistrés maintenant et actualisez les suggestions de réglage", "run_auto_label": "Exécuter l'étiquetage automatique", - "run_both": "Exécuter A vs B", - "run_simulation": "Exécuter la simulation", "save": "Sauvegarder", "save_access_control": "Enregistrer le contrôle d'accès", "save_group": "Enregistrer le groupe", @@ -162,10 +156,8 @@ "save_settings": "Enregistrer les paramètres", "select": "Sélectionner", "select_all": "Tout sélectionner", - "select_last20": "20 derniers", "select_none": "Aucun", "show_only": "Afficher uniquement", - "skip_setup": "Ignorer la configuration", "split": "Diviser", "stop": "Arrêter", "submit_correction": "Soumettre la correction", @@ -174,36 +166,31 @@ "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.", + "unlabel_selected_tip": "Retirer l'étiquette des cycles sélectionnés. Ils restent dans votre historique mais ne façonnent plus ce profil.", "use": "Utiliser", "wipe_all": "Effacer toutes les données", "delete_selected": "Supprimer la sélection ({n})", - "group_suggest": "Groupe {n} : {members}", "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", "import": "Importer", "disconnect": "Déconnecter", "connect_github": "Connecter GitHub", - "save_appliance": "Enregistrer l'appareil", "share_to_store": "Partager sur la boutique", "share": "Partager", - "confirm_appliance": "Confirmer cet appareil", - "confirmed": "Confirmé", "browse_community_setups": "Parcourir les configurations de la communauté", "download_device": "Télécharger cette configuration", "share_device": "⬆ Partager cet appareil", - "share_device_tip": "Partager cet appareil et ses cycles de référence enregistrés sur la boutique communautaire pour que d'autres utilisateurs du même modèle puissent les adopter", + "share_device_tip": "Partager cet appareil et ses cycles de référence sur la boutique communautaire, pour que les propriétaires du même modèle puissent les adopter.", "share_n": "Partager {n} cycle(s)", "export_selected": "Exporter (choisir les données)", "export_all": "Exporter tout rapidement", - "import_raw": "Avancé : tout remplacer depuis un JSON", "download_export": "Télécharger l'export", "analyze_import": "Analyser le fichier", "import_selected": "Importer la sélection", "back": "Retour", "mute_suggestion": "Ne plus suggérer ce paramètre", "reset_muted": "Réinitialiser les suggestions masquées", + "reset_phase_tip": "Annuler votre modification et restaurer la phase intégrée", "import_power_history": "Importer l'historique de puissance", "hist_read_recorder": "Lire depuis Home Assistant", "hist_scan": "Rechercher des cycles", @@ -212,13 +199,15 @@ "save_total_cycles": "Enregistrer le total", "clear": "Effacer", "add_brand": "Ajouter une marque", - "add_appliance": "Ajouter un appareil" + "add_appliance": "Ajouter un appareil", + "retry": "Réessayer", + "confirm_appliance": "Confirmer cet appareil", + "confirmed": "Confirmé", + "create_profile_from_gap": "Créer un profil", + "undo_import": "Annuler la dernière importation", + "pg_rerun": "Mettre à jour la simulation" }, "conflict": { - "anti_wrinkle_exit": { - "exit": "Doit être inférieur au Seuil d'arrêt ({stop} W) ; sinon la puissance de sortie anti-froissage est ignorée", - "stop": "Doit être supérieur à la Puissance de sortie anti-froissage ({exit} W)" - }, "anti_wrinkle_max": { "max": "Doit être supérieur au Seuil de démarrage ({start} W) ; sinon la limite de durée anti-froissage est contournée", "start": "Doit être inférieur à la Puissance max anti-froissage ({max} W)" @@ -226,10 +215,10 @@ "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." + "power_sensor": "Il s'agit du Capteur de puissance de l'appareil, pas d'un prix. Les cycles seraient facturés en 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. Les cycles seraient facturés au relevé du compteur par kWh ; videz ce champ.", + "device_class": "Ce capteur mesure {dc}, pas un prix par kWh. Videz ce champ ou choisissez un capteur de tarif.", + "unit": "Ce capteur mesure en {unit}, pas en prix par kWh. Videz ce champ ou choisissez 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", @@ -264,8 +253,8 @@ "threshold": "Doit être inférieur au Seuil d'arrêt ({stop} W) pour prendre effet" }, "pump_stuck": { - "duration": "Doit être inférieur au Délai d'absence de mise à jour ({to} s) afin que l'alarme de blocage se déclenche avant que le watchdog n'arrête le cycle", - "timeout": "Doit dépasser la Durée de pompe bloquée ({ps} s) afin que l'alarme se déclenche avant l'arrêt forcé du cycle" + "duration": "Doit être inférieur au Délai d'expiration sans mise à jour ({to} s) pour que l'alarme de pompe bloquée puisse se déclencher en premier", + "timeout": "Doit dépasser la Durée de blocage de la pompe ({ps} s) pour que l'alarme de pompe bloquée puisse se déclencher en premier" }, "start_dur": { "sampling": "L'Intervalle d'échantillonnage dépasse la Durée de démarrage ({sdt} s) ; des pics sur un seul échantillon peuvent ouvrir un cycle", @@ -281,10 +270,15 @@ "watchdog": { "interval": "Devrait être au moins 2x l'Intervalle d'échantillonnage ({si} s)", "sampling": "L'Intervalle d'échantillonnage devrait être au plus la moitié de l'Intervalle watchdog ({wi} s)" + }, + "end_energy": { + "energy": "Trop strict pour le Seuil d'arrêt ({w} W) sur le Délai d'arrêt ({d} s) ; le cycle ne peut se terminer que par une voie de secours", + "stop": "L'Énergie de fin ({e} Wh sur {d} s) n'autorise que {p} W" } }, "hdr": { - "ab_comparison": "Comparaison des paramètres A/B", + "custom_tasks": "Vos propres tâches", + "maintenance_supporter": "Besoin de plus ?", "access_control": "Contrôle d'accès", "add_maintenance": "Ajouter un événement d'entretien", "automation_delete": "Supprimer cette automatisation", @@ -293,28 +287,22 @@ "automation_review_this_cycle": "Revoir ce cycle", "automations": "Automatisations", "clear_debug": "Effacer les traces de débogage", - "cycle_simulator": "Simulateur de cycles", "diagnostics_pref": "Diagnostic", "display": "Afficher", - "dtw_inspector": "Inspecteur DTW", "export_import": "Exporter/Importer", - "getting_started": "Prise en main", "live_power": "Alimentation en direct", "logs": "Journaux", - "logs_diagnostics": "Diagnostic", "maintenance": "Actions d'entretien", "maintenance_log": "Journal d'entretien", "maintenance_reminders": "Rappels d'entretien", "manual_recording": "Enregistrement manuel", - "manual_recording_tip": "Effectuez un cycle intentionnellement pendant que WashData enregistre la trace de puissance. Démarrez juste avant que l'appareil démarre, Arrêtez lorsqu'il se termine, puis Traitez pour l'enregistrer comme un profil nommé.", + "manual_recording_tip": "Enregistrez un cycle volontairement : appuyez sur Début juste avant le démarrage de l'appareil et sur Arrêter quand il a fini, puis sur Processus pour l'enregistrer comme profil.", "ml_learned": "Ce que WashData a appris", - "ml_matching_tuning": "Mise au point adaptée au programme", "ml_settings_card": "Paramètres", "ml_smart_learning": "Apprentissage intelligent", "my_preferences": "Mes préférences", "no_automations": "Aucun automatisme ne fait encore référence à cet appareil.", "panel_settings": "Paramètres du panneau", - "param_sweep": "Balayage de paramètre", "pg_settings_source": "Origine des réglages", "phase_catalog": "Catalogue de phases", "playground": "Bac à sable", @@ -323,11 +311,8 @@ "status": "Statut", "status_graph": "Graphique d'état", "storage_stats": "Statistiques de stockage", - "test_history": "Tester sur l'historique", "toggle_sidebar": "Basculer la barre latérale de Home Assistant", - "tools_and_data": "Outils et données", "wipe_history": "Effacer l'historique", - "recommendations": "Recommandations ({n})", "pg_detection_params": "Paramètres de détection", "pg_outcome": "Résultat de la simulation", "pg_across_cycles": "Sur l'ensemble de vos cycles", @@ -335,7 +320,8 @@ "online_account": "Boutique communautaire et fonctionnalités en ligne", "import_power_history": "Importer l'historique de puissance", "service_status": "État de l'entretien", - "online_account_tab": "En ligne et communauté" + "online_account_tab": "En ligne et communauté", + "live_match_debug": "Débogage de la correspondance en direct" }, "health": { "fair": "Qualité de profil acceptable", @@ -343,7 +329,12 @@ "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.", + "task_name": "Tâche", + "every_cycles": "Tous les (cycles)", + "every_days": "Tous les (jours)", + "new_task_name": "Nouvelle tâche", + "days_since_service": "{since} / {total} jours", + "envelope_position_tip": "Position sur la courbe enregistrée du programme correspondant : {pct} %. Obtenue en alignant le cycle sur le profil, elle reste juste même si un cycle est plus long ou plus court. Mise à jour pendant que l'appareil est silencieux, elle peut donc être en retard.", "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})", @@ -355,10 +346,8 @@ "aria_cycle_chart": "Courbe de puissance du cycle", "aria_envelope_chart": "Enveloppe de puissance du profil", "aria_phase_chart": "Graphique de l'éditeur de phases", - "aria_playground_chart": "Courbe de puissance du cycle avec seuils de détection déplaçables", "aria_power_chart": "Graphique de puissance", "aria_spaghetti_chart": "Courbes de puissance de cycles superposées", - "aria_sweep_chart": "Graphique des résultats du balayage de paramètres", "artifact_anomaly": "Anomalie", "artifact_high_power": "Puissance élevée", "artifact_interruption": "Interruption", @@ -372,150 +361,107 @@ "cohesion_good": "cohésion {pct}%", "cohesion_low": "⚠ cohésion faible {pct}%", "compare_profiles": "Comparez avec les profils", - "concurrency": "Simultanéité", "confidence": "Confiance", "confidence_threshold": "Seuil de confiance", "correct_duration": "Durée correcte (min, facultatif)", "correct_profile": "Profil correct", - "correlation": "Corrélation", "cost": "Coût", "create_new_profile": "+ Créer un nouveau profil…", "current_phase": "Phase actuelle", - "curves": "Courbes", "cycle": "Cycle", "cycle_date_display": "Affichage de la date du cycle", - "cycle_health": "Santé cycliste", - "cycle_index": "index du cycle →", - "cycles": "Cycles", + "cycle_context": "Historique du recorder autour du cycle", + "cycle_context_off": "Désactivé", + "cycle_context_minutes": "{min} min", + "cycle_context_series": "Historique du recorder", + "before_start": "Avant le début", + "after_end": "Après la fin", "cycles_count": "Cycles", "cycles_lc": "cycles", - "dashed": "pointillé", "date": "Date", "debug_traces": "Traces de débogage", - "default": "Défaut", "default_access_level": "Niveau par défaut pour les utilisateurs non répertoriés ci-dessous", "default_other": "Par défaut (autres appareils)", "default_tab": "Onglet par défaut lors de l'ouverture du panneau", - "delta": "Δ", "description": "Description", - "dtw_blend_w": "Poids de mélange", - "dtw_blended": "Score combiné", - "dtw_ddtw": "Score DDTW", - "dtw_l1": "Score L1", - "dtw_matched_profile": "(profil correspondant du cycle)", "duration": "Durée", - "duration_agreement": "Concordance de durée", - "emphasis": "Accent", "enable_access_control": "Activer le contrôle d'accès par utilisateur", "energy": "Énergie", - "energy_agreement": "Concordance d'énergie", - "event_timeline": "Chronologie des événements", "event_type": "Type d'événement", "expected": "Attendu", + "suspected_profile": "{name} (probable)", + "runner_up_profile": "{name} (deuxième candidat)", "expected_duration": "Durée prévue (min)", "file_kb": "Fichier (ko)", - "final_score": "Score final", "from": "De", "gap_s": "Écart(s)", "group_name": "Nom du groupe", "head_trim": "Rognage de début (s)", - "health": "Santé", "hide_tabs": "Masquer les onglets pour les non-administrateurs", - "in_use": "Utilisé", "json_data": "Données JSON", "label": "Programme associé", "last": "Derniers", "last_checked": "Dernière vérification :", "load_from_file": "Charger à partir d'un fichier", - "mae_score": "Score MAE", "manual_duration": "Durée manuelle (min, facultatif)", "match": "Correspondance", "members": "Membres", - "metric": "Métrique", "mode_existing_profile": "Ajouter au profil existant", "mode_new_profile": "Créer un nouveau profil", "models_fine_tuned": "(modèles affinés : {count})", - "n_classic_suggestions": "{n} classique", - "n_ml_suggestions": "{n} ML", "n_selected": "{n} sélectionné(s)", - "n_tuning_suggestions": "{n} suggestion de réglage", - "na": "n/d", + "n_tuning_suggestions": "{n} suggestions de réglage", + "n_tuning_suggestions_one": "{n} suggestion de réglage", + "n_tuning_suggestions_many": "{n} suggestions de réglage", + "n_tuning_suggestions_other": "{n} suggestions de réglage", "new_profile_name": "Nouveau nom de profil", "notes": "Notes", - "of": "sur", - "outcome": "Résultat", "panel_default_tab": "Onglet par défaut", "panel_language": "Langue du panneau", "per_cycle_short": "cycle", "pg_anti_wrinkle": "Anti-froissement", - "pg_autofill_tip": "Remplir automatiquement la plage à partir de la valeur actuelle ({lo}–{hi})", "pg_best_value": "Meilleure valeur trouvée", "pg_current_value": "Valeur actuelle :", "pg_drag_hint": "Faites glisser la ligne sur le graphique", - "pg_dtw": "DTW", "pg_ev_ending": "Se termine", "pg_ev_running": "En cours", "pg_group_advanced": "Cas particuliers", "pg_group_detection": "Déclencheurs de détection", "pg_group_timing": "Règles de temporisation", "pg_idle": "Inactif", - "pg_last_run": "Dernière exécution : {date}", - "pg_match_score": "Score de correspondance", - "pg_pct_of": "{p}% sur {total}", "pg_poor_match": "Correspondance médiocre", "pg_preset": "Préréglage du Bac à sable", "pg_preset_name": "Nom du nouveau préréglage", "pg_preset_none": "Sélectionner un préréglage…", "pg_strong_match": "Correspondance forte", - "pg_tile_ambiguous": "Ambigu", - "pg_tile_ambiguous_tip": "Cycles avec une quasi-égalité entre deux programmes", - "pg_tile_detected": "Détecté", - "pg_tile_detected_tip": "Cycles où la puissance a dépassé le seuil de démarrage assez longtemps pour être enregistrée comme en cours", - "pg_tile_matched": "Reconnu", - "pg_tile_matched_tip": "Cycles correctement attribués à un programme connu", - "pg_time_left": "Temps restant", "pg_weak_match": "Correspondance faible", "phase_name": "Nom de la phase", "name": "Nom", "phase_timeline": "Chronologie des phases", "power": "Puissance", "profile": "Profil", - "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. Sélectionnez-en un avant de démarrer l'appareil et il sera appliqué dès le début du prochain cycle.", + "program_tip": "Choisissez à quel programme le cycle en cours est associé. La détection automatique choisit la meilleure correspondance. Épinglez un programme quand la détection automatique se trompe ; choisi avant le démarrage, il s'applique au cycle suivant.", "progress": "Progrès", "raw_socket": "Signal brut", - "read_only": "Lecture seule", "ref_cycle": "Cycle de référence (facultatif)", - "refresh_interval": "Intervalle(s) d'actualisation", "remaining": "Restant", "reminder_every": "Rappeler tous les (cycles)", "remove_label": "- Supprimer l'étiquette -", "rename_profile": "Renommer le profil", - "replay_duration": "Relecture", "resulting_profile": "Profil résultant", "save_mode": "Mode Enregistrer", - "score": "Score", - "score_breakdown": "Détail du score", "seconds_from_start": "Secondes depuis le début", "select_profile": "Sélectionnez un profil", "setting": "Paramètre", - "settings_a": "Paramètres A (actuels)", - "settings_a_short": "A", "settings_advanced": "Avancé", - "settings_b": "Paramètres B (test)", - "settings_b_short": "B", "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 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", - "stage2": "Étape 2 – similarité principale", - "stage3": "Étape 3 – DTW", - "stage4": "Étape 4 – concordance", "status": "Statut", "tail_trim": "Rognage de fin (s)", "tap_to_dismiss": "appuyez sur le graphique pour fermer", @@ -526,7 +472,6 @@ "ungrouped": "Non groupé", "unlabelled": "Sans étiquette", "unlabelled_paren": "(sans étiquette)", - "warp_path": "Chemin de déformation", "input": "Entrée :", "start": "Début", "end": "Fin", @@ -551,9 +496,8 @@ "n_shown": ", {n} affichés", "cycles_title": "Cycles ({n})", "pg_detecting": "Détection", - "pg_best_marker": "Meilleur : {v}", "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", + "pg_time_left_model_tip": "Estimé d'après les phases du programme, pas un compte à rebours fixe", "phase": "Phase", "pg_mode_history": "Tester sur l'historique", "pg_ev_detected": "Détecté", @@ -576,9 +520,6 @@ "pg_alert_unmatched": "Sans correspondance", "pg_alert_ambiguous": "Correspondance ambiguë", "pg_alert_energy": "Anomalie d'énergie", - "pg_alert_stress_cap": "Limite de sécurité atteinte", - "pg_alert_stress_ok": "Terminaison au repos : cycle arrêté", - "pg_alert_stress_warn": "Consommation en veille supérieure au seuil d'arrêt", "pg_newly_correct": "désormais correct", "pg_regressed": "régressé", "pg_end_timing_changed": "moment de fin modifié", @@ -588,12 +529,6 @@ "pg_sweep_param": "Paramètre", "pg_objective": "Objectif", "pg_steps": "Étapes", - "pg_stress_group": "Test de terminaison au repos", - "pg_stress_idle_auto": "Auto", - "pg_stress_idle_w": "Niveau de veille (W)", - "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_obj_match": "Précision de correspondance", "pg_obj_endtiming": "Précision du moment de fin", "pg_obj_falseend": "Taux de fausses fins", @@ -602,7 +537,6 @@ "pg_mode_optimize": "Optimiser", "pg_group_matching": "Correspondance de programme", "pg_zoom_hint": "double-cliquez pour réinitialiser le zoom", - "aria_playground_chart2": "Graphique interactif de puissance du cycle : survolez pour lire le temps/la puissance, faites défiler pour zoomer, faites glisser pour vous déplacer", "pg_alert_timeout_end": "Terminé par délai d'attente, pas par prédiction", "pg_alert_indefinite": "Fonctionnerait indéfiniment", "task_pg_history": "Tester sur l'historique", @@ -614,16 +548,9 @@ "pg_recent_runs": "Exécutions récentes", "pg_load_run": "Recharger cette exécution", "pg_match_confidence": "Confiance de correspondance", - "pg_envelope_fit": "Conformité à l'enveloppe", - "pg_envelope_fit_tip": "Dans quelle mesure le cycle reste à l'intérieur de l'enveloppe de puissance enregistrée de ce profil, un autre angle que la confiance de correspondance.", "n_imported_note": ", {n} importés", "enable_online": "Activer les fonctionnalités en ligne", - "store_brand": "Marque de l'appareil", - "store_model": "Modèle de l'appareil", "anonymous": "Anonyme", - "rate_quality": "Évaluer la qualité", - "rate_n": "{n} étoiles", - "approved_only": "Approuvés uniquement", "adopt_settings": "Adopter aussi les paramètres", "flags": "Indicateurs", "include_phase_map": "Inclure la carte des phases", @@ -640,7 +567,6 @@ "cat_custom_phases": "Phases personnalisées", "cat_profile_groups": "Groupes de profils", "cat_settings": "Réglages de détection & de reconnaissance", - "cat_matching_config": "Réglage de la reconnaissance", "cat_ml_models": "Modèles ML", "cat_feedback": "Retours & étiquettes de révision", "cat_suggestions": "Suggestions", @@ -662,7 +588,6 @@ "hist_csv_data": "Données CSV", "hist_from_recorder": "Ou le lire depuis Home Assistant", "hist_since": "Depuis le", - "days": "jours", "hist_keep": "Conserver ce cycle", "hist_looks_complete": "complet", "peak_power_short": "Crête", @@ -682,7 +607,36 @@ "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" + "correct_total_cycles": "Corriger le total", + "hist_reason_already_recorded": "chevauche un cycle déjà enregistré par WashData", + "share_consent": "Je confirme que ces cycles se sont terminés normalement, sans interruption", + "show_internal": "Afficher les paramètres internes", + "show_internal_tip": "Réglages que WashData ajuste pour vous (échantillonnage, chien de garde, cadence de correspondance, seuils). Affichés quand cet appareil a sa propre valeur, un conflit ou une suggestion.", + "rate_n": "{n} étoiles", + "rate_quality": "Évaluer la qualité", + "pg_autolabel": "Étiquetage automatique", + "pg_label_review": "Demande une révision", + "pg_ev_match_reverted": "Correspondance annulée", + "pg_ev_verified_pause": "Pause vérifiée", + "n_samples": "{n} échantillons", + "n_samples_one": "{n} échantillon", + "n_samples_many": "{n} échantillons", + "n_samples_other": "{n} échantillons", + "n_cycles": "{n} cycles", + "n_cycles_one": "{n} cycle", + "n_cycles_many": "{n} cycles", + "n_cycles_other": "{n} cycles", + "energy_per_cycle": "{v}/cycle", + "energy_total": "{v} au total", + "automation_off": "(désactivée)", + "ref_cycle_none": "Aucun", + "share_name": "Afficher mon nom sur ce que je partage", + "cat_backfill_cycles": "Cycles trouvés dans un historique de puissance importé", + "pg_compare_profile": "Comparer avec un profil", + "pg_obj_early": "Taux de fins anticipées", + "pg_obj_endlag": "Retard de fin (médiane)", + "pg_obj_split": "Taux de divisions", + "pg_sweep_cycles": "Cycles récents" }, "log": { "all_levels": "Tous niveaux", @@ -692,11 +646,9 @@ }, "ml": { "builtin_models": "● Utilisation de modèles intégrés", - "fit_fair": "Acceptable", "fit_good": "Bien", "fit_slight": "Léger", "fit_strong": "Fort", - "fit_weak": "Faible", "fit_word": "adéquation", "personalized": "● Personnalisé pour cette machine", "better_than_baseline": "{pct} % mieux que l'estimation de référence ({metric})", @@ -704,30 +656,41 @@ "trend_declining_tip": "L'ajustement de ce modèle s'est dégradé au fil des dernières vérifications : examiner plus de cycles peut l'aider à réapprendre.", "trend_steady_tip": "L'ajustement de ce modèle est resté à peu près stable au fil des dernières vérifications.", "cap_blurb": { - "end": "Savoir quand un cycle est réellement terminé", - "live_match": "Identifier plus tôt le programme en cours", - "quality": "Repérer les cycles mal détectés ou corrompus", - "remaining_time": "Prédire le temps restant", "total_energy": "Prédire l'énergie totale et le coût" }, "cap_label": { - "end": "Détection de fin de cycle", - "live_match": "Reconnaissance du programme", - "quality": "Contrôle qualité du cycle", - "remaining_time": "Estimation du temps restant", "total_energy": "Estimation de l'énergie" }, - "metric_auc": "AUC {auc} sur les données de validation", - "metric_mae": "erreur {model} vs référence {baseline}" + "metric_mae": "erreur {model} vs référence {baseline}", + "fine_tuned_at": "affiné le {when}", + "held_out_count": "mesuré sur {count} cycles de validation", + "held_out_count_one": "mesuré sur {count} cycle de validation", + "held_out_count_many": "mesuré sur {count} cycles de validation", + "held_out_count_other": "mesuré sur {count} cycles de validation", + "last_run_not_promoted": "Dernière vérification ({when}) : {reason}", + "never": "jamais", + "skip_reason": { + "fit_failed": "L'entraînement a échoué : {error}", + "holdout_too_small": "Trop peu de cycles pour tester équitablement un nouveau modèle : {held_out} ont pu être mis de côté pour le test, il en faut {min}.", + "insufficient_rows": "Pas encore assez de cycles exploitables : {count} cycles étiquetés et propres jusqu'à présent.", + "insufficient_rows_one": "Pas encore assez de cycles exploitables : {count} cycle étiqueté et propre jusqu'à présent.", + "insufficient_rows_many": "Pas encore assez de cycles exploitables : {count} cycles étiquetés et propres jusqu'à présent.", + "insufficient_rows_other": "Pas encore assez de cycles exploitables : {count} cycles étiquetés et propres jusqu'à présent.", + "not_better_than_incumbent": "Le nouveau modèle n'était pas meilleur que celui en service (erreur {model} vs {incumbent}), le modèle actuel a donc été conservé.", + "not_better_than_naive": "Le nouveau modèle n'était pas nettement meilleur que l'estimation simple (erreur {model} vs {naive}).", + "unknown": "Rien de nouveau n'a été appris." + }, + "trained_unknown": "à une date inconnue" }, "modal": { + "remove_task_title": "Retirer la tâche", + "remove_task_msg": "Retirer le rappel « {task} » ? Ses entrées dans le journal d'entretien sont conservées.", "auto_label": "Cycles d'étiquetage automatique", "correct_feedback": "Commentaires corrects", "create_profile": "Créer un profil", "cycle": "Cycle", "edit_group": "Modifier le groupe de profils", "edit_phase": "Phase d'édition", - "import_config": "Importer la configuration", "label_cycle": "Cycle d'étiquetage", "merge_cycles": "Fusionner {n} cycles", "new_group": "Nouveau groupe de profils", @@ -742,6 +705,8 @@ "delete_automation_title": "Supprimer l'automatisation", "delete_automation_msg": "Supprimer l'automatisation « {name} » de Home Assistant ? Cette action est irréversible.", "remove_legacy_title": "Supprimer les actions héritées", + "reset_phase_msg": "Annuler votre modification de « {name} » et restaurer la phase intégrée ? Les programmes conservent leurs plages.", + "reset_phase_title": "Réinitialiser la phase", "remove_legacy_msg": "Supprimer les actions personnalisées héritées ? Elles cesseront de se déclencher lors des événements de cycle. Cette action est irréversible depuis le panneau.", "discard_recording_title": "Supprimer l'enregistrement", "discard_recording_msg": "Supprimer l'enregistrement sauvegardé ? Cette action est irréversible.", @@ -762,63 +727,79 @@ "store_share_device": "Partager cet appareil", "export_select": "Exporter - choisir les données", "import_wizard": "Importer - choisir les données", - "history_import": "Importer l'historique de puissance" + "history_import": "Importer l'historique de puissance", + "apply_suggestions_title": "Appliquer les suggestions", + "apply_suggestions_msg": "Modifier ces {n} paramètres ? L'intégration se recharge après l'enregistrement.", + "discard_settings_title": "Abandonner les modifications non enregistrées ?", + "discard_settings_msg": "Modifications de paramètres non enregistrées : {n}. Quitter Paramètres et les abandonner ?", + "profile_title": "Profil · {name}", + "undo_import_msg": "Les données et réglages de cet appareil reviennent à leur état d'avant la dernière importation ({when}). Les cycles et modifications depuis sont perdus.", + "undo_import_ok": "Annuler l'importation", + "undo_import_title": "Annuler la dernière importation" }, "msg": { + "custom_task_limit": "Vous avez atteint la limite de {max} tâches.", + "custom_tasks_intro": "Ajoutez tout ce dont vous souhaitez qu'on vous rappelle. Une tâche est à effectuer dès que l'un des deux intervalles est atteint ; laissez-en un vide ou à 0 pour n'utiliser que l'autre.", + "maintenance_due_sensor_hint": "Le capteur binaire Entretien à effectuer est activé tant qu'une tâche est due, pour vos propres automatisations de notification.", + "maintenance_supporter_intro": "Maintenance Supporter est une intégration distincte qui en fait bien plus : n'importe quel appareil ou tâche, des échéances selon le temps, l'utilisation ou un capteur, des notifications et un historique. Elle peut utiliser le compteur de cycles de WashData.", "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.", + "pg_preset_delete_confirm": "Supprimer le préréglage « {name} » ?", + "pg_preset_limit": "Limite de préréglages atteinte ({n})", + "pg_preset_overwrite": "Écraser le préréglage « {name} » ?", "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.", - "refresh_catalog_hint": "Les listes de marques et d'appareils de la communauté sont mises en cache afin de maintenir la boutique partagée dans son quota quotidien. Rafraîchissez pour récupérer les entrées ajoutées ou approuvées par d'autres utilisateurs.", + "store_sibling_hint": "Rien de partagé pour votre modèle ? Un modèle proche de la même marque est généralement un bon début.", + "store_declare_appliance": "Indiquez à WashData quel appareil vous possédez pour voir les configurations que d'autres ont partagées pour lui. Ou saisissez une marque ci-dessus pour explorer.", + "refresh_catalog_hint": "Les listes de marques et d'appareils de la communauté sont mises en cache pour économiser le quota quotidien de la boutique. Actualisez pour voir les entrées que d'autres ont ajoutées ou approuvées.", "head_trim_hint": "Nombre de secondes à supprimer au début", "appliance_monitor": "Moniteur d'appareils", "artifact_dip_detail": "Est tombé en dessous de la bande de puissance habituelle pendant environ {n} s.", "artifact_footer": "Mis en évidence sur le graphique ci-dessus. Il s’agit d’artefacts transitoires (par exemple la porte ouverte en cours de cycle), pas nécessairement de problèmes.", "artifact_header": "{n} anomalies détectées au cours de ce cycle", - "artifact_pause_detail": "La puissance a chuté près de zéro pendant environ {n} s puis a repris – la porte a probablement été ouverte en cours de cycle ou le cycle a été mis en pause.", + "artifact_header_one": "{n} anomalie détectée au cours de ce cycle", + "artifact_header_many": "{n} anomalies détectées au cours de ce cycle", + "artifact_header_other": "{n} anomalies détectées au cours de ce cycle", + "artifact_pause_detail": "La puissance est tombée presque à zéro pendant ~{n}s, puis est revenue. La porte a probablement été ouverte ou le cycle mis en pause.", "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, puis retirez-leur l'étiquette (conservés dans l'historique, mais n'influençant plus ce profil) ou supprimez-les définitivement.", + "cleanup_intro": "Tous les cycles étiquetés superposés. Cochez les valeurs aberrantes, puis retirez-leur l'étiquette (conservés dans l'historique, mais ne façonnant plus ce profil) ou supprimez-les.", "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)", - "compare_profiles_tip": "Superposez d'autres enveloppes de profil sur le graphique ci-dessus pour voir laquelle correspond le mieux à ce cycle.", + "compare_profiles_tip": "Superposez d'autres profils sur le graphique pour voir lequel correspond le mieux à ce cycle.", "compare_selected_cycles": "Cycles sélectionnés (solides) – afficher/masquer", - "consider_new_profile": "Pensez à créer un nouveau profil.", - "coverage_gap": "les cycles récents n'ont pas de profil correspondant ({pct} % des 30 derniers).", - "coverage_gap_similar_cycles": "{count} cycles similaires non étiquetés trouvés – créez un profil pour commencer à les identifier.", "cycles_deleted": "{count} cycle(s) supprimé(s)", + "cycle_context_loading": "Chargement de l'historique du recorder…", + "cycle_context_note": "Gris : l'historique du recorder du capteur de puissance autour de ce cycle. Il ne fait pas partie du cycle et n'affecte ni la durée, ni l'énergie, ni la correspondance.", + "cycle_context_none": "Le recorder ne contient aucun relevé autour de ce cycle (Home Assistant conserve 10 jours d'historique par défaut).", + "cycle_context_tip": "Affichage uniquement : montre l'historique du recorder du capteur de puissance autour de ce cycle, s'il est encore conservé. Pour ajouter les tentatives de démarrage échouées aux cycles futurs, utilisez Amorce de courbe (Paramètres > Détection > Début de cycle), qui modifie les durées enregistrées.", "enough_data": "Suffisamment de données pour apprendre ({current}/{min} cycles).", "export_description": "Choisissez précisément quels profils, cycles, réglages et autres exporter vers un JSON, ou analysez un fichier pour n'importer que les éléments souhaités.", "feedback_cycles_pending": "À réviser : {n}", "feedback_prompt": "Confirmez que c'était correct, corrigez le programme ou ignorez-le.", "feedback_relabel_hint": "Réétiqueter ce cycle le résout également.", "filter_by_profile": "Filtrer par profil…", - "group_modal_help": "Programmes groupés de même forme qui diffèrent en température/essorage (les durées peuvent varier). L'appariement note le groupe comme un seul candidat, puis sélectionne le membre le mieux adapté. Choisissez-en au moins 2 ; la superposition montre à quel point ils se ressemblent.", + "group_modal_help": "Regroupez les programmes de même forme qui diffèrent par la température ou l'essorage. La correspondance note le groupe comme un seul, puis choisit le membre qui correspond le mieux. Choisissez-en au moins 2 ; la superposition montre à quel point ils se ressemblent.", "group_not_cohesive": "Ces profils ne sont pas suffisamment similaires pour être regroupés de manière fiable. La correspondance les traite donc individuellement jusqu'à ce que vous supprimiez la valeur aberrante ou divisiez le groupe.", "group_preview_hint": "Cochez 2 membres ou plus pour prévisualiser et comparer leurs courbes de puissance.", - "import_intro": "Chargez un fichier exporté ou collez une charge utile JSON ci-dessous.", - "legacy_actions_title": "{count} action personnalisée héritée toujours en cours d'exécution", + "legacy_actions_title": "{count} actions personnalisées héritées toujours en cours d'exécution", + "legacy_actions_title_one": "{count} action personnalisée héritée toujours en cours d'exécution", + "legacy_actions_title_many": "{count} actions personnalisées héritées toujours en cours d'exécution", + "legacy_actions_title_other": "{count} actions personnalisées héritées toujours en cours d'exécution", "live_chart_loading": "Le tableau de puissance en direct apparaît à mesure que les lectures arrivent.", "loading": "Chargement…", "loading_curve": "Courbe de chargement…", "loading_settings": "Chargement des paramètres…", "log_buffer_hint": "Le plus récent en premier · Met en mémoire tampon les 500 derniers enregistrements ha_washdata depuis le redémarrage · Faites glisser le bord inférieur pour redimensionner.", - "maintenance_advisory": "Une durée/énergie croissante peut indiquer un entretien de l'appareil nécessaire (par exemple, détartrage, nettoyage du filtre).", "maintenance_due": "Entretien à effectuer : {items}", - "maintenance_intro": "Consignez l'entretien que vous effectuez sur cet appareil et soyez rappelé lorsque chaque tâche est à nouveau à effectuer.", + "maintenance_intro": "Consignez l'entretien de cet appareil et recevez un rappel quand chaque tâche revient.", "maintenance_load_error": "Impossible de charger les données d'entretien : {error}", "maintenance_requires_access": "La maintenance et l'exportation/importation nécessitent un accès complet.", - "manual_duration_hint": "Durée de cycle moyenne/attendue du profil, utilisée pour les estimations du temps restant. Modifiez pour le définir ; le laisser inchangé conserve la valeur actuelle.", - "matching_tuning_intro": "Lors de l'apprentissage, WashData ajuste également la mesure dans laquelle la correspondance du programme pèse sur la forme par rapport à la durée et à l'énergie.", + "manual_duration_hint": "Durée de cycle attendue du profil, utilisée pour estimer le temps restant. Laissez-la inchangée pour garder la valeur actuelle.", "merge_intro": "Les cycles sélectionnés sont regroupés en un seul (ordre chronologique ; espaces remplis avec 0 W). Choisissez le profil résultant.", "ml_intro": "WashData est livré avec des modèles intelligents qui fonctionnent immédiatement.", "ml_learned_intro": "Modèles adaptés à cette machine.", - "ml_loading": "chargement de ML…", "ml_settings_intro": "Deux commutateurs indépendants : l'un applique les modèles pendant l'exécution d'un cycle, l'autre permet à WashData de les ajuster avec précision à votre machine au fil du temps.", - "name_first_program": "Vous avez assez de cycles – nommez votre premier programme pour lancer la correspondance.", - "near_duplicate_cluster": "Cluster de profils quasi-dupliqué détecté. Le regroupement permet de choisir de manière fiable entre des sosies (par exemple, le même programme à différentes températures/essorages).", "no_cycles_match": "Aucun cycle ne correspond au filtre actuel.", "no_cycles_profile": "Aucun cycle pour ce profil.", "no_cycles_selected": "Sélectionnez d'abord au moins un cycle.", @@ -843,73 +824,54 @@ "no_suggestions": "Aucune suggestion active.", "notify_services_hint": "Utilisez les IDs de service {entity} (séparés par des virgules pour plusieurs). Variables de gabarit : {vars}.", "old_actions_warning": "Configuré avec l'ancien éditeur d'actions (maintenant supprimé). Ils se déclenchent toujours sur les événements du cycle mais ne peuvent plus être modifiés ici. Convertissez-les en une automatisation normale ou supprimez-les.", - "onboarding_progress": "{n} / 3 cycles observés", - "onboarding_watching": "Utilisez votre appareil normalement – WashData observe. Après 3 cycles, la correspondance des programmes commencera.", "pending_feedback": "En attente de retour de détection", "performance_trend": "Tendance des performances ({n} cycles)", - "pg_analysis_empty": "Chargez un cycle pour voir l'analyse de correspondance.", "pg_apply_confirm": "Appliquer la meilleure valeur : {label} = {value} ?", - "pg_canvas_empty": "Sélectionnez un cycle ci-dessus, puis appuyez sur Charger pour voir sa courbe de puissance.", - "pg_chart_caption": "Taux de détection/correspondance selon les valeurs de {param}", "pg_ctrl_intro": "Les valeurs partent des réglages actifs de cet appareil dans l'intégration. Les modifications restent dans le Bac à sable tant que vous ne les enregistrez pas dans l'intégration.", "pg_live_unavailable": "Réglages actifs indisponibles, affichage des valeurs par défaut.", "pg_load_live_confirm": "Abandonner {n} modification(s) du Bac à sable et recharger les réglages actuels de l'intégration ?", - "pg_match_rate": "{pct}% de taux de correspondance sur {total} cycles", "pg_matches_live": "Identique aux réglages actifs", "pg_n_changed": "{n} valeur(s) modifiée(s) par rapport aux réglages actifs", "pg_sugg_none": "Toutes les suggestions correspondent déjà aux réglages actuels", - "pg_preset_delete_confirm": "Supprimer le préréglage « {name} » ?", - "pg_preset_limit": "Limite de préréglages atteinte ({n})", - "pg_preset_overwrite": "Écraser le préréglage « {name} » ?", "pg_publish_one_confirm": "Enregistrer {label} = {value} dans les réglages de cet appareil ?", "pg_restart_note": "Redémarrez Home Assistant pour activer les outils de simulation.", - "pg_sim_progress": "{done} / {total} cycles", - "pg_starting": "Démarrage…", - "pg_stress_synthetic": "la simulation de repos commence ici", - "pg_sweep_intro": "Et si {param} était différent ? Testez {steps} valeurs sur vos {cycles} derniers cycles pour trouver le réglage où le plus de cycles sont correctement reconnus.", - "pg_sweep_step": "Étape {done} / {total}", - "pg_verdict_bad": "Nécessite une attention: de nombreux cycles ne sont pas détectés.", - "pg_verdict_good": "Bien réglé: la plupart des cycles sont correctement identifiés et reconnus.", - "pg_verdict_ok": "Acceptable: certains cycles manqués. Essayez d'abaisser le seuil de démarrage.", "phase_catalog_intro": "Segments nommés d'un cycle (Prélavage, Chauffage, Essorage…). Attribuez-les à un profil depuis son panneau de contrôle.", "phase_ranges_intro": "Plages de phases (minutes à partir du début du cycle) superposées sur la courbe moyenne. Modifiez les valeurs pour prévisualiser en direct.", - "playground_intro": "Un bac à sable sûr pour rejouer les cycles passés avec différents paramètres, comparer deux configurations côte à côte et examiner comment un cycle s'aligne sur un profil. Rien ici ne modifie vos données en direct tant que vous ne l'appliquez pas explicitement.", - "playground_needs_restart": "Redémarrez Home Assistant pour activer les outils du Bac à sable.", - "preferences_admin": "Préférences, panneau et contrôle d'accès", - "preferences_adv": "Préférences", + "playground_intro": "Essayez des réglages sur les données réelles de vos cycles. Rien ici ne modifie vos réglages actifs tant que vous ne l'appliquez pas.", "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), 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.", + "process_history_hint": "Relance la correspondance sur tous les cycles enregistrés, actualise les suggestions, réentraîne les modèles ML (si activés), recalcule le coût à partir des prix enregistrés et recalcule la santé des cycles. À lancer après une série de vérifications.", "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.", - "rbac_hint": "Lorsqu'il est désactivé, chaque utilisateur de Home Assistant dispose d'un accès complet (valeur par défaut). Les administrateurs ont toujours un accès complet et peuvent gérer tout le monde.", - "recent_logs": "Enregistrements ha_washdata récents", + "rbac_hint": "Désactivé : chaque utilisateur de Home Assistant a un accès complet (par défaut). Les administrateurs ont toujours un accès complet et peuvent gérer tout le monde.", "recording_in_progress": "Enregistrement en cours", "reminders_intro": "Affiche un rappel dans le panneau autant de cycles après le dernier entretien. Laissez vide ou 0 pour désactiver un rappel.", "restart_gap_footer": "Mis en évidence sur le graphique. Les données de puissance sont manquantes pour ces intervalles ; la correspondance n'a utilisé que les relevés réels.", - "restart_gap_header": "{n} coupure redémarrage HA pendant ce cycle", + "restart_gap_header": "{n} coupures redémarrage HA pendant ce cycle", + "restart_gap_header_one": "{n} coupure redémarrage HA pendant ce cycle", + "restart_gap_header_many": "{n} coupures redémarrage HA pendant ce cycle", + "restart_gap_header_other": "{n} coupures redémarrage HA pendant ce cycle", "restart_gap_item": "{dur} coupure", - "review_confirm_help": "Confirmez si ce cycle a été détecté correctement. Vos avis entraînent le modèle sur votre machine : plus vous confirmez de cycles, meilleure est la correspondance et le score de santé. Un rapide Bon/Mauvais suffit.", + "review_confirm_help": "Ce cycle a-t-il été bien détecté ? Confirmer des cycles améliore la correspondance pour votre machine. Un rapide Bien/Mauvais suffit.", "review_in_settings": "Vérifier dans les paramètres", "review_notes_placeholder": "Remarques (facultatif)", "review_notes_tip": "Notes en texte libre pour votre propre référence. Non utilisé par jumelage ou formation.", - "review_profile_tip": "Le programme sous lequel ce cycle est étiqueté. Si le programme détecté automatiquement était erroné, corrigez-le ici - l'étiquetage enseigne la correspondance pour les cycles futurs.", - "review_quality_tip": "À quel point ce cycle est propre. Bon = un exemple classique de ce programme ; Mauvais = détecté mais bruyant ou atypique ; Inutilisable = mal détecté (fusionné, tronqué ou faux). Détermine le score de santé et les cycles autorisés pour entraîner le modèle.", - "review_recorded_tip": "Marquez-le comme un cycle de référence trié sur le volet pour son programme - le même rôle qu'un cycle enregistré manuellement. Les cycles de référence sont toujours conservés, amorcent le modèle correspondant et ne sont jamais supprimés lors du nettoyage. (C'est le drapeau « doré »/enregistré ; les deux sont la même chose.)", - "review_tags_tip": "Indicateurs facultatifs décrivant ce qui n'a pas fonctionné avec ce cycle, afin que la formation et le nettoyage puissent en tenir compte.", + "review_profile_tip": "Le programme sous lequel ce cycle est étiqueté. S'il est faux, corrigez-le ici ; les étiquettes entraînent la correspondance.", + "review_quality_tip": "La propreté de ce cycle. Bien : un exemple type de ce programme. Mauvais : détecté, mais bruité ou inhabituel. Inutilisable : mal détecté (fusionné, tronqué ou parasite). Influe sur le score de santé et sur les cycles qui entraînent le modèle.", + "review_recorded_tip": "Marquez-le comme cycle de référence choisi à la main pour son programme, comme un cycle enregistré manuellement. Les cycles de référence sont toujours conservés, servent de base au modèle de correspondance et ne sont jamais supprimés par le nettoyage.", + "review_tags_tip": "Indicateurs facultatifs sur ce qui s'est mal passé, pour que l'entraînement et le nettoyage en tiennent compte.", "review_to_cycles": "Ouvrir la file d'attente de révision des cycles", "samples_decimated": "Affichage de {shown} sur {total} échantillons (réduits pour l'affichage ; pics conservés). Un grand écart ici est dû à la réduction, pas à des données manquantes.", - "saving_triggers_reload": "L'enregistrement déclenche un rechargement de l'intégration. Les entités HA peuvent apparaître brièvement comme indisponibles.", + "saving_triggers_reload": "L'enregistrement recharge l'intégration. Les entités peuvent apparaître brièvement comme indisponibles.", "search_placeholder": "Paramètres de recherche…", "see_recorder": "Voir le widget enregistreur ci-dessous", "setting_changed": "Modifié de {old} à {new} le {date}", "settings_basic_note": "Affichage des paramètres essentiels. Passez à Avancé pour la liste complète.", - "shape_drift_advisory": "⚠ Dérive de forme", - "shape_drift_detail": "Le profil de puissance a évolué au fil du temps – usure possible de l'appareil ou entretien nécessaire (p. ex. détartrage, nettoyage du filtre).", "show_all_settings": "Afficher tous les paramètres", - "showing_suggestions": "Affichage du paramètre {count} avec des suggestions.", - "split_intro": "Cliquez sur le graphique pour ajouter ou supprimer un point de partage, ou détecter automatiquement les espaces inactifs. Chaque segment résultant peut avoir son propre profil.", - "storage_diagnostics": "Statistiques de stockage, maintenance, export/import", + "showing_suggestions": "Affichage de {count} paramètres avec des suggestions.", + "showing_suggestions_one": "Affichage de {count} paramètre avec des suggestions.", + "showing_suggestions_many": "Affichage de {count} paramètres avec des suggestions.", + "showing_suggestions_other": "Affichage de {count} paramètres avec des suggestions.", + "split_intro": "Cliquez sur le graphique pour ajouter ou retirer un point de découpe, ou détectez automatiquement par les pauses inactives. Chaque partie peut avoir son propre profil.", "sug_staged": "{key} = {val} défini. Enregistrez pour appliquer.", "toast_auto_detect_enabled": "Détection automatique activée", "toast_auto_label_complete": "Étiquetage automatique terminé", @@ -926,7 +888,6 @@ "toast_no_automation": "Impossible de créer une automatisation ici", "toast_profile_name_required": "Le nom du profil est requis", "toast_program_set": "Programme défini : {program}", - "toast_rebuild_failed": "Échec de la reconstruction : {error}", "toast_resume_failed": "Échec de la reprise : {error}", "toast_revert_failed": "Échec du rétablissement : {error}", "toast_reverted": "{key} rétabli", @@ -937,97 +898,82 @@ "trend_energy_up": "Tendance énergétique à la hausse ({pct} %/cycle) – moyenne récente {avg}", "trim_destructive_confirm": "Cela ne conserve que {pct} % du cycle et est irréversible. Continuer ?", "trim_intro": "Faites glisser les poignées rouges ou entrez des valeurs. Tout ce qui se trouve à l'extérieur de la fenêtre est supprimé.", - "tuning_suggestions_available": "{count} suggestion de réglage disponible à partir des cycles observés. Ils apparaissent à côté des champs concernés.", + "tuning_suggestions_available": "{count} suggestions de réglage disponibles à partir des cycles observés. Elles apparaissent à côté des champs concernés.", + "tuning_suggestions_available_one": "{count} suggestion de réglage disponible à partir des cycles observés. Elle apparaît à côté du champ concerné.", + "tuning_suggestions_available_many": "{count} suggestions de réglage disponibles à partir des cycles observés. Elles apparaissent à côté des champs concernés.", + "tuning_suggestions_available_other": "{count} suggestions de réglage disponibles à partir des cycles observés. Elles apparaissent à côté des champs concernés.", "unsure_detected_prefix": "WashData n'est pas sûr d'avoir été détecté", - "updated": "Mis à jour", "warmup_badge": "Apprentissage en cours ({done}/{needed} cycles)", - "warmup_detail": "Ce profil a besoin de {needed} cycles étiquetés avant que la correspondance automatique commence. Chaque cycle confirmé l'aide à apprendre.", + "warmup_detail": "La correspondance fonctionne déjà. WashData vous demande de confirmer les {needed} premiers cycles de ce profil avant d'étiqueter de lui-même.", "wipe_history_warning": "Supprimez définitivement tous les cycles et profils. Ne peut pas être annulé.", "compare_overlay_tip": "Superposez les enveloppes des profils appris pour voir à quel programme ressemble chaque cycle.", - "duration_trend_up_tip": "Durée en hausse ({pct} %/cycle)", - "duration_trend_down_tip": "Durée en baisse ({pct} %/cycle)", - "energy_trend_up_tip": "Énergie en hausse ({pct} %/cycle)", - "energy_trend_down_tip": "Énergie en baisse ({pct} %/cycle)", - "performance_trending": "Évolution des performances", - "pg_select_cycle_hint": "Sélectionnez un cycle et cliquez sur ↺ pour charger", - "pg_sweep_progress": "Étape {done} / {total}", - "pg_analysis_hint": "Sélectionnez un cycle et cliquez sur ↺ pour charger l'analyse.", "diagnostics_load_failed": "Impossible de charger les diagnostics : {error}", - "manual_duration_ref_hint": "Utilisé uniquement lorsqu'aucun cycle de référence n'est sélectionné. Un cycle de référence définit la durée à partir de sa propre longueur.", + "manual_duration_ref_hint": "Utilisé uniquement sans cycle de référence ; un cycle de référence fixe la durée d'après sa propre longueur.", + "duration_learned_hint": "Calculée à partir des cycles de ce programme (valeurs aberrantes exclues) et mise à jour après chaque nouveau cycle. Pour la modifier, rognez ou supprimez les cycles qui s'écartent.", "toast_convert_rolled_back": "La conversion a échoué et a été annulée (aucune automatisation laissée) : {error}", "toast_convert_orphan": "L'automatisation a été créée, mais la suppression des anciennes actions a échoué. Ne réessayez pas : supprimez les anciennes actions manuellement pour éviter une automatisation en double.", - "advisory_duration_trend_up": "Les cycles de '{name}' deviennent de plus en plus longs (environ +{pct}% par cycle). Si le comportement de l'appareil a changé, réenregistrez ou reconstruisez ce profil.", - "advisory_energy_trend_up": "'{name}' consomme de plus en plus d'énergie (environ +{pct}% par cycle) - il peut être utile de vérifier l'appareil si cela est inattendu.", - "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.", + "advisory_duration_trend_up": "Les cycles de '{name}' s'allongent sans cesse (environ +{pct}% par cycle). Si l'appareil a changé, réenregistrez ou reconstruisez ce profil.", + "advisory_energy_trend_up": "'{name}' consomme plus d'énergie à chaque cycle (environ +{pct}% par cycle). Vérifiez l'appareil si c'est inattendu.", + "advisory_poor_health": "'{name}' correspond mal : ses cycles récents varient beaucoup ou correspondent faiblement. Vérifiez ses cycles ou réenregistrez-le pour garder une correspondance et des estimations précises.", + "advisory_shape_drift": "'{name}' s'est éloigné de sa courbe de puissance d'origine, peut-être à cause du calcaire ou de l'usure. Envisagez de le réenregistrer avec des cycles récents.", + "advisory_shape_drift_corr": "'{name}' s'est éloigné de sa courbe de puissance d'origine (corrélation {corr}), peut-être à cause du calcaire ou de l'usure. Envisagez de le réenregistrer avec des cycles récents.", + "advisory_unmatchable": "'{name}' ne pourra jamais correspondre : aucun cycle avec des données de puissance ne lui est associé. Étiquetez ou enregistrez un cycle de ce programme pour y remédier.", + "advisory_unmatchable_no_duration": "'{name}' ne pourra jamais correspondre : son cycle n'a pas de durée exploitable. Réenregistrez ce programme ou définissez sa durée attendue.", "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_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.", "pg_sweep_intro2": "Trouvez le paramètre qui répond le mieux à un objectif sur vos cycles récents. Rien ne change tant que vous ne l'appliquez pas.", - "pg_running_history": "Relecture de vos cycles…", - "pg_running_sweep": "Test de valeurs sur l'ensemble de vos cycles…", "pg_row_load_hint": "Charger ce cycle dans le graphique ci-dessus", - "pg_history_intro2": "Rejouez vos cycles récents dans le détecteur et le moteur de correspondance réels avec les réglages ci-dessus. Cliquez sur une ligne pour charger ce cycle dans le graphique ; modifiez un réglage pour voir une comparaison avant/après.", + "pg_history_intro2": "Rejouez les cycles récents dans le vrai détecteur et le vrai moteur de correspondance avec les réglages ci-dessus. Cliquez sur une ligne pour la charger dans le graphique ; modifiez un réglage pour comparer avant et après.", "pg_simulating": "Simulation du cycle…", - "pg_canvas_empty2": "Choisissez un cycle ci-dessus et appuyez sur Exécuter pour le simuler. Survolez ensuite pour lire les valeurs, faites défiler pour zoomer et faites glisser pour vous déplacer.", "pg_apply_settings_confirm": "Enregistrer ces {n} réglage(s) sur cet appareil ? {list}", - "pg_stress_terminated_detail": "Stabilisé à ~{idle}W en veille -- le cycle s'est terminé {h}h {m}m plus tard via {reason}.", - "pg_stress_above_threshold_detail": "La consommation en veille ~{idle}W est égale ou supérieure au seuil d'arrêt effectif ({stop}W) -- le cycle n'a jamais été considéré comme inactif. Augmentez stop_threshold_w pour corriger.", - "pg_stress_hit_cap_detail": "Le cycle a duré {h}h {m}m sans s'arrêter -- arrêté de force par la limite de sécurité. Consommation en veille {idle}W contre un seuil d'arrêt de {stop}W.", "no_logs_match": "Aucun enregistrement de journal ne correspond à ces filtres.", "pg_analysis_hint2": "Sélectionnez un cycle et cliquez sur Exécuter pour charger l'analyse de correspondance.", - "pg_analysis_empty2": "Cliquez sur Exécuter pour voir l'analyse de correspondance.", - "imported_readonly": "Importé depuis la boutique communautaire. Affiché à titre de référence et pour la correspondance. Il n'est pas compté dans vos statistiques et ne peut pas être modifié.", + "imported_readonly": "Importé depuis la boutique communautaire. Sert de référence et à la correspondance, n'est pas compté dans vos statistiques et ne peut pas être modifié.", "merge_no_imports": "Les enregistrements importés ne peuvent pas être fusionnés dans un cycle réel. Désélectionnez-les pour fusionner.", - "store_enable_hint": "Activez les fonctionnalités en ligne dans les Paramètres pour parcourir et importer les cycles de référence de la communauté.", - "online_intro": "Partagez et téléchargez des cycles de référence avec d'autres utilisateurs de WashData. Toutes les fonctionnalités en ligne sont facultatives et désactivées par défaut.", + "store_enable_hint": "Activez les fonctionnalités en ligne dans Préférences > En ligne et communauté pour parcourir et importer les cycles de référence de la communauté.", "store_share_intro": "Téléversez ce cycle de référence pour que d'autres personnes possédant le même appareil puissent l'utiliser. Il est examiné avant d'apparaître publiquement.", - "online_intro_global": "Parcourez et partagez des enregistrements de référence avec d'autres utilisateurs de WashData, et confirmez les fiches d'appareils. Une seule connexion s'applique à toute votre intégration WashData ; la marque et le modèle de l'appareil se définissent par appareil dans Général. Toutes les fonctionnalités en ligne sont facultatives et désactivées par défaut.", + "online_intro_global": "Parcourez et partagez des enregistrements de référence avec d'autres utilisateurs de WashData. Une connexion couvre tous vos appareils WashData ; la marque et le modèle se règlent par appareil sous Général. Les fonctions en ligne restent désactivées jusqu'à ce que vous les activiez.", "online_unavailable": "Les fonctionnalités en ligne ne sont pas disponibles sur ce serveur.", "store_picker_offline": "Activez les fonctionnalités en ligne dans l'icône des paramètres pour choisir dans le catalogue de la communauté.", "pick_brand_first": "Choisissez d'abord une marque d'appareil.", - "model_not_found": "Pas encore dans le catalogue.", - "connect_to_confirm": "Connectez-vous dans l'icône des paramètres pour confirmer ou évaluer.", "adopt_settings_hint": "Remplacer les seuils de détection et de reconnaissance de cet appareil par ceux partagés. Vos notifications, entités et prix de l'énergie ne sont jamais modifiés.", - "include_settings_hint": "Partager les seuils de reconnaissance et de correspondance de cet appareil (pas vos notifications, entités ou prix de l'énergie). Les adoptants choisissent s'ils souhaitent les appliquer.", + "include_settings_hint": "Partager les seuils de détection et de correspondance de cet appareil (pas les notifications, entités ni prix de l'énergie). Les autres choisissent de les appliquer ou non.", "onboard_download": "Nouvel appareil ? Adoptez une configuration prête à l'emploi (programmes, cycles de référence et phases) d'un autre utilisateur WashData avec le même appareil.", - "share_consent": "Je confirme que ces cycles se sont déroulés normalement jusqu'à la fin, sans interruption", "share_device_none": "Aucun cycle partageable pour l'instant. Marquez d'abord un cycle enregistré ou sélectionné à la main comme cycle de référence (⭐) dans l'onglet Cycles.", "share_guideline_naming": "Nommez chaque profil exactement comme indiqué sur le cadran ou l'afficheur de l'appareil (p. ex. 'Cotton 40', 'Eco 60').", - "share_guideline_quality": "Ne partagez que les cycles qui se sont terminés normalement -- sans interruptions en cours de cycle, ouvertures de porte ou micro-coupures de courant.", + "share_guideline_quality": "Ne partagez que des cycles terminés normalement : sans interruption, ouverture de porte ni micro-coupure.", "share_guideline_review": "Votre envoi commence en attente de validation et apparaît publiquement dès que suffisamment de membres de la communauté le confirment.", "share_guidelines_title": "Avant de partager", - "store_download_device_intro": "Adoptez chaque programme partagé et ses cycles de référence sur votre appareil. Vos propres cycles enregistrés et statistiques ne sont pas affectés.", + "store_download_device_intro": "Ajoute tous les programmes partagés et leurs cycles de référence à votre appareil. Vos propres cycles et statistiques ne sont pas touchés.", "store_share_device_intro": "Envoyez {brand} {model} avec les cycles de référence que vous sélectionnez. D'autres utilisateurs du même appareil peuvent adopter vos programmes. Les entrées sont examinées avant d'apparaître publiquement.", "share_profile_no_cycles": "Aucun cycle de référence -- marquez un cycle comme ⭐ dans l'onglet Cycles pour inclure ce profil", - "advisory_phase_inconsistent": "'{name}' semble mélanger différents programmes ou températures - ses cycles chauffent pendant des durées très variables. Le diviser en profils distincts (p. ex. par température) améliorera la reconnaissance et les estimations de temps.", - "advisory_phase_inconsistent_title": "⚠ Programmes peut-être mélangés", - "export_select_intro": "Cochez exactement ce qu'il faut inclure. Sélectionner des profils sans leurs cycles exporte tout de même un programme reconnaissable (sa forme apprise l'accompagne).", - "import_analyze_hint": "Chargez un fichier exporté (ou collez son JSON). WashData l'analyse et montre exactement ce qui peut être importé avant tout changement.", + "export_select_intro": "Cochez ce qu'il faut inclure. Des profils sans leurs cycles exportent tout de même un programme reconnaissable.", + "import_analyze_hint": "Chargez un fichier exporté (ou collez son JSON). WashData montre ce qui peut être importé avant tout changement.", "analyzing": "Analyse en cours…", - "device_type_mismatch_warn": "Cet export provient d'un autre type d'appareil ({src} au lieu de {local}). Les programmes et cycles peuvent tout de même être importés comme données de référence, mais les réglages spécifiques à l'appareil et l'import dans l'historique réel sont désactivés.", + "device_type_mismatch_warn": "Cet export provient d'un autre type d'appareil ({src} au lieu de {local}). Les programmes et cycles peuvent encore être importés comme données de référence, mais pas les réglages de l'appareil ni l'historique réel.", "merge_hint": "Les éléments importés sont ajoutés ; rien de local n'est perdu. Les conflits de noms sont résolus ci-dessous.", "replace_warn": "Chaque catégorie cochée est effacée et remplacée par le contenu du fichier. Les catégories non cochées restent intactes.", "dest_reference_hint": "Les cycles importés améliorent uniquement la reconnaissance des programmes et n'affectent jamais les statistiques d'utilisation/d'énergie.", - "dest_real_history_hint": "Les cycles importés comptent comme l'historique propre de cet appareil et alimentent les statistiques d'énergie/d'utilisation. À utiliser pour déplacer un appareil vers une nouvelle installation.", + "dest_real_history_hint": "Les cycles importés comptent comme l'historique et les statistiques propres de cet appareil. À utiliser pour déplacer un appareil vers une nouvelle installation.", "sug_muted": "Ce paramètre ne sera plus suggéré", "sug_mute_failed": "Impossible de masquer la suggestion", "sug_unmuted_all": "Les suggestions masquées ont été réinitialisées", - "n_suggestions_muted": "{count} masquée(s) ; le réglage automatique ne proposera plus ces suggestions.", + "n_suggestions_muted": "{count} suggestions masquées ; le réglage automatique ne les proposera plus.", + "n_suggestions_muted_one": "{count} suggestion masquée ; le réglage automatique ne la proposera plus.", + "n_suggestions_muted_many": "{count} suggestions masquées ; le réglage automatique ne les proposera plus.", + "n_suggestions_muted_other": "{count} suggestions masquées ; le réglage automatique ne les proposera plus.", "font_size_hint": "Agrandir ou réduire tout le contenu de ce panneau. S'applique à votre compte sur cet appareil.", - "import_history_description": "Vous aviez déjà une prise connectée avant WashData ? Chargez un export de l'historique de son capteur de puissance, ou lisez-le directement depuis Home Assistant : la détection habituelle s'applique à ces données, si bien que vos cycles passés apparaissent dans votre liste Cycles, prêts à être nommés.", - "hist_input_hint": "Chargez un CSV téléchargé depuis le panneau Historique (entité, état, dernière modification), ou laissez WashData lire directement l'historique du capteur. La détection s'exécute ensuite exactement comme en direct, et vous choisissez lesquels des cycles trouvés conserver.", - "hist_recorder_hint": "Lit depuis la date que vous choisissez jusqu'à maintenant. Home Assistant conserve l'historique détaillé pendant 10 jours par défaut, puis seulement des moyennes horaires, trop grossières pour y détecter des cycles - ne choisissez une date plus ancienne que si le recorder de Home Assistant est configuré pour en conserver davantage.", - "hist_scanning": "Votre historique est rejoué dans le détecteur. Cela s'exécute en arrière-plan : vous pouvez fermer cette fenêtre et y revenir plus tard.", + "import_history_description": "Vous aviez une prise connectée avant WashData ? Chargez son historique de puissance ou lisez-le depuis Home Assistant. La détection le parcourt et les cycles passés apparaissent dans votre liste Cycles, prêts à être nommés.", + "hist_input_hint": "Chargez un CSV du panneau Historique (entité, état, dernière modification), ou laissez WashData lire directement l'historique du capteur. La détection en direct le parcourt et vous choisissez les cycles à conserver.", + "hist_recorder_hint": "Lit de la date choisie jusqu'à maintenant. Home Assistant conserve l'historique détaillé 10 jours par défaut ; les moyennes horaires plus anciennes sont trop grossières pour détecter des cycles.", + "hist_scanning": "Votre historique passe dans le détecteur en arrière-plan. Vous pouvez fermer cette fenêtre et revenir plus tard.", "hist_imported_count": "{n} cycles importés.", "hist_duplicates": "{n} avaient déjà été importés et ont été ignorés.", "hist_capped": "La limite de cycles importés par appareil a été atteinte ; le reste n'a pas été enregistré.", - "hist_next_step": "Ils sont dans votre liste Cycles, marqués comme historique importé. Ouvrez-en un et utilisez Étiqueter pour nommer le programme auquel il correspond.", + "hist_next_step": "Ils sont dans votre liste Cycles, marqués comme historique importé. Ouvrez-en un et utilisez Étiqueter pour nommer son programme.", "hist_rows_read": "{n} mesures lues", "hist_breaks": "{n} coupures où le capteur était indisponible", "hist_other_entity": "{n} mesures d'autres entités ignorées", @@ -1039,60 +985,93 @@ "hist_scan_capped": "Seuls les premiers candidats sont affichés ({n} ont été trouvés).", "hist_recorder_empty": "Home Assistant n'a pas d'historique détaillé pour ce capteur sur cette période.", "hist_scan_failed": "L'analyse a échoué.", - "hist_scan_expired": "Cette analyse n'est plus disponible. Veuillez relancer l'analyse.", + "hist_scan_expired": "Cette analyse n'est plus disponible. Relancez 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.", - "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.", + "imported_history_readonly": "Trouvé dans un historique de puissance importé. Il aide la correspondance, mais n'est pas compté dans vos statistiques et ne peut être ni rogné ni scindé. Étiquetez-le pour nommer le programme.", + "odometer_intro": "Ce total ne fait qu'augmenter. Supprimer un cycle ne le fait pas baisser, pour que les rappels d'entretien restent justes.", + "odometer_correct_hint": "Renseignez cette valeur si WashData a compté un cycle qui n'a jamais eu lieu, ou si l'appareil a 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}", "standby_above_stop_title": "Consomme encore {idle} W à la fin du cycle", "standby_above_stop_sub": "{n} des {total} derniers cycles. Le Délai d'arrêt ne démarre qu'une fois la puissance descendue sous le Seuil d'arrêt ({stop} W) : les cycles se terminent donc en retard ou doivent être arrêtés à la main.", - "advisory_duration_outlier": "{n} cycle(s) étiquetés '{name}' s'écartent trop de sa durée habituelle pour pouvoir un jour lui correspondre (pire cas : {ratio}x). Ils sont probablement mal étiquetés, ou un enregistrement a capté deux exécutions. Réétiquetez-les ou scindez-les pour qu'ils cessent de fausser l'estimation de temps de ce programme.", + "advisory_duration_outlier": "{n} cycle(s) étiquetés '{name}' s'écartent trop de sa durée habituelle pour pouvoir correspondre (pire cas : {ratio}x). Ils sont probablement mal étiquetés ou contiennent deux cycles. Réétiquetez-les ou scindez-les pour qu'ils cessent de fausser son estimation de durée.", "toast_range_required": "Définissez une plage De/À valide", - "no_profiles_option": "(aucun profil, ajoutez-en un)" + "no_profiles_option": "(aucun profil, ajoutez-en un)", + "compare_cycles_title": "Comparer {count} cycles", + "edit_builtin_phase": "Ceci est une phase intégrée. L'enregistrement crée une copie personnalisée ; supprimez la copie pour restaurer l'original.", + "fetch_error": "Échec du chargement des données.", + "connection_lost": "Connexion perdue - affichage des données de {time}.", + "pg_notify_partial": "Les marqueurs ne montrent que les notifications de démarrage, de presque terminé, de fin, de jalons et d'heures silencieuses. Les mises à jour en direct, les rappels de déchargement et les alertes de dépassement ne sont pas simulés.", + "connect_to_confirm": "Connectez-vous dans l'icône des paramètres pour confirmer ou évaluer.", + "coverage_gap_cluster": "{count} cycles récents sans étiquette d'environ {min} min ressemblent à un programme que vous n'avez pas encore créé.", + "pg_label_yes": "Serait étiqueté « {name} » en fin de cycle", + "pg_label_no": "Demanderait une révision : {reason}", + "live_match_debug_tip": "Confiance : degré de correspondance entre la courbe de puissance actuelle et le profil candidat en tête (0-100 %). Ambigu : les scores des deux meilleurs candidats sont à moins de 5 % l'un de l'autre ; l'étiquette reste incertaine jusqu'à la fin du cycle.", + "automation_alias_started": "{device} : cycle démarré", + "automation_alias_finished": "{device} : cycle terminé", + "automation_desc_started": "S'exécute lorsqu'un cycle de {device} démarre. Ajoutez vos actions (notification, lumières, ...).", + "automation_desc_finished": "S'exécute lorsqu'un cycle de {device} se termine. Ajoutez vos actions (notification, lumières, ...).", + "feedback_detected": "WashData a détecté : {name}", + "import_undo_hint": "Un point de restauration d'avant la dernière importation ({when}) est conservé : {profiles} profil(s) et {cycles} cycle(s).", + "replace_undo_hint": "Un point de restauration est d'abord enregistré, vous pouvez donc annuler cette importation depuis Exporter/Importer.", + "pg_in_sample_note": "Ces cycles ont servi à construire les profils auxquels ils sont comparés, les résultats sont donc optimistes : attendez-vous à un peu moins de correspondances justes sur de nouveaux cycles.", + "hist_tail_trimmed": "{min} min d'attente après la fin du cycle ont été supprimées, comme le fait l'intégration en direct", + "invalid_threshold_pair": "Le Seuil d'arrêt ({stop} W) doit être inférieur au Seuil de démarrage ({start} W). Rien n'a été enregistré ; modifiez les deux ensemble.", + "hist_truncated": "arrêté à la limite de {n} mesures, le reste de l'historique n'a donc pas été analysé", + "hist_warn_kw": "La mesure la plus élevée n'est que de {w}. Ce capteur semble indiquer des kilowatts ; WashData a besoin de watts, donc aucun cycle ne peut être détecté.", + "hist_warn_naive_time": "{n} horodatages n'ont pas de fuseau horaire et ont été lus en UTC. Si le fichier est en heure locale, les cycles sont décalés de votre écart avec UTC et des mesures proches d'un changement d'heure peuvent être perdues.", + "hist_warn_naive_time_one": "{n} horodatage n'a pas de fuseau horaire et a été lu en UTC. Si le fichier est en heure locale, les cycles sont décalés de votre écart avec UTC et des mesures proches d'un changement d'heure peuvent être perdues.", + "hist_warn_naive_time_many": "{n} horodatages n'ont pas de fuseau horaire et ont été lus en UTC. Si le fichier est en heure locale, les cycles sont décalés de votre écart avec UTC et des mesures proches d'un changement d'heure peuvent être perdues.", + "hist_warn_naive_time_other": "{n} horodatages n'ont pas de fuseau horaire et ont été lus en UTC. Si le fichier est en heure locale, les cycles sont décalés de votre écart avec UTC et des mesures proches d'un changement d'heure peuvent être perdues.", + "pg_compare_profile_tip": "Choisit uniquement le profil tracé sur le graphique. La simulation recherche toujours elle-même la correspondance du cycle, comme le fait l'intégration en direct.", + "pg_outcome_stale": "Les réglages ont changé depuis l'exécution de cette simulation. Elle affiche encore l'ancien résultat.", + "pg_sweep_guarded": "Termine plus de cycles trop tôt, en divise davantage ou manque des cycles que le réglage actuel détecte : cette valeur n'est donc jamais recommandée.", + "pg_sweep_keep_current": "Conservez la valeur actuelle ({value}, {metric}) : aucune valeur n'a fait mieux d'au moins un cycle.", + "pg_sweep_n_cycles": "sur {count} cycles", + "pg_sweep_n_cycles_one": "sur {count} cycle", + "pg_sweep_n_cycles_many": "sur {count} cycles", + "pg_sweep_n_cycles_other": "sur {count} cycles", + "pg_task_busy": "Un test sur l'historique ou une optimisation est déjà en cours pour cet appareil. Attendez qu'il se termine ou annulez-le." }, "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", + "smart_termination_duration_ratio": "Fraction de la durée moyenne du programme qu'un cycle doit atteindre avant que la Terminaison intelligente puisse l'arrêter plus tôt ; baissez-la si les durées varient", + "anti_crease_finalize_ratio": "Fraction de la durée moyenne du programme qu'un cycle doit atteindre avant que la traîne anti-froissement puisse le terminer ; baissez-la sur les sèche-linge, gardez la valeur par défaut sur les lave-linge", "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_exit_power": "Niveau considéré comme silence entre les impulsions de culbutage ; n'agit qu'au-dessus du Seuil d'arrêt", "anti_wrinkle_idle_timeout": "Temps de silence autorisé entre deux impulsions de culbutage avant la fin de l'anti-froissement", "anti_wrinkle_max_duration": "Une impulsion plus longue met fin à l'anti-froissement et ouvre un nouveau cycle", "anti_wrinkle_max_power": "Une impulsion au-dessus de ce seuil met fin à l'anti-froissement et ouvre un nouveau cycle", "completion_min_seconds": "Plus courte exécution comptant comme un vrai cycle", - "end_repeat_count": "Lectures basses consécutives avant la fin", "interrupted_min_seconds": "Cycles courts signalés comme interrompus", "min_off_gap": "Écart requis pour séparer deux cycles", "off_delay": "Secondes de faible puissance avant la fin du cycle", "start_duration_threshold": "Secondes au-dessus du seuil pour confirmer le démarrage", "start_threshold_w": "Watts minimum pour être compté comme démarré", "stop_threshold_w": "En dessous, la machine est comptée comme éteinte", - "dtw_bandwidth": "Étape 3 : bande de déformation Sakoe-Chiba (0 = DTW désactivé ; défaut 0,2 = 20 % de la longueur du cycle)", - "corr_weight": "Étape 2 : équilibre entre la forme de la courbe (corrélation) et le niveau de puissance (MAE) ; défaut 0,45", - "duration_weight": "Étape 4 : force avec laquelle la concordance de durée affecte le score final (défaut 0,22)", - "energy_weight": "Étape 4 : force avec laquelle la concordance d'énergie affecte le score final (défaut 0,22)", - "profile_match_min_duration_ratio": "Étape 1 : durée de cycle minimale en fraction de la durée du profil ; défaut 0,1 (10 %)", - "profile_match_max_duration_ratio": "Étape 1 : durée de cycle maximale en fraction de la durée du profil ; défaut 1,8 (180 %)", - "keep_min_score": "Étape 2 : score plancher pour rester en lice ; le défaut 0,1 admet les candidats faibles, les étapes suivantes sélectionnent le meilleur", - "dtw_blend": "Étape 3 : 0 = score central uniquement, 1 = score DTW uniquement, 0,5 = mélange égal (défaut)", - "dtw_ensemble_w": "Étape 3 (mode ensemble) : poids sur L1 normalisé vs DTW dérivé ; le défaut 0,7 favorise la sensibilité au niveau", - "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)", + "profile_match_min_duration_ratio": "Durée de cycle minimale en fraction de la durée du profil ; défaut 0,1 (10 %)", + "profile_match_max_duration_ratio": "Durée de cycle maximale en fraction de la durée du profil ; défaut 1,8 (180 %)", "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.", "anti_wrinkle": "Culbutage périodique post-cycle pour réduire le froissement du linge.", + "baking": "Résistance à haute température active pour former la croûte et la mie.", + "cooking": "Phase principale de cuisson, résistance et ventilateur actifs.", "cool_down": "Culbutage sans chaleur vers la fin du cycle.", + "cool_down_af": "Refroidissement par ventilateur seul après la chauffe.", "drain_and_switch": "Période de transition du mode lavage au mode séchage.", "dry": "Étape de séchage par chauffage et/ou chaleur résiduelle.", "drying": "Période principale de culbutage chauffé.", "heat_up": "Préchauffage initial du radiateur avant le début du séchage complet.", + "keep_warm": "Maintien à basse température pour garder les aliments chauds.", + "keep_warm_bm": "Maintien à basse température pour garder le pain chaud après la cuisson.", + "kneading": "Le moteur mélange et pétrit la pâte. Forte consommation.", + "pause_af": "Courte pause pour secouer ou vérifier les aliments.", + "pre_heat": "Chauffe initiale de la chambre de cuisson avant la cuisson complète.", "pre_rinse": "Pulvérisation initiale avant le lavage au détergent.", + "proving": "Levée à basse température pour que la levure fermente et que la pâte gonfle.", + "resting": "Courte pause à faible puissance entre deux pétrissages pour détendre le gluten.", "pre_wash": "Trempage initial ou prétraitement avant le lavage principal.", "rinse": "Étape de rinçage à l’eau propre. Cette phase peut se répéter plusieurs fois.", "sanitize": "Étape de nettoyage à haute température pour les programmes de désinfection.", @@ -1105,6 +1084,7 @@ "wash_dw": "Lavage avec détergent principal avec chauffage." }, "placeholder": { + "custom_task": "p. ex. nettoyer le joint de porte", "add": "ajouter…", "group_name": "ex. Coton 2:47", "json_buttons": "[{\"action\":\"ID\",\"title\":\"Label\"}]", @@ -1132,10 +1112,10 @@ "section": { "dishwasher": { "label": "Lave-vaisselle", - "intro": "Gestion de fin de cycle pour les lave-vaisselle, qui se terminent généralement par une longue phase de séchage quasi silencieuse avant une brève vidange finale." + "intro": "Gestion de fin pour les lave-vaisselle, qui sèchent souvent longtemps en silence avant une courte vidange finale." }, "anti_wrinkle": { - "intro": "Le mode anti-froissement détecte les courtes impulsions de culbutage à faible puissance après la phase principale et les maintient rattachées au cycle terminé.", + "intro": "Rattache au cycle terminé les impulsions de rotation à faible puissance d'un sèche-linge après la phase principale.", "label": "Anti-froissement" }, "basic": { @@ -1143,7 +1123,7 @@ "label": "Général" }, "delay": { - "intro": "La détection de démarrage différé identifie quand un appareil est allumé mais n'a pas encore commencé son cycle.", + "intro": "Détecte un appareil allumé qui attend un départ différé.", "label": "Démarrage différé" }, "detection": { @@ -1167,161 +1147,153 @@ "label": "Chronométrage et surveillance" }, "triggers": { - "intro": "Signaux externes optionnels : déclencheur de fin, capteur de porte, interrupteur de pause et rappel de déchargement.", + "intro": "Options facultatives : déclencheur de fin, capteur de porte, interrupteur de pause et rappel de déchargement.", "label": "Déclencheurs et portes" - }, - "phase_eta": { - "label": "Temps restant", - "intro": "Temps restant tenant compte des phases, pour les machines dont la durée de cycle dépend de la température ou de l'essorage." } }, "setting": { "dishwasher_end_spike_quiet_release": { "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." + "doc": "Après la durée attendue : temps pendant lequel le lave-vaisselle doit rester sous le Seuil d'arrêt avant que WashData cesse d'attendre la vidange finale. Augmentez-le si une vidange tardive après un long séchage est manquée. L'attente ne dépasse jamais 30 minutes." }, "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." + "doc": "Fraction de la durée moyenne du programme qu'un cycle doit atteindre avant que sa rotation à faible puissance compte comme traîne anti-froissement. Baissez-la (p. ex. 0,75) sur les sèche-linge à détection de charge. Gardez la valeur par défaut (0,98) sur les lave-linge, sinon un lavage peut s'arrêter à mi-parcours." }, "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.", + "doc": "Rattacher au cycle terminé les courtes impulsions de rotation à faible puissance d'un sèche-linge après la phase de chauffe principale, au lieu de les lire comme de nouveaux cycles.", "label": "Activer la détection anti-froissement" }, "anti_wrinkle_exit_power": { - "doc": "La puissance doit tomber en dessous entre les impulsions pour que le mode antirides reste actif.", + "doc": "En mode anti-froissement, une puissance sous cette valeur compte comme du silence entre les impulsions de culbutage, et un silence plus long que l'Intervalle maximal entre impulsions termine le mode. Le Seuil d'arrêt est utilisé s'il est plus élevé : ce réglage n'a donc d'effet qu'au-dessus du Seuil d'arrêt.", "label": "Seuil de puissance de sortie" }, "anti_wrinkle_idle_timeout": { - "doc": "Durée pendant laquelle l'appareil peut rester silencieux entre deux impulsions de culbutage avant la fin du mode anti-froissement. Réglez-la au-dessus de l'intervalle le plus long que votre sèche-linge laisse entre les impulsions, sinon chaque impulsion ultérieure est lue comme un faux départ.", + "doc": "Durée pendant laquelle le sèche-linge peut rester silencieux entre deux impulsions avant la fin du mode anti-froissement. Réglez-la au-dessus de la plus longue pause de votre sèche-linge, sinon les impulsions suivantes sont lues comme de faux démarrages.", "label": "Intervalle maximal entre impulsions" }, "anti_wrinkle_max_duration": { - "doc": "Les impulsions plus longues sont traitées comme un véritable cycle plutôt que comme une chute antirides.", + "doc": "Une impulsion plus longue compte comme un vrai cycle, pas comme une rotation anti-froissement.", "label": "Durée maximale" }, "anti_wrinkle_max_power": { - "doc": "Une impulsion supérieure à cette puissance est traitée comme un véritable nouveau cycle et non comme une chute antirides. Réglé juste au-dessus de la puissance d’impulsion.", + "doc": "Une impulsion au-dessus de cette puissance compte comme un vrai nouveau cycle, pas comme une rotation. Réglez-la juste au-dessus de la puissance de vos impulsions.", "label": "Puissance maximale anti-froissement" }, "auto_label_confidence": { - "doc": "Si le score de correspondance à la fin du cycle est égal ou supérieur à ce chiffre, le programme est automatiquement étiqueté sans aucune invite de confirmation. Augmentez-le pour exiger une plus grande certitude avant l’étiquetage automatique ; abaissez-le pour automatiser davantage. Fonctionne en conjonction avec Learning Confidence en dessous.", + "doc": "Score de correspondance en fin de cycle à partir duquel le programme est étiqueté sans demander. Augmentez-le pour plus de certitude ; baissez-le pour étiqueter plus de cycles automatiquement. Gardez-le au-dessus de la Confiance d'apprentissage.", "label": "Confiance d'étiquetage automatique" }, "auto_maintenance": { - "doc": "Effectuez un entretien ménager nocturne : reconstruisez les enveloppes de profil, recalculez l’état du cycle, élaguez les traces de débogage et conservez les cycles les plus récents.", + "doc": "Nettoyage nocturne : étiquette les anciens cycles qui correspondent désormais clairement à un programme, reconstruit les courbes des programmes, actualise les marques d'anomalie et la santé des cycles, et supprime les traces de débogage si Enregistrer les traces de débogage est désactivé. Les cycles et leurs traces de puissance sont toujours conservés.", "label": "Maintenance automatique (nettoyage nocturne)" }, "completion_min_seconds": { - "doc": "Les cycles plus courts que cela sont rejetés comme cycles fantômes (essais, ouverture de la porte pour ajouter une chaussette).", + "doc": "Les cycles plus courts que cette durée (essais, porte ouverte pour ajouter une chaussette) sont enregistrés comme Interrompu et ne reçoivent pas de notification de fin.", "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." + "doc": "Ajoute au début d'un cycle les mesures des tentatives de démarrage échouées, pour les machines qui tâtonnent avant de démarrer (verrouillage de porte, premier remplissage). 300 s suffisent le plus souvent ; 0 = désactivé. Les débuts enregistrés avancent, donc les durées apprises dérivent un moment." }, "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.", + "doc": "La puissance doit rester dans la bande de veille aussi longtemps avant que l'appareil soit considéré comme en attente de démarrage.", "label": "Secondes de confirmation" }, "delay_start_detect_enabled": { - "doc": "Détecte le moment où l'appareil est allumé et en attente (démarrage différé / veille) mais n'a pas commencé son cycle, afin que le tirage en veille ne soit pas confondu avec un cycle en cours.", + "doc": "Détecter un appareil allumé qui attend un départ différé, pour que sa consommation en veille ne soit pas prise pour un cycle en cours.", "label": "Activer la détection de démarrage différé" }, "delay_timeout_hours": { - "doc": "Arrêtez d'attendre en mode démarrage différé après autant d'heures et revenez au ralenti, de sorte qu'une machine laissée sous tension mais jamais démarrée n'attende pas éternellement.", + "doc": "Cesser d'attendre un départ différé après ce nombre d'heures et revenir à l'inactivité.", "label": "Heures de délai d'expiration" }, "device_type": { - "doc": "Classe d'électroménager. Définit des paramètres de détection sensibles par défaut (seuils, délai de déclenchement, gestion de fin) adaptés à ce type d'appareil ; ne le modifiez que si l'appareil a été configuré à l'origine avec un type incorrect.", + "doc": "Définit les valeurs de détection par défaut (seuils, délai d'arrêt, gestion de fin) pour ce type d'appareil. Ne le changez que si l'appareil a été configuré avec un mauvais type.", "label": "Type d'appareil" }, "door_sensor_entity": { - "doc": "Capteur binaire de porte en option. Utilisé pour détecter quand l'appareil a été ouvert/déchargé après un cycle.", + "doc": "Capteur de porte facultatif. Indique quand l'appareil a été ouvert pour être déchargé après un cycle.", "label": "Entité capteur de porte" }, "duration_tolerance": { - "doc": "Tolérance pour les estimations de temps restant (retour d'apprentissage, pas d'appariement). Si la durée réelle se situe à +/-X % de l'estimation, cela compte comme une bonne correspondance.", + "doc": "Retour d'apprentissage uniquement : un cycle à +/-X % de sa durée estimée compte comme une bonne estimation. N'affecte pas la correspondance.", "label": "Tolérance d'estimation" }, "enable_ml_models": { - "doc": "Pendant qu'un cycle s'exécute, laissez les modèles affiner les résultats en direct : une estimation plus stable du temps restant et de l'énergie/coût, et une protection anti-arrêt prématuré lors de la détection de fin (elle ne peut que retarder une fin, ne jamais la terminer plus tôt et est limitée). Utilise vos modèles affinés lorsqu'ils sont disponibles, sinon ceux intégrés. Off = logique classique basée sur la puissance uniquement (toujours fiable).", + "doc": "Utiliser un modèle entraîné sur cette machine pour prévoir l'énergie et le coût d'un cycle en cours, et laisser un programme bien connu qui s'arrête anormalement tôt se terminer sans tout le délai d'arrêt. Désactivé : logique basée sur la puissance uniquement.", "label": "Appliquer les modèles intelligents pendant un cycle" }, "end_energy_threshold": { - "doc": "Pendant le compte à rebours, l'énergie accumulée (watts x temps) est comparée à ce seuil. En cas de dépassement, le compte à rebours se réinitialise – gardant les sèche-linge anti-froissage et les queues de séchage du lave-vaisselle attachés au cycle au lieu de les couper court. Augmentez-le si les cycles se terminent trop tôt pendant le refroidissement ; baissez-le si la détection est lente.", + "doc": "Énergie autorisée pendant le délai d'arrêt. Au-delà, le cycle continue, pour que les rotations et les traînes de séchage en fassent toujours partie. Baissez-la si des cycles se terminent pendant une traîne ; augmentez-la si des cycles terminés continuent d'attendre.", "label": "Énergie de fin" }, - "end_repeat_count": { - "doc": "Nombre de lectures consécutives inférieures au seuil d'arrêt requises avant la fin du cycle. 1 convient à la plupart des prises. Augmentez à 2-3 si votre prise intelligente signale occasionnellement un échantillon faux zéro en cours de cycle et que vos cycles se terminent prématurément.", - "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). 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é.", + "doc": "Capteur indiquant le prix actuel par kWh (p. ex. un tarif dynamique). Il prévaut sur le prix fixe ci-dessous, il doit donc s'agir d'un prix, pas du capteur de puissance ou d'énergie de la prise.", "label": "Entité de prix de l'énergie" }, "energy_price_static": { - "doc": "Prix fixe par kWh utilisé pour les chiffres de coût lorsqu'aucune entité de prix en direct n'est définie ci-dessus.", + "doc": "Prix fixe par kWh, utilisé quand aucune entité de prix n'est définie.", "label": "Prix de l'énergie statique (par kWh)" }, "energy_sensor": { - "doc": "Compteur d'énergie cumulé facultatif (total_increasing en kWh/Wh, par ex. le compteur de consommation totale de la prise elle-même). Lorsqu'il est défini, l'énergie indiquée pour chaque cycle provient de la différence de ce compteur entre le début et la fin, ce qui évite le sous-comptage provoqué par l'intégration d'un capteur de puissance qui se met à jour lentement. Revient à la valeur intégrée si la lecture est absente, si son unité est inconnue ou si le delta n'est pas positif. Laissez vide pour continuer à intégrer le capteur de puissance.", + "doc": "Compteur facultatif en kWh ou Wh (total_increasing, p. ex. le compteur intégré de la prise). L'énergie du cycle y est alors lue, ce qui est plus précis qu'un calcul à partir d'un capteur de puissance lent. Si son relevé est inexploitable, la valeur calculée est utilisée.", "label": "Entité du compteur d'énergie" }, "expose_debug_entities": { - "doc": "Publiez des entités HA de diagnostic supplémentaires (correspondance à la confiance, à l'ambiguïté, aux éléments internes de l'état). Désactivé maintient la liste des entités propre pour une utilisation normale.", + "doc": "Ajouter des entités de diagnostic (confiance de correspondance, ambiguïté, état interne). Désactivé garde la liste des entités épurée.", "label": "Exposer les entités de débogage" }, "external_end_trigger": { - "doc": "Capteur binaire dont le changement d'état marque la fin du cycle (par exemple un contact \"terminé\" d'un appareil ou une intégration compagnon).", + "doc": "Capteur binaire dont le changement marque la fin du cycle (p. ex. un contact « terminé » ou une autre intégration).", "label": "Entité de déclencheur externe" }, "external_end_trigger_enabled": { - "doc": "Laissez un capteur binaire externe signaler la fin d'un cycle, en plus de la détection intégrée basée sur la puissance.", + "doc": "Laisser un capteur binaire signaler la fin d'un cycle, en plus de la détection basée sur la puissance.", "label": "Activer le déclencheur de fin externe" }, "external_end_trigger_inverted": { - "doc": "Traitez l'arrêt du capteur de déclenchement (plutôt que l'allumage) comme signal de fin de cycle.", + "doc": "Considérer le passage du capteur à OFF (et non à ON) comme signal de fin.", "label": "Inverser le déclencheur externe (déclencher sur ARRÊT)" }, "learning_confidence": { - "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.", + "doc": "Score de correspondance minimal pour étiqueter un cycle terminé. Un gagnant net est étiqueté ; un cas serré va dans la file de vérification de Cycles. En dessous, rien n'est étiqueté ni demandé. Gardez-le sous la Confiance d'étiquetage automatique.", "label": "Confiance d'apprentissage" }, "linked_device": { - "doc": "Imbibez éventuellement cet appareil WashData sous un autre appareil (par exemple la prise intelligente) dans le registre des appareils HA, affiché comme « Connecté via… ».", + "doc": "Facultatif. Afficher cet appareil WashData sous un autre appareil (p. ex. la prise connectée) dans Home Assistant, comme « Connecté via ... ».", "label": "Regrouper sous un appareil" }, "min_off_gap": { - "doc": "Si la machine s'éteint moins de ce temps, la séquence marche/arrêt/marche est traitée comme un cycle continu. Empêche les programmes de trempage (la machine s'éteint pendant plusieurs minutes au milieu du lavage) d'être divisés en deux cycles distincts. Réglez-le plus court que l'écart entre vos charges consécutives si vous souhaitez que celles-ci soient comptées comme des cycles séparés. Les valeurs par défaut du type d’appareil protègent la pause intra-cycle typique pour chaque appareil.", + "doc": "Une période d'arrêt plus courte ne coupe pas un cycle, pour que les pauses de trempage restent dans un même lavage. Gardez-la plus courte que la pause entre deux lavages enchaînés, sinon ils fusionnent en un seul cycle.", "label": "Intervalle d'arrêt minimal" }, "min_power": { - "doc": "Puissance minimale absolue considérée comme active. Les lectures inférieures à cette valeur sont traitées comme 0 W (veille), filtrant la charge fantôme des prises intelligentes et des LED de veille.", + "doc": "Les mesures en dessous comptent comme 0 W (veille). Filtre la consommation au repos des prises connectées et des voyants de veille.", "label": "Puissance minimale" }, "ml_training_enabled": { - "doc": "Étudiez périodiquement vos cycles examinés pendant la nuit et ajustez les modèles à cette machine spécifique. Un changement n'est conservé que lorsqu'il obtient de meilleurs résultats sur les cycles maintenus, cela ne peut donc qu'aider ou rester le même - ne jamais régresser.", + "doc": "Réentraîner de temps en temps le modèle d'énergie sur les cycles terminés de cette machine. Un nouveau modèle n'est gardé que s'il bat l'estimation simple sur des cycles de test.", "label": "Apprendre de cette machine" }, "ml_training_hour": { - "doc": "Heure locale de la journée (0-23) pour effectuer le réglage fin de nuit. Choisissez une heure calme comme 2 (02h00).", + "doc": "Heure de la journée (0-23) pour l'entraînement nocturne. Choisissez une heure calme, p. ex. 2.", "label": "Apprendre à l'heure" }, "ml_training_interval_days": { - "doc": "Vérifiez à nouveau les améliorations au maximum une fois tous les jours.", + "doc": "Nombre minimal de jours entre deux réentraînements.", "label": "Vérifier au plus tous les" }, "ml_training_min_cycles": { - "doc": "Attendez qu'au moins autant de cycles aient été enregistrés avant d'effectuer un réglage fin, afin d'avoir suffisamment de leçons à tirer.", + "doc": "Attendre au moins ce nombre de cycles enregistrés avant l'entraînement.", "label": "Cycles nécessaires d'abord" }, "name": { - "doc": "Nom d’affichage affiché dans la liste des intégrations HA et dans le registre des appareils.", + "doc": "Nom affiché dans Home Assistant pour cet appareil.", "label": "Nom de l'appareil" }, "no_update_active_timeout": { - "doc": "Si aucune mise à jour d'alimentation n'arrive pendant cette période de fonctionnement, supposez que la prise est tombée hors ligne et forcez l'arrêt pour éviter un cycle zombie. La valeur par défaut de 600 s permet un décalage dans le cloud ou le maillage.", + "doc": "Si aucune mise à jour de puissance n'arrive pendant ce délai au cours d'un cycle, la prise est supposée hors ligne et le cycle est arrêté de force. La valeur par défaut de 600 s laisse de la marge aux délais du cloud ou du maillage.", "label": "Délai d'expiration sans mise à jour" }, "notify_before_end_minutes": { @@ -1329,35 +1301,35 @@ "label": "Alerte de pré-fin" }, "notify_channel": { - "doc": "Nom du canal de notification Android pour les messages de démarrage/en direct (contrôle le son et la priorité par canal sur l'application mobile). Blank utilise l'application compagnon par défaut.", + "doc": "Canal Android pour les messages de démarrage et en direct ; le canal définit le son et la priorité. Vide : valeur par défaut de l'application.", "label": "Canal Android (démarrage/direct)" }, "notify_cycle_timers": { - "doc": "Notifications à des minutes spécifiques d'un cycle (par exemple pour ajouter un adoucissant). Le message prend en charge {device}, {program}, {minutes}. Activez la pause automatique pour faire une pause à ce stade et recevoir une notification interactive avec un bouton Reprendre ; reprendre via le panneau, le service pause/reprise ou l'action de notification.", + "doc": "Notifications à des minutes précises d'un cycle (p. ex. pour ajouter de l'adoucissant). Le message prend en charge {device}, {program}, {minutes}. Avec Pause automatique, le cycle se met aussi en pause à ce moment et la notification reçoit un bouton Reprendre.", "label": "Minuteries de cycle" }, "notify_finish_channel": { - "doc": "Nom du canal de notification Android pour le message de fin. Blank réutilise le canal start/live.", + "doc": "Canal Android pour le message de fin. Vide : réutilise le canal de démarrage/en direct.", "label": "Canal Android (fin)" }, "notify_finish_message": { - "doc": "Corps envoyé à la fin d'un cycle. Variables du modèle : {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}. {time_finished} et {vs_typical} sont les plus utiles ici.", + "doc": "Corps envoyé à la fin d'un cycle. Variables : {device}, {program}, {duration} (minutes), {duration_hm} (1 h 05 min), {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}, {status} (completed ou force_stopped ; les cycles interrompus n'envoient rien). Avec toute autre variable, le texte par défaut est utilisé.", "label": "Message de fin" }, "notify_finish_services": { - "doc": "notify.* services appelés à la fin d’un cycle. Ajoutez-en un par cible ; laissez vide pour aucune notification de fin.", + "doc": "Services notify.* à appeler à la fin d'un cycle. Laissez vide pour aucun.", "label": "Services de fin" }, "notify_fire_events": { - "doc": "Déclenchez également des événements ha_washdata_* au début/fin du cycle afin que vous puissiez créer vos propres automatisations.", + "doc": "Émettre aussi des événements ha_washdata_* au début et à la fin du cycle, pour vos propres automatisations.", "label": "Déclencher des événements HA pour les notifications" }, "notify_icon": { - "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.", + "doc": "Petite icône de la notification (p. ex. mdi:washing-machine). Envoyée uniquement aux cibles mobile_app, pas aux groupes notify. Sur iOS, elle remplace l'icône de l'application et nécessite l'application compagnon 2026.8 ou plus récente. Vide : valeur par défaut de la plateforme.", "label": "Icône de notification" }, "notify_live_chronometer": { - "doc": "Affichez un compte à rebours avec mise à jour en direct dans la notification (sur les plates-formes qui le prennent en charge) au lieu d'une estimation statique.", + "doc": "Afficher un compte à rebours en direct dans la notification (si pris en charge) au lieu d'une estimation fixe.", "label": "Utiliser le chronomètre en direct" }, "notify_live_interval_seconds": { @@ -1365,143 +1337,135 @@ "label": "Intervalle de mise à jour en direct" }, "notify_live_overrun_percent": { - "doc": "Si un cycle dépasse son estimation de plus de ce pourcentage, envoyez une alerte de dépassement.", + "doc": "Les mises à jour en direct s'arrêtent dès qu'un cycle dépasse sa durée prévue de ce pourcentage (20 = jusqu'à 1,2x le temps prévu).", "label": "% de dépassement en direct avant alerte" }, "notify_live_services": { - "doc": "Les services notify.* ont appelé à des mises à jour de progression en direct pendant l'exécution d'un cycle. Laissez vide pour désactiver les notifications de progression en direct.", + "doc": "Services notify.* pour les mises à jour de progression en direct pendant un cycle. Laissez vide pour les désactiver.", "label": "Services de progression en direct" }, "notify_only_when_home": { - "doc": "N'envoyez des notifications que lorsqu'au moins une des personnes liées (ci-dessus) est à la maison.", + "doc": "Notifier uniquement quand au moins une des personnes ci-dessus est à la maison.", "label": "Notifier uniquement à la maison" }, "notify_people": { - "doc": "person.* entités utilisées par \"Notify Only When Home\" pour décider si quelqu'un est à la maison.", + "doc": "Entités person.* que « Notifier uniquement à la maison » vérifie.", "label": "Personnes (pour Notifier uniquement à la maison)" }, "notify_pre_complete_message": { - "doc": "Corps de l'alerte pré-fin / presque terminée. Variables du modèle : {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Texte de chaque mise à jour de progression en direct. Variables du modèle : {device}, {program}, {minutes} (minutes restantes). Avec toute autre variable, le texte par défaut est utilisé.", "label": "Message de pré-achèvement" }, "notify_reminder_message": { - "doc": "Corps du rappel de déchargement toujours en attente. Vide utilise la valeur par défaut intégrée. Variables du modèle : {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Texte de l'Alerte de pré-fin, envoyée une seule fois. Variables du modèle : {device}, {program}, {minutes} (le réglage Alerte de pré-fin). Avec toute autre variable, le texte par défaut est utilisé.", "label": "Message de rappel" }, "notify_start_message": { - "doc": "Corps envoyé au démarrage d'un cycle. Variables du modèle : {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Corps envoyé au démarrage d'un cycle. Variables du modèle : {device}, {program}. Avec toute autre variable, le texte par défaut est utilisé.", "label": "Message de démarrage" }, "notify_start_services": { - "doc": "notify.* services appelés lorsqu’un cycle démarre. Ajoutez-en un par cible (téléphone, tableau de bord, etc.) ; laissez vide pour aucune notification de démarrage.", + "doc": "Services notify.* à appeler au démarrage d'un cycle (téléphone, tableau de bord, ...). Laissez vide pour aucun.", "label": "Services de démarrage" }, "notify_timeout_seconds": { - "doc": "Ignorez automatiquement la notification après ce nombre de secondes (sur les plates-formes qui la prennent en charge). 0 le conserve jusqu'à ce qu'il soit supprimé manuellement.", + "doc": "Fermer la notification après ce nombre de secondes (si pris en charge). 0 la garde jusqu'à ce que vous la fermiez.", "label": "Masquer automatiquement après" }, "notify_title": { - "doc": "Titre de la notification. Variables du modèle : {device}, {program}, {duration}, {energy}, {cost}, {date}, {time}, {minutes}.", + "doc": "Titre de la notification. Seule {device} est remplacée ; avec toute autre variable, le titre par défaut est utilisé.", "label": "Titre de notification" }, "notify_unload_delay_minutes": { - "doc": "Quelques minutes après la fin d'un cycle avant d'envoyer le rappel \"décharger la machine\" toujours en attente. Définissez 0 pour désactiver le rappel.", + "doc": "Minutes après la fin d'un cycle avant le rappel « décharger la machine ». 0 le désactive.", "label": "Délai de rappel de déchargement" }, "notify_unload_repeat": { - "doc": "Continue de renvoyer le rappel de déchargement toutes les \"Délai de rappel de déchargement\" minutes jusqu'à ce que le déchargement soit confirmé ou que vous appuyiez sur \"Arrêter les rappels\" dans la notification. Nécessite un moyen de confirmer le déchargement : une Entité capteur de porte, une Entité de confirmation de déchargement ou l'option \"Confirmer le déchargement manuellement\" ci-dessous. Le bouton de rejet fonctionne sur les notifications de l'application compagnon (mobile) de Home Assistant.", + "doc": "Renvoyer le rappel de déchargement toutes les « Délai de rappel de déchargement » minutes jusqu'à ce que le déchargement soit confirmé ou que vous appuyiez sur « Arrêter les rappels » (application mobile). Nécessite une Entité capteur de porte, une Entité de confirmation de déchargement ou « Confirmer le déchargement manuellement ».", "label": "Répéter jusqu'au déchargement" }, "off_delay": { - "doc": "Temps d'attente après la coupure de courant avant de déclarer le cycle terminé. Si le courant revient dans cette fenêtre, le cycle continue de manière transparente – cela comble les pauses entre les étapes de lavage. Les lave-vaisselle ont de longues phases de séchage (mise hors tension pendant 20 à 60 minutes), le délai d'arrêt doit donc être supérieur à ce délai pour que l'ensemble du lavage et du séchage se déroule en un seul cycle.", + "doc": "Durée pendant laquelle la puissance doit rester basse avant que le cycle soit terminé. Si la puissance revient dans ce délai, le cycle continue. Les lave-vaisselle peuvent rester au repos 20 à 60 min pendant le séchage, réglez-le donc plus long que cela.", "label": "Délai d'arrêt" }, "pause_cuts_power": { - "doc": "Lorsqu'un cycle est mis en pause, désactivez également l'entité de commutation ci-dessous. Uniquement pour les appareils dont la fiche peut être coupée en toute sécurité en cours de cycle.", + "doc": "Quand un cycle est mis en pause, éteindre aussi l'Entité interrupteur ci-dessous. Uniquement pour les appareils pouvant perdre l'alimentation en cours de cycle sans risque.", "label": "La pause coupe aussi l'alimentation (via un interrupteur)" }, "power_off_delay": { - "doc": "Durée pendant laquelle la puissance doit rester sous le Seuil de mise hors tension après la fin d'un cycle avant que l'état ne repasse à Désactivé. Utilisé uniquement lorsque le Seuil de mise hors tension est supérieur à 0. Vérifié selon la cadence en arrière-plan, de sorte que le délai effectif est arrondi au prochain tick d'expiration d'état.", + "doc": "Durée pendant laquelle la puissance doit rester sous le Seuil de mise hors tension après un cycle avant que l'état repasse à Arrêt. Utilisé uniquement quand ce seuil est supérieur à 0. Vérifié périodiquement, l'attente réelle peut donc être un peu plus longue.", "label": "Délai de mise hors tension" }, "power_off_threshold_w": { - "doc": "Détection facultative de l'état Désactivé basée sur la puissance. Lorsqu'il est supérieur à 0, une fois un cycle terminé et si la puissance reste sous ce niveau pendant le Délai de mise hors tension, la machine est considérée comme éteinte et l'état repasse à Désactivé. Laissez à 0 pour désactiver (valeur par défaut). Réglez-le au-dessus de la consommation réelle à l'arrêt complet et en dessous du Seuil d'arrêt ainsi que de la consommation de veille (terminé mais toujours allumé) de votre machine ; s'il n'est pas inférieur au Seuil d'arrêt, il est ignoré. Lorsqu'elle est activée, cette option remplace le Délai de réinitialisation de progression pour le retour à Désactivé, de sorte qu'une machine terminée reste en Terminé/Propre jusqu'à ce qu'elle soit réellement éteinte.", + "doc": "Après un cycle, une puissance sous cette valeur pendant le Délai de mise hors tension fait passer l'état à Arrêt, au lieu du Délai de réinitialisation de progression. Réglez-la au-dessus de la consommation éteinte, sous la veille et le Seuil d'arrêt. 0 la désactive (par défaut).", "label": "Seuil de mise hors tension" }, "power_sensor": { - "doc": "L'entité de capteur signalant la puissance réelle en watts pour cet appareil (par exemple, sensor.washer_power). Toute détection de cycle est basée sur ce signal.", + "doc": "Le capteur qui indique la puissance actuelle de cet appareil en watts (p. ex. sensor.washer_power). Toute la détection repose sur lui.", "label": "Capteur de puissance" }, - "profile_duration_tolerance": { - "doc": "La bande +/- autour d'une durée moyenne de profil utilisée lors de la mise en correspondance. 0,25 signifie qu'un profil de 60 minutes correspond à des cycles de 45 à 75 minutes.", - "label": "Tolérance de durée du profil" - }, "profile_match_interval": { - "doc": "À quelle fréquence tenter une correspondance de profil au cours d’un cycle en cours d’exécution. La valeur par défaut de 300 s (5 minutes) équilibre la vitesse de détection et le processeur.", + "doc": "Fréquence à laquelle un cycle en cours est comparé aux profils. 300 s (5 min) par défaut.", "label": "Intervalle de correspondance" }, "profile_match_max_duration_ratio": { - "doc": "Durée maximale du cycle par rapport au profil. 1.3 signifie qu'un cycle doit être inférieur à 130 % de la durée du profil pour correspondre.", + "doc": "Un programme est écarté dès que le cycle dépasse ce multiple de sa durée habituelle. 1,8 : un cycle 80 % plus long que le programme ne lui correspond plus.", "label": "Ratio de durée maximale" }, "profile_match_min_duration_ratio": { - "doc": "Durée minimale du cycle par rapport au profil. 0,9 signifie qu'un cycle doit représenter au moins 90 % de la durée du profil pour correspondre.", + "doc": "Un programme n'est envisagé qu'une fois que le cycle a atteint cette fraction de sa durée habituelle. 0,10 : un programme de 2 heures devient candidat après 12 minutes. Non appliqué pendant les 15 premières minutes d'un cycle.", "label": "Ratio de durée minimale" }, "profile_match_threshold": { - "doc": "Score de similarité minimum (0-1) requis à la fin du cycle pour accepter une identification de programme. Augmentez-le pour réduire les fausses identifications ; baissez-le si les programmes de votre machine ne correspondent pas. La valeur par défaut 0,4 est un point de départ conservateur.", + "doc": "Score de correspondance en direct dont un cycle a besoin avant que la Terminaison intelligente ou la fin anti-froissement puisse l'arrêter plus tôt. Ne change pas le programme affiché. Augmentez-le si des correspondances faibles arrêtent des cycles trop tôt ; baissez-le si des correspondances nettes attendent tout le délai d'arrêt.", "label": "Seuil de correspondance" }, "profile_unmatch_threshold": { - "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.", + "doc": "Si le score de correspondance en direct passe sous cette valeur, l'hypothèse de programme actuelle est effacée. Gardez-le un peu sous le Seuil de correspondance pour qu'une brève baisse ne la réinitialise pas.", "label": "Seuil de non-correspondance" }, "progress_reset_delay": { - "doc": "Une fois terminé, maintenez la progression à 100 % pendant ce temps afin que Terminé soit visible sur les tableaux de bord avant de réinitialiser en mode veille.", + "doc": "Après un cycle, garder la progression à 100 % pendant ce délai avant de revenir à Inactif.", "label": "Délai de réinitialisation de progression" }, "pump_stuck_duration": { - "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).", + "doc": "Durée pendant laquelle une pompe peut tourner sans arrêt avant le déclenchement de l'événement de pompe bloquée.", "label": "Durée de blocage de la pompe" }, "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.", + "doc": "Temps minimal entre deux mesures traitées. Les mesures arrivées plus tôt sont ignorées (les mesures basses pendant un cycle sont toujours conservées). Réglez-le sur l'intervalle de remontée de votre prise ou moins ; une valeur plus élevée ne fait que jeter des mesures.", "label": "Intervalle d'échantillonnage" }, "save_debug_traces": { - "doc": "Stockez la trace de puissance complète et les données de débogage correspondantes pour chaque cycle. Utile pour le dépannage mais augmente la taille du stockage.", + "doc": "Enregistrer aussi avec chaque cycle les données de débogage du moteur de correspondance (les scores de chaque candidat), pour le dépannage. Ajoute environ 20 Ko par cycle.", "label": "Enregistrer les traces de débogage" }, "smart_termination_duration_ratio": { - "doc": "À quel point un cycle doit avoir progressé dans la durée prévue du programme correspondant avant que la Terminaison intelligente puisse le terminer plus tôt une fois la puissance retombée. La durée prévue est la moyenne du programme ; ainsi, sur les appareils dont le temps de fonctionnement varie beaucoup - lave-linge selon l'eau d'arrivée froide en hiver ou tiède en été, sèche-linge à sonde d'humidité, programmes dépendant de la charge - environ la moitié des cycles se terminent plus tôt que cette moyenne et ne bénéficient jamais de la fin rapide, s'achevant seulement via le délai de secours avec plusieurs minutes de retard. Abaissez cette valeur (p. ex. 0,85) sur ces machines pour que la fin anticipée se déclenche quand même ; augmentez-la vers 1,0 pour être plus prudent. Laissez vide pour la valeur par défaut (0,98, ou 0,99 pour les lave-vaisselle). Elle ne peut que terminer un cycle plus tôt, jamais plus tard, et ne se déclenche jamais sur une correspondance ambiguë ou peu fiable.", + "doc": "Fraction de la durée moyenne du programme qu'un cycle doit atteindre avant que la Terminaison intelligente puisse l'arrêter plus tôt. Si les durées varient (température de l'eau, séchage par sonde, taille de la charge), baissez-la (p. ex. 0,85). Laissez vide pour 0,98 (0,99 sur les lave-vaisselle).", "label": "Ratio de terminaison intelligente" }, - "smoothing_window": { - "doc": "Dans quelle mesure le signal de puissance brute est lissé. Low (2) est réactif mais bruyant ; high (5) lisse les pointes mais ajoute du décalage.", - "label": "Fenêtre de lissage" - }, "start_duration_threshold": { - "doc": "La puissance doit rester au-dessus du seuil de démarrage aussi longtemps pour confirmer un démarrage réel, empêchant ainsi les bascules marche/arrêt d'une fraction de seconde de démarrer un cycle.", + "doc": "La puissance doit rester au-dessus du Seuil de démarrage aussi longtemps avant qu'un cycle démarre, pour ignorer les brefs à-coups marche/arrêt.", "label": "Durée de démarrage" }, "start_energy_threshold": { - "doc": "Énergie (puissance x temps) que l'appareil doit consommer avant de FONCTIONNER. Un bref pic de puissance élevée a une énergie très faible et est ignoré, évitant ainsi les faux départs.", + "doc": "Énergie que l'appareil doit consommer avant que le cycle soit considéré en marche. Un bref pic de puissance consomme peu d'énergie, il ne démarre donc pas de cycle.", "label": "Énergie de démarrage" }, "start_threshold_w": { - "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.", + "doc": "La puissance doit dépasser cette valeur pour démarrer un cycle. Trop bas : la veille provoque de faux démarrages. Trop haut : les programmes à démarrage lent (remplissage à froid) sont manqués.", "label": "Seuil de démarrage" }, "stop_threshold_w": { - "doc": "La puissance doit descendre en dessous de ce niveau avant que le compte à rebours ne commence. Réglez-le en dessous du seuil de démarrage - l'écart entre eux est la bande d'hystérésis qui empêche le scintillement. Si elles sont réglées trop haut, des phases de faible puissance (attentes de rinçage, anti-froissement) déclenchent faussement la séquence de fin.", + "doc": "La puissance doit passer sous cette valeur avant que le délai d'arrêt commence à compter. Gardez-le sous le Seuil de démarrage. Trop haut, et les phases à faible puissance (pauses de rinçage, anti-froissement) lancent par erreur le compte à rebours de fin.", "label": "Seuil d'arrêt" }, "switch_entity": { - "doc": "Interrupteur optionnel désactivé en pause et réactivé lors de la reprise, utilisé avec \"La pause coupe également l'alimentation\".", + "doc": "Interrupteur facultatif éteint à la pause et rallumé à la reprise, avec « La pause coupe aussi l'alimentation ».", "label": "Entité interrupteur" }, "watchdog_interval": { - "doc": "À quelle fréquence le chien de garde en arrière-plan vérifie les capteurs bloqués et les délais d'attente écoulés. Par défaut 30 s.", + "doc": "Fréquence à laquelle la vérification en arrière-plan cherche les capteurs figés et les délais expirés. 30 s par défaut.", "label": "Intervalle du chien de garde" }, "notify_milestone_message": { @@ -1509,127 +1473,82 @@ "label": "Message de jalon" }, "notify_milestones": { - "doc": "Nombres de cycles séparés par des virgules qui déclenchent une notification de célébration unique lorsqu'ils sont atteints (p. ex. 50, 100, 500, 1000). Vide désactive les notifications de jalons.", + "doc": "Nombres de cycles déclenchant une notification de célébration unique, séparés par des virgules (p. ex. 50, 100, 500, 1000). Vide les désactive.", "label": "Jalons de cycles" }, "notify_quiet_end_hour": { - "doc": "Fin de la plage Ne pas déranger (0-23). Les notifications retenues sont envoyées à cette heure. Laissez vide pour désactiver.", + "doc": "Fin de la plage Ne pas déranger (0-23) ; les notifications retenues sont alors envoyées. Vide la désactive.", "label": "Fin des heures silencieuses" }, "notify_quiet_start_hour": { - "doc": "Début d'une plage Ne pas déranger (0-23). Les notifications de fin, de rappel et de linge propre qui se déclencheraient pendant les heures silencieuses sont retenues et envoyées à la fin de la plage. Laissez vide pour désactiver. Prend en charge les plages qui passent minuit (p. ex. début 22, fin 7).", + "doc": "Début d'une plage Ne pas déranger (0-23). Les notifications de fin, de rappel et de linge propre dans cette plage sont retenues jusqu'à sa fin. Peut passer minuit (p. ex. de 22 à 7). Vide la désactive.", "label": "Début des heures silencieuses" }, "peak_rate_message": { - "doc": "Texte personnalisé facultatif pour le conseil de tarif de pointe ajouté à la notification de démarrage. Variables du modèle : {device}, {price}. Vide utilise la valeur par défaut intégrée.", + "doc": "Texte personnalisé pour l'astuce heure pleine. Variables de modèle : {device}, {price}. Vide utilise le texte par défaut.", "label": "Message de tarif de pointe" }, "peak_rate_threshold": { - "doc": "Lorsqu'un cycle démarre et que le prix actuel par kWh est égal ou supérieur à cette valeur, un conseil de tarif de pointe est ajouté à la notification de démarrage. 0 ou vide désactive le conseil.", + "doc": "Si le prix par kWh est égal ou supérieur à cette valeur au démarrage d'un cycle, une astuce heure pleine est ajoutée à la notification de démarrage. 0 ou vide la désactive.", "label": "Seuil de tarif de pointe (par kWh)" }, - "dtw_bandwidth": { - "label": "Tolérance de correspondance de forme", - "doc": "Ampleur de la déformation temporelle qu'un cycle peut subir pour s'ajuster à la forme d'un programme enregistré. Une valeur élevée tolère davantage de variations de vitesse ; une valeur trop élevée risque de correspondre au mauvais programme." - }, - "corr_weight": { - "label": "Poids forme / niveau", - "doc": "Équilibre entre la forme de la courbe et le niveau de puissance absolu lors de l'évaluation d'une correspondance. Une valeur élevée privilégie la forme ; une valeur faible privilégie la correspondance du niveau de puissance." - }, - "duration_weight": { - "label": "Poids de la durée", - "doc": "À quel point un programme dont la durée habituelle correspond à ce cycle est privilégié." - }, - "energy_weight": { - "label": "Poids de l'énergie", - "doc": "À quel point un programme dont la consommation d'énergie habituelle correspond à ce cycle est privilégié." - }, "store_brand": { "label": "Marque de l'appareil", - "doc": "Facultatif. La marque de l'appareil, choisie dans le catalogue de la communauté. Utilisée pour trouver et partager des enregistrements de référence correspondants. Laissez vide si vous n'utilisez pas les fonctionnalités en ligne." + "doc": "Facultatif. La marque de votre appareil dans le catalogue communautaire, pour trouver et partager des enregistrements de référence. Laissez vide si vous n'utilisez pas les fonctions en ligne." }, "store_model": { "label": "Modèle de l'appareil", - "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." + "doc": "Facultatif. Le modèle de votre appareil dans le catalogue communautaire (choisissez d'abord une marque). S'il n'est pas listé, vous pouvez l'ajouter." }, "show_contributor": { "doc": "Afficher l'attribution « par » sur les appareils de la communauté et les cycles de référence." }, - "enable_phase_matching": { - "label": "Temps restant tenant compte des phases", - "doc": "Découpe chaque cycle en cours en phases (chauffage, lavage, essorage) et répartit le temps restant par phase, mélangé à l'estimation classique - en s'appuyant sur la répartition par phases en début de cycle et sur l'estimation classique vers la fin. Cela personnalise le compte à rebours en fonction de la durée réelle de chauffage et de fonctionnement de votre machine, ce qui est le plus perceptible dans la première moitié du cycle. Désactivé = uniquement l'estimation classique. Seul l'affichage du temps restant est concerné ; la reconnaissance des programmes et la détection des cycles restent inchangées." - }, - "keep_min_score": { - "label": "Score min de correspondance", - "doc": "Score de similarité minimum qu'un candidat doit atteindre pour rester dans la course. Le défaut 0,1 est délibérément permissif - il admet même les candidats faibles et s'appuie sur les étapes suivantes pour trouver la meilleure correspondance. Augmentez-le pour éliminer les profils improbables plus tôt ; réduisez-le pour élargir le pool de candidats initial." - }, - "dtw_blend": { - "label": "Fusion Warp", - "doc": "Dans quelle mesure le score d'alignement par déformation temporelle (DTW) remplace le score central de l'étape 2. 0 = utiliser uniquement le score de l'étape 2, 1 = utiliser uniquement le score DTW, 0,5 (défaut) = mélange égal. Augmentez-le pour vous fier davantage à l'alignement Warp lorsque les programmes ont des niveaux de puissance similaires mais des schémas temporels différents." - }, - "dtw_ensemble_w": { - "label": "Mix Warp ensemble", - "doc": "En mode DTW ensemble (le défaut), le poids sur L1 normalisé par rapport au DTW dérivé (DDTW). 1,0 = tout L1 normalisé, 0 = tout DDTW, 0,7 = défaut. La variante L1 normalisée compare les niveaux de puissance ; la variante dérivée réagit à la façon dont la puissance évolue dans le temps. Ensemble mélange les deux." - }, - "dtw_ddtw_scale": { - "label": "Échelle Warp dérivé", - "doc": "Distance de demi-saturation pour la notation DTW dérivé. Le score est divisé par deux quand la distance DDTW atteint cette valeur. Plus petit = plus sensible aux différences de forme. Le défaut 30 est calibré sur les traces de puissance typiques des appareils. N'affecte que les modes DTW ensemble et ddtw." - }, - "dtw_refine_top_n": { - "label": "Nb candidats affinés Warp", - "doc": "Nombre de candidats de l'étape 2 réévalués par DTW. DTW est plus coûteux, il n'est donc appliqué qu'aux N meilleurs candidats. Défaut 5. Augmentez-le (à 7-9) si le profil correct n'atteint parfois que la 4e ou 5e place après l'étape 2 - DTW peut le sauver. Baisser accélère légèrement la mise en correspondance." - }, - "duration_scale": { - "label": "Précision durée", - "doc": "Précision de la pénalité de concordance de durée à l'étape 4. C'est le log-ratio auquel le score de concordance est divisé par deux. Plus petit = plus strict : un écart de durée pénalise davantage. Le défaut 0,175 correspond à environ 18 % de tolérance de durée à mi-poids. À combiner avec la Pondération de durée." - }, - "energy_scale": { - "label": "Précision énergie", - "doc": "Précision de la pénalité de concordance d'énergie. Plus petit = plus strict : un écart d'énergie pénalise davantage. Le défaut 0,25 est intentionnellement plus indulgent que la Précision de durée, car l'énergie varie avec la charge. À combiner avec la Pondération d'énergie." - }, "power_profile_interval_min": { "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." + "doc": "Taille des tranches de l'attribut power_profile (watts moyens par tranche) utilisé par des planificateurs comme EMHASS et tibber_prices. Plus petit conserve les pics courts ; plus grand lisse la courbe. 15 min par défaut. 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." + "doc": "iOS uniquement. Mettre à jour la progression en direct en silence, avec une priorité économe en batterie que iOS peut regrouper. La première mise à jour et les notifications de début et de fin alertent toujours. Désactivé : chaque mise à jour alerte." }, "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." + "doc": "Android uniquement. Garder la notification de progression en direct quand on appuie dessus. Désactivé : un appui la ferme." }, "notify_live_click_action": { "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." + "doc": "Ce qu'ouvre un appui sur la notification de cet appareil. Vide : le panneau WashData sur cet appareil. Saisissez un chemin de tableau de bord (p. ex. /lovelace/laundry) ou une URL, ou « none » pour aucune action." }, "door_opens_at_end": { "label": "La porte s'ouvre automatiquement en fin de cycle", - "doc": "Pour les lave-vaisselle qui ouvrent la porte en fin de cycle pour le séchage (AirDry et fonctions similaires). Avec cette option activée, une ouverture de porte sur un cycle en cours ne le met plus en pause indéfiniment ; si la porte reste ouverte pendant la durée d'attente ci-dessous, WashData considère le cycle comme terminé. Une ouverture brève (pour ajouter un article) est ignorée. Nécessite une entité capteur de porte." + "doc": "Pour les lave-vaisselle qui ouvrent la porte en fin de cycle pour sécher (p. ex. AirDry). Une porte restée ouverte pendant le délai ci-dessous termine le cycle au lieu de le mettre en pause ; une brève ouverture est ignorée. Nécessite une Entité capteur de porte." }, "door_end_dwell_seconds": { "label": "Durée d'attente porte ouverte", - "doc": "Durée pendant laquelle la porte doit rester ouverte avant que WashData ne termine le cycle, lorsque « La porte s'ouvre automatiquement en fin de cycle » est activé. Assez longue pour ignorer l'ajout rapide d'un article (par défaut 60 s), assez courte pour terminer rapidement une fois que la machine ouvre la porte." + "doc": "Avec « La porte s'ouvre automatiquement en fin de cycle » activé : durée pendant laquelle la porte doit rester ouverte avant la fin du cycle. Assez longue pour ignorer l'ajout d'une assiette (60 s par défaut)." }, "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." + "doc": "Quels types de cycles façonnent la courbe et la correspondance de chaque programme. Décocher un type ne supprime rien ; utile si vous ne faites pas confiance aux données importées. Les statistiques ne comptent toujours que les cycles de cette machine. Tout décocher est ignoré." }, "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." + "doc": "Facturer chaque cycle au prix de chaque instant de son fonctionnement, au lieu du prix à la fin. Nécessite une entité de prix. Traiter l'historique (Diagnostic) recalcule les anciens cycles." }, "unload_confirm_entity": { "label": "Entité de confirmation de déchargement", - "doc": "Entité facultative dont l'activation signifie que la charge a été sortie, pour les machines qui ne peuvent pas avoir de capteur de porte. N'importe quel type convient : un bouton Zigbee ou NFC, une aide input_button, un détecteur de mouvement devant la machine, une scène. Tout changement d'état compte comme une confirmation, sauf l'extinction et le passage à indisponible. Définir cette entité active aussi le rappel de déchargement sur un appareil sans Entité capteur de porte." + "doc": "Facultatif. Toute entité qui confirme que la charge a été sortie, pour les machines sans capteur de porte (un bouton, input_button, détecteur de mouvement, scène). Chaque changement d'état compte, sauf le passage à éteint ou indisponible. Active aussi le rappel de déchargement." }, "unload_track_without_door": { "label": "Confirmer le déchargement manuellement", - "doc": "Exécute le rappel de déchargement sur un appareil sans Entité capteur de porte ni Entité de confirmation de déchargement, et vous confirmez le déchargement depuis votre propre automatisation : appuyez sur l'entité bouton \"Marquer comme déchargé\" ou appelez le service ha_washdata.mark_unloaded. Ignoré lorsqu'une Entité capteur de porte est définie, car elle fournit déjà le signal." + "doc": "Utiliser le rappel de déchargement sans capteur de porte ni entité de confirmation. Confirmez le déchargement depuis votre propre automatisation avec le bouton « Marquer comme déchargé » ou le service ha_washdata.mark_unloaded. Ignoré si une Entité capteur de porte est définie." }, "notify_icon_color": { "label": "Couleur de notification", - "doc": "Couleur d'accentuation des notifications de cet appareil, afin de distinguer d'un coup d'œil un lave-linge, un sèche-linge et un lave-vaisselle. iOS colore l'icône ainsi que la barre de progression de l'activité en direct sur l'écran verrouillé et dans la Dynamic Island. Android reçoit la même couleur, mais l'usage qu'il en fait dépend du téléphone : certains colorent l'icône, et beaucoup (Samsung One UI en particulier) l'ignorent totalement. Le format hexadécimal (par exemple #03A9F4) est compris par les deux. Laissez vide pour la valeur par défaut de la plateforme." + "doc": "Couleur d'accent des notifications de cet appareil, pour distinguer facilement lave-linge, sèche-linge et lave-vaisselle. Sur iOS, elle remplit le cercle derrière l'icône et colore la barre de la Live Activity. Beaucoup de téléphones Android (surtout Samsung) l'ignorent. Utilisez un code hex, p. ex. #03A9F4. Vide : valeur par défaut de la plateforme." + }, + "share_name": { + "doc": "Afficher le nom de votre compte comme auteur des cycles que vous partagez. Désactivé : partagés sans nom." } }, "setting_group": { @@ -1657,9 +1576,6 @@ "door_pause": { "label": "Porte et pause" }, - "dtw_refinement": { - "label": "Affinement DTW" - }, "duration_gates": { "label": "Portes de durée" }, @@ -1736,13 +1652,18 @@ "rebuilding": "Reconstruction…", "analyzing": "Analyse…", "resetting": "Réinitialisation…", - "reverting": "Annulation…", "trimming": "Rognage…", "splitting": "Fractionnement…", + "stalled": "Bloqué", "deleting": "Suppression…", "imported": "Importé", "preparing": "Préparation…", - "unlabelling": "Retrait de l'étiquette…" + "unlabelling": "Retrait de l'étiquette…", + "working": "Traitement…", + "match_uncertain": "Incertain : {top} ou {runner}", + "match_uncertain_single": "Incertain : peut-être {top}", + "match_sure": "~{pct} % de certitude", + "match_uncertain_tip": "WashData n'a pas encore arrêté son choix de programme. Le chiffre indique à quelle fréquence l'hypothèse en tête, {top}, était juste dans les cycles passés quand elle menait d'autant. À titre indicatif uniquement." }, "tab": { "advanced": "Avancé", @@ -1772,6 +1693,11 @@ "wrong_profile": "Mauvais profil" }, "toast": { + "task_name_required": "Donnez un nom à la tâche", + "task_added": "Tâche ajoutée", + "task_save_failed": "Impossible d'enregistrer la tâche : {error}", + "task_removed": "Tâche retirée", + "maint_logged": "Marqué comme fait : {task}", "catalog_refreshed": "Catalogue de la communauté rafraîchi", "access_saved": "Contrôle d'accès enregistré", "all_wiped": "Toutes les données effacées", @@ -1779,7 +1705,6 @@ "auto_label_complete": "Étiquetage automatique terminé", "automation_deleted": "Automatisation supprimée", "correction_submitted": "Correction soumise", - "cycle_deleted": "Cycle supprimé", "cycle_force_stopped": "Cycle forcé arrêté", "cycle_labelled": "Cycle labellisé", "cycle_paused": "Cycle en pause", @@ -1795,11 +1720,9 @@ "group_deleted": "Groupe supprimé", "group_name_required": "Le nom du groupe est obligatoire", "group_saved": "Groupe enregistré", - "import_successful": "Importation réussie ; rechargement de l'intégration", "json_required": "Les données JSON sont requises", "legacy_removed": "Actions héritées supprimées", "logs_exported": "Journaux exportés", - "matching_reverted": "Les poids correspondants sont revenus aux valeurs par défaut", "min_2_profiles": "Sélectionnez au moins 2 profils pour un groupe", "ml_training_no_improvement": "Formation terminée : base de référence conservée (pas d'amélioration)", "models_reverted": "Les modèles sur l'appareil sont revenus à la référence", @@ -1809,8 +1732,8 @@ "pg_preset_loaded": "Préréglage « {name} » chargé", "pg_preset_saved": "Préréglage « {name} » enregistré", "pg_sugg_loaded": "{n} valeur(s) suggérée(s) chargée(s) - lancez le Bac à sable pour comparer", - "pg_sugg_ml_loaded": "{n} valeur(s) calibrée(s) ML chargée(s) - lancez le Bac à sable pour comparer", "phase_name_required": "Le nom de la phase est obligatoire", + "phase_reset": "Phase « {name} » restaurée dans sa version intégrée", "phase_updated": "Phase mise à jour", "phases_saved": "Phases enregistrées", "preferences_saved": "Préférences enregistrées", @@ -1857,15 +1780,11 @@ "store_name_required": "Saisissez un nom de profil", "store_import_failed": "Échec de l'importation : {error}", "store_imported": "Importé dans {profile}", - "store_program_required": "Saisissez un nom de programme", "store_no_appliance": "Définissez d'abord la marque et le modèle de votre appareil dans les Paramètres.", "store_share_failed": "Échec du partage : {error}", "store_shared": "Partagé sur la boutique communautaire, en attente de révision.", "appliance_added": "Appareil ajouté, en attente d'approbation", "contrib_device_changed": "Ajouté à la boutique communautaire, mais l'appareil sélectionné a changé pendant que le formulaire était ouvert. Sélectionnez-le dans les Paramètres.", - "thanks_confirming": "Merci d'avoir confirmé", - "device_approved": "Approuvé par la communauté", - "rating_saved": "Évaluation de la qualité enregistrée", "brand_added": "Marque ajoutée, en attente d'approbation", "profile_added": "Profil ajouté, en attente d'approbation", "saved_except_conflicts": "Enregistré. Corrigez les conflits mis en évidence pour enregistrer le reste.", @@ -1888,11 +1807,52 @@ "odometer_saved": "Total des cycles enregistré", "odometer_save_failed": "Impossible d'enregistrer le total : {error}", "store_pick_profile_share": "Choisissez un profil dans lequel partager", - "remove_failed": "Échec du retrait : {error}" + "remove_failed": "Échec du retrait : {error}", + "analysis_complete": "Analyse terminée : {count} suggestion(s)", + "analysis_failed": "Échec de l'analyse : {error}", + "auto_detect_failed": "Échec de la détection automatique : {error}", + "auto_label_failed": "Échec de l'étiquetage automatique : {error}", + "could_not_load_cycle": "Impossible de charger le cycle : {error}", + "create_failed": "Échec de la création : {error}", + "cycles_deleted": "{count} cycle(s) supprimé(s)", + "debug_cleared": "{count} traces de débogage effacées", + "discard_failed": "Échec de l'abandon : {error}", + "error": "Erreur : {error}", + "export_failed": "Échec de l'exportation : {error}", + "import_failed": "Échec de l'importation : {error}", + "invalid_json": "« {key} » n'est pas un JSON valide ; corrigez-le ou videz le champ avant d'enregistrer.", + "label_failed": "Échec de l'étiquetage : {error}", + "ml_training_promoted": "Entraînement terminé : {count} modèle(s) promu(s)", + "pause_failed": "Échec de la mise en pause : {error}", + "phase_created": "Phase « {name} » créée", + "phase_deleted": "Phase « {name} » supprimée", + "profile_created": "Profil « {name} » créé", + "rename_failed": "Échec du renommage : {error}", + "save_failed": "Échec de l'enregistrement : {error}", + "split_complete": "Divisé en {count} cycles", + "start_failed": "Échec du démarrage : {error}", + "stop_failed": "Échec de l'arrêt : {error}", + "update_failed": "Échec de la mise à jour : {error}", + "store_download_skipped": "{n} enregistrement(s) ignoré(s) : trop court(s), trop lacunaire(s) ou déjà sur votre appareil.", + "store_import_low_quality": "Cet enregistrement est trop court, trop lacunaire ou invraisemblable pour être importé.", + "store_import_duplicate": "Cet enregistrement est déjà sur votre appareil.", + "device_approved": "Approuvé par la communauté", + "thanks_confirming": "Merci d'avoir confirmé", + "rating_saved": "Évaluation de la qualité enregistrée", + "store_download_partial": "{n} programme(s) n'ont pas pu être téléchargé(s) ; réessayez plus tard.", + "import_no_restore_point": "Importé, mais aucun point de restauration n'a pu être enregistré, cette importation ne peut donc pas être annulée", + "import_undo_failed": "Impossible d'annuler l'importation : {error}", + "import_undone": "Importation annulée : {profiles} profil(s) et {cycles} cycle(s) restaurés", + "auto_label_cancelled": "Étiquetage automatique annulé après l'étiquetage de {count} cycles.", + "auto_label_cancelled_one": "Étiquetage automatique annulé après l'étiquetage de {count} cycle.", + "auto_label_cancelled_many": "Étiquetage automatique annulé après l'étiquetage de {count} cycles.", + "auto_label_cancelled_other": "Étiquetage automatique annulé après l'étiquetage de {count} cycles.", + "auto_label_done": "Étiquetage automatique terminé : {count} cycles étiquetés, {skipped} sans étiquette.", + "auto_label_done_one": "Étiquetage automatique terminé : {count} cycle étiqueté, {skipped} sans étiquette.", + "auto_label_done_many": "Étiquetage automatique terminé : {count} cycles étiquetés, {skipped} sans étiquette.", + "auto_label_done_other": "Étiquetage automatique terminé : {count} cycles étiquetés, {skipped} sans étiquette." }, "suggestion": { - "both_agree": "WashData recommande", - "calibrated_label": "Calibré", "observed_label": "Observé", "impact": { "auto_label_confidence": { @@ -1907,10 +1867,6 @@ "higher": "Clôture uniquement les cycles ayant consommé une énergie significative", "lower": "Clôture aussi les cycles courts ou à faible consommation" }, - "end_repeat_count": { - "higher": "Exige que le signal de fin se répète davantage – plus robuste, légèrement plus lent", - "lower": "Termine après moins de répétitions – détection plus rapide, risque de fausse fin accru" - }, "learning_confidence": { "higher": "N'apprend que des correspondances très confiantes – plus lent mais plus fiable", "lower": "Apprend à partir de plus de cycles ; le modèle peut être moins précis" @@ -1949,40 +1905,25 @@ } }, "reason": { - "auto_label_confidence": "Confiance au 15e centile de {cycles} étiquetages automatiques jamais corrigés par l'utilisateur ({p15}).", "completion_min_seconds": "La moitié de la durée p05 des cycles propres ({minutes} min) sur {cycles} cycles ; filtre les cycles fantômes.{excl}", - "dead_zone_batch": "Maintenu court (fenêtre médiane d'instabilité au démarrage sur {cycles} cycles = {s}s) pour qu'une vraie fin soit détectée rapidement.", - "dead_zone_from_dip": "D'après une chute de puissance précoce détectée à {s}s.", - "end_energy_default": "Valeur de référence recommandée par défaut pour le seuil de bruit en fin de cycle.", - "end_energy_false": "Énergie p95 des fausses fins ({p95}Wh) sur {cycles} cycles, plancher à 0.2% de l'énergie médiane d'un cycle ({floor}Wh).", - "end_energy_no_false": "Aucune fausse fin sur {cycles} cycles ; plancher à 0.2% de l'énergie médiane d'un cycle ({floor}Wh).", - "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.", + "match_interval_floored": "Le programme le plus court ({shortest}s) limiterait cette valeur à {cap}s, sous le minimum de {minimum}s, donc le minimum est utilisé : {persistence} correspondances consécutives prennent {budget}s, plus de {pct}% de ce programme. La correspondance ne tourne qu'à l'arrivée d'une mesure (médiane={median}s), cela 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.", "min_off_gap": "D'après {gaps} intervalles entre cycles observés (p05={p05}s). Plancher de l'appareil : {floor}s.", "min_off_gap_bridge": "Dimensionné pour couvrir la plus longue période calme au sein d'un cycle : {span}s + 60s de marge, à partir de {spans} intervalles couverts sur {cycles} cycles propres (reste sous votre intervalle minimal de {ceiling}s entre deux charges consécutives).", "min_power": "40% de la puissance active la plus basse au p05 ({p05}W) sur {cycles} cycles propres, pour garder le seuil d'arrêt sous la consommation réelle.{excl}", - "ml_auto_label": "Confiance la plus basse encore jugée propre par le modèle de qualité (p10 de {cycles} cycles propres = {p10}).", - "ml_end_repeat": "{false}/{total} cycles ({pct}%) ont eu une pause que le détecteur de fin a notée >50% ; exiger {val} confirmation(s) de fin.", - "ml_off_delay": "Pauses confirmées par le détecteur de fin : p95 {p95}s + 60s de marge, à partir de {pauses} pauses vérifiées par le modèle sur {cycles} cycles (plancher {floor}s).", "no_update_timeout": "D'après la cadence de mise à jour observée (p95={p95}s) * 20 (min 60s).", "off_delay_cadence": "D'après la cadence de mise à jour observée (p95={p95}s) * 5", "off_delay_device_floor": "Minimum de sécurité spécifique à l'appareil utilisé pour {device} ({floor}s).", "off_delay_generic_floor": "Minimum de sécurité générique utilisé ({floor}s).", "off_delay_pauses": "Dimensionné pour dépasser les vraies pauses : pause intra-cycle p95 {p95}s + 60s de marge, à partir de {pauses} pauses sur {cycles} cycles propres (plancher {floor}s).", - "profile_match_threshold": "Confiance p10 de {cycles} étiquetages automatiques corrects ({p10}) ; plancher sûr pour une validation en direct.", - "sampling_interval": "Intervalle de mise à jour médian observé sur {cycles} cycles propres ({si}s).{excl}", - "smoothing_window": "Dimensionné pour lisser ~30s de mesures à un échantillonnage de {si}s ({samples} échantillons).", - "start_duration": "Maintenu court (~un intervalle d'échantillonnage de {si}s) pour que la détection démarre le plus tôt possible tout en ignorant les pics ponctuels d'un seul échantillon.", - "thr_batch": "Maintenu juste au-dessus de la puissance active la plus basse au p05 sur {cycles} cycles ({p05}W) pour qu'un démarrage soit détecté le plus tôt possible et que le seuil d'arrêt reste sous la puissance de fonctionnement la plus basse de la machine.", - "tol_per_profile": "p75 de la variance de durée par profil sur {profiles} profils ({cycles} cycles) ; les profils resserrés ne sont pas pénalisés.", - "tol_pooled": "D'après la variance de durée regroupée de {cycles} cycles récemment étiquetés (écart p95={dev}).", - "watchdog": "Maintenu aussi bas que possible en toute sécurité (juste au-dessus de l'écart de mise à jour p95 de {p95}s et au moins 2x l'intervalle d'échantillonnage de {median}s, min 30s) pour détecter rapidement les blocages sans faux arrêts." + "standby_floor": "Chaque cycle récent consommait encore {idle}W à sa fin, donc un seuil en dessous ne pourra jamais détecter que l'appareil s'éteint. Maintenu au-dessus ; les {cycles} cycles propres enregistrés n'ont jamais chuté en dessous pendant plus de {pause}s.", + "thr_batch": "Juste au-dessus de la puissance active la plus basse (p05) sur {cycles} cycles ({p05}W), pour détecter les démarrages tôt et garder le seuil d'arrêt sous la puissance de fonctionnement la plus basse.", + "watchdog": "Maintenu aussi bas que possible en toute sécurité (juste au-dessus de l'écart de mise à jour p95 de {p95}s et au moins 2x l'intervalle d'échantillonnage de {median}s, min 30s) pour détecter rapidement les blocages sans faux arrêts.", + "end_energy_floor": "Relevé au plancher qu'impliquent le seuil d'arrêt ({stop} W) sur le délai d'arrêt ({off_delay} s) ; en dessous, le cycle ne peut se terminer que par une voie de secours.", + "max_duration_ratio_default": "Retour à la valeur par défaut : un plafond plus bas rejette le bon programme chaque fois qu'un cycle dure plus longtemps que d'habitude.", + "min_duration_ratio_default": "Retour à la valeur par défaut : un plancher plus haut empêche de reconnaître un cycle en cours tant qu'il n'est pas bien avancé." }, "exclusions": { "summary": "{total} cycle(s) mal détecté(s) exclu(s) : {parts}.", @@ -2000,13 +1941,18 @@ "no_power": "sans puissance", "no_active_power": "sans puissance active" } - } + }, + "now_value": "(actuellement {v})" }, "trend": { "down": "Tendance à la baisse", "up": "Tendance à la hausse" }, "maint": { + "salt": "Recharger le sel", + "rinse_aid": "Recharger le liquide de rinçage", + "lint_filter": "Nettoyer le filtre à peluches", + "condenser_clean": "Nettoyer le condenseur", "bearing_service": "Entretien des roulements", "descale": "Détartrer", "drum_clean": "Nettoyer le tambour", @@ -2018,12 +1964,14 @@ "status_tip": "Résultat du cycle : Terminé (fin naturelle), Interrompu (chute brusque de puissance), Arrêt forcé (manuel) ou Besoin d'un examen (retour en attente).", "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. 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.", + "energy_tip": "Énergie totale consommée (kWh), calculée à partir de la puissance au fil du temps.", + "cost_tip": "Coût énergétique de ce cycle, figé à la fin. Avec Coût pondéré dans le temps activé, chaque instant est facturé à son propre prix ; sinon au prix de la fin. Définissez un prix dans Paramètres pour remplir cette colonne.", "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.", - "cost_dynamic_tip": "Pondéré dans le temps : facturé au prix en vigueur pendant le cycle (prix effectif {price} {cur}/kWh)." + "cost_dynamic_tip": "Pondéré dans le temps : facturé au prix en vigueur pendant le cycle (prix effectif {price} {cur}/kWh).", + "dbg_conf": "Conf.", + "dbg_mae": "MAE", + "dbg_corr": "Corr." }, "stat": { "avg": "moy.", @@ -2047,7 +1995,10 @@ "notify_finish": "Une notification de fin serait envoyée.", "notify_milestone": "Une notification d'étape serait envoyée.", "notify_held": "Une notification a été différée pour les heures silencieuses.", - "finished": "Le cycle a atteint son état final et s'est terminé." + "finished": "Le cycle a atteint son état final et s'est terminé.", + "match_reverted": "Le programme est revenu en détection : son score est tombé bien en dessous de son propre pic.", + "verified_pause": "L'alignement en direct a confirmé (ou levé) une pause au sein du programme. Tant qu'elle est maintenue, le cycle n'est pas terminé.", + "stalled": "Le cycle est resté à sa consommation de veille plus longtemps que ce programme ne fait jamais de pause à cet endroit, alors qu'il restait du travail. Affiché comme En pause (Bloqué) ; le cycle reste ouvert." }, "store": { "search_ph2": "Rechercher par marque ou modèle…", @@ -2056,11 +2007,7 @@ "programs_count": "Programmes : {n}", "browse": "Parcourir", "device": "Appareil", - "favorites": "Favoris", - "search_ph": "Rechercher par marque ou modèle…", "no_results": "Aucun appareil correspondant trouvé. Essayez une autre recherche.", - "n_programs": "{n} programmes", - "n_cycles": "{n} cycles", "no_programs": "Aucun programme partagé pour cet appareil pour le moment.", "no_cycles": "Aucun cycle de référence partagé pour ce programme pour le moment.", "anon": "anonyme", @@ -2072,19 +2019,21 @@ "merge_target": "Fusionner dans le profil", "description": "Description (facultatif)", "contributed_by": "par {name}", + "website": "Site de la boutique ↗", + "website_tip": "Ouvrir le site de la boutique communautaire", + "favorites": "Favoris", "rating_summary": "★ {avg} ({n})", "no_ratings": "Aucune évaluation pour le moment", - "website": "Site de la boutique ↗", - "website_tip": "Ouvrir le site de la boutique communautaire" + "unreachable": "Impossible de joindre la boutique communautaire. Vérifiez votre connexion et réessayez." }, "settings": { "gear": { - "title": "Paramètres" + "title": "Préférences" } }, "link": { - "manual": "Manuel ↗", - "add_appliance": "Ajouter votre appareil" + "maintenance_supporter": "Maintenance Supporter sur GitHub ↗", + "manual": "Manuel ↗" }, "tip": { "add_brand": "Marque absente de la liste ? Ajoutez-la au catalogue de la communauté", @@ -2118,7 +2067,13 @@ "rebuild": { "envelopes": "Reconstruction des enveloppes" }, - "cancelling": "Annulation..." + "cancelling": "Annulation...", + "auto_label": { + "running": "Étiquetage automatique des cycles" + }, + "store_download": { + "running": "Téléchargement depuis la boutique" + } }, "setup": { "hdr": { @@ -2127,26 +2082,24 @@ }, "phase0": { "washer": "WashData détecte déjà vos cycles. Enregistrez votre premier cycle pour activer les noms de programmes et les estimations de durée.", - "dishwasher": "WashData surveille. Les lave-vaisselle ont des cycles complexes – l'enregistrement du premier cycle est fortement recommandé. Si un cycle détecté dure trop longtemps, utilisez l'éditeur de cycles pour le rogner avant de l'enregistrer comme profil.", + "dishwasher": "WashData observe. Les cycles de lave-vaisselle sont complexes, enregistrez donc votre premier cycle. Si un cycle détecté dure trop longtemps, rognez-le dans l'éditeur de cycles avant de l'enregistrer comme profil.", "generic": "WashData surveille. Enregistrez ou étiquetez un cycle détecté pour commencer à créer des profils." }, "phase1a": { - "labelled": "Bon début – votre premier programme est enregistré. Pour des données optimales, envisagez d'enregistrer votre prochain cycle avec le widget d'enregistrement." + "labelled": "Bon début : votre premier programme est enregistré. Pour des données plus propres, enregistrez votre prochain cycle avec l'enregistreur." }, "phase1b": { - "recorded": "Votre enregistrement a été sauvegardé sous {profile_name}. Enregistrez ou étiquetez maintenant vos autres programmes courants pour améliorer la couverture." + "recorded": "Votre enregistrement a été sauvegardé sous {profile_name}. Enregistrez ou étiquetez maintenant vos autres programmes courants." }, "phase1c": { - "verify": "Vous avez {count} programmes de la communauté. Lancez un cycle pour vérifier que WashData le reconnaît correctement – la correspondance s'améliorera au fil de l'historique de votre appareil." + "verify": "Vous avez {count} programmes de la communauté. Lancez un cycle pour vérifier que WashData le reconnaît ; la correspondance s'améliore à mesure que votre appareil constitue son propre historique." }, "phase2": { - "cluster": "WashData a observé {count} cycles qui ne correspondent à aucun programme enregistré – ils se ressemblent entre eux. Voulez-vous créer un nouveau profil pour eux ?", + "cluster": "WashData a vu {count} cycles similaires qui ne correspondent à aucun programme enregistré. Créer un nouveau profil pour eux ?", "unmatched": "Votre dernier cycle ne correspond à aucun programme enregistré. S'agit-il d'un nouveau programme ?" }, "phase3": { - "suggestions": "WashData a des recommandations de paramètres basées sur votre historique de cycles – examinez-les pour améliorer la précision de détection.", - "groups": "Certains de vos profils ressemblent au même programme à différentes températures. Organisez-les en groupe pour une meilleure correspondance.", - "phases": "Ajoutez des phases de programme à {profile_name} pour des estimations de temps restant plus précises." + "suggestions": "WashData a des suggestions de réglages tirées de votre historique de cycles. Consultez-les pour améliorer la détection." }, "phase4": { "healthy": "Cet appareil est entièrement configuré ({profile_count} profils)." @@ -2159,8 +2112,6 @@ "create_from_cluster": "Créer un profil à partir de ces cycles", "create_profile": "Créer un profil pour ce programme", "review_suggestions": "Examiner les suggestions", - "organise_profiles": "Organiser les profils", - "configure_phases": "Configurer les phases", "skip_step": "Ignorer cette étape", "skip_forever": "Ne plus afficher", "hide_guidance": "Masquer le guide", @@ -2171,6 +2122,7 @@ "min": "min", "entity_not_found": "Entité introuvable", "running_long": "Dépassement", + "stalled": "Bloqué", "remaining": "restant", "progress": "progression", "show_time_remaining": "Afficher le temps restant", @@ -2219,5 +2171,53 @@ "off_delay": { "effective": "Un cycle terminé attend en réalité {eff} s, et non {delay} s : l'Intervalle d'arrêt minimal ({gap} s) est plus long et prime." } + }, + "access": { + "edit": "Modification", + "full": "Complet", + "inherit": "Hériter", + "none": "Aucun (masqué)", + "read": "Lecture" + }, + "pg_label_reason": { + "no_winner": "Aucun programme ne correspond", + "below_floor": "Sous la Confiance d'apprentissage", + "ambiguous": "Correspondance signalée comme ambiguë", + "margin": "Trop proche du deuxième candidat", + "unknown_profile": "Le programme n'existe plus", + "not_evaluated": "Non évalué" + }, + "diagram": { + "accum": "cumul", + "auto": "auto", + "below_off": "en dessous = arrêt", + "confirmed": "confirmé", + "energy_counts": "énergie comptée", + "feedback": "révision", + "gap_below_min": "intervalle sous le min = un cycle", + "heat_phase": "phase de chauffe", + "held_at_100": "maintenu à 100 %", + "ignored": "ignoré", + "kept": "conservé", + "max": "max", + "min": "min", + "minus_tol": "-tol", + "no_match": "non reconnu", + "no_updates": "aucune mise à jour", + "off_delay_wait": "délai d'arrêt", + "off_gap": "intervalle", + "plus_tol": "+tol", + "profile": "profil", + "reading_interval": "intervalle de lecture typique", + "sensor_offline": "capteur hors ligne : arrêt forcé", + "si": "IE", + "spike_ignored": "pic ignoré", + "spike_ignored_short": "pic : ignoré", + "start": "démarrage", + "stop": "arrêt", + "tail_energy_resets": "énergie de fin > seuil : délai relancé", + "thr": "seuil", + "too_short": "trop court", + "tumble_kept": "culbutages conservés" } } diff --git a/custom_components/ha_washdata/translations/panel/hr.json b/custom_components/ha_washdata/translations/panel/hr.json index 255e8b44..a01ec14a 100644 --- a/custom_components/ha_washdata/translations/panel/hr.json +++ b/custom_components/ha_washdata/translations/panel/hr.json @@ -3,6 +3,7 @@ "min": "min", "entity_not_found": "Entitet nije pronađen", "running_long": "Traje predugo", + "stalled": "Zaglavljeno", "remaining": "preostalo", "progress": "napredak", "show_time_remaining": "Prikaži preostalo vrijeme", @@ -48,16 +49,17 @@ } }, "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_tip": "Pri kraju ovaj program utihne na oko {mins} min, a zatim povuče oko {watts} W tijekom {secs} s. Uočeno u {seen} od {measured} izmjerenih ciklusa.", "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", + "artifact_tip": "Otkriveno je {n} anomalija (npr. vrata otvorena usred ciklusa) – otvorite da ih vidite na grafikonu", + "artifact_tip_one": "Otkrivena je {n} anomalija (npr. vrata otvorena usred ciklusa) – otvorite da je vidite na grafikonu", + "artifact_tip_few": "Otkrivene su {n} anomalije (npr. vrata otvorena usred ciklusa) – otvorite da ih vidite na grafikonu", + "artifact_tip_other": "Otkriveno je {n} anomalija (npr. vrata otvorena usred ciklusa) – otvorite da ih vidite na grafikonu", "auto": "(automatski otkriveno)", "built_in_tag": "ugrađeni", "declining": "↘ u opadanju", "energy_low": "Manja energija nego inače", "energy_spike": "Veća energija nego inače", - "fair_fit": "prihvatljiva kvaliteta", - "fair_fit_tip": "Umjerena dosljednost podudaranja – neki ciklusi dodijeljeni ovom profilu imaju niže rezultate pouzdanosti. Označite više ciklusa ili ponovno snimite profil kako biste poboljšali točnost.", "feedback_requested": "Zatražene povratne informacije", "golden_cycle": "Snimljeni referentni ciklus", "duration_outlier": "Ciklusi neobične duljine: {n}", @@ -66,27 +68,38 @@ "needs_review": "Potreban pregled", "overrun": "Trajao duže nego inače", "poor_fit": "loše pristaje", - "poor_fit_tip": "Nedosljedna povijest podudaranja - razmislite o ponovnoj izradi ovog profila", - "restart_gap_tip": "{n} HA praznina ponovnog pokretanja: trag snage ima rupu", + "restart_gap_tip": "{n} HA praznina ponovnog pokretanja: trag snage ima rupe", + "restart_gap_tip_one": "{n} HA praznina ponovnog pokretanja: trag snage ima rupu", + "restart_gap_tip_few": "{n} HA praznine ponovnog pokretanja: trag snage ima rupe", + "restart_gap_tip_other": "{n} HA praznina ponovnog pokretanja: trag snage ima rupe", "reviewed": "Pregledano", "steady": "→ postojan", "underrun": "Završio brže nego inače", - "using_defaults": "Korištenje isporučenih zadanih vrijednosti", - "using_tuned": "Korištenje podešenih težina", "overrun_ratio": "({x}x očekivanog)", "underrun_ratio": "({pct}% očekivanog)", - "awaiting_tip": "Čeka odobrenje zajednice", - "awaiting_n": "Čeka odobrenje · potvrđeno {n}", - "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.", - "armed": "(primjenjuje se na sljedeći ciklus)" + "armed": "(primjenjuje se na sljedeći ciklus)", + "awaiting_tip": "Čeka odobrenje zajednice", + "awaiting_n": "Čeka odobrenje · potvrđeno {n}", + "awaiting": "Čeka odobrenje", + "approved": "Odobreno", + "matcher_unused": "Još bez podudaranja s vašim ciklusima", + "matcher_used": "Podudaranje u {n} od vaših ciklusa", + "matcher_used_one": "Podudaranje u {n} od vaših ciklusa", + "matcher_used_few": "Podudaranje u {n} od vaših ciklusa", + "matcher_used_other": "Podudaranje u {n} od vaših ciklusa", + "matcher_used_tip": "Ciklusi na ovom uređaju koje je WashData sam podudario s ovim uvezenim programom. Uvoz koji se nikad ne podudari možda ne odgovara vašem uređaju; možete ga ukloniti. Broji se lokalno; ništa se ne šalje u trgovinu." }, "btn": { + "pg_preset_delete": "Izbriši preset", + "pg_preset_load": "Učitaj preset", + "pg_preset_load_tip": "Zamijeni vrijednosti u pješčaniku ovim presetom (aktivne postavke ostaju nepromijenjene)", + "pg_preset_save": "Spremi kao preset", + "pg_preset_save_tip": "Spremi sve vrijednosti ispod kao imenovani preset za ovaj uređaj", "reset_zoom": "Poništi zumiranje", "set_brand_model": "Postavi marku i model", "refresh_catalog": "Osvježi katalog", @@ -97,12 +110,11 @@ "add_timer": "+ Dodaj mjerač vremena", "apply_all": "Primijeni sve", "apply_label": "Primijeni oznaku", - "apply_set_b": "Primijeni skup B", "apply_split": "Primijeni podjelu", "apply_trim": "Primijeni obrezivanje", "auto_detect_split": "Automatsko otkrivanje", "auto_label_cycles": "Ciklusi automatskog označavanja", - "auto_label_cycles_tip": "Automatski dodjeljuje nazive profila neoznačenim ciklusima čija pouzdanost podudaranja prelazi prag", + "auto_label_cycles_tip": "Imenujte neoznačene cikluse čija pouzdanost podudaranja prelazi prag.", "cancel": "Otkaži", "clear_debug": "Očisti podatke o otklanjanju pogrešaka", "clear_splits": "Očisti", @@ -112,8 +124,6 @@ "convert_to_automation": "Pretvori u automatizaciju", "correct": "Ispravi…", "create": "Izradi", - "create_from_cluster": "Izradi profil iz {count} ciklusa", - "create_profile": "+ Izradi profil", "delete": "Izbriši", "delete_group": "Izbriši grupu", "delete_group_tip": "Izbriši samo ovu grupu - profili članova ostaju", @@ -122,39 +132,30 @@ "discard_tip": "Odbaci snimljeni trag bez pohrane", "dismiss": "Odbaci", "mute_suggestion": "Prestani predlagati ovu postavku", - "pg_load_calibrated": "Učitaj kalibrirano (ML) ({n})", - "pg_load_calibrated_tip": "Učitaj ML-kalibrirane postavke kao zamjene u pješčaniku", "pg_load_live": "Učitaj aktivne postavke", "pg_load_live_tip": "Ponovno učitaj trenutne postavke integracije i odbaci sve izmjene u pješčaniku", "pg_load_suggested": "Učitaj predloženo ({n})", "pg_load_suggested_tip": "Učitaj aktualne prijedloge automatskog podešavanja kao zamjene u pješčaniku", - "pg_preset_delete": "Izbriši preset", - "pg_preset_load": "Učitaj preset", - "pg_preset_load_tip": "Zamijeni vrijednosti u pješčaniku ovim presetom (aktivne postavke ostaju nepromijenjene)", - "pg_preset_save": "Spremi kao preset", - "pg_preset_save_tip": "Spremi sve vrijednosti ispod kao imenovani preset za ovaj uređaj", "pg_publish_all": "Spremi u integraciju ({n})", "pg_publish_one": "Spremi {label} u integraciju", "reset_muted": "Poništi utišane", + "reset_phase_tip": "Poništite svoju izmjenu i vratite ugrađenu fazu", "done": "Gotovo", "edit": "Uredi", "export": "Izvezi", - "export_json": "Izvoz u JSON", "force_stop": "Prisilno zaustavljanje", "force_stop_tip": "Odmah završi trenutni ciklus i označi ga kao prisilno zaustavljen", "from_template": "Iz predloška ▾", "ignore": "Zanemari", "import_json": "Uvoz iz JSON-a", - "import_overwrite": "Uvoz (prepisuje podatke)", "inspect": "Pregledaj", "label": "Označi", - "load": "Učitaj", "load_more": "Učitaj više", "manage": "Upravljaj", "merge": "Spoji", "new_automation": "+ Nova automatizacija", "new_group": "+ Nova grupa", - "new_group_tip": "Grupirajte skoro identične profile (isti oblik/trajanje, različita temperatura ili centrifuga) kako bi podudaranje pouzdano biralo između njih", + "new_group_tip": "Grupirajte gotovo iste profile (isti oblik, različita temperatura ili centrifuga) kako bi ih podudaranje moglo razlikovati.", "new_phase": "+ Nova faza", "new_profile": "+ Novi profil", "new_profile_tip": "Izradi novi profil programa iz postojećeg označenog ciklusa ili snimanja", @@ -162,23 +163,19 @@ "on_cycle_started": "Na početku ciklusa", "pause_cycle": "Pauza", "pause_cycle_tip": "Pauziraj pokrenuti ciklus – uređaj će se nastaviti od mjesta na kojem je stao", - "pg_apply_device": "Primijeni na uređaj", - "pg_autofill": "Automatski popuni", - "play": "Reproduciraj", "process": "Obradi", "process_history": "Obradi sada", "process_recording": "Spremi snimku", "process_tip": "Spremi snimljeni trag kao novi ili postojeći profil", "rebuild": "Ponovno izgradite omotnice", "rebuild_envelope": "Obnovite omotnicu", - "rebuild_tip": "Ponovo izračunaj očekivanu omotnicu snage (min/maks pojas) za sve profile iz njihovih označenih ciklusa – pokrenite nakon označavanja novih ciklusa ili ispravljanja starih", + "rebuild_tip": "Ponovno izgradite očekivani pojas snage svakog profila iz njegovih označenih ciklusa. Pokrenite nakon označavanja ili ispravljanja ciklusa.", "rec_start_tip": "Počni snimanje traga snage uređaja – pokrenite neposredno prije pokretanja ciklusa", "rec_stop": "Zaustavi", "rec_stop_tip": "Zaustavi snimanje i zadrži snimljeni trag radi pregleda", "record": "Započnite snimanje", "refresh": "Osvježi", "refresh_diag_tip": "Ponovo učitaj statistike pohrane", - "refresh_logs_tip": "Ponovo učitaj najnovije zapise dnevnika", "refresh_settings_tip": "Ponovo učitaj postavke s poslužitelja", "relabel": "Ponovno označi ({count})", "remove": "Ukloni", @@ -186,7 +183,6 @@ "reset": "Resetiraj", "reset_ml_models_tip": "Odbacite fino podešene modele i vratite se ugrađenim. WashData ih može kasnije ponovno naučiti.", "reset_to_builtin": "Vrati na ugrađene modele", - "reset_to_defaults": "Vrati na zadane postavke", "resume_cycle": "Nastavi", "resume_cycle_tip": "Nastavi pauzirani ciklus", "revert_settings": "Odbaci izmjene", @@ -199,8 +195,6 @@ "run_analysis": "🔍 Pokrenite analizu prijedloga", "run_analysis_tip": "Sada analizirajte svoje snimljene cikluse i osvježite prijedloge za ugađanje", "run_auto_label": "Pokrenite Auto-Label", - "run_both": "Pokreni A vs B", - "run_simulation": "Pokreni simulaciju", "save": "Spremi", "save_access_control": "Spremi kontrolu pristupa", "save_group": "Spremi grupu", @@ -212,10 +206,8 @@ "save_settings": "Spremi postavke", "select": "Odaberite", "select_all": "Odaberi sve", - "select_last20": "Zadnjih 20", "select_none": "Očisti", "show_only": "Prikaži samo", - "skip_setup": "Preskoči postavljanje", "split": "Podijeli", "stop": "Zaustavi", "submit_correction": "Pošaljite ispravak", @@ -224,30 +216,24 @@ "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.", + "unlabel_selected_tip": "Uklonite oznaku s odabranih ciklusa. Ostaju u povijesti, ali prestaju oblikovati ovaj profil.", "use": "Koristi", "wipe_all": "Obriši sve podatke", "delete_selected": "Izbriši odabrano ({n})", - "group_suggest": "Grupa {n}: {members}", "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", "import": "Uvezi", "disconnect": "Odspoji", "connect_github": "Poveži GitHub", - "save_appliance": "Spremi uređaj", "share_to_store": "Podijeli u trgovinu", "share": "Podijeli", - "confirm_appliance": "Potvrdi ovaj uređaj", - "confirmed": "Potvrđeno", "browse_community_setups": "Pregledaj postavke zajednice", "download_device": "Preuzmi postavke uređaja", "share_device": "Podijeli postavke uređaja", - "share_device_tip": "Podijelite programe i postavke ovog uređaja sa zajednicom", + "share_device_tip": "Podijelite ovaj uređaj i njegove referentne cikluse u trgovini zajednice kako bi ih vlasnici istog stroja mogli preuzeti.", "share_n": "Podijeli cikluse ({n})", "export_selected": "Izvoz (odaberi podatke)", "export_all": "Brzi izvoz svega", - "import_raw": "Napredno: zamijeni sve iz JSON datoteke", "download_export": "Preuzmi izvoz", "analyze_import": "Analiziraj datoteku", "import_selected": "Uvezi odabrano", @@ -260,7 +246,15 @@ "save_total_cycles": "Spremi ukupan broj", "clear": "Očisti", "add_brand": "Dodaj marku", - "add_appliance": "Dodaj uređaj" + "log_done_task": "Označi kao obavljeno: {task}", + "add_task": "Dodaj zadatak", + "add_appliance": "Dodaj uređaj", + "retry": "Pokušaj ponovno", + "confirm_appliance": "Potvrdi ovaj uređaj", + "confirmed": "Potvrđeno", + "create_profile_from_gap": "Stvori profil", + "undo_import": "Poništi zadnji uvoz", + "pg_rerun": "Ažuriraj simulaciju" }, "note": { "off_delay": { @@ -268,10 +262,6 @@ } }, "conflict": { - "anti_wrinkle_exit": { - "exit": "Mora biti ispod Praga zaustavljanja ({stop} W); inače se izlazna snaga anti-bora zanemaruje", - "stop": "Mora biti iznad Praga snage za izlaz anti-bora ({exit} W)" - }, "anti_wrinkle_max": { "max": "Mora biti iznad Praga pokretanja ({start} W); inače se limit trajanja anti-bora zaobilazi", "start": "Mora biti ispod Maks. snage anti-bora ({max} W)" @@ -309,14 +299,14 @@ "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." + "power_sensor": "Ovo je Senzor snage uređaja, a ne cijena. Ciklusi bi se obračunavali u vatima po kWh; očistite ovo polje.", + "energy_sensor": "Ovo je Brojilo energije uređaja, a ne cijena. Ciklusi bi se obračunavali po očitanju brojila po kWh; očistite ovo polje.", + "device_class": "Ovaj senzor mjeri {dc}, a ne cijenu po kWh. Očistite ovo polje ili odaberite senzor tarife.", + "unit": "Ovaj senzor očitava u {unit}, a ne cijenu po kWh. Očistite ovo polje ili odaberite 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" + "duration": "Mora biti manje od Vremenskog ograničenja bez ažuriranja ({to} s) kako bi alarm zaglavljene pumpe okinuo prvi", + "timeout": "Mora premašiti Trajanje zaglavljene pumpe ({ps} s) kako bi alarm zaglavljene pumpe okinuo prvi" }, "start_dur": { "sampling": "Interval uzorkovanja premašuje Trajanje pokretanja ({sdt} s); jednokratni impuls može otvoriti ciklus", @@ -334,10 +324,15 @@ "sampling": "Interval uzorkovanja trebao bi biti najviše pola Intervala nadzora ({wi} s)" }, "settings_banner": "Sukobi postavki: {n}. Provjerite istaknute odjeljke i ispravite ih prije spremanja.", - "settings_banner_btn": "Idi na prvi" + "settings_banner_btn": "Idi na prvi", + "end_energy": { + "energy": "Prestrogo za Prag zaustavljanja ({w} W) tijekom Odgode isključivanja ({d} s); ciklus može završiti samo rezervnim putem", + "stop": "Energija završetka ({e} Wh tijekom {d} s) dopušta samo {p} W" + } }, "hdr": { - "ab_comparison": "Usporedba postavki A/B", + "custom_tasks": "Vlastiti zadaci", + "maintenance_supporter": "Trebate više?", "access_control": "Kontrola pristupa", "add_maintenance": "Dodaj događaj održavanja", "automation_delete": "Izbriši ovu automatizaciju", @@ -346,28 +341,22 @@ "automation_review_this_cycle": "Pregledajte ovaj ciklus", "automations": "Automatizacije", "clear_debug": "Obrišite tragove otklanjanja pogrešaka", - "cycle_simulator": "Simulator ciklusa", "diagnostics_pref": "Dijagnostika", "display": "Prikaz", - "dtw_inspector": "DTW inspektor", "export_import": "Izvoz / uvoz", - "getting_started": "Početak rada", "live_power": "Snaga uživo", "logs": "Dnevnici", - "logs_diagnostics": "Dijagnostika", "maintenance": "Radnje održavanja", "maintenance_log": "Zapisnik održavanja", "maintenance_reminders": "Podsjetnici za servis", "manual_recording": "Ručno snimanje", - "manual_recording_tip": "Namjerno pokrenite ciklus dok WashData snima trag snage. Pokrenite neposredno prije nego što uređaj krene, zastavite kada završi, zatim obradite za pohranu kao imenovani profil.", + "manual_recording_tip": "Namjerno snimite ciklus: pritisnite Započnite snimanje neposredno prije pokretanja uređaja i Zaustavi kad završi, zatim Obradi za spremanje kao profil.", "ml_learned": "Što je WashData naučio", - "ml_matching_tuning": "Fino podešavanje usklađivanja programa", "ml_settings_card": "Postavke", "ml_smart_learning": "Pametno učenje", "my_preferences": "Moje postavke", "no_automations": "Još nijedna automatizacija ne spominje ovaj uređaj.", "panel_settings": "Postavke ploče", - "param_sweep": "Pretraživanje parametra", "pg_settings_source": "Izvor postavki", "phase_catalog": "Katalog faza", "playground": "Pješčanik", @@ -376,11 +365,8 @@ "status": "Status", "status_graph": "Grafikon stanja", "storage_stats": "Statistika pohrane", - "test_history": "Testiraj na povijesti", "toggle_sidebar": "Uključi/isključi bočnu traku kućnog pomoćnika", - "tools_and_data": "Alati i podaci", "wipe_history": "Obriši povijest", - "recommendations": "Preporuke ({n})", "pg_detection_params": "Postavke otkrivanja", "pg_outcome": "Ishod simulacije", "pg_across_cycles": "Kroz sve vaše cikluse", @@ -388,7 +374,8 @@ "online_account": "Zajednička trgovina i online značajke", "import_power_history": "Uvoz povijesti snage", "service_status": "Status servisa", - "online_account_tab": "Online i zajednica" + "online_account_tab": "Online i zajednica", + "live_match_debug": "Dijagnostika podudaranja uživo" }, "health": { "fair": "Prihvatljiva kvaliteta profila", @@ -396,11 +383,16 @@ "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_tip": "Položaj na snimljenoj krivulji podudarenog programa: {pct}%. Određuje se poravnavanjem ciklusa s profilom, pa ostaje točan kad je rad dulji ili kraći. Osvježava se dok je uređaj tih, pa 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", + "task_name": "Zadatak", + "every_cycles": "Interval (ciklusi)", + "every_days": "Interval (dani)", + "new_task_name": "Novi zadatak", + "days_since_service": "{since} / {total} dana", "actions": "Radnje", "activity": "Aktivnost", "administrators": "Administratori", @@ -408,10 +400,8 @@ "aria_cycle_chart": "Krivulja snage ciklusa", "aria_envelope_chart": "Ovojnica snage profila", "aria_phase_chart": "Grafikon uređivača faza", - "aria_playground_chart": "Krivulja snage ciklusa s pomičnim pragovima otkrivanja", "aria_power_chart": "Grafikon snage", "aria_spaghetti_chart": "Preklopljene krivulje snage ciklusa", - "aria_sweep_chart": "Grafikon rezultata pregleda parametra", "artifact_anomaly": "Anomalija", "artifact_high_power": "Visoka snaga", "artifact_interruption": "Prekid", @@ -425,151 +415,102 @@ "cohesion_good": "kohezija {pct}%", "cohesion_low": "⚠ niska kohezija {pct}%", "compare_profiles": "Usporedi s profilima", - "concurrency": "Istovremenost", "confidence": "Povjerenje", "confidence_threshold": "Prag povjerenja", "correct_duration": "Točno trajanje (min, izborno)", "correct_profile": "Ispravan profil", - "correlation": "Korelacija", "cost": "trošak", "create_new_profile": "+ Izradi novi profil…", "current_phase": "Trenutna faza", - "curves": "Krivulje", "cycle": "Ciklus", "cycle_date_display": "Prikaz datuma ciklusa", - "cycle_health": "Zdravlje ciklusa", - "cycle_index": "indeks ciklusa →", - "cycles": "Ciklusi", "cycles_count": "Ciklusi", "cycles_lc": "ciklusa", - "dashed": "isprekidano", "date": "Datum", "debug_traces": "Tragovi otklanjanja pogrešaka", - "default": "Zadano", "default_access_level": "Zadana razina za korisnike koji nisu dolje navedeni", "default_other": "Zadano (ostali uređaji)", "default_tab": "Zadana kartica pri otvaranju ploče", "font_size": "Veličina fonta ploče", - "delta": "Δ", "description": "Opis", - "dtw_blend_w": "Težina miješanja", - "dtw_blended": "Kombinirani rezultat", - "dtw_ddtw": "DDTW rezultat", - "dtw_l1": "L1 rezultat", - "dtw_matched_profile": "(profil podudaran s ciklusom)", "duration": "Trajanje", - "duration_agreement": "Podudaranje trajanja", - "emphasis": "Naglasak", "enable_access_control": "Omogući kontrolu pristupa po korisniku", "energy": "energija", - "energy_agreement": "Podudaranje energije", - "event_timeline": "Vremenska crta događaja", "event_type": "Vrsta događaja", "expected": "Očekivano", + "suspected_profile": "{name} (pretpostavka)", + "runner_up_profile": "{name} (drugi najbolji)", "expected_duration": "Očekivano trajanje (min)", "file_kb": "Datoteka (kB)", - "final_score": "Konačni rezultat", "from": "Od", "gap_s": "Razmak (s)", "group_name": "Naziv grupe", "head_trim": "Obrezivanje početka (s)", - "health": "Zdravlje", "hide_tabs": "Sakrij kartice za osobe koje nisu administratori", - "in_use": "U upotrebi", "json_data": "JSON podaci", "label": "Podudaranje", "last": "Zadnjih", "last_checked": "Zadnja provjera:", "load_from_file": "Učitaj iz datoteke", - "mae_score": "MAE rezultat", "manual_duration": "Ručno trajanje (min, izborno)", "match": "Podudaranje", "members": "Članovi", - "metric": "Metrika", "mode_existing_profile": "Dodaj u postojeći profil", "mode_new_profile": "Stvori novi profil", "models_fine_tuned": "(fino podešeni modeli: {count})", - "n_classic_suggestions": "{n} klasičan", - "n_ml_suggestions": "{n} ML", "n_selected": "{n} odabrano", - "n_tuning_suggestions": "{n} prijedlog podešavanja", - "na": "n/d", + "n_tuning_suggestions": "{n} prijedloga podešavanja", + "n_tuning_suggestions_one": "{n} prijedlog podešavanja", + "n_tuning_suggestions_few": "{n} prijedloga podešavanja", + "n_tuning_suggestions_other": "{n} prijedloga podešavanja", "new_profile_name": "Novo ime profila", "notes": "Bilješke", - "of": "od", - "outcome": "Ishod", "panel_default_tab": "Zadana kartica", "panel_language": "Jezik ploče", "per_cycle_short": "ciklus", - "pg_autofill_tip": "Automatski popuni raspon iz trenutne vrijednosti ({lo}–{hi})", "pg_best_value": "Pronađena najbolja vrijednost", "pg_current_value": "Trenutna vrijednost:", "pg_drag_hint": "Povucite liniju na grafikonu", "pg_anti_wrinkle": "Anti-bora", - "pg_dtw": "DTW", "pg_ev_ending": "Završava", "pg_ev_running": "U tijeku", "pg_group_advanced": "Rubni slučajevi", "pg_group_detection": "Okidači otkrivanja", "pg_group_timing": "Pravila vremena", "pg_idle": "Neaktivno", - "pg_last_run": "Zadnje pokretanje: {date}", - "pg_match_score": "Rezultat podudaranja", - "pg_pct_of": "{p}% od {total}", "pg_poor_match": "Loše podudaranje", "pg_preset": "Preset pješčanika", "pg_preset_name": "Ime novog preseta", "pg_preset_none": "Odaberite preset…", "pg_strong_match": "Snažno podudaranje", - "pg_tile_ambiguous": "Nejasno", - "pg_tile_ambiguous_tip": "Ciklusi s gotovo izjednačenim rezultatom između dva programa", - "pg_tile_detected": "Otkriveno", - "pg_tile_detected_tip": "Ciklusi kod kojih je snaga prešla prag pokretanja dovoljno dugo da se zabilježi kao rad", - "pg_tile_matched": "Podudareno", - "pg_tile_matched_tip": "Ciklusi ispravno pripisani poznatom programu", - "pg_time_left": "Preostalo vrijeme", "pg_weak_match": "Slabo podudaranje", "phase_name": "Ime faze", "name": "Ime", "phase_timeline": "Vremenska crta faza", "power": "Snaga", "profile": "Profil", - "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. Odaberite program prije pokretanja uređaja i primijenit će se čim započne sljedeći ciklus.", + "program_tip": "Odaberite s kojim se programom podudara trenutni ciklus. Automatsko otkrivanje bira najbolje podudaranje. Prikvačite program kad automatsko otkrivanje griješi; program odabran prije pokretanja vrijedi za sljedeći ciklus.", "progress": "Napredak", "raw_socket": "Sirovi signal", - "read_only": "Samo za čitanje", "ref_cycle": "Referentni ciklus (izborno)", - "refresh_interval": "Interval osvježavanja (s)", "remaining": "Preostalo", "reminder_every": "Podsjeti svakih (ciklusa)", "remove_label": "- Ukloni oznaku -", "rename_profile": "Preimenuj profil", - "replay_duration": "Reprodukcija", "resulting_profile": "Rezultirajući profil", "save_mode": "Način spremanja", - "score": "Rezultat", - "score_breakdown": "Raščlamba rezultata", "seconds_from_start": "Sekundi od početka", "select_profile": "Odaberite Profil", "setting": "Postavka", - "settings_a": "Postavke A (trenutne)", - "settings_a_short": "A", "settings_advanced": "Napredno", - "settings_b": "Postavke B (test)", - "settings_b_short": "B", "settings_basic": "Osnovno", "settings_detail_level": "Razina detalja postavki", - "settings_overrides": "Zamjene postavki", "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", - "stage2": "Faza 2 – osnovna sličnost", - "stage3": "Faza 3 – DTW", - "stage4": "Faza 4 – podudaranje", "status": "Status", "tail_trim": "Obrezivanje kraja (s)", "tap_to_dismiss": "dotaknite grafikon za zatvaranje", @@ -580,7 +521,6 @@ "ungrouped": "Negrupirano", "unlabelled": "Bez oznake", "unlabelled_paren": "(neoznačeno)", - "warp_path": "Putanja iskrivljenja", "input": "Ulaz:", "start": "Početak", "end": "Kraj", @@ -605,9 +545,8 @@ "n_shown": ", prikazano {n}", "cycles_title": "Ciklusi ({n})", "pg_detecting": "Otkrivanje", - "pg_best_marker": "Najbolji: {v}", "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", + "pg_time_left_model_tip": "Procijenjeno prema fazama programa, a ne fiksno odbrojavanje", "phase": "Faza", "pg_mode_history": "Testiraj na povijesti", "pg_ev_detected": "Otkriveno", @@ -647,18 +586,8 @@ "pg_mode_optimize": "Optimiziraj", "pg_group_matching": "Podudaranje programa", "pg_zoom_hint": "dvostruki klik poništava zumiranje", - "aria_playground_chart2": "Interaktivni grafikon snage ciklusa: prijeđite mišem za prikaz vremena/snage, pomičite kotačić za zumiranje, povucite za pomicanje", "pg_alert_timeout_end": "Završeno zbog isteka vremena, ne na temelju predviđanja", "pg_alert_indefinite": "Radio bi beskonačno", - "pg_alert_stress_cap": "Dostignut sigurnosni limit", - "pg_alert_stress_ok": "Detekcija mirovanja: ciklus zaustavljen", - "pg_alert_stress_warn": "Potrošnja u mirovanju iznad praga zaustavljanja", - "pg_stress_group": "Test detekcije mirovanja", - "pg_stress_idle_auto": "Auto", - "pg_stress_idle_w": "Razina mirovanja (W)", - "pg_stress_idle_w_desc": "Prepiši automatski otkrivenu donju granicu u stanju pripravnosti (ostavi prazno za automatsko otkrivanje).", - "pg_stress_toggle": "Testiraj detekciju mirovanja", - "pg_stress_toggle_desc": "Simulira da uređaj ostaje na potrošnji u stanju pripravnosti nakon završetka snimanja -- prikazuje ako i kada WashData zaustavi ciklus.", "task_pg_history": "Test na povijesti", "task_pg_sweep": "Optimiziraj", "task_reprocess": "Ponovna obrada", @@ -668,16 +597,9 @@ "pg_recent_runs": "Nedavna pokretanja", "pg_load_run": "Ponovno učitaj ovo pokretanje", "pg_match_confidence": "Pouzdanost podudaranja", - "pg_envelope_fit": "Uklapanje u ovojnicu", - "pg_envelope_fit_tip": "Koliko dobro ciklus ostaje unutar spremljene ovojnice snage ovog profila, drugačiji pogled od pouzdanosti podudaranja.", "n_imported_note": ", {n} uvezeno", "enable_online": "Omogući online značajke", - "store_brand": "Marka uređaja", - "store_model": "Model uređaja", "anonymous": "Anonimno", - "rate_quality": "Ocijeni kvalitetu", - "rate_n": "{n} zvjezdica", - "approved_only": "Samo odobreno", "adopt_settings": "Prihvati postavke", "flags": "Oznake", "include_phase_map": "Uključi kartu faza", @@ -694,7 +616,6 @@ "cat_custom_phases": "Prilagođene faze", "cat_profile_groups": "Grupe profila", "cat_settings": "Postavke otkrivanja i prepoznavanja", - "cat_matching_config": "Ugađanje prepoznavanja", "cat_ml_models": "ML modeli", "cat_feedback": "Oznake povratnih informacija i pregleda", "cat_suggestions": "Prijedlozi", @@ -714,7 +635,6 @@ "conflict_overwrite": "Prepiši", "hist_csv_data": "CSV podaci", "hist_from_recorder": "Ili je pročitajte iz Home Assistant", - "days": "dana", "hist_keep": "Zadrži ovaj ciklus", "hist_looks_complete": "završen", "peak_power_short": "Vrh", @@ -735,7 +655,42 @@ "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" + "correct_total_cycles": "Ispravi ukupan broj", + "hist_reason_already_recorded": "preklapa se s ciklusom koji je WashData već zabilježio", + "share_consent": "Potvrđujem da su ovi ciklusi završili normalno, bez prekida", + "show_internal": "Prikaži interne postavke", + "show_internal_tip": "Postavke koje WashData podešava umjesto vas (uzorkovanje, nadzor, učestalost podudaranja, pragovi). Prikazuju se kad ovaj uređaj ima vlastitu vrijednost, sukob ili prijedlog.", + "rate_n": "{n} zvjezdica", + "rate_quality": "Ocijeni kvalitetu", + "pg_autolabel": "Automatsko označavanje", + "pg_label_review": "Traži pregled", + "pg_ev_match_reverted": "Podudaranje poništeno", + "pg_ev_verified_pause": "Potvrđena pauza", + "n_samples": "{n} uzoraka", + "n_samples_one": "{n} uzorak", + "n_samples_few": "{n} uzorka", + "n_samples_other": "{n} uzoraka", + "n_cycles": "{n} ciklusa", + "n_cycles_one": "{n} ciklus", + "n_cycles_few": "{n} ciklusa", + "n_cycles_other": "{n} ciklusa", + "energy_per_cycle": "{v}/ciklus", + "energy_total": "ukupno {v}", + "automation_off": "(isključeno)", + "ref_cycle_none": "Nijedan", + "share_name": "Prikaži moje ime uz ono što dijelim", + "cat_backfill_cycles": "Ciklusi pronađeni u uvezenoj povijesti snage", + "pg_compare_profile": "Usporedi s profilom", + "pg_obj_early": "Stopa ranih završetaka", + "pg_obj_endlag": "Kašnjenje završetka (medijan)", + "pg_obj_split": "Stopa podijeljenih ciklusa", + "pg_sweep_cycles": "Nedavni ciklusi", + "cycle_context": "Povijest recordera oko ciklusa", + "cycle_context_off": "Isključeno", + "cycle_context_minutes": "{min} min", + "cycle_context_series": "Povijest recordera", + "before_start": "Prije početka", + "after_end": "Nakon završetka" }, "log": { "all_levels": "Sve razine", @@ -745,11 +700,9 @@ }, "ml": { "builtin_models": "● Korištenje ugrađenih modela", - "fit_fair": "Fer", "fit_good": "Dobro", "fit_slight": "Lagano", "fit_strong": "Jaka", - "fit_weak": "Slabo", "fit_word": "kvaliteta", "personalized": "● Prilagođeno ovom stroju", "better_than_baseline": "{pct}% bolje od osnovne procjene ({metric})", @@ -757,21 +710,31 @@ "trend_declining_tip": "Prilagodba ovog modela pogoršala se u nedavnim ponovnim provjerama, pregled više ciklusa može mu pomoći da ponovno nauči.", "trend_steady_tip": "Prilagodba ovog modela ostala je otprilike stabilna u nedavnim ponovnim provjerama.", "cap_blurb": { - "end": "Prepoznavanje kada je ciklus stvarno završio", - "live_match": "Brže prepoznavanje programa koji je u tijeku", - "quality": "Uočavanje pogrešno prepoznatih ili oštećenih ciklusa", - "remaining_time": "Predviđanje preostalog vremena", "total_energy": "Predviđanje ukupne potrošnje energije i troška" }, "cap_label": { - "end": "Prepoznavanje kraja ciklusa", - "live_match": "Prepoznavanje programa", - "quality": "Provjera kvalitete ciklusa", - "remaining_time": "Procjena preostalog vremena", "total_energy": "Procjena potrošnje energije" }, - "metric_auc": "AUC {auc} na izdvojenim podacima", - "metric_mae": "pogreška {model} u odnosu na osnovnu vrijednost {baseline}" + "metric_mae": "pogreška {model} u odnosu na osnovnu vrijednost {baseline}", + "fine_tuned_at": "fino podešeno {when}", + "held_out_count": "izmjereno na {count} izdvojenih ciklusa", + "held_out_count_one": "izmjereno na {count} izdvojenom ciklusu", + "held_out_count_few": "izmjereno na {count} izdvojena ciklusa", + "held_out_count_other": "izmjereno na {count} izdvojenih ciklusa", + "last_run_not_promoted": "Zadnja provjera ({when}): {reason}", + "never": "nikad", + "skip_reason": { + "fit_failed": "Treniranje nije uspjelo: {error}", + "holdout_too_small": "Premalo ciklusa za pošteno testiranje novog modela: za testiranje se moglo izdvojiti {held_out}, a potrebno je {min}.", + "insufficient_rows": "Još nema dovoljno upotrebljivih ciklusa: zasad {count} označenih, čistih ciklusa.", + "insufficient_rows_one": "Još nema dovoljno upotrebljivih ciklusa: zasad {count} označen, čist ciklus.", + "insufficient_rows_few": "Još nema dovoljno upotrebljivih ciklusa: zasad {count} označena, čista ciklusa.", + "insufficient_rows_other": "Još nema dovoljno upotrebljivih ciklusa: zasad {count} označenih, čistih ciklusa.", + "not_better_than_incumbent": "Novi model nije bio bolji od onog koji se koristi (pogreška {model} u odnosu na {incumbent}), pa je zadržan trenutni.", + "not_better_than_naive": "Novi model nije bio jasno bolji od jednostavne procjene (pogreška {model} u odnosu na {naive}).", + "unknown": "Ništa novo nije naučeno." + }, + "trained_unknown": "u nepoznato vrijeme" }, "modal": { "auto_label": "Ciklusi automatskog označavanja", @@ -780,7 +743,6 @@ "cycle": "Ciklus", "edit_group": "Uredi grupu profila", "edit_phase": "Uredi fazu", - "import_config": "Konfiguracija uvoza", "label_cycle": "Ciklus naljepnica", "merge_cycles": "Spoji {n} ciklusa", "new_group": "Nova grupa profila", @@ -795,6 +757,8 @@ "delete_automation_title": "Izbriši automatizaciju", "delete_automation_msg": "Izbrisati automatizaciju \"{name}\" iz Home Assistant? Ovo se ne može poništiti.", "remove_legacy_title": "Ukloni zastarjele akcije", + "reset_phase_msg": "Poništiti vašu izmjenu faze \"{name}\" i vratiti ugrađenu fazu? Programi zadržavaju svoje raspone.", + "reset_phase_title": "Resetiraj fazu", "remove_legacy_msg": "Ukloniti zastarjele prilagođene akcije? Prestat će se okidati na događajima ciklusa. Ovo se ne može poništiti iz ploče.", "discard_recording_title": "Odbaci snimku", "discard_recording_msg": "Odbaciti spremljenu snimku? Ovo se ne može poništiti.", @@ -815,32 +779,45 @@ "store_share_device": "Podijeli postavke uređaja", "export_select": "Izvoz - odaberi podatke", "import_wizard": "Uvoz - odaberi podatke", - "history_import": "Uvoz povijesti snage" + "history_import": "Uvoz povijesti snage", + "apply_suggestions_title": "Primijeni prijedloge", + "remove_task_title": "Ukloni zadatak", + "remove_task_msg": "Ukloniti podsjetnik \"{task}\"? Njegovi unosi u zapisniku održavanja ostaju sačuvani.", + "apply_suggestions_msg": "Promijeniti ove postavke ({n})? Integracija se ponovno učitava nakon spremanja.", + "discard_settings_title": "Odbaciti nespremljene promjene?", + "discard_settings_msg": "Nespremljene promjene postavki: {n}. Napustiti Postavke i odbaciti ih?", + "profile_title": "Profil · {name}", + "undo_import_msg": "Podaci i postavke ovog uređaja vraćaju se na stanje prije zadnjeg uvoza ({when}). Ciklusi zabilježeni i promjene napravljene od tada bit će izgubljeni.", + "undo_import_ok": "Poništi uvoz", + "undo_import_title": "Poništi zadnji uvoz" }, "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.", + "pg_preset_delete_confirm": "Izbrisati preset \"{name}\"?", + "pg_preset_limit": "Dosegnut limit preseta ({n})", + "pg_preset_overwrite": "Prepisati preset \"{name}\"?", "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.", - "refresh_catalog_hint": "Popisi marki i uređaja zajednice privremeno se spremaju kako bi zajednička trgovina ostala u okviru svog dnevnog ograničenja. Osvježite da preuzmete unose koje su drugi dodali ili odobrili.", + "store_sibling_hint": "Ništa nije podijeljeno za vaš model? Srodan model iste marke obično je dobar početak.", + "store_declare_appliance": "Recite WashData koji uređaj imate da vidite postavke koje su drugi podijelili za njega. Ili upišite marku iznad i razgledajte.", + "refresh_catalog_hint": "Popisi marki i uređaja zajednice privremeno se spremaju radi dnevnog ograničenja trgovine. Osvježite da vidite unose koje su drugi dodali ili odobrili.", "head_trim_hint": "Uklonite ovoliko sekundi s početka", "appliance_monitor": "Monitor uređaja", "artifact_dip_detail": "Palo ispod uobičajenog pojasa snage ~{n}s.", "artifact_footer": "Istaknuto na gornjem grafikonu. To su prolazni artefakti (npr. vrata su se otvorila usred ciklusa), a ne nužno problemi.", "artifact_header": "Tijekom ovog ciklusa otkriveno je {n} anomalija", - "artifact_pause_detail": "Snaga je pala na gotovo nulu za ~{n}s, a zatim se nastavila – vjerojatno su vrata otvorena usred ciklusa ili je ciklus bio pauziran.", + "artifact_header_one": "Tijekom ovog ciklusa otkrivena je {n} anomalija", + "artifact_header_few": "Tijekom ovog ciklusa otkrivene su {n} anomalije", + "artifact_header_other": "Tijekom ovog ciklusa otkriveno je {n} anomalija", + "artifact_pause_detail": "Snaga je pala gotovo na nulu na ~{n}s, a zatim se vratila. Vjerojatno su vrata otvorena ili je ciklus pauziran.", "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": "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.", + "cleanup_intro": "Svi označeni ciklusi jedan preko drugoga. Označite odstupajuće, 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)", - "compare_profiles_tip": "Prekrijte druge omotnice profila na gornjoj tablici kako biste vidjeli koja najbolje odgovara ovom ciklusu.", + "compare_profiles_tip": "Prekrijte grafikon drugim profilima da vidite koji najbolje odgovara ovom ciklusu.", "compare_selected_cycles": "Odabrani ciklusi (puno) – prikaži / sakrij", - "consider_new_profile": "Razmislite o stvaranju novog profila.", - "coverage_gap": "nedavni ciklusi nemaju odgovarajući profil ({pct}% od zadnjih 30).", - "coverage_gap_similar_cycles": "Pronađeno {count} sličnih neoznačenih ciklusa – izradite profil da biste ih počeli podudarati.", "cycles_deleted": "{count} ciklus(a) izbrisano", "enough_data": "Dovoljno podataka za učenje ({current}/{min} ciklusa).", "export_description": "Odaberite točno koje profile, cikluse, postavke i ostalo želite izvesti u JSON datoteku ili analizirajte datoteku i uvezite samo željene dijelove.", @@ -848,30 +825,27 @@ "feedback_prompt": "Potvrdite da je ispravno, ispravite program ili zanemarite.", "feedback_relabel_hint": "Ponovno označavanje ovog ciklusa također ga rješava.", "filter_by_profile": "Filtriraj po profilu…", - "group_modal_help": "Grupirajte programe istog oblika koji se razlikuju po temperaturi/centrifugi (trajanje može varirati). Spajanje ocjenjuje grupu kao jednog kandidata, a zatim odabire člana koji najbolje odgovara. Odaberite barem 2; sloj pokazuje koliko su slični.", + "group_modal_help": "Grupirajte programe istog oblika koji se razlikuju po temperaturi ili centrifugi. Podudaranje ocjenjuje grupu kao cjelinu, a zatim bira člana koji najbolje odgovara. Odaberite barem 2; preklapanje pokazuje koliko su slični.", "group_not_cohesive": "Ovi profili nisu dovoljno slični da bi se pouzdano grupirali, pa ih uparivanje tretira pojedinačno dok ne uklonite izuzetak ili ne podijelite grupu.", "group_preview_hint": "Označite 2 ili više članova za pregled i usporedbu krivulja snage.", - "import_intro": "Učitajte izvezenu datoteku ili zalijepite JSON sadržaj ispod.", - "legacy_actions_title": "{count} naslijeđena prilagođena radnja još uvijek se izvodi", + "legacy_actions_title": "{count} naslijeđenih prilagođenih radnji još uvijek se izvodi", + "legacy_actions_title_one": "{count} naslijeđena prilagođena radnja još uvijek se izvodi", + "legacy_actions_title_few": "{count} naslijeđene prilagođene radnje još uvijek se izvode", + "legacy_actions_title_other": "{count} naslijeđenih prilagođenih radnji još uvijek se izvodi", "live_chart_loading": "Grafikon snage uživo pojavljuje se kako očitanja stižu.", "loading": "učitavanje...", "loading_curve": "Učitavanje krivulje…", "loading_settings": "Učitavanje postavki…", "log_buffer_hint": "Prvo najnovije · sprema zadnjih 500 ha_washdata zapisa od ponovnog pokretanja · povucite donji rub za promjenu veličine.", - "maintenance_advisory": "Povećanje trajanja/energije može ukazivati na potrebu održavanja uređaja (npr. uklanjanje kamenca, čišćenje filtera).", "maintenance_due": "Održavanje na redu: {items}", - "maintenance_intro": "Bilježite servisiranje koje obavljate na ovom uređaju i primite podsjetnik kada svaki zadatak ponovno dođe na red.", + "maintenance_intro": "Bilježite servisiranje ovog uređaja i primite podsjetnik kad svaki zadatak ponovno dođe na red.", "maintenance_load_error": "Nije moguće učitati podatke o održavanju: {error}", "maintenance_requires_access": "Održavanje i izvoz/uvoz zahtijevaju puni pristup.", - "manual_duration_hint": "Prosječna/očekivana duljina ciklusa profila, koja se koristi za procjene preostalog vremena. Uredite da biste ga postavili; ostavljajući ga nepromijenjenim zadržava trenutnu vrijednost.", - "matching_tuning_intro": "Prilikom učenja, WashData također prilagođava koliko podudaranje programa teži obliku u odnosu na trajanje i energiju.", + "manual_duration_hint": "Očekivana duljina ciklusa profila, koristi se za procjenu preostalog vremena. Ostavite nepromijenjeno da zadržite trenutnu vrijednost.", "merge_intro": "Odabrani ciklusi se spajaju u jedan (kronološki red; praznine popunjene s 0 W). Odaberite rezultirajući profil.", "ml_intro": "WashData isporučuje s pametnim modelima koji rade odmah.", "ml_learned_intro": "Modeli prilagođeni ovom stroju.", - "ml_loading": "učitavanje ML-a…", "ml_settings_intro": "Dva neovisna prekidača: jedan primjenjuje modele dok ciklus radi, drugi omogućuje WashData da ih fino prilagodi vašem stroju tijekom vremena.", - "name_first_program": "Imate dovoljno ciklusa – imenujte svoj prvi program da započnete podudaranje.", - "near_duplicate_cluster": "otkriven je skoro duplikat profila. Grupiranje omogućuje uparivanje pouzdanog odabira sličnih programa (npr. isti program na različitoj temperaturi/centrifugi).", "no_cycles_match": "Nijedan ciklus ne odgovara trenutnom filtru.", "no_cycles_profile": "Nema ciklusa za ovaj profil.", "no_cycles_selected": "Prvo odaberite barem jedan ciklus.", @@ -896,79 +870,63 @@ "no_suggestions": "Nema aktivnih prijedloga.", "notify_services_hint": "Koristite ID-ove servisa {entity} (odvojene zarezima za više servisa). Varijable predloška: {vars}.", "old_actions_warning": "Konfiguriran sa starim uređivačem akcija (sada uklonjen). I dalje se aktiviraju na događaje ciklusa, ali se više ne mogu uređivati ovdje. Pretvorite ih u normalnu automatizaciju ili ih uklonite.", - "onboarding_progress": "{n} / 3 ciklusa promatrano", - "onboarding_watching": "Koristite uređaj uobičajeno – WashData promatra. Nakon 3 ciklusa započet će podudaranje programa.", "pending_feedback": "Povratne informacije o otkrivanju na čekanju", "performance_trend": "Trend izvedbe ({n} ciklusa)", - "pg_analysis_empty": "Učitajte ciklus za prikaz analize podudaranja.", "pg_apply_confirm": "Primijeniti najbolju vrijednost: {label} = {value}?", - "pg_canvas_empty": "Odaberite ciklus iznad, zatim pritisnite Učitaj za prikaz njegovog tijeka snage.", - "pg_chart_caption": "Stopa otkrivanja/podudaranja po vrijednostima {param}", "pg_ctrl_intro": "Vrijednosti polaze od aktivnih postavki integracije za ovaj uređaj. Izmjene ostaju u pješčaniku dok ih ne spremite u integraciju.", "pg_live_unavailable": "Aktivne postavke nisu dostupne, prikazane su zadane vrijednosti.", "pg_load_live_confirm": "Odbaciti izmjene u pješčaniku ({n}) i ponovno učitati trenutne postavke integracije?", - "pg_match_rate": "{pct}% stopa podudaranja na {total} ciklusa", "pg_matches_live": "Podudara se s aktivnim postavkama", "pg_n_changed": "Promijenjeno u odnosu na aktivne postavke: {n}", "pg_sugg_none": "Svi prijedlozi već odgovaraju trenutnim postavkama", - "pg_preset_delete_confirm": "Izbrisati preset \"{name}\"?", - "pg_preset_limit": "Dosegnut limit preseta ({n})", - "pg_preset_overwrite": "Prepisati preset \"{name}\"?", "pg_publish_one_confirm": "Spremiti {label} = {value} u postavke ovog uređaja?", "pg_restart_note": "Ponovno pokrenite Home Assistant kako biste omogućili alate za simulaciju.", - "pg_sim_progress": "{done} / {total} ciklusa", - "pg_starting": "Pokretanje…", - "pg_stress_synthetic": "simulacija mirovanja počinje ovdje", - "pg_sweep_intro": "Što ako bi {param} bio drugačiji? Testirajte {steps} vrijednosti na svojih zadnjih {cycles} ciklusa kako biste pronašli postavku pri kojoj se najviše ciklusa ispravno podudara.", - "pg_sweep_step": "Korak {done} / {total}", - "pg_verdict_bad": "Zahtijeva pozornost: mnogi ciklusi ostaju neotkriveni.", - "pg_verdict_good": "Dobro podešeno: većina ciklusa ispravno je prepoznata i podudarana.", - "pg_verdict_ok": "Prihvatljivo: neki ciklusi su propušteni. Pokušajte sniziti prag pokretanja.", "phase_catalog_intro": "Imenovani segmenti ciklusa (pretpranje, grijanje, centrifuga…). Dodijelite ih profilu s njegove upravljačke ploče.", "phase_ranges_intro": "Rasponi faza (minute od početka ciklusa) prekriveni prosječnom krivuljom. Uredite vrijednosti za pregled uživo.", - "playground_intro": "Sigurno okruženje za ponovno pokretanje prošlih ciklusa s različitim postavkama, usporedbu dviju konfiguracija jednu uz drugu i pregled kako se ciklus poravnava s profilom. Ništa ovdje ne mijenja vaše aktivne podatke dok ih izričito ne primijenite.", - "playground_needs_restart": "Ponovno pokrenite Home Assistant kako biste omogućili alate Pješčanika.", - "preferences_admin": "Postavke, panel i kontrola pristupa", - "preferences_adv": "Postavke", + "playground_intro": "Isprobajte postavke na stvarnim podacima svojih ciklusa. Ništa ovdje ne mijenja aktivne postavke dok ih ne primijenite.", "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 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.", + "process_history_hint": "Ponovno pokrenite podudaranje na svim spremljenim ciklusima, osvježite prijedloge, ponovno trenirajte ML modele (ako su omogućeni), preračunajte troškove ciklusa prema zabilježenim cijenama i ponovno izračunajte zdravlje ciklusa. Pokrenite 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.", - "rbac_hint": "Kada je isključen, svaki korisnik kućnog pomoćnika ima potpuni pristup (zadano). Administratori uvijek imaju puni pristup i mogu upravljati svima.", - "recent_logs": "Nedavni ha_washdata zapisi", + "rbac_hint": "Isključeno: svaki korisnik Home Assistanta ima puni pristup (zadano). Administratori uvijek imaju puni pristup i mogu upravljati svima.", "recording_in_progress": "Snimanje u tijeku", "reminders_intro": "Prikaži podsjetnik na ploči toliko ciklusa nakon posljednjeg servisa. Ostavite prazno ili 0 da isključite podsjetnik.", "restart_gap_footer": "Istaknuto na grafikonu. Podaci o snazi nedostaju za ove intervale; podudaranje je koristilo samo stvarne očitavanja.", "restart_gap_header": "{n} HA praznina ponovnog pokretanja tijekom ovog ciklusa", + "restart_gap_header_one": "{n} HA praznina ponovnog pokretanja tijekom ovog ciklusa", + "restart_gap_header_few": "{n} HA praznine ponovnog pokretanja tijekom ovog ciklusa", + "restart_gap_header_other": "{n} HA praznina ponovnog pokretanja tijekom ovog ciklusa", "restart_gap_item": "{dur} praznina", - "review_confirm_help": "Provjerite je li ovaj ciklus ispravno otkriven. Vaše recenzije uvježbavaju model na vašem stroju - što više ciklusa potvrdite, bolje se podudaraju i ocjenjuju zdravlje. Brzi Dobro/Loše je dovoljno.", + "review_confirm_help": "Je li ovaj ciklus ispravno otkriven? Potvrđivanje ciklusa poboljšava podudaranje za vaš stroj. Dovoljno je brzo Dobro/Loš.", "review_in_settings": "Pregledajte u Postavkama", "review_notes_placeholder": "Bilješke (nije obavezno)", "review_notes_tip": "Bilješke slobodnog teksta za vlastitu referencu. Ne koristi se za spajanje ili obuku.", - "review_profile_tip": "Program kojim je ovaj ciklus označen. Ako je automatski detektirani program bio pogrešan, ispravite ga ovdje - označavanje uči podudaranje za buduće cikluse.", - "review_quality_tip": "Kako je ovaj ciklus čist. Dobar = školski primjer ovog programa; Loše = otkriveno, ali bučno ili netipično; Neupotrebljivo = pogrešno otkriveno (spojeno, skraćeno ili lažno). Upravlja ocjenom zdravlja i ciklusima koji su dopušteni za obuku modela.", - "review_recorded_tip": "Označite ovo kao ručno odabran referentni ciklus za svoj program - ista uloga kao ručno snimljeni ciklus. Referentni ciklusi se uvijek čuvaju, postavljaju odgovarajući predložak i nikada se ne odbacuju čišćenjem. (Ovo je \"zlatna\"/snimljena zastava; obje su iste stvari.)", - "review_tags_tip": "Neobavezne oznake koje opisuju što je pošlo po zlu s ovim ciklusom, tako da obuka i čišćenje mogu objasniti to.", + "review_profile_tip": "Program kojim je ovaj ciklus označen. Ako je pogrešan, ispravite ga ovdje; oznake uče podudaranje.", + "review_quality_tip": "Koliko je ovaj ciklus čist. Dobro: školski rad ovog programa. Loš: otkriven, ali bučan ili neobičan. Neupotrebljiv: pogrešno otkriven (spojen, skraćen ili lažan). Utječe na ocjenu zdravlja i na to koji ciklusi treniraju model.", + "review_recorded_tip": "Označite kao ručno odabran referentni ciklus njegova programa, kao ručno snimljeni ciklus. Referentni ciklusi uvijek se čuvaju, čine predložak podudaranja i čišćenje ih nikad ne uklanja.", + "review_tags_tip": "Neobavezne oznake o tome što je pošlo po zlu, kako bi ih treniranje i čišćenje mogli uzeti u obzir.", "review_to_cycles": "Otvorite red čekanja za pregled ciklusa", "samples_decimated": "Prikazano {shown} od {total} uzoraka (prorijeđeno za prikaz; vrhovi zadržani). Široki razmak ovdje znači prorjeđivanje, a ne nedostajuće podatke.", - "saving_triggers_reload": "Spremanje pokreće ponovno učitavanje integracije. HA entiteti mogu se nakratko prikazati kao nedostupni.", + "saving_triggers_reload": "Spremanje ponovno učitava integraciju. Entiteti mogu nakratko biti nedostupni.", "search_placeholder": "Postavke pretraživanja…", "see_recorder": "Pogledajte widget za snimanje u nastavku", "setting_changed": "Promijenjeno s {old} na {new} dana {date}", "settings_basic_note": "Prikazuju se osnovne postavke. Prebacite na Napredno za cijeli popis.", - "shape_drift_advisory": "⚠ Oblik se mijenja", - "shape_drift_detail": "Uzorak snage ovog profila promijenio se s vremenom – moguće habanje uređaja ili potrebno održavanje (npr. uklanjanje kamenca, čišćenje filtera).", "show_all_settings": "Prikaži sve postavke", - "showing_suggestions": "Prikazuje se {count} postavka s prijedlozima.", - "split_intro": "Pritisnite grafikon da biste dodali ili uklonili točku razdvajanja ili automatsko otkrivanje praznina u mirovanju. Svaki rezultirajući segment može dobiti svoj profil.", - "storage_diagnostics": "Statistika pohrane, održavanje, izvoz/uvoz", + "showing_suggestions": "Prikazuje se {count} postavki s prijedlozima.", + "showing_suggestions_one": "Prikazuje se {count} postavka s prijedlozima.", + "showing_suggestions_few": "Prikazuju se {count} postavke s prijedlozima.", + "showing_suggestions_other": "Prikazuje se {count} postavki s prijedlozima.", + "split_intro": "Kliknite grafikon da dodate ili uklonite točku razdvajanja, ili je automatski pronađite po prazninama mirovanja. Svaki dio može dobiti vlastiti profil.", "sug_staged": "Postavljeno {key} = {val}. Spremi za primjenu.", "sug_muted": "Ova postavka više neće biti predlagana", "sug_mute_failed": "Nije moguće utišati prijedlog", "sug_unmuted_all": "Utišani prijedlozi poništeni", - "n_suggestions_muted": "{count} utišano; automatski podešavač neće predlagati ove postavke.", + "n_suggestions_muted": "{count} prijedloga je utišano; automatski podešavač ih neće predlagati.", + "n_suggestions_muted_one": "{count} prijedlog je utišan; automatski podešavač ga neće predlagati.", + "n_suggestions_muted_few": "{count} prijedloga su utišana; automatski podešavač ih neće predlagati.", + "n_suggestions_muted_other": "{count} prijedloga je utišano; automatski podešavač ih neće predlagati.", "toast_auto_detect_enabled": "Automatsko otkrivanje omogućeno", "toast_auto_label_complete": "Automatsko označavanje dovršeno", "toast_auto_label_failed": "Automatsko označavanje neuspješno: {error}", @@ -984,7 +942,6 @@ "toast_no_automation": "Ovdje nije moguće stvoriti automatizaciju", "toast_profile_name_required": "Naziv profila je obavezan", "toast_program_set": "Program postavljen: {program}", - "toast_rebuild_failed": "Obnova neuspješna: {error}", "toast_resume_failed": "Nastavak neuspješan: {error}", "toast_revert_failed": "Vraćanje neuspješno: {error}", "toast_reverted": "{key} vraćeno", @@ -995,95 +952,81 @@ "trend_energy_up": "Energija raste ({pct}%/ciklus) – nedavni prosjek {avg}", "trim_destructive_confirm": "Ovo zadržava samo {pct}% ciklusa i ne može se poništiti. Nastaviti?", "trim_intro": "Povucite crvene ručice ili unesite vrijednosti. Sve izvan prozora je uklonjeno.", - "tuning_suggestions_available": "{count} prijedlog podešavanja dostupan iz promatranih ciklusa. Pojavljuju se pored relevantnih polja.", + "tuning_suggestions_available": "{count} prijedloga podešavanja dostupno je na temelju promatranih ciklusa. Pojavljuju se pored relevantnih polja.", + "tuning_suggestions_available_one": "{count} prijedlog podešavanja dostupan je na temelju promatranih ciklusa. Pojavljuje se pored relevantnog polja.", + "tuning_suggestions_available_few": "{count} prijedloga podešavanja dostupna su na temelju promatranih ciklusa. Pojavljuju se pored relevantnih polja.", + "tuning_suggestions_available_other": "{count} prijedloga podešavanja dostupno je na temelju promatranih ciklusa. Pojavljuju se pored relevantnih polja.", "unsure_detected_prefix": "WashData nije siguran da je otkriven", - "updated": "Ažurirano", "warmup_badge": "Još uči ({done}/{needed} ciklusa)", - "warmup_detail": "Ovaj profil treba {needed} označenih ciklusa prije nego što počne automatsko podudaranje. Svaki potvrđeni ciklus pomaže mu učiti.", + "warmup_detail": "Podudaranje već radi. WashData traži da potvrdite prvih {needed} ciklusa ovog profila prije nego što počne sam označavati.", "wipe_history_warning": "Trajno brisanje svih ciklusa i profila. Ne može se poništiti.", "compare_overlay_tip": "Preklopite naučene omotnice profila da vidite na koji program podsjeća svaki ciklus.", - "duration_trend_up_tip": "Trajanje raste ({pct}%/ciklus)", - "duration_trend_down_tip": "Trajanje pada ({pct}%/ciklus)", - "energy_trend_up_tip": "Energija raste ({pct}%/ciklus)", - "energy_trend_down_tip": "Energija pada ({pct}%/ciklus)", - "performance_trending": "Trend performansi", - "pg_select_cycle_hint": "Odaberite ciklus i kliknite ↺ za učitavanje", - "pg_sweep_progress": "Korak {done} / {total}", - "pg_analysis_hint": "Odaberite ciklus i kliknite ↺ za učitavanje analize.", "diagnostics_load_failed": "Nije moguće učitati dijagnostiku: {error}", - "manual_duration_ref_hint": "Koristi se samo kada nije odabran referentni ciklus. Referentni ciklus postavlja trajanje prema vlastitoj duljini.", + "manual_duration_ref_hint": "Koristi se samo bez referentnog ciklusa; referentni ciklus postavlja trajanje prema svojoj duljini.", + "duration_learned_hint": "Izračunato iz ciklusa ovog programa (bez odstupanja) i ažurira se nakon svakog novog. Za promjenu obrežite ili uklonite cikluse koji odstupaju.", "toast_convert_rolled_back": "Pretvorba nije uspjela i poništena je (nije ostala nijedna automatizacija): {error}", "toast_convert_orphan": "Automatizacija je stvorena, ali brisanje starih radnji nije uspjelo. Nemojte pokušavati ponovno: ručno uklonite stare radnje kako biste izbjegli dupliciranu automatizaciju.", - "advisory_duration_trend_up": "Ciklusi programa '{name}' postaju sve dulji (otprilike +{pct}% po ciklusu). Ako se ponašanje uređaja promijenilo, ponovno snimite ili obnovite ovaj profil.", - "advisory_energy_trend_up": "Program '{name}' troši sve više energije (otprilike +{pct}% po ciklusu) - vrijedi provjeriti uređaj ako je to neočekivano.", - "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.", + "advisory_duration_trend_up": "Ciklusi programa '{name}' postaju sve dulji (oko +{pct}% po ciklusu). Ako se uređaj promijenio, ponovno snimite ili obnovite ovaj profil.", + "advisory_energy_trend_up": "Program '{name}' troši više energije svakim ciklusom (oko +{pct}% po ciklusu). Provjerite uređaj ako je to neočekivano.", + "advisory_poor_health": "Profil '{name}' loše odgovara: njegovi noviji ciklusi jako variraju ili se slabo podudaraju. Pregledajte njegove cikluse ili ga ponovno snimite kako bi podudaranje i procjene ostali točni.", + "advisory_shape_drift": "Profil '{name}' odstupio je od svog izvornog oblika snage, možda zbog kamenca ili trošenja. Razmislite o ponovnom snimanju s novijim ciklusima.", + "advisory_shape_drift_corr": "Profil '{name}' odstupio je od svog izvornog oblika snage (korelacija {corr}), možda zbog kamenca ili trošenja. Razmislite o ponovnom snimanju s novijim ciklusima.", + "advisory_unmatchable": "Profil '{name}' nikada se ne može podudariti: iza njega ne stoji nijedan ciklus s podacima o snazi. Označite ili snimite ciklus ovog programa da to popravite.", + "advisory_unmatchable_no_duration": "Profil '{name}' nikada se ne može podudariti: njegov ciklus nema upotrebljivo trajanje. Ponovno snimite ovaj program ili postavite njegovo očekivano trajanje.", "pg_no_alerts": "U ovom pokretanju nisu otkriveni problemi.", "pg_override_active": "Koriste se vaše uređene postavke umjesto trenutnih", "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.", "pg_sweep_intro2": "Pronađite postavku koja najbolje ispunjava cilj za vaše nedavne cikluse. Ništa se neće promijeniti dok je ne primijenite.", - "pg_running_history": "Ponovno izvođenje vaših ciklusa…", - "pg_running_sweep": "Ispitivanje vrijednosti kroz sve vaše cikluse…", "pg_row_load_hint": "Učitaj ovaj ciklus u grafikon iznad", - "pg_history_intro2": "Ponovno izvedite svoje nedavne cikluse kroz stvarni detektor i modul za podudaranje s postavkama iznad. Kliknite bilo koji redak da učitate taj ciklus u grafikon; uredite postavku da vidite usporedbu prije i poslije.", + "pg_history_intro2": "Ponovno provedite nedavne cikluse kroz stvarni detektor i podudaranje s postavkama iznad. Kliknite redak da ga učitate u grafikon; promijenite postavku da usporedite prije i poslije.", "pg_simulating": "Simuliranje ciklusa…", - "pg_canvas_empty2": "Odaberite ciklus iznad i pritisnite Pokreni da ga simulirate. Zatim prijeđite mišem za prikaz vrijednosti, pomičite kotačić za zumiranje i povucite za pomicanje.", "pg_apply_settings_confirm": "Spremiti ovih {n} postavki na ovaj uređaj? {list}", - "pg_stress_terminated_detail": "Ustalilo se na ~{idle}W mirovanja -- ciklus je završio {h} h {m} min kasnije putem {reason}.", - "pg_stress_above_threshold_detail": "Potrošnja u mirovanju ~{idle}W jednaka je ili viša od stvarnog praga zaustavljanja ({stop}W) -- ciklus nikad nije zabilježen kao miran. Za ispravak povećajte stop_threshold_w.", - "pg_stress_hit_cap_detail": "Ciklus je radio {h} h {m} min bez zaustavljanja -- prisilno zaustavljen sigurnosnim limitom. Potrošnja u mirovanju {idle}W u odnosu na prag zaustavljanja {stop}W.", "no_logs_match": "Nijedan zapis dnevnika ne odgovara ovim filtrima.", "pg_analysis_hint2": "Odaberite ciklus i pritisnite Pokreni za učitavanje analize podudaranja.", - "pg_analysis_empty2": "Pritisnite Pokreni za prikaz analize podudaranja.", - "imported_readonly": "Uvezeno iz zajedničke trgovine. Prikazano za referencu i podudaranje. Ne broji se u vašu statistiku i ne može se uređivati.", + "imported_readonly": "Uvezeno iz trgovine zajednice. Služi kao referenca i za podudaranje, ne broji se u statistiku i ne može se uređivati.", "merge_no_imports": "Uvezene snimke ne mogu se spojiti sa stvarnim ciklusom. Poništite njihov odabir za spajanje.", - "store_enable_hint": "Omogućite online značajke u Postavkama kako biste mogli pregledavati i uvoziti referentne cikluse zajednice.", - "advisory_duration_outlier": "Ciklusi označeni kao '{name}' ({n}) previše odstupaju od njegove uobičajene duljine da bi se ikada mogli podudariti s njim (najveće odstupanje: {ratio}x). Vjerojatno su pogrešno označeni ili je jedno snimanje obuhvatilo dva pokretanja. Ponovno ih označite ili ih podijelite kako bi prestali iskrivljavati procjenu vremena za ovaj program.", + "store_enable_hint": "Uključite online značajke u Preferencije > Online i zajednica kako biste pregledavali i uvozili referentne cikluse zajednice.", + "advisory_duration_outlier": "Ciklusi označeni kao ‘{name}’ ({n}) previše odstupaju od njegove uobičajene duljine da bi se ikad podudarili (najgori: {ratio}x). Vjerojatno su pogrešno označeni ili sadrže dva rada. Ponovno ih označite ili podijelite kako ne bi iskrivljavali procjenu vremena.", "standby_above_stop_title": "Još uvijek povlači {idle} W kada je ciklus završio", "standby_above_stop_sub": "{n} od posljednjih {total} ciklusa. Odgoda isključivanja počinje tek kada snaga padne ispod Praga zaustavljanja ({stop} W), pa ciklusi završavaju sa zakašnjenjem ili ih treba zaustaviti ručno.", - "online_intro": "Dijelite i preuzimajte referentne cikluse s drugim korisnicima WashData. Sve online značajke su dobrovoljne i prema zadanome isključene.", "store_share_intro": "Prenesite ovaj referentni ciklus kako bi ga mogli koristiti drugi s istim uređajem. Pregledava se prije nego što postane javno dostupan.", - "online_intro_global": "Pregledavajte i dijelite referentne snimke s drugim korisnicima WashData te potvrđujte unose uređaja. Jedna veza vrijedi za cijelu vašu WashData integraciju; marka i model uređaja postavljaju se za svaki uređaj u odjeljku Osnovno. Sve online značajke su dobrovoljne i prema zadanome isključene.", + "online_intro_global": "Pregledavajte i dijelite referentne snimke s drugim korisnicima WashData. Jedna veza vrijedi za sve vaše WashData uređaje; marka i model postavljaju se za svaki uređaj pod Osnovno. Mrežne značajke isključene su dok ih ne uključite.", "online_unavailable": "Online značajke nisu dostupne na ovom poslužitelju.", "store_picker_offline": "Omogućite online značajke u zupčaniku postavki kako biste birali iz kataloga zajednice.", "pick_brand_first": "Najprije odaberite marku uređaja.", - "model_not_found": "Još nije u katalogu.", - "connect_to_confirm": "Povežite se u zupčaniku postavki za potvrdu ili ocjenu.", + "custom_task_limit": "Dosegnuli ste najveći broj zadataka ({max}).", + "custom_tasks_intro": "Dodajte sve ostalo na što želite biti podsjećeni. Zadatak je na redu kada se dosegne bilo koji od intervala; ostavite jedan prazan ili 0 da se koristi samo drugi.", + "maintenance_due_sensor_hint": "Binarni senzor Održavanje na redu uključen je dok je bilo koji zadatak na redu; koristite ga u vlastitim automatizacijama obavijesti.", + "maintenance_supporter_intro": "Maintenance Supporter je zasebna integracija koja može mnogo više: bilo koji uređaj ili zadatak, rasporedi prema vremenu, korištenju ili senzoru, obavijesti i povijest. Može koristiti brojač ciklusa WashData.", "adopt_settings_hint": "Primijeni postavke otkrivanja i usporedbe dijeljenog uređaja na ovaj uređaj", - "include_settings_hint": "Uključi pragove otkrivanja, težine usporedbe i ostale parametre u dijeljeni paket", + "include_settings_hint": "Podijelite pragove otkrivanja i podudaranja ovog uređaja (ne obavijesti, entitete ni cijenu energije). Drugi biraju hoće li ih primijeniti.", "onboard_download": "Preuzmi gotove postavke iz trgovine zajednice", - "share_consent": "Dijelite stvarne podatke sa svog uređaja. Ne dijelite ako su vaši obrasci korištenja privatni.", "share_device_none": "Još nisu postavljena nikakva uređaja. Prvo dodajte uređaj.", "share_guideline_naming": "Koristite jasna imena programa (npr. 'Cotton 40', 'Eco 60') kako bi ih drugi mogli prepoznati", - "share_guideline_quality": "Dijelite samo cikluse koji su normalno završili -- bez prekida usred ciklusa, otvaranja vrata ili skokova napajanja.", + "share_guideline_quality": "Dijelite samo cikluse koji su normalno završili: bez prekida, otvaranja vrata i nestanaka struje.", "share_guideline_review": "Pregledajte profile prije dijeljenja -- uklonite one koji izgledaju pogrešno", "share_guidelines_title": "Prije dijeljenja", - "store_download_device_intro": "Preuzmite postavke uređaja iz zajednice i primijenite ih na novi ili postojeći uređaj", + "store_download_device_intro": "Dodajte svom uređaju sve podijeljene programe i njihove referentne cikluse. Vaši vlastiti ciklusi i statistika ostaju netaknuti.", "store_share_device_intro": "Prenesite {brand} {model} s referentnim ciklusima koje odaberete. Drugi s istim uređajem mogu preuzeti vaše programe. Unosi se pregledavaju prije nego što postanu javno dostupni.", "share_profile_no_cycles": "Nema referentnih ciklusa - označite ciklus sa ⭐ u kartici Ciklusi kako biste uključili ovaj profil", - "advisory_phase_inconsistent": "Čini se da '{name}' miješa različite programe ili temperature - njegovi se ciklusi griju vrlo različito dugo. Podjela na zasebne profile (npr. po temperaturi) poboljšat će podudaranje i procjene vremena.", - "advisory_phase_inconsistent_title": "⚠ Možda pomiješani programi", - "export_select_intro": "Označite točno što treba uključiti. Odabir profila bez njihovih ciklusa i dalje izvozi prepoznatljiv program (njegov naučeni oblik prenosi se s njim).", - "import_analyze_hint": "Učitajte izvezenu datoteku (ili zalijepite njezin JSON). WashData je analizira i točno prikazuje što se može uvesti prije nego što se išta promijeni.", + "export_select_intro": "Označite što uključiti. Profili bez svojih ciklusa i dalje izvoze prepoznatljiv program.", + "import_analyze_hint": "Učitajte izvezenu datoteku (ili zalijepite njezin JSON). WashData pokazuje što se može uvesti prije nego što se išta promijeni.", "analyzing": "Analiziranje…", - "device_type_mismatch_warn": "Ovaj je izvoz s druge vrste uređaja ({src} vs {local}). Programe i cikluse i dalje je moguće uvesti kao referentne podatke, ali postavke specifične za uređaj i uvoz stvarne povijesti onemogućeni su.", + "device_type_mismatch_warn": "Ovaj je izvoz s druge vrste uređaja ({src} vs {local}). Programe i cikluse i dalje možete uvesti kao referentne podatke, ali ne i postavke uređaja ni stvarnu povijest.", "merge_hint": "Uvezene stavke se dodaju; ništa lokalno se ne gubi. Sukobi naziva rješavaju se u nastavku.", "replace_warn": "Svaka označena kategorija briše se i zamjenjuje podacima iz datoteke. Neoznačene kategorije ostaju nepromijenjene.", "dest_reference_hint": "Uvezeni ciklusi samo poboljšavaju prepoznavanje programa i nikada ne utječu na statistiku potrošnje/energije.", - "dest_real_history_hint": "Uvezeni ciklusi računaju se kao vlastita povijest ovog uređaja i pridonose statistici energije/potrošnje. Koristite pri premještanju jednog uređaja na novu instalaciju.", - "import_history_description": "Imali ste pametni utikač i prije WashData? Učitajte izvoz povijesti njegovog senzora snage ili je pročitajte izravno iz Home Assistant, a uobičajeno otkrivanje proći će kroz te podatke, pa će se prošli ciklusi pojaviti u vašem popisu Ciklusi, spremni za imenovanje.", - "hist_input_hint": "Učitajte CSV preuzet s ploče Povijest (entitet, stanje, zadnja promjena) ili pustite WashData da izravno pročita povijest senzora. Otkrivanje se zatim izvodi jednako kao i uživo, a vi odabirete koje od nađenih ciklusa zadržati.", - "hist_recorder_hint": "Čita podatke od odabranog datuma do sada. Home Assistant standardno čuva detaljnu povijest 10 dana, a nakon toga samo satne prosjeke, koji su previše grubi za otkrivanje ciklusa - raniji datum odaberite samo ako je vaš recorder postavljen na dulje čuvanje.", - "hist_scanning": "Vašu povijest ponovno provodimo kroz detektor. Ovo se izvodi u pozadini - možete zatvoriti ovaj dijalog i vratiti se kasnije.", + "dest_real_history_hint": "Uvezeni ciklusi računaju se kao vlastita povijest i statistika ovog uređaja. Koristite pri premještanju uređaja na novu instalaciju.", + "import_history_description": "Imali ste pametni utikač prije WashData? Učitajte njegovu povijest snage ili je pročitajte iz Home Assistanta. Otkrivanje prolazi kroz te podatke, a stari ciklusi pojavljuju se na popisu Ciklusi spremni za imenovanje.", + "hist_input_hint": "Učitajte CSV s ploče Povijest (entitet, stanje, zadnja promjena) ili pustite WashData da izravno pročita povijest senzora. Otkrivanje uživo prolazi kroz te podatke, a vi birate koje cikluse zadržati.", + "hist_recorder_hint": "Čita od odabranog datuma do sada. Home Assistant standardno čuva detaljnu povijest 10 dana; stariji satni prosjeci pregrubi su za otkrivanje ciklusa.", + "hist_scanning": "Vaša povijest prolazi kroz detektor u pozadini. Možete zatvoriti ovaj dijalog i vratiti se kasnije.", "hist_imported_count": "Uvezeni ciklusi: {n}.", "hist_duplicates": "Već prije uvezeno i preskočeno: {n}.", "hist_capped": "Dosegnuto je ograničenje uvezenih ciklusa po uređaju; ostatak nije spremljen.", - "hist_next_step": "Nalaze se u vašem popisu Ciklusi, označeni kao uvezena povijest. Otvorite jedan i tipkom Označi mu dodijelite naziv programa kojem pripada.", + "hist_next_step": "Nalaze se na popisu Ciklusi, označeni kao uvezena povijest. Otvorite jedan i tipkom Označi imenujte njegov program.", "hist_rows_read": "Pročitana očitanja: {n}", "hist_breaks": "Praznine u kojima senzor nije bio dostupan: {n}", "hist_other_entity": "Zanemarena očitanja drugih entiteta: {n}", @@ -1095,25 +1038,74 @@ "hist_scan_capped": "Prikazani su samo prvi kandidati (nađeno je {n}).", "hist_recorder_empty": "Home Assistant nema detaljnu povijest za ovaj senzor u tom razdoblju.", "hist_scan_failed": "Pretraživanje nije uspjelo.", - "hist_scan_expired": "To pretraživanje više nije dostupno. Pokrenite ga ponovno.", + "hist_scan_expired": "To skeniranje više nije dostupno. Skenirajte 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.", - "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.", + "imported_history_readonly": "Pronađeno u uvezenoj povijesti snage. Pomaže podudaranju, ali se ne broji u statistiku i ne može se obrezati ni podijeliti. Označite ga da imenujete program.", + "odometer_intro": "Ovaj zbroj samo raste. Brisanje ciklusa ga ne smanjuje, pa podsjetnici za servis ostaju točni.", + "odometer_correct_hint": "Postavite ovo ako je WashData izbrojio ciklus koji se nije dogodio ili je uređaj radio cikluse prije instalacije WashData.", "no_reminders_set": "Još nema postavljenih podsjetnika za servis.", "toast_program_armed": "Program postavljen za sljedeći ciklus: {program}", "toast_range_required": "Postavite ispravan raspon Od/Do", - "no_profiles_option": "(nema profila, dodajte novi)" + "no_profiles_option": "(nema profila, dodajte novi)", + "compare_cycles_title": "Usporedi cikluse ({count})", + "edit_builtin_phase": "Ovo je ugrađena faza. Spremanjem se stvara vlastita kopija; izbrišite kopiju da vratite izvornik.", + "fetch_error": "Učitavanje podataka nije uspjelo.", + "connection_lost": "Veza je prekinuta - prikazano je stanje u {time}.", + "pg_notify_partial": "Oznake prikazuju samo obavijesti o početku, skorom kraju, završetku, prekretnicama i tihim satima. Ažuriranja uživo, podsjetnici za istovar i upozorenja o prekoračenju ne simuliraju se.", + "connect_to_confirm": "Povežite se u zupčaniku postavki za potvrdu ili ocjenu.", + "coverage_gap_cluster": "Nedavni neoznačeni ciklusi ({count}) od otprilike {min} min izgledaju kao jedan program koji još niste stvorili.", + "pg_label_yes": "Na kraju ciklusa bio bi označen kao \"{name}\"", + "pg_label_no": "Bio bi poslan na pregled: {reason}", + "live_match_debug_tip": "Povjerenje: koliko točno trenutačna krivulja snage odgovara najboljem profilu kandidatu (0-100 %). Nejasno: rezultati dvaju najboljih kandidata razlikuju se za manje od 5 %, pa je oznaka nesigurna dok ciklus ne završi.", + "automation_alias_started": "{device}: ciklus je pokrenut", + "automation_alias_finished": "{device}: ciklus je završen", + "automation_desc_started": "Pokreće se kad započne ciklus uređaja {device}. Dodajte svoje radnje (obavijesti, svjetla, ...).", + "automation_desc_finished": "Pokreće se kad završi ciklus uređaja {device}. Dodajte svoje radnje (obavijesti, svjetla, ...).", + "feedback_detected": "WashData je prepoznao: {name}", + "import_undo_hint": "Čuva se točka vraćanja od prije zadnjeg uvoza ({when}): {profiles} profila i {cycles} ciklusa.", + "replace_undo_hint": "Najprije se sprema točka vraćanja, pa ovaj uvoz možete poništiti u odjeljku Izvoz / uvoz.", + "pg_in_sample_note": "Ovi ciklusi pomogli su izgraditi profile s kojima se uspoređuju, pa su rezultati optimistični: na novim ciklusima očekujte nešto manje točnih podudaranja.", + "hist_tail_trimmed": "Odrezano je {min} min čekanja nakon završetka ciklusa, kao što to čini integracija tijekom rada", + "hist_truncated": "zaustavljeno na ograničenju očitanja ({n}), pa ostatak povijesti nije pregledan", + "hist_warn_kw": "Najveće očitanje je samo {w}. Čini se da ovaj senzor javlja kilovate; WashData treba vate, pa se ciklusi ne mogu otkriti.", + "hist_warn_naive_time": "{n} vremenskih oznaka nema vremensku zonu i pročitano je kao UTC. Ako datoteka koristi lokalno vrijeme, ciklusi su pomaknuti za vaš pomak od UTC-a, a očitanja oko promjene vremena mogu se izgubiti.", + "hist_warn_naive_time_one": "{n} vremenska oznaka nema vremensku zonu i pročitana je kao UTC. Ako datoteka koristi lokalno vrijeme, ciklusi su pomaknuti za vaš pomak od UTC-a, a očitanja oko promjene vremena mogu se izgubiti.", + "hist_warn_naive_time_few": "{n} vremenske oznake nemaju vremensku zonu i pročitane su kao UTC. Ako datoteka koristi lokalno vrijeme, ciklusi su pomaknuti za vaš pomak od UTC-a, a očitanja oko promjene vremena mogu se izgubiti.", + "hist_warn_naive_time_other": "{n} vremenskih oznaka nema vremensku zonu i pročitano je kao UTC. Ako datoteka koristi lokalno vrijeme, ciklusi su pomaknuti za vaš pomak od UTC-a, a očitanja oko promjene vremena mogu se izgubiti.", + "pg_compare_profile_tip": "Odabire samo profil iscrtan preko grafikona. Simulacija uvijek sama traži odgovarajući program za ciklus, kao i integracija tijekom rada.", + "pg_outcome_stale": "Postavke su se promijenile nakon pokretanja ove simulacije. I dalje prikazuje stari rezultat.", + "pg_sweep_guarded": "Prerano završava više ciklusa, dijeli više njih ili propušta cikluse koje trenutna postavka hvata, pa se nikada ne preporučuje.", + "pg_sweep_keep_current": "Zadrži trenutnu vrijednost ({value}, {metric}): nijedna vrijednost nije bila bolja za barem jedan ciklus.", + "pg_sweep_n_cycles": "na {count} ciklusa", + "pg_sweep_n_cycles_one": "na {count} ciklusu", + "pg_sweep_n_cycles_few": "na {count} ciklusa", + "pg_sweep_n_cycles_other": "na {count} ciklusa", + "pg_task_busy": "Za ovaj uređaj već je u tijeku „Testiraj na povijesti” ili „Optimiziraj”. Pričekajte da završi ili ga otkažite.", + "cycle_context_loading": "Učitavanje povijesti recordera…", + "cycle_context_note": "Sivo: povijest senzora snage iz recordera oko ovog ciklusa. Nije dio ciklusa i ne utječe na trajanje, energiju ni podudaranje.", + "cycle_context_none": "Recorder nema očitanja oko ovog ciklusa (Home Assistant standardno čuva povijest 10 dana).", + "cycle_context_tip": "Samo za prikaz: pokazuje povijest senzora snage iz recordera oko ovog ciklusa, ako je još spremljena. Da biste neuspjele pokušaje pokretanja dodali budućim ciklusima, koristite Uvodne sekunde krivulje (Postavke > Otkrivanje > Početak ciklusa), što mijenja spremljena trajanja.", + "invalid_threshold_pair": "Prag zaustavljanja ({stop} W) mora biti ispod Praga pokretanja ({start} W). Ništa nije spremljeno; promijenite obje vrijednosti zajedno." }, "phase_desc": { "anti_crease": "Povremena kratka prevrtanja nakon završetka kako bi se smanjile bore.", "anti_wrinkle": "Periodično prevrtanje nakon ciklusa za smanjenje bora.", + "baking": "Grijač na visokoj temperaturi stvara koru i sredinu kruha.", + "cooking": "Glavna faza pripreme s uključenim grijačem i ventilatorom.", "cool_down": "Prevrtanje bez zagrijavanja pri kraju ciklusa.", + "cool_down_af": "Hlađenje samo ventilatorom nakon grijanja.", "drain_and_switch": "Prijelazno razdoblje s načina pranja na način sušenja.", "dry": "Faza sušenja pomoću grijača i/ili preostale topline.", "drying": "Glavno grijano razdoblje prevrtanja.", "heat_up": "Početno zagrijavanje grijača prije početka potpunog sušenja.", + "keep_warm": "Lagano grijanje da hrana ostane topla.", + "keep_warm_bm": "Lagano grijanje da kruh ostane topao nakon pečenja.", + "kneading": "Motor miješa i mijesi tijesto. Velika potrošnja snage.", + "pause_af": "Kratka stanka za protresanje ili provjeru.", + "pre_heat": "Početno zagrijavanje komore prije pune pripreme.", "pre_rinse": "Prvo raspršivanje prije pranja deterdžentom.", + "proving": "Dizanje na niskoj toplini: kvasac fermentira i tijesto raste.", + "resting": "Kratka stanka male snage između miješenja da se gluten opusti.", "pre_wash": "Početno namakanje ili prethodna obrada prije glavnog pranja.", "rinse": "Faza ispiranja čistom vodom. Ova se faza može ponoviti više puta.", "sanitize": "Visokotemperaturni stupanj čišćenja za programe dezinfekcije.", @@ -1126,6 +1118,7 @@ "wash_dw": "Glavno pranje deterdžentom s grijanjem." }, "placeholder": { + "custom_task": "npr. Očisti brtvu vrata", "add": "dodaj…", "group_name": "npr. Pamuk 2:47", "json_buttons": "[{\"action\":\"ID\",\"title\":\"Label\"}]", @@ -1153,10 +1146,10 @@ "section": { "dishwasher": { "label": "Perilica posuđa", - "intro": "Obrada kraja ciklusa za perilice posuđa, koje obično završavaju dugom, gotovo tihom fazom sušenja prije kratkog završnog ispumpavanja vode." + "intro": "Obrada kraja za perilice posuđa, koje često dugo i tiho suše prije kratkog završnog ispumpavanja." }, "anti_wrinkle": { - "intro": "Način anti-bore/anti-gužve otkriva impulse prevrtanja male snage nakon glavne faze i zadržava ih priključene na završeni ciklus.", + "intro": "Zadržava kratke impulse prevrtanja male snage nakon glavne faze sušilice uz završeni ciklus.", "label": "Anti-bore" }, "basic": { @@ -1164,7 +1157,7 @@ "label": "Osnovno" }, "delay": { - "intro": "Otkrivanje odgođenog početka identificira kad je uređaj uključen, ali još nije počeo ciklus.", + "intro": "Otkriva uređaj koji je uključen i čeka odgođeni početak.", "label": "Odgođeni početak" }, "detection": { @@ -1188,161 +1181,153 @@ "label": "Tajming i nadzor" }, "triggers": { - "intro": "Neobavezni vanjski signali: okidač završetka, senzor vrata, prekidač za pauzu i podsjetnik za istovar.", + "intro": "Neobavezni dodaci: okidač završetka, senzor vrata, prekidač za pauzu i podsjetnik za istovar.", "label": "Okidači i vrata" - }, - "phase_eta": { - "label": "Preostalo vrijeme", - "intro": "Preostalo vrijeme koje uzima u obzir faze, za uređaje čija duljina ciklusa ovisi o temperaturi ili centrifugi." } }, "setting": { "dishwasher_end_spike_quiet_release": { "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." + "doc": "Nakon očekivanog trajanja: koliko dugo perilica posuđa mora ostati ispod Praga zaustavljanja prije nego što WashData prestane čekati završno ispumpavanje. Povećajte ako se kasno ispumpavanje nakon dugog sušenja propusti. Čekanje nikad ne prelazi 30 minuta." }, "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.", + "doc": "Udio prosječnog trajanja programa koji ciklus mora dosegnuti prije nego što se prevrtanje male snage računa kao rep anti-bore. Smanjite ga (npr. 0,75) kod sušilica koje se prilagođavaju količini rublja. Kod perilica ostavite zadano (0,98), inače pranje može završiti na pola.", "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.", + "doc": "Zadržite kratke impulse prevrtanja male snage nakon glavne faze grijanja sušilice uz završeni ciklus, umjesto da se čitaju kao novi ciklusi.", "label": "Omogući otkrivanje anti-bora" }, "anti_wrinkle_exit_power": { - "doc": "Snaga mora pasti ispod ove između impulsa kako bi način protiv bora ostao aktivan.", + "doc": "U načinu anti-bore snaga ispod ove vrijednosti smatra se mirovanjem između impulsa okretanja, a mirovanje dulje od Maks. razmaka između impulsa završava način. Ako je Prag zaustavljanja viši, koristi se on, pa ova postavka nešto mijenja samo iznad Praga zaustavljanja.", "label": "Prag snage za izlaz" }, "anti_wrinkle_idle_timeout": { - "doc": "Koliko dugo stroj smije ostati miran između dva impulsa bubnja prije nego što se način anti-bora završi. Postavite ga iznad najdužeg razmaka koji vaša sušilica ostavlja između impulsa, inače se svaki sljedeći impuls čita kao lažni početak.", + "doc": "Koliko dugo sušilica smije mirovati između impulsa prevrtanja prije nego što način anti-bore završi. Postavite iznad najduljeg razmaka vaše sušilice, inače se kasniji impulsi čitaju kao lažni počeci.", "label": "Maks. razmak između impulsa" }, "anti_wrinkle_max_duration": { - "doc": "Pulsevi dulji od ovoga tretiraju se kao pravi ciklus, a ne kao sredstvo protiv bora.", + "doc": "Impuls dulji od ovoga računa se kao pravi ciklus, a ne kao prevrtanje protiv gužvanja.", "label": "Maks. trajanje" }, "anti_wrinkle_max_power": { - "doc": "Puls iznad te snage smatra se pravim novim ciklusom, a ne borama protiv bora. Postavite malo iznad snage prevrtanja.", + "doc": "Impuls iznad ove snage računa se kao pravi novi ciklus, a ne kao prevrtanje. Postavite malo iznad snage impulsa prevrtanja.", "label": "Maks. snaga anti-bora" }, "auto_label_confidence": { - "doc": "Ako je rezultat podudaranja na kraju ciklusa jednak ili viši od ovoga, program se automatski označava bez ikakvog upita za potvrdu. Podignite ga tako da zahtijeva veću sigurnost prije automatskog označavanja; spustite ga kako biste više automatizirali. Djeluje u kombinaciji s Learning Confidence ispod njega.", + "doc": "Rezultat podudaranja na kraju ciklusa od kojeg se program označava bez pitanja. Povećajte za veću sigurnost; smanjite da se automatski označi više ciklusa. Držite iznad Pouzdanosti učenja.", "label": "Pouzdanost za automatsko označavanje" }, "auto_maintenance": { - "doc": "Izvršite noćno održavanje: ponovno izgradite omotnice profila, ponovno izračunajte zdravlje ciklusa, uklonite tragove otklanjanja pogrešaka i zadržite najnovije cikluse.", + "doc": "Noćno čišćenje: označava starije cikluse koji sada jasno odgovaraju programu, ponovno gradi krivulje programa, osvježava oznake anomalija i zdravlje ciklusa te uklanja tragove za otklanjanje pogrešaka ako je Spremi tragove za otklanjanje pogrešaka isključeno. Ciklusi i njihovi zapisi snage uvijek se čuvaju.", "label": "Automatsko održavanje (noćno čišćenje)" }, "completion_min_seconds": { - "doc": "Ciklusi kraći od ovoga odbacuju se kao ciklusi duhova (probne vožnje, otvaranje vrata za dodavanje čarape).", + "doc": "Ciklusi kraći od ovoga (probni radovi, otvaranje vrata za dodavanje čarape) spremaju se kao prekinuti i ne dobivaju obavijest o završetku.", "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.", + "doc": "Dodaje očitanja iz neuspjelih pokušaja pokretanja na početak ciklusa, za strojeve koji se isprobavaju prije pokretanja (zaključavanje vrata, prvo punjenje). 300 s dovoljno je za većinu; 0 = isključeno. Zabilježeni počeci pomiču se ranije, pa se naučena trajanja neko vrijeme mijenjaju.", "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.", + "doc": "Snaga mora ostati u pojasu pripravnosti ovoliko dugo prije nego što se uređaj računa kao da čeka početak.", "label": "Sekunde potvrde" }, "delay_start_detect_enabled": { - "doc": "Otkrijte kada je uređaj uključen i čeka (odgođeno pokretanje/stanje pripravnosti), ali nije započeo svoj ciklus, tako da se povlačenje u stanju pripravnosti ne zamijeni s tekućim ciklusom.", + "doc": "Otkrijte uređaj koji je uključen i čeka odgođeni početak, kako se njegova potrošnja u pripravnosti ne bi smatrala radom ciklusa.", "label": "Omogući otkrivanje odgođenog početka" }, "delay_timeout_hours": { - "doc": "Prestanite čekati u načinu rada s odgođenim pokretanjem nakon toliko sati i vratite se u mirovanje, tako da stroj koji je ostavljen pod napajanjem, ali nikad nije pokrenut, ne čeka zauvijek.", + "doc": "Prestanite čekati odgođeni početak nakon ovoliko sati i vratite se u mirovanje.", "label": "Sati vremenskog ograničenja" }, "device_type": { - "doc": "Klasa uređaja. Postavlja osjetljive zadane vrijednosti detekcije (pragovi, odgoda isključivanja, kraj rukovanja) podešene za tu vrstu uređaja; promijenite ga samo ako je uređaj izvorno postavljen kao pogrešna vrsta.", + "doc": "Postavlja zadane vrijednosti otkrivanja (pragovi, odgoda isključivanja, obrada kraja) za ovu vrstu uređaja. Mijenjajte samo ako je uređaj postavljen kao pogrešna vrsta.", "label": "Vrsta uređaja" }, "door_sensor_entity": { - "doc": "Opcijski binarni senzor vrata. Koristi se za otkrivanje kada je uređaj otvoren/ispražnjen nakon ciklusa.", + "doc": "Neobavezni senzor vrata. Pokazuje kad je uređaj otvoren radi istovara nakon ciklusa.", "label": "Entitet senzora vrata" }, "duration_tolerance": { - "doc": "Tolerancija za procjene preostalog vremena (povratne informacije o učenju, ne podudaranje). Ako je stvarno trajanje unutar +/-X% procjene, računa se kao dobro podudaranje.", + "doc": "Samo za povratne informacije učenja: ciklus unutar +/-X% procijenjenog trajanja računa se kao dobra procjena. Ne utječe na podudaranje.", "label": "Tolerancija procjene" }, "enable_ml_models": { - "doc": "While a cycle runs, let the models refine the live results: a steadier time-remaining and energy/cost estimate, and an anti-premature-stop guard on end detection (it can only ever delay a finish, never end one early, and is bounded). Koristi vaše fino podešene modele kada su dostupni, inače ugrađene. Isključeno = samo klasična logika koja se temelji na snazi (još uvijek pouzdana).", + "doc": "Koristite model treniran na ovom stroju za predviđanje energije i troška ciklusa koji traje, i dopustite dobro poznatom programu koji neobično rano stane da završi bez pune odgode isključivanja. Isključeno: samo logika temeljena na snazi.", "label": "Primijeni pametne modele tijekom ciklusa" }, "end_energy_threshold": { - "doc": "Tijekom odbrojavanja odgode isključenja, akumulirana energija (vati x vrijeme) uspoređuje se s ovim pragom. Ako se prekorači, odbrojavanje se resetira - zadržavajući sustave protiv gužvanja i rubove za sušenje u perilici posuđa pričvršćene na ciklus umjesto da ih skraćuje. Povećajte ga ako ciklusi završe prerano tijekom hlađenja; smanjite ako je otkrivanje sporo.", + "doc": "Energija dopuštena tijekom odgode isključivanja. Iznad toga ciklus se nastavlja, pa prevrtanja i završno sušenje ostaju dio ciklusa. Smanjite ako ciklusi završavaju tijekom repa; povećajte ako završeni ciklusi i dalje čekaju.", "label": "Energija završetka" }, - "end_repeat_count": { - "doc": "Broj uzastopnih očitanja ispod granične vrijednosti potrebnih prije završetka ciklusa. 1 je dobar za većinu utikača. Povećajte na 2-3 ako vaš pametni utikač povremeno javlja uzorak lažne nule usred ciklusa i vaši ciklusi prerano završavaju.", - "label": "Broj ponavljanja završetka" - }, "energy_price_entity": { - "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.", + "doc": "Senzor s trenutnom cijenom po kWh (npr. dinamična tarifa). Ima prednost pred statičkom cijenom ispod, pa mora biti cijena, a ne senzor snage ili energije utikača.", "label": "Entitet cijene energije" }, "energy_price_static": { - "doc": "Fiksna cijena po kWh koja se koristi za podatke o troškovima kada iznad nije postavljen entitet stvarne cijene.", + "doc": "Fiksna cijena po kWh, koristi se kad nije postavljen entitet cijene.", "label": "Statička cijena energije (po kWh)" }, "energy_sensor": { - "doc": "Neobavezno kumulativno brojilo energije (total_increasing kWh/Wh, npr. vlastito brojilo ukupne potrošnje utičnice). Kada je postavljeno, energija prijavljena za svaki ciklus uzima se iz razlike očitanja ovog brojila između početka i kraja, čime se izbjegava podbrojavanje koje nastaje integriranjem sporo izvještavajućeg senzora snage. Vraća se na integriranu vrijednost ako očitanje nedostaje, njegova je jedinica nepoznata ili razlika nije pozitivna. Ostavite prazno da nastavite integrirati senzor snage.", + "doc": "Neobavezno brojilo kWh ili Wh (total_increasing, npr. vlastito brojilo utikača). Energija ciklusa tada se čita iz njega, što je točnije od izračuna iz sporog senzora snage. Ako je njegovo očitanje neupotrebljivo, koristi se izračunata vrijednost.", "label": "Entitet brojila energije" }, "expose_debug_entities": { - "doc": "Objavite dodatne dijagnostičke HA entitete (povjerenje podudaranja, dvosmislenost, interno stanje). Isključeno održava popis entiteta čistim za normalnu upotrebu.", + "doc": "Dodajte dijagnostičke entitete (pouzdanost podudaranja, dvosmislenost, unutarnje stanje). Isključeno: popis entiteta ostaje uredan.", "label": "Izloži entitete za otklanjanje pogrešaka" }, "external_end_trigger": { - "doc": "Binarni senzor čija promjena stanja označava kraj ciklusa (npr. \"završen\" kontakt uređaja ili prateća integracija).", + "doc": "Binarni senzor čija promjena označava kraj ciklusa (npr. kontakt \"završeno\" ili druga integracija).", "label": "Entitet vanjskog okidača" }, "external_end_trigger_enabled": { - "doc": "Neka vanjski binarni senzor signalizira kraj ciklusa, uz ugrađenu detekciju na temelju snage.", + "doc": "Dopustite binarnom senzoru da signalizira kraj ciklusa, uz otkrivanje temeljeno na snazi.", "label": "Omogući vanjski okidač završetka" }, "external_end_trigger_inverted": { - "doc": "ISKLJUČIVANJE (umjesto UKLJUČIVANJA) senzora okidača smatrajte signalom kraja ciklusa.", + "doc": "Smatrajte ISKLJUČIVANJE senzora (a ne uključivanje) signalom kraja.", "label": "Invertiraj vanjski okidač (okidaj na ISKLJUČENO)" }, "learning_confidence": { - "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.", + "doc": "Najniži rezultat podudaranja za označavanje završenog ciklusa. Jasan pobjednik se označava; tijesan rezultat ide u red za pregled u Ciklusima. Ispod ove vrijednosti ništa se ne označava niti pita. Držite je ispod Pouzdanosti za automatsko označavanje.", "label": "Pouzdanost učenja" }, "linked_device": { - "doc": "Opcionalno ugniježdite ovaj WashData uređaj pod drugi uređaj (npr. pametni priključak) u registru HA uređaja, prikazan kao \"Povezan putem ...\".", + "doc": "Neobavezno. Prikažite ovaj WashData uređaj ispod drugog uređaja (npr. pametnog utikača) u Home Assistantu, kao \"Povezano putem ...\".", "label": "Grupiranje pod uređajem" }, "min_off_gap": { - "doc": "Ako se stroj isključi na manje od tog vremena, slijed uključivanja/isključivanja/uključivanja tretira se kao jedan kontinuirani ciklus. Sprječava dijeljenje programa namakanja (stroj se isključuje na nekoliko minuta tijekom pranja) u dva odvojena ciklusa. Postavite ga kraći od razmaka između vaših uzastopnih opterećenja ako želite da se ona računaju kao zasebni ciklusi. Zadane postavke tipa uređaja štite tipičnu pauzu unutar ciklusa za svaki uređaj.", + "doc": "Isključenje kraće od ovoga ne dijeli ciklus, pa pauze namakanja ostaju u jednom pranju. Držite kraće od razmaka između uzastopnih punjenja, inače se ona spajaju u jedan ciklus.", "label": "Min. razmak isključivanja" }, "min_power": { - "doc": "Apsolutna minimalna snaga koja se smatra aktivnom. Očitanja ispod ovoga tretiraju se kao 0 W (pripravnost), filtrirajući fantomsko opterećenje pametnih utikača i LED dioda u stanju pripravnosti.", + "doc": "Očitanja ispod ove vrijednosti računaju se kao 0 W (pripravnost). Filtrira potrošnju u mirovanju pametnih utikača i LED-ica pripravnosti.", "label": "Minimalna snaga" }, "ml_training_enabled": { - "doc": "Povremeno tijekom noći proučite svoje pregledane cikluse i fino podesite modele za ovaj određeni stroj. Promjena se zadržava samo kada stvarno ima bolje rezultate u ciklusima zadržavanja, tako da to može samo pomoći ili ostati isto - nikad nazadovati.", + "doc": "Povremeno ponovno trenirajte model energije na završenim ciklusima ovog stroja. Novi model zadržava se samo ako na testnim ciklusima nadmaši jednostavnu procjenu.", "label": "Uči od ovog stroja" }, "ml_training_hour": { - "doc": "Lokalni sat u danu (0-23) za fino podešavanje tijekom noći. Odaberite tihi sat kao što je 2 (02:00).", + "doc": "Sat u danu (0-23) za noćno treniranje. Odaberite miran sat, npr. 2.", "label": "Uči u satu" }, "ml_training_interval_days": { - "doc": "Ponovno provjerite ima li poboljšanja najviše jednom u ovoliko dana.", + "doc": "Ponovno trenirajte najviše jednom u ovoliko dana.", "label": "Provjeri najviše svakih" }, "ml_training_min_cycles": { - "doc": "Prije finog ugađanja pričekajte da se snimi barem ovoliko ciklusa, tako da ima dovoljno za učenje.", + "doc": "Prije treniranja pričekajte barem ovoliko zabilježenih ciklusa.", "label": "Ciklusi potrebni prije učenja" }, "name": { - "doc": "Ime za prikaz prikazano na popisu HA integracija i registru uređaja.", + "doc": "Naziv ovog uređaja prikazan u Home Assistantu.", "label": "Naziv uređaja" }, "no_update_active_timeout": { - "doc": "Ako tijekom rada ne stignu ažuriranja napajanja ovoliko dugo, pretpostavite da je utikač ispao izvan mreže i prisilno ga zaustavite kako biste izbjegli zombi ciklus. Zadanih 600 s dopušta kašnjenje oblaka ili mreže.", + "doc": "Ako tijekom ciklusa ovoliko dugo ne stigne nijedno ažuriranje snage, pretpostavlja se da je utikač izvan mreže i ciklus se prisilno zaustavlja. Zadanih 600 s pokriva kašnjenja oblaka ili mesh mreže.", "label": "Vremensko ograničenje bez ažuriranja" }, "notify_before_end_minutes": { @@ -1350,35 +1335,35 @@ "label": "Upozorenje prije završetka" }, "notify_channel": { - "doc": "Naziv Android kanala obavijesti za početne/uživo poruke (kontrolira zvuk po kanalu i prioritet u mobilnoj aplikaciji). Prazno koristi zadanu popratnu aplikaciju.", + "doc": "Android kanal za poruke o početku i uživo; kanal određuje zvuk i prioritet. Prazno: zadani kanal aplikacije.", "label": "Android kanal (početak/praćenje)" }, "notify_cycle_timers": { - "doc": "Obavijesti u određenim minutama u ciklusu (npr. za dodavanje omekšivača). Poruka podržava {device}, {program}, {minutes}. Omogućite Automatsku pauzu da biste pauzirali u tom trenutku i primili interaktivnu obavijest s gumbom Nastavi; nastavite putem ploče, usluge pauze/nastavka ili radnje obavijesti.", + "doc": "Obavijesti u zadanim minutama ciklusa (npr. za dodavanje omekšivača). Poruka podržava {device}, {program}, {minutes}. Uz automatsku pauzu ciklus se ondje i pauzira, a obavijest dobiva gumb Nastavi.", "label": "Mjerači vremena ciklusa" }, "notify_finish_channel": { - "doc": "Naziv Android kanala obavijesti za poruku završetka. Prazno ponovno koristi početni kanal/kanal uživo.", + "doc": "Android kanal za poruku o završetku. Prazno: koristi se kanal za početak i uživo.", "label": "Android kanal (završetak)" }, "notify_finish_message": { - "doc": "Tijelo koje se šalje kada ciklus završi. Varijable predloška: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}. {time_finished} i {vs_typical} ovdje su najkorisniji.", + "doc": "Tekst koji se šalje kad ciklus završi. Varijable: {device}, {program}, {duration} (minute), {duration_hm} (1 h 05 min), {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}, {status} (completed ili force_stopped; prekinuti ciklusi ne šalju ništa). Za svaku drugu varijablu koristi se zadani tekst.", "label": "Poruka za završetak" }, "notify_finish_services": { - "doc": "notify.* usluge koje se pozivaju kada ciklus završi. Dodajte jedan po meti; ostavite prazno ako nema obavijesti o završetku.", + "doc": "notify.* usluge koje se pozivaju kad ciklus završi. Ostavite prazno za nijednu.", "label": "Usluge za završetak" }, "notify_fire_events": { - "doc": "Također aktivirajte događaje ha_washdata_* na početku/završetku ciklusa tako da možete izgraditi vlastite automatizacije.", + "doc": "Pokrenite i događaje ha_washdata_* pri početku i završetku ciklusa za vlastite automatizacije.", "label": "Aktiviraj HA događaje za obavijesti" }, "notify_icon": { - "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.", + "doc": "Mala ikona na obavijesti (npr. mdi:washing-machine). Šalje se samo na mobile_app ciljeve, ne na notify grupe. Na iOS-u zamjenjuje ikonu aplikacije i zahtijeva mobilnu aplikaciju 2026.8 ili noviju. Prazno: zadana ikona platforme.", "label": "Ikona obavijesti" }, "notify_live_chronometer": { - "doc": "U obavijesti (na platformama koje to podržavaju) umjesto statične procjene prikažite odbrojivač koji se ažurira uživo.", + "doc": "Prikažite odbrojavanje uživo u obavijesti (gdje je podržano) umjesto fiksne procjene.", "label": "Koristi kronometar uživo" }, "notify_live_interval_seconds": { @@ -1386,143 +1371,135 @@ "label": "Interval ažuriranja uživo" }, "notify_live_overrun_percent": { - "doc": "Ako ciklus premaši svoju procjenu za više od ovog postotka, pošalji upozorenje o prekoračenju.", + "doc": "Ažuriranja uživo prestaju kad ciklus premaši očekivano trajanje za ovaj postotak (20 = do 1,2x očekivanog vremena).", "label": "Prekoračenje uživo (%) prije upozorenja" }, "notify_live_services": { - "doc": "notify.* usluge pozvane za ažuriranje napretka uživo dok traje ciklus. Ostavite prazno da biste onemogućili obavijesti o napretku uživo.", + "doc": "notify.* usluge za ažuriranja napretka uživo dok ciklus traje. Ostavite prazno da ih isključite.", "label": "Usluge za praćenje uživo" }, "notify_only_when_home": { - "doc": "Šaljite obavijesti samo kada je barem jedna od povezanih osoba (gore) kod kuće.", + "doc": "Obavještavajte samo kad je barem jedna od osoba iznad kod kuće.", "label": "Obavijesti samo kod kuće" }, "notify_people": { - "doc": "person.* entiteti koje koristi \"Obavijesti samo kada sam kod kuće\" za odlučivanje je li netko kod kuće.", + "doc": "person.* entiteti koje provjerava \"Obavijesti samo kod kuće\".", "label": "Osobe (za obavijesti samo kod kuće)" }, "notify_pre_complete_message": { - "doc": "Tijelo upozorenja prije kraja / skoro gotovo. Varijable predloška: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Tekst svakog ažuriranja napretka uživo. Varijable predloška: {device}, {program}, {minutes} (preostale minute). Za svaku drugu varijablu koristi se zadani tekst.", "label": "Poruka pred završetak" }, "notify_reminder_message": { - "doc": "Tijelo podsjetnika za istovar koji još uvijek čeka. Prazno koristi ugrađenu zadanu vrijednost. Varijable predloška: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Tekst jednokratnog Upozorenja prije završetka. Varijable predloška: {device}, {program}, {minutes} (postavka Upozorenje prije završetka). Za svaku drugu varijablu koristi se zadani tekst.", "label": "Poruka podsjetnika" }, "notify_start_message": { - "doc": "Tijelo koje se šalje kada ciklus započne. Varijable predloška: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Tekst koji se šalje kada ciklus započne. Varijable predloška: {device}, {program}. Za svaku drugu varijablu koristi se zadani tekst.", "label": "Poruka za početak" }, "notify_start_services": { - "doc": "notify.* usluge koje se pozivaju kada ciklus započne. Dodajte jedan po cilju (telefon, nadzorna ploča itd.); ostavite prazno ako nema obavijesti o početku.", + "doc": "notify.* usluge koje se pozivaju kad ciklus počne (telefon, nadzorna ploča, ...). Ostavite prazno za nijednu.", "label": "Usluge za početak" }, "notify_timeout_seconds": { - "doc": "Automatski odbaci obavijest nakon toliko sekundi (na platformama koje to podržavaju). 0 ga čuva dok se ručno ne odbaci.", + "doc": "Zatvorite obavijest nakon ovoliko sekundi (gdje je podržano). 0 je zadržava dok je ne zatvorite.", "label": "Automatsko odbacivanje nakon" }, "notify_title": { - "doc": "Naslov obavijesti. Varijable predloška: {device}, {program}, {duration}, {energy}, {cost}, {date}, {time}, {minutes}.", + "doc": "Naslov obavijesti. Zamjenjuje se samo {device}; za svaku drugu varijablu koristi se zadani naslov.", "label": "Naslov obavijesti" }, "notify_unload_delay_minutes": { - "doc": "Nekoliko minuta nakon završetka ciklusa prije slanja podsjetnika za \"istovar stroja\" koji još uvijek čeka. Postavite 0 da biste onemogućili podsjetnik.", + "doc": "Minute nakon završetka ciklusa prije podsjetnika \"istovarite stroj\". 0 ga isključuje.", "label": "Odgoda podsjetnika za istovar" }, "notify_unload_repeat": { - "doc": "Ponovno šalje podsjetnik za istovar svakih \"Odgoda podsjetnika za istovar\" minuta dok se istovar ne potvrdi ili dok ne dodirnete \"Prestani podsjećati\" na obavijesti. Potreban je način za potvrdu istovara: Entitet senzora vrata, Entitet potvrde istovara ili opcija \"Ručno potvrdi istovar\" u nastavku. Gumb za odbacivanje radi na obavijestima mobilne aplikacije Home Assistant (companion app).", + "doc": "Ponovno šaljite podsjetnik za istovar svakih \"Odgoda podsjetnika za istovar\" minuta dok se istovar ne potvrdi ili ne dodirnete \"Prestani podsjećati\" (mobilna aplikacija). Zahtijeva Entitet senzora vrata, Entitet potvrde istovara ili \"Ručno potvrdi istovar\".", "label": "Ponavljaj do istovara" }, "off_delay": { - "doc": "Vrijeme čekanja nakon pada struje prije nego što se proglasi da je ciklus završen. Ako se struja nastavi unutar ovog prozora, ciklus se nastavlja neprimjetno - ovo premošćuje pauze između faza pranja. Perilice posuđa imaju duge faze sušenja (isključivanje na 20-60 min) tako da odgoda isključivanja mora biti veća od te kako bi cijeli ciklus pranja+sušenja bio jedan ciklus.", + "doc": "Koliko dugo snaga mora ostati niska prije nego što ciklus završi. Povratak snage u tom vremenu nastavlja ciklus. Perilice posuđa mogu mirovati 20-60 min tijekom sušenja, pa postavite dulje od toga.", "label": "Odgoda isključivanja" }, "pause_cuts_power": { - "doc": "Kada je ciklus pauziran, također isključite entitet Switch u nastavku. Samo za uređaje čiji se utikač može sigurno odrezati usred ciklusa.", + "doc": "Kad je ciklus pauziran, isključite i Entitet prekidača ispod. Samo za uređaje koji sigurno mogu izgubiti napajanje usred ciklusa.", "label": "Pauza prekida napajanje (putem prekidača)" }, "power_off_delay": { - "doc": "Koliko dugo snaga mora ostati ispod Praga isključivanja napajanja nakon završetka ciklusa prije nego što se stanje vrati na „Isključeno“. Koristi se samo kada je Prag isključivanja napajanja iznad 0. Provjerava se u pozadinskom ritmu, pa se efektivna odgoda zaokružuje naviše na sljedeći otkucaj isteka stanja.", + "doc": "Koliko dugo snaga mora ostati ispod Praga isključivanja napajanja nakon ciklusa prije nego što se stanje vrati na Isključeno. Koristi se samo kad je taj prag iznad 0. Provjerava se povremeno, pa stvarno čekanje može biti malo dulje.", "label": "Odgoda isključivanja napajanja" }, "power_off_threshold_w": { - "doc": "Opcijsko otkrivanje isključenog stanja na temelju snage. Kada je iznad 0, nakon što ciklus završi i snaga ostane ispod ove razine tijekom Odgode isključivanja napajanja, stroj se smatra isključenim i stanje se vraća na „Isključeno“. Ostavite na 0 za onemogućavanje (zadano). Postavite ga iznad stvarne razine isključenog napajanja, a ispod Praga zaustavljanja i ispod potrošnje vašeg stroja u stanju pripravnosti (završeno, ali još uključeno); ako nije ispod Praga zaustavljanja, zanemaruje se. Kada je omogućeno, zamjenjuje Odgodu poništavanja napretka za povratak na „Isključeno“, pa završeni stroj ostaje na „Gotovo/Čist“ dok se stvarno ne isključi.", + "doc": "Nakon ciklusa snaga ispod ove vrijednosti tijekom Odgode isključivanja napajanja postavlja stanje na Isključeno, umjesto Odgode poništavanja napretka. Postavite iznad potrošnje isključenog uređaja, a ispod pripravnosti i Praga zaustavljanja. 0 ga isključuje (zadano).", "label": "Prag isključivanja napajanja" }, "power_sensor": { - "doc": "Entitet senzora koji prijavljuje živu snagu u vatima za ovaj uređaj (npr. sensor.washer_power). Sve detekcije ciklusa temelje se na ovom signalu.", + "doc": "Senzor koji javlja trenutnu snagu ovog uređaja u vatima (npr. sensor.washer_power). Cijelo otkrivanje temelji se na njemu.", "label": "Senzor snage" }, - "profile_duration_tolerance": { - "doc": "Pojas +/- oko prosječnog trajanja profila koji se koristi tijekom podudaranja. 0,25 znači da profil od 60 minuta odgovara ciklusima od 45-75 minuta.", - "label": "Tolerancija trajanja profila" - }, "profile_match_interval": { - "doc": "Koliko često pokušavati pronaći profil tijekom ciklusa trčanja. Zadanih 300 s (5 minuta) uravnotežuje brzinu detekcije i CPU.", + "doc": "Koliko često podudarati ciklus u tijeku s profilima. Zadano 300 s (5 min).", "label": "Interval podudaranja" }, "profile_match_max_duration_ratio": { - "doc": "Maksimalna duljina ciklusa u odnosu na profil. 1.3 znači da ciklus mora biti manji od 130% trajanja profila da bi odgovarao.", + "doc": "Program se odbacuje kad ciklus traje dulje od ovog višekratnika njegova uobičajenog trajanja. 1,8: ciklus 80% dulji od programa više mu ne odgovara.", "label": "Maks. omjer trajanja" }, "profile_match_min_duration_ratio": { - "doc": "Minimalna duljina ciklusa u odnosu na profil. 0,9 znači da ciklus mora biti najmanje 90% trajanja profila da bi odgovarao.", + "doc": "Program se razmatra tek kad ciklus prođe ovaj udio njegova uobičajenog trajanja. 0,10: program od 2 sata postaje kandidat nakon 12 minuta. Ne primjenjuje se u prvih 15 minuta ciklusa.", "label": "Min. omjer trajanja" }, "profile_match_threshold": { - "doc": "Minimalni rezultat sličnosti (0-1) potreban na kraju ciklusa za prihvaćanje identifikacije programa. Podignite ga kako biste smanjili krive identifikacije; smanjite ga ako se programi vašeg stroja ne podudaraju. Zadana vrijednost 0,4 je konzervativna početna točka.", + "doc": "Rezultat podudaranja uživo koji ciklus treba prije nego što ga Pametni završetak ili završetak anti-bore smije ranije završiti. Ne mijenja prikazani program. Povećajte ako slaba podudaranja prerano završavaju cikluse; smanjite ako jasna podudaranja čekaju odgodu isključivanja.", "label": "Prag podudaranja" }, "profile_unmatch_threshold": { - "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.", + "doc": "Ako rezultat podudaranja uživo padne ispod ove vrijednosti, trenutna pretpostavka programa se briše. Držite malo ispod Praga podudaranja kako je kratak pad ne bi poništio.", "label": "Prag nepodudaranja" }, "progress_reset_delay": { - "doc": "Nakon završetka, zadržite napredak na 100% ovoliko dugo kako bi Završeno bilo vidljivo na nadzornim pločama prije vraćanja na stanje mirovanja.", + "doc": "Nakon ciklusa držite napredak na 100% ovoliko dugo prije vraćanja na Miruje.", "label": "Odgoda poništavanja napretka" }, "pump_stuck_duration": { - "doc": "Nekoliko sekundi pumpa može raditi neprekidno prije nego što se označi kao moguće zaglavljena (pokreće događaj zaglavljene pumpe).", + "doc": "Koliko dugo pumpa smije raditi bez prekida prije nego što se pokrene događaj zaglavljene pumpe.", "label": "Trajanje zaglavljene pumpe" }, "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.", + "doc": "Najmanje vrijeme između obrađenih očitanja. Očitanja koja stignu ranije preskaču se (niska očitanja tijekom ciklusa uvijek se zadržavaju). Postavite na interval javljanja utikača ili manje; veća vrijednost samo odbacuje očitanja.", "label": "Interval uzorkovanja" }, "save_debug_traces": { - "doc": "Pohranite puno praćenje snage i odgovarajuće podatke o otklanjanju pogrešaka za svaki ciklus. Korisno za rješavanje problema, ali povećava veličinu pohrane.", + "doc": "Spremajte uz svaki ciklus i podatke za otklanjanje pogrešaka podudaranja (rezultate svih kandidata), za rješavanje problema. Dodaje oko 20 KB po ciklusu.", "label": "Spremi tragove za otklanjanje pogrešaka" }, "smart_termination_duration_ratio": { - "doc": "Koliko daleko u očekivanom trajanju podudarajućeg programa ciklus mora biti prije nego što ga Pametni završetak može ranije završiti nakon pada snage. Očekivano trajanje je prosjek programa, pa kod uređaja čije vrijeme rada jako varira - perilice s hladnom dovodnom vodom zimi u odnosu na toplu ljeti, sušilice sa senzorskim sušenjem, programi ovisni o količini rublja - otprilike polovica svih pokretanja završi kraće od tog prosjeka i nikada ne dobije brzo završavanje, već završi tek putem pričuvnog vremenskog ograničenja, nekoliko minuta kasnije. Kod takvih strojeva smanjite ovu vrijednost (npr. 0,85) kako bi se ranije završavanje ipak pokrenulo; povećajte je prema 1,0 za oprezniji pristup. Ostavite prazno za zadanu vrijednost (0,98, ili 0,99 za perilice posuđa). Ciklus može uvijek samo ranije završiti, nikada kasnije, i nikada se ne pokreće kod nejasnog podudaranja ili podudaranja niske pouzdanosti.", + "doc": "Udio prosječnog trajanja programa koji ciklus mora dosegnuti prije nego što ga Pametni završetak smije ranije završiti. Ako vremena rada variraju (temperatura vode, sušenje po senzoru, količina rublja), smanjite ga (npr. 0,85). Ostavite prazno za 0,98 (0,99 kod perilica posuđa).", "label": "Omjer pametnog završetka" }, - "smoothing_window": { - "doc": "Koliko je neobrađeni signal snage izglađen. Low (2) je osjetljiv, ali bučan; visoko (5) izglađuje skokove, ali dodaje kašnjenje.", - "label": "Prozor izglađivanja" - }, "start_duration_threshold": { - "doc": "Snaga mora ostati iznad početnog praga ovoliko dugo da bi se potvrdio pravi početak, sprječavajući prekidače za uključivanje/isključivanje u djeliću sekunde da započnu ciklus.", + "doc": "Snaga mora ostati iznad Praga pokretanja ovoliko dugo prije nego što ciklus počne, kako bi se kratka uključivanja i isključivanja zanemarila.", "label": "Trajanje pokretanja" }, "start_energy_threshold": { - "doc": "Energija (snaga x vrijeme) koju uređaj mora potrošiti prije RADA. Kratki skok velike snage ima vrlo nisku energiju i zanemaruje se, sprječavajući lažne startove.", + "doc": "Energija koju uređaj mora potrošiti prije nego što se ciklus računa kao pokrenut. Kratak skok snage troši malo energije, pa ne pokreće ciklus.", "label": "Energija pokretanja" }, "start_threshold_w": { - "doc": "Snaga mora porasti iznad ove razine kako bi se potvrdilo da je ciklus započeo. Postavljanje na prenisko uzrokuje lažna pokretanja iz stanja pripravnosti; propuštaju se previsoki i sporo startni programi (hladno punjenje). Sugestivni mehanizam postavlja ovo malo iznad uočene najniže aktivne snage stroja.", + "doc": "Snaga mora porasti iznad ove vrijednosti da ciklus počne. Prenisko: potrošnja u pripravnosti uzrokuje lažne početke. Previsoko: programi koji sporo kreću (hladno punjenje) se propuštaju.", "label": "Prag pokretanja" }, "stop_threshold_w": { - "doc": "Snaga mora pasti ispod ove razine prije nego što počne odbrojavanje odgode isključivanja. Postavite ga ispod početnog praga - razmak između njih je pojas histereze koji sprječava treperenje. Ako su postavljene previsoko, faze niske snage (zaustavljanja ispiranja, protiv gužvanja) lažno pokreću završetak slijeda.", + "doc": "Snaga mora pasti ispod ove vrijednosti prije nego što odgoda isključivanja počne odbrojavati. Držite ispod Praga pokretanja. Ako je previsoko, faze male snage (pauze ispiranja, anti-bore) greškom pokreću odbrojavanje kraja.", "label": "Prag zaustavljanja" }, "switch_entity": { - "doc": "Dodatni prekidač koji se isključuje prilikom pauze i ponovno uključuje kada se nastavlja, koristi se zajedno s \"Pauza također prekida napajanje\".", + "doc": "Neobavezni prekidač koji se isključuje pri pauzi i ponovno uključuje pri nastavku, uz \"Pauza prekida napajanje\".", "label": "Entitet prekidača" }, "watchdog_interval": { - "doc": "Koliko često pozadinski nadzorni pas provjerava zaustavljene senzore i protekla vremena. Zadano 30 s.", + "doc": "Koliko često pozadinska provjera traži zastale senzore i istekla vremenska ograničenja. Zadano 30 s.", "label": "Interval nadzora" }, "notify_milestone_message": { @@ -1530,127 +1507,82 @@ "label": "Poruka o prekretnici" }, "notify_milestones": { - "doc": "Brojevi ciklusa odvojeni zarezima koji pri dostizanju pokreću jednokratnu obavijest o proslavi (npr. 50, 100, 500, 1000). Prazno onemogućuje obavijesti o prekretnicama.", + "doc": "Brojevi ciklusa koji pokreću jednokratnu čestitku, odvojeni zarezima (npr. 50, 100, 500, 1000). Prazno ih isključuje.", "label": "Prekretnice ciklusa" }, "notify_quiet_end_hour": { - "doc": "Kraj razdoblja Ne ometaj (0-23). Zadržane obavijesti isporučuju se u ovom satu. Ostavite prazno za onemogućavanje.", + "doc": "Kraj razdoblja ne ometaj (0-23); zadržane obavijesti tada se šalju. Prazno ga isključuje.", "label": "Kraj tihih sati" }, "notify_quiet_start_hour": { - "doc": "Početak razdoblja Ne ometaj (0-23). Obavijesti o završetku, podsjetniku i čistom rublju koje bi se aktivirale tijekom tihih sati zadržavaju se i isporučuju kad razdoblje završi. Ostavite prazno za onemogućavanje. Podržava razdoblja koja prelaze ponoć (npr. početak 22, kraj 7).", + "doc": "Početak razdoblja ne ometaj (0-23). Obavijesti o završetku, podsjetnici i obavijesti o čistom rublju u tom razdoblju zadržavaju se do njegova kraja. Može prelaziti ponoć (npr. 22 do 7). Prazno ga isključuje.", "label": "Početak tihih sati" }, "peak_rate_message": { - "doc": "Neobavezni prilagođeni tekst za savjet o vršnoj tarifi dodan obavijesti o početku. Varijable predloška: {device}, {price}. Prazno koristi ugrađenu zadanu vrijednost.", + "doc": "Vlastiti tekst savjeta o vršnoj tarifi. Varijable predloška: {device}, {price}. Prazno: zadani tekst.", "label": "Poruka o vršnoj tarifi" }, "peak_rate_threshold": { - "doc": "Kada ciklus započne, a trenutna cijena po kWh je jednaka ili viša od ove vrijednosti, obavijesti o početku dodaje se savjet o vršnoj tarifi. 0 ili prazno onemogućuje savjet.", + "doc": "Ako je cijena po kWh na početku ciklusa jednaka ili veća od ove, dodajte savjet o vršnoj tarifi obavijesti o početku. 0 ili prazno ga isključuje.", "label": "Prag vršne tarife (po kWh)" }, - "dtw_bandwidth": { - "label": "Tolerancija podudaranja oblika", - "doc": "Koliko modul za podudaranje smije vremenski iskriviti ciklus da odgovara obliku spremljenog programa. Viša vrijednost podnosi veće varijacije brzine; previsoka riskira podudaranje s pogrešnim programom." - }, - "corr_weight": { - "label": "Težina oblika u odnosu na razinu", - "doc": "Ravnoteža između oblika krivulje i apsolutne razine snage pri ocjenjivanju podudaranja. Viša vrijednost daje prednost obliku; niža daje prednost slaganju razine snage." - }, - "duration_weight": { - "label": "Težina trajanja", - "doc": "Koliko snažno se daje prednost programu čije se tipično trajanje podudara s ovim ciklusom." - }, - "energy_weight": { - "label": "Težina energije", - "doc": "Koliko snažno se daje prednost programu čija se tipična potrošnja energije podudara s ovim ciklusom." - }, "store_brand": { "label": "Marka uređaja", - "doc": "Neobavezno. Marka uređaja odabrana iz kataloga zajednice. Koristi se za pronalaženje i dijeljenje odgovarajućih referentnih snimki. Ostavite prazno ako ne koristite online značajke." + "doc": "Neobavezno. Marka vašeg uređaja iz kataloga zajednice, koristi se za pronalaženje i dijeljenje referentnih snimki. Ostavite prazno ako ne koristite mrežne značajke." }, "store_model": { "label": "Model uređaja", - "doc": "Neobavezno. Model uređaja odabran iz kataloga zajednice nakon što je marka postavljena. Ako vaš model nije na popisu, možete ga dodati u katalog." + "doc": "Neobavezno. Model vašeg uređaja iz kataloga zajednice (najprije odaberite marku). Ako nije na popisu, možete ga dodati." }, "show_contributor": { "doc": "Prikaži ime suradnika na profilima preuzetim iz trgovine zajednice" }, - "enable_phase_matching": { - "label": "Preostalo vrijeme prema fazama", - "doc": "Svaki aktivni ciklus dijeli na faze (grijanje, pranje, centrifuga) i raspoređuje preostalo vrijeme po fazama, spojeno s klasičnom procjenom - na početku ciklusa oslanja se na proračun faza, a pri kraju na klasičnu procjenu. To prilagođava odbrojavanje tome koliko se vaš uređaj stvarno grije i radi, što je najuočljivije u prvoj polovici ciklusa. Isključeno = samo klasična procjena. Utječe samo na prikaz preostalog vremena; podudaranje programa i otkrivanje ciklusa ostaju nepromijenjeni." - }, - "keep_min_score": { - "label": "Min. ocjena podudaranja", - "doc": "Minimalna ocjena sličnosti potrebna kandidatu za ostanak u utrci podudaranja. Zadana vrijednost 0,1 je namjerno blaga - dopušta č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." - }, - "dtw_blend": { - "label": "Miješanje deformacije", - "doc": "Koliko DTW ocjena poravnanja zamjenjuje osnovnu ocjenu Faze 2. 0 = koristi samo ocjenu Faze 2, 1 = koristi samo DTW ocjenu, 0,5 (zadano) = jednako miješanje. Povećajte za veće oslanjanje na warp poravnanje kada programi imaju slične razine snage ali različite obrasce tajminga." - }, - "dtw_ensemble_w": { - "label": "Miks kombinacije DTW", - "doc": "U kombiniranom DTW načinu (zadanom), težina skaliranog L1 nasuprot derivatnog DTW (DDTW). 1,0 = sve skalirano L1, 0 = sve DDTW, 0,7 = zadano. Skalirana L1 varijanta uspoređuje razine snage; derivatna varijanta reagira na promjene snage kroz vrijeme. Kombinirani način spaja oboje." - }, - "dtw_ddtw_scale": { - "label": "Skala derivatne deformacije", - "doc": "Udaljenost polu-zasićenja za ocjenjivanje derivatnim DTW. Ocjena se prepolovi kada je DDTW udaljenost jednaka ovoj vrijednosti. Manje = osjetljivije na razlike u obliku. Zadana vrijednost 30 je kalibrirana za tipične tragove snage kućanskih aparata. Utječe samo na kombinirani i ddtw DTW način." - }, - "dtw_refine_top_n": { - "label": "Broj preciziranih kandidata", - "doc": "Koliko prvih kandidata Faze 2 DTW ponovo ocjenjuje. DTW je skuplji pa se primjenjuje samo na N najboljih kandidata. Zadano 5. Povećajte (na 7-9) ako ispravni profil ponekad dostigne samo 4. ili 5. mjesto nakon Faze 2 - DTW ga može spasiti. Smanjenje malo ubrzava podudaranje." - }, - "duration_scale": { - "label": "Oštrість trajanja", - "doc": "Oštrina kazne za nepodudaranje trajanja u Fazi 4. Ovo je omjer logaritma pri kojemu se ocjena slaganja prepolovi. Manje = strože: nepodudaranje trajanja više snižava ocjenu. Zadana vrijednost 0,175 odgovara toleranciji trajanja od oko 18% pri polu-težini. Koristite zajedno s Težinom trajanja." - }, - "energy_scale": { - "label": "Oštrina energije", - "doc": "Oštrina kazne za nepodudaranje energije u Fazi 4. Manje = strože: nepodudaranje energije više snižava ocjenu. Zadana vrijednost 0,25 je namjerno blaža od Oštrine trajanja jer energija varira s opterećenjem. Koristite zajedno s Težinom energije." - }, "power_profile_interval_min": { "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." + "doc": "Veličina odsječka atributa power_profile (prosječni vati po utoru) koji koriste planeri poput EMHASS i tibber_prices. Manji čuva kratke skokove; veći izglađuje krivulju. Zadano 15 min. 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." + "doc": "Samo iOS. Ažurirajte napredak uživo tiho, uz prioritet koji štedi bateriju i koji iOS može grupirati. Prvo ažuriranje te obavijesti o početku i završetku i dalje upozoravaju. Isključeno: svako ažuriranje upozorava." }, "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." + "doc": "Samo Android. Zadržite obavijest o napretku uživo nakon dodira. Isključeno: dodir je zatvara." }, "notify_live_click_action": { "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." + "doc": "Što otvara dodir obavijesti ovog uređaja. Prazno: WashData panel ovog uređaja. Upišite putanju nadzorne ploče (npr. /lovelace/laundry) ili URL, ili \"none\" da dodir ništa ne otvara." }, "door_opens_at_end": { "label": "Vrata se automatski otvaraju na kraju", - "doc": "Za perilice posuđa koje na kraju ciklusa otvaraju vrata radi sušenja (AirDry i slično). Kada je uključeno, otvaranje vrata za vrijeme pokrenuta ciklusa više ne zaustavlja ciklus zauvijek; umjesto toga, ako vrata ostanu otvorena koliko je navedeno ispod, WashData smatra ciklus završenim. Kratko otvaranje (dodavanje predmeta) se zanemaruje. Zahtijeva entitet senzora vrata." + "doc": "Za perilice posuđa koje na kraju otvaraju vrata radi sušenja (npr. AirDry). Vrata otvorena tijekom vremena zadržavanja ispod završavaju ciklus umjesto da ga pauziraju; kratko otvaranje se zanemaruje. Zahtijeva Entitet senzora vrata." }, "door_end_dwell_seconds": { "label": "Zadržavanje otvorenih vrata za završetak", - "doc": "Koliko dugo vrata moraju ostati otvorena prije nego WashData završi ciklus, kada je uključena postavka \"Vrata se automatski otvaraju na kraju\". Dovoljno dugo da se ignorira brzo dodavanje posuđa (zadano 60 s), dovoljno kratko za brz završetak kad perilica otvori vrata." + "doc": "Uz uključeno \"Vrata se automatski otvaraju na kraju\": koliko dugo vrata moraju ostati otvorena prije nego što ciklus završi. Dovoljno dugo da se zanemari dodavanje posuđa (zadano 60 s)." }, "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." + "doc": "Koje vrste ciklusa oblikuju krivulju i podudaranje svakog programa. Poništavanje odabira vrste ništa ne briše; korisno ako ne vjerujete uvezenim podacima. Statistika uvijek broji samo cikluse ovog stroja. Poništavanje svih se zanemaruje." }, "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." + "doc": "Naplatite svaki ciklus po cijeni u svakom trenutku njegova rada, umjesto po cijeni kad je završio. Zahtijeva entitet cijene. Obrada povijesti (Dijagnostika) preračunava starije cikluse." }, "unload_confirm_entity": { "label": "Entitet potvrde istovara", - "doc": "Opcijski entitet čije aktiviranje znači da je sadržaj izvađen, za strojeve koji ne mogu imati senzor vrata. Radi bilo koja vrsta: Zigbee ili NFC gumb, input_button pomoćnik, senzor pokreta ispred stroja, scena. Svaka promjena stanja broji se kao potvrda, osim isključivanja i prelaska u nedostupno stanje. Postavljanjem ovoga ujedno se omogućuje podsjetnik za istovar na uređaju bez Entiteta senzora vrata." + "doc": "Neobavezno. Bilo koji entitet koji potvrđuje da je sadržaj izvađen, za strojeve bez senzora vrata (gumb, input_button, senzor pokreta, scena). Broji se svaka promjena stanja osim isključivanja i nedostupnosti. Uključuje i podsjetnik za istovar." }, "unload_track_without_door": { "label": "Ručno potvrdi istovar", - "doc": "Pokrenite podsjetnik za istovar na uređaju bez Entiteta senzora vrata i bez Entiteta potvrde istovara te istovar potvrdite iz vlastite automatizacije: pritisnite entitet gumba \"Označi kao istovareno\" ili pozovite uslugu ha_washdata.mark_unloaded. Zanemaruje se kada je postavljen Entitet senzora vrata, koji već daje taj signal." + "doc": "Pokrenite podsjetnik za istovar bez senzora vrata ili entiteta potvrde. Potvrdite istovar iz vlastite automatizacije gumbom \"Označi kao istovareno\" ili uslugom ha_washdata.mark_unloaded. Zanemaruje se kad je postavljen Entitet senzora vrata." }, "notify_icon_color": { "label": "Boja obavijesti", - "doc": "Naglasna boja obavijesti ovog uređaja, kako bi se perilica rublja, sušilica i perilica posuđa razlikovale na prvi pogled. iOS oboji ikonu i traku napretka Live Activity na zaključanom zaslonu i u Dynamic Island. Android dobiva istu boju, ali što će s njom učiniti ovisi o telefonu: neki oboje ikonu, a mnogi (osobito Samsung One UI) je potpuno zanemaruju. Obje platforme razumiju Hex (npr. #03A9F4). Ostavite prazno za zadanu boju platforme." + "doc": "Boja naglaska obavijesti ovog uređaja, kako bi se perilica, sušilica i perilica posuđa lako razlikovale. Na iOS-u ispunjava krug iza ikone i boji traku Live Activity. Mnogi Android telefoni (osobito Samsung) je zanemaruju. Koristite hex, npr. #03A9F4. Prazno: zadana boja platforme." + }, + "share_name": { + "doc": "Prikažite naziv svog računa kao autora ciklusa koje dijelite. Isključeno: dijeli se bez imena." } }, "setting_group": { @@ -1678,9 +1610,6 @@ "door_pause": { "label": "Vrata i pauza" }, - "dtw_refinement": { - "label": "DTW rafiniranje" - }, "duration_gates": { "label": "Vrata trajanja" }, @@ -1757,17 +1686,20 @@ "rebuilding": "Ponovna izgradnja…", "analyzing": "Analiziranje…", "resetting": "Resetiranje…", - "reverting": "Vraćanje…", "trimming": "Obrezivanje…", "splitting": "Dijeljenje…", + "stalled": "Zaglavljeno", "deleting": "Brisanje…", "imported": "Uvezeno", "preparing": "Priprema…", - "unlabelling": "Uklanjanje oznake…" + "unlabelling": "Uklanjanje oznake…", + "working": "Obrada…", + "match_uncertain": "Nesigurno: {top} ili {runner}", + "match_uncertain_single": "Nesigurno: možda {top}", + "match_sure": "~{pct}% sigurnosti", + "match_uncertain_tip": "WashData se još nije odlučio za program. Broj pokazuje koliko je često vodeća pretpostavka, {top}, bila točna u prošlim ciklusima uz ovakvu prednost. Samo informativno." }, "suggestion": { - "both_agree": "WashData preporučuje", - "calibrated_label": "Kalibrirano", "observed_label": "Izmjereno", "impact": { "auto_label_confidence": { @@ -1782,10 +1714,6 @@ "higher": "Zatvara samo cikluse sa značajnom potrošnjom energije", "lower": "Zatvara i kraće ili manje energetske cikluse" }, - "end_repeat_count": { - "higher": "Zahtijeva više ponavljanja signala kraja – pouzdanije, malo sporije", - "lower": "Završava nakon manje ponavljanja – brže otkrivanje, veći rizik od lažnog kraja" - }, "learning_confidence": { "higher": "Uči samo iz visoko pouzdanih podudaranja – sporije, ali pouzdanije", "lower": "Uči iz više ciklusa; model može biti manje precizan" @@ -1824,40 +1752,25 @@ } }, "reason": { - "auto_label_confidence": "Pouzdanost 15. percentila za {cycles} automatskih oznaka koje korisnik nikad nije ispravio ({p15}).", "completion_min_seconds": "Polovica trajanja čistog ciklusa p05 ({minutes} min) kroz {cycles} ciklusa; filtrira lažne cikluse.{excl}", - "dead_zone_batch": "Zadržano kratkim (medijan prozora nestabilnosti pri pokretanju kroz {cycles} ciklusa = {s}s) kako bi se stvarni kraj brzo prepoznao.", - "dead_zone_from_dip": "Na temelju ranog pada snage prepoznatog pri {s}s.", - "end_energy_default": "Zadana preporučena osnovna vrijednost za prag šuma na kraju ciklusa.", - "end_energy_false": "Energija lažnih završetaka p95 ({p95}Wh) kroz {cycles} ciklusa, s donjom granicom od 0,2 % medijana energije ciklusa ({floor}Wh).", - "end_energy_no_false": "Nema lažnih završetaka kroz {cycles} ciklusa; s donjom granicom od 0,2 % medijana energije ciklusa ({floor}Wh).", - "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.", + "match_interval_floored": "Najkraći program ({shortest}s) ograničio bi ovo na {cap}s, ispod minimuma od {minimum}s, pa se koristi minimum: {persistence} uzastopnih podudaranja traje {budget}s, više od {pct}% tog programa. Podudaranje se izvodi samo kad stigne očitanje (medijan={median}s), pa to ništa ne košta.", "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.", "min_off_gap": "Na temelju {gaps} uočenih razmaka između ciklusa (p05={p05}s). Donja granica uređaja: {floor}s.", "min_off_gap_bridge": "Postavljeno tako da premosti najdulji tihi odsječak unutar ciklusa: {span}s + 60s rezerve, iz {spans} premoštenih razmaka kroz {cycles} čistih ciklusa (ostaje ispod vašeg najkraćeg razmaka između dva uzastopna pokretanja, koji iznosi {ceiling}s).", "min_power": "40 % najniže radne snage p05 ({p05}W) kroz {cycles} čistih ciklusa, čime prag isključenja ostaje ispod stvarne potrošnje.{excl}", - "ml_auto_label": "Najniža pouzdanost koju je model kvalitete još ocijenio čistom (p10 od {cycles} čistih ciklusa = {p10}).", - "ml_end_repeat": "{false}/{total} ciklusa ({pct}%) imalo je pauzu koju je detektor kraja ocijenio s >50 %; zatraži {val} potvrdu/potvrde kraja.", - "ml_off_delay": "Pauze potvrđene detektorom kraja: p95 {p95}s + 60s rezerve, iz {pauses} modelom provjerenih pauza kroz {cycles} ciklusa (donja granica {floor}s).", "no_update_timeout": "Na temelju uočene učestalosti ažuriranja (p95={p95}s) * 20 (najmanje 60s).", "off_delay_cadence": "Na temelju uočene učestalosti ažuriranja (p95={p95}s) * 5", "off_delay_device_floor": "Korišten sigurni minimum specifičan za uređaj {device} ({floor}s).", "off_delay_generic_floor": "Korišten opći sigurni minimum ({floor}s).", "off_delay_pauses": "Postavljeno tako da nadmaši stvarne pauze: pauza unutar ciklusa p95 {p95}s + 60s rezerve, iz {pauses} pauza kroz {cycles} čistih ciklusa (donja granica {floor}s).", - "profile_match_threshold": "Pouzdanost p10 za {cycles} točnih automatskih oznaka ({p10}); sigurna donja granica za potvrdu tijekom rada.", - "sampling_interval": "Medijan intervala ažuriranja uočen kroz {cycles} čistih ciklusa ({si}s).{excl}", - "smoothing_window": "Postavljeno za izglađivanje ~30s očitanja pri uzorkovanju od {si}s ({samples} uzoraka).", - "start_duration": "Zadržano kratkim (~jedan interval uzorkovanja od {si}s) kako bi prepoznavanje počelo što ranije, uz zanemarivanje pojedinačnih skokova.", - "thr_batch": "Zadržano tik iznad najniže radne snage p05 kroz {cycles} ciklusa ({p05}W) kako bi se početak uhvatio što ranije, a prag zaustavljanja ostao ispod najniže radne snage stroja.", - "tol_per_profile": "p75 varijance trajanja po profilu kroz {profiles} profila ({cycles} ciklusa); dosljedni profili nisu kažnjeni.", - "tol_pooled": "Na temelju objedinjene varijance trajanja {cycles} novijih označenih ciklusa (p95 odstupanje={dev}).", - "watchdog": "Zadržano što nižim uz sigurnost (tik iznad razmaka ažuriranja p95 od {p95}s i najmanje 2× intervala uzorkovanja od {median}s, najmanje 30s) kako bi se zastoji brzo uhvatili bez lažnih zaustavljanja." + "standby_floor": "Svaki nedavni ciklus je i dalje trošio {idle}W u trenutku završetka, pa prag ispod te vrijednosti nikad ne može registrirati isključivanje uređaja. Zadržan je iznad nje; {cycles} čistih ciklusa u evidenciji nikada nije palo ispod njega dulje od {pause}s.", + "thr_batch": "Malo iznad najniže aktivne snage (p05) u {cycles} ciklusa ({p05}W), kako bi se počeci rano uhvatili, a prag zaustavljanja ostao ispod najniže radne snage.", + "watchdog": "Zadržano što nižim uz sigurnost (tik iznad razmaka ažuriranja p95 od {p95}s i najmanje 2× intervala uzorkovanja od {median}s, najmanje 30s) kako bi se zastoji brzo uhvatili bez lažnih zaustavljanja.", + "end_energy_floor": "Povećano na minimum koji proizlazi iz praga zaustavljanja ({stop} W) tijekom odgode isključivanja ({off_delay} s); ispod toga ciklus može završiti samo rezervnim putem.", + "max_duration_ratio_default": "Vraćeno na zadanu vrijednost: niža gornja granica odbacuje ispravan program kad god ciklus traje dulje nego obično.", + "min_duration_ratio_default": "Vraćeno na zadanu vrijednost: viša donja granica sprječava prepoznavanje ciklusa u tijeku sve dok dobro ne odmakne." }, "exclusions": { "summary": "Isključeno {total} pogrešno otkrivenih ciklusa: {parts}.", @@ -1875,7 +1788,8 @@ "no_power": "bez snage", "no_active_power": "bez aktivne snage" } - } + }, + "now_value": "(trenutno {v})" }, "tab": { "advanced": "Napredno", @@ -1906,13 +1820,17 @@ }, "toast": { "catalog_refreshed": "Katalog zajednice je osvježen", + "task_name_required": "Dajte zadatku naziv", + "task_added": "Zadatak dodan", + "task_save_failed": "Nije moguće spremiti zadatak: {error}", + "task_removed": "Zadatak uklonjen", + "maint_logged": "Označeno kao obavljeno: {task}", "access_saved": "Kontrola pristupa spremljena", "all_wiped": "Svi podaci izbrisani", "analysis_complete_none": "Analiza dovršena: nema novih prijedloga", "auto_label_complete": "Automatsko označavanje dovršeno", "automation_deleted": "Automatizacija izbrisana", "correction_submitted": "Ispravak je poslan", - "cycle_deleted": "Ciklus izbrisan", "cycle_force_stopped": "Ciklus prisilno zaustavljen", "cycle_labelled": "Ciklus označen", "cycle_paused": "Ciklus pauziran", @@ -1928,11 +1846,9 @@ "group_deleted": "Grupa izbrisana", "group_name_required": "Naziv grupe je obavezan", "group_saved": "Grupa je spremljena", - "import_successful": "Uvoz uspješan; ponovno učitavanje integracije", "json_required": "Potrebni su JSON podaci", "legacy_removed": "Naslijeđene radnje uklonjene", "logs_exported": "Dnevnici izvezeni", - "matching_reverted": "Odgovarajuće težine vraćene su na zadane vrijednosti", "min_2_profiles": "Odaberite najmanje 2 profila za grupu", "ml_training_no_improvement": "Obuka završena: osnovna linija zadržana (bez poboljšanja)", "models_reverted": "Modeli na uređaju vraćeni su na početnu vrijednost", @@ -1942,8 +1858,8 @@ "pg_preset_loaded": "Preset \"{name}\" učitan", "pg_preset_saved": "Preset \"{name}\" spremljen", "pg_sugg_loaded": "Učitano {n} predloženih vrijednosti - pokrenite pješčanik za usporedbu", - "pg_sugg_ml_loaded": "Učitano {n} ML-kalibriranih vrijednosti - pokrenite pješčanik za usporedbu", "phase_name_required": "Potreban je naziv faze", + "phase_reset": "Faza \"{name}\" vraćena na ugrađenu verziju", "phase_updated": "Faza ažurirana", "phases_saved": "Faze spremljene", "preferences_saved": "Postavke spremljene", @@ -1990,15 +1906,11 @@ "store_name_required": "Unesite naziv profila", "store_import_failed": "Uvoz nije uspio: {error}", "store_imported": "Uvezeno u {profile}", - "store_program_required": "Unesite naziv programa", "store_no_appliance": "Najprije postavite marku i model uređaja u Postavkama.", "store_share_failed": "Dijeljenje nije uspjelo: {error}", "store_shared": "Podijeljeno u zajedničku trgovinu, čeka pregled.", "contrib_device_changed": "Dodano u zajedničku trgovinu, ali se odabrani uređaj promijenio dok je obrazac bio otvoren. Odaberite ga u Postavkama.", "appliance_added": "Uređaj dodan, čeka odobrenje", - "thanks_confirming": "Hvala na potvrdi", - "device_approved": "Odobreno od strane zajednice", - "rating_saved": "Ocjena kvalitete spremljena", "brand_added": "Marka dodana, čeka odobrenje", "profile_added": "Profil dodan, čeka odobrenje", "saved_except_conflicts": "Spremljeno. Ispravite istaknute sukobe da spremite ostalo.", @@ -2021,7 +1933,50 @@ "odometer_saved": "Ukupan broj ciklusa spremljen", "odometer_save_failed": "Nije moguće spremiti ukupan broj: {error}", "store_pick_profile_share": "Odaberite profil za dijeljenje", - "remove_failed": "Uklanjanje nije uspjelo: {error}" + "remove_failed": "Uklanjanje nije uspjelo: {error}", + "analysis_complete": "Analiza dovršena: prijedloga: {count}", + "analysis_failed": "Analiza nije uspjela: {error}", + "auto_detect_failed": "Automatsko otkrivanje nije uspjelo: {error}", + "auto_label_failed": "Automatsko označavanje nije uspjelo: {error}", + "could_not_load_cycle": "Nije moguće učitati ciklus: {error}", + "create_failed": "Stvaranje nije uspjelo: {error}", + "cycles_deleted": "Izbrisano ciklusa: {count}", + "debug_cleared": "Očišćeno tragova otklanjanja pogrešaka: {count}", + "discard_failed": "Odbacivanje nije uspjelo: {error}", + "error": "Pogreška: {error}", + "export_failed": "Izvoz nije uspio: {error}", + "import_failed": "Uvoz nije uspio: {error}", + "invalid_json": "\"{key}\" nije valjan JSON - ispravite ga ili ispraznite polje prije spremanja.", + "label_failed": "Označavanje nije uspjelo: {error}", + "ml_training_promoted": "Treniranje dovršeno: aktivirano modela: {count}", + "pause_failed": "Pauziranje nije uspjelo: {error}", + "phase_created": "Faza \"{name}\" stvorena", + "phase_deleted": "Faza \"{name}\" izbrisana", + "profile_created": "Profil \"{name}\" stvoren", + "rename_failed": "Preimenovanje nije uspjelo: {error}", + "save_failed": "Spremanje nije uspjelo: {error}", + "split_complete": "Podijeljeno na cikluse: {count}", + "start_failed": "Pokretanje nije uspjelo: {error}", + "stop_failed": "Zaustavljanje nije uspjelo: {error}", + "update_failed": "Ažuriranje nije uspjelo: {error}", + "store_download_skipped": "Preskočene snimke: {n} (prekratke, s previše praznina ili već na vašem uređaju).", + "store_import_low_quality": "Ova snimka je prekratka, s previše praznina ili nevjerodostojna da bi se uvezla.", + "store_import_duplicate": "Ova snimka već je na vašem uređaju.", + "device_approved": "Odobreno od strane zajednice", + "thanks_confirming": "Hvala na potvrdi", + "rating_saved": "Ocjena kvalitete spremljena", + "store_download_partial": "Programi koji se nisu mogli preuzeti: {n}; pokušajte ponovno kasnije.", + "import_no_restore_point": "Uvezeno, ali točka vraćanja nije se mogla spremiti, pa se ovaj uvoz ne može poništiti", + "import_undo_failed": "Uvoz nije moguće poništiti: {error}", + "import_undone": "Uvoz poništen: vraćeno {profiles} profila i {cycles} ciklusa", + "auto_label_cancelled": "Automatsko označavanje otkazano nakon označavanja {count} ciklusa.", + "auto_label_cancelled_one": "Automatsko označavanje otkazano nakon označavanja {count} ciklusa.", + "auto_label_cancelled_few": "Automatsko označavanje otkazano nakon označavanja {count} ciklusa.", + "auto_label_cancelled_other": "Automatsko označavanje otkazano nakon označavanja {count} ciklusa.", + "auto_label_done": "Automatsko označavanje dovršeno: označeno je {count} ciklusa, bez oznake: {skipped}.", + "auto_label_done_one": "Automatsko označavanje dovršeno: označen je {count} ciklus, bez oznake: {skipped}.", + "auto_label_done_few": "Automatsko označavanje dovršeno: označena su {count} ciklusa, bez oznake: {skipped}.", + "auto_label_done_other": "Automatsko označavanje dovršeno: označeno je {count} ciklusa, bez oznake: {skipped}." }, "trend": { "down": "Trend pada", @@ -2029,6 +1984,10 @@ }, "maint": { "bearing_service": "Servis ležajeva", + "salt": "Dopuni sol", + "rinse_aid": "Dopuni sredstvo za ispiranje", + "lint_filter": "Očisti filtar za dlačice", + "condenser_clean": "Očisti kondenzator", "descale": "Uklanjanje kamenca", "drum_clean": "Očisti bubanj", "filter_clean": "Očisti filtar", @@ -2036,34 +1995,22 @@ }, "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", + "smart_termination_duration_ratio": "Udio prosječnog trajanja programa koji ciklus mora dosegnuti prije nego što ga Pametni završetak smije ranije završiti; smanjite ako vremena rada variraju", + "anti_crease_finalize_ratio": "Udio prosječnog trajanja programa koji ciklus mora dosegnuti prije nego što ga rep anti-bore smije dovršiti; smanjite kod sušilica, kod perilica ostavite zadano", "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_exit_power": "Razina mirovanja između impulsa okretanja; djeluje samo iznad Praga zaustavljanja", "anti_wrinkle_idle_timeout": "Dopušteno vrijeme mirovanja između dva impulsa bubnja prije nego što anti-bora završi", "anti_wrinkle_max_duration": "Duži impuls završava anti-boru i otvara novi ciklus", "anti_wrinkle_max_power": "Impuls iznad ove vrijednosti završava anti-boru i otvara novi ciklus", "completion_min_seconds": "Najkraći rad koji se broji kao pravi ciklus", - "end_repeat_count": "Uzastopna niska očitanja prije završetka", "interrupted_min_seconds": "Kratki ciklusi označeni kao prekinuti", "min_off_gap": "Razmak potreban za razdvajanje dvaju ciklusa", "off_delay": "Sekunde niske snage prije završetka ciklusa", "start_duration_threshold": "Sekunde iznad praga za potvrdu pokretanja", "start_threshold_w": "Najmanja snaga u vatima da se broji kao pokrenuto", "stop_threshold_w": "Ispod ovoga uređaj se broji kao isključen", - "dtw_bandwidth": "Faza 3: warp opseg Sakoe-Chiba (0 = DTW isključen; zadano 0,2 = 20% duljine ciklusa)", - "corr_weight": "Faza 2: ravnoteža između oblika krivulje (korelacija) i razine snage (MAE); zadano 0,45", - "duration_weight": "Faza 4: koliko jako slaganje trajanja utječe na konačnu ocjenu (zadano 0,22)", - "energy_weight": "Faza 4: koliko jako slaganje energije utječe na konačnu ocjenu (zadano 0,22)", - "profile_match_min_duration_ratio": "Faza 1: minimalna duljina ciklusa kao udio trajanja profila; zadano 0,1 (10%)", - "profile_match_max_duration_ratio": "Faza 1: maksimalna duljina ciklusa kao udio trajanja profila; zadano 1,8 (180%)", - "keep_min_score": "Faza 2: minimalna ocjena za ostanak u utrci; zadano 0,1 dopušta slabe podudarnosti, kasniji koraci biraju najboljeg", - "dtw_blend": "Faza 3: 0 = samo osnovna ocjena, 1 = samo DTW ocjena, 0,5 = jednako miješanje (zadano)", - "dtw_ensemble_w": "Faza 3 (kombinirani način): težina skaliranog L1 nasuprot derivatnog DTW; zadano 0,7 favorizira razinu", - "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)", + "profile_match_min_duration_ratio": "Minimalna duljina ciklusa kao udio trajanja profila; zadano 0,1 (10%)", + "profile_match_max_duration_ratio": "Maksimalna duljina ciklusa kao udio trajanja profila; zadano 1,8 (180%)", "curve_preroll_seconds": "Sekunde očitanja iz prekinutih pokušaja pokretanja prenesene na početak krivulje ciklusa; 0 = isključeno" }, "col": { @@ -2071,12 +2018,14 @@ "status_tip": "Ishod ciklusa: Dovršeno (prirodni kraj), Prekinuto (nagli pad snage), Prisilno zaustavljeno (ručno) ili Potrebna provjera (čeka se povratna informacija).", "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 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.", + "energy_tip": "Ukupna potrošena energija (kWh), izračunata iz snage kroz vrijeme.", + "cost_tip": "Trošak energije ovog ciklusa, utvrđen kad je završio. Uz uključen Vremenski ponderiran trošak svaki se trenutak naplaćuje po svojoj cijeni; inače po cijeni na kraju. Postavite cijenu u Postavkama da se ovo 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", - "cost_dynamic_tip": "Vremenski ponderirano: naplaćeno po cijeni koja je vrijedila tijekom ciklusa (efektivno {price} {cur}/kWh)." + "cost_dynamic_tip": "Vremenski ponderirano: naplaćeno po cijeni koja je vrijedila tijekom ciklusa (efektivno {price} {cur}/kWh).", + "dbg_conf": "Povj.", + "dbg_mae": "MAE", + "dbg_corr": "Kor." }, "stat": { "avg": "pros.", @@ -2100,7 +2049,10 @@ "notify_finish": "Poslala bi se obavijest o završetku.", "notify_milestone": "Poslala bi se obavijest o prekretnici.", "notify_held": "Obavijest je zadržana zbog tihih sati.", - "finished": "Ciklus je dosegao završno stanje i završio." + "finished": "Ciklus je dosegao završno stanje i završio.", + "match_reverted": "Program se vratio na otkrivanje: njegov rezultat pao je znatno ispod vlastitog vrha.", + "verified_pause": "Poravnanje uživo potvrdilo je (ili ukinulo) pauzu unutar programa. Dok pauza traje, ciklus se ne završava.", + "stalled": "Ciklus je ostao na potrošnji u pripravnosti dulje nego što ovaj program ikad ondje pauzira, a posao nije gotov. Prikazano kao Pauzirano (Zaglavljeno); ciklus ostaje otvoren." }, "store": { "search_ph2": "Pretraži po marki ili modelu…", @@ -2109,11 +2061,7 @@ "programs_count": "Programi: {n}", "browse": "Pregledaj", "device": "Uređaj", - "favorites": "Favoriti", - "search_ph": "Pretraži po marki ili modelu…", "no_results": "Nisu pronađeni odgovarajući uređaji. Pokušajte s drugim pretraživanjem.", - "n_programs": "{n} programa", - "n_cycles": "{n} ciklusa", "no_programs": "Za ovaj uređaj još nema podijeljenih programa.", "no_cycles": "Za ovaj program još nema podijeljenih referentnih ciklusa.", "anon": "anonimno", @@ -2125,19 +2073,21 @@ "merge_target": "Spoji u profil", "description": "Opis (neobavezno)", "contributed_by": "od {name}", + "website": "Web-stranica trgovine ↗", + "website_tip": "Otvorite web-stranicu zajedničke trgovine", + "favorites": "Favoriti", "rating_summary": "★ {avg} ({n})", "no_ratings": "Još nema ocjena", - "website": "Web-stranica trgovine ↗", - "website_tip": "Otvorite web-stranicu zajedničke trgovine" + "unreachable": "Nije moguće povezati se sa zajedničkom trgovinom. Provjerite vezu i pokušajte ponovno." }, "settings": { "gear": { - "title": "Postavke" + "title": "Preferencije" } }, "link": { - "manual": "Priručnik ↗", - "add_appliance": "Dodajte svoj uređaj" + "maintenance_supporter": "Maintenance Supporter na GitHubu ↗", + "manual": "Priručnik ↗" }, "tip": { "add_brand": "Marke nema na popisu? Dodajte je u katalog zajednice", @@ -2171,7 +2121,13 @@ "rebuild": { "envelopes": "Ponovna izgradnja ovojnica" }, - "cancelling": "Otkazujem..." + "cancelling": "Otkazujem...", + "auto_label": { + "running": "Automatsko označavanje ciklusa" + }, + "store_download": { + "running": "Preuzimanje iz trgovine" + } }, "setup": { "hdr": { @@ -2180,26 +2136,24 @@ }, "phase0": { "washer": "WashData već otkriva vaše cikluse. Snimite prvi ciklus kako biste omogućili nazive programa i procjene vremena.", - "dishwasher": "WashData prati. Perilice posuđa imaju složene cikluse – snimanje prvog ciklusa je izrazito preporučeno. Ako otkriveni ciklus traje predugo, upotrijebite uređivač ciklusa za obrezivanje prije pohrane kao profila.", + "dishwasher": "WashData prati. Ciklusi perilice posuđa su složeni, pa snimite prvi ciklus. Ako otkriveni ciklus traje predugo, obrežite ga u uređivaču ciklusa prije spremanja kao profila.", "generic": "WashData prati. Snimite ili označite otkriveni ciklus kako biste počeli graditi profile." }, "phase1a": { - "labelled": "Dobar početak – vaš prvi program je pohranjen. Za najčišće podatke razmislite o snimanju sljedećeg ciklusa pomoću widgeta snimača." + "labelled": "Dobar početak: vaš prvi program je spremljen. Za najčišće podatke snimite sljedeći ciklus snimačem." }, "phase1b": { - "recorded": "Vaše snimanje pohranjeno je kao {profile_name}. Sada snimite ili označite ostale uobičajene programe za izgradnju pokrivenosti." + "recorded": "Vaša snimka spremljena je kao {profile_name}. Sada snimite ili označite ostale česte programe." }, "phase1c": { - "verify": "Imate {count} programa od zajednice. Pokrenite ciklus kako biste provjerili prepoznaje li ih WashData ispravno – podudaranje će se poboljšati kako vaš uređaj gradi vlastitu povijest." + "verify": "Imate programe iz zajednice ({count}). Pokrenite ciklus da provjerite prepoznaje li ga WashData; podudaranje se poboljšava kako uređaj gradi vlastitu povijest." }, "phase2": { - "cluster": "WashData je vidio {count} ciklusa koji ne odgovaraju nijednom pohranjenome programu – nalikuju jedni drugima. Želite li za njih stvoriti novi profil?", + "cluster": "WashData je uočio slične cikluse ({count}) koji ne odgovaraju nijednom spremljenom programu. Izraditi novi profil za njih?", "unmatched": "Vaš zadnji ciklus nije odgovarao nijednom pohranjenome programu. Je li to novi program?" }, "phase3": { - "suggestions": "WashData ima preporuke postavki temeljene na vašoj povijesti ciklusa – pregledajte ih kako biste poboljšali točnost otkrivanja.", - "groups": "Neki vaši profili izgledaju kao isti program pri različitim temperaturama. Organizirajte ih u grupu za bolje podudaranje.", - "phases": "Dodajte faze programa u {profile_name} za točnije procjene preostalog vremena." + "suggestions": "WashData ima prijedloge postavki iz povijesti vaših ciklusa. Pregledajte ih za bolje otkrivanje." }, "phase4": { "healthy": "Ovaj uređaj je potpuno postavljen ({profile_count} profila)." @@ -2212,12 +2166,58 @@ "create_from_cluster": "Stvori profil od ovih ciklusa", "create_profile": "Stvori za njega profil", "review_suggestions": "Preglej prijedloge", - "organise_profiles": "Organiziraj profile", - "configure_phases": "Konfiguriraj faze", "skip_step": "Preskoči ovaj korak", "skip_forever": "Ne prikazuj više", "hide_guidance": "Sakrij upute", "show_guidance": "Prikaži upute" } + }, + "access": { + "edit": "Uređivanje", + "full": "Potpuno", + "inherit": "Naslijedi", + "none": "Nema (skriveno)", + "read": "Čitanje" + }, + "pg_label_reason": { + "no_winner": "Nijedan program ne odgovara", + "below_floor": "Ispod praga \"Pouzdanost učenja\"", + "ambiguous": "Označeno kao nejasno", + "margin": "Preblizu drugom najboljem programu", + "unknown_profile": "Program više ne postoji", + "not_evaluated": "Nije procijenjeno" + }, + "diagram": { + "accum": "akum.", + "auto": "auto", + "below_off": "ispod = isklj.", + "confirmed": "potvrđeno", + "energy_counts": "energija se broji", + "feedback": "povratna inf.", + "gap_below_min": "razmak ispod min. = jedan ciklus", + "heat_phase": "faza grijanja", + "held_at_100": "ostaje na 100 %", + "ignored": "zanemareno", + "kept": "zadržano", + "max": "maks.", + "min": "min.", + "minus_tol": "-tol", + "no_match": "nema podudaranja", + "no_updates": "nema ažuriranja", + "off_delay_wait": "odgoda isključivanja", + "off_gap": "razmak isklj.", + "plus_tol": "+tol", + "profile": "profil", + "reading_interval": "tipični interval očitanja", + "sensor_offline": "senzor izvan mreže: prisilno zaustavljanje", + "si": "IU", + "spike_ignored": "skok zanemaren", + "spike_ignored_short": "skok: zanemaren", + "start": "pokretanje", + "stop": "zaustavljanje", + "tail_energy_resets": "energija kraja iznad praga: odbrojavanje se resetira", + "thr": "prag", + "too_short": "prekratak", + "tumble_kept": "impulsi bubnja zadržani" } } diff --git a/custom_components/ha_washdata/translations/panel/hu.json b/custom_components/ha_washdata/translations/panel/hu.json index 3ecd0e24..b695fd87 100644 --- a/custom_components/ha_washdata/translations/panel/hu.json +++ b/custom_components/ha_washdata/translations/panel/hu.json @@ -3,6 +3,7 @@ "min": "perc", "entity_not_found": "Entitás nem található", "running_long": "Sokáig fut", + "stalled": "Elakadt", "remaining": "hátralévő", "progress": "haladás", "show_time_remaining": "Hátralévő idő megjelenítése", @@ -48,16 +49,16 @@ } }, "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_tip": "A vége felé ez a program kb. {mins} percre elcsendesedik, majd kb. {watts} W-ot vesz fel {secs} másodpercig. {measured} mért ciklusból {seen} esetben fordult elő.", "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", + "artifact_tip": "{n} rendellenesség észlelve (pl. ajtónyitás a ciklus közepén) - nyissa meg, hogy megtekinthesse őket a grafikonon", + "artifact_tip_one": "{n} rendellenesség észlelve (pl. ajtónyitás a ciklus közepén) - nyissa meg, hogy megtekinthesse a grafikonon", + "artifact_tip_other": "{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", "declining": "↘ csökkenő", "energy_low": "A szokásosnál kevesebb energia", "energy_spike": "A szokásosnál több energia", - "fair_fit": "elfogadható minőség", - "fair_fit_tip": "Mérsékelt egyezési konzisztencia – néhány ehhez a profilhoz rendelt ciklus alacsonyabb megbízhatósági pontszámmal rendelkezik. Címkézz fel több ciklust, vagy rögzítsd újra a profilt a pontosság javítása érdekében.", "feedback_requested": "Visszajelzést kértünk", "golden_cycle": "Rögzített referenciaciklus", "duration_outlier": "{n} szokatlan hosszúságú ciklus", @@ -66,27 +67,36 @@ "needs_review": "Felülvizsgálatra szorul", "overrun": "A szokásosnál tovább futott", "poor_fit": "rossz illeszkedés", - "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", + "restart_gap_tip": "{n} HA-újraindítás-rés: a teljesítménygörbén lyukak vannak", + "restart_gap_tip_one": "{n} HA-újraindítás-rés: a teljesítménygörbén van egy lyuk", + "restart_gap_tip_other": "{n} HA-újraindítás-rés: a teljesítménygörbén lyukak vannak", "reviewed": "Áttekintve", "steady": "→ állandó", "underrun": "A szokásosnál gyorsabban fejeződött be", - "using_defaults": "A szállított alapértelmezett értékek használata", - "using_tuned": "Hangolt súlyok használata", "overrun_ratio": "(a várt {x}-szerese)", "underrun_ratio": "(a várt {pct}%-a)", - "awaiting_tip": "Közösségi jóváhagyásra vár", - "awaiting_n": "Jóváhagyásra vár · {n} megerősítés", - "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.", - "armed": "(a következő ciklusra érvényes)" + "armed": "(a következő ciklusra érvényes)", + "awaiting_tip": "Közösségi jóváhagyásra vár", + "awaiting_n": "Jóváhagyásra vár · {n} megerősítés", + "awaiting": "Jóváhagyásra vár", + "approved": "Jóváhagyva", + "matcher_unused": "Még egyik ciklusához sem illesztve", + "matcher_used": "{n} ciklusához illesztve", + "matcher_used_one": "{n} ciklusához illesztve", + "matcher_used_other": "{n} ciklusához illesztve", + "matcher_used_tip": "A készülék azon ciklusai, amelyeket a WashData magától ehhez az importált programhoz egyeztetett. Ha egy import sosem egyezik, lehet, hogy nem illik a készülékéhez; eltávolíthatja. Helyben számolva; semmi sem kerül a boltba." }, "btn": { + "pg_preset_delete": "Előbeállítás törlése", + "pg_preset_load": "Előbeállítás betöltése", + "pg_preset_load_tip": "A Homokozó értékeinek lecserélése erre az előbeállításra (az aktív beállítások változatlanok maradnak)", + "pg_preset_save": "Mentés előbeállításként", + "pg_preset_save_tip": "Az alábbi összes érték mentése névvel ellátott előbeállításként ehhez az eszközhöz", "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", @@ -97,12 +107,11 @@ "add_timer": "+ Időzítő hozzáadása", "apply_all": "Alkalmazza az összeset", "apply_label": "Címke alkalmazása", - "apply_set_b": "B készlet alkalmazása", "apply_split": "Felosztás alkalmazása", "apply_trim": "Vágás alkalmazása", "auto_detect_split": "Automatikus felismerés", "auto_label_cycles": "Automatikus címkézési ciklusok", - "auto_label_cycles_tip": "Automatikusan rendelje hozzá a profilneveket azokhoz a felcímkézetlen ciklusokhoz, amelyek egyezési megbízhatósága eléri a küszöbértéket", + "auto_label_cycles_tip": "Nevezze el azokat a címkézetlen ciklusokat, amelyek egyezési megbízhatósága eléri a küszöböt.", "cancel": "Mégsem", "clear_debug": "Törölje a hibakeresési adatokat", "clear_splits": "Törlés", @@ -112,8 +121,6 @@ "convert_to_automation": "Átalakítás automatizálásra", "correct": "Javítás…", "create": "Létrehozás", - "create_from_cluster": "Profil létrehozása {count} ciklusból", - "create_profile": "+ Profil létrehozása", "delete": "Törlés", "delete_group": "Csoport törlése", "delete_group_tip": "Csak ezt a csoportot törölje – a tagi profilok megmaradnak", @@ -124,22 +131,19 @@ "done": "Kész", "edit": "Szerkesztés", "export": "Exportálás", - "export_json": "Exportálás JSON-ba", "force_stop": "Kényszer leállítás", "force_stop_tip": "Azonnal zárja le az aktuális ciklust, és jelölje erőszakosan leállítottnak", "from_template": "Sablonból ▾", "ignore": "Mellőzés", "import_json": "Importálás JSON-ból", - "import_overwrite": "Importálás (adatok felülírása)", "inspect": "Vizsgálja meg", "label": "Címkézés", - "load": "Betöltés", "load_more": "Több betöltése", "manage": "Kezelése", "merge": "Egyesítés", "new_automation": "+ Új automatizálás", "new_group": "+ Új csoport", - "new_group_tip": "Csoportosítsa a közel azonos profilokat (azonos alak/időtartam, eltérő hőmérséklet vagy centrifugálás), hogy az egyezés megbízhatóan válasszon közöttük", + "new_group_tip": "Csoportosítsa a közel azonos profilokat (azonos alak, eltérő hőmérséklet vagy centrifugálás), hogy az egyeztetés meg tudja különböztetni őket.", "new_phase": "+ Új fázis", "new_profile": "+ Új profil", "new_profile_tip": "Hozzon létre új programprofilt meglévő felcímkézett ciklusból vagy rögzítésből", @@ -147,36 +151,25 @@ "on_cycle_started": "Beindult a ciklus", "pause_cycle": "Szünet", "pause_cycle_tip": "Szüneteltesse a futó ciklust – a készülék onnan folytatja, ahol abbahagyta", - "pg_apply_device": "Alkalmazás az eszközre", - "pg_autofill": "Automatikus kitöltés", - "pg_load_calibrated": "Kalibrált (ML) betöltése ({n})", - "pg_load_calibrated_tip": "Az ML által kalibrált beállításokat betölti a Homokozó felülírásaiként", "pg_load_live": "Aktív beállítások betöltése", "pg_load_live_tip": "Az integráció jelenlegi beállításainak újraolvasása és a Homokozó összes módosításának elvetése", "pg_load_suggested": "Javaslatok betöltése ({n})", "pg_load_suggested_tip": "Az automatikus hangolóeszköz aktuális javaslatait betölti a Homokozó felülírásaiként", - "pg_preset_delete": "Előbeállítás törlése", - "pg_preset_load": "Előbeállítás betöltése", - "pg_preset_load_tip": "A Homokozó értékeinek lecserélése erre az előbeállításra (az aktív beállítások változatlanok maradnak)", - "pg_preset_save": "Mentés előbeállításként", - "pg_preset_save_tip": "Az alábbi összes érték mentése névvel ellátott előbeállításként ehhez az eszközhöz", "pg_publish_all": "Mentés a beállításokba ({n})", "pg_publish_one": "A(z) \"{label}\" mentése az integráció beállításaiba", - "play": "Lejátszás", "process": "Feldolgozás", "process_history": "Feldolgozás most", "process_recording": "Felvétel mentése", "process_tip": "Mentse el a rögzített nyomot új vagy meglévő profilként", "rebuild": "Borítékok újjáépítése", "rebuild_envelope": "Boríték újraépítése", - "rebuild_tip": "Számítsa újra az elvárt energiaburkolót (min/max sáv) az összes profilhoz a felcímkézett ciklusaik alapján – futtassa új ciklusok felcímkézése vagy régiek javítása után", + "rebuild_tip": "Építse újra minden profil várt teljesítménysávját a címkézett ciklusai alapján. Futtassa ciklusok címkézése vagy javítása után.", "rec_start_tip": "Kezdje el rögzíteni a készülék teljesítménynyomát – indítsa közvetlenül a ciklus futtatása előtt", "rec_stop": "Leállítás", "rec_stop_tip": "Állítsa le a rögzítést, és tartsa fenn a rögzített nyomot áttekintésre", "record": "Indítsa el a Felvételt", "refresh": "Frissítés", "refresh_diag_tip": "Töltse újra a tárolási statisztikákat", - "refresh_logs_tip": "Töltse újra a legújabb naplórekordokat", "refresh_settings_tip": "Töltse újra a beállításokat a szerverről", "relabel": "Átcímkézés ({count})", "remove": "Távolítsa el", @@ -184,7 +177,6 @@ "reset": "Visszaállítás", "reset_ml_models_tip": "Dobja el a finomhangolt modelleket, és térjen vissza a beépített modellekhez. A WashData később újra megtanulhatja őket.", "reset_to_builtin": "Állítsa vissza a beépített modellekre", - "reset_to_defaults": "Állítsa vissza az alapértelmezett értékeket", "resume_cycle": "Ciklus folytatása", "resume_cycle_tip": "Folytassa a szüneteltetett ciklust", "revert_settings": "Változtatások elvetése", @@ -197,8 +189,6 @@ "run_analysis": "🔍 Javaslatelemzés futtatása", "run_analysis_tip": "Elemezze a rögzített ciklusokat most, és frissítse a hangolási javaslatokat", "run_auto_label": "Futtassa az Auto-Labelt", - "run_both": "A vs B futtatása", - "run_simulation": "Szimuláció futtatása", "save": "Mentés", "save_access_control": "Hozzáférés-vezérlés mentése", "save_group": "Csoport mentése", @@ -210,10 +200,8 @@ "save_settings": "Beállítások mentése", "select": "Válassza ki", "select_all": "Összes kijelölése", - "select_last20": "Utolsó 20", "select_none": "Egyik sem", "show_only": "Csak megjelenítés", - "skip_setup": "Beállítás kihagyása", "split": "Felosztás", "stop": "Leállítás", "submit_correction": "Javítás benyújtása", @@ -222,36 +210,31 @@ "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.", + "unlabel_selected_tip": "Eltávolítja a címkét a kijelölt ciklusokról. Az előzményekben maradnak, de 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})", - "group_suggest": "{n}. csoport: {members}", "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", "import": "Importálás", "disconnect": "Leválasztás", "connect_github": "GitHub összekapcsolása", - "save_appliance": "Készülék mentése", "share_to_store": "Megosztás a boltban", "share": "Megosztás", - "confirm_appliance": "Készülék megerősítése", - "confirmed": "Megerősítve", "browse_community_setups": "Közösségi beállítások böngészése", "download_device": "Beállítás letöltése", "share_device": "⬆ Eszköz megosztása", - "share_device_tip": "Ossza meg ezt a készüléket és a rögzített referenciaciklusait a közösségi boltban, hogy mások ugyanazzal a géppel magukévá tehessék", + "share_device_tip": "Ossza meg ezt a készüléket és referenciaciklusait a közösségi boltban, hogy az ugyanilyen gép tulajdonosai átvehessék őket.", "share_n": "{n} ciklus megosztása", "export_selected": "Exportálás (adatok kiválasztása)", "export_all": "Gyorsexport: minden", - "import_raw": "Speciális: mindent lecserél a JSON-ból", "download_export": "Export letöltése", "analyze_import": "Fájl elemzése", "import_selected": "Kiválasztottak importálása", "back": "Vissza", "mute_suggestion": "Ne javasolja többé ezt a beállítást", "reset_muted": "Elhallgatottak visszaállítása", + "reset_phase_tip": "A szerkesztés visszavonása és a beépített fázis visszaállítása", "import_power_history": "Teljesítményelőzmények importálása", "hist_read_recorder": "Beolvasás a Home Assistantból", "hist_scan": "Ciklusok keresése", @@ -260,7 +243,15 @@ "save_total_cycles": "Összesítés mentése", "clear": "Törlés", "add_brand": "Márka hozzáadása", - "add_appliance": "Készülék hozzáadása" + "log_done_task": "Elvégzés rögzítése: {task}", + "add_task": "Feladat hozzáadása", + "add_appliance": "Készülék hozzáadása", + "retry": "Újrapróbálás", + "confirm_appliance": "Készülék megerősítése", + "confirmed": "Megerősítve", + "create_profile_from_gap": "Profil létrehozása", + "undo_import": "Utolsó importálás visszavonása", + "pg_rerun": "Szimuláció frissítése" }, "note": { "off_delay": { @@ -268,10 +259,6 @@ } }, "conflict": { - "anti_wrinkle_exit": { - "exit": "A Leállítási küszöb alatt kell lennie ({stop} W); különben a ránctalanítás kilépési teljesítménye figyelmen kívül marad", - "stop": "A Ránctalanítás kilépési teljesítménye felett kell lennie ({exit} W)" - }, "anti_wrinkle_max": { "max": "Az Indítási küszöb felett kell lennie ({start} W); különben a ránctalanítás időtartam-korlátja megkerülődik", "start": "A Ránctalanítás maximális teljesítménye alatt kell lennie ({max} W)" @@ -279,10 +266,10 @@ "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." + "power_sensor": "Ez a készülék Teljesítmény érzékelője, nem ár. A ciklusok költségét a wattértékkel mint kWh-árral számolná; törölje ezt a mezőt.", + "energy_sensor": "Ez a készülék Energiamérője, nem ár. A ciklusok költségét a mérőóra állásával mint kWh-árral számolná; törölje ezt a mezőt.", + "device_class": "Ez az érzékelő {dc} mennyiséget mér, nem kWh-nkénti árat. Törölje ezt a mezőt, vagy válasszon tarifaérzékelőt.", + "unit": "Ez az érzékelő {unit} egységben mér, nem kWh-nkénti árat. Törölje ezt a mezőt, vagy válasszon tarifaérzékelőt." }, "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", @@ -317,8 +304,8 @@ "threshold": "Hatályba lépéséhez a Leállítási küszöb alatt kell lennie ({stop} W)" }, "pump_stuck": { - "duration": "Kisebb kell lennie a Frissítés nélküli időtúllépésnél ({to} s), hogy az elakadás-riasztás a felügyelő előtt aktiválódjon", - "timeout": "Meg kell haladnia az Elakadt szivattyú időtartamát ({ps} s), hogy a riasztás az erőszakos leállítás előtt aktiválódjon" + "duration": "Kisebbnek kell lennie a Frissítés nélküli időtúllépésnél ({to} mp), hogy az elakadás-riasztás előbb jelezhessen", + "timeout": "Nagyobbnak kell lennie az Elakadt szivattyú időtartamánál ({ps} mp), hogy az elakadás-riasztás előbb jelezhessen" }, "start_dur": { "sampling": "A Mintavételezési intervallum meghaladja az Indítási időtartamot ({sdt} s); egymintás tüskék ciklust nyithatnak", @@ -334,10 +321,15 @@ "watchdog": { "interval": "Legalább kétszerese kell legyen a Mintavételezési intervallumnak ({si} s)", "sampling": "A Mintavételezési intervallum legfeljebb a Felügyelő intervallum fele lehet ({wi} s)" + }, + "end_energy": { + "energy": "Túl szigorú a Leállítási küszöbhöz ({w} W) a Kikapcsolási késleltetés ({d} s) alatt; a ciklus csak tartalék úton érhet véget", + "stop": "A Záró energia ({e} Wh {d} s alatt) csak {p} W-ot enged meg" } }, "hdr": { - "ab_comparison": "A/B beállítások összehasonlítása", + "custom_tasks": "Saját feladatok", + "maintenance_supporter": "Többre van szüksége?", "access_control": "Hozzáférés-vezérlés", "add_maintenance": "Karbantartási esemény hozzáadása", "automation_delete": "Törölje ezt az automatizálást", @@ -346,28 +338,22 @@ "automation_review_this_cycle": "Tekintse át ezt a ciklust", "automations": "Automatizálások", "clear_debug": "Törölje a hibakeresési nyomokat", - "cycle_simulator": "Ciklusszimulátor", "diagnostics_pref": "Diagnosztika", "display": "Kijelző", - "dtw_inspector": "DTW-vizsgáló", "export_import": "Exportálás / Importálás", - "getting_started": "Első lépések", "live_power": "Élő teljesítmény", "logs": "Naplók", - "logs_diagnostics": "Diagnosztika", "maintenance": "Karbantartási műveletek", "maintenance_log": "Karbantartási napló", "maintenance_reminders": "Szerviz emlékeztetők", "manual_recording": "Kézi rögzítés", - "manual_recording_tip": "Szándékosan futtasson egy ciklust, miközben a WashData rögzíti a teljesítménynyomot. Indítsa közvetlenül a készülék elindulása előtt, állítsa le, amikor befejezi, majd kezelje, hogy névvel ellátott profilként mentse.", + "manual_recording_tip": "Rögzítsen szándékosan egy ciklust: nyomja meg a Kezdés gombot közvetlenül a készülék indulása előtt, a Leállítás gombot a végén, majd a Feldolgozás gombbal mentse profilként.", "ml_learned": "Amit a WashData megtanult", - "ml_matching_tuning": "Program-illesztő finomhangolás", "ml_settings_card": "Beállítások elemre", "ml_smart_learning": "Intelligens tanulás", "my_preferences": "Saját preferenciáim", "no_automations": "Egyelőre egyetlen automatika sem utal erre az eszközre.", "panel_settings": "Panelbeállítások", - "param_sweep": "Paraméterpásztázás", "pg_settings_source": "Beállítások forrása", "phase_catalog": "Fázis katalógus", "playground": "Homokozó", @@ -376,11 +362,8 @@ "status": "Állapot", "status_graph": "Állapotgrafikon", "storage_stats": "Tárolási statisztikák", - "test_history": "Tesztelés előzményeken", "toggle_sidebar": "Kapcsolja be a Home Assistant oldalsávot", - "tools_and_data": "Eszközök és adatok", "wipe_history": "Törölje az előzményeket", - "recommendations": "Javaslatok ({n})", "pg_detection_params": "Érzékelési beállítások", "pg_outcome": "Szimuláció eredménye", "pg_across_cycles": "Az összes ciklusban", @@ -388,7 +371,8 @@ "online_account": "Közösségi bolt és online funkciók", "import_power_history": "Teljesítményelőzmények importálása", "service_status": "Szerviz állapota", - "online_account_tab": "Online és közösség" + "online_account_tab": "Online és közösség", + "live_match_debug": "Élő egyezés hibakeresése" }, "health": { "fair": "Elfogadható profilminőség", @@ -396,12 +380,17 @@ "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_tip": "Pozíció az egyező program rögzített görbéjén: {pct}%. A ciklus és a profil összeillesztéséből adódik, ezért hosszabb vagy rövidebb futásnál is helyes marad. Akkor frissül, amikor a készülék csendes, ezért késhet.", "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", + "task_name": "Feladat", + "every_cycles": "Gyakoriság (ciklus)", + "every_days": "Gyakoriság (nap)", + "new_task_name": "Új feladat", + "days_since_service": "{since} / {total} nap", "actions": "Akciók", "activity": "Tevékenység", "administrators": "Rendszergazdák", @@ -409,10 +398,8 @@ "aria_cycle_chart": "Ciklus teljesítménygörbe", "aria_envelope_chart": "Profil teljesítményboríték grafikon", "aria_phase_chart": "Fázisszerkesztő grafikon", - "aria_playground_chart": "Ciklus teljesítménygörbe húzható érzékelési küszöbökkel", "aria_power_chart": "Teljesítményfelvétel grafikon", "aria_spaghetti_chart": "Egymásra helyezett ciklus teljesítménygörbék", - "aria_sweep_chart": "Paraméterpásztázás eredményeinek grafikonja", "artifact_anomaly": "Rendellenesség", "artifact_high_power": "Magas teljesítmény", "artifact_interruption": "Megszakítás", @@ -426,149 +413,99 @@ "cohesion_good": "kohézió {pct}%", "cohesion_low": "⚠ alacsony kohézió {pct}%", "compare_profiles": "Hasonlítsa össze a profilokkal", - "concurrency": "Egyidejűség", "confidence": "Bizalom", "confidence_threshold": "Bizalmi küszöb", "correct_duration": "Helyes időtartam (perc, opcionális)", "correct_profile": "Helyes profil", - "correlation": "Korreláció", "cost": "Költség", "create_new_profile": "+ Új profil létrehozása…", "current_phase": "Jelenlegi fázis", - "curves": "Görbék", "cycle": "Ciklus", "cycle_date_display": "Ciklusdátum kijelzés", - "cycle_health": "Ciklus egészség", - "cycle_index": "ciklusindex →", - "cycles": "Ciklusok", "cycles_count": "Ciklusok", "cycles_lc": "ciklus", - "dashed": "szaggatott", "date": "Dátum", "debug_traces": "Hibakeresési nyomok", - "default": "Alapértelmezett", "default_access_level": "Alapértelmezett szint az alábbiakban nem szereplő felhasználók számára", "default_other": "Alapértelmezett (más eszközök)", "default_tab": "Alapértelmezett lap a panel megnyitásakor", - "delta": "Δ", "description": "Leírás", - "dtw_blend_w": "Keverési súly", - "dtw_blended": "Kombinált pontszám", - "dtw_ddtw": "DDTW pontszám", - "dtw_l1": "L1 pontszám", - "dtw_matched_profile": "(a ciklus egyeztetett profilja)", "duration": "Időtartam", - "duration_agreement": "Időtartam-egyezés", - "emphasis": "Hangsúly", "enable_access_control": "Felhasználónkénti hozzáférés-vezérlés engedélyezése", "energy": "Energia", - "energy_agreement": "Energia-egyezés", - "event_timeline": "Eseményidővonal", "event_type": "Eseménytípus", "expected": "Várt", + "suspected_profile": "{name} (feltételezett)", + "runner_up_profile": "{name} (második helyezett)", "expected_duration": "Várható időtartam (perc)", "file_kb": "Fájl (kB)", - "final_score": "Végső pontszám", "from": "Ettől", "gap_s": "rés(ek)", "group_name": "Csoport neve", "head_trim": "Vágás az elejéről (s)", - "health": "Egészség", "hide_tabs": "Lapok elrejtése nem rendszergazdák számára", - "in_use": "Használatban", "json_data": "JSON adatok", "label": "Program", "last": "Utolsó", "last_checked": "Utoljára ellenőrizve:", "load_from_file": "Betöltés fájlból", - "mae_score": "MAE pontszám", "manual_duration": "Manuális időtartam (perc, opcionális)", "match": "Egyezés", "members": "Tagok", - "metric": "Mérőszám", "mode_existing_profile": "Hozzáadás a meglévő profilhoz", "mode_new_profile": "Új profil létrehozása", "models_fine_tuned": "(finomhangolt modellek: {count})", - "n_classic_suggestions": "{n} klasszikus", - "n_ml_suggestions": "{n} ML", "n_selected": "{n} kijelölve", "n_tuning_suggestions": "{n} hangolási javaslat", - "na": "nincs", + "n_tuning_suggestions_one": "{n} hangolási javaslat", + "n_tuning_suggestions_other": "{n} hangolási javaslat", "new_profile_name": "Új profilnév", "notes": "Jegyzetek", - "of": "/", - "outcome": "Eredmény", "panel_default_tab": "Alapértelmezett lap", "panel_language": "Panel nyelve", "per_cycle_short": "ciklus", - "pg_autofill_tip": "Tartomány automatikus kitöltése a jelenlegi értékből ({lo}–{hi})", "pg_best_value": "A talált legjobb érték", "pg_current_value": "Jelenlegi érték:", "pg_drag_hint": "Húzza a vonalat a grafikonon", - "pg_dtw": "DTW", "pg_ev_ending": "Befejezés", "pg_ev_running": "Fut", "pg_group_advanced": "Szélsőséges esetek", "pg_group_detection": "Érzékelési kiváltók", "pg_group_timing": "Időzítési szabályok", "pg_idle": "Tétlen", - "pg_last_run": "Utolsó futtatás: {date}", - "pg_match_score": "Egyezési pontszám", - "pg_pct_of": "{total} {p}%-a", "pg_poor_match": "Rossz egyezés", "pg_preset": "Homokozó előbeállítás", "pg_preset_name": "Új előbeállítás neve", "pg_preset_none": "Válasszon előbeállítást…", "pg_strong_match": "Erős egyezés", - "pg_tile_ambiguous": "Kétértelmű", - "pg_tile_ambiguous_tip": "Ciklusok, ahol két program szinte holtversenyben van", - "pg_tile_detected": "Érzékelt", - "pg_tile_detected_tip": "Ciklusok, ahol a teljesítmény elég sokáig lépte túl az indítási küszöböt ahhoz, hogy futóként regisztrálódjon", - "pg_tile_matched": "Egyezett", - "pg_tile_matched_tip": "Ismert programhoz helyesen hozzárendelt ciklusok", - "pg_time_left": "Hátralévő idő", "pg_weak_match": "Gyenge egyezés", "phase_name": "Fázis neve", "name": "Név", "phase_timeline": "Fázisidővonal", "power": "Teljesítmény", "profile": "Profil", - "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. 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.", + "program_tip": "Válassza ki, melyik programhoz egyeztesse az aktuális ciklust. Az Automatikus felismerés a legjobb egyezést választja. Rögzítsen egy programot, ha az automatikus felismerés téved; indítás előtt kiválasztva a következő ciklusra érvényes.", "progress": "Előrehaladás", "raw_socket": "Nyers csatlakozó", - "read_only": "Csak olvasható", "ref_cycle": "Referenciaciklus (opcionális)", - "refresh_interval": "Frissítési időköz (s)", "remaining": "Többi", "reminder_every": "Emlékeztetés (ciklusonként)", "remove_label": "- Távolítsa el a címkét -", "rename_profile": "Profil átnevezése", - "replay_duration": "Visszajátszás", "resulting_profile": "Az eredményül kapott profil", "save_mode": "Mentés mód", - "score": "Pontszám", - "score_breakdown": "Pontszám részletezése", "seconds_from_start": "Másodpercek az elejétől", "select_profile": "Válassza a Profil lehetőséget", "setting": "Beállítás", - "settings_a": "Beállítások A (jelenlegi)", - "settings_a_short": "A", "settings_advanced": "Fejlett", - "settings_b": "Beállítások B (teszt)", - "settings_b_short": "B", "settings_basic": "Alap", "settings_detail_level": "Beállítások részletességi szintje", - "settings_overrides": "Beállítás-felülírások", "show_debug": "Élő mérkőzés hibakereső kártya megjelenítése az Állapot oldalon (bizalom, kétértelműség, legjobb jelöltek)", "show_expected": "Várható görbefedvény megjelenítése (egyező profil, narancssárga)", "show_raw": "Nyers csatlakozó kapcsoló megjelenítése az élő teljesítménygrafikonon", "sparkline": "Legutóbbi ciklusidőtartam-trend", - "stage2": "2. szakasz – alapvető hasonlóság", - "stage3": "3. szakasz – DTW", - "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", @@ -579,7 +516,6 @@ "ungrouped": "Csoportosítás nélkül", "unlabelled": "Címkézetlen", "unlabelled_paren": "(címkézetlen)", - "warp_path": "Torzítási útvonal", "input": "Bemenet:", "start": "Kezdés", "end": "Vége", @@ -604,9 +540,8 @@ "n_shown": ", {n} látható", "cycles_title": "Ciklusok ({n})", "pg_detecting": "Észlelés folyamatban", - "pg_best_marker": "Legjobb: {v}", "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ó", + "pg_time_left_model_tip": "A program fázisai alapján becsült, nem fix visszaszámlálás", "phase": "Fázis", "pg_mode_history": "Tesztelés előzményeken", "pg_ev_detected": "Észlelve", @@ -638,12 +573,6 @@ "pg_sweep_param": "Paraméter", "pg_objective": "Cél", "pg_steps": "Lépések", - "pg_stress_group": "Üresjárati leállás tesztje", - "pg_stress_idle_auto": "Auto", - "pg_stress_idle_w": "Üresjárati szint (W)", - "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_obj_match": "Egyezés pontossága", "pg_obj_endtiming": "Befejezési idő pontossága", "pg_obj_falseend": "Téves befejezések aránya", @@ -652,12 +581,8 @@ "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", - "aria_playground_chart2": "Interaktív ciklusteljesítmény-grafikon: vigye rá a mutatót az idő/teljesítmény leolvasásához, görgessen a nagyításhoz, húzza az eltoláshoz", "pg_alert_timeout_end": "Időtúllépés miatt ért véget, nem előrejelzés alapján", "pg_alert_indefinite": "Végtelen ideig futna", - "pg_alert_stress_cap": "Elérte a biztonsági korlátot", - "pg_alert_stress_ok": "Üresjárati leállás: a ciklus megállt", - "pg_alert_stress_warn": "Üresjárati fogyasztás meghaladja a leállási küszöböt", "task_pg_history": "Tesztelés előzményeken", "task_pg_sweep": "Optimalizálás", "task_reprocess": "Újrafeldolgozás", @@ -667,16 +592,9 @@ "pg_recent_runs": "Legutóbbi futtatások", "pg_load_run": "Futtatás újratöltése", "pg_match_confidence": "Egyezés megbízhatósága", - "pg_envelope_fit": "Illeszkedés a burkológörbéhez", - "pg_envelope_fit_tip": "Mennyire pontosan marad a ciklus a profil mentett teljesítmény-burkológörbéjén belül, más nézőpont, mint az egyezés megbízhatósága.", "n_imported_note": ", {n} importált", "enable_online": "Online funkciók engedélyezése", - "store_brand": "Készülék márkája", - "store_model": "Készülék modellje", "anonymous": "Névtelen", - "rate_quality": "Minőség értékelése", - "rate_n": "{n} csillag", - "approved_only": "Csak a jóváhagyottak", "adopt_settings": "Beállítások átvétele is", "flags": "Jelzők", "include_phase_map": "Fázistérkép belefoglalása", @@ -693,7 +611,6 @@ "cat_custom_phases": "Egyéni fázisok", "cat_profile_groups": "Profilcsoportok", "cat_settings": "Észlelési és illesztési beállítások", - "cat_matching_config": "Illesztés finomhangolása", "cat_ml_models": "ML-modellek", "cat_feedback": "Visszajelzési és ellenőrzési címkék", "cat_suggestions": "Javaslatok", @@ -715,7 +632,6 @@ "hist_csv_data": "CSV-adatok", "hist_from_recorder": "Vagy olvassa be a Home Assistantból", "hist_since": "Kezdő dátum", - "days": "nap", "hist_keep": "Ciklus megtartása", "hist_looks_complete": "teljes", "peak_power_short": "Csúcs", @@ -735,7 +651,40 @@ "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" + "correct_total_cycles": "Összesített ciklusszám javítása", + "hist_reason_already_recorded": "átfedésben van egy ciklussal, amelyet a WashData már rögzített", + "share_consent": "Megerősítem, hogy ezek a ciklusok normálisan, megszakítás nélkül fejeződtek be", + "show_internal": "Belső beállítások megjelenítése", + "show_internal_tip": "A WashData által Ön helyett hangolt beállítások (mintavételezés, felügyelő, egyeztetési gyakoriság, küszöbök). Akkor jelennek meg, ha ennek az eszköznek saját értéke, ütközése vagy javaslata van.", + "rate_n": "{n} csillag", + "rate_quality": "Minőség értékelése", + "pg_autolabel": "Automatikus címkézés", + "pg_label_review": "Felülvizsgálatot kér", + "pg_ev_match_reverted": "Egyezés visszavonva", + "pg_ev_verified_pause": "Megerősített szünet", + "n_samples": "{n} minta", + "n_samples_one": "{n} minta", + "n_samples_other": "{n} minta", + "n_cycles": "{n} ciklus", + "n_cycles_one": "{n} ciklus", + "n_cycles_other": "{n} ciklus", + "energy_per_cycle": "{v}/ciklus", + "energy_total": "{v} összesen", + "automation_off": "(kikapcsolva)", + "ref_cycle_none": "Nincs", + "share_name": "A nevem megjelenítése azon, amit megosztok", + "cat_backfill_cycles": "Importált teljesítményelőzményekben talált ciklusok", + "pg_compare_profile": "Összehasonlítás profillal", + "pg_obj_early": "Korai befejezések aránya", + "pg_obj_endlag": "Befejezési késés (medián)", + "pg_obj_split": "Szétválások aránya", + "pg_sweep_cycles": "Legutóbbi ciklusok", + "cycle_context": "Rögzítő-előzmények a ciklus körül", + "cycle_context_off": "Ki", + "cycle_context_minutes": "{min} perc", + "cycle_context_series": "Rögzítő-előzmények", + "before_start": "Kezdés előtt", + "after_end": "Befejezés után" }, "log": { "all_levels": "Minden szinten", @@ -745,11 +694,9 @@ }, "ml": { "builtin_models": "● Beépített modellek használata", - "fit_fair": "Elfogadható", "fit_good": "Jó", "fit_slight": "Enyhe", "fit_strong": "Erős", - "fit_weak": "Gyenge", "fit_word": "illeszkedési minőség", "personalized": "● Személyre szabott ehhez a géphez", "better_than_baseline": "{pct}%-kal jobb az alap becslésnél ({metric})", @@ -757,21 +704,29 @@ "trend_declining_tip": "Ennek a modellnek az illeszkedése romlott a legutóbbi újraellenőrzések során · több ciklus áttekintése segíthet az újratanulásban.", "trend_steady_tip": "Ennek a modellnek az illeszkedése nagyjából változatlan maradt a legutóbbi újraellenőrzések során.", "cap_blurb": { - "end": "Annak felismerése, hogy egy ciklus valóban befejeződött", - "live_match": "A futó program gyorsabb azonosítása", - "quality": "Tévesen észlelt vagy sérült ciklusok felismerése", - "remaining_time": "A hátralévő idő előrejelzése", "total_energy": "A teljes energia és költség előrejelzése" }, "cap_label": { - "end": "Ciklus végének észlelése", - "live_match": "Programfelismerés", - "quality": "Ciklusminőség ellenőrzése", - "remaining_time": "Hátralévő idő becslése", "total_energy": "Energiabecslés" }, - "metric_auc": "AUC {auc} a félretett adatokon", - "metric_mae": "hiba: {model} a(z) {baseline} referenciához képest" + "metric_mae": "hiba: {model} a(z) {baseline} referenciához képest", + "fine_tuned_at": "finomhangolva: {when}", + "last_run_not_promoted": "Utolsó ellenőrzés ({when}): {reason}", + "never": "soha", + "skip_reason": { + "fit_failed": "A tanítás sikertelen: {error}", + "holdout_too_small": "Túl kevés ciklus egy új modell tisztességes teszteléséhez: {held_out} tehető félre tesztelésre, {min} szükséges.", + "not_better_than_incumbent": "Az új modell nem volt jobb a használatban lévőnél (hiba: {model} vs. {incumbent}), ezért a jelenlegi maradt.", + "not_better_than_naive": "Az új modell nem volt egyértelműen jobb az egyszerű becslésnél (hiba: {model} vs. {naive}).", + "unknown": "Semmi újat nem tanult.", + "insufficient_rows": "Még nincs elég használható ciklus: eddig {count} címkézett, tiszta ciklus.", + "insufficient_rows_one": "Még nincs elég használható ciklus: eddig {count} címkézett, tiszta ciklus.", + "insufficient_rows_other": "Még nincs elég használható ciklus: eddig {count} címkézett, tiszta ciklus." + }, + "trained_unknown": "ismeretlen időpontban", + "held_out_count": "{count} félretett cikluson mérve", + "held_out_count_one": "{count} félretett cikluson mérve", + "held_out_count_other": "{count} félretett cikluson mérve" }, "modal": { "auto_label": "Automatikus címkézési ciklusok", @@ -780,7 +735,6 @@ "cycle": "Ciklus", "edit_group": "Profilcsoport szerkesztése", "edit_phase": "Fázis szerkesztése", - "import_config": "Konfiguráció importálása", "label_cycle": "Címkeciklus", "merge_cycles": "Egyesítsen {n} ciklust", "new_group": "Új profilcsoport", @@ -795,6 +749,8 @@ "delete_automation_title": "Automatizálás törlése", "delete_automation_msg": "Törlöd a(z) \"{name}\" automatizálást a Home Assistantból? Ez nem vonható vissza.", "remove_legacy_title": "Régi műveletek eltávolítása", + "reset_phase_msg": "Visszavonod a(z) \"{name}\" fázison végzett szerkesztést, és visszaállítod a beépített fázist? A programok megtartják a tartományaikat.", + "reset_phase_title": "Fázis visszaállítása", "remove_legacy_msg": "Eltávolítod a régi egyéni műveleteket? A ciklusesemények során többé nem fognak elsülni. Ez a panelről nem vonható vissza.", "discard_recording_title": "Felvétel eldobása", "discard_recording_msg": "Eldobod a mentett felvételt? Ez nem vonható vissza.", @@ -815,32 +771,44 @@ "store_share_device": "Eszköz megosztása", "export_select": "Exportálás - adatok kiválasztása", "import_wizard": "Importálás - adatok kiválasztása", - "history_import": "Teljesítményelőzmények importálása" + "history_import": "Teljesítményelőzmények importálása", + "apply_suggestions_title": "Javaslatok alkalmazása", + "remove_task_title": "Feladat eltávolítása", + "remove_task_msg": "Eltávolítja a(z) \"{task}\" emlékeztetőt? A karbantartási naplóban lévő bejegyzései megmaradnak.", + "apply_suggestions_msg": "Módosítja ezt a(z) {n} beállítást? Az integráció mentés után újratöltődik.", + "discard_settings_title": "Eldobja a nem mentett módosításokat?", + "discard_settings_msg": "Nem mentett beállításmódosítások: {n}. Kilép a Beállításokból, és eldobja őket?", + "profile_title": "Profil · {name}", + "undo_import_msg": "Az eszköz adatai és beállításai visszaállnak a legutóbbi importálás ({when}) előtti állapotra. Az azóta rögzített ciklusok és módosítások elvesznek.", + "undo_import_ok": "Importálás visszavonása", + "undo_import_title": "Utolsó importálás visszavoná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.", + "pg_preset_delete_confirm": "Törli a(z) \"{name}\" előbeállítást?", + "pg_preset_limit": "Elérte az előbeállítások korlátját ({n})", + "pg_preset_overwrite": "Felülírja a(z) \"{name}\" előbeállítást?", "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.", - "refresh_catalog_hint": "A közösségi márka- és készüléklisták gyorsítótárba kerülnek, hogy a megosztott bolt a napi lekérdezési keretén belül maradjon. Frissítsen a mások által hozzáadott vagy jóváhagyott bejegyzések betöltéséhez.", + "store_sibling_hint": "Nincs semmi megosztva az Ön modelljéhez? Ugyanannak a márkának egy rokon modellje általában jó kiindulópont.", + "store_declare_appliance": "Adja meg a WashDatának, melyik készülék az Öné, hogy lássa a mások által hozzá megosztott beállításokat. Vagy írjon be fent egy márkát a böngészéshez.", + "refresh_catalog_hint": "A közösségi márka- és készüléklisták gyorsítótárban vannak, hogy kíméljék a bolt napi keretét. Frissítsen, hogy lássa a mások által hozzáadott vagy jóváhagyott bejegyzéseket.", "head_trim_hint": "Ennyi másodperc eltávolítása az elejéről", "appliance_monitor": "Készülék monitor", "artifact_dip_detail": "~{n}s-re a szokásos energiasáv alá esett.", "artifact_footer": "A fenti grafikonon kiemelve. Ezek átmeneti műtermékek (pl. a ciklus közepén kinyílt ajtó), nem feltétlenül problémák.", "artifact_header": "{n} rendellenesség észlelve ebben a ciklusban", - "artifact_pause_detail": "A teljesítmény ~{n}s-re közel nullára esett, majd visszatért – valószínűleg kinyílt az ajtó a ciklus közben, vagy a ciklus szünetelt.", + "artifact_header_one": "{n} rendellenesség észlelve ebben a ciklusban", + "artifact_header_other": "{n} rendellenesség észlelve ebben a ciklusban", + "artifact_pause_detail": "A teljesítmény ~{n} mp-re közel nullára esett, majd visszatért. Valószínűleg kinyitották az ajtót, vagy a ciklus szünetelt.", "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 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.", + "cleanup_intro": "Minden címkézett ciklus egymásra vetítve. Jelölje be a kiugrókat, majd vegye le 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.", "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)", - "compare_profiles_tip": "Fedjen rá más profilborítékokat a fenti diagramra, hogy megtudja, melyik illik legjobban ehhez a ciklushoz.", + "compare_profiles_tip": "Vetítsen rá más profilokat a diagramra, hogy lássa, melyik illik legjobban ehhez a ciklushoz.", "compare_selected_cycles": "Kiválasztott ciklusok (folytonos) – megjelenítés/elrejtés", - "consider_new_profile": "Fontolja meg egy új profil létrehozását.", - "coverage_gap": "a legutóbbi ciklusoknak nincs megfelelő profilja (az utolsó 30 {pct}%-a).", - "coverage_gap_similar_cycles": "{count} hasonló felcímkézetlen ciklus található – hozzon létre egy profilt az egyeztetés megkezdéséhez.", "cycles_deleted": "{count} ciklus törölve", "enough_data": "Elegendő adat a tanuláshoz ({current}/{min} ciklus).", "export_description": "Válaszd ki pontosan, mely profilokat, ciklusokat, beállításokat és egyebeket exportálsz JSON-ba, vagy elemezz egy fájlt, és csak a kívánt részeket importáld.", @@ -848,30 +816,26 @@ "feedback_prompt": "Erősítse meg, hogy helyes volt, javítsa ki a programot, vagy hagyja figyelmen kívül.", "feedback_relabel_hint": "Ennek a ciklusnak az újracímkézése ezt is megoldja.", "filter_by_profile": "Szűrés profil szerint…", - "group_modal_help": "Azonos alakú programok, amelyek hőmérséklete/centrifugálása különbözik (az időtartam változhat). Az egyezés pontozza a csoportot, mint egy jelöltet, majd kiválasztja a legmegfelelőbb tagot. Válasszon legalább 2-t; az átfedés megmutatja, mennyire hasonlítanak egymásra.", + "group_modal_help": "Csoportosítsa az azonos alakú, de hőmérsékletben vagy centrifugálásban eltérő programokat. Az egyeztetés a csoportot egyként pontozza, majd kiválasztja a legjobban illő tagot. Válasszon legalább 2-t; az átfedés megmutatja, mennyire hasonlók.", "group_not_cohesive": "Ezek a profilok nem eléggé hasonlóak ahhoz, hogy megbízhatóan csoportosítsák, ezért az egyeztetés egyenként kezeli őket, amíg el nem távolítja a kiugró értéket, vagy fel nem osztja a csoportot.", "group_preview_hint": "Jelöljön ki 2+ tagot, hogy előnézze és összehasonlítsa teljesítménygörbéiket.", - "import_intro": "Töltsön be egy exportált fájlt, vagy illesszen be egy JSON-adatot alább.", "legacy_actions_title": "{count} örökölt egyéni művelet továbbra is fut", + "legacy_actions_title_one": "{count} örökölt egyéni művelet továbbra is fut", + "legacy_actions_title_other": "{count} örökölt egyéni művelet továbbra is fut", "live_chart_loading": "Az élő teljesítmény diagram megjelenik a leolvasások megérkezésekor.", "loading": "Terhelés…", "loading_curve": "Görbe betöltése…", "loading_settings": "Beállítások betöltése…", "log_buffer_hint": "A legújabbak előtt · puffereli az utolsó 500 ha_washdata rekordot az újraindítás óta · húzza az alsó szélét az átméretezéshez.", - "maintenance_advisory": "A növekvő időtartam/energiafogyasztás jelezheti a készülék karbantartását (pl. vízkőmentesítés, szűrőtisztítás).", "maintenance_due": "Esedékes karbantartás: {items}", - "maintenance_intro": "Rögzítse az ezen a készüléken végzett karbantartást, és emlékeztetőt kap, amikor egy feladat ismét esedékessé válik.", + "maintenance_intro": "Rögzítse a készülék karbantartásait, és kapjon emlékeztetőt, amikor egy feladat ismét esedékes.", "maintenance_load_error": "Nem sikerült betölteni a karbantartási adatokat: {error}", "maintenance_requires_access": "A karbantartás és az export/import teljes hozzáférést igényel.", - "manual_duration_hint": "A profil átlagos/várható ciklushossza, a hátralévő idő becsléseihez használva. Szerkesztés a beállításhoz; változatlanul hagyva az aktuális értéket megtartja.", - "matching_tuning_intro": "Tanuláskor a WashData azt is beállítja, hogy a programillesztés mennyi súlyt jelent a formának az időtartamhoz és az energiához képest.", + "manual_duration_hint": "A profil várható ciklushossza, a hátralévő idő becsléséhez. Ha változatlanul hagyja, a jelenlegi érték marad.", "merge_intro": "A kiválasztott ciklusokat egyesítjük (időrendi sorrendben; 0 W-tal kitöltött hézagok). Válassza ki a kapott profilt.", "ml_intro": "A WashData intelligens modellekkel szállítjuk, amelyek már a dobozból kivehetők.", "ml_learned_intro": "Ehhez a géphez finomhangolt modellek.", - "ml_loading": "ML betöltése…", "ml_settings_intro": "Két független kapcsoló: az egyik a ciklus futása közben alkalmazza a modelleket, a másik lehetővé teszi, hogy a WashData idővel finomhangolja őket a gépére.", - "name_first_program": "Elég ciklusa van – nevezze el az első programját az egyeztetés elindításához.", - "near_duplicate_cluster": "majdnem ismétlődő profilfürt észlelve. A csoportosítás lehetővé teszi, hogy a párosítás megbízhatóan válasszon a hasonlók között (pl. ugyanaz a program különböző hőmérsékleten/centrifugáláson).", "no_cycles_match": "Egyetlen ciklus sem felel meg az aktuális szűrőnek.", "no_cycles_profile": "Nincs ciklus ehhez a profilhoz.", "no_cycles_selected": "Először válasszon legalább egy ciklust.", @@ -896,73 +860,52 @@ "no_suggestions": "Nincsenek aktív javaslatok.", "notify_services_hint": "Használja a(z) {entity} szolgáltatásazonosítókat (több esetén vesszővel elválasztva). Sablonváltozók: {vars}.", "old_actions_warning": "A régi műveletszerkesztővel konfigurálva (most eltávolítva). Továbbra is aktiválják a cikluseseményeket, de már nem szerkeszthetők itt. Alakítsa át őket normál automatizálássá, vagy távolítsa el őket.", - "onboarding_progress": "{n} / 3 ciklus megfigyelve", - "onboarding_watching": "Használja a készülékét a szokásos módon – a WashData figyel. 3 ciklus után elindul a programegyeztetés.", "pending_feedback": "Függőben lévő észlelési visszajelzés", "performance_trend": "Teljesítménytrend ({n} ciklus)", - "pg_analysis_empty": "Töltsön be egy ciklust az egyezéselemzés megtekintéséhez.", "pg_apply_confirm": "Alkalmazza a legjobb értéket: {label} = {value}?", - "pg_canvas_empty": "Válasszon fent egy ciklust, majd nyomja meg a Betöltés gombot a teljesítménygörbéjének megtekintéséhez.", - "pg_chart_caption": "Érzékelési/egyezési arány a {param} értékei szerint", "pg_ctrl_intro": "Az értékek az eszköz aktív integrációs beállításaiból indulnak. A módosítások a Homokozóban maradnak, amíg el nem menti őket a beállításokba.", "pg_live_unavailable": "Az aktív beállítások nem érhetők el, az alapértelmezett értékek láthatók.", "pg_load_live_confirm": "Elveti a Homokozó módosításait ({n}), és újratölti az integráció jelenlegi beállításait?", - "pg_match_rate": "{pct}% egyezési arány {total} cikluson", "pg_matches_live": "Megegyezik az aktív beállításokkal", "pg_n_changed": "Eltérés az aktív beállításoktól: {n}", "pg_sugg_none": "Minden javaslat már megfelel a jelenlegi beállításoknak", - "pg_preset_delete_confirm": "Törli a(z) \"{name}\" előbeállítást?", - "pg_preset_limit": "Elérte az előbeállítások korlátját ({n})", - "pg_preset_overwrite": "Felülírja a(z) \"{name}\" előbeállítást?", "pg_publish_one_confirm": "Elmenti a(z) {label} = {value} értéket az eszköz beállításaiba?", "pg_restart_note": "Indítsa újra a Home Assistantot a szimulációs eszközök engedélyezéséhez.", - "pg_sim_progress": "{done} / {total} ciklus", - "pg_starting": "Indítás…", - "pg_stress_synthetic": "az üresjárati szimuláció itt kezdődik", - "pg_sweep_intro": "Mi lenne, ha a {param} más lenne? Teszteljen {steps} értéket az utolsó {cycles} cikluson, hogy megtalálja azt a beállítást, ahol a legtöbb ciklus helyesen egyezik.", - "pg_sweep_step": "{done} / {total}. lépés", - "pg_verdict_bad": "Figyelmet igényel: sok ciklus észrevétlen marad.", - "pg_verdict_good": "Jól hangolva: a legtöbb ciklust helyesen azonosítja és párosítja.", - "pg_verdict_ok": "Elfogadható: néhány ciklus kimaradt. Próbálja meg csökkenteni az indítási küszöböt.", "phase_catalog_intro": "Egy ciklus elnevezett szegmensei (Előmosás, Fűtés, Centrifugálás…). Rendelje hozzá őket egy profilhoz a vezérlőpultjáról.", "phase_ranges_intro": "A fázistartományok (a ciklus kezdetétől számított percek) átfedik az átlaggörbét. Szerkessze az értékeket az élő előnézethez.", - "playground_intro": "Biztonságos homokozó a korábbi ciklusok különböző beállításokkal való lejátszásához, két konfiguráció egymás melletti összehasonlításához, és annak vizsgálatához, hogyan illeszkedik egy ciklus egy profilhoz. Itt semmi sem változtatja meg az élő adatait, amíg kifejezetten nem alkalmazza.", - "playground_needs_restart": "Indítsa újra a Home Assistantot a Homokozó eszközök engedélyezéséhez.", - "preferences_admin": "Beállítások, panel és hozzáférés-vezérlés", - "preferences_adv": "Preferenciák", + "playground_intro": "Próbáljon ki beállításokat a valós ciklusadatain. Itt semmi sem módosítja az élő beállításokat, amíg nem alkalmazza.", "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), 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.", + "process_history_hint": "Újrafuttatja az egyeztetést minden tárolt cikluson, frissíti a javaslatokat, újratanítja az ML-modelleket (ha engedélyezve vannak), a rögzített árakból újraszámolja a ciklusok költségét, és újraszámolja a ciklusok egészségét. Futtassa egy adag 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.", - "rbac_hint": "Ha ki van kapcsolva, minden Home Assistant felhasználó teljes hozzáféréssel rendelkezik (alapértelmezett). A rendszergazdák mindig teljes hozzáféréssel rendelkeznek, és mindenkit kezelhetnek.", - "recent_logs": "Legutóbbi ha_washdata rekordok", + "rbac_hint": "Kikapcsolva: minden Home Assistant felhasználó teljes hozzáféréssel rendelkezik (alapértelmezett). A rendszergazdák mindig teljes hozzáféréssel rendelkeznek, és mindenkit kezelhetnek.", "recording_in_progress": "Felvétel folyamatban", "reminders_intro": "Emlékeztető megjelenítése a panelen ennyi ciklussal az utolsó szerviz után. Hagyja üresen vagy 0-ra az emlékeztető kikapcsolásához.", "restart_gap_footer": "Kiemelve a grafikonon. A teljesítményadatok hiányoznak ezekre az intervallumokra; az egyeztetés csak valós méréseket használt.", "restart_gap_header": "{n} HA-újraindítás-rés ezen ciklus alatt", + "restart_gap_header_one": "{n} HA-újraindítás-rés ezen ciklus alatt", + "restart_gap_header_other": "{n} HA-újraindítás-rés ezen ciklus alatt", "restart_gap_item": "{dur} rés", - "review_confirm_help": "Ellenőrizze, hogy ezt a ciklust megfelelően észlelték-e. Az Ön véleménye megtanítja a modellt a gépén – minél több ciklust erősít meg, annál jobb lesz az egyezés és az állapotpontozás. Elég egy gyors Jó/Rossz.", + "review_confirm_help": "Helyesen lett észlelve ez a ciklus? A ciklusok megerősítése javítja az egyeztetést az Ön gépén. Elég egy gyors Jó/Rossz.", "review_in_settings": "Tekintse át a Beállításokban", "review_notes_placeholder": "Megjegyzések (opcionális)", "review_notes_tip": "Szabad szövegű jegyzetek saját referenciaként. Nem használtak párosításhoz vagy edzéshez.", - "review_profile_tip": "A program ezt a ciklust címkével látja el. Ha az automatikusan észlelt program hibás volt, javítsa ki itt – a címkézés megtanítja a jövőbeli ciklusokhoz való illeszkedést.", - "review_quality_tip": "Milyen tiszta ez a ciklus. Jó = a program tankönyvi példája; Rossz = észlelt, de zajos vagy atipikus; Használhatatlan = rosszul észlelt (egyesített, csonkolt vagy hamis). Megadja az állapotpontszámot és azt, hogy mely ciklusok engedélyezettek a modell betanításához.", - "review_recorded_tip": "Jelölje meg ezt a programhoz tartozó, kézzel kiválasztott referenciaciklusként – ugyanaz a szerep, mint egy kézzel rögzített ciklus. A referenciaciklusokat mindig megőrizzük, a megfelelő sablont vetjük be, és soha nem vetik el őket a tisztítás során. (Ez az \"arany\"/rekord zászló; mindkettő ugyanaz.)", - "review_tags_tip": "Opcionális jelzők, amelyek leírják, hogy mi hibázott ebben a ciklusban, így a képzés és a tisztítás megmagyarázhatja.", + "review_profile_tip": "A program, amellyel ez a ciklus fel van címkézve. Ha hibás, javítsa itt; a címkékből tanul az egyeztetés.", + "review_quality_tip": "Milyen tiszta ez a ciklus. Jó: a program tankönyvi futása. Rossz: észlelt, de zajos vagy szokatlan. Használhatatlan: hibásan észlelt (összevont, csonka vagy hamis). Hat az állapotpontszámra és arra, mely ciklusokból tanul a modell.", + "review_recorded_tip": "Jelölje meg ezt a program kézzel választott referenciaciklusaként, mint egy kézzel rögzített ciklust. A referenciaciklusok mindig megmaradnak, ezekből indul az egyeztetési sablon, és a tisztítás sosem törli őket.", + "review_tags_tip": "Opcionális jelzők arról, mi ment félre, hogy a tanítás és a tisztítás számolhasson vele.", "review_to_cycles": "Nyissa meg a Ciklusok áttekintési sort", "samples_decimated": "{shown}/{total} minta megjelenítése (megjelenítéshez ritkítva; a csúcsok megtartva). Az itt látható széles hézag ritkítás, nem hiányzó adat.", - "saving_triggers_reload": "A mentés az integráció újratöltését indítja el. A HA entitások rövid időre elérhetetlenként jelenhetnek meg.", + "saving_triggers_reload": "A mentés újratölti az integrációt. Az entitások rövid ideig elérhetetlennek látszhatnak.", "search_placeholder": "Keresési beállítások…", "see_recorder": "Lásd alább a felvevő widgetet", "setting_changed": "Módosítva: {old} → {new} ekkor: {date}", "settings_basic_note": "Az alapvető beállítások láthatók. Váltson Fejlett nézetre a teljes listához.", - "shape_drift_advisory": "⚠ Alakváltás", - "shape_drift_detail": "Ennek a profilnak a teljesítménymintázata idővel megváltozott – lehetséges készülékhasználat okozta kopás vagy szükséges karbantartás (pl. vízkőmentesítés, szűrőtisztítás).", "show_all_settings": "Minden beállítás megjelenítése", "showing_suggestions": "{count} beállítás megjelenítése javaslatokkal.", - "split_intro": "Kattintson a diagramra egy osztási pont hozzáadásához vagy eltávolításához, vagy az üresjárati hézagok automatikus észleléséhez. Minden eredményül kapott szegmens saját profilt kaphat.", - "storage_diagnostics": "Tárolási statisztikák, karbantartás, export/import", + "showing_suggestions_one": "{count} beállítás megjelenítése javaslatokkal.", + "showing_suggestions_other": "{count} beállítás megjelenítése javaslatokkal.", + "split_intro": "Kattintson a diagramra osztási pont hozzáadásához vagy eltávolításához, vagy használja az üresjárati szünetek automatikus felismerését. Minden rész saját profilt kaphat.", "sug_staged": "{key} = {val} beállítva. Mentse el az alkalmazáshoz.", "toast_auto_detect_enabled": "Automatikus felismerés engedélyezve", "toast_auto_label_complete": "Automatikus jelölés kész", @@ -979,7 +922,6 @@ "toast_no_automation": "Itt nem lehet automatizálást létrehozni", "toast_profile_name_required": "A profil neve kötelező", "toast_program_set": "Program beállítva: {program}", - "toast_rebuild_failed": "Újraépítés sikertelen: {error}", "toast_resume_failed": "Folytatás sikertelen: {error}", "toast_revert_failed": "Visszaállítás sikertelen: {error}", "toast_reverted": "{key} visszaállítva", @@ -991,99 +933,86 @@ "trim_destructive_confirm": "Ez a ciklusnak csak {pct}%-át tartja meg, és nem vonható vissza. Folytatja?", "trim_intro": "Húzza el a piros fogantyúkat, vagy írjon be értékeket. Az ablakon kívül mindent eltávolítanak.", "tuning_suggestions_available": "{count} hangolási javaslat érhető el a megfigyelt ciklusokból. A megfelelő mezők mellett jelennek meg.", + "tuning_suggestions_available_one": "{count} hangolási javaslat érhető el a megfigyelt ciklusokból. A megfelelő mező mellett jelenik meg.", + "tuning_suggestions_available_other": "{count} hangolási javaslat érhető el a megfigyelt ciklusokból. A megfelelő mezők mellett jelennek meg.", "unsure_detected_prefix": "A WashData nem biztos abban, hogy észlelte", - "updated": "Frissítve", "warmup_badge": "Még tanul ({done}/{needed} ciklus)", - "warmup_detail": "Ez a profil {needed} felcímkézett ciklust igényel, mielőtt az automatikus egyeztetés megkezdődik. Minden megerősített ciklus segít tanulni.", + "warmup_detail": "Az egyeztetés már működik. A WashData arra kéri, erősítse meg ennek a profilnak az első {needed} ciklusát, mielőtt önállóan címkézne.", "wipe_history_warning": "Véglegesen törölje az összes ciklust és profilt. Nem vonható vissza.", "compare_overlay_tip": "Rárajzolt tanult profilsávok, hogy lásd, melyik programra hasonlít az egyes ciklusok.", - "duration_trend_up_tip": "Időtartam nő ({pct}%/ciklus)", - "duration_trend_down_tip": "Időtartam csökken ({pct}%/ciklus)", - "energy_trend_up_tip": "Energia nő ({pct}%/ciklus)", - "energy_trend_down_tip": "Energia csökken ({pct}%/ciklus)", - "performance_trending": "Teljesítmény tendenciát mutat", - "pg_select_cycle_hint": "Jelölj ki egy ciklust, majd kattints a ↺ gombra a betöltéshez", - "pg_sweep_progress": "{done}. / {total}. lépés", - "pg_analysis_hint": "Jelölj ki egy ciklust, majd kattints a ↺ gombra az elemzés betöltéséhez.", "diagnostics_load_failed": "Nem sikerült betölteni a diagnosztikát: {error}", - "manual_duration_ref_hint": "Csak akkor használatos, ha nincs kiválasztva referenciaciklus. A referenciaciklus a saját hossza alapján állítja be az időtartamot.", + "manual_duration_ref_hint": "Csak referenciaciklus nélkül használatos; a referenciaciklus a saját hosszából állítja be az időtartamot.", + "duration_learned_hint": "E program ciklusaiból számolva (a kiugrók nélkül), és minden új ciklus után frissül. A módosításhoz vágja le vagy távolítsa el az eltérő ciklusokat.", "toast_convert_rolled_back": "Az átalakítás sikertelen volt, és visszavonásra került (nem maradt automatizálás): {error}", "toast_convert_orphan": "Az automatizálás létrejött, de a régi műveletek törlése sikertelen volt. Ne próbálja újra: távolítsa el a régi műveleteket kézzel a duplikált automatizálás elkerülése érdekében.", - "advisory_duration_trend_up": "A(z) '{name}' ciklusai fokozatosan hosszabbak (ciklusonként kb. +{pct}%). Ha a készülék működése megváltozott, rögzítse újra vagy építse újra ezt a profilt.", - "advisory_energy_trend_up": "A(z) '{name}' fokozatosan több energiát fogyaszt (ciklusonként kb. +{pct}%) - érdemes ellenőrizni a készüléket, ha ez váratlan.", - "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.", + "advisory_duration_trend_up": "A(z) '{name}' ciklusai egyre hosszabbak (ciklusonként kb. +{pct}%). Ha a készülék megváltozott, rögzítse vagy építse újra ezt a profilt.", + "advisory_energy_trend_up": "A(z) '{name}' minden ciklusban több energiát fogyaszt (ciklusonként kb. +{pct}%). Ha ez váratlan, ellenőrizze a készüléket.", + "advisory_poor_health": "A(z) '{name}' rosszul illeszkedik: a legutóbbi ciklusai erősen ingadoznak vagy gyengén egyeznek. Nézze át a ciklusait, vagy rögzítse újra, hogy a felismerés és a becslések pontosak maradjanak.", + "advisory_shape_drift": "A(z) '{name}' eltért az eredeti teljesítménygörbéjétől, talán vízkő vagy kopás miatt. Fontolja meg az újrarögzítését friss ciklusokkal.", + "advisory_shape_drift_corr": "A(z) '{name}' eltért az eredeti teljesítménygörbéjétől (korreláció: {corr}), talán vízkő vagy kopás miatt. Fontolja meg az újrarögzítését friss ciklusokkal.", + "advisory_unmatchable": "A(z) '{name}' soha nem egyezhet: nincs mögötte teljesítményadatos ciklus. A javításhoz címkézzen fel vagy rögzítsen egy ciklust ebből a programból.", + "advisory_unmatchable_no_duration": "A(z) '{name}' soha nem egyezhet: a ciklusának nincs használható időtartama. Rögzítse újra ezt a programot, vagy adja meg a várható időtartamát.", "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_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.", "pg_sweep_intro2": "Keresse meg azt a beállítást, amely a legjobban teljesíti a célt a legutóbbi ciklusaiban. Semmi sem változik, amíg nem alkalmazza.", - "pg_running_history": "Ciklusaid visszajátszása…", - "pg_running_sweep": "Értékek tesztelése az összes ciklusodon…", "pg_row_load_hint": "Ciklus betöltése a fenti grafikonba", - "pg_history_intro2": "Játszd vissza a legutóbbi ciklusaidat a valódi érzékelőn és illesztőn keresztül a fenti beállításokkal. Kattints bármely sorra a ciklus grafikonba töltéséhez; módosíts egy beállítást az előtte/utána összehasonlításhoz.", + "pg_history_intro2": "Játszd vissza a legutóbbi ciklusokat a valódi érzékelőn és illesztőn a fenti beállításokkal. Kattints egy sorra, hogy betöltsd a grafikonba; módosíts egy beállítást az előtte/utána összehasonlításhoz.", "pg_simulating": "Ciklus szimulálása…", - "pg_canvas_empty2": "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 a nagyításhoz, és húzd az eltoláshoz.", "pg_apply_settings_confirm": "Elmenti ezt a(z) {n} beállítást erre az eszközre? {list}", - "pg_stress_terminated_detail": "Körülbelül {idle} W üresjárati szinten állandósult -- a ciklus {h} ó {m} p múlva {reason} révén ért véget.", - "pg_stress_above_threshold_detail": "Az üresjárati fogyasztás (kb. {idle} W) eléri vagy meghaladja a tényleges leállási küszöböt ({stop} W) -- a ciklust soha nem érzékelte csendesnek. A javításhoz növelje a stop_threshold_w értékét.", - "pg_stress_hit_cap_detail": "A ciklus {h} ó {m} p futott leállás nélkül -- a biztonsági korlát kényszerítette leállásra. Üresjárati fogyasztás {idle} W a(z) {stop} W leállási küszöbhöz képest.", "no_logs_match": "Egyetlen naplóbejegyzés sem felel meg ezeknek a szűrőknek.", "pg_analysis_hint2": "Válasszon egy ciklust, és nyomja meg a Futtatás gombot az egyezéselemzés betöltéséhez.", - "pg_analysis_empty2": "Nyomja meg a Futtatás gombot az egyezéselemzés megtekintéséhez.", - "imported_readonly": "A közösségi boltból importálva. Referenciaként és egyeztetéshez jelenik meg. Nem számít bele a statisztikákba, és nem szerkeszthető.", + "imported_readonly": "A közösségi boltból importálva. Referenciaként és egyeztetéshez használt, nem számít bele a statisztikákba, és nem szerkeszthető.", "merge_no_imports": "Az importált felvételek nem egyesíthetők valódi ciklussal. Az egyesítéshez szüntesse meg a kijelölésüket.", - "store_enable_hint": "A közösségi referenciaciklusok böngészéséhez és importálásához engedélyezze az online funkciókat a Beállításokban.", - "advisory_duration_outlier": "A(z) '{name}' címkével ellátott {n} ciklus hossza túlságosan eltér a szokásos hosszától ahhoz, hogy valaha is illeszkedjen hozzá (a legrosszabb: {ratio}x). Valószínűleg rosszul vannak címkézve, vagy egy felvétel két futást rögzített. Címkézze át vagy vágja szét őket, hogy ne torzítsák ennek a programnak az időbecslését.", + "store_enable_hint": "A közösségi referenciaciklusok böngészéséhez és importálásához kapcsolja be az online funkciókat itt: Preferenciák > Online és közösség.", + "advisory_duration_outlier": "A(z) '{name}' címkéjű {n} ciklus túl messze esik a szokásos hossztól ahhoz, hogy valaha egyezzen (legrosszabb: {ratio}x). Valószínűleg rosszul címkézték őket, vagy két futást tartalmaznak. Címkézze át vagy ossza fel őket, hogy ne torzítsák az időbecslést.", "standby_above_stop_title": "A ciklus végén még mindig {idle} W-ot vett fel", "standby_above_stop_sub": "Az utolsó {total} ciklusból {n}. A kikapcsolási késleltetés csak akkor indul el, amikor a teljesítmény a Leállítási küszöb ({stop} W) alá esik, így a ciklusok későn fejeződnek be, vagy kézzel kell leállítani őket.", - "online_intro": "Ossza meg és töltsön le referenciaciklusokat más WashData-felhasználókkal. Minden online funkció opcionális és alapértelmezés szerint ki van kapcsolva.", "store_share_intro": "Töltse fel ezt a referenciaciklust, hogy az azonos készülékkel rendelkezők is használhassák. Nyilvános megjelenés előtt ellenőrzésre kerül.", - "online_intro_global": "Böngésszen és osszon meg referenciafelvételeket más WashData-felhasználókkal, és erősítsen meg készülékbejegyzéseket. Egyetlen kapcsolat a teljes WashData-integrációra vonatkozik; a készülék márkája és modellje eszközönként, az Alapkonfiguráció szakaszban állítható be. Minden online funkció opcionális és alapértelmezés szerint ki van kapcsolva.", + "online_intro_global": "Böngésszen és osszon meg referenciafelvételeket más WashData-felhasználókkal. Egy kapcsolat az összes WashData-eszközére vonatkozik; a márka és a modell eszközönként, az Alapok szakaszban állítható. Az online funkciók addig ki vannak kapcsolva, amíg be nem kapcsolja őket.", "online_unavailable": "Az online funkciók nem érhetők el ezen a szerveren.", "store_picker_offline": "A közösségi katalógusból való választáshoz engedélyezze az online funkciókat a beállítások fogaskerekénél.", "pick_brand_first": "Először válasszon készülékmárkát.", - "model_not_found": "Még nincs a katalógusban.", - "connect_to_confirm": "A megerősítéshez vagy értékeléshez csatlakozzon a beállítások fogaskerekénél.", + "custom_task_limit": "Elérte a feladatok {max} darabos korlátját.", + "custom_tasks_intro": "Adjon hozzá bármi mást, amire emlékeztetőt szeretne. Egy feladat akkor esedékes, ha bármelyik időköz letelik; hagyja az egyiket üresen vagy 0-n, ha csak a másikat szeretné használni.", + "maintenance_due_sensor_hint": "Az \"Esedékes karbantartás\" bináris érzékelő bekapcsol, amíg bármelyik feladat esedékes, a saját értesítési automatizálásaihoz.", + "maintenance_supporter_intro": "A Maintenance Supporter egy különálló integráció, amely sokkal többet tud: bármilyen készülék vagy feladat, ütemezés idő, használat vagy érzékelő alapján, értesítések és előzmények. Használhatja a WashData ciklusszámlálóját.", "adopt_settings_hint": "Felülírja az eszköz érzékelési és párosítási küszöbeit a megosztottakkal. Az értesítések, entitások és energiaár soha nem változnak.", - "include_settings_hint": "Ossza meg az eszköz felismerési és párosítási küszöbeit (nem az értesítéseit, entitásait vagy energiaárát). Az átvevők döntik el, hogy alkalmazzák-e.", + "include_settings_hint": "Ossza meg az eszköz észlelési és egyeztetési küszöbeit (az értesítéseket, entitásokat és energiaárat nem). Mások döntik el, hogy alkalmazzák-e.", "onboard_download": "Új eszköz? Vegyen át egy kész beállítást (programok, referenciaciklusok és fázisok) egy másik WashData-felhasználótól, akinek ugyanolyan készüléke van.", - "share_consent": "Megerősítem, hogy ezek a ciklusok normálisan, megszakítás nélkül fejeződtek be", "share_device_none": "Még nincsenek megosztható ciklusok. Először jelöljön meg egy rögzített vagy kézzel kiválasztott ciklust referenciaciklusként (⭐) a Ciklusok lapon.", "share_guideline_naming": "Minden profilt pontosan úgy nevezzen el, ahogy a készülék tárcsáján vagy kijelzőjén megjelenik (pl. 'Cotton 40', 'Eco 60').", - "share_guideline_quality": "Csak normálisan befejezett ciklusokat osszon meg -- sem ciklus közbeni megszakítás, sem ajtónyitás, sem teljesítményhiba.", + "share_guideline_quality": "Csak normálisan befejezett ciklusokat osszon meg: megszakítás, ajtónyitás és rövid áramkimaradás nélkül.", "share_guideline_review": "A feltöltés függőben indul, és akkor válik nyilvánossá, ha elegendő közösségi tag megerősíti.", "share_guidelines_title": "Megosztás előtt", - "store_download_device_intro": "Vegyen át minden megosztott programot és annak referenciaciklusait az eszközére. A saját rögzített ciklusait és statisztikáit nem érinti.", + "store_download_device_intro": "Adja hozzá az eszközéhez az összes megosztott programot és referenciaciklusaikat. A saját ciklusait és statisztikáit nem érinti.", "store_share_device_intro": "Töltse fel a(z) {brand} {model} eszközt a kiválasztott referenciaciklusokkal. Más, ugyanolyan készülékkel rendelkezők átvehetik programjait. A bejegyzések felülvizsgálatra kerülnek nyilvánossá válás előtt.", "share_profile_no_cycles": "Nincsenek referenciaciklusok -- jelöljön meg egy ciklust ⭐-gal a Ciklusok lapon, hogy belefoglalhassa ezt a profilt", - "advisory_phase_inconsistent": "Úgy tűnik, hogy a(z) '{name}' különböző programokat vagy hőmérsékleteket kever - ciklusai nagyon eltérő ideig fűtenek. Ha külön profilokra bontja (pl. hőmérsékletenként), az javítja a párosítást és az időbecsléseket.", - "advisory_phase_inconsistent_title": "⚠ Esetleg kevert programok", - "export_select_intro": "Jelöld be pontosan, mit szeretnél belefoglalni. Ha a profilokat a ciklusaik nélkül választod ki, akkor is felismerhető program exportálódik (a megtanult görbéje vele utazik).", - "import_analyze_hint": "Tölts be egy exportált fájlt (vagy illeszd be a JSON-ját). A WashData elemzi, és pontosan megmutatja, mi importálható, mielőtt bármi megváltozna.", + "export_select_intro": "Jelöld be, mi kerüljön bele. A profilok a ciklusaik nélkül is egyeztethető programként exportálódnak.", + "import_analyze_hint": "Tölts be egy exportált fájlt (vagy illeszd be a JSON-ját). A WashData megmutatja, mi importálható, mielőtt bármi megváltozna.", "analyzing": "Elemzés…", - "device_type_mismatch_warn": "Ez az export egy másik készüléktípusból származik ({src} vs {local}). A programok és ciklusok továbbra is importálhatók referenciaadatként, de a készülékspecifikus beállítások és a valós előzmények importálása le van tiltva.", + "device_type_mismatch_warn": "Ez az export másik készüléktípusból származik ({src} vs {local}). A programok és ciklusok referenciaadatként így is importálhatók, a készülékbeállítások és a valós előzmények viszont nem.", "merge_hint": "Az importált elemek hozzáadódnak; semmi helyi adat nem vész el. A névütközéseket alább oldhatod fel.", "replace_warn": "Minden bejelölt kategória törlődik, és a fájlból cserélődik le. A be nem jelölt kategóriák érintetlenül maradnak.", "dest_reference_hint": "Az importált ciklusok csak a programfelismerést javítják, és soha nem befolyásolják a használati/energia-statisztikákat.", - "dest_real_history_hint": "Az importált ciklusok ennek a készüléknek a saját előzményeiként számítanak, és az energia-/használati statisztikákat táplálják. Egy készülék új telepítésre költöztetéséhez használd.", + "dest_real_history_hint": "Az importált ciklusok ennek a készüléknek a saját előzményeiként és statisztikájaként számítanak. Ezzel költöztethetsz át egy készüléket új telepítésre.", "sug_muted": "Ezt a beállítást nem javasolja többé", "sug_mute_failed": "Nem sikerült elhallgattatni a javaslatot", "sug_unmuted_all": "Az elhallgattatott javaslatok visszaállítva", - "n_suggestions_muted": "{count} elhallgattatva; az automatikus finomhangolás nem fogja javasolni ezeket.", + "n_suggestions_muted": "{count} javaslat elhallgattatva; az automatikus finomhangolás nem fogja javasolni ezeket.", + "n_suggestions_muted_one": "{count} javaslat elhallgattatva; az automatikus finomhangolás nem fogja javasolni.", + "n_suggestions_muted_other": "{count} javaslat elhallgattatva; az automatikus finomhangolás nem fogja javasolni ezeket.", "font_size_hint": "Tegye nagyobbá vagy kisebbé mindent a panelen. Az ezen az eszközön lévő fiókjára vonatkozik.", - "import_history_description": "Volt már okoskonnektora a WashData előtt? Töltse fel a teljesítményérzékelő előzmény-exportját, vagy olvassa be közvetlenül a Home Assistantból: a szokásos észlelés lefut rajta, így a régi ciklusok megjelennek a Ciklusok listában, készen az elnevezésre.", - "hist_input_hint": "Töltsön fel az Előzmények panelről letöltött CSV-t (entity, state, last changed), vagy hagyja, hogy a WashData közvetlenül beolvassa az érzékelő előzményeit. Ezután pontosan ugyanaz az észlelés fut le rajta, mint élőben, és Ön választja ki, mely megtalált ciklusokat tartja meg.", - "hist_recorder_hint": "A kiválasztott dátumtól a mostani időpontig olvas be. A Home Assistant alapértelmezés szerint 10 napig tart meg részletes előzményeket, utána már csak óránkénti átlagokat, amelyek túl durvák a ciklusok észleléséhez - csak akkor válasszon korábbi dátumot, ha a recorder többet tart meg.", - "hist_scanning": "Az előzményeket újra lejátsszuk az észlelőn keresztül. Ez a háttérben fut - bezárhatja ezt a párbeszédpanelt, és később visszatérhet hozzá.", + "import_history_description": "Volt okoskonnektora a WashData előtt? Töltse fel a teljesítményelőzményeit, vagy olvassa be a Home Assistantból. Az észlelés lefut rajta, és a régi ciklusok elnevezésre készen megjelennek a Ciklusok listában.", + "hist_input_hint": "Töltsön fel egy CSV-t az Előzmények panelről (entity, state, last changed), vagy hagyja, hogy a WashData közvetlenül beolvassa az érzékelő előzményeit. Az élő észlelés lefut rajta, és Ön választja ki, mely ciklusokat tartja meg.", + "hist_recorder_hint": "A kiválasztott dátumtól mostanáig olvas. A Home Assistant alapértelmezés szerint 10 napig tart meg részletes előzményeket; a régebbi óránkénti átlagok túl durvák a ciklusok észleléséhez.", + "hist_scanning": "Az előzményeket a háttérben futtatjuk át az észlelőn. Bezárhatja ezt a párbeszédpanelt, és később visszatérhet.", "hist_imported_count": "{n} ciklus importálva.", "hist_duplicates": "{n} korábban már importálva volt, ezért kimaradt.", "hist_capped": "Elérte az eszközre vonatkozó importált ciklus korlátot; a többi nem lett eltárolva.", - "hist_next_step": "A Ciklusok listában találhatók, importált előzményként megjelölve. Nyisson meg egyet, és a Címkézés funkcióval adja meg a hozzá tartozó programot.", + "hist_next_step": "A Ciklusok listában vannak, importált előzményként megjelölve. Nyisson meg egyet, és a Címkézés gombbal nevezze el a programját.", "hist_rows_read": "{n} mérés beolvasva", "hist_entity_substituted": "{used} beolvasva (ehhez az eszközhöz {wanted} van beállítva)", "hist_breaks": "{n} szakasz, ahol az érzékelő nem volt elérhető", @@ -1095,25 +1024,72 @@ "hist_scan_capped": "Csak az első találatok láthatók (összesen {n} volt).", "hist_recorder_empty": "A Home Assistantnak nincs részletes előzménye erről az érzékelőről abban az időszakban.", "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_scan_expired": "Ez a vizsgálat már nem elérhető. 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.", - "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.", + "imported_history_readonly": "Importált teljesítményelőzményekben találva. Segíti az egyeztetést, de nem számít bele a statisztikákba, és nem vágható vagy osztható. Címkézze fel a program megnevezéséhez.", + "odometer_intro": "Ez az összesítés csak nő. Egy ciklus törlése nem csökkenti, így a szervizemlékeztetők pontosak maradnak.", + "odometer_correct_hint": "Állítsa be, ha a WashData olyan futást számolt, amely meg sem történt, 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}", "toast_range_required": "Adjon meg érvényes Ettől/Eddig tartományt.", - "no_profiles_option": "(még nincs profil, adjon hozzá egyet)" + "no_profiles_option": "(még nincs profil, adjon hozzá egyet)", + "compare_cycles_title": "{count} ciklus összehasonlítása", + "edit_builtin_phase": "Ez egy beépített fázis. A mentés egyéni másolatot hoz létre; a másolat törlésével visszaáll az eredeti.", + "fetch_error": "Nem sikerült betölteni az adatokat.", + "connection_lost": "A kapcsolat megszakadt - a megjelenített adatok időpontja: {time}.", + "pg_notify_partial": "A jelölők csak az indítási, majdnem kész, befejezési, mérföldkő- és csendes órás értesítéseket mutatják. Az élő frissítések, a kirakodási emlékeztetők és a túlfutási riasztások nincsenek szimulálva.", + "connect_to_confirm": "A megerősítéshez vagy értékeléshez csatlakozzon a beállítások fogaskerekénél.", + "coverage_gap_cluster": "{count} legutóbbi, körülbelül {min} perces címkézetlen ciklus egyetlen olyan programnak tűnik, amelyet még nem hozott létre.", + "pg_label_yes": "A ciklus végén \"{name}\" címkét kapna", + "pg_label_no": "Felülvizsgálatot kérne: {reason}", + "live_match_debug_tip": "Bizalom: mennyire egyezik az aktuális teljesítménygörbe a legjobb jelölt profillal (0-100%). Bizonytalan: a két legjobb jelölt pontszáma egymástól 5%-on belül van - a címke bizonytalan, amíg a ciklus be nem fejeződik.", + "automation_alias_started": "{device}: a ciklus elindult", + "automation_alias_finished": "{device}: a ciklus befejeződött", + "automation_desc_started": "Akkor fut le, amikor egy {device} ciklus elindul. Adja hozzá a saját műveleteit (értesítés, világítás, ...).", + "automation_desc_finished": "Akkor fut le, amikor egy {device} ciklus befejeződik. Adja hozzá a saját műveleteit (értesítés, világítás, ...).", + "feedback_detected": "A WashData ezt észlelte: {name}", + "import_undo_hint": "Megmarad egy visszaállítási pont a legutóbbi importálás ({when}) előttről: {profiles} profil és {cycles} ciklus.", + "replace_undo_hint": "Először egy visszaállítási pont mentődik, így ez az importálás az Exportálás / Importálás részben visszavonható.", + "pg_in_sample_note": "Ezek a ciklusok azokat a profilokat is építették, amelyekhez illesztik őket, ezért az eredmények optimisták: új ciklusoknál valamivel kevesebb helyes illesztésre számítson.", + "hist_tail_trimmed": "A ciklus vége utáni {min} perc várakozás levágva, ahogy az élő integráció is levágja", + "hist_truncated": "leállt a(z) {n} mérésből álló korlátnál, így az előzmények többi része nem lett átvizsgálva", + "hist_warn_kw": "A legmagasabb mérés csak {w}. Úgy tűnik, ez az érzékelő kilowattban jelent; a WashData wattot vár, így nem észlelhető ciklus.", + "pg_compare_profile_tip": "Csak azt választja ki, melyik profil jelenjen meg a grafikonon. A szimuláció mindig önállóan illeszti a ciklust, ahogy az élő integráció is.", + "pg_outcome_stale": "A beállítások megváltoztak a szimuláció futtatása óta. Még a régi eredményt mutatja.", + "pg_sweep_guarded": "Több ciklust fejez be korán, többet bont szét, vagy kihagy olyan ciklusokat, amelyeket a jelenlegi beállítás észlel, ezért soha nem ajánlott.", + "pg_sweep_keep_current": "Tartsa meg a jelenlegi értéket ({value}, {metric}): egyik érték sem volt jobb legalább egy ciklussal.", + "pg_task_busy": "Ennél az eszköznél már fut egy Tesztelés előzményeken vagy Optimalizálás. Várja meg, vagy szakítsa meg.", + "hist_warn_naive_time": "{n} időbélyegnek nincs időzónája, ezért UTC-ként lett beolvasva. Ha a fájl helyi időt használ, a ciklusok az Ön UTC-eltolásával elcsúsznak, és a nyári időszámítás váltása körüli mérések kimaradhatnak.", + "hist_warn_naive_time_one": "{n} időbélyegnek nincs időzónája, ezért UTC-ként lett beolvasva. Ha a fájl helyi időt használ, a ciklusok az Ön UTC-eltolásával elcsúsznak, és a nyári időszámítás váltása körüli mérések kimaradhatnak.", + "hist_warn_naive_time_other": "{n} időbélyegnek nincs időzónája, ezért UTC-ként lett beolvasva. Ha a fájl helyi időt használ, a ciklusok az Ön UTC-eltolásával elcsúsznak, és a nyári időszámítás váltása körüli mérések kimaradhatnak.", + "pg_sweep_n_cycles": "{count} ciklus alapján", + "pg_sweep_n_cycles_one": "{count} ciklus alapján", + "pg_sweep_n_cycles_other": "{count} ciklus alapján", + "cycle_context_loading": "Rögzítő-előzmények betöltése…", + "cycle_context_note": "Szürke: a teljesítményérzékelő rögzítő-előzményei a ciklus körül. Nem része a ciklusnak, és nem befolyásolja az időtartamot, az energiát vagy az egyeztetést.", + "cycle_context_none": "A rögzítő nem tárol leolvasásokat a ciklus körül (a Home Assistant alapértelmezés szerint 10 napnyi előzményt őriz meg).", + "cycle_context_tip": "Csak megjelenítés: a teljesítményérzékelő rögzítő-előzményeit mutatja a ciklus körül, ha még megvannak. Ha a megszakadt indítási kísérleteket a jövőbeli ciklusokhoz szeretné adni, használja a Görbe előfutama beállítást (Beállítások > Érzékelés > Ciklus indítása), amely megváltoztatja a tárolt időtartamokat.", + "invalid_threshold_pair": "A Leállítási küszöbnek ({stop} W) kisebbnek kell lennie az Indítási küszöbnél ({start} W). Semmi sem lett mentve; a kettőt együtt módosítsa." }, "phase_desc": { "anti_crease": "Alkalmankénti rövid bukdácsolás a befejezés után a ráncok csökkentése érdekében.", "anti_wrinkle": "Időszakos ciklus utáni zuhanás a ráncok csökkentése érdekében.", + "baking": "A fűtőelem magas hőfokon működik a héj és a bél kialakulásához.", + "cooking": "Fő sütési szakasz, működő fűtéssel és ventilátorral.", "cool_down": "Zuhanás hő nélkül a ciklus vége felé.", + "cool_down_af": "Csak ventilátoros hűtés a fűtés után.", "drain_and_switch": "Átmeneti időszak a mosásból a szárítási módba.", "dry": "Szárítási szakasz fűtőberendezéssel és/vagy maradékhővel.", "drying": "Fő fűtött bukfencezési időszak.", "heat_up": "A fűtőelem kezdeti bemelegítése a teljes szárítás megkezdése előtt.", + "keep_warm": "Alacsony hőfokú tartás, hogy az étel meleg maradjon.", + "keep_warm_bm": "Alacsony hőfokú tartás, hogy a kenyér sütés után meleg maradjon.", + "kneading": "A motor keveri és dagasztja a tésztát. Nagy teljesítményfelvétel.", + "pause_af": "Rövid szünet rázáshoz vagy ellenőrzéshez.", + "pre_heat": "A sütőtér kezdeti felfűtése a teljes sütés előtt.", "pre_rinse": "Első lepermetezés a mosószeres mosás előtt.", + "proving": "Kelesztés alacsony hőfokon: az élesztő erjed, a tészta megkel.", + "resting": "Rövid, kis teljesítményű szünet a dagasztások között, hogy a sikér pihenjen.", "pre_wash": "Első áztatás vagy előkezelés a főmosás előtt.", "rinse": "Tiszta vizes öblítési szakasz. Ez a fázis többször is megismétlődhet.", "sanitize": "Magas hőmérsékletű tisztítási szakasz fertőtlenítési programokhoz.", @@ -1126,6 +1102,7 @@ "wash_dw": "Főmosószeres mosás melegítéssel." }, "placeholder": { + "custom_task": "pl. Ajtótömítés tisztítása", "add": "hozzáadás…", "group_name": "pl. Pamut 2:47", "json_buttons": "[{\"action\":\"ID\",\"title\":\"Label\"}]", @@ -1153,10 +1130,10 @@ "section": { "dishwasher": { "label": "Mosogatógép", - "intro": "A ciklus végének kezelése mosogatógépekhez, amelyek jellemzően egy hosszú, szinte néma szárítási fázissal fejeződnek be egy rövid végső vízleeresztés előtt." + "intro": "Ciklusvég-kezelés mosogatógépekhez, amelyek gyakran sokáig csendben szárítanak egy rövid végső vízleeresztés előtt." }, "anti_wrinkle": { - "intro": "A ránctalanító/gyűrőtlenítő mód alacsony teljesítményű dobpulzusokat érzékel a főfázis után, és a befejezett ciklushoz csatolja őket.", + "intro": "A szárítógép főfázis utáni, alacsony teljesítményű dobpulzusait a befejezett ciklushoz csatolja.", "label": "Ránctalanítás" }, "basic": { @@ -1164,7 +1141,7 @@ "label": "Alapok" }, "delay": { - "intro": "A késleltetett indítás érzékelése azonosítja, amikor egy eszköz be van kapcsolva, de még nem kezdte el a ciklust.", + "intro": "Érzékeli, ha a készülék be van kapcsolva és késleltetett indításra vár.", "label": "Késleltetett indítás" }, "detection": { @@ -1188,153 +1165,145 @@ "label": "Időzítés és felügyelet" }, "triggers": { - "intro": "Opcionális külső jelek: záró trigger, ajtóérzékelő, szünetkapcsoló és a kirakodási emlékeztető.", + "intro": "Opcionális extrák: záró trigger, ajtóérzékelő, szünetkapcsoló és kirakodási emlékeztető.", "label": "Triggerek és ajtók" - }, - "phase_eta": { - "label": "Hátralévő idő", - "intro": "Fázistudatos hátralévő idő olyan gépekhez, amelyek ciklushossza a hőmérséklettől vagy a centrifugálástól függ." } }, "setting": { "dishwasher_end_spike_quiet_release": { "label": "Passzív szárítás csendes feloldása", - "doc": "Miután a ciklus túllépi a várható időtartamát, mennyi ideig kell a mosogatógépnek csendben maradnia (a Leállítási küszöb alatt), mielőtt a WashData abbahagyja a végső vízleeresztésre való várakozást és befejezi a ciklust. Növelje, ha a gépének hosszú, néma szárítási fázisa van egy késői végső vízleeresztés előtt, amely kimarad - a szélesebb ablak lehetővé teszi, hogy a megtanult időtartam kövesse a szezonális eltolódást (hidegebb bevezetett víz = hosszabb ciklusok), ahelyett hogy a régi átlaghoz rögzülne. Csak lerövidítheti a várakozást a belső 30 perces végső-csúcs korláthoz képest, soha nem hosszabbítja meg." + "doc": "A várható időtartam után mennyi ideig kell a mosogatógépnek a Leállítási küszöb alatt maradnia, mielőtt a WashData már nem vár a végső vízleeresztésre. Növelje, ha egy hosszú szárítás utáni késői leeresztés kimarad. A várakozás sosem haladja meg a 30 percet." }, "anti_wrinkle_enabled": { - "doc": "Ismerje fel a szárítógép által a fő fűtési fázis után kibocsátott rövid, kis teljesítményű szárítópulzusokat, és tartsa őket a kész ciklushoz kapcsolva, ahelyett, hogy új ciklusként olvasná őket.", + "doc": "A szárítógép fő fűtési fázis utáni rövid, alacsony teljesítményű dobpulzusai a befejezett ciklushoz tartoznak, nem számítanak új ciklusnak.", "label": "Ránctalanítás érzékelés engedélyezése" }, "anti_wrinkle_exit_power": { - "doc": "A teljesítménynek ez alá kell csökkennie az impulzusok között, hogy a ránctalanító üzemmód aktív maradjon.", + "doc": "Ránctalanító módban az ennél kisebb teljesítmény csendnek számít a dobimpulzusok között, és a Max. impulzusszünetnél hosszabb csend befejezi a módot. Ha a Leállítási küszöb magasabb, az érvényes, így ez a beállítás csak a Leállítási küszöb fölé állítva változtat bármit.", "label": "Kilépési teljesítmény küszöb" }, "anti_wrinkle_idle_timeout": { - "doc": "Meddig maradhat a gép csendben két szárítópulzus között, mielőtt a ránctalanító üzemmód véget ér. Állítsa hosszabbra, mint a leghosszabb szünet, amit a szárítógép a pulzusok között hagy, különben minden későbbi pulzus téves indításként lesz értelmezve.", + "doc": "Meddig maradhat csendben a szárítógép két dobpulzus között, mielőtt a ránctalanító mód véget ér. Állítsa a szárítógép leghosszabb szüneténél hosszabbra, különben a későbbi pulzusok téves indításnak számítanak.", "label": "Max. impulzusszünet" }, "anti_wrinkle_max_duration": { - "doc": "Az ennél hosszabb impulzusokat valódi ciklusnak tekintik, nem pedig ránctalanítónak.", + "doc": "Az ennél hosszabb pulzus valódi ciklusnak számít, nem ránctalanító dobforgásnak.", "label": "Max. időtartam" }, "anti_wrinkle_max_power": { - "doc": "Az ezen az erőn felüli impulzus valódi új ciklusként kezelendő, nem ránctalanító zuhanásként. Állítsa közvetlenül az impulzusteljesítmény fölé.", + "doc": "Az ennél nagyobb teljesítményű pulzus valódi új ciklusnak számít, nem dobforgásnak. Állítsa közvetlenül a dobpulzusok teljesítménye fölé.", "label": "Max. ránctalanítási teljesítmény" }, "auto_label_confidence": { - "doc": "Ha a ciklus végén az egyezési pontszám e felett van, akkor a program automatikusan, minden megerősítés nélkül felcímkézik. Emelje fel, hogy nagyobb biztonságot igényeljen az automatikus címkézés előtt; csökkentse az automatizálás érdekében. Az alatta lévő Learning Confidence funkcióval együtt működik.", + "doc": "Az a ciklusvégi egyezési pontszám, amelytől a program kérdés nélkül címkét kap. Emelje a nagyobb biztonságért; csökkentse, hogy több ciklus kapjon automatikusan címkét. Tartsa a Tanulási megbízhatóság felett.", "label": "Automatikus jelölési megbízhatóság" }, "auto_maintenance": { - "doc": "Futtasson éjszakai takarítást: újjáépítse a profilborítékokat, újraszámolja a ciklus állapotát, törölje a hibakeresési nyomokat és őrizze meg a legutóbbi ciklusokat.", + "doc": "Éjszakai karbantartás: felcímkézi a régi ciklusokat, amelyek már egyértelműen egy programhoz illenek, újraépíti a programgörbéket, frissíti a rendellenesség-jelöléseket és a ciklusok egészségét, és törli a hibakeresési nyomokat, ha a \"Hibakeresési nyomok mentése\" ki van kapcsolva. A ciklusok és teljesítménygörbéik mindig megmaradnak.", "label": "Automatikus karbantartás (éjszakai tisztítás)" }, "completion_min_seconds": { - "doc": "Az ennél rövidebb ciklusokat a rendszer szellemciklusként elveti (próbafutások, az ajtó kinyitása zokni hozzáadásához).", + "doc": "Az ennél rövidebb ciklusok (próbafutások, ajtónyitás egy zokni hozzáadásához) Megszakítottként mentődnek, befejezési értesítés nélkül.", "label": "Min. ciklusidőtartam" }, "delay_confirm_seconds": { - "doc": "A tápfeszültségnek ennyi ideig a készenléti sávban kell maradnia, mielőtt a készüléket indításra várónak tekintik, nem pedig működésre.", + "doc": "A teljesítménynek ennyi ideig a készenléti sávban kell maradnia, mielőtt a készülék indításra várónak számít.", "label": "Megerősítési másodpercek" }, "delay_start_detect_enabled": { - "doc": "Érzékeli, ha a készülék be van kapcsolva, és vár (késleltetett indítás / készenlét), de még nem kezdte meg a ciklusát, így a készenléti húzást nem tévesztik össze egy futó ciklussal.", + "doc": "Érzékeli, ha a készülék be van kapcsolva és késleltetett indításra vár, így a készenléti fogyasztását nem veszi futó ciklusnak.", "label": "Késleltetett indítás érzékelés engedélyezése" }, "delay_timeout_hours": { - "doc": "Hagyja abba a várakozást a késleltetett indítási módban ennyi óra elteltével, és térjen vissza üresjáratba, így a gép, amelyet áram alatt hagytak, de nem indult el, nem vár örökké.", + "doc": "Ennyi óra után feladja a késleltetett indításra várakozást, és visszatér tétlen állapotba.", "label": "Időtúllépési órák" }, "device_type": { - "doc": "Készülék osztály. Beállítja az ésszerű észlelési alapértékeket (küszöbértékek, kikapcsolási késleltetés, végkezelés) az adott készüléktípushoz hangolva; csak akkor módosítsa, ha az eszközt eredetileg rossz típusként állította be.", + "doc": "Beállítja az észlelési alapértékeket (küszöbök, kikapcsolási késleltetés, ciklusvég-kezelés) az adott készüléktípushoz. Csak akkor módosítsa, ha az eszközt rossz típussal állította be.", "label": "Eszköz típusa" }, "door_sensor_entity": { - "doc": "Opcionális ajtó bináris érzékelő. Annak érzékelésére szolgál, ha a készüléket egy ciklus után kinyitották/kirakták.", + "doc": "Opcionális ajtóérzékelő. Megmutatja, mikor nyitották ki a készüléket kirakodáshoz egy ciklus után.", "label": "Ajtóérzékelő entitás" }, "duration_tolerance": { - "doc": "Tolerancia a hátralévő időre vonatkozó becslésekhez (visszajelzés tanulás, nem egyeztetés). Ha a tényleges időtartam a becsült +/-X%-on belül van, az jó egyezésnek számít.", + "doc": "Csak a tanulási visszajelzéshez: a becsült időtartam +/-X%-án belüli ciklus jó becslésnek számít. Az egyeztetést nem befolyásolja.", "label": "Becslési tolerancia" }, "enable_ml_models": { - "doc": "Amíg a ciklus fut, hagyja, hogy a modellek finomítsák az élő eredményeket: egyenletesebb hátralévő idő és energia/költségbecslés, valamint idő előtti leállás elleni védelem a végérzékelésnél (csak késleltetheti a befejezést, soha nem fejezheti be korábban, és korlátos). Az Ön finomhangolt modelljeit használja, ha rendelkezésre állnak, ellenkező esetben a beépített modelleket. Ki = csak a klasszikus teljesítményalapú logika (még mindig megbízható).", + "doc": "Egy ezen a gépen tanított modell előrejelzi a futó ciklus energiáját és költségét, és egy jól ismert program, amely szokatlanul korán leáll, a teljes kikapcsolási késleltetés nélkül befejeződhet. Kikapcsolva: csak teljesítményalapú logika.", "label": "Intelligens modellek alkalmazása ciklus közben" }, "end_energy_threshold": { - "doc": "A kikapcsolási késleltetés visszaszámlálása során a felhalmozott energia (watt x idő) ehhez a küszöbértékhez kerül összehasonlításra. Ha túllépi, a visszaszámlálás újraindul – a gyűrődésgátló szárak és a mosogatógépes szárítószárak a ciklushoz rögzítve maradnak, ahelyett, hogy rövidre vágnák őket. Emelje fel, ha a ciklusok túl korán érnek véget a lehűlés során; csökkentse, ha az észlelés lassú.", + "doc": "A kikapcsolási késleltetés alatt megengedett energia. Ha ennél több fogy, a ciklus folytatódik, így a dobforgatások és a száradási utószakaszok a ciklushoz tartoznak. Csökkentse, ha a ciklusok egy utószakasz közben érnek véget; emelje, ha a befejezett ciklusok tovább várakoznak.", "label": "Záró energia" }, - "end_repeat_count": { - "doc": "Az egymást követő leállási küszöbérték alatti leolvasások száma a ciklus vége előtt. Az 1 megfelelő a legtöbb csatlakozóhoz. Emelje 2-3-ra, ha az intelligens csatlakozó időnként hamis nullát jelez a ciklus közepén, és a ciklusok idő előtt véget érnek.", - "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). Á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á.", + "doc": "Érzékelő az aktuális kWh-nkénti árral (pl. dinamikus tarifa). Felülírja az alábbi statikus árat, ezért árnak kell lennie, nem a konnektor teljesítmény- vagy energiaérzékelőjének.", "label": "Energiaár entitás" }, "energy_price_static": { - "doc": "Rögzített kWh-nkénti ár a költségadatokhoz használatos, ha nincs fent beállítva élő ár entitás.", + "doc": "Rögzített kWh-nkénti ár, ha nincs ár entitás beállítva.", "label": "Statikus energiaár (kWh-nként)" }, "energy_sensor": { - "doc": "Opcionális kumulatív energiaszámláló (total_increasing kWh/Wh, pl. a konnektor saját élettartam-mérője). Ha be van állítva, minden ciklus jelentett energiája ennek a számlálónak a kezdő- és záróértéke közötti különbségéből származik, ami elkerüli azt az alulmérést, amit egy lassan jelentő teljesítményérzékelő integrálásából kapna. Visszavált az integrált értékre, ha a leolvasás hiányzik, mértékegysége ismeretlen, vagy a különbség nem pozitív. Hagyja üresen, hogy továbbra is a teljesítményérzékelőt integrálja.", + "doc": "Opcionális kWh- vagy Wh-számláló (total_increasing, pl. a konnektor saját mérője). A ciklus energiája ebből származik, ami pontosabb, mint egy lassú teljesítményérzékelőből számolni. Ha a leolvasás használhatatlan, a számolt érték érvényes.", "label": "Energiamérő entitás" }, "expose_debug_entities": { - "doc": "Extra diagnosztikai HA entitások közzététele (egyezési megbízhatóság, kétértelműség, belső állapot). Az Off (Kikapcsolva) funkció tisztán tartja az entitáslistát normál használathoz.", + "doc": "Diagnosztikai entitások hozzáadása (egyezési megbízhatóság, kétértelműség, belső állapot). Kikapcsolva tiszta marad az entitáslista.", "label": "Hibakeresési entitások közzététele" }, "external_end_trigger": { - "doc": "Bináris érzékelő, amelynek állapotváltozása jelzi a ciklus végét (pl. egy készülék „kész” érintkezője vagy egy társintegráció).", + "doc": "Bináris érzékelő, amelynek változása jelzi a ciklus végét (pl. egy „kész” érintkező vagy egy másik integráció).", "label": "Külső trigger entitás" }, "external_end_trigger_enabled": { - "doc": "Hagyja, hogy egy külső bináris érzékelő jelezze a ciklus végét, a beépített energiaalapú érzékelés mellett.", + "doc": "Egy bináris érzékelő is jelezheti a ciklus végét, a teljesítményalapú érzékelés mellett.", "label": "Külső záró trigger engedélyezése" }, "external_end_trigger_inverted": { - "doc": "A kioldó érzékelő kikapcsolását (nem BE) tekintse a ciklus végének jeleként.", + "doc": "Az érzékelő KI állapotba váltása (nem a BE) jelzi a ciklus végét.", "label": "Külső trigger invertálása (trigger KI-re)" }, "learning_confidence": { - "doc": "Ha a találati pontszám e és az automatikus címke megbízhatósága közé esik, egy visszajelzési értesítés kéri, hogy ellenőrizze az azonosított programot. Ez alatt a pontszám alatt a mérkőzés túl bizonytalan ahhoz, hogy felszínre kerüljön. Az automatikus címke megbízhatósága alatt kell tartani.", + "doc": "A legkisebb egyezési pontszám egy befejezett ciklus megjelöléséhez. Egyértelmű győztest megjelöl; szoros eredménynél a ciklus a Ciklusok lap felülvizsgálati sorába kerül. Ez alatt semmit nem jelöl meg és nem kérdez. Tartsa az Automatikus jelölési megbízhatóság alatt.", "label": "Tanulási megbízhatóság" }, "linked_device": { - "doc": "Opcionálisan beágyazza ezt a WashData-eszközt egy másik eszköz (például az intelligens csatlakozó) alá a HA-eszköz-nyilvántartásban, amely a „Csatlakozva…” jelzéssel jelenik meg.", + "doc": "Opcionális. Ez a WashData-eszköz egy másik eszköz (pl. az okoskonnektor) alatt jelenik meg a Home Assistantban, „Csatlakozva ...” jelzéssel.", "label": "Csoportosítás eszköz alá" }, "min_off_gap": { - "doc": "Ha a gép ennél az időnél rövidebb ideig kapcsol ki, a be-/ki/bekapcsolás sorozatot egy folyamatos ciklusnak tekinti. Megakadályozza, hogy az áztatási programok (a mosás közben néhány percre kikapcsol) ne oszlanak fel két külön ciklusra. Állítsa rövidebbre, mint az egymás melletti terhelések közötti hézag, ha azt szeretné, hogy azokat külön ciklusnak számítsa. Az eszköztípus alapértelmezett beállításai védik az egyes készülékek tipikus cikluson belüli szünetét.", + "doc": "Az ennél rövidebb kikapcsolt szakasz nem osztja ketté a ciklust, így az áztatási szünetek egy mosásban maradnak. Tartsa rövidebbre az egymást követő töltetek közötti szünetnél, különben azok egy ciklussá olvadnak.", "label": "Min. kikapcsolási rés" }, "min_power": { - "doc": "Aktívnak tekintett abszolút minimális teljesítmény. Az ez alatti értékeket a rendszer 0 W-nak (készenléti) tekinti, kiszűrve az intelligens csatlakozók és a készenléti LED-ek fantomterhelését.", + "doc": "Az ez alatti leolvasások 0 W-nak (készenlét) számítanak. Kiszűri az okoskonnektorok üresjárati fogyasztását és a készenléti LED-eket.", "label": "Minimális teljesítmény" }, "ml_training_enabled": { - "doc": "Rendszeresen tanulmányozza az áttekintett ciklusokat egyik napról a másikra, és finomítsa a modelleket ehhez a géphez. A változás csak akkor marad meg, ha valóban jobb eredményt ér el a kitartott ciklusokon, tehát ez csak segíthet, vagy változatlan marad – soha ne regresszió.", + "doc": "Időnként újratanítja az energiamodellt a gép befejezett ciklusain. Az új modell csak akkor marad meg, ha a tesztciklusokon jobb az egyszerű becslésnél.", "label": "Tanulás erről a gépről" }, "ml_training_hour": { - "doc": "Helyi napszak (0-23) az éjszakai finomhangolás elvégzéséhez. Válasszon egy csendes órát, például 2 (02:00).", + "doc": "Az éjszakai tanítás órája (0-23). Válasszon csendes órát, pl. 2.", "label": "Tanulás ennyi órakor" }, "ml_training_interval_days": { - "doc": "Ennyi napon belül legfeljebb egyszer ellenőrizze a fejlesztéseket.", + "doc": "Legfeljebb ennyi naponta egyszer tanít újra.", "label": "Legfeljebb ennyi naponta ellenőrizve" }, "ml_training_min_cycles": { - "doc": "A finomhangolás előtt várja meg, amíg legalább ennyi ciklus rögzítésre kerül, hogy legyen miből tanulni.", + "doc": "A tanítás előtt legalább ennyi rögzített ciklust vár meg.", "label": "Előbb ennyi ciklus szükséges" }, "name": { - "doc": "A HA integrációs listában és az eszköznyilvántartásban látható megjelenített név.", + "doc": "Az eszköz neve a Home Assistantban.", "label": "Eszköz neve" }, "no_update_active_timeout": { - "doc": "Ha futás közben ilyen sokáig nem érkezik tápellátás-frissítés, tegyük fel, hogy a csatlakozó leesett offline állapotban, és kényszerítse a leállítást, hogy elkerülje a zombi ciklust. Az alapértelmezett 600 s felhő- vagy hálókésést tesz lehetővé.", + "doc": "Ha ciklus közben ennyi ideig nem érkezik teljesítményfrissítés, a konnektort offline-nak tekinti, és kényszerrel leállítja a ciklust. Az alapértelmezett 600 mp a felhő- vagy mesh-hálózati késéseket is kibírja.", "label": "Frissítés nélküli időtúllépés" }, "notify_before_end_minutes": { @@ -1342,35 +1311,35 @@ "label": "Befejezés előtti riasztás" }, "notify_channel": { - "doc": "Android értesítési csatorna neve kezdő/élő üzenetekhez (csatornánként szabályozza a hangot és a prioritást a mobilalkalmazásban). Az üres a kísérőalkalmazás alapértelmezését használja.", + "doc": "Android-csatorna az indítási és élő üzenetekhez; a csatorna szabja meg a hangot és a prioritást. Üres: az alkalmazás alapértelmezése.", "label": "Android csatorna (indítás/élő)" }, "notify_cycle_timers": { - "doc": "Értesítések a ciklus adott perceiben (például lágyító hozzáadása esetén). Az üzenet támogatja a következőt: {device}, {program}, {minutes}. Engedélyezze az Automatikus szünetet, hogy szüneteljen az adott ponton, és interaktív értesítést kapjon a Folytatás gombbal; folytatás a panelen, a szüneteltetési/folytatási szolgáltatáson vagy az értesítési műveleten keresztül.", + "doc": "Értesítések a ciklus megadott perceiben (pl. öblítő hozzáadásához). Az üzenetben használható: {device}, {program}, {minutes}. Automatikus szünettel a ciklus ott szünetel is, és az értesítés Folytatás gombot kap.", "label": "Ciklus időzítők" }, "notify_finish_channel": { - "doc": "Android értesítési csatorna neve a befejező üzenethez. Üres újra felhasználja a kezdő/élő csatornát.", + "doc": "Android-csatorna a befejező üzenethez. Üresen az indítási/élő csatornát használja.", "label": "Android csatorna (befejezés)" }, "notify_finish_message": { - "doc": "A ciklus befejezésekor elküldött üzenettörzs. Sablonváltozók: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}. Itt a {time_finished} és a {vs_typical} a leghasznosabbak.", + "doc": "A ciklus befejezésekor küldött szöveg. Változók: {device}, {program}, {duration} (perc), {duration_hm} (1 h 05 min), {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}, {status} (completed vagy force_stopped; a megszakított ciklusok nem küldenek semmit). Más változóval az alapértelmezett szöveget küldi.", "label": "Befejezési üzenet" }, "notify_finish_services": { - "doc": "notify.* szolgáltatások hívják meg a ciklus végén. Adjon hozzá egyet célonként; hagyja üresen, hogy ne legyen befejezésért értesítés.", + "doc": "A ciklus befejezésekor meghívott notify.* szolgáltatások. Hagyja üresen, ha nem kell.", "label": "Befejezési szolgáltatások" }, "notify_fire_events": { - "doc": "A ha_washdata_* eseményeket is aktiválja a ciklus indításakor/befejezésekor, így megépítheti saját automatikáját.", + "doc": "A ciklus indításakor és befejezésekor ha_washdata_* eseményeket is kivált a saját automatizálásaihoz.", "label": "HA események aktiválása értesítésekhez" }, "notify_icon": { - "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.", + "doc": "Kis ikon az értesítésen (pl. mdi:washing-machine). Csak a mobile_app célok kapják meg, a notify csoportok nem. iOS-en az alkalmazásikont helyettesíti, és 2026.8-as vagy újabb kísérőalkalmazás kell hozzá. Üres: a platform alapértelmezése.", "label": "Értesítés ikonja" }, "notify_live_chronometer": { - "doc": "Statikus becslés helyett élőben frissített visszaszámláló megjelenítése az értesítésben (az ezt támogató platformokon).", + "doc": "Élő visszaszámláló az értesítésben (ahol támogatott) rögzített becslés helyett.", "label": "Élő kronométer használata" }, "notify_live_interval_seconds": { @@ -1378,143 +1347,135 @@ "label": "Élő frissítési intervallum" }, "notify_live_overrun_percent": { - "doc": "Ha egy ciklus ennél a százaléknál nagyobb mértékben haladja meg a becsült értéket, küldjön túlfutási riasztást.", + "doc": "Az élő frissítések leállnak, ha a ciklus ennyi százalékkal túllépi a várható időtartamát (20 = legfeljebb a várható idő 1,2-szerese).", "label": "Élő túlfutás % riasztás előtt" }, "notify_live_services": { - "doc": "notify.* szolgáltatások élő folyamatfrissítést kértek a ciklus lefutása közben. Hagyja üresen az élő folyamatról szóló értesítések letiltásához.", + "doc": "notify.* szolgáltatások az élő folyamatfrissítésekhez a ciklus közben. Hagyja üresen a kikapcsolásukhoz.", "label": "Élő folyamat szolgáltatások" }, "notify_only_when_home": { - "doc": "Csak akkor küldjön értesítést, ha a linkelt személyek közül legalább egy (fent) otthon van.", + "doc": "Csak akkor értesít, ha a fenti személyek közül legalább egy otthon van.", "label": "Csak otthon értesítés" }, "notify_people": { - "doc": "személy.* entitások, amelyeket a „Csak otthonról értesítés” használ annak eldöntésére, hogy valaki otthon van-e.", + "doc": "person.* entitások, amelyeket a „Csak otthon értesítés” ellenőriz.", "label": "Személyek (Csak otthon értesítéshez)" }, "notify_pre_complete_message": { - "doc": "A vég előtti / majdnem kész riasztás törzse. Sablonváltozók: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Minden élő folyamatfrissítés szövege. Sablonváltozók: {device}, {program}, {minutes} (hátralévő perc). Más változóval az alapértelmezett szöveget küldi.", "label": "Befejezés előtti üzenet" }, "notify_reminder_message": { - "doc": "A még várakozó kirakodási emlékeztető törzse. Az üres a beépített alapértelmezést használja. Sablonváltozók: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Az egyszeri Befejezés előtti riasztás szövege. Sablonváltozók: {device}, {program}, {minutes} (a Befejezés előtti riasztás beállítása). Más változóval az alapértelmezett szöveget küldi.", "label": "Emlékeztető üzenet" }, "notify_start_message": { - "doc": "A ciklus indulásakor elküldött üzenettörzs. Sablonváltozók: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "A ciklus indulásakor küldött szöveg. Sablonváltozók: {device}, {program}. Más változóval az alapértelmezett szöveget küldi.", "label": "Indítási üzenet" }, "notify_start_services": { - "doc": "notify.* szolgáltatások hívják meg a ciklus indulásakor. Adjon hozzá egyet célonként (telefon, műszerfal stb.); hagyja üresen, hogy ne induljon értesítés.", + "doc": "A ciklus indulásakor meghívott notify.* szolgáltatások (telefon, irányítópult, ...). Hagyja üresen, ha nem kell.", "label": "Indítási szolgáltatások" }, "notify_timeout_seconds": { - "doc": "Az értesítés automatikus elvetése ennyi másodperc után (az azt támogató platformokon). A 0 megőrzi, amíg manuálisan el nem távolítja.", + "doc": "Ennyi másodperc után eltünteti az értesítést (ahol támogatott). 0: megmarad, amíg el nem veti.", "label": "Automatikus elvetés után" }, "notify_title": { - "doc": "Értesítés címe. Sablonváltozók: {device}, {program}, {duration}, {energy}, {cost}, {date}, {time}, {minutes}.", + "doc": "Értesítés címe. Csak a {device} helyettesítődik be; más változóval az alapértelmezett cím jelenik meg.", "label": "Értesítés címe" }, "notify_unload_delay_minutes": { - "doc": "Percekkel a ciklus lejárta után, mielőtt elküldené a még mindig várakozó \"üresd ki a gépet\" emlékeztetőt. Állítsa be a 0-t az emlékeztető letiltásához.", + "doc": "Hány perccel a ciklus vége után jöjjön az \"üresd ki a gépet\" emlékeztető. 0 kikapcsolja.", "label": "Kirakodási emlékeztető késleltetése" }, "notify_unload_repeat": { - "doc": "Újraküldi a kirakodási emlékeztetőt „Kirakodási emlékeztető késleltetése” percenként, amíg a kirakodás megerősítést nem kap, vagy meg nem érinti az „Emlékeztetés leállítása” gombot az értesítésen. Szükség van a kirakodás megerősítésének valamilyen módjára: Ajtóérzékelő entitás, Kirakodást megerősítő entitás vagy az alábbi „Kirakodás kézi megerősítése” beállítás. Az elvetés gomb a Home Assistant kísérőalkalmazás (mobil) értesítésein működik.", + "doc": "Újraküldi a kirakodási emlékeztetőt „Kirakodási emlékeztető késleltetése” percenként, amíg a kirakodást meg nem erősítik, vagy rá nem koppint az „Emlékeztetés leállítása” gombra (mobilalkalmazás). Ajtóérzékelő entitás, Kirakodást megerősítő entitás vagy „Kirakodás kézi megerősítése” szükséges.", "label": "Ismétlés kirakodásig" }, "off_delay": { - "doc": "Várakozási idő a teljesítmény csökkenése után, mielőtt befejezettnek nyilvánítaná a ciklust. Ha az áramellátás ezen az ablakon belül folytatódik, a ciklus zökkenőmentesen folytatódik – ez áthidalja a szüneteket a mosási fázisok között. A mosogatógépek hosszú szárítási fázisokkal rendelkeznek (20-60 percre kikapcsolva), így a kikapcsolási késleltetésnek meg kell haladnia ezt, hogy az egész mosás+szárítás egy ciklusban maradjon.", + "doc": "Mennyi ideig kell a teljesítménynek alacsonyan maradnia, mielőtt a ciklus befejeződik. Ha ezalatt visszatér, a ciklus folytatódik. A mosogatógépek szárításkor 20-60 percig tétlenek lehetnek, ezért ennél hosszabbra állítsa.", "label": "Kikapcsolási késleltetés" }, "pause_cuts_power": { - "doc": "Ha egy ciklus szünetel, kapcsolja ki az alábbi Kapcsoló entitást is. Csak olyan készülékekhez, amelyek dugója biztonságosan levágható a ciklus közepén.", + "doc": "Ciklus szüneteltetésekor az alábbi Kapcsoló entitást is kikapcsolja. Csak olyan készülékekhez, amelyek ciklus közben biztonságosan áramtalaníthatók.", "label": "Szünet megszakítja az áramot (kapcsolón keresztül)" }, "power_off_delay": { - "doc": "Mennyi ideig kell a teljesítménynek a Kikapcsolás teljesítményküszöbe alatt maradnia egy ciklus befejezése után, mielőtt az állapot visszatér a Kikapcsolva értékre. Csak akkor használatos, ha a Kikapcsolás teljesítményküszöbe 0 fölött van. A háttérben futó ütem szerint ellenőrzi, így a tényleges késleltetés felfelé kerekít a következő állapotlejárati lépéshez.", + "doc": "Mennyi ideig kell a teljesítménynek a Kikapcsolás teljesítményküszöbe alatt maradnia a ciklus után, mielőtt az állapot Kikapcsolva lesz. Csak akkor használatos, ha ez a küszöb 0 fölött van. Időszakosan ellenőrzi, így a tényleges várakozás kicsit hosszabb lehet.", "label": "Kikapcsolás késleltetése" }, "power_off_threshold_w": { - "doc": "Opcionális, teljesítményalapú Kikapcsolva állapot érzékelése. Ha az érték 0 fölött van, akkor amint egy ciklus befejeződött és a teljesítmény a Kikapcsolás késleltetése ideig e szint alatt marad, a gépet kikapcsoltnak tekinti, és az állapot visszatér a Kikapcsolva értékre. A letiltáshoz hagyja 0 értéken (alapértelmezett). Állítsa a valódi kikapcsolt szint fölé, de a Leállítási küszöb és a gép befejezett-de-bekapcsolt készenléti fogyasztása alá; ha nincs a Leállítási küszöb alatt, a rendszer figyelmen kívül hagyja. Bekapcsolva a Kikapcsolva állapotba való visszatéréshez a Folyamat visszaállítási késleltetés helyébe lép, így a befejezett gép Befejezett/Tiszta állapotban marad, amíg ténylegesen ki nem kapcsolják.", + "doc": "Ciklus után, ha a teljesítmény a Kikapcsolás késleltetése ideig ez alatt marad, az állapot Kikapcsolva lesz, nem a Folyamat visszaállítási késleltetés állítja be. Állítsa a kikapcsolt fogyasztás fölé, a készenlét és a Leállítási küszöb alá. 0 kikapcsolja (alapértelmezett).", "label": "Kikapcsolás teljesítményküszöbe" }, "power_sensor": { - "doc": "Az érzékelő entitás, amely a készülék élő teljesítményét jelenti wattban (pl. sensor.washer_power). Minden ciklusérzékelés ezen a jelen alapul.", + "doc": "Az érzékelő, amely a készülék élő teljesítményét jelenti wattban (pl. sensor.washer_power). Minden észlelés ezen alapul.", "label": "Teljesítmény érzékelő" }, - "profile_duration_tolerance": { - "doc": "Az egyeztetés során használt profil átlagos időtartama körüli +/- sáv. A 0,25 azt jelenti, hogy a 60 perces profil 45-75 perces ciklusoknak felel meg.", - "label": "Profil időtartam tolerancia" - }, "profile_match_interval": { - "doc": "Milyen gyakran próbálja meg a profilegyeztetést egy futási ciklus során. Az alapértelmezett 300 s (5 perc) egyensúlyban tartja az észlelési sebességet és a CPU-t.", + "doc": "Milyen gyakran egyezteti a futó ciklust a profilokkal. Alapértelmezett 300 mp (5 perc).", "label": "Egyezési intervallum" }, "profile_match_max_duration_ratio": { - "doc": "Maximális ciklushossz a profilhoz képest. Az 1.3 azt jelenti, hogy a ciklusnak a profil időtartamának 130%-a alatt kell lennie, hogy megfeleljen.", + "doc": "Egy program kiesik, ha a ciklus tovább fut, mint a szokásos időtartamának ennyiszerese. 1,8: a programnál 80%-kal hosszabb ciklus már nem egyezik vele.", "label": "Max. időtartam arány" }, "profile_match_min_duration_ratio": { - "doc": "Minimális ciklushossz a profilhoz képest. A 0,9 azt jelenti, hogy egy ciklusnak a profil időtartamának legalább 90%-ának kell lennie, hogy illeszkedjen.", + "doc": "Egy program csak akkor jön számításba, ha a ciklus a szokásos időtartamának ekkora hányadáig futott. 0,10: egy 2 órás program 12 perc után válik jelöltté. A ciklus első 15 percében nem érvényes.", "label": "Min. időtartam arány" }, "profile_match_threshold": { - "doc": "Minimális hasonlósági pontszám (0-1) szükséges a ciklus végén a programazonosító elfogadásához. Emelje fel a hibás azonosítások csökkentése érdekében; csökkentse, ha a gép programjai nem illeszkednek. Az alapértelmezett 0,4 konzervatív kiindulópont.", + "doc": "Az élő egyezési pontszám, amely ahhoz kell, hogy az intelligens leállítás vagy a ránctalanítási lezárás korán befejezze a ciklust. A megjelenített programot nem változtatja. Emelje, ha gyenge egyezések korán zárják a ciklusokat; csökkentse, ha az egyértelmű egyezések kivárják a kikapcsolási késleltetést.", "label": "Egyezési küszöb" }, "profile_unmatch_threshold": { - "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.", + "doc": "Ha az élő egyezési pontszám ez alá esik, az aktuális programtipp törlődik. Tartsa kicsivel az Egyezési küszöb alatt, hogy egy rövid visszaesés ne állítsa vissza.", "label": "Nem egyező küszöb" }, "progress_reset_delay": { - "doc": "A befejezés után tartsa 100%-on az előrehaladást ennyi ideig, hogy a Befejezett felirat látható legyen az irányítópulton, mielőtt visszaállítaná az üresjárati állapotot.", + "doc": "Ciklus után ennyi ideig marad 100%-on az előrehaladás, mielőtt Tétlen állapotra áll vissza.", "label": "Folyamat visszaállítási késleltetés" }, "pump_stuck_duration": { - "doc": "Másodpercekkel a szivattyú folyamatosan működhet, mielőtt esetleg elakadtként jelölik meg (kiváltja a beragadt szivattyú eseményt).", + "doc": "Mennyi ideig működhet a szivattyú megállás nélkül, mielőtt kiváltódik az elakadt szivattyú esemény.", "label": "Elakadt szivattyú időtartama" }, "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.", + "doc": "A feldolgozott leolvasások közötti minimális idő. A hamarabb érkező leolvasásokat kihagyja (a ciklus alatti alacsony leolvasások mindig megmaradnak). Állítsa a konnektor jelentési intervallumára vagy alacsonyabbra; a magasabb érték csak eldobja a leolvasásokat.", "label": "Mintavételezési intervallum" }, "save_debug_traces": { - "doc": "Tárolja a teljes teljesítmény nyomkövetést és a megfelelő hibakeresési adatokat minden ciklushoz. Hasznos hibaelhárításhoz, de növeli a tárhely méretét.", + "doc": "Minden ciklushoz az egyeztető hibakeresési adatait (minden jelölt pontszámait) is eltárolja hibaelhárításhoz. Ciklusonként kb. 20 KB.", "label": "Hibakeresési nyomok mentése" }, "smart_termination_duration_ratio": { - "doc": "Mennyire kell előrehaladnia a ciklusnak az egyeztetett program várható időtartamán belül, mielőtt az intelligens leállítás a teljesítmény csökkenésekor korábban befejezheti azt. A várható időtartam a program átlaga, így azokon a készülékeken, amelyek futásideje sokat ingadozik - mosógépek hideg téli és meleg nyári befolyó vízzel, szárazságérzékelős szárítógépek, terheléstől függő programok - az összes futás nagyjából fele ennél az átlagnál rövidebben fejeződik be, és soha nem kapja meg a gyors befejezést, csak a tartalék időtúllépésen keresztül ér véget, percekkel később. Csökkentse ezt (pl. 0.85) ezeken a gépeken, hogy a korai befejezés így is elinduljon; emelje 1.0 felé az óvatosabb működéshez. Hagyja üresen az alapértelmezett értékhez (0.98, mosogatógépeknél 0.99). Csak korábban fejezhet be egy ciklust, soha nem később, és soha nem indul el bizonytalan vagy alacsony megbízhatóságú egyezésnél.", + "doc": "A program átlagos időtartamának hányada, amelyet a ciklusnak el kell érnie, mielőtt az intelligens leállítás korán befejezheti. Ha a futásidők ingadoznak (vízhőmérséklet, szenzoros szárítás, töltet mérete), csökkentse (pl. 0.85). Hagyja üresen a 0.98-hoz (mosogatógépeknél 0.99).", "label": "Intelligens leállítási arány" }, - "smoothing_window": { - "doc": "Mennyire van simítva a nyers teljesítményjel. Alacsony (2) érzékeny, de zajos; magas (5) kisimítja a tüskéket, de növeli a késést.", - "label": "Simítási ablak" - }, "start_duration_threshold": { - "doc": "A teljesítménynek ennyi ideig az indítási küszöb felett kell maradnia, hogy megerősítse a valódi indítást, és megakadályozza, hogy a másodperc törtrészes be/ki kapcsolói elindítsák a ciklust.", + "doc": "A teljesítménynek ennyi ideig az Indítási küszöb felett kell maradnia a ciklus indulása előtt, így a rövid be/ki villanásokat figyelmen kívül hagyja.", "label": "Indítási időtartam" }, "start_energy_threshold": { - "doc": "A készüléknek energiát (teljesítmény x idő) kell fogyasztania MŰKÖDÉS ELŐTT. Egy rövid, nagy teljesítményű tüskének nagyon alacsony az energiája, és figyelmen kívül hagyják, megakadályozva a téves indításokat.", + "doc": "Az energia, amelyet a készüléknek fel kell használnia, mielőtt a ciklus futónak számít. Egy rövid teljesítménytüske kevés energiát használ, így nem indít ciklust.", "label": "Indítási energia" }, "start_threshold_w": { - "doc": "A teljesítménynek e szint fölé kell emelkednie a ciklus elindításának megerősítéséhez. Túl alacsonyra állítása téves indítást okoz készenléti tápellátásról; túl magas és lassan induló programok (hideg töltés) elmaradnak. A javaslatmotor ezt éppen a gép megfigyelt legalacsonyabb aktív teljesítménye fölé állítja.", + "doc": "A teljesítménynek e fölé kell emelkednie a ciklus indításához. Túl alacsony: a készenléti teljesítmény téves indítást okoz. Túl magas: a lassan induló programok (hideg vízbeeresztés) kimaradnak.", "label": "Indítási küszöb" }, "stop_threshold_w": { - "doc": "A teljesítménynek e szint alá kell csökkennie, mielőtt a kikapcsolási késleltetés visszaszámlálása megkezdődik. Állítsa az indítási küszöb alá – a köztük lévő hézag a hiszterézis sáv, amely megakadályozza a villogást. Ha túl magasra van állítva, az alacsony teljesítményű fázisok (öblítőstop, gyűrődésgátló) hamisan elindítják a végsorozatot.", + "doc": "A teljesítménynek ez alá kell esnie, mielőtt a kikapcsolási késleltetés számolni kezd. Tartsa az Indítási küszöb alatt. Ha túl magas, az alacsony teljesítményű fázisok (öblítőstop, ránctalanítás) tévesen elindítják a végső visszaszámlálást.", "label": "Leállítási küszöb" }, "switch_entity": { - "doc": "Az opcionális kapcsoló szüneteltetéskor kikapcsolható, majd újraindításkor visszakapcsolható, a „Szünet is leállítja az áramot” funkcióval együtt használható.", + "doc": "Opcionális kapcsoló, amely szüneteltetéskor kikapcsol, folytatáskor visszakapcsol, a „Szünet megszakítja az áramot” beállítással.", "label": "Kapcsoló entitás" }, "watchdog_interval": { - "doc": "Milyen gyakran ellenőrzi a háttérfigyelő az elakadt érzékelőket és az eltelt időtúllépéseket. Alapértelmezett 30 s.", + "doc": "Milyen gyakran keresi a háttérellenőrzés az elakadt érzékelőket és az időtúllépéseket. Alapértelmezett 30 mp.", "label": "Felügyelő intervallum" }, "notify_milestone_message": { @@ -1522,135 +1483,90 @@ "label": "Mérföldkő üzenet" }, "notify_milestones": { - "doc": "Vesszővel elválasztott ciklusszámok, amelyek elérésekor egyszeri ünneplő értesítést váltanak ki (pl. 50, 100, 500, 1000). Üresen a mérföldkő-értesítések ki vannak kapcsolva.", + "doc": "Ciklusszámok, amelyek egyszeri ünneplő értesítést váltanak ki, vesszővel elválasztva (pl. 50, 100, 500, 1000). Üresen kikapcsolva.", "label": "Ciklus mérföldkövek" }, "notify_quiet_end_hour": { - "doc": "A ne zavarjanak időszak vége (0-23). A visszatartott értesítéseket ekkor kézbesíti. Hagyja üresen a kikapcsoláshoz.", + "doc": "A ne zavarjanak időszak vége (0-23); a visszatartott értesítések ekkor mennek ki. Üresen kikapcsolva.", "label": "Csendes órák vége" }, "notify_quiet_start_hour": { - "doc": "A ne zavarjanak időszak kezdete (0-23). A csendes órák alatt kiváltódó befejezési, emlékeztető és tiszta ruha értesítéseket visszatartja, és az időszak végén kézbesíti. Hagyja üresen a kikapcsoláshoz. Támogatja az éjfélt átlépő időszakokat (pl. kezdet 22, vég 7).", + "doc": "A ne zavarjanak időszak kezdete (0-23). Az ebben az időszakban keletkező befejezési, emlékeztető és tiszta ruha értesítéseket a végéig visszatartja. Átnyúlhat éjfélen (pl. 22-től 7-ig). Üresen kikapcsolva.", "label": "Csendes órák kezdete" }, "peak_rate_message": { - "doc": "Opcionális egyéni szöveg az indítási értesítéshez fűzött csúcstarifa-tipphez. Sablonváltozók: {device}, {price}. Az üres a beépített alapértelmezést használja.", + "doc": "Egyéni szöveg a csúcstarifa-tipphez. Sablonváltozók: {device}, {price}. Üresen az alapértelmezést használja.", "label": "Csúcstarifa üzenet" }, "peak_rate_threshold": { - "doc": "Amikor egy ciklus elindul, és az aktuális kWh-ár eléri vagy meghaladja ezt az értéket, csúcstarifa-tippet fűz az indítási értesítéshez. A 0 vagy üres kikapcsolja a tippet.", + "doc": "Ha a kWh-ár a ciklus indulásakor eléri vagy meghaladja ezt, csúcstarifa-tippet fűz az indítási értesítéshez. 0 vagy üres kikapcsolja.", "label": "Csúcstarifa-küszöb (kWh-ként)" }, - "dtw_bandwidth": { - "label": "Alakillesztési tűrés", - "doc": "Mennyire torzíthatja időben az illesztő egy ciklust, hogy illeszkedjen egy mentett program alakjához. A magasabb érték nagyobb sebességingadozást tűr; a túl magas rossz programot illeszthet." - }, - "corr_weight": { - "label": "Alak és szint súlya", - "doc": "Egyensúly a görbe alakja és az abszolút teljesítményszint között az illesztés pontozásakor. A magasabb érték az alakot részesíti előnyben; az alacsonyabb a teljesítményszint illesztését." - }, - "duration_weight": { - "label": "Időtartam súlya", - "doc": "Mennyire részesül előnyben az a program, amelynek tipikus időtartama illeszkedik ehhez a ciklushoz." - }, - "energy_weight": { - "label": "Energia súlya", - "doc": "Mennyire részesül előnyben az a program, amelynek tipikus energiafogyasztása illeszkedik ehhez a ciklushoz." - }, "store_brand": { "label": "Készülék márkája", - "doc": "Opcionális. A készülék márkája a közösségi katalógusból kiválasztva. A megfelelő referenciafelvételek megkeresésére és megosztására szolgál. Hagyja üresen, ha nem használja az online funkciókat." + "doc": "Opcionális. A készülék márkája a közösségi katalógusból, referenciafelvételek kereséséhez és megosztásához. Hagyja üresen, ha nem használ online funkciókat." }, "store_model": { "label": "Készülék modellje", - "doc": "Opcionális. A készülék modellje a közösségi katalógusból kiválasztva, miután megadta a márkát. Ha a modellje nem szerepel a listában, hozzáadhatja a katalógushoz." + "doc": "Opcionális. A készülék modellje a közösségi katalógusból (előbb válasszon márkát). Ha nincs a listában, hozzáadhatja." }, "show_contributor": { "doc": "Jelenítse meg a \" által\" jelölést a közösségi készülékeken és referenciaciklusokon." }, - "enable_phase_matching": { - "label": "Fázistudatos hátralévő idő", - "doc": "Bontsa minden futó ciklust fázisokra (fűtés, mosás, centrifugálás), és a hátralévő időt fázisonként ossza be, a klasszikus becsléssel keverve - a ciklus elején a fázisbeosztásra, a vége felé pedig a klasszikus becslésre támaszkodva. Ez a visszaszámlálót ahhoz igazítja, hogy a gépe ténylegesen mennyi ideig fűt és működik, ami leginkább a ciklus első felében figyelhető meg. Ki = csak a klasszikus becslés. Csak a hátralévő idő kijelzését érinti; a program párosítása és a ciklusérzékelés változatlan marad." - }, - "keep_min_score": { - "label": "Min. Egyezési Pontszám", - "doc": "A minimális hasonlósági pontszám, amelyre egy jelöltnek szüksége van az egyezési versenyben való maradáshoz. Az alapértelmezett 0.1 szándékosan megengedő - még a gyenge jelölteket is beengedi, és a következő szakaszokra bízza a legjobb egyezés megtalálását. Növelje a valószínűtlen profilok korábbi kizárásához; csökkentse a kezdeti jelöltállomány bővítéséhez." - }, - "dtw_blend": { - "label": "Torzítás Keverék", - "doc": "Mennyire helyettesíti az időtorzítás (DTW) igazítási pontszám a 2. szakasz alappontszámát. 0 = csak a 2. szakasz pontszáma, 1 = csak a DTW-pontszám, 0.5 (alapértelmezett) = egyenlő keverék. Növelje, hogy jobban támaszkodjon a torzítási igazításra, ha a programoknak hasonló a teljesítményszintjük, de eltérő az időbeli mintájuk." - }, - "dtw_ensemble_w": { - "label": "Torzítás Együttes Mix", - "doc": "Együttes DTW módban (az alapértelmezett), a súly a skálázott L1 és a derivált DTW (DDTW) között. 1.0 = kizárólag skálázott-L1, 0 = kizárólag DDTW, 0.7 = alapértelmezett. A skálázott-L1 változat a teljesítményszinteket hasonlítja össze; a derivált változat arra reagál, ahogyan a teljesítmény változik az idő során. Az együttes mindkettőt kombinálja." - }, - "dtw_ddtw_scale": { - "label": "Derivált Torzítás Skála", - "doc": "Féltelítési távolság a derivált DTW pontozáshoz. A pontszám megfeleződik, ha a DDTW-távolság eléri ezt az értéket. Kisebb = érzékenyebb az alakkülönbségekre. Az alapértelmezett 30 a tipikus háztartási gépek teljesítménynyomaihoz van kalibrálva. Csak az együttes és ddtw DTW-módokat érinti." - }, - "dtw_refine_top_n": { - "label": "Torzítás Finomítás Szám", - "doc": "Hány legjobb 2. szakasz jelöltet pontozza újra a DTW. A DTW számításigényesebb, ezért csak a legjobb N jelöltre alkalmazzák. Alapértelmezett 5. Növelje (7-9-re), ha a helyes profil néha csak a 4. vagy 5. helyen végez a 2. szakasz után - a DTW meg tudja menteni. A csökkentés enyhén felgyorsítja az egyeztetést." - }, - "duration_scale": { - "label": "Időtartam Élesség", - "doc": "A 4. szakasz időtartam-egyezési büntetésének élessége. Ez az a logaritmikus arány, amelynél az egyezési pontszám megfeleződik. Kisebb = szigorúbb: egy időtartam-eltérés jobban büntet. Az alapértelmezett 0.175 nagyjából 18%-os időtartam-toleranciának felel meg a félsúlynál. Párosítsa az Időtartam Súlyával." - }, - "energy_scale": { - "label": "Energia Élesség", - "doc": "A 4. szakasz energia-egyezési büntetésének élessége. Kisebb = szigorúbb: egy energia-eltérés jobban büntet. Az alapértelmezett 0.25 szándékosan megengedőbb az Időtartam Élességénél, mivel az energia változik a terheléssel. Párosítsa az Energia Súlyával." - }, "power_profile_interval_min": { "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." + "doc": "A power_profile attribútum vödörmérete (időrésenkénti átlagos watt), amelyet tervezők, például az EMHASS és a tibber_prices használnak. A kisebb megőrzi a rövid csúcsokat; a nagyobb simítja a görbét. Alapértelmezés: 15 perc. Az észlelé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." + "doc": "Csak iOS. Az élő folyamat csendben frissül, akkumulátorkímélő prioritással, amelyet az iOS összevonhat. Az első frissítés és az indítási/befejezési értesítések továbbra is jeleznek. Kikapcsolva: minden frissítés jelez." }, "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." + "doc": "Csak Android. Koppintás után is megmarad az élő folyamatértesítés. Kikapcsolva: a koppintás bezárja." }, "notify_live_click_action": { "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." + "doc": "Mit nyit meg egy koppintás a készülék értesítésén. Üres: a WashData panel ennél a készüléknél. Adjon meg egy irányítópult-útvonalat (pl. /lovelace/laundry) vagy URL-t, illetve a \"none\" értéket, ha ne legyen koppintási művelet." }, "door_opens_at_end": { "label": "Az ajtó automatikusan kinyílik a végén", - "doc": "Olyan mosogatógépekhez, amelyek a ciklus végén kinyitják az ajtót szárítás céljából (AirDry és hasonlók). Ha ez be van kapcsolva, a futó ciklus közbeni ajtónyitás nem szünetelteti azt véglegesen; ehelyett ha az ajtó az alábbi időtartam erejéig nyitva marad, a WashData a ciklust befejezettnek tekinti. A rövid nyitás (tárgy betöltése) figyelmen kívül marad. Ajtóérzékelő entitás szükséges." + "doc": "Olyan mosogatógépekhez, amelyek a végén szárításhoz kinyitják az ajtót (pl. AirDry). Ha az ajtó az alábbi ideig nyitva marad, a ciklus befejeződik, nem szünetel; a rövid nyitást figyelmen kívül hagyja. Ajtóérzékelő entitás szükséges." }, "door_end_dwell_seconds": { "label": "Ajtó-nyitásvégi várakozási idő", - "doc": "Mennyi ideig kell nyitva maradnia az ajtónak, mielőtt a WashData befejezi a ciklust, amikor az \"Ajtó automatikusan kinyílik a végén\" be van kapcsolva. Elég hosszú ahhoz, hogy figyelmen kívül hagyja a gyors edény betöltését (alapértelmezés: 60 s), elég rövid ahhoz, hogy gyorsan befejezzen, miután a gép kinyitja az ajtót." + "doc": "Bekapcsolt \"Az ajtó automatikusan kinyílik a végén\" beállításnál: mennyi ideig kell nyitva maradnia az ajtónak, mielőtt a ciklus véget ér. Elég hosszú legyen ahhoz, hogy egy edény betöltését figyelmen kívül hagyja (alapértelmezés: 60 mp)." }, "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." + "doc": "Mely ciklusfajták alakítják a programok görbéjét és az egyeztetést. Egy fajta kikapcsolása semmit sem töröl; hasznos, ha nem bízik az importált adatokban. A statisztikák mindig csak ennek a gépnek a ciklusait számolják. Ha mindet kikapcsolja, azt figyelmen kívül hagyja." }, "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." + "doc": "Minden ciklust a futása minden pillanatában érvényes áron számol el, nem a befejezéskori áron. Ár entitás szükséges. Az Előzmények feldolgozása (Diagnosztika) újraszámolja a régebbi ciklusok költségét." }, "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." + "doc": "A program átlagos időtartamának hányada, amelyet a ciklusnak el kell érnie, mielőtt az alacsony teljesítményű dobforgása ránctalanítási utószakasznak számít. Töltetérzékelős szárítógépen csökkentse (pl. 0,75). A mosógépeket hagyja az alapértelmezésen (0,98), különben egy mosás félúton véget érhet." }, "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." + "doc": "A megszakadt indítási kísérletek leolvasásait a ciklus elejéhez adja azoknál a gépeknél, amelyek indulás előtt próbálkoznak (ajtózár, első vízbeeresztés). A 300 mp a legtöbbhöz elég; 0 = ki. A rögzített kezdés korábbra kerül, így a megtanult időtartamok egy ideig elcsúsznak." }, "unload_confirm_entity": { "label": "Kirakodást megerősítő entitás", - "doc": "Opcionális entitás, amelynek aktiválása azt jelenti, hogy az adagot kivették, olyan gépekhez, amelyekre nem szerelhető ajtóérzékelő. Bármilyen típus megfelel: Zigbee vagy NFC gomb, input_button segéd, a gép előtti mozgásérzékelő, jelenet. Minden állapotváltozás megerősítésnek számít, kivéve a kikapcsolást és az elérhetetlenné válást. Ennek beállítása bekapcsolja a kirakodási emlékeztetőt olyan eszközön is, amelyhez nincs Ajtóérzékelő entitás." + "doc": "Opcionális. Bármely entitás, amely megerősíti, hogy az adagot kivették, ajtóérzékelő nélküli gépekhez (gomb, input_button, mozgásérzékelő, jelenet). Minden állapotváltozás számít, kivéve a kikapcsolást és az elérhetetlenné válást. A kirakodási emlékeztetőt is bekapcsolja." }, "unload_track_without_door": { "label": "Kirakodás kézi megerősítése", - "doc": "Futtatja a kirakodási emlékeztetőt olyan eszközön, amelyhez nincs Ajtóérzékelő entitás és nincs Kirakodást megerősítő entitás, a kirakodást pedig saját automatizálásból erősíti meg: nyomja meg a „Megjelölés kirakodottként” gombentitást, vagy hívja meg a ha_washdata.mark_unloaded szolgáltatást. Figyelmen kívül marad, ha be van állítva Ajtóérzékelő entitás, mert az már megadja ezt a jelzést." + "doc": "Ajtóérzékelő vagy megerősítő entitás nélkül is futtatja a kirakodási emlékeztetőt. A kirakodást saját automatizálásból erősítse meg a „Megjelölés kirakodottként” gombbal vagy a ha_washdata.mark_unloaded szolgáltatással. Figyelmen kívül marad, ha Ajtóérzékelő entitás be van állítva." }, "notify_icon_color": { "label": "Értesítés színe", - "doc": "Kiemelőszín ennek a készüléknek az értesítéseihez, így a mosógép, a szárítógép és a mosogatógép egyetlen pillantással megkülönböztethető. Az iOS az ikont, valamint az Élő tevékenység folyamatjelzőjét színezi a Zárolási képernyőn és a Dynamic Islanden. Az Android ugyanazt a színt kapja meg, de hogy mit kezd vele, az a telefontól függ: egyesek az ikont színezik, sok készülék pedig (különösen a Samsung One UI) teljesen figyelmen kívül hagyja. Mindkettő érti a hexa kódot (pl. #03A9F4). Hagyja üresen a platform alapértelmezett beállításához." + "doc": "Kiemelőszín a készülék értesítéseihez, hogy a mosógép, a szárítógép és a mosogatógép könnyen megkülönböztethető legyen. iOS-en kitölti az ikon mögötti kört, és színezi az Élő tevékenység sávját. Sok Android-telefon (főleg a Samsung) figyelmen kívül hagyja. Hexa kódot használjon, pl. #03A9F4. Üres: a platform alapértelmezése." + }, + "share_name": { + "doc": "A fiókneve feltöltőként jelenik meg az Ön által megosztott ciklusoknál. Kikapcsolva: név nélkül osztja meg." } }, "setting_group": { @@ -1678,9 +1594,6 @@ "door_pause": { "label": "Ajtó és szünet" }, - "dtw_refinement": { - "label": "DTW finomítás" - }, "duration_gates": { "label": "Időtartam kapuk" }, @@ -1757,13 +1670,18 @@ "rebuilding": "Újraépítés…", "analyzing": "Elemzés…", "resetting": "Visszaállítás…", - "reverting": "Visszavonás…", "trimming": "Vágás…", "splitting": "Felosztás…", + "stalled": "Elakadt", "deleting": "Törlés…", "imported": "Importált", "preparing": "Előkészítés…", - "unlabelling": "Címke eltávolítása…" + "unlabelling": "Címke eltávolítása…", + "working": "Folyamatban…", + "match_uncertain": "Bizonytalan: {top} vagy {runner}", + "match_uncertain_single": "Bizonytalan: talán {top}", + "match_sure": "~{pct}% biztos", + "match_uncertain_tip": "A WashData még nem döntött a programról. A szám azt mutatja, a korábbi ciklusokban milyen gyakran volt helyes a vezető tipp, {top}, amikor ennyivel vezetett. Csak tájékoztató." }, "tab": { "advanced": "Fejlett", @@ -1795,13 +1713,17 @@ "toast": { "catalog_refreshed": "A közösségi katalógus frissítve", "contrib_device_changed": "Hozzáadva a közösségi boltba, de a kiválasztott készülék megváltozott, amíg az űrlap nyitva volt. Válassza ki a Beállításokban.", + "task_name_required": "Adjon nevet a feladatnak", + "task_added": "Feladat hozzáadva", + "task_save_failed": "Nem sikerült menteni a feladatot: {error}", + "task_removed": "Feladat eltávolítva", + "maint_logged": "Elvégzettként rögzítve: {task}", "access_saved": "A hozzáférés-szabályozás mentve", "all_wiped": "Minden adat törölve", "analysis_complete_none": "Az elemzés kész: nincs új javaslat", "auto_label_complete": "Az automatikus címkézés kész", "automation_deleted": "Automatizálás törölve", "correction_submitted": "Helyesbítés benyújtva", - "cycle_deleted": "Ciklus törölve", "cycle_force_stopped": "Kényszer-leállított ciklus", "cycle_labelled": "Ciklus feliratozott", "cycle_paused": "A ciklus szünetel", @@ -1817,11 +1739,9 @@ "group_deleted": "Csoport törölve", "group_name_required": "A csoportnév megadása kötelező", "group_saved": "Csoport mentve", - "import_successful": "Sikeres importálás; integráció újratöltése", "json_required": "JSON-adatok szükségesek", "legacy_removed": "Az örökölt műveletek eltávolítva", "logs_exported": "A naplók exportálva", - "matching_reverted": "A megfelelő súlyok visszaálltak az alapértelmezett értékekre", "min_2_profiles": "Válasszon ki legalább 2 profilt egy csoporthoz", "ml_training_no_improvement": "Az edzés befejeződött: az alapérték megtartva (nincs javulás)", "models_reverted": "Az eszközön lévő modellek visszaálltak az alapszintre", @@ -1831,8 +1751,8 @@ "pg_preset_loaded": "A(z) \"{name}\" előbeállítás betöltve", "pg_preset_saved": "A(z) \"{name}\" előbeállítás elmentve", "pg_sugg_loaded": "{n} javasolt érték betöltve - futtassa a Homokozót az összehasonlításhoz", - "pg_sugg_ml_loaded": "{n} ML által kalibrált érték betöltve - futtassa a Homokozót az összehasonlításhoz", "phase_name_required": "A fázisnév megadása kötelező", + "phase_reset": "A(z) \"{name}\" fázis visszaállítva a beépített változatra", "phase_updated": "Fázis frissítve", "phases_saved": "Fázisok mentve", "preferences_saved": "A beállítások mentve", @@ -1879,14 +1799,10 @@ "store_name_required": "Adjon meg egy profilnevet", "store_import_failed": "Az importálás sikertelen: {error}", "store_imported": "Importálva ide: {profile}", - "store_program_required": "Adjon meg egy programnevet", "store_no_appliance": "Először adja meg a készülék márkáját és modelljét a Beállításokban.", "store_share_failed": "A megosztás sikertelen: {error}", "store_shared": "Megosztva a közösségi boltban - felülvizsgálatra vár.", "appliance_added": "Készülék hozzáadva - jóváhagyásra vár", - "thanks_confirming": "Köszönjük a megerősítést", - "device_approved": "A közösség jóváhagyta", - "rating_saved": "Minőségértékelés mentve", "brand_added": "Márka hozzáadva - jóváhagyásra vár", "profile_added": "Profil hozzáadva - jóváhagyásra vár", "saved_except_conflicts": "Mentve. Javítsa ki a kijelölt ütközéseket a többi mentéséhez.", @@ -1909,11 +1825,50 @@ "odometer_saved": "Összesített ciklusszám mentve", "odometer_save_failed": "Nem sikerült menteni az összesítést: {error}", "store_pick_profile_share": "Válassza ki a profilt, amelybe megosztja", - "remove_failed": "Az eltávolítás sikertelen: {error}" + "remove_failed": "Az eltávolítás sikertelen: {error}", + "analysis_complete": "Az elemzés kész: {count} javaslat", + "analysis_failed": "Elemzés sikertelen: {error}", + "auto_detect_failed": "Automatikus felismerés sikertelen: {error}", + "auto_label_failed": "Automatikus címkézés sikertelen: {error}", + "could_not_load_cycle": "Nem sikerült betölteni a ciklust: {error}", + "create_failed": "Létrehozás sikertelen: {error}", + "cycles_deleted": "{count} ciklus törölve", + "debug_cleared": "{count} hibakeresési nyom törölve", + "discard_failed": "Elvetés sikertelen: {error}", + "error": "Hiba: {error}", + "export_failed": "Exportálás sikertelen: {error}", + "import_failed": "Importálás sikertelen: {error}", + "invalid_json": "A(z) \"{key}\" nem érvényes JSON - javítsa ki, vagy ürítse ki a mezőt mentés előtt.", + "label_failed": "Címkézés sikertelen: {error}", + "ml_training_promoted": "A tanítás befejeződött: {count} modell előléptetve", + "pause_failed": "Szüneteltetés sikertelen: {error}", + "phase_created": "A(z) \"{name}\" fázis létrehozva", + "phase_deleted": "A(z) \"{name}\" fázis törölve", + "profile_created": "A(z) \"{name}\" profil létrehozva", + "rename_failed": "Átnevezés sikertelen: {error}", + "save_failed": "Mentés sikertelen: {error}", + "split_complete": "Felosztva {count} ciklusra", + "start_failed": "Indítás sikertelen: {error}", + "stop_failed": "Leállítás sikertelen: {error}", + "update_failed": "Frissítés sikertelen: {error}", + "store_download_skipped": "{n} felvétel kihagyva: túl rövid, túl hézagos vagy már megvan az eszközön.", + "store_import_low_quality": "Ez a felvétel túl rövid, túl hézagos vagy valószerűtlen, ezért nem importálható.", + "store_import_duplicate": "Ez a felvétel már megvan az eszközön.", + "device_approved": "A közösség jóváhagyta", + "thanks_confirming": "Köszönjük a megerősítést", + "rating_saved": "Minőségértékelés mentve", + "store_download_partial": "{n} programot nem sikerült letölteni; próbálja újra később.", + "import_no_restore_point": "Importálva, de nem sikerült visszaállítási pontot menteni, ezért ez az importálás nem vonható vissza", + "import_undo_failed": "Az importálás visszavonása sikertelen: {error}", + "import_undone": "Importálás visszavonva: {profiles} profil és {cycles} ciklus visszaállítva", + "auto_label_cancelled": "Az automatikus címkézés megszakítva {count} ciklus címkézése után.", + "auto_label_cancelled_one": "Az automatikus címkézés megszakítva {count} ciklus címkézése után.", + "auto_label_cancelled_other": "Az automatikus címkézés megszakítva {count} ciklus címkézése után.", + "auto_label_done": "Az automatikus címkézés kész: {count} ciklus felcímkézve, {skipped} címkézetlen maradt.", + "auto_label_done_one": "Az automatikus címkézés kész: {count} ciklus felcímkézve, {skipped} címkézetlen maradt.", + "auto_label_done_other": "Az automatikus címkézés kész: {count} ciklus felcímkézve, {skipped} címkézetlen maradt." }, "suggestion": { - "both_agree": "A WashData javasolja", - "calibrated_label": "Kalibrált", "observed_label": "Megfigyelt", "impact": { "auto_label_confidence": { @@ -1928,10 +1883,6 @@ "higher": "Csak jelentős energiát felhasználó ciklusokat zár be", "lower": "Rövidebb vagy alacsony energiájú ciklusokat is bezár" }, - "end_repeat_count": { - "higher": "A befejezési jelet több ismétlés után fogadja el – megbízhatóbb, kissé lassabb", - "lower": "Kevesebb ismétlés után ér véget – gyorsabb érzékelés, több hamis befejezési kockázat" - }, "learning_confidence": { "higher": "Csak nagyon magabiztos egyezésekből tanul – lassabb, de megbízhatóbb", "lower": "Több ciklusból tanul; a modell zajosabb lehet" @@ -1970,40 +1921,25 @@ } }, "reason": { - "auto_label_confidence": "{cycles} olyan automatikus címke 15. percentilis megbízhatósága, amelyet a felhasználó soha nem javított ({p15}).", "completion_min_seconds": "A p05 tiszta ciklus időtartamának fele ({minutes} perc) {cycles} cikluson át; kiszűri a fantomciklusokat.{excl}", - "dead_zone_batch": "Rövidre hagyva (a médián indítási instabilitási ablak {cycles} cikluson át = {s}s), hogy a valódi vég gyorsan észlelhető legyen.", - "dead_zone_from_dip": "A(z) {s}s-nál észlelt korai teljesítménycsökkenés alapján.", - "end_energy_default": "Alapértelmezett ajánlott referenciaérték a ciklus végi zajküszöbhöz.", - "end_energy_false": "p95 téves végű energia ({p95}Wh) {cycles} cikluson át, alsó korláttal a médián ciklusenergia 0.2%-ánál ({floor}Wh).", - "end_energy_no_false": "Nincs téves vég {cycles} cikluson át; alsó korláttal a médián ciklusenergia 0.2%-ánál ({floor}Wh).", - "end_repeat_count": "{false}/{total} tiszta ciklusban ({pct}%) volt >60s belső szünet, amely folytatódott; {val} végmegerősítés szükséges.", - "learning_confidence": "{cycles} felhasználó által címkézett ciklus p05 megbízhatósága ({p05}); ez alatt ellenőrzés kérése.", "match_interval": "A megfigyelt frissítési ütem alapján (médián={median}s) * 10.", "max_duration_ratio": "A címkézett ciklusok időtartama alapján (p95={p95}).", - "min_active": "Az utolsó ciklusban megfigyelt legkisebb aktív teljesítmény ({min}W) alapján.", - "min_duration_ratio": "A lehető legalacsonyabbra állítva, hogy a program a ciklus elején felismerhető legyen; a megbízhatósági és bizonytalansági küszöbök megakadályozzák a korai megerősítést.", "min_off_gap": "{gaps} megfigyelt ciklusközi szünet alapján (p05={p05}s). Készülék alsó korlátja: {floor}s.", "min_off_gap_bridge": "Úgy méretezve, hogy áthidalja a cikluson belüli leghosszabb csendes szakaszt: {span}s + 60s ráhagyás, {spans} áthidalt szünetből {cycles} tiszta cikluson át ({ceiling}s alatt marad, ami a két egymást követő indítás között hagyott legrövidebb szüneted).", "min_power": "A(z) {cycles} tiszta ciklus p05 legkisebb aktív teljesítményének ({p05}W) 40%-a, hogy a kikapcsolási küszöb a valós fogyasztás alatt maradjon.{excl}", - "ml_auto_label": "A legalacsonyabb megbízhatóság, amelyet a minőségi modell még tisztának minősített (a(z) {cycles} tiszta ciklus p10 értéke = {p10}).", - "ml_end_repeat": "{false}/{total} ciklusban ({pct}%) volt olyan szünet, amelyet a végérzékelő >50%-ra értékelt; {val} végmegerősítés szükséges.", - "ml_off_delay": "Végérzékelővel megerősített szünetek: p95 {p95}s + 60s ráhagyás, {pauses} modell által ellenőrzött szünetből {cycles} cikluson át (alsó korlát {floor}s).", "no_update_timeout": "A megfigyelt frissítési ütem alapján (p95={p95}s) * 20 (min. 60s).", "off_delay_cadence": "A megfigyelt frissítési ütem alapján (p95={p95}s) * 5", "off_delay_device_floor": "A(z) {device} eszközre jellemző biztonságos minimum használva ({floor}s).", "off_delay_generic_floor": "Általános biztonságos minimum használva ({floor}s).", "off_delay_pauses": "Úgy méretezve, hogy túlélje a valódi szüneteket: p95 cikluson belüli szünet {p95}s + 60s ráhagyás, {pauses} szünetből {cycles} tiszta cikluson át (alsó korlát {floor}s).", - "profile_match_threshold": "{cycles} helyes automatikus címke p10 megbízhatósága ({p10}); biztonságos élő megerősítési alsó korlát.", - "sampling_interval": "A(z) {cycles} tiszta cikluson át megfigyelt médián frissítési intervallum ({si}s).{excl}", - "smoothing_window": "Úgy méretezve, hogy ~30s-nyi mérést simítson {si}s mintavételezésnél ({samples} minta).", - "start_duration": "Rövidre hagyva (~egy {si}s mintavételi intervallum), hogy az észlelés a lehető legkorábban induljon, miközben az egyetlen mintás kiugrásokat figyelmen kívül hagyja.", - "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}).", + "standby_floor": "Minden legutóbbi ciklus még {idle} W-ot fogyasztott, amikor befejezte, így az ez alatti küszöb soha nem látja, ahogy a készülék kikapcsol. Az érték ez felett marad; a rögzített {cycles} tiszta ciklus egyike sem szünetelt alatta {pause} másodpercnél tovább.", + "thr_batch": "Épp a legkisebb aktív teljesítmény (p05) fölött {cycles} cikluson át ({p05}W), így az indítások korán észlelhetők, a leállítási küszöb pedig a legkisebb üzemi teljesítmény alatt marad.", "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." + "match_interval_floored": "A legrövidebb program ({shortest} mp) {cap} mp-re korlátozná ezt, ami a(z) {minimum} mp-es minimum alatt van, ezért a minimum érvényes: {persistence} egymást követő egyezés {budget} mp, ami a program {pct}%-ánál több. Az egyeztetés csak leolvasás érkezésekor fut (medián={median} mp), így ez semmibe sem kerül.", + "end_energy_floor": "Megemelve arra az alsó határra, amelyet a Leállítási küszöb ({stop} W) a Kikapcsolási késleltetés ({off_delay} s) alatt megkövetel; ez alatt a ciklus csak tartalék úton érhet véget.", + "max_duration_ratio_default": "Vissza az alapértékre: egy alacsonyabb felső határ elveti a helyes programot, ha egy futás a szokásosnál tovább tart.", + "min_duration_ratio_default": "Vissza az alapértékre: egy magasabb alsó határ miatt a futó ciklus csak akkor ismerhető fel, amikor már jócskán előrehaladt." }, "exclusions": { "summary": "{total} tévesen észlelt ciklus kizárva: {parts}.", @@ -2021,7 +1957,8 @@ "no_power": "nincs teljesítmény", "no_active_power": "nincs aktív teljesítmény" } - } + }, + "now_value": "(most {v})" }, "trend": { "down": "Lefelé trend", @@ -2029,6 +1966,10 @@ }, "maint": { "bearing_service": "Csapágy szerviz", + "salt": "Só utántöltése", + "rinse_aid": "Öblítőszer utántöltése", + "lint_filter": "Szöszszűrő tisztítása", + "condenser_clean": "Kondenzátor tisztítása", "descale": "Vízkőmentesítés", "drum_clean": "Dob tisztítása", "filter_clean": "Szűrő tisztítása", @@ -2036,34 +1977,22 @@ }, "pg_desc": { "dishwasher_end_spike_quiet_release": "Mosogatógép: csendes másodpercek a várható időtartam után, mielőtt a ciklus végi leeresztésre való várakozás feloldódik", - "smart_termination_duration_ratio": "Az egyeztetett program várható időtartamának hányada, amelyet a ciklusnak el kell érnie, mielőtt az intelligens leállítás korábban befejezheti; csökkentse a terheléstől vagy hőmérséklettől függő gépeknél", + "smart_termination_duration_ratio": "A program átlagos időtartamának hányada, amelyet a ciklusnak el kell érnie, mielőtt az intelligens leállítás korán befejezheti; csökkentse, ha a futásidők ingadoznak", "anti_wrinkle_enabled": "Nyelje el a fő fázis utáni szárítópulzusokat, ahelyett hogy új ciklusként olvasná őket", - "anti_wrinkle_exit_power": "A ránctalanítás aktívan tartásához a teljesítménynek az impulzusok között ez alá kell csökkennie", + "anti_wrinkle_exit_power": "Csendes szint a dobimpulzusok között; csak a Leállítási küszöb felett hat", "anti_wrinkle_idle_timeout": "Két szárítópulzus között engedélyezett csendes idő, mielőtt a ránctalanítás véget ér", "anti_wrinkle_max_duration": "Az ennél hosszabb impulzus befejezi a ránctalanítást és új ciklust nyit", "anti_wrinkle_max_power": "Az ennél magasabb impulzus befejezi a ránctalanítást és új ciklust nyit", "completion_min_seconds": "A legrövidebb futás, ami valódi ciklusnak számít", - "end_repeat_count": "Egymást követő alacsony leolvasások a befejezés előtt", "interrupted_min_seconds": "A rövid ciklusokat megszakítottként jelöli", "min_off_gap": "Két ciklus elválasztásához szükséges rés", "off_delay": "Alacsony teljesítmény másodpercei a ciklus vége előtt", "start_duration_threshold": "A küszöb feletti másodpercek az indulás megerősítéséhez", "start_threshold_w": "Minimális wattérték az indulás elismeréséhez", "stop_threshold_w": "Ez alatt a gép kikapcsoltnak számít", - "dtw_bandwidth": "3. szakasz: Sakoe-Chiba torzítási sáv (0 = DTW kikapcsolva; alapértelmezett 0.2 = a ciklushossz 20%-a)", - "corr_weight": "2. szakasz: egyensúly a görbe alakja (korreláció) és a teljesítményszint (MAE) között; alapértelmezett 0.45", - "duration_weight": "4. szakasz: mennyire befolyásolja a futási idő egyezése a végső pontszámot (alapértelmezett 0.22)", - "energy_weight": "4. szakasz: mennyire befolyásolja az energia-egyezés a végső pontszámot (alapértelmezett 0.22)", - "profile_match_min_duration_ratio": "1. szakasz: minimális ciklushossz a profil időtartamának törtjeként; alapértelmezett 0.1 (10%)", - "profile_match_max_duration_ratio": "1. szakasz: maximális ciklushossz a profil időtartamának törtjeként; alapértelmezett 1.8 (180%)", - "keep_min_score": "2. szakasz: minimális pontszám a versenyben maradáshoz; az alapértelmezett 0.1 gyenge egyezéseket is beenged, a következő szakaszok választják ki a legjobbat", - "dtw_blend": "3. szakasz: 0 = csak alappontszám, 1 = csak DTW-pontszám, 0.5 = egyenlő keverék (alapértelmezett)", - "dtw_ensemble_w": "3. szakasz (együttes mód): súly a skálázott-L1 és derivált DTW között; alapértelmezett 0.7 a szintfüggőt részesíti előnyben", - "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)", - "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", + "profile_match_min_duration_ratio": "Minimális ciklushossz a profil időtartamának hányadaként; alapértelmezett 0.1 (10%)", + "profile_match_max_duration_ratio": "Maximális ciklushossz a profil időtartamának hányadaként; alapértelmezett 1.8 (180%)", + "anti_crease_finalize_ratio": "A program átlagos időtartamának hányada, amelyet a ciklusnak el kell érnie, mielőtt a ránctalanítási utószakasz lezárhatja; szárítógépen csökkentse, mosógépen 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": { @@ -2071,12 +2000,14 @@ "status_tip": "A ciklus kimenetele: Befejezve (természetes vég), Megszakítva (hirtelen teljesítményesés), Kényszerítve leállítva (kézi) vagy Áttekintendő (függőben lévő visszajelzés).", "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. 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.", + "energy_tip": "Összes felhasznált energia (kWh), a teljesítményből idő szerint számítva.", + "cost_tip": "A ciklus energiaköltsége, befejezéskor rögzítve. Bekapcsolt Időben súlyozott költség esetén minden pillanat a saját árán számít; különben a végén érvényes áron. Állíts be á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.", - "cost_dynamic_tip": "Időben súlyozott: a ciklus alatt érvényes áron elszámolva (tényleges {price} {cur}/kWh)." + "cost_dynamic_tip": "Időben súlyozott: a ciklus alatt érvényes áron elszámolva (tényleges {price} {cur}/kWh).", + "dbg_conf": "Biz.", + "dbg_mae": "MAE", + "dbg_corr": "Korr." }, "stat": { "avg": "átlag", @@ -2100,7 +2031,10 @@ "notify_finish": "Egy befejezési értesítés kerülne elküldésre.", "notify_milestone": "Egy mérföldkő-értesítés kerülne elküldésre.", "notify_held": "Egy értesítés visszatartásra került a csendes órák miatt.", - "finished": "A ciklus elérte a végállapotot és befejeződött." + "finished": "A ciklus elérte a végállapotot és befejeződött.", + "match_reverted": "A program visszakerült az észlelés állapotba: pontszáma jóval a saját csúcsértéke alá esett.", + "verified_pause": "Az élő igazítás megerősített (vagy feloldott) egy szünetet a programon belül. Amíg ez érvényben van, a ciklus nem ér véget.", + "stalled": "A ciklus tovább maradt készenléti fogyasztáson, mint ameddig ez a program valaha szünetel, pedig még volt hátra munka. Szüneteltetve (Elakadt) állapotként jelenik meg; a ciklus nyitva marad." }, "store": { "search_ph2": "Keresés márka vagy típus szerint…", @@ -2109,11 +2043,7 @@ "programs_count": "Programok: {n}", "browse": "Böngészés", "device": "Készülék", - "favorites": "Kedvencek", - "search_ph": "Keresés márka vagy modell szerint…", "no_results": "Nem található megfelelő készülék. Próbáljon másképp keresni.", - "n_programs": "{n} program", - "n_cycles": "{n} ciklus", "no_programs": "Ehhez a készülékhez még nincsenek megosztott programok.", "no_cycles": "Ehhez a programhoz még nincsenek megosztott referenciaciklusok.", "anon": "névtelen", @@ -2125,19 +2055,21 @@ "merge_target": "Egyesítés a profilba", "description": "Leírás (opcionális)", "contributed_by": "szerző: {name}", + "website": "Bolt weboldala ↗", + "website_tip": "A közösségi bolt weboldalának megnyitása", + "favorites": "Kedvencek", "rating_summary": "★ {avg} ({n})", "no_ratings": "Még nincsenek értékelések", - "website": "Bolt weboldala ↗", - "website_tip": "A közösségi bolt weboldalának megnyitása" + "unreachable": "Nem sikerült elérni a közösségi boltot. Ellenőrizze a kapcsolatot, és próbálja újra." }, "settings": { "gear": { - "title": "Beállítások" + "title": "Preferenciák" } }, "link": { - "manual": "Kézikönyv ↗", - "add_appliance": "Készülék hozzáadása" + "maintenance_supporter": "Maintenance Supporter a GitHubon ↗", + "manual": "Kézikönyv ↗" }, "tip": { "add_brand": "A márka nem szerepel a listában? Adja hozzá a közösségi katalógushoz", @@ -2171,7 +2103,13 @@ "rebuild": { "envelopes": "Burkológörbék újraépítése" }, - "cancelling": "Megszakítás..." + "cancelling": "Megszakítás...", + "auto_label": { + "running": "Ciklusok automatikus címkézése" + }, + "store_download": { + "running": "Letöltés a boltból" + } }, "setup": { "hdr": { @@ -2180,26 +2118,24 @@ }, "phase0": { "washer": "A WashData már észleli a ciklusait. Rögzítse az első ciklust a programnevek és az időbecslések engedélyezéséhez.", - "dishwasher": "A WashData figyel. A mosogatógépek összetett ciklusokkal rendelkeznek – az első ciklus rögzítése erősen ajánlott. Ha egy észlelt ciklus túl hosszú ideig tart, a profilként való mentés előtt a ciklusszerkesztővel kurtítsa le.", + "dishwasher": "A WashData figyel. A mosogatógépek ciklusai összetettek, ezért rögzítse az első ciklust. Ha egy észlelt ciklus túl hosszú, vágja le a ciklusszerkesztőben, mielőtt profilként menti.", "generic": "A WashData figyel. Rögzítsen vagy jelöljön meg egy észlelt ciklust a profilok létrehozásának megkezdéséhez." }, "phase1a": { - "labelled": "Jó kezdés – az első programja el van mentve. A legtisztább adatokhoz érdemes a következő ciklust a rögzítő widgettel felvenni." + "labelled": "Jó kezdés: az első programja el van mentve. A legtisztább adatokért a következő ciklust a rögzítővel vegye fel." }, "phase1b": { - "recorded": "A felvétele {profile_name} névvel lett elmentve. Most rögzítse vagy jelölje meg a többi gyakran használt programját a lefedettség növeléséhez." + "recorded": "A felvétele {profile_name} néven mentve. Most rögzítse vagy címkézze fel a többi gyakori programját." }, "phase1c": { - "verify": "Önnek {count} program van a közösségtől. Futtasson egy ciklust, hogy ellenőrizze, a WashData helyesen ismeri-e fel – az egyeztetés javul, ahogy az eszköz saját előzményeket gyűjt." + "verify": "{count} közösségi programja van. Futtasson egy ciklust, és ellenőrizze, hogy a WashData felismeri-e; az egyeztetés javul, ahogy az eszköz saját előzményeket gyűjt." }, "phase2": { - "cluster": "A WashData {count} ciklust látott, amelyek nem felelnek meg egyetlen mentett programnak sem – hasonlítanak egymásra. Szeretne létrehozni nekik egy új profilt?", + "cluster": "A WashData {count} hasonló ciklust látott, amelyek egyetlen mentett programmal sem egyeznek. Létrehoz nekik egy új profilt?", "unmatched": "Az utolsó ciklus nem felelt meg egyetlen mentett programnak sem. Ez egy új program?" }, "phase3": { - "suggestions": "A WashData a ciklustörténete alapján beállítási javaslatokkal rendelkezik – tekintse át ezeket az észlelési pontosság javítása érdekében.", - "groups": "Néhány profilja ugyanolyannak tűnik, mint ugyanaz a program különböző hőmérsékleteken. Rendezze csoportba a jobb egyeztetés érdekében.", - "phases": "Adjon programfázisokat a(z) {profile_name} profilhoz a pontosabb hátralévő idő becslések érdekében." + "suggestions": "A WashData beállítási javaslatokat talált a ciklusai alapján. Tekintse át őket az észlelés javításához." }, "phase4": { "healthy": "Ez az eszköz teljesen be van állítva ({profile_count} profil)." @@ -2212,12 +2148,58 @@ "create_from_cluster": "Profil létrehozása ezekből a ciklusokból", "create_profile": "Profil létrehozása hozzá", "review_suggestions": "Javaslatok áttekintése", - "organise_profiles": "Profilok rendezése", - "configure_phases": "Fázisok konfigurálása", "skip_step": "Lépés kihagyása", "skip_forever": "Ne mutasd újra", "hide_guidance": "Útmutatás elrejtése", "show_guidance": "Útmutatás megjelenítése" } + }, + "access": { + "edit": "Szerkesztés", + "full": "Teljes", + "inherit": "Öröklés", + "none": "Nincs (rejtett)", + "read": "Olvasás" + }, + "pg_label_reason": { + "no_winner": "Egyik program sem illett", + "below_floor": "A Tanulási megbízhatóság alatt", + "ambiguous": "Bizonytalannak jelölve", + "margin": "Túl közel a második helyezetthez", + "unknown_profile": "A program már nem létezik", + "not_evaluated": "Nincs kiértékelve" + }, + "diagram": { + "accum": "halm.", + "auto": "auto", + "below_off": "alatta = ki", + "confirmed": "megerősítve", + "energy_counts": "az energia számít", + "feedback": "visszajelzés", + "gap_below_min": "min. alatti rés = egy ciklus", + "heat_phase": "fűtési fázis", + "held_at_100": "100%-on tartva", + "ignored": "figyelmen kívül", + "kept": "megtartva", + "max": "max.", + "min": "min.", + "minus_tol": "-tol.", + "no_match": "nincs egyezés", + "no_updates": "nincs frissítés", + "off_delay_wait": "kikapcs. késleltetés", + "off_gap": "kikapcs. rés", + "plus_tol": "+tol.", + "profile": "profil", + "reading_interval": "jellemző mérési intervallum", + "sensor_offline": "érzékelő offline: kényszerleállítás", + "si": "MI", + "spike_ignored": "tüske figyelmen kívül", + "spike_ignored_short": "tüske: kihagyva", + "start": "indítás", + "stop": "leállás", + "tail_energy_resets": "záró energia a küszöb felett: az időzítő újraindul", + "thr": "küszöb", + "too_short": "túl rövid", + "tumble_kept": "dobimpulzusok megtartva" } } diff --git a/custom_components/ha_washdata/translations/panel/is.json b/custom_components/ha_washdata/translations/panel/is.json index 46e8cdce..a9198962 100644 --- a/custom_components/ha_washdata/translations/panel/is.json +++ b/custom_components/ha_washdata/translations/panel/is.json @@ -3,6 +3,7 @@ "min": "mín.", "entity_not_found": "Eining fannst ekki", "running_long": "Keyrir lengur", + "stalled": "Stöðvuð", "remaining": "eftir", "progress": "framvinda", "show_time_remaining": "Sýna tíma sem eftir er", @@ -48,16 +49,16 @@ } }, "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_tip": "Undir lokin verður þetta kerfi kyrrt í um {mins} mín. og dregur svo um {watts} W í {secs} s. Sést í {seen} af {measured} mældum lotum.", "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", + "artifact_tip": "{n} frávik fundust (t.d. hurð opnuð í miðri lotu) - opnaðu til að sjá þau á línuritinu", + "artifact_tip_one": "{n} frávik fannst (t.d. hurð opnuð í miðri lotu) - opnaðu til að sjá frávik á línuritinu", + "artifact_tip_other": "{n} frávik fundust (t.d. hurð opnuð í miðri lotu) - opnaðu til að sjá þau á línuritinu", "auto": "(sjálfvirkt greint)", "built_in_tag": "innbyggður", "declining": "↘ minnkandi", "energy_low": "Minni orka en venjulega", "energy_spike": "Meiri orka en venjulega", - "fair_fit": "viðunandi gæði", - "fair_fit_tip": "Í meðallagi samræmi við samsvörun - sumar lotur sem eru úthlutaðar þessum prófíl hafa lægri öryggisstig. Merktu fleiri lotur eða skráðu sniðið aftur til að bæta nákvæmni.", "feedback_requested": "Óskað er eftir umsögn", "golden_cycle": "Skráð viðmiðunarlota", "duration_outlier": "{n} lota/lotur með óvenjulega lengd", @@ -66,27 +67,38 @@ "needs_review": "Þarfnast endurskoðunar", "overrun": "Keyrði lengur en venjulega", "poor_fit": "léleg gæði", - "poor_fit_tip": "Ósamkvæmur samsvörunarferill – íhugaðu að endurbyggja þennan prófíl", - "restart_gap_tip": "{n} HA endurræsingarbil: aflsporið hefur gat", + "restart_gap_tip": "{n} HA endurræsingarbil: aflsporið hefur göt", + "restart_gap_tip_one": "{n} HA endurræsingarbil: aflsporið hefur gat", + "restart_gap_tip_other": "{n} HA endurræsingarbil: aflsporið hefur göt", "reviewed": "Farið yfir", "steady": "→ stöðugt", "underrun": "Kláraðist hraðar en venjulega", - "using_defaults": "Notar sjálfgefnar stillingar", - "using_tuned": "Notaðu stilltar lóðir", "overrun_ratio": "({x}x áætlað)", "underrun_ratio": "({pct}% af áætluðu)", - "awaiting_tip": "Bíður samþykkis samfélagsins", - "awaiting_n": "Bíður samþykkis · {n} staðfestingar", - "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.", - "armed": "(gildir fyrir næstu lotu)" + "armed": "(gildir fyrir næstu lotu)", + "awaiting_tip": "Bíður samþykkis samfélagsins", + "awaiting_n": "Bíður samþykkis · {n} staðfestingar", + "awaiting": "Bíður samþykkis", + "approved": "Samþykkt", + "matcher_unused": "Engin samsvörun við loturnar þínar enn", + "matcher_used": "Samsvaraði {n} af lotunum þínum", + "matcher_used_one": "Samsvaraði {n} af lotunum þínum", + "matcher_used_other": "Samsvaraði {n} af lotunum þínum", + "matcher_used_tip": "Lotur á þessu tæki sem WashData samsvaraði sjálft við þetta innflutta kerfi. Innflutningur sem aldrei samsvarar passar hugsanlega ekki við tækið þitt; þú getur fjarlægt hann. Talið á staðnum; ekkert er sent í samfélagsverslunina." }, "btn": { + "log_done_task": "Skrá sem lokið: {task}", + "add_task": "Bæta við verki", + "pg_preset_delete": "Eyða forstillingu", + "pg_preset_load": "Hlaða forstillingu", + "pg_preset_load_tip": "Skipta gildunum á tilraunasvæðinu út fyrir þessa forstillingu (virku stillingarnar haldast óbreyttar)", + "pg_preset_save": "Vista sem forstillingu", + "pg_preset_save_tip": "Vista öll gildin hér að neðan sem nefnda forstillingu fyrir þetta tæki", "reset_zoom": "Endurstilla aðdrátt", "set_brand_model": "Stilla vörumerki og gerð", "refresh_catalog": "Endurnýja skrána", @@ -97,12 +109,11 @@ "add_timer": "+ Bæta við tímamæli", "apply_all": "Notaðu allt", "apply_label": "Notaðu merki", - "apply_set_b": "Beita stillingum B", "apply_split": "Notaðu Split", "apply_trim": "Beita klippingu", "auto_detect_split": "Sjálfvirk skynjun", "auto_label_cycles": "Sjálfvirk merkingarlotur", - "auto_label_cycles_tip": "Úthlutaðu sjálfkrafa prófílnöfnum á ómerktar lotur þar sem samsvörunaröryggi nær þröskuldinum", + "auto_label_cycles_tip": "Gefðu ómerktum lotum heiti þegar samsvörunaröryggi þeirra nær þröskuldinum.", "cancel": "Hætta við", "clear_debug": "Hreinsaðu villuleitargögn", "clear_splits": "Hreinsa", @@ -112,8 +123,6 @@ "convert_to_automation": "Umbreyta í sjálfvirkni", "correct": "Leiðrétta…", "create": "Búa til", - "create_from_cluster": "Búðu til prófíl úr {count} lotum", - "create_profile": "+ Búðu til prófíl", "delete": "Eyða", "delete_group": "Eyða hópi", "delete_group_tip": "Eyddu aðeins þessum hópi - meðlimasniðin eru geymd", @@ -124,59 +133,45 @@ "done": "Búið", "edit": "Breyta", "export": "Útflutningur", - "export_json": "Flytja út í JSON", "force_stop": "Þvingaðu stöðvun", "force_stop_tip": "Endaðu núverandi lotu strax og merktu hana sem þvingað stöðvaða", "from_template": "Frá sniðmáti ▾", "ignore": "Hunsa", "import_json": "Flytja inn frá JSON", - "import_overwrite": "Flytja inn (skrifar yfir gögn)", "inspect": "Skoðaðu", "label": "Merki", - "load": "Hlaða", "load_more": "Hlaða meira", "manage": "Stjórna", "merge": "Sameina", "new_automation": "+ Ný sjálfvirkni", "new_group": "+ Nýr hópur", - "new_group_tip": "Flokkaðu næstum eins snið (söm lögun/lengd, mismunandi hitastig eða snúningur) svo samsvörun velji áreiðanlega á milli þeirra", + "new_group_tip": "Flokkaðu næstum eins snið (sama lögun, annað hitastig eða vinding) svo samsvörunin geti greint þau í sundur.", "new_phase": "+ Nýr áfangi", "new_profile": "+ Nýtt snið", "new_profile_tip": "Búðu til nýjan prófíl fyrir kerfi úr núverandi merktri lotu eða upptöku", - "on_cycle_finished": "Á hringrás lokið", - "on_cycle_started": "Á hringrás byrjaði", + "on_cycle_finished": "Þegar lotu lýkur", + "on_cycle_started": "Þegar lota hefst", "pause_cycle": "Gera hlé", "pause_cycle_tip": "Gerðu hlé á gengandi lotu – tækið mun halda áfram þar sem það stoppaði", - "pg_apply_device": "Beita á tæki", - "pg_autofill": "Fylla sjálfkrafa", - "pg_load_calibrated": "Hlaða kvarðaðar (ML) ({n})", - "pg_load_calibrated_tip": "Flytur ML-kvarðaðar stillingar sem hnekki á tilraunasvæðið", "pg_load_live": "Sækja virkar stillingar", "pg_load_live_tip": "Lesa núverandi stillingar samþættingarinnar aftur og henda öllum breytingum á tilraunasvæðinu", "pg_load_suggested": "Hlaða tillögur ({n})", "pg_load_suggested_tip": "Flytur núverandi tillögur sjálfvirkrar stillingar sem hnekki á tilraunasvæðið", - "pg_preset_delete": "Eyða forstillingu", - "pg_preset_load": "Hlaða forstillingu", - "pg_preset_load_tip": "Skipta gildunum á tilraunasvæðinu út fyrir þessa forstillingu (virku stillingarnar haldast óbreyttar)", - "pg_preset_save": "Vista sem forstillingu", - "pg_preset_save_tip": "Vista öll gildin hér að neðan sem nefnda forstillingu fyrir þetta tæki", "pg_publish_all": "Vista {n} í samþættinguna", "pg_publish_one": "Vista {label} í samþættinguna", - "play": "Spila", "process": "Vinna", "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", "rebuild_envelope": "Endurbyggja umslag", - "rebuild_tip": "Endurreiknaðu vænt orkuumslag (min/max bil) fyrir öll snið úr merktu lotum þeirra – keyrðu eftir að hafa merkt nýjar lotur eða leiðrétt gamlar", + "rebuild_tip": "Endurbyggðu vænt aflbil hvers sniðs úr merktum lotum þess. Keyrðu eftir að lotur eru merktar eða leiðréttar.", "rec_start_tip": "Byrjaðu að taka upp aflferil tækisins – byrjaðu rétt áður en lota er keyrð", "rec_stop": "Hættu", "rec_stop_tip": "Stöðvaðu upptöku og geymdu fangna ferilinn til skoðunar", "record": "Byrjaðu upptöku", "refresh": "Endurnýja", "refresh_diag_tip": "Endurhlaða geymslutölfræði", - "refresh_logs_tip": "Endurhlaða nýjustu annálsfærslur", "refresh_settings_tip": "Endurhlaða stillingar frá netþjóni", "relabel": "Endurmerkja ({count})", "remove": "Fjarlægja", @@ -184,7 +179,6 @@ "reset": "Endurstilla", "reset_ml_models_tip": "Fleygðu fínstilltu gerðum og farðu aftur í innbyggðu. WashData getur lært þau aftur síðar.", "reset_to_builtin": "Endurstilla á innbyggðar gerðir", - "reset_to_defaults": "Endurstilla í sjálfgefna stillingar", "resume_cycle": "Halda áfram", "resume_cycle_tip": "Haltu áfram lotu í bið", "revert_settings": "Afturkalla breytingar", @@ -197,8 +191,6 @@ "run_analysis": "🔍 Keyra tillögugreiningu", "run_analysis_tip": "Greindu upptökur þínar núna og endurnýjaðu stillingartillögur", "run_auto_label": "Keyra Auto-Label", - "run_both": "Keyra A á móti B", - "run_simulation": "Keyra hermun", "save": "Vista", "save_access_control": "Vista aðgangsstýringu", "save_group": "Vista hóp", @@ -210,10 +202,8 @@ "save_settings": "Vista stillingar", "select": "Veldu", "select_all": "Velja allt", - "select_last20": "Síðustu 20", "select_none": "Hreinsa", "show_only": "Sýna aðeins", - "skip_setup": "Sleppa uppsetningu", "split": "Skipta", "stop": "Stöðva", "submit_correction": "Sendu leiðréttingu", @@ -222,36 +212,32 @@ "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.", + "unlabel_selected_tip": "Fjarlægðu merkinguna af völdum lotum. Þær verða áfram í sögunni þinni en móta ekki lengur þetta snið.", "use": "Notaðu", "wipe_all": "Þurrkaðu öll gögn", + "pg_rerun": "Uppfæra hermun", "delete_selected": "Eyða völdum ({n})", - "group_suggest": "Hópur {n}: {members}", "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", "import": "Flytja inn", "disconnect": "Aftengja", "connect_github": "Tengja GitHub", - "save_appliance": "Vista tæki", "share_to_store": "Deila í verslun", "share": "Deila", - "confirm_appliance": "Staðfesta þetta tæki", - "confirmed": "Staðfest", "browse_community_setups": "Skoða samfélagsuppsetningar", "download_device": "Hlaða niður þessari uppsetningu", "share_device": "⬆ Deila þessu tæki", - "share_device_tip": "Deila þessu tæki og uptökum viðmiðunarlota þess í samfélagsverslunina svo aðrir með sömu vél geti tekið þær yfir", + "share_device_tip": "Deildu þessu tæki og viðmiðunarlotum þess í samfélagsverslunina svo eigendur sömu vélar geti tekið þær upp.", "share_n": "Deila {n} lotu/lotum", "export_selected": "Flytja út (velja gögn)", "export_all": "Flytja allt út hratt", - "import_raw": "Ítarlegt: skipta öllu út úr JSON", "download_export": "Sækja útflutning", "analyze_import": "Greina skrá", "import_selected": "Flytja inn valið", "back": "Til baka", "mute_suggestion": "Hætta að stinga upp á þessari stillingu", "reset_muted": "Endurstilla þaggaðar tillögur", + "reset_phase_tip": "Afturkalla breytinguna þína og endurheimta innbyggða áfangann", "import_power_history": "Flytja inn aflsögu", "hist_read_recorder": "Lesa úr Home Assistant", "hist_scan": "Leita að lotum", @@ -260,7 +246,12 @@ "save_total_cycles": "Vista samtölu", "clear": "Hreinsa", "add_brand": "Bæta við vörumerki", - "add_appliance": "Bæta við tæki" + "add_appliance": "Bæta við tæki", + "retry": "Reyna aftur", + "confirm_appliance": "Staðfesta þetta tæki", + "confirmed": "Staðfest", + "create_profile_from_gap": "Búðu til prófíl", + "undo_import": "Afturkalla síðasta innflutning" }, "note": { "off_delay": { @@ -268,10 +259,6 @@ } }, "conflict": { - "anti_wrinkle_exit": { - "exit": "Verður að vera undir Stöðvunarþröskuldi ({stop} W); annars er útgönguafl hrukkuleysis hunsað", - "stop": "Verður að vera yfir Útgönguaflþröskuldi hrukkuleysis ({exit} W)" - }, "anti_wrinkle_max": { "max": "Verður að vera yfir Ræsingarþröskuldi ({start} W); annars er hámarkslengdartakmörk hrukkuleysis sleppt", "start": "Verður að vera undir Hám. hrukkuleys-afli ({max} W)" @@ -301,22 +288,22 @@ "watchdog": "Verður að vera minna en Engin-uppfærsla-tímamark ({to} s)" }, "off_delay": { - "gap": "Lágm. slökkubil ætti að vera að minnsta kosti Slökkviðráð ({delay} s)", - "off_delay": "Slökkviðráð ({off_delay} s) fer yfir Lágm. slökkubil ({gap} s); samliggjandi lotur gætu sameinast" + "gap": "Lágm. slökkubil ætti að vera að minnsta kosti Slökkvitöf ({delay} s)", + "off_delay": "Slökkvitöf ({off_delay} s) fer yfir Lágm. slökkubil ({gap} s); samliggjandi lotur gætu sameinast" }, "power_off": { "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." + "power_sensor": "Þetta er Aflskynjari tækisins, ekki verð. Lotur yrðu reiknaðar í vöttum á kWh; hreinsaðu þennan reit.", + "energy_sensor": "Þetta er Orkumælir tækisins, ekki verð. Lotur yrðu reiknaðar á stöðu mælisins á kWh; hreinsaðu þennan reit.", + "device_class": "Þessi skynjari mælir {dc}, ekki verð á kWh. Hreinsaðu þennan reit eða veldu gjaldskrárskynjara.", + "unit": "Þessi skynjari mælir í {unit}, ekki verð á kWh. Hreinsaðu þennan reit eða veldu 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ð" + "duration": "Verður að vera undir Engin-uppfærsla-tímamarki ({to} s) svo viðvörun um fasta dælu geti farið fyrst í gang", + "timeout": "Verður að vera yfir Tímamörkum fastrar dælu ({ps} s) svo viðvörun um fasta dælu geti farið fyrst í gang" }, "start_dur": { "sampling": "Sýnitakabil fer yfir Ræsingarlengd ({sdt} s); einstaks sýnis toppar gætu opnað lotu", @@ -334,40 +321,39 @@ "sampling": "Sýnitakabil ætti að vera í mesta lagi helmingur Gæsluabils ({wi} s)" }, "settings_banner": "Árekstrar í stillingum: {n}. Skoðaðu auðkenndu hlutana og lagaðu þá áður en þú vistar.", - "settings_banner_btn": "Fara á fyrsta" + "settings_banner_btn": "Fara á fyrsta", + "end_energy": { + "energy": "Of strangt fyrir Stöðvunarþröskuld ({w} W) yfir Slökkvitöf ({d} s); lotan getur aðeins endað eftir varaleið", + "stop": "Lokaorka ({e} Wh á {d} s) leyfir aðeins {p} W" + } }, "hdr": { - "ab_comparison": "Samanburður á stillingum A/B", + "custom_tasks": "Þín eigin verk", + "maintenance_supporter": "Þarftu meira?", "access_control": "Aðgangsstýring", "add_maintenance": "Bæta við viðhaldsatburði", "automation_delete": "Eyða þessari sjálfvirkni", - "automation_needs_review": "Þessi hringrás þarfnast endurskoðunar", + "automation_needs_review": "Þessi lota þarfnast endurskoðunar", "automation_open": "Opnaðu í sjálfvirkni ritlinum", "automation_review_this_cycle": "Skoðaðu þessa lotu", "automations": "Sjálfvirkni", "clear_debug": "Hreinsaðu villuleitarspor", - "cycle_simulator": "Lotuhermir", "diagnostics_pref": "Greining", "display": "Skjár", - "dtw_inspector": "DTW skoðari", "export_import": "Útflutningur / Innflutningur", - "getting_started": "Að byrja", "live_power": "Rauntímaafl", "logs": "Annálar", - "logs_diagnostics": "Greining", "maintenance": "Viðhaldsaðgerðir", "maintenance_log": "Viðhaldsskrá", "maintenance_reminders": "Þjónustuáminningar", "manual_recording": "Handvirk upptaka", - "manual_recording_tip": "Keyrðu lotu af ásettu ráði á meðan WashData skráir aflsporið. Smelltu á Byrjaðu upptöku rétt áður en tækið ræsist, Hættu þegar því lýkur, og síðan Ferli til að vista það sem nafngreint snið.", + "manual_recording_tip": "Taktu lotu upp viljandi: ýttu á Upphaf rétt áður en tækið fer í gang og Stöðva þegar því lýkur, og síðan Vinna til að vista hana sem snið.", "ml_learned": "Það sem WashData hefur lært", - "ml_matching_tuning": "Fínstilling kerfissamsvörunar", "ml_settings_card": "Stillingar", "ml_smart_learning": "Snjallt nám", "my_preferences": "Mínar óskir", "no_automations": "Engar sjálfvirkni vísar til þessa tækis ennþá.", "panel_settings": "Pallborðsstillingar", - "param_sweep": "Færibreytuyfirferð", "pg_settings_source": "Uppruni stillinga", "phase_catalog": "Áfangaskrá", "playground": "Tilraunasvæði", @@ -376,11 +362,8 @@ "status": "Staða", "status_graph": "Staða graf", "storage_stats": "Geymslutölfræði", - "test_history": "Prófa á sögu", "toggle_sidebar": "Skiptu um hliðarstiku Home Assistant", - "tools_and_data": "Verkfæri og gögn", "wipe_history": "Þurrkaðu sögu", - "recommendations": "Ráðleggingar ({n})", "pg_detection_params": "Greiningarstillingar", "pg_outcome": "Niðurstaða hermunar", "pg_across_cycles": "Yfir lotur þínar", @@ -388,7 +371,8 @@ "online_account": "Samfélagsverslun og nettengdir eiginleikar", "import_power_history": "Flytja inn aflsögu", "service_status": "Staða þjónustu", - "online_account_tab": "Net & samfélag" + "online_account_tab": "Net & samfélag", + "live_match_debug": "Villuleit samsvörunar í beinni" }, "health": { "fair": "Viðunanlegt prófíl", @@ -396,7 +380,12 @@ "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.", + "task_name": "Verk", + "every_cycles": "Millibil (lotur)", + "every_days": "Millibil (dagar)", + "new_task_name": "Nýtt verk", + "days_since_service": "{since} / {total} dagar", + "envelope_position_tip": "Staðsetning á skráðum ferli samsvaraða kerfisins: {pct}%. Fundið með því að stilla lotuna saman við sniðið, svo hún helst rétt þó keyrsla sé lengri eða styttri. Uppfærist á meðan tækið er kyrrt, svo hún getur dregist aftur úr.", "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})", @@ -408,10 +397,8 @@ "aria_cycle_chart": "Aflferill lotu", "aria_envelope_chart": "Línurit aflsviðs prófíls", "aria_phase_chart": "Línurit fasaritils", - "aria_playground_chart": "Aflferill lotu með draganlegum greiningarþröskuldum", "aria_power_chart": "Línurit aflnotkunar", "aria_spaghetti_chart": "Yfirlagðir aflferlar lota", - "aria_sweep_chart": "Línurit niðurstaðna færibreytuyfirferðar", "artifact_anomaly": "Frávik", "artifact_high_power": "Mikið afl", "artifact_interruption": "Hlé", @@ -425,155 +412,105 @@ "cohesion_good": "samheldni {pct}%", "cohesion_low": "⚠ lág samheldni {pct}%", "compare_profiles": "Bera saman við snið", - "concurrency": "Samhliða vinnsla", "confidence": "Öryggi", "confidence_threshold": "Öryggisþröskuldur", "correct_duration": "Rétt lengd (mín., valfrjálst)", "correct_profile": "Rétt prófíl", - "correlation": "Fylgni", "cost": "Kostnaður", "create_new_profile": "+ Búðu til nýjan prófíl...", "current_phase": "Núverandi fasi", - "curves": "Ferlar", "cycle": "Lota", - "cycle_date_display": "Sýning á dagsetningu hringrásar", - "cycle_health": "Heilsa lotu", - "cycle_index": "lotunúmer →", - "cycles": "Lotur", - "cycles_count": "Hringrásir", + "cycle_date_display": "Sýning á dagsetningu lotu", + "cycle_context": "Recorder-saga í kringum lotuna", + "cycle_context_off": "Af", + "cycle_context_minutes": "{min} mín.", + "cycle_context_series": "Recorder-saga", + "before_start": "Fyrir upphaf", + "after_end": "Eftir lok", + "cycles_count": "Lotur", "cycles_lc": "lotur", - "dashed": "strikað", "date": "Dagsetning", "debug_traces": "Villuleita spor", - "default": "Sjálfgefið", "default_access_level": "Sjálfgefið stig fyrir notendur sem ekki eru taldir upp hér að neðan", "default_other": "Sjálfgefið (önnur tæki)", "default_tab": "Sjálfgefinn flipi þegar spjaldið er opnað", - "delta": "Δ", "description": "Lýsing", - "dtw_blend_w": "Blöndunarvægi", - "dtw_blended": "Blönduð einkunn", - "dtw_ddtw": "DDTW-einkunn", - "dtw_l1": "L1-einkunn", - "dtw_matched_profile": "(samsvarað snið lotunnar)", "duration": "Lengd", - "duration_agreement": "Samræmi tímalengdar", - "emphasis": "Áherslur", "enable_access_control": "Virkja aðgangsstýringu fyrir hvern notanda", "energy": "Orka", - "energy_agreement": "Samræmi orku", - "event_timeline": "Tímalína atburða", "event_type": "Tegund atburðar", "expected": "Vænt", + "suspected_profile": "{name} (líklegt)", + "runner_up_profile": "{name} (næstbest)", "expected_duration": "Áætluð lengd (mín.)", "file_kb": "Skrá (kB)", - "final_score": "Lokaeinkunn", "from": "Frá", "gap_s": "Bil (s)", "group_name": "Nafn flokks", "head_trim": "Klippt framan af (s)", - "health": "Heilsa", "hide_tabs": "Fela flipa fyrir ekki stjórnendur", - "in_use": "Í notkun", "json_data": "JSON gögn", "label": "Samsvörun", "last": "Síðustu", "last_checked": "Síðast athugað:", "load_from_file": "Hlaða úr skrá", - "mae_score": "MAE-einkunn", "manual_duration": "Handvirk lengd (mín., valfrjálst)", "match": "Samsvörun", "members": "Meðlimir", - "metric": "Mæligildi", "mode_existing_profile": "Bæta við núverandi prófíl", "mode_new_profile": "Búðu til nýjan prófíl", "models_fine_tuned": "(fínstillt líkön: {count})", - "n_classic_suggestions": "{n} klassískt", - "n_ml_suggestions": "{n} ML", "n_selected": "{n} valin", - "n_tuning_suggestions": "{n} stillinga­tillaga", - "na": "á ekki við", + "n_tuning_suggestions": "{n} stillinga­tillögur", + "n_tuning_suggestions_one": "{n} stillinga­tillaga", + "n_tuning_suggestions_other": "{n} stillinga­tillögur", "new_profile_name": "Nýtt prófílnafn", "notes": "Athugasemdir", - "of": "af", - "outcome": "Niðurstaða", "panel_default_tab": "Sjálfgefinn flipi", "panel_language": "Tungumál stjórnborðs", "per_cycle_short": "lota", - "pg_autofill_tip": "Fylltu bilið sjálfkrafa út frá núverandi gildi ({lo}–{hi})", "pg_best_value": "Besta gildið sem fannst", "pg_current_value": "Núverandi gildi:", "pg_drag_hint": "Dragðu línuna á grafinu", - "pg_dtw": "DTW", "pg_ev_ending": "Að ljúka", "pg_ev_running": "Í gangi", "pg_group_advanced": "Jaðartilvik", "pg_group_detection": "Skynjunarkveikjur", "pg_group_timing": "Tímasetningarreglur", "pg_idle": "Aðgerðalaus", + "pg_poor_match": "Léleg samsvörun", "pg_preset": "Forstilling tilraunasvæðis", "pg_preset_name": "Nafn á nýrri forstillingu", "pg_preset_none": "Veldu forstillingu…", - "pg_stress_group": "Próf á aðgerðalausum lokum", - "pg_stress_idle_auto": "Auto", - "pg_stress_idle_w": "Aðgerðalaust stig (W)", - "pg_stress_idle_w_desc": "Hnekktu sjálfvirkt greindum biðstraumsgólfi (skildu eftir autt fyrir sjálfvirkt).", - "pg_stress_toggle": "Prófa aðgerðalausa lok", - "pg_stress_toggle_desc": "Hermir eftir því að tækið haldist við biðstrauminn sinn eftir lok upptöku - sýnir hvort og hvenær WashData stöðvar lotuna.", - "pg_last_run": "Síðasta keyrsla: {date}", - "pg_match_score": "Samsvörunareinkunn", - "pg_pct_of": "{p}% af {total}", - "pg_poor_match": "Léleg samsvörun", "pg_strong_match": "Sterk samsvörun", - "pg_tile_ambiguous": "Óljóst", - "pg_tile_ambiguous_tip": "Lotur þar sem tvö kerfi eru næstum jöfn", - "pg_tile_detected": "Greint", - "pg_tile_detected_tip": "Lotur þar sem afl fór yfir upphafsþröskuldinn nógu lengi til að skrást sem í gangi", - "pg_tile_matched": "Samsvarað", - "pg_tile_matched_tip": "Lotur sem eru rétt tengdar við þekkt kerfi", - "pg_time_left": "Tími eftir", "pg_weak_match": "Veik samsvörun", - "phase_name": "Nafn fasa", + "phase_name": "Nafn áfanga", "name": "Nafn", "phase_timeline": "Tímalína fasa", "power": "Afl", "profile": "Prófíll", - "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. Veldu kerfi áður en þú setur tækið í gang og það verður notað um leið og næsta lota byrjar.", + "program_tip": "Veldu hvaða kerfi núverandi lota er samsvöruð við. Sjálfvirk skynjun velur bestu samsvörun. Festu kerfi þegar sjálfvirk skynjun hefur rangt fyrir sér; ef það er valið fyrir ræsingu gildir það um næstu lotu.", "progress": "Framfarir", "raw_socket": "Hrár tengi", - "read_only": "Eingöngu lesin", "ref_cycle": "Viðmiðunarlota (valfrjálst)", - "refresh_interval": "Uppfærslubil (s)", "remaining": "Eftir", "reminder_every": "Minna á eftir hverjar (lotur)", "remove_label": "- Fjarlægðu merkimiðann -", "rename_profile": "Endurnefna prófíl", - "replay_duration": "Endurspilun", "resulting_profile": "Úrkomandi prófíll", "save_mode": "Vista ham", - "score": "Einkunn", - "score_breakdown": "Sundurliðun einkunnar", "seconds_from_start": "Sekúndur frá upphafi", "select_profile": "Veldu prófíl", "setting": "Stilling", - "settings_a": "Stillingar A (núverandi)", - "settings_a_short": "A", "settings_advanced": "Ítarlegri", - "settings_b": "Stillingar B (prófun)", - "settings_b_short": "B", "settings_basic": "Grunn", "settings_detail_level": "Nákvæmnisstig stillinga", - "settings_overrides": "Yfirskriftir stillinga", "show_debug": "Sýna villuleitarspjald í beinni á stöðusíðunni (öryggi, tvíræðni, efstu kandídatar)", "show_expected": "Sýna væntanleg ferilyfirlag (samsvörun snið, appelsínugult)", "show_raw": "Sýna hráan innstunga-víxlara í rauntíma aflsriti", "sparkline": "Nýleg þróun lotulengdar", - "stage2": "Þrep 2 – kjarnalíkindi", - "stage3": "Þrep 3 – DTW", - "stage4": "Þrep 4 – samræmi", "status": "Staða", "tail_trim": "Klippt aftan af (s)", "tap_to_dismiss": "pikkaðu á ritið til að loka", @@ -584,7 +521,6 @@ "ungrouped": "Óhópað", "unlabelled": "Ómerkt", "unlabelled_paren": "(ómerkt)", - "warp_path": "Sveigjuleið", "input": "Inntak:", "start": "Upphaf", "end": "Endir", @@ -609,9 +545,8 @@ "n_shown": ", {n} sýnd", "cycles_title": "Lotur ({n})", "pg_detecting": "Greinir", - "pg_best_marker": "Best: {v}", "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", + "pg_time_left_model_tip": "Áætlað út frá áföngum kerfisins, ekki föst niðurtalning", "phase": "Fasi", "pg_mode_history": "Prófa á sögu", "pg_ev_detected": "Greint", @@ -651,12 +586,8 @@ "pg_mode_optimize": "Fínstilla", "pg_group_matching": "Kerfissamsvörun", "pg_zoom_hint": "tvísmelltu til að núllstilla aðdrátt", - "aria_playground_chart2": "Gagnvirkt aflrit fyrir lotuna: haltu bendlinum yfir til að lesa tíma/afl, skrunaðu til að þysja, dragðu til að hliðra", "pg_alert_timeout_end": "Endaði vegna tímamarka, ekki vegna spár", "pg_alert_indefinite": "Myndi ganga endalaust", - "pg_alert_stress_cap": "Öryggismörk náð", - "pg_alert_stress_ok": "Aðgerðalaus lok: lota stöðvuð", - "pg_alert_stress_warn": "Biðstraumur yfir stöðvunarþröskuldi", "task_pg_history": "Prófa á sögu", "task_pg_sweep": "Bestun", "task_reprocess": "Endurvinnsla", @@ -666,16 +597,9 @@ "pg_recent_runs": "Nýlegar keyrslur", "pg_load_run": "Endurhlaða þessa keyrslu", "pg_match_confidence": "Áreiðanleiki samsvörunar", - "pg_envelope_fit": "Samræmi við hjúpferil", - "pg_envelope_fit_tip": "Hversu vel lotan helst innan vistaðs afl-hjúpferils þessa sniðs, önnur sýn en áreiðanleiki samsvörunar.", "n_imported_note": ", {n} fluttar inn", "enable_online": "Virkja nettengda eiginleika", - "store_brand": "Vörumerki tækis", - "store_model": "Gerð tækis", "anonymous": "Nafnlaus", - "rate_quality": "Gefa gæðum einkunn", - "rate_n": "{n} stjörnur", - "approved_only": "Aðeins samþykkt", "adopt_settings": "Taka einnig yfir stillingar", "flags": "Flagg", "include_phase_map": "Hafa fassakort með", @@ -692,7 +616,6 @@ "cat_custom_phases": "Sérsniðnir fasar", "cat_profile_groups": "Prófílhópar", "cat_settings": "Greiningar- & samsvörunarstillingar", - "cat_matching_config": "Fínstilling samsvörunar", "cat_ml_models": "ML-líkön", "cat_feedback": "Umsögn & yfirferðarmerki", "cat_suggestions": "Tillögur", @@ -715,7 +638,6 @@ "hist_csv_data": "CSV-gögn", "hist_from_recorder": "Eða lestu hana úr Home Assistant", "hist_since": "Síðan", - "days": "dagar", "hist_keep": "Halda þessari lotu", "hist_looks_complete": "fullgerð", "peak_power_short": "Toppur", @@ -735,7 +657,34 @@ "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" + "correct_total_cycles": "Leiðréttu samtöluna", + "hist_reason_already_recorded": "skarast á við lotu sem WashData hefur þegar skráð", + "share_consent": "Ég staðfesti að þessar lotur kláruðust eðlilega án truflana", + "show_internal": "Sýna innri stillingar", + "show_internal_tip": "Stillingar sem WashData fínstillir fyrir þig (sýnataka, gæsla, tíðni samsvörunar, þröskuldar). Birtast þegar þetta tæki hefur eigið gildi, árekstur eða tillögu.", + "rate_n": "{n} stjörnur", + "rate_quality": "Gefa gæðum einkunn", + "pg_autolabel": "Sjálfvirk merking", + "pg_label_review": "Biður um endurskoðun", + "pg_ev_match_reverted": "Samsvörun afturkölluð", + "pg_ev_verified_pause": "Staðfest hlé", + "n_samples": "{n} sýni", + "n_samples_one": "{n} sýni", + "n_samples_other": "{n} sýni", + "n_cycles": "{n} lotur", + "n_cycles_one": "{n} lota", + "n_cycles_other": "{n} lotur", + "energy_per_cycle": "{v}/lotu", + "energy_total": "{v} alls", + "automation_off": "(óvirk)", + "ref_cycle_none": "Engin", + "share_name": "Sýna nafnið mitt á því sem ég deili", + "pg_compare_profile": "Bera saman við snið", + "pg_obj_endlag": "Lokatöf (miðgildi)", + "pg_obj_early": "Hlutfall of snemmbúinna loka", + "pg_obj_split": "Hlutfall skiptra lota", + "pg_sweep_cycles": "Nýlegar lotur", + "cat_backfill_cycles": "Lotur fundnar í innfluttri aflsögu" }, "log": { "all_levels": "Öll stig", @@ -745,11 +694,9 @@ }, "ml": { "builtin_models": "● Að nota innbyggðar gerðir", - "fit_fair": "Viðunanlegt", "fit_good": "Gott", "fit_slight": "Lítilsháttar", "fit_strong": "Sterkur", - "fit_weak": "Veik", "fit_word": "passa", "personalized": "● Sérsniðin að þessari vél", "better_than_baseline": "{pct}% betri en grunnmatið ({metric})", @@ -757,30 +704,39 @@ "trend_declining_tip": "Mátun þessa líkans hefur versnað í nýlegum endurskoðunum. Að fara yfir fleiri lotur getur hjálpað því að læra á ný.", "trend_steady_tip": "Mátun þessa líkans hefur haldist nokkurn veginn stöðug í nýlegum endurskoðunum.", "cap_blurb": { - "end": "Að vita hvenær lotu er raunverulega lokið", - "live_match": "Að bera kennsl á kerfið sem er í gangi fyrr", - "quality": "Að koma auga á rangt greindar eða skemmdar lotur", - "remaining_time": "Að spá fyrir um hversu langt er eftir", "total_energy": "Að spá fyrir um heildarorku og kostnað" }, "cap_label": { - "end": "Greining á lokum lotu", - "live_match": "Kerfisgreining", - "quality": "Gæðaeftirlit lotu", - "remaining_time": "Mat á tíma sem eftir er", "total_energy": "Orkumat" }, - "metric_auc": "AUC {auc} á staðfestingargögnum", - "metric_mae": "villa {model} á móti grunnlínu {baseline}" + "metric_mae": "villa {model} á móti grunnlínu {baseline}", + "fine_tuned_at": "fínstillt {when}", + "never": "aldrei", + "trained_unknown": "á óþekktum tíma", + "held_out_count": "mælt á {count} lotum sem haldið var utan við þjálfunina", + "held_out_count_one": "mælt á {count} lotu sem haldið var utan við þjálfunina", + "held_out_count_other": "mælt á {count} lotum sem haldið var utan við þjálfunina", + "last_run_not_promoted": "Síðasta athugun ({when}): {reason}", + "skip_reason": { + "insufficient_rows": "Ekki nógu margar nothæfar lotur enn: {count} merktar, hreinar lotur hingað til.", + "insufficient_rows_one": "Ekki nógu margar nothæfar lotur enn: {count} merkt, hrein lota hingað til.", + "insufficient_rows_other": "Ekki nógu margar nothæfar lotur enn: {count} merktar, hreinar lotur hingað til.", + "fit_failed": "Þjálfun mistókst: {error}", + "unknown": "Ekkert nýtt lærðist.", + "holdout_too_small": "Of fáar lotur til að prófa nýtt líkan á sanngjarnan hátt: hægt var að taka {held_out} frá til prófunar en {min} þarf.", + "not_better_than_naive": "Nýja líkanið var ekki greinilega betra en einfalda matið (villa {model} á móti {naive}).", + "not_better_than_incumbent": "Nýja líkanið var ekki betra en það sem er í notkun (villa {model} á móti {incumbent}), svo núverandi líkani var haldið." + } }, "modal": { + "remove_task_title": "Fjarlægja verk", + "remove_task_msg": "Fjarlægja áminninguna \"{task}\"? Færslur hennar í viðhaldsskránni eru varðveittar.", "auto_label": "Sjálfvirk merkingarlotur", "correct_feedback": "Leiðrétta endurgjöf", "create_profile": "Búðu til prófíl", - "cycle": "Hringrás", + "cycle": "Lota", "edit_group": "Breyta prófílflokki", "edit_phase": "Breyta áfanga", - "import_config": "Flytja inn stillingar", "label_cycle": "Merkja lotu", "merge_cycles": "Sameina {n} lotur", "new_group": "Nýr prófílflokkur", @@ -795,14 +751,16 @@ "delete_automation_title": "Eyða sjálfvirkni", "delete_automation_msg": "Eyða sjálfvirkninni \"{name}\" úr Home Assistant? Þetta er ekki hægt að afturkalla.", "remove_legacy_title": "Fjarlægja eldri aðgerðir", + "reset_phase_msg": "Afturkalla breytinguna þína á \"{name}\" og endurheimta innbyggða áfangann? Kerfin halda sviðum sínum.", + "reset_phase_title": "Endurstilla áfanga", "remove_legacy_msg": "Fjarlægja eldri sérsniðnu aðgerðirnar? Þær hætta að keyra við lotuatburði. Þetta er ekki hægt að afturkalla úr stjórnborðinu.", "discard_recording_title": "Henda upptöku", "discard_recording_msg": "Henda vistuðu upptökunni? Þetta er ekki hægt að afturkalla.", "dismiss_all_title": "Hafna allri endurgjöf", "dismiss_all_msg": "Hafna öllum {count} biðjandi endurgjöfarbeiðnum?", "dismiss_all_ok": "Hafna öllum", - "delete_phase_title": "Eyða fasa", - "delete_phase_msg": "Eyða fasa \"{name}\"?", + "delete_phase_title": "Eyða áfanga", + "delete_phase_msg": "Eyða áfanga \"{name}\"?", "clear_debug_title": "Hreinsa villuleitargögn", "clear_debug_msg": "Eyða öllum vistuðum villuleitarferlum?", "wipe_all_title": "Þurrka út öll gögn", @@ -815,63 +773,77 @@ "store_share_device": "Deila þessu tæki", "export_select": "Útflutningur - velja gögn", "import_wizard": "Innflutningur - velja gögn", - "history_import": "Flytja inn aflsögu" + "history_import": "Flytja inn aflsögu", + "apply_suggestions_title": "Beita tillögum", + "apply_suggestions_msg": "Breyta þessum {n} stillingum? Samþættingin endurhleðst eftir vistun.", + "discard_settings_title": "Henda óvistuðum breytingum?", + "discard_settings_msg": "Óvistaðar stillingabreytingar: {n}. Fara úr Stillingum og henda þeim?", + "profile_title": "Prófíll · {name}", + "undo_import_msg": "Gögn og stillingar þessa tækis fara aftur í það horf sem var fyrir síðasta innflutning ({when}). Lotur og breytingar síðan þá tapast.", + "undo_import_ok": "Afturkalla innflutning", + "undo_import_title": "Afturkalla síðasta innflutning" }, "msg": { + "custom_task_limit": "Þú hefur náð hámarki upp á {max} verk.", + "custom_tasks_intro": "Bættu við öllu öðru sem þú vilt fá áminningu um. Verk er tímabært þegar annað hvort millibilið næst; hafðu annað autt eða 0 til að nota aðeins hitt.", + "maintenance_due_sensor_hint": "Tvíundarskynjarinn Viðhald tímabært er á meðan eitthvert verk er tímabært, fyrir þína eigin tilkynningasjálfvirkni.", + "maintenance_supporter_intro": "Maintenance Supporter er sérstök samþætting sem gerir miklu meira: hvaða tæki eða verk sem er, áætlanir eftir tíma, notkun eða skynjara, tilkynningar og feril. Hún getur notað lotuteljara WashData.", "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.", + "pg_preset_delete_confirm": "Eyða forstillingunni \"{name}\"?", + "pg_preset_limit": "Hámarksfjölda forstillinga náð ({n})", + "pg_preset_overwrite": "Skrifa yfir forstillinguna \"{name}\"?", "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.", - "refresh_catalog_hint": "Listar samfélagsins yfir vörumerki og tæki eru vistaðir í skyndiminni til að samfélagsverslunin haldi sig innan daglegra marka. Endurnýjaðu til að fá færslur sem aðrir hafa bætt við eða samþykkt.", + "store_sibling_hint": "Engu deilt fyrir þína gerð? Skyld gerð frá sama vörumerki er oftast góð byrjun.", + "store_declare_appliance": "Segðu WashData hvaða tæki þú átt til að sjá uppsetningar sem aðrir hafa deilt fyrir það. Eða sláðu inn vörumerki hér fyrir ofan til að skoða þig um.", + "refresh_catalog_hint": "Listar samfélagsins yfir vörumerki og tæki eru í skyndiminni til að spara daglega kvóta verslunarinnar. Endurnýjaðu til að sjá færslur sem aðrir hafa bætt við eða samþykkt.", "head_trim_hint": "Fjarlægðu þennan fjölda sekúndna frá byrjun", "appliance_monitor": "Vöktunartæki", "artifact_dip_detail": "Féll undir venjulegt aflband í ~{n}s.", "artifact_footer": "Auðkenndur á grafinu hér að ofan. Þetta eru skammvinnir gripir (t.d. hurðin opnuð í miðjum lotu), ekki endilega vandamál.", "artifact_header": "{n} frávik fundust í þessari lotu", - "artifact_pause_detail": "Afl féll nærri núlli í ~{n}s og hækkaði svo aftur – líklega var hurðin opnuð um miðja lotu eða lota sett í bið.", + "artifact_header_one": "{n} frávik fannst í þessari lotu", + "artifact_header_other": "{n} frávik fundust í þessari lotu", + "artifact_pause_detail": "Aflið féll nærri núlli í ~{n}s og kom svo aftur. Líklega var hurðin opnuð eða gert hlé á lotunni.", "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": "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.", + "cleanup_intro": "Allar merktar lotur lagðar hver yfir aðra. Hakaðu við frávik og fjarlægðu svo merkingu þeirra (þær haldast í sögunni en móta ekki lengur þetta snið) eða eyddu þeim.", "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)", - "compare_profiles_tip": "Leggðu önnur prófílumslög yfir á töfluna hér að ofan til að sjá hver þeirra passar best við þessa lotu.", + "compare_profiles_tip": "Leggðu önnur snið yfir línuritið til að sjá hvert passar best við þessa lotu.", "compare_selected_cycles": "Valdar lotur (fastar) – sýna / fela", - "consider_new_profile": "Íhugaðu að búa til nýjan prófíl.", - "coverage_gap": "Nýlegar lotur hafa ekkert samsvarandi prófíl ({pct}% af síðustu 30).", - "coverage_gap_similar_cycles": "{count} svipaðar ómerktar lotur fundust – búðu til prófíl til að byrja að samstemma þær.", "cycles_deleted": "{count} lotu(m) eytt", + "cycle_context_loading": "Hleð recorder-sögu…", + "cycle_context_note": "Grátt: recorder-saga aflskynjarans í kringum þessa lotu. Hún er ekki hluti af lotunni og hefur ekki áhrif á lengd, orku eða samsvörun.", + "cycle_context_none": "Recorder geymir engar mælingar í kringum þessa lotu (Home Assistant geymir sjálfgefið 10 daga af sögu).", + "cycle_context_tip": "Aðeins til sýnis: sýnir recorder-sögu aflskynjarans í kringum þessa lotu, ef hún er enn geymd. Til að bæta misheppnuðum ræsitilraunum við framtíðarlotur skaltu nota Aðdragandi ferils (Stillingar > Greining > Upphaf lotu), sem breytir vistuðum lengdum.", "enough_data": "Nóg gögn til að læra af ({current}/{min} lotur).", "export_description": "Veldu nákvæmlega hvaða prófíla, lotur, stillingar og fleira á að flytja út í JSON, eða greindu skrá og fluttu aðeins inn þá hluta sem þú vilt.", "feedback_cycles_pending": "Til yfirferðar: {n}", "feedback_prompt": "Staðfestu að það hafi verið rétt, leiðréttu kerfið eða hunsaðu.", "feedback_relabel_hint": "Að endurmerkja þessa lotu lýkur einnig yfirferðinni.", "filter_by_profile": "Sía eftir prófíl…", - "group_modal_help": "Flokkaðu kerfi með sömu lögun sem eru mismunandi hvað varðar hitastig/snúning (lengd getur verið mismunandi). Samsvörun gefur hópnum eina einkunn og velur síðan þann meðlim sem passar best. Veldu að minnsta kosti 2; yfirlagið sýnir hversu lík þau eru.", + "group_modal_help": "Flokkaðu kerfi með sömu lögun sem eru ólík í hitastigi eða vindingu. Samsvörunin metur hópinn sem eina heild og velur svo þann meðlim sem passar best. Veldu að minnsta kosti 2; yfirlögnin sýnir hversu lík þau eru.", "group_not_cohesive": "Þessir snið eru ekki nógu lík til að hópa á áreiðanlegan hátt, þannig að samsvörun meðhöndlar þá hver fyrir sig þar til þú fjarlægir frávikið eða skiptir hópnum.", "group_preview_hint": "Merktu 2+ meðlimi til að forskoða og bera saman aflferla þeirra.", - "import_intro": "Hlaðið út útfluttri skrá eða límdu JSON-hleðslu fyrir neðan.", - "legacy_actions_title": "{count} eldri sérsniðin aðgerð enn í gangi", + "legacy_actions_title": "{count} eldri sérsniðnar aðgerðir enn í gangi", + "legacy_actions_title_one": "{count} eldri sérsniðin aðgerð enn í gangi", + "legacy_actions_title_other": "{count} eldri sérsniðnar aðgerðir enn í gangi", "live_chart_loading": "Lifandi aflkort birtist þegar álestur berast.", "loading": "Hleður…", "loading_curve": "Hleður feril...", "loading_settings": "Hleður stillingum...", "log_buffer_hint": "Nýjasta fyrst · biðminni síðustu 500 ha_washdata færslur frá endurræsingu · dragðu neðri brúnina til að breyta stærð.", - "maintenance_advisory": "Vaxandi tímalengd/orka getur bent til þess að viðhalda þörfum tækisins (t.d. kalkhreinsun, síuþrif).", "maintenance_due": "Viðhald tímabært: {items}", - "maintenance_intro": "Skráðu þjónustu sem þú framkvæmir á þessu tæki og fáðu áminningu þegar hvert verk er aftur tímabært.", + "maintenance_intro": "Skráðu þjónustu á þessu tæki og fáðu áminningu þegar hvert verk er aftur tímabært.", "maintenance_load_error": "Ekki tókst að hlaða viðhaldsgögnum: {error}", "maintenance_requires_access": "Viðhald og útflutningur/innflutningur krefjast fulls aðgangs.", - "manual_duration_hint": "Meðaltal/vænt hringrásarlengd sniðsins, notuð fyrir tíma sem eftir er. Breyta til að stilla það; ef það er óbreytt heldur núverandi gildi.", - "matching_tuning_intro": "Þegar WashData lærir stillir það einnig hversu mikið kerfissamsvörun vegur lögun á móti lengd og orku.", + "manual_duration_hint": "Væntanleg lotulengd sniðsins, notuð til að meta tíma sem eftir er. Hafðu hana óbreytta til að halda núverandi gildi.", "merge_intro": "Valdar lotur eru sameinaðar í eina (tímaröð; eyður fyllt með 0 W). Veldu prófílinn sem myndast.", "ml_intro": "WashData er sent með snjöllum gerðum sem vinna úr kassanum.", "ml_learned_intro": "Módel fínstillt að þessari vél.", - "ml_loading": "hleður ML…", - "ml_settings_intro": "Tveir óháðir rofar: annar notar gerðir á meðan hringrás er í gangi, hinn gerir WashData kleift að fínstilla þær að vélinni þinni með tímanum.", - "name_first_program": "Þú ert með nægar lotur – gefðu fyrsta prógramminu nafn til að hefja samsvörun.", - "near_duplicate_cluster": "næstum tvítekinn prófílklasi fannst. Flokkun gerir samsvörun kleift að velja áreiðanlega á milli þeirra sem líkjast hver öðrum (t.d. sama kerfið við mismunandi hitastig/snúning).", + "ml_settings_intro": "Tveir óháðir rofar: annar notar gerðir á meðan lota er í gangi, hinn gerir WashData kleift að fínstilla þær að vélinni þinni með tímanum.", "no_cycles_match": "Engar lotur passa við núverandi síu.", "no_cycles_profile": "Engar lotur fyrir þetta snið.", "no_cycles_selected": "Veldu að minnsta kosti eina lotu fyrst.", @@ -889,80 +861,59 @@ "no_phases_assigned": "Engum áföngum úthlutað.", "no_profiles_compare": "Engir prófílar til að bera saman.", "no_profiles_overlay": "Engir snið til að leggja yfir.", - "no_profiles_yet": "Engir prófílar ennþá. Búðu til einn úr merktri hringrás.", + "no_profiles_yet": "Engir prófílar ennþá. Búðu til einn úr merktri lotu.", "no_profiles_yet_short": "Engir prófílar ennþá.", "no_settings_match": "Engar stillingar passa við \"{q}\"", "no_split_points": "Engin skipting stig ennþá.", "no_suggestions": "Engar virkar tillögur.", "notify_services_hint": "Notaðu {entity} þjónustuauðkenni (aðskilin með kommum fyrir mörg). Sniðmátsbreytur: {vars}.", - "old_actions_warning": "Stillt með gamla aðgerðaritlinum (nú fjarlægður). Þeir virkjast enn við hringrásarviðburði en ekki er lengur hægt að breyta þeim hér. Umbreyttu þeim í venjulega sjálfvirkni eða fjarlægðu þau.", - "onboarding_progress": "{n} / 3 lotur skoðaðar", - "onboarding_watching": "Notaðu tækið eins og venjulega – WashData fylgist með. Eftir 3 lotur hefst samsvörun prógramma.", + "old_actions_warning": "Stillt með gamla aðgerðaritlinum (nú fjarlægður). Þeir virkjast enn við lotuviðburði en ekki er lengur hægt að breyta þeim hér. Umbreyttu þeim í venjulega sjálfvirkni eða fjarlægðu þau.", "pending_feedback": "Bíður viðbrögð við uppgötvunum", "performance_trend": "Árangursþróun ({n} lotur)", - "pg_analysis_empty": "Hladdu lotu til að sjá samsvörunargreiningu.", "pg_apply_confirm": "Beita besta gildi: {label} = {value}?", - "pg_canvas_empty": "Veldu lotu að ofan og ýttu svo á Hlaða til að sjá aflferil hennar.", - "pg_chart_caption": "Greiningar-/samsvörunarhlutfall eftir gildum {param}", "pg_ctrl_intro": "Gildin byrja á virkum stillingum þessa tækis í samþættingunni. Breytingar haldast á tilraunasvæðinu þar til þú vistar þær í samþættinguna.", "pg_live_unavailable": "Virkar stillingar eru ekki tiltækar, sjálfgefin gildi eru sýnd.", "pg_load_live_confirm": "Henda {n} breytingu(m) á tilraunasvæðinu og hlaða núverandi stillingum samþættingarinnar aftur?", - "pg_match_rate": "{pct}% samsvörunarhlutfall á {total} lotum", "pg_matches_live": "Samsvarar virkum stillingum", "pg_n_changed": "{n} breytt miðað við virkar stillingar", "pg_sugg_none": "Allar tillögur stemma nú þegar við virkar stillingar", - "pg_preset_delete_confirm": "Eyða forstillingunni \"{name}\"?", - "pg_preset_limit": "Hámarksfjölda forstillinga náð ({n})", - "pg_preset_overwrite": "Skrifa yfir forstillinguna \"{name}\"?", "pg_publish_one_confirm": "Vista {label} = {value} í stillingar þessa tækis?", "pg_restart_note": "Endurræstu Home Assistant til að virkja hermitólin.", - "pg_sim_progress": "{done} / {total} lotur", - "pg_starting": "Ræsir…", - "pg_stress_synthetic": "aðgerðalaus hermun hefst hér", - "pg_sweep_intro": "Hvað ef {param} væri öðruvísi? Prófaðu {steps} gildi yfir síðustu {cycles} loturnar þínar til að finna stillinguna þar sem flestar lotur samsvarast rétt.", - "pg_sweep_step": "Skref {done} / {total}", - "pg_verdict_bad": "Þarfnast athygli: margar lotur greinast ekki.", - "pg_verdict_good": "Vel stillt: flestar lotur eru rétt greindar og samsvaraðar.", - "pg_verdict_ok": "Ásættanlegt: sumar lotur misstust. Prófaðu að lækka upphafsþröskuldinn.", - "phase_catalog_intro": "Nafngreindir hlutar hringrásar (Forþvottur, Upphitun, Snúning…). Úthlutaðu þeim á prófíl frá stjórnborði þess.", + "phase_catalog_intro": "Nafngreindir hlutar lotu (Forþvottur, Upphitun, Snúning…). Úthlutaðu þeim á prófíl frá stjórnborði þess.", "phase_ranges_intro": "Áfangasvið (mínútur frá upphafi lotu) lögð yfir meðalferilinn. Breyttu gildum til að forskoða í beinni.", - "playground_intro": "Öruggt tilraunasvæði til að spila fyrri lotur aftur með mismunandi stillingum, bera saman tvær uppsetningar hlið við hlið og skoða hvernig lota samræmist sniði. Ekkert hér breytir raungögnunum þínum fyrr en þú beitir því sérstaklega.", - "playground_needs_restart": "Endurræstu Home Assistant til að virkja tólin á Tilraunasvæðinu.", - "preferences_admin": "Óskir, pallborð og aðgangsstýring", - "preferences_adv": "Óskir", + "playground_intro": "Prófaðu stillingar á raunverulegum lotugögnum þínum. Ekkert hér breytir virku stillingunum þínum fyrr en þú beitir því.", "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 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.", + "process_history_hint": "Keyrðu samsvörun aftur á öllum vistuðum lotum, uppfærðu tillögur, endurþjálfaðu ML-líkön (ef virkt), endurreiknaðu kostnað út frá skráðum verðum og endurreiknaðu heilsu lotanna. Keyrðu þetta eftir lotu af yfirferðum.", "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.", - "rbac_hint": "Þegar slökkt er á því hefur hver Home Assistant notandi fullan aðgang (sjálfgefið). Stjórnendur hafa alltaf fullan aðgang og geta stjórnað öllum.", - "recent_logs": "Nýlegar ha_washdata færslur", + "rbac_hint": "Slökkt: allir Home Assistant-notendur hafa fullan aðgang (sjálfgefið). Stjórnendur hafa alltaf fullan aðgang og geta stjórnað öllum.", "recording_in_progress": "Upptaka í gangi", "reminders_intro": "Sýna áminningu í spjaldinu svona mörgum lotum eftir síðustu þjónustu. Skildu eftir autt eða 0 til að slökkva á áminningu.", "restart_gap_footer": "Auðkennt á línuritinu. Aflgögn vantar fyrir þessi bil; samsvörun notaði aðeins raunverulegar mælingar.", - "restart_gap_header": "{n} HA endurræsingarbil á þessum hringrás", + "restart_gap_header": "{n} HA endurræsingarbil í þessari lotu", + "restart_gap_header_one": "{n} HA endurræsingarbil í þessari lotu", + "restart_gap_header_other": "{n} HA endurræsingarbil í þessari lotu", "restart_gap_item": "{dur} bil", - "review_confirm_help": "Staðfestu hvort þessi hringrás hafi fundist rétt. Umsagnir þínar þjálfa líkanið á vélinni þinni - því fleiri lotur sem þú staðfestir, því betri samsvörun og heilsustig fást. Fljótlegt gott/slæmt er nóg.", + "review_confirm_help": "Var þessi lota greind rétt? Staðfestar lotur bæta samsvörun fyrir vélina þína. Fljótlegt Gott/Slæmt nægir.", "review_in_settings": "Skoðaðu í stillingum", "review_notes_placeholder": "Skýringar (valfrjálst)", "review_notes_tip": "Skýringar með frjálsum texta til eigin viðmiðunar. Ekki notað við samsvörun eða þjálfun.", - "review_profile_tip": "Kerfið sem þessi lota er merkt sem. Ef sjálfvirkt greint kerfi var rangt skaltu leiðrétta það hér - merking kennir samsvöruninni fyrir framtíðarlotur.", - "review_quality_tip": "Hversu hrein þessi lota er. Gott = kennslubókardæmi um þetta kerfi; Slæmt = greinist en hávaðasöm eða óhefðbundin; Ónothæft = rangt greint (sameinað, stytt eða falsað). Stýrir heilsustiginu og því hvaða lotur má nota til að þjálfa líkanið.", - "review_recorded_tip": "Merktu þetta sem handvalda viðmiðunarlotu fyrir kerfið sem hún tilheyrir - sama hlutverk og handvirkt skráð lota. Viðmiðunarlotum er alltaf haldið, þær móta samsvörunarsniðmátið og þeim er aldrei sleppt við hreinsun. (Þetta er „gyllti“/skráði fáninn; hvort tveggja er það sama.)", - "review_tags_tip": "Valfrjálsir fánar sem lýsa því sem fór úrskeiðis við þessa lotu, svo þjálfun og hreinsun getur gert grein fyrir því.", - "review_to_cycles": "Opnaðu Cycles review biðröðina", + "review_profile_tip": "Kerfið sem þessi lota er merkt sem. Ef það er rangt skaltu leiðrétta það hér; merkingar kenna samsvöruninni.", + "review_quality_tip": "Hversu hrein þessi lota er. Gott: kennslubókardæmi um þetta kerfi. Slæmt: greind, en með truflunum eða óvenjuleg. Ónothæft: rangt greind (sameinuð, stytt eða fölsk). Hefur áhrif á heilsueinkunnina og hvaða lotur þjálfa líkanið.", + "review_recorded_tip": "Merktu þetta sem handvalda viðmiðunarlotu fyrir kerfið hennar, eins og handvirkt upptekna lotu. Viðmiðunarlotum er alltaf haldið, þær leggja grunninn að samsvörunarsniðmátinu og eru aldrei fjarlægðar við hreinsun.", + "review_tags_tip": "Valfrjálsir fánar um hvað fór úrskeiðis, svo þjálfun og hreinsun geti tekið tillit til þess.", + "review_to_cycles": "Opna yfirferðarbiðröðina í Lotum", "samples_decimated": "Sýnir {shown} af {total} sýnum (grisjað fyrir birtingu; toppum haldið). Breið eyða hér stafar af grisjun, ekki af gögnum sem vantar.", - "saving_triggers_reload": "Vistun kallar á endurhleðslu samþættingar. HA einingar gætu sýnt stuttlega sem ófáanlegur.", + "saving_triggers_reload": "Vistun endurhleður samþættinguna. Einingar geta birst stuttlega sem ótiltækar.", "search_placeholder": "Leitarstillingar...", "see_recorder": "Sjá upptökugræju hér að neðan", "setting_changed": "Breytt úr {old} í {new} þann {date}", "settings_basic_note": "Sýni nauðsynlegar stillingar. Skiptu yfir í Ítarlegri fyrir allan listann.", - "shape_drift_advisory": "⚠ Lögun að breytast", - "shape_drift_detail": "Aflmynstur þessa prófíls hefur breyst yfir tíma – mögulegt slit á tæki eða viðhald þarf (t.d. kalkhreinsa, hreinsa síu).", "show_all_settings": "Sýna allar stillingar", - "showing_suggestions": "Sýnir {count} stillingu með tillögum.", - "split_intro": "Smelltu á línuritið til að bæta við eða fjarlægja skiptingarpunkt, eða greina sjálfvirkt með aðgerðalausum eyðum. Hver hluti sem myndast getur fengið sinn eigin prófíl.", - "storage_diagnostics": "Geymslutölfræði, viðhald, útflutningur/innflutningur", + "showing_suggestions": "Sýnir {count} stillingar með tillögum.", + "showing_suggestions_one": "Sýnir {count} stillingu með tillögum.", + "showing_suggestions_other": "Sýnir {count} stillingar með tillögum.", + "split_intro": "Smelltu á línuritið til að bæta við eða fjarlægja skiptipunkt, eða greindu sjálfvirkt út frá aðgerðalausum hléum. Hver hluti getur fengið sitt eigið snið.", "sug_staged": "Stillt {key} = {val}. Vista til að nota.", "toast_auto_detect_enabled": "Sjálfvirk greining virk", "toast_auto_label_complete": "Sjálfvirk merking lokið", @@ -979,7 +930,6 @@ "toast_no_automation": "Get ekki búið til sjálfvirkni hér", "toast_profile_name_required": "Nafn sniðs er krafist", "toast_program_set": "Kerfi stillt: {program}", - "toast_rebuild_failed": "Endurgerð mistókst: {error}", "toast_resume_failed": "Haldið áfram mistókst: {error}", "toast_revert_failed": "Endurstilling mistókst: {error}", "toast_reverted": "{key} afturkallað", @@ -990,106 +940,89 @@ "trend_energy_up": "Orka hækkar ({pct}%/lotu) – nýleg meðaltal {avg}", "trim_destructive_confirm": "Aðeins {pct}% af lotunni verður eftir. Þetta er ekki hægt að afturkalla. Halda áfram?", "trim_intro": "Dragðu rauðu handföngin eða sláðu inn gildi. Allt utan valda tímabilsins er fjarlægt.", - "tuning_suggestions_available": "{count} uppástunga að stilla í boði frá athuguðum lotum. Þau birtast við hlið viðkomandi reita.", + "tuning_suggestions_available": "{count} stillingatillögur í boði út frá athuguðum lotum. Þær birtast við hlið viðkomandi reita.", + "tuning_suggestions_available_one": "{count} stillingatillaga í boði út frá athuguðum lotum. Hún birtist við hlið viðkomandi reits.", + "tuning_suggestions_available_other": "{count} stillingatillögur í boði út frá athuguðum lotum. Þær birtast við hlið viðkomandi reita.", "unsure_detected_prefix": "WashData er ekki viss um að það hafi fundist", - "updated": "Uppfært", "warmup_badge": "Enn að læra ({done}/{needed} lotur)", - "warmup_detail": "Þessi prófíll þarfnast {needed} merktra lota áður en sjálfvirk samsvörun hefst. Sérhver staðfest lota hjálpar honum að læra.", + "warmup_detail": "Samsvörun virkar nú þegar. WashData biður þig að staðfesta fyrstu {needed} lotur þessa sniðs áður en það merkir sjálft.", "wipe_history_warning": "Eyða öllum lotum og sniðum varanlega. Ekki hægt að afturkalla.", "compare_overlay_tip": "Leggðu lærð aflsvið prófíla yfir hvert annað til að sjá hvaða kerfi hver lota líkist.", - "duration_trend_up_tip": "Lengd eykst ({pct}%/lotu)", - "duration_trend_down_tip": "Lengd minnkar ({pct}%/lotu)", - "energy_trend_up_tip": "Orka eykst ({pct}%/lotu)", - "energy_trend_down_tip": "Orka minnkar ({pct}%/lotu)", - "performance_trending": "Afkastaþróun", - "pg_select_cycle_hint": "Veldu lotu og smelltu á ↺ til að hlaða", - "pg_sweep_progress": "Skref {done} / {total}", - "pg_analysis_hint": "Veldu lotu og smelltu á ↺ til að hlaða greiningunni.", "diagnostics_load_failed": "Ekki tókst að hlaða greiningu: {error}", - "manual_duration_ref_hint": "Aðeins notað þegar engin viðmiðunarlota er valin. Viðmiðunarlota ákvarðar lengdina út frá eigin tímalengd.", + "manual_duration_ref_hint": "Aðeins notað án viðmiðunarlotu; viðmiðunarlota ákvarðar lengdina út frá eigin lengd.", + "duration_learned_hint": "Reiknað út frá lotum þessa kerfis (frávik undanskilin) og uppfært eftir hverja nýja lotu. Til að breyta því skaltu klippa til eða fjarlægja loturnar sem skera sig úr.", "toast_convert_rolled_back": "Umbreyting mistókst og var afturkölluð (engin sjálfvirkni skilin eftir): {error}", "toast_convert_orphan": "Sjálfvirknin var búin til, en hreinsun gömlu aðgerðanna mistókst. Reyndu ekki aftur: fjarlægðu gömlu aðgerðirnar handvirkt til að forðast tvítekna sjálfvirkni.", - "advisory_duration_trend_up": "Lotur fyrir '{name}' verða smám saman lengri (um +{pct}% á hverja lotu). Ef hegðun tækisins hefur breyst skaltu taka þetta snið upp aftur eða endurbyggja það.", - "advisory_energy_trend_up": "'{name}' dregur smám saman meiri orku (um +{pct}% á hverja lotu) - það er þess virði að athuga tækið ef þetta er óvænt.", - "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.", + "advisory_duration_trend_up": "Lotur '{name}' verða sífellt lengri (um +{pct}% á hverja lotu). Ef tækið hefur breyst skaltu taka þetta snið upp aftur eða endurbyggja það.", + "advisory_energy_trend_up": "'{name}' notar meiri orku í hverri lotu (um +{pct}% á hverja lotu). Athugaðu tækið ef þetta er óvænt.", + "advisory_poor_health": "'{name}' passar illa: nýlegar lotur þess eru mjög breytilegar eða samsvara illa. Farðu yfir lotur þess eða taktu það upp aftur svo samsvörun og mat haldist nákvæm.", + "advisory_shape_drift": "'{name}' hefur fjarlægst upprunalega aflferil sinn, hugsanlega vegna kalks eða slits. Íhugaðu að taka það upp aftur með nýlegum lotum.", + "advisory_shape_drift_corr": "'{name}' hefur fjarlægst upprunalega aflferil sinn (fylgni {corr}), hugsanlega vegna kalks eða slits. Íhugaðu að taka það upp aftur með nýlegum lotum.", + "advisory_unmatchable": "'{name}' getur aldrei samsvarað: engin lota með aflgögnum liggur að baki. Merktu eða taktu upp lotu af þessu kerfi til að laga það.", + "advisory_unmatchable_no_duration": "'{name}' getur aldrei samsvarað: lota þess hefur enga nothæfa lengd. Taktu þetta kerfi upp aftur eða stilltu vænta lengd þess.", "pg_no_alerts": "Engin vandamál greindust í þessari keyrslu.", "pg_override_active": "Notar breyttu stillingarnar þínar á móti núverandi", "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ð.", "pg_sweep_intro2": "Finndu stillinguna sem uppfyllir markmið best yfir nýlegar lotur þínar. Ekkert breytist fyrr en þú beitir henni.", - "pg_running_history": "Endurspila lotur þínar…", - "pg_running_sweep": "Prófa gildi yfir lotur þínar…", "pg_row_load_hint": "Hlaða þessari lotu í ritið að ofan", - "pg_history_intro2": "Endurspilaðu nýlegar lotur í gegnum raunverulegu greininguna og samsvörunina með stillingunum að ofan. Smelltu á línu til að hlaða þeirri lotu í ritið; breyttu stillingu til að sjá samanburð fyrir og eftir.", + "pg_history_intro2": "Endurspilaðu nýlegar lotur í gegnum raunverulegu greininguna og samsvörunina með stillingunum að ofan. Smelltu á línu til að hlaða henni í línuritið; breyttu stillingu til að bera saman fyrir og eftir.", "pg_simulating": "Hermir lotu…", - "pg_canvas_empty2": "Veldu lotu að ofan og ýttu á Keyra til að herma hana. Haltu svo bendlinum yfir til að lesa gildi, skrunaðu til að þysja og dragðu til að hliðra.", "pg_apply_settings_confirm": "Vista þessar {n} stilling(ar) á þetta tæki? {list}", - "pg_stress_terminated_detail": "Náði jafnvægi við ~{idle}W í aðgerðaleysi -- lotan endaði {h} klst. {m} mín. síðar með {reason}.", - "pg_stress_above_threshold_detail": "Biðstraumur ~{idle}W er við eða yfir virkum stöðvunarþröskuldi ({stop}W) -- lotan var aldrei skráð sem óvirk. Hækkaðu stop_threshold_w til að laga þetta.", - "pg_stress_hit_cap_detail": "Lotan keyrði {h} klst. {m} mín. án þess að stöðvast -- stöðvuð með valdi af öryggismörkunum. Biðstraumur {idle}W á móti stöðvunarþröskuldi {stop}W.", "no_logs_match": "Engar annálafærslur passa við þessar síur.", "pg_analysis_hint2": "Veldu lotu og ýttu á Keyra til að hlaða samsvörunargreiningu.", - "pg_analysis_empty2": "Ýttu á Keyra til að sjá samsvörunargreiningu.", - "imported_readonly": "Flutt inn úr samfélagsversluninni. Sýnt til viðmiðunar og samsvörunar. Það er ekki talið með í tölfræðinni þinni og ekki hægt að breyta því.", + "imported_readonly": "Flutt inn úr samfélagsversluninni. Notað til viðmiðunar og samsvörunar, telst ekki með í tölfræðinni þinni og er ekki hægt að breyta.", "merge_no_imports": "Ekki er hægt að sameina innfluttar upptökur við raunverulega lotu. Afveldu þær til að sameina.", - "store_enable_hint": "Virkjaðu nettengda eiginleika í Stillingum til að skoða og flytja inn viðmiðunarlotur samfélagsins.", - "advisory_duration_outlier": "{n} lota/lotur merktar '{name}' eru of langt frá venjulegri lengd kerfisins til að geta nokkurn tíma fengið samsvörun við það (verst: {ratio}x). Þær eru líklega rangt merktar, eða ein upptaka náði tveimur keyrslum. Merktu þær aftur eða skiptu þeim upp svo þær skekki ekki lengur tímaáætlun þessa kerfis.", + "store_enable_hint": "Kveiktu á nettengdum eiginleikum undir Kjörstillingar > Net & samfélag til að skoða og flytja inn viðmiðunarlotur samfélagsins.", + "advisory_duration_outlier": "{n} lota/lotur merktar '{name}' eru of langt frá venjulegri lengd þess til að geta nokkurn tíma samsvarað (verst: {ratio}x). Þær eru líklega rangt merktar eða innihalda tvær keyrslur. Merktu þær aftur eða skiptu þeim svo þær skekki ekki lengur tímamatið.", "standby_above_stop_title": "Dró enn {idle} W þegar lotunni lauk", - "standby_above_stop_sub": "{n} af síðustu {total} lotum. Slökkviðráð fer ekki af stað fyrr en aflið er komið undir Stöðvunarþröskuld ({stop} W), svo lotur enda of seint eða þarf að stöðva handvirkt.", - "online_intro": "Deildu viðmiðunarlotum með öðrum WashData-notendum og halaðu þeim niður. Allir nettengdir eiginleikar eru valfrjálsir og sjálfgefið óvirkir.", + "standby_above_stop_sub": "{n} af síðustu {total} lotum. Slökkvitöf fer ekki af stað fyrr en aflið er komið undir Stöðvunarþröskuld ({stop} W), svo lotur enda of seint eða þarf að stöðva handvirkt.", "store_share_intro": "Hladdu þessari viðmiðunarlotu upp svo aðrir með sama tæki geti notað hana. Hún er yfirfarin áður en hún birtist opinberlega.", - "online_intro_global": "Skoðaðu og deildu viðmiðunarupptökum með öðrum WashData-notendum og staðfestu tækjafærslur. Ein tenging gildir fyrir alla WashData-samþættinguna þína; vörumerki og gerð tækis eru stillt fyrir hvert tæki undir Grunnstillingum. Allir nettengdir eiginleikar eru valfrjálsir og sjálfgefið óvirkir.", + "online_intro_global": "Skoðaðu og deildu viðmiðunarupptökum með öðrum WashData-notendum. Ein tenging nær yfir öll WashData-tækin þín; vörumerki og gerð eru stillt fyrir hvert tæki undir Grunnstillingar. Nettengdir eiginleikar eru óvirkir þar til þú kveikir á þeim.", "online_unavailable": "Nettengdir eiginleikar eru ekki í boði á þessum netþjóni.", "store_picker_offline": "Virkjaðu nettengda eiginleika í stillingatannhjólinu til að velja úr samfélagsskránni.", "pick_brand_first": "Veldu fyrst vörumerki tækis.", - "model_not_found": "Ekki enn í skránni.", - "connect_to_confirm": "Tengstu í stillingatannhjólinu til að staðfesta eða gefa einkunn.", "adopt_settings_hint": "Skrifar yfir greiningar- og samsvörunarmörk þessa tækis með deildum. Tilkynningar þínar, einingar og orkuverð breytast aldrei.", - "include_settings_hint": "Deilir greiningar- og samsvörunarmörkum þessa tækis (ekki tilkynningar, einingar eða orkuverð). Notendur velja sjálfir hvort þeir vilji nota þær.", + "include_settings_hint": "Deildu greiningar- og samsvörunarþröskuldum þessa tækis (ekki tilkynningum, einingum eða orkuverði). Aðrir velja sjálfir hvort þeir nota þá.", "onboard_download": "Nýtt tæki? Taktu yfir tilbúna uppsetningu (kerfi, viðmiðunarlotur og fasa) frá öðrum WashData-notanda með sama tæki.", - "share_consent": "Ég staðfesti að þessar lotir voru kláraðar eðlilega án truflana", "share_device_none": "Engar deilanlegar lotir enn. Merktu fyrst skráða eða handvalda lotu sem viðmiðunarlotu (⭐) í flipanum Lotir.", "share_guideline_naming": "Gefðu hverjum prófíl nákvæmlega það nafn sem sést á hringtakkanum eða skjánum tækisins (t.d. 'Cotton 40', 'Eco 60').", - "share_guideline_quality": "Deildu aðeins lotum sem kláruðust eðlilega -- engar truflanir á miðjum lotu, opnaðar hurðir eða aflbrostir.", + "share_guideline_quality": "Deildu aðeins lotum sem kláruðust eðlilega: engum truflunum, hurðaropnunum eða rafmagnsflökti.", "share_guideline_review": "Upphleðslan þín byrjar sem í bið og birtist opinberlega þegar nægilega margir samfélagsmeðlimir staðfesta hana.", "share_guidelines_title": "Áður en þú deilir", - "store_download_device_intro": "Taktu yfir öll deildu kerfin og viðmiðunarlotur þeirra á tækið þitt. Þínar eigin skráðu lotur og tölfræði verða ekki fyrir áhrifum.", + "store_download_device_intro": "Bættu öllum deildum kerfum og viðmiðunarlotum þeirra við tækið þitt. Þínar eigin lotur og tölfræði verða ekki fyrir áhrifum.", "store_share_device_intro": "Hlaðaðu upp {brand} {model} með viðmiðunarlotunum sem þú velur. Aðrir með sama tæki geta tekið yfir kerfin þín. Færslur eru yfirfarnar áður en þær birtast opinberlega.", "share_profile_no_cycles": "Engar viðmiðunarlotir -- merktu lotu sem ⭐ í flipanum Lotir til að hafa þennan prófíl með", - "advisory_phase_inconsistent": "'{name}' virðist blanda saman ólíkum kerfum eða hitastigum - lotur þess hita mislengi. Ef þú skiptir því upp í aðskilin snið (t.d. eftir hitastigi) batnar samsvörun og tímamat.", - "advisory_phase_inconsistent_title": "⚠ Hugsanlega blönduð kerfi", - "export_select_intro": "Merktu nákvæmlega við hvað á að hafa með. Að velja prófíla án lotanna þeirra flytur samt út kerfi sem hægt er að samsvara (lærða lögunin fylgir með).", - "import_analyze_hint": "Hladdu inn útfluttri skrá (eða límdu inn JSON-ið hennar). WashData greinir hana og sýnir nákvæmlega hvað er hægt að flytja inn áður en nokkuð breytist.", + "export_select_intro": "Hakaðu við það sem á að fylgja. Snið án lotanna sinna flytja samt út kerfi sem hægt er að samsvara.", + "import_analyze_hint": "Hladdu inn útfluttri skrá (eða límdu inn JSON-ið hennar). WashData sýnir hvað er hægt að flytja inn áður en nokkuð breytist.", "analyzing": "Greini…", - "device_type_mismatch_warn": "Þessi útflutningur er frá annarri tegund tækis ({src} á móti {local}). Enn er hægt að flytja inn kerfi og lotur sem viðmiðunargögn, en tækjabundnar stillingar og innflutningur á raunverulegri sögu eru óvirk.", + "device_type_mismatch_warn": "Þessi útflutningur er frá annarri tegund tækis ({src} á móti {local}). Enn er hægt að flytja inn kerfi og lotur sem viðmiðunargögn, en ekki tækjastillingar eða raunverulega sögu.", "merge_hint": "Innfluttum atriðum er bætt við; ekkert staðbundið tapast. Nafnaárekstrar eru leystir hér að neðan.", "replace_warn": "Hver valinn flokkur er hreinsaður og settur í staðinn úr skránni. Óvaldir flokkar haldast óbreyttir.", "dest_reference_hint": "Innfluttar lotur bæta aðeins kerfissamsvörun og hafa aldrei áhrif á notkunar-/orkutölfræði.", - "dest_real_history_hint": "Innfluttar lotur teljast sem eigin saga þessa tækis og fæða orku-/notkunartölfræði. Notaðu þetta til að flytja eitt tæki yfir í nýja uppsetningu.", + "dest_real_history_hint": "Innfluttar lotur teljast sem eigin saga og tölfræði þessa tækis. Notaðu þetta til að flytja tæki yfir í nýja uppsetningu.", "sug_muted": "Mun ekki stinga upp á þessari stillingu aftur", "sug_mute_failed": "Gat ekki þaggað tillögu", "sug_unmuted_all": "Þaggaðar tillögur endurstilltar", - "n_suggestions_muted": "{count} þaggaðar; sjálfvirki stillirinn leggur ekki til þessar.", + "n_suggestions_muted": "{count} tillögur þaggaðar; sjálfvirki stillirinn leggur þær ekki til.", + "n_suggestions_muted_one": "{count} tillaga þögguð; sjálfvirki stillirinn leggur hana ekki til.", + "n_suggestions_muted_other": "{count} tillögur þaggaðar; sjálfvirki stillirinn leggur þær ekki til.", "font_size_hint": "Gerðu allt á þessum glugga stærra eða minna. Gildir fyrir reikning þinn á þessum tæki.", - "import_history_description": "Varstu þegar með snjalltengil áður en þú fékkst WashData? Hlaðdu upp söguútflutningi frá aflmælinum hans, eða láttu lesa hana beint úr Home Assistant, og venjuleg greining fer yfir hana svo eldri lotur birtast í Hringrásir-listanum þínum, tilbúnar til að fá heiti.", - "hist_input_hint": "Hlaðdu upp CSV-skrá sem þú hlóðst niður úr Saga-spjaldinu (eining, staða, síðast breytt), eða láttu WashData lesa sögu mælisins beint. Greiningin fer svo yfir hana alveg eins og í beinni, og þú velur hvaða lotur á að halda af þeim sem finnast.", - "hist_recorder_hint": "Les frá þeirri dagsetningu sem þú velur og fram til nú. Home Assistant heldur nákvæmri sögu í 10 daga sjálfgefið og eftir það aðeins klukkustundarmeðaltölum, sem eru of grófgerð til að greina lotur úr - veldu dagsetningu lengra aftur í tímann aðeins ef recorder er stilltur á að geyma meira.", - "hist_scanning": "Sagan þín er spiluð í gegnum greininguna. Þetta gengur í bakgrunni - þú getur lokað þessum glugga og komið til baka síðar.", + "import_history_description": "Varstu með snjalltengil áður en þú fékkst WashData? Hlaðdu upp aflsögu hans eða lestu hana úr Home Assistant. Greiningin fer yfir hana og fyrri lotur birtast í Lotur-listanum þínum, tilbúnar til að fá heiti.", + "hist_input_hint": "Hlaðdu upp CSV úr Saga-spjaldinu (eining, staða, síðast breytt), eða láttu WashData lesa sögu skynjarans beint. Greiningin í beinni fer yfir hana og þú velur hvaða lotum á að halda.", + "hist_recorder_hint": "Les frá valinni dagsetningu og fram til nú. Home Assistant geymir nákvæma sögu í 10 daga sjálfgefið; eldri klukkustundarmeðaltöl eru of gróf fyrir lotugreiningu.", + "hist_scanning": "Sagan þín fer í gegnum greininguna í bakgrunni. Þú getur lokað þessum glugga og komið aftur síðar.", "hist_imported_count": "{n} lotur fluttar inn.", "hist_duplicates": "{n} voru þegar fluttar inn og var sleppt.", "hist_capped": "Hámarki innfluttra lota fyrir hvert tæki var náð; það sem eftir stóð var ekki vistað.", - "hist_next_step": "Þær eru í Hringrásir-listanum þínum, merktar sem innflutt saga. Opnaðu eina og notaðu Merki til að gefa kerfinu sem hún tilheyrir heiti.", + "hist_next_step": "Þær eru í Lotur-listanum þínum, merktar sem innflutt saga. Opnaðu eina og notaðu Merki til að gefa kerfi hennar heiti.", "hist_rows_read": "{n} mælingar lesnar", "hist_entity_substituted": "las {used} (þetta tæki er stillt á {wanted})", "hist_breaks": "{n} göt þar sem mælirinn var ekki tiltækur", "hist_other_entity": "{n} mælingum fyrir aðrar einingar var sleppt", "hist_skipped_spans": "Bil sem var sleppt", - "hist_settings_used": "Greint með núverandi stillingum þessa tækis (lágmarksafl {w} W, slökkviðráð {s} s).", + "hist_settings_used": "Greint með núverandi stillingum þessa tækis (lágmarksafl {w} W, slökkvitöf {s} s).", "hist_none_found": "Engar lotur greindust í þeirri sögu.", "hist_found": "Fann {n} lotur. Taktu hakið af öllu sem lítur ekki út eins og raunveruleg keyrsla - ekkert er vistað fyrr en þú flytur inn.", "hist_scan_capped": "Aðeins fyrstu lotutillögurnar eru sýndar ({n} fundust).", @@ -1097,55 +1030,86 @@ "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.", - "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.", + "imported_history_readonly": "Fannst í innfluttri aflsögu. Hún hjálpar samsvöruninni en telst ekki með í tölfræðinni þinni og ekki er hægt að klippa hana eða skipta henni. Merktu hana til að gefa kerfinu heiti.", + "odometer_intro": "Þessi samtala hækkar aðeins. Það að eyða lotu lækkar hana ekki, svo þjónustuáminningar haldist réttar.", + "odometer_correct_hint": "Stilltu þetta ef WashData taldi keyrslu sem aldrei fór fram, eða ef tækið keyrði 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}", "toast_range_required": "Stilltu gilt Frá/Til-svið.", - "no_profiles_option": "(engir prófílar ennþá, bættu einum við)" + "no_profiles_option": "(engir prófílar ennþá, bættu einum við)", + "compare_cycles_title": "Bera saman {count} lotur", + "edit_builtin_phase": "Þetta er innbyggður áfangi. Við vistun verður til sérsniðið afrit; eyddu afritinu til að endurheimta upprunalega útgáfu.", + "fetch_error": "Ekki tókst að hlaða gögnum.", + "connection_lost": "Tenging rofnaði - sýnir gögn frá kl. {time}.", + "pg_notify_partial": "Merki sýna aðeins tilkynningar um upphaf, næstum lokið, lok, áfanga og hljóðlátan tíma. Uppfærslur í beinni, losunaráminningar og viðvaranir um of langa keyrslu eru ekki hermdar.", + "connect_to_confirm": "Tengstu í stillingatannhjólinu til að staðfesta eða gefa einkunn.", + "coverage_gap_cluster": "Nýlegar ómerktar lotur ({count}), um {min} mín. hver, líta út eins og eitt kerfi sem þú hefur ekki enn búið til.", + "pg_label_yes": "Yrði merkt \"{name}\" við lok lotu", + "pg_label_no": "Myndi biðja um endurskoðun: {reason}", + "live_match_debug_tip": "Öryggi: hversu vel núverandi aflferill passar við efsta kandídatsprófílinn (0-100%). Óljóst: tveir bestu kandídatarnir eru innan við 5% hvor frá öðrum - merkið er óvíst þar til lotunni lýkur.", + "automation_alias_started": "{device}: lota hafin", + "automation_alias_finished": "{device}: lotu lokið", + "automation_desc_started": "Keyrir þegar lota hefst á {device}. Bættu við aðgerðum (tilkynning, ljós, ...).", + "automation_desc_finished": "Keyrir þegar lotu lýkur á {device}. Bættu við aðgerðum (tilkynning, ljós, ...).", + "feedback_detected": "WashData greindi: {name}", + "pg_outcome_stale": "Stillingum hefur verið breytt síðan þessi hermun keyrði. Hún sýnir enn gömlu niðurstöðuna.", + "pg_compare_profile_tip": "Velur aðeins sniðið sem er teiknað yfir ritið. Hermunin finnur alltaf samsvörun fyrir lotuna sjálf, eins og samþættingin gerir í rauntíma.", + "pg_task_busy": "Prófa á sögu eða Fínstilla er þegar í gangi fyrir þetta tæki. Bíddu eftir því eða hættu við.", + "pg_sweep_guarded": "Lýkur fleiri lotum of snemma, skiptir fleiri þeirra upp eða missir af lotum sem núverandi stilling nær, svo þetta gildi er aldrei ráðlagt.", + "pg_sweep_n_cycles": "yfir {count} lotur", + "pg_sweep_n_cycles_one": "yfir {count} lotu", + "pg_sweep_n_cycles_other": "yfir {count} lotur", + "pg_sweep_keep_current": "Halda núverandi gildi ({value}, {metric}): ekkert gildi stóð sig betur um að minnsta kosti eina lotu.", + "hist_truncated": "stöðvaðist við hámarkið {n} mælingar, svo afgangurinn af sögunni var ekki skoðaður", + "hist_warn_kw": "Hæsta mælingin er aðeins {w}. Þessi skynjari virðist skila kílóvöttum; WashData þarf vött, svo engar lotur er hægt að greina.", + "hist_warn_naive_time": "{n} tímastimplar hafa ekkert tímabelti og voru lesnir sem UTC. Ef skráin notar staðartíma hliðrast lotur um UTC-frávikið þitt, og mælingar í kringum skipti yfir í sumartíma geta glatast.", + "hist_warn_naive_time_one": "{n} tímastimpill hefur ekkert tímabelti og var lesinn sem UTC. Ef skráin notar staðartíma hliðrast lotur um UTC-frávikið þitt, og mælingar í kringum skipti yfir í sumartíma geta glatast.", + "hist_warn_naive_time_other": "{n} tímastimplar hafa ekkert tímabelti og voru lesnir sem UTC. Ef skráin notar staðartíma hliðrast lotur um UTC-frávikið þitt, og mælingar í kringum skipti yfir í sumartíma geta glatast.", + "hist_tail_trimmed": "{min} mín. bið eftir lok lotunnar var klippt af, eins og samþættingin gerir í rauntíma", + "invalid_threshold_pair": "Stöðvunarþröskuldur ({stop} W) verður að vera undir Ræsingarþröskuldi ({start} W). Ekkert var vistað; breyttu báðum saman.", + "import_undo_hint": "Endurheimtarpunktur frá því fyrir síðasta innflutning ({when}) er geymdur með {profiles} prófíl(um) og {cycles} lotu(m).", + "replace_undo_hint": "Endurheimtarpunktur er vistaður fyrst, svo þú getur afturkallað þennan innflutning undir Útflutningur / Innflutningur.", + "pg_in_sample_note": "Þessar lotur hjálpuðu til við að byggja sniðin sem þær eru bornar saman við, svo niðurstöðurnar eru bjartsýnar: búast má við aðeins færri réttum samsvörunum á nýjum lotum." }, "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ð", - "smart_termination_duration_ratio": "Hlutfall af væntanlegri lengd samsvaraða kerfisins sem lota þarf að ná áður en snjöll lokun má ljúka henni snemma; lækkaðu það fyrir álags- eða hitaháðar vélar", + "smart_termination_duration_ratio": "Hlutfall af meðallengd kerfisins sem lota þarf að ná áður en snjöll lokun má ljúka henni snemma; lækkaðu það ef keyrslutímar eru breytilegir", "completion_min_seconds": "Stysta keyrsla sem telst raunveruleg lota", - "end_repeat_count": "Lágir álestrar í röð áður en lýkur", "interrupted_min_seconds": "Stuttar lotur merktar sem truflaðar", "min_off_gap": "Bil sem þarf til að aðgreina tvær lotur", "off_delay": "Sekúndur af lágu afli áður en lotu lýkur", "start_duration_threshold": "Sekúndur yfir þröskuldi til að staðfesta ræsingu", "start_threshold_w": "Lágmarksvött til að teljast ræst", "stop_threshold_w": "Undir þessu telst vélin af", - "dtw_bandwidth": "Þrep 3: Sakoe-Chiba-tímabjögunarband (0 = DTW af; sjálfgefið 0,2 = 20% af lotulegð)", - "corr_weight": "Þrep 2: jafnvægi milli ferilforms (fylgni) og aflstigs (MAE); sjálfgefið 0,45", - "duration_weight": "Þrep 4: hversu sterklega samræmi í keyrslulengd hefur áhrif á lokaskorið (sjálfgefið 0,22)", - "energy_weight": "Þrep 4: hversu sterklega samræmi í orkunotkun hefur áhrif á lokaskorið (sjálfgefið 0,22)", - "profile_match_min_duration_ratio": "Þrep 1: stysta lota sem hlutfall af lotulegð sniðs; sjálfgefið 0,1 (10%)", - "profile_match_max_duration_ratio": "Þrep 1: lengsta lota sem hlutfall af lotulegð sniðs; sjálfgefið 1,8 (180%)", - "keep_min_score": "Þrep 2: lægsta einkunn til að vera eftir í samsvöruninni; sjálfgefið 0,1 leyfir veika frambjóðendur, síðari þrep velja best", - "dtw_blend": "Þrep 3: 0 = aðeins kjarnaskor, 1 = aðeins DTW-skor, 0,5 = jöfn blanda (sjálfgefið)", - "dtw_ensemble_w": "Þrep 3 (ensemble-hamur): þunginn á skaluðum L1 gagnvart afleiðu-DTW; sjálfgefið 0,7 hlynnar að stiga-meðvitund", - "dtw_ddtw_scale": "Þrep 3: DDTW hálf-mettunarfjarlægð; minna = meira formnæmt (sjálfgefið 30)", - "dtw_refine_top_n": "Þrep 3: frambjóðendur sem DTW endurgerir skor fyrir; hækkið í 7-9 ef rétt snið lendir á 4.-5. sæti (sjálfgefið 5)", - "duration_scale": "Þrep 4: log-hlutfall þar sem samræmi í keyrslulengd helmingast; minna = strangara viðurlög (sjálfgefið 0,175)", - "energy_scale": "Þrep 4: log-hlutfall þar sem samræmi í orkunotkun helmingast; minna = strangara viðurlög (sjálfgefið 0,25)", + "profile_match_min_duration_ratio": "Stysta lotulengd sem hlutfall af lengd sniðsins; sjálfgefið 0,1 (10%)", + "profile_match_max_duration_ratio": "Lengsta lotulengd sem hlutfall af lengd sniðsins; sjálfgefið 1,8 (180%)", "anti_wrinkle_idle_timeout": "Leyfilegur kyrrðartími milli tveggja veltipúlsa áður en hrukkuleysi lýkur", "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_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", + "anti_wrinkle_exit_power": "Kyrrðarstig milli veltipúlsa; virkar aðeins yfir Stöðvunarþröskuldi", + "anti_crease_finalize_ratio": "Hlutfall af meðallengd kerfisins sem lota þarf að ná áður en hrukkuvarnarhalinn má ljúka henni; lækkaðu það á þurrkurum, hafðu þvottavélar á sjálfgefnu", "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.", - "anti_wrinkle": "Reglubundin velting eftir hringrás til að draga úr hrukkum.", - "cool_down": "Veltur án hita nálægt lok hringrásar.", + "anti_wrinkle": "Reglubundin velting eftir lotu til að draga úr hrukkum.", + "baking": "Hitaelement á háum hita virkt til að mynda skorpu og baka brauðið í gegn.", + "cooking": "Aðaleldunarstig með virkum hitara og viftu.", + "cool_down": "Veltur án hita undir lok lotunnar.", + "cool_down_af": "Kæling eingöngu með viftu eftir hitun.", "drain_and_switch": "Aðlögunartímabil frá þvotti yfir í þurrkun.", "dry": "Þurrkunarstig með hitara og/eða afgangshita.", "drying": "Aðal upphitað veltitímabil.", "heat_up": "Upphitun hitari í upphafi áður en full þurrkun hefst.", + "keep_warm": "Lágur hiti til að halda matnum heitum.", + "keep_warm_bm": "Lágur hiti til að halda brauðinu heitu eftir bakstur.", + "kneading": "Mótorinn hrærir og hnoðar deigið. Mikil aflnotkun.", + "pause_af": "Stutt hlé til að hrista eða athuga matinn.", + "pre_heat": "Upphafshitun klefans fyrir fulla eldun.", "pre_rinse": "Sprautaðu fyrst fyrir þvottaefnisþvott.", + "proving": "Hefun við lágan hita svo gerið vinni og deigið lyftist.", + "resting": "Stutt hlé á lágu afli milli hnoðunarstiga svo glútenið slakni.", "pre_wash": "Upphafsbleyting eða formeðferð fyrir aðalþvott.", "rinse": "Hreinvatnsskolunarstig. Þessi áfangi getur endurtekið sig mörgum sinnum.", "sanitize": "Háhitaþrifastig fyrir hreinsunaráætlanir.", @@ -1158,6 +1122,7 @@ "wash_dw": "Aðalþvottaefnisþvottur með hita." }, "placeholder": { + "custom_task": "t.d. hreinsa hurðarþéttingu", "add": "bæta við…", "group_name": "t.d. Bómull 2:47", "json_buttons": "[{\"action\":\"ID\",\"title\":\"Label\"}]", @@ -1185,10 +1150,10 @@ "section": { "dishwasher": { "label": "Uppþvottavél", - "intro": "Meðhöndlun loka lotu fyrir uppþvottavélar, sem ljúka yfirleitt með löngum, næstum hljóðlátum þurrkfasa á undan stuttri lokaútdælingu." + "intro": "Meðhöndlun loka fyrir uppþvottavélar, sem þurrka oft lengi hljóðlaust á undan stuttri lokaútdælingu." }, "anti_wrinkle": { - "intro": "Hrukkuleysi-/krumpluleys-stilling greinir lágtaktsveltiboð eftir aðalfasann og heldur þeim tengdum við lokinni lotu.", + "intro": "Heldur aflitlum veltipúlsum þurrkara eftir aðalfasann við lotuna sem lauk.", "label": "Hrukkuleysi" }, "basic": { @@ -1196,7 +1161,7 @@ "label": "Grunnstillingar" }, "delay": { - "intro": "Greining á seinkaðri ræsingu þekkir þegar tæki er tengt en hefur ekki byrjað lotu.", + "intro": "Greinir tæki sem er í gangi og bíður eftir seinkaðri ræsingu.", "label": "Seinkuð ræsing" }, "detection": { @@ -1220,161 +1185,153 @@ "label": "Tímasetning og gæsla" }, "triggers": { - "intro": "Valfrjálsir ytri merkar: lokakveikin, dyraskynjari, hlébilaskipti og losunarminnir.", + "intro": "Valfrjálsir aukahlutir: lokakveikja, hurðarskynjari, hlérofi og losunaráminning.", "label": "Kveikjar og dyr" - }, - "phase_eta": { - "label": "Tími sem eftir er", - "intro": "Tími sem eftir er sem tekur mið af fösum, fyrir vélar þar sem lengd lotu ræðst af hitastigi eða vindingu." } }, "setting": { "dishwasher_end_spike_quiet_release": { "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." + "doc": "Eftir væntanlega lengd: hversu lengi uppþvottavélin þarf að vera undir Stöðvunarþröskuldi áður en WashData hættir að bíða eftir lokaútdælingunni. Hækkaðu það ef sein útdæling eftir langa þurrkun fer fram hjá. Biðin fer aldrei yfir 30 mínútur." }, "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." + "doc": "Hlutfall af meðallengd kerfisins sem lota þarf að ná áður en aflítil velta hennar telst hrukkuvarnarhalinn. Lækkaðu það (t.d. 0,75) á þurrkurum með rakaskynjun. Hafðu þvottavélar á sjálfgefnu (0,98), annars getur þvottur endað í miðjum klíðum." }, "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" + "doc": "Haltu stuttum, aflitlum veltipúlsum þurrkara eftir aðalhitunarfasann við lotuna sem lauk, í stað þess að túlka þá sem nýjar lotur.", + "label": "Virkja hrukkuleysisgreiningu" }, "anti_wrinkle_exit_power": { - "doc": "Krafturinn verður að falla undir þetta á milli púlsa til að hrukkuvarnarstilling haldist virkur.", + "doc": "Í hrukkuleysisstillingu telst afl undir þessu kyrrð milli veltipúlsa, og kyrrð lengri en Hám. púlsbil lýkur stillingunni. Stöðvunarþröskuldurinn er notaður ef hann er hærri, svo þetta breytir aðeins einhverju þegar það er sett yfir Stöðvunarþröskuld.", "label": "Útgönguaflþröskuldur" }, "anti_wrinkle_idle_timeout": { - "doc": "Hversu lengi vélin má vera kyrr á milli tveggja veltipúlsa áður en hrukkuleysisstillingu lýkur. Stilltu það hærra en lengsta bilið sem þurrkarinn þinn skilur eftir á milli púlsa, annars er hver síðari púls lesinn sem röng ræsing.", + "doc": "Hversu lengi þurrkarinn má vera kyrr á milli veltipúlsa áður en hrukkuvarnarstillingu lýkur. Stilltu það yfir lengsta hlé þurrkarans, annars eru síðari púlsar túlkaðir sem rangar ræsingar.", "label": "Hám. púlsbil" }, "anti_wrinkle_max_duration": { - "doc": "Púlsar sem eru lengri en þetta eru meðhöndlaðir sem alvöru hringrás frekar en hrukkuvörn.", + "doc": "Lengri púls telst raunveruleg lota, ekki hrukkuvarnarvelta.", "label": "Hámarkslengd" }, "anti_wrinkle_max_power": { - "doc": "Púls yfir þessum krafti er meðhöndluð sem alvöru ný hringrás, ekki hrukkuvörn. Stillt rétt fyrir ofan fallpúlsaflið.", + "doc": "Púls yfir þessu afli telst raunveruleg ný lota, ekki velta. Stilltu það rétt yfir afl veltipúlsanna þinna.", "label": "Hám. hrukkuleys-afl" }, "auto_label_confidence": { - "doc": "Ef samsvörunarskorið í lok lotu er við eða yfir þessu er kerfið merkt sjálfkrafa án staðfestingar. Hækkaðu það til að krefjast meiri vissu fyrir sjálfvirka merkingu; lækkaðu það til að auka sjálfvirkni. Virkar í tengslum við „Námsöryggi“ hér fyrir neðan.", + "doc": "Samsvörunarskor í lok lotu sem kerfið er merkt við eða yfir án þess að spyrja. Hækkaðu það fyrir meiri vissu; lækkaðu það til að merkja fleiri lotur sjálfkrafa. Hafðu það yfir Námsöryggi.", "label": "Sjálfvirk merkingaöryggi" }, "auto_maintenance": { - "doc": "Keyrðu næturþrif: endurbyggðu aflsvið prófíla, endurreiknaðu heilsu lotanna, fjarlægðu kembiferla og geymdu nýjustu loturnar.", + "doc": "Næturhreinsun: merkir gamlar lotur sem passa nú greinilega við kerfi, endurbyggir ferla kerfa, uppfærir fráviksmerkingar og heilsu lotna, og fjarlægir villuleitarspor ef slökkt er á Vista villuleitarspor. Lotum og aflsporum þeirra er alltaf haldið.", "label": "Sjálfvirkt viðhald (næturþrif)" }, "completion_min_seconds": { - "doc": "Hringrásum sem eru styttri en þetta er fleygt sem draugalotum (prófun, opnun á hurðinni til að bæta við sokk).", + "doc": "Lotur sem eru styttri en þetta (prufukeyrslur, hurð opnuð til að bæta við sokki) eru vistaðar sem Truflað og fá enga lokatilkynningu.", "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." + "doc": "Bætir mælingum úr misheppnuðum ræsitilraunum framan við lotu, fyrir vélar sem þreifa fyrir sér áður en þær fara í gang (hurðarlæsing, fyrsta vatnsáfylling). 300 s duga flestum; 0 = slökkt. Skráðar ræsingar færast fyrr, svo lærðar lengdir hliðrast um tíma." }, "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.", + "doc": "Aflið verður að haldast á biðstöðubilinu svona lengi áður en tækið telst bíða eftir ræsingu.", "label": "Staðfestingarsekúndur" }, "delay_start_detect_enabled": { - "doc": "Finndu þegar kveikt er á heimilistækinu og bíður (seinkað ræsingu / biðstöðu) en hefur ekki hafið lotuna, þannig að biðhleðsla telst ekki vera í gangi.", - "label": "Virkja seinkuð-ræsing-greiningu" + "doc": "Greindu tæki sem er í gangi og bíður eftir seinkaðri ræsingu, svo biðstöðunotkun þess teljist ekki lota í gangi.", + "label": "Virkja greiningu seinkaðrar ræsingar" }, "delay_timeout_hours": { - "doc": "Hættu að bíða í seinkaðri ræsingu eftir þessa marga klukkutíma og farðu aftur í aðgerðalausa stöðu, þannig að vél sem er eftir afl en aldrei ræst bíður ekki að eilífu.", + "doc": "Hættu að bíða eftir seinkaðri ræsingu eftir svona marga klukkutíma og farðu aftur í aðgerðaleysi.", "label": "Tímamarksklst." }, "device_type": { - "doc": "Tækjaflokkur. Stillir skynsamlegar sjálfgefnar greiningar (þröskuldar, slökkt á seinkun, lokameðferð) stillt fyrir þá tegund tækis; breyta því aðeins ef tækið var upphaflega sett upp sem ranga gerð.", + "doc": "Stillir sjálfgefin greiningargildi (þröskulda, slökkvitöf, meðhöndlun loka) fyrir þessa tegund tækis. Breyttu því aðeins ef tækið var sett upp sem röng tegund.", "label": "Tegund tækis" }, "door_sensor_entity": { - "doc": "Valfrjáls hurð tvöfaldur skynjari. Notað til að greina hvenær heimilistækið hefur verið opnað/affermt eftir lotu.", + "doc": "Valfrjáls hurðarskynjari. Sýnir hvenær tækið var opnað til að tæma það eftir lotu.", "label": "Dyraskynjareining" }, "duration_tolerance": { - "doc": "Umburðarlyndi fyrir mati sem eftir er af tíma (námsendurgjöf, ekki samsvörun). Ef raunveruleg tímalengd er innan +/-X% af áætluninni telst það góð samsvörun.", - "label": "Matþolinmæði" + "doc": "Aðeins námsendurgjöf: lota innan +/-X% af áætlaðri lengd sinni telst gott mat. Hefur ekki áhrif á samsvörun.", + "label": "Vikmörk mats" }, "enable_ml_models": { - "doc": "Á meðan hringrás er í gangi, láttu líkönin betrumbæta niðurstöðurnar í beinni: stöðugri tíma sem eftir er og orku-/kostnaðaráætlun, og vörn gegn ótímabærum stöðvun við endaskynjun (það getur alltaf seinkað frágangi, aldrei endað snemma og er takmarkað). Notar fínstilltu gerðir þínar þegar þær eru tiltækar, annars þær innbyggðu. Slökkt = aðeins klassískt afl-undirstaða rökfræði (enn áreiðanleg).", + "doc": "Notaðu líkan sem er þjálfað á þessari vél til að spá fyrir um orku og kostnað lotu í gangi, og leyfðu vel þekktu kerfi sem stöðvast óvenju snemma að ljúka án fullrar slökkvitafar. Slökkt: aðeins aflbyggð rökfræði.", "label": "Nota greind líkön meðan á lotu stendur" }, "end_energy_threshold": { - "doc": "Meðan á niðurtalningunni stendur er uppsöfnuð orka (vött x tími) borin saman við þennan þröskuld. Ef farið er yfir það endurstillist niðurtalningin - þannig að krumluvörn og uppþvottavélarþurrkunarhalar eru festir við hringrásina í stað þess að stytta þær. Hækka það ef lotum lýkur of snemma meðan á kólnun stendur; lækka það ef uppgötvun er treg.", + "doc": "Orka sem leyfð er á meðan slökkvitöfin stendur. Meira en þetta og lotan heldur áfram, svo veltur og þurrkhalar haldast hluti af henni. Lækkaðu það ef lotur enda á meðan hali stendur; hækkaðu það ef loknar lotur halda áfram að bíða.", "label": "Lokaorka" }, - "end_repeat_count": { - "doc": "Fjöldi samfelldra aflestra undir stöðvunarþröskuldi sem þarf áður en lotunni lýkur. 1 er fínt fyrir flestar innstungur. Hækkaðu í 2-3 ef snjalltappið þitt tilkynnir stundum um rangt núll sýni í miðjum lotu og loturnar þínar eru að ljúka of snemma.", - "label": "Fjöldi lokakorna" - }, "energy_price_entity": { - "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.", + "doc": "Skynjari með núverandi verði á kWh (t.d. breytileg gjaldskrá). Hann tekur fram yfir fasta verðið hér að neðan, svo hann verður að vera verð, ekki afl- eða orkuskynjari innstungunnar.", "label": "Orkuverðeining" }, "energy_price_static": { - "doc": "Fast verð á kWst notað fyrir kostnaðartölur þegar engin lifandi verðeining er stillt hér að ofan.", + "doc": "Fast verð á kWh, notað þegar engin verðeining er stillt.", "label": "Fast orkuverð (á kWh)" }, "energy_sensor": { - "doc": "Valfrjáls uppsafnaður orkuteljari (total_increasing í kWh/Wh, t.d. eigin líftímamælir innstungunnar). Þegar hann er stilltur er uppgefin orka hverrar lotu tekin úr mismun þessa teljara milli upphafs og enda, sem kemur í veg fyrir vantalninguna sem hlýst af því að heilda hægvirkan aflskynjara. Fer aftur í heildaða gildið ef mælingin vantar, eining hennar er óþekkt eða mismunurinn er ekki jákvæður. Skildu eftir autt til að halda áfram að heilda aflskynjarann.", + "doc": "Valfrjáls kWh- eða Wh-teljari (total_increasing, t.d. eigin mælir innstungunnar). Orka lotunnar er þá lesin af honum, sem er nákvæmara en að reikna hana út frá hægum aflskynjara. Ef mæling hans er ónothæf er reiknaða gildið notað.", "label": "Orkumælieining" }, "expose_debug_entities": { - "doc": "Birtu auka greiningar HA einingar (samsvörun, tvíræðni, innri hluti ríkisins). Slökkt heldur einingarlistanum hreinum fyrir venjulega notkun.", + "doc": "Bættu við greiningareiningum (samsvörunaröryggi, tvíræðni, innra ástand). Slökkt heldur einingalistanum hreinum.", "label": "Birta villuleitareiningar" }, "external_end_trigger": { - "doc": "Tvöfaldur skynjari þar sem ástandsbreytingin markar lok lotunnar (t.d. „lokið“ tengiliður tækis eða fylgisamþætting).", + "doc": "Tvíundarskynjari þar sem breyting markar lok lotunnar (t.d. „lokið“-snerta eða önnur samþætting).", "label": "Ytri kveikjaeining" }, "external_end_trigger_enabled": { - "doc": "Láttu utanaðkomandi tvíundarskynjara gefa merki um lok lotu, auk innbyggðrar raforkugreiningar.", + "doc": "Leyfðu tvíundarskynjara að gefa merki um lok lotu, til viðbótar við aflbyggða greiningu.", "label": "Virkja ytri lokakveikin" }, "external_end_trigger_inverted": { - "doc": "Líttu á að kveikjuskynjarinn slekkur á (frekar en KVEIKT) sem lok lotumerkis.", + "doc": "Líttu á það þegar skynjarinn fer í SLÖKKT (ekki KVEIKT) sem lokamerkið.", "label": "Snúa ytri kveikin (kvikna á SLÖKKT)" }, "learning_confidence": { - "doc": "Ef samsvörunarskorið fellur á milli þessa og „Sjálfvirk merkingaöryggi“ biður endurgjöfstilkynning þig um að staðfesta kerfið sem var auðkennt. Undir þessu skori er samsvörunin of óviss til að birta. Verður að vera lægra en „Sjálfvirk merkingaöryggi“.", + "doc": "Lægsta samsvörunarskor til að merkja lotu sem er lokið. Skýr sigurvegari er merktur; tvísýnt tilvik fer í yfirferðarbiðröðina í Lotum. Fyrir neðan þetta er ekkert merkt eða spurt. Hafðu það undir Sjálfvirku merkingaöryggi.", "label": "Námsöryggi" }, "linked_device": { - "doc": "Hægt er að hreiða þetta WashData tæki undir annað tæki (t.d. snjalltappið) í HA tækjaskránni, sýnt sem „Tengt í gegnum ...“.", + "doc": "Valfrjálst. Sýndu þetta WashData-tæki undir öðru tæki (t.d. snjalltenglinum) í Home Assistant, sem „Tengt í gegnum ...“.", "label": "Flokka undir tæki" }, "min_off_gap": { - "doc": "Ef vélin slekkur á sér í skemmri tíma en þennan tíma er kveikt/slökkt/kveikt röð meðhöndluð sem eina samfellda lotu. Kemur í veg fyrir að þvottakerfi (vélin slekkur á sér í nokkrar mínútur í miðjum þvotti) skiptist í tvær aðskildar lotur. Stilltu það styttra en bilið á milli bak-til-baks álags ef þú vilt að þær teljist sem aðskildar lotur. Sjálfgefnar stillingar tækis vernda dæmigerða hlé í hringrás fyrir hvert tæki.", + "doc": "Slökkt tímabil sem er styttra en þetta skiptir ekki lotu, svo bleytihlé haldast í einum þvotti. Hafðu það styttra en hléið milli þvotta sem keyrðir eru hver á eftir öðrum, annars renna þeir saman í eina lotu.", "label": "Lágm. slökkubil" }, "min_power": { - "doc": "Algjört lágmarksafl talið virkt. Lestrar fyrir neðan þetta eru meðhöndlaðir sem 0 W (biðstaða), sem síar út draugaálag á snjalltengjum og biðstöðu LED.", + "doc": "Mælingar undir þessu teljast 0 W (biðstaða). Síar burt biðstöðunotkun snjalltengla og biðstöðuljósa.", "label": "Lágmarksafl" }, "ml_training_enabled": { - "doc": "Skoðaðu endurskoðaða lotur þínar reglulega yfir nótt og fínstilltu módelin að þessari tilteknu vél. Breytingu er aðeins haldið þegar hún skorar raunverulega betur á úthaldslotum, þannig að þetta getur aðeins hjálpað eða verið óbreytt - aldrei dregið til baka.", - "label": "Læra frá þessar vél" + "doc": "Endurþjálfaðu orkulíkanið á loknum lotum þessarar vélar öðru hvoru. Nýju líkani er aðeins haldið ef það slær einfalda matið á prófunarlotum.", + "label": "Læra af þessari vél" }, "ml_training_hour": { - "doc": "Staðbundinn klukkutími dags (0-23) til að gera fínstillingu á einni nóttu. Veldu rólegan tíma eins og 2 (02:00).", + "doc": "Klukkustund dags (0-23) fyrir næturþjálfun. Veldu rólegan tíma, t.d. 2.", "label": "Læra á klst." }, "ml_training_interval_days": { - "doc": "Athugaðu aftur fyrir umbætur í mesta lagi einu sinni á þessa marga daga.", + "doc": "Endurþjálfaðu í mesta lagi einu sinni á svona mörgum dögum.", "label": "Athuga í mesta lagi á hverju" }, "ml_training_min_cycles": { - "doc": "Bíddu þar til að minnsta kosti svona margar lotur hafa verið skráðar áður en þú fínstillir, svo það er af nógu að læra.", + "doc": "Bíddu eftir að minnsta kosti svona mörgum skráðum lotum áður en þjálfað er.", "label": "Lotur þarf fyrst" }, "name": { - "doc": "Sýningarnafn sýnt í HA samþættingarlistanum og tækjaskránni.", + "doc": "Heitið sem birtist í Home Assistant fyrir þetta tæki.", "label": "Nafn tækis" }, "no_update_active_timeout": { - "doc": "Ef engar afluppfærslur berast í svona langan tíma á meðan hún er í gangi, gerðu ráð fyrir að innstungan hafi dottið úr sambandi og þvingað til að stöðva til að forðast uppvakningahring. Sjálfgefin 600 s gerir ráð fyrir skýja- eða möskvatöf.", + "doc": "Ef engin afluppfærsla berst svona lengi á meðan lota er í gangi er gert ráð fyrir að innstungan sé ótengd og lotan er þvinguð til að stöðvast. Sjálfgefið 600 s gefur svigrúm fyrir tafir í skýi eða möskvaneti.", "label": "Engin-uppfærsla-tímamark" }, "notify_before_end_minutes": { @@ -1382,179 +1339,171 @@ "label": "Forlokaviðvörun" }, "notify_channel": { - "doc": "Nafn Android tilkynningarásar fyrir upphaf/bein skilaboð (stýrir hljóði á hverja rás og forgang í farsímaforritinu). Blank notar sjálfgefið fylgiforrit.", + "doc": "Android-rás fyrir upphafsskilaboð og skilaboð í beinni; rásin ræður hljóði og forgangi. Autt: sjálfgefið í forritinu.", "label": "Android rás (ræsing/bein)" }, "notify_cycle_timers": { - "doc": "Tilkynningar á tilteknum mínútum í lotu (t.d. til að bæta við mýkingarefni). Skilaboð styður {device}, {program}, {minutes}. Virkjaðu sjálfvirka hlé til að gera hlé á þeim tímapunkti og fá gagnvirka tilkynningu með hnappi Halda áfram; halda áfram í gegnum spjaldið, hlé/halda áfram þjónustu eða tilkynningaaðgerðina.", + "doc": "Tilkynningar á tilteknum mínútum í lotu (t.d. til að bæta við mýkingarefni). Skilaboðin styðja {device}, {program}, {minutes}. Með Sjálfvirkt hlé er líka gert hlé á lotunni þar og tilkynningin fær Halda áfram-hnapp.", "label": "Lotuklukka" }, "notify_finish_channel": { - "doc": "Nafn Android tilkynningarásar fyrir lokaskilaboðin. Blank endurnotar upphafs-/beinarásina.", + "doc": "Android-rás fyrir lokaskilaboðin. Autt: notar sömu rás og upphaf/bein.", "label": "Android rás (lok)" }, "notify_finish_message": { - "doc": "Meginmál sent þegar lotu lýkur. Sniðbreytur: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}. {time_finished} og {vs_typical} eru gagnlegust hér.", + "doc": "Meginmál sent þegar lotu lýkur. Breytur: {device}, {program}, {duration} (mínútur), {duration_hm} (1 h 05 min), {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}, {status} (completed eða force_stopped; truflaðar lotur senda ekkert). Ef önnur breyta er notuð gildir sjálfgefni textinn.", "label": "Lokaskilaboð" }, "notify_finish_services": { - "doc": "tilkynna.* þjónustu kallað þegar lotu lýkur. Bættu við einu við hvert markmið; skildu eftir tómt fyrir tilkynningu um að ekki sé lokið.", + "doc": "notify.* þjónustur sem kallað er á þegar lotu lýkur. Skildu eftir autt fyrir engar.", "label": "Lokaþjónustur" }, "notify_fire_events": { - "doc": "Kveiktu einnig á ha_washdata_* atburðum við upphaf/lok lotu svo þú getir smíðað þína eigin sjálfvirkni.", + "doc": "Sendu einnig ha_washdata_* atburði við upphaf og lok lotu, fyrir þína eigin sjálfvirkni.", "label": "Kveikja HA viðburði fyrir tilkynningar" }, "notify_icon": { - "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.", + "doc": "Lítið tákn á tilkynningunni (t.d. mdi:washing-machine). Aðeins sent til mobile_app-viðtakenda, ekki notify-hópa. Á iOS kemur það í stað forritstáknsins og krefst fylgiforritsins 2026.8 eða nýrra. Autt: sjálfgefið á kerfinu.", "label": "Tilkynningatákn" }, "notify_live_chronometer": { - "doc": "Sýndu niðurtalningartíma sem uppfærist í beinni í tilkynningunni (á kerfum sem styðja hana) í stað kyrrstöðu mats.", + "doc": "Sýndu niðurtalningu í gangi í tilkynningunni (þar sem það er stutt) í stað fasts mats.", "label": "Nota beina tímamæling" }, "notify_live_interval_seconds": { - "doc": "Hversu oft tilkynningar um framvindu í beinni eru endurnýjaðar á meðan hringrás er í gangi.", + "doc": "Hversu oft framvindutilkynningar í beinni eru uppfærðar á meðan lota er í gangi.", "label": "Bein uppfærsluabil" }, "notify_live_overrun_percent": { - "doc": "Ef lota fer yfir áætlun sína um meira en þetta hlutfall, sendu umframviðvörun.", + "doc": "Uppfærslur í beinni hætta þegar lota fer þetta mörg prósent fram yfir væntanlega lengd (20 = allt að 1,2x væntanlegur tími).", "label": "Bein yfirkeyrð % áður en viðvörun" }, "notify_live_services": { - "doc": "tilkynna.* þjónustu kallað á lifandi framvinduuppfærslur á meðan hringrás keyrir. Skildu eftir autt til að slökkva á tilkynningum um framvindu í beinni.", + "doc": "notify.* þjónustur fyrir framvinduuppfærslur í beinni á meðan lota er í gangi. Skildu eftir autt til að slökkva á þeim.", "label": "Beinar framfaraþjónustur" }, "notify_only_when_home": { - "doc": "Sendu aðeins tilkynningar þegar að minnsta kosti einn af tengdum einstaklingum (fyrir ofan) er heima.", + "doc": "Tilkynntu aðeins þegar að minnsta kosti einn af einstaklingunum hér fyrir ofan er heima.", "label": "Tilkynna aðeins þegar heima" }, "notify_people": { - "doc": "person.* einingar notaðar af „Tilkynna aðeins þegar heima“ til að ákveða hvort einhver sé heima.", + "doc": "person.* einingar sem „Tilkynna aðeins þegar heima“ athugar.", "label": "Fólk (fyrir Aðeins Heima)" }, "notify_pre_complete_message": { - "doc": "Meginmál viðvörunar fyrir lok / næstum búið. Sniðbreytur: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Texti hverrar framvinduuppfærslu í beinni. Sniðbreytur: {device}, {program}, {minutes} (mínútur eftir). Ef önnur breyta er notuð gildir sjálfgefni textinn.", "label": "For-lokar­skilaboð" }, "notify_reminder_message": { - "doc": "Meginmál áminningar um að taka enn úr. Autt notar innbyggða sjálfgefna gildið. Sniðbreytur: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Texti Forlokaviðvörunar, sem er send einu sinni. Sniðbreytur: {device}, {program}, {minutes} (stillingin Forlokaviðvörun). Ef önnur breyta er notuð gildir sjálfgefni textinn.", "label": "Minningarboð" }, "notify_start_message": { - "doc": "Meginmál sent þegar lota hefst. Sniðbreytur: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Meginmál sent þegar lota hefst. Sniðbreytur: {device}, {program}. Ef önnur breyta er notuð gildir sjálfgefni textinn.", "label": "Ræsingarskilaboð" }, "notify_start_services": { - "doc": "tilkynna.* þjónustu sem hringt er í þegar hringrás hefst. Bættu við einu við hvert markmið (sími, mælaborð osfrv.); skildu eftir tómt fyrir engin byrjunartilkynning.", + "doc": "notify.* þjónustur sem kallað er á þegar lota hefst (sími, mælaborð, ...). Skildu eftir autt fyrir engar.", "label": "Ræsingarþjónustur" }, "notify_timeout_seconds": { - "doc": "Hunsaðu tilkynninguna sjálfkrafa eftir þessar margar sekúndur (á kerfum sem styðja hana). 0 geymir það þar til henni er vísað frá handvirkt.", + "doc": "Lokaðu tilkynningunni eftir svona margar sekúndur (þar sem það er stutt). 0 heldur henni þar til þú lokar henni.", "label": "Sjálfvirk lokun eftir" }, "notify_title": { - "doc": "Titill tilkynningar. Sniðbreytur: {device}, {program}, {duration}, {energy}, {cost}, {date}, {time}, {minutes}.", + "doc": "Titill tilkynningar. Aðeins {device} er skipt út; ef önnur breyta er notuð gildir sjálfgefni titillinn.", "label": "Tilkynninga­fyrirsögn" }, "notify_unload_delay_minutes": { - "doc": "Mínútum eftir að lotu lýkur áður en þú sendir áminninguna um „afferma vélina“ sem enn bíður. Stilltu 0 til að slökkva á áminningunni.", + "doc": "Mínútur eftir að lotu lýkur þar til áminningin „tæmdu vélina“ berst. 0 slekkur á henni.", "label": "Losunarminni-seinkun" }, "notify_unload_repeat": { - "doc": "Heldur áfram að senda losunaráminninguna aftur á \"Losunarminni-seinkun\" mínútna fresti þar til losunin er staðfest eða þú ýtir á \"Hætta að minna\" í tilkynningunni. Krefst leiðar til að staðfesta losunina: Dyraskynjareiningar, Losunarstaðfestingareiningar eða valkostsins \"Staðfesta losun handvirkt\" hér að neðan. Hunsa-hnappurinn virkar á tilkynningum úr Home Assistant fylgiforritinu (farsíma).", + "doc": "Sendu losunaráminninguna aftur á \"Losunarminni-seinkun\" mínútna fresti þar til losunin er staðfest eða þú ýtir á \"Hætta að minna\" (farsímaforrit). Krefst Dyraskynjareiningar, Losunarstaðfestingareiningar eða \"Staðfesta losun handvirkt\".", "label": "Endurtaka þar til losað" }, "off_delay": { - "doc": "Tími til að bíða eftir að kraftur minnkar áður en hringrásinni er lokið. Ef straumur fer aftur innan þessa glugga heldur hringrásin óaðfinnanlega áfram - þetta brúar hlé á milli þvottastiga. Uppþvottavélar eru með langa þurrkunarfasa (slökkt á slökkt í 20-60 mín) þannig að slökkvunartöfin verður að fara yfir það til að halda öllum þvotti+þurrkum sem einni lotu.", - "label": "Slökkviðráð" + "doc": "Hversu lengi aflið þarf að haldast lágt áður en lotunni er lokið. Ef aflið kemur aftur innan þessa tíma heldur lotan áfram. Uppþvottavélar geta verið kyrrar í 20-60 mín. við þurrkun, svo stilltu þetta lengur.", + "label": "Slökkvitöf" }, "pause_cuts_power": { - "doc": "Þegar gert er hlé á lotu skaltu einnig slökkva á Switch Entity fyrir neðan. Aðeins fyrir tæki þar sem hægt er að skera úr klóinu á öruggan hátt í miðjum lotu.", + "doc": "Þegar gert er hlé á lotu er einnig slökkt á Rofaeiningunni hér fyrir neðan. Aðeins fyrir tæki sem þola örugglega að missa straum í miðri lotu.", "label": "Hlé slær einnig af straum (með rofa)" }, "power_off_delay": { - "doc": "Hversu lengi aflið verður að haldast undir Slökkunarþröskuldi eftir að lotu lýkur áður en staðan fer aftur í Slökkt. Aðeins notað þegar Slökkunarþröskuldur er yfir 0. Athugað á bakgrunnstaktinum, þannig að raunveruleg töf er námunduð upp að næsta stöðulokatakti.", + "doc": "Hversu lengi aflið þarf að haldast undir Slökkunarþröskuldi eftir lotu áður en staðan fer aftur í Slökkt. Aðeins notað þegar sá þröskuldur er yfir 0. Athugað reglulega, svo raunveruleg bið getur orðið aðeins lengri.", "label": "Slökkunartöf" }, "power_off_threshold_w": { - "doc": "Valfrjáls aflbyggð greining á slökkun. Þegar gildið er yfir 0 telst tækið slökkt og staðan fer aftur í Slökkt um leið og lotu er lokið og aflið helst undir þessu marki alla Slökkunartöfina. Hafðu á 0 til að slökkva á þessu (sjálfgefið). Stilltu það yfir raunverulegt slökkt-núllstig og undir Stöðvunarþröskuld og undir biðstöðunotkun tækisins í stöðunni lokið-en-kveikt; ef það er ekki undir Stöðvunarþröskuldi er það hunsað. Þegar kveikt er á því kemur það í stað framfaraendurstillingartafarinnar við að fara aftur í Slökkt, þannig að tæki sem er búið helst í Lokið/Hreint þar til raunverulega er slökkt á því.", + "doc": "Eftir lotu setur afl undir þessu í Slökkunartöf stöðuna á Slökkt, í stað þess að Töf á endurstillingu framvindu geri það. Stilltu það yfir notkun í slökktu ástandi, undir biðstöðu og Stöðvunarþröskuldi. 0 slekkur á því (sjálfgefið).", "label": "Slökkunarþröskuldur" }, "power_sensor": { - "doc": "Skynjaraeiningin tilkynnir um lifandi afl í vöttum fyrir þetta tæki (t.d. sensor.washer_power). Öll hringrásargreining byggist á þessu merki.", + "doc": "Skynjarinn sem gefur upp núverandi afl þessa tækis í vöttum (t.d. sensor.washer_power). Öll greining byggist á honum.", "label": "Aflskynjari" }, - "profile_duration_tolerance": { - "doc": "+/- bandið í kringum meðallengd prófíls sem notuð er við samsvörun. 0,25 þýðir að 60 mín prófíl passar við 45-75 mín lotur.", - "label": "Þolinmæði lotulendar sniðs" - }, "profile_match_interval": { - "doc": "Hversu oft á að reyna prófílsamsvörun meðan á hlaupi stendur. Sjálfgefin 300 s (5 mínútur) jafnvægi skynjunarhraða og CPU.", + "doc": "Hversu oft lota í gangi er borin saman við snið. Sjálfgefið 300 s (5 mín.).", "label": "Samsvarabil" }, "profile_match_max_duration_ratio": { - "doc": "Hámarks hringrásarlengd miðað við sniðið. 1.3 þýðir að hringrás verður að vera undir 130% af lengd prófílsins til að passa.", + "doc": "Kerfi er útilokað þegar lotan hefur gengið lengur en þetta margfeldi af dæmigerðri lengd þess. 1,8: lota sem er 80% lengri en kerfið samsvarar því ekki lengur.", "label": "Hám. lengdarhlutfall" }, "profile_match_min_duration_ratio": { - "doc": "Lágmarks hringrásarlengd miðað við sniðið. 0,9 þýðir að hringrás verður að vera að minnsta kosti 90% af lengd prófílsins til að passa.", + "doc": "Kerfi kemur aðeins til greina þegar lotan hefur gengið þetta hlutfall af dæmigerðri lengd þess. 0,10: 2 klukkustunda kerfi kemur til greina eftir 12 mínútur. Ekki beitt á fyrstu 15 mínútum lotu.", "label": "Lágm. lengdarhlutfall" }, "profile_match_threshold": { - "doc": "Lágmarks líkindisstig (0-1) sem krafist er í lok lotu til að samþykkja auðkenningu kerfis. Hækkaðu það til að draga úr röngum auðkenningum; lækkaðu það ef kerfi vélarinnar þinnar fá ekki samsvörun. Sjálfgefið 0.4 er íhaldssamur upphafspunktur.", + "doc": "Samsvörunarskor í beinni sem lota þarf áður en snjöll lokun eða hrukkuvarnarlok mega ljúka henni snemma. Breytir ekki kerfinu sem birtist. Hækkaðu það ef veikar samsvaranir ljúka lotum of snemma; lækkaðu það ef skýrar samsvaranir bíða út slökkvitöfina.", "label": "Samsvarþröskuldur" }, "profile_unmatch_threshold": { - "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“.", + "doc": "Ef samsvörunarskorið í beinni fer undir þetta er núverandi ágiskun um kerfi hreinsuð. Hafðu það aðeins undir Samsvarþröskuldi svo stutt dýfa endurstilli hana ekki.", "label": "Ósamsvarþröskuldur" }, "progress_reset_delay": { - "doc": "Eftir að hafa lokið, haltu framvindu við 100% í þetta lengi svo Lokið sé sýnilegt á mælaborðum áður en þú endurstillir í aðgerðalaus.", - "label": "Framfaraendurstillingðráð" + "doc": "Eftir lotu er framvindu haldið í 100% svona lengi áður en hún er endurstillt í Aðgerðarlaus.", + "label": "Töf á endurstillingu framvindu" }, "pump_stuck_duration": { - "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" + "doc": "Hversu lengi dæla má ganga samfellt áður en atburðurinn um fasta dælu er sendur.", + "label": "Tímamörk fastrar dælu" }, "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.", + "doc": "Lágmarkstími milli unninna mælinga. Mælingum sem berast fyrr er sleppt (lágum mælingum á meðan lota stendur er alltaf haldið). Stilltu þetta á tilkynningarbil innstungunnar eða lægra; hærra gildi hendir bara mælingum.", "label": "Sýnitakabil" }, "save_debug_traces": { - "doc": "Geymdu fulla aflrekningu og samsvarandi villuleitargögn fyrir hverja lotu. Gagnlegt fyrir bilanaleit en eykur geymslustærð.", + "doc": "Geymdu einnig villuleitargögn samsvörunarinnar (skor allra valkosta) með hverri lotu, fyrir bilanaleit. Tekur um 20 KB á hverja lotu.", "label": "Vista villuleitarspor" }, "smart_termination_duration_ratio": { - "doc": "Hversu langt inn í væntanlega lengd samsvaraða kerfisins lota þarf að vera komin áður en snjöll lokun má ljúka henni snemma þegar aflið fellur. Væntanlega lengdin er meðaltal kerfisins, þannig að á tækjum þar sem keyrslutími er mjög breytilegur - þvottavélar með köldu inntaksvatni að vetri á móti heitu að sumri, þurrkarar með rakaskynjara, álagsháð kerfi - ljúka um það bil helmingur allra keyrslna styttri en það meðaltal og fá aldrei hina hröðu lokun, heldur ljúka fyrst mínútum síðar með vara-tímamörkunum. Lækkaðu þetta gildi (t.d. 0,85) á slíkum vélum svo að snemmbúna lokunin virkist samt; hækkaðu það í átt að 1,0 til að vera varkárari. Skildu eftir autt fyrir sjálfgefið gildi (0,98, eða 0,99 fyrir uppþvottavélar). Hún getur aðeins lokið lotu fyrr, aldrei síðar, og virkist aldrei við óljósa eða óörugga samsvörun.", + "doc": "Hlutfall af meðallengd kerfisins sem lota þarf að ná áður en snjöll lokun má ljúka henni snemma. Ef keyrslutímar eru breytilegir (vatnshiti, rakaskynjun, magn) skaltu lækka það (t.d. 0,85). Skildu eftir autt fyrir 0,98 (0,99 á uppþvottavélum).", "label": "Hlutfall snjallrar lokunar" }, - "smoothing_window": { - "doc": "Hversu mikið hráorkumerkið er sléttað. Low (2) er móttækilegur en hávær; hár (5) sléttir toppa en bætir við töf.", - "label": "Jöfnunargluggi" - }, "start_duration_threshold": { - "doc": "Krafturinn verður að vera fyrir ofan upphafsþröskuldinn svona lengi til að staðfesta raunverulega byrjun, sem kemur í veg fyrir að kveikja/slökkva á sekúndubroti geti hafið lotu.", + "doc": "Aflið þarf að haldast yfir Ræsingarþröskuldi svona lengi áður en lota hefst, svo stuttir kveikt/slökkt-toppar séu hunsaðir.", "label": "Ræsingarlengd" }, "start_energy_threshold": { - "doc": "Orka (afl x tími) sem heimilistækið verður að eyða áður en það er í gangi. Stuttur aflmikill toppur hefur mjög litla orku og er hunsuð, sem kemur í veg fyrir rangræsingar.", + "doc": "Orka sem tækið þarf að nota áður en lotan telst í gangi. Stuttur afltoppur notar litla orku, svo hann ræsir ekki lotu.", "label": "Ræsingaorka" }, "start_threshold_w": { - "doc": "Afl verður að hækka yfir þetta stig til að staðfesta að lota sé hafin. Of lág stilling veldur fölskum upphafsgreiningum frá biðstöðuafli; of há stilling lætur kerfi sem byrja hægt (köld fylling) fara fram hjá. Tillöguvélin setur þetta rétt fyrir ofan lægsta virka afl vélarinnar.", + "doc": "Aflið þarf að fara yfir þetta til að ræsa lotu. Of lágt: biðstöðuafl veldur röngum ræsingum. Of hátt: kerfi sem fara hægt af stað (köld vatnsáfylling) fara fram hjá.", "label": "Ræsingarþröskuldur" }, "stop_threshold_w": { - "doc": "Afl verður að falla niður fyrir þetta stig áður en niðurtalning frá töf hefst. Stilltu það undir Start Threshold - bilið á milli þeirra er hysteresis bandið sem kemur í veg fyrir flökt. Ef stillt er of hátt, ræsa lágt afl fasar (skolunartímar, hrukkuvörn) ranglega af stað lokaröðinni.", + "doc": "Aflið þarf að fara undir þetta áður en slökkvitöfin byrjar að telja. Hafðu það undir Ræsingarþröskuldi. Of hátt, og aflitlir fasar (skolhlé, hrukkuvörn) ræsa lokaniðurtalninguna fyrir mistök.", "label": "Stöðvunarþröskuldur" }, "switch_entity": { - "doc": "Valfrjáls rofi slökkt á hléi og aftur kveikt á þegar haldið er áfram, notaður ásamt „Hlé slekkur líka á rafmagni“.", + "doc": "Valfrjáls rofi sem slökkt er á við hlé og kveikt aftur á þegar haldið er áfram, með „Hlé slær einnig af straum“.", "label": "Rofaeining" }, "watchdog_interval": { - "doc": "Hversu oft bakgrunnsvaktin athugar hvort skynjarar hafi verið stöðvaðir og liðinn tími. Sjálfgefið 30 s.", + "doc": "Hversu oft bakgrunnsathugunin leitar að stöðvuðum skynjurum og útrunnum tímamörkum. Sjálfgefið 30 s.", "label": "Gæsluabil" }, "notify_milestone_message": { @@ -1562,127 +1511,82 @@ "label": "Áfangaskilaboð" }, "notify_milestones": { - "doc": "Kommuaðgreind lotutalning sem kveikir á einstakri fagnaðartilkynningu þegar henni er náð (t.d. 50, 100, 500, 1000). Autt slekkur á áfangatilkynningum.", + "doc": "Lotufjöldi sem kveikir á einstakri fagnaðartilkynningu, aðgreindur með kommum (t.d. 50, 100, 500, 1000). Autt slekkur á þeim.", "label": "Lotuáfangar" }, "notify_quiet_end_hour": { - "doc": "Lok ónæðislauss glugga (0-23). Tafðar tilkynningar eru afhentar á þessari klukkustund. Skildu eftir autt til að slökkva.", + "doc": "Lok ónæðislausa gluggans (0-23); tilkynningar sem bíða eru þá sendar. Autt slekkur á honum.", "label": "Lok hljóðláts tíma" }, "notify_quiet_start_hour": { - "doc": "Upphaf ónæðislauss glugga (0-23). Loka-, áminningar- og hreinþvottatilkynningar sem myndu berast á hljóðlátum tíma eru tafðar og afhentar þegar glugganum lýkur. Skildu eftir autt til að slökkva. Styður glugga sem ná yfir miðnætti (t.d. upphaf 22, lok 7).", + "doc": "Upphaf ónæðislauss glugga (0-23). Loka-, áminningar- og hreinþvottatilkynningum í þessum glugga er haldið þar til honum lýkur. Getur náð yfir miðnætti (t.d. 22 til 7). Autt slekkur á honum.", "label": "Upphaf hljóðláts tíma" }, "peak_rate_message": { - "doc": "Valfrjáls sérsniðinn texti fyrir háannaálagsábendinguna sem bætt er við ræsitilkynninguna. Sniðbreytur: {device}, {price}. Autt notar innbyggða sjálfgefna gildið.", + "doc": "Eigin texti fyrir háannaverðsábendinguna. Sniðbreytur: {device}, {price}. Autt notar sjálfgefna textann.", "label": "Skilaboð um háannaverð" }, "peak_rate_threshold": { - "doc": "Þegar lota hefst og núverandi verð á kWst er á eða yfir þessu gildi er háannaverðsábendingu bætt við ræsitilkynninguna. 0 eða autt slekkur á ábendingunni.", + "doc": "Ef verð á kWh er við eða yfir þessu þegar lota hefst er háannaverðsábendingu bætt við upphafstilkynninguna. 0 eða autt slekkur á henni.", "label": "Háannaverðsmörk (á kWst)" }, - "dtw_bandwidth": { - "label": "Vikmörk formsamsvörunar", - "doc": "Hversu mikið samsvörunin má tímabjaga lotu til að passa við form vistaðs kerfis. Hærra þolir meiri hraðabreytileika; of hátt getur samsvarað röngu kerfi." - }, - "corr_weight": { - "label": "Vægi forms á móti aflstigi", - "doc": "Jafnvægi milli ferilforms og algjörs aflstigs þegar samsvörun er metin. Hærra ívilnar forminu; lægra ívilnar því að hitta aflstigið." - }, - "duration_weight": { - "label": "Vægi tímalengdar", - "doc": "Hversu sterkt kerfi, sem hefur dæmigerðan keyrslutíma sem passar við þessa lotu, er tekið fram yfir önnur." - }, - "energy_weight": { - "label": "Vægi orku", - "doc": "Hversu sterkt kerfi, sem hefur dæmigerða orkunotkun sem passar við þessa lotu, er tekið fram yfir önnur." - }, "store_brand": { "label": "Vörumerki tækis", - "doc": "Valfrjálst. Vörumerki tækisins, valið úr samfélagsskránni. Notað til að finna og deila samsvarandi viðmiðunarupptökum. Skildu eftir autt ef þú notar ekki nettengda eiginleika." + "doc": "Valfrjálst. Vörumerki tækisins úr samfélagsskránni, notað til að finna og deila viðmiðunarupptökum. Skildu eftir autt ef þú notar ekki nettengda eiginleika." }, "store_model": { "label": "Gerð tækis", - "doc": "Valfrjálst. Gerð tækisins, valin úr samfélagsskránni þegar vörumerki hefur verið sett. Ef gerðin þín er ekki á listanum geturðu bætt henni við skrána." + "doc": "Valfrjálst. Gerð tækisins úr samfélagsskránni (veldu vörumerki fyrst). Ef hún er ekki á listanum geturðu bætt henni við." }, "show_contributor": { "doc": "Sýnir tilvísunina 'eftir ' á samfélagstækjum og viðmiðunarlotum." }, - "enable_phase_matching": { - "label": "Fasaskiptur tími sem eftir er", - "doc": "Skiptu hverri lotu í gangi upp í fasa (upphitun, þvottur, vinda) og áætlaðu tímann sem eftir er fyrir hvern fasa, blandað saman við klassíska matið - snemma í lotunni styðst það við fasaáætlunina og undir lokin við klassíska matið. Þetta sérsníður niðurtalninguna að því hversu lengi vélin þín hitar og gengur í raun, sem er mest áberandi í fyrri helmingi lotu. Slökkt = aðeins klassíska matið. Aðeins birting á tíma sem eftir er verður fyrir áhrifum; kerfissamsvörun og lotugreining haldast óbreytt." - }, - "keep_min_score": { - "label": "Lágm. samsvörunareinkunn", - "doc": "Lægsta likhetseinkunn sem frambjóðandi þarf til að vera eftir í samsvöruninni. Sjálfgefið 0,1 er vísvitandi rýmt – það leyfir jafnvel veika frambjóðendur og treystir á síðari þrep til að finna besta samsvörun. Hækkið til að sía út ósennilegar sniðmát fyrr; lækkkið til að víkka upphaflega frambjóðendasafnið." - }, - "dtw_blend": { - "label": "DTW-blandning", - "doc": "Hversu mikið DTW-jöfnunarskorið kemur í stað kjarnaskorsins frá Þrepi 2. 0 = notaðu aðeins skor Þreps 2, 1 = notaðu aðeins DTW-skor, 0,5 (sjálfgefið) = jöfn blanda. Hækkaðu það til að treysta meira á tímajöfnun þegar kerfi hafa svipuð aflstig en ólík tímamynstur." - }, - "dtw_ensemble_w": { - "label": "DTW-ensemble-blanda", - "doc": "Í ensemble-DTW-ham (sjálfgefinn) er þetta þunginn á skaluðum L1 gagnvart afleiðu-DTW (DDTW). 1,0 = aðeins skalað L1, 0 = aðeins DDTW, 0,7 = sjálfgefið. Skalade útgáfan ber saman aflstig; afleiðuútgáfan bregst við því hvernig afl breytist með tíma. Ensemble blandar bæði." - }, - "dtw_ddtw_scale": { - "label": "Afleiðu-DTW-kvarði", - "doc": "Hálf-mettunarfjarlægð fyrir afleiðu-DTW-stigagjöf. Einkunnin helmingast þegar DDTW-fjarlægðin er jöfn þessu gildi. Minna = næmara fyrir formmunur. Sjálfgefið 30 er kvarðað fyrir dæmigerðar aflferil tækja. Hefur aðeins áhrif á ensemble- og ddtw-DTW-hama." - }, - "dtw_refine_top_n": { - "label": "DTW-fínstillingarfjöldi", - "doc": "Hversu marga toppframbjóðendur frá Þrepi 2 DTW endurgerir skor fyrir. DTW er dýrara svo það er aðeins beitt á bestu N frambjóðendurna. Sjálfgefið 5. Hækkið (í 7-9) ef rétt snið lendir stundum aðeins á 4. eða 5. sæti eftir Þrep 2 – DTW getur bjargað því. Lægra gildi hraðar samsvörun lítið." - }, - "duration_scale": { - "label": "Lengdarskerpa", - "doc": "Skerpa í viðurlögum Þreps 4 vegna samræmis í keyrslulengd. Þetta er log-hlutfallið þar sem samræmisskorið helmingast. Minna = strangara: frávik í keyrslulengd skemmir meira. Sjálfgefið 0,175 samsvarar um 18% þolmörkum í keyrslulengd við hálft þyngd. Notaðu með Keyrslulengdarþyngd." - }, - "energy_scale": { - "label": "Orkuskerpa", - "doc": "Skerpa í viðurlögum Þreps 4 vegna samræmis í orkunotkun. Minna = strangara: frávik í orkunotkun skemmir meira. Sjálfgefið 0,25 er vísvitandi rýmra en keyrslulengdarskerpan vegna þess að orka breytist með álagi. Notaðu með Orkuþyngd." - }, "power_profile_interval_min": { "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." + "doc": "Stærð hvers bils í eigindinu power_profile (meðalvött á bil) sem skipuleggjendur eins og EMHASS og tibber_prices nota. Minna heldur stuttum toppum; stærra jafnar ferilinn. Sjálfgefið 15 mín. Hefur ekki á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." + "doc": "Aðeins iOS. Uppfærir framvindu í beinni hljóðlaust, með rafhlöðusparandi forgangi sem iOS getur safnað saman. Fyrsta uppfærslan og upphafs- og lokatilkynningar láta enn vita. Slökkt: hver uppfærsla lætur vita." }, "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." + "doc": "Aðeins Android. Heldur framvindutilkynningunni þegar ýtt er á hana. Slökkt: ýting lokar henni." }, "notify_live_click_action": { "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ð." + "doc": "Hvað opnast þegar ýtt er á tilkynningu þessa tækis. Autt: WashData-spjaldið á þessu tæki. Sláðu inn slóð á mælaborð (t.d. /lovelace/laundry) eða URL, eða „none“ fyrir enga aðgerð." }, "door_opens_at_end": { "label": "Hurð opnast sjálfkrafa í lok", - "doc": "Fyrir uppþvottavélar sem spretta hurðinni upp í lok hringsins til þurrkunar (AirDry og svipað). Þegar þetta er virkt setur hurðaropnun á yfirstandandi hring hann ekki í bið að eilífu; ef hurðin helst opin í stillt dvaltími meðhöndlar WashData hringinn sem lokinn. Stutt opnun (að bæta við fat) er hundsuð. Krefst hurðarskynjaraeiningar." + "doc": "Fyrir uppþvottavélar sem opna hurðina í lokin til að þurrka (t.d. AirDry). Hurð sem stendur opin í biðtímann hér að neðan lýkur lotunni í stað þess að gera hlé á henni; stutt opnun er hunsuð. Krefst Dyraskynjareiningar." }, "door_end_dwell_seconds": { "label": "Dvaltími hurðar við lok", - "doc": "Hversu lengi hurðin þarf að vera opin áður en WashData lýkur hringnum, þegar \"Hurð opnast sjálfkrafa í lok\" er virkt. Nógu lengi til að hunsa hraða tilbúningu fats (sjálfgefið 60 s), nógu stutt til að ljúka fljótt þegar vél spretter hurðinni upp." + "doc": "Þegar „Hurð opnast sjálfkrafa í lok“ er virkt: hversu lengi hurðin þarf að vera opin áður en lotunni lýkur. Nógu lengi til að hunsa að diski sé bætt við (sjálfgefið 60 s)." }, "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." + "doc": "Hvaða gerðir lotna móta feril og samsvörun hvers kerfis. Það að taka hakið af gerð eyðir engu; gagnlegt ef þú treystir ekki innfluttum gögnum. Tölfræði telur alltaf aðeins lotur þessarar vélar. Ef hakið er tekið af öllu er það hunsað." }, "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." + "doc": "Reiknaðu hverja lotu á verðinu á hverri stundu sem hún keyrði, í stað verðsins þegar henni lauk. Krefst verðeiningar. Vinna úr sögu (Greining) endurreiknar eldri lotur." }, "unload_confirm_entity": { "label": "Losunarstaðfestingareining", - "doc": "Valfrjáls eining þar sem virkjun merkir að þvotturinn hafi verið tekinn út, fyrir vélar sem geta ekki haft hurðarskynjara. Allar gerðir virka: Zigbee- eða NFC-hnappur, input_button hjálpareining, hreyfiskynjari fyrir framan vélina, sena. Hver stöðubreyting telst sem staðfesting, nema þegar slökkt er á einingunni eða hún verður ótiltæk. Að stilla þetta kveikir einnig á losunaráminningunni á tæki án Dyraskynjareiningar." + "doc": "Valfrjálst. Hvaða eining sem er sem staðfestir að þvotturinn hafi verið tekinn út, fyrir vélar án hurðarskynjara (hnappur, input_button, hreyfiskynjari, sena). Hver stöðubreyting telst, nema þegar slökkt er eða einingin verður ótiltæk. Virkjar einnig losunaráminninguna." }, "unload_track_without_door": { "label": "Staðfesta losun handvirkt", - "doc": "Keyrir losunaráminninguna á tæki án Dyraskynjareiningar og án Losunarstaðfestingareiningar, og þú staðfestir losunina þess í stað úr þinni eigin sjálfvirkni: ýttu á hnappaeininguna \"Merkja sem losað\" eða kallaðu á aðgerðina ha_washdata.mark_unloaded. Hunsað þegar Dyraskynjareining er stillt, því hún gefur merkið nú þegar." + "doc": "Keyrðu losunaráminninguna án hurðarskynjara eða staðfestingareiningar. Staðfestu losunina úr þinni eigin sjálfvirkni með hnappinum \"Merkja sem losað\" eða þjónustunni ha_washdata.mark_unloaded. Hunsað þegar Dyraskynjareining er stillt." }, "notify_icon_color": { "label": "Tilkynningalitur", - "doc": "Áherslulitur fyrir tilkynningar frá þessu tæki, svo að þvottavél, þurrkari og uppþvottavél þekkist í sjónhendingu. iOS litar táknið og framvindustikuna í Live Activity á lásskjánum og í Dynamic Island. Android fær sama litinn sendan, en hvað síminn gerir við hann er undir honum komið: sum tæki lita táknið og mörg (einkum Samsung One UI) hunsa hann alveg. Bæði kerfin skilja hex-gildi (t.d. #03A9F4). Skildu eftir autt fyrir sjálfgefinn lit kerfisins." + "doc": "Áherslulitur fyrir tilkynningar þessa tækis, svo auðvelt sé að greina þvottavél, þurrkara og uppþvottavél í sundur. Á iOS fyllir hann hringinn á bak við táknið og litar stikuna í Live Activity. Margir Android-símar (sérstaklega Samsung) hunsa hann. Notaðu hex, t.d. #03A9F4. Autt: sjálfgefið á kerfinu." + }, + "share_name": { + "doc": "Sýndu reikningsnafnið þitt sem upphleðsluaðila lotna sem þú deilir. Slökkt: deilt án nafns." } }, "setting_group": { @@ -1710,9 +1614,6 @@ "door_pause": { "label": "Dyr og hlé" }, - "dtw_refinement": { - "label": "DTW fínstilling" - }, "duration_gates": { "label": "Lengdargáttir" }, @@ -1789,17 +1690,20 @@ "rebuilding": "Endurbyggi…", "analyzing": "Greini…", "resetting": "Endurstilli…", - "reverting": "Afturkalla…", "trimming": "Klippi til…", "splitting": "Skipti…", + "stalled": "Stöðvuð", "deleting": "Eyði…", "imported": "Flutt inn", "preparing": "Undirbý…", - "unlabelling": "Fjarlægi merkingu…" + "unlabelling": "Fjarlægi merkingu…", + "working": "Í vinnslu…", + "match_uncertain": "Óvíst: {top} eða {runner}", + "match_uncertain_single": "Óvíst: kannski {top}", + "match_sure": "~{pct}% vissa", + "match_uncertain_tip": "WashData hefur ekki enn ákveðið kerfi. Talan sýnir hversu oft efsta ágiskunin, {top}, reyndist rétt í fyrri lotum þegar hún var svona langt á undan. Aðeins til upplýsinga." }, "suggestion": { - "both_agree": "WashData mælir með", - "calibrated_label": "Kvarðað", "observed_label": "Mælt", "impact": { "auto_label_confidence": { @@ -1814,10 +1718,6 @@ "higher": "Lokar aðeins lotum með umtalsverðan orkuNotkun", "lower": "Lokar einnig styttri eða orkuminni lotum" }, - "end_repeat_count": { - "higher": "Lokaboð þarf að endurtaka sig oftar – öruggara, lítið eitt hægar", - "lower": "Lýkur eftir færri endurtekningar – hraðari greining, meiri hætta á röngu loki" - }, "learning_confidence": { "higher": "Lærir aðeins af mjög öruggum samsvörunum – hægar en áreiðanlegra", "lower": "Lærir af fleiri lotum – líkan kann að verða ónákvæmara" @@ -1856,40 +1756,25 @@ } }, "reason": { - "auto_label_confidence": "Vissa við 15. hundraðsmark fyrir {cycles} sjálfvirkar merkingar sem notandinn leiðrétti aldrei ({p15}).", "completion_min_seconds": "Helmingur af p05-lengd hreinna lota ({minutes} min) yfir {cycles} lotur; síar út draugalotur.{excl}", - "dead_zone_batch": "Haldið stuttu (miðgildi óstöðugleikaglugga við ræsingu yfir {cycles} lotur = {s}s) svo að raunveruleg lok greinist fljótt.", - "dead_zone_from_dip": "Byggt á snemmbúnu aflfalli sem greindist við {s}s.", - "end_energy_default": "Sjálfgefin ráðlögð grunnlína fyrir suðþröskuld við lok lotu.", - "end_energy_false": "p95 fyrir orku við fölsk lok ({p95}Wh) yfir {cycles} lotur, með lágmark við 0.2% af miðgildi orku lotu ({floor}Wh).", - "end_energy_no_false": "Engin fölsk lok yfir {cycles} lotur; með lágmark við 0.2% af miðgildi orku lotu ({floor}Wh).", - "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.", + "match_interval_floored": "Stysta kerfið ({shortest}s) myndi takmarka þetta við {cap}s, undir {minimum}s lágmarkinu, svo lágmarkið er notað: {persistence} samfelldar samsvaranir taka {budget}s, yfir {pct}% af því kerfi. Samsvörun keyrir aðeins þegar mæling berst (miðgildi={median}s), svo þetta 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.", "min_off_gap": "Byggt á {gaps} mældum millibilum milli lota (p05={p05}s). Lágmark tækis: {floor}s.", "min_off_gap_bridge": "Stærðin valin til að brúa lengsta kyrrðartímabilið innan lotu: {span}s + 60s biðminni, úr {spans} brúuðum bilum yfir {cycles} hreinar lotur (helst undir stysta bilinu þínu, {ceiling}s, milli tveggja keyrslna).", "min_power": "40% af lægsta virka afli við p05 ({p05}W) yfir {cycles} hreinar lotur, sem heldur slökkviþröskuldinum undir raunverulegri notkun.{excl}", - "ml_auto_label": "Lægsta vissa sem gæðalíkanið mat samt sem hreint (p10 af {cycles} hreinum lotum = {p10}).", - "ml_end_repeat": "{false}/{total} lotur ({pct}%) höfðu hlé sem lokagreinirinn gaf einkunnina >50%; krefst {val} staðfestinga á lokum.", - "ml_off_delay": "Hlé staðfest af lokagreini: p95 {p95}s + 60s biðminni, úr {pauses} hléum sem líkanið staðfesti yfir {cycles} lotur (lágmark {floor}s).", "no_update_timeout": "Byggt á mældri uppfærslutíðni (p95={p95}s) * 20 (lágm. 60s).", "off_delay_cadence": "Byggt á mældri uppfærslutíðni (p95={p95}s) * 5", "off_delay_device_floor": "Notaði tækjasértækt öruggt lágmark fyrir {device} ({floor}s).", "off_delay_generic_floor": "Notaði almennt öruggt lágmark ({floor}s).", "off_delay_pauses": "Stærðin valin til að endast lengur en raunveruleg hlé: p95 fyrir hlé innan lotu {p95}s + 60s biðminni, úr {pauses} hléum yfir {cycles} hreinar lotur (lágmark {floor}s).", - "profile_match_threshold": "p10-vissa fyrir {cycles} réttar sjálfvirkar merkingar ({p10}); öruggt lágmark fyrir staðfestingu í rauntíma.", - "sampling_interval": "Miðgildi uppfærslubils sem mældist yfir {cycles} hreinar lotur ({si}s).{excl}", - "smoothing_window": "Stærðin valin til að jafna út um það bil 30s af mælingum við {si}s sýnatökubil ({samples} sýni).", - "start_duration": "Haldið stuttu (~eitt {si}s sýnatökubil) svo að greining hefjist eins snemma og hægt er en hunsi samt staka toppa.", - "thr_batch": "Haldið rétt yfir lægsta virka afli við p05 yfir {cycles} lotur ({p05}W) svo að ræsing náist eins snemma og hægt er og stöðvunarþröskuldurinn haldist undir lægsta gangafli vélarinnar.", - "tol_per_profile": "p75 af dreifni lengdar á hvert snið yfir {profiles} snið ({cycles} lotur); þröngum sniðum er ekki refsað.", - "tol_pooled": "Byggt á sameinaðri dreifni lengdar {cycles} nýjustu merktu lota (p95 frávik={dev}).", - "watchdog": "Haldið eins lágu og óhætt er (rétt yfir p95-uppfærslubilinu {p95}s og að minnsta kosti 2x sýnatökubilinu {median}s, lágm. 30s) svo að stöðnun greinist fljótt án falskra stöðvana." + "standby_floor": "Sérhver nýleg lota dró enn {idle}W þegar henni lauk, þannig að þröskuldur undir því getur aldrei séð að tækið slökkni. Haldið yfir því; {cycles} hreinar lotur í yfirlitinu hafa aldrei gert hlé undir því í meira en {pause}s.", + "thr_batch": "Rétt yfir lægsta virka afli (p05) yfir {cycles} lotur ({p05}W), svo ræsingar náist snemma og stöðvunarþröskuldurinn haldist undir lægsta gangafli.", + "watchdog": "Haldið eins lágu og óhætt er (rétt yfir p95-uppfærslubilinu {p95}s og að minnsta kosti 2x sýnatökubilinu {median}s, lágm. 30s) svo að stöðnun greinist fljótt án falskra stöðvana.", + "end_energy_floor": "Hækkað upp í lágmarkið sem Stöðvunarþröskuldur ({stop} W) yfir Slökkvitöf ({off_delay} s) felur í sér; fyrir neðan það getur lotan aðeins endað eftir varaleið.", + "max_duration_ratio_default": "Aftur í sjálfgefið gildi: lægra hámark hafnar réttu kerfi í hvert sinn sem keyrsla er lengri en venjulega.", + "min_duration_ratio_default": "Aftur í sjálfgefið gildi: hærra lágmark kemur í veg fyrir að lota í gangi þekkist fyrr en hún er vel á veg komin." }, "exclusions": { "summary": "Útilokaði {total} ranggreindar lotur: {parts}.", @@ -1907,12 +1792,13 @@ "no_power": "ekkert afl", "no_active_power": "ekkert virkt afl" } - } + }, + "now_value": "(nú {v})" }, "tab": { "advanced": "Ítarlegri", "diagnostics": "Greining", - "history": "Hringrásir", + "history": "Lotur", "ml": "ML þjálfun", "pp_cleanup": "Hreinsun", "pp_manage": "Stjórna", @@ -1930,13 +1816,18 @@ "tag": { "early_end": "Snemma enda", "late_start": "Seint byrjun", - "merged": "Sameinaðir hringrásir", + "merged": "Sameinaðar lotur", "noise": "Hávaði", "sensor_gap": "Skynjarabil", - "split": "Skipt hringrás", + "split": "Skipt lota", "wrong_profile": "Rangt snið" }, "toast": { + "task_name_required": "Gefðu verkinu nafn", + "task_added": "Verki bætt við", + "task_save_failed": "Ekki tókst að vista verkið: {error}", + "task_removed": "Verk fjarlægt", + "maint_logged": "Skráð sem lokið: {task}", "catalog_refreshed": "Samfélagsskráin endurnýjuð", "access_saved": "Aðgangsstýring vistuð", "all_wiped": "Öll gögn þurrkuð út", @@ -1944,13 +1835,12 @@ "auto_label_complete": "Sjálfvirkt merki lokið", "automation_deleted": "Sjálfvirkni eytt", "correction_submitted": "Leiðrétting lögð fram", - "cycle_deleted": "Hringrás eytt", - "cycle_force_stopped": "Hringrás afl-stöðvuð", - "cycle_labelled": "Hringrás merkt", - "cycle_paused": "Hlé gert á hringrásinni", - "cycle_resumed": "Hringrás hafin aftur", + "cycle_force_stopped": "Lota þvingað stöðvuð", + "cycle_labelled": "Lota merkt", + "cycle_paused": "Hlé gert á lotunni", + "cycle_resumed": "Lota hafin aftur", "cycle_trimmed": "Lota klippt til", - "cycles_merged": "Hringrásir sameinaðar", + "cycles_merged": "Lotur sameinaðar", "envelope_rebuilt": "Umslag endurbyggt", "envelopes_rebuilt": "Umslög endurbyggð", "export_downloaded": "Útflutningur hlaðið niður", @@ -1960,11 +1850,9 @@ "group_deleted": "Hópi eytt", "group_name_required": "Nafn hóps er áskilið", "group_saved": "Hópur vistaður", - "import_successful": "Innflutningur tókst; endurhleðsla samþættingar", "json_required": "JSON gögn eru nauðsynleg", "legacy_removed": "Eldri aðgerðir fjarlægðar", "logs_exported": "Logar fluttir út", - "matching_reverted": "Samsvarandi þyngd færð aftur í sjálfgefna stillingar", "min_2_profiles": "Veldu að minnsta kosti 2 prófíla fyrir hóp", "ml_training_no_improvement": "Þjálfun lokið: grunnlínu haldið (engar umbætur)", "models_reverted": "Módel í tæki færð aftur til grunnlínu", @@ -1974,8 +1862,8 @@ "pg_preset_loaded": "Forstillingin \"{name}\" var hlaðin", "pg_preset_saved": "Forstillingin \"{name}\" var vistuð", "pg_sugg_loaded": "{n} tillögugildi flutt yfir - keyrðu tilraunasvæðið til að bera saman", - "pg_sugg_ml_loaded": "{n} ML-kvarðað gildi flutt yfir - keyrðu tilraunasvæðið til að bera saman", - "phase_name_required": "Fasaheiti er áskilið", + "phase_name_required": "Áfangaheiti er áskilið", + "phase_reset": "\"{name}\" færður aftur í innbyggða áfangann", "phase_updated": "Áfangi uppfærður", "phases_saved": "Áfangar vistaðir", "preferences_saved": "Kjörstillingar vistaðar", @@ -2009,7 +1897,7 @@ "pg_run_gone": "Þessi keyrsla er ekki lengur tiltæk.", "task_cancelled": "Hætt við.", "processed": "Vinnslu lokið: {bits}", - "processed_cycles": "{n} hringrásir", + "processed_cycles": "{n} lotur", "processed_suggestions": "{n} tillögur", "processed_models": "{n} líkön virkjuð", "store_search_failed": "Leit mistókst: {error}", @@ -2022,14 +1910,10 @@ "store_name_required": "Sláðu inn heiti prófíls", "store_import_failed": "Innflutningur mistókst: {error}", "store_imported": "Flutt inn í {profile}", - "store_program_required": "Sláðu inn heiti kerfis", "store_no_appliance": "Stilltu fyrst vörumerki og gerð tækisins í Stillingum.", "store_share_failed": "Deiling mistókst: {error}", "store_shared": "Deilt í samfélagsversluninni, bíður yfirferðar.", "appliance_added": "Tæki bætt við, bíður samþykkis", - "thanks_confirming": "Takk fyrir staðfestinguna", - "device_approved": "Samþykkt af samfélaginu", - "rating_saved": "Gæðaeinkunn vistuð", "brand_added": "Vörumerki bætt við, bíður samþykkis", "profile_added": "Prófíl bætt við, bíður samþykkis", "saved_except_conflicts": "Vistað. Leggðu úr hófi merktu árekstrana til að vista afganginn.", @@ -2053,13 +1937,58 @@ "odometer_save_failed": "Ekki tókst að vista samtöluna: {error}", "contrib_device_changed": "Bætt við samfélagsverslunina, en skipt var um tæki meðan eyðublaðið var opið. Veldu það í Stillingum.", "store_pick_profile_share": "Veldu prófíl til að deila", - "remove_failed": "Fjarlæging mistókst: {error}" + "remove_failed": "Fjarlæging mistókst: {error}", + "analysis_complete": "Greiningu lokið: {count} tillögur", + "analysis_failed": "Greining mistókst: {error}", + "auto_detect_failed": "Sjálfvirk skynjun mistókst: {error}", + "auto_label_failed": "Sjálfvirk merking mistókst: {error}", + "auto_label_done": "Sjálfvirkri merkingu lokið: {count} lotur merktar, {skipped} án merkingar.", + "auto_label_done_one": "Sjálfvirkri merkingu lokið: {count} lota merkt, {skipped} án merkingar.", + "auto_label_done_other": "Sjálfvirkri merkingu lokið: {count} lotur merktar, {skipped} án merkingar.", + "auto_label_cancelled": "Hætt við sjálfvirka merkingu eftir að {count} lotur voru merktar.", + "auto_label_cancelled_one": "Hætt við sjálfvirka merkingu eftir að {count} lota var merkt.", + "auto_label_cancelled_other": "Hætt við sjálfvirka merkingu eftir að {count} lotur voru merktar.", + "could_not_load_cycle": "Ekki tókst að hlaða lotu: {error}", + "create_failed": "Stofnun mistókst: {error}", + "cycles_deleted": "Lotum eytt: {count}", + "debug_cleared": "Villuleitarspor hreinsuð: {count}", + "discard_failed": "Ekki tókst að henda: {error}", + "error": "Villa: {error}", + "export_failed": "Útflutningur mistókst: {error}", + "import_failed": "Innflutningur mistókst: {error}", + "invalid_json": "\"{key}\" er ekki gilt JSON - lagaðu það eða hreinsaðu reitinn áður en þú vistar.", + "label_failed": "Merking mistókst: {error}", + "ml_training_promoted": "Þjálfun lokið: {count} líkön virkjuð", + "pause_failed": "Ekki tókst að gera hlé: {error}", + "phase_created": "Áfangi \"{name}\" búinn til", + "phase_deleted": "Áfanga \"{name}\" eytt", + "profile_created": "Prófíll \"{name}\" búinn til", + "rename_failed": "Endurnefning mistókst: {error}", + "save_failed": "Vistun mistókst: {error}", + "split_complete": "Skipt í {count} lotur", + "start_failed": "Ræsing mistókst: {error}", + "stop_failed": "Stöðvun mistókst: {error}", + "update_failed": "Uppfærsla mistókst: {error}", + "store_download_skipped": "{n} upptöku/upptökum sleppt: of stuttar, of gloppóttar eða þegar á tækinu þínu.", + "store_import_low_quality": "Þessi upptaka er of stutt, of gloppótt eða ótrúverðug til að hægt sé að flytja hana inn.", + "store_import_duplicate": "Þessi upptaka er þegar á tækinu þínu.", + "device_approved": "Samþykkt af samfélaginu", + "thanks_confirming": "Takk fyrir staðfestinguna", + "rating_saved": "Gæðaeinkunn vistuð", + "store_download_partial": "Ekki tókst að sækja {n} kerfi; reyndu aftur síðar.", + "import_no_restore_point": "Flutt inn, en ekki tókst að vista endurheimtarpunkt, svo ekki er hægt að afturkalla þennan innflutning", + "import_undo_failed": "Ekki tókst að afturkalla innflutninginn: {error}", + "import_undone": "Innflutningur afturkallaður: endurheimti {profiles} prófíl(a) og {cycles} lotu(r)" }, "trend": { "down": "Stefnir niður", "up": "Stefna upp" }, "maint": { + "salt": "Fylla á salt", + "rinse_aid": "Fylla á gljáa", + "lint_filter": "Hreinsa lósíu", + "condenser_clean": "Hreinsa eimsvala", "bearing_service": "Þjónusta á legum", "descale": "Afkalka", "drum_clean": "Hreinsa tromlu", @@ -2071,12 +2000,14 @@ "status_tip": "Niðurstaða lotu: Lokið (eðlilegur endir), Truflað (skyndilegt aflfall), Þvingað stopp (handvirkt) eða Þarf yfirferð (endurgjöf í bið).", "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 þ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.", + "energy_tip": "Heildarorka sem notuð var (kWh), reiknuð út frá afli yfir tíma.", + "cost_tip": "Orkukostnaður þessarar lotu, festur þegar henni lauk. Þegar Tímaveginn kostnaður er virkur er hver stund reiknuð á eigin verði; annars á verðinu í 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.", - "cost_dynamic_tip": "Tímavegið: reiknað á því verði sem gilti meðan lotan keyrði (raunverð {price} {cur}/kWh)." + "cost_dynamic_tip": "Tímavegið: reiknað á því verði sem gilti meðan lotan keyrði (raunverð {price} {cur}/kWh).", + "dbg_conf": "Öryggi", + "dbg_mae": "MAE", + "dbg_corr": "Fylgni" }, "stat": { "avg": "meðalt.", @@ -2100,7 +2031,10 @@ "notify_finish": "Lokatilkynning yrði send.", "notify_milestone": "Áfangatilkynning yrði send.", "notify_held": "Tilkynningu var haldið eftir vegna hljóðlátra stunda.", - "finished": "Lotan náði lokastöðu og endaði." + "finished": "Lotan náði lokastöðu og endaði.", + "match_reverted": "Kerfið féll aftur í greiningu: skor þess fór langt niður fyrir eigið hámark.", + "verified_pause": "Samstillingin í rauntíma staðfesti hlé innan kerfisins (eða aflétti því). Á meðan það varir er lotunni ekki lokið.", + "stalled": "Lotan sat á biðstöðunotkun lengur en þetta kerfi gerir nokkurn tíma hlé þar, þótt verki væri ólokið. Sýnt sem Gert hlé (Stöðvuð); lotan helst opin." }, "store": { "search_ph2": "Leita eftir vörumerki eða gerð…", @@ -2109,11 +2043,7 @@ "programs_count": "Kerfi: {n}", "browse": "Skoða", "device": "Tæki", - "favorites": "Uppáhald", - "search_ph": "Leita eftir vörumerki eða gerð…", "no_results": "Engin samsvarandi tæki fundust. Prófaðu aðra leit.", - "n_programs": "{n} kerfi", - "n_cycles": "{n} lotur", "no_programs": "Engum kerfum hefur verið deilt fyrir þetta tæki enn.", "no_cycles": "Engum viðmiðunarlotum hefur verið deilt fyrir þetta kerfi enn.", "anon": "nafnlaus", @@ -2125,19 +2055,21 @@ "merge_target": "Sameina við prófíl", "description": "Lýsing (valfrjálst)", "contributed_by": "eftir {name}", + "website": "Vefur samfélagsverslunar ↗", + "website_tip": "Opna vef samfélagsverslunar", + "favorites": "Uppáhald", "rating_summary": "★ {avg} ({n})", "no_ratings": "Engar einkunnir enn", - "website": "Vefur samfélagsverslunar ↗", - "website_tip": "Opna vef samfélagsverslunar" + "unreachable": "Ekki náðist samband við samfélagsverslunina. Athugaðu tenginguna og reyndu aftur." }, "settings": { "gear": { - "title": "Stillingar" + "title": "Kjörstillingar" } }, "link": { - "manual": "Handbók ↗", - "add_appliance": "Bæta við tækinu þínu" + "maintenance_supporter": "Maintenance Supporter á GitHub ↗", + "manual": "Handbók ↗" }, "tip": { "add_brand": "Vörumerki ekki á listanum? Bættu því við samfélagsskrána", @@ -2171,6 +2103,12 @@ }, "rebuild": { "envelopes": "Endurreikna aflhjúpa" + }, + "auto_label": { + "running": "Merki lotur sjálfvirkt" + }, + "store_download": { + "running": "Sæki úr versluninni" } }, "setup": { @@ -2180,26 +2118,24 @@ }, "phase0": { "washer": "WashData greinir nú þegar lotur þínar. Taktu upp fyrstu lotuna til að virkja heiti kerfa og tímamat.", - "dishwasher": "WashData fylgist með. Uppþvottavélar hafa flóknar lotur – mjög mælt er með að taka upp fyrstu lotuna. Ef greind lota keyrir of lengi skaltu nota lotu-ritilinn til að snyrta hana áður en hún er vistuð sem snið.", + "dishwasher": "WashData fylgist með. Uppþvottalotur eru flóknar, svo taktu upp fyrstu lotuna þína. Ef greind lota er of löng skaltu snyrta hana í lotu-ritlinum áður en þú vistar hana sem snið.", "generic": "WashData fylgist með. Taktu upp eða merktu greinda lotu til að hefja uppbyggingu sniða." }, "phase1a": { - "labelled": "Gott upphaf - fyrsta kerfið þitt er vistað. Fyrir hreinustu gögn skaltu íhuga að taka upp næstu lotu með upptökugræjunni." + "labelled": "Gott upphaf: fyrsta kerfið þitt er vistað. Fyrir hreinustu gögnin skaltu taka næstu lotu upp með upptökutækinu." }, "phase1b": { - "recorded": "Upptakan þín var vistuð sem {profile_name}. Taktu nú upp eða merktu önnur algeng kerfi þín til að byggja upp þekju." + "recorded": "Upptakan þín var vistuð sem {profile_name}. Taktu nú upp eða merktu önnur algeng kerfi þín." }, "phase1c": { - "verify": "Þú ert með {count} kerfi úr samfélaginu. Keyrðu lotu til að staðfesta að WashData þekki hana rétt - samsvörun batnar eftir því sem tækið byggir upp sína eigin sögu." + "verify": "Þú ert með {count} kerfi úr samfélaginu. Keyrðu lotu til að athuga hvort WashData þekki hana; samsvörun batnar eftir því sem tækið byggir upp eigin sögu." }, "phase2": { - "cluster": "WashData hefur séð {count} lotur sem samsvara ekki neinu vistuðu kerfi - þær líkjast hver annarri. Viltu búa til nýtt snið fyrir þær?", + "cluster": "WashData hefur séð {count} svipaðar lotur sem samsvara engu vistuðu kerfi. Búa til nýtt snið fyrir þær?", "unmatched": "Síðasta lota þín samsvaraði ekki neinu vistuðu kerfi. Er þetta nýtt kerfi?" }, "phase3": { - "suggestions": "WashData hefur stillingatillögur byggðar á lotuferli þínum – farðu yfir þær til að bæta nákvæmni greiningar.", - "groups": "Sum snið þín líkjast sama kerfi við mismunandi hitastig. Settu þau í hóp til að fá betri samsvörun.", - "phases": "Bættu fösum kerfisins við {profile_name} fyrir nákvæmara mat á tíma sem eftir er." + "suggestions": "WashData hefur stillingatillögur út frá lotusögu þinni. Farðu yfir þær til að bæta greininguna." }, "phase4": { "healthy": "Þetta tæki er að fullu sett upp ({profile_count} snið)." @@ -2212,12 +2148,58 @@ "create_from_cluster": "Búa til snið úr þessum lotum", "create_profile": "Búðu til snið fyrir hana", "review_suggestions": "Yfirfara tillögur", - "organise_profiles": "Skipuleggja snið", - "configure_phases": "Stilla fasa", "skip_step": "Sleppa þessu skrefi", "skip_forever": "Sýna ekki aftur", "hide_guidance": "Fela leiðbeiningar", "show_guidance": "Sýna leiðbeiningar" } + }, + "access": { + "edit": "Breyta", + "full": "Fullt", + "inherit": "Erfa", + "none": "Ekkert (falið)", + "read": "Lesa" + }, + "pg_label_reason": { + "no_winner": "Ekkert kerfi passaði", + "below_floor": "Undir Námsöryggi", + "ambiguous": "Merkt sem óljós samsvörun", + "margin": "Of nálægt næstbesta kerfinu", + "unknown_profile": "Kerfið er ekki lengur til", + "not_evaluated": "Ekki metið" + }, + "diagram": { + "accum": "uppsafnað", + "auto": "sjálfv.", + "below_off": "undir = slökkt", + "confirmed": "staðfest", + "energy_counts": "orka telur", + "feedback": "umsögn", + "gap_below_min": "bil undir lágm. = ein lota", + "heat_phase": "hitunaráfangi", + "held_at_100": "haldið í 100%", + "ignored": "hunsað", + "kept": "haldið", + "max": "hám.", + "min": "lágm.", + "minus_tol": "-vikm.", + "no_match": "engin samsvörun", + "no_updates": "engar uppfærslur", + "off_delay_wait": "slökkvitöf", + "off_gap": "slökkubil", + "plus_tol": "+vikm.", + "profile": "prófíll", + "reading_interval": "dæmigert mælibil", + "sensor_offline": "skynjari ótengdur: þvinguð stöðvun", + "si": "SB", + "spike_ignored": "toppur hunsaður", + "spike_ignored_short": "toppur: hunsaður", + "start": "ræsing", + "stop": "stöðvun", + "tail_energy_resets": "lokaorka yfir þrösk.: teljari núllast", + "thr": "þrösk.", + "too_short": "of stutt", + "tumble_kept": "veltupúlsum haldið" } } diff --git a/custom_components/ha_washdata/translations/panel/it.json b/custom_components/ha_washdata/translations/panel/it.json index 36097674..e655f918 100644 --- a/custom_components/ha_washdata/translations/panel/it.json +++ b/custom_components/ha_washdata/translations/panel/it.json @@ -1,15 +1,16 @@ { "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_tip": "Verso la fine questo programma resta in silenzio per circa {mins} min, poi assorbe circa {watts} W per {secs} s. Osservato in {seen} su {measured} cicli misurati.", "quiet_tail": "silenzio finale ~{mins} min", "artifact_tip": "{n} anomalie rilevate (ad esempio porta aperta a metà ciclo): apri per vederle sul grafico", + "artifact_tip_one": "{n} anomalia rilevata (ad esempio porta aperta a metà ciclo): apri per vederla sul grafico", + "artifact_tip_many": "{n} anomalie rilevate (ad esempio porta aperta a metà ciclo): apri per vederle sul grafico", + "artifact_tip_other": "{n} anomalie rilevate (ad esempio porta aperta a metà ciclo): apri per vederle sul grafico", "auto": "(rilevato automaticamente)", "built_in_tag": "integrato", "declining": "↘ in calo", "energy_low": "Energia inferiore al solito", "energy_spike": "Energia superiore al solito", - "fair_fit": "qualità accettabile", - "fair_fit_tip": "Coerenza della corrispondenza moderata: alcuni cicli assegnati a questo profilo hanno punteggi di confidenza inferiori. Etichettare più cicli o registrare nuovamente il profilo per migliorare la precisione.", "feedback_requested": "Feedback richiesto", "golden_cycle": "Ciclo di riferimento registrato", "improving": "↗ in miglioramento", @@ -17,28 +18,41 @@ "needs_review": "Necessita di revisione", "overrun": "Ha funzionato più a lungo del solito", "poor_fit": "scarsa vestibilità", - "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", + "restart_gap_tip": "{n} interruzioni riavvio HA: il tracciato di potenza ha dei buchi", + "restart_gap_tip_one": "{n} interruzione riavvio HA: il tracciato di potenza ha un buco", + "restart_gap_tip_many": "{n} interruzioni riavvio HA: il tracciato di potenza ha dei buchi", + "restart_gap_tip_other": "{n} interruzioni riavvio HA: il tracciato di potenza ha dei buchi", "reviewed": "Revisionato", "steady": "→ stabile", "underrun": "Terminato prima del solito", - "using_defaults": "Utilizzo delle impostazioni predefinite fornite", - "using_tuned": "Utilizzo di pesi accordati", "overrun_ratio": "({x}x il previsto)", "underrun_ratio": "({pct}% del previsto)", - "awaiting_tip": "In attesa dell'approvazione della community", - "awaiting_n": "In attesa di approvazione · {n} confermati", - "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.", "armed": "(si applica al prossimo ciclo)", - "duration_outlier": "{n} ciclo/i di durata anomala" + "duration_outlier": "{n} ciclo/i di durata anomala", + "awaiting_tip": "In attesa dell'approvazione della community", + "awaiting_n": "In attesa di approvazione · {n} confermati", + "awaiting": "In attesa di approvazione", + "approved": "Approvato", + "matcher_unused": "Nessuna corrispondenza con i tuoi cicli finora", + "matcher_used": "Corrisponde a {n} dei tuoi cicli", + "matcher_used_one": "Corrisponde a {n} dei tuoi cicli", + "matcher_used_many": "Corrisponde a {n} dei tuoi cicli", + "matcher_used_other": "Corrisponde a {n} dei tuoi cicli", + "matcher_used_tip": "Cicli di questo elettrodomestico che WashData ha associato da solo a questo programma importato. Un'importazione che non trova mai corrispondenza potrebbe non adattarsi al tuo elettrodomestico; puoi rimuoverla. Conteggiato in locale; nulla viene inviato allo store della community." }, "btn": { + "log_done_task": "Segna come fatto: {task}", + "add_task": "Aggiungi attività", + "pg_preset_delete": "Elimina preimpostazione", + "pg_preset_load": "Carica preimpostazione", + "pg_preset_load_tip": "Sostituisci i valori del Playground con questa preimpostazione (le impostazioni attive non vengono toccate)", + "pg_preset_save": "Salva come preimpostazione", + "pg_preset_save_tip": "Salva tutti i valori qui sotto come preimpostazione con nome per questo dispositivo", "reset_zoom": "Reimposta zoom", "set_brand_model": "Imposta marca e modello", "refresh_catalog": "Aggiorna catalogo", @@ -49,12 +63,11 @@ "add_timer": "+ Aggiungi timer", "apply_all": "Applica tutto", "apply_label": "Applica etichetta", - "apply_set_b": "Applica set B", "apply_split": "Applica divisione", "apply_trim": "Applica ritaglio", "auto_detect_split": "Rilevamento automatico", "auto_label_cycles": "Cicli di etichettatura automatica", - "auto_label_cycles_tip": "Assegna automaticamente nomi di profilo ai cicli senza etichetta la cui confidenza di corrispondenza supera la soglia", + "auto_label_cycles_tip": "Dai un nome ai cicli senza etichetta la cui confidenza di corrispondenza supera la soglia.", "cancel": "Cancellare", "clear_debug": "Cancella dati di debug", "clear_splits": "Cancella", @@ -64,8 +77,6 @@ "convert_to_automation": "Convertire all'automazione", "correct": "Corretto…", "create": "Creare", - "create_from_cluster": "Crea profilo da {count} cicli", - "create_profile": "+ Crea profilo", "delete": "Eliminare", "delete_group": "Elimina gruppo", "delete_group_tip": "Elimina solo questo gruppo: i profili dei membri verranno mantenuti", @@ -76,22 +87,19 @@ "done": "Fatto", "edit": "Modificare", "export": "Esporta", - "export_json": "Esporta in JSON", "force_stop": "Forza l'arresto", "force_stop_tip": "Termina immediatamente il ciclo corrente e lo contrassegna come arrestato forzatamente", "from_template": "Dal modello ▾", "ignore": "Ignorare", "import_json": "Importa da JSON", - "import_overwrite": "Importa (sovrascrive i dati)", "inspect": "Ispezionare", "label": "Assegna etichetta", - "load": "Carica", "load_more": "Carica altro", "manage": "Gestisci", "merge": "Unisci", "new_automation": "+ Nuova automazione", "new_group": "+ Nuovo gruppo", - "new_group_tip": "Raggruppa profili quasi identici (stessa forma/durata, temperatura o centrifuga diversi) in modo che il comparatore li distingua in modo affidabile", + "new_group_tip": "Raggruppa profili quasi identici (stessa forma, temperatura o centrifuga diversa) così la corrispondenza riesce a distinguerli.", "new_phase": "+ Nuova fase", "new_profile": "+ Nuovo profilo", "new_profile_tip": "Crea un nuovo profilo di programma da un ciclo etichettato esistente o da una registrazione", @@ -99,36 +107,25 @@ "on_cycle_started": "Il ciclo è iniziato", "pause_cycle": "Pausa", "pause_cycle_tip": "Metti in pausa il ciclo in corso – l'elettrodomestico riprenderà da dove si era fermato", - "pg_apply_device": "Applica al dispositivo", - "pg_autofill": "Compilazione automatica", - "pg_load_calibrated": "Carica Calibrato (ML) ({n})", - "pg_load_calibrated_tip": "Carica le impostazioni calibrate da ML come sostituzioni nel Playground", "pg_load_live": "Carica le impostazioni attive", "pg_load_live_tip": "Rileggi le impostazioni attuali dell'integrazione e scarta tutte le modifiche fatte nel Playground", "pg_load_suggested": "Carica suggerimenti ({n})", "pg_load_suggested_tip": "Carica i suggerimenti attuali dell'ottimizzatore automatico come sostituzioni nel Playground", - "pg_preset_delete": "Elimina preimpostazione", - "pg_preset_load": "Carica preimpostazione", - "pg_preset_load_tip": "Sostituisci i valori del Playground con questa preimpostazione (le impostazioni attive non vengono toccate)", - "pg_preset_save": "Salva come preimpostazione", - "pg_preset_save_tip": "Salva tutti i valori qui sotto come preimpostazione con nome per questo dispositivo", "pg_publish_all": "Salva {n} impostazioni nell'integrazione", "pg_publish_one": "Salva {label} nell'integrazione", - "play": "Riproduci", "process": "Processo", "process_history": "Elabora ora", "process_recording": "Salva registrazione", "process_tip": "Salva la traccia registrata come profilo nuovo o esistente", "rebuild": "Ricostruisci le buste", "rebuild_envelope": "Ricostruisci la busta", - "rebuild_tip": "Ricalcola la busta di potenza attesa (banda min/max) per tutti i profili dai loro cicli etichettati – eseguire dopo aver etichettato nuovi cicli o corretto quelli esistenti", + "rebuild_tip": "Ricostruisci la banda di potenza attesa di ogni profilo dai suoi cicli etichettati. Esegui dopo aver etichettato o corretto dei cicli.", "rec_start_tip": "Avvia la registrazione della traccia di potenza dell'elettrodomestico – avvia appena prima di eseguire un ciclo", "rec_stop": "Fermare", "rec_stop_tip": "Ferma la registrazione e mantieni la traccia acquisita per la revisione", "record": "Inizia a registrare", "refresh": "Aggiorna", "refresh_diag_tip": "Ricarica le statistiche di archiviazione", - "refresh_logs_tip": "Ricarica i record di registro più recenti", "refresh_settings_tip": "Ricarica le impostazioni dal server", "relabel": "Rietichetta ({count})", "remove": "Rimuovere", @@ -136,7 +133,6 @@ "reset": "Reset", "reset_ml_models_tip": "Scarta i modelli ottimizzati e torna a quelli integrati. WashData può riapprenderli in seguito.", "reset_to_builtin": "Ripristina i modelli integrati", - "reset_to_defaults": "Ripristina le impostazioni predefinite", "resume_cycle": "Riprendi ciclo", "resume_cycle_tip": "Riprendi il ciclo in pausa", "revert_settings": "Annulla modifiche", @@ -149,8 +145,6 @@ "run_analysis": "🔍 Esegui l'analisi dei suggerimenti", "run_analysis_tip": "Analizza subito i tuoi cicli registrati e aggiorna i suggerimenti di ottimizzazione", "run_auto_label": "Esegui l'etichettatura automatica", - "run_both": "Esegui A vs B", - "run_simulation": "Esegui simulazione", "save": "Salva", "save_access_control": "Salva controllo accessi", "save_group": "Salva gruppo", @@ -162,10 +156,8 @@ "save_settings": "Salva impostazioni", "select": "Selezionare", "select_all": "Seleziona tutto", - "select_last20": "Ultimi 20", "select_none": "Deseleziona", "show_only": "Mostra solo", - "skip_setup": "Salta configurazione", "split": "Diviso", "stop": "Ferma", "submit_correction": "Invia correzione", @@ -174,36 +166,31 @@ "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.", + "unlabel_selected_tip": "Rimuovi l'etichetta dai cicli selezionati. Restano nella tua cronologia ma smettono di plasmare questo profilo.", "use": "Usa", "wipe_all": "Cancella tutti i dati", "delete_selected": "Elimina selezionati ({n})", - "group_suggest": "Gruppo {n}: {members}", "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", "import": "Importa", "disconnect": "Disconnetti", "connect_github": "Connetti GitHub", - "save_appliance": "Salva elettrodomestico", "share_to_store": "Condividi nello store", "share": "Condividi", - "confirm_appliance": "Conferma questo elettrodomestico", - "confirmed": "Confermato", "browse_community_setups": "Sfoglia le configurazioni della community", "download_device": "Scarica questa configurazione", "share_device": "⬆ Condividi questo dispositivo", - "share_device_tip": "Condividi questo elettrodomestico e i suoi cicli di riferimento registrati nello store della community così altri con lo stesso dispositivo possono adottarli", + "share_device_tip": "Condividi questo elettrodomestico e i suoi cicli di riferimento nello store della community, così chi ha la stessa macchina può adottarli.", "share_n": "Condividi {n} ciclo/i", "export_selected": "Esporta (scegli i dati)", "export_all": "Esporta tutto rapidamente", - "import_raw": "Avanzato: sostituisci tutto da JSON", "download_export": "Scarica l'esportazione", "analyze_import": "Analizza il file", "import_selected": "Importa la selezione", "back": "Indietro", "mute_suggestion": "Non suggerire più questa impostazione", "reset_muted": "Ripristina i silenziati", + "reset_phase_tip": "Annulla la modifica e ripristina la fase integrata", "import_power_history": "Importa lo storico di potenza", "hist_read_recorder": "Leggi da Home Assistant", "hist_scan": "Cerca cicli", @@ -212,13 +199,15 @@ "save_total_cycles": "Salva totale", "clear": "Cancella", "add_brand": "Aggiungi marca", - "add_appliance": "Aggiungi elettrodomestico" + "add_appliance": "Aggiungi elettrodomestico", + "retry": "Riprova", + "confirm_appliance": "Conferma questo elettrodomestico", + "confirmed": "Confermato", + "create_profile_from_gap": "Crea profilo", + "undo_import": "Annulla l'ultima importazione", + "pg_rerun": "Aggiorna simulazione" }, "conflict": { - "anti_wrinkle_exit": { - "exit": "Deve essere inferiore alla Soglia di arresto ({stop} W); altrimenti la potenza di uscita anti-grinze viene ignorata", - "stop": "Deve essere superiore alla Potenza di uscita anti-grinze ({exit} W)" - }, "anti_wrinkle_max": { "max": "Deve essere superiore alla Soglia di avvio ({start} W); altrimenti il limite di durata anti-grinze viene ignorato", "start": "Deve essere inferiore alla Potenza massima anti-grinze ({max} W)" @@ -226,10 +215,10 @@ "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." + "power_sensor": "Questo è il Sensore di potenza del dispositivo, non un prezzo. I cicli verrebbero addebitati in watt per kWh; svuota questo campo.", + "energy_sensor": "Questa è l'Entità del contatore di energia del dispositivo, non un prezzo. I cicli verrebbero addebitati alla lettura del contatore per kWh; svuota questo campo.", + "device_class": "Questo sensore misura {dc}, non un prezzo per kWh. Svuota questo campo o scegli un sensore di tariffa.", + "unit": "Questo sensore misura in {unit}, non in prezzo per kWh. Svuota questo campo o scegli 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", @@ -264,8 +253,8 @@ "threshold": "Deve essere inferiore alla Soglia di arresto ({stop} W) per avere effetto" }, "pump_stuck": { - "duration": "Deve essere inferiore al Timeout assenza aggiornamenti ({to} s) affinché l'allarme pompa bloccata scatti prima che il watchdog termini il ciclo", - "timeout": "Deve superare la Durata pompa bloccata ({ps} s) affinché l'allarme scatti prima che il ciclo venga fermato forzatamente" + "duration": "Deve essere inferiore al Timeout senza aggiornamenti ({to} s) perché l'allarme pompa bloccata possa scattare per primo", + "timeout": "Deve superare la Durata pompa bloccata ({ps} s) perché l'allarme pompa bloccata possa scattare per primo" }, "start_dur": { "sampling": "L'Intervallo di campionamento supera la Durata di avvio ({sdt} s); picchi su un singolo campione possono aprire un ciclo", @@ -281,10 +270,15 @@ "watchdog": { "interval": "Dovrebbe essere almeno 2x l'Intervallo di campionamento ({si} s)", "sampling": "L'Intervallo di campionamento dovrebbe essere al massimo la metà dell'Intervallo watchdog ({wi} s)" + }, + "end_energy": { + "energy": "Troppo restrittiva per la Soglia di arresto ({w} W) nell'arco del Ritardo spegnimento ({d} s); il ciclo può terminare solo tramite un percorso di riserva", + "stop": "Energia fine ciclo ({e} Wh in {d} s) consente solo {p} W" } }, "hdr": { - "ab_comparison": "Confronto impostazioni A/B", + "custom_tasks": "Le tue attività", + "maintenance_supporter": "Ti serve di più?", "access_control": "Controllo degli accessi", "add_maintenance": "Aggiungi evento di manutenzione", "automation_delete": "Elimina questa automazione", @@ -293,28 +287,22 @@ "automation_review_this_cycle": "Rivedi questo ciclo", "automations": "Automazioni", "clear_debug": "Cancella tracce di debug", - "cycle_simulator": "Simulatore di cicli", "diagnostics_pref": "Diagnostica", "display": "Visualizzazione", - "dtw_inspector": "Ispettore DTW", "export_import": "Esporta/Importa", - "getting_started": "Per iniziare", "live_power": "Potenza viva", "logs": "Registri", - "logs_diagnostics": "Diagnostica", "maintenance": "Azioni di manutenzione", "maintenance_log": "Registro manutenzione", "maintenance_reminders": "Promemoria di servizio", "manual_recording": "Registrazione manuale", - "manual_recording_tip": "Esegui intenzionalmente un ciclo mentre WashData registra la traccia di potenza. Avvia appena prima che l'elettrodomestico parta, Ferma quando ha finito, poi Elabora per salvarlo come profilo con nome.", + "manual_recording_tip": "Registra un ciclo di proposito: premi Inizio subito prima che l'elettrodomestico parta e Ferma quando finisce, poi Processo per salvarlo come profilo.", "ml_learned": "Cosa ha imparato WashData", - "ml_matching_tuning": "Personalizzazione della corrispondenza del programma", "ml_settings_card": "Impostazioni", "ml_smart_learning": "Apprendimento intelligente", "my_preferences": "Le mie preferenze", "no_automations": "Nessuna automazione fa ancora riferimento a questo dispositivo.", "panel_settings": "Impostazioni del pannello", - "param_sweep": "Scansione parametri", "pg_settings_source": "Origine delle impostazioni", "phase_catalog": "Catalogo delle fasi", "playground": "Playground", @@ -323,11 +311,8 @@ "status": "Stato", "status_graph": "Grafico dello stato", "storage_stats": "Statistiche di archiviazione", - "test_history": "Prova sullo storico", "toggle_sidebar": "Attiva/disattiva la barra laterale di Home Assistant", - "tools_and_data": "Strumenti e dati", "wipe_history": "Cancella la storia", - "recommendations": "Consigli ({n})", "pg_detection_params": "Impostazioni di rilevamento", "pg_outcome": "Esito della simulazione", "pg_across_cycles": "Su tutti i tuoi cicli", @@ -335,7 +320,8 @@ "online_account": "Store della community e funzionalità online", "import_power_history": "Importa lo storico di potenza", "service_status": "Stato della manutenzione", - "online_account_tab": "Online e community" + "online_account_tab": "Online e community", + "live_match_debug": "Debug abbinamento in tempo reale" }, "health": { "fair": "Qualità profilo accettabile", @@ -343,7 +329,12 @@ "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.", + "task_name": "Attività", + "every_cycles": "Ogni (cicli)", + "every_days": "Ogni (giorni)", + "new_task_name": "Nuova attività", + "days_since_service": "{since} / {total} giorni", + "envelope_position_tip": "Posizione sulla curva registrata del programma corrispondente: {pct}%. Ricavata allineando il ciclo al profilo, quindi resta corretta anche se un ciclo dura di più o di meno. Si aggiorna mentre l'elettrodomestico è in silenzio, quindi può restare indietro.", "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})", @@ -355,10 +346,8 @@ "aria_cycle_chart": "Curva di potenza del ciclo", "aria_envelope_chart": "Inviluppo di potenza del profilo", "aria_phase_chart": "Grafico dell'editor delle fasi", - "aria_playground_chart": "Curva di potenza del ciclo con soglie di rilevamento trascinabili", "aria_power_chart": "Grafico di potenza", "aria_spaghetti_chart": "Curve di potenza dei cicli sovrapposte", - "aria_sweep_chart": "Grafico dei risultati della scansione dei parametri", "artifact_anomaly": "Anomalia", "artifact_high_power": "Alta potenza", "artifact_interruption": "Interruzione", @@ -372,150 +361,107 @@ "cohesion_good": "coesione {pct}%", "cohesion_low": "⚠ coesione bassa {pct}%", "compare_profiles": "Confronta con i profili", - "concurrency": "Concorrenza", "confidence": "Fiducia", "confidence_threshold": "Soglia di fiducia", "correct_duration": "Durata corretta (min, facoltativo)", "correct_profile": "Profilo corretto", - "correlation": "Correlazione", "cost": "Costo", "create_new_profile": "+ Crea un nuovo profilo…", "current_phase": "Fase attuale", - "curves": "Curve", "cycle": "Ciclo", "cycle_date_display": "Visualizzazione della data del ciclo", - "cycle_health": "Salute del ciclo", - "cycle_index": "indice ciclo →", - "cycles": "Cicli", + "cycle_context": "Storico del recorder attorno al ciclo", + "cycle_context_off": "Disattivato", + "cycle_context_minutes": "{min} min", + "cycle_context_series": "Storico del recorder", + "before_start": "Prima dell'avvio", + "after_end": "Dopo la fine", "cycles_count": "Cicli", "cycles_lc": "cicli", - "dashed": "tratteggiato", "date": "Data", "debug_traces": "Tracce di debug", - "default": "Predefinito", "default_access_level": "Livello predefinito per gli utenti non elencati di seguito", "default_other": "Predefinito (altri dispositivi)", "default_tab": "Scheda predefinita all'apertura del pannello", - "delta": "Δ", "description": "Descrizione", - "dtw_blend_w": "Peso di fusione", - "dtw_blended": "Punteggio combinato", - "dtw_ddtw": "Punteggio DDTW", - "dtw_l1": "Punteggio L1", - "dtw_matched_profile": "(profilo abbinato del ciclo)", "duration": "Durata", - "duration_agreement": "Concordanza durata", - "emphasis": "Enfasi", "enable_access_control": "Abilita il controllo dell'accesso per utente", "energy": "Energia", - "energy_agreement": "Concordanza energia", - "event_timeline": "Cronologia eventi", "event_type": "Tipo di evento", "expected": "Atteso", + "suspected_profile": "{name} (probabile)", + "runner_up_profile": "{name} (secondo candidato)", "expected_duration": "Durata prevista (min)", "file_kb": "File (kB)", - "final_score": "Punteggio finale", "from": "Da", "gap_s": "Divario/i", "group_name": "Nome del gruppo", "head_trim": "Ritaglio iniziale (s)", - "health": "Salute", "hide_tabs": "Nascondi le schede per i non amministratori", - "in_use": "In uso", "json_data": "Dati JSON", "label": "Programma", "last": "Ultimi", "last_checked": "Ultimo controllo:", "load_from_file": "Carica da file", - "mae_score": "Punteggio MAE", "manual_duration": "Durata manuale (min, opzionale)", "match": "Abbinamento", "members": "Membri", - "metric": "Metrica", "mode_existing_profile": "Aggiungi al profilo esistente", "mode_new_profile": "Crea nuovo profilo", "models_fine_tuned": "(modelli ottimizzati: {count})", - "n_classic_suggestions": "{n} classico", - "n_ml_suggestions": "{n} ML", "n_selected": "{n} selezionati", - "n_tuning_suggestions": "{n} suggerimento di ottimizzazione", - "na": "n/d", + "n_tuning_suggestions": "{n} suggerimenti di ottimizzazione", + "n_tuning_suggestions_one": "{n} suggerimento di ottimizzazione", + "n_tuning_suggestions_many": "{n} suggerimenti di ottimizzazione", + "n_tuning_suggestions_other": "{n} suggerimenti di ottimizzazione", "new_profile_name": "Nuovo nome del profilo", "notes": "Note", - "of": "di", - "outcome": "Esito", "panel_default_tab": "Scheda predefinita", "panel_language": "Lingua del pannello", "per_cycle_short": "ciclo", "pg_anti_wrinkle": "Antirughe", - "pg_autofill_tip": "Compila automaticamente l'intervallo dal valore attuale ({lo}–{hi})", "pg_best_value": "Miglior valore trovato", "pg_current_value": "Valore attuale:", "pg_drag_hint": "Trascina la linea sul grafico", - "pg_dtw": "DTW", "pg_ev_ending": "In chiusura", "pg_ev_running": "In esecuzione", "pg_group_advanced": "Casi limite", "pg_group_detection": "Trigger di rilevamento", "pg_group_timing": "Regole di tempistica", "pg_idle": "Inattivo", - "pg_last_run": "Ultima esecuzione: {date}", - "pg_match_score": "Punteggio di abbinamento", - "pg_pct_of": "{p}% di {total}", "pg_poor_match": "Abbinamento scarso", "pg_preset": "Preimpostazione del Playground", "pg_preset_name": "Nome della nuova preimpostazione", "pg_preset_none": "Seleziona una preimpostazione…", "pg_strong_match": "Abbinamento forte", - "pg_tile_ambiguous": "Ambiguo", - "pg_tile_ambiguous_tip": "Cicli con un quasi pareggio tra due programmi", - "pg_tile_detected": "Rilevato", - "pg_tile_detected_tip": "Cicli in cui la potenza ha superato la soglia di avvio abbastanza a lungo da essere registrata come in esecuzione", - "pg_tile_matched": "Riconosciuto", - "pg_tile_matched_tip": "Cicli correttamente attribuiti a un programma noto", - "pg_time_left": "Tempo rimanente", "pg_weak_match": "Abbinamento debole", "phase_name": "Nome della fase", "name": "Nome", "phase_timeline": "Cronologia fasi", "power": "Energia", "profile": "Profilo", - "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. Scegline uno prima di avviare l'elettrodomestico e verrà applicato non appena inizia il prossimo ciclo.", + "program_tip": "Scegli a quale programma viene abbinato il ciclo corrente. Il rilevamento automatico sceglie la corrispondenza migliore. Fissa un programma quando il rilevamento automatico sbaglia; se lo scegli prima dell'avvio, vale per il ciclo successivo.", "progress": "Progressi", "raw_socket": "Segnale grezzo", - "read_only": "Sola lettura", "ref_cycle": "Ciclo di riferimento (facoltativo)", - "refresh_interval": "Intervallo di aggiornamento (s)", "remaining": "Rimanente", "reminder_every": "Ricorda ogni (cicli)", "remove_label": "- Rimuovi l'etichetta -", "rename_profile": "Rinomina profilo", - "replay_duration": "Riproduzione", "resulting_profile": "Profilo risultante", "save_mode": "Modalità di salvataggio", - "score": "Punteggio", - "score_breakdown": "Dettaglio punteggio", "seconds_from_start": "Secondi dall'inizio", "select_profile": "Seleziona Profilo", "setting": "Impostazione", - "settings_a": "Impostazioni A (attuali)", - "settings_a_short": "A", "settings_advanced": "Avanzato", - "settings_b": "Impostazioni B (test)", - "settings_b_short": "B", "settings_basic": "Base", "settings_detail_level": "Livello di dettaglio impostazioni", - "settings_overrides": "Override impostazioni", "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", - "stage2": "Fase 2 – somiglianza di base", - "stage3": "Fase 3 – DTW", - "stage4": "Fase 4 – concordanza", "status": "Stato", "tail_trim": "Ritaglio finale (s)", "tap_to_dismiss": "tocca il grafico per chiudere", @@ -526,7 +472,6 @@ "ungrouped": "Non raggruppato", "unlabelled": "Senza etichetta", "unlabelled_paren": "(senza etichetta)", - "warp_path": "Percorso di warping", "input": "Input:", "start": "Inizio", "end": "Fine", @@ -551,9 +496,8 @@ "n_shown": ", {n} mostrati", "cycles_title": "Cicli ({n})", "pg_detecting": "Rilevamento", - "pg_best_marker": "Migliore: {v}", "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", + "pg_time_left_model_tip": "Stimato dalle fasi del programma, non un conto alla rovescia fisso", "phase": "Fase", "pg_mode_history": "Prova sullo storico", "pg_ev_detected": "Rilevato", @@ -576,9 +520,6 @@ "pg_alert_unmatched": "Senza abbinamento", "pg_alert_ambiguous": "Abbinamento ambiguo", "pg_alert_energy": "Anomalia di energia", - "pg_alert_stress_cap": "Limite di sicurezza raggiunto", - "pg_alert_stress_ok": "Terminazione a riposo: ciclo interrotto", - "pg_alert_stress_warn": "Consumo in standby superiore alla soglia di arresto", "pg_newly_correct": "ora corretto", "pg_regressed": "peggiorato", "pg_end_timing_changed": "momento di fine modificato", @@ -588,12 +529,6 @@ "pg_sweep_param": "Parametro", "pg_objective": "Obiettivo", "pg_steps": "Passaggi", - "pg_stress_group": "Test di terminazione a riposo", - "pg_stress_idle_auto": "Auto", - "pg_stress_idle_w": "Livello a riposo (W)", - "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_obj_match": "Precisione di abbinamento", "pg_obj_endtiming": "Precisione del momento di fine", "pg_obj_falseend": "Tasso di false conclusioni", @@ -602,7 +537,6 @@ "pg_mode_optimize": "Ottimizza", "pg_group_matching": "Corrispondenza del programma", "pg_zoom_hint": "fai doppio clic per reimpostare lo zoom", - "aria_playground_chart2": "Grafico interattivo della potenza del ciclo: passa il puntatore per leggere tempo/potenza, scorri per ingrandire, trascina per spostarti", "pg_alert_timeout_end": "Terminato per timeout, non per previsione", "pg_alert_indefinite": "Continuerebbe all'infinito", "task_pg_history": "Prova sullo storico", @@ -614,16 +548,9 @@ "pg_recent_runs": "Esecuzioni recenti", "pg_load_run": "Ricarica questa esecuzione", "pg_match_confidence": "Affidabilità dell'abbinamento", - "pg_envelope_fit": "Aderenza all'inviluppo", - "pg_envelope_fit_tip": "Quanto il ciclo rimane all'interno dell'inviluppo di potenza salvato di questo profilo, una prospettiva diversa dall'affidabilità dell'abbinamento.", "n_imported_note": ", {n} importati", "enable_online": "Abilita le funzionalità online", - "store_brand": "Marca dell'elettrodomestico", - "store_model": "Modello dell'elettrodomestico", "anonymous": "Anonimo", - "rate_quality": "Valuta la qualità", - "rate_n": "{n} stelle", - "approved_only": "Solo approvati", "adopt_settings": "Adotta anche le impostazioni", "flags": "Indicatori", "include_phase_map": "Includi mappa delle fasi", @@ -640,7 +567,6 @@ "cat_custom_phases": "Fasi personalizzate", "cat_profile_groups": "Gruppi di profili", "cat_settings": "Impostazioni di rilevamento e riconoscimento", - "cat_matching_config": "Regolazione del riconoscimento", "cat_ml_models": "Modelli ML", "cat_feedback": "Feedback ed etichette di revisione", "cat_suggestions": "Suggerimenti", @@ -662,7 +588,6 @@ "hist_csv_data": "Dati CSV", "hist_from_recorder": "Oppure leggilo da Home Assistant", "hist_since": "Dal", - "days": "giorni", "hist_keep": "Mantieni questo ciclo", "hist_looks_complete": "completo", "peak_power_short": "Picco", @@ -682,7 +607,36 @@ "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" + "correct_total_cycles": "Correggi il totale", + "hist_reason_already_recorded": "si sovrappone a un ciclo già registrato da WashData", + "share_consent": "Confermo che questi cicli sono terminati normalmente, senza interruzioni", + "show_internal": "Mostra impostazioni interne", + "show_internal_tip": "Impostazioni che WashData regola per te (campionamento, supervisore, cadenza di corrispondenza, soglie). Mostrate quando questo dispositivo ha un valore proprio, un conflitto o un suggerimento.", + "rate_n": "{n} stelle", + "rate_quality": "Valuta la qualità", + "pg_autolabel": "Etichettatura automatica", + "pg_label_review": "Richiede revisione", + "pg_ev_match_reverted": "Abbinamento annullato", + "pg_ev_verified_pause": "Pausa verificata", + "n_samples": "{n} campioni", + "n_samples_one": "{n} campione", + "n_samples_many": "{n} campioni", + "n_samples_other": "{n} campioni", + "n_cycles": "{n} cicli", + "n_cycles_one": "{n} ciclo", + "n_cycles_many": "{n} cicli", + "n_cycles_other": "{n} cicli", + "energy_per_cycle": "{v}/ciclo", + "energy_total": "{v} in totale", + "automation_off": "(disattivata)", + "ref_cycle_none": "Nessuno", + "share_name": "Mostra il mio nome su ciò che condivido", + "cat_backfill_cycles": "Cicli trovati in uno storico di potenza importato", + "pg_compare_profile": "Confronta con profilo", + "pg_obj_early": "Tasso di conclusioni anticipate", + "pg_obj_endlag": "Ritardo di fine (mediana)", + "pg_obj_split": "Tasso di divisioni", + "pg_sweep_cycles": "Cicli recenti" }, "log": { "all_levels": "Tutti i livelli", @@ -692,11 +646,9 @@ }, "ml": { "builtin_models": "● Utilizzo di modelli integrati", - "fit_fair": "Accettabile", "fit_good": "Bene", "fit_slight": "Lieve", "fit_strong": "Forte", - "fit_weak": "Debole", "fit_word": "accuratezza", "personalized": "● Personalizzato per questa macchina", "better_than_baseline": "{pct}% migliore della stima di riferimento ({metric})", @@ -704,30 +656,41 @@ "trend_declining_tip": "L'adattamento di questo modello è peggiorato nelle ultime verifiche: rivedere più cicli può aiutarlo a riapprendere.", "trend_steady_tip": "L'adattamento di questo modello è rimasto pressoché stabile nelle ultime verifiche.", "cap_blurb": { - "end": "Capire quando un ciclo è davvero terminato", - "live_match": "Identificare prima il programma in corso", - "quality": "Individuare cicli rilevati male o corrotti", - "remaining_time": "Prevedere quanto tempo manca", "total_energy": "Prevedere l'energia totale e il costo" }, "cap_label": { - "end": "Rilevamento di fine ciclo", - "live_match": "Riconoscimento del programma", - "quality": "Controllo qualità del ciclo", - "remaining_time": "Stima del tempo rimanente", "total_energy": "Stima dell'energia" }, - "metric_auc": "AUC {auc} sui dati di validazione", - "metric_mae": "errore {model} vs baseline {baseline}" + "metric_mae": "errore {model} vs baseline {baseline}", + "fine_tuned_at": "ottimizzato il {when}", + "held_out_count": "misurato su {count} cicli di validazione", + "held_out_count_one": "misurato su {count} ciclo di validazione", + "held_out_count_many": "misurato su {count} cicli di validazione", + "held_out_count_other": "misurato su {count} cicli di validazione", + "last_run_not_promoted": "Ultimo controllo ({when}): {reason}", + "never": "mai", + "skip_reason": { + "fit_failed": "Addestramento non riuscito: {error}", + "holdout_too_small": "Troppo pochi cicli per testare un nuovo modello in modo equo: {held_out} potevano essere messi da parte per il test, ne servono {min}.", + "insufficient_rows": "Non ci sono ancora abbastanza cicli utilizzabili: finora {count} cicli etichettati e puliti.", + "insufficient_rows_one": "Non ci sono ancora abbastanza cicli utilizzabili: finora {count} ciclo etichettato e pulito.", + "insufficient_rows_many": "Non ci sono ancora abbastanza cicli utilizzabili: finora {count} cicli etichettati e puliti.", + "insufficient_rows_other": "Non ci sono ancora abbastanza cicli utilizzabili: finora {count} cicli etichettati e puliti.", + "not_better_than_incumbent": "Il nuovo modello non era migliore di quello in uso (errore {model} vs {incumbent}), quindi è stato mantenuto quello attuale.", + "not_better_than_naive": "Il nuovo modello non era chiaramente migliore della stima semplice (errore {model} vs {naive}).", + "unknown": "Non è stato appreso nulla di nuovo." + }, + "trained_unknown": "in un momento sconosciuto" }, "modal": { + "remove_task_title": "Rimuovi attività", + "remove_task_msg": "Rimuovere il promemoria \"{task}\"? Le sue voci nel registro di manutenzione vengono conservate.", "auto_label": "Cicli di etichettatura automatica", "correct_feedback": "Feedback corretto", "create_profile": "Crea profilo", "cycle": "Ciclo", "edit_group": "Modifica gruppo di profili", "edit_phase": "Modifica fase", - "import_config": "Importa configurazione", "label_cycle": "Ciclo delle etichette", "merge_cycles": "Unisci {n} cicli", "new_group": "Nuovo gruppo di profili", @@ -742,6 +705,8 @@ "delete_automation_title": "Elimina automazione", "delete_automation_msg": "Eliminare l'automazione \"{name}\" da Home Assistant? L'operazione non può essere annullata.", "remove_legacy_title": "Rimuovi azioni legacy", + "reset_phase_msg": "Annullare la modifica di \"{name}\" e ripristinare la fase integrata? I programmi mantengono i loro intervalli.", + "reset_phase_title": "Ripristina fase", "remove_legacy_msg": "Rimuovere le azioni personalizzate legacy? Smetteranno di attivarsi sugli eventi di ciclo. L'operazione non può essere annullata dal pannello.", "discard_recording_title": "Elimina la registrazione", "discard_recording_msg": "Eliminare la registrazione salvata? L'operazione non può essere annullata.", @@ -762,63 +727,79 @@ "store_share_device": "Condividi questo dispositivo", "export_select": "Esporta - scegli i dati", "import_wizard": "Importa - scegli i dati", - "history_import": "Importa lo storico di potenza" + "history_import": "Importa lo storico di potenza", + "apply_suggestions_title": "Applica suggerimenti", + "apply_suggestions_msg": "Modificare queste {n} impostazioni? L'integrazione si ricarica dopo il salvataggio.", + "discard_settings_title": "Scartare le modifiche non salvate?", + "discard_settings_msg": "Modifiche alle impostazioni non salvate: {n}. Uscire da Impostazioni e scartarle?", + "profile_title": "Profilo · {name}", + "undo_import_msg": "Dati e impostazioni di questo dispositivo tornano com'erano prima dell'ultima importazione ({when}). I cicli e le modifiche successivi vanno persi.", + "undo_import_ok": "Annulla importazione", + "undo_import_title": "Annulla l'ultima importazione" }, "msg": { + "custom_task_limit": "Hai raggiunto il limite di {max} attività.", + "custom_tasks_intro": "Aggiungi qualsiasi altra cosa di cui vuoi un promemoria. Un'attività è in scadenza quando viene raggiunto uno dei due intervalli; lasciane uno vuoto o a 0 per usare solo l'altro.", + "maintenance_due_sensor_hint": "Il sensore binario Manutenzione in scadenza è attivo finché c'è un'attività in scadenza, per le tue automazioni di notifica.", + "maintenance_supporter_intro": "Maintenance Supporter è un'integrazione separata che fa molto di più: qualsiasi elettrodomestico o attività, scadenze in base a tempo, utilizzo o un sensore, notifiche e cronologia. Può usare il contatore dei cicli di WashData.", "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.", + "pg_preset_delete_confirm": "Eliminare la preimpostazione \"{name}\"?", + "pg_preset_limit": "Limite di preimpostazioni raggiunto ({n})", + "pg_preset_overwrite": "Sovrascrivere la preimpostazione \"{name}\"?", "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.", - "refresh_catalog_hint": "Gli elenchi di marche ed elettrodomestici della community sono memorizzati nella cache per mantenere lo store condiviso entro il suo limite giornaliero. Aggiorna per recuperare le voci aggiunte o approvate da altri utenti.", + "store_sibling_hint": "Niente di condiviso per il tuo modello? Un modello simile della stessa marca è di solito un buon inizio.", + "store_declare_appliance": "Indica a WashData quale elettrodomestico possiedi per vedere le configurazioni che altri hanno condiviso per esso. Oppure scrivi una marca qui sopra per dare un'occhiata.", + "refresh_catalog_hint": "Gli elenchi di marche ed elettrodomestici della community sono in cache per risparmiare la quota giornaliera dello store. Aggiorna per vedere le voci aggiunte o approvate da altri.", "head_trim_hint": "Numero di secondi da rimuovere dall'inizio", "appliance_monitor": "Monitor degli elettrodomestici", "artifact_dip_detail": "È sceso sotto la banda di potenza normale per ~{n}s.", "artifact_footer": "Evidenziato nel grafico sopra. Si tratta di artefatti temporanei (ad esempio la porta aperta a metà ciclo), non necessariamente problemi.", "artifact_header": "{n} anomalie rilevate durante questo ciclo", - "artifact_pause_detail": "La potenza è scesa quasi a zero per ~{n}s poi è ripresa – probabilmente la porta è stata aperta a metà ciclo o il ciclo è stato messo in pausa.", + "artifact_header_one": "{n} anomalia rilevata durante questo ciclo", + "artifact_header_many": "{n} anomalie rilevate durante questo ciclo", + "artifact_header_other": "{n} anomalie rilevate durante questo ciclo", + "artifact_pause_detail": "La potenza è scesa quasi a zero per ~{n}s, poi è tornata. Probabilmente è stata aperta la porta o il ciclo è stato messo in pausa.", "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, poi rimuovi la loro etichetta (restano nella cronologia, ma non influenzano più questo profilo) oppure eliminali del tutto.", + "cleanup_intro": "Tutti i cicli etichettati sovrapposti. Spunta i valori anomali, poi rimuovi la loro etichetta (restano nella cronologia, ma smettono di plasmare questo profilo) o eliminali.", "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)", - "compare_profiles_tip": "Sovrapponi altri involucri del profilo sulla tabella sopra per vedere quale si adatta meglio a questo ciclo.", + "compare_profiles_tip": "Sovrapponi altri profili al grafico per vedere quale si adatta meglio a questo ciclo.", "compare_selected_cycles": "Cicli selezionati (fissi) – mostra/nascondi", - "consider_new_profile": "Valuta la possibilità di creare un nuovo profilo.", - "coverage_gap": "i cicli recenti non hanno un profilo corrispondente ({pct}% degli ultimi 30).", - "coverage_gap_similar_cycles": "{count} cicli simili senza etichetta trovati – crea un profilo per iniziare a trovare corrispondenze.", "cycles_deleted": "{count} ciclo/i eliminato/i", + "cycle_context_loading": "Caricamento dello storico del recorder…", + "cycle_context_note": "Grigio: lo storico del recorder del sensore di potenza intorno a questo ciclo. Non fa parte del ciclo e non incide su durata, energia o corrispondenza.", + "cycle_context_none": "Il recorder non contiene letture attorno a questo ciclo (Home Assistant conserva 10 giorni di storico per impostazione predefinita).", + "cycle_context_tip": "Solo visualizzazione: mostra lo storico del recorder del sensore di potenza intorno a questo ciclo, se ancora conservato. Per aggiungere i tentativi di avvio falliti ai cicli futuri, usa Anticipo della curva (Impostazioni > Rilevamento > Avvio ciclo), che cambia le durate salvate.", "enough_data": "Dati sufficienti per apprendere ({current}/{min} cicli).", "export_description": "Scegli esattamente quali profili, cicli, impostazioni e altro esportare in JSON, oppure analizza un file e importa solo le parti che vuoi.", "feedback_cycles_pending": "Da revisionare: {n}", "feedback_prompt": "Conferma che fosse corretto, correggi il programma o ignoralo.", "feedback_relabel_hint": "Anche rietichettare questo ciclo lo risolve.", "filter_by_profile": "Filtra per profilo…", - "group_modal_help": "Raggruppare programmi con la stessa forma che differiscono per temperatura/centrifuga (le durate possono variare). La corrispondenza valuta il gruppo come un candidato, quindi sceglie il membro più adatto. Scegline almeno 2; la sovrapposizione mostra quanto sono simili.", + "group_modal_help": "Raggruppa programmi con la stessa forma che differiscono per temperatura o centrifuga. La corrispondenza valuta il gruppo come uno solo, poi sceglie il membro più adatto. Scegline almeno 2; la sovrapposizione mostra quanto si somigliano.", "group_not_cohesive": "Questi profili non sono sufficientemente simili per essere raggruppati in modo affidabile, quindi la corrispondenza li tratta individualmente finché non rimuovi il valore anomalo o dividi il gruppo.", "group_preview_hint": "Seleziona 2 o più membri per visualizzare in anteprima e confrontare le loro curve di potenza.", - "import_intro": "Carica un file esportato o incolla un payload JSON di seguito.", - "legacy_actions_title": "{count} azione personalizzata precedente ancora in esecuzione", + "legacy_actions_title": "{count} azioni personalizzate precedenti ancora in esecuzione", + "legacy_actions_title_one": "{count} azione personalizzata precedente ancora in esecuzione", + "legacy_actions_title_many": "{count} azioni personalizzate precedenti ancora in esecuzione", + "legacy_actions_title_other": "{count} azioni personalizzate precedenti ancora in esecuzione", "live_chart_loading": "Il grafico della potenza in tempo reale viene visualizzato quando arrivano le letture.", "loading": "Caricamento…", "loading_curve": "Curva di caricamento…", "loading_settings": "Caricamento impostazioni…", "log_buffer_hint": "Prima il più recente · memorizza nel buffer gli ultimi 500 record ha_washdata dal riavvio · trascina il bordo inferiore per ridimensionare.", - "maintenance_advisory": "L'aumento della durata/energia può indicare la necessità di manutenzione dell'apparecchio (ad esempio decalcificazione, pulizia del filtro).", "maintenance_due": "Manutenzione in scadenza: {items}", - "maintenance_intro": "Registra la manutenzione che esegui su questo elettrodomestico e ricevi un promemoria quando ogni attività è di nuovo in scadenza.", + "maintenance_intro": "Registra la manutenzione di questo elettrodomestico e ricevi un promemoria quando ogni attività torna in scadenza.", "maintenance_load_error": "Impossibile caricare i dati di manutenzione: {error}", "maintenance_requires_access": "La manutenzione e l'esportazione/importazione richiedono l'accesso completo.", - "manual_duration_hint": "La durata del ciclo media/prevista del profilo, utilizzata per le stime del tempo rimanente. Modifica per impostarlo; lasciandolo invariato mantiene il valore corrente.", - "matching_tuning_intro": "Durante l'apprendimento, WashData regola anche la misura in cui l'abbinamento del programma pesa la forma rispetto alla durata e all'energia.", + "manual_duration_hint": "La durata prevista del ciclo del profilo, usata per stimare il tempo rimanente. Lasciala invariata per mantenere il valore attuale.", "merge_intro": "I cicli selezionati vengono combinati in uno solo (ordine cronologico; spazi vuoti riempiti con 0 W). Scegli il profilo risultante.", "ml_intro": "WashData viene fornito con modelli intelligenti che funzionano immediatamente.", "ml_learned_intro": "Modelli ottimizzati per questa macchina.", - "ml_loading": "caricamento ML...", "ml_settings_intro": "Due interruttori indipendenti: uno applica i modelli durante l'esecuzione di un ciclo, l'altro consente a WashData di adattarli alla tua macchina nel tempo.", - "name_first_program": "Hai cicli sufficienti – assegna un nome al tuo primo programma per iniziare l'abbinamento.", - "near_duplicate_cluster": "rilevato cluster di profili quasi duplicati. Il raggruppamento consente all'abbinamento di scegliere in modo affidabile tra sosia (ad esempio lo stesso programma a temperatura/centrifuga diversa).", "no_cycles_match": "Nessun ciclo corrisponde al filtro corrente.", "no_cycles_profile": "Nessun ciclo per questo profilo.", "no_cycles_selected": "Seleziona prima almeno un ciclo.", @@ -843,73 +824,54 @@ "no_suggestions": "Nessun suggerimento attivo.", "notify_services_hint": "Usa gli ID servizio {entity} (separati da virgole per più servizi). Variabili del modello: {vars}.", "old_actions_warning": "Configurato con il vecchio editor delle azioni (ora rimosso). Si attivano ancora durante gli eventi del ciclo ma non possono più essere modificati qui. Convertili in una normale automazione o rimuovili.", - "onboarding_progress": "{n} / 3 cicli osservati", - "onboarding_watching": "Usa l'elettrodomestico normalmente – WashData sta osservando. Dopo 3 cicli inizierà l'abbinamento dei programmi.", "pending_feedback": "In attesa di feedback sul rilevamento", "performance_trend": "Andamento delle prestazioni ({n} cicli)", - "pg_analysis_empty": "Carica un ciclo per vedere l'analisi di abbinamento.", "pg_apply_confirm": "Applicare il valore migliore: {label} = {value}?", - "pg_canvas_empty": "Seleziona un ciclo qui sopra, quindi premi Carica per vederne la curva di potenza.", - "pg_chart_caption": "Tasso di rilevamento/abbinamento sui valori di {param}", "pg_ctrl_intro": "I valori partono dalle impostazioni attive di questo dispositivo nell'integrazione. Le modifiche restano nel Playground finché non le salvi nell'integrazione.", "pg_live_unavailable": "Impostazioni attive non disponibili, vengono mostrati i valori predefiniti.", "pg_load_live_confirm": "Scartare {n} modifiche del Playground e ricaricare le impostazioni attuali dell'integrazione?", - "pg_match_rate": "{pct}% di tasso di abbinamento su {total} cicli", "pg_matches_live": "Corrisponde alle impostazioni attive", "pg_n_changed": "{n} valori diversi dalle impostazioni attive", "pg_sugg_none": "Tutti i suggerimenti corrispondono già alle impostazioni attuali", - "pg_preset_delete_confirm": "Eliminare la preimpostazione \"{name}\"?", - "pg_preset_limit": "Limite di preimpostazioni raggiunto ({n})", - "pg_preset_overwrite": "Sovrascrivere la preimpostazione \"{name}\"?", "pg_publish_one_confirm": "Salvare {label} = {value} nelle impostazioni di questo dispositivo?", "pg_restart_note": "Riavvia Home Assistant per abilitare gli strumenti di simulazione.", - "pg_sim_progress": "{done} / {total} cicli", - "pg_starting": "Avvio…", - "pg_stress_synthetic": "la simulazione in standby inizia qui", - "pg_sweep_intro": "E se {param} fosse diverso? Testa {steps} valori sui tuoi ultimi {cycles} cicli per trovare l'impostazione in cui il maggior numero di cicli viene riconosciuto correttamente.", - "pg_sweep_step": "Passaggio {done} / {total}", - "pg_verdict_bad": "Richiede attenzione: molti cicli non vengono rilevati.", - "pg_verdict_good": "Ben regolato: la maggior parte dei cicli viene identificata e riconosciuta correttamente.", - "pg_verdict_ok": "Accettabile: alcuni cicli mancati. Prova ad abbassare la soglia di avvio.", "phase_catalog_intro": "Segmenti con nome di un ciclo (Prelavaggio, Riscaldamento, Centrifuga...). Assegnali a un profilo dal suo pannello di controllo.", "phase_ranges_intro": "Intervalli di fasi (minuti dall'inizio del ciclo) sovrapposti alla curva media. Modifica i valori per visualizzare l'anteprima dal vivo.", - "playground_intro": "Un ambiente sicuro per riprodurre i cicli passati con impostazioni diverse, confrontare due configurazioni affiancate ed esaminare come un ciclo si allinea a un profilo. Nulla qui modifica i tuoi dati reali finché non lo applichi esplicitamente.", - "playground_needs_restart": "Riavvia Home Assistant per abilitare gli strumenti Playground.", - "preferences_admin": "Preferenze, pannello e controllo degli accessi", - "preferences_adv": "Preferenze", + "playground_intro": "Prova le impostazioni sui dati reali dei tuoi cicli. Nulla qui cambia le tue impostazioni attive finché non lo applichi.", "prefs_personal": "Questi si applicano solo al tuo account Home Assistant.", - "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.", + "process_history_hint": "Riesegue la corrispondenza su tutti i cicli salvati, aggiorna i suggerimenti, riaddestra i modelli ML (se attivi), ricalcola il costo dai prezzi registrati e ricalcola la salute dei cicli. Eseguilo 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.", - "rbac_hint": "Quando è disattivato, ogni utente di Home Assistant ha accesso completo (impostazione predefinita). Gli amministratori hanno sempre accesso completo e possono gestire tutti.", - "recent_logs": "Record ha_washdata recenti", + "rbac_hint": "Disattivato: ogni utente di Home Assistant ha accesso completo (predefinito). Gli amministratori hanno sempre accesso completo e possono gestire tutti.", "recording_in_progress": "Registrazione in corso", "reminders_intro": "Mostra un promemoria nel pannello dopo questo numero di cicli dall'ultimo servizio. Lascia vuoto o 0 per disattivare un promemoria.", "restart_gap_footer": "Evidenziato nel grafico. I dati di potenza mancano per questi intervalli; il riconoscimento ha usato solo letture reali.", - "restart_gap_header": "{n} interruzione riavvio HA durante questo ciclo", + "restart_gap_header": "{n} interruzioni riavvio HA durante questo ciclo", + "restart_gap_header_one": "{n} interruzione riavvio HA durante questo ciclo", + "restart_gap_header_many": "{n} interruzioni riavvio HA durante questo ciclo", + "restart_gap_header_other": "{n} interruzioni riavvio HA durante questo ciclo", "restart_gap_item": "{dur} interruzione", - "review_confirm_help": "Confermare se questo ciclo è stato rilevato correttamente. Le tue revisioni addestrano il modello sulla tua macchina: più cicli confermi, migliore sarà la corrispondenza e il punteggio di salute. È sufficiente un breve Buono/Cattivo.", + "review_confirm_help": "Questo ciclo è stato rilevato correttamente? Confermare i cicli migliora la corrispondenza per la tua macchina. Basta un rapido Bene/Cattivo.", "review_in_settings": "Controlla nelle Impostazioni", "review_notes_placeholder": "Note (facoltativo)", "review_notes_tip": "Note di testo libero per riferimento personale. Non utilizzato dall'abbinamento o dalla formazione.", - "review_profile_tip": "Il programma con cui è etichettato questo ciclo. Se il programma rilevato automaticamente era sbagliato, correggilo qui: l'etichettatura insegna la corrispondenza per i cicli futuri.", - "review_quality_tip": "Quanto è pulito questo ciclo. Buono = un esempio da manuale di questo programma; Cattivo = rilevato ma rumoroso o atipico; Inutilizzabile = rilevato erroneamente (unito, troncato o spurio). Determina il punteggio di integrità e quali cicli sono autorizzati ad addestrare il modello.", - "review_recorded_tip": "Contrassegnalo come ciclo di riferimento selezionato manualmente per il suo programma: lo stesso ruolo di un ciclo registrato manualmente. I cicli di riferimento vengono sempre mantenuti, seminano il modello corrispondente e non vengono mai eliminati dalla pulizia. (Questa è la bandiera \"d'oro\"/registrata; entrambe sono la stessa cosa.)", - "review_tags_tip": "Flag facoltativi che descrivono cosa è andato storto in questo ciclo, in modo che l'addestramento e la pulizia possano tenerne conto.", + "review_profile_tip": "Il programma con cui è etichettato questo ciclo. Se è sbagliato, correggilo qui; le etichette insegnano alla corrispondenza.", + "review_quality_tip": "Quanto è pulito questo ciclo. Bene: un esempio da manuale di questo programma. Cattivo: rilevato, ma rumoroso o insolito. Inutilizzabile: rilevato male (unito, troncato o spurio). Incide sul punteggio di salute e su quali cicli addestrano il modello.", + "review_recorded_tip": "Contrassegnalo come ciclo di riferimento scelto a mano per il suo programma, come un ciclo registrato manualmente. I cicli di riferimento vengono sempre conservati, fanno da base al modello di corrispondenza e la pulizia non li rimuove mai.", + "review_tags_tip": "Indicatori facoltativi su cosa è andato storto, così addestramento e pulizia possono tenerne conto.", "review_to_cycles": "Apri la coda di revisione dei cicli", "samples_decimated": "Visualizzazione di {shown} di {total} campioni (ridotti per la visualizzazione; picchi mantenuti). Un ampio spazio vuoto qui è dovuto alla riduzione, non a dati mancanti.", - "saving_triggers_reload": "Il salvataggio attiva un ricaricamento dell'integrazione. Le entità HA potrebbero essere visualizzate brevemente come non disponibili.", + "saving_triggers_reload": "Il salvataggio ricarica l'integrazione. Le entità potrebbero apparire brevemente come non disponibili.", "search_placeholder": "Cerca impostazioni...", "see_recorder": "Vedi il widget del registratore di seguito", "setting_changed": "Modificato da {old} a {new} il {date}", "settings_basic_note": "Vengono mostrate le impostazioni essenziali. Passa ad Avanzato per l'elenco completo.", - "shape_drift_advisory": "⚠ Forma in deriva", - "shape_drift_detail": "Il profilo di potenza per questo profilo è cambiato nel tempo – possibile usura dell'apparecchio o manutenzione necessaria (es. decalcificazione, pulizia del filtro).", "show_all_settings": "Mostra tutte le impostazioni", - "showing_suggestions": "Visualizzazione dell'impostazione {count} con suggerimenti.", - "split_intro": "Fare clic sul grafico per aggiungere o rimuovere un punto di divisione o per rilevare automaticamente gli spazi inattivi. Ogni segmento risultante può ottenere il proprio profilo.", - "storage_diagnostics": "Statistiche di archiviazione, manutenzione, esportazione/importazione", + "showing_suggestions": "Visualizzazione di {count} impostazioni con suggerimenti.", + "showing_suggestions_one": "Visualizzazione di {count} impostazione con suggerimenti.", + "showing_suggestions_many": "Visualizzazione di {count} impostazioni con suggerimenti.", + "showing_suggestions_other": "Visualizzazione di {count} impostazioni con suggerimenti.", + "split_intro": "Fai clic sul grafico per aggiungere o rimuovere un punto di divisione, oppure rileva automaticamente dalle pause inattive. Ogni parte può avere il suo profilo.", "sug_staged": "{key} = {val} impostato. Salva per applicare.", "toast_auto_detect_enabled": "Rilevamento automatico abilitato", "toast_auto_label_complete": "Etichettatura automatica completata", @@ -926,7 +888,6 @@ "toast_no_automation": "Impossibile creare automazione qui", "toast_profile_name_required": "Il nome del profilo è obbligatorio", "toast_program_set": "Programma impostato: {program}", - "toast_rebuild_failed": "Ricostruzione fallita: {error}", "toast_resume_failed": "Ripresa fallita: {error}", "toast_revert_failed": "Ripristino fallito: {error}", "toast_reverted": "{key} ripristinato", @@ -937,97 +898,82 @@ "trend_energy_up": "Energia in aumento ({pct}%/ciclo) – media recente {avg}", "trim_destructive_confirm": "Questo mantiene solo il {pct}% del ciclo e l'operazione non può essere annullata. Continuare?", "trim_intro": "Trascina le maniglie rosse o inserisci i valori. Tutto fuori dalla finestra viene rimosso.", - "tuning_suggestions_available": "{count} suggerimento di ottimizzazione disponibile dai cicli osservati. Appaiono accanto ai campi pertinenti.", + "tuning_suggestions_available": "{count} suggerimenti di ottimizzazione disponibili dai cicli osservati. Appaiono accanto ai campi pertinenti.", + "tuning_suggestions_available_one": "{count} suggerimento di ottimizzazione disponibile dai cicli osservati. Appare accanto al campo pertinente.", + "tuning_suggestions_available_many": "{count} suggerimenti di ottimizzazione disponibili dai cicli osservati. Appaiono accanto ai campi pertinenti.", + "tuning_suggestions_available_other": "{count} suggerimenti di ottimizzazione disponibili dai cicli osservati. Appaiono accanto ai campi pertinenti.", "unsure_detected_prefix": "WashData non è sicuro di averlo rilevato", - "updated": "Aggiornato", "warmup_badge": "In apprendimento ({done}/{needed} cicli)", - "warmup_detail": "Questo profilo richiede {needed} cicli etichettati prima che inizi la corrispondenza automatica. Ogni ciclo confermato aiuta l'apprendimento.", + "warmup_detail": "La corrispondenza funziona già. WashData ti chiede di confermare i primi {needed} cicli di questo profilo prima di etichettare da solo.", "wipe_history_warning": "Elimina definitivamente tutti i cicli e i profili. L'operazione non può essere annullata.", "compare_overlay_tip": "Sovrapponi le buste dei profili appresi per vedere a quale programma somiglia ogni ciclo.", - "duration_trend_up_tip": "Durata in aumento ({pct}%/ciclo)", - "duration_trend_down_tip": "Durata in calo ({pct}%/ciclo)", - "energy_trend_up_tip": "Energia in aumento ({pct}%/ciclo)", - "energy_trend_down_tip": "Energia in calo ({pct}%/ciclo)", - "performance_trending": "Andamento delle prestazioni", - "pg_select_cycle_hint": "Seleziona un ciclo e clicca su ↺ per caricare", - "pg_sweep_progress": "Passo {done} / {total}", - "pg_analysis_hint": "Seleziona un ciclo e clicca su ↺ per caricare l'analisi.", "diagnostics_load_failed": "Impossibile caricare la diagnostica: {error}", - "manual_duration_ref_hint": "Utilizzato solo quando non è selezionato alcun ciclo di riferimento. Un ciclo di riferimento imposta la durata in base alla propria lunghezza.", + "manual_duration_ref_hint": "Usato solo senza ciclo di riferimento; un ciclo di riferimento imposta la durata in base alla propria lunghezza.", + "duration_learned_hint": "Calcolata dai cicli di questo programma (esclusi i valori anomali) e aggiornata dopo ogni nuovo ciclo. Per cambiarla, ritaglia o rimuovi i cicli fuori norma.", "toast_convert_rolled_back": "Conversione non riuscita e annullata (nessuna automazione lasciata): {error}", "toast_convert_orphan": "L'automazione è stata creata, ma la cancellazione delle vecchie azioni non è riuscita. Non riprovare: rimuovi manualmente le vecchie azioni per evitare un'automazione duplicata.", - "advisory_duration_trend_up": "I cicli di '{name}' durano sempre di più (circa +{pct}% per ciclo). Se il comportamento dell'elettrodomestico è cambiato, registra di nuovo o ricostruisci questo profilo.", - "advisory_energy_trend_up": "'{name}' consuma sempre più energia (circa +{pct}% per ciclo) - vale la pena controllare l'elettrodomestico se non è previsto.", - "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.", + "advisory_duration_trend_up": "I cicli di '{name}' durano sempre di più (circa +{pct}% per ciclo). Se l'elettrodomestico è cambiato, registra di nuovo o ricostruisci questo profilo.", + "advisory_energy_trend_up": "'{name}' consuma più energia a ogni ciclo (circa +{pct}% per ciclo). Controlla l'elettrodomestico se non è previsto.", + "advisory_poor_health": "'{name}' si adatta male: i suoi cicli recenti variano molto o corrispondono poco. Controlla i suoi cicli o registralo di nuovo per mantenere precise corrispondenza e stime.", + "advisory_shape_drift": "'{name}' si è allontanato dalla sua curva di potenza originale, forse per calcare o usura. Valuta di registrarlo di nuovo con cicli recenti.", + "advisory_shape_drift_corr": "'{name}' si è allontanato dalla sua curva di potenza originale (correlazione {corr}), forse per calcare o usura. Valuta di registrarlo di nuovo con cicli recenti.", + "advisory_unmatchable": "'{name}' non potrà mai trovare corrispondenza: non ha alle spalle alcun ciclo con dati di potenza. Etichetta o registra un ciclo di questo programma per risolvere.", + "advisory_unmatchable_no_duration": "'{name}' non potrà mai trovare corrispondenza: il suo ciclo non ha una durata utilizzabile. Registra di nuovo questo programma o imposta la sua durata prevista.", "pg_no_alerts": "Nessun problema rilevato in questa esecuzione.", "pg_override_active": "Utilizzo delle tue impostazioni modificate vs attuali", "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.", "pg_sweep_intro2": "Trova l'impostazione che soddisfa al meglio un obiettivo sui tuoi cicli recenti. Nulla cambia finché non la applichi.", - "pg_running_history": "Riproduzione dei tuoi cicli…", - "pg_running_sweep": "Test dei valori su tutti i tuoi cicli…", "pg_row_load_hint": "Carica questo ciclo nel grafico qui sopra", - "pg_history_intro2": "Riproduci i tuoi cicli recenti attraverso il rilevatore e il sistema di corrispondenza reali con le impostazioni qui sopra. Fai clic su una riga per caricare quel ciclo nel grafico; modifica un'impostazione per vedere un confronto prima/dopo.", + "pg_history_intro2": "Riproduci i cicli recenti con il vero rilevatore e il vero comparatore usando le impostazioni qui sopra. Fai clic su una riga per caricarla nel grafico; cambia un'impostazione per confrontare prima e dopo.", "pg_simulating": "Simulazione del ciclo…", - "pg_canvas_empty2": "Scegli un ciclo qui sopra e premi Esegui per simularlo. Poi passa il puntatore per leggere i valori, scorri per ingrandire e trascina per spostarti.", "pg_apply_settings_confirm": "Salvare queste {n} impostazioni su questo dispositivo? {list}", - "pg_stress_terminated_detail": "Stabilizzato a ~{idle}W in standby -- il ciclo è terminato {h}h {m}m dopo tramite {reason}.", - "pg_stress_above_threshold_detail": "Il consumo in standby ~{idle}W è pari o superiore alla soglia di arresto effettiva ({stop}W) -- il ciclo non è mai stato considerato inattivo. Aumenta stop_threshold_w per correggere.", - "pg_stress_hit_cap_detail": "Il ciclo è durato {h}h {m}m senza fermarsi -- interrotto forzatamente dal limite di sicurezza. Consumo in standby {idle}W rispetto alla soglia di arresto {stop}W.", "no_logs_match": "Nessun record di log corrisponde a questi filtri.", "pg_analysis_hint2": "Seleziona un ciclo e premi Esegui per caricare l'analisi di abbinamento.", - "pg_analysis_empty2": "Premi Esegui per vedere l'analisi di abbinamento.", - "imported_readonly": "Importato dallo store della community. Mostrato come riferimento e per la corrispondenza. Non viene conteggiato nelle tue statistiche e non può essere modificato.", + "imported_readonly": "Importato dallo store della community. Usato come riferimento e per la corrispondenza, non conta nelle tue statistiche e non può essere modificato.", "merge_no_imports": "Le registrazioni importate non possono essere unite a un ciclo reale. Deselezionale per unire.", - "store_enable_hint": "Abilita le funzionalità online nelle Impostazioni per sfogliare e importare i cicli di riferimento della community.", - "online_intro": "Condividi e scarica cicli di riferimento con altri utenti di WashData. Tutte le funzionalità online sono facoltative e disattivate per impostazione predefinita.", + "store_enable_hint": "Attiva le funzionalità online in Preferenze > Online e community per sfogliare e importare i cicli di riferimento della community.", "store_share_intro": "Carica questo ciclo di riferimento in modo che altri con lo stesso elettrodomestico possano usarlo. Viene revisionato prima di apparire pubblicamente.", - "online_intro_global": "Sfoglia e condividi registrazioni di riferimento con altri utenti di WashData e conferma le voci degli elettrodomestici. Un'unica connessione si applica a tutta la tua integrazione WashData; la marca e il modello dell'elettrodomestico si impostano per dispositivo in Base. Tutte le funzionalità online sono facoltative e disattivate per impostazione predefinita.", + "online_intro_global": "Sfoglia e condividi registrazioni di riferimento con altri utenti di WashData. Una connessione vale per tutti i tuoi dispositivi WashData; marca e modello si impostano per dispositivo in Base. Le funzioni online restano disattivate finché non le attivi.", "online_unavailable": "Le funzionalità online non sono disponibili su questo server.", "store_picker_offline": "Abilita le funzionalità online nell'icona delle impostazioni per scegliere dal catalogo della community.", "pick_brand_first": "Scegli prima una marca di elettrodomestico.", - "model_not_found": "Non è ancora nel catalogo.", - "connect_to_confirm": "Connettiti nell'icona delle impostazioni per confermare o valutare.", "adopt_settings_hint": "Sovrascrivi le soglie di rilevamento e riconoscimento di questo dispositivo con quelle condivise. Le notifiche, le entità e il prezzo dell'energia non vengono mai modificati.", - "include_settings_hint": "Condividi le soglie di riconoscimento e corrispondenza di questo dispositivo (non le notifiche, le entità o il prezzo dell'energia). Chi adotta sceglie se applicarle.", + "include_settings_hint": "Condividi le soglie di rilevamento e corrispondenza di questo dispositivo (non notifiche, entità o prezzo dell'energia). Gli altri scelgono se applicarle.", "onboard_download": "Nuovo dispositivo? Adotta una configurazione pronta (programmi, cicli di riferimento e fasi) da un altro utente WashData con lo stesso elettrodomestico.", - "share_consent": "Confermo che questi cicli si sono conclusi normalmente senza interruzioni", "share_device_none": "Nessun ciclo condivisibile ancora. Contrassegna prima un ciclo registrato o selezionato manualmente come ciclo di riferimento (⭐) nella scheda Cicli.", "share_guideline_naming": "Assegna a ogni profilo esattamente il nome mostrato sulla manopola o sul display dell'elettrodomestico (ad es. 'Cotton 40', 'Eco 60').", - "share_guideline_quality": "Condividi solo i cicli completati normalmente -- senza interruzioni a metà ciclo, aperture di porta o anomalie di potenza.", + "share_guideline_quality": "Condividi solo cicli terminati normalmente: senza interruzioni, aperture della porta o cali di corrente.", "share_guideline_review": "Il tuo caricamento inizia come in attesa di revisione e appare pubblicamente una volta che abbastanza membri della community lo confermano.", "share_guidelines_title": "Prima di condividere", - "store_download_device_intro": "Adotta ogni programma condiviso e i suoi cicli di riferimento sul tuo dispositivo. I tuoi cicli registrati e le tue statistiche non vengono modificati.", + "store_download_device_intro": "Aggiungi ogni programma condiviso e i suoi cicli di riferimento al tuo dispositivo. I tuoi cicli e le tue statistiche non vengono toccati.", "store_share_device_intro": "Carica {brand} {model} con i cicli di riferimento che selezioni. Altri con lo stesso elettrodomestico possono adottare i tuoi programmi. Le voci vengono revisionate prima di apparire pubblicamente.", "share_profile_no_cycles": "Nessun ciclo di riferimento -- contrassegna un ciclo come ⭐ nella scheda Cicli per includere questo profilo", - "advisory_phase_inconsistent": "'{name}' sembra mescolare programmi o temperature diversi - i suoi cicli riscaldano per intervalli di tempo molto diversi. Dividerlo in profili separati (ad es. per temperatura) migliorerà la corrispondenza e le stime dei tempi.", - "advisory_phase_inconsistent_title": "⚠ Possibili programmi misti", - "export_select_intro": "Spunta esattamente cosa includere. Selezionare i profili senza i loro cicli esporta comunque un programma riconoscibile (la sua forma appresa viaggia insieme).", - "import_analyze_hint": "Carica un file esportato (o incolla il suo JSON). WashData lo analizza e mostra esattamente cosa si può importare prima di qualsiasi modifica.", + "export_select_intro": "Spunta cosa includere. I profili senza i loro cicli esportano comunque un programma riconoscibile.", + "import_analyze_hint": "Carica un file esportato (o incolla il suo JSON). WashData mostra cosa si può importare prima di qualsiasi modifica.", "analyzing": "Analisi in corso…", - "device_type_mismatch_warn": "Questa esportazione proviene da un tipo di elettrodomestico diverso ({src} invece di {local}). Programmi e cicli possono comunque essere importati come dati di riferimento, ma le impostazioni specifiche del dispositivo e l'importazione nella cronologia reale sono disabilitate.", + "device_type_mismatch_warn": "Questa esportazione proviene da un tipo di elettrodomestico diverso ({src} invece di {local}). Programmi e cicli possono ancora essere importati come dati di riferimento, ma non le impostazioni del dispositivo né lo storico reale.", "merge_hint": "Gli elementi importati vengono aggiunti; nulla di locale va perso. I conflitti di nome si risolvono qui sotto.", "replace_warn": "Ogni categoria spuntata viene cancellata e sostituita dal contenuto del file. Le categorie non spuntate restano invariate.", "dest_reference_hint": "I cicli importati migliorano solo il riconoscimento dei programmi e non influiscono mai sulle statistiche di utilizzo/energia.", - "dest_real_history_hint": "I cicli importati contano come cronologia propria di questo dispositivo e alimentano le statistiche di energia/utilizzo. Da usare per spostare un elettrodomestico su una nuova installazione.", + "dest_real_history_hint": "I cicli importati contano come cronologia e statistiche proprie di questo dispositivo. Usalo per spostare un elettrodomestico su una nuova installazione.", "sug_muted": "Non verrà più suggerita questa impostazione", "sug_mute_failed": "Impossibile silenziare il suggerimento", "sug_unmuted_all": "Suggerimenti silenziati ripristinati", - "n_suggestions_muted": "{count} silenziati; il sistema di ottimizzazione automatica non proporrà questi.", + "n_suggestions_muted": "{count} suggerimenti silenziati; il sistema di ottimizzazione automatica non li proporrà.", + "n_suggestions_muted_one": "{count} suggerimento silenziato; il sistema di ottimizzazione automatica non lo proporrà.", + "n_suggestions_muted_many": "{count} suggerimenti silenziati; il sistema di ottimizzazione automatica non li proporrà.", + "n_suggestions_muted_other": "{count} suggerimenti silenziati; il sistema di ottimizzazione automatica non li proporrà.", "font_size_hint": "Rendi tutto in questo pannello più grande o più piccolo. Si applica al tuo account su questo dispositivo.", - "import_history_description": "Avevi già una presa intelligente prima di WashData? Carica un'esportazione dello storico del suo sensore di potenza, oppure leggilo direttamente da Home Assistant: il normale rilevamento viene eseguito su quei dati, così i cicli passati compaiono nel tuo elenco Cicli, pronti da nominare.", - "hist_input_hint": "Carica un CSV scaricato dal pannello Cronologia (entità, stato, ultima modifica), oppure lascia che WashData legga direttamente lo storico del sensore. Il rilevamento viene poi eseguito esattamente come in tempo reale e sei tu a scegliere quali dei cicli trovati mantenere.", - "hist_recorder_hint": "Legge dalla data che scegli fino a ora. Home Assistant conserva lo storico dettagliato per 10 giorni per impostazione predefinita e successivamente solo medie orarie, troppo grossolane per rilevare i cicli - scegli una data più indietro nel tempo solo se il recorder di Home Assistant è configurato per conservarne di più.", - "hist_scanning": "Il tuo storico viene rieseguito attraverso il rilevatore. L'operazione avviene in background: puoi chiudere questa finestra e tornarci più tardi.", + "import_history_description": "Avevi una presa intelligente prima di WashData? Carica il suo storico di potenza o leggilo da Home Assistant. Il rilevamento lo percorre e i cicli passati compaiono nel tuo elenco Cicli, pronti da nominare.", + "hist_input_hint": "Carica un CSV dal pannello Cronologia (entità, stato, ultima modifica), oppure lascia che WashData legga direttamente lo storico del sensore. Il rilevamento in tempo reale lo percorre e scegli tu quali cicli tenere.", + "hist_recorder_hint": "Legge dalla data scelta fino a ora. Home Assistant conserva lo storico dettagliato per 10 giorni per impostazione predefinita; le medie orarie più vecchie sono troppo grossolane per rilevare i cicli.", + "hist_scanning": "Il tuo storico passa nel rilevatore in background. Puoi chiudere questa finestra e tornare più tardi.", "hist_imported_count": "{n} cicli importati.", "hist_duplicates": "{n} erano già stati importati e sono stati saltati.", "hist_capped": "È stato raggiunto il limite di cicli importati per dispositivo; il resto non è stato salvato.", - "hist_next_step": "Sono nel tuo elenco Cicli, contrassegnati come storico importato. Aprine uno e usa Assegna etichetta per indicare il programma a cui appartiene.", + "hist_next_step": "Sono nel tuo elenco Cicli, contrassegnati come storico importato. Aprine uno e usa Assegna etichetta per dare un nome al suo programma.", "hist_rows_read": "{n} letture lette", "hist_breaks": "{n} interruzioni in cui il sensore non era disponibile", "hist_other_entity": "{n} letture di altre entità ignorate", @@ -1041,58 +987,91 @@ "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.", - "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.", + "imported_history_readonly": "Trovato in uno storico di potenza importato. Aiuta la corrispondenza, ma non conta nelle tue statistiche e non può essere tagliato né diviso. Etichettalo per dare un nome al programma.", + "odometer_intro": "Questo totale può solo aumentare. Eliminare un ciclo non lo abbassa, così i promemoria di manutenzione restano corretti.", + "odometer_correct_hint": "Imposta questo valore se WashData ha contato un ciclo mai avvenuto, oppure se l'elettrodomestico ha eseguito 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}", "standby_above_stop_title": "Assorbe ancora {idle} W alla fine del ciclo", "standby_above_stop_sub": "{n} degli ultimi {total} cicli. Il Ritardo spegnimento parte solo quando la potenza scende sotto la Soglia di arresto ({stop} W), quindi i cicli finiscono in ritardo o vanno fermati a mano.", - "advisory_duration_outlier": "{n} ciclo/i etichettati come '{name}' si discostano troppo dalla sua durata abituale per potergli mai corrispondere (caso peggiore: {ratio}x). Probabilmente sono etichettati male, oppure una registrazione ha catturato due esecuzioni. Rietichettali o dividili, così smettono di falsare la stima dei tempi di questo programma.", + "advisory_duration_outlier": "{n} ciclo/i etichettati come '{name}' si discostano troppo dalla sua durata abituale per poter mai corrispondere (caso peggiore: {ratio}x). Probabilmente sono etichettati male o contengono due cicli. Rietichettali o dividili, così smettono di falsare la sua stima dei tempi.", "toast_range_required": "Imposta un intervallo Da/A valido", - "no_profiles_option": "(nessun profilo, aggiungine uno)" + "no_profiles_option": "(nessun profilo, aggiungine uno)", + "compare_cycles_title": "Confronta {count} cicli", + "edit_builtin_phase": "Questa è una fase integrata. Il salvataggio crea una copia personalizzata; elimina la copia per ripristinare l'originale.", + "fetch_error": "Impossibile caricare i dati.", + "connection_lost": "Connessione persa - dati aggiornati alle {time}.", + "pg_notify_partial": "I marcatori mostrano solo le notifiche di avvio, quasi finito, fine, traguardi e ore di silenzio. Aggiornamenti in tempo reale, promemoria di scarico e avvisi di sforamento non sono simulati.", + "connect_to_confirm": "Connettiti nell'icona delle impostazioni per confermare o valutare.", + "coverage_gap_cluster": "{count} cicli recenti senza etichetta di circa {min} min sembrano un programma che non hai ancora creato.", + "pg_label_yes": "Verrebbe etichettato \"{name}\" a fine ciclo", + "pg_label_no": "Richiederebbe una revisione: {reason}", + "live_match_debug_tip": "Fiducia: quanto la curva di potenza attuale corrisponde al profilo candidato migliore (0-100%). Ambiguo: i due candidati migliori hanno punteggi entro il 5% l'uno dall'altro; l'etichetta resta incerta finché il ciclo non termina.", + "automation_alias_started": "{device}: ciclo avviato", + "automation_alias_finished": "{device}: ciclo terminato", + "automation_desc_started": "Viene eseguita quando inizia un ciclo di {device}. Aggiungi le tue azioni (notifica, luci, ...).", + "automation_desc_finished": "Viene eseguita quando termina un ciclo di {device}. Aggiungi le tue azioni (notifica, luci, ...).", + "feedback_detected": "WashData ha rilevato: {name}", + "import_undo_hint": "Viene conservato un punto di ripristino precedente all'ultima importazione ({when}): {profiles} profilo/i e {cycles} ciclo/i.", + "replace_undo_hint": "Prima viene salvato un punto di ripristino, così puoi annullare questa importazione da Esporta/Importa.", + "pg_in_sample_note": "Questi cicli hanno contribuito a costruire i profili con cui vengono confrontati, quindi i risultati sono ottimistici: aspettati qualche corrispondenza corretta in meno sui cicli nuovi.", + "hist_tail_trimmed": "Sono stati tagliati {min} min di attesa dopo la fine del ciclo, come fa l'integrazione in tempo reale", + "invalid_threshold_pair": "La Soglia di arresto ({stop} W) deve essere inferiore alla Soglia di avvio ({start} W). Non è stato salvato nulla; modificale entrambe insieme.", + "hist_truncated": "interrotto al limite di {n} letture, quindi il resto dello storico non è stato analizzato", + "hist_warn_kw": "La lettura più alta è solo {w}. Questo sensore sembra riportare kilowatt; WashData ha bisogno di watt, quindi non si può rilevare alcun ciclo.", + "hist_warn_naive_time": "{n} marche temporali non hanno fuso orario e sono state lette come UTC. Se il file usa l'ora locale, i cicli risultano spostati del tuo scarto rispetto a UTC e le letture vicine a un cambio dell'ora legale possono andare perse.", + "hist_warn_naive_time_one": "{n} marca temporale non ha fuso orario ed è stata letta come UTC. Se il file usa l'ora locale, i cicli risultano spostati del tuo scarto rispetto a UTC e le letture vicine a un cambio dell'ora legale possono andare perse.", + "hist_warn_naive_time_many": "{n} marche temporali non hanno fuso orario e sono state lette come UTC. Se il file usa l'ora locale, i cicli risultano spostati del tuo scarto rispetto a UTC e le letture vicine a un cambio dell'ora legale possono andare perse.", + "hist_warn_naive_time_other": "{n} marche temporali non hanno fuso orario e sono state lette come UTC. Se il file usa l'ora locale, i cicli risultano spostati del tuo scarto rispetto a UTC e le letture vicine a un cambio dell'ora legale possono andare perse.", + "pg_compare_profile_tip": "Sceglie solo il profilo disegnato sul grafico. La simulazione cerca sempre da sola la corrispondenza del ciclo, come fa l'integrazione in tempo reale.", + "pg_outcome_stale": "Le impostazioni sono cambiate dall'esecuzione di questa simulazione. Mostra ancora il risultato precedente.", + "pg_sweep_guarded": "Termina più cicli in anticipo, ne divide di più o perde cicli che l'impostazione attuale rileva, quindi questo valore non viene mai consigliato.", + "pg_sweep_keep_current": "Mantieni il valore attuale ({value}, {metric}): nessun valore ha fatto meglio di almeno un ciclo.", + "pg_sweep_n_cycles": "su {count} cicli", + "pg_sweep_n_cycles_one": "su {count} ciclo", + "pg_sweep_n_cycles_many": "su {count} cicli", + "pg_sweep_n_cycles_other": "su {count} cicli", + "pg_task_busy": "Per questo dispositivo è già in corso un'esecuzione di Prova sullo storico o Ottimizza. Attendi che finisca o annullala." }, "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", + "smart_termination_duration_ratio": "Frazione della durata media del programma che un ciclo deve raggiungere prima che la Terminazione intelligente possa chiuderlo in anticipo; abbassala se le durate variano", + "anti_crease_finalize_ratio": "Frazione della durata media del programma che un ciclo deve raggiungere prima che la coda antirughe possa concluderlo; abbassala sulle asciugatrici, lascia le lavatrici al 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_exit_power": "Livello considerato silenzio tra gli impulsi di rotazione; ha effetto solo sopra la Soglia di arresto", "anti_wrinkle_idle_timeout": "Tempo di silenzio consentito tra due impulsi di rotazione prima che la modalità antirughe termini", "anti_wrinkle_max_duration": "Un impulso più lungo di questo termina la modalità antirughe e apre un nuovo ciclo", "anti_wrinkle_max_power": "Un impulso superiore a questo termina la modalità antirughe e apre un nuovo ciclo", "completion_min_seconds": "Esecuzione più breve che conta come ciclo reale", - "end_repeat_count": "Letture basse consecutive prima della fine", "interrupted_min_seconds": "Cicli brevi contrassegnati come interrotti", "min_off_gap": "Intervallo necessario per separare due cicli", "off_delay": "Secondi di bassa potenza prima della fine del ciclo", "start_duration_threshold": "Secondi sopra la soglia per confermare l'avvio", "start_threshold_w": "Watt minimi per contare come avviato", "stop_threshold_w": "Al di sotto, la macchina è considerata spenta", - "dtw_bandwidth": "Fase 3: banda di deformazione Sakoe-Chiba (0 = DTW disattivato; predefinito 0,2 = 20% della lunghezza del ciclo)", - "corr_weight": "Fase 2: equilibrio tra forma della curva (correlazione) e livello di potenza (MAE); predefinito 0,45", - "duration_weight": "Fase 4: quanto la concordanza di durata influisce sul punteggio finale (predefinito 0,22)", - "energy_weight": "Fase 4: quanto la concordanza di energia influisce sul punteggio finale (predefinito 0,22)", - "profile_match_min_duration_ratio": "Fase 1: durata minima del ciclo come frazione della durata del profilo; predefinito 0,1 (10%)", - "profile_match_max_duration_ratio": "Fase 1: durata massima del ciclo come frazione della durata del profilo; predefinito 1,8 (180%)", - "keep_min_score": "Fase 2: punteggio minimo per restare in gara; il predefinito 0,1 ammette candidati deboli, le fasi successive scelgono il migliore", - "dtw_blend": "Fase 3: 0 = solo punteggio centrale, 1 = solo punteggio DTW, 0,5 = mix equilibrato (predefinito)", - "dtw_ensemble_w": "Fase 3 (modalità ensemble): peso su L1 scalato rispetto a DTW derivato; il predefinito 0,7 favorisce la sensibilità al livello", - "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)", + "profile_match_min_duration_ratio": "Durata minima del ciclo come frazione della durata del profilo; predefinito 0,1 (10%)", + "profile_match_max_duration_ratio": "Durata massima del ciclo come frazione della durata del profilo; predefinito 1,8 (180%)", "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.", "anti_wrinkle": "Tumble periodico post-ciclo per ridurre le grinze del bucato.", + "baking": "Resistenza ad alta temperatura attiva per formare crosta e mollica.", + "cooking": "Fase di cottura principale con resistenza e ventola attive.", "cool_down": "Burattatura senza calore verso la fine del ciclo.", + "cool_down_af": "Raffreddamento con la sola ventola dopo il riscaldamento.", "drain_and_switch": "Periodo di transizione dalla modalità lavaggio a asciugatura.", "dry": "Fase di asciugatura tramite riscaldatore e/o calore residuo.", "drying": "Periodo principale di burattatura riscaldato.", "heat_up": "Riscaldamento iniziale del riscaldatore prima dell'inizio dell'asciugatura completa.", + "keep_warm": "Fase a basso calore per tenere caldo il cibo.", + "keep_warm_bm": "Fase a basso calore per tenere caldo il pane dopo la cottura.", + "kneading": "Il motore mescola e lavora l'impasto. Assorbimento elevato.", + "pause_af": "Breve pausa per scuotere o controllare il cibo.", + "pre_heat": "Riscaldamento iniziale della camera prima della cottura vera e propria.", "pre_rinse": "Spruzzatura iniziale prima del lavaggio con detergente.", + "proving": "Lievitazione a basso calore per far fermentare il lievito e crescere l'impasto.", + "resting": "Breve pausa a bassa potenza tra un'impastatura e l'altra per rilassare il glutine.", "pre_wash": "Ammollo iniziale o pretrattamento prima del lavaggio principale.", "rinse": "Fase di risciacquo con acqua pulita. Questa fase può ripetersi più volte.", "sanitize": "Fase di pulizia ad alta temperatura per programmi di sanificazione.", @@ -1105,6 +1084,7 @@ "wash_dw": "Lavaggio principale con detersivo con riscaldamento." }, "placeholder": { + "custom_task": "es. pulisci guarnizione oblò", "add": "aggiungi…", "group_name": "es. Cotone 2:47", "json_buttons": "[{\"action\":\"ID\",\"title\":\"Label\"}]", @@ -1132,10 +1112,10 @@ "section": { "dishwasher": { "label": "Lavastoviglie", - "intro": "Gestione del fine ciclo per le lavastoviglie, che di solito terminano con una lunga fase di asciugatura quasi silenziosa prima di un breve scarico finale." + "intro": "Gestione della fine per le lavastoviglie, che spesso asciugano a lungo in silenzio prima di un breve scarico finale." }, "anti_wrinkle": { - "intro": "La modalità antirughe/antistropicciamento rileva gli impulsi di tumbling a bassa potenza dopo la fase principale e li mantiene collegati al ciclo completato.", + "intro": "Mantiene nel ciclo terminato gli impulsi di rotazione a bassa potenza di un'asciugatrice dopo la fase principale.", "label": "Antirughe" }, "basic": { @@ -1143,7 +1123,7 @@ "label": "Base" }, "delay": { - "intro": "Il rilevamento dell'avvio ritardato identifica quando un elettrodomestico è alimentato ma non ha ancora iniziato il suo ciclo.", + "intro": "Rileva un elettrodomestico acceso in attesa di una partenza ritardata.", "label": "Avvio ritardato" }, "detection": { @@ -1167,161 +1147,153 @@ "label": "Temporizzazione e supervisore" }, "triggers": { - "intro": "Segnali esterni opzionali: un trigger di fine, un sensore di porta, un interruttore di pausa e il promemoria di scarico.", + "intro": "Extra facoltativi: trigger di fine, sensore porta, interruttore di pausa e promemoria di scarico.", "label": "Trigger e porte" - }, - "phase_eta": { - "label": "Tempo rimanente", - "intro": "Tempo rimanente in base alle fasi, per le macchine la cui durata del ciclo dipende dalla temperatura o dalla centrifuga." } }, "setting": { "dishwasher_end_spike_quiet_release": { "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." + "doc": "Dopo la durata prevista: quanto a lungo la lavastoviglie deve restare sotto la Soglia di arresto prima che WashData smetta di attendere lo scarico finale. Alzalo se uno scarico tardivo dopo una lunga asciugatura viene perso. L'attesa non supera mai i 30 minuti." }, "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." + "doc": "Frazione della durata media del programma che un ciclo deve raggiungere prima che la sua rotazione a bassa potenza conti come coda antirughe. Abbassala (es. 0,75) sulle asciugatrici con sensore di carico. Lascia le lavatrici al valore predefinito (0,98), altrimenti un lavaggio può terminare a metà." }, "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.", + "doc": "Mantieni nel ciclo terminato i brevi impulsi di rotazione a bassa potenza di un'asciugatrice dopo la fase principale di riscaldamento, invece di leggerli come nuovi cicli.", "label": "Abilita rilevamento antirughe" }, "anti_wrinkle_exit_power": { - "doc": "La potenza deve scendere al di sotto di questo valore tra gli impulsi affinché la modalità antirughe rimanga attiva.", + "doc": "In modalità antirughe, una potenza sotto questo valore conta come silenzio tra gli impulsi di rotazione, e un silenzio più lungo dell'Intervallo massimo tra impulsi termina la modalità. Se la Soglia di arresto è più alta si usa quella, quindi questo valore conta solo se è sopra la Soglia di arresto.", "label": "Soglia potenza uscita" }, "anti_wrinkle_idle_timeout": { - "doc": "Per quanto tempo la macchina può rimanere silenziosa tra due impulsi di rotazione prima che la modalità antirughe termini. Impostalo sopra l'intervallo più lungo che la tua asciugatrice lascia tra gli impulsi, altrimenti ogni impulso successivo viene letto come una falsa partenza.", + "doc": "Quanto a lungo l'asciugatrice può restare in silenzio tra un impulso e l'altro prima che la modalità antirughe termini. Impostalo oltre la pausa più lunga della tua asciugatrice, altrimenti gli impulsi successivi vengono letti come false partenze.", "label": "Intervallo massimo tra impulsi" }, "anti_wrinkle_max_duration": { - "doc": "Gli impulsi più lunghi vengono trattati come un vero e proprio ciclo piuttosto che come una caduta antirughe.", + "doc": "Un impulso più lungo conta come un ciclo vero, non come rotazione antirughe.", "label": "Durata massima" }, "anti_wrinkle_max_power": { - "doc": "Un impulso superiore a questa potenza viene trattato come un vero e proprio nuovo ciclo, non come un capitombolo antirughe. Impostare appena sopra la potenza dell'impulso di rotazione.", + "doc": "Un impulso sopra questa potenza conta come un nuovo ciclo vero, non come rotazione. Impostalo appena sopra la potenza dei tuoi impulsi di rotazione.", "label": "Potenza massima antirughe" }, "auto_label_confidence": { - "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.", + "doc": "Punteggio di corrispondenza a fine ciclo oltre il quale il programma viene etichettato senza chiedere. Alzalo per avere più certezza; abbassalo per etichettare più cicli in automatico. Tienilo sopra la Confidenza apprendimento.", "label": "Confidenza etichettatura automatica" }, "auto_maintenance": { - "doc": "Esegui le pulizie notturne: ricostruisci le buste del profilo, ricalcola l'integrità del ciclo, elimina le tracce di debug e conserva i cicli più recenti.", + "doc": "Pulizia notturna: etichetta i vecchi cicli che ora corrispondono chiaramente a un programma, ricostruisce le curve dei programmi, aggiorna i segni di anomalia e la salute dei cicli, e rimuove le tracce di debug se Salva tracce di debug è disattivato. I cicli e le loro tracce di potenza vengono sempre conservati.", "label": "Manutenzione automatica (pulizia notturna)" }, "completion_min_seconds": { - "doc": "I cicli più brevi di questo vengono scartati come cicli fantasma (corse di prova, apertura della porta per aggiungere un calzino).", + "doc": "I cicli più brevi di così (prove, porta aperta per aggiungere un calzino) vengono salvati come Interrotto e non ricevono la notifica di fine.", "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." + "doc": "Aggiunge all'inizio di un ciclo le letture dei tentativi di avvio falliti, per le macchine che fanno prove prima di partire (blocco porta, primo carico d'acqua). 300 s bastano nella maggior parte dei casi; 0 = disattivato. Gli avvii registrati si spostano prima, quindi le durate apprese deviano per un po'." }, "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.", + "doc": "La potenza deve restare nella banda di standby per questo tempo prima che l'elettrodomestico conti come in attesa di partire.", "label": "Secondi di conferma" }, "delay_start_detect_enabled": { - "doc": "Rileva quando l'apparecchio è acceso e in attesa (avvio ritardato/standby) ma non ha iniziato il ciclo, quindi l'erogazione in standby non viene confusa con un ciclo in esecuzione.", + "doc": "Rileva un elettrodomestico acceso in attesa di una partenza ritardata, così il suo consumo in standby non viene scambiato per un ciclo in corso.", "label": "Abilita rilevamento avvio ritardato" }, "delay_timeout_hours": { - "doc": "Smetti di aspettare in modalità di avvio ritardato dopo così tante ore e torna al minimo, quindi una macchina lasciata accesa ma mai avviata non aspetta per sempre.", + "doc": "Smetti di attendere una partenza ritardata dopo questo numero di ore e torna a inattivo.", "label": "Ore di timeout" }, "device_type": { - "doc": "Classe dell'apparecchio. Imposta valori predefiniti di rilevamento sensibili (soglie, ritardo di disattivazione, gestione delle estremità) ottimizzati per quel tipo di apparecchio; cambiarlo solo se il dispositivo era stato originariamente configurato come tipo sbagliato.", + "doc": "Imposta i valori di rilevamento predefiniti (soglie, ritardo spegnimento, gestione della fine) per questo tipo di elettrodomestico. Cambialo solo se il dispositivo è stato configurato con il tipo sbagliato.", "label": "Tipo di dispositivo" }, "door_sensor_entity": { - "doc": "Sensore binario porta opzionale. Utilizzato per rilevare quando l'apparecchiatura è stata aperta/scaricata dopo un ciclo.", + "doc": "Sensore porta facoltativo. Mostra quando l'elettrodomestico è stato aperto per scaricarlo dopo un ciclo.", "label": "Entità sensore porta" }, "duration_tolerance": { - "doc": "Tolleranza per le stime del tempo rimanente (feedback di apprendimento, non corrispondenza). Se la durata effettiva rientra nel +/-X% della stima, viene considerata una buona corrispondenza.", + "doc": "Solo per l'apprendimento: un ciclo entro +/-X% della durata stimata conta come una buona stima. Non incide sulla corrispondenza.", "label": "Tolleranza stima" }, "enable_ml_models": { - "doc": "Mentre un ciclo viene eseguito, lascia che i modelli affinino i risultati in tempo reale: una stima più stabile del tempo rimanente e di energia/costi e una protezione anti-arresto prematuro al rilevamento della fine (può solo ritardare un traguardo, mai terminarne uno in anticipo ed è limitato). Utilizza i tuoi modelli ottimizzati quando disponibili, altrimenti quelli integrati. Spento = solo la classica logica basata sulla potenza (ancora affidabile).", + "doc": "Usa un modello addestrato su questa macchina per prevedere energia e costo di un ciclo in corso, e lascia che un programma ben noto che si ferma insolitamente presto termini senza l'intero ritardo spegnimento. Disattivato: solo logica basata sulla potenza.", "label": "Applica modelli intelligenti durante un ciclo" }, "end_energy_threshold": { - "doc": "Durante il conto alla rovescia del ritardo di spegnimento, l'energia accumulata (watt x tempo) viene confrontata con questa soglia. Se superato, il conto alla rovescia si azzera, mantenendo i capi antipiega e i residui di asciugatura della lavastoviglie attaccati al ciclo invece di tagliarli. Alzarlo se i cicli terminano troppo presto durante il raffreddamento; abbassarlo se il rilevamento è lento.", + "doc": "Energia consentita durante il ritardo spegnimento. Oltre questo valore il ciclo continua, così rotazioni e code di asciugatura restano parte del ciclo. Abbassala se i cicli terminano durante una coda; alzala se i cicli finiti continuano ad aspettare.", "label": "Energia fine ciclo" }, - "end_repeat_count": { - "doc": "Numero di letture consecutive al di sotto della soglia di arresto richieste prima della fine del ciclo. 1 va bene per la maggior parte delle prese. Aumenta a 2-3 se la tua presa intelligente segnala occasionalmente un campione di falsi zero a metà ciclo e i tuoi cicli terminano prematuramente.", - "label": "Conteggio ripetizioni fine" - }, "energy_price_entity": { - "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.", + "doc": "Sensore con il prezzo attuale per kWh (es. una tariffa dinamica). Ha la precedenza sul prezzo fisso qui sotto, quindi deve essere un prezzo, non il sensore di potenza o di energia della presa.", "label": "Entità prezzo energia" }, "energy_price_static": { - "doc": "Prezzo fisso per kWh utilizzato per i dati sui costi quando non è impostata alcuna entità di prezzo in tempo reale sopra.", + "doc": "Prezzo fisso per kWh, usato quando non è impostata alcuna entità di prezzo.", "label": "Prezzo energia statico (per kWh)" }, "energy_sensor": { - "doc": "Contatore di energia cumulativa opzionale (total_increasing in kWh/Wh, ad es. il contatore di consumo totale della presa stessa). Quando è impostato, l'energia riportata di ogni ciclo viene ricavata dalla differenza di questo contatore tra inizio e fine, il che evita la sottostima che si ottiene integrando un sensore di potenza che aggiorna lentamente. Ripiega sul valore integrato se la lettura manca, se la sua unità è sconosciuta o se il delta non è positivo. Lascia vuoto per continuare a integrare il sensore di potenza.", + "doc": "Contatore facoltativo in kWh o Wh (total_increasing, es. il contatore integrato della presa). L'energia del ciclo viene allora letta da lì, il che è più preciso che calcolarla da un sensore di potenza lento. Se la sua lettura è inutilizzabile, si usa il valore calcolato.", "label": "Entità del contatore di energia" }, "expose_debug_entities": { - "doc": "Pubblica entità HA diagnostiche aggiuntive (confidenza della corrispondenza, ambiguità, stati interni). Disattivato mantiene l'elenco delle entità pulito per l'uso normale.", + "doc": "Aggiungi entità di diagnostica (confidenza di corrispondenza, ambiguità, stato interno). Disattivato mantiene pulito l'elenco delle entità.", "label": "Esponi entità di debug" }, "external_end_trigger": { - "doc": "Sensore binario il cui cambio di stato segna la fine del ciclo (es. contatto \"finito\" di un elettrodomestico o integrazione di un compagno).", + "doc": "Sensore binario il cui cambiamento segna la fine del ciclo (es. un contatto \"finito\" o un'altra integrazione).", "label": "Entità trigger esterno" }, "external_end_trigger_enabled": { - "doc": "Lascia che un sensore binario esterno segnali la fine di un ciclo, oltre al rilevamento integrato basato sulla potenza.", + "doc": "Lascia che un sensore binario segnali la fine di un ciclo, oltre al rilevamento basato sulla potenza.", "label": "Abilita trigger di fine esterno" }, "external_end_trigger_inverted": { - "doc": "Considerare lo spegnimento del sensore di trigger (anziché l'attivazione) come segnale di fine ciclo.", + "doc": "Considera come segnale di fine il passaggio del sensore a OFF (non a ON).", "label": "Inverti trigger esterno (attiva su SPENTO)" }, "learning_confidence": { - "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.", + "doc": "Punteggio di corrispondenza minimo per etichettare un ciclo terminato. Un vincitore netto viene etichettato; un caso incerto va nella coda di revisione in Cicli. Sotto questo valore non si etichetta né si chiede nulla. Tienilo sotto la Confidenza etichettatura automatica.", "label": "Confidenza apprendimento" }, "linked_device": { - "doc": "Facoltativamente nidificare questo dispositivo WashData sotto un altro dispositivo (ad esempio la presa intelligente) nel registro dei dispositivi HA, mostrato come \"Connesso tramite ...\".", + "doc": "Facoltativo. Mostra questo dispositivo WashData sotto un altro dispositivo (es. la presa intelligente) in Home Assistant, come \"Connesso tramite ...\".", "label": "Raggruppa sotto il dispositivo" }, "min_off_gap": { - "doc": "Se la macchina si spegne per meno di questo tempo, la sequenza di accensione/spegnimento/accensione viene trattata come un ciclo continuo. Impedisce che i programmi di ammollo (la macchina si spegne per diversi minuti durante il lavaggio) vengano suddivisi in due cicli separati. Impostalo più breve dell'intervallo tra i carichi consecutivi se desideri che vengano conteggiati come cicli separati. Le impostazioni predefinite del tipo di dispositivo proteggono la tipica pausa intraciclo per ciascun apparecchio.", + "doc": "Un periodo di spegnimento più breve non divide un ciclo, così le pause di ammollo restano in un unico lavaggio. Tienilo più breve della pausa tra carichi consecutivi, altrimenti si fondono in un solo ciclo.", "label": "Intervallo minimo di spegnimento" }, "min_power": { - "doc": "Potenza minima assoluta considerata attiva. Le letture inferiori a questo valore vengono trattate come 0 W (standby), filtrando il carico fantasma delle prese intelligenti e dei LED di standby.", + "doc": "Le letture sotto questo valore contano come 0 W (standby). Filtra il consumo a riposo delle prese intelligenti e dei LED di standby.", "label": "Potenza minima" }, "ml_training_enabled": { - "doc": "Studia periodicamente i cicli esaminati durante la notte e ottimizza i modelli per questa macchina specifica. Un cambiamento viene mantenuto solo quando ottiene effettivamente risultati migliori nei cicli sospesi, quindi questo può solo aiutare o rimanere lo stesso, senza mai regredire.", + "doc": "Riaddestra di tanto in tanto il modello dell'energia sui cicli terminati di questa macchina. Un nuovo modello viene tenuto solo se batte la stima semplice sui cicli di prova.", "label": "Impara da questa macchina" }, "ml_training_hour": { - "doc": "Ora locale del giorno (0-23) per effettuare la messa a punto notturna. Scegli un'ora tranquilla, ad esempio le 2 (02:00).", + "doc": "Ora del giorno (0-23) per l'addestramento notturno. Scegli un'ora tranquilla, es. 2.", "label": "Impara all'ora" }, "ml_training_interval_days": { - "doc": "Ricontrolla i miglioramenti al massimo una volta ogni questi giorni.", + "doc": "Numero minimo di giorni tra due riaddestramenti.", "label": "Controlla al massimo ogni" }, "ml_training_min_cycles": { - "doc": "Attendere fino a quando almeno questo numero di cicli non sarà stato registrato prima della messa a punto, in modo che ci sia abbastanza da cui imparare.", + "doc": "Attendi almeno questo numero di cicli registrati prima dell'addestramento.", "label": "Cicli necessari prima" }, "name": { - "doc": "Nome visualizzato mostrato nell'elenco delle integrazioni HA e nel registro del dispositivo.", + "doc": "Nome mostrato in Home Assistant per questo dispositivo.", "label": "Nome dispositivo" }, "no_update_active_timeout": { - "doc": "Se non arrivano aggiornamenti di alimentazione per così tanto tempo durante il funzionamento, presumi che la spina sia caduta offline e forza l'arresto per evitare un ciclo di zombie. Il valore predefinito di 600 s consente il ritardo cloud o mesh.", + "doc": "Se durante un ciclo non arriva alcun aggiornamento di potenza per questo tempo, si presume che la presa sia offline e il ciclo viene fermato forzatamente. Il valore predefinito di 600 s lascia margine ai ritardi del cloud o della rete mesh.", "label": "Timeout senza aggiornamenti" }, "notify_before_end_minutes": { @@ -1329,35 +1301,35 @@ "label": "Avviso pre-fine" }, "notify_channel": { - "doc": "Nome del canale di notifica Android per i messaggi di avvio/live (controlla l'audio e la priorità per canale sull'app mobile). Vuoto utilizza l'impostazione predefinita dell'app complementare.", + "doc": "Canale Android per i messaggi di avvio e in tempo reale; il canale definisce suono e priorità. Vuoto: predefinito dell'app.", "label": "Canale Android (avvio/tempo reale)" }, "notify_cycle_timers": { - "doc": "Notifiche in minuti specifici dell'inizio di un ciclo (ad esempio per aggiungere ammorbidente). Il messaggio supporta {device}, {program}, {minutes}. Abilita la pausa automatica per mettere in pausa a quel punto e ricevere una notifica interattiva con un pulsante Riprendi; riprendere tramite il pannello, il servizio Pausa/Riprendi o l'azione di notifica.", + "doc": "Notifiche a minuti stabiliti di un ciclo (es. per aggiungere l'ammorbidente). Il messaggio supporta {device}, {program}, {minutes}. Con Pausa automatica, il ciclo va anche in pausa lì e la notifica riceve un pulsante Riprendi.", "label": "Timer ciclo" }, "notify_finish_channel": { - "doc": "Nome del canale di notifica Android per il messaggio di fine. Vuoto riutilizza il canale iniziale/live.", + "doc": "Canale Android per il messaggio di fine. Vuoto: riusa il canale di avvio/tempo reale.", "label": "Canale Android (fine)" }, "notify_finish_message": { - "doc": "Corpo inviato al termine di un ciclo. Variabili modello: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}. {time_finished} e {vs_typical} sono i più utili qui.", + "doc": "Testo inviato quando un ciclo termina. Variabili: {device}, {program}, {duration} (minuti), {duration_hm} (1 h 05 min), {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}, {status} (completed o force_stopped; i cicli interrotti non inviano nulla). Con qualsiasi altra variabile si usa il testo predefinito.", "label": "Messaggio di fine" }, "notify_finish_services": { - "doc": "notify.* servizi chiamati al termine di un ciclo. Aggiungine uno per bersaglio; lasciare vuoto per nessuna notifica di fine.", + "doc": "Servizi notify.* da chiamare quando un ciclo termina. Lascia vuoto per nessuno.", "label": "Servizi di fine" }, "notify_fire_events": { - "doc": "Attiva anche gli eventi ha_washdata_* all'inizio/fine del ciclo in modo da poter creare le tue automazioni.", + "doc": "Genera anche eventi ha_washdata_* all'inizio e alla fine del ciclo, per le tue automazioni.", "label": "Genera eventi HA per le notifiche" }, "notify_icon": { - "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.", + "doc": "Piccola icona della notifica (es. mdi:washing-machine). Inviata solo ai destinatari mobile_app, non ai gruppi notify. Su iOS sostituisce l'icona dell'app e richiede l'app companion 2026.8 o successiva. Vuoto: predefinito della piattaforma.", "label": "Icona notifica" }, "notify_live_chronometer": { - "doc": "Mostra un timer per il conto alla rovescia con aggiornamento in tempo reale nella notifica (sulle piattaforme che lo supportano) invece di una stima statica.", + "doc": "Mostra un conto alla rovescia in corso nella notifica (dove supportato) invece di una stima fissa.", "label": "Usa cronometro in tempo reale" }, "notify_live_interval_seconds": { @@ -1365,143 +1337,135 @@ "label": "Intervallo aggiornamento in tempo reale" }, "notify_live_overrun_percent": { - "doc": "Se un ciclo supera la stima di oltre questa percentuale, invia un avviso di superamento.", + "doc": "Gli aggiornamenti in tempo reale si fermano quando un ciclo supera di questa percentuale la durata prevista (20 = fino a 1,2x il tempo previsto).", "label": "% Sforamento in tempo reale prima dell'avviso" }, "notify_live_services": { - "doc": "notify.* servizi richiedevano aggiornamenti in tempo reale sullo stato di avanzamento durante l'esecuzione di un ciclo. Lascia vuoto per disabilitare le notifiche di avanzamento in tempo reale.", + "doc": "Servizi notify.* per gli aggiornamenti di avanzamento in tempo reale durante un ciclo. Lascia vuoto per disattivarli.", "label": "Servizi avanzamento in tempo reale" }, "notify_only_when_home": { - "doc": "Invia notifiche solo quando almeno una delle persone collegate (sopra) è a casa.", + "doc": "Notifica solo quando almeno una delle persone qui sopra è a casa.", "label": "Notifica solo quando a casa" }, "notify_people": { - "doc": "persona.* entità utilizzate da \"Notifica solo quando a casa\" per decidere se qualcuno è a casa.", + "doc": "Entità person.* che \"Notifica solo quando a casa\" controlla.", "label": "Persone (per Solo a Casa)" }, "notify_pre_complete_message": { - "doc": "Corpo dell'avviso di pre-fine / quasi completato. Variabili modello: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Testo di ogni aggiornamento di avanzamento in tempo reale. Variabili di modello: {device}, {program}, {minutes} (minuti rimanenti). Con qualsiasi altra variabile si usa il testo predefinito.", "label": "Messaggio pre-completamento" }, "notify_reminder_message": { - "doc": "Corpo del promemoria di scarico ancora in attesa. Vuoto usa il valore predefinito integrato. Variabili modello: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Testo dell'Avviso pre-fine, inviato una sola volta. Variabili di modello: {device}, {program}, {minutes} (l'impostazione Avviso pre-fine). Con qualsiasi altra variabile si usa il testo predefinito.", "label": "Messaggio promemoria" }, "notify_start_message": { - "doc": "Corpo inviato all'avvio di un ciclo. Variabili modello: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Testo inviato all'avvio di un ciclo. Variabili di modello: {device}, {program}. Con qualsiasi altra variabile si usa il testo predefinito.", "label": "Messaggio di avvio" }, "notify_start_services": { - "doc": "notify.* servizi chiamati all'avvio di un ciclo. Aggiungine uno per target (telefono, dashboard, ecc.); lasciare vuoto per nessuna notifica di avvio.", + "doc": "Servizi notify.* da chiamare quando un ciclo inizia (telefono, dashboard, ...). Lascia vuoto per nessuno.", "label": "Servizi di avvio" }, "notify_timeout_seconds": { - "doc": "Ignora automaticamente la notifica dopo questo numero di secondi (sulle piattaforme che la supportano). 0 lo mantiene finché non viene eliminato manualmente.", + "doc": "Chiudi la notifica dopo questo numero di secondi (dove supportato). 0 la mantiene finché non la chiudi.", "label": "Chiusura automatica dopo" }, "notify_title": { - "doc": "Titolo della notifica. Variabili modello: {device}, {program}, {duration}, {energy}, {cost}, {date}, {time}, {minutes}.", + "doc": "Titolo della notifica. Viene sostituito solo {device}; con qualsiasi altra variabile si usa il titolo predefinito.", "label": "Titolo notifica" }, "notify_unload_delay_minutes": { - "doc": "Minuti dopo la fine di un ciclo prima dell'invio del promemoria \"scaricare la macchina\" ancora in attesa. Impostare 0 per disabilitare il promemoria.", + "doc": "Minuti dopo la fine di un ciclo prima del promemoria \"scarica la macchina\". 0 lo disattiva.", "label": "Ritardo promemoria scarico" }, "notify_unload_repeat": { - "doc": "Continua a inviare il promemoria di scarico ogni \"Ritardo promemoria scarico\" minuti finché lo scarico non viene confermato o non tocchi \"Interrompi promemoria\" nella notifica. Richiede un modo per confermare lo scarico: un'Entità sensore porta, un'Entità di conferma scarico o l'opzione \"Conferma scarico manualmente\" qui sotto. Il pulsante di chiusura funziona sulle notifiche dell'app complementare (mobile) di Home Assistant.", + "doc": "Invia di nuovo il promemoria di scarico ogni \"Ritardo promemoria scarico\" minuti finché lo scarico non viene confermato o tocchi \"Interrompi promemoria\" (app mobile). Richiede un'Entità sensore porta, un'Entità di conferma scarico o \"Conferma scarico manualmente\".", "label": "Ripeti fino alla conferma di scarico" }, "off_delay": { - "doc": "È tempo di attendere dopo l'interruzione della corrente prima di dichiarare terminato il ciclo. Se l'alimentazione viene ripristinata entro questo intervallo, il ciclo continua senza interruzioni, consentendo di colmare le pause tra le fasi di lavaggio. Le lavastoviglie hanno fasi di asciugatura lunghe (spegnimento per 20-60 minuti), quindi il ritardo di spegnimento deve superare quello per mantenere l'intero ciclo di lavaggio+asciugatura come un ciclo.", + "doc": "Quanto a lungo la potenza deve restare bassa prima che il ciclo sia finito. Se la potenza torna entro questo tempo, il ciclo continua. Le lavastoviglie possono restare ferme 20-60 min durante l'asciugatura, quindi impostalo più lungo.", "label": "Ritardo spegnimento" }, "pause_cuts_power": { - "doc": "Quando un ciclo è in pausa, spegni anche l'entità Switch di seguito. Solo per apparecchi la cui spina può essere tagliata in sicurezza a metà ciclo.", + "doc": "Quando un ciclo va in pausa, spegni anche l'Entità interruttore qui sotto. Solo per elettrodomestici che possono perdere l'alimentazione a metà ciclo in sicurezza.", "label": "La pausa interrompe anche l'alimentazione (tramite interruttore)" }, "power_off_delay": { - "doc": "Per quanto tempo la potenza deve rimanere sotto la Soglia di spegnimento dopo la fine di un ciclo prima che lo stato torni a Spento. Usato solo quando la Soglia di spegnimento è maggiore di 0. Viene controllato secondo la cadenza in background, quindi il ritardo effettivo viene arrotondato per eccesso al successivo tick di scadenza dello stato.", + "doc": "Quanto a lungo la potenza deve restare sotto la Soglia di spegnimento dopo un ciclo prima che lo stato torni a Spento. Usato solo quando quella soglia è sopra 0. Viene controllato periodicamente, quindi l'attesa reale può essere un po' più lunga.", "label": "Ritardo di spegnimento" }, "power_off_threshold_w": { - "doc": "Rilevamento facoltativo dello stato Spento basato sulla potenza. Quando è maggiore di 0, una volta terminato un ciclo e se la potenza rimane sotto questo livello per il Ritardo di spegnimento, la macchina viene considerata spenta e lo stato torna a Spento. Lascia a 0 per disattivarlo (valore predefinito). Impostalo sopra il consumo reale a macchina spenta e sotto la Soglia di arresto e il consumo in standby della tua macchina (terminata ma ancora accesa); se non è inferiore alla Soglia di arresto, viene ignorato. Quando è attivo, sostituisce il Ritardo reset avanzamento per il ritorno a Spento, così una macchina terminata resta su Completato/Pulito finché non viene effettivamente spenta.", + "doc": "Dopo un ciclo, una potenza sotto questo valore per il Ritardo di spegnimento porta lo stato a Spento, invece del Ritardo reset avanzamento. Impostalo sopra il consumo da spento, sotto lo standby e la Soglia di arresto. 0 lo disattiva (predefinito).", "label": "Soglia di spegnimento" }, "power_sensor": { - "doc": "L'entità sensore che segnala la potenza in tempo reale in watt per questo elettrodomestico (ad esempio sensor.washer_power). Tutto il rilevamento del ciclo si basa su questo segnale.", + "doc": "Il sensore che riporta la potenza attuale di questo elettrodomestico in watt (es. sensor.washer_power). Tutto il rilevamento si basa su di esso.", "label": "Sensore di potenza" }, - "profile_duration_tolerance": { - "doc": "La fascia +/- attorno alla durata media del profilo utilizzata durante la corrispondenza. 0,25 significa che un profilo di 60 minuti corrisponde a cicli di 45-75 minuti.", - "label": "Tolleranza durata profilo" - }, "profile_match_interval": { - "doc": "La frequenza con cui tentare la corrispondenza del profilo durante un ciclo di esecuzione. Il valore predefinito di 300 s (5 minuti) bilancia la velocità di rilevamento e la CPU.", + "doc": "Ogni quanto un ciclo in corso viene confrontato con i profili. Predefinito 300 s (5 min).", "label": "Intervallo di corrispondenza" }, "profile_match_max_duration_ratio": { - "doc": "Durata massima del ciclo rispetto al profilo. 1.3 significa che un ciclo deve essere inferiore al 130% della durata del profilo per corrispondere.", + "doc": "Un programma viene escluso quando il ciclo supera questo multiplo della sua durata tipica. 1,8: un ciclo più lungo dell'80% rispetto al programma non gli corrisponde più.", "label": "Rapporto durata massima" }, "profile_match_min_duration_ratio": { - "doc": "Durata minima del ciclo rispetto al profilo. 0,9 significa che un ciclo deve corrispondere almeno al 90% della durata del profilo per corrispondere.", + "doc": "Un programma viene considerato solo quando il ciclo ha raggiunto questa frazione della sua durata tipica. 0,10: un programma di 2 ore diventa candidato dopo 12 minuti. Non applicato nei primi 15 minuti di un ciclo.", "label": "Rapporto durata minima" }, "profile_match_threshold": { - "doc": "Punteggio di somiglianza minimo (0-1) richiesto alla fine del ciclo per accettare l'identificazione del programma. Alzarlo per ridurre le identificazioni errate; abbassarlo se i programmi della macchina non corrispondono. Il valore predefinito 0,4 è un punto di partenza conservativo.", + "doc": "Punteggio di corrispondenza in tempo reale che serve a un ciclo prima che la Terminazione intelligente o la chiusura antirughe possano terminarlo in anticipo. Non cambia il programma mostrato. Alzalo se corrispondenze deboli chiudono i cicli troppo presto; abbassalo se corrispondenze chiare aspettano tutto il ritardo spegnimento.", "label": "Soglia di corrispondenza" }, "profile_unmatch_threshold": { - "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.", + "doc": "Se il punteggio di corrispondenza in tempo reale scende sotto questo valore, l'ipotesi di programma attuale viene cancellata. Tienilo un po' sotto la Soglia di corrispondenza, così un breve calo non la azzera.", "label": "Soglia di non corrispondenza" }, "progress_reset_delay": { - "doc": "Al termine, mantieni l'avanzamento al 100% per questo tempo in modo che Completato sia visibile sui dashboard prima di reimpostare su Inattivo.", + "doc": "Dopo un ciclo, mantieni l'avanzamento al 100% per questo tempo prima di tornare a Inattivo.", "label": "Ritardo reset avanzamento" }, "pump_stuck_duration": { - "doc": "Secondi in cui una pompa può funzionare continuamente prima che venga contrassegnata come probabilmente bloccata (avvia l'evento pompa bloccata).", + "doc": "Per quanto tempo una pompa può funzionare senza sosta prima che scatti l'evento pompa bloccata.", "label": "Durata pompa bloccata" }, "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.", + "doc": "Tempo minimo tra letture elaborate. Le letture che arrivano prima vengono saltate (le letture basse durante un ciclo vengono sempre conservate). Impostalo sull'intervallo di invio della tua presa o meno; un valore più alto scarta solo letture.", "label": "Intervallo di campionamento" }, "save_debug_traces": { - "doc": "Memorizzare la traccia di potenza completa e i dati di debug corrispondenti per ciascun ciclo. Utile per la risoluzione dei problemi ma aumenta le dimensioni dello spazio di archiviazione.", + "doc": "Salva anche con ogni ciclo i dati di debug del comparatore (i punteggi di ogni candidato), per la risoluzione dei problemi. Aggiunge circa 20 KB per ciclo.", "label": "Salva tracce di debug" }, "smart_termination_duration_ratio": { - "doc": "Quanto un ciclo deve essere avanzato nella durata prevista del programma corrispondente prima che la Terminazione intelligente possa concluderlo in anticipo una volta che la potenza cala. La durata prevista è la media del programma, quindi sugli apparecchi il cui tempo di funzionamento varia molto - lavatrici a seconda dell'acqua di ingresso fredda d'inverno o tiepida d'estate, asciugatrici con sensore di umidità, programmi che dipendono dal carico - circa la metà dei cicli termina prima di quella media e non ottiene mai la conclusione rapida, terminando solo tramite il timeout di riserva con minuti di ritardo. Abbassa questo valore (ad es. 0,85) su quelle macchine affinché la conclusione anticipata si attivi comunque; alzalo verso 1,0 per essere più prudente. Lascia vuoto per il valore predefinito (0,98, o 0,99 per le lavastoviglie). Può solo concludere un ciclo prima, mai dopo, e non si attiva mai su una corrispondenza ambigua o a bassa affidabilità.", + "doc": "Frazione della durata media del programma che un ciclo deve raggiungere prima che la Terminazione intelligente possa chiuderlo in anticipo. Se le durate variano (temperatura dell'acqua, asciugatura a sensore, dimensione del carico), abbassala (es. 0,85). Lascia vuoto per 0,98 (0,99 sulle lavastoviglie).", "label": "Rapporto di terminazione intelligente" }, - "smoothing_window": { - "doc": "Quanto viene attenuato il segnale di potenza grezza. Basso (2) è reattivo ma rumoroso; alto (5) attenua i picchi ma aggiunge ritardo.", - "label": "Finestra di smussamento" - }, "start_duration_threshold": { - "doc": "La potenza deve rimanere al di sopra della soglia di avvio per questo tempo per confermare un avvio reale, impedendo che gli interruttori di accensione/spegnimento in frazioni di secondo avviino un ciclo.", + "doc": "La potenza deve restare sopra la Soglia di avvio per questo tempo prima che un ciclo parta, così i brevi picchi di accensione/spegnimento vengono ignorati.", "label": "Durata avvio" }, "start_energy_threshold": { - "doc": "Energia (potenza x tempo) che l'apparecchio deve consumare prima di FUNZIONARE. Un breve picco ad alta potenza ha un'energia molto bassa e viene ignorato, impedendo false partenze.", + "doc": "Energia che l'elettrodomestico deve consumare prima che il ciclo conti come in corso. Un breve picco di potenza consuma poca energia, quindi non avvia un ciclo.", "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 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.", + "doc": "La potenza deve salire sopra questo valore per avviare un ciclo. Troppo basso: lo standby causa false partenze. Troppo alto: i programmi a partenza lenta (carico d'acqua fredda) vengono persi.", "label": "Soglia di avvio" }, "stop_threshold_w": { - "doc": "La potenza deve scendere al di sotto di questo livello prima che inizi il conto alla rovescia del ritardo di disattivazione. Impostalo al di sotto della Soglia iniziale: lo spazio tra loro è la banda di isteresi che impedisce lo sfarfallio. Se impostato su un valore troppo alto, le fasi a basso consumo (trattenimento risciacquo, antipiega) attivano erroneamente la sequenza finale.", + "doc": "La potenza deve scendere sotto questo valore prima che il ritardo spegnimento inizi a contare. Tienilo sotto la Soglia di avvio. Se è troppo alto, le fasi a bassa potenza (pause di risciacquo, antirughe) avviano per errore il conto alla rovescia di fine.", "label": "Soglia di arresto" }, "switch_entity": { - "doc": "L'interruttore opzionale può essere disattivato durante la pausa e riattivato alla ripresa, utilizzato insieme a \"La pausa interrompe anche l'alimentazione\".", + "doc": "Interruttore facoltativo che si spegne in pausa e si riaccende alla ripresa, con \"La pausa interrompe anche l'alimentazione\".", "label": "Entità interruttore" }, "watchdog_interval": { - "doc": "La frequenza con cui il watchdog in background controlla i sensori bloccati e i timeout trascorsi. Predefinito 30 secondi.", + "doc": "Ogni quanto il controllo in background cerca sensori bloccati e timeout scaduti. Predefinito 30 s.", "label": "Intervallo supervisore" }, "notify_milestone_message": { @@ -1509,127 +1473,82 @@ "label": "Messaggio traguardo" }, "notify_milestones": { - "doc": "Conteggi di cicli separati da virgola che attivano una notifica di celebrazione una tantum al raggiungimento (es. 50, 100, 500, 1000). Vuoto disattiva le notifiche dei traguardi.", + "doc": "Numeri di cicli che attivano una notifica di festeggiamento una tantum, separati da virgole (es. 50, 100, 500, 1000). Vuoto li disattiva.", "label": "Traguardi cicli" }, "notify_quiet_end_hour": { - "doc": "Fine della finestra Non disturbare (0-23). Le notifiche trattenute vengono consegnate a quest'ora. Lascia vuoto per disattivare.", + "doc": "Fine della fascia non disturbare (0-23); le notifiche trattenute vengono inviate allora. Vuoto la disattiva.", "label": "Fine ore di silenzio" }, "notify_quiet_start_hour": { - "doc": "Inizio di una finestra Non disturbare (0-23). Le notifiche di fine, promemoria e bucato pulito che scatterebbero durante le ore di silenzio vengono trattenute e consegnate al termine della finestra. Lascia vuoto per disattivare. Supporta finestre che attraversano la mezzanotte (es. inizio 22, fine 7).", + "doc": "Inizio di una fascia non disturbare (0-23). Le notifiche di fine, promemoria e bucato pulito in questa fascia vengono trattenute fino al suo termine. Può superare la mezzanotte (es. dalle 22 alle 7). Vuoto la disattiva.", "label": "Inizio ore di silenzio" }, "peak_rate_message": { - "doc": "Testo personalizzato facoltativo per il suggerimento sulla tariffa di picco aggiunto alla notifica di avvio. Variabili modello: {device}, {price}. Vuoto usa il valore predefinito integrato.", + "doc": "Testo personalizzato per il suggerimento sulla tariffa di punta. Variabili di modello: {device}, {price}. Vuoto usa il testo predefinito.", "label": "Messaggio tariffa di picco" }, "peak_rate_threshold": { - "doc": "Quando un ciclo inizia e il prezzo attuale per kWh è pari o superiore a questo valore, aggiunge un suggerimento sulla tariffa di picco alla notifica di avvio. 0 o vuoto disattiva il suggerimento.", + "doc": "Se il prezzo per kWh è pari o superiore a questo valore all'avvio di un ciclo, aggiunge un suggerimento sulla tariffa di punta alla notifica di avvio. 0 o vuoto lo disattiva.", "label": "Soglia tariffa di picco (per kWh)" }, - "dtw_bandwidth": { - "label": "Tolleranza di corrispondenza della forma", - "doc": "Quanta deformazione temporale può applicare il sistema di corrispondenza a un ciclo per adattarlo alla forma di un programma salvato. Un valore più alto tollera maggiori variazioni di velocità; troppo alto rischia di corrispondere al programma sbagliato." - }, - "corr_weight": { - "label": "Peso forma / livello", - "doc": "Equilibrio tra la forma della curva e il livello di potenza assoluto nel valutare una corrispondenza. Un valore più alto privilegia la forma; uno più basso privilegia la corrispondenza del livello di potenza." - }, - "duration_weight": { - "label": "Peso della durata", - "doc": "Quanto viene preferito un programma la cui durata tipica corrisponde a questo ciclo." - }, - "energy_weight": { - "label": "Peso dell'energia", - "doc": "Quanto viene preferito un programma il cui consumo di energia tipico corrisponde a questo ciclo." - }, "store_brand": { "label": "Marca dell'elettrodomestico", - "doc": "Facoltativo. La marca dell'elettrodomestico, scelta dal catalogo della community. Usata per trovare e condividere registrazioni di riferimento corrispondenti. Lascia vuoto se non usi le funzionalità online." + "doc": "Facoltativo. La marca del tuo elettrodomestico nel catalogo della community, per trovare e condividere registrazioni di riferimento. Lascia vuoto se non usi le funzioni online." }, "store_model": { "label": "Modello dell'elettrodomestico", - "doc": "Facoltativo. Il modello dell'elettrodomestico, scelto dal catalogo della community una volta impostata la marca. Se il tuo modello non è elencato, puoi aggiungerlo al catalogo." + "doc": "Facoltativo. Il modello del tuo elettrodomestico nel catalogo della community (scegli prima una marca). Se non è in elenco, puoi aggiungerlo." }, "show_contributor": { "doc": "Mostra l'attribuzione \"di \" sugli elettrodomestici della community e sui cicli di riferimento." }, - "enable_phase_matching": { - "label": "Tempo rimanente in base alle fasi", - "doc": "Suddivide ogni ciclo in corso in fasi (riscaldamento, lavaggio, centrifuga) e distribuisce il tempo rimanente per fase, combinato con la stima classica - basandosi sulla ripartizione per fasi all'inizio del ciclo e sulla stima classica verso la fine. Questo personalizza il conto alla rovescia in base a quanto la tua macchina riscalda e funziona realmente, cosa più evidente nella prima metà di un ciclo. Disattivato = solo la stima classica. È interessata solo la visualizzazione del tempo rimanente; la corrispondenza dei programmi e il rilevamento dei cicli restano invariati." - }, - "keep_min_score": { - "label": "Score min corrispondenza", - "doc": "Punteggio di similarità minimo che un candidato deve avere per restare in gara. Il predefinito 0,1 è volutamente permissivo - ammette anche candidati deboli e si affida alle fasi successive per trovare la migliore corrispondenza. Aumentarlo per eliminare prima i profili improbabili; ridurlo per ampliare il pool iniziale di candidati." - }, - "dtw_blend": { - "label": "Fusione Warp", - "doc": "Quanto il punteggio di allineamento per deformazione temporale (DTW) sostituisce il punteggio centrale della fase 2. 0 = usa solo il punteggio della fase 2, 1 = usa solo il punteggio DTW, 0,5 (predefinito) = mix equilibrato. Aumentarlo per affidarsi maggiormente all'allineamento Warp quando i programmi hanno livelli di potenza simili ma schemi temporali diversi." - }, - "dtw_ensemble_w": { - "label": "Mix Warp ensemble", - "doc": "In modalità DTW ensemble (la predefinita), il peso su L1 scalato rispetto a DTW derivato (DDTW). 1,0 = solo L1 scalato, 0 = solo DDTW, 0,7 = predefinito. La variante L1 scalata confronta i livelli di potenza; la variante derivata reagisce a come la potenza cambia nel tempo. Ensemble li combina entrambi." - }, - "dtw_ddtw_scale": { - "label": "Scala Warp derivata", - "doc": "Distanza di semi-saturazione per la valutazione DTW derivata. Il punteggio si dimezza quando la distanza DDTW è uguale a questo valore. Minore = maggiore sensibilità alle differenze di forma. Il predefinito 30 è calibrato su tracce di potenza tipiche degli elettrodomestici. Influisce solo sulle modalità DTW ensemble e ddtw." - }, - "dtw_refine_top_n": { - "label": "Conteggio affinamento Warp", - "doc": "Quanti candidati principali della fase 2 vengono rivalutati da DTW. DTW è più costoso, quindi viene applicato solo agli N migliori candidati. Predefinito 5. Aumentarlo (a 7-9) se il profilo corretto a volte raggiunge solo il 4° o 5° posto dopo la fase 2 - DTW può recuperarlo. Ridurlo velocizza leggermente il matching." - }, - "duration_scale": { - "label": "Nitidezza durata", - "doc": "Precisione della penalità di concordanza di durata nella fase 4. È il log-rapporto in cui il punteggio di concordanza si dimezza. Minore = più severo: una discordanza di durata penalizza di più. Il predefinito 0,175 corrisponde a circa il 18% di tolleranza di durata a metà peso. Combinare con Peso durata." - }, - "energy_scale": { - "label": "Nitidezza energia", - "doc": "Precisione della penalità di concordanza di energia. Minore = più severo: una discordanza di energia penalizza di più. Il predefinito 0,25 è intenzionalmente più indulgente della Precisione di durata, perché l'energia varia con il carico. Combinare con Peso energia." - }, "power_profile_interval_min": { "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." + "doc": "Dimensione degli intervalli dell'attributo power_profile (watt medi per intervallo) usato da pianificatori come EMHASS e tibber_prices. Più piccolo conserva i picchi brevi; più grande ammorbidisce la curva. Predefinito 15 min. Non incide 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." + "doc": "Solo iOS. Aggiorna l'avanzamento in tempo reale in silenzio, con una priorità a basso consumo di batteria che iOS può raggruppare. Il primo aggiornamento e le notifiche di avvio e fine avvisano comunque. Disattivato: ogni aggiornamento avvisa." }, "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." + "doc": "Solo Android. Mantieni la notifica di avanzamento in tempo reale quando la tocchi. Disattivato: un tocco la chiude." }, "notify_live_click_action": { "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." + "doc": "Cosa apre un tocco sulla notifica di questo elettrodomestico. Vuoto: il pannello WashData su questo elettrodomestico. Inserisci un percorso di dashboard (es. /lovelace/laundry) o un URL, oppure \"none\" per nessuna azione." }, "door_opens_at_end": { "label": "Porta si apre automaticamente al termine", - "doc": "Per le lavastoviglie che aprono automaticamente lo sportello al termine del ciclo per asciugare (AirDry e simili). Con questa opzione attiva, un'apertura dello sportello durante un ciclo non lo mette più in pausa per sempre; invece, se lo sportello rimane aperto per il tempo di attesa indicato sotto, WashData considera il ciclo terminato. Un'apertura breve (per aggiungere un oggetto) viene ignorata. Richiede un'entità sensore della porta." + "doc": "Per lavastoviglie che aprono la porta alla fine per asciugare (es. AirDry). Una porta che resta aperta per il tempo di permanenza qui sotto chiude il ciclo invece di metterlo in pausa; una breve apertura viene ignorata. Richiede un'Entità sensore porta." }, "door_end_dwell_seconds": { "label": "Attesa apertura porta al termine", - "doc": "Per quanto tempo lo sportello deve restare aperto prima che WashData termini il ciclo, quando \"Porta si apre automaticamente al termine\" è attiva. Abbastanza lungo da ignorare l'aggiunta rapida di un piatto (predefinito 60 s), abbastanza breve da terminare prontamente quando la macchina apre lo sportello." + "doc": "Con \"Porta si apre automaticamente al termine\" attivo: quanto a lungo la porta deve restare aperta prima che il ciclo termini. Abbastanza da ignorare l'aggiunta di un piatto (predefinito 60 s)." }, "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." + "doc": "Quali tipi di cicli plasmano la curva e la corrispondenza di ogni programma. Togliere la spunta a un tipo non elimina nulla; utile se non ti fidi dei dati importati. Le statistiche contano sempre solo i cicli di questa macchina. Togliere tutte le spunte viene ignorato." }, "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." + "doc": "Addebita ogni ciclo al prezzo di ogni momento in cui era in funzione, invece del prezzo alla fine. Richiede un'entità di prezzo. Elabora storico (Diagnostica) ricalcola i cicli più vecchi." }, "unload_confirm_entity": { "label": "Entità di conferma scarico", - "doc": "Entità facoltativa la cui attivazione indica che il carico è stato tolto, per le macchine che non possono avere un sensore porta. Va bene qualsiasi tipo: un pulsante Zigbee o NFC, un helper input_button, un sensore di movimento davanti alla macchina, una scena. Ogni cambio di stato conta come conferma, tranne lo spegnimento e il passaggio a non disponibile. Impostarla abilita anche il promemoria di scarico su un dispositivo senza Entità sensore porta." + "doc": "Facoltativo. Qualsiasi entità che confermi che il carico è stato tolto, per macchine senza sensore porta (un pulsante, input_button, sensore di movimento, scena). Conta ogni cambio di stato tranne lo spegnimento o il passaggio a non disponibile. Attiva anche il promemoria di scarico." }, "unload_track_without_door": { "label": "Conferma scarico manualmente", - "doc": "Esegue il promemoria di scarico su un dispositivo senza Entità sensore porta e senza Entità di conferma scarico, e lo scarico lo confermi tu dalla tua automazione: premi l'entità pulsante \"Segna come scaricato\" oppure chiama il servizio ha_washdata.mark_unloaded. Ignorato quando è impostata un'Entità sensore porta, che fornisce già il segnale." + "doc": "Usa il promemoria di scarico senza sensore porta né entità di conferma. Conferma lo scarico dalla tua automazione con il pulsante \"Segna come scaricato\" o il servizio ha_washdata.mark_unloaded. Ignorato se è impostata un'Entità sensore porta." }, "notify_icon_color": { "label": "Colore notifica", - "doc": "Colore d'accento per le notifiche di questo elettrodomestico, così da distinguere a colpo d'occhio una lavatrice, un'asciugatrice e una lavastoviglie. iOS colora l'icona e la barra di avanzamento dell'Attività in tempo reale sulla schermata di blocco e nella Dynamic Island. Ad Android viene inviato lo stesso colore, ma ciò che ne fa dipende dal telefono: alcuni colorano l'icona e molti (in particolare Samsung One UI) lo ignorano del tutto. Il formato esadecimale (es. #03A9F4) è compreso da entrambi. Lascia vuoto per l'impostazione predefinita della piattaforma." + "doc": "Colore di accento per le notifiche di questo elettrodomestico, per distinguere facilmente lavatrice, asciugatrice e lavastoviglie. Su iOS riempie il cerchio dietro l'icona e colora la barra della Live Activity. Molti telefoni Android (soprattutto Samsung) lo ignorano. Usa un valore hex, es. #03A9F4. Vuoto: predefinito della piattaforma." + }, + "share_name": { + "doc": "Mostra il nome del tuo account come autore dei cicli che condividi. Disattivato: condivisi senza nome." } }, "setting_group": { @@ -1657,9 +1576,6 @@ "door_pause": { "label": "Porta e pausa" }, - "dtw_refinement": { - "label": "Raffinamento DTW" - }, "duration_gates": { "label": "Porte durata" }, @@ -1736,13 +1652,18 @@ "rebuilding": "Ricostruzione…", "analyzing": "Analisi…", "resetting": "Reimpostazione…", - "reverting": "Ripristino…", "trimming": "Ritaglio…", "splitting": "Divisione…", + "stalled": "Bloccato", "deleting": "Eliminazione…", "imported": "Importato", "preparing": "Preparazione…", - "unlabelling": "Rimozione etichetta…" + "unlabelling": "Rimozione etichetta…", + "working": "Elaborazione…", + "match_uncertain": "Incerto: {top} o {runner}", + "match_uncertain_single": "Incerto: forse {top}", + "match_sure": "~{pct}% di certezza", + "match_uncertain_tip": "WashData non ha ancora scelto un programma. Il valore indica quanto spesso l'ipotesi in testa, {top}, era giusta nei cicli passati quando era così avanti. Solo informativo." }, "tab": { "advanced": "Avanzato", @@ -1772,6 +1693,11 @@ "wrong_profile": "Profilo sbagliato" }, "toast": { + "task_name_required": "Dai un nome all'attività", + "task_added": "Attività aggiunta", + "task_save_failed": "Impossibile salvare l'attività: {error}", + "task_removed": "Attività rimossa", + "maint_logged": "Segnato come fatto: {task}", "catalog_refreshed": "Catalogo della community aggiornato", "access_saved": "Controllo accessi salvato", "all_wiped": "Tutti i dati cancellati", @@ -1779,7 +1705,6 @@ "auto_label_complete": "Etichetta automatica completata", "automation_deleted": "Automazione eliminata", "correction_submitted": "Correzione inviata", - "cycle_deleted": "Ciclo cancellato", "cycle_force_stopped": "Ciclo con arresto forzato", "cycle_labelled": "Ciclo etichettato", "cycle_paused": "Ciclo in pausa", @@ -1795,11 +1720,9 @@ "group_deleted": "Gruppo eliminato", "group_name_required": "Il nome del gruppo è obbligatorio", "group_saved": "Gruppo salvato", - "import_successful": "Importazione riuscita; ricarica dell'integrazione", "json_required": "I dati JSON sono obbligatori", "legacy_removed": "Azioni legacy rimosse", "logs_exported": "Registri esportati", - "matching_reverted": "I pesi corrispondenti sono stati ripristinati ai valori predefiniti", "min_2_profiles": "Seleziona almeno 2 profili per un gruppo", "ml_training_no_improvement": "Formazione completata: linea di base mantenuta (nessun miglioramento)", "models_reverted": "I modelli sul dispositivo sono tornati ai valori di base", @@ -1809,8 +1732,8 @@ "pg_preset_loaded": "Preimpostazione \"{name}\" caricata", "pg_preset_saved": "Preimpostazione \"{name}\" salvata", "pg_sugg_loaded": "{n} valore/i suggerito/i caricato/i - avvia il Playground per confrontare", - "pg_sugg_ml_loaded": "{n} valore/i calibrato/i ML caricato/i - avvia il Playground per confrontare", "phase_name_required": "Il nome della fase è obbligatorio", + "phase_reset": "Fase \"{name}\" ripristinata alla versione integrata", "phase_updated": "Fase aggiornata", "phases_saved": "Fasi salvate", "preferences_saved": "Preferenze salvate", @@ -1857,15 +1780,11 @@ "store_name_required": "Inserisci un nome di profilo", "store_import_failed": "Importazione non riuscita: {error}", "store_imported": "Importato in {profile}", - "store_program_required": "Inserisci un nome di programma", "store_no_appliance": "Imposta prima la marca e il modello del tuo elettrodomestico nelle Impostazioni.", "store_share_failed": "Condivisione non riuscita: {error}", "store_shared": "Condiviso nello store della community, in attesa di revisione.", "appliance_added": "Elettrodomestico aggiunto, in attesa di approvazione", "contrib_device_changed": "Aggiunto allo store della community, ma il dispositivo selezionato è cambiato mentre il modulo era aperto. Selezionalo nelle Impostazioni.", - "thanks_confirming": "Grazie per la conferma", - "device_approved": "Approvato dalla community", - "rating_saved": "Valutazione della qualità salvata", "brand_added": "Marca aggiunta, in attesa di approvazione", "profile_added": "Profilo aggiunto, in attesa di approvazione", "saved_except_conflicts": "Salvato. Correggi i conflitti evidenziati per salvare il resto.", @@ -1888,11 +1807,52 @@ "odometer_saved": "Totale dei cicli salvato", "odometer_save_failed": "Impossibile salvare il totale: {error}", "store_pick_profile_share": "Scegli un profilo in cui condividere", - "remove_failed": "Rimozione non riuscita: {error}" + "remove_failed": "Rimozione non riuscita: {error}", + "analysis_complete": "Analisi completata: {count} suggerimento/i", + "analysis_failed": "Analisi non riuscita: {error}", + "auto_detect_failed": "Rilevamento automatico non riuscito: {error}", + "auto_label_failed": "Etichettatura automatica non riuscita: {error}", + "could_not_load_cycle": "Impossibile caricare il ciclo: {error}", + "create_failed": "Creazione non riuscita: {error}", + "cycles_deleted": "Cicli eliminati: {count}", + "debug_cleared": "Tracce di debug cancellate: {count}", + "discard_failed": "Impossibile scartare: {error}", + "error": "Errore: {error}", + "export_failed": "Esportazione non riuscita: {error}", + "import_failed": "Importazione non riuscita: {error}", + "invalid_json": "\"{key}\" non è un JSON valido: correggerlo o svuotare il campo prima di salvare.", + "label_failed": "Etichettatura non riuscita: {error}", + "ml_training_promoted": "Addestramento completato, modelli promossi: {count}", + "pause_failed": "Pausa non riuscita: {error}", + "phase_created": "Fase \"{name}\" creata", + "phase_deleted": "Fase \"{name}\" eliminata", + "profile_created": "Profilo \"{name}\" creato", + "rename_failed": "Rinomina non riuscita: {error}", + "save_failed": "Salvataggio non riuscito: {error}", + "split_complete": "Diviso in {count} cicli", + "start_failed": "Avvio non riuscito: {error}", + "stop_failed": "Arresto non riuscito: {error}", + "update_failed": "Aggiornamento non riuscito: {error}", + "store_download_skipped": "{n} registrazione/i saltata/e: troppo brevi, troppo lacunose o già sul tuo dispositivo.", + "store_import_low_quality": "Questa registrazione è troppo breve, troppo lacunosa o non plausibile per essere importata.", + "store_import_duplicate": "Questa registrazione è già sul tuo dispositivo.", + "device_approved": "Approvato dalla community", + "thanks_confirming": "Grazie per la conferma", + "rating_saved": "Valutazione della qualità salvata", + "store_download_partial": "Impossibile scaricare {n} programma/i; riprova più tardi.", + "import_no_restore_point": "Importato, ma non è stato possibile salvare un punto di ripristino, quindi questa importazione non può essere annullata", + "import_undo_failed": "Impossibile annullare l'importazione: {error}", + "import_undone": "Importazione annullata: ripristinati {profiles} profilo/i e {cycles} ciclo/i", + "auto_label_cancelled": "Etichettatura automatica annullata dopo aver etichettato {count} cicli.", + "auto_label_cancelled_one": "Etichettatura automatica annullata dopo aver etichettato {count} ciclo.", + "auto_label_cancelled_many": "Etichettatura automatica annullata dopo aver etichettato {count} cicli.", + "auto_label_cancelled_other": "Etichettatura automatica annullata dopo aver etichettato {count} cicli.", + "auto_label_done": "Etichettatura automatica completata: {count} cicli etichettati, {skipped} senza etichetta.", + "auto_label_done_one": "Etichettatura automatica completata: {count} ciclo etichettato, {skipped} senza etichetta.", + "auto_label_done_many": "Etichettatura automatica completata: {count} cicli etichettati, {skipped} senza etichetta.", + "auto_label_done_other": "Etichettatura automatica completata: {count} cicli etichettati, {skipped} senza etichetta." }, "suggestion": { - "both_agree": "WashData consiglia", - "calibrated_label": "Calibrato", "observed_label": "Osservato", "impact": { "auto_label_confidence": { @@ -1907,10 +1867,6 @@ "higher": "Chiude solo i cicli che hanno consumato energia sostanziale", "lower": "Chiude anche cicli brevi o a basso consumo energetico" }, - "end_repeat_count": { - "higher": "Richiede che il segnale di fine si ripeta più volte – più robusto, leggermente più lento", - "lower": "Termina dopo meno ripetizioni – rilevamento più rapido, maggior rischio di fine falsa" - }, "learning_confidence": { "higher": "Impara solo da corrispondenze molto affidabili – più lento ma più preciso", "lower": "Impara da più cicli; il modello potrebbe essere meno preciso" @@ -1949,40 +1905,25 @@ } }, "reason": { - "auto_label_confidence": "Confidenza al 15° percentile di {cycles} etichettature automatiche mai corrette dall'utente ({p15}).", "completion_min_seconds": "Metà della durata p05 dei cicli puliti ({minutes} min) su {cycles} cicli; filtra i cicli fantasma.{excl}", - "dead_zone_batch": "Mantenuto breve (finestra mediana di instabilità all'avvio su {cycles} cicli = {s}s) in modo da rilevare tempestivamente una fine reale.", - "dead_zone_from_dip": "In base a un calo di potenza precoce rilevato a {s}s.", - "end_energy_default": "Valore di riferimento consigliato per impostazione predefinita per la soglia di rumore a fine ciclo.", - "end_energy_false": "Energia p95 delle false fini ({p95}Wh) su {cycles} cicli, con un minimo dello 0.2% dell'energia mediana del ciclo ({floor}Wh).", - "end_energy_no_false": "Nessuna falsa fine su {cycles} cicli; con un minimo dello 0.2% dell'energia mediana del ciclo ({floor}Wh).", - "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.", + "match_interval_floored": "Il programma più breve ({shortest}s) limiterebbe questo valore a {cap}s, sotto il minimo di {minimum}s, quindi si usa il minimo: {persistence} corrispondenze consecutive richiedono {budget}s, oltre il {pct}% di quel programma. La corrispondenza gira solo quando arriva una lettura (mediana={median}s), quindi non costa nulla.", "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.", "min_off_gap": "In base a {gaps} intervalli tra cicli osservati (p05={p05}s). Minimo del dispositivo: {floor}s.", "min_off_gap_bridge": "Dimensionato per coprire il periodo di silenzio più lungo all'interno di un ciclo: {span}s + 60s di margine, da {spans} intervalli coperti su {cycles} cicli puliti (resta sotto il tuo intervallo minimo di {ceiling}s tra due carichi consecutivi).", "min_power": "40% della potenza attiva più bassa al p05 ({p05}W) su {cycles} cicli puliti, mantenendo la soglia di spegnimento sotto il consumo reale.{excl}", - "ml_auto_label": "Confidenza più bassa che il modello di qualità ha comunque valutato come pulita (p10 di {cycles} cicli puliti = {p10}).", - "ml_end_repeat": "{false}/{total} cicli ({pct}%) hanno avuto una pausa che il rilevatore di fine ha valutato >50%; richiedere {val} conferma/e di fine.", - "ml_off_delay": "Pause confermate dal rilevatore di fine: p95 {p95}s + 60s di margine, da {pauses} pause verificate dal modello su {cycles} cicli (minimo {floor}s).", "no_update_timeout": "In base alla cadenza di aggiornamento osservata (p95={p95}s) * 20 (min 60s).", "off_delay_cadence": "In base alla cadenza di aggiornamento osservata (p95={p95}s) * 5", "off_delay_device_floor": "Utilizzato il minimo sicuro specifico del dispositivo per {device} ({floor}s).", "off_delay_generic_floor": "Utilizzato il minimo sicuro generico ({floor}s).", "off_delay_pauses": "Dimensionato per superare le pause reali: pausa intraciclo p95 {p95}s + 60s di margine, da {pauses} pause su {cycles} cicli puliti (minimo {floor}s).", - "profile_match_threshold": "Confidenza p10 di {cycles} etichettature automatiche corrette ({p10}); soglia minima sicura per la conferma in tempo reale.", - "sampling_interval": "Intervallo di aggiornamento mediano osservato su {cycles} cicli puliti ({si}s).{excl}", - "smoothing_window": "Dimensionato per lisciare ~30s di letture con campionamento a {si}s ({samples} campioni).", - "start_duration": "Mantenuto breve (~un intervallo di campionamento di {si}s) affinché il rilevamento inizi il prima possibile ignorando comunque i picchi di un solo campione.", - "thr_batch": "Mantenuto appena sopra la potenza attiva più bassa al p05 su {cycles} cicli ({p05}W) affinché un avvio venga rilevato il prima possibile e la soglia di arresto resti sotto la potenza di funzionamento più bassa della macchina.", - "tol_per_profile": "p75 della varianza di durata per profilo su {profiles} profili ({cycles} cicli); i profili stretti non vengono penalizzati.", - "tol_pooled": "In base alla varianza di durata aggregata di {cycles} cicli etichettati di recente (deviazione p95={dev}).", - "watchdog": "Mantenuto il più basso possibile in sicurezza (poco sopra l'intervallo di aggiornamento p95 di {p95}s e almeno 2x l'intervallo di campionamento di {median}s, min 30s) per rilevare rapidamente i blocchi senza arresti falsi." + "standby_floor": "Ogni ciclo recente consumava ancora {idle}W alla fine, quindi una soglia inferiore a questo valore non potrà mai rilevare lo spegnimento dell'elettrodomestico. Mantenuta sopra; i {cycles} cicli puliti registrati non sono mai scesi al di sotto per più di {pause}s.", + "thr_batch": "Appena sopra la potenza attiva più bassa (p05) su {cycles} cicli ({p05}W), così gli avvii vengono rilevati presto e la soglia di arresto resta sotto la potenza di funzionamento più bassa.", + "watchdog": "Mantenuto il più basso possibile in sicurezza (poco sopra l'intervallo di aggiornamento p95 di {p95}s e almeno 2x l'intervallo di campionamento di {median}s, min 30s) per rilevare rapidamente i blocchi senza arresti falsi.", + "end_energy_floor": "Portato al minimo implicato dalla soglia di arresto ({stop} W) nell'arco del ritardo spegnimento ({off_delay} s); al di sotto, il ciclo può terminare solo tramite un percorso di riserva.", + "max_duration_ratio_default": "Ripristinato il valore predefinito: un limite superiore più basso scarta il programma giusto ogni volta che un ciclo dura più del solito.", + "min_duration_ratio_default": "Ripristinato il valore predefinito: un limite inferiore più alto impedisce di riconoscere un ciclo in corso finché non è ben avviato." }, "exclusions": { "summary": "Esclusi {total} cicli rilevati male: {parts}.", @@ -2000,13 +1941,18 @@ "no_power": "senza potenza", "no_active_power": "senza potenza attiva" } - } + }, + "now_value": "(ora {v})" }, "trend": { "down": "Tendenza al ribasso", "up": "Tendenza in alto" }, "maint": { + "salt": "Ricarica sale", + "rinse_aid": "Ricarica brillantante", + "lint_filter": "Pulisci filtro lanugine", + "condenser_clean": "Pulisci condensatore", "bearing_service": "Manutenzione cuscinetti", "descale": "Decalcifica", "drum_clean": "Pulisci cestello", @@ -2018,12 +1964,14 @@ "status_tip": "Esito del ciclo: Completato (fine naturale), Interrotto (calo improvviso di potenza), Arrestato forzatamente (manuale) o Necessita di revisione (feedback in sospeso).", "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. 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.", + "energy_tip": "Energia totale consumata (kWh), calcolata dalla potenza nel tempo.", + "cost_tip": "Costo energetico di questo ciclo, fissato alla fine. Con Costo ponderato nel tempo attivo, ogni momento viene addebitato al suo prezzo; altrimenti al prezzo della fine. Imposta un prezzo in Impostazioni per compilarlo.", "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.", - "cost_dynamic_tip": "Ponderato nel tempo: addebitato al prezzo in vigore durante il ciclo (prezzo effettivo {price} {cur}/kWh)." + "cost_dynamic_tip": "Ponderato nel tempo: addebitato al prezzo in vigore durante il ciclo (prezzo effettivo {price} {cur}/kWh).", + "dbg_conf": "Conf.", + "dbg_mae": "MAE", + "dbg_corr": "Corr." }, "stat": { "avg": "media", @@ -2047,7 +1995,10 @@ "notify_finish": "Verrebbe inviata una notifica di completamento.", "notify_milestone": "Verrebbe inviata una notifica di traguardo.", "notify_held": "Una notifica è stata trattenuta per le ore di silenzio.", - "finished": "Il ciclo ha raggiunto lo stato finale e si è concluso." + "finished": "Il ciclo ha raggiunto lo stato finale e si è concluso.", + "match_reverted": "Il programma è tornato in rilevamento: il suo punteggio è sceso molto al di sotto del proprio picco.", + "verified_pause": "L'allineamento in tempo reale ha confermato (o rilasciato) una pausa all'interno del programma. Finché resta valida, il ciclo non viene concluso.", + "stalled": "Il ciclo è rimasto al consumo di standby più a lungo di quanto questo programma faccia mai una pausa in quel punto, con lavoro ancora da fare. Mostrato come In pausa (Bloccato); il ciclo resta aperto." }, "store": { "search_ph2": "Cerca per marca o modello…", @@ -2056,11 +2007,7 @@ "programs_count": "Programmi: {n}", "browse": "Sfoglia", "device": "Dispositivo", - "favorites": "Preferiti", - "search_ph": "Cerca per marca o modello…", "no_results": "Nessun elettrodomestico corrispondente trovato. Prova un'altra ricerca.", - "n_programs": "{n} programmi", - "n_cycles": "{n} cicli", "no_programs": "Nessun programma condiviso per questo elettrodomestico per ora.", "no_cycles": "Nessun ciclo di riferimento condiviso per questo programma per ora.", "anon": "anonimo", @@ -2072,19 +2019,21 @@ "merge_target": "Unisci al profilo", "description": "Descrizione (facoltativa)", "contributed_by": "di {name}", + "website": "Sito dello store ↗", + "website_tip": "Apri il sito dello store della community", + "favorites": "Preferiti", "rating_summary": "★ {avg} ({n})", "no_ratings": "Ancora nessuna valutazione", - "website": "Sito dello store ↗", - "website_tip": "Apri il sito dello store della community" + "unreachable": "Impossibile raggiungere lo store della community. Controlla la connessione e riprova." }, "settings": { "gear": { - "title": "Impostazioni" + "title": "Preferenze" } }, "link": { - "manual": "Manuale ↗", - "add_appliance": "Aggiungi il tuo elettrodomestico" + "maintenance_supporter": "Maintenance Supporter su GitHub ↗", + "manual": "Manuale ↗" }, "tip": { "add_brand": "Marca non elencata? Aggiungila al catalogo della community", @@ -2118,7 +2067,13 @@ "rebuild": { "envelopes": "Ricostruzione degli inviluppi" }, - "cancelling": "Annullamento..." + "cancelling": "Annullamento...", + "auto_label": { + "running": "Etichettatura automatica dei cicli" + }, + "store_download": { + "running": "Scaricamento dallo store" + } }, "setup": { "hdr": { @@ -2127,26 +2082,24 @@ }, "phase0": { "washer": "WashData sta già rilevando i tuoi cicli. Registra il tuo primo ciclo per abilitare i nomi dei programmi e le stime dei tempi.", - "dishwasher": "WashData sta monitorando. Le lavastoviglie hanno cicli complessi – è vivamente consigliato registrare il primo ciclo. Se un ciclo rilevato dura troppo a lungo, usa l'editor dei cicli per ritagliarlo prima di salvarlo come profilo.", + "dishwasher": "WashData sta osservando. I cicli delle lavastoviglie sono complessi, quindi registra il tuo primo ciclo. Se un ciclo rilevato dura troppo, accorcialo nell'editor dei cicli prima di salvarlo come profilo.", "generic": "WashData sta monitorando. Registra o etichetta un ciclo rilevato per iniziare a creare profili." }, "phase1a": { - "labelled": "Buon inizio – il tuo primo programma è salvato. Per dati più precisi, considera di registrare il prossimo ciclo con il widget di registrazione." + "labelled": "Buon inizio: il tuo primo programma è salvato. Per dati più puliti, registra il prossimo ciclo con il registratore." }, "phase1b": { - "recorded": "La tua registrazione è stata salvata come {profile_name}. Ora registra o etichetta gli altri programmi che usi di solito per ampliare la copertura." + "recorded": "La tua registrazione è stata salvata come {profile_name}. Ora registra o etichetta gli altri programmi che usi spesso." }, "phase1c": { - "verify": "Hai {count} programmi dalla community. Esegui un ciclo per verificare che WashData li riconosca correttamente – l'abbinamento migliorerà man mano che il dispositivo accumula il proprio storico." + "verify": "Hai {count} programmi dalla community. Avvia un ciclo per verificare che WashData lo riconosca; la corrispondenza migliora man mano che il tuo dispositivo crea il proprio storico." }, "phase2": { - "cluster": "WashData ha rilevato {count} cicli che non corrispondono a nessun programma salvato – si assomigliano tra loro. Vuoi creare un nuovo profilo per essi?", + "cluster": "WashData ha visto {count} cicli simili che non corrispondono ad alcun programma salvato. Creare un nuovo profilo per loro?", "unmatched": "L'ultimo ciclo non ha corrisposto a nessun programma salvato. È un nuovo programma?" }, "phase3": { - "suggestions": "WashData ha raccomandazioni sulle impostazioni basate sulla cronologia dei cicli – revisionale per migliorare la precisione di rilevamento.", - "groups": "Alcuni dei tuoi profili sembrano lo stesso programma a temperature diverse. Organizzali in un gruppo per un abbinamento più affidabile.", - "phases": "Aggiungi le fasi del programma a {profile_name} per stime più accurate del tempo rimanente." + "suggestions": "WashData ha suggerimenti sulle impostazioni basati sullo storico dei tuoi cicli. Rivedili per migliorare il rilevamento." }, "phase4": { "healthy": "Questo dispositivo è completamente configurato ({profile_count} profili)." @@ -2159,8 +2112,6 @@ "create_from_cluster": "Crea profilo da questi cicli", "create_profile": "Crea un profilo per questo programma", "review_suggestions": "Rivedi i suggerimenti", - "organise_profiles": "Organizza i profili", - "configure_phases": "Configura le fasi", "skip_step": "Salta questo passaggio", "skip_forever": "Non mostrare più", "hide_guidance": "Nascondi guida", @@ -2171,6 +2122,7 @@ "min": "min", "entity_not_found": "Entità non trovata", "running_long": "In ritardo", + "stalled": "Bloccato", "remaining": "rimanente", "progress": "avanzamento", "show_time_remaining": "Mostra tempo rimanente", @@ -2219,5 +2171,53 @@ "off_delay": { "effective": "Un ciclo terminato attende in realtà {eff} s, non {delay} s: l'Intervallo minimo di spegnimento ({gap} s) è più lungo e ha la precedenza." } + }, + "access": { + "edit": "Modifica", + "full": "Completo", + "inherit": "Eredita", + "none": "Nessuno (nascosto)", + "read": "Lettura" + }, + "pg_label_reason": { + "no_winner": "Nessun programma corrisponde", + "below_floor": "Sotto la Confidenza apprendimento", + "ambiguous": "Abbinamento segnalato come ambiguo", + "margin": "Troppo vicino al secondo candidato", + "unknown_profile": "Il programma non esiste più", + "not_evaluated": "Non valutato" + }, + "diagram": { + "accum": "accum.", + "auto": "auto", + "below_off": "sotto = spento", + "confirmed": "confermato", + "energy_counts": "energia conteggiata", + "feedback": "revisione", + "gap_below_min": "intervallo sotto il min = un ciclo", + "heat_phase": "fase di riscaldamento", + "held_at_100": "fermo al 100%", + "ignored": "ignorato", + "kept": "mantenuto", + "max": "max", + "min": "min", + "minus_tol": "-tol", + "no_match": "non abbinato", + "no_updates": "nessun aggiornamento", + "off_delay_wait": "ritardo spegnimento", + "off_gap": "intervallo", + "plus_tol": "+tol", + "profile": "profilo", + "reading_interval": "intervallo tipico di lettura", + "sensor_offline": "sensore offline: arresto forzato", + "si": "IC", + "spike_ignored": "picco ignorato", + "spike_ignored_short": "picco: ignorato", + "start": "avvio", + "stop": "arresto", + "tail_energy_resets": "energia finale > soglia: timer azzerato", + "thr": "soglia", + "too_short": "troppo breve", + "tumble_kept": "rotazioni mantenute" } } diff --git a/custom_components/ha_washdata/translations/panel/ja.json b/custom_components/ha_washdata/translations/panel/ja.json index 512d05aa..9bb0df44 100644 --- a/custom_components/ha_washdata/translations/panel/ja.json +++ b/custom_components/ha_washdata/translations/panel/ja.json @@ -3,6 +3,7 @@ "min": "分", "entity_not_found": "エンティティが見つかりません", "running_long": "超過中", + "stalled": "停滞中", "remaining": "残り", "progress": "進捗", "show_time_remaining": "残り時間を表示", @@ -48,16 +49,15 @@ } }, "badge": { - "quiet_tail_tip": "このプログラムは終盤で約{mins}分間ほとんど電力を使わなくなり、その後、終了前に約{secs}秒間だけ約{watts} W を消費します。測定した{measured}サイクル中{seen}回で確認されました: 毎回そうなるわけではありません。", + "quiet_tail_tip": "このプログラムは終盤に約{mins}分間静かになり、その後約{secs}秒間、約{watts} W を消費します。測定した{measured}サイクル中{seen}回で確認されました。", "quiet_tail": "静かな終盤 約{mins}分", - "artifact_tip": "{n} 個の異常が検出されました (例: サイクルの途中でドアが開いた) – 開いてグラフ上で確認します", + "artifact_tip": "{n} 個の異常が検出されました (例: サイクルの途中でドアが開いた) - 開くとグラフ上で確認できます", + "artifact_tip_other": "{n} 個の異常が検出されました (例: サイクルの途中でドアが開いた) - 開くとグラフ上で確認できます", "auto": "(自動検出)", "built_in_tag": "内蔵", "declining": "↘ 悪化", "energy_low": "普段より消費エネルギーが少ない", "energy_spike": "普段より消費エネルギーが多い", - "fair_fit": "普通", - "fair_fit_tip": "中程度の一致一貫性 - このプロファイルに割り当てられた一部のサイクルの信頼スコアが低くなります。精度を向上させるには、さらに多くのサイクルにラベルを付けるか、プロファイルを再記録します。", "feedback_requested": "フィードバックが必要です", "golden_cycle": "記録された基準サイクル", "duration_outlier": "長さが異常なサイクル {n} 件", @@ -66,27 +66,34 @@ "needs_review": "見直しが必要", "overrun": "普段より長く動作した", "poor_fit": "低品質", - "poor_fit_tip": "マッチ履歴に一貫性がありません - このプロファイルの再構築を検討してください", - "restart_gap_tip": "{n} HA 再起動ギャップ: 電力トレースに欠損区間があります", + "restart_gap_tip": "HA 再起動ギャップ {n} 件: 電力トレースに欠損区間があります", + "restart_gap_tip_other": "HA 再起動ギャップ {n} 件: 電力トレースに欠損区間があります", "reviewed": "レビュー済み", "steady": "→安定", "underrun": "普段より早く終了した", - "using_defaults": "出荷時のデフォルトを使用する", - "using_tuned": "調整された重みの使用", "overrun_ratio": "(予想の{x}倍)", "underrun_ratio": "(予想の{pct}%)", - "awaiting_tip": "コミュニティの承認待ち", - "awaiting_n": "承認待ち · {n} 件確認済み", - "approved": "承認済み", - "awaiting": "承認待ち", "imported_tip": "コミュニティストアからインポートしました。マッチングにのみ使用され、統計には含まれません。", "unmatchable": "マッチング不可", "not_importable": "対象外", "exists": "既存", "backfilled_tip": "インポートした電力履歴から検出されました。プログラムのマッチングにのみ影響し、統計には含まれません。", - "armed": "(次のサイクルに適用)" + "armed": "(次のサイクルに適用)", + "awaiting_tip": "コミュニティの承認待ち", + "awaiting_n": "承認待ち · {n} 件確認済み", + "awaiting": "承認待ち", + "approved": "承認済み", + "matcher_unused": "まだあなたのサイクルにマッチしていません", + "matcher_used": "あなたのサイクル {n} 件にマッチ", + "matcher_used_other": "あなたのサイクル {n} 件にマッチ", + "matcher_used_tip": "この機器のサイクルのうち、WashData がこのインポート済みプログラムに自動でマッチさせたものです。一度もマッチしないインポートはお使いの機器に合わない可能性があるため、削除できます。カウントはローカルで行われ、ストアには何も送信されません。" }, "btn": { + "pg_preset_delete": "プリセットを削除", + "pg_preset_load": "プリセットを読み込む", + "pg_preset_load_tip": "プレイグラウンドの値をこのプリセットで置き換えます(有効な設定は変更されません)", + "pg_preset_save": "プリセットとして保存", + "pg_preset_save_tip": "以下のすべての値に名前を付けて、このデバイスのプリセットとして保存します", "reset_zoom": "ズームをリセット", "set_brand_model": "ブランドとモデルを設定", "refresh_catalog": "カタログを更新", @@ -97,12 +104,11 @@ "add_timer": "+ タイマーを追加", "apply_all": "全て適用", "apply_label": "ラベルを適用する", - "apply_set_b": "セット B を適用", "apply_split": "分割を適用する", "apply_trim": "トリムを適用", "auto_detect_split": "自動検出", "auto_label_cycles": "サイクルを自動ラベル付け", - "auto_label_cycles_tip": "一致信頼度がしきい値をクリアするラベルなしサイクルにプロファイル名を自動的に割り当てます", + "auto_label_cycles_tip": "マッチ信頼度がしきい値を超えたラベルなしサイクルに名前を付けます。", "cancel": "キャンセル", "clear_debug": "デバッグデータのクリア", "clear_splits": "クリア", @@ -112,8 +118,6 @@ "convert_to_automation": "自動化への変換", "correct": "修正…", "create": "作成する", - "create_from_cluster": "{count} サイクルからプロファイルを作成", - "create_profile": "+ プロファイルの作成", "delete": "削除", "delete_group": "グループの削除", "delete_group_tip": "このグループのみを削除します - メンバーのプロファイルは保持されます", @@ -124,22 +128,19 @@ "done": "完了", "edit": "編集", "export": "エクスポート", - "export_json": "JSON にエクスポート", "force_stop": "強制停止", "force_stop_tip": "現在のサイクルをすぐに終了し、強制停止としてマークします", "from_template": "テンプレートから▾", "ignore": "無視する", "import_json": "JSONからインポート", - "import_overwrite": "インポート(データを上書き)", "inspect": "検査する", "label": "ラベル付け", - "load": "読み込み", "load_more": "さらに読み込む", "manage": "管理", "merge": "マージ", "new_automation": "+ 新しい自動化", "new_group": "+ 新しいグループ", - "new_group_tip": "ほぼ同一のプロファイルをグループ化(同じ形状/期間、異なる温度または脱水)して、マッチングが確実にどれかを選択できるようにします", + "new_group_tip": "ほぼ同じプロファイル(形状が同じで温度や脱水が異なるもの)をグループ化し、マッチングで区別できるようにします。", "new_phase": "+ 新しいフェーズ", "new_profile": "+ 新しいプロファイル", "new_profile_tip": "既存のラベル付きサイクルまたは記録から新しいプログラムプロファイルを作成します", @@ -147,36 +148,25 @@ "on_cycle_started": "サイクル開始時", "pause_cycle": "一時停止", "pause_cycle_tip": "実行中のサイクルを一時停止します – 家電は停止した場所から再開します", - "pg_apply_device": "デバイスに適用", - "pg_autofill": "自動入力", - "pg_load_calibrated": "調整済み (ML) を読み込む ({n})", - "pg_load_calibrated_tip": "ML 調整済みの設定をプレイグラウンドの上書き値として適用します", "pg_load_live": "有効な設定を読み込む", "pg_load_live_tip": "統合の現在の設定を読み直し、プレイグラウンドでの編集をすべて破棄します", "pg_load_suggested": "提案を読み込む ({n})", "pg_load_suggested_tip": "自動調整ツールの現在の提案をプレイグラウンドの上書き値として適用します", - "pg_preset_delete": "プリセットを削除", - "pg_preset_load": "プリセットを読み込む", - "pg_preset_load_tip": "プレイグラウンドの値をこのプリセットで置き換えます(有効な設定は変更されません)", - "pg_preset_save": "プリセットとして保存", - "pg_preset_save_tip": "以下のすべての値に名前を付けて、このデバイスのプリセットとして保存します", "pg_publish_all": "{n} 件を統合に適用", "pg_publish_one": "{label} を統合に適用", - "play": "再生", "process": "処理", "process_history": "今すぐ処理", "process_recording": "記録の保存", "process_tip": "記録されたトレースを新しいまたは既存のプロファイルとして保存します", "rebuild": "エンベロープを再構築する", "rebuild_envelope": "エンベロープの再構築", - "rebuild_tip": "ラベル付きサイクルからすべてのプロファイルの予想電力エンベロープ(最小/最大バンド)を再計算します – 新しいサイクルにラベルを付けた後や古いラベルを修正した後に実行します", + "rebuild_tip": "ラベル付きサイクルから、すべてのプロファイルの予想電力バンドを再構築します。サイクルのラベル付けや修正の後に実行してください。", "rec_start_tip": "家電の電力トレースの記録を開始します – サイクルを実行する直前に開始します", "rec_stop": "停止", "rec_stop_tip": "記録を停止し、キャプチャされたトレースをレビューのために保持します", "record": "記録を開始する", "refresh": "リフレッシュ", "refresh_diag_tip": "ストレージ統計を再読み込みします", - "refresh_logs_tip": "最新のログレコードを再読み込みします", "refresh_settings_tip": "サーバーから設定を再読み込みします", "relabel": "再ラベル付け ({count})", "remove": "取り除く", @@ -184,7 +174,6 @@ "reset": "リセット", "reset_ml_models_tip": "微調整されたモデルを破棄し、組み込みモデルに戻ります。 WashData は後でそれらを再学習できます。", "reset_to_builtin": "内蔵モデルにリセット", - "reset_to_defaults": "デフォルトにリセットする", "resume_cycle": "再開する", "resume_cycle_tip": "一時停止されたサイクルを再開します", "revert_settings": "変更を破棄", @@ -197,8 +186,6 @@ "run_analysis": "🔍 提案分析を実行する", "run_analysis_tip": "記録されたサイクルを今すぐ分析し、調整の提案を更新します", "run_auto_label": "自動ラベルの実行", - "run_both": "A と B を実行", - "run_simulation": "シミュレーションを実行", "save": "保存", "save_access_control": "アクセス制御の保存", "save_group": "グループの保存", @@ -210,10 +197,8 @@ "save_settings": "設定の保存", "select": "選択", "select_all": "すべて選択", - "select_last20": "直近 20 件", "select_none": "クリア", "show_only": "表示のみ", - "skip_setup": "セットアップをスキップ", "split": "分割", "stop": "停止", "submit_correction": "修正の送信", @@ -222,36 +207,31 @@ "trim_split": "トリム/分割", "undo": "元に戻す", "unlabel_selected": "選択項目のラベルを解除({n})", - "unlabel_selected_tip": "選択したサイクルからプロファイルのラベルを解除します。サイクルは履歴に残り、後で再びラベル付けできます。このプロファイルを形づくらなくなるだけです。", + "unlabel_selected_tip": "選択したサイクルのラベルを解除します。サイクルは履歴に残りますが、このプロファイルの形成には使われなくなります。", "use": "使用", "wipe_all": "すべてのデータを消去", "delete_selected": "選択項目を削除({n})", - "group_suggest": "グループ {n}:{members}", "pg_apply_best": "最適値を適用", - "pg_apply_to_settings": "設定に保存", "pg_apply_to_settings_tip": "ここで編集した値をこのデバイスの現在有効な設定にコピーします", "import": "インポート", "disconnect": "切断", "connect_github": "GitHub に接続", - "save_appliance": "家電を保存", "share_to_store": "ストアに共有", "share": "共有", - "confirm_appliance": "この家電を確認", - "confirmed": "確認済み", "browse_community_setups": "コミュニティのセットアップを閲覧", "download_device": "このセットアップをダウンロード", "share_device": "⬆ このデバイスを共有", - "share_device_tip": "この機器と記録済みリファレンスサイクルをコミュニティストアに共有して、同じ機種を持つ他のユーザーが利用できるようにします", + "share_device_tip": "この機器とそのリファレンスサイクルをコミュニティストアに共有し、同じ機種の所有者が利用できるようにします。", "share_n": "{n}件のサイクルを共有", "export_selected": "エクスポート(データを選択)", "export_all": "すべてをクイックエクスポート", - "import_raw": "詳細設定:JSONからすべて置き換え", "download_export": "エクスポートをダウンロード", "analyze_import": "ファイルを解析", "import_selected": "選択項目をインポート", "back": "戻る", "mute_suggestion": "この設定の提案を停止", "reset_muted": "ミュートをリセット", + "reset_phase_tip": "編集を元に戻し、組み込みのフェーズを復元します", "import_power_history": "電力履歴をインポート", "hist_read_recorder": "Home Assistant から読み込む", "hist_scan": "サイクルを検索", @@ -260,7 +240,15 @@ "save_total_cycles": "累計を保存", "clear": "クリア", "add_brand": "ブランドを追加", - "add_appliance": "家電を追加" + "log_done_task": "完了を記録: {task}", + "add_task": "タスクを追加", + "add_appliance": "家電を追加", + "retry": "再試行", + "confirm_appliance": "この家電を確認", + "confirmed": "確認済み", + "create_profile_from_gap": "プロファイルを作成", + "undo_import": "前回のインポートを元に戻す", + "pg_rerun": "シミュレーションを更新" }, "note": { "off_delay": { @@ -268,10 +256,6 @@ } }, "conflict": { - "anti_wrinkle_exit": { - "exit": "停止しきい値 ({stop} W) を下回る必要があります。そうでないとしわ防止終了電力は無視されます", - "stop": "しわ防止終了電力 ({exit} W) を上回る必要があります" - }, "anti_wrinkle_max": { "max": "開始しきい値 ({start} W) を上回る必要があります。そうでないとしわ防止の時間制限がバイパスされます", "start": "しわ防止最大電力 ({max} W) を下回る必要があります" @@ -309,14 +293,14 @@ "threshold": "有効にするには停止しきい値 ({stop} W) を下回る必要があります" }, "price_entity": { - "power_sensor": "これはこのデバイスの電力センサーであり、価格ではありません。価格エンティティは下の静的エネルギー価格より優先されるため、ワットの値が kWh あたりの単価として扱われ、サイクルの料金が計算されてしまいます。このフィールドを空にしてください。", - "energy_sensor": "これはこのデバイスのエネルギーメーターであり、価格ではありません。価格エンティティは下の静的エネルギー価格より優先されるため、メーターの読み取り値が kWh あたりの単価として扱われ、サイクルの料金が計算されてしまいます。このフィールドを空にしてください。", - "device_class": "このセンサーが測定しているのは {dc} であり、kWh あたりの価格ではありません。価格エンティティは下の静的エネルギー価格より優先されます。このフィールドを空にするか、電力料金単価のセンサーを指定してください。", - "unit": "このセンサーの読み取り単位は {unit} で、kWh あたりの価格ではありません。価格エンティティは下の静的エネルギー価格より優先されます。このフィールドを空にするか、電力料金単価のセンサーを指定してください。" + "power_sensor": "これはこのデバイスの電力センサーで、価格ではありません。ワット値が kWh あたりの単価として計算されてしまうため、このフィールドを空にしてください。", + "energy_sensor": "これはこのデバイスのエネルギーメーターで、価格ではありません。メーターの読み取り値が kWh あたりの単価として計算されてしまうため、このフィールドを空にしてください。", + "device_class": "このセンサーが測定しているのは {dc} で、kWh あたりの価格ではありません。このフィールドを空にするか、電力料金のセンサーを選んでください。", + "unit": "このセンサーの単位は {unit} で、kWh あたりの価格ではありません。このフィールドを空にするか、電力料金のセンサーを選んでください。" }, "pump_stuck": { - "duration": "ウォッチドッグがサイクルを強制終了する前にスタックアラームが発動するよう、更新なしタイムアウト ({to} 秒) より小さい必要があります", - "timeout": "サイクルが強制停止される前にスタックアラームが発動するよう、ポンプ停止継続時間 ({ps} 秒) を超える必要があります" + "duration": "スタックアラームが先に発動できるよう、更新なしタイムアウト ({to} 秒) より小さくする必要があります", + "timeout": "スタックアラームが先に発動できるよう、ポンプ停止継続時間 ({ps} 秒) を超える必要があります" }, "start_dur": { "sampling": "サンプリング間隔が開始継続時間 ({sdt} 秒) を超えています。単一サンプルのスパイクがサイクルを開始する可能性があります", @@ -334,10 +318,15 @@ "sampling": "サンプリング間隔はウォッチドッグ間隔 ({wi} 秒) の半分以下である必要があります" }, "settings_banner": "設定の競合: {n}。強調表示されたセクションを確認し、保存前に修正してください。", - "settings_banner_btn": "最初へ" + "settings_banner_btn": "最初へ", + "end_energy": { + "energy": "停止しきい値 ({w} W) とオフ遅延 ({d} s) に対して厳しすぎます。サイクルはフォールバック経路でしか終了できません", + "stop": "終了エネルギー ({d} s で {e} Wh) では {p} W までしか許容されません" + } }, "hdr": { - "ab_comparison": "A/B 設定の比較", + "custom_tasks": "独自のタスク", + "maintenance_supporter": "もっと機能が必要ですか?", "access_control": "アクセス制御", "add_maintenance": "メンテナンスイベントの追加", "automation_delete": "このオートメーションを削除します", @@ -346,28 +335,22 @@ "automation_review_this_cycle": "このサイクルを見直す", "automations": "自動化", "clear_debug": "デバッグトレースをクリアする", - "cycle_simulator": "サイクルシミュレーター", "diagnostics_pref": "診断", "display": "表示", - "dtw_inspector": "DTW インスペクター", "export_import": "エクスポート/インポート", - "getting_started": "はじめに", "live_power": "ライブ電力", "logs": "ログ", - "logs_diagnostics": "診断", "maintenance": "メンテナンスアクション", "maintenance_log": "メンテナンス履歴", "maintenance_reminders": "サービスリマインダー", "manual_recording": "手動記録", - "manual_recording_tip": "WashData が電力トレースを記録する間、意図的にサイクルを実行します。家電が起動する直前に開始し、終了したら停止し、次に処理して名前付きプロファイルとして保存します。", + "manual_recording_tip": "サイクルを意図的に記録します。家電が動き出す直前に「開始」を、終了したら「停止」を押し、「処理」でプロファイルとして保存します。", "ml_learned": "WashData が学んだこと", - "ml_matching_tuning": "プログラムマッチングの微調整", "ml_settings_card": "設定", "ml_smart_learning": "スマートラーニング", "my_preferences": "個人設定", "no_automations": "このデバイスを参照するオートメーションはまだありません。", "panel_settings": "パネル設定", - "param_sweep": "パラメータースイープ", "pg_settings_source": "設定の取得元", "phase_catalog": "フェーズカタログ", "playground": "プレイグラウンド", @@ -376,11 +359,8 @@ "status": "状態", "status_graph": "ステータスグラフ", "storage_stats": "ストレージ統計", - "test_history": "履歴でテスト", "toggle_sidebar": "ホームアシスタントサイドバーの切り替え", - "tools_and_data": "ツールとデータ", "wipe_history": "履歴を消去", - "recommendations": "推奨事項({n})", "pg_detection_params": "検出設定", "pg_outcome": "シミュレーション結果", "pg_across_cycles": "全サイクルにわたって", @@ -388,7 +368,8 @@ "online_account": "コミュニティストアとオンライン機能", "import_power_history": "電力履歴のインポート", "service_status": "メンテナンス状況", - "online_account_tab": "オンラインとコミュニティ" + "online_account_tab": "オンラインとコミュニティ", + "live_match_debug": "ライブマッチのデバッグ" }, "health": { "fair": "プロファイル品質: 普通", @@ -396,11 +377,16 @@ "poor": "⚠ プロファイル品質: 低" }, "lbl": { - "envelope_position_tip": "マッチしたプログラム自身の記録された曲線上の位置、{pct}%。時間を数えるのではなく、このサイクルをプロファイルに重ね合わせて測定するため、いつもより長い運転でも短い運転でも正確なままです。機器が静かなときに更新されるため、稼働中のフェーズでは遅れることがあります。", + "envelope_position_tip": "マッチしたプログラムの記録曲線上の位置: {pct}%。サイクルをプロファイルに重ね合わせて求めるため、運転が長くても短くても正確です。機器が静かなときに更新されるため、遅れることがあります。", "envelope_position": "曲線 {pct}%", "aria_playground_chart3": "インタラクティブなサイクル電力グラフ: ホバーで時刻/電力を表示、スクロールまたはピンチでズーム、ドラッグで移動", "device_unresolved": "利用不可のデバイス ({id})", "drag_to_resize": "ドラッグしてサイズ変更", + "task_name": "タスク", + "every_cycles": "間隔 (サイクル)", + "every_days": "間隔 (日)", + "new_task_name": "新しいタスク", + "days_since_service": "{since} / {total} 日", "actions": "アクション", "activity": "活動", "administrators": "管理者", @@ -408,10 +394,8 @@ "aria_cycle_chart": "サイクル電力推移", "aria_envelope_chart": "プロファイルの電力エンベロープグラフ", "aria_phase_chart": "フェーズエディターグラフ", - "aria_playground_chart": "ドラッグ可能な検出しきい値付きのサイクル電力推移", "aria_power_chart": "消費電力グラフ", "aria_spaghetti_chart": "重ね合わせたサイクル電力推移", - "aria_sweep_chart": "パラメータースイープ結果グラフ", "artifact_anomaly": "異常", "artifact_high_power": "高出力", "artifact_interruption": "中断", @@ -425,150 +409,99 @@ "cohesion_good": "凝集度 {pct}%", "cohesion_low": "⚠ 低凝集度 {pct}%", "compare_profiles": "プロファイルと比較する", - "concurrency": "並列実行数", "confidence": "信頼度", "confidence_threshold": "信頼閾値", "correct_duration": "正しい継続時間 (分、オプション)", "correct_profile": "正しいプロファイル", - "correlation": "相関", "cost": "料金", "create_new_profile": "+ 新しいプロファイルを作成…", "current_phase": "現在のフェーズ", - "curves": "曲線", "cycle": "サイクル", "cycle_date_display": "サイクル日付表示", - "cycle_health": "サイクルの健康", - "cycle_index": "サイクル番号 →", - "cycles": "サイクル", "cycles_count": "サイクル", "cycles_lc": "サイクル", - "dashed": "破線", "date": "日付", "debug_traces": "デバッグトレース", - "default": "デフォルト", "default_access_level": "以下にリストされていないユーザーのデフォルト レベル", "default_other": "デフォルト(他のデバイス)", "default_tab": "パネルを開いたときのデフォルトのタブ", - "delta": "Δ", "description": "説明", - "dtw_blend_w": "ブレンド重み", - "dtw_blended": "ブレンドスコア", - "dtw_ddtw": "DDTW スコア", - "dtw_l1": "L1 スコア", - "dtw_matched_profile": "(サイクルの一致プロファイル)", "duration": "継続時間", - "duration_agreement": "継続時間の一致度", - "emphasis": "強調", "enable_access_control": "ユーザーごとのアクセス制御を有効にする", "energy": "エネルギー", - "energy_agreement": "エネルギーの一致度", - "event_timeline": "イベントタイムライン", "event_type": "イベントの種類", "expected": "予想", + "suspected_profile": "{name} (推定)", + "runner_up_profile": "{name} (次点)", "expected_duration": "予想される所要時間 (分)", "file_kb": "ファイル (kB)", - "final_score": "最終スコア", "from": "開始", "gap_s": "ギャップ (秒)", "group_name": "グループ名", "head_trim": "先頭のトリム (秒)", - "health": "健康", "hide_tabs": "管理者以外のタブを非表示にする", - "in_use": "使用中", "json_data": "JSONデータ", "label": "一致したプログラム", "last": "直近", "last_checked": "最終確認:", "load_from_file": "ファイルからロード", - "mae_score": "MAE スコア", "manual_duration": "手動持続時間 (分、オプション)", "match": "マッチ", "members": "メンバー", - "metric": "指標", "mode_existing_profile": "既存のプロファイルに追加", "mode_new_profile": "新しいプロファイルの作成", "models_fine_tuned": "(微調整済みモデル: {count})", - "n_classic_suggestions": "{n} クラシック", - "n_ml_suggestions": "{n} ML", "n_selected": "{n} 件選択中", "n_tuning_suggestions": "{n} 件のチューニング提案", - "na": "該当なし", + "n_tuning_suggestions_other": "{n} 件のチューニング提案", "new_profile_name": "新しいプロファイル名", "notes": "メモ", - "of": "/", - "outcome": "結果", "panel_default_tab": "デフォルトのタブ", "panel_language": "パネル言語", "per_cycle_short": "サイクル", "pg_anti_wrinkle": "しわ防止", - "pg_autofill_tip": "現在の値から範囲を自動入力({lo}–{hi})", "pg_best_value": "見つかった最適値", "pg_current_value": "現在の値:", "pg_drag_hint": "グラフ上の線をドラッグ", - "pg_dtw": "DTW", "pg_ev_ending": "終了中", "pg_ev_running": "実行中", "pg_group_advanced": "エッジケース", "pg_group_detection": "検出トリガー", "pg_group_timing": "タイミングルール", "pg_idle": "アイドル", - "pg_last_run": "前回の実行: {date}", - "pg_match_score": "マッチスコア", - "pg_pct_of": "{total} 中 {p}%", "pg_poor_match": "不十分なマッチ", "pg_preset": "プレイグラウンドのプリセット", "pg_preset_name": "新しいプリセット名", "pg_preset_none": "プリセットを選択…", "pg_strong_match": "強いマッチ", - "pg_tile_ambiguous": "あいまい", - "pg_tile_ambiguous_tip": "2つのプログラムがほぼ同点だったサイクル", - "pg_tile_detected": "検出", - "pg_tile_detected_tip": "電力が開始しきい値を十分な時間超えて、実行中として登録されたサイクル", - "pg_tile_matched": "マッチ", - "pg_tile_matched_tip": "既知のプログラムに正しく割り当てられたサイクル", - "pg_time_left": "残り時間", "pg_weak_match": "弱いマッチ", "phase_name": "フェーズ名", "name": "名前", "phase_timeline": "フェーズタイムライン", "power": "電力", "profile": "プロファイル", - "profile_index": "プロファイル番号 →", "profile_name": "プロファイル名", "program": "プログラム", - "program_tip": "現在のサイクルに一致するプロファイルを上書きします。自動検出では、統合が最適な一致を自動的に選択します。自動検出が誤っている場合や実行中のプログラムが分かっている場合は、特定のプログラムを固定して強制的に一致させます。家電を運転する前に選んでおくと、次のサイクルが始まった時点で適用されます。", + "program_tip": "現在のサイクルをどのプログラムにマッチさせるかを選びます。自動検出では最適なマッチが選ばれます。自動検出が間違っているときはプログラムを固定してください。運転前に選ぶと次のサイクルに適用されます。", "progress": "進捗", "raw_socket": "生のソケット", - "read_only": "読み取り専用", "ref_cycle": "基準サイクル (オプション)", - "refresh_interval": "リフレッシュ間隔 (秒)", "remaining": "残り", "reminder_every": "リマインド間隔 (サイクル)", "remove_label": "- ラベルを削除 -", "rename_profile": "プロファイル名の変更", - "replay_duration": "再生", "resulting_profile": "結果として得られるプロファイル", "save_mode": "保存モード", - "score": "スコア", - "score_breakdown": "スコアの内訳", "seconds_from_start": "開始からの秒数", "select_profile": "プロファイルの選択", "setting": "設定", - "settings_a": "設定 A (現在)", - "settings_a_short": "A", "settings_advanced": "高度な", - "settings_b": "設定 B (テスト)", - "settings_b_short": "B", "settings_basic": "基本", "settings_detail_level": "設定の詳細レベル", - "settings_overrides": "設定の上書き", "show_debug": "ステータス ページにライブ マッチ デバッグ カードを表示します (信頼性、曖昧さ、上位候補)", "show_expected": "予想される曲線オーバーレイを表示 (一致したプロファイル、オレンジ色)", "show_raw": "ライブ電力グラフに生ソケット切替を表示", "sparkline": "最近のサイクル継続時間の傾向", - "stage2": "ステージ 2 – コア類似度", - "stage3": "ステージ 3 – DTW", - "stage4": "ステージ 4 – 一致度", "status": "状態", "tail_trim": "末尾のトリム (秒)", "tap_to_dismiss": "グラフをタップして閉じる", @@ -579,7 +512,6 @@ "ungrouped": "グループ化されていない", "unlabelled": "ラベルなし", "unlabelled_paren": "(ラベルなし)", - "warp_path": "ワープパス", "input": "入力:", "start": "開始", "end": "終了", @@ -604,9 +536,8 @@ "n_shown": "、{n} 件表示", "cycles_title": "サイクル({n})", "pg_detecting": "検出中", - "pg_best_marker": "ベスト:{v}", "pg_time_left_model": "残り時間(モデル)", - "pg_time_left_model_tip": "モデルが推定する残り時間(フェーズ推定+MLブレンド)。固定のカウントダウンではありません。", + "pg_time_left_model_tip": "プログラムのフェーズから推定。固定のカウントダウンではありません。", "phase": "フェーズ", "pg_mode_history": "履歴テスト", "pg_ev_detected": "検出", @@ -638,12 +569,6 @@ "pg_sweep_param": "パラメーター", "pg_objective": "目的", "pg_steps": "ステップ数", - "pg_stress_group": "待機終了テスト", - "pg_stress_idle_auto": "Auto", - "pg_stress_idle_w": "待機レベル (W)", - "pg_stress_idle_w_desc": "自動検出された待機電力下限を上書きします(自動の場合は空欄)。", - "pg_stress_toggle": "待機終了をテスト", - "pg_stress_toggle_desc": "記録終了後に待機電力レベルで稼働し続ける機器をシミュレートします - WashData がサイクルを停止するかどうか、およびいつ停止するかを確認します。", "pg_obj_match": "マッチ精度", "pg_obj_endtiming": "終了タイミング精度", "pg_obj_falseend": "誤終了率", @@ -652,12 +577,8 @@ "pg_mode_optimize": "最適化", "pg_group_matching": "プログラムマッチング", "pg_zoom_hint": "ダブルクリックでズームをリセット", - "aria_playground_chart2": "インタラクティブなサイクル電力グラフ: ホバーで時刻/電力を表示、スクロールでズーム、ドラッグで移動", "pg_alert_timeout_end": "予測ではなくタイムアウトで終了しました", "pg_alert_indefinite": "いつまでも終了しません", - "pg_alert_stress_cap": "安全制限に達しました", - "pg_alert_stress_ok": "待機終了: サイクルが停止しました", - "pg_alert_stress_warn": "待機電力が停止しきい値を超えています", "task_pg_history": "履歴でテスト", "task_pg_sweep": "最適化", "task_reprocess": "再処理", @@ -667,16 +588,9 @@ "pg_recent_runs": "最近の実行", "pg_load_run": "この実行を再読み込み", "pg_match_confidence": "マッチ信頼度", - "pg_envelope_fit": "エンベロープ適合度", - "pg_envelope_fit_tip": "このプロファイルの保存された電力エンベロープ内にサイクルがどれだけ収まっているか。マッチ信頼度とは異なる視点です。", "n_imported_note": "、{n} 件インポート済み", "enable_online": "オンライン機能を有効化", - "store_brand": "家電ブランド", - "store_model": "家電モデル", "anonymous": "匿名", - "rate_quality": "品質を評価", - "rate_n": "星{n}つ", - "approved_only": "承認済みのみ", "adopt_settings": "設定も引き継ぐ", "flags": "フラグ", "include_phase_map": "フェーズマップを含める", @@ -693,7 +607,6 @@ "cat_custom_phases": "カスタムフェーズ", "cat_profile_groups": "プロファイルグループ", "cat_settings": "検出と認識の設定", - "cat_matching_config": "認識の調整", "cat_ml_models": "MLモデル", "cat_feedback": "フィードバックとレビューラベル", "cat_suggestions": "提案", @@ -715,7 +628,6 @@ "hist_csv_data": "CSV データ", "hist_from_recorder": "または Home Assistant から読み込む", "hist_since": "開始日", - "days": "日", "hist_keep": "このサイクルを保持", "hist_looks_complete": "完全", "peak_power_short": "ピーク", @@ -735,7 +647,38 @@ "zoom_hint_touch_zoomed": "ピンチでズーム · ダブルタップでリセット", "cycles_since_service": "{since} / {total} サイクル", "total_cycles_run": "累計サイクル数", - "correct_total_cycles": "累計を修正" + "correct_total_cycles": "累計を修正", + "hist_reason_already_recorded": "WashData が既に記録したサイクルと重なっている", + "share_consent": "これらのサイクルが中断なく正常に完了したことを確認します", + "show_internal": "内部設定を表示", + "show_internal_tip": "WashData が自動で調整する設定(サンプリング、ウォッチドッグ、マッチ頻度、しきい値)。このデバイスに独自の値、競合、提案がある場合に表示されます。", + "rate_n": "星{n}つ", + "rate_quality": "品質を評価", + "pg_autolabel": "自動ラベル", + "pg_label_review": "見直しを依頼", + "pg_ev_match_reverted": "マッチ取り消し", + "pg_ev_verified_pause": "確認済み一時停止", + "n_samples": "{n} サンプル", + "n_samples_other": "{n} サンプル", + "n_cycles": "{n} サイクル", + "n_cycles_other": "{n} サイクル", + "energy_per_cycle": "{v}/サイクル", + "energy_total": "合計 {v}", + "automation_off": "(オフ)", + "ref_cycle_none": "なし", + "share_name": "共有したものに自分の名前を表示する", + "cat_backfill_cycles": "インポートした電力履歴から検出したサイクル", + "pg_compare_profile": "比較するプロファイル", + "pg_obj_early": "早期終了率", + "pg_obj_endlag": "終了遅延(中央値)", + "pg_obj_split": "分割率", + "pg_sweep_cycles": "最近のサイクル数", + "cycle_context": "サイクル前後のレコーダー履歴", + "cycle_context_off": "オフ", + "cycle_context_minutes": "{min}分", + "cycle_context_series": "レコーダー履歴", + "before_start": "開始前", + "after_end": "終了後" }, "log": { "all_levels": "すべてのレベル", @@ -745,11 +688,9 @@ }, "ml": { "builtin_models": "● 組み込みモデルの使用", - "fit_fair": "普通", "fit_good": "良い", "fit_slight": "わずかに", "fit_strong": "強い", - "fit_weak": "弱い", "fit_word": "フィット", "personalized": "● このマシンに合わせてカスタマイズ可能", "better_than_baseline": "ベースライン推定より {pct}% 良好({metric})", @@ -757,21 +698,27 @@ "trend_declining_tip": "このモデルのフィットは最近の再チェックで低下しました。さらにサイクルをレビューすると再学習に役立つ場合があります。", "trend_steady_tip": "このモデルのフィットは最近の再チェックでおおむね安定しています。", "cap_blurb": { - "end": "サイクルが本当に終了したタイミングを把握する", - "live_match": "実行中のプログラムをより早く識別する", - "quality": "誤検出や不完全なサイクルを見つける", - "remaining_time": "残り時間を予測する", "total_energy": "総消費エネルギーとコストを予測する" }, "cap_label": { - "end": "サイクル終了の検出", - "live_match": "プログラム照合", - "quality": "サイクル品質チェック", - "remaining_time": "残り時間の推定", "total_energy": "エネルギー推定" }, - "metric_auc": "検証データでの AUC {auc}", - "metric_mae": "誤差 {model} vs ベースライン {baseline}" + "metric_mae": "誤差 {model} vs ベースライン {baseline}", + "fine_tuned_at": "{when} に微調整", + "last_run_not_promoted": "前回の確認({when}): {reason}", + "never": "なし", + "skip_reason": { + "fit_failed": "学習に失敗しました: {error}", + "holdout_too_small": "新しいモデルを公正にテストするにはサイクルが少なすぎます。テスト用に確保できたのは {held_out} 件で、{min} 件必要です。", + "not_better_than_incumbent": "新しいモデルは使用中のモデルより優れていなかったため(誤差 {model} vs {incumbent})、現在のモデルを維持しました。", + "not_better_than_naive": "新しいモデルは単純な推定より明確に優れてはいませんでした(誤差 {model} vs {naive})。", + "unknown": "新たに学習した内容はありません。", + "insufficient_rows": "使用できるサイクルがまだ足りません。これまでのラベル付き・クリーンなサイクルは {count} 件です。", + "insufficient_rows_other": "使用できるサイクルがまだ足りません。これまでのラベル付き・クリーンなサイクルは {count} 件です。" + }, + "trained_unknown": "不明な日時", + "held_out_count": "検証用サイクル {count} 件で測定", + "held_out_count_other": "検証用サイクル {count} 件で測定" }, "modal": { "auto_label": "自動ラベル付けサイクル", @@ -780,7 +727,6 @@ "cycle": "サイクル", "edit_group": "プロファイルグループを編集", "edit_phase": "編集フェーズ", - "import_config": "設定のインポート", "label_cycle": "ラベルサイクル", "merge_cycles": "{n} サイクルを結合", "new_group": "新しいプロファイルグループ", @@ -795,6 +741,8 @@ "delete_automation_title": "オートメーションを削除", "delete_automation_msg": "オートメーション「{name}」を Home Assistant から削除しますか?この操作は元に戻せません。", "remove_legacy_title": "従来のアクションを削除", + "reset_phase_msg": "「{name}」への編集を元に戻し、組み込みのフェーズを復元しますか?プログラムの範囲はそのまま保持されます。", + "reset_phase_title": "フェーズをリセット", "remove_legacy_msg": "従来のカスタムアクションを削除しますか?サイクルイベントでの発火が停止します。この操作はパネルから元に戻せません。", "discard_recording_title": "録音を破棄", "discard_recording_msg": "保存された録音を破棄しますか?この操作は元に戻せません。", @@ -815,32 +763,43 @@ "store_share_device": "このデバイスを共有", "export_select": "エクスポート - データを選択", "import_wizard": "インポート - データを選択", - "history_import": "電力履歴のインポート" + "history_import": "電力履歴のインポート", + "apply_suggestions_title": "提案を適用", + "remove_task_title": "タスクを削除", + "remove_task_msg": "リマインダー「{task}」を削除しますか?メンテナンス履歴の記録は保持されます。", + "apply_suggestions_msg": "これら {n} 件の設定を変更しますか?保存後に統合がリロードされます。", + "discard_settings_title": "未保存の変更を破棄しますか?", + "discard_settings_msg": "未保存の設定変更が {n} 件あります。設定を離れて変更を破棄しますか?", + "profile_title": "プロファイル · {name}", + "undo_import_msg": "このデバイスのデータと設定が、前回のインポート({when})より前の状態に戻ります。それ以降に記録されたサイクルや行った変更は失われます。", + "undo_import_ok": "インポートを元に戻す", + "undo_import_title": "前回のインポートを元に戻す" }, "msg": { "pg_canvas_empty3": "上でサイクルを選び、「実行」を押してシミュレーションします。その後、ホバーで値を表示、スクロールまたはピンチでズーム、ドラッグで移動できます。", + "pg_preset_delete_confirm": "プリセット「{name}」を削除しますか?", + "pg_preset_limit": "プリセットの上限に達しました({n})", + "pg_preset_overwrite": "プリセット「{name}」を上書きしますか?", "tail_trim_hint": "末尾から削除する秒数", - "store_sibling_hint": "お使いの機種そのものが共有されていない場合は、同じブランドの近い機種から始めるとうまくいくことが多いです。", - "store_declare_appliance": "お使いの家電を WashData に指定すると、このタブに他のユーザーが共有したプログラムや録音が表示されます。上でブランド名を入力して見て回ることもできます。", - "refresh_catalog_hint": "コミュニティのブランドと家電の一覧はキャッシュされ、共有ストアが 1 日あたりの読み取り上限内に収まるようにしています。更新すると、他のユーザーが追加または承認した項目を取得できます。", + "store_sibling_hint": "お使いのモデルの共有がありませんか?同じブランドの近いモデルから始めるとうまくいくことが多いです。", + "store_declare_appliance": "お使いの家電を WashData に指定すると、他のユーザーがその家電向けに共有した設定を見られます。上でブランド名を入力して見て回ることもできます。", + "refresh_catalog_hint": "コミュニティのブランドと家電の一覧は、ストアの 1 日あたりの上限を節約するためキャッシュされています。更新すると、他のユーザーが追加または承認した項目が表示されます。", "head_trim_hint": "先頭から削除する秒数", "appliance_monitor": "アプライアンスモニター", "artifact_dip_detail": "通常の電力帯の下側に約{n}秒間落ちました。", "artifact_footer": "上のグラフで強調表示されています。これらは一時的なアーチファクト (サイクルの途中でドアが開いたなど) であり、必ずしも問題があるわけではありません。", "artifact_header": "このサイクル中に {n} 個の異常が検出されました", - "artifact_pause_detail": "電力が約{n}秒間ほぼゼロに落ちた後、再開されました。サイクルの途中でドアが開かれたか、一時停止された可能性があります。", + "artifact_header_other": "このサイクル中に {n} 個の異常が検出されました", + "artifact_pause_detail": "電力が約{n}秒間ほぼゼロに落ちた後、再開しました。ドアが開かれたか、サイクルが一時停止された可能性があります。", "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": "オーバーレイプロファイル (淡い)", - "compare_profiles_tip": "他のプロファイル エンベロープを上のチャートに重ねて、どれがこのサイクルに最も適合するかを確認します。", + "compare_profiles_tip": "他のプロファイルをグラフに重ねて、このサイクルに最も合うものを確認します。", "compare_selected_cycles": "選択されたサイクル (実線) – 表示/非表示", - "consider_new_profile": "新しいプロファイルの作成を検討してください。", - "coverage_gap": "最近のサイクルには一致するプロファイルがありません (過去 30 回の {pct}%)。", - "coverage_gap_similar_cycles": "{count} 件の類似した未ラベルサイクルが見つかりました – プロファイルを作成してマッチングを開始してください。", "cycles_deleted": "{count} 件のサイクルを削除しました", "enough_data": "学習に十分なデータがあります({current}/{min} サイクル)。", "export_description": "JSONにエクスポートするプロファイル、サイクル、設定などを正確に選択するか、ファイルを解析して必要な部分だけをインポートできます。", @@ -848,30 +807,25 @@ "feedback_prompt": "それが正しかったことを確認し、プログラムを修正するか、無視してください。", "feedback_relabel_hint": "このサイクルを再ラベル付けすると、これも解決します。", "filter_by_profile": "プロファイルでフィルター…", - "group_modal_help": "温度/スピンが異なる同じ形状のグループ プログラム (期間は異なる場合があります)。マッチングでは、グループを 1 つの候補としてスコア付けし、最も適合するメンバーを選択します。少なくとも 2 つ選択してください。オーバーレイは、それらがどれだけ似ているかを示します。", + "group_modal_help": "形状が同じで温度や脱水が異なるプログラムをグループ化します。マッチングはグループをひとつとして採点し、最も合うメンバーを選びます。2 つ以上選んでください。重ね表示で似ている度合いがわかります。", "group_not_cohesive": "これらのプロファイルは、確実にグループ化できるほど類似していないため、外れ値を削除するかグループを分割するまで、マッチングでは個別に扱われます。", "group_preview_hint": "2つ以上のメンバーを選択してパワーカーブをプレビュー・比較できます。", - "import_intro": "エクスポートされたファイルをロードするか、以下の JSON ペイロードを貼り付けます。", - "legacy_actions_title": "{count} 個の従来のカスタム アクションが引き続き実行中", + "legacy_actions_title": "{count} 個の従来のカスタム アクションが引き続き実行中です", + "legacy_actions_title_other": "{count} 個の従来のカスタム アクションが引き続き実行中です", "live_chart_loading": "測定値が到着すると、ライブパワーチャートが表示されます。", "loading": "読み込み中…", "loading_curve": "荷重曲線…", "loading_settings": "設定を読み込み中…", "log_buffer_hint": "新しい順 · 再起動以降の最後の 500 ha_washdata レコードをバッファします · 下端をドラッグしてサイズ変更します。", - "maintenance_advisory": "持続時間/エネルギーの増加は、器具のメンテナンスが必要であることを示している可能性があります (スケール除去、フィルターの掃除など)。", "maintenance_due": "メンテナンス時期: {items}", - "maintenance_intro": "この家電に対して行ったメンテナンスを記録すると、各作業の次回時期にリマインドされます。", + "maintenance_intro": "この家電のメンテナンスを記録し、各作業の次回時期にリマインドを受け取ります。", "maintenance_load_error": "メンテナンスデータを読み込めませんでした: {error}", "maintenance_requires_access": "メンテナンスとエクスポート/インポートにはフルアクセスが必要です。", - "manual_duration_hint": "プロファイルの平均/予想サイクル長。残り時間の推定に使用されます。編集して設定します。変更しないままにすると、現在の値が維持されます。", - "matching_tuning_intro": "学習時に、WashData はプログラム マッチングによって形状と持続時間およびエネルギーをどの程度重みづけるかも調整します。", + "manual_duration_hint": "プロファイルの予想サイクル長で、残り時間の推定に使われます。変更しなければ現在の値が維持されます。", "merge_intro": "選択したサイクルが 1 つに結合されます (時系列順、ギャップは 0 W で埋められます)。結果として得られるプロファイルを選択します。", "ml_intro": "WashData には、すぐに使えるスマート モデルが同梱されています。", "ml_learned_intro": "本機に合わせてチューニングされたモデル。", - "ml_loading": "ML を読み込み中…", "ml_settings_intro": "2 つの独立したスイッチ: 1 つはサイクルの実行中にモデルを適用し、もう 1 つは WashData が時間の経過とともにモデルをマシンに合わせて微調整します。", - "name_first_program": "十分なサイクルがそろいました – 最初のプログラムに名前を付けて一致を開始しましょう。", - "near_duplicate_cluster": "ほぼ重複したプロファイル クラスターが検出されました。グループ化により、類似したもの (例: 異なる温度/スピンの同じプログラム) の間でマッチングを確実に選択できるようになります。", "no_cycles_match": "現在のフィルターに一致するサイクルはありません。", "no_cycles_profile": "このプロファイルにはサイクルがありません。", "no_cycles_selected": "まずサイクルを 1 つ以上選択してください。", @@ -896,73 +850,50 @@ "no_suggestions": "積極的な提案はありません。", "notify_services_hint": "{entity} サービス ID を使用してください(複数はカンマ区切り)。テンプレート変数:{vars}。", "old_actions_warning": "古いアクション エディター (現在は削除されています) で構成されています。これらは引き続きサイクル イベントで起動されますが、ここでは編集できなくなりました。それらを通常のオートメーションに変換するか、削除します。", - "onboarding_progress": "{n} / 3 サイクルを観測", - "onboarding_watching": "家電をいつも通り使用してください – WashData が監視しています。3 サイクル後にプログラムの一致が始まります。", "pending_feedback": "保留中の検出フィードバック", "performance_trend": "パフォーマンスの傾向 ({n} サイクル)", - "pg_analysis_empty": "マッチ分析を見るにはサイクルを読み込んでください。", "pg_apply_confirm": "最適値を適用: {label} = {value}?", - "pg_canvas_empty": "上でサイクルを選択し、「読み込み」を押すと電力トレースが表示されます。", - "pg_chart_caption": "{param} の値ごとの検出/マッチ率", "pg_ctrl_intro": "値はこのデバイスの有効な統合設定から始まります。編集内容は、適用するまでプレイグラウンド内にとどまります。", "pg_live_unavailable": "有効な設定を取得できないため、既定値を表示しています。", "pg_load_live_confirm": "プレイグラウンドの編集 {n} 件を破棄して、統合の現在の設定を再読み込みしますか?", - "pg_match_rate": "{total} サイクルで {pct}% のマッチ率", "pg_matches_live": "有効な設定と一致しています", "pg_n_changed": "有効な設定との差分 {n} 件", "pg_sugg_none": "すべての提案は現在の設定に一致しています", - "pg_preset_delete_confirm": "プリセット「{name}」を削除しますか?", - "pg_preset_limit": "プリセットの上限に達しました({n})", - "pg_preset_overwrite": "プリセット「{name}」を上書きしますか?", "pg_publish_one_confirm": "{label} = {value} をこのデバイスの設定に保存しますか?", "pg_restart_note": "シミュレーションツールを有効にするには Home Assistant を再起動してください。", - "pg_sim_progress": "{done} / {total} サイクル", - "pg_starting": "開始中…", - "pg_stress_synthetic": "待機シミュレーションはここから開始", - "pg_sweep_intro": "もし {param} が違っていたら? 最も多くのサイクルが正しくマッチする設定を見つけるため、直近 {cycles} サイクルにわたって {steps} 個の値をテストします。", - "pg_sweep_step": "ステップ {done} / {total}", - "pg_verdict_bad": "要注意: 多くのサイクルが検出されずに終わっています。", - "pg_verdict_good": "よく調整されています: ほとんどのサイクルが正しく識別・マッチされています。", - "pg_verdict_ok": "許容範囲: 一部のサイクルを見逃しています。開始しきい値を下げてみてください。", "phase_catalog_intro": "サイクルの名前付きセグメント (前洗浄、加熱、脱水など)。コントロール パネルからプロファイルにそれらを割り当てます。", "phase_ranges_intro": "位相範囲 (サイクル開始からの分) が平均曲線に重ねられます。値を編 集してライブでプレビューします。", - "playground_intro": "過去のサイクルを異なる設定で再生し、2 つの構成を並べて比較し、サイクルがプロフィールにどう整合するかを確認できる安全なサンドボックスです。明示的に適用するまで、ここでの操作が実データを変更することはありません。", - "playground_needs_restart": "プレイグラウンドツールを有効にするには Home Assistant を再起動してください。", - "preferences_admin": "設定、パネル、アクセス制御", - "preferences_adv": "設定", + "playground_intro": "実際のサイクルデータで設定を試せます。適用するまで、ここでの操作が現在の設定を変えることはありません。", "prefs_personal": "これらはホーム アシスタント アカウントにのみ適用されます。", - "process_history_hint": "保存されているすべてのサイクルに対してマッチングを再実行し、チューニング提案を更新し、MLモデルを再訓練し(有効な場合)、記録されたエネルギー価格でサイクルの料金を計算し直し、サイクルの健全性を再計算します。一括レビュー後に実行してください。", + "process_history_hint": "保存されたすべてのサイクルのマッチングを再実行し、提案を更新し、ML モデルを再学習し(有効な場合)、記録された価格でサイクルの料金を再計算し、サイクルの健全性を再計算します。まとめてレビューした後に実行してください。", "profile_deleted": "プロフィールを削除しました", - "profile_poor_health_detail": "このプロファイルに割り当てられたサイクルには、形状が一貫していないか、信頼性が低いです。エンベロープを再構築するか、ラベル付きサイクルを確認することを検討してください。", "profiles_intro": "プロファイルをクリックすると、統計、フェーズ、クリーンアップが表示されます。ほぼ同一のプログラム (同じ形状/持続時間、異なる温度またはスピン) をグループ化して、それらの間でマッチングを確実に選択します。", - "rbac_hint": "オフの場合、すべてのホーム アシスタント ユーザーはフル アクセス権を持ちます (デフォルト)。管理者は常に完全なアクセス権を持ち、全員を管理できます。", - "recent_logs": "最近の ha_washdata レコード", + "rbac_hint": "オフ: すべての Home Assistant ユーザーがフルアクセスできます(既定)。管理者は常にフルアクセスでき、全員を管理できます。", "recording_in_progress": "録音中です", "reminders_intro": "最後のサービスからこのサイクル数が経過した後にパネルにリマインダーを表示します。空欄または 0 でリマインダーをオフにします。", "restart_gap_footer": "グラフ上でハイライト表示されます。これらの区間では電力データが欠損しています。マッチングには実際の計測値のみを使用しました。", - "restart_gap_header": "{n} HA 再起動ギャップ(このサイクル中)", + "restart_gap_header": "このサイクル中に HA 再起動ギャップが {n} 件あります", + "restart_gap_header_other": "このサイクル中に HA 再起動ギャップが {n} 件あります", "restart_gap_item": "{dur} の欠損", - "review_confirm_help": "このサイクルが正しく検出されたかどうかを確認します。レビューによってマシン上でモデルがトレーニングされます。確認するサイクルが増えるほど、マッチングと健全性のスコアが向上します。簡単な「良い/悪い」で十分です。", + "review_confirm_help": "このサイクルは正しく検出されましたか?サイクルを確認すると、お使いの機器でのマッチングが向上します。「良い」か「悪い」を選ぶだけで十分です。", "review_in_settings": "設定で確認する", "review_notes_placeholder": "注記 (オプション)", "review_notes_tip": "自分の参照用の自由記述メモ。マッチングやトレーニングでは使用されません。", - "review_profile_tip": "このサイクルにラベルが付けられているプログラム。自動検出されたプログラムが間違っていた場合は、ここで修正します。ラベル付けにより、将来のサイクルのマッチングが教示されます。", - "review_quality_tip": "このサイクルはなんてクリーンなんだろう。良い = このプログラムの教科書的な例。 Bad = 検出されましたが、ノイズが多いか異常です。使用できない = 誤検出 (マージ、切り捨て、または偽)。ヘルススコアと、モデルのトレーニングに許可されるサイクルを決定します。", - "review_recorded_tip": "これをプログラム用に厳選された参照サイクルとしてマークします。これは、手動で記録されたサイクルと同じ役割です。参照サイクルは常に保持され、一致するテンプレートにシードされ、クリーンアップによって削除されることはありません。 (これは「ゴールデン」/記録済みフラグです。両方とも同じものです。)", - "review_tags_tip": "このサイクルで何が問題だったかを説明するオプションのフラグ。トレーニングとクリーンアップで問題を解決できます。", + "review_profile_tip": "このサイクルに付けられているプログラムのラベルです。間違っている場合はここで修正してください。ラベルはマッチングの学習に使われます。", + "review_quality_tip": "このサイクルのデータの質です。良い: このプログラムの模範的な運転。悪い: 検出はされたが、ノイズが多いか異常。使用不可: 誤検出(結合、途中で切れた、または偽物)。健全性スコアと、モデルの学習に使うサイクルに影響します。", + "review_recorded_tip": "このサイクルを、手動で記録したサイクルと同様に、そのプログラムの厳選された基準サイクルとしてマークします。基準サイクルは常に保持され、マッチング用テンプレートの元になり、クリーンアップで削除されることはありません。", + "review_tags_tip": "何が問題だったかを示す任意のフラグです。学習とクリーンアップで考慮されます。", "review_to_cycles": "Cycles レビューキューを開く", "samples_decimated": "{total} サンプル中 {shown} 件を表示中 (表示用に間引き。ピークは保持)。ここでの大きな間隔は間引きであり、データの欠落ではありません。", - "saving_triggers_reload": "保存すると統合のリロードがトリガーされます。 HA エンティティが一時的に利用不可として表示される場合があります。", + "saving_triggers_reload": "保存すると統合が再読み込みされます。エンティティが一時的に利用不可と表示される場合があります。", "search_placeholder": "検索設定…", "see_recorder": "以下のレコーダー ウィジェットを参照してください", "setting_changed": "{date} に {old} から {new} に変更", "settings_basic_note": "必須の設定のみ表示しています。すべての一覧は「高度な」に切り替えてください。", - "shape_drift_advisory": "⚠ 形状がずれています", - "shape_drift_detail": "このプロファイルの電力パターンが時間とともに変化しました – 家電の劣化またはメンテナンスが必要な可能性があります(例:スケール除去、フィルター清掃)。", "show_all_settings": "すべての設定を表示", - "showing_suggestions": "{count} 設定を候補とともに表示しています。", - "split_intro": "グラフをクリックして分割ポイントを追加または削除するか、アイドル ギャップを自動検出します。結果として得られる各セグメントは、独自のプロファイルを取得できます。", - "storage_diagnostics": "ストレージ統計、メンテナンス、エクスポート/インポート", + "showing_suggestions": "提案のある設定を {count} 件表示しています。", + "showing_suggestions_other": "提案のある設定を {count} 件表示しています。", + "split_intro": "グラフをクリックして分割点を追加・削除するか、アイドル間隔から自動検出します。分割した各部分に個別のプロファイルを付けられます。", "sug_staged": "{key} = {val} を設定しました。保存して適用してください。", "toast_auto_detect_enabled": "自動検出が有効になりました", "toast_auto_label_complete": "自動ラベル付けが完了しました", @@ -979,7 +910,6 @@ "toast_no_automation": "ここではオートメーションを作成できません", "toast_profile_name_required": "プロファイル名が必要です", "toast_program_set": "プログラムを設定しました: {program}", - "toast_rebuild_failed": "再構築に失敗しました: {error}", "toast_resume_failed": "再開に失敗しました: {error}", "toast_revert_failed": "元に戻すことに失敗しました: {error}", "toast_reverted": "{key}を元に戻しました", @@ -990,100 +920,85 @@ "trend_energy_up": "エネルギーは増加傾向にあります ({pct}%/サイクル) - 最近の平均 {avg}", "trim_destructive_confirm": "サイクルの{pct}%だけが残り、この操作は元に戻せません。続行しますか?", "trim_intro": "赤いハンドルをドラッグするか、値を入力します。窓の外側のものはすべて削除されます。", - "tuning_suggestions_available": "{count} 観察されたサイクルから得られる調整提案。これらは関連するフィールドの横に表示されます。", + "tuning_suggestions_available": "観察されたサイクルから {count} 件のチューニング提案があります。関連するフィールドの横に表示されます。", + "tuning_suggestions_available_other": "観察されたサイクルから {count} 件のチューニング提案があります。関連するフィールドの横に表示されます。", "unsure_detected_prefix": "WashData が検出したかどうかは不明", - "updated": "更新", "warmup_badge": "学習中 ({done}/{needed} サイクル)", - "warmup_detail": "このプロファイルは自動マッチングが開始されるまで {needed} 個のラベル付きサイクルが必要です。確認されたサイクルごとに学習が進みます。", + "warmup_detail": "マッチングはすでに機能しています。WashData が自動でラベル付けを始める前に、このプロファイルの最初の {needed} 件のサイクルを確認してください。", "wipe_history_warning": "すべてのサイクルとプロファイルを完全に削除します。元に戻すことはできません。", "compare_overlay_tip": "学習済みプロファイルのエンベロープを重ねて、各サイクルがどのプログラムに似ているかを確認します。", - "duration_trend_up_tip": "継続時間の増加({pct}%/サイクル)", - "duration_trend_down_tip": "継続時間の減少({pct}%/サイクル)", - "energy_trend_up_tip": "エネルギーの増加({pct}%/サイクル)", - "energy_trend_down_tip": "エネルギーの減少({pct}%/サイクル)", - "performance_trending": "パフォーマンスの傾向", - "pg_select_cycle_hint": "サイクルを選択し、↺ をクリックして読み込みます", - "pg_sweep_progress": "ステップ {done} / {total}", - "pg_analysis_hint": "サイクルを選択し、↺ をクリックして分析を読み込みます。", "diagnostics_load_failed": "診断を読み込めませんでした: {error}", - "manual_duration_ref_hint": "基準サイクルが選択されていない場合にのみ使用されます。基準サイクルは、自身の長さから持続時間を設定します。", + "manual_duration_ref_hint": "基準サイクルがない場合にのみ使用されます。基準サイクルがある場合は、その長さから所要時間が決まります。", + "duration_learned_hint": "このプログラムのサイクル(外れ値を除く)から算出され、新しいサイクルごとに更新されます。変更するには、外れているサイクルをトリムまたは削除してください。", "toast_convert_rolled_back": "変換に失敗し、ロールバックされました(オートメーションは残っていません): {error}", "toast_convert_orphan": "オートメーションは作成されましたが、古いアクションのクリアに失敗しました。再試行しないでください。オートメーションの重複を避けるため、古いアクションを手動で削除してください。", - "advisory_duration_trend_up": "'{name}' のサイクルが次第に長くなっています(1サイクルあたり約 +{pct}%)。機器の動作が変わった場合は、このプロファイルを再記録または再構築してください。", - "advisory_energy_trend_up": "'{name}' の消費エネルギーが次第に増えています(1サイクルあたり約 +{pct}%)。想定外であれば機器を確認することをおすすめします。", - "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 にはマッチングの基準となる長さがありません。このプログラムを記録し直すか、予想所要時間を設定すると、再びマッチングの対象になります。", + "advisory_duration_trend_up": "'{name}' のサイクルが長くなり続けています(1サイクルあたり約 +{pct}%)。機器が変わった場合は、このプロファイルを再記録または再構築してください。", + "advisory_energy_trend_up": "'{name}' はサイクルごとに消費エネルギーが増えています(1サイクルあたり約 +{pct}%)。想定外であれば機器を点検してください。", + "advisory_poor_health": "'{name}' の適合度が低くなっています。最近のサイクルのばらつきが大きいか、マッチが弱いためです。マッチングと推定の精度を保つため、サイクルを確認するか再記録してください。", + "advisory_shape_drift": "'{name}' が元の電力波形から変化しています。石灰の付着や摩耗が原因かもしれません。最近のサイクルで再記録することを検討してください。", + "advisory_shape_drift_corr": "'{name}' が元の電力波形から変化しています(相関 {corr})。石灰の付着や摩耗が原因かもしれません。最近のサイクルで再記録することを検討してください。", + "advisory_unmatchable": "'{name}' は決してマッチしません。電力データを含むサイクルがありません。このプログラムのサイクルにラベルを付けるか記録すると解決します。", + "advisory_unmatchable_no_duration": "'{name}' は決してマッチしません。そのサイクルに有効な所要時間がありません。このプログラムを再記録するか、予想所要時間を設定してください。", "pg_no_alerts": "この実行で問題は検出されませんでした。", "pg_override_active": "編集した設定を現在の設定と比較して使用中", "pg_history_empty": "「実行」を押してサイクルを再生します。", "pg_history_summary": "{detected}/{total} 検出 · {correct} 正しくマッチ", "pg_sweep_no_metric": "この目的を評価するにはデータが不足しています。", "pg_sweep_intro2": "直近のサイクル全体で目的を最もよく満たす設定を見つけます。適用するまで何も変更されません。", - "pg_running_history": "サイクルを再生しています…", - "pg_running_sweep": "全サイクルで値を検証しています…", "pg_row_load_hint": "このサイクルを上のグラフに読み込む", - "pg_history_intro2": "上記の設定で、最近のサイクルを実際の検出器とマッチャーで再生します。行をクリックするとそのサイクルをグラフに読み込みます。設定を変更すると、変更前後の比較が表示されます。", + "pg_history_intro2": "上記の設定で、最近のサイクルを実際の検出器とマッチャーで再生します。行をクリックするとグラフに読み込まれます。設定を変更すると変更前後を比較できます。", "pg_simulating": "サイクルをシミュレーションしています…", - "pg_canvas_empty2": "上でサイクルを選び、「実行」を押してシミュレーションします。その後、ホバーで値を表示、スクロールでズーム、ドラッグで移動できます。", "pg_apply_settings_confirm": "これら {n} 件の設定をこのデバイスに保存しますか? {list}", - "pg_stress_terminated_detail": "約 {idle}W の待機状態に落ち着きました -- {h}時間{m}分後に {reason} によってサイクルが終了しました。", - "pg_stress_above_threshold_detail": "待機電力 約 {idle}W が実効停止しきい値 ({stop}W) 以上です -- サイクルが一度も静音状態として認識されませんでした。修正するには stop_threshold_w を上げてください。", - "pg_stress_hit_cap_detail": "サイクルが停止せずに {h}時間{m}分 稼働しました -- 安全制限により強制停止されました。待機電力 {idle}W に対して停止しきい値 {stop}W です。", "no_logs_match": "これらのフィルターに一致するログレコードはありません。", "pg_analysis_hint2": "サイクルを選択して「実行」を押すと、マッチ分析が読み込まれます。", - "pg_analysis_empty2": "「実行」を押すとマッチ分析が表示されます。", - "imported_readonly": "コミュニティストアからインポートしました。参照とマッチングのために表示されます。統計には含まれず、編集できません。", + "imported_readonly": "コミュニティストアからインポートしました。参照とマッチングに使われ、統計には含まれず、編集できません。", "merge_no_imports": "インポートした録音は実際のサイクルに統合できません。統合するには選択を解除してください。", - "store_enable_hint": "設定でオンライン機能を有効にすると、コミュニティの基準サイクルを閲覧・インポートできます。", - "advisory_duration_outlier": "'{name}' とラベル付けされたサイクル {n} 件は、このプログラムの通常の長さから離れすぎており、マッチングされることはありません (最大 {ratio}x)。ラベルが間違っているか、1 回の記録に 2 回分の運転が含まれている可能性があります。ラベルを付け直すか分割して、このプログラムの時間推定がゆがまないようにしてください。", + "store_enable_hint": "環境設定 > オンラインとコミュニティ でオンライン機能をオンにすると、コミュニティの基準サイクルを閲覧・インポートできます。", + "advisory_duration_outlier": "'{name}' とラベル付けされたサイクル {n} 件は、通常の長さから離れすぎていてマッチしません(最大 {ratio}x)。ラベルの誤りか、2 回分の運転を含んでいる可能性があります。ラベルを付け直すか分割して、時間推定がゆがまないようにしてください。", "standby_above_stop_title": "サイクル終了時にまだ {idle} W を消費しています", "standby_above_stop_sub": "直近 {total} サイクル中 {n} 件。オフ遅延は電力が停止しきい値 ({stop} W) を下回ってから始まるため、サイクルの終了が遅れたり、手動で停止する必要が生じたりします。", - "online_intro": "他の WashData ユーザーと基準サイクルを共有・ダウンロードできます。すべてのオンライン機能はオプトインで、デフォルトではオフです。", "store_share_intro": "この基準サイクルをアップロードすると、同じ家電を持つ他のユーザーが利用できます。公開前に審査されます。", - "online_intro_global": "他の WashData ユーザーと基準録音を閲覧・共有し、家電エントリを確認できます。1 つの接続が WashData 統合全体に適用されます。家電のブランドとモデルは「基本設定」でデバイスごとに設定します。すべてのオンライン機能はオプトインで、デフォルトではオフです。", + "online_intro_global": "他の WashData ユーザーと基準録音を閲覧・共有できます。1 つの接続ですべての WashData デバイスに対応します。ブランドとモデルは「基本」でデバイスごとに設定します。オンライン機能は、オンにするまでオフのままです。", "online_unavailable": "このサーバーではオンライン機能を利用できません。", "store_picker_offline": "設定(歯車アイコン)でオンライン機能を有効にすると、コミュニティカタログから選択できます。", "pick_brand_first": "先に家電ブランドを選択してください。", - "model_not_found": "まだカタログにありません。", - "connect_to_confirm": "確認や評価を行うには、設定(歯車アイコン)で接続してください。", + "custom_task_limit": "タスクの上限 ({max} 件) に達しました。", + "custom_tasks_intro": "リマインドしてほしいその他の作業を追加します。どちらかの間隔に達するとタスクは要対応になります。片方だけを使う場合は、もう片方を空欄または 0 にします。", + "maintenance_due_sensor_hint": "「メンテナンス時期」バイナリセンサーは、いずれかのタスクが期限を迎えている間オンになります。独自の通知オートメーションに使えます。", + "maintenance_supporter_intro": "Maintenance Supporter は、さらに多くのことができる別の統合です。あらゆる家電やタスク、時間・使用量・センサーによるスケジュール、通知、履歴に対応します。WashData のサイクルカウンターを利用できます。", "adopt_settings_hint": "このデバイスの検出・マッチングしきい値を共有されたものに上書きします。通知、エンティティ、電力料金は変更されません。", - "include_settings_hint": "このデバイスの認識・マッチングしきい値を共有します(通知、エンティティ、電力料金は除く)。引き継ぐかどうかは引継ぎ側が選択します。", + "include_settings_hint": "このデバイスの検出・マッチングのしきい値を共有します(通知、エンティティ、電力料金は除く)。適用するかどうかは受け取った側が選びます。", "onboard_download": "新しいデバイスですか?同じ機器を持つ他のWashDataユーザーの既成セットアップ(プログラム、リファレンスサイクル、フェーズ)を引き継ぎましょう。", - "share_consent": "これらのサイクルが中断なく正常に完了したことを確認します", "share_device_none": "まだ共有できるサイクルがありません。サイクルタブで記録または手動選択したサイクルをリファレンスサイクル(⭐)として先にマークしてください。", "share_guideline_naming": "各プロファイルは機器のダイヤルやディスプレイに表示されている通りに名前を付けてください(例:'Cotton 40'、'Eco 60')。", - "share_guideline_quality": "正常に完了したサイクルのみ共有してください -- サイクル途中の中断、ドア開放、電力の乱れがないものに限ります。", + "share_guideline_quality": "正常に終了したサイクルだけを共有してください。中断、ドアの開閉、電力の乱れがないものに限ります。", "share_guideline_review": "アップロードは審査中として開始され、十分なコミュニティメンバーが確認すると公開されます。", "share_guidelines_title": "共有前に", - "store_download_device_intro": "共有されたすべてのプログラムとそのリファレンスサイクルをデバイスに引き継ぎます。ご自身の記録済みサイクルと統計には影響しません。", + "store_download_device_intro": "共有されたすべてのプログラムとそのリファレンスサイクルをデバイスに追加します。ご自身のサイクルと統計には影響しません。", "store_share_device_intro": "選択したリファレンスサイクルとともに{brand} {model}をアップロードします。同じ機器を持つ他のユーザーがあなたのプログラムを引き継ぐことができます。エントリは公開前に審査されます。", "share_profile_no_cycles": "リファレンスサイクルなし -- サイクルタブでサイクルを⭐としてマークしてこのプロファイルを含めてください", - "advisory_phase_inconsistent": "'{name}' は異なるプログラムや温度が混在しているようです。加熱時間がサイクルごとに大きく異なります。温度ごとなど別々のプロファイルに分割すると、マッチングと時間推定が改善されます。", - "advisory_phase_inconsistent_title": "⚠ プログラム混在の可能性", - "export_select_intro": "含める項目を正確にチェックしてください。サイクルなしでプロファイルを選択しても、認識可能なプログラムはエクスポートされます(学習した形状も一緒に移行します)。", - "import_analyze_hint": "エクスポートしたファイルを読み込む(またはそのJSONを貼り付ける)と、WashDataが解析し、変更前にインポート可能な内容を正確に表示します。", + "export_select_intro": "含める項目にチェックを入れてください。サイクルなしのプロファイルでも、マッチング可能なプログラムとしてエクスポートされます。", + "import_analyze_hint": "エクスポートしたファイルを読み込む(または JSON を貼り付ける)と、変更前にインポートできる内容を WashData が表示します。", "analyzing": "解析中…", - "device_type_mismatch_warn": "このエクスポートは異なる機器タイプのものです({src} と {local})。プログラムとサイクルは参照データとしてインポートできますが、機器固有の設定と実履歴のインポートは無効になります。", + "device_type_mismatch_warn": "このエクスポートは異なる機器タイプのものです({src} と {local})。プログラムとサイクルは参照データとしてインポートできますが、機器の設定と実履歴はインポートできません。", "merge_hint": "インポートした項目は追加され、ローカルのデータは失われません。名前の衝突は以下で解決します。", "replace_warn": "チェックした各カテゴリは消去され、ファイルの内容で置き換えられます。チェックしていないカテゴリはそのまま残ります。", "dest_reference_hint": "インポートしたサイクルはプログラムの認識精度を向上させるだけで、使用量やエネルギーの統計には影響しません。", - "dest_real_history_hint": "インポートしたサイクルはこの機器自身の履歴として扱われ、エネルギーや使用量の統計に反映されます。1台の機器を新しい環境に移行する際に使用します。", + "dest_real_history_hint": "インポートしたサイクルは、この機器自身の履歴と統計として扱われます。機器を新しい環境に移行するときに使います。", "sug_muted": "この設定は今後提案されません", "sug_mute_failed": "提案のミュートに失敗しました", "sug_unmuted_all": "ミュートした提案をリセットしました", - "n_suggestions_muted": "{count} 件がミュート済み。自動チューナーはこれらを提案しません。", + "n_suggestions_muted": "{count} 件の提案がミュート済みです。自動チューナーはこれらを提案しません。", + "n_suggestions_muted_other": "{count} 件の提案がミュート済みです。自動チューナーはこれらを提案しません。", "font_size_hint": "このパネル全体の文字を大きくまたは小さくします。このデバイスのあなたのアカウントに適用されます。", - "import_history_description": "WashData を使う前からスマートプラグをお使いでしたか?その電力センサーの履歴エクスポートをアップロードするか、Home Assistant から直接読み込むと、通常の検出がそのデータに対して実行され、過去のサイクルが「サイクル」一覧に表示されて名前を付けられる状態になります。", - "hist_input_hint": "履歴パネルからダウンロードした CSV (entity, state, last changed) をアップロードするか、WashData にセンサーの履歴を直接読み込ませてください。その後、ライブ動作とまったく同じ検出が実行され、見つかったサイクルのうちどれを保持するかを選択できます。", - "hist_recorder_hint": "選択した日付から現在までを読み込みます。Home Assistant は既定で詳細な履歴を 10 日間保持し、それ以降は 1 時間ごとの平均値のみを保持します。平均値は粗すぎてサイクルを検出できないため、さらに過去の日付を選ぶのは recorder がより長く保持する設定になっている場合だけにしてください。", - "hist_scanning": "履歴を検出器で再生しています。この処理はバックグラウンドで実行されます。このダイアログを閉じて後で戻ってきても問題ありません。", + "import_history_description": "WashData より前からスマートプラグを使っていましたか?その電力履歴をアップロードするか、Home Assistant から読み込んでください。検出が実行され、過去のサイクルが「サイクル」一覧に表示されて名前を付けられます。", + "hist_input_hint": "履歴パネルの CSV (entity, state, last changed) をアップロードするか、WashData にセンサーの履歴を直接読み込ませてください。ライブと同じ検出が実行され、残すサイクルを選べます。", + "hist_recorder_hint": "選択した日付から現在までを読み込みます。Home Assistant は既定で詳細な履歴を 10 日間保持します。それより古い 1 時間ごとの平均値は粗すぎてサイクルを検出できません。", + "hist_scanning": "バックグラウンドで履歴を検出器にかけています。このダイアログを閉じて後で戻ってこられます。", "hist_imported_count": "{n} サイクルをインポートしました。", "hist_duplicates": "{n} 件はすでにインポート済みのためスキップしました。", "hist_capped": "デバイスごとのインポート済みサイクル数の上限に達したため、残りは保存されませんでした。", - "hist_next_step": "「サイクル」一覧にインポートした履歴として表示されています。1 つ開いて「ラベル付け」から該当するプログラム名を設定してください。", + "hist_next_step": "「サイクル」一覧に、インポートした履歴として表示されています。1 つ開いて「ラベル付け」でプログラム名を設定してください。", "hist_rows_read": "{n} 件の計測値を読み込みました", "hist_entity_substituted": "{used} を読み込みました(このデバイスには {wanted} が設定されています)", "hist_breaks": "センサーが利用できなかった空白が {n} 箇所", @@ -1095,19 +1010,53 @@ "hist_scan_capped": "最初の候補のみを表示しています (合計 {n} 件見つかりました)。", "hist_recorder_empty": "Home Assistant には、その期間のこのセンサーの詳細な履歴がありません。", "hist_scan_failed": "スキャンに失敗しました。", - "hist_scan_expired": "そのスキャン結果はすでに利用できません。もう一度スキャンしてください。", + "hist_scan_expired": "そのスキャン結果は利用できなくなりました。もう一度スキャンしてください。", "hist_import_failed": "インポートに失敗しました。", - "imported_history_readonly": "インポートした電力履歴から検出されました。プログラムのマッチングには反映されますが、統計には含まれず、トリミングや分割はできません。ラベル付けしてプログラム名を設定してください。", - "odometer_intro": "この累計は増える一方です。サイクルの記録を削除しても変わらず、保存される履歴の上限を超えてもカウントを続けるため、サービスリマインダーは正確なままです。", - "odometer_correct_hint": "WashData が実際には行われていない稼働を記録した場合や、WashData を導入する前に家電がすでにサイクルを実行していた場合は、この値を設定してください。", + "imported_history_readonly": "インポートした電力履歴から検出されました。マッチングに役立ちますが、統計には含まれず、トリミングや分割はできません。ラベル付けしてプログラム名を設定してください。", + "odometer_intro": "この累計は増える一方です。サイクルを削除しても減らないため、サービスリマインダーは正確なままです。", + "odometer_correct_hint": "WashData が実際にはなかった運転を数えた場合や、WashData の導入前に家電がサイクルを実行していた場合に設定してください。", "no_reminders_set": "サービスリマインダーはまだ設定されていません。", "toast_program_armed": "次のサイクル用にプログラムを設定しました: {program}", "toast_range_required": "有効な開始/終了の範囲を設定してください。", - "no_profiles_option": "(プロファイルなし、追加してください)" + "no_profiles_option": "(プロファイルなし、追加してください)", + "compare_cycles_title": "{count} 件のサイクルを比較", + "edit_builtin_phase": "これは組み込みのフェーズです。保存するとカスタムのコピーが作成されます。コピーを削除すると元に戻ります。", + "fetch_error": "データの読み込みに失敗しました。", + "connection_lost": "接続が切断されました。{time} 時点のデータを表示しています。", + "pg_notify_partial": "マーカーが示すのは、開始、まもなく完了、完了、マイルストーン、サイレント時間の通知のみです。ライブ更新、取り出しリマインダー、超過アラートはシミュレートされません。", + "connect_to_confirm": "確認や評価を行うには、設定(歯車アイコン)で接続してください。", + "coverage_gap_cluster": "約{min}分の最近のラベルなしサイクル{count}件は、まだ作成されていない1つのプログラムのようです。", + "pg_label_yes": "サイクル終了時に「{name}」とラベル付けされます", + "pg_label_no": "見直しを依頼します: {reason}", + "live_match_debug_tip": "信頼度: 現在の電力カーブが最上位の候補プロファイルにどれだけ一致しているか (0-100%)。あいまい: 上位 2 つの候補のスコア差が 5% 以内です。サイクルが終了するまでラベルは確定しません。", + "automation_alias_started": "{device}: サイクル開始", + "automation_alias_finished": "{device}: サイクル終了", + "automation_desc_started": "{device} のサイクルが開始したときに実行されます。アクション (通知、照明、...) を追加してください。", + "automation_desc_finished": "{device} のサイクルが終了したときに実行されます。アクション (通知、照明、...) を追加してください。", + "feedback_detected": "WashData の検出結果: {name}", + "import_undo_hint": "前回のインポート({when})より前の復元ポイントが保持されています: {profiles} 件のプロファイルと {cycles} 件のサイクル。", + "replace_undo_hint": "先に復元ポイントが保存されるため、このインポートは「エクスポート/インポート」から元に戻せます。", + "pg_in_sample_note": "これらのサイクルはマッチ先のプロファイルの作成に使われているため、結果は楽観的です。新しいサイクルでは正しいマッチがやや少なくなると考えてください。", + "hist_tail_trimmed": "稼働中の統合と同様に、サイクル終了後の待機 {min} 分を切り詰めました", + "hist_truncated": "{n} 件の計測値の上限で停止したため、残りの履歴はスキャンされていません", + "hist_warn_kw": "最大の計測値がわずか {w} です。このセンサーはキロワット単位で報告しているようです。WashData にはワット単位が必要なため、サイクルを検出できません。", + "pg_compare_profile_tip": "グラフに重ねて表示するプロファイルを選ぶだけです。シミュレーションは、稼働中の統合と同じく、常にサイクルを独自にマッチングします。", + "pg_outcome_stale": "このシミュレーションの実行後に設定が変更されました。表示されているのは古い結果のままです。", + "pg_sweep_guarded": "早期終了や分割されるサイクルが増える、または現在の設定で検出できるサイクルを見逃すため、推奨されることはありません。", + "pg_sweep_keep_current": "現在の値({value}、{metric})のままにしてください。1 サイクル以上改善した値はありませんでした。", + "pg_task_busy": "このデバイスでは履歴テストまたは最適化がすでに実行中です。完了を待つか、キャンセルしてください。", + "hist_warn_naive_time": "{n} 件のタイムスタンプにタイムゾーンがなく、UTC として読み込みました。ファイルが現地時刻の場合、サイクルは UTC との時差の分だけずれ、夏時間の切り替え前後の計測値は欠落することがあります。", + "hist_warn_naive_time_other": "{n} 件のタイムスタンプにタイムゾーンがなく、UTC として読み込みました。ファイルが現地時刻の場合、サイクルは UTC との時差の分だけずれ、夏時間の切り替え前後の計測値は欠落することがあります。", + "pg_sweep_n_cycles": "{count} サイクルで評価", + "pg_sweep_n_cycles_other": "{count} サイクルで評価", + "cycle_context_loading": "レコーダー履歴を読み込み中…", + "cycle_context_note": "グレー: このサイクル前後の電力センサーのレコーダー履歴です。サイクルの一部ではなく、所要時間、エネルギー、マッチングには影響しません。", + "cycle_context_none": "レコーダーにはこのサイクル前後の測定値がありません(Home Assistant は既定で 10 日分の履歴を保持します)。", + "cycle_context_tip": "表示専用です。このサイクル前後の電力センサーのレコーダー履歴を、まだ保持されていれば表示します。失敗した開始試行を今後のサイクルに含めるには「曲線のプリロール」(設定 > 検出 > サイクル開始)を使ってください。保存される所要時間が変わります。", + "invalid_threshold_pair": "停止しきい値({stop} W)は開始しきい値({start} W)より小さくする必要があります。何も保存されていません。2 つを合わせて変更してください。" }, "pg_desc": { "completion_min_seconds": "実サイクルとみなす最短の稼働時間", - "end_repeat_count": "終了前に連続する低い測定値の回数", "interrupted_min_seconds": "短いサイクルは中断としてフラグ付け", "min_off_gap": "2つのサイクルを分けるのに必要な間隔", "off_delay": "サイクル終了までの低電力の秒数", @@ -1115,37 +1064,36 @@ "start_threshold_w": "開始とみなす最小ワット数", "stop_threshold_w": "これを下回ると停止とみなす", "anti_wrinkle_enabled": "メインフェーズ後のタンブルパルスを新しいサイクルと解釈せず吸収する", - "anti_wrinkle_exit_power": "しわ防止を有効に保つには、パルス間の電力がこの値を下回る必要がある", + "anti_wrinkle_exit_power": "タンブルパルス間の静止レベル。停止しきい値より高い場合のみ有効", "anti_wrinkle_idle_timeout": "しわ防止が終了するまでに2つのタンブルパルスの間に許容される静止時間", "anti_wrinkle_max_duration": "これより長いパルスはしわ防止を終了し、新しいサイクルを開始する", "anti_wrinkle_max_power": "これを超えるパルスはしわ防止を終了し、新しいサイクルを開始する", - "dtw_bandwidth": "ステージ 3: Sakoe-Chiba ワープバンド (0 = DTW 無効; デフォルト 0.2 = サイクル長の 20%)", - "corr_weight": "ステージ 2: 曲線形状(相関)と電力レベル(MAE)のバランス; デフォルト 0.45", - "duration_weight": "ステージ 4: 運転時間の一致が最終スコアにどれだけ影響するか(デフォルト 0.22)", - "energy_weight": "ステージ 4: エネルギーの一致が最終スコアにどれだけ影響するか(デフォルト 0.22)", - "profile_match_min_duration_ratio": "ステージ 1: プロファイル所要時間に対するサイクルの最短許容比率; デフォルト 0.1 (10%)", - "profile_match_max_duration_ratio": "ステージ 1: プロファイル所要時間に対するサイクルの最長許容比率; デフォルト 1.8 (180%)", - "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)", - "dtw_refine_top_n": "ステージ 3: DTW が再スコアリングする候補数; 正しいプロファイルが 4-5 位になる場合は 7-9 に増やす(デフォルト 5)", - "duration_scale": "ステージ 4: 運転時間の一致スコアが半分になる対数比率; 小さいほど厳格なペナルティ(デフォルト 0.175)", - "energy_scale": "ステージ 4: エネルギーの一致スコアが半分になる対数比率; 小さいほど厳格なペナルティ(デフォルト 0.25)", + "profile_match_min_duration_ratio": "プロファイルの所要時間に対するサイクルの最短比率; デフォルト 0.1 (10%)", + "profile_match_max_duration_ratio": "プロファイルの所要時間に対するサイクルの最長比率; デフォルト 1.8 (180%)", "dishwasher_end_spike_quiet_release": "食器洗い機: 予想所要時間を過ぎた後、サイクル終了時の排水待機を解除するまでの静音秒数", - "smart_termination_duration_ratio": "一致したプログラムの想定所要時間のうち、スマート終了が早期に終了できるようになるまでにサイクルが到達すべき割合。負荷や温度に左右されるマシンでは下げる", - "anti_crease_finalize_ratio": "しわ防止のタンブル末尾を確定してよくなるまでに、一致したプログラムの想定所要時間に対してサイクルが到達すべき割合。タンブル末尾が長い乾燥機では下げ、洗濯機はデフォルトのままにする", + "smart_termination_duration_ratio": "スマート終了がサイクルを早期に終了できるようになるまでに、プログラムの平均所要時間に対して到達すべき割合。運転時間がばらつく場合は下げる", + "anti_crease_finalize_ratio": "しわ防止の末尾でサイクルを終了できるようになるまでに、プログラムの平均所要時間に対して到達すべき割合。乾燥機では下げ、洗濯機はデフォルトのままにする", "curve_preroll_seconds": "中断された開始試行の測定値をサイクルの曲線の先頭に取り込む秒数。0 = 無効" }, "phase_desc": { "anti_crease": "メインサイクル後にドラムが断続的に回転し、衣類のシワを防止します。", "anti_wrinkle": "メインサイクル後にドラムが断続的に回転し、衣類のシワを防止します。", + "baking": "ヒーターを高温で動作させ、パンの皮と中身を焼き上げます。", + "cooking": "ヒーターとファンが動作するメインの調理フェーズ。", "cool_down": "サイクル終了近くで熱を伴わないタンブリング。", + "cool_down_af": "加熱後にファンのみで冷却する段階。", "drain_and_switch": "洗濯モードから乾燥モードへの移行期間。", "dry": "ヒーターや余熱を使用した乾燥ステージ。", "drying": "メインの加熱タンブリング期間。", "heat_up": "完全な乾燥が始まる前に最初のヒーターのウォームアップを行います。", + "keep_warm": "低温で料理を温かく保つ段階。", + "keep_warm_bm": "焼き上がり後、低温でパンを温かく保つ段階。", + "kneading": "モーターで生地を混ぜてこねる工程。消費電力が大きい。", + "pause_af": "振ったり確認したりするための短い一時停止。", + "pre_heat": "本調理の前に庫内を温める最初の加熱。", "pre_rinse": "洗剤で洗う前の最初のスプレーダウン。", + "proving": "低温で酵母を発酵させ、生地を膨らませる期間。", + "resting": "こねる工程の間に、グルテンを休ませる低電力の短い休止。", "pre_wash": "本洗浄前の最初の浸漬または前処理。", "rinse": "浄水すすぎ段階。このフェーズは複数回繰り返される場合があります。", "sanitize": "消毒プログラム用の高温洗浄ステージ。", @@ -1158,6 +1106,7 @@ "wash_dw": "主な洗剤は加熱しながら洗浄します。" }, "placeholder": { + "custom_task": "例: ドアパッキンの清掃", "add": "追加…", "group_name": "例:綿 2:47", "json_buttons": "[{\"action\":\"ID\",\"title\":\"Label\"}]", @@ -1184,7 +1133,7 @@ }, "section": { "anti_wrinkle": { - "intro": "しわ防止モードは、メインフェーズ後の低電力タンブルパルスを検出し、終了したサイクルに付加します。", + "intro": "メインフェーズ後の乾燥機の低電力タンブルパルスを、終了したサイクルに含めます。", "label": "しわ防止" }, "basic": { @@ -1192,7 +1141,7 @@ "label": "基本" }, "delay": { - "intro": "遅延スタート検出は、家電製品に電源が入っているがサイクルがまだ開始されていない状態を識別します。", + "intro": "電源が入っていて遅延スタートを待っている家電を検出します。", "label": "遅延スタート" }, "detection": { @@ -1216,153 +1165,145 @@ "label": "タイミングと監視" }, "triggers": { - "intro": "オプションの外部信号:終了トリガー、ドアセンサー、一時停止スイッチ、取り出しリマインダー。", + "intro": "任意の追加機能: 終了トリガー、ドアセンサー、一時停止スイッチ、取り出しリマインダー。", "label": "トリガーとドア" }, - "phase_eta": { - "label": "残り時間", - "intro": "サイクルの長さが温度や脱水に左右される機器向けの、フェーズを考慮した残り時間。" - }, "dishwasher": { "label": "食器洗い機", - "intro": "食器洗い機のサイクル終了処理。食器洗い機は通常、短い最終排水の前に、ほぼ無電力の長い乾燥フェーズを経て終了します。" + "intro": "食器洗い機の終了処理です。食器洗い機は短い最終排水の前に、静かな乾燥が長く続くことがよくあります。" } }, "setting": { "anti_wrinkle_enabled": { - "doc": "乾燥機が主加熱フェーズの後に発する短い低電力タンブル パルスを認識し、新しいサイクルとして読み取るのではなく、終了したサイクルに接続したままにします。", + "doc": "主加熱フェーズ後の乾燥機の短い低電力タンブルパルスを、新しいサイクルとして扱わずに、終了したサイクルに含めます。", "label": "しわ防止検出を有効化" }, "anti_wrinkle_exit_power": { - "doc": "しわ防止モードがアクティブな状態を維持するには、パルス間の電力がこれを下回る必要があります。", + "doc": "しわ防止モードでは、この値を下回る電力はタンブルパルス間の静止とみなされ、最大パルス間隔より長く静止するとモードが終了します。停止しきい値の方が高い場合はそちらが使われるため、この設定は停止しきい値より高くしたときにのみ効果があります。", "label": "終了電力しきい値" }, "anti_wrinkle_idle_timeout": { "label": "最大パルス間隔", - "doc": "しわ防止モードが終了するまでに、機器が2つのタンブルパルスの間に静止していられる時間。乾燥機がパルス間に残す最長の間隔より長く設定してください。そうしないと、以降のパルスがすべて誤った開始として読み取られます。" + "doc": "しわ防止モードが終了するまでに、乾燥機がタンブルパルスの間に静止していられる時間です。乾燥機の最長の間隔より長く設定してください。短いと、以降のパルスが誤スタートとして扱われます。" }, "anti_wrinkle_max_duration": { "doc": "これより長いパルスは、しわ防止タンブルではなく実際のサイクルとして扱われます。", "label": "最大継続時間" }, "anti_wrinkle_max_power": { - "doc": "このパワーを超えるパルスは、しわ防止タンブルではなく、実際の新しいサイクルとして扱われます。タンブルパルス出力のすぐ上に設定します。", + "doc": "この電力を超えるパルスは、タンブルではなく実際の新しいサイクルとして扱われます。タンブルパルスの電力より少し高く設定してください。", "label": "しわ防止最大電力" }, "auto_label_confidence": { - "doc": "サイクル終了時の一致スコアがこれ以上の場合、プログラムには確認プロンプトなしで自動的にラベルが付けられます。自動ラベル付けの前に、より高い確実性を要求するには、この値を上げます。さらに自動化するには、値を下げます。その下の Learning Confidence と連動して機能します。", + "doc": "サイクル終了時のマッチスコアがこの値以上なら、確認なしでプログラムのラベルが付きます。確実性を高めるには上げ、より多くのサイクルを自動でラベル付けするには下げます。学習信頼度より高く保ってください。", "label": "自動ラベル信頼度" }, "auto_maintenance": { - "doc": "夜間のハウスキーピングを実行します。プロファイル エンベロープを再構築し、サイクルの健全性を再計算し、デバッグ トレースを整理し、最新のサイクルを保持します。", + "doc": "夜間のクリーンアップ: いずれかのプログラムに明確にマッチするようになった古いサイクルにラベルを付け、プログラムの曲線を再構築し、異常マークとサイクルの健全性を更新し、「デバッグトレースを保存」がオフならデバッグトレースを削除します。サイクルとその電力トレースは常に保持されます。", "label": "自動メンテナンス(夜間クリーンアップ)" }, "completion_min_seconds": { - "doc": "これより短いサイクルはゴースト サイクル (テスト実行、靴下を追加するためにドアを開ける) として破棄されます。", + "doc": "これより短いサイクル(テスト運転、靴下を追加するためにドアを開けた場合など)は「中断」として保存され、完了通知は送信されません。", "label": "サイクル最小時間" }, "delay_confirm_seconds": { - "doc": "アプライアンスが実行中ではなく起動待機中として扱われる前に、電力はこの時間スタンバイ帯域に留まる必要があります。", + "doc": "家電が開始待ちとみなされるには、電力がこの時間だけ待機電力の範囲にとどまる必要があります。", "label": "確認秒数" }, "delay_start_detect_enabled": { - "doc": "アプライアンスが電源オンで待機中 (遅延開始/スタンバイ) であるが、サイクルが開始されていないことを検出します。そのため、スタンバイ描画が実行サイクルと誤認されません。", + "doc": "電源が入っていて遅延スタートを待っている家電を検出し、待機電力を運転中のサイクルと誤認しないようにします。", "label": "遅延スタート検出を有効化" }, "delay_timeout_hours": { - "doc": "この時間が経過したら遅延開始モードでの待機を停止し、アイドル状態に戻ります。これにより、電源が入ったままのまま起動されなかったマシンが永遠に待機することがなくなります。", + "doc": "この時間が経過したら遅延スタートの待機をやめ、アイドル状態に戻ります。", "label": "タイムアウト時間" }, "device_type": { - "doc": "アプライアンスクラス。 Sets sensible detection defaults (thresholds, off-delay, end handling) tuned for that appliance type; change it only if the device was originally set up as the wrong type.", + "doc": "この種類の家電向けの検出のデフォルト(しきい値、オフ遅延、終了処理)を設定します。デバイスが間違った種類で設定された場合にのみ変更してください。", "label": "デバイスタイプ" }, "door_sensor_entity": { - "doc": "オプションのドアバイナリセンサー。サイクル後にアプライアンスが開かれた/アンロードされたことを検出するために使用されます。", + "doc": "任意のドアセンサーです。サイクル後に家電が開けられて中身が取り出されたことを示します。", "label": "ドアセンサーエンティティ" }, "duration_tolerance": { - "doc": "残り時間の推定値の許容範囲 (学習フィードバック、一致ではない)。実際の継続時間が推定値の +/-X% 以内であれば、良好な一致としてカウントされます。", + "doc": "学習フィードバック専用: 推定所要時間の +/-X% 以内のサイクルは良い推定とみなされます。マッチングには影響しません。", "label": "推定許容誤差" }, "enable_ml_models": { - "doc": "サイクルの実行中に、モデルにライブ結果を改良させます。つまり、より安定した残り時間とエネルギー/コストの推定、および終了検出時の早期停止防止ガード (終了を遅らせることしかできず、早期に終了することはなく、制限されています)。利用可能な場合は微調整されたモデルを使用し、それ以外の場合は組み込みモデルを使用します。オフ = 従来の電源ベースのロジックのみ (依然として信頼性があります)。", + "doc": "この機器で学習したモデルを使って、実行中のサイクルのエネルギーとコストを予測します。また、よく知っているプログラムが異常に早く止まった場合は、オフ遅延を最後まで待たずに終了させます。オフ: 電力ベースのロジックのみ。", "label": "サイクル中にスマートモデルを適用" }, "end_energy_threshold": { - "doc": "オフディレーのカウントダウン中に、蓄積されたエネルギー (ワット x 時間) がこのしきい値と比較されます。それを超えると、カウントダウンがリセットされ、しわ防止タンブルと食器洗い機乾燥テールを短くするのではなく、サイクルに取り付けたままにします。クールダウン中にサイクルが早すぎる場合は値を上げます。検出が遅い場合は値を下げます。", + "doc": "オフ遅延の間に許容されるエネルギーです。これを超えるとサイクルは続行されるため、タンブルや乾燥の終盤がサイクルに含まれたままになります。終盤の途中でサイクルが終わる場合は下げ、終了したサイクルが待ち続ける場合は上げてください。", "label": "終了エネルギー" }, - "end_repeat_count": { - "doc": "サイクルが終了するまでに必要な連続停止しきい値未満の読み取り値の数。ほとんどのプラグでは 1 で問題ありません。スマート プラグがサイクル途中で偽ゼロ サンプルを報告することがあり、サイクルが途中で終了する場合は、2 ~ 3 に上げます。", - "label": "終了繰り返し回数" - }, "energy_price_entity": { - "doc": "現在の kWh あたりの電力価格を示すセンサー(動的料金など)。価格を示すものである必要があり、プラグ自身の電力エンティティやエネルギーエンティティは指定できません。以下の静的価格よりも優先されるため、ここに kWh の積算メーターを指定すると、各サイクルが料金単価ではなくメーターの読み取り値で計算されてしまいます。「時間加重コスト」がオンの場合、各サイクルは動作していた各時点で有効だった価格で計算されます。オフの場合は、終了時に有効だった価格で確定されます。", + "doc": "現在の kWh あたりの価格を示すセンサー(動的料金など)。下の静的価格より優先されるため、プラグの電力センサーやエネルギーセンサーではなく、価格である必要があります。", "label": "エネルギー価格エンティティ" }, "energy_price_static": { - "doc": "上記のライブ価格エンティティが設定されていない場合、コスト数値に使用される kWh あたりの固定価格。", + "doc": "価格エンティティが設定されていないときに使う、kWh あたりの固定価格。", "label": "静的エネルギー価格(kWhあたり)" }, "energy_sensor": { - "doc": "積算エネルギーカウンター(total_increasing の kWh/Wh。例: プラグ自身の積算メーター)を任意で指定します。設定すると、各サイクルの表示エネルギーはこのカウンターの開始時から終了時までの差分から算出され、更新の遅い電力センサーを積分する際に生じる過少計上を回避できます。読み取り値が欠落している場合、単位が不明な場合、または差分が正でない場合は、積分値にフォールバックします。空欄のままにすると、電力センサーの積分を継続します。", + "doc": "任意の kWh または Wh カウンター(total_increasing。例: プラグ自身のメーター)。サイクルのエネルギーをここから読み取るため、更新の遅い電力センサーから計算するより正確です。読み取り値が使えない場合は計算値が使われます。", "label": "エネルギーメーターエンティティ" }, "expose_debug_entities": { - "doc": "追加の診断 HA エンティティ (一致信頼性、曖昧性、状態内部) を公開します。オフにすると、通常の使用のためにエンティティ リストがクリーンな状態に保たれます。", + "doc": "診断用エンティティ(マッチ信頼度、あいまいさ、内部状態)を追加します。オフにするとエンティティ一覧がすっきりします。", "label": "デバッグエンティティを公開" }, "external_end_trigger": { - "doc": "状態の変化がサイクルの終了を示すバイナリ センサー (例: アプライアンスの「完了」接触またはコンパニオン統合)。", + "doc": "変化したときにサイクルの終了を示すバイナリセンサー(例: 「完了」接点や他の統合)。", "label": "外部トリガーエンティティ" }, "external_end_trigger_enabled": { - "doc": "内蔵の電力ベースの検出に加えて、外部バイナリ センサーでサイクルの終了を知らせます。", + "doc": "電力ベースの検出に加えて、バイナリセンサーでサイクルの終了を知らせられるようにします。", "label": "外部終了トリガーを有効化" }, "external_end_trigger_inverted": { - "doc": "トリガー センサーが (オンではなく) オフになることをサイクル終了信号として扱います。", + "doc": "センサーがオンではなくオフになったときを終了信号として扱います。", "label": "外部トリガーを反転(OFFでトリガー)" }, "learning_confidence": { - "doc": "一致スコアがこの値と自動ラベル信頼度の間にある場合、識別されたプログラムの確認を求めるフィードバック通知が届きます。このスコアを下回ると、一致が不確実すぎるため表示されません。自動ラベル信頼度より低く保つ必要があります。", + "doc": "終了したサイクルにラベルを付ける最低マッチスコア。明確な 1 位にはラベルを付け、僅差の場合は「サイクル」のレビューキューに入ります。これを下回ると、ラベル付けも確認も行いません。自動ラベル信頼度より低く保ってください。", "label": "学習信頼度" }, "linked_device": { - "doc": "必要に応じて、この WashData デバイスを HA デバイス レジストリ内の別のデバイス (スマート プラグなど) の下にネストします (「接続経由 ...」と表示されます)。", + "doc": "任意。Home Assistant で、この WashData デバイスを別のデバイス(スマートプラグなど)の下に「... 経由で接続」として表示します。", "label": "デバイス下にグループ化" }, "min_off_gap": { - "doc": "マシンの電源がこの時間未満でオフになった場合、オン/オフ/オンのシーケンスは 1 つの連続したサイクルとして扱われます。浸しプログラム (洗濯中に数分間機械の電源がオフになる) が 2 つの別々のサイクルに分割されるのを防ぎます。連続した負荷を別々のサイクルとしてカウントしたい場合は、連続した負荷の間のギャップよりも短く設定します。デバイス タイプのデフォルトは、各アプライアンスの一般的なサイクル内の一時停止を保護します。", + "doc": "これより短いオフ期間ではサイクルが分割されないため、つけ置きの休止は 1 回の洗濯として扱われます。連続した洗濯の間隔より短く設定してください。長いと、それらが 1 つのサイクルにまとめられます。", "label": "最小オフギャップ" }, "min_power": { - "doc": "アクティブとみなされる絶対最小電力。これを下回る測定値は 0 W (スタンバイ) として扱われ、スマート プラグとスタンバイ LED のファントム負荷が除外されます。", + "doc": "これを下回る測定値は 0 W(待機)とみなされます。スマートプラグや待機 LED のわずかな消費電力を除外します。", "label": "最小電力" }, "ml_training_enabled": { - "doc": "レビューしたサイクルを一晩かけて定期的に調査し、この特定のマシンに合わせてモデルを微調整します。変更は、保持されたサイクルで本当にスコアが向上した場合にのみ保持されるため、これは役立つか、同じままであり、後退することはありません。", + "doc": "この機器の完了したサイクルを使って、エネルギーモデルを定期的に再学習します。新しいモデルは、テスト用サイクルで単純な推定を上回った場合にのみ採用されます。", "label": "この機器から学習" }, "ml_training_hour": { - "doc": "夜間の微調整を行うローカル時間 (0 ~ 23)。 2 (02:00) などの静かな時間を選択します。", + "doc": "夜間の学習を行う時刻(0-23)。2 など、静かな時間帯を選んでください。", "label": "学習時刻" }, "ml_training_interval_days": { - "doc": "改善点を再確認するのは、この日数に 1 回だけです。", + "doc": "再学習はこの日数に最大 1 回です。", "label": "最大確認間隔" }, "ml_training_min_cycles": { - "doc": "微調整する前に、少なくともこの数のサイクルが記録されるまで待ってください。これにより、そこから学ぶことが十分になります。", + "doc": "学習を始める前に、少なくともこの数のサイクルが記録されるまで待ちます。", "label": "必要なサイクル数" }, "name": { - "doc": "HA 統合リストおよびデバイス レジストリに表示される表示名。", + "doc": "Home Assistant に表示されるこのデバイスの名前。", "label": "デバイス名" }, "no_update_active_timeout": { - "doc": "実行中にこれほど長い時間電源アップデートが届かない場合は、プラグがオフラインになったとみなして強制停止し、ゾンビ サイクルを回避します。デフォルトの 600 秒では、クラウドまたはメッシュのラグが考慮されます。", + "doc": "サイクル中にこの時間だけ電力の更新が届かない場合、プラグがオフラインになったとみなしてサイクルを強制停止します。デフォルトの 600 秒は、クラウドやメッシュの遅延を考慮した値です。", "label": "更新なしタイムアウト" }, "notify_before_end_minutes": { @@ -1370,35 +1311,35 @@ "label": "終了前アラート" }, "notify_channel": { - "doc": "開始/ライブ メッセージの Android 通知チャネル名 (チャネルごとのサウンドとモバイル アプリの優先度を制御します)。空白はコンパニオン アプリのデフォルトを使用します。", + "doc": "開始メッセージとライブメッセージ用の Android チャネル。サウンドと優先度はチャネルで決まります。空欄: アプリのデフォルト。", "label": "Androidチャンネル(開始/ライブ)" }, "notify_cycle_timers": { - "doc": "サイクルの特定の分での通知 (柔軟剤の追加など)。メッセージは {device}、{program}、{minutes} をサポートしています。 [自動一時停止] を有効にすると、その時点で一時停止し、[再開] ボタンでインタラクティブな通知を受け取ります。パネル、一時停止/再開サービス、または通知アクションを介して再開します。", + "doc": "サイクル開始から指定した分数で送る通知(柔軟剤の投入など)。メッセージでは {device}、{program}、{minutes} を使えます。「自動一時停止」を使うと、その時点でサイクルも一時停止し、通知に「再開」ボタンが付きます。", "label": "サイクルタイマー" }, "notify_finish_channel": { - "doc": "終了メッセージの Android 通知チャネル名。空白の場合は、開始/ライブ チャネルを再利用します。", + "doc": "完了メッセージ用の Android チャネル。空欄の場合は開始/ライブのチャネルを使います。", "label": "Androidチャンネル(終了)" }, "notify_finish_message": { - "doc": "サイクル終了時に送信される本文。テンプレート変数: {device}、{duration}、{minutes}、{program}、{energy_kwh}、{cost}、{time_finished}、{vs_typical}、{cycle_count}。ここでは {time_finished} と {vs_typical} が最も役立ちます。", + "doc": "サイクル終了時に送信される本文。変数: {device}、{program}、{duration}(分)、{duration_hm}(1 h 05 min)、{energy_kwh}、{cost}、{time_finished}、{vs_typical}、{cycle_count}、{status}(completed または force_stopped。中断されたサイクルは何も送信しません)。それ以外の変数を使うと既定の本文になります。", "label": "終了メッセージ" }, "notify_finish_services": { - "doc": "Notice.* サイクルが終了すると呼び出されるサービス。ターゲットごとに 1 つ追加します。終了通知を行わない場合は空のままにします。", + "doc": "サイクル終了時に呼び出す notify.* サービス。不要な場合は空欄のままにします。", "label": "終了サービス" }, "notify_fire_events": { - "doc": "また、サイクルの開始時と終了時に ha_washdata_* イベントを発生させて、独自の自動化を構築できるようにします。", + "doc": "独自のオートメーション用に、サイクルの開始時と終了時に ha_washdata_* イベントも発行します。", "label": "通知用HAイベントを発火" }, "notify_icon": { - "doc": "通知に表示される小さなアイコン(例: mdi:washing-machine)。Android はステータスバーに表示し、iOS はアプリのアイコンの代わりに表示します(コンパニオンアプリ 2026.8 以降が必要)。プラットフォームのデフォルトを使う場合は空欄のままにします。", + "doc": "通知の小さなアイコン(例: mdi:washing-machine)。mobile_app の送信先にのみ送られ、notify グループには送られません。iOS ではアプリのアイコンの代わりに表示され、コンパニオンアプリ 2026.8 以降が必要です。空欄: プラットフォームのデフォルト。", "label": "通知アイコン" }, "notify_live_chronometer": { - "doc": "静的な推定値の代わりに、ライブ更新カウントダウン タイマーを通知 (サポートするプラットフォーム上) に表示します。", + "doc": "固定の推定値の代わりに、通知にリアルタイムのカウントダウンタイマーを表示します(対応している場合)。", "label": "ライブクロノメーターを使用" }, "notify_live_interval_seconds": { @@ -1406,143 +1347,135 @@ "label": "ライブ更新間隔" }, "notify_live_overrun_percent": { - "doc": "サイクルがこのパーセンテージを超えて推定を超えて実行される場合、オーバーラン アラートが送信されます。", + "doc": "サイクルが予想時間をこの割合だけ超えると、ライブ更新を停止します(20 = 予想時間の 1.2 倍まで)。", "label": "ライブ超過%アラート" }, "notify_live_services": { - "doc": "Notice.* サービスは、サイクルの実行中にライブ進行状況の更新を要求します。ライブ進行状況通知を無効にするには、空のままにします。", + "doc": "サイクル実行中のライブ進捗更新に使う notify.* サービス。オフにするには空欄のままにします。", "label": "ライブ進捗サービス" }, "notify_only_when_home": { - "doc": "リンクされたユーザー (上記) の少なくとも 1 人が在宅している場合にのみ通知を送信します。", + "doc": "上記の人のうち少なくとも 1 人が在宅しているときだけ通知します。", "label": "在宅時のみ通知" }, "notify_people": { - "doc": "person.* エンティティは、誰かが在宅しているかどうかを判断するために「在宅時のみ通知」によって使用されます。", + "doc": "「在宅時のみ通知」が確認する person.* エンティティ。", "label": "対象ユーザー(在宅時のみ)" }, "notify_pre_complete_message": { - "doc": "終了前 / まもなく完了アラートの本文。テンプレート変数: {device}、{duration}、{minutes}、{program}、{energy_kwh}、{cost}、{time_finished}、{vs_typical}、{cycle_count}。", + "doc": "ライブ進捗更新ごとの本文。テンプレート変数: {device}、{program}、{minutes}(残り分数)。それ以外の変数を使うと既定の本文になります。", "label": "完了前メッセージ" }, "notify_reminder_message": { - "doc": "取り出し待ちリマインダーの本文。空欄で組み込みの既定値を使用します。テンプレート変数: {device}、{duration}、{minutes}、{program}、{energy_kwh}、{cost}、{time_finished}、{vs_typical}、{cycle_count}。", + "doc": "1 回だけ送信される終了前アラートの本文。テンプレート変数: {device}、{program}、{minutes}(終了前アラートの設定値)。それ以外の変数を使うと既定の本文になります。", "label": "リマインダーメッセージ" }, "notify_start_message": { - "doc": "サイクル開始時に送信される本文。テンプレート変数: {device}、{duration}、{minutes}、{program}、{energy_kwh}、{cost}、{time_finished}、{vs_typical}、{cycle_count}。", + "doc": "サイクル開始時に送信される本文。テンプレート変数: {device}、{program}。それ以外の変数を使うと既定の本文になります。", "label": "開始メッセージ" }, "notify_start_services": { - "doc": "サイクルの開始時に呼び出される、notify.* サービス。ターゲット (電話、ダッシュボードなど) ごとに 1 つ追加します。開始通知を行わない場合は空のままにします。", + "doc": "サイクル開始時に呼び出す notify.* サービス(スマートフォン、ダッシュボードなど)。不要な場合は空欄のままにします。", "label": "開始サービス" }, "notify_timeout_seconds": { - "doc": "この秒数が経過すると、通知が自動的に閉じられます (通知をサポートしているプラ ットフォームの場合)。 0 は手動で終了するまで保持します。", + "doc": "この秒数が経過すると通知を閉じます(対応している場合)。0 にすると、自分で閉じるまで残ります。", "label": "自動消去まで" }, "notify_title": { - "doc": "通知のタイトル。テンプレート変数: {device}、{program}、{duration}、{energy}、{cost}、{date}、{time}、{minutes}。", + "doc": "通知のタイトル。置き換えられるのは {device} のみで、それ以外の変数を使うと既定のタイトルになります。", "label": "通知タイトル" }, "notify_unload_delay_minutes": { - "doc": "サイクルが終了してから、まだ待機中の「マシンをアンロードする」リマインダーを送信するまでの数分。リマインダーを無効にするには、0 を設定します。", + "doc": "サイクル終了から「中身を取り出してください」リマインダーを送るまでの分数。0 で無効になります。", "label": "取り出し通知遅延" }, "notify_unload_repeat": { - "doc": "取り出しが確認されるか、通知の \"リマインダーを停止\" をタップするまで、\"取り出し通知遅延\" 分ごとに取り出しリマインダーを再送信し続けます。取り出しを確認する手段が必要です: ドアセンサーエンティティ、取り出し確認エンティティ、または下の \"取り出しを手動で確認\" オプション。解除ボタンは Home Assistant コンパニオンアプリ(モバイル)の通知で機能します。", + "doc": "取り出しが確認されるか、「リマインダーを停止」をタップする(モバイルアプリ)まで、「取り出し通知遅延」の分数ごとに取り出しリマインダーを再送信します。ドアセンサーエンティティ、取り出し確認エンティティ、または「取り出しを手動で確認」が必要です。", "label": "取り出すまで繰り返す" }, "off_delay": { - "doc": "電力が低下した後、サイクルの終了を宣言するまで待機する時間。このウィンドウ内で電力が再開されると、サイクルはシームレスに継続されます。これにより、洗浄段階の間で一時停止が生じます。食器洗い機には長い乾燥段階 (20 ~ 60 分間電源がオフになる) があるため、洗浄と乾燥全体を 1 サイクルとして維持するには、オフディレーをそれを超える必要があります。", + "doc": "サイクルが終了とみなされるまでに、電力が低いままでいる必要がある時間です。この時間内に電力が戻ればサイクルは続きます。食器洗い機は乾燥中に 20-60 分止まることがあるため、それより長く設定してください。", "label": "オフ遅延" }, "pause_cuts_power": { - "doc": "サイクルが一時停止されている場合は、以下のスイッチ エンティティもオフにします。サイクルの途中でプラグを安全に切断できる器具に限ります。", + "doc": "サイクルが一時停止されたときに、下のスイッチエンティティもオフにします。サイクルの途中で電源を切っても安全な家電にのみ使ってください。", "label": "一時停止で電力もカット(スイッチ経由)" }, "power_off_delay": { - "doc": "サイクル完了後、状態がオフに戻るまでに、電力が電源オフしきい値を下回ったまま維持しなければならない時間です。電源オフしきい値が 0 より大きい場合にのみ使用されます。バックグラウンドの周期でチェックされるため、実際の遅延は次の状態失効タイミングまで切り上げられます。", + "doc": "サイクル後、状態がオフに戻るまでに、電力が電源オフしきい値を下回ったままでいる必要がある時間です。そのしきい値が 0 より大きい場合にのみ使われます。定期的にチェックされるため、実際の待ち時間は少し長くなることがあります。", "label": "電源オフ遅延" }, "power_off_threshold_w": { - "doc": "消費電力に基づくオフ検出(任意)。0 より大きい値に設定すると、サイクルが完了した後、電源オフ遅延の間、電力がこのレベルを下回ったままになると、機器はスイッチが切られたとみなされ、状態がオフに戻ります。無効にするには 0 のままにします(既定値)。実際にスイッチを切ったときの下限より高く、停止しきい値と、機器が完了後もオンのままの待機時消費電力より低い値に設定してください。停止しきい値を下回っていない場合は無視されます。有効にすると、オフに戻す処理で進捗リセット遅延の代わりに使用されるため、完了した機器は実際に電源が切られるまで完了/クリーンのままになります。", + "doc": "サイクル後、電力が電源オフ遅延の間この値を下回ると、進捗リセット遅延の代わりに状態をオフにします。スイッチオフ時の消費電力より高く、待機電力と停止しきい値より低く設定してください。0 で無効(デフォルト)。", "label": "電源オフしきい値" }, "power_sensor": { - "doc": "このアプライアンスのライブ電力をワット単位で報告するセンサー エンティティ (例: sensor.washer_power)。すべてのサイクル検出はこの信号に基づいています。", + "doc": "この家電のリアルタイムの電力をワット単位で報告するセンサー(例: sensor.washer_power)。すべての検出はこのセンサーに基づきます。", "label": "電力センサー" }, - "profile_duration_tolerance": { - "doc": "マッチング中に使用されるプロファイル平均継続時間の周囲の +/- バンド。 0.25 は、60 分のプロファイルが 45 ~ 75 分のサイクルに一致することを意味します。", - "label": "プロファイル時間許容誤差" - }, "profile_match_interval": { - "doc": "実行サイクル中にプロファイルの一致を試行する頻度。デフォルトの 300 秒 (5 分) は、検出速度と CPU のバランスをとります。", + "doc": "実行中のサイクルをプロファイルとマッチングする頻度。デフォルト 300 秒(5 分)。", "label": "マッチ間隔" }, "profile_match_max_duration_ratio": { - "doc": "プロファイルに対する最大サイクル長。 1.3 は、サイクルがプロファイル期間の 130% 未満でなければ一致しないことを意味します。", + "doc": "サイクルがプログラムの標準的な所要時間のこの倍数を超えると、そのプログラムは候補から外れます。1.8: プログラムより 80% 長いサイクルはマッチしなくなります。", "label": "最大時間比率" }, "profile_match_min_duration_ratio": { - "doc": "プロファイルに対する最小サイクル長。 0.9 は、サイクルがプロファイル期間の少なくとも 90% でな ければ一致しないことを意味します。", + "doc": "サイクルがプログラムの標準的な所要時間のこの割合に達した後にのみ、そのプログラムが候補になります。0.10: 2 時間のプログラムは 12 分後に候補になります。サイクルの最初の 15 分間は適用されません。", "label": "最小時間比率" }, "profile_match_threshold": { - "doc": "プログラム識別を受け入れるためにサイクル終了時に必要な最小類似性スコア (0 ~ 1)。誤った識別を減らすには値を上げます。マシンのプログラムが一致しない場合は、値を下げてください。デフォルトの 0.4 は控えめな開始点です。", + "doc": "スマート終了やしわ防止確定がサイクルを早期に終了するために必要なライブマッチスコアです。表示されるプログラムは変わりません。弱いマッチでサイクルが早く終わる場合は上げ、明確なマッチでもオフ遅延を待つ場合は下げてください。", "label": "マッチしきい値" }, "profile_unmatch_threshold": { - "doc": "ライブのミッドサイクルマッチがこのスコアを下回った場合、暫定的な識別はクリアされます。類似性が一時的に下がっても表示が不一致に戻らないように、一致しきい値より少し下に保ちます。", + "doc": "ライブマッチスコアがこの値を下回ると、現在のプログラムの推定がクリアされます。一時的な低下でリセットされないよう、マッチしきい値より少し低くしてください。", "label": "アンマッチしきい値" }, "progress_reset_delay": { - "doc": "終了後、アイドル状態にリセットする前にダッシュボードに「完了」が表示されるように、進行状況を 100% のままにしておきます。", + "doc": "サイクル後、アイドルにリセットするまで進捗を 100% のまま保つ時間。", "label": "進捗リセット遅延" }, "pump_stuck_duration": { - "doc": "ポンプがスタックしている可能性があるとフラグが立てられる (スタックポンプ イベントが発生する) までに、ポンプが継続的に実行できる秒数。", + "doc": "ポンプスタックイベントが発生するまでに、ポンプが連続して動作できる時間。", "label": "ポンプ停止継続時間" }, "sampling_interval": { - "doc": "センサー読み取り間の予想時間 - スムージング ウィンドウのサイズを調整し、デバウンスを正しく開始するために使用されます。この値に関係なく、すべてのセンサーの更新がキャプチャされます。下流の計算のみを調整します。提案エンジンは、過去のサイクルからセンサーの実際のケイデンスを測定し、これを自動的に設定します。", + "doc": "処理する測定値どうしの最小間隔です。これより早く届いた測定値はスキップされます(サイクル中の低い測定値は常に保持)。プラグの報告間隔以下に設定してください。大きい値は測定値を捨てるだけです。", "label": "サンプリング間隔" }, "save_debug_traces": { - "doc": "各サイクルの全電力トレースと一致するデバッグ データを保存します。トラブルシューティングには役立ちますが、ストレージ サイズが増加します。", + "doc": "トラブルシューティング用に、各サイクルとともにマッチングのデバッグデータ(すべての候補のスコア)も保存します。1 サイクルあたり約 20 KB 増えます。", "label": "デバッグトレースを保存" }, "smart_termination_duration_ratio": { - "doc": "一致したプログラムの想定所要時間のうち、どこまでサイクルが進んでから、電力低下時にスマート終了がサイクルを早期に終了できるかを指定します。想定所要時間はそのプログラムの平均値のため、実行時間が大きく変動する家電 (冬の冷たい給水と夏の温かい給水で変わる洗濯機、センサー乾燥式の乾燥機、負荷に依存するプログラムなど) では、全実行の約半数が平均より短く終わり、早期終了が働かず、フォールバックのタイムアウトによって数分遅れて終了するだけになります。こうしたマシンではこの値を下げて (例: 0.85)、早期終了が確実に作動するようにし、より慎重にしたい場合は 1.0 に近づけます。デフォルト (0.98、食器洗い機は 0.99) にするには空欄のままにします。サイクルを早めに終了することしかできず、遅らせることは決してなく、あいまいな一致や信頼度の低い一致では作動しません。", + "doc": "スマート終了がサイクルを早期に終了できるようになるまでに、プログラムの平均所要時間に対して到達すべき割合です。運転時間がばらつく場合(水温、センサー乾燥、洗濯物の量)は下げてください(例: 0.85)。空欄で 0.98(食器洗い機は 0.99)。", "label": "スマート終了比率" }, - "smoothing_window": { - "doc": "生の電力信号がどの程度平滑化されるか。低 (2) は応答性は高くなりますが、ノイズが多くなります。高 (5) はスパイクを滑らかにしますが、ラグが追加されます。", - "label": "スムージングウィンドウ" - }, "start_duration_threshold": { - "doc": "実際の開始を確認するには、電力が開始しきい値をこの期間上に維持する必要があり、一瞬のオン/オフの切り替えによるサイクルの開始が防止されます。", + "doc": "サイクルが開始するには、電力がこの時間だけ開始しきい値を上回ったままでいる必要があります。短いオン/オフの変動は無視されます。", "label": "開始継続時間" }, "start_energy_threshold": { - "doc": "アプライアンスが実行する前に消費しなければならないエネルギー (電力 x 時間)。短時間の高出力スパイクはエネルギーが非常に低いため無視され、誤った始動が防止されます。", + "doc": "サイクルが運転中とみなされるまでに家電が消費する必要があるエネルギーです。短い電力スパイクはエネルギーが少ないため、サイクルを開始しません。", "label": "開始エネルギー" }, "start_threshold_w": { - "doc": "サイクルが開始されたことを確認するには、電力がこのレベルを超える必要があります。設定が低すぎると、スタンバイ電源からの誤った起動が発生します。高すぎると、起動が遅いプログラム (コールドフィル) が失われます。提案エンジンは、これをマシンの観測された最低有効電力のすぐ上に設定します。", + "doc": "サイクルを開始するには、電力がこの値を超える必要があります。低すぎると待機電力で誤スタートが起き、高すぎると立ち上がりの遅いプログラム(冷水給水)を見逃します。", "label": "開始しきい値" }, "stop_threshold_w": { - "doc": "オフディレーのカウントダウンが開始される前に、電力はこのレベルを下回る必要があります。開始しきい値よりも低く設定します。それらの間のギャップは、ちらつきを防ぐヒステリシス帯域です。設定が高すぎると、低電力フェーズ (すすぎ保留、しわ防止) が誤って終了シーケンスをトリガーします。", + "doc": "オフ遅延のカウントが始まるには、電力がこの値を下回る必要があります。開始しきい値より低くしてください。高すぎると、低電力のフェーズ(すすぎの待機、しわ防止)で誤って終了のカウントダウンが始まります。", "label": "停止しきい値" }, "switch_entity": { - "doc": "オプションのスイッチは、一時停止時にオフになり、再開時に再びオンに切り替わります。「一時停止すると電源も切断されます」と併用します。", + "doc": "「一時停止で電力もカット」と併用する任意のスイッチで、一時停止時にオフ、再開時にオンになります。", "label": "スイッチエンティティ" }, "watchdog_interval": { - "doc": "バックグラウンド ウォッチドッグがセンサーの停止とタイムアウトの経過をチェックする頻度。デフォルトは 30 秒です。", + "doc": "バックグラウンドチェックが、停止したセンサーやタイムアウトを確認する頻度。デフォルト 30 秒。", "label": "ウォッチドッグ間隔" }, "notify_milestone_message": { @@ -1550,139 +1483,94 @@ "label": "マイルストーンメッセージ" }, "notify_milestones": { - "doc": "達成時に一度だけお祝い通知を送るサイクル数をカンマ区切りで指定します (例: 50, 100, 500, 1000)。空欄でマイルストーン通知を無効にします。", + "doc": "一度だけお祝い通知を送るサイクル数をカンマ区切りで指定します(例: 50, 100, 500, 1000)。空欄で無効になります。", "label": "サイクルマイルストーン" }, "notify_quiet_end_hour": { - "doc": "サイレント時間帯の終了 (0-23)。保留中の通知はこの時刻に配信されます。空欄で無効にします。", + "doc": "サイレント時間帯の終了 (0-23)。保留中の通知はこの時刻に送信されます。空欄で無効になります。", "label": "サイレント時間の終了" }, "notify_quiet_start_hour": { - "doc": "サイレント時間帯の開始 (0-23)。サイレント時間中に発生する終了・リマインダー・洗濯完了の通知は保留され、時間帯の終了時に配信されます。空欄で無効にします。深夜をまたぐ時間帯にも対応 (例: 開始 22、終了 7)。", + "doc": "サイレント時間帯の開始 (0-23)。この時間帯の終了・リマインダー・洗濯完了の通知は、時間帯が終わるまで保留されます。深夜をまたぐこともできます(例: 22 から 7)。空欄で無効になります。", "label": "サイレント時間の開始" }, "peak_rate_message": { - "doc": "開始通知に追加するピーク料金のヒント用のカスタムテキスト (任意)。テンプレート変数: {device}、{price}。空欄で組み込みの既定値を使用します。", + "doc": "ピーク料金のヒントのカスタムテキスト。テンプレート変数: {device}、{price}。空欄でデフォルトを使います。", "label": "ピーク料金メッセージ" }, "peak_rate_threshold": { - "doc": "サイクル開始時に現在の kWh あたりの価格がこの値以上の場合、開始通知にピーク料金のヒントを追加します。0 または空欄でヒントを無効にします。", + "doc": "サイクル開始時の kWh あたりの価格がこの値以上なら、開始通知にピーク料金のヒントを追加します。0 または空欄で無効になります。", "label": "ピーク料金のしきい値 (kWh あたり)" }, - "dtw_bandwidth": { - "label": "形状マッチングの許容度", - "doc": "保存済みプログラムの形状に合わせるために、マッチャーがサイクルをどれだけ時間ワーピングできるかを決めます。値を大きくすると速度の変動をより許容しますが、大きすぎると誤ったプログラムにマッチするおそれがあります。" - }, - "corr_weight": { - "label": "形状とレベルの重み", - "doc": "マッチのスコアリング時に、曲線の形状と絶対的な電力レベルのどちらを重視するかのバランスです。値を大きくすると形状を、小さくすると電力レベルの一致を重視します。" - }, - "duration_weight": { - "label": "実行時間の重み", - "doc": "一般的な実行時間がこのサイクルと一致するプログラムを、どれだけ優先するかを決めます。" - }, - "energy_weight": { - "label": "エネルギーの重み", - "doc": "一般的な消費電力量がこのサイクルと一致するプログラムを、どれだけ優先するかを決めます。" - }, "store_brand": { "label": "家電ブランド", - "doc": "任意。コミュニティカタログから選択した家電ブランドです。一致する基準録音の検索と共有に使用されます。オンライン機能を使用しない場合は空欄のままにしてください。" + "doc": "任意。コミュニティカタログから選ぶ家電のブランドで、基準録音の検索と共有に使います。オンライン機能を使わない場合は空欄のままにしてください。" }, "store_model": { "label": "家電モデル", - "doc": "任意。ブランドを設定すると、コミュニティカタログから選択できる家電モデルです。お使いのモデルが一覧にない場合は、カタログに追加できます。" + "doc": "任意。コミュニティカタログから選ぶ家電のモデルです(先にブランドを選択)。一覧にない場合は追加できます。" }, "show_contributor": { "doc": "コミュニティの家電および基準サイクルに「by 」のクレジット表示をします。" }, - "enable_phase_matching": { - "label": "フェーズを考慮した残り時間", - "doc": "実行中の各サイクルをフェーズ(加熱、洗い、脱水)に分割し、フェーズごとに残り時間を見積もって従来の推定値とブレンドします。サイクルの序盤ではフェーズごとの見積もりを重視し、終盤に近づくにつれて従来の推定値を重視します。これにより、お使いの機器が実際に加熱・運転する時間に合わせてカウントダウンが調整され、サイクルの前半で最も効果が表れます。オフ = 従来の推定値のみ。影響を受けるのは残り時間の表示だけで、プログラムのマッチングとサイクル検出は変わりません。" - }, - "keep_min_score": { - "label": "最低マッチスコア", - "doc": "候補がマッチング競合に残るために必要な最低類似度スコア。デフォルトの 0.1 は意図的に緩やかで、弱い候補も許可し、最良のマッチングを後のステージに委ねます。可能性の低いプロファイルを早期に除外するには値を上げ、候補プールを広げるには値を下げてください。" - }, - "dtw_blend": { - "label": "ワープブレンド", - "doc": "時間ワーピング (DTW) アライメントスコアがステージ 2 コアスコアをどれほど置き換えるか。0 = ステージ 2 スコアのみ使用、1 = DTW スコアのみ使用、0.5 (デフォルト) = 等割合ブレンド。プログラムの電力レベルが似ていても時間パターンが異なる場合、ワープアライメントへの依存度を高めるために値を上げてください。" - }, - "dtw_ensemble_w": { - "label": "ワープアンサンブル混合", - "doc": "アンサンブル DTW モード(デフォルト)において、スケール付き L1 と微分 DTW (DDTW) の重み配分。1.0 = スケール付き-L1 のみ、0 = DDTW のみ、0.7 = デフォルト。スケール付き-L1 変種は電力レベルを比較し、微分変種は電力がどう変化するかに反応します。アンサンブルは両者を組み合わせます。" - }, - "dtw_ddtw_scale": { - "label": "微分ワープスケール", - "doc": "微分 DTW スコアリングの半飽和距離。DDTW 距離がこの値と等しいとき、スコアが半分になります。小さいほど形状の差異に敏感になります。デフォルトの 30 は一般的な家電製品の電力トレースに合わせて調整されています。アンサンブルおよび ddtw DTW モードのみに影響します。" - }, - "dtw_refine_top_n": { - "label": "ワープ精製候補数", - "doc": "DTW によって再スコアリングされるステージ 2 上位候補の数。DTW は計算コストが高いため、上位 N 候補にのみ適用されます。デフォルト 5。ステージ 2 後に正しいプロファイルが 4 位または 5 位にしかならない場合は値を上げてください (7-9 程度) - DTW がそれを救えます。値を下げるとマッチングが若干速くなります。" - }, - "duration_scale": { - "label": "継続時間シャープネス", - "doc": "ステージ 4 の運転時間一致ペナルティの厳格さ。一致スコアが半分になる対数比率を指します。小さいほど厳格で、時間のズレはより大きな影響を与えます。デフォルト 0.175 は半ウェイト時に約 18% の許容誤差に相当します。運転時間の重みと組み合わせて使用してください。" - }, - "energy_scale": { - "label": "エネルギーシャープネス", - "doc": "ステージ 4 のエネルギー一致ペナルティの厳格さ。小さいほど厳格で、エネルギーのズレはより大きな影響を与えます。デフォルト 0.25 はエネルギーが負荷によって変動するため、時間の厳格さよりも意図的に緩やかに設定されています。エネルギーの重みと組み合わせて使用してください。" - }, "power_profile_interval_min": { "label": "電力プロファイル間隔", - "doc": "EMHASS や tibber_prices などの外部プランナーが使用する per-profile の power_profile センサー属性(スロットごとの平均ワット数の配列)のバケットサイズ。小さいバケットは短い電力スパイクを鮮明に保ち、大きいバケットは形状を平滑化します。デフォルト 15 分。読み取り時のみ使用され、検出には影響しません。" + "doc": "EMHASS や tibber_prices などのプランナーが使う power_profile 属性(スロットごとの平均ワット数)のバケットサイズ。小さいと短いスパイクが残り、大きいと曲線が平滑になります。デフォルト 15 分。検出には影響しません。" }, "notify_live_silent": { "label": "サイレントなライブ更新", - "doc": "iOS のみ。ライブ進捗を静かに更新します。各更新はサウンドや振動なしで届き、バッテリーを節約する低い優先度で送信されるため、iOS がまとめて配信することがあります。サイクルの最初の更新(ライブアクティビティを開始する更新)と開始/完了の通知は、これまでどおり通知されます。すべての更新で通知を受け取りたい場合は、これをオフにしてください。" + "doc": "iOS のみ。ライブ進捗を静かに更新します。バッテリーを節約する優先度で送られるため、iOS がまとめて配信することがあります。最初の更新と開始/完了の通知は通常どおり通知されます。オフ: すべての更新で通知されます。" }, "notify_live_sticky": { "label": "タップ後もライブ通知を維持", - "doc": "Android のみ。実行中のスレッドをタップしても通知が消えないよう、ライブ進捗通知を固定(スティッキー)にします。オフにすると、タップで通知が消えるデフォルトの動作になります。" + "doc": "Android のみ。ライブ進捗通知をタップしても消えないようにします。オフ: タップすると消えます。" }, "notify_live_click_action": { "label": "通知のタップ先", - "doc": "この家電の通知をタップしたときに開く場所を、Android と iOS の両方で指定します。空白のままにすると、この家電を表示した WashData パネルが開きます。別の場所を開くにはダッシュボードのパス(例: /lovelace/laundry)またはフル URL を入力し、タップ先を設けない場合は「none」と入力します。" + "doc": "この家電の通知をタップしたときに開くもの。空欄: この家電を表示した WashData パネル。ダッシュボードのパス(例: /lovelace/laundry)か URL を入力するか、タップ時の動作をなしにするには「none」と入力します。" }, "door_opens_at_end": { "label": "終了時に自動でドアが開く", - "doc": "サイクル終了後にドアが自動的に開いて乾燥する食器洗い機(AirDry など)向けの設定です。有効にすると、実行中のサイクルでドアが開いてもサイクルが永久に一時停止されなくなります。代わりに、ドアが下の待機時間以上開いた状態が続いた場合、WashData はサイクルが完了したと判断します。一瞬の開け閉め(食器の追加など)は無視されます。ドアセンサーエンティティが必要です。" + "doc": "終了時にドアを開けて乾燥させる食器洗い機(AirDry など)向けです。下の待機時間だけドアが開いたままになると、一時停止ではなくサイクルを終了します。短い開閉は無視されます。ドアセンサーエンティティが必要です。" }, "door_end_dwell_seconds": { "label": "ドア開放の終了待機時間", - "doc": "「終了時に自動でドアが開く」が有効のとき、WashData がサイクルを終了するまでドアが開いた状態を維持する必要がある時間。食器を素早く追加する場合(すぐ閉めるだけ)を無視できるほど長く(デフォルト 60 秒)、機器がドアを開けた後に迅速に終了できるほど短く設定します。" + "doc": "「終了時に自動でドアが開く」がオンのとき、サイクルが終了するまでにドアが開いたままでいる必要がある時間です。食器を追加する程度の開閉を無視できる長さにします(デフォルト 60 秒)。" }, "dishwasher_end_spike_quiet_release": { "label": "パッシブ乾燥の静音解除", - "doc": "サイクルが予想所要時間を過ぎた後、WashData が最終排水を待つのをやめてサイクルを終了するまでに、食器洗い機が静音状態(停止しきい値を下回る状態)を維持しなければならない時間です。遅れて発生する最終排水の前に長い無音の乾燥フェーズがあり、その排水が見逃されている場合は、この値を上げてください。ウィンドウを広げると、学習された所要時間が古い平均値に固定されず、季節変動(給水温度が低い=サイクルが長くなる)に追従できます。この設定は、内部の 30 分間の終了スパイク待機上限に対して待機時間を短縮することしかできず、延長することはありません。" + "doc": "予想所要時間を過ぎた後、WashData が最終排水を待つのをやめるまでに、食器洗い機が停止しきい値を下回ったままでいる必要がある時間です。長い乾燥の後の遅い排水が見逃される場合は上げてください。待機は 30 分を超えることはありません。" }, "profile_evidence_sources": { "label": "プログラムを形づくるサイクル", - "doc": "各プログラムの電力カーブを作成し、完了したサイクルをそれと照合するために使用するサイクルの種類です。種類のチェックを外すと、その種類はプログラムに反映されなくなりますが、何も削除されません。サイクルはサイクル一覧に残り、ラベル付けや削除も引き続きできます。インポートしたデータを信頼できない場合に便利です。統計には影響しません。統計は常に、この機器が実際に実行したサイクルのみを数えます。すべてのチェックを外した場合は無視されます。裏付けとなるサイクルがないプログラムは、決してマッチングできないためです。" + "doc": "各プログラムの曲線とマッチングに使うサイクルの種類です。チェックを外しても何も削除されません。インポートしたデータを信頼できない場合に便利です。統計は常にこの機器のサイクルだけを数えます。すべて外した場合は無視されます。" }, "energy_price_dynamic": { "label": "時間加重コスト", - "doc": "終了時点の価格ひとつで計算するのではなく、サイクルが動作していた各時点で有効だった価格で料金を計算します。価格エンティティを設定している場合のみ有効です(静的価格は変動しないため)。追加の設定は不要です。サイクルの実行中は価格をリアルタイムで追跡し、診断の「履歴の処理」を実行すると過去のサイクルも recorder の記録から再計算されます。オフ = 従来どおり、終了時の価格ひとつで確定します。" + "doc": "終了時の価格ではなく、サイクルが動作していた各時点の価格で料金を計算します。価格エンティティが必要です。過去のサイクルは診断の「履歴の処理」で再計算されます。" }, "anti_crease_finalize_ratio": { "label": "しわ防止確定比率", - "doc": "一致したプログラムの想定所要時間のうち、サイクルがどこまで進んだら、WashData が「いま見えている低電力のタンブルはしわ防止の末尾である」と判断してそこでサイクルを終了してよいかを表す割合です。想定所要時間はそのプログラムの平均値なので、センサー乾燥で運転時間が洗濯物の量に左右される乾燥機では、ほとんどの運転がその 98% に届かず、末尾はフォールバックのタイムアウトまで残り続けます。実際に機械が停止してから 15 ~ 40 分後になることもよくあります。そのような乾燥機ではこの値を下げてください(例: 0.75)。洗濯機はデフォルトのままにしてください。洗濯機はサイクルの大半を「しわ防止最大電力」より下で過ごすため、静かな洗濯途中のフェーズと末尾を隔てているのはこの割合だけであり、下げると洗濯が途中で半分に切られてしまうことがあります。空欄にするとデフォルト(0.98)になります。これは、別の早期終了判定を制御する「スマート終了比率」とは異なる設定です。" + "doc": "低電力のタンブルがしわ防止の末尾とみなされるまでに、サイクルがプログラムの平均所要時間に対して到達すべき割合です。負荷検知式の乾燥機では下げてください(例: 0.75)。洗濯機はデフォルト(0.98)のままにしてください。下げると洗濯が途中で終了することがあります。" }, "curve_preroll_seconds": { "label": "曲線のプリロール", - "doc": "中断された開始試行の測定値を、サイクルの曲線の先頭に何秒ぶん取り込めるかを指定します。サイクルの曲線は通常、最終的に成功した開始試行から始まります。そのため、プログラム選択、ドアロック、最初の給水などで数回試してから安定するマシンでは、実際の動作の最初の数分がどの曲線からも欠落します。お使いのマシンで最も長い立ち上がりより少し大きい値を設定してください(多くの場合 300 秒あれば十分です)。0 にすると無効になります。有効にすると記録される開始時刻が前倒しになるため、変更の前後に記録されたサイクルは同じプログラムでも所要時間が異なります。古いサイクルが入れ替わるまで、学習された平均値はずれ続けます。" + "doc": "開始前に何度か試行するマシン(ドアロック、最初の給水など)向けに、失敗した開始試行の測定値をサイクルの先頭に追加します。ほとんどの場合 300 秒で足ります。0 = オフ。記録される開始時刻が早まるため、学習された所要時間はしばらくずれます。" }, "unload_confirm_entity": { "label": "取り出し確認エンティティ", - "doc": "ドアセンサーを取り付けられない機器向けに、作動したら中身を取り出したとみなすエンティティ(任意)。種類は問いません: Zigbee や NFC のボタン、input_button ヘルパー、機器の前に置いたモーションセンサー、シーンなど。オフになった場合と利用不可になった場合を除き、すべての状態変化が確認とみなされます。これを設定すると、ドアセンサーエンティティのないデバイスでも取り出しリマインダーが有効になります。" + "doc": "任意。ドアセンサーのない機器向けに、中身を取り出したことを確認するエンティティ(ボタン、input_button、モーションセンサー、シーンなど)。オフや利用不可への変化を除き、すべての状態変化が確認とみなされます。取り出しリマインダーも有効になります。" }, "unload_track_without_door": { "label": "取り出しを手動で確認", - "doc": "ドアセンサーエンティティも取り出し確認エンティティもないデバイスで取り出しリマインダーを動作させ、代わりに自作のオートメーションから取り出しを確認します: \"取り出し済みとしてマーク\" ボタンエンティティを押すか、ha_washdata.mark_unloaded サービスを呼び出します。ドアセンサーエンティティが設定されている場合は、すでに信号が得られるため無視されます。" + "doc": "ドアセンサーや確認エンティティなしで取り出しリマインダーを動作させます。自作のオートメーションから「取り出し済みとしてマーク」ボタンまたは ha_washdata.mark_unloaded サービスで取り出しを確認します。ドアセンサーエンティティが設定されている場合は無視されます。" }, "notify_icon_color": { "label": "通知の色", - "doc": "この家電の通知に使うアクセントカラー。洗濯機、乾燥機、食器洗い機をひと目で見分けられます。iOS はアイコンと、ロック画面や Dynamic Island に表示されるライブアクティビティの進行状況バーに色を付けます。Android にも同じ色が送られますが、それをどう扱うかは端末次第で、アイコンに色を付けるものもあれば、まったく無視するもの(特に Samsung One UI)も多くあります。16 進数(例: #03A9F4)はどちらでも使えます。プラットフォームのデフォルトを使う場合は空欄のままにします。" + "doc": "この家電の通知のアクセントカラー。洗濯機、乾燥機、食器洗い機を見分けやすくなります。iOS ではアイコンの背後の円を塗りつぶし、ライブアクティビティのバーに色を付けます。多くの Android 端末(特に Samsung)は無視します。16 進数で指定します(例: #03A9F4)。空欄: プラットフォームのデフォルト。" + }, + "share_name": { + "doc": "共有するサイクルのアップロード者として、アカウント名を表示します。オフ: 名前なしで共有されます。" } }, "setting_group": { @@ -1710,9 +1598,6 @@ "door_pause": { "label": "ドアと一時停止" }, - "dtw_refinement": { - "label": "DTW精度向上" - }, "duration_gates": { "label": "時間ゲート" }, @@ -1789,13 +1674,18 @@ "rebuilding": "再構築中…", "analyzing": "分析中…", "resetting": "リセット中…", - "reverting": "元に戻しています…", "trimming": "トリム中…", "splitting": "分割中…", + "stalled": "停滞中", "deleting": "削除中…", "imported": "インポート済み", "preparing": "準備中…", - "unlabelling": "ラベル解除中…" + "unlabelling": "ラベル解除中…", + "working": "処理中…", + "match_uncertain": "不確定: {top} または {runner}", + "match_uncertain_single": "不確定: {top} の可能性", + "match_sure": "確度 約{pct}%", + "match_uncertain_tip": "WashData はまだプログラムを確定していません。この数値は、過去のサイクルで最有力候補の {top} がこれだけリードしていたときに正しかった割合です。参考情報です。" }, "tab": { "advanced": "高度な", @@ -1826,13 +1716,17 @@ }, "toast": { "catalog_refreshed": "コミュニティカタログを更新しました", + "task_name_required": "タスクに名前を付けてください", + "task_added": "タスクを追加しました", + "task_save_failed": "タスクを保存できませんでした: {error}", + "task_removed": "タスクを削除しました", + "maint_logged": "完了として記録しました: {task}", "access_saved": "アクセス制御が保存されました", "all_wiped": "すべてのデータが消去されました", "analysis_complete_none": "分析が完了しました: 新しい提案はありません", "auto_label_complete": "自動ラベル付け完了", "automation_deleted": "オートメーションが削除されました", "correction_submitted": "修正を提出しました", - "cycle_deleted": "サイクルが削除されました", "cycle_force_stopped": "サイクル強制停止", "cycle_labelled": "サイクルラベル付き", "cycle_paused": "サイクルが一時停止されました", @@ -1848,11 +1742,9 @@ "group_deleted": "グループが削除されました", "group_name_required": "グループ名は必 須です", "group_saved": "グループが保存されました", - "import_successful": "インポートは成功しました。統合のリロード", "json_required": "JSONデータは必須です", "legacy_removed": "従来のアクションが削除されました", "logs_exported": "エクスポートされたログ", - "matching_reverted": "マッチングウェイトがデフォルトに戻った", "min_2_profiles": "グループに対して少なくとも 2 つのプロファイルを選択してください", "ml_training_no_improvement": "トレーニング完了: ベースラインは維持 (改善なし)", "models_reverted": "オンデバイスモデルがベースラインに戻った", @@ -1862,8 +1754,8 @@ "pg_preset_loaded": "プリセット「{name}」を読み込みました", "pg_preset_saved": "プリセット「{name}」を保存しました", "pg_sugg_loaded": "{n} 件の提案値を適用しました - プレイグラウンドで比較してください", - "pg_sugg_ml_loaded": "{n} 件の ML 調整済み値を適用しました - プレイグラウンドで比較してください", "phase_name_required": "フェーズ名は必須です", + "phase_reset": "「{name}」を組み込みのフェーズに戻しました", "phase_updated": "フェーズが更新されました", "phases_saved": "保存されたフェーズ", "preferences_saved": "設定が保存されました", @@ -1910,14 +1802,10 @@ "store_name_required": "プロファイル名を入力してください", "store_import_failed": "インポートに失敗しました: {error}", "store_imported": "{profile} にインポートしました", - "store_program_required": "プログラム名を入力してください", "store_no_appliance": "先に設定で家電のブランドとモデルを設定してください。", "store_share_failed": "共有に失敗しました: {error}", "store_shared": "コミュニティストアに共有しました(審査待ち)", "appliance_added": "家電を追加しました(承認待ち)", - "thanks_confirming": "確認していただきありがとうございます", - "device_approved": "コミュニティによって承認されました", - "rating_saved": "品質評価を保存しました", "brand_added": "ブランドを追加しました(承認待ち)", "profile_added": "プロファイルを追加しました(承認待ち)", "saved_except_conflicts": "保存しました。残りを保存するにはハイライトされた競合を修正してください。", @@ -1941,11 +1829,48 @@ "odometer_save_failed": "累計を保存できませんでした: {error}", "contrib_device_changed": "コミュニティストアに追加しましたが、入力フォームを開いている間にデバイスが変更されました。設定で選択してください。", "store_pick_profile_share": "共有先のプロファイルを選択してください", - "remove_failed": "削除に失敗しました: {error}" + "remove_failed": "削除に失敗しました: {error}", + "analysis_complete": "分析が完了しました: {count} 件の提案", + "analysis_failed": "分析に失敗しました: {error}", + "auto_detect_failed": "自動検出に失敗しました: {error}", + "auto_label_failed": "自動ラベル付けに失敗しました: {error}", + "could_not_load_cycle": "サイクルを読み込めませんでした: {error}", + "create_failed": "作成に失敗しました: {error}", + "cycles_deleted": "{count} 件のサイクルを削除しました", + "debug_cleared": "{count} 件のデバッグトレースを消去しました", + "discard_failed": "破棄に失敗しました: {error}", + "error": "エラー: {error}", + "export_failed": "エクスポートに失敗しました: {error}", + "import_failed": "インポートに失敗しました: {error}", + "invalid_json": "「{key}」は有効な JSON ではありません。保存する前に修正するか、フィールドを空にしてください。", + "label_failed": "ラベル付けに失敗しました: {error}", + "ml_training_promoted": "トレーニング完了: {count} 件のモデルを昇格しました", + "pause_failed": "一時停止に失敗しました: {error}", + "phase_created": "フェーズ「{name}」を作成しました", + "phase_deleted": "フェーズ「{name}」を削除しました", + "profile_created": "プロファイル「{name}」を作成しました", + "rename_failed": "名前の変更に失敗しました: {error}", + "save_failed": "保存に失敗しました: {error}", + "split_complete": "{count} 件のサイクルに分割しました", + "start_failed": "開始に失敗しました: {error}", + "stop_failed": "停止に失敗しました: {error}", + "update_failed": "更新に失敗しました: {error}", + "store_download_skipped": "{n}件の記録をスキップしました。短すぎるか、欠落が多すぎるか、既にデバイスにあります。", + "store_import_low_quality": "この記録は短すぎるか、欠落が多すぎるか、妥当性に欠けるためインポートできません。", + "store_import_duplicate": "この記録は既にデバイスにあります。", + "device_approved": "コミュニティによって承認されました", + "thanks_confirming": "確認していただきありがとうございます", + "rating_saved": "品質評価を保存しました", + "store_download_partial": "{n} 件のプログラムをダウンロードできませんでした。後でもう一度お試しください。", + "import_no_restore_point": "インポートしましたが、復元ポイントを保存できなかったため、このインポートは元に戻せません", + "import_undo_failed": "インポートを元に戻せませんでした: {error}", + "import_undone": "インポートを元に戻しました: {profiles} 件のプロファイルと {cycles} 件のサイクルを復元しました", + "auto_label_cancelled": "{count} 件のサイクルにラベル付けした時点で、自動ラベル付けをキャンセルしました。", + "auto_label_cancelled_other": "{count} 件のサイクルにラベル付けした時点で、自動ラベル付けをキャンセルしました。", + "auto_label_done": "自動ラベル付け完了: {count} 件のサイクルにラベル付けし、{skipped} 件はラベルなしのままです。", + "auto_label_done_other": "自動ラベル付け完了: {count} 件のサイクルにラベル付けし、{skipped} 件はラベルなしのままです。" }, "suggestion": { - "both_agree": "WashDataの推奨", - "calibrated_label": "調整済み", "observed_label": "観測値", "impact": { "auto_label_confidence": { @@ -1960,10 +1885,6 @@ "higher": "相当なエネルギーを使用したサイクルのみを終了します", "lower": "短いまたは低エネルギーのサイクルも終了します" }, - "end_repeat_count": { - "higher": "終了信号をより多く繰り返す必要があります。より堅牢ですが、わずかに遅くなります", - "lower": "より少ない繰り返しで終了します。検出が速くなりますが、誤終了のリスクが高まります" - }, "learning_confidence": { "higher": "非常に確信度の高いマッチングのみから学習します。遅くなりますが、より信頼性が高くなります", "lower": "より多くのサイクルから学習します。モデルにノイズが増える可能性があります" @@ -2002,40 +1923,25 @@ } }, "reason": { - "auto_label_confidence": "ユーザーが一度も修正しなかった {cycles} 件の自動ラベルの15パーセンタイル信頼度({p15})。", "completion_min_seconds": "{cycles} サイクルにわたる p05 の正常サイクル時間の半分({minutes} min)。誤検出サイクルを除外します。{excl}", - "dead_zone_batch": "短めに設定({cycles} サイクルの起動時の不安定期間の中央値 = {s}s)。これにより本当の終了を速やかに検出します。", - "dead_zone_from_dip": "{s}s で検出された初期の電力低下に基づく。", - "end_energy_default": "サイクル終了時のノイズゲートに推奨される既定値。", - "end_energy_false": "{cycles} サイクルにわたる p95 の誤終了エネルギー({p95}Wh)。サイクルエネルギー中央値の 0.2%({floor}Wh)を下限とします。", - "end_energy_no_false": "{cycles} サイクルで誤終了なし。サイクルエネルギー中央値の 0.2%({floor}Wh)を下限とします。", - "end_repeat_count": "{false}/{total} 件の正常サイクル({pct}%)で、再開を伴う 60s を超える内部の一時停止がありました。{val} 回の終了確認を要求します。", - "learning_confidence": "{cycles} 件のユーザーラベル付きサイクルの p05 信頼度({p05})。これを下回る場合は確認を要求します。", "match_interval": "観測された更新間隔に基づく(中央値={median}s)* 10。", "max_duration_ratio": "ラベル付きサイクル時間に基づく(p95={p95})。", - "min_active": "直近のサイクルで観測された最小の稼働電力({min}W)に基づく。", - "min_duration_ratio": "プログラムをサイクルの早い段階で識別できるよう、できるだけ低く設定。信頼度と曖昧さのゲートが早すぎる確定を防ぎます。", "min_off_gap": "観測された {gaps} 件のサイクル間の間隔に基づく(p05={p05}s)。機器の下限: {floor}s。", "min_off_gap_bridge": "サイクル内で電力がほぼ止まる最長区間をまたげるよう設定: {span}s + 60s のバッファ。{cycles} 件の正常サイクルにわたる {spans} 件のまたいだ間隔から算出(続けて運転する際にあける最短間隔 {ceiling}s を下回ります)。", "min_power": "{cycles} 件の正常サイクルにわたる p05 の最低稼働電力({p05}W)の 40%。オフゲートを実際の消費電力より低く保ちます。{excl}", - "ml_auto_label": "品質モデルが正常と判定した最低の信頼度({cycles} 件の正常サイクルの p10 = {p10})。", - "ml_end_repeat": "{false}/{total} サイクル({pct}%)で、終了検出器が 50% を超えるスコアを付けた一時停止がありました。{val} 回の終了確認を要求します。", - "ml_off_delay": "終了検出器が確認した一時停止: p95 {p95}s + 60s のバッファ。{cycles} サイクルにわたるモデル検証済みの一時停止 {pauses} 件から算出(下限 {floor}s)。", "no_update_timeout": "観測された更新間隔に基づく(p95={p95}s)* 20(最小 60s)。", "off_delay_cadence": "観測された更新間隔に基づく(p95={p95}s)* 5", "off_delay_device_floor": "{device} 向けの機器固有の安全な最小値を使用({floor}s)。", "off_delay_generic_floor": "汎用の安全な最小値を使用({floor}s)。", "off_delay_pauses": "実際の一時停止を上回るよう設定: p95 のサイクル内一時停止 {p95}s + 60s のバッファ。{cycles} 件の正常サイクルにわたる {pauses} 件の一時停止から算出(下限 {floor}s)。", - "profile_match_threshold": "{cycles} 件の正しい自動ラベルの p10 信頼度({p10})。安全なライブ確定の下限。", - "sampling_interval": "{cycles} 件の正常サイクルにわたって観測された更新間隔の中央値({si}s)。{excl}", - "smoothing_window": "{si}s のサンプリングで約 30s 分の測定値を平滑化するよう設定({samples} サンプル)。", - "start_duration": "短めに設定(約 1 回の {si}s サンプル間隔)。単一サンプルのスパイクを無視しつつ、検出をできるだけ早く開始します。", - "thr_batch": "{cycles} サイクルにわたる p05 の最低稼働電力({p05}W)のすぐ上に設定。これにより開始をできるだけ早く捉え、停止しきい値は機器の最低稼働電力を下回ったままになります。", - "tol_per_profile": "{profiles} 件のプロファイル({cycles} サイクル)にわたるプロファイルごとの時間分散の p75。ばらつきの小さいプロファイルは不利になりません。", - "tol_pooled": "{cycles} 件の直近のラベル付きサイクルの統合時間分散に基づく(p95 偏差={dev})。", + "standby_floor": "最近のサイクルはすべて終了時にまだ {idle} W を消費していたため、それを下回るしきい値では機器のオフを検出できません。しきい値はその上に維持されています。記録された {cycles} 件の正常なサイクルは、しきい値以下で {pause} 秒を超えて停止したことは一度もありません。", + "thr_batch": "{cycles} サイクルの最低稼働電力(p05、{p05}W)のすぐ上です。これにより開始を早く捉え、停止しきい値は最低稼働電力より低く保たれます。", "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} 秒)、最小値にしてもコストはかかりません。" + "match_interval_floored": "最短プログラム({shortest} 秒)では上限が {cap} 秒となり、最小値の {minimum} 秒を下回るため、最小値を使います。{persistence} 回連続のマッチングには {budget} 秒かかり、そのプログラムの {pct}% を超えます。マッチングは測定値が届いたときにのみ実行されるため(中央値={median} 秒)、コストはかかりません。", + "end_energy_floor": "停止しきい値 ({stop} W) とオフ遅延 ({off_delay} s) から導かれる下限まで引き上げました。これを下回ると、サイクルはフォールバック経路でしか終了できません。", + "max_duration_ratio_default": "既定値に戻しました。上限を下げると、運転がいつもより長引くたびに正しいプログラムが除外されてしまいます。", + "min_duration_ratio_default": "既定値に戻しました。下限を上げると、実行中のサイクルがかなり進むまで認識されなくなります。" }, "exclusions": { "summary": "{total} 件の誤検出サイクルを除外しました: {parts}。", @@ -2053,7 +1959,8 @@ "no_power": "電力なし", "no_active_power": "稼働電力なし" } - } + }, + "now_value": "(現在 {v})" }, "trend": { "down": "下降傾向", @@ -2061,6 +1968,10 @@ }, "maint": { "bearing_service": "ベアリングの整備", + "salt": "塩の補充", + "rinse_aid": "リンス剤の補充", + "lint_filter": "糸くずフィルターの清掃", + "condenser_clean": "コンデンサーの清掃", "descale": "スケール除去", "drum_clean": "ドラムの清掃", "filter_clean": "フィルターの清掃", @@ -2071,12 +1982,14 @@ "status_tip": "サイクルの結果: 完了(自然終了)、中断(急激な電力低下)、強制停止(手動)、または要レビュー(フィードバック保留中)。", "date_tip": "サイクルが開始された日時。", "duration_tip": "開始から終了までのサイクル全体の実行時間。", - "energy_tip": "消費された総エネルギー(kWh)。電力を時間で積分して計算されます。", - "cost_tip": "このサイクルのエネルギー料金。完了時に確定されます。「時間加重コスト」がオンの場合は、電力トレースの各時点で有効だった価格で計算されます。オフの場合はエネルギー x 終了時に有効だった価格ひとつです。設定で価格を指定すると入力されます。", + "energy_tip": "使用した総エネルギー(kWh)。電力の時間変化から計算されます。", + "cost_tip": "このサイクルのエネルギー料金で、終了時に確定します。「時間加重コスト」がオンの場合は各時点の価格で、オフの場合は終了時の価格で計算されます。設定で価格を指定すると表示されます。", "confidence_tip": "プロファイルのマッチ信頼度(0~100%)。サイクルの電力曲線が識別されたプログラムとどれだけ一致したか。", - "health_tip": "ML サイクルの健康度(高いほど良好)。サイクルをクリックして検査・レビューします。", "flags_tip": "サイクルのレビュー、異常、ソースフラグ。アイコンにカーソルを合わせると詳細が表示されます。", - "cost_dynamic_tip": "時間加重: サイクル中に有効だった価格で計算されています(実効 {price} {cur}/kWh)。" + "cost_dynamic_tip": "時間加重: サイクル中に有効だった価格で計算されています(実効 {price} {cur}/kWh)。", + "dbg_conf": "信頼度", + "dbg_mae": "MAE", + "dbg_corr": "相関" }, "stat": { "avg": "平均", @@ -2100,7 +2013,10 @@ "notify_finish": "完了通知が送信されます。", "notify_milestone": "マイルストーン通知が送信されます。", "notify_held": "サイレント時間帯のため、通知が保留されました。", - "finished": "サイクルが終了状態に達して終了しました。" + "finished": "サイクルが終了状態に達して終了しました。", + "match_reverted": "プログラムが検出中の状態に戻りました。スコアが自身のピークを大きく下回ったためです。", + "verified_pause": "リアルタイムの位置合わせにより、プログラム内の一時停止が確認(または解除)されました。一時停止が有効な間、サイクルは終了しません。", + "stalled": "作業が残っているのに、サイクルがこのプログラムのどの一時停止よりも長く待機電力のままでした。一時停止中 (停滞中) と表示され、サイクルは継続中のままです。" }, "store": { "search_ph2": "メーカーまたは型番で検索…", @@ -2109,11 +2025,7 @@ "programs_count": "プログラム: {n}", "browse": "閲覧", "device": "デバイス", - "favorites": "お気に入り", - "search_ph": "ブランドまたはモデルで検索…", "no_results": "一致する家電が見つかりません。別の検索をお試しください。", - "n_programs": "{n} 個のプログラム", - "n_cycles": "{n} 個のサイクル", "no_programs": "この家電にはまだ共有されたプログラムがありません。", "no_cycles": "このプログラムにはまだ共有された基準サイクルがありません。", "anon": "匿名", @@ -2125,19 +2037,21 @@ "merge_target": "統合先プロファイル", "description": "説明(任意)", "contributed_by": "提供: {name}", + "website": "ストアウェブサイト ↗", + "website_tip": "コミュニティストアのウェブサイトを開く", + "favorites": "お気に入り", "rating_summary": "★ {avg} ({n})", "no_ratings": "まだ評価がありません", - "website": "ストアウェブサイト ↗", - "website_tip": "コミュニティストアのウェブサイトを開く" + "unreachable": "コミュニティストアに接続できませんでした。接続を確認して、もう一度お試しください。" }, "settings": { "gear": { - "title": "設定" + "title": "環境設定" } }, "link": { - "manual": "マニュアル ↗", - "add_appliance": "家電を追加" + "maintenance_supporter": "GitHub の Maintenance Supporter ↗", + "manual": "マニュアル ↗" }, "tip": { "add_brand": "ブランドが一覧にありませんか?コミュニティカタログに追加してください", @@ -2171,6 +2085,12 @@ }, "rebuild": { "envelopes": "エンベロープを再構築中" + }, + "auto_label": { + "running": "サイクルを自動ラベル付け中" + }, + "store_download": { + "running": "ストアからダウンロード中" } }, "setup": { @@ -2180,26 +2100,24 @@ }, "phase0": { "washer": "WashData はすでにサイクルを検出しています。プログラム名と残り時間の推定を有効にするには、最初のサイクルを記録してください。", - "dishwasher": "WashData は監視しています。食器洗い機のサイクルは複雑です – 最初のサイクルの記録を強くお勧めします。検出されたサイクルが長すぎる場合は、プロファイルとして保存する前にサイクル編集ツールで切り詰めてください。", + "dishwasher": "WashData は監視中です。食器洗い機のサイクルは複雑なので、最初のサイクルを記録してください。検出されたサイクルが長すぎる場合は、プロファイルとして保存する前にサイクル編集ツールで切り詰めてください。", "generic": "WashData は監視しています。プロファイルの構築を始めるには、検出されたサイクルを記録またはラベル付けしてください。" }, "phase1a": { - "labelled": "良いスタートです – 最初のプログラムが保存されました。より正確なデータを得るには、レコーダーウィジェットで次のサイクルを記録することをご検討ください。" + "labelled": "良いスタートです。最初のプログラムが保存されました。最も正確なデータを得るには、次のサイクルを「手動記録」で記録してください。" }, "phase1b": { - "recorded": "記録が {profile_name} として保存されました。カバレッジを広げるために、他のよく使うプログラムも記録またはラベル付けしてください。" + "recorded": "記録が {profile_name} として保存されました。次は、よく使う他のプログラムも記録またはラベル付けしてください。" }, "phase1c": { - "verify": "コミュニティから {count} 個のプログラムがあります。WashData が正しく認識するか確認するためにサイクルを実行してください – デバイスが独自の履歴を積み重ねるにつれ、マッチングが向上します。" + "verify": "コミュニティから取得したプログラムが {count} 個あります。サイクルを実行して WashData が認識するか確認してください。デバイス自身の履歴が増えるにつれてマッチングは向上します。" }, "phase2": { - "cluster": "WashData は、保存されているプログラムに一致しない {count} 件のサイクルを確認しました – それらは互いに似ています。これらのための新しいプロファイルを作成しますか?", + "cluster": "WashData は、保存されたどのプログラムにもマッチしない、似たサイクルを {count} 件検出しました。これらの新しいプロファイルを作成しますか?", "unmatched": "直前のサイクルはどの保存済みプログラムにも一致しませんでした。新しいプログラムでしょうか?" }, "phase3": { - "suggestions": "WashData はサイクル履歴に基づいた設定の推奨事項があります – 検出精度を向上させるために確認してください。", - "groups": "いくつかのプロファイルが異なる温度の同じプログラムのように見えます。より良いマッチングのためにグループに整理してください。", - "phases": "より正確な残り時間の推定のために {profile_name} にプログラムフェーズを追加してください。" + "suggestions": "WashData にはサイクル履歴に基づく設定の提案があります。確認して検出を改善してください。" }, "phase4": { "healthy": "このデバイスは完全にセットアップされています({profile_count} 個のプロファイル)。" @@ -2212,12 +2130,58 @@ "create_from_cluster": "これらのサイクルからプロファイルを作成", "create_profile": "これのプロファイルを作成", "review_suggestions": "提案を確認", - "organise_profiles": "プロファイルを整理", - "configure_phases": "フェーズを設定", "skip_step": "このステップをスキップ", "skip_forever": "今後表示しない", "hide_guidance": "ガイダンスを非表示", "show_guidance": "ガイダンスを表示" } + }, + "access": { + "edit": "編集", + "full": "フル", + "inherit": "継承", + "none": "なし(非表示)", + "read": "読み取り" + }, + "pg_label_reason": { + "no_winner": "合致するプログラムなし", + "below_floor": "学習信頼度未満", + "ambiguous": "あいまいと判定", + "margin": "次点との差が小さすぎる", + "unknown_profile": "プログラムはもう存在しません", + "not_evaluated": "未評価" + }, + "diagram": { + "accum": "累積", + "auto": "自動", + "below_off": "以下 = オフ", + "confirmed": "確定", + "energy_counts": "エネルギーで判定", + "feedback": "フィードバック", + "gap_below_min": "最小未満の間隔 = 1 サイクル", + "heat_phase": "加熱フェーズ", + "held_at_100": "100% で保持", + "ignored": "無視", + "kept": "保持", + "max": "最大", + "min": "最小", + "minus_tol": "-許容", + "no_match": "マッチなし", + "no_updates": "更新なし", + "off_delay_wait": "オフ遅延の待機", + "off_gap": "オフギャップ", + "plus_tol": "+許容", + "profile": "プロファイル", + "reading_interval": "通常の読み取り間隔", + "sensor_offline": "センサーオフライン: 強制停止", + "si": "間隔", + "spike_ignored": "スパイクを無視", + "spike_ignored_short": "スパイク: 無視", + "start": "開始", + "stop": "停止", + "tail_energy_resets": "終盤のエネルギーがしきい値超過: タイマーをリセット", + "thr": "しきい値", + "too_short": "短すぎる", + "tumble_kept": "タンブルパルスを保持" } } diff --git a/custom_components/ha_washdata/translations/panel/ko.json b/custom_components/ha_washdata/translations/panel/ko.json index df324f8d..1a4a5092 100644 --- a/custom_components/ha_washdata/translations/panel/ko.json +++ b/custom_components/ha_washdata/translations/panel/ko.json @@ -3,6 +3,7 @@ "min": "분", "entity_not_found": "엔티티를 찾을 수 없음", "running_long": "지연 중", + "stalled": "멈춤", "remaining": "남음", "progress": "진행률", "show_time_remaining": "남은 시간 표시", @@ -48,16 +49,15 @@ } }, "badge": { - "quiet_tail_tip": "이 프로그램은 끝날 무렵 약 {mins}분 동안 전력을 거의 쓰지 않다가, 종료 전에 약 {secs}초 동안 약 {watts} W를 사용합니다. 측정된 {measured}회 사이클 중 {seen}회에서 확인되었습니다: 기기가 매번 이렇게 동작하지는 않습니다.", + "quiet_tail_tip": "이 프로그램은 끝날 무렵 약 {mins}분 동안 조용해졌다가 약 {secs}초 동안 약 {watts} W를 사용합니다. 측정된 사이클 {measured}회 중 {seen}회에서 확인되었습니다.", "quiet_tail": "조용한 마무리 ~{mins}분", - "artifact_tip": "{n} 이상 현상이 감지되었습니다(예: 사이클 중간에 문이 열림) - 열어서 그래프에서 확인", + "artifact_tip": "이상 현상 {n}건이 감지되었습니다(예: 사이클 중간에 문이 열림) - 열어서 그래프에서 확인하세요", + "artifact_tip_other": "이상 현상 {n}건이 감지되었습니다(예: 사이클 중간에 문이 열림) - 열어서 그래프에서 확인하세요", "auto": "(자동 감지)", "built_in_tag": "내장", "declining": "↘ 악화", "energy_low": "평소보다 에너지 사용량이 적음", "energy_spike": "평소보다 에너지 사용량이 많음", - "fair_fit": "적절함", - "fair_fit_tip": "중간 수준의 일치 일관성 - 이 프로필에 할당된 일부 주기의 신뢰도 점수가 낮습니다. 정확성을 높이려면 더 많은 주기에 라벨을 붙이거나 프로필을 다시 기록하십시오.", "feedback_requested": "피드백 요청됨", "golden_cycle": "기록된 참조 사이클", "duration_outlier": "길이가 이상한 사이클 {n}개", @@ -66,27 +66,34 @@ "needs_review": "검토 필요", "overrun": "평소보다 오래 작동함", "poor_fit": "품질 낮음", - "poor_fit_tip": "일관성 없는 매칭 기록 – 이 프로필을 다시 작성하는 것이 좋습니다.", - "restart_gap_tip": "{n} HA 재시작 공백: 전력 그래프에 빈 구간이 있습니다", + "restart_gap_tip": "HA 재시작 공백 {n}개: 전력 그래프에 빈 구간이 있습니다", + "restart_gap_tip_other": "HA 재시작 공백 {n}개: 전력 그래프에 빈 구간이 있습니다", "reviewed": "검토됨", "steady": "→ 안정", "underrun": "평소보다 빨리 완료됨", - "using_defaults": "기본 제공 기본값 사용", - "using_tuned": "조정된 가중치 사용", "overrun_ratio": "(예상의 {x}배)", "underrun_ratio": "(예상의 {pct}%)", - "awaiting_tip": "커뮤니티 승인 대기 중", - "awaiting_n": "승인 대기 중 · {n}개 확인됨", - "approved": "승인됨", - "awaiting": "승인 대기 중", "imported_tip": "커뮤니티 스토어에서 가져왔습니다. 매칭에만 사용되며 통계에는 포함되지 않습니다.", "unmatchable": "매칭 불가", "not_importable": "해당 없음", "exists": "존재함", "backfilled_tip": "가져온 전력 이력에서 감지되었습니다. 프로그램 매칭에만 영향을 주며 통계에는 포함되지 않습니다.", - "armed": "(다음 사이클에 적용)" + "armed": "(다음 사이클에 적용)", + "awaiting_tip": "커뮤니티 승인 대기 중", + "awaiting_n": "승인 대기 중 · {n}개 확인됨", + "awaiting": "승인 대기 중", + "approved": "승인됨", + "matcher_unused": "아직 내 사이클과 매칭되지 않음", + "matcher_used": "내 사이클 {n}개와 매칭됨", + "matcher_used_other": "내 사이클 {n}개와 매칭됨", + "matcher_used_tip": "WashData가 이 가져온 프로그램으로 스스로 매칭한 이 기기의 사이클입니다. 한 번도 매칭되지 않는 가져온 프로그램은 기기에 맞지 않을 수 있으니 삭제해도 됩니다. 로컬에서 집계되며 스토어로는 아무것도 전송되지 않습니다." }, "btn": { + "pg_preset_delete": "프리셋 삭제", + "pg_preset_load": "프리셋 불러오기", + "pg_preset_load_tip": "플레이그라운드 값을 이 프리셋으로 바꿉니다(적용 중인 설정은 그대로 유지됩니다)", + "pg_preset_save": "프리셋으로 저장", + "pg_preset_save_tip": "아래의 모든 값을 이 기기의 이름 있는 프리셋으로 저장합니다", "reset_zoom": "확대/축소 초기화", "set_brand_model": "브랜드 및 모델 설정", "refresh_catalog": "카탈로그 새로 고침", @@ -97,12 +104,11 @@ "add_timer": "+ 타이머 추가", "apply_all": "모두 적용", "apply_label": "라벨 적용", - "apply_set_b": "세트 B 적용", "apply_split": "분할 적용", "apply_trim": "자르기 적용", "auto_detect_split": "자동 감지", "auto_label_cycles": "사이클 자동 라벨 지정", - "auto_label_cycles_tip": "일치 신뢰도가 임계값을 초과하는 미라벨 주기에 프로필 이름을 자동으로 할당합니다", + "auto_label_cycles_tip": "매칭 신뢰도가 임계값을 넘는 미라벨 사이클에 이름을 지정합니다.", "cancel": "취소", "clear_debug": "디버그 데이터 지우기", "clear_splits": "지우기", @@ -112,8 +118,6 @@ "convert_to_automation": "자동화로 전환", "correct": "수정…", "create": "만들기", - "create_from_cluster": "{count}개 사이클로 프로필 만들기", - "create_profile": "+ 프로필 만들기", "delete": "삭제", "delete_group": "그룹 삭제", "delete_group_tip": "이 그룹만 삭제 - 구성원 프로필은 유지됩니다.", @@ -124,22 +128,19 @@ "done": "완료", "edit": "편집", "export": "내보내기", - "export_json": "JSON으로 내보내기", "force_stop": "강제 정지", "force_stop_tip": "현재 사이클을 즉시 종료하고 강제 정지로 표시합니다", "from_template": "템플릿에서 ▾", "ignore": "무시", "import_json": "JSON에서 가져오기", - "import_overwrite": "가져오기(데이터 덮어쓰기)", "inspect": "검사", "label": "라벨 지정", - "load": "불러오기", "load_more": "더 불러오기", "manage": "관리", "merge": "병합", "new_automation": "+ 새로운 자동화", "new_group": "+ 새 그룹", - "new_group_tip": "거의 동일한 프로필(같은 형태/시간, 다른 온도 또는 탈수)을 그룹화하여 매처가 안정적으로 선택할 수 있도록 합니다", + "new_group_tip": "거의 같은 프로필(형태는 같고 온도나 탈수가 다른 프로필)을 그룹으로 묶어 매칭이 구분할 수 있게 합니다.", "new_phase": "+ 새로운 단계", "new_profile": "+ 새 프로필", "new_profile_tip": "기존의 라벨이 지정된 주기 또는 기록에서 새 프로그램 프로필을 만듭니다", @@ -147,36 +148,25 @@ "on_cycle_started": "주기 시작됨", "pause_cycle": "일시 중지", "pause_cycle_tip": "실행 중인 사이클을 일시 중지합니다 – 기기가 중단된 지점부터 재개됩니다", - "pg_apply_device": "장치에 적용", - "pg_autofill": "자동 채우기", - "pg_load_calibrated": "조정된 (ML) 불러오기 ({n})", - "pg_load_calibrated_tip": "ML 조정된 설정을 플레이그라운드 재정의로 적용합니다", "pg_load_live": "적용 중인 설정 불러오기", "pg_load_live_tip": "통합의 현재 설정을 다시 읽고 플레이그라운드의 편집 내용을 모두 버립니다", "pg_load_suggested": "제안 불러오기 ({n})", "pg_load_suggested_tip": "자동 조정 도구의 현재 제안을 플레이그라운드 재정의로 적용합니다", - "pg_preset_delete": "프리셋 삭제", - "pg_preset_load": "프리셋 불러오기", - "pg_preset_load_tip": "플레이그라운드 값을 이 프리셋으로 바꿉니다(적용 중인 설정은 그대로 유지됩니다)", - "pg_preset_save": "프리셋으로 저장", - "pg_preset_save_tip": "아래의 모든 값을 이 기기의 이름 있는 프리셋으로 저장합니다", "pg_publish_all": "{n}개를 통합에 적용", "pg_publish_one": "{label}을(를) 통합에 적용", - "play": "재생", "process": "처리", "process_history": "지금 처리", "process_recording": "기록 저장", "process_tip": "기록된 트레이스를 새 프로필 또는 기존 프로필로 저장합니다", "rebuild": "엔벨로프 다시 작성", "rebuild_envelope": "엔벨로프 재구축", - "rebuild_tip": "라벨이 지정된 주기에서 모든 프로필의 예상 전력 포락선(최소/최대 범위)을 다시 계산합니다 – 새 주기에 라벨을 지정하거나 기존 주기를 수정한 후 실행합니다", + "rebuild_tip": "라벨이 지정된 사이클로 모든 프로필의 예상 전력 범위를 다시 만듭니다. 사이클에 라벨을 지정하거나 수정한 후 실행하세요.", "rec_start_tip": "기기의 전력 트레이스 기록을 시작합니다 – 주기를 실행하기 직전에 시작합니다", "rec_stop": "중지", "rec_stop_tip": "기록을 중지하고 캡처된 트레이스를 검토를 위해 보관합니다", "record": "기록 시작", "refresh": "새로 고침", "refresh_diag_tip": "저장소 통계를 다시 로드합니다", - "refresh_logs_tip": "최신 로그 기록을 다시 로드합니다", "refresh_settings_tip": "서버에서 설정을 다시 로드합니다", "relabel": "다시 라벨 지정 ({count})", "remove": "제거", @@ -184,7 +174,6 @@ "reset": "재설정", "reset_ml_models_tip": "미세 조정된 모델을 폐기하고 내장된 모델로 돌아갑니다. WashData는 나중에 다시 학습할 수 있습니다.", "reset_to_builtin": "내장 모델로 재설정", - "reset_to_defaults": "기본값으로 재설정", "resume_cycle": "사이클 재개", "resume_cycle_tip": "일시 중지된 사이클을 재개합니다", "revert_settings": "변경 사항 취소", @@ -197,8 +186,6 @@ "run_analysis": "🔍 제안 분석 실행", "run_analysis_tip": "지금 기록된 주기를 분석하고 튜닝 제안을 새로 고치세요.", "run_auto_label": "자동 라벨 실행", - "run_both": "A 대 B 실행", - "run_simulation": "시뮬레이션 실행", "save": "저장", "save_access_control": "액세스 제어 저장", "save_group": "그룹 저장", @@ -210,10 +197,8 @@ "save_settings": "설정 저장", "select": "선택", "select_all": "모두 선택", - "select_last20": "최근 20개", "select_none": "지우기", "show_only": "표시만", - "skip_setup": "설정 건너뛰기", "split": "분할", "stop": "정지", "submit_correction": "정정사항 제출", @@ -222,36 +207,31 @@ "trim_split": "자르기/분할", "undo": "실행 취소", "unlabel_selected": "선택 항목 라벨 해제 ({n})", - "unlabel_selected_tip": "선택한 사이클에서 프로필 라벨을 해제합니다. 사이클은 기록에 그대로 남아 있고 나중에 다시 라벨을 지정할 수 있으며, 이 프로필을 형성하지 않게 될 뿐입니다.", + "unlabel_selected_tip": "선택한 사이클의 라벨을 해제합니다. 사이클은 기록에 남지만 더 이상 이 프로필을 형성하지 않습니다.", "use": "사용", "wipe_all": "모든 데이터 지우기", "delete_selected": "선택 항목 삭제 ({n})", - "group_suggest": "그룹 {n}: {members}", "pg_apply_best": "최적 값 적용", - "pg_apply_to_settings": "설정에 저장", "pg_apply_to_settings_tip": "여기서 편집한 값을 이 기기에 현재 적용 중인 설정으로 복사합니다", "import": "가져오기", "disconnect": "연결 해제", "connect_github": "GitHub 연결", - "save_appliance": "가전제품 저장", "share_to_store": "스토어에 공유", "share": "공유", - "confirm_appliance": "이 가전제품 확인", - "confirmed": "확인됨", "browse_community_setups": "커뮤니티 설정 둘러보기", "download_device": "이 설정 다운로드", "share_device": "⬆ 이 기기 공유", - "share_device_tip": "이 가전제품과 기록된 참조 사이클을 커뮤니티 스토어에 공유하여 같은 기기를 가진 다른 사용자가 채택할 수 있도록 합니다", + "share_device_tip": "이 기기와 참조 사이클을 커뮤니티 스토어에 공유하여 같은 기기를 가진 사용자가 채택할 수 있게 합니다.", "share_n": "{n}개 사이클 공유", "export_selected": "내보내기 (데이터 선택)", "export_all": "모두 빠르게 내보내기", - "import_raw": "고급: JSON에서 전체 교체", "download_export": "내보내기 다운로드", "analyze_import": "파일 분석", "import_selected": "선택 항목 가져오기", "back": "뒤로", "mute_suggestion": "이 설정 제안 중지", "reset_muted": "뮤트 초기화", + "reset_phase_tip": "편집을 취소하고 기본 제공 단계를 복원합니다", "import_power_history": "전력 이력 가져오기", "hist_read_recorder": "Home Assistant에서 읽기", "hist_scan": "사이클 검색", @@ -260,7 +240,15 @@ "save_total_cycles": "누적 값 저장", "clear": "지우기", "add_brand": "브랜드 추가", - "add_appliance": "가전제품 추가" + "log_done_task": "완료 기록: {task}", + "add_task": "작업 추가", + "add_appliance": "가전제품 추가", + "retry": "다시 시도", + "confirm_appliance": "이 가전제품 확인", + "confirmed": "확인됨", + "create_profile_from_gap": "프로필 만들기", + "undo_import": "마지막 가져오기 실행 취소", + "pg_rerun": "시뮬레이션 업데이트" }, "note": { "off_delay": { @@ -268,10 +256,6 @@ } }, "conflict": { - "anti_wrinkle_exit": { - "exit": "정지 임계값 ({stop} W) 미만이어야 합니다. 그렇지 않으면 구김 방지 종료 전력이 무시됩니다", - "stop": "구김 방지 종료 전력 ({exit} W) 이상이어야 합니다" - }, "anti_wrinkle_max": { "max": "시작 임계값 ({start} W) 이상이어야 합니다. 그렇지 않으면 구김 방지 시간 제한이 무시됩니다", "start": "구김 방지 최대 전력 ({max} W) 미만이어야 합니다" @@ -309,14 +293,14 @@ "threshold": "적용되려면 정지 임계값 ({stop} W) 미만이어야 합니다" }, "price_entity": { - "power_sensor": "이것은 이 기기의 전력 센서이며 가격이 아닙니다. 가격 엔티티는 아래의 고정 에너지 가격보다 우선하므로, 와트 값이 kWh당 단가로 사용되어 사이클 비용이 계산됩니다. 이 필드를 비워 주세요.", - "energy_sensor": "이것은 이 기기의 에너지 계량기이며 가격이 아닙니다. 가격 엔티티는 아래의 고정 에너지 가격보다 우선하므로, 계량기 값이 kWh당 단가로 사용되어 사이클 비용이 계산됩니다. 이 필드를 비워 주세요.", - "device_class": "이 센서가 측정하는 값은 {dc}이며, kWh당 가격이 아닙니다. 가격 엔티티는 아래의 고정 에너지 가격보다 우선합니다. 이 필드를 비우거나 전기 요금 단가 센서를 지정하세요.", - "unit": "이 센서의 읽기 단위는 {unit}이며, 이는 kWh당 가격이 아닙니다. 가격 엔티티는 아래의 고정 에너지 가격보다 우선합니다. 이 필드를 비우거나 전기 요금 단가 센서를 지정하세요." + "power_sensor": "이것은 이 기기의 전력 센서이며 가격이 아닙니다. 와트 값이 kWh당 단가로 계산되므로 이 필드를 비우세요.", + "energy_sensor": "이것은 이 기기의 에너지 계량기이며 가격이 아닙니다. 계량기 값이 kWh당 단가로 계산되므로 이 필드를 비우세요.", + "device_class": "이 센서가 측정하는 값은 {dc}이며 kWh당 가격이 아닙니다. 이 필드를 비우거나 전기 요금 센서를 선택하세요.", + "unit": "이 센서의 단위는 {unit}이며 kWh당 가격이 아닙니다. 이 필드를 비우거나 전기 요금 센서를 선택하세요." }, "pump_stuck": { - "duration": "워치독이 사이클을 강제 종료하기 전에 정지 알람이 작동하도록 업데이트 없음 타임아웃 ({to} 초) 미만이어야 합니다", - "timeout": "사이클이 강제 정지되기 전에 정지 알람이 작동하도록 펌프 정지 지속 시간 ({ps} 초)을 초과해야 합니다" + "duration": "정지 알람이 먼저 작동할 수 있도록 업데이트 없음 타임아웃({to} 초)보다 작아야 합니다", + "timeout": "정지 알람이 먼저 작동할 수 있도록 펌프 정지 지속 시간({ps} 초)을 초과해야 합니다" }, "start_dur": { "sampling": "샘플링 간격이 시작 지속 시간 ({sdt} 초)을 초과합니다. 단일 샘플 스파이크가 사이클을 시작할 수 있습니다", @@ -334,10 +318,15 @@ "sampling": "샘플링 간격은 워치독 간격 ({wi} 초)의 절반 이하이어야 합니다" }, "settings_banner": "설정 충돌: {n}. 강조 표시된 섹션을 확인하고 저장 전에 수정하세요.", - "settings_banner_btn": "첫 번째로" + "settings_banner_btn": "첫 번째로", + "end_energy": { + "energy": "중지 임계값 ({w} W)과 오프 지연 ({d} s)에 비해 너무 엄격합니다. 사이클이 대체 경로로만 종료될 수 있습니다", + "stop": "종료 에너지 ({d} s 동안 {e} Wh)로는 {p} W까지만 허용됩니다" + } }, "hdr": { - "ab_comparison": "A/B 설정 비교", + "custom_tasks": "직접 추가한 작업", + "maintenance_supporter": "더 필요하신가요?", "access_control": "접근 제어", "add_maintenance": "유지보수 이벤트 추가", "automation_delete": "이 자동화 삭제", @@ -346,28 +335,22 @@ "automation_review_this_cycle": "이 주기를 검토하세요", "automations": "자동화", "clear_debug": "디버그 추적 지우기", - "cycle_simulator": "사이클 시뮬레이터", "diagnostics_pref": "진단", "display": "표시", - "dtw_inspector": "DTW 검사기", "export_import": "내보내기/가져오기", - "getting_started": "시작하기", "live_power": "라이브 전력", "logs": "로그", - "logs_diagnostics": "진단", "maintenance": "유지 관리 작업", "maintenance_log": "유지보수 기록", "maintenance_reminders": "서비스 알림", "manual_recording": "수동 기록", - "manual_recording_tip": "WashData가 전력 트레이스를 기록하는 동안 의도적으로 사이클을 실행합니다. 기기가 시작되기 직전에 시작하고, 완료되면 중지한 다음 처리하여 이름이 지정된 프로필로 저장합니다.", + "manual_recording_tip": "사이클을 직접 기록합니다. 기기가 시작되기 직전에 시작을, 끝나면 중지를 누른 다음 처리를 눌러 프로필로 저장하세요.", "ml_learned": "WashData가 배운 것", - "ml_matching_tuning": "프로그램 매칭 미세 조정", "ml_settings_card": "설정", "ml_smart_learning": "스마트러닝", "my_preferences": "내 환경설정", "no_automations": "아직 이 장치를 참조하는 자동화가 없습니다.", "panel_settings": "패널 설정", - "param_sweep": "매개변수 스윕", "pg_settings_source": "설정 출처", "phase_catalog": "단계 카탈로그", "playground": "플레이그라운드", @@ -376,11 +359,8 @@ "status": "상태", "status_graph": "상태 그래프", "storage_stats": "스토리지 통계", - "test_history": "기록으로 테스트", "toggle_sidebar": "홈어시스턴트 사이드바 전환", - "tools_and_data": "도구 및 데이터", "wipe_history": "기록 지우기", - "recommendations": "권장 사항 ({n})", "pg_detection_params": "감지 설정", "pg_outcome": "시뮬레이션 결과", "pg_across_cycles": "전체 사이클에 걸쳐", @@ -388,7 +368,8 @@ "online_account": "커뮤니티 스토어 및 온라인 기능", "import_power_history": "전력 이력 가져오기", "service_status": "유지보수 상태", - "online_account_tab": "온라인 및 커뮤니티" + "online_account_tab": "온라인 및 커뮤니티", + "live_match_debug": "실시간 매칭 디버그" }, "health": { "fair": "프로파일 품질: 보통", @@ -396,11 +377,16 @@ "poor": "⚠ 프로파일 품질: 불량" }, "lbl": { - "envelope_position_tip": "매칭된 프로그램이 기록한 곡선상의 위치, {pct}%입니다. 시간을 세는 대신 이 사이클을 프로필에 정렬해 측정하므로, 평소보다 길거나 짧은 운전에서도 정확하게 유지됩니다. 기기가 조용할 때 갱신되므로 활성 단계 중에는 뒤처질 수 있습니다.", + "envelope_position_tip": "매칭된 프로그램의 기록 곡선상 위치: {pct}%. 사이클을 프로필에 정렬해 구하므로 운전이 길거나 짧아도 정확합니다. 기기가 조용할 때 갱신되므로 늦을 수 있습니다.", "envelope_position": "곡선 {pct}%", "aria_playground_chart3": "대화형 사이클 전력 그래프: 마우스를 올리면 시간/전력 표시, 스크롤 또는 두 손가락으로 확대/축소, 드래그하여 이동", "device_unresolved": "사용할 수 없는 기기 ({id})", "drag_to_resize": "드래그하여 크기 조정", + "task_name": "작업", + "every_cycles": "주기 (사이클)", + "every_days": "주기 (일)", + "new_task_name": "새 작업", + "days_since_service": "{since} / {total}일", "actions": "작업", "activity": "활동", "administrators": "관리자", @@ -408,10 +394,8 @@ "aria_cycle_chart": "사이클 전력 곡선", "aria_envelope_chart": "프로필 전력 엔벨로프 그래프", "aria_phase_chart": "단계 편집기 그래프", - "aria_playground_chart": "드래그 가능한 감지 임계값이 있는 사이클 전력 곡선", "aria_power_chart": "전력 소비 그래프", "aria_spaghetti_chart": "겹쳐진 사이클 전력 곡선", - "aria_sweep_chart": "매개변수 스윕 결과 그래프", "artifact_anomaly": "이상", "artifact_high_power": "고전력", "artifact_interruption": "중단", @@ -425,150 +409,99 @@ "cohesion_good": "응집도 {pct}%", "cohesion_low": "⚠ 낮은 응집도 {pct}%", "compare_profiles": "프로필과 비교", - "concurrency": "동시 실행 수", "confidence": "신뢰도", "confidence_threshold": "신뢰도 임계값", "correct_duration": "올바른 기간(분, 선택 사항)", "correct_profile": "올바른 프로필", - "correlation": "상관관계", "cost": "비용", "create_new_profile": "+ 새 프로필 만들기…", "current_phase": "현재 단계", - "curves": "곡선", "cycle": "사이클", "cycle_date_display": "주기 날짜 표시", - "cycle_health": "사이클 건강", - "cycle_index": "사이클 번호 →", - "cycles": "사이클", "cycles_count": "사이클", "cycles_lc": "사이클", - "dashed": "점선", "date": "날짜", "debug_traces": "디버그 추적", - "default": "기본", "default_access_level": "아래에 나열되지 않은 사용자의 기본 수준", "default_other": "기본값(기타 장치)", "default_tab": "패널을 열 때 기본 탭", - "delta": "Δ", "description": "설명", - "dtw_blend_w": "혼합 가중치", - "dtw_blended": "혼합 점수", - "dtw_ddtw": "DDTW 점수", - "dtw_l1": "L1 점수", - "dtw_matched_profile": "(사이클의 일치 프로필)", "duration": "지속 시간", - "duration_agreement": "지속 시간 일치도", - "emphasis": "중요성", "enable_access_control": "사용자별 액세스 제어 활성화", "energy": "에너지", - "energy_agreement": "에너지 일치도", - "event_timeline": "이벤트 타임라인", "event_type": "이벤트 유형", "expected": "예상", + "suspected_profile": "{name} (추정)", + "runner_up_profile": "{name} (차순위)", "expected_duration": "예상 기간(분)", "file_kb": "파일(KB)", - "final_score": "최종 점수", "from": "시작", "gap_s": "간격(초)", "group_name": "그룹 이름", "head_trim": "시작 부분 자르기(초)", - "health": "건강", "hide_tabs": "관리자가 아닌 사용자를 위한 탭 숨기기", - "in_use": "사용 중", "json_data": "JSON 데이터", "label": "매칭된 프로그램", "last": "최근", "last_checked": "마지막 확인:", "load_from_file": "파일에서 로드", - "mae_score": "MAE 점수", "manual_duration": "수동 기간(분, 선택 사항)", "match": "매칭", "members": "구성원", - "metric": "지표", "mode_existing_profile": "기존 프로필에 추가", "mode_new_profile": "새 프로필 만들기", "models_fine_tuned": "(미세 조정된 모델: {count})", - "n_classic_suggestions": "{n} 클래식", - "n_ml_suggestions": "{n} ML", "n_selected": "{n}개 선택됨", - "n_tuning_suggestions": "{n} 조정 제안", - "na": "해당 없음", + "n_tuning_suggestions": "조정 제안 {n}개", + "n_tuning_suggestions_other": "조정 제안 {n}개", "new_profile_name": "새 프로필 이름", "notes": "메모", - "of": "/", - "outcome": "결과", "panel_default_tab": "기본 탭", "panel_language": "패널 언어", "per_cycle_short": "사이클", "pg_anti_wrinkle": "구김 방지", - "pg_autofill_tip": "현재 값에서 범위 자동 채우기 ({lo}–{hi})", "pg_best_value": "찾은 최적 값", "pg_current_value": "현재 값:", "pg_drag_hint": "그래프의 선을 드래그하세요", - "pg_dtw": "DTW", "pg_ev_ending": "종료 중", "pg_ev_running": "실행 중", "pg_group_advanced": "예외 상황", "pg_group_detection": "감지 트리거", "pg_group_timing": "타이밍 규칙", "pg_idle": "대기", - "pg_last_run": "마지막 실행: {date}", - "pg_match_score": "매칭 점수", - "pg_pct_of": "{total}개 중 {p}%", "pg_poor_match": "불량 매칭", "pg_preset": "플레이그라운드 프리셋", "pg_preset_name": "새 프리셋 이름", "pg_preset_none": "프리셋 선택…", "pg_strong_match": "강한 매칭", - "pg_tile_ambiguous": "모호함", - "pg_tile_ambiguous_tip": "두 프로그램이 거의 동점인 사이클", - "pg_tile_detected": "감지됨", - "pg_tile_detected_tip": "전력이 실행 중으로 등록될 만큼 충분히 오래 시작 임계값을 넘은 사이클", - "pg_tile_matched": "매칭됨", - "pg_tile_matched_tip": "알려진 프로그램에 올바르게 귀속된 사이클", - "pg_time_left": "남은 시간", "pg_weak_match": "약한 매칭", "phase_name": "단계 이름", "name": "이름", "phase_timeline": "단계 타임라인", "power": "전력", "profile": "프로필", - "profile_index": "프로필 번호 →", "profile_name": "프로필 이름", "program": "프로그램", - "program_tip": "현재 사이클에 매칭되는 프로파일을 재정의합니다. 자동 감지를 통해 통합이 최적의 일치를 자동으로 선택합니다. 자동 감지가 잘못되었거나 실행 중인 프로그램을 알 때 특정 프로그램을 고정하여 강제 일치시킵니다. 기기를 작동하기 전에 하나를 선택하면 다음 사이클이 시작되는 즉시 적용됩니다.", + "program_tip": "현재 사이클을 어떤 프로그램에 매칭할지 선택합니다. 자동 감지는 가장 잘 맞는 프로그램을 고릅니다. 자동 감지가 틀리면 프로그램을 고정하세요. 시작 전에 선택하면 다음 사이클에 적용됩니다.", "progress": "진행률", "raw_socket": "원시 소켓", - "read_only": "읽기 전용", "ref_cycle": "참조 주기(선택 사항)", - "refresh_interval": "새로 고침 간격(초)", "remaining": "남은 시간", "reminder_every": "알림 주기 (사이클)", "remove_label": "- 라벨 제거 -", "rename_profile": "프로필 이름 바꾸기", - "replay_duration": "재생", "resulting_profile": "결과 프로필", "save_mode": "저장 모드", - "score": "점수", - "score_breakdown": "점수 세부 내역", "seconds_from_start": "시작부터 경과 시간(초)", "select_profile": "프로필 선택", "setting": "설정", - "settings_a": "설정 A (현재)", - "settings_a_short": "A", "settings_advanced": "고급", - "settings_b": "설정 B (테스트)", - "settings_b_short": "B", "settings_basic": "기본", "settings_detail_level": "설정 상세 수준", - "settings_overrides": "설정 재정의", "show_debug": "상태 페이지에 실시간 매치 디버그 카드 표시(신뢰도, 모호함, 상위 후보)", "show_expected": "예상 곡선 오버레이 표시(일치하는 프로필, 주황색)", "show_raw": "라이브 전력 그래프에 원시 소켓 토글 표시", "sparkline": "최근 사이클 지속 시간 추세", - "stage2": "2단계 – 핵심 유사도", - "stage3": "3단계 – DTW", - "stage4": "4단계 – 일치도", "status": "상태", "tail_trim": "끝부분 자르기(초)", "tap_to_dismiss": "그래프를 탭하여 닫기", @@ -579,7 +512,6 @@ "ungrouped": "그룹 없음", "unlabelled": "라벨이 지정되지 않음", "unlabelled_paren": "(라벨 없음)", - "warp_path": "워프 경로", "input": "입력:", "start": "시작", "end": "종료", @@ -604,9 +536,8 @@ "n_shown": ", {n}개 표시됨", "cycles_title": "사이클 ({n})", "pg_detecting": "감지 중", - "pg_best_marker": "최적: {v}", "pg_time_left_model": "남은 시간(모델)", - "pg_time_left_model_tip": "모델이 추정한 남은 시간(단계 추정 + ML 혼합)으로, 고정된 카운트다운이 아닙니다.", + "pg_time_left_model_tip": "프로그램 단계를 기준으로 추정하며, 고정된 카운트다운이 아닙니다.", "phase": "단계", "pg_mode_history": "이력 테스트", "pg_ev_detected": "감지됨", @@ -638,12 +569,6 @@ "pg_sweep_param": "매개변수", "pg_objective": "목표", "pg_steps": "단계 수", - "pg_stress_group": "대기 상태 종료 테스트", - "pg_stress_idle_auto": "Auto", - "pg_stress_idle_w": "대기 레벨 (W)", - "pg_stress_idle_w_desc": "자동 감지된 대기 전력 하한값을 재정의합니다(자동의 경우 빈칸으로 두세요).", - "pg_stress_toggle": "대기 상태 종료 테스트", - "pg_stress_toggle_desc": "기록 종료 후 기기가 대기 전력 수준에서 계속 작동하는 상황을 시뮬레이션합니다 - WashData가 사이클을 중지하는지, 언제 중지하는지 확인합니다.", "pg_obj_match": "매칭 정확도", "pg_obj_endtiming": "종료 타이밍 정확도", "pg_obj_falseend": "잘못된 종료율", @@ -652,12 +577,8 @@ "pg_mode_optimize": "최적화", "pg_group_matching": "프로그램 매칭", "pg_zoom_hint": "더블클릭하여 확대/축소 초기화", - "aria_playground_chart2": "대화형 사이클 전력 그래프: 마우스를 올리면 시간/전력 표시, 스크롤하여 확대/축소, 드래그하여 이동", "pg_alert_timeout_end": "예측이 아닌 타임아웃으로 종료됨", "pg_alert_indefinite": "무한정 계속 실행됨", - "pg_alert_stress_cap": "안전 한도에 도달했습니다", - "pg_alert_stress_ok": "대기 상태 종료: 사이클이 중지되었습니다", - "pg_alert_stress_warn": "대기 전력이 정지 임계값을 초과합니다", "task_pg_history": "기록으로 테스트", "task_pg_sweep": "최적화", "task_reprocess": "재처리", @@ -667,16 +588,9 @@ "pg_recent_runs": "최근 실행", "pg_load_run": "이 실행 다시 불러오기", "pg_match_confidence": "매칭 신뢰도", - "pg_envelope_fit": "엔벨로프 적합도", - "pg_envelope_fit_tip": "이 프로필의 저장된 전력 엔벨로프 안에 사이클이 얼마나 잘 들어맞는지 보여줍니다. 매칭 신뢰도와는 다른 관점입니다.", "n_imported_note": ", {n}개 가져옴", "enable_online": "온라인 기능 활성화", - "store_brand": "가전제품 브랜드", - "store_model": "가전제품 모델", "anonymous": "익명", - "rate_quality": "품질 평가", - "rate_n": "별 {n}개", - "approved_only": "승인된 항목만", "adopt_settings": "설정도 채택", "flags": "플래그", "include_phase_map": "페이즈 맵 포함", @@ -693,7 +607,6 @@ "cat_custom_phases": "사용자 지정 단계", "cat_profile_groups": "프로필 그룹", "cat_settings": "감지 및 인식 설정", - "cat_matching_config": "인식기 조정", "cat_ml_models": "ML 모델", "cat_feedback": "피드백 및 검토 라벨", "cat_suggestions": "제안", @@ -715,7 +628,6 @@ "hist_csv_data": "CSV 데이터", "hist_from_recorder": "또는 Home Assistant에서 읽기", "hist_since": "시작 날짜", - "days": "일", "hist_keep": "이 사이클 유지", "hist_looks_complete": "완전", "peak_power_short": "피크", @@ -735,7 +647,38 @@ "zoom_hint_touch_zoomed": "두 손가락으로 확대/축소 · 두 번 탭하여 초기화", "cycles_since_service": "{since} / {total} 사이클", "total_cycles_run": "누적 실행 사이클", - "correct_total_cycles": "누적 값 수정" + "correct_total_cycles": "누적 값 수정", + "hist_reason_already_recorded": "WashData가 이미 기록한 사이클과 겹침", + "share_consent": "이 사이클들이 중단 없이 정상적으로 완료되었음을 확인합니다", + "show_internal": "내부 설정 표시", + "show_internal_tip": "WashData가 자동으로 조정하는 설정입니다(샘플링, 워치독, 매칭 주기, 임계값). 이 기기에 자체 값, 충돌 또는 제안이 있으면 표시됩니다.", + "rate_n": "별 {n}개", + "rate_quality": "품질 평가", + "pg_autolabel": "자동 라벨", + "pg_label_review": "검토 요청", + "pg_ev_match_reverted": "매칭 취소", + "pg_ev_verified_pause": "확인된 일시 중지", + "n_samples": "샘플 {n}개", + "n_samples_other": "샘플 {n}개", + "n_cycles": "사이클 {n}개", + "n_cycles_other": "사이클 {n}개", + "energy_per_cycle": "{v}/사이클", + "energy_total": "총 {v}", + "automation_off": "(꺼짐)", + "ref_cycle_none": "없음", + "share_name": "공유하는 항목에 내 이름 표시", + "cat_backfill_cycles": "가져온 전력 이력에서 발견된 사이클", + "pg_compare_profile": "비교할 프로필", + "pg_obj_early": "조기 종료율", + "pg_obj_endlag": "종료 지연(중앙값)", + "pg_obj_split": "분할률", + "pg_sweep_cycles": "최근 사이클 수", + "cycle_context": "사이클 전후의 레코더 이력", + "cycle_context_off": "끄기", + "cycle_context_minutes": "{min}분", + "cycle_context_series": "레코더 이력", + "before_start": "시작 전", + "after_end": "종료 후" }, "log": { "all_levels": "모든 레벨", @@ -745,11 +688,9 @@ }, "ml": { "builtin_models": "● 내장 모델 사용", - "fit_fair": "보통", "fit_good": "좋은", "fit_slight": "근소한", "fit_strong": "강한", - "fit_weak": "약한", "fit_word": "맞다", "personalized": "● 이 기계에 맞춤화됨", "better_than_baseline": "기준 추정치보다 {pct}% 향상 ({metric})", @@ -757,21 +698,27 @@ "trend_declining_tip": "이 모델의 적합도가 최근 재확인에서 하락했습니다 · 더 많은 사이클을 검토하면 다시 학습하는 데 도움이 될 수 있습니다.", "trend_steady_tip": "이 모델의 적합도가 최근 재확인에서 거의 꾸준히 유지되었습니다.", "cap_blurb": { - "end": "사이클이 실제로 완료된 시점 파악", - "live_match": "실행 중인 프로그램을 더 빨리 식별", - "quality": "잘못 감지되었거나 손상된 사이클 찾기", - "remaining_time": "남은 시간 예측", "total_energy": "총 에너지와 비용 예측" }, "cap_label": { - "end": "사이클 종료 감지", - "live_match": "프로그램 매칭", - "quality": "사이클 품질 검사", - "remaining_time": "남은 시간 추정", "total_energy": "에너지 추정" }, - "metric_auc": "검증 데이터 기준 AUC {auc}", - "metric_mae": "오차 {model} vs 기준선 {baseline}" + "metric_mae": "오차 {model} vs 기준선 {baseline}", + "fine_tuned_at": "{when}에 미세 조정됨", + "last_run_not_promoted": "마지막 확인({when}): {reason}", + "never": "없음", + "skip_reason": { + "fit_failed": "학습 실패: {error}", + "holdout_too_small": "새 모델을 공정하게 테스트하기에는 사이클이 너무 적습니다. 테스트용으로 따로 둘 수 있는 사이클은 {held_out}개이며 {min}개가 필요합니다.", + "not_better_than_incumbent": "새 모델이 사용 중인 모델보다 낫지 않아(오차 {model} vs {incumbent}) 현재 모델을 유지했습니다.", + "not_better_than_naive": "새 모델이 단순 추정보다 확실히 낫지 않았습니다(오차 {model} vs {naive}).", + "unknown": "새로 학습한 내용이 없습니다.", + "insufficient_rows": "사용할 수 있는 사이클이 아직 부족합니다. 지금까지 라벨이 지정된 정상 사이클은 {count}개입니다.", + "insufficient_rows_other": "사용할 수 있는 사이클이 아직 부족합니다. 지금까지 라벨이 지정된 정상 사이클은 {count}개입니다." + }, + "trained_unknown": "알 수 없는 시간", + "held_out_count": "검증용 사이클 {count}개로 측정", + "held_out_count_other": "검증용 사이클 {count}개로 측정" }, "modal": { "auto_label": "자동 라벨 주기", @@ -780,7 +727,6 @@ "cycle": "주기", "edit_group": "프로필 그룹 편집", "edit_phase": "단계 편집", - "import_config": "구성 가져오기", "label_cycle": "라벨주기", "merge_cycles": "{n} 사이클 병합", "new_group": "새 프로필 그룹", @@ -795,6 +741,8 @@ "delete_automation_title": "자동화 삭제", "delete_automation_msg": "Home Assistant에서 자동화 \"{name}\"을(를) 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.", "remove_legacy_title": "레거시 작업 제거", + "reset_phase_msg": "\"{name}\"에 대한 편집을 취소하고 기본 제공 단계를 복원하시겠습니까? 프로그램의 범위는 유지됩니다.", + "reset_phase_title": "단계 초기화", "remove_legacy_msg": "레거시 맞춤 작업을 제거하시겠습니까? 사이클 이벤트에서 더 이상 실행되지 않습니다. 이 작업은 패널에서 취소할 수 없습니다.", "discard_recording_title": "녹음 삭제", "discard_recording_msg": "저장된 녹음을 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.", @@ -815,32 +763,43 @@ "store_share_device": "이 기기 공유", "export_select": "내보내기 - 데이터 선택", "import_wizard": "가져오기 - 데이터 선택", - "history_import": "전력 이력 가져오기" + "history_import": "전력 이력 가져오기", + "apply_suggestions_title": "제안 적용", + "remove_task_title": "작업 제거", + "remove_task_msg": "알림 \"{task}\"을(를) 제거하시겠습니까? 유지보수 기록의 항목은 유지됩니다.", + "apply_suggestions_msg": "이 {n}개 설정을 변경하시겠습니까? 저장 후 통합이 다시 로드됩니다.", + "discard_settings_title": "저장하지 않은 변경 사항을 버리시겠습니까?", + "discard_settings_msg": "저장되지 않은 설정 변경 사항이 {n}개 있습니다. 설정을 나가고 변경 사항을 버리시겠습니까?", + "profile_title": "프로필 · {name}", + "undo_import_msg": "이 장치의 데이터와 설정이 마지막 가져오기({when}) 이전 상태로 돌아갑니다. 그 이후에 기록된 사이클과 변경 사항은 사라집니다.", + "undo_import_ok": "가져오기 실행 취소", + "undo_import_title": "마지막 가져오기 실행 취소" }, "msg": { "pg_canvas_empty3": "위에서 사이클을 선택하고 실행을 눌러 시뮬레이션하세요. 그런 다음 마우스를 올려 값을 확인하고, 스크롤 또는 두 손가락으로 확대/축소하고, 드래그하여 이동할 수 있습니다.", + "pg_preset_delete_confirm": "프리셋 \"{name}\"을(를) 삭제하시겠습니까?", + "pg_preset_limit": "프리셋 개수 제한에 도달했습니다({n})", + "pg_preset_overwrite": "프리셋 \"{name}\"을(를) 덮어쓰시겠습니까?", "tail_trim_hint": "끝부분에서 제거할 초 수", - "store_sibling_hint": "정확히 같은 모델로 공유된 항목이 없나요? 같은 브랜드의 비슷한 모델이 보통 좋은 출발점이 됩니다.", - "store_declare_appliance": "보유한 가전제품을 WashData에 지정하면 이 탭에 다른 사용자가 공유한 프로그램과 녹음이 표시됩니다. 위에 브랜드를 입력해 둘러볼 수도 있습니다.", - "refresh_catalog_hint": "커뮤니티 브랜드 및 가전제품 목록은 공유 스토어가 일일 조회 한도를 넘지 않도록 캐시됩니다. 새로 고치면 다른 사용자가 추가하거나 승인한 항목을 가져옵니다.", + "store_sibling_hint": "내 모델로 공유된 항목이 없나요? 같은 브랜드의 비슷한 모델로 시작하는 것이 보통 좋습니다.", + "store_declare_appliance": "보유한 가전제품을 WashData에 알려 주면 다른 사용자가 공유한 설정을 볼 수 있습니다. 위에 브랜드를 입력해 둘러볼 수도 있습니다.", + "refresh_catalog_hint": "커뮤니티 브랜드 및 가전제품 목록은 스토어의 일일 한도를 아끼기 위해 캐시됩니다. 새로 고치면 다른 사용자가 추가하거나 승인한 항목을 볼 수 있습니다.", "head_trim_hint": "시작 부분에서 제거할 초 수", "appliance_monitor": "가전제품 모니터", "artifact_dip_detail": "일반 전력 범위 아래로 약 {n}초 동안 내려갔습니다.", "artifact_footer": "위 그래프에 강조표시되어 있습니다. 이는 일시적인 아티팩트(예: 사이클 중간에 문이 열림)이며 반드시 문제가 되는 것은 아닙니다.", - "artifact_header": "이 주기 동안 {n}개의 이상 현상이 감지되었습니다.", - "artifact_pause_detail": "전력이 약 {n}초 동안 거의 0으로 떨어진 후 재개되었습니다. 사이클 도중 문이 열리거나 일시 정지된 것으로 보입니다.", + "artifact_header": "이 사이클 동안 이상 현상 {n}건이 감지되었습니다.", + "artifact_header_other": "이 사이클 동안 이상 현상 {n}건이 감지되었습니다.", + "artifact_pause_detail": "전력이 약 {n}초 동안 거의 0으로 떨어졌다가 재개되었습니다. 문이 열렸거나 사이클이 일시 정지된 것으로 보입니다.", "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": "오버레이 프로필(희미함)", - "compare_profiles_tip": "위 차트에 다른 프로필 봉투를 겹쳐서 어느 것이 이 주기에 가장 적합한지 확인하세요.", + "compare_profiles_tip": "다른 프로필을 차트에 겹쳐 이 사이클에 가장 잘 맞는 것을 확인하세요.", "compare_selected_cycles": "선택한 주기(단색) – 표시/숨기기", - "consider_new_profile": "새 프로필을 만드는 것을 고려해 보세요.", - "coverage_gap": "최근 주기에 일치하는 프로필이 없습니다(지난 30개 중 {pct}%).", - "coverage_gap_similar_cycles": "유사한 미라벨 사이클 {count}개를 발견했습니다 – 프로필을 만들어 매칭을 시작하세요.", "cycles_deleted": "{count}개 사이클이 삭제되었습니다", "enough_data": "학습할 충분한 데이터가 있습니다({current}/{min}사이클)。", "export_description": "JSON으로 내보낼 프로필, 사이클, 설정 등을 정확히 선택하거나, 파일을 분석하여 원하는 부분만 가져올 수 있습니다.", @@ -848,30 +807,25 @@ "feedback_prompt": "그것이 옳았는지 확인하고, 프로그램을 수정하거나, 무시하십시오.", "feedback_relabel_hint": "이 사이클을 다시 라벨 지정하면 이것도 해결됩니다.", "filter_by_profile": "프로필로 필터링…", - "group_modal_help": "온도/회전이 다른 동일한 모양의 프로그램을 그룹화합니다(기간은 다를 수 있음). 매칭은 그룹을 하나의 후보로 평가한 다음 가장 적합한 멤버를 선택합니다. 최소 2개를 선택하세요. 오버레이는 얼마나 유사한지 보여줍니다.", + "group_modal_help": "형태는 같고 온도나 탈수가 다른 프로그램을 그룹으로 묶습니다. 매칭은 그룹을 하나로 평가한 다음 가장 잘 맞는 멤버를 고릅니다. 2개 이상 선택하세요. 오버레이에서 얼마나 비슷한지 볼 수 있습니다.", "group_not_cohesive": "이러한 프로필은 안정적으로 그룹화할 만큼 유사하지 않으므로 일치는 이상값을 제거하거나 그룹을 분할할 때까지 프로필을 개별적으로 처리합니다.", "group_preview_hint": "2명 이상의 멤버를 선택하여 전력 곡선을 미리 보고 비교하세요.", - "import_intro": "내보낸 파일을 로드하거나 아래에 JSON 페이로드를 붙여넣으세요.", - "legacy_actions_title": "{count} 레거시 맞춤 작업이 아직 실행 중입니다.", + "legacy_actions_title": "레거시 맞춤 작업 {count}개가 아직 실행 중입니다.", + "legacy_actions_title_other": "레거시 맞춤 작업 {count}개가 아직 실행 중입니다.", "live_chart_loading": "판독값이 도착하면 실시간 전력 차트가 나타납니다.", "loading": "로드 중…", "loading_curve": "곡선 로드 중…", "loading_settings": "설정 로드 중…", "log_buffer_hint": "최신 항목부터 · 다시 시작한 이후 마지막 500개의 ha_washdata 레코드를 버퍼링합니다. · 크기를 조정하려면 아래쪽 가장자리를 드래그합니다.", - "maintenance_advisory": "지속 시간/에너지가 늘어나면 기기 유지 관리(예: 석회질 제거, 필터 청소)가 필요함을 나타낼 수 있습니다.", "maintenance_due": "유지보수 예정: {items}", - "maintenance_intro": "이 가전에 수행한 정비를 기록하면 각 작업의 다음 예정 시기에 알림을 받습니다.", + "maintenance_intro": "이 가전의 정비를 기록하고 각 작업의 다음 예정 시기에 알림을 받으세요.", "maintenance_load_error": "유지보수 데이터를 불러올 수 없습니다: {error}", "maintenance_requires_access": "유지 관리 및 내보내기/가져오기에는 전체 액세스 권한이 필요합니다.", - "manual_duration_hint": "남은 시간 추정에 사용되는 프로필의 평균/예상 주기 길이입니다. 설정하려면 편집하세요. 변경하지 않고 그대로 두면 현재 값이 유지됩니다.", - "matching_tuning_intro": "학습할 때 WashData는 프로그램 일치가 모양, 지속 시간 및 에너지에 얼마나 큰 영향을 미치는지 조정합니다.", + "manual_duration_hint": "남은 시간 추정에 사용되는 프로필의 예상 사이클 길이입니다. 변경하지 않으면 현재 값이 유지됩니다.", "merge_intro": "선택한 사이클이 하나로 결합됩니다(연대순, 간격이 0W로 채워짐). 결과 프로필을 선택합니다.", "ml_intro": "WashData는 즉시 사용 가능한 스마트 모델과 함께 제공됩니다.", "ml_learned_intro": "이 기계에 맞게 미세 조정된 모델입니다.", - "ml_loading": "ML 로드 중…", "ml_settings_intro": "두 개의 독립적인 스위치: 하나는 사이클이 실행되는 동안 모델을 적용하고 다른 하나는 WashData가 시간이 지남에 따라 기계에 맞게 미세 조정할 수 있도록 합니다.", - "name_first_program": "충분한 사이클이 모였습니다 – 첫 프로그램에 이름을 지정하여 일치를 시작하세요.", - "near_duplicate_cluster": "거의 중복된 프로필 클러스터가 감지되었습니다. 그룹화를 통해 일치하는 항목을 유사 항목(예: 서로 다른 온도/회전의 동일한 프로그램) 중에서 안정적으로 선택할 수 있습니다.", "no_cycles_match": "현재 필터와 일치하는 사이클이 없습니다.", "no_cycles_profile": "이 프로필에는 사이클이 없습니다.", "no_cycles_selected": "먼저 사이클을 하나 이상 선택하세요.", @@ -896,73 +850,50 @@ "no_suggestions": "활성 제안이 없습니다.", "notify_services_hint": "{entity} 서비스 ID를 사용하세요(여러 개는 쉼표로 구분)。템플릿 변수:{vars}。", "old_actions_warning": "이전 작업 편집기로 구성되었습니다(현재 제거됨). 사이클 이벤트에서는 여전히 실행되지만 여기에서는 더 이상 편집할 수 없습니다. 일반 자동화로 변환하거나 제거하십시오.", - "onboarding_progress": "{n} / 3개 사이클 관찰됨", - "onboarding_watching": "가전을 평소처럼 사용하세요 – WashData가 관찰하고 있습니다. 3개 사이클 후 프로그램 일치가 시작됩니다.", "pending_feedback": "보류 중인 감지 피드백", "performance_trend": "실적 추세({n}주기)", - "pg_analysis_empty": "매칭 분석을 보려면 사이클을 불러오세요.", "pg_apply_confirm": "최적 값 적용: {label} = {value}?", - "pg_canvas_empty": "위에서 사이클을 선택한 다음 불러오기를 눌러 전력 트레이스를 확인하세요.", - "pg_chart_caption": "{param} 값에 따른 감지/매칭률", "pg_ctrl_intro": "값은 이 기기에 적용 중인 통합 설정에서 시작합니다. 편집한 내용은 적용하기 전까지 플레이그라운드에만 남습니다.", "pg_live_unavailable": "적용 중인 설정을 가져올 수 없어 기본값을 표시합니다.", "pg_load_live_confirm": "플레이그라운드 편집 {n}개를 버리고 통합의 현재 설정을 다시 불러오시겠습니까?", - "pg_match_rate": "{total}개 사이클에서 {pct}% 매칭률", "pg_matches_live": "적용 중인 설정과 동일", "pg_n_changed": "적용 중인 설정과 {n}개 다름", "pg_sugg_none": "모든 제안이 현재 설정과 이미 일치합니다", - "pg_preset_delete_confirm": "프리셋 \"{name}\"을(를) 삭제하시겠습니까?", - "pg_preset_limit": "프리셋 개수 제한에 도달했습니다({n})", - "pg_preset_overwrite": "프리셋 \"{name}\"을(를) 덮어쓰시겠습니까?", "pg_publish_one_confirm": "{label} = {value}을(를) 이 기기의 설정에 저장하시겠습니까?", "pg_restart_note": "시뮬레이션 도구를 활성화하려면 Home Assistant를 다시 시작하세요.", - "pg_sim_progress": "{done} / {total} 사이클", - "pg_starting": "시작 중…", - "pg_stress_synthetic": "대기 시뮬레이션이 여기서 시작됩니다", - "pg_sweep_intro": "{param}이(가) 달랐다면 어땠을까요? 가장 많은 사이클이 올바르게 매칭되는 설정을 찾기 위해 최근 {cycles}개 사이클에서 {steps}개 값을 테스트합니다.", - "pg_sweep_step": "단계 {done} / {total}", - "pg_verdict_bad": "주의 필요: 많은 사이클이 감지되지 않고 있습니다.", - "pg_verdict_good": "잘 조정됨: 대부분의 사이클이 올바르게 식별되고 매칭됩니다.", - "pg_verdict_ok": "양호: 일부 사이클을 놓쳤습니다. 시작 임계값을 낮춰 보세요.", "phase_catalog_intro": "주기의 명명된 부분(세탁 전, 가열, 탈수…). 제어판에서 프로필에 할당합니다.", "phase_ranges_intro": "평균 곡선에 중첩된 단계 범위(사이클 시작 후 분). 실시간으로 미리 보려면 값을 편집하세요.", - "playground_intro": "과거 사이클을 다른 설정으로 재생하고, 두 구성을 나란히 비교하며, 사이클이 프로필에 어떻게 정렬되는지 살펴볼 수 있는 안전한 샌드박스입니다. 명시적으로 적용하기 전까지 여기서의 작업은 실제 데이터를 변경하지 않습니다.", - "playground_needs_restart": "플레이그라운드 도구를 활성화하려면 Home Assistant를 다시 시작하세요.", - "preferences_admin": "환경 설정, 패널 및 액세스 제어", - "preferences_adv": "환경설정", + "playground_intro": "실제 사이클 데이터로 설정을 시험해 보세요. 적용하기 전까지 여기서의 작업은 현재 설정을 바꾸지 않습니다.", "prefs_personal": "이는 홈어시스턴트 계정에만 적용됩니다.", - "process_history_hint": "저장된 모든 사이클에 대해 매칭을 재실행하고, 튜닝 제안을 새로 고치고, ML 모델을 재훈련하고(활성화된 경우), 기록된 에너지 가격으로 사이클 비용을 다시 계산하고, 사이클 건강을 재계산합니다. 일괄 검토 후 실행하세요.", + "process_history_hint": "저장된 모든 사이클의 매칭을 다시 실행하고, 제안을 새로 고치고, ML 모델을 재학습하고(활성화된 경우), 기록된 가격으로 사이클 비용을 다시 계산하고, 사이클 건강을 재계산합니다. 여러 사이클을 검토한 후 실행하세요.", "profile_deleted": "프로필이 삭제되었습니다", - "profile_poor_health_detail": "이 프로필에 할당된 주기의 모양이 일관되지 않거나 신뢰도가 낮습니다. 봉투를 재구성하거나 라벨이 붙은 주기를 검토하는 것을 고려하십시오.", "profiles_intro": "통계, 단계 및 정리를 보려면 프로필을 클릭하세요. 거의 동일한 프로그램(동일한 모양/기간, 다른 온도 또는 회전)을 그룹화하여 일치하는 프로그램을 안정적으로 선택합니다.", - "rbac_hint": "꺼져 있으면 모든 홈어시스턴트 사용자가 전체 액세스 권한을 갖습니다(기본값). 관리자는 항상 전체 액세스 권한을 가지며 모든 사람을 관리할 수 있습니다.", - "recent_logs": "최근 ha_washdata 기록", + "rbac_hint": "꺼짐: 모든 Home Assistant 사용자가 전체 액세스 권한을 가집니다(기본값). 관리자는 항상 전체 액세스 권한이 있으며 모든 사용자를 관리할 수 있습니다.", "recording_in_progress": "녹음 진행 중", "reminders_intro": "마지막 서비스 이후 지정한 사이클 수가 지나면 패널에 알림을 표시합니다. 비워 두거나 0으로 설정하면 알림이 꺼집니다.", "restart_gap_footer": "그래프에서 강조 표시됩니다. 이 구간에 전력 데이터가 없습니다. 매칭에는 실제 측정값만 사용되었습니다.", - "restart_gap_header": "{n} HA 재시작 공백 (이 사이클 중)", + "restart_gap_header": "이 사이클 중 HA 재시작 공백 {n}개", + "restart_gap_header_other": "이 사이클 중 HA 재시작 공백 {n}개", "restart_gap_item": "{dur} 공백", - "review_confirm_help": "이 주기가 올바르게 감지되었는지 확인하십시오. 귀하의 리뷰는 귀하의 기계에서 모델을 훈련시킵니다. 더 많은 주기를 확인할수록 더 나은 일치 및 상태 점수를 얻을 수 있습니다. 빠른 좋음/나쁨이면 충분합니다.", + "review_confirm_help": "이 사이클이 올바르게 감지되었나요? 사이클을 확인하면 기기의 매칭이 개선됩니다. 간단히 '좋은' 또는 '나쁜'만 골라도 충분합니다.", "review_in_settings": "설정에서 검토", "review_notes_placeholder": "메모(선택사항)", "review_notes_tip": "자신만의 참조를 위한 자유 텍스트 메모입니다. 일치 또는 훈련에서는 사용되지 않습니다.", - "review_profile_tip": "이 주기에 라벨이 붙은 프로그램입니다. 자동 감지된 프로그램이 잘못된 경우 여기에서 수정하십시오. 라벨링은 향후 주기에 대한 일치를 가르칩니다.", - "review_quality_tip": "이주기가 얼마나 깨끗한 지. 좋음 = 이 프로그램의 교과서 예제; 나쁨 = 감지되었지만 잡음이 많거나 비정형적임; 사용할 수 없음 = 잘못 감지됨(병합, 잘림 또는 허위). 상태 점수와 모델 학습에 허용되는 주기를 결정합니다.", - "review_recorded_tip": "이를 프로그램에 대해 직접 선택한 참조 주기로 표시합니다. 이는 수동으로 기록된 주기와 동일한 역할입니다. 참조 순환은 항상 유지되고 일치하는 템플릿을 시드하며 정리로 인해 삭제되지 않습니다. (이것은 \"황금\"/기록된 플래그입니다. 둘 다 동일합니다.)", - "review_tags_tip": "이 주기에 무엇이 잘못되었는지 설명하는 선택적 플래그이므로 훈련과 정리가 이를 설명할 수 있습니다.", + "review_profile_tip": "이 사이클에 지정된 프로그램 라벨입니다. 틀렸다면 여기서 수정하세요. 라벨은 매칭 학습에 사용됩니다.", + "review_quality_tip": "이 사이클의 품질입니다. '좋은': 이 프로그램의 모범적인 운전. '나쁜': 감지는 되었지만 잡음이 많거나 특이함. '쓸 수 없는': 잘못 감지됨(병합, 잘림 또는 허위). 상태 점수와 모델 학습에 사용할 사이클에 영향을 줍니다.", + "review_recorded_tip": "이 사이클을 수동으로 기록한 사이클처럼 해당 프로그램의 엄선된 참조 사이클로 표시합니다. 참조 사이클은 항상 보존되고 매칭 템플릿의 기반이 되며 정리 시 삭제되지 않습니다.", + "review_tags_tip": "무엇이 잘못되었는지 표시하는 선택적 플래그로, 학습과 정리에서 이를 고려합니다.", "review_to_cycles": "사이클 검토 대기열 열기", "samples_decimated": "{total}개 샘플 중 {shown}개 표시 중 (표시용으로 솎아냄; 피크는 유지). 여기서 넓은 간격은 솎아낸 것이며 누락된 데이터가 아닙니다.", - "saving_triggers_reload": "저장하면 통합 다시 로드가 트리거됩니다. HA 엔터티는 일시적으로 사용할 수 없는 것으로 표시될 수 있습니다.", + "saving_triggers_reload": "저장하면 통합이 다시 로드됩니다. 엔티티가 잠시 사용할 수 없음으로 표시될 수 있습니다.", "search_placeholder": "검색 설정…", "see_recorder": "아래의 녹음기 위젯을 참조하세요", "setting_changed": "{date}에 {old}에서 {new}(으)로 변경됨", "settings_basic_note": "필수 설정만 표시하고 있습니다. 전체 목록을 보려면 고급으로 전환하세요.", - "shape_drift_advisory": "⚠ 형태 변화 감지됨", - "shape_drift_detail": "이 프로필의 전력 패턴이 시간이 지남에 따라 변화했습니다 – 가전제품 마모 또는 유지보수가 필요할 수 있습니다 (예: 스케일 제거, 필터 청소).", "show_all_settings": "모든 설정 표시", - "showing_suggestions": "제안사항과 함께 {count} 설정을 표시합니다.", - "split_intro": "그래프를 클릭하여 분할 지점을 추가 또는 제거하거나 유휴 간격으로 자동 감지합니다. 각 결과 세그먼트는 자체 프로필을 얻을 수 있습니다.", - "storage_diagnostics": "스토리지 통계, 유지 관리, 내보내기/가져오기", + "showing_suggestions": "제안이 있는 설정 {count}개를 표시합니다.", + "showing_suggestions_other": "제안이 있는 설정 {count}개를 표시합니다.", + "split_intro": "그래프를 클릭해 분할 지점을 추가하거나 제거하고, 유휴 간격으로 자동 감지할 수도 있습니다. 각 부분에 별도의 프로필을 지정할 수 있습니다.", "sug_staged": "{key} = {val} 설정됨. 저장하면 적용됩니다.", "toast_auto_detect_enabled": "자동 감지 활성화됨", "toast_auto_label_complete": "자동 레이블 지정 완료", @@ -979,7 +910,6 @@ "toast_no_automation": "여기서는 자동화를 생성할 수 없습니다", "toast_profile_name_required": "프로필 이름이 필요합니다", "toast_program_set": "프로그램이 설정됨: {program}", - "toast_rebuild_failed": "재구성 실패: {error}", "toast_resume_failed": "재개 실패: {error}", "toast_revert_failed": "되돌리기 실패: {error}", "toast_reverted": "{key}을(를) 되돌렸습니다", @@ -990,100 +920,85 @@ "trend_energy_up": "에너지 상승 추세({pct}%/주기) – 최근 평균 {avg}", "trim_destructive_confirm": "사이클의 {pct}%만 남으며 이 작업은 취소할 수 없습니다. 계속하시겠습니까?", "trim_intro": "빨간색 핸들을 드래그하거나 값을 입력합니다. 창 밖의 모든 것이 제거됩니다.", - "tuning_suggestions_available": "관찰된 주기에서 사용할 수 있는 {count} 조정 제안입니다. 관련 필드 옆에 표시됩니다.", + "tuning_suggestions_available": "관찰된 사이클에서 조정 제안 {count}개를 사용할 수 있습니다. 관련 필드 옆에 표시됩니다.", + "tuning_suggestions_available_other": "관찰된 사이클에서 조정 제안 {count}개를 사용할 수 있습니다. 관련 필드 옆에 표시됩니다.", "unsure_detected_prefix": "WashData가 감지되었는지 확실하지 않습니다.", - "updated": "업데이트됨", "warmup_badge": "학습 중 ({done}/{needed} 사이클)", - "warmup_detail": "이 프로필은 자동 매칭이 시작되기 전에 {needed}개의 라벨이 지정된 사이클이 필요합니다. 확인된 사이클마다 학습에 도움이 됩니다.", + "warmup_detail": "매칭은 이미 작동합니다. WashData가 스스로 라벨을 지정하기 전에 이 프로필의 처음 {needed}개 사이클을 확인해 주세요.", "wipe_history_warning": "모든 사이클과 프로필을 영구적으로 삭제합니다. 취소할 수 없습니다.", "compare_overlay_tip": "학습된 프로필 봉투를 겹쳐서 각 사이클이 어떤 프로그램과 유사한지 확인하세요.", - "duration_trend_up_tip": "지속 시간 증가 ({pct}%/사이클)", - "duration_trend_down_tip": "지속 시간 감소 ({pct}%/사이클)", - "energy_trend_up_tip": "에너지 증가 ({pct}%/사이클)", - "energy_trend_down_tip": "에너지 감소 ({pct}%/사이클)", - "performance_trending": "실적 추세", - "pg_select_cycle_hint": "사이클을 선택하고 ↺를 클릭하여 불러오세요", - "pg_sweep_progress": "단계 {done} / {total}", - "pg_analysis_hint": "사이클을 선택하고 ↺를 클릭하여 분석을 불러오세요.", "diagnostics_load_failed": "진단을 불러올 수 없습니다: {error}", - "manual_duration_ref_hint": "참조 주기가 선택되지 않았을 때만 사용됩니다. 참조 주기는 자체 길이에 따라 기간을 설정합니다.", + "manual_duration_ref_hint": "참조 사이클이 없을 때만 사용됩니다. 참조 사이클이 있으면 그 길이로 소요 시간이 정해집니다.", + "duration_learned_hint": "이 프로그램의 사이클(이상값 제외)로 계산되며 새 사이클마다 갱신됩니다. 바꾸려면 벗어난 사이클을 자르거나 삭제하세요.", "toast_convert_rolled_back": "변환에 실패하여 롤백되었습니다(남은 자동화 없음): {error}", "toast_convert_orphan": "자동화는 생성되었지만 이전 작업 삭제에 실패했습니다. 다시 시도하지 마세요. 중복 자동화를 방지하려면 이전 작업을 수동으로 제거하세요.", - "advisory_duration_trend_up": "'{name}' 사이클이 점점 길어지고 있습니다(사이클당 약 +{pct}%). 기기 동작이 바뀌었다면 이 프로필을 다시 기록하거나 재구성하세요.", - "advisory_energy_trend_up": "'{name}'의 에너지 사용량이 점점 늘고 있습니다(사이클당 약 +{pct}%) - 예상 밖이라면 기기를 점검해 보세요.", - "advisory_poor_health": "'{name}'의 적합도 점수가 낮습니다 - 최근 사이클의 편차가 크거나 매칭이 약합니다. 매칭과 시간 추정의 정확도를 유지하려면 사이클을 검토하거나 프로필을 다시 기록하세요.", - "advisory_shape_drift": "'{name}'이(가) 원래 전력 파형에서 크게 벗어났습니다. 시간이 지나며 기기 동작이 변했을 수 있습니다(예: 물때, 마모). 최근 사이클로 이 프로필을 다시 기록하는 것을 고려하세요.", - "advisory_shape_drift_corr": "'{name}'이(가) 원래 전력 파형에서 크게 벗어났습니다(상관관계 {corr}). 시간이 지나며 기기 동작이 변했을 수 있습니다(예: 물때, 마모). 최근 사이클로 이 프로필을 다시 기록하는 것을 고려하세요.", - "advisory_unmatchable": "'{name}'은(는) 매칭될 수 없습니다. 전력 데이터가 있는 사이클이 하나도 없어 WashData가 실행 중인 사이클과 비교할 대상이 없습니다. 이 프로그램의 사이클에 라벨을 붙이거나 사이클을 기록하면 다시 매칭 대상이 됩니다.", - "advisory_unmatchable_no_duration": "'{name}'은(는) 매칭될 수 없습니다. 전력 데이터가 있는 사이클은 있지만 사용할 수 있는 소요 시간이 없어 WashData가 매칭의 기준으로 삼을 길이가 없습니다. 이 프로그램을 다시 기록하거나 예상 소요 시간을 설정하면 다시 매칭 대상이 됩니다.", + "advisory_duration_trend_up": "'{name}' 사이클이 계속 길어지고 있습니다(사이클당 약 +{pct}%). 기기가 바뀌었다면 이 프로필을 다시 기록하거나 재구성하세요.", + "advisory_energy_trend_up": "'{name}'의 에너지 사용량이 사이클마다 늘고 있습니다(사이클당 약 +{pct}%). 예상 밖이라면 기기를 점검하세요.", + "advisory_poor_health": "'{name}'의 적합도가 낮습니다. 최근 사이클의 편차가 크거나 매칭이 약합니다. 매칭과 추정의 정확도를 유지하려면 사이클을 검토하거나 다시 기록하세요.", + "advisory_shape_drift": "'{name}'이(가) 원래 전력 파형에서 벗어났습니다. 물때나 마모 때문일 수 있습니다. 최근 사이클로 다시 기록하는 것을 고려하세요.", + "advisory_shape_drift_corr": "'{name}'이(가) 원래 전력 파형에서 벗어났습니다(상관관계 {corr}). 물때나 마모 때문일 수 있습니다. 최근 사이클로 다시 기록하는 것을 고려하세요.", + "advisory_unmatchable": "'{name}'은(는) 매칭될 수 없습니다. 전력 데이터가 있는 사이클이 없습니다. 이 프로그램의 사이클에 라벨을 지정하거나 기록하면 해결됩니다.", + "advisory_unmatchable_no_duration": "'{name}'은(는) 매칭될 수 없습니다. 사이클에 사용할 수 있는 소요 시간이 없습니다. 이 프로그램을 다시 기록하거나 예상 소요 시간을 설정하세요.", "pg_no_alerts": "이번 실행에서 문제가 감지되지 않았습니다.", "pg_override_active": "편집한 설정을 현재 설정과 비교하여 사용 중", "pg_history_empty": "실행을 눌러 사이클을 다시 재생하세요.", "pg_history_summary": "{detected}/{total} 감지됨 · {correct} 올바르게 매칭", "pg_sweep_no_metric": "이 목표를 평가하기에 데이터가 부족합니다.", "pg_sweep_intro2": "최근 사이클 전체에서 목표를 가장 잘 충족하는 설정을 찾습니다. 적용하기 전까지는 아무것도 변경되지 않습니다.", - "pg_running_history": "사이클을 다시 재생하는 중…", - "pg_running_sweep": "전체 사이클에서 값을 테스트하는 중…", "pg_row_load_hint": "이 사이클을 위 그래프에 불러오기", - "pg_history_intro2": "위 설정으로 최근 사이클을 실제 감지기와 매처를 통해 다시 재생합니다. 행을 클릭하면 해당 사이클을 그래프에 불러옵니다. 설정을 변경하면 변경 전후 비교를 확인할 수 있습니다.", + "pg_history_intro2": "위 설정으로 최근 사이클을 실제 감지기와 매처로 다시 재생합니다. 행을 클릭하면 그래프에 불러옵니다. 설정을 바꾸면 변경 전후를 비교할 수 있습니다.", "pg_simulating": "사이클을 시뮬레이션하는 중…", - "pg_canvas_empty2": "위에서 사이클을 선택하고 실행을 눌러 시뮬레이션하세요. 그런 다음 마우스를 올려 값을 확인하고, 스크롤하여 확대/축소하고, 드래그하여 이동할 수 있습니다.", "pg_apply_settings_confirm": "이 {n}개 설정을 이 기기에 저장하시겠습니까? {list}", - "pg_stress_terminated_detail": "약 {idle}W 대기 상태로 안정되었습니다 -- {h}시간 {m}분 후 {reason}(으)로 인해 사이클이 종료되었습니다.", - "pg_stress_above_threshold_detail": "대기 전력 약 {idle}W가 유효 정지 임계값 ({stop}W) 이상입니다 -- 사이클이 한 번도 조용한 상태로 인식되지 않았습니다. 해결하려면 stop_threshold_w 값을 높이세요.", - "pg_stress_hit_cap_detail": "사이클이 멈추지 않고 {h}시간 {m}분 동안 실행되었습니다 -- 안전 한도에 의해 강제 중지되었습니다. 대기 전력 {idle}W 대 정지 임계값 {stop}W.", "no_logs_match": "이 필터와 일치하는 로그 기록이 없습니다.", "pg_analysis_hint2": "사이클을 선택하고 실행을 누르면 매칭 분석이 로드됩니다.", - "pg_analysis_empty2": "실행을 누르면 매칭 분석이 표시됩니다.", - "imported_readonly": "커뮤니티 스토어에서 가져왔습니다. 참조 및 매칭용으로 표시됩니다. 통계에는 포함되지 않으며 편집할 수 없습니다.", + "imported_readonly": "커뮤니티 스토어에서 가져왔습니다. 참조와 매칭에 사용되며 통계에는 포함되지 않고 편집할 수 없습니다.", "merge_no_imports": "가져온 녹음은 실제 사이클에 병합할 수 없습니다. 병합하려면 선택을 해제하세요.", - "store_enable_hint": "설정에서 온라인 기능을 활성화하면 커뮤니티 참조 주기를 탐색하고 가져올 수 있습니다.", - "advisory_duration_outlier": "'{name}'(으)로 라벨이 지정된 사이클 {n}개는 이 프로그램의 일반적인 길이에서 너무 멀어 매칭될 수 없습니다(최대 {ratio}x). 라벨이 잘못되었거나, 한 번의 기록에 두 번의 운전이 담겼을 수 있습니다. 라벨을 다시 지정하거나 분리해서 이 프로그램의 시간 추정이 왜곡되지 않도록 하세요.", + "store_enable_hint": "환경설정 > 온라인 및 커뮤니티에서 온라인 기능을 켜면 커뮤니티 참조 사이클을 탐색하고 가져올 수 있습니다.", + "advisory_duration_outlier": "'{name}'(으)로 라벨이 지정된 사이클 {n}개는 일반적인 길이와 너무 달라 매칭될 수 없습니다(최대 {ratio}x). 라벨이 잘못되었거나 두 번의 운전이 담겼을 수 있습니다. 라벨을 다시 지정하거나 분할해서 시간 추정이 왜곡되지 않게 하세요.", "standby_above_stop_title": "사이클이 끝났을 때 여전히 {idle} W를 사용 중입니다", "standby_above_stop_sub": "최근 {total}개 사이클 중 {n}개입니다. 오프 지연은 전력이 중지 임계값({stop} W) 아래로 내려간 뒤에야 시작되므로, 사이클이 늦게 끝나거나 수동으로 정지해야 합니다.", - "online_intro": "다른 WashData 사용자와 참조 주기를 공유하고 다운로드하세요. 모든 온라인 기능은 선택 사항이며 기본적으로 꺼져 있습니다.", "store_share_intro": "이 참조 주기를 업로드하면 같은 가전제품을 사용하는 다른 사용자가 이용할 수 있습니다. 공개되기 전에 검토를 거칩니다.", - "online_intro_global": "다른 WashData 사용자와 참조 녹음을 탐색하고 공유하며 가전제품 항목을 확인하세요. 하나의 연결이 전체 WashData 통합에 적용됩니다. 가전제품 브랜드와 모델은 '기본 구성'에서 기기별로 설정합니다. 모든 온라인 기능은 선택 사항이며 기본적으로 꺼져 있습니다.", + "online_intro_global": "다른 WashData 사용자와 참조 녹음을 탐색하고 공유하세요. 하나의 연결로 모든 WashData 기기에 적용되며, 브랜드와 모델은 '기본'에서 기기별로 설정합니다. 온라인 기능은 직접 켜기 전까지 꺼져 있습니다.", "online_unavailable": "이 서버에서는 온라인 기능을 사용할 수 없습니다.", "store_picker_offline": "설정(톱니바퀴)에서 온라인 기능을 활성화하면 커뮤니티 카탈로그에서 선택할 수 있습니다.", "pick_brand_first": "먼저 가전제품 브랜드를 선택하세요.", - "model_not_found": "아직 카탈로그에 없습니다.", - "connect_to_confirm": "확인하거나 평가하려면 설정(톱니바퀴)에서 연결하세요.", + "custom_task_limit": "작업 한도 {max}개에 도달했습니다.", + "custom_tasks_intro": "알림을 받고 싶은 다른 항목을 추가하세요. 두 간격 중 하나에 도달하면 작업이 예정 상태가 됩니다. 하나만 사용하려면 다른 하나를 비워 두거나 0으로 설정하세요.", + "maintenance_due_sensor_hint": "\"유지보수 예정\" 바이너리 센서는 예정된 작업이 있는 동안 켜집니다. 직접 만든 알림 자동화에 사용하세요.", + "maintenance_supporter_intro": "Maintenance Supporter는 훨씬 더 많은 기능을 제공하는 별도의 통합입니다. 모든 가전이나 작업, 시간·사용량·센서 기반 일정, 알림, 기록을 지원합니다. WashData 사이클 카운터를 사용할 수 있습니다.", "adopt_settings_hint": "이 기기의 감지 및 매칭 임계값을 공유된 것으로 덮어씁니다. 알림, 엔티티, 에너지 가격은 변경되지 않습니다.", - "include_settings_hint": "이 기기의 인식 및 매칭 임계값을 공유합니다(알림, 엔티티 또는 에너지 가격 제외). 채택자가 적용 여부를 선택합니다.", + "include_settings_hint": "이 기기의 감지 및 매칭 임계값을 공유합니다(알림, 엔티티, 에너지 가격 제외). 적용 여부는 다른 사용자가 선택합니다.", "onboard_download": "새 기기인가요? 같은 가전제품을 가진 다른 WashData 사용자의 완성된 설정(프로그램, 참조 사이클, 페이즈)을 채택하세요.", - "share_consent": "이 사이클들이 중단 없이 정상적으로 완료되었음을 확인합니다", "share_device_none": "아직 공유 가능한 사이클이 없습니다. 먼저 사이클 탭에서 기록되거나 수동으로 선택한 사이클을 참조 사이클(⭐)로 표시하세요.", "share_guideline_naming": "각 프로파일의 이름을 기기의 다이얼이나 디스플레이에 표시된 대로 정확히 지정하세요(예: 'Cotton 40', 'Eco 60').", - "share_guideline_quality": "정상적으로 완료된 사이클만 공유하세요 -- 사이클 중간 중단, 문 열림 이벤트 또는 전력 이상이 없는 것만 공유합니다.", + "share_guideline_quality": "정상적으로 끝난 사이클만 공유하세요. 중단, 문 열림, 전력 이상이 없어야 합니다.", "share_guideline_review": "업로드는 검토 중으로 시작되며 충분한 커뮤니티 멤버가 확인하면 공개됩니다.", "share_guidelines_title": "공유하기 전에", - "store_download_device_intro": "공유된 모든 프로그램과 해당 참조 사이클을 기기에 채택합니다. 직접 기록한 사이클과 통계는 영향을 받지 않습니다.", + "store_download_device_intro": "공유된 모든 프로그램과 참조 사이클을 기기에 추가합니다. 직접 기록한 사이클과 통계에는 영향이 없습니다.", "store_share_device_intro": "선택한 참조 사이클과 함께 {brand} {model}을 업로드합니다. 같은 가전제품을 가진 다른 사용자가 프로그램을 채택할 수 있습니다. 항목은 공개 전에 검토됩니다.", "share_profile_no_cycles": "참조 사이클 없음 -- 이 프로파일을 포함하려면 사이클 탭에서 사이클을 ⭐로 표시하세요", - "advisory_phase_inconsistent": "'{name}'은(는) 서로 다른 프로그램이나 온도가 섞여 있는 것 같습니다. 사이클마다 가열 시간이 크게 다릅니다. 온도별 등으로 별도의 프로필로 나누면 매칭과 시간 추정이 개선됩니다.", - "advisory_phase_inconsistent_title": "⚠ 프로그램이 섞였을 수 있음", - "export_select_intro": "포함할 항목을 정확히 선택하세요. 사이클 없이 프로필만 선택해도 인식 가능한 프로그램은 내보내집니다(학습된 형태도 함께 이동합니다).", - "import_analyze_hint": "내보낸 파일을 불러오거나 해당 JSON을 붙여넣으세요. WashData가 이를 분석하여 변경 전에 가져올 수 있는 항목을 정확히 보여줍니다.", + "export_select_intro": "포함할 항목을 선택하세요. 사이클 없이 프로필만 선택해도 매칭 가능한 프로그램으로 내보내집니다.", + "import_analyze_hint": "내보낸 파일을 불러오거나 JSON을 붙여넣으세요. 변경하기 전에 가져올 수 있는 항목을 WashData가 보여줍니다.", "analyzing": "분석 중…", - "device_type_mismatch_warn": "이 내보내기는 다른 기기 유형에서 생성되었습니다({src} 대 {local}). 프로그램과 사이클은 참조 데이터로 가져올 수 있지만, 기기별 설정과 실제 기록 가져오기는 비활성화됩니다.", + "device_type_mismatch_warn": "이 내보내기는 다른 기기 유형에서 생성되었습니다({src} 대 {local}). 프로그램과 사이클은 참조 데이터로 가져올 수 있지만 기기 설정과 실제 기록은 가져올 수 없습니다.", "merge_hint": "가져온 항목이 추가되며 로컬 데이터는 손실되지 않습니다. 이름 충돌은 아래에서 해결합니다.", "replace_warn": "선택한 각 범주는 삭제되고 파일 내용으로 교체됩니다. 선택하지 않은 범주는 그대로 유지됩니다.", "dest_reference_hint": "가져온 사이클은 프로그램 인식만 개선하며 사용량/에너지 통계에는 영향을 주지 않습니다.", - "dest_real_history_hint": "가져온 사이클은 이 기기 자체의 기록으로 집계되어 에너지/사용량 통계에 반영됩니다. 기기 하나를 새 설치 환경으로 옮길 때 사용하세요.", + "dest_real_history_hint": "가져온 사이클은 이 기기 자체의 기록과 통계로 집계됩니다. 기기를 새 설치 환경으로 옮길 때 사용하세요.", "sug_muted": "이 설정은 더 이상 제안되지 않습니다", "sug_mute_failed": "제안을 음소거할 수 없습니다", "sug_unmuted_all": "뮤트된 제안을 초기화했습니다", - "n_suggestions_muted": "{count}개 뮤트됨. 자동 튜너가 이 설정들을 제안하지 않습니다.", + "n_suggestions_muted": "제안 {count}개가 뮤트됨. 자동 튜너가 이 제안들을 다시 제안하지 않습니다.", + "n_suggestions_muted_other": "제안 {count}개가 뮤트됨. 자동 튜너가 이 제안들을 다시 제안하지 않습니다.", "font_size_hint": "이 패널의 모든 내용을 크거나 작게 표시합니다. 이 기기의 내 계정에 적용됩니다.", - "import_history_description": "WashData를 사용하기 전에도 스마트 플러그를 쓰고 있었나요? 해당 전력 센서의 이력 내보내기 파일을 업로드하거나 Home Assistant에서 바로 읽어오면 평소와 같은 감지가 그 데이터에 실행되어, 과거 사이클이 사이클 목록에 나타나 이름을 지정할 수 있습니다.", - "hist_input_hint": "이력 패널에서 다운로드한 CSV(entity, state, last changed)를 업로드하거나, WashData가 센서 이력을 직접 읽도록 하세요. 그러면 실시간과 똑같은 감지가 실행되고, 찾은 사이클 중 어떤 것을 보관할지 직접 선택합니다.", - "hist_recorder_hint": "선택한 날짜부터 현재까지 읽습니다. Home Assistant는 기본적으로 상세 이력을 10일간 보관하고 그 이후에는 시간별 평균만 남기는데, 이는 사이클을 감지하기에 너무 거칩니다. recorder가 더 오래 보관하도록 설정되어 있을 때만 더 이전 날짜를 선택하세요.", - "hist_scanning": "이력을 감지기로 다시 재생하고 있습니다. 이 작업은 백그라운드에서 실행되므로 이 창을 닫고 나중에 다시 열어도 됩니다.", + "import_history_description": "WashData 이전부터 스마트 플러그를 쓰셨나요? 전력 이력을 업로드하거나 Home Assistant에서 읽어오세요. 감지가 실행되어 과거 사이클이 사이클 목록에 나타나고 이름을 지정할 수 있습니다.", + "hist_input_hint": "이력 패널의 CSV(entity, state, last changed)를 업로드하거나 WashData가 센서 이력을 직접 읽게 하세요. 실시간과 같은 감지가 실행되고, 보관할 사이클을 직접 선택합니다.", + "hist_recorder_hint": "선택한 날짜부터 현재까지 읽습니다. Home Assistant는 기본적으로 상세 이력을 10일간 보관하며, 그보다 오래된 시간별 평균은 사이클을 감지하기에 너무 거칩니다.", + "hist_scanning": "백그라운드에서 이력을 감지기로 처리하고 있습니다. 이 창을 닫고 나중에 다시 와도 됩니다.", "hist_imported_count": "{n}개 사이클을 가져왔습니다.", "hist_duplicates": "{n}개는 이미 가져온 항목이라 건너뛰었습니다.", "hist_capped": "기기당 가져온 사이클 한도에 도달하여 나머지는 저장되지 않았습니다.", - "hist_next_step": "가져온 이력으로 표시되어 사이클 목록에 있습니다. 하나를 열고 라벨 지정으로 해당 프로그램 이름을 지정하세요.", + "hist_next_step": "가져온 이력으로 표시되어 사이클 목록에 있습니다. 하나를 열고 라벨 지정으로 프로그램 이름을 지정하세요.", "hist_rows_read": "측정값 {n}개 읽음", "hist_entity_substituted": "{used}을(를) 읽었습니다 (이 기기에는 {wanted}이(가) 설정되어 있습니다)", "hist_breaks": "센서를 사용할 수 없었던 공백 {n}곳", @@ -1095,19 +1010,53 @@ "hist_scan_capped": "처음 후보만 표시합니다(총 {n}개 발견).", "hist_recorder_empty": "Home Assistant에 해당 기간의 이 센서 상세 이력이 없습니다.", "hist_scan_failed": "검색에 실패했습니다.", - "hist_scan_expired": "해당 검색 결과는 더 이상 사용할 수 없습니다. 다시 검색해 주세요.", + "hist_scan_expired": "해당 검색 결과는 더 이상 사용할 수 없습니다. 다시 검색하세요.", "hist_import_failed": "가져오기에 실패했습니다.", - "imported_history_readonly": "가져온 전력 이력에서 감지되었습니다. 프로그램 매칭에 반영되지만 통계에는 포함되지 않으며, 자르거나 분할할 수 없습니다. 라벨을 지정해 프로그램 이름을 정하세요.", - "odometer_intro": "이 누적 값은 줄어들지 않습니다. 사이클 기록을 삭제해도 값이 바뀌지 않으며, 저장되는 기록 한도를 넘어서도 계속 집계되므로 서비스 알림이 정확하게 유지됩니다.", - "odometer_correct_hint": "WashData가 실제로 일어나지 않은 작동을 기록했거나, WashData를 설치하기 전에 기기가 이미 사이클을 실행한 적이 있다면 이 값을 설정하세요.", + "imported_history_readonly": "가져온 전력 이력에서 감지되었습니다. 매칭에 도움이 되지만 통계에는 포함되지 않으며 자르거나 분할할 수 없습니다. 라벨을 지정해 프로그램 이름을 정하세요.", + "odometer_intro": "이 누적 값은 늘어나기만 합니다. 사이클을 삭제해도 줄지 않으므로 서비스 알림이 정확하게 유지됩니다.", + "odometer_correct_hint": "WashData가 실제로 없었던 작동을 집계했거나, WashData 설치 전에 기기가 사이클을 실행한 적이 있다면 이 값을 설정하세요.", "no_reminders_set": "설정된 서비스 알림이 아직 없습니다.", "toast_program_armed": "다음 사이클용 프로그램이 설정됨: {program}", "toast_range_required": "유효한 시작/끝 범위를 설정하세요.", - "no_profiles_option": "(프로필 없음, 하나 추가하세요)" + "no_profiles_option": "(프로필 없음, 하나 추가하세요)", + "compare_cycles_title": "사이클 {count}개 비교", + "edit_builtin_phase": "기본 제공 단계입니다. 저장하면 사용자 지정 사본이 만들어집니다. 사본을 삭제하면 원본이 복원됩니다.", + "fetch_error": "데이터를 불러오지 못했습니다.", + "connection_lost": "연결이 끊어졌습니다. {time} 기준 데이터를 표시합니다.", + "pg_notify_partial": "마커는 시작, 거의 완료, 완료, 마일스톤, 방해 금지 시간 알림만 표시합니다. 실시간 업데이트, 꺼내기 알림, 초과 경고는 시뮬레이션되지 않습니다.", + "connect_to_confirm": "확인하거나 평가하려면 설정(톱니바퀴)에서 연결하세요.", + "coverage_gap_cluster": "약 {min}분 길이의 최근 라벨 없는 사이클 {count}개가 아직 만들지 않은 하나의 프로그램으로 보입니다.", + "pg_label_yes": "사이클 종료 시 \"{name}\"(으)로 라벨이 지정됩니다", + "pg_label_no": "검토를 요청합니다: {reason}", + "live_match_debug_tip": "신뢰도: 현재 전력 곡선이 최상위 후보 프로필과 얼마나 일치하는지 나타냅니다(0-100%). 모호함: 상위 두 후보의 점수 차이가 5% 이내입니다. 사이클이 끝날 때까지 라벨이 확정되지 않습니다.", + "automation_alias_started": "{device}: 사이클 시작됨", + "automation_alias_finished": "{device}: 사이클 완료됨", + "automation_desc_started": "{device} 사이클이 시작되면 실행됩니다. 원하는 동작(알림, 조명, ...)을 추가하세요.", + "automation_desc_finished": "{device} 사이클이 완료되면 실행됩니다. 원하는 동작(알림, 조명, ...)을 추가하세요.", + "feedback_detected": "WashData 감지 결과: {name}", + "import_undo_hint": "마지막 가져오기({when}) 이전의 복원 지점이 보관되어 있습니다: 프로필 {profiles}개, 사이클 {cycles}개.", + "replace_undo_hint": "먼저 복원 지점이 저장되므로 내보내기/가져오기에서 이 가져오기를 실행 취소할 수 있습니다.", + "pg_in_sample_note": "이 사이클들은 매칭 대상 프로필을 만드는 데 사용되었으므로 결과가 낙관적입니다. 새 사이클에서는 올바른 매칭이 약간 적을 것으로 예상하세요.", + "hist_tail_trimmed": "실제 통합과 마찬가지로 사이클 종료 후 대기 시간 {min}분을 잘라냈습니다", + "hist_truncated": "측정값 {n}개 한도에서 중지되어 나머지 기록은 검사하지 않았습니다", + "hist_warn_kw": "가장 높은 측정값이 {w}에 불과합니다. 이 센서는 킬로와트 단위로 보고하는 것 같습니다. WashData는 와트 단위가 필요하므로 사이클을 감지할 수 없습니다.", + "pg_compare_profile_tip": "그래프 위에 겹쳐 표시할 프로필만 선택합니다. 시뮬레이션은 실제 통합과 마찬가지로 항상 사이클을 자체적으로 매칭합니다.", + "pg_outcome_stale": "이 시뮬레이션을 실행한 후 설정이 변경되었습니다. 아직 이전 결과가 표시되고 있습니다.", + "pg_sweep_guarded": "더 많은 사이클을 일찍 종료하거나, 더 많이 분할하거나, 현재 설정이 감지하는 사이클을 놓치므로 권장되지 않습니다.", + "pg_sweep_keep_current": "현재 값({value}, {metric})을 유지하세요. 최소 한 사이클 이상 더 나은 값이 없었습니다.", + "pg_task_busy": "이 기기에서 이미 이력 테스트 또는 최적화가 실행 중입니다. 기다리거나 취소하세요.", + "hist_warn_naive_time": "타임스탬프 {n}개에 시간대가 없어 UTC로 읽었습니다. 파일이 현지 시간 기준이라면 사이클이 UTC 오프셋만큼 어긋나고, 서머타임 전환 전후의 측정값이 누락될 수 있습니다.", + "hist_warn_naive_time_other": "타임스탬프 {n}개에 시간대가 없어 UTC로 읽었습니다. 파일이 현지 시간 기준이라면 사이클이 UTC 오프셋만큼 어긋나고, 서머타임 전환 전후의 측정값이 누락될 수 있습니다.", + "pg_sweep_n_cycles": "사이클 {count}개 기준", + "pg_sweep_n_cycles_other": "사이클 {count}개 기준", + "cycle_context_loading": "레코더 이력 불러오는 중…", + "cycle_context_note": "회색: 이 사이클 전후의 전력 센서 레코더 이력입니다. 사이클에 포함되지 않으며 소요 시간, 에너지, 매칭에 영향을 주지 않습니다.", + "cycle_context_none": "레코더에 이 사이클 전후의 측정값이 없습니다(Home Assistant는 기본적으로 10일간의 이력을 보관합니다).", + "cycle_context_tip": "표시 전용입니다. 이 사이클 전후의 전력 센서 레코더 이력을 아직 남아 있으면 보여 줍니다. 실패한 시작 시도를 이후 사이클에 포함하려면 곡선 프리롤(설정 > 감지 > 사이클 시작)을 사용하세요. 저장되는 소요 시간이 바뀝니다.", + "invalid_threshold_pair": "중지 임계값({stop} W)은 시작 임계값({start} W)보다 낮아야 합니다. 아무것도 저장되지 않았습니다. 두 값을 함께 변경하세요." }, "pg_desc": { "completion_min_seconds": "실제 사이클로 인정되는 최단 실행", - "end_repeat_count": "종료 전 연속되는 낮은 측정값 횟수", "interrupted_min_seconds": "짧은 사이클은 중단으로 표시", "min_off_gap": "두 사이클을 구분하는 데 필요한 간격", "off_delay": "사이클 종료 전 저전력 지속 초", @@ -1115,37 +1064,36 @@ "start_threshold_w": "시작으로 간주할 최소 와트", "stop_threshold_w": "이 값 미만이면 기기가 꺼진 것으로 간주", "anti_wrinkle_enabled": "메인 단계 이후의 회전 펄스를 새 사이클로 인식하지 않고 흡수", - "anti_wrinkle_exit_power": "구김 방지가 활성 상태를 유지하려면 펄스 사이 전력이 이 값 아래로 떨어져야 함", + "anti_wrinkle_exit_power": "회전 펄스 사이의 정지 수준. 중지 임계값보다 높을 때만 적용", "anti_wrinkle_idle_timeout": "구김 방지가 끝나기 전 두 회전 펄스 사이에 허용되는 정지 시간", "anti_wrinkle_max_duration": "이보다 긴 펄스는 구김 방지를 끝내고 새 사이클을 시작", "anti_wrinkle_max_power": "이보다 높은 펄스는 구김 방지를 끝내고 새 사이클을 시작", - "dtw_bandwidth": "Stage 3: Sakoe-Chiba 워프 대역폭 (0 = DTW 비활성; 기본값 0.2 = 사이클 길이의 20%)", - "corr_weight": "Stage 2: 곡선 형태(상관관계)와 전력 수준(MAE) 사이의 균형; 기본값 0.45", - "duration_weight": "Stage 4: 실행 시간 일치가 최종 점수에 미치는 영향(기본값 0.22)", - "energy_weight": "Stage 4: 에너지 일치가 최종 점수에 미치는 영향(기본값 0.22)", - "profile_match_min_duration_ratio": "Stage 1: 프로파일 지속 시간의 분수로서 최소 사이클 길이; 기본값 0.1 (10%)", - "profile_match_max_duration_ratio": "Stage 1: 프로파일 지속 시간의 분수로서 최대 사이클 길이; 기본값 1.8 (180%)", - "keep_min_score": "Stage 2: 경쟁에 남기 위한 최소 점수; 기본값 0.1은 약한 후보도 허용하며, 이후 단계에서 최선을 선택", - "dtw_blend": "Stage 3: 0 = 핵심 점수만, 1 = DTW 점수만, 0.5 = 동등 혼합(기본값)", - "dtw_ensemble_w": "Stage 3 (앙상블 모드): 스케일 L1 대 도함수 DTW 가중치; 기본값 0.7은 수준 인식 방식을 선호", - "dtw_ddtw_scale": "Stage 3: DDTW 반포화 거리; 작을수록 형상에 민감(기본값 30)", - "dtw_refine_top_n": "Stage 3: DTW가 재점수화하는 후보 수; 올바른 프로파일이 4-5위면 7-9로 높임(기본값 5)", - "duration_scale": "Stage 4: 지속 시간 일치 점수가 반으로 줄어드는 로그 비율; 작을수록 엄격한 패널티(기본값 0.175)", - "energy_scale": "Stage 4: 에너지 일치 점수가 반으로 줄어드는 로그 비율; 작을수록 엄격한 패널티(기본값 0.25)", + "profile_match_min_duration_ratio": "프로필 소요 시간 대비 최소 사이클 길이 비율; 기본값 0.1 (10%)", + "profile_match_max_duration_ratio": "프로필 소요 시간 대비 최대 사이클 길이 비율; 기본값 1.8 (180%)", "dishwasher_end_spike_quiet_release": "식기세척기: 예상 소요 시간 이후 사이클 종료 배수 대기가 해제되기 전 조용히 유지되는 초", - "smart_termination_duration_ratio": "일치된 프로그램의 예상 소요 시간 중 스마트 종료가 조기에 끝낼 수 있으려면 주기가 도달해야 하는 비율; 부하나 온도에 따라 달라지는 기기에서는 낮추십시오", - "anti_crease_finalize_ratio": "구김 방지 텀블 꼬리를 확정하기 전에 사이클이 도달해야 하는, 일치된 프로그램 예상 소요 시간의 비율. 텀블 꼬리가 긴 건조기에서는 낮추고 세탁기는 기본값으로 두십시오", + "smart_termination_duration_ratio": "스마트 종료가 사이클을 조기에 끝낼 수 있기 전에 도달해야 하는 프로그램 평균 소요 시간의 비율; 운전 시간이 들쭉날쭉하면 낮추세요", + "anti_crease_finalize_ratio": "구김 방지 꼬리로 사이클을 끝낼 수 있기 전에 도달해야 하는 프로그램 평균 소요 시간의 비율; 건조기에서는 낮추고 세탁기는 기본값으로 두세요", "curve_preroll_seconds": "중단된 시작 시도의 측정값을 사이클 곡선 앞부분에 포함하는 초 수. 0 = 끄기" }, "phase_desc": { "anti_crease": "메인 사이클 후 드럼이 간헐적으로 회전하여 의류 구김을 방지합니다.", "anti_wrinkle": "메인 사이클 후 드럼이 간헐적으로 회전하여 의류 구김을 방지합니다.", + "baking": "히터가 고온으로 작동해 빵 껍질과 속을 만듭니다.", + "cooking": "히터와 팬이 작동하는 주 조리 단계.", "cool_down": "사이클 종료 근처에서 열 없이 회전합니다.", + "cool_down_af": "가열 후 팬만으로 식히는 단계.", "drain_and_switch": "세탁 모드에서 건조 모드로 전환되는 기간입니다.", "dry": "히터 및/또는 여열을 이용한 건조 단계입니다.", "drying": "주요 가열 텀블링 기간.", "heat_up": "완전 건조가 시작되기 전 초기 히터 예열.", + "keep_warm": "낮은 열로 음식을 따뜻하게 유지하는 단계.", + "keep_warm_bm": "굽기 후 낮은 열로 빵을 따뜻하게 유지하는 단계.", + "kneading": "모터로 반죽을 섞고 치대는 단계. 전력 소모가 큽니다.", + "pause_af": "흔들거나 확인하기 위한 짧은 일시 정지.", + "pre_heat": "본 조리 전에 내부를 처음 가열하는 단계.", "pre_rinse": "세제 세척 전 초기 스프레이다운.", + "proving": "낮은 열에서 효모가 발효되고 반죽이 부푸는 단계.", + "resting": "반죽 단계 사이에 글루텐을 쉬게 하는 짧은 저전력 휴지.", "pre_wash": "본 세탁 전 초기 담금 또는 전처리.", "rinse": "깨끗한 물로 헹구는 단계. 이 단계는 여러 번 반복될 수 있습니다.", "sanitize": "살균 프로그램을 위한 고온 세척 단계.", @@ -1158,6 +1106,7 @@ "wash_dw": "주요세제를 가열하여 세탁합니다." }, "placeholder": { + "custom_task": "예: 도어 씰 청소", "add": "추가…", "group_name": "예:면 2:47", "json_buttons": "[{\"action\":\"ID\",\"title\":\"Label\"}]", @@ -1184,7 +1133,7 @@ }, "section": { "anti_wrinkle": { - "intro": "구김 방지 모드는 메인 단계 이후 낮은 전력 텀블 펄스를 감지하고 완료된 사이클에 포함합니다.", + "intro": "메인 단계 이후 건조기의 저전력 텀블 펄스를 완료된 사이클에 포함합니다.", "label": "구김 방지" }, "basic": { @@ -1192,7 +1141,7 @@ "label": "기본" }, "delay": { - "intro": "지연 시작 감지는 기기가 켜져 있지만 사이클이 아직 시작되지 않은 상태를 식별합니다.", + "intro": "전원이 켜진 채 지연 시작을 기다리는 기기를 감지합니다.", "label": "지연 시작" }, "detection": { @@ -1216,153 +1165,145 @@ "label": "타이밍과 감시" }, "triggers": { - "intro": "선택적 외부 신호: 종료 트리거, 문 센서, 일시 정지 스위치, 꺼내기 알림.", + "intro": "선택적 추가 기능: 종료 트리거, 문 센서, 일시 정지 스위치, 꺼내기 알림.", "label": "트리거와 문" }, - "phase_eta": { - "label": "남은 시간", - "intro": "사이클 길이가 온도나 탈수에 따라 달라지는 기기를 위한, 단계를 고려한 남은 시간." - }, "dishwasher": { "label": "식기세척기", - "intro": "식기세척기의 사이클 종료 처리입니다. 식기세척기는 일반적으로 짧은 최종 배수 전에 전력을 거의 소비하지 않는 긴 건조 단계로 끝납니다." + "intro": "식기세척기의 종료 처리입니다. 식기세척기는 짧은 최종 배수 전에 조용히 오래 건조하는 경우가 많습니다." } }, "setting": { "anti_wrinkle_enabled": { - "doc": "건조기가 주요 가열 단계 후에 방출하는 짧은 저전력 텀블 펄스를 인식하고 이를 새 주기로 읽는 대신 완료된 주기에 연결해 두십시오.", + "doc": "주 가열 단계 이후 건조기의 짧은 저전력 텀블 펄스를 새 사이클로 읽지 않고 완료된 사이클에 포함합니다.", "label": "구김 방지 감지 활성화" }, "anti_wrinkle_exit_power": { - "doc": "주름 방지 모드가 활성 상태를 유지하려면 펄스 사이에 전력이 이 값 아래로 낮아져야 합니다.", + "doc": "구김 방지 모드에서는 이 값보다 낮은 전력을 회전 펄스 사이의 정지로 보며, 최대 펄스 간격보다 오래 정지하면 모드가 끝납니다. 중지 임계값이 더 높으면 그 값이 쓰이므로, 이 설정은 중지 임계값보다 높게 설정할 때만 효과가 있습니다.", "label": "종료 전력 임계값" }, "anti_wrinkle_idle_timeout": { "label": "최대 펄스 간격", - "doc": "구김 방지 모드가 종료되기 전에 기기가 두 회전 펄스 사이에서 정지 상태로 있을 수 있는 시간입니다. 건조기가 펄스 사이에 남기는 가장 긴 간격보다 높게 설정하십시오. 그렇지 않으면 이후의 모든 펄스가 잘못된 시작으로 인식됩니다." + "doc": "구김 방지 모드가 끝나기 전에 건조기가 텀블 펄스 사이에 조용히 있을 수 있는 시간입니다. 건조기의 가장 긴 간격보다 길게 설정하세요. 그렇지 않으면 이후 펄스가 잘못된 시작으로 인식됩니다." }, "anti_wrinkle_max_duration": { - "doc": "이보다 긴 펄스는 주름 방지 텀블이 아닌 실제 주기로 간주됩니다.", + "doc": "이보다 긴 펄스는 구김 방지 텀블이 아닌 실제 사이클로 간주됩니다.", "label": "최대 지속 시간" }, "anti_wrinkle_max_power": { - "doc": "이 전력 이상의 펄스는 주름 방지 텀블이 아닌 실제 새로운 주기로 간주됩니다. 회전 펄스 전력 바로 위에 설정하십시오.", + "doc": "이 전력을 넘는 펄스는 텀블이 아닌 실제 새 사이클로 간주됩니다. 텀블 펄스 전력보다 약간 높게 설정하세요.", "label": "구김 방지 최대 전력" }, "auto_label_confidence": { - "doc": "사이클 종료 시 일치 점수가 이 이상인 경우 확인 메시지 없이 프로그램에 자동으로 레이블이 지정됩니다. 자동 라벨링 전에 더 높은 확실성을 요구하려면 이 값을 높이세요. 더 많이 자동화하려면 낮추세요. 아래의 학습 자신감과 함께 작동합니다.", + "doc": "사이클 종료 시 매칭 점수가 이 값 이상이면 묻지 않고 프로그램 라벨을 지정합니다. 더 확실하게 하려면 높이고, 더 많은 사이클에 자동으로 라벨을 지정하려면 낮추세요. 학습 신뢰도보다 높게 유지하세요.", "label": "자동 라벨 신뢰도" }, "auto_maintenance": { - "doc": "야간 관리 실행: 프로필 봉투 재구축, 주기 상태 재계산, 디버그 추적 정리 및 최신 주기 유지.", + "doc": "야간 정리: 이제 특정 프로그램과 명확히 일치하는 오래된 사이클에 라벨을 지정하고, 프로그램 곡선을 다시 만들고, 이상 표시와 사이클 건강을 새로 고치며, \"디버그 추적 저장\"이 꺼져 있으면 디버그 추적을 제거합니다. 사이클과 전력 기록은 항상 보존됩니다.", "label": "자동 유지 관리 (야간 정리)" }, "completion_min_seconds": { - "doc": "이보다 짧은 주기는 고스트 주기(테스트 실행, 양말 추가를 위해 문 열기)로 폐기됩니다.", + "doc": "이보다 짧은 사이클(테스트 실행, 양말을 넣으려고 문을 연 경우 등)은 중단됨으로 저장되며 완료 알림을 보내지 않습니다.", "label": "사이클 최소 시간" }, "delay_confirm_seconds": { - "doc": "어플라이언스가 실행되지 않고 시작 대기 중인 것으로 처리되기 전에 전원은 오랫동안 대기 대역에 유지되어야 합니다.", + "doc": "기기가 시작 대기 중으로 간주되려면 전력이 이 시간 동안 대기 전력 범위에 머물러야 합니다.", "label": "확인 초" }, "delay_start_detect_enabled": { - "doc": "어플라이언스의 전원이 켜져 있고 대기 중(지연된 시작/대기)이지만 주기가 시작되지 않은 경우를 감지하므로 대기 그리기가 실행 중인 주기로 착각되지 않습니다.", + "doc": "전원이 켜진 채 지연 시작을 기다리는 기기를 감지하여 대기 전력을 실행 중인 사이클로 오인하지 않게 합니다.", "label": "지연 시작 감지 활성화" }, "delay_timeout_hours": { - "doc": "이 많은 시간이 지나면 지연된 시작 모드에서 대기하는 것을 중지하고 유휴 상태로 돌아가므로 전원이 켜져 있지만 시작되지 않은 기계는 영원히 기다리지 않습니다.", + "doc": "이 시간이 지나면 지연 시작 대기를 멈추고 유휴 상태로 돌아갑니다.", "label": "타임아웃 시간" }, "device_type": { - "doc": "가전제품 등급. 해당 기기 유형에 맞게 조정된 합리적인 감지 기본값(임계값, 오프 지연, 종료 처리)을 설정합니다. 장치가 원래 잘못된 유형으로 설정된 경우에만 변경하십시오.", + "doc": "이 종류의 기기에 맞는 감지 기본값(임계값, 오프 지연, 종료 처리)을 설정합니다. 기기를 잘못된 유형으로 설정한 경우에만 변경하세요.", "label": "기기 유형" }, "door_sensor_entity": { - "doc": "옵션 도어 바이너리 센서. 사이클 후 기기가 열리거나 내려지는 시기를 감지하는 데 사용됩니다.", + "doc": "선택적 문 센서입니다. 사이클 후 기기를 열어 내용물을 꺼낸 시점을 알려 줍니다.", "label": "문 센서 엔티티" }, "duration_tolerance": { - "doc": "남은 추정 시간에 대한 허용 오차(학습 피드백, 일치하지 않음). 실제 기간이 추정치의 +/-X% 내에 있으면 좋은 일치로 간주됩니다.", + "doc": "학습 피드백 전용: 예상 소요 시간의 +/-X% 이내인 사이클은 좋은 추정으로 간주됩니다. 매칭에는 영향을 주지 않습니다.", "label": "추정 허용 오차" }, "enable_ml_models": { - "doc": "사이클이 실행되는 동안 모델이 실시간 결과를 개선하도록 하십시오. 즉, 보다 안정적인 남은 시간 및 에너지/비용 추정, 종료 감지 시 조기 중지 방지 가드(완료를 지연시킬 수만 있고 일찍 끝나지 않으며 제한됨)입니다. 사용 가능한 경우 미세 조정된 모델을 사용하고, 그렇지 않은 경우에는 내장된 모델을 사용합니다. 꺼짐 = 고전적인 전력 기반 논리만(여전히 안정적임)", + "doc": "이 기기에서 학습한 모델로 실행 중인 사이클의 에너지와 비용을 예측하고, 잘 아는 프로그램이 비정상적으로 일찍 멈추면 오프 지연을 끝까지 기다리지 않고 종료합니다. 꺼짐: 전력 기반 논리만 사용합니다.", "label": "사이클 중 스마트 모델 적용" }, "end_energy_threshold": { - "doc": "꺼짐 지연 카운트다운 동안 축적된 에너지(와트 x 시간)가 이 임계값과 비교됩니다. 초과하면 카운트다운이 재설정됩니다. 주름 방지 텀블러와 식기세척기 건조 테일을 짧게 자르는 대신 사이클에 부착해 두세요. 냉각 중에 사이클이 너무 일찍 끝나면 이 값을 올립니다. 감지가 느린 경우에는 낮추십시오.", + "doc": "오프 지연 동안 허용되는 에너지입니다. 이를 넘으면 사이클이 계속되므로 텀블과 건조 마무리 구간이 사이클에 포함됩니다. 마무리 구간 중에 사이클이 끝나면 낮추고, 완료된 사이클이 계속 대기하면 높이세요.", "label": "종료 에너지" }, - "end_repeat_count": { - "doc": "사이클이 끝나기 전에 필요한 연속 정지 임계값 미만 판독 횟수입니다. 대부분의 플러그에는 1이 적합합니다. 스마트 플러그가 주기 중간에 가끔 잘못된 제로 샘플을 보고하고 주기가 조기에 종료되는 경우 2~3으로 올리세요.", - "label": "종료 반복 횟수" - }, "energy_price_entity": { - "doc": "kWh당 현재 전기 가격을 제공하는 센서(예: 동적 요금제). 반드시 가격이어야 하며, 플러그 자체의 전력이나 에너지 엔티티를 지정해서는 안 됩니다. 아래의 고정 가격보다 우선 적용되므로, 여기에 kWh 적산 계량기를 지정하면 모든 사이클이 요금 단가가 아닌 계량기 값으로 계산됩니다. 시간 가중 비용을 켜면 각 사이클이 실행된 매 순간에 적용되던 가격으로 계산되고, 끄면 완료 시점에 적용되던 가격으로 고정됩니다.", + "doc": "kWh당 현재 가격을 제공하는 센서(예: 동적 요금제). 아래의 고정 가격보다 우선하므로 플러그의 전력 또는 에너지 센서가 아닌 가격이어야 합니다.", "label": "에너지 가격 엔티티" }, "energy_price_static": { - "doc": "위에 실시간 가격 엔터티가 설정되지 않은 경우 비용 수치에 사용되는 kWh당 고정 가격입니다.", + "doc": "가격 엔티티가 설정되지 않았을 때 사용하는 kWh당 고정 가격입니다.", "label": "고정 에너지 가격 (kWh당)" }, "energy_sensor": { - "doc": "선택적 누적 에너지 카운터(total_increasing kWh/Wh, 예: 플러그 자체의 누적 계량기)입니다. 설정하면 각 사이클의 표시 에너지가 이 카운터의 시작부터 종료까지의 차이(delta)에서 계산되므로, 보고 주기가 느린 전력 센서를 적분할 때 발생하는 과소 집계를 방지합니다. 판독값이 없거나 단위를 알 수 없거나 차이가 양수가 아닌 경우 적분된 값으로 대체됩니다. 비워 두면 전력 센서를 계속 적분합니다.", + "doc": "선택적 kWh 또는 Wh 카운터(total_increasing, 예: 플러그 자체 계량기)입니다. 사이클 에너지를 여기서 읽으므로 느린 전력 센서로 계산하는 것보다 정확합니다. 판독값을 쓸 수 없으면 계산된 값이 사용됩니다.", "label": "에너지 계량기 엔티티" }, "expose_debug_entities": { - "doc": "추가 진단 HA 엔터티를 게시합니다(신뢰도, 모호성, 상태 내부 일치). 끄면 일반적인 사용을 위해 엔터티 목록을 깨끗하게 유지합니다.", + "doc": "진단 엔티티(매칭 신뢰도, 모호성, 내부 상태)를 추가합니다. 끄면 엔티티 목록이 깔끔하게 유지됩니다.", "label": "디버그 엔티티 노출" }, "external_end_trigger": { - "doc": "상태 변경이 주기 종료를 표시하는 바이너리 센서(예: 기기 \"완료\" 접점 또는 동반 통합)", + "doc": "상태가 바뀌면 사이클 종료를 나타내는 바이너리 센서입니다(예: \"완료\" 접점 또는 다른 통합).", "label": "외부 트리거 엔티티" }, "external_end_trigger_enabled": { - "doc": "내장된 전력 기반 감지 기능 외에도 외부 바이너리 센서가 사이클 종료 신호를 보내도록 하세요.", + "doc": "전력 기반 감지 외에 바이너리 센서가 사이클 종료를 알릴 수 있게 합니다.", "label": "외부 종료 트리거 활성화" }, "external_end_trigger_inverted": { - "doc": "트리거 센서가 꺼진 상태(켜짐이 아닌)를 주기 종료 신호로 처리합니다.", + "doc": "센서가 켜질 때가 아니라 꺼질 때를 종료 신호로 처리합니다.", "label": "외부 트리거 반전 (OFF에서 트리거)" }, "learning_confidence": { - "doc": "일치 점수가 이 값과 자동 라벨 신뢰도 사이에 있으면 식별된 프로그램을 확인하라는 피드백 알림이 표시됩니다. 이 점수 미만에서는 일치가 너무 불확실하여 표시되지 않습니다. 자동 라벨 신뢰도보다 낮게 유지해야 합니다.", + "doc": "완료된 사이클에 라벨을 붙이는 최소 매칭 점수. 확실한 1위는 라벨이 붙고, 근소한 차이는 사이클 탭의 검토 대기열로 갑니다. 이보다 낮으면 라벨을 붙이지도, 묻지도 않습니다. 자동 라벨 신뢰도보다 낮게 유지하세요.", "label": "학습 신뢰도" }, "linked_device": { - "doc": "선택적으로 \"Connected via ...\"로 표시된 HA 장치 레지스트리의 다른 장치(예: 스마트 플러그) 아래에 이 WashData 장치를 중첩합니다.", + "doc": "선택 사항. Home Assistant에서 이 WashData 장치를 다른 장치(예: 스마트 플러그) 아래에 \"Connected via ...\"로 표시합니다.", "label": "기기 아래 그룹화" }, "min_off_gap": { - "doc": "이 시간보다 짧은 시간 동안 기기의 전원이 꺼지면 켜기/끄기/켜기 순서가 하나의 연속 주기로 처리됩니다. 담금 프로그램(세탁 도중 몇 분 동안 기계 전원이 꺼짐)이 두 개의 별도 주기로 분할되는 것을 방지합니다. 별도의 주기로 계산하려면 연속 하중 사이의 간격보다 짧게 설정하십시오. 장치 유형 기본값은 각 어플라이언스에 대한 일반적인 주기 내 일시 중지를 보호합니다.", + "doc": "이보다 짧은 꺼짐 구간은 사이클을 나누지 않으므로 불림 중 멈춤이 하나의 세탁으로 유지됩니다. 연달아 돌리는 세탁 사이의 간격보다 짧게 설정하세요. 그렇지 않으면 하나의 사이클로 합쳐집니다.", "label": "최소 오프 간격" }, "min_power": { - "doc": "절대 최소 전력은 활성으로 간주됩니다. 이 아래의 판독값은 0W(대기)로 처리되어 스마트 플러그 및 대기 LED의 팬텀 부하를 필터링합니다.", + "doc": "이보다 낮은 측정값은 0 W(대기)로 간주됩니다. 스마트 플러그와 대기 LED의 대기 전력을 걸러냅니다.", "label": "최소 전력" }, "ml_training_enabled": { - "doc": "밤새 검토한 사이클을 주기적으로 연구하고 모델을 이 특정 기계에 맞게 미세 조정하세요. 변경 사항은 보류 주기에서 실제로 더 나은 점수를 얻은 경우에만 유지되므로 이는 도움이 되거나 동일하게 유지될 수 있으며 결코 퇴보하지 않습니다.", + "doc": "이 기기의 완료된 사이클로 에너지 모델을 주기적으로 다시 학습합니다. 새 모델은 테스트 사이클에서 단순 추정보다 나을 때만 유지됩니다.", "label": "이 기기에서 학습" }, "ml_training_hour": { - "doc": "밤새 미세 조정을 수행할 현지 시간(0-23)입니다. 2시(02:00)와 같이 조용한 시간을 선택하세요.", + "doc": "야간 학습을 할 시각(0-23)입니다. 2시처럼 조용한 시간을 선택하세요.", "label": "학습 시간" }, "ml_training_interval_days": { - "doc": "며칠에 한 번만 개선 사항을 다시 확인하세요.", + "doc": "최대 이 일수마다 한 번 다시 학습합니다.", "label": "최대 확인 간격" }, "ml_training_min_cycles": { - "doc": "미세 조정하기 전에 최소한 이만큼의 주기가 기록될 때까지 기다리십시오. 그러면 학습할 내용이 충분합니다.", + "doc": "학습 전에 최소 이만큼의 사이클이 기록될 때까지 기다립니다.", "label": "필요한 사이클 수" }, "name": { - "doc": "HA 통합 목록 및 장치 레지스트리에 표시되는 표시 이름입니다.", + "doc": "Home Assistant에 표시되는 이 장치의 이름입니다.", "label": "기기 이름" }, "no_update_active_timeout": { - "doc": "실행 중에 오랫동안 전원 업데이트가 도착하지 않으면 플러그가 오프라인으로 떨어졌다고 가정하고 좀비 사이클을 피하기 위해 강제 중지하십시오. 기본 600초는 클라우드 또는 메시 지연을 허용합니다.", + "doc": "사이클 중 이 시간 동안 전력 업데이트가 없으면 플러그가 오프라인이 된 것으로 보고 사이클을 강제 중지합니다. 기본값 600초는 클라우드나 메시 지연을 고려한 값입니다.", "label": "업데이트 없음 타임아웃" }, "notify_before_end_minutes": { @@ -1370,35 +1311,35 @@ "label": "종료 전 알림" }, "notify_channel": { - "doc": "시작/실시간 메시지에 대한 Android 알림 채널 이름(모바일 앱의 채널별 사운드 및 우선순위 제어) Blank는 동반 앱 기본값을 사용합니다.", + "doc": "시작 및 실시간 메시지용 Android 채널입니다. 소리와 우선순위는 채널이 정합니다. 비워 두면 앱 기본값을 사용합니다.", "label": "Android 채널 (시작/라이브)" }, "notify_cycle_timers": { - "doc": "주기의 특정 분에 알림(예: 연화제 추가) 메시지는 {device}, {program}, {minutes}를 지원합니다. 해당 지점에서 일시 중지하고 재개 버튼을 통해 대화형 알림을 받으려면 자동 일시 중지를 활성화하세요. 패널, 일시 중지/재개 서비스 또는 알림 작업을 통해 재개합니다.", + "doc": "사이클 시작 후 지정한 분에 보내는 알림입니다(예: 섬유유연제 투입). 메시지에서 {device}, {program}, {minutes}를 사용할 수 있습니다. 자동 일시중지를 켜면 그 시점에 사이클도 일시 정지되고 알림에 재개 버튼이 표시됩니다.", "label": "사이클 타이머" }, "notify_finish_channel": { - "doc": "완료 메시지에 대한 Android 알림 채널 이름입니다. 공백은 시작/라이브 채널을 재사용합니다.", + "doc": "완료 메시지용 Android 채널입니다. 비워 두면 시작/실시간 채널을 사용합니다.", "label": "Android 채널 (종료)" }, "notify_finish_message": { - "doc": "사이클이 끝날 때 전송되는 본문. 템플릿 변수: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}. 여기서는 {time_finished}와 {vs_typical}이 가장 유용합니다.", + "doc": "사이클이 끝날 때 보내는 본문. 변수: {device}, {program}, {duration}(분), {duration_hm}(1 h 05 min), {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}, {status}(completed 또는 force_stopped, 중단된 사이클은 아무것도 보내지 않음). 다른 변수를 쓰면 기본 텍스트가 사용됩니다.", "label": "종료 메시지" }, "notify_finish_services": { - "doc": "inform.* 주기가 완료되면 호출되는 서비스입니다. 대상당 하나씩 추가합니다. 완료 알림을 보내지 않으려면 비워 두세요.", + "doc": "사이클이 끝날 때 호출할 notify.* 서비스입니다. 필요 없으면 비워 두세요.", "label": "종료 서비스" }, "notify_fire_events": { - "doc": "또한 주기 시작/종료 시 ha_washdata_* 이벤트를 실행하여 자체 자동화를 구축할 수 있습니다.", + "doc": "직접 만든 자동화를 위해 사이클 시작과 종료 시 ha_washdata_* 이벤트도 발생시킵니다.", "label": "알림용 HA 이벤트 실행" }, "notify_icon": { - "doc": "알림에 표시되는 작은 아이콘(예: mdi:washing-machine). Android는 상태 표시줄에 표시하고, iOS는 앱 아이콘 대신 표시하며 컴패니언 앱 2026.8 이상이 필요합니다. 플랫폼 기본값을 사용하려면 비워 두세요.", + "doc": "알림의 작은 아이콘(예: mdi:washing-machine)입니다. mobile_app 대상에만 전송되며 notify 그룹에는 전송되지 않습니다. iOS에서는 앱 아이콘을 대체하며 컴패니언 앱 2026.8 이상이 필요합니다. 비워 두면 플랫폼 기본값을 사용합니다.", "label": "알림 아이콘" }, "notify_live_chronometer": { - "doc": "정적 추정 대신 알림(지원하는 플랫폼의 경우)에 실시간 업데이트 카운트다운 타이머를 표시합니다.", + "doc": "고정된 추정치 대신 알림에 실시간 카운트다운 타이머를 표시합니다(지원되는 경우).", "label": "라이브 크로노미터 사용" }, "notify_live_interval_seconds": { @@ -1406,143 +1347,135 @@ "label": "라이브 업데이트 간격" }, "notify_live_overrun_percent": { - "doc": "주기가 이 백분율보다 더 많이 예상을 초과하는 경우 오버런 경고를 보냅니다.", + "doc": "사이클이 예상 시간을 이 비율만큼 넘기면 라이브 업데이트가 멈춥니다(20 = 예상 시간의 1.2배까지).", "label": "라이브 초과 % 알림" }, "notify_live_services": { - "doc": "통지.* 주기가 실행되는 동안 실시간 진행 업데이트를 위해 호출되는 서비스입니다. 실시간 진행 상황 알림을 비활성화하려면 비워 두세요.", + "doc": "사이클 실행 중 실시간 진행 업데이트에 사용할 notify.* 서비스입니다. 끄려면 비워 두세요.", "label": "라이브 진행 서비스" }, "notify_only_when_home": { - "doc": "연결된 사람(위) 중 한 명 이상이 집에 있을 때만 알림을 보내세요.", + "doc": "위의 사람 중 한 명 이상이 집에 있을 때만 알립니다.", "label": "집에 있을 때만 알림" }, "notify_people": { - "doc": "person.* \"집에 있을 때만 알림\"이 집에 있는지 여부를 결정하는 데 사용되는 엔터티입니다.", + "doc": "\"집에 있을 때만 알림\"이 확인하는 person.* 엔티티입니다.", "label": "사람 (집에 있을 때)" }, "notify_pre_complete_message": { - "doc": "종료 전 / 거의 완료 알림의 본문. 템플릿 변수: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "각 라이브 진행 업데이트의 텍스트. 템플릿 변수: {device}, {program}, {minutes}(남은 분). 다른 변수를 쓰면 기본 텍스트가 사용됩니다.", "label": "완료 전 메시지" }, "notify_reminder_message": { - "doc": "아직 대기 중인 꺼내기 알림의 본문. 비워 두면 기본 제공 값이 사용됩니다. 템플릿 변수: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "한 번만 보내는 종료 전 알림의 텍스트. 템플릿 변수: {device}, {program}, {minutes}(종료 전 알림 설정값). 다른 변수를 쓰면 기본 텍스트가 사용됩니다.", "label": "알림 메시지" }, "notify_start_message": { - "doc": "사이클이 시작될 때 전송되는 본문. 템플릿 변수: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "사이클이 시작될 때 보내는 본문. 템플릿 변수: {device}, {program}. 다른 변수를 쓰면 기본 텍스트가 사용됩니다.", "label": "시작 메시지" }, "notify_start_services": { - "doc": "inform.* 서비스는 주기가 시작될 때 호출됩니다. 대상(전화, 대시보드 등)당 하나씩 추가합니다. 시작 알림을 보내지 않으려면 비워 두세요.", + "doc": "사이클이 시작될 때 호출할 notify.* 서비스입니다(휴대폰, 대시보드 등). 필요 없으면 비워 두세요.", "label": "시작 서비스" }, "notify_timeout_seconds": { - "doc": "이 시간(초) 후에 알림을 자동으로 해제합니다(지원하는 플랫폼의 경우). 0은 수동으로 해제할 때까지 유지합니다.", + "doc": "이 시간(초)이 지나면 알림을 닫습니다(지원되는 경우). 0이면 직접 닫을 때까지 유지됩니다.", "label": "자동 닫기" }, "notify_title": { - "doc": "알림 제목. 템플릿 변수: {device}, {program}, {duration}, {energy}, {cost}, {date}, {time}, {minutes}.", + "doc": "알림 제목. {device}만 치환되며, 다른 변수를 쓰면 기본 제목이 사용됩니다.", "label": "알림 제목" }, "notify_unload_delay_minutes": { - "doc": "사이클이 종료된 후 아직 대기 중인 \"머신 언로드\" 알림을 전송하기 몇 분 전입니다. 알림을 비활성화하려면 0을 설정합니다.", + "doc": "사이클이 끝난 후 \"내용물 꺼내기\" 알림을 보내기까지의 시간(분)입니다. 0이면 비활성화됩니다.", "label": "꺼내기 알림 지연" }, "notify_unload_repeat": { - "doc": "꺼내기가 확인되거나 알림에서 \"알림 중지\"를 누를 때까지 \"꺼내기 알림 지연\" 분마다 꺼내기 알림을 계속 다시 보냅니다. 꺼내기를 확인할 방법이 필요합니다: 문 센서 엔티티, 꺼내기 확인 엔티티 또는 아래의 \"꺼내기 수동 확인\" 옵션. 해제 버튼은 Home Assistant 컴패니언 앱(모바일) 알림에서 작동합니다.", + "doc": "꺼내기가 확인되거나 \"알림 중지\"를 누를 때까지(모바일 앱) \"꺼내기 알림 지연\" 분마다 꺼내기 알림을 다시 보냅니다. 문 센서 엔티티, 꺼내기 확인 엔티티 또는 \"꺼내기 수동 확인\"이 필요합니다.", "label": "꺼낼 때까지 반복" }, "off_delay": { - "doc": "사이클 완료를 선언하기 전에 전원이 꺼진 후 기다리는 시간입니다. 이 창 내에서 전원이 재개되면 주기가 원활하게 계속됩니다. 이로 인해 세탁 단계 사이가 일시 중지됩니다. 식기세척기는 건조 단계가 길기 때문에(20~60분 동안 전원 꺼짐) 전체 세탁+건조를 하나의 주기로 유지하려면 꺼짐 지연이 이를 초과해야 합니다.", + "doc": "사이클이 완료되기 전에 전력이 낮게 유지되어야 하는 시간입니다. 이 시간 안에 전력이 돌아오면 사이클이 계속됩니다. 식기세척기는 건조 중 20-60분 동안 멈춰 있을 수 있으므로 그보다 길게 설정하세요.", "label": "오프 지연" }, "pause_cuts_power": { - "doc": "사이클이 일시 중지되면 아래의 스위치 엔터티도 끄십시오. 사용 중에 플러그를 안전하게 절단할 수 있는 기기에만 해당됩니다.", + "doc": "사이클이 일시 정지되면 아래 스위치 엔티티도 끕니다. 사이클 도중 전원이 끊겨도 안전한 기기에만 사용하세요.", "label": "일시 정지로 전력도 차단 (스위치 경유)" }, "power_off_delay": { - "doc": "사이클이 완료된 후 상태가 꺼짐으로 돌아가기 전까지 전력이 전원 꺼짐 임계값 미만으로 유지되어야 하는 시간입니다. 전원 꺼짐 임계값이 0보다 클 때만 사용됩니다. 백그라운드 주기로 확인되므로 실제 지연 시간은 다음 상태 만료 시점까지 올림 처리됩니다.", + "doc": "사이클 후 상태가 꺼짐으로 돌아가기 전에 전력이 전원 꺼짐 임계값 미만으로 유지되어야 하는 시간입니다. 그 임계값이 0보다 클 때만 사용됩니다. 주기적으로 확인하므로 실제 대기 시간은 조금 더 길 수 있습니다.", "label": "전원 꺼짐 지연" }, "power_off_threshold_w": { - "doc": "전력 기반 꺼짐 감지(선택 사항)입니다. 0보다 큰 값으로 설정하면 사이클이 완료된 후 전력이 전원 꺼짐 지연 시간 동안 이 값 미만으로 유지될 경우 기기가 꺼진 것으로 간주되어 상태가 꺼짐으로 돌아갑니다. 사용하지 않으려면 0으로 두세요(기본값). 실제로 전원을 끈 상태의 최저값보다 높고, 중지 임계값 및 기기가 완료된 후에도 켜져 있을 때의 대기 전력보다 낮게 설정하세요. 중지 임계값 미만이 아니면 무시됩니다. 활성화하면 꺼짐으로 돌아갈 때 진행 초기화 지연 대신 사용되므로, 완료된 기기는 실제로 전원이 꺼질 때까지 완료됨/깨끗한 상태로 유지됩니다.", + "doc": "사이클 후 전력이 전원 꺼짐 지연 동안 이 값보다 낮으면 진행 초기화 지연 대신 상태를 꺼짐으로 바꿉니다. 전원을 끈 상태의 전력보다 높고, 대기 전력과 중지 임계값보다 낮게 설정하세요. 0이면 비활성화됩니다(기본값).", "label": "전원 꺼짐 임계값" }, "power_sensor": { - "doc": "이 기기의 실제 전력을 와트 단위로 보고하는 센서 엔터티입니다(예: sensor.washer_power). 모든 사이클 감지는 이 신호를 기반으로 합니다.", + "doc": "이 기기의 실시간 전력을 와트 단위로 보고하는 센서입니다(예: sensor.washer_power). 모든 감지는 이 센서를 기반으로 합니다.", "label": "전력 센서" }, - "profile_duration_tolerance": { - "doc": "일치하는 동안 사용된 프로필 평균 지속 시간 주변의 +/- 밴드입니다. 0.25는 60분 프로필이 45~75분 주기와 일치함을 의미합니다.", - "label": "프로필 기간 허용 오차" - }, "profile_match_interval": { - "doc": "실행 주기 동안 프로필 일치를 시도하는 빈도입니다. 기본 300초(5분)는 감지 속도와 CPU의 균형을 유지합니다.", + "doc": "실행 중인 사이클을 프로필과 매칭하는 빈도입니다. 기본값 300초(5분).", "label": "매칭 간격" }, "profile_match_max_duration_ratio": { - "doc": "프로파일에 따른 최대 사이클 길이. 1.3은 주기가 일치하려면 프로필 기간의 130% 미만이어야 함을 의미합니다.", + "doc": "사이클이 프로그램의 일반적인 소요 시간의 이 배수보다 오래 진행되면 해당 프로그램은 제외됩니다. 1.8: 프로그램보다 80% 긴 사이클은 더 이상 매칭되지 않습니다.", "label": "최대 기간 비율" }, "profile_match_min_duration_ratio": { - "doc": "프로파일에 따른 최소 사이클 길이. 0.9는 주기가 일치하려면 프로필 기간의 90% 이상이어야 함을 의미합니다.", + "doc": "사이클이 프로그램의 일반적인 소요 시간의 이 비율만큼 진행된 후에만 해당 프로그램을 고려합니다. 0.10: 2시간짜리 프로그램은 12분 후에 후보가 됩니다. 사이클의 처음 15분 동안에는 적용되지 않습니다.", "label": "최소 기간 비율" }, "profile_match_threshold": { - "doc": "프로그램 식별을 수락하려면 주기 종료 시 최소 유사성 점수(0-1)가 필요합니다. 잘못된 식별을 줄이기 위해 높이십시오. 기계의 프로그램이 일치하지 않으면 값을 낮추십시오. 기본값 0.4는 보수적인 시작점입니다.", + "doc": "스마트 종료나 구김 방지 확정이 사이클을 조기에 끝내기 위해 필요한 실시간 매칭 점수입니다. 표시되는 프로그램은 바뀌지 않습니다. 약한 매칭으로 사이클이 일찍 끝나면 높이고, 명확한 매칭인데도 오프 지연을 기다리면 낮추세요.", "label": "매칭 임계값" }, "profile_unmatch_threshold": { - "doc": "사이클 진행 중의 실시간 매칭 점수가 이 값 아래로 떨어지면 임시 식별이 해제됩니다. 매칭 임계값보다 약간 낮게 유지하여 유사도가 잠시 떨어지더라도 표시가 매칭 안 됨 상태로 되돌아가지 않도록 하세요.", + "doc": "실시간 매칭 점수가 이 값 아래로 떨어지면 현재 프로그램 추정이 해제됩니다. 잠깐 떨어질 때 초기화되지 않도록 매칭 임계값보다 약간 낮게 유지하세요.", "label": "언매칭 임계값" }, "progress_reset_delay": { - "doc": "완료 후에는 진행 상태를 오랫동안 100%로 유지하여 유휴 상태로 재설정하기 전에 완료됨이 대시보드에 표시되도록 합니다.", + "doc": "사이클 후 유휴 상태로 초기화하기 전에 진행률을 100%로 유지하는 시간입니다.", "label": "진행 초기화 지연" }, "pump_stuck_duration": { - "doc": "펌프가 고착되었을 가능성이 있는 것으로 표시되기 전에 펌프가 계속 작동할 수 있는 시간(초)입니다(펌프 고착 이벤트 발생).", + "doc": "펌프 고착 이벤트가 발생하기 전에 펌프가 쉬지 않고 작동할 수 있는 시간입니다.", "label": "펌프 정지 지속 시간" }, "sampling_interval": { - "doc": "센서 판독 사이의 예상 시간 - 스무딩 창의 크기를 조정하고 디바운싱을 올바르게 시작하는 데 사용됩니다. 모든 센서 업데이트는 이 값에 관계없이 캡처됩니다. 다운스트림 계산만 보정합니다. 제안 엔진은 과거 주기에서 센서의 실제 케이던스를 측정하고 이를 자동으로 설정합니다.", + "doc": "처리되는 측정값 사이의 최소 시간입니다. 이보다 빨리 도착한 측정값은 건너뜁니다(사이클 중 낮은 측정값은 항상 유지). 플러그의 보고 간격 이하로 설정하세요. 값이 더 크면 측정값을 버릴 뿐입니다.", "label": "샘플링 간격" }, "save_debug_traces": { - "doc": "각 사이클에 대해 전체 전력 추적 및 일치하는 디버그 데이터를 저장합니다. 문제 해결에 유용하지만 저장소 크기가 늘어납니다.", + "doc": "문제 해결을 위해 각 사이클과 함께 매칭 디버그 데이터(모든 후보의 점수)도 저장합니다. 사이클당 약 20KB가 추가됩니다.", "label": "디버그 추적 저장" }, "smart_termination_duration_ratio": { - "doc": "전력이 떨어졌을 때 스마트 종료가 주기를 조기에 끝낼 수 있으려면, 일치된 프로그램의 예상 소요 시간 중 어느 정도까지 주기가 진행되어야 하는지를 지정합니다. 예상 소요 시간은 해당 프로그램의 평균값이므로, 실행 시간이 크게 달라지는 가전제품(겨울철 차가운 급수 대 여름철 따뜻한 급수의 세탁기, 센서 건조식 건조기, 부하에 따라 달라지는 프로그램)에서는 전체 실행의 약 절반이 그 평균보다 짧게 끝나 빠른 종료가 적용되지 않고, 폴백 시간 초과를 통해서만 몇 분 늦게 종료됩니다. 이러한 기기에서는 이 값을 낮춰(예: 0.85) 조기 종료가 계속 작동하도록 하고, 더 보수적으로 하려면 1.0에 가깝게 높이십시오. 기본값(0.98, 식기세척기는 0.99)을 사용하려면 비워 두십시오. 주기를 더 일찍 끝낼 수만 있고 더 늦출 수는 없으며, 모호하거나 신뢰도가 낮은 일치에서는 작동하지 않습니다.", + "doc": "스마트 종료가 사이클을 조기에 끝낼 수 있기 전에 사이클이 도달해야 하는 프로그램 평균 소요 시간의 비율입니다. 운전 시간이 들쭉날쭉하면(물 온도, 센서 건조, 세탁물 양) 낮추세요(예: 0.85). 비워 두면 0.98(식기세척기는 0.99)입니다.", "label": "스마트 종료 비율" }, - "smoothing_window": { - "doc": "원시 전력 신호가 얼마나 평탄화되는지입니다. 낮음(2)은 반응이 좋지만 소음이 있습니다. 높음(5)은 스파이크를 부드럽게 하지만 지연을 추가합니다.", - "label": "스무딩 윈도우" - }, "start_duration_threshold": { - "doc": "실제 시작을 확인하려면 전력이 이렇게 오랫동안 시작 임계값 이상으로 유지되어야 하며, 순간적인 켜기/끄기 전환으로 인해 사이클이 시작되는 것을 방지할 수 있습니다.", + "doc": "사이클이 시작되려면 전력이 이 시간 동안 시작 임계값 이상으로 유지되어야 하므로, 짧은 켜짐/꺼짐 변동은 무시됩니다.", "label": "시작 지속 시간" }, "start_energy_threshold": { - "doc": "기기가 작동하기 전에 소비해야 하는 에너지(전력 x 시간)입니다. 짧은 고전력 스파이크는 에너지가 매우 낮으므로 무시되어 잘못된 시작을 방지합니다.", + "doc": "사이클이 실행 중으로 간주되기 전에 기기가 사용해야 하는 에너지입니다. 짧은 전력 급등은 에너지가 적어 사이클을 시작하지 않습니다.", "label": "시작 에너지" }, "start_threshold_w": { - "doc": "사이클이 시작되었음을 확인하려면 전력이 이 수준 이상으로 높아져야 합니다. 너무 낮게 설정하면 대기 전원에서 잘못된 시작이 발생합니다. 너무 높거나 느리게 시작되는 프로그램(콜드 필)이 누락됩니다. 제안 엔진은 이를 기계에서 관찰된 최저 유효 전력 바로 위에 설정합니다.", + "doc": "사이클이 시작되려면 전력이 이 값을 넘어야 합니다. 너무 낮으면 대기 전력으로 잘못된 시작이 발생하고, 너무 높으면 천천히 시작하는 프로그램(냉수 급수)을 놓칩니다.", "label": "시작 임계값" }, "stop_threshold_w": { - "doc": "꺼짐 지연 카운트다운이 시작되기 전에 전력이 이 수준 아래로 떨어져야 합니다. 시작 임계값 아래로 설정하십시오. 둘 사이의 간격은 깜박임을 방지하는 히스테리시스 밴드입니다. 너무 높게 설정하면 저전력 단계(헹굼 유지, 주름 방지)가 종료 시퀀스를 잘못 트리거합니다.", + "doc": "오프 지연 카운트가 시작되려면 전력이 이 값 아래로 떨어져야 합니다. 시작 임계값보다 낮게 유지하세요. 너무 높으면 저전력 단계(헹굼 대기, 구김 방지)에서 종료 카운트다운이 잘못 시작됩니다.", "label": "중지 임계값" }, "switch_entity": { - "doc": "일시 중지 시 꺼졌다가 재개 시 다시 켜지는 옵션 스위치는 '일시 중지 시 전원도 차단됨'과 함께 사용됩니다.", + "doc": "\"일시 정지로 전력도 차단\"과 함께 사용하는 선택적 스위치로, 일시 정지 시 꺼지고 재개 시 다시 켜집니다.", "label": "스위치 엔티티" }, "watchdog_interval": { - "doc": "백그라운드 감시 장치가 정지된 센서와 경과된 시간 초과를 확인하는 빈도입니다. 기본값은 30초입니다.", + "doc": "백그라운드 검사가 멈춘 센서와 시간 초과를 확인하는 빈도입니다. 기본값 30초.", "label": "워치독 간격" }, "notify_milestone_message": { @@ -1550,139 +1483,94 @@ "label": "마일스톤 메시지" }, "notify_milestones": { - "doc": "도달 시 한 번만 축하 알림을 보낼 사이클 수를 쉼표로 구분하여 지정합니다(예: 50, 100, 500, 1000). 비워 두면 마일스톤 알림이 비활성화됩니다.", + "doc": "한 번만 축하 알림을 보낼 사이클 수를 쉼표로 구분해 입력합니다(예: 50, 100, 500, 1000). 비워 두면 비활성화됩니다.", "label": "사이클 마일스톤" }, "notify_quiet_end_hour": { - "doc": "방해 금지 시간대의 종료(0-23). 보류된 알림은 이 시각에 전달됩니다. 비워 두면 비활성화됩니다.", + "doc": "방해 금지 시간대의 종료(0-23). 보류된 알림은 이때 전송됩니다. 비워 두면 비활성화됩니다.", "label": "방해 금지 시간 종료" }, "notify_quiet_start_hour": { - "doc": "방해 금지 시간대의 시작(0-23). 방해 금지 시간 중 발생할 완료, 알림, 세탁 완료 알림은 보류되었다가 시간대가 끝날 때 전달됩니다. 비워 두면 비활성화됩니다. 자정을 넘는 시간대도 지원합니다(예: 시작 22, 종료 7).", + "doc": "방해 금지 시간대의 시작(0-23). 이 시간대의 완료, 꺼내기 알림, 세탁 완료 알림은 시간대가 끝날 때까지 보류됩니다. 자정을 넘길 수 있습니다(예: 22부터 7까지). 비워 두면 비활성화됩니다.", "label": "방해 금지 시간 시작" }, "peak_rate_message": { - "doc": "시작 알림에 추가되는 피크 요금 팁의 사용자 지정 텍스트(선택 사항). 템플릿 변수: {device}, {price}. 비워 두면 기본 제공 값이 사용됩니다.", + "doc": "피크 요금 팁의 사용자 지정 텍스트입니다. 템플릿 변수: {device}, {price}. 비워 두면 기본값을 사용합니다.", "label": "피크 요금 메시지" }, "peak_rate_threshold": { - "doc": "사이클이 시작될 때 현재 kWh당 가격이 이 값 이상이면 시작 알림에 피크 요금 팁을 추가합니다. 0 또는 비워 두면 팁이 비활성화됩니다.", + "doc": "사이클 시작 시 kWh당 가격이 이 값 이상이면 시작 알림에 피크 요금 팁을 추가합니다. 0 또는 비워 두면 비활성화됩니다.", "label": "피크 요금 임계값 (kWh당)" }, - "dtw_bandwidth": { - "label": "형상 매칭 허용치", - "doc": "저장된 프로그램의 형상에 맞추기 위해 매처가 사이클을 얼마나 시간 왜곡할 수 있는지를 결정합니다. 값이 클수록 속도 변화를 더 허용하지만, 너무 크면 잘못된 프로그램에 매칭될 위험이 있습니다." - }, - "corr_weight": { - "label": "형상 대 레벨 가중치", - "doc": "매칭 점수를 계산할 때 곡선 형상과 절대 전력 레벨 사이의 균형입니다. 값이 클수록 형상을, 작을수록 전력 레벨 일치를 우선합니다." - }, - "duration_weight": { - "label": "실행 시간 가중치", - "doc": "일반적인 실행 시간이 이 사이클과 일치하는 프로그램을 얼마나 우선할지 결정합니다." - }, - "energy_weight": { - "label": "에너지 가중치", - "doc": "일반적인 에너지 사용량이 이 사이클과 일치하는 프로그램을 얼마나 우선할지 결정합니다." - }, "store_brand": { "label": "가전제품 브랜드", - "doc": "선택 사항입니다. 커뮤니티 카탈로그에서 선택한 가전제품 브랜드입니다. 일치하는 참조 녹음을 찾고 공유하는 데 사용됩니다. 온라인 기능을 사용하지 않는 경우 비워 두세요." + "doc": "선택 사항. 커뮤니티 카탈로그에서 고르는 가전제품 브랜드로, 참조 녹음을 찾고 공유하는 데 사용됩니다. 온라인 기능을 사용하지 않으면 비워 두세요." }, "store_model": { "label": "가전제품 모델", - "doc": "선택 사항입니다. 브랜드를 설정하면 커뮤니티 카탈로그에서 선택할 수 있는 가전제품 모델입니다. 목록에 모델이 없으면 카탈로그에 추가할 수 있습니다." + "doc": "선택 사항. 커뮤니티 카탈로그에서 고르는 가전제품 모델입니다(먼저 브랜드 선택). 목록에 없으면 추가할 수 있습니다." }, "show_contributor": { "doc": "커뮤니티 가전제품 및 참조 사이클에 \"by \" 기여자 표시를 보여줍니다." }, - "enable_phase_matching": { - "label": "단계 기반 남은 시간", - "doc": "실행 중인 각 사이클을 단계(가열, 세탁, 탈수)로 나누고 단계별로 남은 시간을 배분하여 기존 추정값과 혼합합니다. 사이클 초반에는 단계별 배분을, 종료에 가까워질수록 기존 추정값을 더 중시합니다. 이렇게 하면 기기가 실제로 가열하고 작동하는 시간에 맞춰 카운트다운이 개인화되며, 사이클 전반부에서 가장 뚜렷하게 나타납니다. 꺼짐 = 기존 추정값만 사용. 남은 시간 표시에만 영향을 미치며, 프로그램 매칭과 사이클 감지는 변경되지 않습니다." - }, - "keep_min_score": { - "label": "최소 매칭 점수", - "doc": "후보가 매칭 경쟁에 남기 위해 필요한 최소 유사도 점수입니다. 기본값 0.1은 의도적으로 허용적입니다. 약한 후보도 허용하고 이후 단계에서 최선을 찾도록 합니다. 가능성이 낮은 프로파일을 더 일찍 제거하려면 높이고, 초기 후보 풀을 넓히려면 낮추세요." - }, - "dtw_blend": { - "label": "워프 블렌드", - "doc": "시간 워프(DTW) 정렬 점수가 Stage 2 핵심 점수를 얼마나 대체하는지 나타냅니다. 0 = Stage 2 점수만 사용, 1 = DTW 점수만 사용, 0.5 (기본값) = 동등 혼합. 프로그램들이 유사한 전력 수준이지만 다른 타이밍 패턴을 가질 때 워프 정렬에 더 의존하려면 높이세요." - }, - "dtw_ensemble_w": { - "label": "워프 앙상블 혼합", - "doc": "앙상블 DTW 모드(기본값)에서 스케일 L1과 도함수 DTW(DDTW)의 가중치 배분입니다. 1.0 = 모두 스케일-L1, 0 = 모두 DDTW, 0.7 = 기본값. 스케일-L1 방식은 전력 수준을 비교하고, 도함수 방식은 전력이 시간에 따라 변하는 방식에 반응합니다. 앙상블은 두 가지를 결합합니다." - }, - "dtw_ddtw_scale": { - "label": "미분 워프 스케일", - "doc": "도함수 DTW 점수화를 위한 반포화 거리입니다. DDTW 거리가 이 값과 같을 때 점수가 반으로 줄어듭니다. 작을수록 형상 차이에 민감합니다. 기본값 30은 일반적인 가전제품 전력 추적에 맞게 보정되어 있습니다. 앙상블 및 ddtw DTW 모드에만 영향을 미칩니다." - }, - "dtw_refine_top_n": { - "label": "워프 정제 개수", - "doc": "DTW가 재점수화하는 Stage 2 상위 후보 수입니다. DTW는 계산 비용이 높아 상위 N개 후보에만 적용됩니다. 기본값 5. 올바른 프로파일이 Stage 2 후 4위나 5위에 그치는 경우 값을 높이세요(7-9) - DTW가 이를 구할 수 있습니다. 낮추면 매칭 속도가 약간 빨라집니다." - }, - "duration_scale": { - "label": "지속 시간 선명도", - "doc": "Stage 4 지속 시간 일치 패널티의 날카로움입니다. 이것은 일치 점수가 반으로 줄어드는 로그 비율입니다. 작을수록 엄격합니다. 지속 시간 불일치가 더 큰 영향을 미칩니다. 기본값 0.175는 반가중치에서 약 18%의 지속 시간 허용 오차에 해당합니다. 지속 시간 가중치와 함께 사용하세요." - }, - "energy_scale": { - "label": "에너지 선명도", - "doc": "Stage 4 에너지 일치 패널티의 날카로움입니다. 작을수록 엄격합니다. 에너지 불일치가 더 큰 영향을 미칩니다. 기본값 0.25는 에너지가 부하에 따라 변하기 때문에 지속 시간 날카로움보다 의도적으로 더 허용적입니다. 에너지 가중치와 함께 사용하세요." - }, "power_profile_interval_min": { "label": "전력 프로파일 간격", - "doc": "EMHASS, tibber_prices 등 외부 계획 도구가 사용하는 per-profile power_profile 센서 속성(슬롯당 평균 와트 배열)의 버킷 크기입니다. 작은 버킷은 짧은 전력 급등을 선명하게 유지하고, 큰 버킷은 형태를 완만하게 합니다. 기본값 15분. 읽기 시에만 사용되며 검출에는 영향을 주지 않습니다." + "doc": "EMHASS, tibber_prices 등의 계획 도구가 사용하는 power_profile 속성(슬롯당 평균 와트)의 버킷 크기입니다. 작으면 짧은 급등이 유지되고, 크면 곡선이 완만해집니다. 기본값 15분. 감지에는 영향을 주지 않습니다." }, "notify_live_silent": { "label": "무음 라이브 업데이트", - "doc": "iOS 전용. 라이브 진행 상황을 조용히 새로 고칩니다. 각 업데이트는 소리나 진동 없이 도착하며, 배터리를 아끼는 낮은 우선순위로 전송되므로 iOS가 잠시 묶어서 전달할 수 있습니다. 사이클의 첫 업데이트(실시간 현황을 시작하는 업데이트)와 시작/완료 알림은 평소대로 알려 줍니다. 모든 업데이트마다 알림을 받으려면 이 옵션을 끄세요." + "doc": "iOS 전용. 실시간 진행 상황을 조용히 업데이트하며, 배터리를 아끼는 우선순위로 전송되어 iOS가 묶어서 전달할 수 있습니다. 첫 업데이트와 시작/완료 알림은 그대로 알려 줍니다. 꺼짐: 모든 업데이트마다 알립니다." }, "notify_live_sticky": { "label": "탭 후 실시간 알림 유지", - "doc": "Android 전용. 실시간 진행 알림을 고정(스티키)으로 만들어 탭해도 진행 중인 알림 스레드가 사라지지 않도록 합니다. 꺼져 있으면 탭 시 알림이 사라지는 기본 동작이 유지됩니다." + "doc": "Android 전용. 실시간 진행 알림을 탭해도 사라지지 않게 합니다. 꺼짐: 탭하면 사라집니다." }, "notify_live_click_action": { "label": "알림 탭 대상", - "doc": "이 가전제품의 알림을 탭했을 때 열리는 위치로, Android와 iOS에 모두 적용됩니다. 비워 두면 이 가전제품이 열린 WashData 패널로 이동합니다. 다른 곳으로 보내려면 대시보드 경로(예: /lovelace/laundry)나 전체 URL을 입력하고, 탭 대상을 아예 두지 않으려면 'none'을 입력하세요." + "doc": "이 가전제품의 알림을 탭하면 열리는 곳입니다. 비워 두면 이 가전제품의 WashData 패널이 열립니다. 대시보드 경로(예: /lovelace/laundry)나 URL을 입력하거나, 탭 동작을 없애려면 'none'을 입력하세요." }, "door_opens_at_end": { "label": "종료 시 자동으로 문 열림", - "doc": "세척 후 건조를 위해 자동으로 문이 열리는 식기세척기(AirDry 등)용 설정입니다. 이 기능을 켜면 실행 중인 사이클에서 문이 열려도 사이클이 영구적으로 일시 정지되지 않습니다. 대신 문이 아래의 대기 시간 이상 열려 있으면 WashData가 사이클을 완료로 처리합니다. 잠깐 여는 동작(식기 추가 등)은 무시됩니다. 문 센서 엔티티가 필요합니다." + "doc": "종료 시 건조를 위해 문을 여는 식기세척기(AirDry 등)용입니다. 문이 아래 대기 시간 동안 열려 있으면 일시 정지 대신 사이클을 종료합니다. 잠깐 여는 것은 무시됩니다. 문 센서 엔티티가 필요합니다." }, "door_end_dwell_seconds": { "label": "문 열림 종료 대기 시간", - "doc": "\"종료 시 자동으로 문 열림\"이 켜져 있을 때, WashData가 사이클을 종료하기 전에 문이 열려 있어야 하는 시간입니다. 식기를 빨리 추가하는 경우(바로 닫힘)는 무시하면서도 기기가 문을 열었을 때 신속하게 종료할 수 있을 만큼 짧게 설정합니다(기본값 60초)." + "doc": "\"종료 시 자동으로 문 열림\"이 켜져 있을 때 사이클이 끝나기 전에 문이 열려 있어야 하는 시간입니다. 식기를 추가하는 정도는 무시할 만큼 길게 설정합니다(기본값 60초)." }, "dishwasher_end_spike_quiet_release": { "label": "자연 건조 조용 시 해제", - "doc": "사이클이 예상 소요 시간을 지난 후, WashData가 최종 배수를 더 이상 기다리지 않고 사이클을 종료하기까지 식기세척기가 조용한 상태(중지 임계값 미만)를 유지해야 하는 시간입니다. 늦게 발생하는 최종 배수 전에 긴 무음 건조 단계가 있고 그 배수가 감지되지 않고 있다면 값을 높이세요. 창을 넓히면 학습된 소요 시간이 예전 평균값에 고정되지 않고 계절적 변동(급수 온도가 낮을수록 사이클이 길어짐)을 따라갈 수 있습니다. 이 설정은 내부의 30분 종료 스파이크 상한에 비해 대기 시간을 단축할 수만 있고 연장하지는 않습니다." + "doc": "예상 소요 시간이 지난 후, WashData가 최종 배수를 더 이상 기다리지 않기까지 식기세척기가 중지 임계값 미만으로 유지되어야 하는 시간입니다. 긴 건조 후 늦은 배수가 감지되지 않으면 값을 높이세요. 대기 시간은 30분을 넘지 않습니다." }, "profile_evidence_sources": { "label": "프로그램을 형성하는 사이클", - "doc": "각 프로그램의 전력 곡선을 만들고 완료된 사이클을 그것과 매칭할 때 사용할 사이클 종류입니다. 종류의 선택을 해제하면 해당 종류는 더 이상 프로그램에 반영되지 않지만 아무것도 삭제되지 않습니다. 사이클은 사이클 목록에 그대로 남아 있으며 계속 라벨을 지정하거나 삭제할 수 있습니다. 가져온 데이터를 신뢰할 수 없을 때 유용합니다. 통계에는 영향이 없습니다. 통계는 항상 이 기기가 실제로 실행한 사이클만 계산합니다. 모두 선택 해제하면 무시됩니다. 근거가 되는 사이클이 없는 프로그램은 결코 매칭될 수 없기 때문입니다." + "doc": "각 프로그램의 곡선과 매칭에 사용할 사이클 종류입니다. 선택을 해제해도 아무것도 삭제되지 않으며, 가져온 데이터를 신뢰할 수 없을 때 유용합니다. 통계는 항상 이 기기의 사이클만 계산합니다. 모두 해제하면 무시됩니다." }, "energy_price_dynamic": { "label": "시간 가중 비용", - "doc": "완료 시점의 가격 하나로 계산하는 대신, 사이클이 실행된 매 순간에 적용되던 가격으로 비용을 계산합니다. 가격 엔티티를 설정한 경우에만 적용됩니다(고정 가격은 변하지 않습니다). 추가 설정은 필요 없습니다. 사이클이 진행되는 동안 가격을 실시간으로 추적하며, 진단의 기록 처리를 실행하면 이전 사이클도 recorder 기록을 바탕으로 다시 계산합니다. 끄면 기존 방식대로 종료 시점의 가격 하나로 고정됩니다." + "doc": "완료 시점의 가격 대신 사이클이 실행된 매 순간의 가격으로 비용을 계산합니다. 가격 엔티티가 필요합니다. 이전 사이클은 진단의 기록 처리로 다시 계산됩니다." }, "anti_crease_finalize_ratio": { "label": "구김 방지 확정 비율", - "doc": "WashData 가 지금 보이는 저전력 텀블을 구김 방지 꼬리로 판단하고 거기서 사이클을 끝내도 되는 시점을, 일치된 프로그램의 예상 소요 시간에 대한 비율로 지정합니다. 예상 소요 시간은 해당 프로그램의 평균이므로, 센서 건조로 운전 시간이 세탁물 양에 따라 달라지는 건조기에서는 대부분의 운전이 그 98%에 도달하지 못하고 꼬리가 대체 타임아웃까지 그대로 남습니다. 기계가 실제로 멈춘 뒤 15~40분이 지나는 경우도 흔합니다. 그런 건조기에서는 이 값을 낮추십시오(예: 0.75). 세탁기는 기본값으로 두십시오. 세탁기는 사이클의 대부분을 '구김 방지 최대 전력' 아래에서 보내므로 조용한 세탁 중간 단계와 꼬리를 구분하는 것은 이 비율뿐이며, 값을 낮추면 세탁이 절반에서 잘릴 수 있습니다. 비워 두면 기본값(0.98)이 사용됩니다. 이 설정은 별도의 조기 종료 검사를 제어하는 '스마트 종료 비율'과는 다릅니다." + "doc": "저전력 텀블이 구김 방지 꼬리로 간주되기 전에 사이클이 도달해야 하는 프로그램 평균 소요 시간의 비율입니다. 부하 감지 건조기에서는 낮추세요(예: 0.75). 세탁기는 기본값(0.98)으로 두세요. 그렇지 않으면 세탁이 중간에 끝날 수 있습니다." }, "curve_preroll_seconds": { "label": "곡선 프리롤", - "doc": "중단된 시작 시도에서 얻은 측정값을 사이클 곡선의 앞부분에 몇 초까지 포함할 수 있는지 지정합니다. 사이클 곡선은 보통 최종적으로 성공한 시작 시도부터 시작하므로, 프로그램 선택, 도어 잠금, 첫 급수처럼 몇 번 시도한 뒤에야 안정되는 기계에서는 실제 동작의 첫 몇 분이 모든 곡선에서 빠집니다. 기계에서 가장 긴 준비 구간보다 조금 큰 값으로 설정하십시오(대부분 300초면 충분합니다). 0이면 꺼집니다. 이 기능을 켜면 기록되는 시작 시점이 앞당겨지므로, 변경 전후에 기록된 사이클은 같은 프로그램이라도 소요 시간이 달라집니다. 오래된 사이클이 밀려날 때까지 학습된 평균값은 어긋난 상태로 유지됩니다." + "doc": "시작 전에 몇 번 시도하는 기기(도어 잠금, 첫 급수)를 위해 실패한 시작 시도의 측정값을 사이클 앞부분에 추가합니다. 대부분 300초면 충분합니다. 0 = 끔. 기록되는 시작 시점이 앞당겨지므로 학습된 소요 시간이 한동안 어긋납니다." }, "unload_confirm_entity": { "label": "꺼내기 확인 엔티티", - "doc": "문 센서를 달 수 없는 기기를 위해, 작동하면 내용물을 꺼낸 것으로 간주할 엔티티(선택 사항). 종류는 상관없습니다: Zigbee 또는 NFC 버튼, input_button 도우미, 기기 앞에 둔 동작 감지 센서, 장면 등. 꺼짐과 사용 불가로 바뀌는 경우를 제외한 모든 상태 변화가 확인으로 인정됩니다. 이 항목을 설정하면 문 센서 엔티티가 없는 장치에서도 꺼내기 알림이 활성화됩니다." + "doc": "선택 사항. 문 센서가 없는 기기를 위해 내용물을 꺼냈음을 확인하는 엔티티입니다(버튼, input_button, 동작 감지 센서, 장면 등). 꺼짐이나 사용 불가로 바뀌는 경우를 제외한 모든 상태 변화가 확인으로 인정됩니다. 꺼내기 알림도 활성화됩니다." }, "unload_track_without_door": { "label": "꺼내기 수동 확인", - "doc": "문 센서 엔티티와 꺼내기 확인 엔티티가 모두 없는 장치에서도 꺼내기 알림을 실행하고, 대신 직접 만든 자동화에서 꺼내기를 확인합니다: \"꺼냄으로 표시\" 버튼 엔티티를 누르거나 ha_washdata.mark_unloaded 서비스를 호출하세요. 문 센서 엔티티가 설정되어 있으면 이미 신호가 있으므로 무시됩니다." + "doc": "문 센서나 확인 엔티티 없이 꺼내기 알림을 실행합니다. 직접 만든 자동화에서 \"꺼냄으로 표시\" 버튼이나 ha_washdata.mark_unloaded 서비스로 꺼내기를 확인하세요. 문 센서 엔티티가 설정되어 있으면 무시됩니다." }, "notify_icon_color": { "label": "알림 색상", - "doc": "이 가전제품의 알림에 사용할 강조 색상으로, 세탁기와 건조기, 식기세척기를 한눈에 구분할 수 있습니다. iOS는 아이콘과 잠금 화면 및 Dynamic Island에 표시되는 실시간 현황의 진행률 표시줄에 색을 입힙니다. Android에도 같은 색상이 전달되지만 이를 어떻게 처리할지는 휴대폰에 달려 있습니다. 아이콘에 색을 입히는 기기도 있고, 아예 무시하는 기기도 많습니다(특히 Samsung One UI). 16진수(예: #03A9F4)는 양쪽 모두 인식합니다. 플랫폼 기본값을 사용하려면 비워 두세요." + "doc": "이 가전제품 알림의 강조 색상으로, 세탁기와 건조기, 식기세척기를 쉽게 구분할 수 있습니다. iOS에서는 아이콘 뒤의 원을 채우고 실시간 현황 막대에 색을 입힙니다. 많은 Android 휴대폰(특히 Samsung)은 이를 무시합니다. 16진수로 입력하세요(예: #03A9F4). 비워 두면 플랫폼 기본값을 사용합니다." + }, + "share_name": { + "doc": "공유하는 사이클의 업로더로 계정 이름을 표시합니다. 꺼짐: 이름 없이 공유됩니다." } }, "setting_group": { @@ -1710,9 +1598,6 @@ "door_pause": { "label": "문과 일시 정지" }, - "dtw_refinement": { - "label": "DTW 정밀화" - }, "duration_gates": { "label": "기간 게이트" }, @@ -1789,13 +1674,18 @@ "rebuilding": "재구축 중…", "analyzing": "분석 중…", "resetting": "재설정 중…", - "reverting": "되돌리는 중…", "trimming": "자르는 중…", "splitting": "분할 중…", + "stalled": "멈춤", "deleting": "삭제 중…", "imported": "가져옴", "preparing": "준비 중…", - "unlabelling": "라벨 해제 중…" + "unlabelling": "라벨 해제 중…", + "working": "처리 중…", + "match_uncertain": "불확실: {top} 또는 {runner}", + "match_uncertain_single": "불확실: 아마도 {top}", + "match_sure": "약 {pct}% 확신", + "match_uncertain_tip": "WashData가 아직 프로그램을 확정하지 않았습니다. 이 수치는 과거 사이클에서 선두 후보인 {top}이(가) 이만큼 앞섰을 때 맞았던 비율입니다. 참고용입니다." }, "tab": { "advanced": "고급", @@ -1826,13 +1716,17 @@ }, "toast": { "catalog_refreshed": "커뮤니티 카탈로그를 새로 고쳤습니다", + "task_name_required": "작업 이름을 입력하세요", + "task_added": "작업이 추가되었습니다", + "task_save_failed": "작업을 저장할 수 없습니다: {error}", + "task_removed": "작업이 제거되었습니다", + "maint_logged": "완료로 기록됨: {task}", "access_saved": "액세스 제어가 저장되었습니다.", "all_wiped": "모든 데이터가 지워졌습니다.", "analysis_complete_none": "분석 완료: 새로운 제안 없음", "auto_label_complete": "자동 라벨 완료", "automation_deleted": "자동화가 삭제되었습니다.", "correction_submitted": "수정사항이 제출되었습니다.", - "cycle_deleted": "주기가 삭제되었습니다.", "cycle_force_stopped": "사이클 강제 정지", "cycle_labelled": "라벨이 붙은 사이클", "cycle_paused": "주기가 일시중지됨", @@ -1848,11 +1742,9 @@ "group_deleted": "그룹이 삭제되었습니다.", "group_name_required": "그룹 이름은 필수 항목입니다.", "group_saved": "그룹이 저장되었습니다", - "import_successful": "가져오기가 성공했습니다. 통합 다시 로드", "json_required": "JSON 데이터가 필요합니다", "legacy_removed": "기존 작업이 제거되었습니다.", "logs_exported": "내보낸 로그", - "matching_reverted": "일치하는 가중치가 기본값으로 되돌아갔습니다.", "min_2_profiles": "그룹당 프로필을 2개 이상 선택하세요.", "ml_training_no_improvement": "훈련 완료: 기준선 유지(개선 없음)", "models_reverted": "온디바이스 모델이 기준으로 되돌아갔습니다.", @@ -1862,8 +1754,8 @@ "pg_preset_loaded": "프리셋 \"{name}\"을(를) 불러왔습니다", "pg_preset_saved": "프리셋 \"{name}\"을(를) 저장했습니다", "pg_sugg_loaded": "{n}개 제안 값을 적용했습니다 - 플레이그라운드에서 비교해 보세요", - "pg_sugg_ml_loaded": "{n}개 ML 조정 값을 적용했습니다 - 플레이그라운드에서 비교해 보세요", "phase_name_required": "단계 이름은 필수 항목입니다.", + "phase_reset": "\"{name}\"이(가) 기본 제공 단계로 복원되었습니다", "phase_updated": "단계가 업데이트되었습니다.", "phases_saved": "저장된 단계", "preferences_saved": "환경설정이 저장되었습니다.", @@ -1910,14 +1802,10 @@ "store_name_required": "프로필 이름을 입력하세요", "store_import_failed": "가져오기 실패: {error}", "store_imported": "{profile}(으)로 가져왔습니다", - "store_program_required": "프로그램 이름을 입력하세요", "store_no_appliance": "먼저 설정에서 가전제품 브랜드와 모델을 설정하세요.", "store_share_failed": "공유 실패: {error}", "store_shared": "커뮤니티 스토어에 공유되었습니다 (검토 대기 중)", "appliance_added": "가전제품이 추가되었습니다 (승인 대기 중)", - "thanks_confirming": "확인해 주셔서 감사합니다", - "device_approved": "커뮤니티에서 승인되었습니다", - "rating_saved": "품질 평가가 저장되었습니다", "brand_added": "브랜드가 추가되었습니다 (승인 대기 중)", "profile_added": "프로필이 추가되었습니다 (승인 대기 중)", "saved_except_conflicts": "저장되었습니다. 나머지를 저장하려면 강조된 충돌을 수정하세요.", @@ -1941,11 +1829,48 @@ "odometer_save_failed": "누적 값을 저장할 수 없습니다: {error}", "contrib_device_changed": "커뮤니티 스토어에 추가했지만 입력 양식이 열려 있는 동안 기기가 변경되었습니다. 설정에서 선택하세요.", "store_pick_profile_share": "공유할 프로필을 선택하세요", - "remove_failed": "제거 실패: {error}" + "remove_failed": "제거 실패: {error}", + "analysis_complete": "분석 완료: 제안 {count}개", + "analysis_failed": "분석 실패: {error}", + "auto_detect_failed": "자동 감지 실패: {error}", + "auto_label_failed": "자동 라벨 지정 실패: {error}", + "could_not_load_cycle": "사이클을 불러올 수 없습니다: {error}", + "create_failed": "생성 실패: {error}", + "cycles_deleted": "사이클 {count}개를 삭제했습니다", + "debug_cleared": "디버그 추적 {count}개를 지웠습니다", + "discard_failed": "폐기 실패: {error}", + "error": "오류: {error}", + "export_failed": "내보내기 실패: {error}", + "import_failed": "가져오기 실패: {error}", + "invalid_json": "\"{key}\"은(는) 유효한 JSON이 아닙니다. 저장하기 전에 수정하거나 필드를 비우세요.", + "label_failed": "라벨 지정 실패: {error}", + "ml_training_promoted": "훈련 완료: 모델 {count}개 승격됨", + "pause_failed": "일시정지 실패: {error}", + "phase_created": "단계 \"{name}\"이(가) 생성되었습니다", + "phase_deleted": "단계 \"{name}\"이(가) 삭제되었습니다", + "profile_created": "프로필 \"{name}\"이(가) 생성되었습니다", + "rename_failed": "이름 변경 실패: {error}", + "save_failed": "저장 실패: {error}", + "split_complete": "사이클 {count}개로 분할했습니다", + "start_failed": "시작 실패: {error}", + "stop_failed": "중지 실패: {error}", + "update_failed": "업데이트 실패: {error}", + "store_download_skipped": "기록 {n}개 건너뜀: 너무 짧거나, 공백이 너무 많거나, 이미 기기에 있습니다.", + "store_import_low_quality": "이 기록은 너무 짧거나, 공백이 너무 많거나, 타당하지 않아 가져올 수 없습니다.", + "store_import_duplicate": "이 기록은 이미 기기에 있습니다.", + "device_approved": "커뮤니티에서 승인되었습니다", + "thanks_confirming": "확인해 주셔서 감사합니다", + "rating_saved": "품질 평가가 저장되었습니다", + "store_download_partial": "프로그램 {n}개를 다운로드하지 못했습니다. 나중에 다시 시도하세요.", + "import_no_restore_point": "가져왔지만 복원 지점을 저장할 수 없어 이 가져오기는 실행 취소할 수 없습니다", + "import_undo_failed": "가져오기를 실행 취소하지 못했습니다: {error}", + "import_undone": "가져오기 실행 취소됨: 프로필 {profiles}개와 사이클 {cycles}개를 복원했습니다", + "auto_label_cancelled": "사이클 {count}개에 라벨을 지정한 후 자동 라벨 지정이 취소되었습니다.", + "auto_label_cancelled_other": "사이클 {count}개에 라벨을 지정한 후 자동 라벨 지정이 취소되었습니다.", + "auto_label_done": "자동 라벨 지정 완료: 사이클 {count}개에 라벨을 지정했고 {skipped}개는 라벨 없이 남았습니다.", + "auto_label_done_other": "자동 라벨 지정 완료: 사이클 {count}개에 라벨을 지정했고 {skipped}개는 라벨 없이 남았습니다." }, "suggestion": { - "both_agree": "WashData 권장", - "calibrated_label": "보정됨", "observed_label": "관측값", "impact": { "auto_label_confidence": { @@ -1960,10 +1885,6 @@ "higher": "상당한 에너지를 사용한 사이클만 종료합니다", "lower": "짧거나 저에너지 사이클도 종료합니다" }, - "end_repeat_count": { - "higher": "종료 신호를 더 많이 반복해야 합니다. 더 안정적이지만 약간 느려집니다", - "lower": "더 적은 반복 후 종료합니다. 빠른 감지이지만 오종료 위험이 있습니다" - }, "learning_confidence": { "higher": "매우 신뢰도 높은 매칭에서만 학습합니다. 느리지만 더 신뢰할 수 있습니다", "lower": "더 많은 사이클에서 학습합니다. 모델에 노이즈가 더 많을 수 있습니다" @@ -2002,40 +1923,25 @@ } }, "reason": { - "auto_label_confidence": "사용자가 한 번도 수정하지 않은 {cycles}개 자동 라벨의 15백분위 신뢰도({p15}).", "completion_min_seconds": "{cycles} 사이클의 p05 정상 사이클 시간의 절반({minutes} min). 허위 사이클을 걸러냅니다.{excl}", - "dead_zone_batch": "짧게 유지({cycles} 사이클의 시작 불안정 구간 중앙값 = {s}s). 실제 종료를 신속하게 감지합니다.", - "dead_zone_from_dip": "{s}s에서 감지된 초기 전력 저하를 기반으로 함.", - "end_energy_default": "사이클 종료 노이즈 게이트에 권장되는 기본값.", - "end_energy_false": "{cycles} 사이클의 p95 허위 종료 에너지({p95}Wh). 사이클 에너지 중앙값의 0.2%({floor}Wh)를 하한으로 함.", - "end_energy_no_false": "{cycles} 사이클에서 허위 종료 없음. 사이클 에너지 중앙값의 0.2%({floor}Wh)를 하한으로 함.", - "end_repeat_count": "{false}/{total}개 정상 사이클({pct}%)에서 재개된 60s 초과 내부 일시정지가 있었습니다. {val}회의 종료 확인을 요구합니다.", - "learning_confidence": "{cycles}개 사용자 라벨 사이클의 p05 신뢰도({p05}). 이보다 낮으면 확인을 요청합니다.", "match_interval": "관측된 업데이트 주기 기반(중앙값={median}s) * 10.", "max_duration_ratio": "라벨이 지정된 사이클 시간 기반(p95={p95}).", - "min_active": "마지막 사이클에서 관측된 최소 활성 전력({min}W) 기반.", - "min_duration_ratio": "프로그램을 사이클 초반에 식별할 수 있도록 최대한 낮게 유지합니다. 신뢰도와 모호성 게이트가 조기 확정을 방지합니다.", "min_off_gap": "관측된 {gaps}개 사이클 간 간격 기반(p05={p05}s). 기기 하한: {floor}s.", "min_off_gap_bridge": "사이클 내에서 전력이 거의 없는 가장 긴 구간을 넘기도록 설정: {span}s + 60s 버퍼. {cycles}개 정상 사이클에 걸친 {spans}건의 넘긴 간격에서 산출(연속 실행 사이에 두는 최소 간격 {ceiling}s 미만 유지).", "min_power": "{cycles}개 정상 사이클의 p05 최저 활성 전력({p05}W)의 40%. 꺼짐 게이트를 실제 소비 전력보다 낮게 유지합니다.{excl}", - "ml_auto_label": "품질 모델이 여전히 정상으로 평가한 최저 신뢰도({cycles}개 정상 사이클의 p10 = {p10}).", - "ml_end_repeat": "{false}/{total} 사이클({pct}%)에서 종료 검출기가 50% 초과로 평가한 일시정지가 있었습니다. {val}회의 종료 확인을 요구합니다.", - "ml_off_delay": "종료 검출기가 확인한 일시정지: p95 {p95}s + 60s 버퍼. {cycles} 사이클에 걸친 모델 검증 일시정지 {pauses}건에서 산출(하한 {floor}s).", "no_update_timeout": "관측된 업데이트 주기 기반(p95={p95}s) * 20(최소 60s).", "off_delay_cadence": "관측된 업데이트 주기 기반(p95={p95}s) * 5", "off_delay_device_floor": "{device}에 대한 기기별 안전 최소값을 사용({floor}s).", "off_delay_generic_floor": "일반 안전 최소값을 사용({floor}s).", "off_delay_pauses": "실제 일시정지보다 길도록 설정: p95 사이클 내 일시정지 {p95}s + 60s 버퍼. {cycles}개 정상 사이클에 걸친 {pauses}건의 일시정지에서 산출(하한 {floor}s).", - "profile_match_threshold": "{cycles}개 올바른 자동 라벨의 p10 신뢰도({p10}). 안전한 실시간 확정 하한.", - "sampling_interval": "{cycles}개 정상 사이클에서 관측된 중앙값 업데이트 간격({si}s).{excl}", - "smoothing_window": "{si}s 샘플링에서 약 30s 분량의 측정값을 평활화하도록 설정({samples} 샘플).", - "start_duration": "짧게 유지(약 1회 {si}s 샘플 간격). 단일 샘플 스파이크는 무시하면서 감지를 최대한 빨리 시작합니다.", - "thr_batch": "{cycles} 사이클의 p05 최저 활성 전력({p05}W) 바로 위로 유지합니다. 그래서 시작을 최대한 빨리 포착하고, 정지 임계값은 기기의 최저 작동 전력보다 낮게 유지됩니다.", - "tol_per_profile": "{profiles}개 프로필({cycles} 사이클)의 프로필별 시간 분산 p75. 편차가 작은 프로필은 불이익을 받지 않습니다.", - "tol_pooled": "{cycles}개 최근 라벨 사이클의 통합 시간 분산 기반(p95 편차={dev}).", + "standby_floor": "최근 모든 사이클이 종료 시 여전히 {idle} W를 소비하고 있었으므로, 그 미만의 임계값은 기기의 꺼짐을 감지할 수 없습니다. 임계값은 그 위로 유지됩니다. 기록된 {cycles}개의 정상 사이클은 그 아래로 {pause}초 이상 멈춘 적이 없습니다.", + "thr_batch": "{cycles} 사이클의 최저 활성 전력(p05, {p05}W) 바로 위입니다. 그래서 시작을 일찍 포착하고 중지 임계값은 최저 작동 전력보다 낮게 유지됩니다.", "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} 초) 최소값으로 두어도 부담이 없습니다." + "match_interval_floored": "최단 프로그램({shortest} 초)으로는 상한이 {cap} 초가 되어 최소값 {minimum} 초보다 낮으므로 최소값을 사용합니다. 연속 {persistence}회 매칭에는 {budget} 초가 걸리며, 이는 해당 프로그램의 {pct}%를 넘습니다. 매칭은 측정값이 도착할 때만 실행되므로(중앙값={median} 초) 부담이 없습니다.", + "end_energy_floor": "중지 임계값 ({stop} W)과 오프 지연 ({off_delay} s)이 의미하는 하한으로 올렸습니다. 이보다 낮으면 사이클이 대체 경로로만 종료될 수 있습니다.", + "max_duration_ratio_default": "기본값으로 되돌렸습니다. 상한이 낮으면 운전이 평소보다 길어질 때마다 올바른 프로그램이 제외됩니다.", + "min_duration_ratio_default": "기본값으로 되돌렸습니다. 하한이 높으면 실행 중인 사이클이 상당히 진행될 때까지 인식되지 않습니다." }, "exclusions": { "summary": "잘못 감지된 사이클 {total}개를 제외했습니다: {parts}.", @@ -2053,7 +1959,8 @@ "no_power": "전력 없음", "no_active_power": "활성 전력 없음" } - } + }, + "now_value": "(현재 {v})" }, "trend": { "down": "하락세", @@ -2061,6 +1968,10 @@ }, "maint": { "bearing_service": "베어링 정비", + "salt": "소금 보충", + "rinse_aid": "린스 보충", + "lint_filter": "보풀 필터 청소", + "condenser_clean": "콘덴서 청소", "descale": "석회질 제거", "drum_clean": "드럼 청소", "filter_clean": "필터 청소", @@ -2071,12 +1982,14 @@ "status_tip": "사이클 결과: 완료됨(자연 종료), 중단됨(급격한 전력 강하), 강제 정지됨(수동), 또는 검토 필요(피드백 대기 중).", "date_tip": "사이클이 시작된 날짜와 시간입니다.", "duration_tip": "시작부터 종료까지의 총 사이클 실행 시간입니다.", - "energy_tip": "소비된 총 에너지(kWh)입니다. 시간에 따라 전력을 적분하여 계산합니다.", - "cost_tip": "이 사이클의 에너지 비용으로, 완료 시점에 고정됩니다. 시간 가중 비용을 켜면 전력 곡선이 각 시점에 적용되던 가격으로 계산되고, 끄면 에너지 x 종료 시점의 가격 하나로 계산됩니다. 값을 채우려면 설정에서 가격을 지정하세요.", + "energy_tip": "사용한 총 에너지(kWh)로, 시간에 따른 전력으로 계산합니다.", + "cost_tip": "이 사이클의 에너지 비용으로, 완료 시 확정됩니다. 시간 가중 비용을 켜면 각 시점의 가격으로, 끄면 종료 시점의 가격으로 계산됩니다. 값을 표시하려면 설정에서 가격을 지정하세요.", "confidence_tip": "프로필 매칭 신뢰도(0-100%)입니다. 사이클 전력 곡선이 식별된 프로그램과 얼마나 근접하게 일치했는지를 나타냅니다.", - "health_tip": "ML 사이클 건강(높을수록 좋음)입니다. 검사하고 검토하려면 사이클을 클릭하세요.", "flags_tip": "사이클의 검토, 이상, 소스 플래그. 아이콘에 마우스를 올리면 세부 정보가 표시됩니다.", - "cost_dynamic_tip": "시간 가중: 사이클 진행 중 적용되던 가격으로 계산됨(실효 {price} {cur}/kWh)." + "cost_dynamic_tip": "시간 가중: 사이클 진행 중 적용되던 가격으로 계산됨(실효 {price} {cur}/kWh).", + "dbg_conf": "신뢰도", + "dbg_mae": "MAE", + "dbg_corr": "상관" }, "stat": { "avg": "평균", @@ -2100,7 +2013,10 @@ "notify_finish": "완료 알림이 전송됩니다.", "notify_milestone": "마일스톤 알림이 전송됩니다.", "notify_held": "방해 금지 시간대라서 알림이 보류되었습니다.", - "finished": "사이클이 종료 상태에 도달하여 끝났습니다." + "finished": "사이클이 종료 상태에 도달하여 끝났습니다.", + "match_reverted": "프로그램이 감지 중 상태로 되돌아갔습니다. 점수가 자체 최고치보다 크게 떨어졌습니다.", + "verified_pause": "실시간 정렬이 프로그램 내부의 일시 중지를 확인(또는 해제)했습니다. 이 상태가 유지되는 동안에는 사이클이 종료되지 않습니다.", + "stalled": "남은 작업이 있는데도 사이클이 이 프로그램의 어떤 일시 정지보다 오래 대기 전력 상태에 머물렀습니다. 일시 정지됨 (멈춤)으로 표시되며 사이클은 열린 상태로 유지됩니다." }, "store": { "search_ph2": "제조사 또는 모델명 검색…", @@ -2109,11 +2025,7 @@ "programs_count": "프로그램: {n}", "browse": "탐색", "device": "기기", - "favorites": "즐겨찾기", - "search_ph": "브랜드 또는 모델로 검색…", "no_results": "일치하는 가전제품을 찾을 수 없습니다. 다른 검색어로 시도하세요.", - "n_programs": "프로그램 {n}개", - "n_cycles": "사이클 {n}개", "no_programs": "이 가전제품에 대해 아직 공유된 프로그램이 없습니다.", "no_cycles": "이 프로그램에 대해 아직 공유된 참조 주기가 없습니다.", "anon": "익명", @@ -2125,19 +2037,21 @@ "merge_target": "병합 대상 프로필", "description": "설명(선택 사항)", "contributed_by": "{name} 제공", + "website": "스토어 웹사이트 ↗", + "website_tip": "커뮤니티 스토어 웹사이트 열기", + "favorites": "즐겨찾기", "rating_summary": "★ {avg} ({n})", "no_ratings": "아직 평가 없음", - "website": "스토어 웹사이트 ↗", - "website_tip": "커뮤니티 스토어 웹사이트 열기" + "unreachable": "커뮤니티 스토어에 연결할 수 없습니다. 연결 상태를 확인하고 다시 시도하세요." }, "settings": { "gear": { - "title": "설정" + "title": "환경설정" } }, "link": { - "manual": "설명서 ↗", - "add_appliance": "가전제품 추가" + "maintenance_supporter": "GitHub의 Maintenance Supporter ↗", + "manual": "설명서 ↗" }, "tip": { "add_brand": "브랜드가 목록에 없나요? 커뮤니티 카탈로그에 추가하세요", @@ -2171,7 +2085,13 @@ "rebuild": { "envelopes": "엔벨로프 재구성 중" }, - "cancelling": "취소 중..." + "cancelling": "취소 중...", + "auto_label": { + "running": "사이클 자동 라벨 지정 중" + }, + "store_download": { + "running": "스토어에서 다운로드 중" + } }, "setup": { "hdr": { @@ -2180,26 +2100,24 @@ }, "phase0": { "washer": "WashData가 이미 사이클을 감지하고 있습니다. 프로그램 이름과 시간 추정을 활성화하려면 첫 번째 사이클을 기록하세요.", - "dishwasher": "WashData가 관찰 중입니다. 식기세척기 사이클은 복잡합니다 – 첫 번째 사이클을 기록하는 것이 강력히 권장됩니다. 감지된 사이클이 너무 길게 실행되면 프로필로 저장하기 전에 사이클 편집기를 사용하여 다듬으세요.", + "dishwasher": "WashData가 관찰 중입니다. 식기세척기 사이클은 복잡하므로 첫 사이클을 기록하세요. 감지된 사이클이 너무 길면 프로필로 저장하기 전에 사이클 편집기에서 다듬으세요.", "generic": "WashData가 관찰 중입니다. 프로필 구축을 시작하려면 감지된 사이클을 기록하거나 라벨을 지정하세요." }, "phase1a": { - "labelled": "좋은 시작입니다 – 첫 번째 프로그램이 저장되었습니다. 가장 정확한 데이터를 위해 녹음기 위젯으로 다음 사이클을 기록하는 것을 고려해 보세요." + "labelled": "좋은 시작입니다. 첫 번째 프로그램이 저장되었습니다. 가장 정확한 데이터를 위해 다음 사이클은 수동 기록으로 기록하세요." }, "phase1b": { - "recorded": "녹음이 {profile_name}으로 저장되었습니다. 이제 다른 자주 사용하는 프로그램을 기록하거나 라벨을 지정하여 커버리지를 구축하세요." + "recorded": "기록이 {profile_name}(으)로 저장되었습니다. 이제 자주 사용하는 다른 프로그램도 기록하거나 라벨을 지정하세요." }, "phase1c": { - "verify": "커뮤니티에서 {count}개의 프로그램이 있습니다. WashData가 올바르게 인식하는지 확인하기 위해 사이클을 실행하세요 – 기기가 자체 기록을 쌓으면서 매칭이 개선됩니다." + "verify": "커뮤니티에서 받은 프로그램이 {count}개 있습니다. 사이클을 실행해 WashData가 인식하는지 확인하세요. 기기 자체 기록이 쌓이면 매칭이 개선됩니다." }, "phase2": { - "cluster": "WashData가 저장된 프로그램과 일치하지 않는 {count}개의 사이클을 발견했습니다 – 서로 비슷해 보입니다. 이를 위한 새 프로필을 만들고 싶으신가요?", + "cluster": "WashData가 저장된 프로그램과 일치하지 않는 비슷한 사이클 {count}개를 발견했습니다. 이 사이클들로 새 프로필을 만들까요?", "unmatched": "마지막 사이클이 저장된 프로그램과 일치하지 않았습니다. 새 프로그램인가요?" }, "phase3": { - "suggestions": "WashData가 사이클 기록을 기반으로 설정 권장 사항을 제안합니다 – 감지 정확도를 높이기 위해 검토하세요.", - "groups": "일부 프로필이 다른 온도의 동일한 프로그램처럼 보입니다. 더 나은 매칭을 위해 그룹으로 구성하세요.", - "phases": "더 정확한 잔여 시간 추정을 위해 {profile_name}에 프로그램 단계를 추가하세요." + "suggestions": "WashData가 사이클 기록을 바탕으로 설정 제안을 준비했습니다. 검토하여 감지를 개선하세요." }, "phase4": { "healthy": "이 기기는 완전히 설정되었습니다 ({profile_count}개 프로필)." @@ -2212,12 +2130,58 @@ "create_from_cluster": "이 사이클들로 프로필 만들기", "create_profile": "이를 위한 프로필 만들기", "review_suggestions": "제안 검토", - "organise_profiles": "프로필 구성", - "configure_phases": "단계 구성", "skip_step": "이 단계 건너뛰기", "skip_forever": "다시 표시 안 함", "hide_guidance": "안내 숨기기", "show_guidance": "안내 표시" } + }, + "access": { + "edit": "편집", + "full": "전체", + "inherit": "상속", + "none": "없음 (숨김)", + "read": "읽기" + }, + "pg_label_reason": { + "no_winner": "맞는 프로그램 없음", + "below_floor": "학습 신뢰도 미만", + "ambiguous": "모호함으로 표시됨", + "margin": "차순위와의 차이가 너무 작음", + "unknown_profile": "프로그램이 더 이상 존재하지 않음", + "not_evaluated": "평가되지 않음" + }, + "diagram": { + "accum": "누적", + "auto": "자동", + "below_off": "미만 = 꺼짐", + "confirmed": "확정", + "energy_counts": "에너지로 판단", + "feedback": "피드백", + "gap_below_min": "최소 미만 간격 = 한 사이클", + "heat_phase": "가열 단계", + "held_at_100": "100% 유지", + "ignored": "무시됨", + "kept": "유지됨", + "max": "최대", + "min": "최소", + "minus_tol": "-허용", + "no_match": "매칭 없음", + "no_updates": "업데이트 없음", + "off_delay_wait": "오프 지연 대기", + "off_gap": "오프 간격", + "plus_tol": "+허용", + "profile": "프로필", + "reading_interval": "일반적인 측정 간격", + "sensor_offline": "센서 오프라인: 강제 정지", + "si": "간격", + "spike_ignored": "스파이크 무시됨", + "spike_ignored_short": "스파이크: 무시", + "start": "시작", + "stop": "정지", + "tail_energy_resets": "후반 에너지가 임계값 초과: 타이머 재설정", + "thr": "임계값", + "too_short": "너무 짧음", + "tumble_kept": "텀블 펄스 유지됨" } } diff --git a/custom_components/ha_washdata/translations/panel/lt.json b/custom_components/ha_washdata/translations/panel/lt.json index 60a33f8a..83c77f65 100644 --- a/custom_components/ha_washdata/translations/panel/lt.json +++ b/custom_components/ha_washdata/translations/panel/lt.json @@ -3,6 +3,7 @@ "min": "min", "entity_not_found": "Objektas nerastas", "running_long": "Veikia ilgai", + "stalled": "Užstrigo", "remaining": "liko", "progress": "eiga", "show_time_remaining": "Rodyti likusį laiką", @@ -48,16 +49,18 @@ } }, "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_tip": "Pabaigos link ši programa maždaug {mins} min nutyla, tada {secs} s naudoja apie {watts} W. Pastebėta {seen} iš {measured} išmatuotų ciklų.", "quiet_tail": "tyli pabaiga ~{mins} min", - "artifact_tip": "Aptikta {n} anomalijų (pvz., durys atidarytos ciklo viduryje) – atidarykite, kad pamatytumėte juos diagramoje", + "artifact_tip": "Aptikta {n} anomalijų (pvz., durys atidarytos ciklo viduryje) - atidarykite, kad pamatytumėte jas diagramoje", + "artifact_tip_one": "Aptikta {n} anomalija (pvz., durys atidarytos ciklo viduryje) - atidarykite, kad pamatytumėte ją diagramoje", + "artifact_tip_few": "Aptiktos {n} anomalijos (pvz., durys atidarytos ciklo viduryje) - atidarykite, kad pamatytumėte jas diagramoje", + "artifact_tip_many": "Aptikta {n} anomalijos (pvz., durys atidarytos ciklo viduryje) - atidarykite, kad pamatytumėte jas diagramoje", + "artifact_tip_other": "Aptikta {n} anomalijų (pvz., durys atidarytos ciklo viduryje) - atidarykite, kad pamatytumėte jas diagramoje", "auto": "(aptikta automatiškai)", "built_in_tag": "įmontuotas", "declining": "↘ mažėja", "energy_low": "Mažesnė energija nei įprastai", "energy_spike": "Didesnė energija nei įprastai", - "fair_fit": "Priimtina", - "fair_fit_tip": "Vidutinis atitikties nuoseklumas – kai kurie šiam profiliui priskirti ciklai turi žemesnius patikimumo balus. Pažymėkite daugiau ciklų arba iš naujo įrašykite profilį, kad pagerintumėte tikslumą.", "feedback_requested": "Prašoma atsiliepimo", "golden_cycle": "Užregistruotas atskaitos ciklas", "duration_outlier": "{n} neįprastos trukmės ciklų", @@ -66,27 +69,40 @@ "needs_review": "Reikia peržiūros", "overrun": "Bėgo ilgiau nei įprastai", "poor_fit": "prastai tinka", - "poor_fit_tip": "Nenuosekli atitikčių istorija – apsvarstykite galimybę atkurti šį profilį", - "restart_gap_tip": "{n} HA paleidimo iš naujo spraga: galios kreivė turi skylę", + "restart_gap_tip": "{n} HA paleidimo iš naujo spragų: galios kreivė turi skylių", + "restart_gap_tip_one": "{n} HA paleidimo iš naujo spraga: galios kreivė turi skylę", + "restart_gap_tip_few": "{n} HA paleidimo iš naujo spragos: galios kreivė turi skylių", + "restart_gap_tip_many": "{n} HA paleidimo iš naujo spragos: galios kreivė turi skylių", + "restart_gap_tip_other": "{n} HA paleidimo iš naujo spragų: galios kreivė turi skylių", "reviewed": "Peržiūrėta", "steady": "→ pastovus", "underrun": "Baigėsi greičiau nei įprastai", - "using_defaults": "Naudojami pristatyti numatytieji nustatymai", - "using_tuned": "Naudojant suderintus svorius", "overrun_ratio": "({x}x tikėtino)", "underrun_ratio": "({pct}% tikėtino)", - "awaiting_tip": "Laukiama bendruomenės patvirtinimo", - "awaiting_n": "Laukiama patvirtinimo · {n} patvirtinimai", - "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ą.", - "armed": "(taikoma kitam ciklui)" + "armed": "(taikoma kitam ciklui)", + "awaiting_tip": "Laukiama bendruomenės patvirtinimo", + "awaiting_n": "Laukiama patvirtinimo · {n} patvirtinimai", + "awaiting": "Laukiama patvirtinimo", + "approved": "Patvirtinta", + "matcher_unused": "Dar neatitiko nė vieno jūsų ciklo", + "matcher_used": "Atitiko {n} jūsų ciklų", + "matcher_used_one": "Atitiko {n} jūsų ciklą", + "matcher_used_few": "Atitiko {n} jūsų ciklus", + "matcher_used_many": "Atitiko {n} jūsų ciklo", + "matcher_used_other": "Atitiko {n} jūsų ciklų", + "matcher_used_tip": "Šio prietaiso ciklai, kuriuos WashData savarankiškai atpažino kaip šią importuotą programą. Jei importuota programa niekada neatpažįstama, ji gali netikti jūsų prietaisui; galite ją pašalinti. Skaičiuojama vietoje, į parduotuvę nieko nesiunčiama." }, "btn": { + "pg_preset_delete": "Ištrinti rinkinį", + "pg_preset_load": "Įkelti rinkinį", + "pg_preset_load_tip": "Pakeisti bandymų aikštelės reikšmes šiuo rinkiniu (aktyvūs nustatymai nekeičiami)", + "pg_preset_save": "Išsaugoti kaip rinkinį", + "pg_preset_save_tip": "Išsaugoti visas toliau esančias reikšmes kaip pavadintą šio įrenginio nustatymų rinkinį", "reset_zoom": "Atstatyti mastelį", "set_brand_model": "Nustatyti prekės ženklą ir modelį", "refresh_catalog": "Atnaujinti katalogą", @@ -97,12 +113,11 @@ "add_timer": "+ Pridėti laikmatį", "apply_all": "Taikyti viską", "apply_label": "Taikyti etiketę", - "apply_set_b": "Taikyti rinkinį B", "apply_split": "Taikyti padalijimą", "apply_trim": "Taikyti apkarpymą", "auto_detect_split": "Automatinis aptikimas", "auto_label_cycles": "Automatinio ženklinimo ciklai", - "auto_label_cycles_tip": "Automatiškai priskirti profilių pavadinimus nepažymėtiems ciklams, kurių atitikties patikimumas viršija slenkstį", + "auto_label_cycles_tip": "Priskirti programą nepažymėtiems ciklams, kurių atitikties patikimumas viršija slenkstį.", "cancel": "Atšaukti", "clear_debug": "Išvalyti derinimo duomenis", "clear_splits": "Išvalyti", @@ -112,8 +127,6 @@ "convert_to_automation": "Konvertuoti į automatizavimą", "correct": "Taisyti…", "create": "Sukurti", - "create_from_cluster": "Sukurti profilį iš {count} ciklų", - "create_profile": "+ Sukurti profilį", "delete": "Ištrinti", "delete_group": "Ištrinti grupę", "delete_group_tip": "Ištrinti tik šią grupę – narių profiliai išsaugomi", @@ -124,22 +137,19 @@ "done": "Atlikta", "edit": "Redaguoti", "export": "Eksportuoti", - "export_json": "Eksportuoti į JSON", "force_stop": "Priverstinis sustabdymas", "force_stop_tip": "Nedelsdami nutraukti dabartinį ciklą ir pažymėti jį kaip priverstinai sustabdytą", "from_template": "Iš šablono ▾", "ignore": "Ignoruoti", "import_json": "Importuoti iš JSON", - "import_overwrite": "Importuoti (perrašo duomenis)", "inspect": "Apžiūrėti", "label": "Pažymėti", - "load": "Įkelti", "load_more": "Įkelti daugiau", "manage": "Tvarkyti", "merge": "Sujungti", "new_automation": "+ Nauja automatika", "new_group": "+ Nauja grupė", - "new_group_tip": "Sugrupuoti beveik identiškus profilius (ta pati forma/trukmė, skirtinga temperatūra arba sukimas), kad atitikmenų ieškiklis patikimai pasirinktų tarp jų", + "new_group_tip": "Sugrupuoti beveik vienodus profilius (ta pati forma, kita temperatūra ar gręžimas), kad atpažinimas juos atskirtų.", "new_phase": "+ Naujas etapas", "new_profile": "+ Naujas profilis", "new_profile_tip": "Sukurti naują programos profilį iš esamo pažymėto ciklo arba įrašo", @@ -147,36 +157,25 @@ "on_cycle_started": "Prasidėjus ciklui", "pause_cycle": "Pauzė", "pause_cycle_tip": "Pristabdyti veikiantį ciklą – prietaisas tęsis nuo tos vietos, kur sustojo", - "pg_apply_device": "Taikyti įrenginiui", - "pg_autofill": "Automatinis užpildymas", - "pg_load_calibrated": "Įkelti kalibruotus (ML) ({n})", - "pg_load_calibrated_tip": "Įkelti ML kalibruotus nustatymus kaip bandymų aikštelės perrašymus", "pg_load_live": "Įkelti aktyvius nustatymus", "pg_load_live_tip": "Iš naujo nuskaityti dabartinius integracijos nustatymus ir atmesti visus bandymų aikštelės pakeitimus", "pg_load_suggested": "Įkelti siūlomus ({n})", "pg_load_suggested_tip": "Įkelti dabartines automatinio derintuvo rekomendacijas kaip bandymų aikštelės perrašymus", - "pg_preset_delete": "Ištrinti rinkinį", - "pg_preset_load": "Įkelti rinkinį", - "pg_preset_load_tip": "Pakeisti bandymų aikštelės reikšmes šiuo rinkiniu (aktyvūs nustatymai nekeičiami)", - "pg_preset_save": "Išsaugoti kaip rinkinį", - "pg_preset_save_tip": "Išsaugoti visas toliau esančias reikšmes kaip pavadintą šio įrenginio nustatymų rinkinį", "pg_publish_all": "Įrašyti į nustatymus ({n})", "pg_publish_one": "Įrašyti „{label}“ į integracijos nustatymus", - "play": "Leisti", "process": "Apdoroti", "process_history": "Apdoroti dabar", "process_recording": "Išsaugoti įrašą", "process_tip": "Išsaugoti įrašytą seką kaip naują arba esamą profilį", "rebuild": "Atstatyti vokus", "rebuild_envelope": "Atstatyti voką", - "rebuild_tip": "Perskaičiuoti laukiamą galios voką (min/maks juosta) visiems profiliams iš jų pažymėtų ciklų – paleiskite pažymėję naujus ciklus arba pataisę senus", + "rebuild_tip": "Iš naujo sudaryti kiekvieno profilio laukiamą galios juostą pagal jo pažymėtus ciklus. Paleiskite pažymėję ar pataisę ciklus.", "rec_start_tip": "Pradėti įrašyti prietaiso galios seką – pradėkite prieš pat paleidžiant ciklą", "rec_stop": "Stabdyti", "rec_stop_tip": "Sustabdyti įrašymą ir laikyti užfiksuotą seką peržiūrai", "record": "Pradėti įrašymą", "refresh": "Atnaujinti", "refresh_diag_tip": "Atnaujinti saugyklos statistiką", - "refresh_logs_tip": "Atnaujinti naujausius žurnalo įrašus", "refresh_settings_tip": "Iš naujo įkelti nustatymus iš serverio", "relabel": "Pažymėti iš naujo ({count})", "remove": "Pašalinti", @@ -184,7 +183,6 @@ "reset": "Nustatyti iš naujo", "reset_ml_models_tip": "Išmeskite sureguliuotus modelius ir grįžkite prie įmontuotų. „WashData“ vėliau gali juos išmokti iš naujo.", "reset_to_builtin": "Iš naujo nustatyti įtaisytuosius modelius", - "reset_to_defaults": "Atstatyti į numatytuosius nustatymus", "resume_cycle": "Tęsti ciklą", "resume_cycle_tip": "Atnaujinti pristabdytą ciklą", "revert_settings": "Atmesti pakeitimus", @@ -197,8 +195,6 @@ "run_analysis": "🔍 Vykdykite pasiūlymų analizę", "run_analysis_tip": "Analizuokite įrašytus ciklus dabar ir atnaujinkite derinimo pasiūlymus", "run_auto_label": "Paleiskite „Auto-Label“.", - "run_both": "Vykdyti A prieš B", - "run_simulation": "Vykdyti simuliaciją", "save": "Išsaugoti", "save_access_control": "Išsaugoti prieigos valdymą", "save_group": "Išsaugoti grupę", @@ -210,10 +206,8 @@ "save_settings": "Išsaugoti nustatymus", "select": "Pasirinkite", "select_all": "Žymėti viską", - "select_last20": "Paskutiniai 20", "select_none": "Išvalyti", "show_only": "Rodyti tik", - "skip_setup": "Praleisti sąranką", "split": "Padalinti", "stop": "Stabdyti", "submit_correction": "Pateikite pataisymą", @@ -222,36 +216,31 @@ "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į.", + "unlabel_selected_tip": "Pašalina žymėjimą iš pasirinktų ciklų. Jie lieka istorijoje, bet nebeformuoja šio profilio.", "use": "Naudokite", "wipe_all": "Išvalykite visus duomenis", "delete_selected": "Ištrinti pasirinktus ({n})", - "group_suggest": "Grupė {n}: {members}", "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", "import": "Importuoti", "disconnect": "Atjungti", "connect_github": "Prijungti GitHub", - "save_appliance": "Išsaugoti prietaisą", "share_to_store": "Bendrinti parduotuvėje", "share": "Bendrinti", - "confirm_appliance": "Patvirtinti šį prietaisą", - "confirmed": "Patvirtinta", "browse_community_setups": "Naršyti bendruomenės konfigūracijas", "download_device": "Atsisiųsti šią konfigūraciją", "share_device": "⬆ Dalintis šiuo įrenginiu", - "share_device_tip": "Bendrinkite šį prietaisą ir jo įrašytus etaloniniais ciklus bendruomenės parduotuvėje, kad kiti, turintys tą patį aparatą, galėtų juos perimti", + "share_device_tip": "Bendrinkite šį prietaisą ir jo etaloninius ciklus bendruomenės parduotuvėje, kad to paties aparato savininkai galėtų juos perimti.", "share_n": "Bendrinti {n} ciklą(-us)", "export_selected": "Eksportuoti (pasirinkti duomenis)", "export_all": "Greitasis eksportas: viskas", - "import_raw": "Išplėstinis: pakeisti viską iš JSON", "download_export": "Atsisiųsti eksportą", "analyze_import": "Analizuoti failą", "import_selected": "Importuoti pasirinktus", "back": "Atgal", "mute_suggestion": "Nebesiūlyti šio nustatymo", "reset_muted": "Atstatyti nutildytus", + "reset_phase_tip": "Atšaukti pakeitimą ir atkurti įmontuotą fazę", "import_power_history": "Importuoti galios istoriją", "hist_read_recorder": "Nuskaityti iš Home Assistant", "hist_scan": "Ieškoti ciklų", @@ -260,7 +249,15 @@ "save_total_cycles": "Išsaugoti bendrą skaičių", "clear": "Išvalyti", "add_brand": "Pridėti prekės ženklą", - "add_appliance": "Pridėti prietaisą" + "log_done_task": "Pažymėti kaip atliktą: {task}", + "add_task": "Pridėti užduotį", + "add_appliance": "Pridėti prietaisą", + "retry": "Bandyti dar kartą", + "confirm_appliance": "Patvirtinti šį prietaisą", + "confirmed": "Patvirtinta", + "create_profile_from_gap": "Sukurti profilį", + "undo_import": "Anuliuoti paskutinį importą", + "pg_rerun": "Atnaujinti simuliaciją" }, "note": { "off_delay": { @@ -268,10 +265,6 @@ } }, "conflict": { - "anti_wrinkle_exit": { - "exit": "Turi būti žemiau sustabdymo slenksčio ({stop} W); priešingu atveju apsaugos nuo raukšlių išėjimo galia ignoruojama", - "stop": "Turi būti aukščiau apsaugos nuo raukšlių išėjimo galios ({exit} W)" - }, "anti_wrinkle_max": { "max": "Turi būti aukščiau paleidimo slenksčio ({start} W); priešingu atveju apsaugos nuo raukšlių trukmės apribojimas apeinamas", "start": "Turi būti žemiau maksimalios apsaugos nuo raukšlių galios ({max} W)" @@ -279,10 +272,10 @@ "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į." + "power_sensor": "Tai įrenginio Galios jutiklis, o ne kaina. Ciklai būtų įkainojami vatais už kWh; išvalykite šį lauką.", + "energy_sensor": "Tai įrenginio Energijos skaitiklis, o ne kaina. Ciklai būtų įkainojami pagal skaitiklio rodmenį už kWh; išvalykite šį lauką.", + "device_class": "Šis jutiklis matuoja {dc}, o ne kainą už kWh. Išvalykite šį lauką arba pasirinkite tarifo jutiklį.", + "unit": "Šio jutiklio matavimo vienetas yra {unit}, o ne kaina už kWh. Išvalykite šį lauką arba pasirinkite tarifo jutiklį." }, "settings_banner": "Nustatymų konfliktai: {n}. Patikrinkite pažymėtas sekcijas ir ištaisykite jas prieš išsaugant.", "settings_banner_btn": "Eiti į pirmą", @@ -317,8 +310,8 @@ "threshold": "Turi būti žemiau sustabdymo slenksčio ({stop} W), kad įsigaliotų" }, "pump_stuck": { - "duration": "Turi būti mažiau nei laikotarpis be atnaujinimų ({to} s), kad įstrigimo aliarmas suveiktų prieš tai, kai stebėtojas nutrauks ciklą", - "timeout": "Turi viršyti siurblio įstrigimo trukmę ({ps} s), kad įstrigimo aliarmas suveiktų prieš priverstinį ciklo sustabdymą" + "duration": "Turi būti mažesnė už „Be atnaujinimų skirtasis laikas“ ({to} s), kad įstrigimo aliarmas suveiktų pirmas", + "timeout": "Turi būti didesnis už „Siurblio įstrigimo trukmė“ ({ps} s), kad įstrigimo aliarmas suveiktų pirmas" }, "start_dur": { "sampling": "Mėginių ėmimo intervalas viršija paleidimo trukmę ({sdt} s); vieno mėginio smaigaliai gali atidaryti ciklą", @@ -334,10 +327,15 @@ "watchdog": { "interval": "Turi būti bent 2 kartus didesnis nei mėginių ėmimo intervalas ({si} s)", "sampling": "Mėginių ėmimo intervalas turi būti ne daugiau kaip pusė stebėjimo intervalo ({wi} s)" + }, + "end_energy": { + "energy": "Per griežta, kai Sustabdymo slenkstis yra {w} W, o Išjungimo delsimas {d} s; ciklas gali baigtis tik atsarginiu būdu", + "stop": "Pabaigos energija ({e} Wh per {d} s) leidžia tik {p} W" } }, "hdr": { - "ab_comparison": "A/B nustatymų palyginimas", + "custom_tasks": "Jūsų užduotys", + "maintenance_supporter": "Reikia daugiau?", "access_control": "Prieigos kontrolė", "add_maintenance": "Pridėti priežiūros įvykį", "automation_delete": "Ištrinkite šią automatiką", @@ -346,28 +344,22 @@ "automation_review_this_cycle": "Peržiūrėkite šį ciklą", "automations": "Automatikos", "clear_debug": "Išvalyti derinimo pėdsakus", - "cycle_simulator": "Ciklų simuliatorius", "diagnostics_pref": "Diagnostika", "display": "Ekranas", - "dtw_inspector": "DTW inspektorius", "export_import": "Eksportas / Importas", - "getting_started": "Pradžia", "live_power": "Gyva galia", "logs": "Žurnalai", - "logs_diagnostics": "Diagnostika", "maintenance": "Priežiūros veiksmai", "maintenance_log": "Priežiūros žurnalas", "maintenance_reminders": "Aptarnavimo priminimai", "manual_recording": "Rankinis įrašymas", - "manual_recording_tip": "Paleiskite ciklą tyčia, kol WashData įrašo galios seką. Pradėkite prieš pat prietaiso paleidimą, Sustokite, kai jis baigiasi, tada apdorokite, kad išsaugotumėte kaip pavadintą profilį.", + "manual_recording_tip": "Įrašykite ciklą tyčia: paspauskite „Pradžia“ prieš pat prietaiso paleidimą, „Stabdyti“, kai jis baigia, tada „Apdoroti“, kad išsaugotumėte kaip profilį.", "ml_learned": "Ką WashData išmoko", - "ml_matching_tuning": "Tikslus programos derinimas", "ml_settings_card": "Nustatymai", "ml_smart_learning": "Išmanusis mokymasis", "my_preferences": "Mano nuostatos", "no_automations": "Dar jokia automatika nenurodo šio įrenginio.", "panel_settings": "Skydelio nustatymai", - "param_sweep": "Parametrų skenavimas", "pg_settings_source": "Nustatymų šaltinis", "phase_catalog": "Fazių katalogas", "playground": "Bandymų aikštelė", @@ -376,11 +368,8 @@ "status": "Būsena", "status_graph": "Būsenos grafikas", "storage_stats": "Saugyklos statistika", - "test_history": "Testavimas su istorija", "toggle_sidebar": "Perjungti „Home Assistant“ šoninę juostą", - "tools_and_data": "Įrankiai ir duomenys", "wipe_history": "Išvalyti istoriją", - "recommendations": "Rekomendacijos ({n})", "pg_detection_params": "Aptikimo nustatymai", "pg_outcome": "Simuliacijos rezultatas", "pg_across_cycles": "Per visus jūsų ciklus", @@ -388,7 +377,8 @@ "online_account": "Bendruomenės parduotuvė ir interneto funkcijos", "import_power_history": "Galios istorijos importavimas", "service_status": "Aptarnavimo būsena", - "online_account_tab": "Internetas ir bendruomenė" + "online_account_tab": "Internetas ir bendruomenė", + "live_match_debug": "Tiesioginės atitikties derinimas" }, "health": { "fair": "Priimtina profilio kokybė", @@ -396,12 +386,17 @@ "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_tip": "Padėtis atpažintos programos įrašytoje kreivėje: {pct}%. Nustatoma lyginant ciklą su profiliu, todėl lieka teisinga, kai ciklas ilgesnis ar trumpesnis. Atnaujinama, kai prietaisas tyli, todėl 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ų", + "task_name": "Užduotis", + "every_cycles": "Intervalas (ciklai)", + "every_days": "Intervalas (dienos)", + "new_task_name": "Nauja užduotis", + "days_since_service": "{since} / {total} dienų", "actions": "Veiksmai", "activity": "Veikla", "administrators": "Administratoriai", @@ -409,10 +404,8 @@ "aria_cycle_chart": "Ciklo galios kreivė", "aria_envelope_chart": "Profilio galios voko grafikas", "aria_phase_chart": "Fazių redaktoriaus grafikas", - "aria_playground_chart": "Ciklo galios kreivė su velkamais aptikimo slenksčiais", "aria_power_chart": "Galios suvartojimo grafikas", "aria_spaghetti_chart": "Perdengtos ciklų galios kreivės", - "aria_sweep_chart": "Parametrų skenavimo rezultatų grafikas", "artifact_anomaly": "Anomalija", "artifact_high_power": "Didelė galia", "artifact_interruption": "Pertraukimas", @@ -426,149 +419,101 @@ "cohesion_good": "sanglauda {pct}%", "cohesion_low": "⚠ maža sanglauda {pct}%", "compare_profiles": "Palyginkite su profiliais", - "concurrency": "Lygiagretumas", "confidence": "Pasitikėjimas", "confidence_threshold": "Pasitikėjimo slenkstis", "correct_duration": "Tinkama trukmė (min., neprivaloma)", "correct_profile": "Teisingas profilis", - "correlation": "Koreliacija", "cost": "Kaina", "create_new_profile": "+ Sukurti naują profilį…", "current_phase": "Dabartinė fazė", - "curves": "Kreivės", "cycle": "Ciklas", "cycle_date_display": "Ciklo datos rodymas", - "cycle_health": "Ciklo sveikata", - "cycle_index": "ciklo numeris →", - "cycles": "Ciklai", "cycles_count": "Ciklai", "cycles_lc": "ciklai", - "dashed": "brūkšninė", "date": "Data", "debug_traces": "Derinimo pėdsakai", - "default": "Numatytoji", "default_access_level": "Numatytasis naudotojų lygis, nenurodytas toliau", "default_other": "Numatytasis (kiti įrenginiai)", "default_tab": "Numatytasis skirtukas atidarant skydelį", - "delta": "Δ", "description": "Aprašymas", - "dtw_blend_w": "Maišymo svoris", - "dtw_blended": "Sujungtas balas", - "dtw_ddtw": "DDTW balas", - "dtw_l1": "L1 balas", - "dtw_matched_profile": "(ciklo suderintas profilis)", "duration": "Trukmė", - "duration_agreement": "Trukmės atitikimas", - "emphasis": "Pabrėžimas", "enable_access_control": "Įgalinti kiekvieno vartotojo prieigos valdymą", "energy": "Energija", - "energy_agreement": "Energijos atitikimas", - "event_timeline": "Įvykių laiko juosta", "event_type": "Įvykio tipas", "expected": "Numatoma", + "suspected_profile": "{name} (spėjama)", + "runner_up_profile": "{name} (antrasis kandidatas)", "expected_duration": "Numatoma trukmė (min.)", "file_kb": "Failas (kB)", - "final_score": "Galutinis balas", "from": "Nuo", "gap_s": "Tarpas (-iai)", "group_name": "Grupės pavadinimas", "head_trim": "Apkarpymas nuo pradžios (s)", - "health": "Sveikata", "hide_tabs": "Slėpti skirtukus ne administratoriams", - "in_use": "Naudojamas", "json_data": "JSON duomenys", "label": "Atitikta programa", "last": "Paskutiniai", "last_checked": "Paskutinį kartą tikrinta:", "load_from_file": "Įkelti iš failo", - "mae_score": "MAE balas", "manual_duration": "Rankinio darbo trukmė (min., neprivaloma)", "match": "Atitiktis", "members": "Nariai", - "metric": "Metrika", "mode_existing_profile": "Pridėti prie esamo profilio", "mode_new_profile": "Sukurti naują profilį", "models_fine_tuned": "(tiksliai suderinti modeliai: {count})", - "n_classic_suggestions": "{n} klasikinis", - "n_ml_suggestions": "{n} ML", "n_selected": "Pasirinkta: {n}", - "n_tuning_suggestions": "{n} derinimo pasiūlymas", - "na": "n/d", + "n_tuning_suggestions": "{n} derinimo pasiūlymų", + "n_tuning_suggestions_one": "{n} derinimo pasiūlymas", + "n_tuning_suggestions_few": "{n} derinimo pasiūlymai", + "n_tuning_suggestions_many": "{n} derinimo pasiūlymo", + "n_tuning_suggestions_other": "{n} derinimo pasiūlymų", "new_profile_name": "Naujo profilio pavadinimas", "notes": "Pastabos", - "of": "iš", - "outcome": "Rezultatas", "panel_default_tab": "Numatytasis skirtukas", "panel_language": "Skydelio kalba", "per_cycle_short": "ciklas", - "pg_autofill_tip": "Automatiškai užpildyti intervalą pagal dabartinę reikšmę ({lo}–{hi})", "pg_best_value": "Rasta geriausia reikšmė", "pg_current_value": "Dabartinė reikšmė:", "pg_drag_hint": "Vilkite liniją grafike", - "pg_dtw": "DTW", "pg_ev_ending": "Baigiama", "pg_ev_running": "Vykdoma", "pg_group_advanced": "Kraštutiniai atvejai", "pg_group_detection": "Aptikimo trigeriai", "pg_group_timing": "Laiko taisyklės", "pg_idle": "Tuščia eiga", - "pg_last_run": "Paskutinis paleidimas: {date}", - "pg_match_score": "Atitikties balas", - "pg_pct_of": "{p}% iš {total}", "pg_poor_match": "Prasta atitiktis", "pg_preset": "Bandymų aikštelės rinkinys", "pg_preset_name": "Naujo rinkinio pavadinimas", "pg_preset_none": "Pasirinkite rinkinį…", "pg_strong_match": "Stipri atitiktis", - "pg_tile_ambiguous": "Dviprasmiška", - "pg_tile_ambiguous_tip": "Ciklai, kuriuose dvi programos beveik lygios", - "pg_tile_detected": "Aptikta", - "pg_tile_detected_tip": "Ciklai, kai galia pakankamai ilgai viršijo paleidimo slenkstį, kad būtų užregistruota kaip veikianti", - "pg_tile_matched": "Atitikta", - "pg_tile_matched_tip": "Ciklai, teisingai priskirti žinomai programai", - "pg_time_left": "Likęs laikas", "pg_weak_match": "Silpna atitiktis", "phase_name": "Fazės pavadinimas", "name": "Vardas", "phase_timeline": "Fazių laiko juosta", "power": "Galia", "profile": "Profilis", - "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. Pasirinkite programą prieš paleisdami prietaisą ir ji bus pritaikyta vos prasidėjus kitam ciklui.", + "program_tip": "Pasirinkite, kuriai programai priskiriamas dabartinis ciklas. Automatinis aptikimas parenka geriausią atitiktį. Prisekite programą, kai automatinis aptikimas klysta; pasirinkta prieš paleidimą, ji taikoma kitam ciklui.", "progress": "Pažanga", "raw_socket": "Neapdorotas lizdas", - "read_only": "Tik skaityti", "ref_cycle": "Etaloninis ciklas (neprivaloma)", - "refresh_interval": "Atnaujinimo intervalas (-ai)", "remaining": "Likę", "reminder_every": "Priminti kas (ciklų)", "remove_label": "- Pašalinti etiketę -", "rename_profile": "Pervardyti profilį", - "replay_duration": "Pakartojimas", "resulting_profile": "Gautas profilis", "save_mode": "Išsaugoti režimą", - "score": "Balas", - "score_breakdown": "Balo išklotinė", "seconds_from_start": "Sekundės nuo pradžios", "select_profile": "Pasirinkite Profilis", "setting": "Nustatymas", - "settings_a": "Nustatymai A (dabartiniai)", - "settings_a_short": "A", "settings_advanced": "Išplėstinė", - "settings_b": "Nustatymai B (bandymas)", - "settings_b_short": "B", "settings_basic": "Pagrindiniai", "settings_detail_level": "Nustatymų detalumo lygis", - "settings_overrides": "Nustatymų nepaisymai", "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", - "stage2": "2 etapas – pagrindinis panašumas", - "stage3": "3 etapas – DTW", - "stage4": "4 etapas – atitikimas", "status": "Būsena", "tail_trim": "Apkarpymas nuo galo (s)", "tap_to_dismiss": "palieskite grafiką, kad uždarytumėte", @@ -579,7 +524,6 @@ "ungrouped": "Nesugrupuota", "unlabelled": "Nepaženklinta", "unlabelled_paren": "(be etiketės)", - "warp_path": "Iškraipymo kelias", "input": "Įvestis:", "start": "Pradžia", "end": "Pabaiga", @@ -604,9 +548,8 @@ "n_shown": ", rodoma {n}", "cycles_title": "Ciklai ({n})", "pg_detecting": "Aptinkama", - "pg_best_marker": "Geriausia: {v}", "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", + "pg_time_left_model_tip": "Įvertinta pagal programos fazes, ne fiksuotas atgalinis skaičiavimas", "phase": "Fazė", "pg_mode_history": "Bandyti su istorija", "pg_ev_detected": "Aptikta", @@ -638,12 +581,6 @@ "pg_sweep_param": "Parametras", "pg_objective": "Tikslas", "pg_steps": "Žingsniai", - "pg_stress_group": "Budėjimo nutraukimo testas", - "pg_stress_idle_auto": "Auto", - "pg_stress_idle_w": "Budėjimo lygis (W)", - "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_obj_match": "Atitikties tikslumas", "pg_obj_endtiming": "Pabaigos laiko tikslumas", "pg_obj_falseend": "Klaidingų pabaigų dažnis", @@ -652,12 +589,8 @@ "pg_mode_optimize": "Optimizuoti", "pg_group_matching": "Programų atitiktis", "pg_zoom_hint": "dukart spustelėkite, kad atstatytumėte mastelį", - "aria_playground_chart2": "Interaktyvus ciklo galios grafikas: užveskite žymeklį, kad nuskaitytumėte laiką/galią, slinkite, kad priartintumėte, vilkite, kad pastumtumėte", "pg_alert_timeout_end": "Baigta pasibaigus skirtajam laikui, o ne pagal prognozę", "pg_alert_indefinite": "Veiktų neribotą laiką", - "pg_alert_stress_cap": "Pasiektas apsauginis limitas", - "pg_alert_stress_ok": "Budėjimo nutraukimas: ciklas sustabdytas", - "pg_alert_stress_warn": "Budėjimo suvartojimas viršija sustabdymo slenkstį", "task_pg_history": "Testuoti su istorija", "task_pg_sweep": "Optimizuoti", "task_reprocess": "Pakartotinis apdorojimas", @@ -667,16 +600,9 @@ "pg_recent_runs": "Naujausi vykdymai", "pg_load_run": "Iš naujo įkelti šį vykdymą", "pg_match_confidence": "Atitikties patikimumas", - "pg_envelope_fit": "Atitikimas gaubtinei", - "pg_envelope_fit_tip": "Kaip tiksliai ciklas telpa šio profilio išsaugotoje galios gaubtinėje, kitoks požiūris nei atitikties patikimumas.", "n_imported_note": ", {n} importuota", "enable_online": "Įjungti interneto funkcijas", - "store_brand": "Prietaiso prekės ženklas", - "store_model": "Prietaiso modelis", "anonymous": "Anonimas", - "rate_quality": "Įvertinti kokybę", - "rate_n": "{n} žvaigždutės", - "approved_only": "Tik patvirtinti", "adopt_settings": "Taip pat perimti nustatymus", "flags": "Žymės", "include_phase_map": "Įtraukti fazių žemėlapį", @@ -693,7 +619,6 @@ "cat_custom_phases": "Pasirinktinės fazės", "cat_profile_groups": "Profilių grupės", "cat_settings": "Aptikimo ir atpažinimo nustatymai", - "cat_matching_config": "Atpažinimo derinimas", "cat_ml_models": "ML modeliai", "cat_feedback": "Atsiliepimų ir peržiūros žymos", "cat_suggestions": "Pasiūlymai", @@ -715,7 +640,6 @@ "hist_csv_data": "CSV duomenys", "hist_from_recorder": "Arba nuskaitykite iš Home Assistant", "hist_since": "Nuo", - "days": "d.", "hist_keep": "Palikti šį ciklą", "hist_looks_complete": "užbaigtas", "peak_power_short": "Pikas", @@ -735,7 +659,44 @@ "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ų" + "correct_total_cycles": "Pataisyti bendrą skaičių", + "hist_reason_already_recorded": "persidengia su ciklu, kurį WashData jau įrašė", + "share_consent": "Patvirtinu, kad šie ciklai normaliai baigėsi be pertraukimų", + "show_internal": "Rodyti vidinius nustatymus", + "show_internal_tip": "Nustatymai, kuriuos WashData derina už jus (mėginių ėmimas, stebėjimas, atitikimo dažnis, slenksčiai). Rodomi, kai šis įrenginys turi savo reikšmę, konfliktą ar pasiūlymą.", + "rate_n": "{n} žvaigždutės", + "rate_quality": "Įvertinti kokybę", + "pg_autolabel": "Automatinis žymėjimas", + "pg_label_review": "Prašo peržiūros", + "pg_ev_match_reverted": "Atitiktis atšaukta", + "pg_ev_verified_pause": "Patvirtinta pauzė", + "n_samples": "{n} mėginių", + "n_samples_one": "{n} mėginys", + "n_samples_few": "{n} mėginiai", + "n_samples_many": "{n} mėginio", + "n_samples_other": "{n} mėginių", + "n_cycles": "{n} ciklų", + "n_cycles_one": "{n} ciklas", + "n_cycles_few": "{n} ciklai", + "n_cycles_many": "{n} ciklo", + "n_cycles_other": "{n} ciklų", + "energy_per_cycle": "{v}/ciklą", + "energy_total": "iš viso {v}", + "automation_off": "(išjungta)", + "ref_cycle_none": "Nėra", + "share_name": "Rodyti mano vardą prie bendrinamo turinio", + "cat_backfill_cycles": "Ciklai, rasti importuotoje galios istorijoje", + "pg_compare_profile": "Palyginti su profiliu", + "pg_obj_early": "Ankstyvų pabaigų dalis", + "pg_obj_endlag": "Pabaigos vėlavimas (mediana)", + "pg_obj_split": "Padalinimų dalis", + "pg_sweep_cycles": "Naujausi ciklai", + "cycle_context": "Recorder istorija prieš ciklą ir po jo", + "cycle_context_off": "Išjungta", + "cycle_context_minutes": "{min} min", + "cycle_context_series": "Recorder istorija", + "before_start": "Prieš pradžią", + "after_end": "Po pabaigos" }, "log": { "all_levels": "Visi lygiai", @@ -745,11 +706,9 @@ }, "ml": { "builtin_models": "● Naudojant įmontuotus modelius", - "fit_fair": "Priimtina", "fit_good": "Gerai", "fit_slight": "Nežymus", "fit_strong": "Stiprus", - "fit_weak": "Silpnas", "fit_word": "tinka", "personalized": "● Pritaikyta šiai mašinai", "better_than_baseline": "{pct}% geriau nei bazinis įvertis ({metric})", @@ -757,21 +716,33 @@ "trend_declining_tip": "Šio modelio atitiktis suprastėjo per pastarąsias patikras · daugiau peržiūrėtų ciklų gali padėti jam iš naujo mokytis.", "trend_steady_tip": "Šio modelio atitiktis per pastarąsias patikras išliko maždaug pastovi.", "cap_blurb": { - "end": "Nustatoma, kada ciklas iš tikrųjų baigėsi", - "live_match": "Greičiau atpažįstama vykdoma programa", - "quality": "Aptinkami klaidingai nustatyti ar sugadinti ciklai", - "remaining_time": "Numatoma, kiek liko laiko", "total_energy": "Numatoma bendra energija ir kaina" }, "cap_label": { - "end": "Ciklo pabaigos nustatymas", - "live_match": "Programos atpažinimas", - "quality": "Ciklo kokybės patikra", - "remaining_time": "Likusio laiko įvertis", "total_energy": "Energijos įvertis" }, - "metric_auc": "AUC {auc} patikros duomenimis", - "metric_mae": "paklaida: {model} palyginti su {baseline} baze" + "metric_mae": "paklaida: {model} palyginti su {baseline} baze", + "fine_tuned_at": "tiksliai suderinta {when}", + "last_run_not_promoted": "Paskutinis patikrinimas ({when}): {reason}", + "never": "niekada", + "skip_reason": { + "fit_failed": "Apmokyti nepavyko: {error}", + "holdout_too_small": "Per mažai ciklų, kad būtų galima teisingai išbandyti naują modelį: bandymui galima atidėti {held_out}, reikia {min}.", + "not_better_than_incumbent": "Naujas modelis nebuvo geresnis už naudojamą (paklaida {model}, palyginti su {incumbent}), todėl paliktas dabartinis.", + "not_better_than_naive": "Naujas modelis nebuvo aiškiai geresnis už paprastą įvertį (paklaida {model}, palyginti su {naive}).", + "unknown": "Nieko naujo neišmokta.", + "insufficient_rows": "Dar nepakanka tinkamų ciklų: kol kas yra {count} pažymėtų švarių ciklų.", + "insufficient_rows_one": "Dar nepakanka tinkamų ciklų: kol kas yra {count} pažymėtas švarus ciklas.", + "insufficient_rows_few": "Dar nepakanka tinkamų ciklų: kol kas yra {count} pažymėti švarūs ciklai.", + "insufficient_rows_many": "Dar nepakanka tinkamų ciklų: kol kas yra {count} pažymėto švaraus ciklo.", + "insufficient_rows_other": "Dar nepakanka tinkamų ciklų: kol kas yra {count} pažymėtų švarių ciklų." + }, + "trained_unknown": "nežinomu laiku", + "held_out_count": "išmatuota naudojant {count} patikros ciklų", + "held_out_count_one": "išmatuota naudojant {count} patikros ciklą", + "held_out_count_few": "išmatuota naudojant {count} patikros ciklus", + "held_out_count_many": "išmatuota naudojant {count} patikros ciklo", + "held_out_count_other": "išmatuota naudojant {count} patikros ciklų" }, "modal": { "auto_label": "Automatinio etikečių ciklai", @@ -780,7 +751,6 @@ "cycle": "Ciklas", "edit_group": "Redaguoti profilių grupę", "edit_phase": "Redaguoti fazę", - "import_config": "Importuoti konfigūraciją", "label_cycle": "Etikečių ciklas", "merge_cycles": "Sujungti {n} ciklus", "new_group": "Nauja profilių grupė", @@ -795,6 +765,8 @@ "delete_automation_title": "Ištrinti automatizavimą", "delete_automation_msg": "Ištrinti automatizavimą „{name}“ iš Home Assistant? To nebus galima atšaukti.", "remove_legacy_title": "Pašalinti pasenusius veiksmus", + "reset_phase_msg": "Atšaukti fazės „{name}“ pakeitimą ir atkurti įmontuotą fazę? Programos išsaugo savo diapazonus.", + "reset_phase_title": "Atkurti fazę", "remove_legacy_msg": "Pašalinti pasenusius pasirinktinius veiksmus? Jie nustos veikti ciklo įvykiuose. To nebus galima atšaukti iš skydelio.", "discard_recording_title": "Išmesti įrašą", "discard_recording_msg": "Išmesti išsaugotą įrašą? To nebus galima atšaukti.", @@ -815,32 +787,46 @@ "store_share_device": "Bendrinti šį įrenginį", "export_select": "Eksportas - pasirinkti duomenis", "import_wizard": "Importas - pasirinkti duomenis", - "history_import": "Galios istorijos importavimas" + "history_import": "Galios istorijos importavimas", + "apply_suggestions_title": "Taikyti pasiūlymus", + "remove_task_title": "Pašalinti užduotį", + "remove_task_msg": "Pašalinti priminimą „{task}“? Jo įrašai priežiūros žurnale išsaugomi.", + "apply_suggestions_msg": "Pakeisti šiuos nustatymus ({n})? Išsaugojus integracija bus įkelta iš naujo.", + "discard_settings_title": "Išmesti neišsaugotus pakeitimus?", + "discard_settings_msg": "Neišsaugoti nustatymų pakeitimai: {n}. Išeiti iš Nustatymų ir juos išmesti?", + "profile_title": "Profilis · {name}", + "undo_import_msg": "Šio įrenginio duomenys ir nustatymai grąžinami į būseną prieš paskutinį importą ({when}). Nuo tada įrašyti ciklai ir atlikti pakeitimai prarandami.", + "undo_import_ok": "Anuliuoti importą", + "undo_import_title": "Anuliuoti paskutinį importą" }, "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.", + "pg_preset_delete_confirm": "Ištrinti rinkinį „{name}“?", + "pg_preset_limit": "Pasiekta rinkinių riba ({n})", + "pg_preset_overwrite": "Perrašyti rinkinį „{name}“?", "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.", - "refresh_catalog_hint": "Bendruomenės prekės ženklų ir prietaisų sąrašai laikomi podėlyje, kad bendrinama parduotuvė neviršytų savo dienos užklausų limito. Atnaujinkite, kad pamatytumėte kitų pridėtus ar patvirtintus įrašus.", + "store_sibling_hint": "Jūsų modeliui nieko nebendrinta? Giminingas to paties prekės ženklo modelis paprastai yra gera pradžia.", + "store_declare_appliance": "Nurodykite WashData, kokį prietaisą turite, ir matysite kitų jam bendrintas konfigūracijas. Arba viršuje įveskite prekės ženklą ir pasidairykite.", + "refresh_catalog_hint": "Bendruomenės prekės ženklų ir prietaisų sąrašai laikomi podėlyje, kad būtų taupomas parduotuvės dienos limitas. Atnaujinkite, kad pamatytumėte kitų pridėtus ar patvirtintus įrašus.", "head_trim_hint": "Kiek sekundžių pašalinti nuo pradžios", "appliance_monitor": "Prietaisų monitorius", "artifact_dip_detail": "Nukrito žemiau įprastos galios juostos maždaug {n}s.", "artifact_footer": "Paryškinta aukščiau esančiame grafike. Tai yra trumpalaikiai artefaktai (pvz., durys atidarytos ciklo viduryje), nebūtinai problemos.", - "artifact_header": "Per šį ciklą aptikta anomalijų: {n}", - "artifact_pause_detail": "Galia krito beveik iki nulio maždaug {n}s, tada atsinaujino - greičiausiai buvo atidarytos durys ciklo metu arba ciklas buvo pristabdytas.", + "artifact_header": "Per šį ciklą aptikta {n} anomalijų", + "artifact_header_one": "Per šį ciklą aptikta {n} anomalija", + "artifact_header_few": "Per šį ciklą aptiktos {n} anomalijos", + "artifact_header_many": "Per šį ciklą aptikta {n} anomalijos", + "artifact_header_other": "Per šį ciklą aptikta {n} anomalijų", + "artifact_pause_detail": "Galia maždaug {n}s krito beveik iki nulio, tada atsinaujino. Greičiausiai buvo atidarytos durys arba ciklas pristabdytas.", "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": "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.", + "cleanup_intro": "Visi pažymėti ciklai rodomi vienas ant kito. Pasirinkite nuokrypius ir panaikinkite jų žymėjimą (liks istorijoje, bet nebeformuos šio profilio) arba ištrinkite juos.", "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)", - "compare_profiles_tip": "Aukščiau pateiktoje diagramoje uždėkite kitus profilio vokus, kad sužinotumėte, kuris iš jų geriausiai tinka šiam ciklui.", + "compare_profiles_tip": "Uždėkite kitus profilius ant diagramos ir pažiūrėkite, kuris geriausiai tinka šiam ciklui.", "compare_selected_cycles": "Pasirinkti ciklai (vientisas) – rodyti / slėpti", - "consider_new_profile": "Apsvarstykite galimybę sukurti naują profilį.", - "coverage_gap": "paskutiniai ciklai neturi atitinkančio profilio ({pct} % iš paskutinių 30).", - "coverage_gap_similar_cycles": "Rasti {count} panašūs nepažymėti ciklai – sukurkite profilį, kad pradėtumėte juos atpažinti.", "cycles_deleted": "Ištrinta ciklų: {count}", "enough_data": "Pakankamai duomenų mokymui ({current}/{min} ciklų).", "export_description": "Pasirinkite tiksliai, kuriuos profilius, ciklus, nustatymus ir kita eksportuoti į JSON, arba išanalizuokite failą ir importuokite tik norimas dalis.", @@ -848,30 +834,28 @@ "feedback_prompt": "Patvirtinkite, kad tai buvo teisinga, pataisykite programą arba ignoruokite.", "feedback_relabel_hint": "Šio ciklo pažymėjimas iš naujo taip pat išsprendžia peržiūrą.", "filter_by_profile": "Filtruoti pagal profilį…", - "group_modal_help": "Grupuoti tos pačios formos programas, kurios skiriasi temperatūra / sukimu (trukmė gali skirtis). Atitikimas įvertina grupę kaip vieną kandidatą, tada parenka geriausiai tinkantį narį. Pasirinkite bent 2; perdanga parodo, kokie jie panašūs.", + "group_modal_help": "Grupuokite tos pačios formos programas, kurios skiriasi temperatūra ar gręžimu. Atpažinimas vertina grupę kaip vieną, tada parenka geriausiai tinkantį narį. Pasirinkite bent 2; perdanga parodo, kiek jos panašios.", "group_not_cohesive": "Šie profiliai nėra pakankamai panašūs, kad būtų galima patikimai sugrupuoti, todėl derinant jie traktuojami atskirai, kol pašalinsite nuokrypį arba padalinsite grupę.", "group_preview_hint": "Pažymėkite 2+ narius, kad peržiūrėtumėte ir palygintumėte jų galios kreives.", - "import_intro": "Toliau įkelkite eksportuotą failą arba įklijuokite JSON naudingąjį failą.", - "legacy_actions_title": "{count} senas priskirtas veiksmas vis dar vykdomas", + "legacy_actions_title": "{count} senų priskirtų veiksmų vis dar vykdoma", + "legacy_actions_title_one": "{count} senas priskirtas veiksmas vis dar vykdomas", + "legacy_actions_title_few": "{count} seni priskirti veiksmai vis dar vykdomi", + "legacy_actions_title_many": "{count} seno priskirto veiksmo vis dar vykdoma", + "legacy_actions_title_other": "{count} senų priskirtų veiksmų vis dar vykdoma", "live_chart_loading": "Tiesioginė galios diagrama pasirodo gavus rodmenis.", "loading": "Įkeliama…", "loading_curve": "Įkeliama kreivė…", "loading_settings": "Įkeliami nustatymai…", "log_buffer_hint": "Pirmiau naujausi · Buferyje saugomi paskutiniai 500 ha_washdata įrašų nuo paleidimo iš naujo · Vilkite apatinį kraštą, kad pakeistumėte dydį.", - "maintenance_advisory": "Ilgėjanti trukmė / energija gali reikšti, kad reikalinga prietaiso priežiūra (pvz., nukalkinimas, filtro valymas).", "maintenance_due": "Laikas atlikti priežiūrą: {items}", - "maintenance_intro": "Registruokite šio buitinio prietaiso aptarnavimą ir gaukite priminimą, kai vėl reikės atlikti kiekvieną užduotį.", + "maintenance_intro": "Registruokite šio prietaiso aptarnavimą ir gaukite priminimą, kai vėl ateis laikas kiekvienai užduočiai.", "maintenance_load_error": "Nepavyko įkelti priežiūros duomenų: {error}", "maintenance_requires_access": "Priežiūra ir eksportas / importas reikalauja visiškos prieigos.", - "manual_duration_hint": "Profilio vidutinė / numatoma ciklo trukmė, naudojama likusiam laikui apskaičiuoti. Redaguoti, kad jį nustatytumėte; paliekant jį nepakeistą, išlaikoma dabartinė vertė.", - "matching_tuning_intro": "Mokydamiesi „WashData“ taip pat koreguoja, kiek programos atitikimas sveria formą, palyginti su trukme ir energija.", + "manual_duration_hint": "Laukiama profilio ciklo trukmė, naudojama likusiam laikui įvertinti. Nekeiskite, jei norite palikti dabartinę reikšmę.", "merge_intro": "Pasirinkti ciklai sujungiami į vieną (chronologine tvarka; tarpai užpildyti 0 W). Pasirinkite gautą profilį.", "ml_intro": "„WashData“ pristatomi su išmaniaisiais modeliais, kurie veikia iš karto.", "ml_learned_intro": "Modeliai, pritaikyti šiai mašinai.", - "ml_loading": "įkeliamas ML…", "ml_settings_intro": "Du nepriklausomi jungikliai: vienas pritaiko modelius, kol veikia ciklas, kitas leidžia WashData laikui bėgant tiksliai suderinti juos su jūsų aparatu.", - "name_first_program": "Turite pakankamai ciklų – pavadinkite pirmąją programą, kad prasidėtų suderinimas.", - "near_duplicate_cluster": "Aptikta beveik pasikartojanti profilių grupė. Grupavimas leidžia suderinti patikimai pasirinkti panašius dalykus (pvz., ta pati programa esant skirtingai temperatūrai / sukimuisi).", "no_cycles_match": "Nė vienas ciklas neatitinka esamo filtro.", "no_cycles_profile": "Šiam profiliui nėra ciklų.", "no_cycles_selected": "Pirmiausia pasirinkite bent vieną ciklą.", @@ -896,73 +880,56 @@ "no_suggestions": "Nėra aktyvių pasiūlymų.", "notify_services_hint": "Naudokite {entity} paslaugų ID (keliems – atskirkite kableliais). Šablono kintamieji: {vars}.", "old_actions_warning": "Sukonfigūruota naudojant seną veiksmų rengyklę (dabar pašalinta). Jie vis dar suaktyvinami ciklo įvykiuose, bet čia jų redaguoti nebegalima. Konvertuokite juos į įprastą automatiką arba pašalinkite.", - "onboarding_progress": "Stebėta ciklų: {n} / 3", - "onboarding_watching": "Naudokite prietaisą įprastai – „WashData“ stebi. Po 3 ciklų prasidės programų suderinimas.", "pending_feedback": "Laukiama aptikimo atsiliepimo", "performance_trend": "Našumo tendencija ({n} ciklai)", - "pg_analysis_empty": "Įkelkite ciklą, kad pamatytumėte atitikties analizę.", "pg_apply_confirm": "Taikyti geriausią reikšmę: {label} = {value}?", - "pg_canvas_empty": "Pasirinkite ciklą aukščiau, tada paspauskite „Įkelti“, kad pamatytumėte jo galios kreivę.", - "pg_chart_caption": "Aptikimo / atitikties rodiklis pagal {param} reikšmes", "pg_ctrl_intro": "Reikšmės imamos iš aktyvių šio įrenginio integracijos nustatymų. Pakeitimai lieka bandymų aikštelėje, kol jų neįrašysite į nustatymus.", "pg_live_unavailable": "Aktyvūs nustatymai nepasiekiami, rodomos numatytosios reikšmės.", "pg_load_live_confirm": "Atmesti bandymų aikštelės pakeitimus ({n}) ir iš naujo įkelti dabartinius integracijos nustatymus?", - "pg_match_rate": "{pct}% atitikties rodiklis iš {total} ciklų", "pg_matches_live": "Sutampa su aktyviais nustatymais", "pg_n_changed": "Skirtumų nuo aktyvių nustatymų: {n}", "pg_sugg_none": "Visi pasiūlymai jau atitinka dabartinius nustatymus", - "pg_preset_delete_confirm": "Ištrinti rinkinį „{name}“?", - "pg_preset_limit": "Pasiekta rinkinių riba ({n})", - "pg_preset_overwrite": "Perrašyti rinkinį „{name}“?", "pg_publish_one_confirm": "Įrašyti {label} = {value} į šio įrenginio nustatymus?", "pg_restart_note": "Iš naujo paleiskite „Home Assistant“, kad įjungtumėte simuliavimo įrankius.", - "pg_sim_progress": "{done} / {total} ciklai", - "pg_starting": "Pradedama…", - "pg_stress_synthetic": "budėjimo imitacija prasideda čia", - "pg_sweep_intro": "O kas, jei {param} būtų kitokia? Išbandykite {steps} reikšmes paskutiniuose {cycles} cikluose ir raskite nustatymą, su kuriuo teisingai atitinka daugiausiai ciklų.", - "pg_sweep_step": "Žingsnis {done} / {total}", - "pg_verdict_bad": "Reikia dėmesio: daugelis ciklų lieka neaptikti.", - "pg_verdict_good": "Gerai suderinta: dauguma ciklų teisingai atpažįstami ir atitinkami.", - "pg_verdict_ok": "Priimtina: kai kurie ciklai praleisti. Pabandykite sumažinti paleidimo slenkstį.", "phase_catalog_intro": "Pavadinti ciklo segmentai (išankstinis plovimas, šildymas, gręžimas…). Priskirkite juos profiliui iš valdymo skydelio.", "phase_ranges_intro": "Fazių diapazonai (minutės nuo ciklo pradžios) perdengti vidurkio kreive. Redaguokite vertes, kad galėtumėte peržiūrėti tiesiogiai.", - "playground_intro": "Saugi bandymų aplinka, kurioje galima iš naujo paleisti ankstesnius ciklus su kitais nustatymais, palyginti dvi konfigūracijas greta ir peržiūrėti, kaip ciklas sutampa su profiliu. Niekas čia nekeičia jūsų tikrųjų duomenų, kol to aiškiai nepritaikysite.", - "playground_needs_restart": "Kad įjungtumėte bandymų aikštelės įrankius, iš naujo paleiskite „Home Assistant“.", - "preferences_admin": "Parinktys, skydelis ir prieigos valdymas", - "preferences_adv": "Parinktys", + "playground_intro": "Išbandykite nustatymus su savo tikrais ciklų duomenimis. Niekas čia nekeičia veikiančių nustatymų, kol jų nepritaikysite.", "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), perskaičiuoti ciklų kainą pagal įrašytas energijos kainas ir perskaičiuoti ciklų sveikatą. Paleiskite tai atlikę keletą peržiūrų.", + "process_history_hint": "Pakartotinai paleisti atitikimą visiems saugomiems ciklams, atnaujinti pasiūlymus, iš naujo apmokyti ML modelius (jei įjungta), perskaičiuoti ciklų kainą pagal įrašytas kainas ir perskaičiuoti ciklų sveikatą. Paleiskite 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ų.", - "rbac_hint": "Kai išjungta, kiekvienas namų asistento naudotojas turi visišką prieigą (numatytasis). Administratoriai visada turi visišką prieigą ir gali valdyti visus.", - "recent_logs": "Naujausi ha_washdata įrašai", + "rbac_hint": "Išjungta: kiekvienas Home Assistant naudotojas turi visišką prieigą (numatytoji). Administratoriai visada turi visišką prieigą ir gali valdyti visus.", "recording_in_progress": "Įrašymas vyksta", "reminders_intro": "Rodyti priminimą skydelyje praėjus tiek ciklų po paskutinio aptarnavimo. Palikite tuščią arba 0, kad priminimas būtų išjungtas.", "restart_gap_footer": "Paryškinta grafike. Galios duomenų trūksta šiems intervalams; suderinimas naudojo tik tikrus rodmenis.", - "restart_gap_header": "{n} HA paleidimo iš naujo spraga šio ciklo metu", + "restart_gap_header": "{n} HA paleidimo iš naujo spragų šio ciklo metu", + "restart_gap_header_one": "{n} HA paleidimo iš naujo spraga šio ciklo metu", + "restart_gap_header_few": "{n} HA paleidimo iš naujo spragos šio ciklo metu", + "restart_gap_header_many": "{n} HA paleidimo iš naujo spragos šio ciklo metu", + "restart_gap_header_other": "{n} HA paleidimo iš naujo spragų šio ciklo metu", "restart_gap_item": "{dur} spraga", - "review_confirm_help": "Patvirtinkite, ar šis ciklas buvo aptiktas teisingai. Jūsų atsiliepimai lavina modelį jūsų įrenginyje – kuo daugiau ciklų patvirtinsite, tuo geresnis atitikimas ir būklės įvertinimas. Užtenka greito Gero/Blogo.", + "review_confirm_help": "Ar šis ciklas aptiktas teisingai? Patvirtinti ciklai gerina jūsų aparato atpažinimą. Užtenka greito „Gerai“ ar „Blogai“.", "review_in_settings": "Peržiūrėkite nustatymuose", "review_notes_placeholder": "Pastabos (neprivaloma)", "review_notes_tip": "Laisvo teksto pastabos jūsų patiems. Nenaudojamas atitikimui ar mokymui.", - "review_profile_tip": "Šio ciklo programa pažymėta kaip. Jei automatiškai aptikta programa buvo neteisinga, pataisykite ją čia – ženklinimas moko suderinti būsimus ciklus.", - "review_quality_tip": "Koks švarus šis ciklas. Geras = šios programos vadovėlis pavyzdys; Blogai = aptikta, bet triukšminga arba netipiška; Netinkamas = klaidingai aptiktas (sujungtas, sutrumpintas arba klaidingas). Nurodo sveikatos balą ir kokius ciklus leidžiama treniruoti modelį.", - "review_recorded_tip": "Pažymėkite tai kaip savo programos etaloninį ciklą – tą patį vaidmenį kaip ir rankiniu būdu įrašytą ciklą. Etaloniniai ciklai visada išsaugomi, imamasi atitinkamo šablono ir niekada nenuleidžiami valant. (Tai yra „auksinė“ / įrašyta vėliava; abi yra tas pats.)", - "review_tags_tip": "Pasirenkamos vėliavėlės, nurodančios, kas nutiko šiame cikle, todėl mokymai ir valymas gali tai paaiškinti.", + "review_profile_tip": "Programa, kuria pažymėtas šis ciklas. Jei ji neteisinga, pataisykite čia; žymės moko atpažinimą.", + "review_quality_tip": "Kiek švarus šis ciklas. Gerai: pavyzdinis šios programos ciklas. Blogai: aptiktas, bet triukšmingas ar neįprastas. Netinkamas naudoti: klaidingai aptiktas (sujungtas, nutrauktas ar netikras). Veikia sveikatos balą ir tai, kurie ciklai moko modelį.", + "review_recorded_tip": "Pažymėkite kaip rinktinį šios programos etaloninį ciklą, lygiavertį rankiniu būdu įrašytam. Etaloniniai ciklai visada saugomi, iš jų pradedamas atitikimo šablonas, o valymas jų niekada nepašalina.", + "review_tags_tip": "Pasirenkamos vėliavėlės, nurodančios, kas nutiko ne taip, kad mokymas ir valymas į tai atsižvelgtų.", "review_to_cycles": "Atidarykite ciklų peržiūros eilę", "samples_decimated": "Rodoma {shown} iš {total} pavyzdžių (praretinta rodymui; smailės išsaugotos). Platus tarpas čia yra praretinimas, o ne trūkstami duomenys.", - "saving_triggers_reload": "Išsaugojus suaktyvinamas integracijos įkėlimas iš naujo. HA objektai trumpam gali būti rodomi kaip nepasiekiami.", + "saving_triggers_reload": "Išsaugojus integracija įkeliama iš naujo. Objektai trumpam gali būti nepasiekiami.", "search_placeholder": "Paieškos nustatymai…", "see_recorder": "Žiūrėkite įrašymo valdiklį žemiau", "setting_changed": "Pakeista iš {old} į {new} {date}", "settings_basic_note": "Rodomi esminiai nustatymai. Norėdami pamatyti visą sąrašą, perjunkite į „Išplėstinė“.", - "shape_drift_advisory": "⚠ Formos dreifas", - "shape_drift_detail": "Šio profilio galios šablonas laikui bėgant pasikeitė – galimas prietaiso susidėvėjimas arba reikalinga priežiūra (pvz., nukalkinimas, filtro valymas).", "show_all_settings": "Rodyti visus nustatymus", - "showing_suggestions": "Rodomas {count} nustatymas su pasiūlymais.", - "split_intro": "Spustelėkite diagramą, kad pridėtumėte arba pašalintumėte padalijimo tašką, arba automatiškai aptiktumėte pagal tuščiosios eigos tarpus. Kiekvienas gautas segmentas gali gauti savo profilį.", - "storage_diagnostics": "Sandėliavimo statistika, priežiūra, eksportas/importas", + "showing_suggestions": "Rodoma {count} nustatymų su pasiūlymais.", + "showing_suggestions_one": "Rodomas {count} nustatymas su pasiūlymais.", + "showing_suggestions_few": "Rodomi {count} nustatymai su pasiūlymais.", + "showing_suggestions_many": "Rodoma {count} nustatymo su pasiūlymais.", + "showing_suggestions_other": "Rodoma {count} nustatymų su pasiūlymais.", + "split_intro": "Spustelėkite diagramą, kad pridėtumėte ar pašalintumėte padalijimo tašką, arba leiskite juos aptikti automatiškai pagal neveikos tarpus. Kiekviena dalis gali gauti savo profilį.", "sug_staged": "{key} = {val} nustatyta. Išsaugokite, kad pritaikytumėte.", "toast_auto_detect_enabled": "Automatinis aptikimas įjungtas", "toast_auto_label_complete": "Automatinis žymėjimas baigtas", @@ -979,7 +946,6 @@ "toast_no_automation": "Čia negalima sukurti automatizavimo", "toast_profile_name_required": "Profilio pavadinimas yra privalomas", "toast_program_set": "Programa nustatyta: {program}", - "toast_rebuild_failed": "Atstatymas nepavyko: {error}", "toast_resume_failed": "Atnaujinimas nepavyko: {error}", "toast_revert_failed": "Grąžinimas nepavyko: {error}", "toast_reverted": "{key} grąžinta", @@ -990,96 +956,87 @@ "trend_energy_up": "Energijos augimas ({pct} % per ciklą) – pastaruoju metu vid. {avg}", "trim_destructive_confirm": "Bus išsaugota tik {pct}% ciklo ir to nebus galima atšaukti. Tęsti?", "trim_intro": "Vilkite raudonas rankenas arba įveskite vertes. Viskas, kas yra už lango, pašalinama.", - "tuning_suggestions_available": "Galimas {count} derinimo pasiūlymas iš stebimų ciklų. Jie rodomi šalia atitinkamų laukų.", + "tuning_suggestions_available": "Galima {count} derinimo pasiūlymų iš stebimų ciklų. Jie rodomi šalia atitinkamų laukų.", + "tuning_suggestions_available_one": "Galimas {count} derinimo pasiūlymas iš stebimų ciklų. Jis rodomas šalia atitinkamo lauko.", + "tuning_suggestions_available_few": "Galimi {count} derinimo pasiūlymai iš stebimų ciklų. Jie rodomi šalia atitinkamų laukų.", + "tuning_suggestions_available_many": "Galima {count} derinimo pasiūlymo iš stebimų ciklų. Jie rodomi šalia atitinkamų laukų.", + "tuning_suggestions_available_other": "Galima {count} derinimo pasiūlymų iš stebimų ciklų. Jie rodomi šalia atitinkamų laukų.", "unsure_detected_prefix": "„WashData“ nėra tikras, kad aptiko", - "updated": "Atnaujinta", "warmup_badge": "Vis dar mokosi ({done}/{needed} ciklai)", - "warmup_detail": "Šiam profiliui reikia {needed} pažymėtų ciklų, kol prasidės automatinis atitikimas. Kiekvienas patvirtintas ciklas padeda mokytis.", + "warmup_detail": "Atitikimas jau veikia. Prieš pradėdama žymėti savarankiškai, WashData prašo patvirtinti pirmuosius šio profilio ciklus ({needed}).", "wipe_history_warning": "Visam laikui ištrinti visus ciklus ir profilius. Negalima anuliuoti.", "compare_overlay_tip": "Uždėkite išmoktų profilių galios juostas, kad matytumėte, kurią programą primena kiekvienas ciklas.", - "duration_trend_up_tip": "Trukmė didėja ({pct}%/ciklui)", - "duration_trend_down_tip": "Trukmė mažėja ({pct}%/ciklui)", - "energy_trend_up_tip": "Energija didėja ({pct}%/ciklui)", - "energy_trend_down_tip": "Energija mažėja ({pct}%/ciklui)", - "performance_trending": "Našumo tendencija", - "pg_select_cycle_hint": "Pasirinkite ciklą ir spustelėkite ↺, kad įkeltumėte", - "pg_sweep_progress": "Žingsnis {done} / {total}", - "pg_analysis_hint": "Pasirinkite ciklą ir spustelėkite ↺, kad įkeltumėte analizę.", "diagnostics_load_failed": "Nepavyko įkelti diagnostikos: {error}", - "manual_duration_ref_hint": "Naudojama tik tada, kai nepasirinktas etaloninis ciklas. Etaloninis ciklas nustato trukmę pagal savo paties ilgį.", + "manual_duration_ref_hint": "Naudojama tik be etaloninio ciklo; etaloninis ciklas trukmę nustato pagal savo ilgį.", + "duration_learned_hint": "Apskaičiuota iš šios programos ciklų (be išskirčių) ir atnaujinama po kiekvieno naujo. Norėdami pakeisti, apkarpykite arba pašalinkite netinkamus ciklus.", "toast_convert_rolled_back": "Konvertavimas nepavyko ir buvo atšauktas (nepaliktas joks automatizavimas): {error}", "toast_convert_orphan": "Automatizavimas buvo sukurtas, tačiau senų veiksmų išvalyti nepavyko. Nebandykite dar kartą: senus veiksmus pašalinkite rankiniu būdu, kad išvengtumėte pasikartojančio automatizavimo.", - "advisory_duration_trend_up": "'{name}' ciklai vis ilgėja (maždaug +{pct}% per ciklą). Jei prietaiso veikimas pasikeitė, įrašykite iš naujo arba perkurkite šį profilį.", - "advisory_energy_trend_up": "'{name}' naudoja vis daugiau energijos (maždaug +{pct}% per ciklą) - verta patikrinti prietaisą, jei to nesitikėjote.", - "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.", + "advisory_duration_trend_up": "'{name}' ciklai vis ilgėja (maždaug +{pct}% per ciklą). Jei prietaisas pasikeitė, įrašykite iš naujo arba perkurkite šį profilį.", + "advisory_energy_trend_up": "'{name}' su kiekvienu ciklu naudoja vis daugiau energijos (maždaug +{pct}% per ciklą). Jei to nesitikėjote, patikrinkite prietaisą.", + "advisory_poor_health": "'{name}' atitinka prastai: naujausi jo ciklai labai skiriasi arba silpnai atpažįstami. Peržiūrėkite jo ciklus arba įrašykite iš naujo, kad atpažinimas ir įverčiai liktų tikslūs.", + "advisory_shape_drift": "'{name}' nukrypo nuo pradinės galios kreivės, galbūt dėl kalkių ar nusidėvėjimo. Apsvarstykite galimybę įrašyti jį iš naujo pagal naujausius ciklus.", + "advisory_shape_drift_corr": "'{name}' nukrypo nuo pradinės galios kreivės (koreliacija {corr}), galbūt dėl kalkių ar nusidėvėjimo. Apsvarstykite galimybę įrašyti jį iš naujo pagal naujausius ciklus.", + "advisory_unmatchable": "'{name}' niekada nebus atpažintas: už jo nėra nė vieno ciklo su galios duomenimis. Pažymėkite arba įrašykite šios programos ciklą, kad tai ištaisytumėte.", + "advisory_unmatchable_no_duration": "'{name}' niekada nebus atpažintas: jo ciklas neturi tinkamos trukmės. Įrašykite šią programą iš naujo arba nurodykite numatomą jos trukmę.", "pg_no_alerts": "Šiame paleidime problemų neaptikta.", "pg_override_active": "Naudojami jūsų redaguoti nustatymai, palyginti su dabartiniais", "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.", "pg_sweep_intro2": "Raskite nustatymą, kuris geriausiai atitinka tikslą jūsų naujausiuose cikluose. Niekas nepasikeis, kol jo nepritaikysite.", - "pg_running_history": "Atkuriami jūsų ciklai…", - "pg_running_sweep": "Tikrinamos reikšmės visuose jūsų cikluose…", "pg_row_load_hint": "Įkelti šį ciklą į aukščiau esantį grafiką", - "pg_history_intro2": "Atkurkite naujausius ciklus per tikrąjį detektorių ir programų derinimo modulį su aukščiau nurodytais nustatymais. Spustelėkite bet kurią eilutę, kad įkeltumėte tą ciklą į grafiką; pakeiskite nustatymą, kad pamatytumėte palyginimą prieš ir po.", + "pg_history_intro2": "Atkurkite naujausius ciklus per tikrąjį detektorių ir atitikimo modulį su aukščiau esančiais nustatymais. Spustelėkite eilutę, kad ją įkeltumėte į grafiką; pakeiskite nustatymą, kad palygintumėte prieš ir po.", "pg_simulating": "Simuliuojamas ciklas…", - "pg_canvas_empty2": "Pasirinkite ciklą aukščiau ir paspauskite Vykdyti, kad jį simuliuotumėte. Tada užveskite žymeklį, kad nuskaitytumėte reikšmes, slinkite, kad priartintumėte, ir vilkite, kad pastumtumėte.", "pg_apply_settings_confirm": "Ar išsaugoti šiuos {n} nustatymus šiame įrenginyje? {list}", - "pg_stress_terminated_detail": "Nusistovėjo ties maždaug {idle} W budėjimo lygiu -- ciklas baigėsi po {h} val. {m} min. dėl {reason}.", - "pg_stress_above_threshold_detail": "Budėjimo suvartojimas (~{idle} W) yra ties sustabdymo slenksčiu ({stop} W) arba jį viršija -- ciklas niekada nebuvo užregistruotas kaip tylus. Norėdami pataisyti, padidinkite stop_threshold_w.", - "pg_stress_hit_cap_detail": "Ciklas veikė {h} val. {m} min. nesustodamas -- priverstinai sustabdytas apsauginio limito. Budėjimo suvartojimas {idle} W, palyginti su sustabdymo slenksčiu {stop} W.", "no_logs_match": "Nė vienas žurnalo įrašas neatitinka šių filtrų.", "pg_analysis_hint2": "Pasirinkite ciklą ir paspauskite Vykdyti, kad įkeltumėte atitikties analizę.", - "pg_analysis_empty2": "Paspauskite Vykdyti, kad pamatytumėte atitikties analizę.", - "imported_readonly": "Importuota iš bendruomenės parduotuvės. Rodoma kaip nuoroda ir atitikčiai nustatyti. Ji neįskaičiuojama į jūsų statistiką ir jos negalima redaguoti.", + "imported_readonly": "Importuota iš bendruomenės parduotuvės. Naudojama kaip etalonas ir atpažinimui, neįskaičiuojama į jūsų statistiką ir jos negalima redaguoti.", "merge_no_imports": "Importuotų įrašų negalima sujungti su tikru ciklu. Panaikinkite jų žymėjimą, kad galėtumėte sujungti.", - "store_enable_hint": "Įjunkite interneto funkcijas nustatymuose, kad galėtumėte naršyti ir importuoti bendruomenės etaloninius ciklus.", - "advisory_duration_outlier": "{n} ciklų, pažymėtų '{name}', trukmė per daug skiriasi nuo įprastos šios programos trukmės, kad kada nors ją atitiktų (blogiausias: {ratio}x). Tikėtina, kad jie pažymėti neteisingai arba viename įraše užfiksuoti du paleidimai. Pažymėkite juos iš naujo arba padalykite, kad jie nebeiškraipytų šios programos laiko įverčio.", + "store_enable_hint": "Įjunkite interneto funkcijas skiltyje Nuostatos > Internetas ir bendruomenė, kad galėtumėte naršyti ir importuoti bendruomenės etaloninius ciklus.", + "advisory_duration_outlier": "Ciklai, pažymėti '{name}' ({n}), per daug skiriasi nuo įprastos šios programos trukmės, kad kada nors būtų atpažinti (blogiausias: {ratio}x). Tikriausiai jie pažymėti klaidingai arba juose yra du paleidimai. Pažymėkite juos iš naujo arba padalykite, kad nebeiškraipytų laiko įverčio.", "standby_above_stop_title": "Ciklui pasibaigus vis dar naudojama {idle} W", "standby_above_stop_sub": "{n} iš paskutinių {total} ciklų. Išjungimo delsimas prasideda tik tada, kai galia nukrenta žemiau Sustabdymo slenksčio ({stop} W), todėl ciklai baigiasi pavėluotai arba juos tenka sustabdyti rankiniu būdu.", - "online_intro": "Dalykitės ir atsisiųskite etaloninius ciklus su kitais WashData naudotojais. Visos interneto funkcijos yra pasirenkamos ir pagal numatytuosius nustatymus išjungtos.", "store_share_intro": "Įkelkite šį etaloninį ciklą, kad juo galėtų naudotis kiti tą patį prietaisą turintys naudotojai. Prieš paskelbiant viešai jis peržiūrimas.", - "online_intro_global": "Naršykite ir bendrinkite etaloninius įrašus su kitais WashData naudotojais bei patvirtinkite prietaisų įrašus. Vienas prisijungimas taikomas visai jūsų WashData integracijai; prietaiso prekės ženklas ir modelis nustatomi kiekvienam prietaisui atskirai skiltyje Pagrindinė konfigūracija. Visos interneto funkcijos yra pasirenkamos ir pagal numatytuosius nustatymus išjungtos.", + "online_intro_global": "Naršykite ir bendrinkite etaloninius įrašus su kitais WashData naudotojais. Vienas prisijungimas tinka visiems jūsų WashData įrenginiams; prekės ženklas ir modelis nustatomi kiekvienam įrenginiui skiltyje Pagrindiniai. Interneto funkcijos išjungtos, kol jų neįjungsite.", "online_unavailable": "Interneto funkcijos šiame serveryje nepasiekiamos.", "store_picker_offline": "Įjunkite interneto funkcijas nustatymų krumpliaratyje, kad galėtumėte rinktis iš bendruomenės katalogo.", "pick_brand_first": "Pirmiausia pasirinkite prietaiso prekės ženklą.", - "model_not_found": "Kol kas nėra kataloge.", - "connect_to_confirm": "Prisijunkite nustatymų krumpliaratyje, kad galėtumėte patvirtinti ar įvertinti.", + "custom_task_limit": "Pasiekėte didžiausią užduočių skaičių ({max}).", + "custom_tasks_intro": "Pridėkite bet ką kita, apie ką norite gauti priminimą. Užduotį laikas atlikti, kai pasiekiamas bet kuris iš intervalų; vieną palikite tuščią arba 0, kad būtų naudojamas tik kitas.", + "maintenance_due_sensor_hint": "Dvejetainis jutiklis „Reikia priežiūros“ įjungtas, kol bent viena užduotis laukia atlikimo. Jį galite naudoti savo pranešimų automatizavimams.", + "maintenance_supporter_intro": "Maintenance Supporter yra atskira integracija, kuri gali daug daugiau: bet kokie prietaisai ar užduotys, tvarkaraščiai pagal laiką, naudojimą ar jutiklį, pranešimai ir istorija. Ji gali naudoti WashData ciklų skaitiklį.", "adopt_settings_hint": "Perrašykite šio įrenginio aptikimo ir atitikties slenksčius bendrais. Jūsų pranešimai, esybės ir energijos kaina niekada nekeičiami.", - "include_settings_hint": "Bendrinkite šio įrenginio atpažinimo ir atitikties slenksčius (ne jūsų pranešimus, esybes ar energijos kainą). Perėmėjai pasirenka, ar juos taikyti.", + "include_settings_hint": "Bendrinkite šio įrenginio aptikimo ir atitikimo slenksčius (ne pranešimus, objektus ar energijos kainą). Kiti patys renkasi, ar juos taikyti.", "onboard_download": "Naujas įrenginys? Perimkite paruoštą konfigūraciją (programas, etalonius ciklus ir fazes) iš kito WashData naudotojo, turinčio tą patį prietaisą.", - "share_consent": "Patvirtinu, kad šie ciklai vyko normaliai iki pabaigos be pertrūkių", "share_device_none": "Dar nėra bendrintinų ciklų. Pirmiausia Ciklų skirtuke pažymėkite įrašytą arba rankiniu būdu pasirinktą ciklą kaip etaloninį ciklą (⭐).", "share_guideline_naming": "Pavadinkite kiekvieną profilį tiksliai taip, kaip rodoma prietaiso reguliatoriuje ar ekrane (pvz., 'Cotton 40', 'Eco 60').", - "share_guideline_quality": "Bendrinkite tik ciklus, kurie baigėsi normaliai -- be pertrūkių ciklo viduryje, durų atidarymo įvykių ar galios sutrikimų.", + "share_guideline_quality": "Bendrinkite tik normaliai pasibaigusius ciklus: be pertrūkių, durų atidarymų ar galios trikdžių.", "share_guideline_review": "Jūsų įkėlimas pradedamas kaip laukiantis ir tampa viešas, kai pakankamai bendruomenės narių jį patvirtina.", "share_guidelines_title": "Prieš dalijantis", - "store_download_device_intro": "Perimkite kiekvieną bendrą programą ir jos etalonius ciklus į savo įrenginį. Jūsų pačių įrašyti ciklai ir statistika neįtakojami.", + "store_download_device_intro": "Pridėkite visas bendrintas programas ir jų etaloninius ciklus į savo įrenginį. Jūsų ciklai ir statistika nepaliečiami.", "store_share_device_intro": "Įkelkite {brand} {model} su pasirinktais etaloniniais ciklais. Kiti, turintys tą patį prietaisą, gali perimti jūsų programas. Įrašai peržiūrimi prieš viešinant.", "share_profile_no_cycles": "Nėra etaloninių ciklų -- pažymėkite ciklą kaip ⭐ Ciklų skirtuke, kad įtrauktumėte šį profilį", - "advisory_phase_inconsistent": "Atrodo, kad '{name}' maišo skirtingas programas ar temperatūras - jo ciklai kaista labai skirtingą laiką. Padalijus jį į atskirus profilius (pvz., pagal temperatūrą), pagerės atitikimas ir laiko įverčiai.", - "advisory_phase_inconsistent_title": "⚠ Galimai sumaišytos programos", - "export_select_intro": "Pažymėkite tiksliai, ką įtraukti. Pasirinkus profilius be jų ciklų, vis tiek eksportuojama atpažįstama programa (jos išmokta forma keliauja kartu).", - "import_analyze_hint": "Įkelkite eksportuotą failą (arba įklijuokite jo JSON). WashData jį išanalizuoja ir tiksliai parodo, ką galima importuoti, prieš ką nors keičiant.", + "export_select_intro": "Pasirinkite, ką įtraukti. Profiliai be jų ciklų vis tiek eksportuojami kaip atpažįstama programa.", + "import_analyze_hint": "Įkelkite eksportuotą failą (arba įklijuokite jo JSON). WashData parodo, ką galima importuoti, prieš ką nors pakeisdama.", "analyzing": "Analizuojama…", - "device_type_mismatch_warn": "Šis eksportas yra iš kito prietaiso tipo ({src} vs {local}). Programas ir ciklus vis tiek galima importuoti kaip atskaitos duomenis, tačiau konkrečiam prietaisui skirti nustatymai ir tikros istorijos importas yra išjungti.", + "device_type_mismatch_warn": "Šis eksportas yra iš kito prietaiso tipo ({src} vs {local}). Programas ir ciklus vis tiek galima importuoti kaip atskaitos duomenis, bet ne įrenginio nustatymus ar tikrą istoriją.", "merge_hint": "Importuoti elementai pridedami; jokie vietiniai duomenys neprarandami. Pavadinimų konfliktai sprendžiami toliau.", "replace_warn": "Kiekviena pažymėta kategorija ištrinama ir pakeičiama iš failo. Nepažymėtos kategorijos lieka nepaliestos.", "dest_reference_hint": "Importuoti ciklai tik pagerina programų atpažinimą ir niekada neveikia naudojimo/energijos statistikos.", - "dest_real_history_hint": "Importuoti ciklai laikomi šio prietaiso paties istorija ir papildo energijos/naudojimo statistiką. Naudokite perkeliant vieną prietaisą į naują įdiegimą.", + "dest_real_history_hint": "Importuoti ciklai laikomi paties šio įrenginio istorija ir įskaičiuojami į statistiką. Naudokite perkeldami prietaisą į naują įdiegimą.", "sug_muted": "Šio nustatymo daugiau nebesiūlys", "sug_mute_failed": "Nepavyko nutildyti pasiūlymo", "sug_unmuted_all": "Nutildyti pasiūlymai atstatyti", - "n_suggestions_muted": "{count} nutildyti; automatinis derintuvas jų nebesiūlys.", + "n_suggestions_muted": "{count} pasiūlymų nutildyta; automatinis derintuvas jų nebesiūlys.", + "n_suggestions_muted_one": "{count} pasiūlymas nutildytas; automatinis derintuvas jo nebesiūlys.", + "n_suggestions_muted_few": "{count} pasiūlymai nutildyti; automatinis derintuvas jų nebesiūlys.", + "n_suggestions_muted_many": "{count} pasiūlymo nutildyta; automatinis derintuvas jų nebesiūlys.", + "n_suggestions_muted_other": "{count} pasiūlymų nutildyta; automatinis derintuvas jų nebesiūlys.", "font_size_hint": "Padidinkite arba sumažinkite visą tekstą šiame skydelyje. Galioja jūsų paskyrai šiame įrenginyje.", - "import_history_description": "Išmanųjį kištuką turėjote dar prieš WashData? Įkelkite jo galios sensoriaus istorijos eksportą arba nuskaitykite ją tiesiai iš Home Assistant, ir įprastas aptikimas peržiūrės tuos duomenis, todėl ankstesni ciklai atsiras jūsų „Ciklai“ sąraše ir juos bus galima pavadinti.", - "hist_input_hint": "Įkelkite CSV, atsisiųstą iš „Istorijos“ skydelio (objektas, būsena, paskutinis pakeitimas), arba leiskite WashData nuskaityti sensoriaus istoriją tiesiogiai. Tada aptikimas vykdomas lygiai taip pat kaip realiu laiku, o jūs pasirenkate, kuriuos rastus ciklus palikti.", - "hist_recorder_hint": "Nuskaito nuo jūsų pasirinktos datos iki dabar. Home Assistant pagal numatytuosius nustatymus išsamią istoriją saugo 10 dienų, o vėliau tik valandinius vidurkius, kurie yra per stambūs, kad iš jų būtų galima aptikti ciklus - senesnę datą rinkitės tik tuo atveju, jei jūsų recorder nustatytas saugoti ilgiau.", - "hist_scanning": "Jūsų istorija leidžiama per detektorių. Tai vykdoma fone - galite užverti šį dialogą ir sugrįžti vėliau.", + "import_history_description": "Išmanųjį kištuką turėjote dar prieš WashData? Įkelkite jo galios istoriją arba nuskaitykite ją iš Home Assistant. Per ją paleidžiamas aptikimas, o ankstesni ciklai atsiranda „Ciklai“ sąraše, paruošti pavadinti.", + "hist_input_hint": "Įkelkite CSV iš „Istorijos“ skydelio (objektas, būsena, paskutinis pakeitimas) arba leiskite WashData nuskaityti jutiklio istoriją tiesiogiai. Per ją paleidžiamas realaus laiko aptikimas, o jūs pasirenkate, kuriuos ciklus palikti.", + "hist_recorder_hint": "Nuskaito nuo pasirinktos datos iki dabar. Home Assistant pagal numatytuosius nustatymus išsamią istoriją saugo 10 dienų; senesni valandiniai vidurkiai per stambūs ciklams aptikti.", + "hist_scanning": "Jūsų istorija fone leidžiama per detektorių. Galite užverti šį dialogą ir sugrįžti vėliau.", "hist_imported_count": "Importuota ciklų: {n}.", "hist_duplicates": "Jau anksčiau importuota ir praleista: {n}.", "hist_capped": "Pasiektas šio įrenginio importuotų ciklų limitas; likusieji neišsaugoti.", @@ -1095,25 +1052,76 @@ "hist_scan_capped": "Rodomi tik pirmieji kandidatai (rasta: {n}).", "hist_recorder_empty": "Home Assistant neturi išsamios šio sensoriaus istorijos tuo laikotarpiu.", "hist_scan_failed": "Skenavimas nepavyko.", - "hist_scan_expired": "To skenavimo rezultatų jau nėra. Nuskenuokite dar kartą.", + "hist_scan_expired": "Šio skenavimo rezultatų nebė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ą.", - "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ą.", + "imported_history_readonly": "Rastas importuotoje galios istorijoje. Padeda atpažinti programas, bet neįskaičiuojamas į jūsų statistiką, jo negalima apkarpyti ar padalyti. Paspauskite „Pažymėti“, kad nurodytumėte programą.", + "odometer_intro": "Šis bendras skaičius tik didėja. Ištrynus ciklą jis nesumažėja, todėl aptarnavimo priminimai lieka tikslūs.", + "odometer_correct_hint": "Nustatykite, jei WashData užskaitė ciklą, kurio nebuvo, arba prietaisas atliko ciklų dar prieš įdiegiant WashData.", "no_reminders_set": "Kol kas nenustatyta jokių aptarnavimo priminimų.", "toast_program_armed": "Programa nustatyta kitam ciklui: {program}", "toast_range_required": "Nustatykite tinkamą Nuo/Iki diapazoną.", - "no_profiles_option": "(profilių dar nėra, pridėkite)" + "no_profiles_option": "(profilių dar nėra, pridėkite)", + "compare_cycles_title": "Palyginti ciklus ({count})", + "edit_builtin_phase": "Tai integruota fazė. Išsaugojus sukuriama pasirinktinė kopija; ištrynę kopiją atkursite originalą.", + "fetch_error": "Nepavyko įkelti duomenų.", + "connection_lost": "Ryšys nutrūko - rodomi {time} duomenys.", + "pg_notify_partial": "Žymos rodo tik pradžios, „beveik baigta“, pabaigos, etapų ir tylos valandų pranešimus. Tiesioginiai atnaujinimai, iškrovimo priminimai ir užsitęsimo įspėjimai nemodeliuojami.", + "connect_to_confirm": "Prisijunkite nustatymų krumpliaratyje, kad galėtumėte patvirtinti ar įvertinti.", + "coverage_gap_cluster": "Naujausi nepažymėti ciklai ({count}), trunkantys apie {min} min, atrodo kaip viena programa, kurios dar nesukūrėte.", + "pg_label_yes": "Ciklo pabaigoje būtų pažymėtas kaip „{name}“", + "pg_label_no": "Būtų prašoma peržiūros: {reason}", + "live_match_debug_tip": "Pasitikėjimas: kaip tiksliai dabartinė galios kreivė atitinka geriausią kandidatinį profilį (0-100%). Neaiški: dviejų geriausių kandidatų balai skiriasi mažiau nei 5% - žyma neaiški, kol ciklas nesibaigs.", + "automation_alias_started": "{device}: ciklas prasidėjo", + "automation_alias_finished": "{device}: ciklas baigėsi", + "automation_desc_started": "Vykdoma, kai prasideda {device} ciklas. Pridėkite savo veiksmus (pranešimai, šviesos, ...).", + "automation_desc_finished": "Vykdoma, kai baigiasi {device} ciklas. Pridėkite savo veiksmus (pranešimai, šviesos, ...).", + "feedback_detected": "WashData aptiko: {name}", + "import_undo_hint": "Saugomas atkūrimo taškas, sukurtas prieš paskutinį importą ({when}): {profiles} profilių ir {cycles} ciklų.", + "replace_undo_hint": "Pirmiausia išsaugomas atkūrimo taškas, todėl šį importą galėsite anuliuoti skiltyje Eksportas / Importas.", + "pg_in_sample_note": "Šie ciklai padėjo sukurti profilius, su kuriais lyginami, todėl rezultatai optimistiniai: naujiems ciklams tikėkitės šiek tiek mažiau teisingų atitikčių.", + "hist_tail_trimmed": "Nukirpta {min} min laukimo po ciklo pabaigos, kaip tai daro ir veikianti integracija", + "hist_truncated": "sustota ties {n} rodmenų riba, todėl likusi istorija nenuskaityta", + "hist_warn_kw": "Didžiausias rodmuo tik {w}. Panašu, kad šis jutiklis praneša kilovatais; WashData reikia vatų, todėl ciklų aptikti neįmanoma.", + "pg_compare_profile_tip": "Tik pasirenkamas profilis, rodomas ant grafiko. Simuliacija visada pati atpažįsta ciklo programą, kaip ir veikianti integracija.", + "pg_outcome_stale": "Nuo šios simuliacijos paleidimo nustatymai pasikeitė. Joje vis dar rodomas senas rezultatas.", + "pg_sweep_guarded": "Daugiau ciklų baigia per anksti, daugiau jų padalija arba praleidžia ciklus, kuriuos aptinka dabartinis nustatymas, todėl niekada nerekomenduojama.", + "pg_sweep_keep_current": "Palikite dabartinę reikšmę ({value}, {metric}): jokia reikšmė nebuvo geresnė bent vienu ciklu.", + "pg_task_busy": "Šiam įrenginiui jau vykdoma „Bandyti su istorija“ arba „Optimizuoti“ užduotis. Palaukite, kol ji baigsis, arba ją atšaukite.", + "hist_warn_naive_time": "{n} laiko žymų neturi laiko juostos, todėl jos nuskaitytos kaip UTC. Jei faile naudojamas vietos laikas, ciklai paslinkti per jūsų UTC poslinkį, o rodmenys ties vasaros laiko perjungimu gali būti atmesti.", + "hist_warn_naive_time_one": "{n} laiko žyma neturi laiko juostos, todėl ji nuskaityta kaip UTC. Jei faile naudojamas vietos laikas, ciklai paslinkti per jūsų UTC poslinkį, o rodmenys ties vasaros laiko perjungimu gali būti atmesti.", + "hist_warn_naive_time_few": "{n} laiko žymos neturi laiko juostos, todėl jos nuskaitytos kaip UTC. Jei faile naudojamas vietos laikas, ciklai paslinkti per jūsų UTC poslinkį, o rodmenys ties vasaros laiko perjungimu gali būti atmesti.", + "hist_warn_naive_time_many": "{n} laiko žymos neturi laiko juostos, todėl jos nuskaitytos kaip UTC. Jei faile naudojamas vietos laikas, ciklai paslinkti per jūsų UTC poslinkį, o rodmenys ties vasaros laiko perjungimu gali būti atmesti.", + "hist_warn_naive_time_other": "{n} laiko žymų neturi laiko juostos, todėl jos nuskaitytos kaip UTC. Jei faile naudojamas vietos laikas, ciklai paslinkti per jūsų UTC poslinkį, o rodmenys ties vasaros laiko perjungimu gali būti atmesti.", + "pg_sweep_n_cycles": "remiantis {count} ciklų", + "pg_sweep_n_cycles_one": "remiantis {count} ciklu", + "pg_sweep_n_cycles_few": "remiantis {count} ciklais", + "pg_sweep_n_cycles_many": "remiantis {count} ciklo", + "pg_sweep_n_cycles_other": "remiantis {count} ciklų", + "cycle_context_loading": "Įkeliama recorder istorija…", + "cycle_context_note": "Pilka: galios jutiklio recorder istorija aplink šį ciklą. Ji nėra ciklo dalis ir neturi įtakos trukmei, energijai ar atitikimui.", + "cycle_context_none": "Recorder neturi rodmenų prieš šį ciklą ir po jo (Home Assistant pagal numatytuosius nustatymus saugo 10 dienų istoriją).", + "cycle_context_tip": "Tik rodymui: rodo galios jutiklio recorder istoriją aplink šį ciklą, jei ji dar saugoma. Norėdami nepavykusius paleidimo bandymus pridėti prie būsimų ciklų, naudokite „Kreivės įžanginės sekundės“ (Nustatymai > Aptikimas > Ciklo pradžia); tai pakeičia išsaugotas trukmes.", + "invalid_threshold_pair": "Sustabdymo slenkstis ({stop} W) turi būti mažesnis už Paleidimo slenkstį ({start} W). Niekas neišsaugota; keiskite abu kartu." }, "phase_desc": { "anti_crease": "Po pagrindinio ciklo būgnas intermitiškai sukasi, kad išvengtų audinių raukšlių.", "anti_wrinkle": "Po pagrindinio ciklo būgnas intermitiškai sukasi, kad išvengtų audinių raukšlių.", + "baking": "Kaitinimas aukšta temperatūra: susidaro pluta ir minkštimas.", + "cooking": "Pagrindinė gaminimo fazė su įjungtu kaitintuvu ir ventiliatoriumi.", "cool_down": "Vartymas be karščio artėjant ciklo pabaigai.", + "cool_down_af": "Vėsinimas tik ventiliatoriumi po kaitinimo.", "drain_and_switch": "Perėjimo iš skalbimo į džiovinimo režimą laikotarpis.", "dry": "Džiovinimo etapas naudojant šildytuvą ir (arba) likutinę šilumą.", "drying": "Pagrindinis kaitinimo laikotarpis.", "heat_up": "Pradinis šildytuvo pašildymas prieš pradedant visišką džiovinimą.", + "keep_warm": "Silpnas kaitinimas, kad maistas liktų šiltas.", + "keep_warm_bm": "Silpnas kaitinimas, kad duona po kepimo liktų šilta.", + "kneading": "Variklis maišo ir minko tešlą. Didelė naudojama galia.", + "pause_af": "Trumpa pauzė supurtyti arba patikrinti.", + "pre_heat": "Pradinis kameros įkaitinimas prieš gaminimą.", "pre_rinse": "Pradinis purškimas prieš skalbimą plovikliu.", + "proving": "Kildinimas silpnoje šilumoje: mielės rūgsta, tešla kyla.", + "resting": "Trumpa mažos galios pauzė tarp minkymų, kad atsipalaiduotų glitimas.", "pre_wash": "Pirminis mirkymas arba paruošiamasis apdorojimas prieš pagrindinį skalbimą.", "rinse": "Skalavimo švariu vandeniu etapas. Ši fazė gali kartotis kelis kartus.", "sanitize": "Aukštos temperatūros valymo etapas, skirtas dezinfekavimo programoms.", @@ -1126,6 +1134,7 @@ "wash_dw": "Pagrindinis ploviklis, skalbimas su šildymu." }, "placeholder": { + "custom_task": "pvz., Valyti durelių sandariklį", "add": "pridėti…", "group_name": "pvz. Medvilnė 2:47", "json_buttons": "[{\"action\":\"ID\",\"title\":\"Label\"}]", @@ -1153,10 +1162,10 @@ "section": { "dishwasher": { "label": "Indaplovė", - "intro": "Ciklo pabaigos apdorojimas indaplovėms, kurios paprastai baigiasi ilga, beveik tylia džiovinimo faze prieš trumpą galutinį vandens išleidimą." + "intro": "Pabaigos apdorojimas indaplovėms, kurios dažnai ilgai tyliai džiovina prieš trumpą galutinį vandens išleidimą." }, "anti_wrinkle": { - "intro": "Apsaugos nuo raukšlių režimas aptinka mažos galios sukimosi impulsus po pagrindinės fazės ir prideda juos prie baigto ciklo.", + "intro": "Po pagrindinės fazės vykstančius mažos galios džiovyklės būgno sukimo impulsus palieka baigto ciklo dalimi.", "label": "Apsauga nuo raukšlių" }, "basic": { @@ -1164,7 +1173,7 @@ "label": "Pagrindiniai" }, "delay": { - "intro": "Atidėto paleidimo aptikimas identifikuoja, kai prietaisas įjungtas, bet ciklas dar neprasidėjo.", + "intro": "Aptinka įjungtą prietaisą, laukiantį atidėto paleidimo.", "label": "Atidėtas paleidimas" }, "detection": { @@ -1188,161 +1197,153 @@ "label": "Laikas ir stebėjimas" }, "triggers": { - "intro": "Pasirenkami išoriniai signalai: pabaigos trigeris, durų jutiklis, pauzės jungiklis ir iškrovimo priminimas.", + "intro": "Pasirenkami priedai: pabaigos trigeris, durų jutiklis, pauzės jungiklis ir iškrovimo priminimas.", "label": "Trigeriai ir durys" - }, - "phase_eta": { - "label": "Likęs laikas", - "intro": "Į fazes atsižvelgiantis likęs laikas mašinoms, kurių ciklo trukmė priklauso nuo temperatūros ar gręžimo." } }, "setting": { "dishwasher_end_spike_quiet_release": { "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." + "doc": "Kiek laiko po numatytos trukmės indaplovė turi išlikti žemiau Sustabdymo slenksčio, kol WashData nustoja laukti galutinio vandens išleidimo. Padidinkite, jei praleidžiamas vėlyvas išleidimas po ilgo džiovinimo. Laukimas niekada neviršija 30 minučių." }, "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ą.", + "doc": "Programos vidutinės trukmės dalis, kurią ciklas turi pasiekti, kad jo mažos galios būgno sukimas būtų laikomas apsaugos nuo raukšlių uodega. Džiovyklėms su jutikliniu džiovinimu sumažinkite (pvz., 0,75). Skalbyklėms palikite numatytąją (0,98), antraip skalbimas gali baigtis pusiaukelėje.", "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.", + "doc": "Trumpus mažos galios džiovyklės būgno impulsus po pagrindinės šildymo fazės laikyti baigto ciklo dalimi, o ne naujais ciklais.", "label": "Įjungti apsaugą nuo raukšlių" }, "anti_wrinkle_exit_power": { - "doc": "Kad raukšlių mažinimo režimas išliktų aktyvus, tarp impulsų galia turi būti mažesnė.", + "doc": "Apsaugos nuo raukšlių režimu galia, mažesnė už šią, laikoma pauze tarp sukimo impulsų, o pauzė, ilgesnė nei Maks. tarpas tarp impulsų, režimą baigia. Jei Sustabdymo slenkstis didesnis, naudojamas jis, todėl šis nustatymas ką nors keičia tik tada, kai nustatytas virš Sustabdymo slenksčio.", "label": "Išėjimo galios slenkstis" }, "anti_wrinkle_idle_timeout": { - "doc": "Kiek ilgai mašina gali tylėti tarp dviejų būgninių impulsų, kol baigiasi apsaugos nuo raukšlių režimas. Nustatykite ilgesnį nei ilgiausias tarpas, kurį jūsų džiovyklė palieka tarp impulsų, antraip kiekvienas vėlesnis impulsas bus laikomas klaidingu paleidimu.", + "doc": "Kiek ilgai džiovyklė gali tylėti tarp būgno impulsų, kol baigiasi apsaugos nuo raukšlių režimas. Nustatykite ilgesnį nei ilgiausias jūsų džiovyklės tarpas, antraip vėlesni impulsai bus laikomi klaidingais paleidimais.", "label": "Maks. tarpas tarp impulsų" }, "anti_wrinkle_max_duration": { - "doc": "Ilgesni impulsai laikomi tikru ciklu, o ne raukšlių mažinimu.", + "doc": "Ilgesnis impulsas laikomas tikru ciklu, o ne apsaugos nuo raukšlių sukimu.", "label": "Maksimali trukmė" }, "anti_wrinkle_max_power": { - "doc": "A pulse above this power is treated as a real new cycle, not an anti-wrinkle tumble. Nustatykite tiesiai virš būgninio impulso galios.", + "doc": "Impulsas, viršijantis šią galią, laikomas tikru nauju ciklu, o ne būgno sukimu. Nustatykite šiek tiek virš būgno impulsų galios.", "label": "Maks. apsaugos nuo raukšlių galia" }, "auto_label_confidence": { - "doc": "Jei atitikties rezultatas ciklo pabaigoje yra lygus arba didesnis, programa automatiškai pažymima be jokio patvirtinimo raginimo. Padidinkite jį, kad būtų reikalaujama didesnio tikrumo prieš automatinį ženklinimą; sumažinkite jį, kad daugiau automatizuotų. Veikia kartu su žemiau esančia mokymosi pasitikėjimo funkcija.", + "doc": "Atitikties balas ciklo pabaigoje, nuo kurio programa pažymima neklausiant. Padidinkite, jei norite daugiau tikrumo; sumažinkite, kad daugiau ciklų būtų pažymima automatiškai. Laikykite jį virš Mokymosi pasitikėjimo.", "label": "Automatinio žymėjimo pasitikėjimas" }, "auto_maintenance": { - "doc": "Naktį tvarkykite: perkurkite profilio vokus, perskaičiuokite ciklo būseną, apkarpykite derinimo pėdsakus ir išsaugokite naujausius ciklus.", + "doc": "Naktinis valymas: pažymi senus ciklus, kurie dabar aiškiai atitinka programą, iš naujo sudaro programų kreives, atnaujina anomalijų žymes ir ciklų sveikatą, o išjungus \"Įrašyti derinimo pėdsakus\" pašalina derinimo pėdsakus. Ciklai ir jų galios įrašai visada išsaugomi.", "label": "Automatinė priežiūra (naktinis valymas)" }, "completion_min_seconds": { - "doc": "Trumpesni nei šis ciklai atmetami kaip vaiduokliniai ciklai (bandomieji važiavimai, atidarant dureles, norint įdėti kojines).", + "doc": "Trumpesni ciklai (bandomieji paleidimai, durelių atidarymas įdėti kojinę) išsaugomi kaip nutraukti ir pabaigos pranešimo negauna.", "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.", + "doc": "Prie ciklo pradžios prideda rodmenis iš nepavykusių paleidimo bandymų; skirta mašinoms, kurios prieš pradėdamos kelis kartus pabando (durelių užraktas, pirmasis pripildymas). Daugumai užtenka 300 s; 0 = išjungta. Įrašyta pradžia paankstinama, todėl išmoktos trukmės kurį laiką svyruoja.", "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.", + "doc": "Galia turi tiek laiko išlikti budėjimo juostoje, kad prietaisas būtų laikomas laukiančiu paleidimo.", "label": "Patvirtinimo sekundės" }, "delay_start_detect_enabled": { - "doc": "Nustatykite, kada prietaisas yra įjungtas ir laukia (uždelstas paleidimas / budėjimo režimas), bet nepradėjo ciklo, todėl budėjimo režimo ištraukimas nėra painiojamas su veikimo ciklu.", + "doc": "Aptikti įjungtą prietaisą, laukiantį atidėto paleidimo, kad jo budėjimo režimo suvartojimas nebūtų palaikytas veikiančiu ciklu.", "label": "Įjungti atidėto paleidimo aptikimą" }, "delay_timeout_hours": { - "doc": "Po daugelio valandų nustokite laukti atidėto paleidimo režimu ir grįžkite į tuščiąja eiga, todėl mašina, kuri buvo palikta varoma, bet niekada neįjungta, nelaukia amžinai.", + "doc": "Po tiek valandų nustoti laukti atidėto paleidimo ir grįžti į tuščiąją eigą.", "label": "Skirtojo laiko valandos" }, "device_type": { - "doc": "Buitinės technikos klasė. Nustato protingus numatytuosius aptikimo nustatymus (slenksčius, išjungimo delsą, pabaigos tvarkymą), pritaikytas tam prietaiso tipui; pakeiskite jį tik tuo atveju, jei įrenginys iš pradžių buvo nustatytas kaip netinkamo tipo.", + "doc": "Nustato šio prietaiso tipo numatytuosius aptikimo nustatymus (slenksčius, išjungimo delsimą, pabaigos apdorojimą). Keiskite tik tada, jei įrenginys buvo nustatytas kaip netinkamo tipo.", "label": "Įrenginio tipas" }, "door_sensor_entity": { - "doc": "Pasirenkamas dvejetainis durų jutiklis. Naudojamas nustatyti, kada prietaisas buvo atidarytas/iškrautas po ciklo.", + "doc": "Pasirenkamas durų jutiklis. Rodo, kada prietaisas po ciklo buvo atidarytas iškrauti.", "label": "Durų jutiklio objektas" }, "duration_tolerance": { - "doc": "Likusio laiko įvertinimų tolerancija (mokomasis grįžtamasis ryšys, nesutapimas). Jei tikroji trukmė neviršija +/-X % apskaičiuoto, tai laikoma gera atitiktimi.", + "doc": "Tik mokymosi grįžtamajam ryšiui: ciklas, kurio trukmė neviršija +/-X% įvertintos, laikomas geru įverčiu. Atpažinimui įtakos neturi.", "label": "Įvertinimo tolerancija" }, "enable_ml_models": { - "doc": "Kol vyksta ciklas, leiskite modeliams patikslinti tiesioginius rezultatus: stabilesnį likusį laiką ir energijos / sąnaudų sąmatą bei apsaugą nuo priešlaikinio sustojimo aptikus pabaigą (ji gali tik atidėti finišą, niekada nesibaigti anksčiau ir yra ribota). Naudoja jūsų sureguliuotus modelius, kai jie yra, o kitu atveju įtaisytuosius. Išjungta = tik klasikinė galia pagrįsta logika (vis dar patikima).", + "doc": "Naudoti šiame aparate apmokytą modelį veikiančio ciklo energijai ir kainai prognozuoti, o gerai žinomai programai, kuri sustoja neįprastai anksti, leisti baigtis nelaukiant viso išjungimo delsimo. Išjungta: tik galia pagrįsta logika.", "label": "Taikyti išmaniuosius modelius ciklo metu" }, "end_energy_threshold": { - "doc": "Išjungimo delsos atgalinio skaičiavimo metu sukaupta energija (vatai x laikas) lyginama su šia riba. Jei viršijamas, atgalinis skaičiavimas nustatomas iš naujo – prie ciklo lieka pritvirtintos nesiglamžančios būgninės ir indaplovės džiovinimo uodegėlės, o ne trumpinamos. Pakelkite, jei ciklai baigiasi per anksti vėsinimo metu; sumažinkite jį, jei aptikimas yra vangus.", + "doc": "Energija, leidžiama per išjungimo delsimą. Jei jos daugiau, ciklas tęsiasi, todėl būgno sukimai ir džiovinimo uodegos lieka prijungti. Sumažinkite, jei ciklai baigiasi uodegos metu; padidinkite, jei baigti ciklai vis dar laukia.", "label": "Pabaigos energija" }, - "end_repeat_count": { - "doc": "Iš eilės mažesnių nei sustojimo slenksčio rodmenų skaičius, reikalingas iki ciklo pabaigos. 1 tinka daugeliui kištukų. Padidinkite iki 2–3, jei jūsų išmanusis kištukas ciklo viduryje kartais praneša apie klaidingą nulį, o ciklai baigiasi anksčiau laiko.", - "label": "Pabaigos pakartojimų sk." - }, "energy_price_entity": { - "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.", + "doc": "Jutiklis su esama kaina už kWh (pvz., dinaminis tarifas). Jis turi pirmenybę prieš toliau nurodytą statinę kainą, todėl turi būti kaina, o ne kištuko galios ar energijos jutiklis.", "label": "Energijos kainos objektas" }, "energy_price_static": { - "doc": "Fiksuota kaina už kWh, naudojama sąnaudų skaičiams, kai aukščiau nenustatytas joks tiesioginės kainos subjektas.", + "doc": "Fiksuota kaina už kWh, naudojama, kai nenustatytas kainos objektas.", "label": "Statinė energijos kaina (už kWh)" }, "energy_sensor": { - "doc": "Neprivalomas kaupiamasis energijos skaitiklis (total_increasing kWh/Wh, pvz., paties kištuko viso laikotarpio skaitiklis). Kai nustatytas, kiekvieno ciklo pateikiama energija imama iš šio skaitiklio rodmens pokyčio nuo pradžios iki pabaigos, taip išvengiama per mažo skaičiavimo, kurį gaunate integruodami lėtai duomenis pateikiantį galios jutiklį. Grįžta prie integruotos reikšmės, jei rodmens nėra, jo vienetas nežinomas arba skirtumas nėra teigiamas. Palikite tuščią, kad būtų ir toliau integruojamas galios jutiklis.", + "doc": "Pasirenkamas kWh ar Wh skaitiklis (total_increasing, pvz., paties kištuko skaitiklis). Tada ciklo energija imama iš jo, o tai tiksliau nei skaičiuoti iš lėto galios jutiklio. Jei jo rodmuo netinkamas, naudojama apskaičiuota reikšmė.", "label": "Energijos skaitiklio objektas" }, "expose_debug_entities": { - "doc": "Paskelbkite papildomus diagnostinius HA objektus (atitikties patikimumą, dviprasmiškumą, būsenos vidines savybes). Išjungta objektų sąrašas išlieka švarus normaliam naudojimui.", + "doc": "Pridėti diagnostinius objektus (atitikties patikimumas, dviprasmiškumas, vidinė būsena). Išjungus objektų sąrašas lieka švarus.", "label": "Rodyti derinimo objektus" }, "external_end_trigger": { - "doc": "Dvejetainis jutiklis, kurio būsenos pasikeitimas žymi ciklo pabaigą (pvz., prietaiso „užbaigtas“ kontaktas arba papildomas integravimas).", + "doc": "Dvejetainis jutiklis, kurio pasikeitimas žymi ciklo pabaigą (pvz., „baigta“ kontaktas ar kita integracija).", "label": "Išorinio trigerio objektas" }, "external_end_trigger_enabled": { - "doc": "Tegul išorinis dvejetainis jutiklis signalizuoja apie ciklo pabaigą, neskaitant integruoto galia pagrįsto aptikimo.", + "doc": "Leisti dvejetainiam jutikliui pranešti apie ciklo pabaigą kartu su galia pagrįstu aptikimu.", "label": "Įjungti išorinį pabaigos trigerį" }, "external_end_trigger_inverted": { - "doc": "Trigerio jutiklio išsijungimą (o ne ĮJUNGTA) laikykite ciklo pabaigos signalu.", + "doc": "Pabaigos signalu laikyti jutiklio išsijungimą, o ne įsijungimą.", "label": "Invertuoti išorinį trigerį (OFF)" }, "learning_confidence": { - "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ą.", + "doc": "Mažiausias atitikties balas baigtam ciklui pažymėti. Aiškus nugalėtojas pažymimas; jei rezultatai artimi, ciklas patenka į peržiūros eilę skiltyje Ciklai. Žemiau šios reikšmės niekas nežymima ir neklausiama. Laikykite ją mažesnę už Automatinio žymėjimo pasitikėjimą.", "label": "Mokymosi pasitikėjimas" }, "linked_device": { - "doc": "Pasirinktinai įdėkite šį „WashData“ įrenginį į kitą įrenginį (pvz., išmanųjį kištuką) HA įrenginių registre, rodomą kaip „Prisijungta per...“.", + "doc": "Nebūtina. Rodyti šį WashData įrenginį po kitu įrenginiu (pvz., išmaniuoju kištuku) Home Assistant, kaip „Prisijungta per ...“.", "label": "Grupuoti po įrenginiu" }, "min_off_gap": { - "doc": "Jei aparatas išsijungia trumpiau nei šį laiką, įjungimo/išjungimo/įjungimo seka laikoma vienu nepertraukiamu ciklu. Neleidžia mirkymo programoms (mašina kelioms minutėms išsijungia skalbimo viduryje) padalyti į du atskirus ciklus. Jei norite, kad apkrovos būtų skaičiuojamos kaip atskiri ciklai, nustatykite jį trumpesnį nei atstumas tarp apkrovų iš vienos pusės į kitą. Įrenginio tipo numatytieji nustatymai apsaugo tipinę kiekvieno prietaiso ciklo pauzę.", + "doc": "Trumpesnis išsijungimas ciklo nepadalija, todėl mirkymo pauzės lieka viename skalbime. Laikykite jį trumpesnį nei tarpas tarp viena po kitos paleidžiamų skalbimų, antraip jie susilies į vieną ciklą.", "label": "Min. išjungimo tarpas" }, "min_power": { - "doc": "Absoliuti minimali galia laikoma aktyvia. Žemiau esantys rodmenys traktuojami kaip 0 W (parengties režimas), filtruojant fantominę išmaniųjų kištukų ir budėjimo šviesos diodų apkrovą.", + "doc": "Žemesni rodmenys laikomi 0 W (budėjimo režimas). Filtruoja išmaniųjų kištukų ir budėjimo šviesos diodų suvartojimą.", "label": "Minimali galia" }, "ml_training_enabled": { - "doc": "Periodiškai per naktį ištirkite peržiūrėtus ciklus ir tiksliai suderinkite modelius su šia konkrečia mašina. Pokyčiai išlaikomi tik tada, kai jie iš tikrųjų pasiekia geresnių rezultatų per užlaikytus ciklus, todėl tai gali tik padėti arba išlikti nepakitusi – niekada neregresuoti.", + "doc": "Kartkartėmis iš naujo apmokyti energijos modelį pagal šio aparato baigtus ciklus. Naujas modelis paliekamas tik tada, kai patikros cikluose pranoksta paprastą įvertį.", "label": "Mokytis iš šio įrenginio" }, "ml_training_hour": { - "doc": "Vietinė paros valanda (0–23), kad atliktumėte nakties koregavimą. Pasirinkite ramią valandą, pvz., 2 (02:00).", + "doc": "Paros valanda (0-23) naktiniam mokymui. Pasirinkite ramią valandą, pvz., 2.", "label": "Mokymosi valanda" }, "ml_training_interval_days": { - "doc": "Dar kartą patikrinkite, ar nėra patobulinimų, bent kartą per tiek dienų.", + "doc": "Mokyti iš naujo ne dažniau kaip kartą per tiek dienų.", "label": "Maks. tikrinimo intervalas" }, "ml_training_min_cycles": { - "doc": "Palaukite, kol bus įrašyta bent tiek ciklų, kad galėtumėte tiksliai derinti, kad būtų iš ko pasimokyti.", + "doc": "Prieš mokymą palaukti, kol bus įrašyta bent tiek ciklų.", "label": "Reikalingi ciklai" }, "name": { - "doc": "Rodomas pavadinimas, rodomas HA integracijų sąraše ir įrenginių registre.", + "doc": "Šio įrenginio pavadinimas, rodomas Home Assistant.", "label": "Įrenginio pavadinimas" }, "no_update_active_timeout": { - "doc": "Jei veikiant tiek ilgai negaunama jokių maitinimo atnaujinimų, manykite, kad kištukas nukrito neprisijungus, ir priverstinai sustabdykite, kad išvengtumėte zombių ciklo. Numatytoji 600 s leidžia debesies arba tinklelio delsą.", + "doc": "Jei ciklo metu tiek laiko negaunama galios atnaujinimų, laikoma, kad kištukas atsijungė, ir ciklas priverstinai sustabdomas. Numatytoji 600 s reikšmė atsižvelgia į debesies ar mesh tinklo delsas.", "label": "Be atnaujinimų skirtasis laikas" }, "notify_before_end_minutes": { @@ -1350,35 +1351,35 @@ "label": "Įspėjimas prieš pabaigą" }, "notify_channel": { - "doc": "„Android“ pranešimų kanalo pavadinimas, skirtas pradžios / tiesioginiams pranešimams (valdo kiekvieno kanalo garsą ir prioritetą programoje mobiliesiems). Blank naudoja numatytąją papildomą programą.", + "doc": "Android kanalas pradžios ir tiesioginiams pranešimams; kanalas nustato garsą ir prioritetą. Tuščia: programėlės numatytasis.", "label": "Android kanalas (paleidimas/tiesioginis)" }, "notify_cycle_timers": { - "doc": "Pranešimai tam tikromis ciklo minutėmis (pvz., norint pridėti minkštiklio). Pranešimas palaiko {device}, {program}, {minutes}. Įjunkite automatinį pristabdymą, kad pristabdytumėte tuo metu ir gautumėte interaktyvų pranešimą su mygtuku Tęsti; tęsti per skydelį, pristabdymo / atnaujinimo paslaugą arba pranešimo veiksmą.", + "doc": "Pranešimai nustatytomis ciklo minutėmis (pvz., įpilti minkštiklio). Pranešime galima naudoti {device}, {program}, {minutes}. Su automatiniu pristabdymu ciklas tuo metu ir pristabdomas, o pranešime atsiranda mygtukas „Tęsti“.", "label": "Ciklo laikmačiai" }, "notify_finish_channel": { - "doc": "„Android“ pranešimų kanalo pavadinimas, skirtas užbaigimo pranešimui. Blank pakartotinai naudoja pradžios / tiesioginį kanalą.", + "doc": "Android kanalas pabaigos pranešimui. Tuščia: naudojamas pradžios / tiesioginių pranešimų kanalas.", "label": "Android kanalas (baigimas)" }, "notify_finish_message": { - "doc": "Tekstas, siunčiamas ciklui pasibaigus. Šablono kintamieji: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}. Čia naudingiausi {time_finished} ir {vs_typical}.", + "doc": "Tekstas, siunčiamas ciklui pasibaigus. Kintamieji: {device}, {program}, {duration} (minutės), {duration_hm} (1 h 05 min), {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}, {status} (completed arba force_stopped; nutraukti ciklai nieko nesiunčia). Su bet kuriuo kitu kintamuoju naudojamas numatytasis tekstas.", "label": "Baigimo žinutė" }, "notify_finish_services": { - "doc": "notify.* paslaugos iškviečiamos, kai ciklas baigiasi. Pridėkite po vieną kiekvienam tikslui; palikite tuščią, kad nepraneštumėte apie pabaigą.", + "doc": "notify.* paslaugos, iškviečiamos ciklui pasibaigus. Palikite tuščią, jei nereikia.", "label": "Baigimo paslaugos" }, "notify_fire_events": { - "doc": "Taip pat suaktyvinkite ha_washdata_* įvykius ciklo pradžioje / pabaigoje, kad galėtumėte kurti savo automatizavimą.", + "doc": "Taip pat skelbti ha_washdata_* įvykius ciklo pradžioje ir pabaigoje, kad galėtumėte kurti savo automatizavimus.", "label": "Paleisti HA įvykius pranešimams" }, "notify_icon": { - "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.", + "doc": "Maža piktograma pranešime (pvz., mdi:washing-machine). Siunčiama tik mobile_app gavėjams, ne notify grupėms. iOS ji pakeičia programėlės piktogramą; tam reikia programėlės 2026.8 ar naujesnės versijos. Tuščia: platformos numatytoji.", "label": "Pranešimo piktograma" }, "notify_live_chronometer": { - "doc": "Pranešime rodykite tiesioginį atvirkštinės atskaitos laikmatį (jį palaikančiose platformose), o ne statinį įvertinimą.", + "doc": "Pranešime rodyti tiesioginį atgalinės atskaitos laikmatį (kur palaikoma), o ne fiksuotą įvertį.", "label": "Naudoti tiesioginį chronometrą" }, "notify_live_interval_seconds": { @@ -1386,143 +1387,135 @@ "label": "Tiesioginio atnaujinimo intervalas" }, "notify_live_overrun_percent": { - "doc": "Jei ciklas viršija savo įvertinimą daugiau nei šiuo procentu, išsiųskite įspėjimą apie viršijimą.", + "doc": "Tiesioginiai atnaujinimai sustoja, kai ciklas viršija numatomą trukmę šiuo procentu (20 = iki 1,2x numatomo laiko).", "label": "Tiesioginio viršijimo % įspėjimas" }, "notify_live_services": { - "doc": "notify.* paslaugos reikalavo tiesioginių eigos atnaujinimų, kol vyksta ciklas. Palikite tuščią, kad išjungtumėte tiesioginės eigos pranešimus.", + "doc": "notify.* paslaugos tiesioginiams eigos atnaujinimams ciklo metu. Palikite tuščią, kad juos išjungtumėte.", "label": "Tiesioginės pažangos paslaugos" }, "notify_only_when_home": { - "doc": "Siųskite pranešimus tik tada, kai bent vienas iš susietų žmonių (aukščiau) yra namuose.", + "doc": "Pranešti tik tada, kai bent vienas iš aukščiau nurodytų žmonių yra namuose.", "label": "Pranešti tik kai namuose" }, "notify_people": { - "doc": "asmuo.* subjektai, kuriuos naudoja „Pranešti tik tada, kai namuose“, kad nuspręstų, ar kas nors yra namuose.", + "doc": "person.* objektai, kuriuos tikrina „Pranešti tik kai namuose“.", "label": "Žmonės (kai namuose)" }, "notify_pre_complete_message": { - "doc": "Įspėjimo prieš pabaigą / beveik baigta tekstas. Šablono kintamieji: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Kiekvieno tiesioginio eigos atnaujinimo tekstas. Šablono kintamieji: {device}, {program}, {minutes} (likusios minutės). Su bet kuriuo kitu kintamuoju naudojamas numatytasis tekstas.", "label": "Žinutė prieš baigimą" }, "notify_reminder_message": { - "doc": "Vis dar laukiančio iškrovimo priminimo tekstas. Tuščia naudoja įtaisytąją numatytąją reikšmę. Šablono kintamieji: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Vienkartinio įspėjimo prieš pabaigą tekstas. Šablono kintamieji: {device}, {program}, {minutes} (nustatymas „Įspėjimas prieš pabaigą“). Su bet kuriuo kitu kintamuoju naudojamas numatytasis tekstas.", "label": "Priminimo žinutė" }, "notify_start_message": { - "doc": "Tekstas, siunčiamas ciklui prasidėjus. Šablono kintamieji: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Tekstas, siunčiamas ciklui prasidėjus. Šablono kintamieji: {device}, {program}. Su bet kuriuo kitu kintamuoju naudojamas numatytasis tekstas.", "label": "Paleidimo žinutė" }, "notify_start_services": { - "doc": "pranešti.* paslaugos iškviečiamos, kai prasideda ciklas. Pridėkite vieną prie kiekvieno tikslo (telefono, prietaisų skydelio ir kt.); palikite tuščią, kad nepradtumėte pranešimo.", + "doc": "notify.* paslaugos, iškviečiamos ciklui prasidėjus (telefonas, prietaisų skydelis, ...). Palikite tuščią, jei nereikia.", "label": "Paleidimo paslaugos" }, "notify_timeout_seconds": { - "doc": "Po tiek sekundžių automatiškai atsisakykite pranešimo (jį palaikončiose platformose). 0 išsaugo jį tol, kol atmetamas rankiniu būdu.", + "doc": "Uždaryti pranešimą po tiek sekundžių (kur palaikoma). 0 palieka jį, kol uždarysite patys.", "label": "Automatinis uždarymas po" }, "notify_title": { - "doc": "Pranešimo pavadinimas. Šablono kintamieji: {device}, {program}, {duration}, {energy}, {cost}, {date}, {time}, {minutes}.", + "doc": "Pranešimo pavadinimas. Įstatomas tik {device}; su bet kuriuo kitu kintamuoju naudojamas numatytasis pavadinimas.", "label": "Pranešimo pavadinimas" }, "notify_unload_delay_minutes": { - "doc": "Minutės po ciklo pabaigos prieš siunčiant vis dar laukiantį priminimą „iškrauti mašiną“. Norėdami išjungti priminimą, nustatykite 0.", + "doc": "Minutės po ciklo pabaigos iki priminimo „iškrauti mašiną“. 0 jį išjungia.", "label": "Iškrovimo priminimo delsimas" }, "notify_unload_repeat": { - "doc": "Iškrovimo priminimas pakartotinai siunčiamas kas „Iškrovimo priminimo delsimas“ minučių, kol iškrovimas nebus patvirtintas arba pranešime nepaliesite „Nustoti priminti“. Reikalingas būdas patvirtinti iškrovimą: Durų jutiklio objektas, Iškrovimo patvirtinimo objektas arba žemiau esanti parinktis „Patvirtinti iškrovimą rankiniu būdu“. Atmetimo mygtukas veikia Home Assistant mobiliosios (companion) programėlės pranešimuose.", + "doc": "Kartoti iškrovimo priminimą kas „Iškrovimo priminimo delsimas“ minučių, kol iškrovimas patvirtinamas arba paliečiate „Nustoti priminti“ (mobilioji programėlė). Reikalingas Durų jutiklio objektas, Iškrovimo patvirtinimo objektas arba „Patvirtinti iškrovimą rankiniu būdu“.", "label": "Kartoti, kol bus iškrauta" }, "off_delay": { - "doc": "Laikas palaukti, kai nukrenta galia, prieš paskelbiant, kad ciklas baigtas. Jei maitinimas atnaujinamas šiame lange, ciklas tęsiasi sklandžiai – tai sujungia pauzes tarp plovimo etapų. Indaplovės turi ilgas džiovinimo fazes (maitinimas išjungiamas 20–60 min.), todėl išjungimo delsa turi būti didesnė už tą, kad visas plovimas+džiovinimas išliktų kaip vienas ciklas.", + "doc": "Kiek laiko galia turi išlikti žema, kol ciklas laikomas baigtu. Jei per tą laiką galia grįžta, ciklas tęsiasi. Indaplovės džiovindamos gali būti neaktyvios 20-60 min, todėl nustatykite ilgiau.", "label": "Išjungimo delsimas" }, "pause_cuts_power": { - "doc": "Kai ciklas pristabdytas, taip pat išjunkite toliau pateiktą jungiklio objektą. Tik prietaisams, kurių kištuką galima saugiai atpjauti ciklo viduryje.", + "doc": "Pristabdžius ciklą, išjungti ir toliau nurodytą Jungiklio objektą. Tik prietaisams, kurie gali saugiai netekti maitinimo ciklo viduryje.", "label": "Pauzė taip pat išjungia galią (per jungiklį)" }, "power_off_delay": { - "doc": "Kiek laiko galia turi išlikti žemiau Maitinimo išjungimo slenksčio pasibaigus ciklui, kol būsena grįžta į Išjungta. Naudojama tik tada, kai Maitinimo išjungimo slenkstis didesnis nei 0. Tikrinama pagal foninį ciklą, todėl faktinis delsimas suapvalinamas iki kitos būsenos galiojimo pabaigos žymos.", + "doc": "Kiek laiko po ciklo galia turi išlikti žemiau Maitinimo išjungimo slenksčio, kol būsena grįžta į Išjungta. Naudojama tik tada, kai tas slenkstis didesnis nei 0. Tikrinama periodiškai, todėl tikrasis laukimas gali būti šiek tiek ilgesnis.", "label": "Maitinimo išjungimo delsimas" }, "power_off_threshold_w": { - "doc": "Pasirenkamas galia pagrįstas Išjungta būsenos aptikimas. Kai reikšmė didesnė nei 0 ir ciklas baigėsi, o galia išlieka žemiau šio lygio Maitinimo išjungimo delsimo laiką, laikoma, kad mašina išjungta, ir būsena grįžta į Išjungta. Norėdami išjungti, palikite 0 (numatytoji reikšmė). Nustatykite ją aukščiau tikrojo išjungtos būsenos lygio, bet žemiau Sustabdymo slenksčio ir jūsų mašinos baigto-bet-įjungto budėjimo režimo suvartojimo; jei ji nėra žemiau Sustabdymo slenksčio, ji ignoruojama. Kai įjungta, grįžtant į Išjungta ji pakeičia Pažangos atstatymo delsimą, todėl baigusi mašina lieka Baigta/Švarus būsenoje, kol iš tikrųjų neišjungiama.", + "doc": "Jei po ciklo galia Maitinimo išjungimo delsimo laiką išlieka žemiau šios ribos, būsena tampa Išjungta (to nebedaro Pažangos atstatymo delsimas). Nustatykite aukščiau išjungto prietaiso suvartojimo, bet žemiau budėjimo režimo ir Sustabdymo slenksčio. 0 išjungia (numatytoji).", "label": "Maitinimo išjungimo slenkstis" }, "power_sensor": { - "doc": "Jutiklio subjektas, pranešantis šio prietaiso tiesioginę galią vatais (pvz., sensor.washer_power). Visas ciklo aptikimas pagrįstas šiuo signalu.", + "doc": "Jutiklis, pranešantis esamą šio prietaiso galią vatais (pvz., sensor.washer_power). Juo pagrįstas visas aptikimas.", "label": "Galios jutiklis" }, - "profile_duration_tolerance": { - "doc": "+/- juosta aplink vidutinę profilio trukmę, naudojamą derinant. 0,25 reiškia, kad 60 minučių profilis atitinka 45–75 minučių ciklus.", - "label": "Profilio trukmės tolerancija" - }, "profile_match_interval": { - "doc": "Kaip dažnai bandyti suderinti profilį bėgimo ciklo metu. Numatytoji 300 s (5 minutės) subalansuoja aptikimo greitį ir procesorių.", + "doc": "Kaip dažnai veikiantį ciklą lyginti su profiliais. Numatytoji 300 s (5 min).", "label": "Atitikimo intervalas" }, "profile_match_max_duration_ratio": { - "doc": "Maksimalus ciklo ilgis, palyginti su profiliu. 1.3 reiškia, kad ciklas turi būti mažesnis nei 130 % profilio trukmės, kad atitiktų.", + "doc": "Programa atmetama, kai ciklas trunka ilgiau nei jos įprasta trukmė, padauginta iš šios reikšmės. 1,8: ciklas, 80 % ilgesnis už programą, jos nebeatitinka.", "label": "Maks. trukmės santykis" }, "profile_match_min_duration_ratio": { - "doc": "Minimalus ciklo ilgis, palyginti su profiliu. 0,9 reiškia, kad ciklas turi sudaryti bent 90 % profilio trukmės, kad atitiktų.", + "doc": "Programa svarstoma tik tada, kai ciklas pasiekia šią jos įprastos trukmės dalį. 0,10: 2 valandų programa tampa kandidate po 12 minučių. Netaikoma per pirmąsias 15 ciklo minučių.", "label": "Min. trukmės santykis" }, "profile_match_threshold": { - "doc": "Kad būtų priimtas programos identifikavimas, ciklo pabaigoje būtinas minimalus panašumo balas (0–1). Pakelkite jį, kad sumažintumėte klaidingų identifikacijų skaičių; sumažinkite jį, jei jūsų mašinos programos nesuderinamos. Numatytasis 0,4 yra konservatyvus atskaitos taškas.", + "doc": "Tiesioginės atitikties balas, kurio ciklui reikia, kad išmanusis užbaigimas ar apsaugos nuo raukšlių užbaigimas galėtų jį užbaigti anksčiau. Rodomos programos jis nekeičia. Padidinkite, jei silpnos atitiktys per anksti užbaigia ciklus; sumažinkite, jei aiškios atitiktys laukia išjungimo delsimo.", "label": "Atitikimo slenkstis" }, "profile_unmatch_threshold": { - "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.", + "doc": "Jei tiesioginės atitikties balas nukrenta žemiau šios reikšmės, dabartinis programos spėjimas išvalomas. Laikykite šiek tiek žemiau Atitikimo slenksčio, kad trumpas kritimas jo nepanaikintų.", "label": "Neatitikimo slenkstis" }, "progress_reset_delay": { - "doc": "Baigę palaikykite 100 % pažangą tiek laiko, kad prieš iš naujo nustatant į neveiklumą prietaisų skydeliuose būtų rodoma baigta.", + "doc": "Po ciklo tiek laiko rodyti 100 % pažangą, prieš grįžtant į būseną „Tuščia eiga“.", "label": "Pažangos atstatymo delsimas" }, "pump_stuck_duration": { - "doc": "Po kelių sekundžių siurblys gali veikti nepertraukiamai, kol jis bus pažymėtas kaip galimai užstrigęs (įjungiamas įstrigusio siurblio įvykis).", + "doc": "Kiek ilgai siurblys gali veikti be pertraukos, kol suveikia įstrigusio siurblio įvykis.", "label": "Siurblio įstrigimo trukmė" }, "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.", + "doc": "Mažiausias laikas tarp apdorojamų rodmenų. Anksčiau gauti rodmenys praleidžiami (žemi rodmenys ciklo metu visada išsaugomi). Nustatykite lygų savo kištuko ataskaitų intervalui arba mažesnį; didesnė reikšmė tik išmeta rodmenis.", "label": "Mėginių ėmimo intervalas" }, "save_debug_traces": { - "doc": "Išsaugokite visą galios pėdsaką ir atitinkamus kiekvieno ciklo derinimo duomenis. Naudinga trikčių šalinimui, bet padidina saugyklos dydį.", + "doc": "Su kiekvienu ciklu taip pat saugoti atitikimo modulio derinimo duomenis (kiekvieno kandidato balus) trikčių šalinimui. Prideda apie 20 KB kiekvienam ciklui.", "label": "Įrašyti derinimo pėdsakus" }, "smart_termination_duration_ratio": { - "doc": "Kaip toli į atitiktos programos numatytą trukmę ciklas turi būti pažengęs, kad išmanusis užbaigimas galėtų jį užbaigti anksčiau, kai sumažėja galia. Numatyta trukmė yra programos vidurkis, todėl prietaisuose, kurių veikimo laikas labai svyruoja - skalbyklės su šaltu žiemos ir šiltu vasaros tiekiamu vandeniu, jutiklinio džiovinimo džiovyklės, nuo apkrovos priklausančios programos - maždaug pusė visų paleidimų baigiasi trumpiau nei šis vidurkis ir niekada nesulaukia greito užbaigimo, o baigiasi tik per atsarginį skirtąjį laiką, kelias minutes vėliau. Sumažinkite tai (pvz., 0,85) tose mašinose, kad ankstyvas užbaigimas vis tiek suveiktų; padidinkite jį link 1,0, kad veiktų atsargiau. Palikite tuščią numatytajai vertei (0,98 arba 0,99 indaplovėms). Jis gali užbaigti ciklą tik anksčiau, niekada ne vėliau, ir niekada nesuveikia esant neaiškiai ar mažo patikimumo atitikčiai.", + "doc": "Programos vidutinės trukmės dalis, kurią ciklas turi pasiekti, kad išmanusis užbaigimas galėtų jį užbaigti anksčiau. Jei veikimo trukmė svyruoja (vandens temperatūra, jutiklinis džiovinimas, krūvio dydis), sumažinkite (pvz., 0,85). Palikite tuščią, kad būtų 0,98 (indaplovėms 0,99).", "label": "Išmaniojo užbaigimo santykis" }, - "smoothing_window": { - "doc": "Kiek išlyginamas neapdorotos galios signalas. Žemas (2) yra jautrus, bet triukšmingas; aukštas (5) išlygina smailes, bet padidina atsilikimą.", - "label": "Išlyginimo langas" - }, "start_duration_threshold": { - "doc": "Galia turi taip ilgai išlikti virš paleidimo slenksčio, kad būtų patvirtintas tikras startas, kad sekundės įjungimo/išjungimo jungikliai nepradėtų ciklo.", + "doc": "Galia tiek laiko turi išlikti virš Paleidimo slenksčio, kol prasideda ciklas, kad trumpi įjungimo ir išjungimo šuoliai būtų ignoruojami.", "label": "Paleidimo trukmė" }, "start_energy_threshold": { - "doc": "Energijos (galia x laikas) prietaisas turi sunaudoti prieš VEIKIANT. Trumpas didelės galios smaigalys turi labai mažai energijos ir yra ignoruojamas, užkertant kelią klaidingam paleidimui.", + "doc": "Energija, kurią prietaisas turi sunaudoti, kad ciklas būtų laikomas veikiančiu. Trumpas galios šuolis sunaudoja mažai energijos, todėl ciklo nepradeda.", "label": "Paleidimo energija" }, "start_threshold_w": { - "doc": "Galia turi pakilti virš šio lygio, kad būtų patvirtintas ciklas. Nustačius per žemą, atsiranda klaidingas paleidimas iš budėjimo režimo maitinimo; per aukštai ir lėtai paleidžiamos programos (šaltas užpildymas) praleidžiamos. Pasiūlymo variklis nustato tai šiek tiek aukščiau už pastebėtą mažiausią mašinos aktyviąją galią.", + "doc": "Galia turi pakilti virš šios ribos, kad prasidėtų ciklas. Per žema: budėjimo galia sukelia klaidingus paleidimus. Per aukšta: lėtai įsibėgėjančios programos (šaltas pripildymas) praleidžiamos.", "label": "Paleidimo slenkstis" }, "stop_threshold_w": { - "doc": "Prieš prasidedant išjungimo delsos skaičiavimui, galia turi nukristi žemiau šio lygio. Nustatykite jį žemiau pradžios slenksčio – tarpas tarp jų yra histerezės juosta, kuri neleidžia mirgėti. Jei nustatyta per didelė, mažos galios fazės (skalavimo sulaikymo, glamžymo) klaidingai suaktyvina pabaigos seką.", + "doc": "Galia turi nukristi žemiau šios ribos, kad prasidėtų išjungimo delsimo skaičiavimas. Laikykite ją žemiau Paleidimo slenksčio. Per aukšta: mažos galios fazės (skalavimo pauzės, apsauga nuo raukšlių) per klaidą pradeda pabaigos atskaitą.", "label": "Sustabdymo slenkstis" }, "switch_entity": { - "doc": "Pasirenkamas jungiklis išjungiamas pristabdant ir vėl įjungiamas, kai tęsiamas, naudojamas kartu su „Pauzė taip pat atjungia maitinimą“.", + "doc": "Pasirenkamas jungiklis, išjungiamas pristabdžius ir vėl įjungiamas tęsiant, kai įjungta „Pauzė taip pat išjungia galią“.", "label": "Jungiklio objektas" }, "watchdog_interval": { - "doc": "Kaip dažnai foninis sergėtojas tikrina, ar neužstrigo jutikliai ir ar nėra laiko. Numatytasis 30 s.", + "doc": "Kaip dažnai foninis patikrinimas ieško užstrigusių jutiklių ir pasibaigusio skirtojo laiko. Numatytoji 30 s.", "label": "Stebėjimo intervalas" }, "notify_milestone_message": { @@ -1530,127 +1523,82 @@ "label": "Etapo pranešimas" }, "notify_milestones": { - "doc": "Kableliais atskirti ciklų skaičiai, kuriuos pasiekus vieną kartą siunčiamas šventinis pranešimas (pvz., 50, 100, 500, 1000). Tuščia išjungia etapų pranešimus.", + "doc": "Ciklų skaičiai, kuriuos pasiekus vieną kartą siunčiamas šventinis pranešimas, atskirti kableliais (pvz., 50, 100, 500, 1000). Tuščia: išjungta.", "label": "Ciklų etapai" }, "notify_quiet_end_hour": { - "doc": "Netrukdymo lango pabaiga (0-23). Sulaikyti pranešimai pristatomi šią valandą. Palikite tuščią, kad išjungtumėte.", + "doc": "Netrukdymo lango pabaiga (0-23); tada išsiunčiami sulaikyti pranešimai. Tuščia: išjungta.", "label": "Tylos valandų pabaiga" }, "notify_quiet_start_hour": { - "doc": "Netrukdymo lango pradžia (0-23). Pabaigos, priminimo ir švarių skalbinių pranešimai, kurie suveiktų tylos valandomis, sulaikomi ir pristatomi langui pasibaigus. Palikite tuščią, kad išjungtumėte. Palaikomi langai, kertantys vidurnaktį (pvz., pradžia 22, pabaiga 7).", + "doc": "Netrukdymo lango pradžia (0-23). Pabaigos, priminimo ir švarių skalbinių pranešimai šiame lange sulaikomi iki jo pabaigos. Gali kirsti vidurnaktį (pvz., nuo 22 iki 7). Tuščia: išjungta.", "label": "Tylos valandų pradžia" }, "peak_rate_message": { - "doc": "Neprivalomas pasirinktinis tekstas piko tarifo patarimui, pridedamam prie paleidimo pranešimo. Šablono kintamieji: {device}, {price}. Tuščia naudoja įtaisytąją numatytąją reikšmę.", + "doc": "Pasirinktinis piko tarifo patarimo tekstas. Šablono kintamieji: {device}, {price}. Tuščia: numatytasis.", "label": "Piko tarifo pranešimas" }, "peak_rate_threshold": { - "doc": "Kai ciklas prasideda ir dabartinė kaina už kWh yra lygi šiai reikšmei ar didesnė, prie paleidimo pranešimo pridedamas piko tarifo patarimas. 0 arba tuščia išjungia patarimą.", + "doc": "Jei ciklui prasidedant kaina už kWh yra lygi šiai reikšmei ar didesnė, prie pradžios pranešimo pridedamas piko tarifo patarimas. 0 arba tuščia išjungia.", "label": "Piko tarifo slenkstis (už kWh)" }, - "dtw_bandwidth": { - "label": "Formos atitikties tolerancija", - "doc": "Kiek derinimo modulis gali laike iškraipyti ciklą, kad jis atitiktų išsaugotos programos formą. Didesnė reikšmė leidžia didesnį greičio kitimą; per didelė gali priskirti ne tą programą." - }, - "corr_weight": { - "label": "Formos ir lygio svoris", - "doc": "Pusiausvyra tarp kreivės formos ir absoliučiojo galios lygio vertinant atitiktį. Didesnė reikšmė teikia pirmenybę formai; mažesnė, galios lygio atitikimui." - }, - "duration_weight": { - "label": "Trukmės svoris", - "doc": "Kaip stipriai teikiama pirmenybė programai, kurios tipinė trukmė atitinka šį ciklą." - }, - "energy_weight": { - "label": "Energijos svoris", - "doc": "Kaip stipriai teikiama pirmenybė programai, kurios tipinės energijos sąnaudos atitinka šį ciklą." - }, "store_brand": { "label": "Prietaiso prekės ženklas", - "doc": "Nebūtina. Prietaiso prekės ženklas, pasirenkamas iš bendruomenės katalogo. Naudojamas atitinkamiems etaloniniams įrašams rasti ir bendrinti. Palikite tuščią, jei nenaudojate interneto funkcijų." + "doc": "Nebūtina. Jūsų prietaiso prekės ženklas iš bendruomenės katalogo, naudojamas etaloniniams įrašams rasti ir bendrinti. Palikite tuščią, jei nenaudojate interneto funkcijų." }, "store_model": { "label": "Prietaiso modelis", - "doc": "Nebūtina. Prietaiso modelis, pasirenkamas iš bendruomenės katalogo, kai nustatytas prekės ženklas. Jei jūsų modelio sąraše nėra, galite jį įtraukti į katalogą." + "doc": "Nebūtina. Jūsų prietaiso modelis iš bendruomenės katalogo (pirmiausia pasirinkite prekės ženklą). Jei jo sąraše nėra, galite jį pridėti." }, "show_contributor": { "doc": "Rodyti \"autorius: \" priskyrimą bendruomenės prietaisuose ir etaloniniuose cikluose." }, - "enable_phase_matching": { - "label": "Į fazes atsižvelgiantis likęs laikas", - "doc": "Padalykite kiekvieną vykstantį ciklą į fazes (šildymas, plovimas, gręžimas) ir paskirstykite likusį laiką kiekvienai fazei, sumaišydami su klasikiniu įverčiu - ciklo pradžioje remdamiesi fazių paskirstymu, o pabaigoje - klasikiniu įverčiu. Tai pritaiko laikmatį prie to, kiek laiko jūsų mašina iš tikrųjų kaista ir veikia, o tai labiausiai pastebima pirmoje ciklo pusėje. Išjungta = tik klasikinis įvertis. Paveikiamas tik likusio laiko rodinys; programų atitikimas ir ciklų aptikimas nekeičiami." - }, - "keep_min_score": { - "label": "Min. atitikties balas", - "doc": "Žemiausias panašumo balas, kurio kandidatui reikia likti derinimo procese. Numatytoji 0,1 yra sąmoningai leistina – ji leidžia net silpnus kandidatus ir pasitiki vėlesniais etapais rasti geriausią atitiktį. Padidinkite, kad anksčiau filtruotumėte mažai tikėtinus profilius; sumažinkite pradiniam kandidatų telkiniui išplėsti." - }, - "dtw_blend": { - "label": "DTW mišinys", - "doc": "Kiek DTW lygiavimo balas pakeičia 2 etapo pagrindinį balą. 0 = naudokite tik 2 etapo balą, 1 = naudokite tik DTW balą, 0,5 (numatytoji) = lygus mišinys. Padidinkite, kad labiau pasikliauti laiko lygiavimo, kai programos turi panašius galios lygius, bet skirtingus laiko modelius." - }, - "dtw_ensemble_w": { - "label": "DTW ansamblio mišinys", - "doc": "Ansamblio DTW režime (numatytoji) tai yra skalieroto L1 svoris lyginant su išvestiniu DTW (DDTW). 1,0 = tik skalierota L1, 0 = tik DDTW, 0,7 = numatytoji. Skalierota variacija lygina galios lygius; išvestinė variacija reaguoja į tai, kaip galia keičiasi bėgant laikui. Ansamblis derina abi." - }, - "dtw_ddtw_scale": { - "label": "Išvest. DTW skalė", - "doc": "Pusiau prisotinimo atstumas išvestiniam DTW vertinimui. Balas perpus sumažėja, kai DDTW atstumas lygus šiai reikšmei. Mažesnis = jautresnis formos skirtumams. Numatytoji 30 sukalibruota tipiškai buitinių prietaisų galios kreivėms. Veikia tik ansamblio ir ddtw DTW režimus." - }, - "dtw_refine_top_n": { - "label": "DTW tikslinimo skaičius", - "doc": "Kiek geriausių 2 etapo kandidatų DTW perskaičiuoja balus. DTW yra brangesnė operacija, todėl taikoma tik N geriausiems. Numatytoji 5. Padidinkite (iki 7-9), jei teisingas profilis kartais po 2 etapo patenka tik į 4 arba 5 vietą – DTW gali jį išgelbėti. Mažesnė reikšmė šiek tiek pagreitina derinimą." - }, - "duration_scale": { - "label": "Trukmės ryškumas", - "doc": "4 etapo trukmės atitikimo bausmės ryškumas. Tai yra logaritmas, ties kuriuo atitikimo balas perpus sumažėja. Mažesnis = griežtesnis: trukmės neatitikimas kenkia labiau. Numatytoji 0,175 atitinka maždaug 18% trukmės toleranciją ties pusiau svoriu. Naudokite su Trukmės svoriu." - }, - "energy_scale": { - "label": "Energijos ryškumas", - "doc": "4 etapo energijos atitikimo bausmės ryškumas. Mažesnis = griežtesnis: energijos neatitikimas kenkia labiau. Numatytoji 0,25 sąmoningai atlaidesnė nei trukmės ryškumas, nes energija kinta priklausomai nuo apkrovos. Naudokite su Energijos svoriu." - }, "power_profile_interval_min": { "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." + "doc": "Atributo power_profile atkarpos dydis (vidutiniai vatai per atkarpą), naudojamas planuoklių, pvz., EMHASS ir tibber_prices. Mažesnė atkarpa išlaiko trumpus šuolius, didesnė lygina kreivę. Numatytoji 15 min. Aptikimui įtakos neturi." }, "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ą." + "doc": "Tik iOS. Tiesioginę eigą atnaujinti tyliai, bateriją tausojančiu prioritetu, kurį iOS gali grupuoti. Pirmasis atnaujinimas ir pradžios bei pabaigos pranešimai vis tiek būna su garsu. Išjungta: su garsu kiekvienas atnaujinimas." }, "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." + "doc": "Tik Android. Paspaudus palikti tiesioginės eigos pranešimą. Išjungta: paspaudus jis uždaromas." }, "notify_live_click_action": { "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ų." + "doc": "Kas atidaroma paspaudus šio prietaiso pranešimą. Tuščia: WashData skydelis su šiuo prietaisu. Įveskite prietaisų skydelio kelią (pvz., /lovelace/laundry) ar URL, arba „none“, kad paspaudimas nieko nedarytų." }, "door_opens_at_end": { "label": "Durys automatiškai atsidaromos pabaigoje", - "doc": "Indaplovėms, kurios ciklo pabaigoje atidaro duris džiovinimui (AirDry ir panašūs). Įjungus, durų atidarymas veikiančio ciklo metu nebepristabdo jo amžinai; vietoj to, jei durys lieka atviros toliau nurodytą laikotarpį, WashData traktuoja ciklą kaip baigtą. Trumpas atidarymas (dedant indą) ignoruojamas. Reikalingas durų jutiklio objektas." + "doc": "Indaplovėms, kurios pabaigoje atidaro duris džiovinimui (pvz., AirDry). Jei durys lieka atviros toliau nurodytą laiką, ciklas užbaigiamas, o ne pristabdomas; trumpas atidarymas ignoruojamas. Reikalingas Durų jutiklio objektas." }, "door_end_dwell_seconds": { "label": "Durų atidarymo pabaigos laukimas", - "doc": "Kiek laiko durys turi likti atviros, kad WashData užbaigtų ciklą, kai įjungta parinktis \"Durys automatiškai atsidaromos pabaigoje\". Pakankamai ilgai, kad ignoruotų greitai dedamą indą (numatytasis 60 s), pakankamai trumpai, kad greitai užbaigtų, kai prietaisas atidaro duris." + "doc": "Kai įjungta \"Durys automatiškai atsidaromos pabaigoje\": kiek laiko durys turi likti atviros, kad ciklas baigtųsi. Pakankamai ilgai, kad būtų ignoruojamas indo įdėjimas (numatytoji 60 s)." }, "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." + "doc": "Kokių rūšių ciklai formuoja kiekvienos programos kreivę ir atpažinimą. Atžymėjus rūšį niekas neištrinama; naudinga, jei nepasitikite importuotais duomenimis. Statistikoje visada skaičiuojami tik šio aparato ciklai. Visų rūšių atžymėjimas ignoruojamas." }, "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." + "doc": "Kiekvieną ciklą įkainoti pagal kainą, galiojusią kiekvieną jo veikimo akimirką, o ne pagal kainą jam pasibaigus. Reikalingas kainos objektas. Veiksmas Apdoroti istoriją (Diagnostika) perskaičiuoja senesnių ciklų kainą." }, "unload_confirm_entity": { "label": "Iškrovimo patvirtinimo objektas", - "doc": "Pasirenkamas objektas, kurio suveikimas reiškia, kad turinys išimtas; skirtas mašinoms, prie kurių negalima pritaisyti durų jutiklio. Tinka bet koks: Zigbee ar NFC mygtukas, input_button pagalbininkas, judesio jutiklis priešais mašiną, scena. Patvirtinimu laikomas bet koks būsenos pasikeitimas, išskyrus išjungimą ir tapimą nepasiekiamu. Nustačius šį objektą, iškrovimo priminimas įjungiamas ir įrenginiui, kuris neturi Durų jutiklio objekto." + "doc": "Nebūtina. Bet koks objektas, patvirtinantis, kad turinys išimtas, mašinoms be durų jutiklio (mygtukas, input_button, judesio jutiklis, scena). Skaičiuojamas kiekvienas būsenos pasikeitimas, išskyrus išjungimą ar nepasiekiamumą. Taip pat įjungia iškrovimo priminimą." }, "unload_track_without_door": { "label": "Patvirtinti iškrovimą rankiniu būdu", - "doc": "Naudoti iškrovimo priminimą įrenginyje, kuris neturi nei Durų jutiklio objekto, nei Iškrovimo patvirtinimo objekto, o iškrovimą patvirtinti sava automatika: paspauskite mygtuko objektą „Pažymėti kaip iškrautą“ arba iškvieskite paslaugą ha_washdata.mark_unloaded. Ignoruojama, kai nustatytas Durų jutiklio objektas, nes jis šį signalą jau teikia." + "doc": "Naudoti iškrovimo priminimą be durų jutiklio ar patvirtinimo objekto. Iškrovimą patvirtinkite savo automatizavimu per mygtuką „Pažymėti kaip iškrautą“ arba paslaugą ha_washdata.mark_unloaded. Ignoruojama, kai nustatytas Durų jutiklio objektas." }, "notify_icon_color": { "label": "Pranešimo spalva", - "doc": "Akcentinė spalva šio prietaiso pranešimams, kad skalbimo mašiną, džiovintuvą ir indaplovę būtų galima atskirti iš pirmo žvilgsnio. iOS nuspalvina piktogramą ir Live Activity eigos juostą užrakto ekrane bei Dynamic Island. Android siunčiama ta pati spalva, tačiau ką su ja daryti, sprendžia pats telefonas: vieni nuspalvina piktogramą, o daugelis (ypač Samsung One UI) ją visiškai ignoruoja. Abi platformos supranta hex kodą (pvz., #03A9F4). Palikite tuščią, kad būtų naudojama numatytoji platformos spalva." + "doc": "Šio prietaiso pranešimų akcento spalva, kad skalbimo mašiną, džiovyklę ir indaplovę būtų lengva atskirti. iOS ji užpildo apskritimą už piktogramos ir nuspalvina Live Activity juostą. Daugelis Android telefonų (ypač Samsung) ją ignoruoja. Naudokite hex, pvz., #03A9F4. Tuščia: platformos numatytoji." + }, + "share_name": { + "doc": "Rodyti paskyros vardą kaip jūsų bendrinamų ciklų įkėlėjo. Išjungta: bendrinama be vardo." } }, "setting_group": { @@ -1678,9 +1626,6 @@ "door_pause": { "label": "Durys ir pauzė" }, - "dtw_refinement": { - "label": "DTW tikslinimas" - }, "duration_gates": { "label": "Trukmės vartai" }, @@ -1757,13 +1702,18 @@ "rebuilding": "Perkuriama…", "analyzing": "Analizuojama…", "resetting": "Atstatoma…", - "reverting": "Grąžinama…", "trimming": "Apkarpoma…", "splitting": "Padalijama…", + "stalled": "Užstrigo", "deleting": "Trinama…", "imported": "Importuota", "preparing": "Ruošiama…", - "unlabelling": "Naikinamas žymėjimas…" + "unlabelling": "Naikinamas žymėjimas…", + "working": "Vykdoma…", + "match_uncertain": "Neaišku: {top} ar {runner}", + "match_uncertain_single": "Neaišku: galbūt {top}", + "match_sure": "~{pct} % tikrumas", + "match_uncertain_tip": "WashData dar nenusprendė, kuri tai programa. Skaičius rodo, kaip dažnai pirmaujantis spėjimas, {top}, ankstesniuose cikluose pasitvirtindavo, kai pirmavo tiek pat. Tik informacija." }, "tab": { "advanced": "Išplėstinė", @@ -1795,13 +1745,17 @@ "toast": { "catalog_refreshed": "Bendruomenės katalogas atnaujintas", "contrib_device_changed": "Pridėta į bendruomenės parduotuvę, bet, kol forma buvo atidaryta, pasikeitė pasirinktas prietaisas. Pasirinkite jį nustatymuose.", + "task_name_required": "Suteikite užduočiai pavadinimą", + "task_added": "Užduotis pridėta", + "task_save_failed": "Nepavyko išsaugoti užduoties: {error}", + "task_removed": "Užduotis pašalinta", + "maint_logged": "Pažymėta kaip atlikta: {task}", "access_saved": "Prieigos valdymas išsaugotas", "all_wiped": "Visi duomenys ištrinti", "analysis_complete_none": "Analizė baigta: jokių naujų pasiūlymų", "auto_label_complete": "Automatinis žymėjimas baigtas", "automation_deleted": "Automatika ištrinta", "correction_submitted": "Pateiktas pataisymas", - "cycle_deleted": "Ciklas ištrintas", "cycle_force_stopped": "Ciklas priverstinai sustabdytas", "cycle_labelled": "Ciklas pažymėtas", "cycle_paused": "Ciklas pristabdytas", @@ -1817,11 +1771,9 @@ "group_deleted": "Grupė ištrinta", "group_name_required": "Būtinas grupės pavadinimas", "group_saved": "Grupė išsaugota", - "import_successful": "Sėkmingas importas; integracijos perkrovimas", "json_required": "Reikalingi JSON duomenys", "legacy_removed": "Pašalinti pasenę veiksmai", "logs_exported": "Žurnalai eksportuoti", - "matching_reverted": "Atitinkantys svoriai grąžinti į numatytuosius", "min_2_profiles": "Pasirinkite bent 2 grupės profilius", "ml_training_no_improvement": "Mokymas baigtas: pradinis lygis išlaikytas (nėra pagerėjimo)", "models_reverted": "Įrenginyje esantys modeliai grįžo į pradinį lygį", @@ -1831,8 +1783,8 @@ "pg_preset_loaded": "Rinkinys „{name}“ įkeltas", "pg_preset_saved": "Rinkinys „{name}” išsaugotas", "pg_sugg_loaded": "Įkelta {n} siūlomų reikšmių - paleiskite bandymų aikštelę palyginti", - "pg_sugg_ml_loaded": "Įkelta {n} ML kalibruotų reikšmių - paleiskite bandymų aikštelę palyginti", "phase_name_required": "Būtinas fazės pavadinimas", + "phase_reset": "Atkurta įmontuota fazė „{name}“", "phase_updated": "Atnaujintas etapas", "phases_saved": "Fazės išsaugotos", "preferences_saved": "Parinktys išsaugotos", @@ -1879,14 +1831,10 @@ "store_name_required": "Įveskite profilio pavadinimą", "store_import_failed": "Nepavyko importuoti: {error}", "store_imported": "Importuota į {profile}", - "store_program_required": "Įveskite programos pavadinimą", "store_no_appliance": "Pirmiausia nustatymuose nurodykite prietaiso prekės ženklą ir modelį.", "store_share_failed": "Nepavyko bendrinti: {error}", "store_shared": "Bendrinta bendruomenės parduotuvėje - laukiama peržiūros.", "appliance_added": "Prietaisas pridėtas - laukiama patvirtinimo", - "thanks_confirming": "Ačiū už patvirtinimą", - "device_approved": "Bendruomenė patvirtino", - "rating_saved": "Kokybės įvertinimas išsaugotas", "brand_added": "Prekės ženklas pridėtas - laukiama patvirtinimo", "profile_added": "Profilis pridėtas - laukiama patvirtinimo", "saved_except_conflicts": "Išsaugota. Ištaisykite paryškintus konfliktus, kad išsaugotumėte likusį.", @@ -1909,11 +1857,54 @@ "odometer_saved": "Bendras ciklų skaičius išsaugotas", "odometer_save_failed": "Nepavyko išsaugoti bendro skaičiaus: {error}", "store_pick_profile_share": "Pasirinkite profilį, į kurį bendrinti", - "remove_failed": "Nepavyko pašalinti: {error}" + "remove_failed": "Nepavyko pašalinti: {error}", + "analysis_complete": "Analizė baigta: pasiūlymų: {count}", + "analysis_failed": "Nepavyko atlikti analizės: {error}", + "auto_detect_failed": "Nepavyko automatiškai aptikti: {error}", + "auto_label_failed": "Nepavyko automatiškai pažymėti: {error}", + "could_not_load_cycle": "Nepavyko įkelti ciklo: {error}", + "create_failed": "Nepavyko sukurti: {error}", + "cycles_deleted": "Ištrinta ciklų: {count}", + "debug_cleared": "Išvalyta derinimo pėdsakų: {count}", + "discard_failed": "Nepavyko išmesti: {error}", + "error": "Klaida: {error}", + "export_failed": "Nepavyko eksportuoti: {error}", + "import_failed": "Nepavyko importuoti: {error}", + "invalid_json": "„{key}“ nėra tinkamas JSON - pataisykite jį arba prieš išsaugodami išvalykite lauką.", + "label_failed": "Nepavyko pažymėti: {error}", + "ml_training_promoted": "Mokymas baigtas: aktyvuota modelių: {count}", + "pause_failed": "Nepavyko pristabdyti: {error}", + "phase_created": "Fazė „{name}“ sukurta", + "phase_deleted": "Fazė „{name}“ ištrinta", + "profile_created": "Profilis „{name}“ sukurtas", + "rename_failed": "Nepavyko pervadinti: {error}", + "save_failed": "Nepavyko išsaugoti: {error}", + "split_complete": "Padalinta į ciklus: {count}", + "start_failed": "Nepavyko paleisti: {error}", + "stop_failed": "Nepavyko sustabdyti: {error}", + "update_failed": "Nepavyko atnaujinti: {error}", + "store_download_skipped": "Praleista įrašų: {n} (per trumpi, su per daug spragų arba jau yra jūsų įrenginyje).", + "store_import_low_quality": "Šis įrašas per trumpas, turi per daug spragų arba yra neįtikimas, todėl jo negalima importuoti.", + "store_import_duplicate": "Šis įrašas jau yra jūsų įrenginyje.", + "device_approved": "Bendruomenė patvirtino", + "thanks_confirming": "Ačiū už patvirtinimą", + "rating_saved": "Kokybės įvertinimas išsaugotas", + "store_download_partial": "Nepavyko atsisiųsti programų: {n}. Bandykite vėliau dar kartą.", + "import_no_restore_point": "Importuota, bet nepavyko išsaugoti atkūrimo taško, todėl šio importo anuliuoti negalima", + "import_undo_failed": "Nepavyko anuliuoti importo: {error}", + "import_undone": "Importas anuliuotas: atkurta {profiles} profilių ir {cycles} ciklų", + "auto_label_cancelled": "Automatinis žymėjimas atšauktas pažymėjus {count} ciklų.", + "auto_label_cancelled_one": "Automatinis žymėjimas atšauktas pažymėjus {count} ciklą.", + "auto_label_cancelled_few": "Automatinis žymėjimas atšauktas pažymėjus {count} ciklus.", + "auto_label_cancelled_many": "Automatinis žymėjimas atšauktas pažymėjus {count} ciklo.", + "auto_label_cancelled_other": "Automatinis žymėjimas atšauktas pažymėjus {count} ciklų.", + "auto_label_done": "Automatinis žymėjimas baigtas: pažymėta {count} ciklų, be žymos liko {skipped}.", + "auto_label_done_one": "Automatinis žymėjimas baigtas: pažymėtas {count} ciklas, be žymos liko {skipped}.", + "auto_label_done_few": "Automatinis žymėjimas baigtas: pažymėti {count} ciklai, be žymos liko {skipped}.", + "auto_label_done_many": "Automatinis žymėjimas baigtas: pažymėta {count} ciklo, be žymos liko {skipped}.", + "auto_label_done_other": "Automatinis žymėjimas baigtas: pažymėta {count} ciklų, be žymos liko {skipped}." }, "suggestion": { - "both_agree": "WashData rekomenduoja", - "calibrated_label": "Sukalibruota", "observed_label": "Pastebėta", "impact": { "auto_label_confidence": { @@ -1928,10 +1919,6 @@ "higher": "Uždaro tik ciklus, kurie sunaudojo daug energijos", "lower": "Uždaro ir trumpesnius arba mažiau energijos naudojančius ciklus" }, - "end_repeat_count": { - "higher": "Reikalauja, kad pabaigos signalas kartotųsi daugiau kartų – patikimesnis, šiek tiek lėtesnis", - "lower": "Baigia po mažiau pasikartojimų – greitesnis aptikimas, didesnis klaidingo pabaigos pavojus" - }, "learning_confidence": { "higher": "Mokosi tik iš labai tikrų atitikimų – lėčiau, bet patikimiau", "lower": "Mokosi iš daugiau ciklų; modelis gali būti triukšmingesnis" @@ -1970,40 +1957,25 @@ } }, "reason": { - "auto_label_confidence": "15-ojo procentilio patikimumas {cycles} automatinių žymų, kurių naudotojas niekada netaisė ({p15}).", "completion_min_seconds": "Pusė p05 švarių ciklų trukmės ({minutes} min) iš {cycles} ciklų; atfiltruoja netikrus ciklus.{excl}", - "dead_zone_batch": "Paliktas trumpas (medianinis paleidimo nestabilumo langas per {cycles} ciklus = {s}s), kad tikra pabaiga būtų aptikta greitai.", - "dead_zone_from_dip": "Pagrįsta ankstyvu galios kryčiu, aptiktu ties {s}s.", - "end_energy_default": "Numatytoji rekomenduojama bazė ciklo pabaigos triukšmo slenksčiui.", - "end_energy_false": "p95 klaidingos pabaigos energija ({p95}Wh) per {cycles} ciklus, apribota iki 0.2% medianinės ciklo energijos ({floor}Wh).", - "end_energy_no_false": "Nėra klaidingų pabaigų per {cycles} ciklus; apribota iki 0.2% medianinės ciklo energijos ({floor}Wh).", - "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.", + "match_interval_floored": "Trumpiausia programa ({shortest}s) apribotų šią reikšmę iki {cap}s, o tai mažiau nei {minimum}s minimumas, todėl naudojamas minimumas: {persistence} atitiktys iš eilės užtrunka {budget}s, daugiau nei {pct}% tos programos. Atitikimas vykdomas tik gavus rodmenį (mediana={median}s), todėl tai 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.", "min_off_gap": "Pagrįsta {gaps} stebimais tarpcikliniais tarpais (p05={p05}s). Prietaiso apatinė riba: {floor}s.", "min_off_gap_bridge": "Nustatyta taip, kad perdengtų ilgiausią tylos tarpsnį ciklo viduje: {span}s + 60s buferis, iš {spans} perdengtų tarpų per {cycles} švarius ciklus (lieka žemiau trumpiausio jūsų tarpo tarp dviejų paleidimų iš eilės, kuris yra {ceiling}s).", "min_power": "40% p05 mažiausios aktyvios galios ({p05}W) per {cycles} švarius ciklus, išlaikant išjungimo slenkstį žemiau realaus suvartojimo.{excl}", - "ml_auto_label": "Mažiausias patikimumas, kurį kokybės modelis vis dar įvertino kaip švarų (p10 iš {cycles} švarių ciklų = {p10}).", - "ml_end_repeat": "{false}/{total} ciklų ({pct}%) turėjo pauzę, kurią pabaigos detektorius įvertino >50%; reikalauti {val} pabaigos patvirtinimo(-ų).", - "ml_off_delay": "Pabaigos detektoriaus patvirtintos pauzės: p95 {p95}s + 60s buferis, iš {pauses} modelio patvirtintų pauzių per {cycles} ciklus (apatinė riba {floor}s).", "no_update_timeout": "Pagrįsta stebimu atnaujinimo dažniu (p95={p95}s) * 20 (min. 60s).", "off_delay_cadence": "Pagrįsta stebimu atnaujinimo dažniu (p95={p95}s) * 5", "off_delay_device_floor": "Naudotas prietaisui {device} būdingas saugus minimumas ({floor}s).", "off_delay_generic_floor": "Naudotas bendrinis saugus minimumas ({floor}s).", "off_delay_pauses": "Nustatyta taip, kad viršytų realias pauzes: p95 ciklo vidinė pauzė {p95}s + 60s buferis, iš {pauses} pauzių per {cycles} švarius ciklus (apatinė riba {floor}s).", - "profile_match_threshold": "p10 patikimumas {cycles} teisingų automatinių žymų ({p10}); saugi tiesioginio patvirtinimo riba.", - "sampling_interval": "Medianinis atnaujinimo intervalas, stebėtas per {cycles} švarius ciklus ({si}s).{excl}", - "smoothing_window": "Nustatyta taip, kad išlygintų ~30s rodmenų esant {si}s diskretizavimui ({samples} pavyzdžiai).", - "start_duration": "Paliktas trumpas (~vienas {si}s pavyzdžių intervalas), kad aptikimas prasidėtų kuo anksčiau, tačiau būtų ignoruojami pavieniai pavyzdžio šuoliai.", - "thr_batch": "Paliktas šiek tiek virš p05 mažiausios aktyvios galios per {cycles} ciklus ({p05}W), kad pradžia būtų aptikta kuo anksčiau, o sustabdymo slenkstis liktų žemiau mažiausios veikiančios mašinos galios.", - "tol_per_profile": "p75 kiekvieno profilio trukmės dispersijos per {profiles} profilius ({cycles} ciklai); stabilūs profiliai nebaudžiami.", - "tol_pooled": "Pagrįsta sujungta {cycles} naujausių pažymėtų ciklų trukmės dispersija (p95 nuokrypis={dev}).", - "watchdog": "Paliktas kuo mažesnis, kiek saugu (vos didesnis nei p95 atnaujinimo tarpas {p95}s ir bent 2x mėginių ėmimo intervalas {median}s, min. 30s), kad strigtys būtų aptiktos greitai be klaidingų sustabdymų." + "standby_floor": "Kiekvienas pastarasis ciklas vis dar naudojo {idle}W, kai baigėsi, todėl slenkstis žemiau šios vertės niekada nepastebės prietaiso išsijungimo. Paliktas aukščiau šios vertės; {cycles} švarūs ciklai įrašuose niekada nenukrito žemiau jo daugiau nei {pause}s.", + "thr_batch": "Šiek tiek virš mažiausios aktyvios galios (p05) per {cycles} ciklus ({p05}W), kad pradžia būtų aptikta anksti, o sustabdymo slenkstis liktų žemiau mažiausios veikimo galios.", + "watchdog": "Paliktas kuo mažesnis, kiek saugu (vos didesnis nei p95 atnaujinimo tarpas {p95}s ir bent 2x mėginių ėmimo intervalas {median}s, min. 30s), kad strigtys būtų aptiktos greitai be klaidingų sustabdymų.", + "end_energy_floor": "Padidinta iki minimumo, kurį lemia sustabdymo slenkstis ({stop} W) per išjungimo delsimą ({off_delay} s); žemiau jo ciklas gali baigtis tik atsarginiu būdu.", + "max_duration_ratio_default": "Grąžinta numatytoji reikšmė: žemesnė viršutinė riba atmeta teisingą programą kiekvieną kartą, kai ciklas trunka ilgiau nei įprastai.", + "min_duration_ratio_default": "Grąžinta numatytoji reikšmė: aukštesnė apatinė riba neleidžia atpažinti vykstančio ciklo, kol jis gerokai neįsibėgėja." }, "exclusions": { "summary": "Neįtraukta {total} klaidingai aptiktų ciklų: {parts}.", @@ -2021,7 +1993,8 @@ "no_power": "nėra galios", "no_active_power": "nėra aktyvios galios" } - } + }, + "now_value": "(dabar {v})" }, "trend": { "down": "Tendencija žemyn", @@ -2029,6 +2002,10 @@ }, "maint": { "bearing_service": "Guolių aptarnavimas", + "salt": "Papildyti druskos", + "rinse_aid": "Papildyti skalavimo priemonės", + "lint_filter": "Valyti pūkų filtrą", + "condenser_clean": "Valyti kondensatorių", "descale": "Kalkių šalinimas", "drum_clean": "Valyti būgną", "filter_clean": "Valyti filtrą", @@ -2036,34 +2013,22 @@ }, "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ę", + "smart_termination_duration_ratio": "Programos vidutinės trukmės dalis, kurią ciklas turi pasiekti, kad išmanusis užbaigimas galėtų jį užbaigti anksčiau; sumažinkite, jei veikimo trukmė svyruoja", + "anti_crease_finalize_ratio": "Programos vidutinės trukmės dalis, kurią ciklas turi pasiekti, kad apsaugos nuo raukšlių uodega galėtų jį užbaigti; džiovyklėms sumažinkite, skalbyklėms palikite numatytąją", "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_exit_power": "Galios lygis pauzėse tarp sukimo impulsų; veikia tik virš Sustabdymo slenksčio", "anti_wrinkle_idle_timeout": "Tylos laikas, leidžiamas tarp dviejų būgninių impulsų prieš pasibaigiant apsaugai nuo raukšlių", "anti_wrinkle_max_duration": "Ilgesnis nei šis impulsas užbaigia apsaugą nuo raukšlių ir pradeda naują ciklą", "anti_wrinkle_max_power": "Už šią didesnis impulsas užbaigia apsaugą nuo raukšlių ir pradeda naują ciklą", "completion_min_seconds": "Trumpiausias veikimas, laikomas tikru ciklu", - "end_repeat_count": "Kiek žemų rodmenų iš eilės prieš pabaigą", "interrupted_min_seconds": "Trumpi ciklai pažymimi kaip pertraukti", "min_off_gap": "Tarpas, reikalingas dviem ciklams atskirti", "off_delay": "Kiek sekundžių žemos galios prieš baigiantis ciklui", "start_duration_threshold": "Sekundės virš slenksčio startui patvirtinti", "start_threshold_w": "Mažiausia galia (W), kad būtų skaičiuojama kaip prasidėjęs", "stop_threshold_w": "Žemiau šios ribos aparatas laikomas išjungtu", - "dtw_bandwidth": "3 etapas: Sakoe-Chiba laiko lankstymo juosta (0 = DTW išjungtas; numatytoji 0,2 = 20% ciklo ilgio)", - "corr_weight": "2 etapas: balansas tarp kreivės formos (koreliacija) ir galios lygio (MAE); numatytoji 0,45", - "duration_weight": "4 etapas: kaip stipriai trukmės atitikimas veikia galutinį balą (numatytoji 0,22)", - "energy_weight": "4 etapas: kaip stipriai energijos atitikimas veikia galutinį balą (numatytoji 0,22)", - "profile_match_min_duration_ratio": "1 etapas: trumpiausias ciklas kaip profilio trukmės dalis; numatytoji 0,1 (10%)", - "profile_match_max_duration_ratio": "1 etapas: ilgiausias ciklas kaip profilio trukmės dalis; numatytoji 1,8 (180%)", - "keep_min_score": "2 etapas: žemiausias balas likti lenktynėse; numatytoji 0,1 leidžia silpnus kandidatus, vėlesni etapai parenka geriausią", - "dtw_blend": "3 etapas: 0 = tik pagrindinis balas, 1 = tik DTW balas, 0,5 = lygus mišinys (numatytoji)", - "dtw_ensemble_w": "3 etapas (ansamblio režimas): skalieroto L1 svoris prieš išvestinės DTW; numatytoji 0,7 teikia pirmenybę lygio sąmoningumui", - "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)", + "profile_match_min_duration_ratio": "Trumpiausias ciklas kaip profilio trukmės dalis; numatytoji 0,1 (10%)", + "profile_match_max_duration_ratio": "Ilgiausias ciklas kaip profilio trukmės dalis; numatytoji 1,8 (180%)", "curve_preroll_seconds": "Sekundės rodmenų iš nutrauktų paleidimo bandymų, perkeltų į ciklo kreivės pradžią; 0 = išjungta" }, "col": { @@ -2071,12 +2036,14 @@ "status_tip": "Ciklo rezultatas: Baigta (natūrali pabaiga), Nutraukta (staigus galios kritimas), Priverstinai sustabdyta (rankiniu būdu) arba Reikia peržiūros (laukia atsiliepimo).", "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. 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.", + "energy_tip": "Bendras suvartotos energijos kiekis (kWh), apskaičiuotas iš galios per laiką.", + "cost_tip": "Šio ciklo energijos kaina, užfiksuota jam pasibaigus. Kai įjungta „Pagal laiką svertinė kaina“, kiekviena akimirka įkainojama savo kaina; kitaip taikoma pabaigos kaina. Nustatykite kainą Nustatymuose, kad tai 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.", - "cost_dynamic_tip": "Pagal laiką sverta: įkainota pagal ciklo metu galiojusią kainą (faktinė {price} {cur}/kWh)." + "cost_dynamic_tip": "Pagal laiką sverta: įkainota pagal ciklo metu galiojusią kainą (faktinė {price} {cur}/kWh).", + "dbg_conf": "Pasit.", + "dbg_mae": "MAE", + "dbg_corr": "Kor." }, "stat": { "avg": "vid.", @@ -2100,7 +2067,10 @@ "notify_finish": "Būtų išsiųstas pabaigos pranešimas.", "notify_milestone": "Būtų išsiųstas svarbaus etapo pranešimas.", "notify_held": "Pranešimas buvo sulaikytas dėl tylos valandų.", - "finished": "Ciklas pasiekė galutinę būseną ir baigėsi." + "finished": "Ciklas pasiekė galutinę būseną ir baigėsi.", + "match_reverted": "Programa grįžo į aptikimo būseną: jos rezultatas nukrito gerokai žemiau savo didžiausios reikšmės.", + "verified_pause": "Tiesioginis sulygiavimas patvirtino (arba panaikino) pauzę programos viduje. Kol ji galioja, ciklas nebaigiamas.", + "stalled": "Ciklas ties budėjimo galia išbuvo ilgiau, nei ši programa kada nors ten stabteli, nors darbo dar liko. Rodoma kaip Pristabdyta (Užstrigo); ciklas lieka atviras." }, "store": { "search_ph2": "Ieškoti pagal prekės ženklą ar modelį…", @@ -2109,11 +2079,7 @@ "programs_count": "Programos: {n}", "browse": "Naršyti", "device": "Prietaisas", - "favorites": "Mėgstamiausi", - "search_ph": "Ieškoti pagal prekės ženklą ar modelį…", "no_results": "Atitinkančių prietaisų nerasta. Bandykite kitą paiešką.", - "n_programs": "{n} programos", - "n_cycles": "{n} ciklai", "no_programs": "Šiam prietaisui dar nėra bendrintų programų.", "no_cycles": "Šiai programai dar nėra bendrintų etaloninių ciklų.", "anon": "anonimas", @@ -2125,19 +2091,21 @@ "merge_target": "Sujungti su profiliu", "description": "Aprašymas (nebūtina)", "contributed_by": "autorius {name}", + "website": "Parduotuvės svetainė ↗", + "website_tip": "Atidaryti bendruomenės parduotuvės svetainę", + "favorites": "Mėgstamiausi", "rating_summary": "★ {avg} ({n})", "no_ratings": "Kol kas nėra įvertinimų", - "website": "Parduotuvės svetainė ↗", - "website_tip": "Atidaryti bendruomenės parduotuvės svetainę" + "unreachable": "Nepavyko pasiekti bendruomenės parduotuvės. Patikrinkite ryšį ir bandykite dar kartą." }, "settings": { "gear": { - "title": "Nustatymai" + "title": "Nuostatos" } }, "link": { - "manual": "Vadovas ↗", - "add_appliance": "Pridėkite savo prietaisą" + "maintenance_supporter": "Maintenance Supporter GitHub svetainėje ↗", + "manual": "Vadovas ↗" }, "tip": { "add_brand": "Prekės ženklo nėra sąraše? Pridėkite jį į bendruomenės katalogą", @@ -2171,7 +2139,13 @@ "rebuild": { "envelopes": "Perkuriamos gaubtinės" }, - "cancelling": "Atšaukiama..." + "cancelling": "Atšaukiama...", + "auto_label": { + "running": "Ciklai žymimi automatiškai" + }, + "store_download": { + "running": "Atsisiunčiama iš parduotuvės" + } }, "setup": { "hdr": { @@ -2180,26 +2154,24 @@ }, "phase0": { "washer": "WashData jau aptinka jūsų ciklus. Įrašykite pirmąjį ciklą, kad galėtumėte naudoti programų pavadinimus ir laiko įverčius.", - "dishwasher": "WashData stebi. Indaplovių ciklai yra sudėtingi – labai rekomenduojame įrašyti pirmąjį ciklą. Jei aptiktas ciklas trunka per ilgai, prieš išsaugant kaip profilį, apkarpykite jį ciklo redaktoriuje.", + "dishwasher": "WashData stebi. Indaplovių ciklai sudėtingi, todėl įrašykite pirmąjį ciklą. Jei aptiktas ciklas per ilgas, prieš išsaugodami kaip profilį apkarpykite jį ciklo redaktoriuje.", "generic": "WashData stebi. Įrašykite arba pažymėkite aptiktą ciklą, kad pradėtumėte kurti profilius." }, "phase1a": { - "labelled": "Gera pradžia – jūsų pirmoji programa išsaugota. Kad duomenys būtų kuo švaresni, apsvarstykite galimybę įrašyti kitą ciklą naudodami įrašymo valdiklį." + "labelled": "Gera pradžia: pirmoji programa išsaugota. Kad duomenys būtų švariausi, kitą ciklą įrašykite įrašymo valdikliu." }, "phase1b": { - "recorded": "Jūsų įrašas išsaugotas kaip {profile_name}. Dabar įrašykite arba pažymėkite kitas dažniausiai naudojamas programas, kad padidintumėte aprėptį." + "recorded": "Jūsų įrašas išsaugotas kaip {profile_name}. Dabar įrašykite arba pažymėkite kitas dažnas programas." }, "phase1c": { - "verify": "Turite {count} bendruomenės programas. Paleiskite ciklą, kad patikrintumėte, ar WashData jas teisingai atpažįsta – atitikimas gerės, kai jūsų įrenginys sukaupia savo istoriją." + "verify": "Bendruomenės programų: {count}. Paleiskite ciklą ir patikrinkite, ar WashData jį atpažįsta; atpažinimas gerės, kai jūsų įrenginys sukaups savo istoriją." }, "phase2": { - "cluster": "WashData aptiko {count} ciklus, kurie neatitinka jokios išsaugotos programos – jie yra panašūs vienas į kitą. Ar norite sukurti jiems naują profilį?", + "cluster": "WashData aptiko panašių ciklų ({count}), kurie neatitinka jokios išsaugotos programos. Sukurti jiems naują profilį?", "unmatched": "Paskutinis ciklas neatitiko jokios išsaugotos programos. Ar tai nauja programa?" }, "phase3": { - "suggestions": "WashData pateikia nustatymų rekomendacijas pagal jūsų ciklų istoriją – peržiūrėkite jas, kad pagerintumėte aptikimo tikslumą.", - "groups": "Kai kurie jūsų profiliai atrodo kaip ta pati programa skirtingose temperatūrose. Sugrupuokite juos, kad atitikimas būtų tikslesnis.", - "phases": "Pridėkite programų fazes prie {profile_name}, kad likusio laiko įverčiai būtų tikslesni." + "suggestions": "WashData turi nustatymų pasiūlymų pagal jūsų ciklų istoriją. Peržiūrėkite juos, kad pagerintumėte aptikimą." }, "phase4": { "healthy": "Šis įrenginys yra visiškai sukonfigūruotas ({profile_count} profiliai)." @@ -2212,12 +2184,58 @@ "create_from_cluster": "Sukurti profilį iš šių ciklų", "create_profile": "Sukurti jam profilį", "review_suggestions": "Peržiūrėti pasiūlymus", - "organise_profiles": "Tvarkyti profilius", - "configure_phases": "Konfigūruoti fazes", "skip_step": "Praleisti šį žingsnį", "skip_forever": "Daugiau nerodyti", "hide_guidance": "Slėpti nurodymus", "show_guidance": "Rodyti nurodymus" } + }, + "access": { + "edit": "Redagavimas", + "full": "Visiška", + "inherit": "Paveldėti", + "none": "Nėra (paslėpta)", + "read": "Skaitymas" + }, + "pg_label_reason": { + "no_winner": "Netiko jokia programa", + "below_floor": "Žemiau mokymosi pasitikėjimo", + "ambiguous": "Pažymėta kaip neaiški", + "margin": "Per arti antrosios vietos", + "unknown_profile": "Programa nebeegzistuoja", + "not_evaluated": "Neįvertinta" + }, + "diagram": { + "accum": "sukaup.", + "auto": "auto", + "below_off": "žemiau = išj.", + "confirmed": "patvirtinta", + "energy_counts": "skaičiuojama energija", + "feedback": "atsiliepimas", + "gap_below_min": "tarpas < min = vienas ciklas", + "heat_phase": "šildymo fazė", + "held_at_100": "laikoma 100%", + "ignored": "ignoruojama", + "kept": "išlaikyta", + "max": "maks.", + "min": "min.", + "minus_tol": "-tol.", + "no_match": "nėra atitikties", + "no_updates": "nėra atnaujinimų", + "off_delay_wait": "išjungimo delsimas", + "off_gap": "išjungimo tarpas", + "plus_tol": "+tol.", + "profile": "profilis", + "reading_interval": "įprastas rodmenų intervalas", + "sensor_offline": "jutiklis neprisijungęs: priverstinis sustabdymas", + "si": "MI", + "spike_ignored": "smaigalys ignoruojamas", + "spike_ignored_short": "smaigalys: ignoruojamas", + "start": "paleidimas", + "stop": "sustabdymas", + "tail_energy_resets": "pabaigos energija virš slenksčio: laikmatis paleidžiamas iš naujo", + "thr": "slenk.", + "too_short": "per trumpas", + "tumble_kept": "būgno impulsai išlaikyti" } } diff --git a/custom_components/ha_washdata/translations/panel/lv.json b/custom_components/ha_washdata/translations/panel/lv.json index c231ba93..e58ad981 100644 --- a/custom_components/ha_washdata/translations/panel/lv.json +++ b/custom_components/ha_washdata/translations/panel/lv.json @@ -3,6 +3,7 @@ "min": "min", "entity_not_found": "Entītija nav atrasta", "running_long": "Darbojas ilgi", + "stalled": "Iestrēdzis", "remaining": "atlicis", "progress": "progress", "show_time_remaining": "Rādīt atlikušo laiku", @@ -48,16 +49,17 @@ } }, "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_tip": "Beigu daļā šī programma uz aptuveni {mins} min apklust, pēc tam {secs} s patērē aptuveni {watts} W. Novērots {seen} no {measured} izmērītajiem cikliem.", "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ā", + "artifact_tip": "Konstatētas {n} anomālijas (piemēram, durvis atvērtas cikla vidū) - atveriet, lai tās skatītu diagrammā", + "artifact_tip_zero": "Konstatētas {n} anomālijas (piemēram, durvis atvērtas cikla vidū) - atveriet, lai tās skatītu diagrammā", + "artifact_tip_one": "Konstatēta {n} anomālija (piemēram, durvis atvērtas cikla vidū) - atveriet, lai to skatītu diagrammā", + "artifact_tip_other": "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", "declining": "↘ samazinās", "energy_low": "Zemāka enerģija nekā parasti", "energy_spike": "Augstāka enerģija nekā parasti", - "fair_fit": "godīga atbilstība", - "fair_fit_tip": "Mērena atbilstības konsekvence – dažiem šim profilam piešķirtajiem cikliem ir zemāki ticamības rādītāji. Iezīmējiet vairāk ciklu vai atkārtoti ierakstiet profilu, lai uzlabotu precizitāti.", "feedback_requested": "Pieprasītas atsauksmes", "golden_cycle": "Reģistrēts atskaites cikls", "duration_outlier": "{n} neparasta garuma cikli", @@ -66,27 +68,38 @@ "needs_review": "Nepieciešama pārskatīšana", "overrun": "Skrēja ilgāk nekā parasti", "poor_fit": "slikta atbilstība", - "poor_fit_tip": "Nekonsekventa spēļu vēsture – apsveriet iespēju atjaunot šo profilu", - "restart_gap_tip": "{n} HA restartēšanas atstarpe: jaudas trasē ir caurums", + "restart_gap_tip": "{n} HA restartēšanas atstarpes: jaudas trasē ir caurumi", + "restart_gap_tip_zero": "{n} HA restartēšanas atstarpes: jaudas trasē ir caurumi", + "restart_gap_tip_one": "{n} HA restartēšanas atstarpe: jaudas trasē ir caurums", + "restart_gap_tip_other": "{n} HA restartēšanas atstarpes: jaudas trasē ir caurumi", "reviewed": "Pārskatīts", "steady": "→ stabils", "underrun": "Pabeidza ātrāk nekā parasti", - "using_defaults": "Tiek izmantoti piegādātie noklusējuma iestatījumi", - "using_tuned": "Izmantojot noregulētus svarus", "overrun_ratio": "({x}x paredzētā)", "underrun_ratio": "({pct}% no paredzētā)", - "awaiting_tip": "Gaida kopienas apstiprinājumu", - "awaiting_n": "Gaida apstiprinājumu · {n} apstiprinājumi", - "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ā.", - "armed": "(attiecas uz nākamo ciklu)" + "armed": "(attiecas uz nākamo ciklu)", + "awaiting_tip": "Gaida kopienas apstiprinājumu", + "awaiting_n": "Gaida apstiprinājumu · {n} apstiprinājumi", + "awaiting": "Gaida apstiprinājumu", + "approved": "Apstiprināts", + "matcher_unused": "Vēl nav saskaņots ne ar vienu no jūsu cikliem", + "matcher_used": "Saskaņots ar {n} jūsu cikliem", + "matcher_used_one": "Saskaņots ar {n} jūsu ciklu", + "matcher_used_zero": "Saskaņots ar {n} jūsu cikliem", + "matcher_used_other": "Saskaņots ar {n} jūsu cikliem", + "matcher_used_tip": "Šīs ierīces cikli, kurus WashData pats saskaņoja ar šo importēto programmu. Imports, kas nekad netiek saskaņots, var neatbilst jūsu ierīcei; to var noņemt. Tiek skaitīts lokāli; uz veikalu nekas netiek sūtīts." }, "btn": { + "pg_preset_delete": "Dzēst priekšiestatījumu", + "pg_preset_load": "Ielādēt priekšiestatījumu", + "pg_preset_load_tip": "Aizstāt izmēģinājumu laukuma vērtības ar šo priekšiestatījumu (aktīvie iestatījumi netiek mainīti)", + "pg_preset_save": "Saglabāt kā priekšiestatījumu", + "pg_preset_save_tip": "Saglabāt visas tālāk norādītās vērtības kā nosauktu šīs ierīces priekšiestatījumu", "reset_zoom": "Atiestatīt tuvinājumu", "set_brand_model": "Iestatīt zīmolu un modeli", "refresh_catalog": "Atsvaidzināt katalogu", @@ -97,12 +110,11 @@ "add_timer": "+ Pievienojiet taimeri", "apply_all": "Lietot visu", "apply_label": "Lietot etiķeti", - "apply_set_b": "Lietot kopu B", "apply_split": "Lietot sadalījumu", "apply_trim": "Lietot apgriešanu", "auto_detect_split": "Automātiska noteikšana", "auto_label_cycles": "Automātiskās marķēšanas cikli", - "auto_label_cycles_tip": "Automātiski piešķirt profila nosaukumus nemarķētiem cikliem, kuru atbilstības ticamība sasniedz slieksni", + "auto_label_cycles_tip": "Piešķirt nosaukumu nemarķētiem cikliem, kuru atbilstības ticamība sasniedz slieksni.", "cancel": "Atcelt", "clear_debug": "Notīrīt atkļūdošanas datus", "clear_splits": "Notīrīt", @@ -112,8 +124,6 @@ "convert_to_automation": "Konvertēt uz automatizāciju", "correct": "Labot…", "create": "Izveidot", - "create_from_cluster": "Izveidot profilu no {count} cikliem", - "create_profile": "+ Izveidojiet profilu", "delete": "Dzēst", "delete_group": "Dzēst grupu", "delete_group_tip": "Dzēst tikai šo grupu – dalībnieku profili tiek saglabāti", @@ -124,22 +134,19 @@ "done": "Gatavs", "edit": "Rediģēt", "export": "Eksportēt", - "export_json": "Eksportēt uz JSON", "force_stop": "Piespiedu apstāšanās", "force_stop_tip": "Nekavējoties pārtraukt pašreizējo ciklu un atzīmēt to kā piespiedu kārtā apturētu", "from_template": "No veidnes ▾", "ignore": "Ignorēt", "import_json": "Importēt no JSON", - "import_overwrite": "Importēt (pārraksta datus)", "inspect": "Pārbaudīt", "label": "Iezīmēt", - "load": "Ielādēt", "load_more": "Ielādēt vairāk", "manage": "Pārvaldīt", "merge": "Apvienot", "new_automation": "+ Jauna automatizācija", "new_group": "+ Jauna grupa", - "new_group_tip": "Grupēt gandrīz identiskus profilus (tāda pati forma/ilgums, atšķirīga temperatūra vai centrifūga), lai saskaņotājs uzticami izvēlētos starp tiem", + "new_group_tip": "Grupēt gandrīz identiskus profilus (tāda pati forma, cita temperatūra vai centrifūga), lai saskaņošana tos spētu atšķirt.", "new_phase": "+ Jauns posms", "new_profile": "+ Jauns profils", "new_profile_tip": "Izveidot jaunu programmas profilu no esoša iezīmēta cikla vai ieraksta", @@ -147,36 +154,25 @@ "on_cycle_started": "Cikls sākās", "pause_cycle": "Pauze", "pause_cycle_tip": "Pauzēt pašreizējo ciklu – ierīce turpinās no pārtraukuma vietas", - "pg_apply_device": "Lietot ierīcei", - "pg_autofill": "Automātiskā aizpilde", - "pg_load_calibrated": "Ielādēt kalibrētos (ML) ({n})", - "pg_load_calibrated_tip": "Ielādēt ML kalibrētos iestatījumus kā izmēģinājumu laukuma aizstājumus", "pg_load_live": "Ielādēt aktīvos iestatījumus", "pg_load_live_tip": "No jauna nolasīt pašreizējos integrācijas iestatījumus un atmest visas izmēģinājumu laukuma izmaiņas", "pg_load_suggested": "Ielādēt ieteiktos ({n})", "pg_load_suggested_tip": "Ielādēt automātiskā pielāgotāja pašreizējos ieteikumus kā izmēģinājumu laukuma aizstājumus", - "pg_preset_delete": "Dzēst priekšiestatījumu", - "pg_preset_load": "Ielādēt priekšiestatījumu", - "pg_preset_load_tip": "Aizstāt izmēģinājumu laukuma vērtības ar šo priekšiestatījumu (aktīvie iestatījumi netiek mainīti)", - "pg_preset_save": "Saglabāt kā priekšiestatījumu", - "pg_preset_save_tip": "Saglabāt visas tālāk norādītās vērtības kā nosauktu šīs ierīces priekšiestatījumu", "pg_publish_all": "Saglabāt iestatījumos ({n})", "pg_publish_one": "Saglabāt \"{label}\" integrācijas iestatījumos", - "play": "Atskaņot", "process": "Apstrādāt", "process_history": "Apstrādāt tūlīt", "process_recording": "Saglabāt ierakstu", "process_tip": "Saglabāt ierakstīto trasē kā jaunu vai esošu profilu", "rebuild": "Pārbūvēt aploksnes", "rebuild_envelope": "Pārbūvēt aploksni", - "rebuild_tip": "Pārrēķināt paredzamo jaudas aploksni (min/maks josla) visiem profiliem no to iezīmētajiem cikliem – palaidiet pēc jaunu ciklu iezīmēšanas vai veco labošanas", + "rebuild_tip": "Pārbūvēt katra profila paredzamo jaudas joslu no tā iezīmētajiem cikliem. Palaidiet pēc ciklu iezīmēšanas vai labošanas.", "rec_start_tip": "Sākt ierīces jaudas trasē ierakstīšanu – sāciet tieši pirms cikla palaišanas", "rec_stop": "Apturēt", "rec_stop_tip": "Pārtraukt ierakstīšanu un saglabāt uzņemto trasē pārskatīšanai", "record": "Sāciet ierakstīšanu", "refresh": "Atsvaidzināt", "refresh_diag_tip": "Atsvaidzināt krātuves statistiku", - "refresh_logs_tip": "Atsvaidzināt jaunākos žurnāla ierakstus", "refresh_settings_tip": "Pārlādēt iestatījumus no servera", "relabel": "Pārmarķēt ({count})", "remove": "Noņemt", @@ -184,7 +180,6 @@ "reset": "Atiestatīt", "reset_ml_models_tip": "Izmetiet precizētos modeļus un atgriezieties pie iebūvētajiem. WashData vēlāk var tos apgūt no jauna.", "reset_to_builtin": "Atiestatīt uz iebūvētajiem modeļiem", - "reset_to_defaults": "Atiestatīt uz noklusējuma iestatījumiem", "resume_cycle": "Atsākt", "resume_cycle_tip": "Atsākt apturēto ciklu", "revert_settings": "Atmest izmaiņas", @@ -197,8 +192,6 @@ "run_analysis": "🔍 Palaidiet ieteikumu analīzi", "run_analysis_tip": "Analizējiet savus ierakstītos ciklus tūlīt un atsvaidziniet regulēšanas ieteikumus", "run_auto_label": "Palaidiet automātisko etiķeti", - "run_both": "Palaist A pret B", - "run_simulation": "Palaist simulāciju", "save": "Saglabāt", "save_access_control": "Saglabāt piekļuves kontroli", "save_group": "Saglabāt grupu", @@ -210,10 +203,8 @@ "save_settings": "Saglabāt iestatījumus", "select": "Izvēlieties", "select_all": "Atlasīt visu", - "select_last20": "Pēdējie 20", "select_none": "Notīrīt", "show_only": "Rādīt tikai", - "skip_setup": "Izlaist iestatīšanu", "split": "Sadalīt", "stop": "Apturēt", "submit_correction": "Iesniedziet labojumu", @@ -222,36 +213,31 @@ "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.", + "unlabel_selected_tip": "Noņem marķējumu atlasītajiem cikliem. Tie paliek jūsu vēsturē, bet vairs neveido šo profilu.", "use": "Izmantot", "wipe_all": "Noslaukiet visus datus", "delete_selected": "Dzēst atlasītos ({n})", - "group_suggest": "Grupa {n}: {members}", "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", "import": "Importēt", "disconnect": "Atvienot", "connect_github": "Savienot GitHub", - "save_appliance": "Saglabāt ierīci", "share_to_store": "Kopīgot veikalā", "share": "Kopīgot", - "confirm_appliance": "Apstiprināt šo ierīci", - "confirmed": "Apstiprināts", "browse_community_setups": "Pārlūkot kopienas iestatījumus", "download_device": "Lejupielādēt šo iestatījumu", "share_device": "⬆ Kopīgot šo ierīci", - "share_device_tip": "Kopīgojiet šo ierīci un tās reģistrētos atskaites ciklus kopienas veikalā, lai citi ar to pašu mašīnu varētu tos pieņemt", + "share_device_tip": "Kopīgojiet šo ierīci un tās atskaites ciklus kopienas veikalā, lai tādas pašas mašīnas īpašnieki varētu tos pieņemt.", "share_n": "Kopīgot {n} ciklu(-s)", "export_selected": "Eksportēt (izvēlēties datus)", "export_all": "Ātrā eksportēšana: viss", - "import_raw": "Papildu: aizstāt visu no JSON", "download_export": "Lejupielādēt eksportu", "analyze_import": "Analizēt failu", "import_selected": "Importēt atlasītos", "back": "Atpakaļ", "mute_suggestion": "Pārtraukt ieteikt šo iestatījumu", "reset_muted": "Atiestatīt klusinātos", + "reset_phase_tip": "Atsaukt labojumu un atjaunot iebūvēto fāzi", "import_power_history": "Importēt jaudas vēsturi", "hist_read_recorder": "Nolasīt no Home Assistant", "hist_scan": "Meklēt ciklus", @@ -260,7 +246,15 @@ "save_total_cycles": "Saglabāt kopējo skaitu", "clear": "Notīrīt", "add_brand": "Pievienot zīmolu", - "add_appliance": "Pievienot ierīci" + "log_done_task": "Atzīmēt kā izpildītu: {task}", + "add_task": "Pievienot uzdevumu", + "add_appliance": "Pievienot ierīci", + "retry": "Mēģināt vēlreiz", + "confirm_appliance": "Apstiprināt šo ierīci", + "confirmed": "Apstiprināts", + "create_profile_from_gap": "Izveidot profilu", + "undo_import": "Atsaukt pēdējo importēšanu", + "pg_rerun": "Atjaunināt simulāciju" }, "note": { "off_delay": { @@ -268,10 +262,6 @@ } }, "conflict": { - "anti_wrinkle_exit": { - "exit": "Jābūt zem apstāšanās sliekšņa ({stop} W); pretējā gadījumā pretgrumbu izejas jauda tiek ignorēta", - "stop": "Jābūt virs pretgrumbu izejas jaudas ({exit} W)" - }, "anti_wrinkle_max": { "max": "Jābūt virs sākuma sliekšņa ({start} W); pretējā gadījumā pretgrumbu ilguma ierobežojums tiek apiets", "start": "Jābūt zem maksimālās pretgrumbu jaudas ({max} W)" @@ -279,10 +269,10 @@ "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." + "power_sensor": "Šis ir ierīces Jaudas sensors, nevis cena. Izmaksas tiktu rēķinātas vatos par kWh; notīriet šo lauku.", + "energy_sensor": "Šis ir ierīces Enerģijas skaitītājs, nevis cena. Izmaksas tiktu rēķinātas pēc skaitītāja rādījuma par kWh; notīriet šo lauku.", + "device_class": "Šī sensora klase ir {dc}, nevis cena par kWh. Notīriet šo lauku vai izvēlieties tarifa sensoru.", + "unit": "Šī sensora mērvienība ir {unit}, nevis cena par kWh. Notīriet šo lauku vai izvēlieties 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", @@ -317,8 +307,8 @@ "threshold": "Jābūt zem apstāšanās sliekšņa ({stop} W), lai stātos spēkā" }, "pump_stuck": { - "duration": "Jābūt mazākam par taimauta bez atjauninājumiem ({to} s), lai iestrēgšanas signāls aktivizētos pirms sargsuns pārtrauc ciklu", - "timeout": "Jāpārsniedz sūkņa iestrēgšanas ilgums ({ps} s), lai iestrēgšanas signāls aktivizētos pirms cikls tiek piespiedu kārtā apturēts" + "duration": "Jābūt mazākam par iestatījumu Laiks bez atjauninājuma ({to} s), lai iestrēgšanas signāls nostrādātu pirmais", + "timeout": "Jābūt lielākam par iestatījumu Iestrēgušā sūkņa ilgums ({ps} s), lai iestrēgšanas signāls nostrādātu pirmais" }, "start_dur": { "sampling": "Paraugu ņemšanas intervāls pārsniedz starta ilgumu ({sdt} s); viena parauga smailes var atvērt ciklu", @@ -334,10 +324,15 @@ "watchdog": { "interval": "Jābūt vismaz 2x lielākam par paraugu ņemšanas intervālu ({si} s)", "sampling": "Paraugu ņemšanas intervālam jābūt ne vairāk kā pusei sargsuna intervāla ({wi} s)" + }, + "end_energy": { + "energy": "Pārāk stingrs, ja Apstāšanās slieksnis ir {w} W un Izslēgšanas aizkave ir {d} s; cikls var beigties tikai ar rezerves mehānismu", + "stop": "Beigu enerģija ({e} Wh {d} s laikā) pieļauj tikai {p} W" } }, "hdr": { - "ab_comparison": "A/B iestatījumu salīdzinājums", + "custom_tasks": "Jūsu uzdevumi", + "maintenance_supporter": "Vajag vairāk?", "access_control": "Piekļuves kontrole", "add_maintenance": "Pievienot apkopes notikumu", "automation_delete": "Dzēst šo automatizāciju", @@ -346,28 +341,22 @@ "automation_review_this_cycle": "Pārskatiet šo ciklu", "automations": "Automatizācijas", "clear_debug": "Notīrīt atkļūdošanas pēdas", - "cycle_simulator": "Ciklu simulators", "diagnostics_pref": "Diagnostika", "display": "Displejs", - "dtw_inspector": "DTW inspektors", "export_import": "Eksportēt / importēt", - "getting_started": "Darba sākšana", "live_power": "Reāllaika jauda", "logs": "Žurnāli", - "logs_diagnostics": "Diagnostika", "maintenance": "Apkopes darbības", "maintenance_log": "Apkopes žurnāls", "maintenance_reminders": "Apkopes atgādinājumi", "manual_recording": "Manuāla ierakstīšana", - "manual_recording_tip": "Palaidiet ciklu apzināti, kamēr WashData ieraksta jaudas trasē. Sāciet tieši pirms ierīces ieslēgšanās, apturiet, kad tas pabeigts, pēc tam apstrādājiet, lai saglabātu kā nosauktu profilu.", + "manual_recording_tip": "Ierakstiet ciklu apzināti: nospiediet Sākums tieši pirms ierīce sāk darbu un Apturēt, kad tā beidz, pēc tam Apstrādāt, lai saglabātu to kā profilu.", "ml_learned": "Ko WashData ir iemācījies", - "ml_matching_tuning": "Programmu saskaņošanas precizēšana", "ml_settings_card": "Iestatījumi", "ml_smart_learning": "Gudra mācīšanās", "my_preferences": "Manas preferences", "no_automations": "Vēl neviena automatizācija neatsaucas uz šo ierīci.", "panel_settings": "Paneļa iestatījumi", - "param_sweep": "Parametru skenēšana", "pg_settings_source": "Iestatījumu avots", "phase_catalog": "Fāzes katalogs", "playground": "Izmēģinājumu laukums", @@ -376,11 +365,8 @@ "status": "Statuss", "status_graph": "Statusa grafiks", "storage_stats": "Krātuves statistika", - "test_history": "Testēšana ar vēsturi", "toggle_sidebar": "Pārslēgt mājas palīga sānjoslu", - "tools_and_data": "Rīki un dati", "wipe_history": "Noslaukiet vēsturi", - "recommendations": "Ieteikumi ({n})", "pg_detection_params": "Noteikšanas iestatījumi", "pg_outcome": "Simulācijas rezultāts", "pg_across_cycles": "Pa visiem jūsu cikliem", @@ -388,7 +374,8 @@ "online_account": "Kopienas veikals un tiešsaistes funkcijas", "import_power_history": "Jaudas vēstures importēšana", "service_status": "Apkopes statuss", - "online_account_tab": "Tiešsaiste un kopiena" + "online_account_tab": "Tiešsaiste un kopiena", + "live_match_debug": "Tiešraides atbilstības atkļūdošana" }, "health": { "fair": "Pieņemama profila kvalitāte", @@ -396,12 +383,17 @@ "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_tip": "Pozīcija atbilstošās programmas ierakstītajā līknē: {pct}%. To nosaka, salīdzinot ciklu ar profilu, tāpēc tā paliek pareiza, ja cikls ir garāks vai īsāks. Atjaunojas, kamēr ierīce ir klusa, tāpēc 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", + "task_name": "Uzdevums", + "every_cycles": "Intervāls (cikli)", + "every_days": "Intervāls (dienas)", + "new_task_name": "Jauns uzdevums", + "days_since_service": "{since} / {total} dienas", "actions": "Darbības", "activity": "Aktivitāte", "administrators": "Administratori", @@ -409,10 +401,8 @@ "aria_cycle_chart": "Cikla jaudas līkne", "aria_envelope_chart": "Profila jaudas aploksnes grafiks", "aria_phase_chart": "Fāžu redaktora grafiks", - "aria_playground_chart": "Cikla jaudas līkne ar velkamiem noteikšanas sliekšņiem", "aria_power_chart": "Jaudas patēriņa grafiks", "aria_spaghetti_chart": "Pārklātas ciklu jaudas līknes", - "aria_sweep_chart": "Parametru skenēšanas rezultātu grafiks", "artifact_anomaly": "Anomālija", "artifact_high_power": "Augsta jauda", "artifact_interruption": "Pārtraukums", @@ -426,149 +416,100 @@ "cohesion_good": "kohēzija {pct}%", "cohesion_low": "⚠ zema kohēzija {pct}%", "compare_profiles": "Salīdziniet ar profiliem", - "concurrency": "Vienlaicīgums", "confidence": "Pārliecība", "confidence_threshold": "Pārliecības slieksnis", "correct_duration": "Pareizais ilgums (min, pēc izvēles)", "correct_profile": "Pareizs profils", - "correlation": "Korelācija", "cost": "Izmaksas", "create_new_profile": "+ Izveidot jaunu profilu…", "current_phase": "Pašreizējā fāze", - "curves": "Līknes", "cycle": "Cikls", "cycle_date_display": "Cikla datuma displejs", - "cycle_health": "Cikla veselība", - "cycle_index": "cikla numurs →", - "cycles": "Cikli", "cycles_count": "Cikli", "cycles_lc": "cikli", - "dashed": "raustīta", "date": "Datums", "debug_traces": "Atkļūdošanas pēdas", - "default": "Noklusējums", "default_access_level": "Noklusējuma līmenis lietotājiem, kas nav norādīti tālāk", "default_other": "Noklusējums (citas ierīces)", "default_tab": "Noklusējuma cilne, atverot paneli", - "delta": "Δ", "description": "Apraksts", - "dtw_blend_w": "Sajaukšanas svars", - "dtw_blended": "Sajauktais rezultāts", - "dtw_ddtw": "DDTW rezultāts", - "dtw_l1": "L1 rezultāts", - "dtw_matched_profile": "(cikla saskaņotais profils)", "duration": "Ilgums", - "duration_agreement": "Ilguma atbilstība", - "emphasis": "Uzsvars", "enable_access_control": "Iespējot katra lietotāja piekļuves kontroli", "energy": "Enerģija", - "energy_agreement": "Enerģijas atbilstība", - "event_timeline": "Notikumu laika josla", "event_type": "Notikuma veids", "expected": "Gaidāmais", + "suspected_profile": "{name} (iespējamā)", + "runner_up_profile": "{name} (otrais kandidāts)", "expected_duration": "Paredzamais ilgums (min)", "file_kb": "Fails (kB)", - "final_score": "Galīgais rezultāts", "from": "No", "gap_s": "Atstarpe (s)", "group_name": "Grupas nosaukums", "head_trim": "Apgriešana no sākuma (s)", - "health": "Veselība", "hide_tabs": "Slēpt cilnes, kas nav administratori", - "in_use": "Lietošanā", "json_data": "JSON dati", "label": "Match", "last": "Pēdējie", "last_checked": "Pēdējoreiz pārbaudīts:", "load_from_file": "Ielādēt no faila", - "mae_score": "MAE rezultāts", "manual_duration": "Manuālais ilgums (min, pēc izvēles)", "match": "Atbilstība", "members": "Dalībnieki", - "metric": "Metrika", "mode_existing_profile": "Pievienot esošajam profilam", "mode_new_profile": "Izveidot jaunu profilu", "models_fine_tuned": "(precizētie modeļi: {count})", - "n_classic_suggestions": "{n} klasiskais", - "n_ml_suggestions": "{n} ML", "n_selected": "Atlasīti: {n}", - "n_tuning_suggestions": "{n} regulēšanas ieteikums", - "na": "n/p", + "n_tuning_suggestions": "{n} regulēšanas ieteikumi", + "n_tuning_suggestions_zero": "{n} regulēšanas ieteikumi", + "n_tuning_suggestions_one": "{n} regulēšanas ieteikums", + "n_tuning_suggestions_other": "{n} regulēšanas ieteikumi", "new_profile_name": "Jauns profila nosaukums", "notes": "Piezīmes", - "of": "no", - "outcome": "Iznākums", "panel_default_tab": "Noklusējuma cilne", "panel_language": "Paneļa valoda", "per_cycle_short": "cikls", - "pg_autofill_tip": "Automātiski aizpildīt diapazonu no pašreizējās vērtības ({lo}–{hi})", "pg_best_value": "Atrastā labākā vērtība", "pg_current_value": "Pašreizējā vērtība:", "pg_drag_hint": "Velciet līniju diagrammā", - "pg_dtw": "DTW", "pg_ev_ending": "Beidzas", "pg_ev_running": "Darbojas", "pg_group_advanced": "Robežgadījumi", "pg_group_detection": "Noteikšanas trigeri", "pg_group_timing": "Laika noteikumi", "pg_idle": "Dīkstāvē", - "pg_last_run": "Pēdējā palaišana: {date}", - "pg_match_score": "Atbilstības rezultāts", - "pg_pct_of": "{p}% no {total}", "pg_poor_match": "Slikta atbilstība", "pg_preset": "Izmēģinājumu laukuma priekšiestatījums", "pg_preset_name": "Jauna priekšiestatījuma nosaukums", "pg_preset_none": "Atlasiet priekšiestatījumu…", "pg_strong_match": "Spēcīga atbilstība", - "pg_tile_ambiguous": "Neviennozīmīgi", - "pg_tile_ambiguous_tip": "Cikli, kuros divas programmas ir gandrīz vienādas", - "pg_tile_detected": "Noteikti", - "pg_tile_detected_tip": "Cikli, kuros jauda pietiekami ilgi pārsniedza sākuma slieksni, lai reģistrētos kā darbojas", - "pg_tile_matched": "Saskaņoti", - "pg_tile_matched_tip": "Cikli, kas pareizi piešķirti zināmai programmai", - "pg_time_left": "Atlikušais laiks", "pg_weak_match": "Vāja atbilstība", "phase_name": "Fāzes nosaukums", "name": "Vārds", "phase_timeline": "Fāžu laika josla", "power": "Jauda", "profile": "Profils", - "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. Izvēlieties programmu pirms ierīces palaišanas, un tā tiks lietota, tiklīdz sāksies nākamais cikls.", + "program_tip": "Izvēlieties, ar kuru programmu saskaņot pašreizējo ciklu. Automātiska noteikšana izvēlas labāko atbilstību. Piespraudiet programmu, ja automātiskā noteikšana kļūdās; ja izvēlaties to pirms starta, tā attiecas uz nākamo ciklu.", "progress": "Progress", "raw_socket": "Neapstrādāts kontakts", - "read_only": "Tikai lasāms", "ref_cycle": "Atsauces cikls (pēc izvēles)", - "refresh_interval": "Atsvaidzināšanas intervāls (-i)", "remaining": "Atlikušais", "reminder_every": "Atgādināt ik pēc (cikliem)", "remove_label": "- Noņemiet etiķeti -", "rename_profile": "Pārdēvēt profilu", - "replay_duration": "Atkārtojums", "resulting_profile": "Iegūtais profils", "save_mode": "Saglabāšanas režīms", - "score": "Rezultāts", - "score_breakdown": "Rezultāta sadalījums", "seconds_from_start": "Sekundes no sākuma", "select_profile": "Atlasiet Profils", "setting": "Iestatījums", - "settings_a": "Iestatījumi A (pašreizējie)", - "settings_a_short": "A", "settings_advanced": "Papildu", - "settings_b": "Iestatījumi B (tests)", - "settings_b_short": "B", "settings_basic": "Pamata", "settings_detail_level": "Iestatījumu detalizācijas līmenis", - "settings_overrides": "Iestatījumu aizstāšana", "show_debug": "Rādīt tiešraides atbilstošā atkļūdošanas kartīti statusa lapā (pārliecība, neskaidrība, labākie kandidāti)", "show_expected": "Rādīt paredzamo līknes pārklājumu (atbilstošs profils, oranžs)", "show_raw": "Rādīt neapstrādātas kontaktligzdas pārslēgu tiešraides jaudas grafikā", "sparkline": "Neseno ciklu ilguma tendence", - "stage2": "2. posms – galvenā līdzība", - "stage3": "3. posms – DTW", - "stage4": "4. posms – atbilstība", "status": "Statuss", "tail_trim": "Apgriešana no beigām (s)", "tap_to_dismiss": "pieskarieties diagrammai, lai aizvērtu", @@ -579,7 +520,6 @@ "ungrouped": "Negrupēts", "unlabelled": "Bez etiķetes", "unlabelled_paren": "(bez etiķetes)", - "warp_path": "Deformācijas ceļš", "input": "Ievade:", "start": "Sākums", "end": "Beigas", @@ -604,9 +544,8 @@ "n_shown": ", parādīti {n}", "cycles_title": "Cikli ({n})", "pg_detecting": "Notiek noteikšana", - "pg_best_marker": "Labākais: {v}", "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", + "pg_time_left_model_tip": "Novērtēts pēc programmas fāzēm, nevis fiksēta atpakaļskaitīšana", "phase": "Fāze", "pg_mode_history": "Testēt uz vēstures", "pg_ev_detected": "Noteikts", @@ -638,12 +577,6 @@ "pg_sweep_param": "Parametrs", "pg_objective": "Mērķis", "pg_steps": "Soļi", - "pg_stress_group": "Dīkstāves apturēšanas tests", - "pg_stress_idle_auto": "Auto", - "pg_stress_idle_w": "Dīkstāves līmenis (W)", - "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_obj_match": "Atbilstības precizitāte", "pg_obj_endtiming": "Beigu laika precizitāte", "pg_obj_falseend": "Kļūdaino beigu īpatsvars", @@ -652,12 +585,8 @@ "pg_mode_optimize": "Optimizēt", "pg_group_matching": "Programmu saskaņošana", "pg_zoom_hint": "veiciet dubultklikšķi, lai atiestatītu tuvinājumu", - "aria_playground_chart2": "Interaktīva cikla jaudas diagramma: turiet kursoru virs, lai nolasītu laiku/jaudu, ritiniet, lai tuvinātu, velciet, lai pārvietotu", "pg_alert_timeout_end": "Beidzās pēc noildzes, nevis prognozes", "pg_alert_indefinite": "Darbotos bezgalīgi", - "pg_alert_stress_cap": "Sasniegts drošības ierobežojums", - "pg_alert_stress_ok": "Dīkstāves apturēšana: cikls apturēts", - "pg_alert_stress_warn": "Dīkstāves patēriņš pārsniedz apstāšanās slieksni", "task_pg_history": "Pārbaudīt vēsturē", "task_pg_sweep": "Optimizēt", "task_reprocess": "Atkārtota apstrāde", @@ -667,16 +596,9 @@ "pg_recent_runs": "Nesenās palaišanas", "pg_load_run": "Pārlādēt šo palaišanu", "pg_match_confidence": "Atbilstības ticamība", - "pg_envelope_fit": "Iekļaušanās apvalklīknē", - "pg_envelope_fit_tip": "Cik precīzi cikls iekļaujas šī profila saglabātajā jaudas apvalklīknē, cits skatījums nekā atbilstības ticamība.", "n_imported_note": ", {n} importēti", "enable_online": "Iespējot tiešsaistes funkcijas", - "store_brand": "Ierīces zīmols", - "store_model": "Ierīces modelis", "anonymous": "Anonīms", - "rate_quality": "Novērtēt kvalitāti", - "rate_n": "{n} zvaigznes", - "approved_only": "Tikai apstiprinātie", "adopt_settings": "Pieņemt arī iestatījumus", "flags": "Karodziņi", "include_phase_map": "Iekļaut fāžu karti", @@ -693,7 +615,6 @@ "cat_custom_phases": "Pielāgotās fāzes", "cat_profile_groups": "Profilu grupas", "cat_settings": "Noteikšanas un atpazīšanas iestatījumi", - "cat_matching_config": "Atpazīšanas pielāgošana", "cat_ml_models": "ML modeļi", "cat_feedback": "Atsauksmju un pārskatīšanas atzīmes", "cat_suggestions": "Ieteikumi", @@ -715,7 +636,6 @@ "hist_csv_data": "CSV dati", "hist_from_recorder": "Vai nolasiet to no Home Assistant", "hist_since": "No", - "days": "dienas", "hist_keep": "Saglabāt šo ciklu", "hist_looks_complete": "pabeigts", "peak_power_short": "Maksimums", @@ -735,7 +655,42 @@ "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" + "correct_total_cycles": "Labot kopējo skaitu", + "hist_reason_already_recorded": "pārklājas ar ciklu, ko WashData jau ir ierakstījis", + "share_consent": "Apstiprinu, ka šie cikli normāli noritēja līdz beigām bez pārtraukumiem", + "show_internal": "Rādīt iekšējos iestatījumus", + "show_internal_tip": "Iestatījumi, ko WashData regulē jūsu vietā (paraugu ņemšana, sargsuns, saskaņošanas biežums, sliekšņi). Tiek rādīti, ja šai ierīcei ir sava vērtība, konflikts vai ieteikums.", + "rate_n": "{n} zvaigznes", + "rate_quality": "Novērtēt kvalitāti", + "pg_autolabel": "Automātiskā marķēšana", + "pg_label_review": "Lūdz pārskatīšanu", + "pg_ev_match_reverted": "Atbilstība atsaukta", + "pg_ev_verified_pause": "Apstiprināta pauze", + "n_samples": "{n} paraugi", + "n_samples_one": "{n} paraugs", + "n_samples_zero": "{n} paraugi", + "n_samples_other": "{n} paraugi", + "n_cycles": "{n} cikli", + "n_cycles_one": "{n} cikls", + "n_cycles_zero": "{n} cikli", + "n_cycles_other": "{n} cikli", + "energy_per_cycle": "{v}/ciklu", + "energy_total": "kopā {v}", + "automation_off": "(izslēgta)", + "ref_cycle_none": "Nav", + "share_name": "Rādīt manu vārdu pie tā, ko kopīgoju", + "cat_backfill_cycles": "Cikli, kas atrasti importētā jaudas vēsturē", + "pg_compare_profile": "Salīdzināt ar profilu", + "pg_obj_early": "Priekšlaicīgu beigu īpatsvars", + "pg_obj_endlag": "Beigu aizkave (mediāna)", + "pg_obj_split": "Sadalīšanu īpatsvars", + "pg_sweep_cycles": "Nesenie cikli", + "cycle_context": "Recorder vēsture pirms un pēc cikla", + "cycle_context_off": "Izslēgts", + "cycle_context_minutes": "{min} min", + "cycle_context_series": "Recorder vēsture", + "before_start": "Pirms sākuma", + "after_end": "Pēc beigām" }, "log": { "all_levels": "Visi līmeņi", @@ -745,11 +700,9 @@ }, "ml": { "builtin_models": "● Izmantojot iebūvētos modeļus", - "fit_fair": "Pieņemami", "fit_good": "Labi", "fit_slight": "Neliels", "fit_strong": "Spēcīgs", - "fit_weak": "Vāja", "fit_word": "der", "personalized": "● Personalizēta šai iekārtai", "better_than_baseline": "{pct}% labāk nekā bāzes novērtējums ({metric})", @@ -757,21 +710,31 @@ "trend_declining_tip": "Šā modeļa atbilstība ir pasliktinājusies pēdējās atkārtotajās pārbaudēs, vairāk ciklu pārskatīšana var palīdzēt tam pārmācīties.", "trend_steady_tip": "Šā modeļa atbilstība ir saglabājusies aptuveni nemainīga pēdējās atkārtotajās pārbaudēs.", "cap_blurb": { - "end": "Noteikt, kad cikls patiešām ir pabeigts", - "live_match": "Ātrāk atpazīt notiekošo programmu", - "quality": "Atklāt kļūdaini noteiktus vai bojātus ciklus", - "remaining_time": "Paredzēt, cik ilgs laiks atlicis", "total_energy": "Paredzēt kopējo enerģiju un izmaksas" }, "cap_label": { - "end": "Cikla beigu noteikšana", - "live_match": "Programmas atpazīšana", - "quality": "Cikla kvalitātes pārbaude", - "remaining_time": "Atlikušā laika novērtējums", "total_energy": "Enerģijas novērtējums" }, - "metric_auc": "AUC {auc} uz validācijas datiem", - "metric_mae": "kļūda: {model} pret {baseline} bāzlīniju" + "metric_mae": "kļūda: {model} pret {baseline} bāzlīniju", + "fine_tuned_at": "precizēts {when}", + "last_run_not_promoted": "Pēdējā pārbaude ({when}): {reason}", + "never": "nekad", + "skip_reason": { + "fit_failed": "Apmācība neizdevās: {error}", + "holdout_too_small": "Par maz ciklu, lai godīgi pārbaudītu jaunu modeli: pārbaudei varēja atlikt {held_out}, nepieciešami {min}.", + "not_better_than_incumbent": "Jaunais modelis nebija labāks par izmantoto (kļūda {model} pret {incumbent}), tāpēc tika paturēts pašreizējais.", + "not_better_than_naive": "Jaunais modelis nebija skaidri labāks par vienkāršo novērtējumu (kļūda {model} pret {naive}).", + "unknown": "Nekas jauns netika apgūts.", + "insufficient_rows": "Vēl nepietiek izmantojamu ciklu: līdz šim ir {count} marķēti, tīri cikli.", + "insufficient_rows_zero": "Vēl nepietiek izmantojamu ciklu: līdz šim ir {count} marķēti, tīri cikli.", + "insufficient_rows_one": "Vēl nepietiek izmantojamu ciklu: līdz šim ir {count} marķēts, tīrs cikls.", + "insufficient_rows_other": "Vēl nepietiek izmantojamu ciklu: līdz šim ir {count} marķēti, tīri cikli." + }, + "trained_unknown": "nezināmā laikā", + "held_out_count": "mērīts uz {count} validācijas cikliem", + "held_out_count_zero": "mērīts uz {count} validācijas cikliem", + "held_out_count_one": "mērīts uz {count} validācijas cikla", + "held_out_count_other": "mērīts uz {count} validācijas cikliem" }, "modal": { "auto_label": "Automātiskās etiķetes cikli", @@ -780,7 +743,6 @@ "cycle": "Cikls", "edit_group": "Rediģēt profilu grupu", "edit_phase": "Rediģēšanas fāze", - "import_config": "Importēt konfigurāciju", "label_cycle": "Etiķetes cikls", "merge_cycles": "Apvienot {n} ciklus", "new_group": "Jauna profilu grupa", @@ -795,6 +757,8 @@ "delete_automation_title": "Dzēst automatizāciju", "delete_automation_msg": "Dzēst automatizāciju \"{name}\" no Home Assistant? Šo nevar atsaukt.", "remove_legacy_title": "Noņemt novecojušās darbības", + "reset_phase_msg": "Atsaukt fāzes \"{name}\" labojumu un atjaunot iebūvēto fāzi? Programmas saglabā savus diapazonus.", + "reset_phase_title": "Atiestatīt fāzi", "remove_legacy_msg": "Noņemt novecojušās pielāgotās darbības? Tās vairs netiks izpildītas cikla notikumos. Šo no paneļa nevar atsaukt.", "discard_recording_title": "Izmest ierakstu", "discard_recording_msg": "Izmest saglabāto ierakstu? Šo nevar atsaukt.", @@ -815,32 +779,45 @@ "store_share_device": "Kopīgot šo ierīci", "export_select": "Eksports - izvēlēties datus", "import_wizard": "Imports - izvēlēties datus", - "history_import": "Jaudas vēstures importēšana" + "history_import": "Jaudas vēstures importēšana", + "apply_suggestions_title": "Lietot ieteikumus", + "remove_task_title": "Noņemt uzdevumu", + "remove_task_msg": "Noņemt atgādinājumu \"{task}\"? Tā ieraksti apkopes žurnālā tiek saglabāti.", + "apply_suggestions_msg": "Mainīt šos iestatījumus ({n})? Pēc saglabāšanas integrācija tiks ielādēta no jauna.", + "discard_settings_title": "Izmest nesaglabātās izmaiņas?", + "discard_settings_msg": "Nesaglabātas iestatījumu izmaiņas: {n}. Pamest Iestatījumus un izmest tās?", + "profile_title": "Profils · {name}", + "undo_import_msg": "Šīs ierīces dati un iestatījumi tiek atjaunoti stāvoklī pirms pēdējās importēšanas ({when}). Kopš tā laika reģistrētie cikli un veiktās izmaiņas tiek zaudēti.", + "undo_import_ok": "Atsaukt importēšanu", + "undo_import_title": "Atsaukt pēdējo importēšanu" }, "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.", + "pg_preset_delete_confirm": "Vai dzēst priekšiestatījumu \"{name}\"?", + "pg_preset_limit": "Sasniegts priekšiestatījumu ierobežojums ({n})", + "pg_preset_overwrite": "Vai pārrakstīt priekšiestatījumu \"{name}\"?", "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.", - "refresh_catalog_hint": "Kopienas zīmolu un ierīču saraksti tiek saglabāti kešatmiņā, lai kopīgotais veikals nepārsniegtu savu dienas pieprasījumu limitu. Atsvaidziniet, lai iegūtu citu pievienotos vai apstiprinātos ierakstus.", + "store_sibling_hint": "Jūsu modelim nekas nav kopīgots? Radniecīgs tā paša zīmola modelis parasti ir labs sākums.", + "store_declare_appliance": "Norādiet WashData, kura ierīce jums pieder, lai redzētu citu tai kopīgotās konfigurācijas. Vai arī augšā ievadiet zīmolu un paskatieties apkārt.", + "refresh_catalog_hint": "Kopienas zīmolu un ierīču saraksti tiek glabāti kešatmiņā, lai taupītu veikala dienas limitu. Atsvaidziniet, lai redzētu citu pievienotos vai apstiprinātos ierakstus.", "head_trim_hint": "Cik sekundes noņemt no sākuma", "appliance_monitor": "Ierīces monitors", "artifact_dip_detail": "Krita zem parastā jaudas joslas ~{n}s.", "artifact_footer": "Izcelts augstāk esošajā grafikā. Tie ir pārejoši artefakti (piemēram, durvis atvērtas cikla vidū), ne vienmēr problēmas.", "artifact_header": "Šajā ciklā konstatētas {n} anomālijas", - "artifact_pause_detail": "Jauda krita gandrīz līdz nullei ~{n}s, pēc tam atjaunojās – visticamāk, durvis tika atvērtas cikla vidū vai cikls tika pauzēts.", + "artifact_header_zero": "Šajā ciklā konstatētas {n} anomālijas", + "artifact_header_one": "Šajā ciklā konstatēta {n} anomālija", + "artifact_header_other": "Šajā ciklā konstatētas {n} anomālijas", + "artifact_pause_detail": "Jauda ~{n}s krita gandrīz līdz nullei, pēc tam atjaunojās. Visticamāk, tika atvērtas durvis vai cikls tika pauzēts.", "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": "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.", + "cleanup_intro": "Visi marķētie cikli pārklāti viens uz otra. Atzīmējiet novirzes un noņemiet tiem marķējumu (paliek vēsturē, bet vairs neveido šo profilu) vai izdzēsiet tos.", "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)", - "compare_profiles_tip": "Iepriekš redzamajā diagrammā pārklājiet citas profila aploksnes, lai redzētu, kura no tām vislabāk atbilst šim ciklam.", + "compare_profiles_tip": "Pārklājiet diagrammā citus profilus, lai redzētu, kurš vislabāk atbilst šim ciklam.", "compare_selected_cycles": "Atlasītie cikli (pastāvīgi) – parādīt/slēpt", - "consider_new_profile": "Apsveriet iespēju izveidot jaunu profilu.", - "coverage_gap": "pēdējiem cikliem nav atbilstoša profila ({pct}% no pēdējiem 30).", - "coverage_gap_similar_cycles": "Atrasti {count} līdzīgi nemarķēti cikli – izveidojiet profilu, lai sāktu tos saskaņot.", "cycles_deleted": "Dzēsti cikli: {count}", "enough_data": "Pietiek datu mācīšanai ({current}/{min} cikli).", "export_description": "Izvēlieties precīzi, kurus profilus, ciklus, iestatījumus un citus datus eksportēt uz JSON, vai analizējiet failu un importējiet tikai vēlamās daļas.", @@ -848,30 +825,27 @@ "feedback_prompt": "Apstipriniet, ka tas bija pareizi, labojiet programmu vai ignorējiet.", "feedback_relabel_hint": "Šī cikla atkārtota marķēšana arī to atrisina.", "filter_by_profile": "Filtrēt pēc profila…", - "group_modal_help": "Grupējiet programmas ar vienādu formu, kas atšķiras pēc temperatūras/griešanās (ilgums var atšķirties). Atbilstības gadījumā grupa tiek vērtēta kā viens kandidāts, pēc tam tiek izvēlēts atbilstošākais dalībnieks. Izvēlieties vismaz 2; pārklājums parāda, cik tie ir līdzīgi.", + "group_modal_help": "Grupējiet programmas ar vienādu formu, kas atšķiras pēc temperatūras vai griešanās. Saskaņošana vērtē grupu kā vienu, pēc tam izvēlas atbilstošāko dalībnieku. Izvēlieties vismaz 2; pārklājums parāda, cik tie ir līdzīgi.", "group_not_cohesive": "Šie profili nav pietiekami līdzīgi, lai tos varētu droši grupēt, tāpēc saskaņošanas funkcija tos apstrādā atsevišķi, līdz noņemat novirzi vai sadalāt grupu.", "group_preview_hint": "Atzīmējiet 2+ dalībniekus, lai priekšskatītu un salīdzinātu to jaudas līknes.", - "import_intro": "Tālāk ielādējiet eksportētu failu vai ielīmējiet JSON lietderīgo slodzi.", - "legacy_actions_title": "Joprojām darbojas {count} mantotā pielāgotā darbība", + "legacy_actions_title": "Joprojām darbojas {count} mantotās pielāgotās darbības", + "legacy_actions_title_zero": "Joprojām darbojas {count} mantotās pielāgotās darbības", + "legacy_actions_title_one": "Joprojām darbojas {count} mantotā pielāgotā darbība", + "legacy_actions_title_other": "Joprojām darbojas {count} mantotās pielāgotās darbības", "live_chart_loading": "Tiklīdz tiek parādīti rādījumi, parādās tiešā jaudas diagramma.", "loading": "Notiek ielāde…", "loading_curve": "Notiek līknes ielāde…", "loading_settings": "Notiek iestatījumu ielāde…", "log_buffer_hint": "Vispirms jaunākie · buferē pēdējos 500 ha_washdata ierakstus kopš restartēšanas · velciet apakšējo malu, lai mainītu izmērus.", - "maintenance_advisory": "Ilgums/enerģijas palielināšanās var liecināt par ierīces apkopes nepieciešamību (piemēram, atkaļķošanu, filtru tīrīšanu).", "maintenance_due": "Jāveic apkope: {items}", - "maintenance_intro": "Reģistrējiet apkopi, ko veicat šai ierīcei, un saņemiet atgādinājumu, kad katrs uzdevums atkal jāveic.", + "maintenance_intro": "Reģistrējiet šīs ierīces apkopi un saņemiet atgādinājumu, kad katrs uzdevums atkal jāveic.", "maintenance_load_error": "Neizdevās ielādēt apkopes datus: {error}", "maintenance_requires_access": "Uzturēšanai un eksportam/importēšanai nepieciešama pilna piekļuve.", - "manual_duration_hint": "Profila vidējais/paredzamais cikla garums, ko izmanto atlikušā laika aprēķiniem. Rediģēt, lai to iestatītu; atstājot to nemainīgu, saglabā pašreizējo vērtību.", - "matching_tuning_intro": "Mācoties, WashData pielāgo arī to, cik daudz programmas atbilstības sver formu pret ilgumu un enerģiju.", + "manual_duration_hint": "Profila paredzamais cikla garums, ko izmanto atlikušā laika aplēsēm. Atstājiet nemainītu, lai saglabātu pašreizējo vērtību.", "merge_intro": "Atlasītie cikli tiek apvienoti vienā (hronoloģiskā secībā; spraugas aizpildītas ar 0 W). Izvēlieties iegūto profilu.", "ml_intro": "WashData tiek piegādāts ar viediem modeļiem, kas darbojas jau no kastes.", "ml_learned_intro": "Modeļi, kas precīzi pielāgoti šai iekārtai.", - "ml_loading": "tiek ielādēts ML…", "ml_settings_intro": "Divi neatkarīgi slēdži: viens piemēro modeļus, kamēr darbojas cikls, otrs ļauj WashData laika gaitā precīzi noregulēt tos jūsu mašīnai.", - "name_first_program": "Jums ir pietiekami daudz ciklu – nosauciet savu pirmo programmu, lai sāktu saskaņošanu.", - "near_duplicate_cluster": "Konstatēts gandrīz dublikāts profilu kopa. Grupēšana ļauj saskaņošanai droši izvēlēties starp līdzīgiem (piemēram, viena un tā pati programma atšķirīgā temperatūrā/griešanā).", "no_cycles_match": "Neviens cikls neatbilst pašreizējam filtram.", "no_cycles_profile": "Šim profilam nav ciklu.", "no_cycles_selected": "Vispirms atlasiet vismaz vienu ciklu.", @@ -896,73 +870,54 @@ "no_suggestions": "Nav aktīvu ieteikumu.", "notify_services_hint": "Izmantojiet {entity} pakalpojumu ID (vairākiem – atdaliet ar komatiem). Veidnes mainīgie: {vars}.", "old_actions_warning": "Konfigurēts ar veco darbību redaktoru (tagad noņemts). Tie joprojām aktivizē cikla notikumus, taču tos vairs nevar rediģēt šeit. Pārvērtiet tos par parastu automatizāciju vai noņemiet tos.", - "onboarding_progress": "Novēroti cikli: {n} / 3", - "onboarding_watching": "Lietojiet ierīci kā parasti – WashData vēro. Pēc 3 cikliem sāksies programmu saskaņošana.", "pending_feedback": "Gaida atsauksmes par noteikšanu", "performance_trend": "Veiktspējas tendence ({n} cikli)", - "pg_analysis_empty": "Ielādējiet ciklu, lai redzētu atbilstības analīzi.", "pg_apply_confirm": "Lietot labāko vērtību: {label} = {value}?", - "pg_canvas_empty": "Atlasiet ciklu augšpusē, pēc tam nospiediet Ielādēt, lai redzētu tā jaudas līkni.", - "pg_chart_caption": "Noteikšanas/atbilstības rādītājs pēc {param} vērtībām", "pg_ctrl_intro": "Vērtības tiek ņemtas no šīs ierīces aktīvajiem integrācijas iestatījumiem. Izmaiņas paliek izmēģinājumu laukumā, līdz tās saglabājat iestatījumos.", "pg_live_unavailable": "Aktīvie iestatījumi nav pieejami, tiek rādītas noklusējuma vērtības.", "pg_load_live_confirm": "Vai atmest izmēģinājumu laukuma izmaiņas ({n}) un no jauna ielādēt pašreizējos integrācijas iestatījumus?", - "pg_match_rate": "{pct}% atbilstības rādītājs {total} ciklos", "pg_matches_live": "Atbilst aktīvajiem iestatījumiem", "pg_n_changed": "Atšķirību no aktīvajiem iestatījumiem: {n}", "pg_sugg_none": "Visi ieteikumi jau atbilst pašreizējiem iestatījumiem", - "pg_preset_delete_confirm": "Vai dzēst priekšiestatījumu \"{name}\"?", - "pg_preset_limit": "Sasniegts priekšiestatījumu ierobežojums ({n})", - "pg_preset_overwrite": "Vai pārrakstīt priekšiestatījumu \"{name}\"?", "pg_publish_one_confirm": "Vai saglabāt {label} = {value} šīs ierīces iestatījumos?", "pg_restart_note": "Restartējiet Home Assistant, lai iespējotu simulācijas rīkus.", - "pg_sim_progress": "{done} / {total} cikli", - "pg_starting": "Sākas…", - "pg_stress_synthetic": "dīkstāves simulācija sākas šeit", - "pg_sweep_intro": "Kā būtu, ja {param} atšķirtos? Pārbaudiet {steps} vērtības pēdējos {cycles} ciklos, lai atrastu iestatījumu, kurā pareizi tiek saskaņoti visvairāk ciklu.", - "pg_sweep_step": "Solis {done} / {total}", - "pg_verdict_bad": "Nepieciešama uzmanība: daudzi cikli netiek noteikti.", - "pg_verdict_good": "Labi noregulēts: lielākā daļa ciklu tiek pareizi identificēti un saskaņoti.", - "pg_verdict_ok": "Pieņemami: daži cikli palaisti garām. Mēģiniet pazemināt sākuma slieksni.", "phase_catalog_intro": "Nosauktie cikla segmenti (priekšmazgāšana, sildīšana, izgriešana…). Piešķiriet tos profilam no tā vadības paneļa.", "phase_ranges_intro": "Fāžu diapazoni (minūtes no cikla sākuma), kas pārklāti ar vidējo līkni. Rediģējiet vērtības, lai priekšskatītu tiešraidē.", - "playground_intro": "Droša izmēģinājumu vide, kur atkārtoti palaist iepriekšējos ciklus ar citiem iestatījumiem, salīdzināt divas konfigurācijas blakus un pārbaudīt, kā cikls saskan ar profilu. Nekas šeit nemaina jūsu reālos datus, kamēr to nepārprotami nelietojat.", - "playground_needs_restart": "Lai iespējotu izmēģinājumu laukuma rīkus, restartējiet Home Assistant.", - "preferences_admin": "Preferences, panelis un piekļuves kontrole", - "preferences_adv": "Preferences", + "playground_intro": "Izmēģiniet iestatījumus ar saviem reālajiem ciklu datiem. Nekas šeit nemaina aktīvos iestatījumus, kamēr tos nepiemērojat.", "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), 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.", + "process_history_hint": "Atkārtoti saskaņot visus saglabātos ciklus, atjaunināt ieteikumus, pārtrenēt ML modeļus (ja iespējots), pārrēķināt ciklu izmaksas pēc reģistrētajām 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.", - "rbac_hint": "Kad tas ir izslēgts, katram mājas palīga lietotājam ir pilna piekļuve (noklusējums). Administratoriem vienmēr ir pilna piekļuve, un viņi var pārvaldīt visus.", - "recent_logs": "Jaunākie ha_washdata ieraksti", + "rbac_hint": "Izslēgts: katram Home Assistant lietotājam ir pilna piekļuve (noklusējums). Administratoriem vienmēr ir pilna piekļuve, un viņi var pārvaldīt visus.", "recording_in_progress": "Notiek ierakstīšana", "reminders_intro": "Rādīt atgādinājumu panelī pēc tik cikliem kopš pēdējās apkopes. Atstājiet tukšu vai 0, lai izslēgtu atgādinājumu.", "restart_gap_footer": "Izcelts diagrammā. Jaudas dati trūkst šiem intervāliem; atbilstības noteikšana izmantoja tikai faktiskos rādījumus.", - "restart_gap_header": "{n} HA restartēšanas atstarpe šī cikla laikā", + "restart_gap_header": "{n} HA restartēšanas atstarpes šī cikla laikā", + "restart_gap_header_zero": "{n} HA restartēšanas atstarpes šī cikla laikā", + "restart_gap_header_one": "{n} HA restartēšanas atstarpe šī cikla laikā", + "restart_gap_header_other": "{n} HA restartēšanas atstarpes šī cikla laikā", "restart_gap_item": "{dur} atstarpe", - "review_confirm_help": "Apstipriniet, vai šis cikls tika noteikts pareizi. Jūsu atsauksmes apmāca modeli jūsu mašīnā – jo vairāk ciklu apstiprināsit, jo labāks atbilstības un veselības novērtējums. Pietiek ar ātru Labu/Sliktu.", + "review_confirm_help": "Vai šis cikls tika noteikts pareizi? Apstiprinot ciklus, uzlabojas saskaņošana jūsu mašīnai. Pietiek ar ātru Labi/Slikti.", "review_in_settings": "Pārskatiet sadaļā Iestatījumi", "review_notes_placeholder": "Piezīmes (pēc izvēles)", "review_notes_tip": "Brīvā teksta piezīmes jūsu uzziņai. Netiek izmantots saskaņošanā vai apmācībā.", - "review_profile_tip": "Šī cikla programma ir apzīmēta kā. Ja automātiski noteikta programma bija nepareiza, izlabojiet to šeit – marķējums māca noteikt atbilstību nākamajiem cikliem.", - "review_quality_tip": "Cik tīrs ir šis cikls. Labi = šīs programmas mācību grāmatas piemērs; Slikti = atklāts, bet trokšņains vai netipisks; Nelietojams = nepareizi noteikts (sapludināts, saīsināts vai viltots). Vada veselības rādītāju un to, kuri cikli ir atļauti, lai apmācītu modeli.", - "review_recorded_tip": "Atzīmējiet to kā manuāli izvēlētu atsauces ciklu savai programmai – tāda pati loma kā manuāli ierakstītam ciklam. Atsauces cikli vienmēr tiek saglabāti, ievietojiet atbilstošo veidni, un tīrīšanas laikā tie nekad netiek atmesti. (Šis ir \"zelta\"/ierakstītais karogs; abi ir viens un tas pats.)", - "review_tags_tip": "Izvēles karodziņi, kas apraksta, kas šajā ciklā nogāja greizi, tāpēc apmācība un tīrīšana var to izskaidrot.", + "review_profile_tip": "Programma, ar kuru šis cikls ir iezīmēts. Ja tā ir nepareiza, izlabojiet to šeit; iezīmes māca saskaņošanu.", + "review_quality_tip": "Cik tīrs ir šis cikls. Labi: šīs programmas paraugcikls. Slikti: noteikts, bet trokšņains vai neparasts. Nelietojama: nepareizi noteikts (sapludināts, saīsināts vai viltus). Ietekmē veselības rādītāju un to, kuri cikli apmāca modeli.", + "review_recorded_tip": "Atzīmējiet šo kā manuāli izvēlētu atsauces ciklu tā programmai, līdzīgi kā manuāli ierakstītu ciklu. Atsauces cikli vienmēr tiek saglabāti, veido saskaņošanas veidnes pamatu un tīrīšanā nekad netiek noņemti.", + "review_tags_tip": "Neobligāti karodziņi par to, kas nogāja greizi, lai apmācība un tīrīšana to ņemtu vērā.", "review_to_cycles": "Atveriet ciklu pārskatīšanas rindu", "samples_decimated": "Rāda {shown} no {total} paraugiem (attēlošanai retināts; virsotnes saglabātas). Plaša atstarpe šeit ir retināšana, nevis trūkstoši dati.", - "saving_triggers_reload": "Saglabāšana aktivizē integrācijas atkārtotu ielādi. HA entītijas var īslaicīgi parādīties kā nepieejamas.", + "saving_triggers_reload": "Saglabāšana pārlādē integrāciju. Entītijas var īslaicīgi būt nepieejamas.", "search_placeholder": "Meklēšanas iestatījumi…", "see_recorder": "Skatiet ierakstītāja logrīku zemāk", "setting_changed": "Mainīts no {old} uz {new} {date}", "settings_basic_note": "Tiek rādīti būtiskākie iestatījumi. Pārslēdzieties uz Papildu, lai skatītu pilnu sarakstu.", - "shape_drift_advisory": "⚠ Formas novirze", - "shape_drift_detail": "Šī profila jaudas raksts laika gaitā ir mainījies – iespējama ierīces nodilšana vai nepieciešama apkope (piemēram, atkaļķošana, filtru tīrīšana).", "show_all_settings": "Rādīt visus iestatījumus", - "showing_suggestions": "Tiek rādīts {count} iestatījums ar ieteikumiem.", - "split_intro": "Noklikšķiniet uz diagrammas, lai pievienotu vai noņemtu šķelšanās punktu, vai automātiski noteiktu dīkstāves spraugas. Katrs iegūtais segments var iegūt savu profilu.", - "storage_diagnostics": "Uzglabāšanas statistika, apkope, eksports/imports", + "showing_suggestions": "Tiek rādīti {count} iestatījumi ar ieteikumiem.", + "showing_suggestions_zero": "Tiek rādīti {count} iestatījumi ar ieteikumiem.", + "showing_suggestions_one": "Tiek rādīts {count} iestatījums ar ieteikumiem.", + "showing_suggestions_other": "Tiek rādīti {count} iestatījumi ar ieteikumiem.", + "split_intro": "Noklikšķiniet uz diagrammas, lai pievienotu vai noņemtu sadalīšanas punktu, vai nosakiet tos automātiski pēc dīkstāves pauzēm. Katrai daļai var būt savs profils.", "sug_staged": "{key} = {val} iestatīts. Saglabājiet, lai piemērotu.", "toast_auto_detect_enabled": "Automātiskā noteikšana ieslēgta", "toast_auto_label_complete": "Automātiskā marķēšana pabeigta", @@ -979,7 +934,6 @@ "toast_no_automation": "Šeit nevar izveidot automatizāciju", "toast_profile_name_required": "Profila nosaukums ir obligāts", "toast_program_set": "Programma iestatīta: {program}", - "toast_rebuild_failed": "Atjaunošana neizdevās: {error}", "toast_resume_failed": "Atsākšana neizdevās: {error}", "toast_revert_failed": "Atgriešana neizdevās: {error}", "toast_reverted": "{key} atgriezts", @@ -990,100 +944,89 @@ "trend_energy_up": "Enerģijas tendence pieaug ({pct}%/ciklā) – nesenā vidējā {avg}", "trim_destructive_confirm": "Tas saglabā tikai {pct}% no cikla, un šo nevar atsaukt. Turpināt?", "trim_intro": "Velciet sarkanos rokturus vai ievadiet vērtības. Viss, kas atrodas ārpus loga, tiek noņemts.", - "tuning_suggestions_available": "No novērotajiem cikliem pieejams {count} regulēšanas ieteikums. Tie parādās blakus attiecīgajiem laukiem.", + "tuning_suggestions_available": "No novērotajiem cikliem pieejami {count} regulēšanas ieteikumi. Tie parādās blakus attiecīgajiem laukiem.", + "tuning_suggestions_available_zero": "No novērotajiem cikliem pieejami {count} regulēšanas ieteikumi. Tie parādās blakus attiecīgajiem laukiem.", + "tuning_suggestions_available_one": "No novērotajiem cikliem pieejams {count} regulēšanas ieteikums. Tas parādās blakus attiecīgajam laukam.", + "tuning_suggestions_available_other": "No novērotajiem cikliem pieejami {count} regulēšanas ieteikumi. Tie parādās blakus attiecīgajiem laukiem.", "unsure_detected_prefix": "WashData nav pārliecināts, ka tas ir atklāts", - "updated": "Atjaunināts", "warmup_badge": "Vēl mācoties ({done}/{needed} cikli)", - "warmup_detail": "Šim profilam nepieciešami {needed} iezīmēti cikli, pirms sākas automātiskā saskaņošana. Katrs apstiprināts cikls palīdz mācīties.", + "warmup_detail": "Saskaņošana jau darbojas. WashData lūdz apstiprināt šī profila pirmos ciklus ({needed}), pirms sāk iezīmēt patstāvīgi.", "wipe_history_warning": "Neatgriezeniski dzēst visus ciklus un profilus. Nevar atsaukt.", "compare_overlay_tip": "Pārklājiet apgūto profilu joslas, lai redzētu, kurai programmai katrs cikls līdzinās.", - "duration_trend_up_tip": "Ilgums pieaug ({pct}%/ciklā)", - "duration_trend_down_tip": "Ilgums samazinās ({pct}%/ciklā)", - "energy_trend_up_tip": "Enerģija pieaug ({pct}%/ciklā)", - "energy_trend_down_tip": "Enerģija samazinās ({pct}%/ciklā)", - "performance_trending": "Veiktspējas tendence", - "pg_select_cycle_hint": "Atlasiet ciklu un noklikšķiniet ↺, lai ielādētu", - "pg_sweep_progress": "Solis {done} / {total}", - "pg_analysis_hint": "Atlasiet ciklu un noklikšķiniet ↺, lai ielādētu analīzi.", "diagnostics_load_failed": "Neizdevās ielādēt diagnostiku: {error}", - "manual_duration_ref_hint": "Izmanto tikai tad, ja nav atlasīts atsauces cikls. Atsauces cikls nosaka ilgumu, ņemot vērā tā paša garumu.", + "manual_duration_ref_hint": "Izmanto tikai bez atsauces cikla; atsauces cikls nosaka ilgumu pēc sava garuma.", + "duration_learned_hint": "Aprēķināts no šīs programmas cikliem (bez izlēcējiem) un atjaunināts pēc katra jauna cikla. Lai to mainītu, apgrieziet vai noņemiet neatbilstošos ciklus.", "toast_convert_rolled_back": "Konvertēšana neizdevās un tika atcelta (neviena automatizācija netika atstāta): {error}", "toast_convert_orphan": "Automatizācija tika izveidota, taču veco darbību notīrīšana neizdevās. Nemēģiniet vēlreiz: manuāli noņemiet vecās darbības, lai izvairītos no dublētas automatizācijas.", - "advisory_duration_trend_up": "'{name}' cikli kļūst arvien garāki (apmēram +{pct}% katrā ciklā). Ja ierīces darbība ir mainījusies, ierakstiet no jauna vai pārbūvējiet šo profilu.", - "advisory_energy_trend_up": "'{name}' patērē arvien vairāk enerģijas (apmēram +{pct}% katrā ciklā) - vērts pārbaudīt ierīci, ja tas ir negaidīti.", - "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ā.", + "advisory_duration_trend_up": "'{name}' cikli kļūst arvien garāki (apmēram +{pct}% katrā ciklā). Ja ierīce ir mainījusies, ierakstiet no jauna vai pārbūvējiet šo profilu.", + "advisory_energy_trend_up": "'{name}' patērē arvien vairāk enerģijas (apmēram +{pct}% katrā ciklā). Ja tas ir negaidīti, pārbaudiet ierīci.", + "advisory_poor_health": "'{name}' atbilst slikti: tā nesenie cikli stipri atšķiras vai tiek vāji atpazīti. Pārskatiet tā ciklus vai ierakstiet to no jauna, lai saskaņošana un aplēses paliktu precīzas.", + "advisory_shape_drift": "'{name}' ir novirzījies no sākotnējās jaudas līknes, iespējams, katlakmens vai nolietojuma dēļ. Apsveriet tā ierakstīšanu no jauna ar nesenajiem cikliem.", + "advisory_shape_drift_corr": "'{name}' ir novirzījies no sākotnējās jaudas līknes (korelācija {corr}), iespējams, katlakmens vai nolietojuma dēļ. Apsveriet tā ierakstīšanu no jauna ar nesenajiem cikliem.", + "advisory_unmatchable": "'{name}' nekad netiks atpazīts: tam nav neviena cikla ar jaudas datiem. Iezīmējiet vai ierakstiet šīs programmas ciklu, lai to labotu.", + "advisory_unmatchable_no_duration": "'{name}' nekad netiks atpazīts: tā ciklam nav izmantojama ilguma. Ierakstiet šo programmu no jauna vai norādiet tās paredzamo ilgumu.", "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_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.", "pg_sweep_intro2": "Atrodiet iestatījumu, kas vislabāk atbilst mērķim jūsu nesenajos ciklos. Nekas nemainās, kamēr to nelietojat.", - "pg_running_history": "Notiek jūsu ciklu atskaņošana…", - "pg_running_sweep": "Notiek vērtību pārbaude visos jūsu ciklos…", "pg_row_load_hint": "Ielādēt šo ciklu augšējā diagrammā", - "pg_history_intro2": "Atskaņojiet savus nesenos ciklus caur īsto detektoru un saskaņotāju ar iepriekš norādītajiem iestatījumiem. Noklikšķiniet uz jebkuras rindas, lai ielādētu šo ciklu diagrammā; mainiet iestatījumu, lai redzētu salīdzinājumu pirms/pēc.", + "pg_history_intro2": "Atskaņojiet nesenos ciklus caur īsto detektoru un saskaņotāju ar iepriekš norādītajiem iestatījumiem. Noklikšķiniet uz rindas, lai to ielādētu diagrammā; mainiet iestatījumu, lai salīdzinātu pirms un pēc.", "pg_simulating": "Notiek cikla simulācija…", - "pg_canvas_empty2": "Izvēlieties ciklu augšā un nospiediet Palaist, lai to simulētu. Pēc tam turiet kursoru virs, lai nolasītu vērtības, ritiniet, lai tuvinātu, un velciet, lai pārvietotu.", "pg_apply_settings_confirm": "Vai saglabāt šos {n} iestatījumus šajā ierīcē? {list}", - "pg_stress_terminated_detail": "Nostabilizējās aptuveni {idle} W dīkstāves līmenī -- cikls beidzās {h} h {m} min vēlāk {reason} dēļ.", - "pg_stress_above_threshold_detail": "Dīkstāves patēriņš (~{idle} W) ir vienāds ar faktisko apstāšanās slieksni ({stop} W) vai to pārsniedz -- cikls nekad netika reģistrēts kā kluss. Lai to labotu, palieliniet stop_threshold_w.", - "pg_stress_hit_cap_detail": "Cikls darbojās {h} h {m} min bez apstāšanās -- to piespiedu kārtā apturēja drošības ierobežojums. Dīkstāves patēriņš {idle} W pret apstāšanās slieksni {stop} W.", "no_logs_match": "Neviens žurnāla ieraksts neatbilst šiem filtriem.", "pg_analysis_hint2": "Atlasiet ciklu un nospiediet Palaist, lai ielādētu atbilstības analīzi.", - "pg_analysis_empty2": "Nospiediet Palaist, lai skatītu atbilstības analīzi.", - "imported_readonly": "Importēts no kopienas veikala. Tiek rādīts atsaucei un saskaņošanai. Tas netiek ieskaitīts jūsu statistikā, un to nevar rediģēt.", + "imported_readonly": "Importēts no kopienas veikala. Izmanto atsaucei un saskaņošanai, netiek ieskaitīts jūsu statistikā, un to nevar rediģēt.", "merge_no_imports": "Importētos ierakstus nevar sapludināt ar īstu ciklu. Noņemiet to atzīmi, lai sapludinātu.", - "store_enable_hint": "Iestatījumos iespējojiet tiešsaistes funkcijas, lai pārlūkotu un importētu kopienas atsauces ciklus.", - "advisory_duration_outlier": "{n} cikliem, kas marķēti kā '{name}', ilgums ir pārāk tālu no tā parastā garuma, lai tam jebkad atbilstu (sliktākais: {ratio}x). Tie, visticamāk, ir nepareizi marķēti vai arī viens ieraksts aptver divas darbības reizes. Pārmarķējiet vai sadaliet tos, lai tie vairs nekropļotu šīs programmas laika aplēsi.", + "store_enable_hint": "Iespējojiet tiešsaistes funkcijas sadaļā Preferences > Tiešsaiste un kopiena, lai pārlūkotu un importētu kopienas atsauces ciklus.", + "advisory_duration_outlier": "{n} cikliem, kas marķēti kā '{name}', ilgums ir pārāk tālu no tā parastā garuma, lai tie jebkad atbilstu (sliktākais: {ratio}x). Tie, visticamāk, ir nepareizi marķēti vai satur divas darbības reizes. Pārmarķējiet vai sadaliet tos, lai tie vairs nekropļotu tā laika aplēsi.", "standby_above_stop_title": "Ciklam beidzoties, joprojām patērē {idle} W", "standby_above_stop_sub": "{n} no pēdējiem {total} cikliem. Izslēgšanas aizkave sākas tikai tad, kad jauda ir zem Apstāšanās sliekšņa ({stop} W), tāpēc cikli beidzas ar nokavēšanos vai tie jāaptur manuāli.", - "online_intro": "Kopīgojiet un lejupielādējiet atsauces ciklus ar citiem WashData lietotājiem. Visas tiešsaistes funkcijas ir brīvprātīgas un pēc noklusējuma izslēgtas.", "store_share_intro": "Augšupielādējiet šo atsauces ciklu, lai to varētu izmantot citi ar to pašu ierīci. Pirms publiskošanas tas tiek pārskatīts.", - "online_intro_global": "Pārlūkojiet un kopīgojiet atsauces ierakstus ar citiem WashData lietotājiem un apstipriniet ierīču ierakstus. Viens savienojums attiecas uz visu jūsu WashData integrāciju; ierīces zīmols un modelis tiek iestatīts katrai ierīcei atsevišķi sadaļā Pamatkonfigurācija. Visas tiešsaistes funkcijas ir brīvprātīgas un pēc noklusējuma izslēgtas.", + "online_intro_global": "Pārlūkojiet un kopīgojiet atsauces ierakstus ar citiem WashData lietotājiem. Viens savienojums aptver visas jūsu WashData ierīces; zīmolu un modeli iestata katrai ierīcei sadaļā Pamata. Tiešsaistes funkcijas ir izslēgtas, līdz tās ieslēdzat.", "online_unavailable": "Tiešsaistes funkcijas šajā serverī nav pieejamas.", "store_picker_offline": "Iespējojiet tiešsaistes funkcijas iestatījumu zobratā, lai izvēlētos no kopienas kataloga.", "pick_brand_first": "Vispirms izvēlieties ierīces zīmolu.", - "model_not_found": "Vēl nav katalogā.", - "connect_to_confirm": "Savienojiet iestatījumu zobratā, lai apstiprinātu vai novērtētu.", + "custom_task_limit": "Sasniegts maksimālais uzdevumu skaits ({max}).", + "custom_tasks_intro": "Pievienojiet jebko citu, par ko vēlaties saņemt atgādinājumu. Uzdevums jāveic, kad sasniegts jebkurš no intervāliem; vienu atstājiet tukšu vai 0, lai izmantotu tikai otru.", + "maintenance_due_sensor_hint": "Binārais sensors \"Jāveic apkope\" ir ieslēgts, kamēr kāds uzdevums ir jāveic; izmantojiet to savām paziņojumu automatizācijām.", + "maintenance_supporter_intro": "Maintenance Supporter ir atsevišķa integrācija, kas spēj daudz vairāk: jebkura ierīce vai uzdevums, grafiki pēc laika, lietojuma vai sensora, paziņojumi un vēsture. Tā var izmantot WashData ciklu skaitītāju.", "adopt_settings_hint": "Pārrakstīt šīs ierīces noteikšanas un saskaņošanas sliekšņus ar kopīgotajiem. Jūsu paziņojumi, entītijas un enerģijas cena netiek mainīti.", - "include_settings_hint": "Kopīgot šīs ierīces atpazīšanas un saskaņošanas sliekšņus (ne jūsu paziņojumus, entītijas vai enerģijas cenu). Pieņēmēji izvēlas, vai piemērot tos.", + "include_settings_hint": "Kopīgot šīs ierīces noteikšanas un saskaņošanas sliekšņus (ne paziņojumus, entītijas vai enerģijas cenu). Citi izvēlas, vai tos piemērot.", "onboard_download": "Jauna ierīce? Pieņemiet gatavu iestatījumu (programmas, atskaites ciklus un fāzes) no cita WashData lietotāja ar to pašu ierīci.", - "share_consent": "Es apstiprinu, ka šie cikli noritēja normāli līdz galam bez pārtraukumiem", "share_device_none": "Vēl nav kopīgojamu ciklu. Vispirms cilnē Cikli atzīmējiet ierakstītu vai roku izvēlētu ciklu kā atskaites ciklu (⭐).", "share_guideline_naming": "Nosauciet katru profilu tieši tā, kā parādīts ierīces ciparnīcā vai displejā (piem., 'Cotton 40', 'Eco 60').", - "share_guideline_quality": "Kopīgojiet tikai ciklus, kas pabeidzās normāli -- bez pārtraukumiem cikla vidū, durvju atvēršanas notikumiem vai jaudas svārstībām.", + "share_guideline_quality": "Kopīgojiet tikai ciklus, kas pabeidzās normāli: bez pārtraukumiem, durvju atvēršanas vai jaudas svārstībām.", "share_guideline_review": "Jūsu augšupielāde sākas gaidīšanas statusā un kļūst publiska, kad pietiekams kopienas dalībnieku skaits to apstiprina.", "share_guidelines_title": "Pirms kopīgošanas", - "store_download_device_intro": "Pieņemiet katru kopīgoto programmu un tās atskaites ciklus savā ierīcē. Jūsu pašu ierakstītie cikli un statistika netiek ietekmēti.", + "store_download_device_intro": "Pievienojiet savai ierīcei katru kopīgoto programmu un tās atskaites ciklus. Jūsu pašu cikli un statistika netiek ietekmēti.", "store_share_device_intro": "Augšupielādējiet {brand} {model} ar atlasītajiem atskaites cikliem. Citi ar to pašu ierīci var pieņemt jūsu programmas. Ieraksti tiek pārskatīti pirms publiskošanas.", "share_profile_no_cycles": "Nav atskaites ciklu -- atzīmējiet ciklu kā ⭐ cilnē Cikli, lai iekļautu šo profilu", - "advisory_phase_inconsistent": "Šķiet, ka '{name}' sajauc dažādas programmas vai temperatūras - tā cikli sildās ļoti atšķirīgu laiku. Sadalot to atsevišķos profilos (piemēram, pēc temperatūras), tiks uzlabota saskaņošana un laika aprēķini.", - "advisory_phase_inconsistent_title": "⚠ Iespējams, jauktas programmas", - "export_select_intro": "Atzīmējiet precīzi, ko iekļaut. Izvēloties profilus bez to cikliem, joprojām tiek eksportēta atpazīstama programma (tās apgūtā forma ceļo līdzi).", - "import_analyze_hint": "Ielādējiet eksportēto failu (vai ielīmējiet tā JSON). WashData to analizē un precīzi parāda, ko var importēt, pirms kaut kas mainās.", + "export_select_intro": "Atzīmējiet, ko iekļaut. Profili bez cikliem joprojām eksportē atpazīstamu programmu.", + "import_analyze_hint": "Ielādējiet eksportēto failu (vai ielīmējiet tā JSON). WashData parāda, ko var importēt, pirms kaut kas mainās.", "analyzing": "Notiek analīze…", - "device_type_mismatch_warn": "Šis eksports ir no cita ierīces tipa ({src} vs {local}). Programmas un ciklus joprojām var importēt kā atsauces datus, taču ierīcei specifiskie iestatījumi un reālās vēstures imports ir atspējoti.", + "device_type_mismatch_warn": "Šis eksports ir no cita ierīces tipa ({src} vs {local}). Programmas un ciklus joprojām var importēt kā atsauces datus, bet ne ierīces iestatījumus vai reālo vēsturi.", "merge_hint": "Importētie vienumi tiek pievienoti; nekas no vietējiem datiem netiek zaudēts. Nosaukumu konflikti tiek risināti tālāk.", "replace_warn": "Katra atzīmētā kategorija tiek dzēsta un aizstāta no faila. Neatzīmētās kategorijas paliek neskartas.", "dest_reference_hint": "Importētie cikli tikai uzlabo programmu atpazīšanu un nekad neietekmē lietojuma/enerģijas statistiku.", - "dest_real_history_hint": "Importētie cikli tiek uzskaitīti kā šīs ierīces pašas vēsture un papildina enerģijas/lietojuma statistiku. Izmantojiet, pārvietojot vienu ierīci uz jaunu instalāciju.", + "dest_real_history_hint": "Importētie cikli tiek uzskaitīti kā šīs ierīces pašas vēsture un statistika. Izmantojiet to, lai pārceltu ierīci uz jaunu instalāciju.", "sug_muted": "Šis iestatījums vairs netiks ieteikts", "sug_mute_failed": "Neizdevās klusināt ieteikumu", "sug_unmuted_all": "Klusināto ieteikumu atiestatīšana pabeigta", - "n_suggestions_muted": "{count} klusināti; automātiskais regulētājs tos vairs nepiedāvās.", + "n_suggestions_muted": "{count} ieteikumi klusināti; automātiskais regulētājs tos vairs nepiedāvās.", + "n_suggestions_muted_zero": "{count} ieteikumi klusināti; automātiskais regulētājs tos vairs nepiedāvās.", + "n_suggestions_muted_one": "{count} ieteikums klusināts; automātiskais regulētājs to vairs nepiedāvās.", + "n_suggestions_muted_other": "{count} ieteikumi klusināti; automātiskais regulētājs tos vairs nepiedāvās.", "font_size_hint": "Palieliniet vai samaziniet visu šajā panelī. Attiecas uz jūsu kontu šajā ierīcē.", - "import_history_description": "Viedā kontaktdakša jums bija jau pirms WashData? Augšupielādējiet tās jaudas sensora vēstures eksportu vai nolasiet to tieši no Home Assistant, un parastā noteikšana izies tam cauri, tāpēc iepriekšējie cikli parādīsies jūsu Ciklu sarakstā gatavi nosaukšanai.", - "hist_input_hint": "Augšupielādējiet CSV, kas lejupielādēts no Vēstures paneļa (entītija, stāvoklis, pēdējās izmaiņas), vai ļaujiet WashData nolasīt sensora vēsturi tieši. Pēc tam noteikšana izies tam cauri tieši tāpat kā reāllaikā, un jūs izvēlaties, kurus no atrastajiem cikliem paturēt.", - "hist_recorder_hint": "Nolasa no jūsu izvēlētā datuma līdz pašreizējam brīdim. Home Assistant pēc noklusējuma glabā detalizētu vēsturi 10 dienas, bet pēc tam tikai vidējās vērtības pa stundām, kas ir pārāk rupjas, lai no tām noteiktu ciklus - senāku datumu izvēlieties tikai tad, ja jūsu recorder ir iestatīts glabāt vairāk.", - "hist_scanning": "Jūsu vēsture tiek izlaista caur detektoru. Tas notiek fonā - varat aizvērt šo dialogu un atgriezties vēlāk.", + "import_history_description": "Viedā kontaktdakša jums bija jau pirms WashData? Augšupielādējiet tās jaudas vēsturi vai nolasiet to no Home Assistant. Noteikšana to apstrādās, un iepriekšējie cikli parādīsies jūsu Ciklu sarakstā, gatavi nosaukšanai.", + "hist_input_hint": "Augšupielādējiet CSV no Vēstures paneļa (entītija, stāvoklis, pēdējās izmaiņas) vai ļaujiet WashData nolasīt sensora vēsturi tieši. Noteikšana to apstrādā tāpat kā reāllaikā, un jūs izvēlaties, kurus ciklus paturēt.", + "hist_recorder_hint": "Nolasa no izvēlētā datuma līdz šim brīdim. Home Assistant pēc noklusējuma glabā detalizētu vēsturi 10 dienas; vecākas stundas vidējās vērtības ir pārāk rupjas ciklu noteikšanai.", + "hist_scanning": "Jūsu vēsture fonā tiek izlaista caur detektoru. Varat aizvērt šo dialogu un atgriezties vēlāk.", "hist_imported_count": "Importēti cikli: {n}.", "hist_duplicates": "Jau iepriekš importēti un izlaisti: {n}.", "hist_capped": "Sasniegts ierīces importēto ciklu ierobežojums; pārējie netika saglabāti.", - "hist_next_step": "Tie ir jūsu Ciklu sarakstā, atzīmēti kā importēta vēsture. Atveriet vienu un izmantojiet pogu Iezīmēt, lai norādītu tā programmu.", + "hist_next_step": "Tie ir jūsu Ciklu sarakstā, atzīmēti kā importēta vēsture. Atveriet vienu un ar pogu Iezīmēt norādiet tā programmu.", "hist_rows_read": "Nolasīti rādījumi: {n}", "hist_entity_substituted": "nolasīts {used} (šai ierīcei ir konfigurēts {wanted})", "hist_breaks": "Pārtraukumi, kad sensors nebija pieejams: {n}", @@ -1095,25 +1038,74 @@ "hist_scan_capped": "Tiek parādīti tikai pirmie kandidāti (atrasti: {n}).", "hist_recorder_empty": "Home Assistant nav detalizētas vēstures par šo sensoru šajā periodā.", "hist_scan_failed": "Skenēšana neizdevās.", - "hist_scan_expired": "Šī skenēšana vairs nav pieejama. Lūdzu, skenējiet vēlreiz.", + "hist_scan_expired": "Šī skenēšana vairs nav pieejama. 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.", - "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.", + "imported_history_readonly": "Atrasts importētā jaudas vēsturē. Tas palīdz saskaņošanā, bet netiek ieskaitīts jūsu statistikā, un to nevar apgriezt vai sadalīt. Iezīmējiet to, lai norādītu programmu.", + "odometer_intro": "Šis kopējais skaits tikai palielinās. Cikla dzēšana to nesamazina, tāpēc apkopes atgādinājumi paliek precīzi.", + "odometer_correct_hint": "Iestatiet to, ja WashData ir saskaitījis ciklu, kas nekad nav noticis, vai ierīce veica 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}", "toast_range_required": "Iestatiet derīgu No/Līdz diapazonu.", - "no_profiles_option": "(vēl nav profilu, pievienojiet vienu)" + "no_profiles_option": "(vēl nav profilu, pievienojiet vienu)", + "compare_cycles_title": "Salīdzināt ciklus ({count})", + "edit_builtin_phase": "Šī ir iebūvēta fāze. Saglabājot tiek izveidota pielāgota kopija; izdzēsiet kopiju, lai atjaunotu oriģinālu.", + "fetch_error": "Neizdevās ielādēt datus.", + "connection_lost": "Savienojums zaudēts - rādīti dati uz {time}.", + "pg_notify_partial": "Marķieri rāda tikai sākuma, „gandrīz gatavs”, beigu, atskaites punktu un kluso stundu paziņojumus. Reāllaika atjauninājumi, izkraušanas atgādinājumi un pārsnieguma brīdinājumi netiek simulēti.", + "connect_to_confirm": "Savienojiet iestatījumu zobratā, lai apstiprinātu vai novērtētu.", + "coverage_gap_cluster": "Nesenie nemarķētie cikli ({count}), aptuveni {min} min gari, izskatās pēc vienas programmas, kas vēl nav izveidota.", + "pg_label_yes": "Cikla beigās tiktu marķēts kā \"{name}\"", + "pg_label_no": "Lūgtu pārskatīšanu: {reason}", + "live_match_debug_tip": "Pārliecība: cik precīzi pašreizējā jaudas līkne atbilst labākajam kandidāta profilam (0-100%). Neskaidra: divu labāko kandidātu rezultāti atšķiras mazāk nekā par 5% - atzīme ir nedroša, līdz cikls beidzas.", + "automation_alias_started": "{device}: cikls sākās", + "automation_alias_finished": "{device}: cikls beidzās", + "automation_desc_started": "Darbojas, kad sākas ierīces {device} cikls. Pievienojiet savas darbības (paziņojumi, gaismas, ...).", + "automation_desc_finished": "Darbojas, kad beidzas ierīces {device} cikls. Pievienojiet savas darbības (paziņojumi, gaismas, ...).", + "feedback_detected": "WashData noteica: {name}", + "import_undo_hint": "Tiek glabāts atjaunošanas punkts no laika pirms pēdējās importēšanas ({when}): {profiles} profili un {cycles} cikli.", + "replace_undo_hint": "Vispirms tiek saglabāts atjaunošanas punkts, tāpēc šo importēšanu var atsaukt sadaļā Eksportēt / importēt.", + "pg_in_sample_note": "Šie cikli palīdzēja izveidot profilus, ar kuriem tie tiek saskaņoti, tāpēc rezultāti ir optimistiski: jauniem cikliem sagaidiet nedaudz mazāk pareizu saskaņojumu.", + "hist_tail_trimmed": "Tika apgrieztas {min} min gaidīšanas pēc cikla beigām, kā to dara arī darbojošā integrācija", + "hist_truncated": "apstājās pie {n} rādījumu ierobežojuma, tāpēc pārējā vēsture netika skenēta", + "hist_warn_kw": "Augstākais rādījums ir tikai {w}. Šķiet, ka šis sensors ziņo kilovatos; WashData vajag vatus, tāpēc ciklus nevar noteikt.", + "pg_compare_profile_tip": "Izvēlas tikai profilu, kas tiek attēlots virs grafika. Simulācija vienmēr pati saskaņo ciklu ar programmu, tāpat kā darbojošā integrācija.", + "pg_outcome_stale": "Kopš šīs simulācijas palaišanas iestatījumi ir mainījušies. Tā joprojām rāda veco rezultātu.", + "pg_sweep_guarded": "Priekšlaicīgi beidz vairāk ciklu, sadala vairāk ciklu vai palaiž garām ciklus, kurus pašreizējais iestatījums notver, tāpēc tas nekad netiek ieteikts.", + "pg_sweep_keep_current": "Paturiet pašreizējo vērtību ({value}, {metric}): neviena vērtība nebija labāka vismaz par vienu ciklu.", + "pg_task_busy": "Šai ierīcei jau notiek režīma Testēt uz vēstures vai Optimizēt palaišana. Pagaidiet vai atceliet to.", + "hist_warn_naive_time": "{n} laikspiedoliem nav laika joslas, un tie tika nolasīti kā UTC. Ja failā ir vietējais laiks, cikli ir nobīdīti par jūsu UTC nobīdi, un rādījumi ap vasaras laika maiņu var tikt izmesti.", + "hist_warn_naive_time_zero": "{n} laikspiedoliem nav laika joslas, un tie tika nolasīti kā UTC. Ja failā ir vietējais laiks, cikli ir nobīdīti par jūsu UTC nobīdi, un rādījumi ap vasaras laika maiņu var tikt izmesti.", + "hist_warn_naive_time_one": "{n} laikspiedolam nav laika joslas, un tas tika nolasīts kā UTC. Ja failā ir vietējais laiks, cikli ir nobīdīti par jūsu UTC nobīdi, un rādījumi ap vasaras laika maiņu var tikt izmesti.", + "hist_warn_naive_time_other": "{n} laikspiedoliem nav laika joslas, un tie tika nolasīti kā UTC. Ja failā ir vietējais laiks, cikli ir nobīdīti par jūsu UTC nobīdi, un rādījumi ap vasaras laika maiņu var tikt izmesti.", + "pg_sweep_n_cycles": "{count} ciklos", + "pg_sweep_n_cycles_zero": "{count} ciklos", + "pg_sweep_n_cycles_one": "{count} ciklā", + "pg_sweep_n_cycles_other": "{count} ciklos", + "cycle_context_loading": "Ielādē recorder vēsturi…", + "cycle_context_note": "Pelēkā krāsā: jaudas sensora recorder vēsture ap šo ciklu. Tā nav cikla daļa un neietekmē ilgumu, enerģiju vai saskaņošanu.", + "cycle_context_none": "Recorder nesatur nolasījumus pirms un pēc šī cikla (Home Assistant pēc noklusējuma glabā vēsturi 10 dienas).", + "cycle_context_tip": "Tikai attēlošanai: rāda jaudas sensora recorder vēsturi ap šo ciklu, ja tā vēl glabājas. Lai turpmākajiem cikliem pievienotu neizdevušos starta mēģinājumus, izmantojiet Līknes ievadsekundes (Iestatījumi > Noteikšana > Cikla sākums), kas maina saglabātos ilgumus.", + "invalid_threshold_pair": "Apstāšanās slieksnim ({stop} W) jābūt zemākam par Starta slieksni ({start} W). Nekas netika saglabāts; mainiet abas vērtības kopā." }, "phase_desc": { "anti_crease": "Laiku pa laikam pēc pabeigšanas veiciet īsus gājienus, lai samazinātu grumbiņas.", "anti_wrinkle": "Periodiska griešana pēc cikla, lai samazinātu grumbas.", + "baking": "Karsēšana augstā temperatūrā, lai izveidotos garoza un mīkstums.", + "cooking": "Galvenā gatavošanas fāze ar ieslēgtu sildītāju un ventilatoru.", "cool_down": "Tuvošanās cikla beigām bez siltuma.", + "cool_down_af": "Dzesēšana tikai ar ventilatoru pēc karsēšanas.", "drain_and_switch": "Pārejas periods no mazgāšanas uz žāvēšanas režīmu.", "dry": "Žāvēšanas posms, izmantojot sildītāju un/vai atlikušo siltumu.", "drying": "Galvenais apsildāmās sūšanas periods.", "heat_up": "Sākotnējā sildītāja iesildīšana pirms pilnīgas žāvēšanas.", + "keep_warm": "Viegla sildīšana, lai ēdiens paliktu silts.", + "keep_warm_bm": "Viegla sildīšana, lai maize pēc cepšanas paliktu silta.", + "kneading": "Motors maisa un mīca mīklu. Liels jaudas patēriņš.", + "pause_af": "Īsa pauze sakratīšanai vai pārbaudei.", + "pre_heat": "Kameras sākotnējā uzkarsēšana pirms gatavošanas.", "pre_rinse": "Sākotnējā izsmidzināšana pirms mazgāšanas ar mazgāšanas līdzekli.", + "proving": "Rūgšana nelielā siltumā: raugs rūgst un mīkla kāpj.", + "resting": "Īsa mazas jaudas pauze starp mīcīšanām, lai lipeklis atslābtu.", "pre_wash": "Sākotnējā mērcēšana vai pirmapstrāde pirms galvenās mazgāšanas.", "rinse": "Tīra ūdens skalošanas stadija. Šī fāze var atkārtot vairākas reizes.", "sanitize": "Augstas temperatūras tīrīšanas posms sanitārijas programmām.", @@ -1126,6 +1118,7 @@ "wash_dw": "Galvenā mazgāšanas līdzekļa mazgāšana ar sildīšanu." }, "placeholder": { + "custom_task": "piem., Tīrīt durvju blīvējumu", "add": "pievienot…", "group_name": "piem. Kokvilna 2:47", "json_buttons": "[{\"action\":\"ID\",\"title\":\"Label\"}]", @@ -1153,10 +1146,10 @@ "section": { "dishwasher": { "label": "Trauku mazgājamā mašīna", - "intro": "Cikla beigu apstrāde trauku mazgājamām mašīnām, kuras parasti beidzas ar ilgu, gandrīz klusu žāvēšanas fāzi pirms īsas galīgās ūdens izvadīšanas." + "intro": "Beigu apstrāde trauku mazgājamām mašīnām, kas bieži ilgi klusi žāvē pirms īsas ūdens izvadīšanas beigās." }, "anti_wrinkle": { - "intro": "Pretgrumbu/pretlocīšanas režīms atpazīst zemas jaudas kratīšanas impulsus pēc galvenās fāzes un pievieno tos pabeigtajam ciklam.", + "intro": "Pievieno pabeigtajam ciklam žāvētāja zemas jaudas griešanās impulsus pēc galvenās fāzes.", "label": "Pretgrumbu" }, "basic": { @@ -1164,7 +1157,7 @@ "label": "Pamata" }, "delay": { - "intro": "Aizkavēta starta noteikšana identificē, kad ierīce ir ieslēgta, bet vēl nav sākusi ciklu.", + "intro": "Nosaka ierīci, kas ir ieslēgta un gaida aizkavētu startu.", "label": "Aizkavēts starts" }, "detection": { @@ -1188,161 +1181,153 @@ "label": "Laika grafiks un sargsuns" }, "triggers": { - "intro": "Neobligātie ārējie signāli: beigu aktivators, durvju sensors, pauzes slēdzis un izkraušanas atgādinājums.", + "intro": "Neobligāti papildinājumi: beigu aktivators, durvju sensors, pauzes slēdzis un izkraušanas atgādinājums.", "label": "Aktivatori un durvis" - }, - "phase_eta": { - "label": "Atlikušais laiks", - "intro": "Uz fāzēm balstīts atlikušais laiks mašīnām, kuru cikla ilgums ir atkarīgs no temperatūras vai izgriešanas." } }, "setting": { "dishwasher_end_spike_quiet_release": { "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." + "doc": "Pēc paredzētā ilguma: cik ilgi trauku mazgājamai mašīnai jāpaliek zem Apstāšanās sliekšņa, pirms WashData pārtrauc gaidīt beigu ūdens izvadīšanu. Palieliniet to, ja vēla izvadīšana pēc ilgas žāvēšanas tiek palaista garām. Gaidīšana nekad nepārsniedz 30 minūtes." }, "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.", + "doc": "Programmas vidējā ilguma daļa, kas ciklam jāsasniedz, pirms tā zemas jaudas griešanās tiek uzskatīta par pretgrumbu asti. Žāvētājiem ar slodzes sensoru samaziniet to (piem., 0,75). Veļas mašīnām atstājiet noklusējumu (0,98), citādi mazgāšana var beigties pusceļā.", "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.", + "doc": "Saglabāt žāvētāja īsos zemas jaudas griešanās impulsus pēc galvenās karsēšanas fāzes kopā ar pabeigto ciklu, nevis uzskatīt tos par jauniem cikliem.", "label": "Iespējot pretgrumbu noteikšanu" }, "anti_wrinkle_exit_power": { - "doc": "Lai pretgrumbu režīms paliktu aktīvs, starp impulsiem jaudai ir jāsamazinās zem šī.", + "doc": "Pretgrumbu režīmā jauda zem šīs vērtības tiek uzskatīta par pauzi starp griešanas impulsiem, un pauze, kas ilgāka par Maks. atstarpi starp impulsiem, režīmu beidz. Ja Apstāšanās slieksnis ir augstāks, tiek izmantots tas, tāpēc šis iestatījums kaut ko maina tikai virs Apstāšanās sliekšņa.", "label": "Izejas jaudas slieksnis" }, "anti_wrinkle_idle_timeout": { - "doc": "Cik ilgi mašīna drīkst palikt klusa starp diviem griešanās impulsiem, pirms pretgrumbu režīms beidzas. Iestatiet to garāku par garāko atstarpi, ko jūsu žāvētājs atstāj starp impulsiem, citādi katrs nākamais impulss tiek uztverts kā kļūdains starts.", + "doc": "Cik ilgi žāvētājs drīkst palikt kluss starp griešanās impulsiem, pirms pretgrumbu režīms beidzas. Iestatiet to garāku par garāko žāvētāja pauzi, citādi vēlākie impulsi tiek uzskatīti par kļūdainiem startiem.", "label": "Maks. atstarpe starp impulsiem" }, "anti_wrinkle_max_duration": { - "doc": "Pākšaugi, kas ir garāki par šo, tiek uzskatīti par īstu ciklu, nevis kā pretgrumbu izkrišanu.", + "doc": "Impulss, kas ir garāks par šo, tiek uzskatīts par īstu ciklu, nevis pretgrumbu griešanos.", "label": "Maks. ilgums" }, "anti_wrinkle_max_power": { - "doc": "Pulss, kas pārsniedz šo jaudu, tiek uzskatīts par īstu jaunu ciklu, nevis pretgrumbu griešanos. Iestatiet tieši virs impulsa jaudas.", + "doc": "Impulss virs šīs jaudas tiek uzskatīts par īstu jaunu ciklu, nevis griešanos. Iestatiet to nedaudz virs griešanās impulsa jaudas.", "label": "Maks. pretgrumbu jauda" }, "auto_label_confidence": { - "doc": "Ja atbilstības rezultāts cikla beigās ir vienāds vai lielāks par šo, programma tiek automātiski marķēta bez apstiprinājuma uzvednes. Paaugstiniet to, lai pirms automātiskās marķēšanas būtu nepieciešama lielāka noteiktība; samaziniet to, lai vairāk automatizētu. Darbojas kopā ar Learning Confidence zem tā.", + "doc": "Atbilstības rezultāts cikla beigās, no kura programma tiek marķēta bez jautāšanas. Paaugstiniet to lielākai noteiktībai; samaziniet, lai automātiski marķētu vairāk ciklu. Turiet to virs Mācīšanās pārliecības.", "label": "Automātiskās marķēšanas pārliecība" }, "auto_maintenance": { - "doc": "Veiciet nakts uzkopšanu: atjaunojiet profila aploksnes, pārrēķiniet cikla stāvokli, izgrieziet atkļūdošanas pēdas un saglabājiet jaunākos ciklus.", + "doc": "Nakts uzkopšana: marķē vecus ciklus, kas tagad skaidri atbilst kādai programmai, pārbūvē programmu līknes, atsvaidzina anomāliju atzīmes un ciklu veselību un noņem atkļūdošanas pēdas, ja \"Saglabāt atkļūdošanas pēdas\" ir izslēgts. Cikli un to jaudas trases vienmēr tiek saglabāti.", "label": "Automātiskā apkope (nakts tīrīšana)" }, "completion_min_seconds": { - "doc": "Cikli, kas ir īsāki par šo, tiek izmesti kā spoku cikli (pārbaudes darbi, atverot durvis, lai pievienotu zeķi).", + "doc": "Cikli, kas ir īsāki par šo (testa palaišanas, durvju atvēršana, lai ieliktu zeķi), tiek saglabāti kā pārtraukti un nesaņem beigu paziņojumu.", "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.", + "doc": "Pievieno cikla sākumam neizdevušos starta mēģinājumu nolasījumus, ja mašīna pirms starta pamēģina (durvju bloķēšana, pirmā uzpilde). 300 s pietiek vairumam; 0 = izslēgts. Reģistrētais sākums pārceļas agrāk, tāpēc apgūtie ilgumi kādu laiku mainās.", "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.", + "doc": "Jaudai tik ilgi jāpaliek gaidstāves joslā, pirms tiek uzskatīts, ka ierīce gaida startu.", "label": "Apstiprināšanas sekundes" }, "delay_start_detect_enabled": { - "doc": "Noteikt, kad ierīce ir ieslēgta un gaida (aizkavēta palaišana / gaidīšanas režīms), bet nav sākusi savu ciklu, tāpēc gaidstāves vilkšana netiek sajaukta ar darbības ciklu.", + "doc": "Noteikt ierīci, kas ir ieslēgta un gaida aizkavētu startu, lai tās gaidstāves patēriņu neuzskatītu par darbojošos ciklu.", "label": "Iespējot aizkavēta starta noteikšanu" }, "delay_timeout_hours": { - "doc": "Pārtrauciet gaidīt aizkavētās palaišanas režīmā pēc tik daudzām stundām un atgriezieties dīkstāvē, lai mašīna, kas atstāta darbināta, bet nekad nav iedarbināta, negaida mūžīgi.", + "doc": "Pēc tik daudz stundām pārtraukt gaidīt aizkavētu startu un atgriezties dīkstāvē.", "label": "Noilguma stundas" }, "device_type": { - "doc": "Ierīču klase. Iestata saprātīgus noteikšanas noklusējuma iestatījumus (sliekšņus, izslēgšanas aizkavi, beigu apstrādi), kas pielāgoti šim ierīces veidam; mainiet to tikai tad, ja ierīce sākotnēji tika iestatīta kā nepareiza tipa.", + "doc": "Iestata šāda veida ierīcei noteikšanas noklusējumus (sliekšņus, izslēgšanas aizkavi, beigu apstrādi). Mainiet to tikai tad, ja ierīce tika iestatīta ar nepareizu tipu.", "label": "Ierīces veids" }, "door_sensor_entity": { - "doc": "Pēc izvēles durvju binārais sensors. Izmanto, lai noteiktu, kad ierīce ir atvērta/izkrauta pēc cikla.", + "doc": "Neobligāts durvju sensors. Rāda, kad ierīce pēc cikla tika atvērta izkraušanai.", "label": "Durvju sensora entītija" }, "duration_tolerance": { - "doc": "Pielaide atlikušā laika aplēsēm (apgūstot atsauksmes, nevis saskaņošanu). Ja faktiskais ilgums ir +/-X% robežās no aplēstā, tas tiek uzskatīts par labu atbilstību.", + "doc": "Tikai mācīšanās atgriezeniskajai saitei: cikls +/-X% robežās no aplēstā ilguma tiek uzskatīts par labu aplēsi. Neietekmē saskaņošanu.", "label": "Novērtējuma tolerance" }, "enable_ml_models": { - "doc": "Cikls darbojas, ļaujiet modeļiem uzlabot reāllaika rezultātus: stabilāku atlikušā laika un enerģijas/izmaksu aprēķinu, kā arī pretagrīnas apstāšanās aizsargu, kad tiek konstatēts cikls (tas var tikai aizkavēt finišu, nekad nevar beigties ātrāk un ir ierobežots). Izmanto jūsu precizētos modeļus, kad tie ir pieejami, pretējā gadījumā iebūvētos. Izslēgts = tikai klasiskā uz jaudu balstītā loģika (joprojām uzticama).", + "doc": "Izmantot šai mašīnai apmācītu modeli, lai prognozētu darbojošā cikla enerģiju un izmaksas, un ļaut labi zināmai programmai, kas apstājas neparasti agri, beigties bez pilnas izslēgšanas aizkaves. Izslēgts: tikai uz jaudu balstīta loģika.", "label": "Lietot viedos modeļus cikla laikā" }, "end_energy_threshold": { - "doc": "Izslēgšanas aizkaves atpakaļskaitīšanas laikā uzkrātā enerģija (vati x laiks) tiek salīdzināta ar šo slieksni. Ja tas tiek pārsniegts, atpakaļskaitīšanas laiks tiek atiestatīts – ciklam tiek pievienotas pretburzīšanās un trauku mazgājamās mašīnas žāvēšanas astes, nevis saīsinātas. Paceliet to, ja atdzišanas laikā cikli beidzas pārāk agri; samaziniet to, ja noteikšana ir gausa.", + "doc": "Izslēgšanas aizkaves laikā pieļaujamā enerģija. Ja tā ir lielāka, cikls turpinās, tāpēc griešanās un žāvēšanas nobeigumi paliek pievienoti. Samaziniet to, ja cikli beidzas nobeiguma laikā; palieliniet, ja pabeigti cikli turpina gaidīt.", "label": "Beigu enerģija" }, - "end_repeat_count": { - "doc": "Pirms cikla beigām nepieciešamo secīgo rādījumu skaits zem apstāšanās sliekšņa. 1 ir piemērots lielākajai daļai spraudņu. Palieliniet vērtību līdz 2–3, ja viedā spraudnis cikla vidū laiku pa laikam ziņo par nepatiesu nulli un cikli beidzas priekšlaicīgi.", - "label": "Beigu atkārtojumu skaits" - }, "energy_price_entity": { - "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.", + "doc": "Sensors ar pašreizējo cenu par kWh (piem., dinamisks tarifs). Tas aizstāj zemāk norādīto fiksēto cenu, tāpēc tam jābūt cenai, nevis spraudņa jaudas vai enerģijas sensoram.", "label": "Enerģijas cenas entītija" }, "energy_price_static": { - "doc": "Fiksētā cena par kWh, ko izmanto izmaksu skaitļiem, ja iepriekš nav iestatīta reāllaika cenas vienība.", + "doc": "Fiksētā cena par kWh, ko izmanto, ja nav iestatīta cenas entītija.", "label": "Fiksētā enerģijas cena (par kWh)" }, "energy_sensor": { - "doc": "Neobligāts kumulatīvs enerģijas skaitītājs (total_increasing kWh/Wh, piemēram, paša kontaktdakšas kopējais skaitītājs). Ja iestatīts, katra cikla uzrādītā enerģija tiek ņemta no šī skaitītāja rādījuma starpības no sākuma līdz beigām, kas novērš nepietiekamu uzskaiti, ko rada lēni ziņojoša jaudas sensora integrēšana. Atgriežas pie integrētās vērtības, ja rādījuma nav, tā mērvienība nav zināma vai starpība nav pozitīva. Atstājiet tukšu, lai turpinātu integrēt jaudas sensoru.", + "doc": "Neobligāts kWh vai Wh skaitītājs (total_increasing, piem., spraudņa paša skaitītājs). Cikla enerģija tiek nolasīta no tā, kas ir precīzāk nekā aprēķins no lēna jaudas sensora. Ja rādījums nav izmantojams, tiek lietota aprēķinātā vērtība.", "label": "Enerģijas skaitītāja entītija" }, "expose_debug_entities": { - "doc": "Publicējiet papildu diagnostikas HA entītijas (atbilstības ticamība, neskaidrība, stāvokļa iekšējās vērtības). Izslēgts saglabā entītiju sarakstu tīru normālai lietošanai.", + "doc": "Pievienot diagnostikas entītijas (atbilstības ticamība, neskaidrība, iekšējais stāvoklis). Izslēgts: entītiju saraksts paliek tīrs.", "label": "Atklāt atkļūdošanas entītijas" }, "external_end_trigger": { - "doc": "Binārais sensors, kura stāvokļa maiņa iezīmē cikla beigas (piemēram, ierīces \"pabeigts\" kontakts vai pavadošā integrācija).", + "doc": "Binārais sensors, kura maiņa iezīmē cikla beigas (piemēram, \"pabeigts\" kontakts vai cita integrācija).", "label": "Ārējā aktivatora entītija" }, "external_end_trigger_enabled": { - "doc": "Ļaujiet ārējam binārajam sensoram signalizēt par cikla beigām, papildus iebūvētajai jaudas noteikšanai.", + "doc": "Ļaut binārajam sensoram signalizēt cikla beigas papildus jaudas noteikšanai.", "label": "Iespējot ārējo beigu aktivatoru" }, "external_end_trigger_inverted": { - "doc": "Uztveriet sprūda sensora izslēgšanu (nevis IESLĒGTS) kā cikla beigu signālu.", + "doc": "Uzskatīt sensora pāreju uz IZSLĒGTS (nevis IESLĒGTS) par beigu signālu.", "label": "Invertēt ārējo aktivatoru (aktivēt pie IZSLĒGTS)" }, "learning_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.", + "doc": "Zemākais atbilstības rezultāts pabeigta cikla marķēšanai. Skaidrs uzvarētājs tiek marķēts; ja rezultāti ir tuvi, cikls nonāk pārskatīšanas rindā sadaļā Cikli. Zem šīs vērtības nekas netiek marķēts un nekas netiek jautāts. Turiet to zem Automātiskās marķēšanas pārliecības.", "label": "Mācīšanās pārliecība" }, "linked_device": { - "doc": "Pēc izvēles ievietojiet šo WashData ierīci zem citas ierīces (piemēram, viedspraudņa) HA ierīču reģistrā, kas tiek parādīta kā “Savienots, izmantojot...”.", + "doc": "Neobligāti. Rādīt šo WashData ierīci zem citas ierīces (piem., viedā spraudņa) Home Assistant, kā “Savienots, izmantojot ...”.", "label": "Grupēt zem ierīces" }, "min_off_gap": { - "doc": "Ja iekārta izslēdzas mazāk par šo laiku, ieslēgšanas/izslēgšanas/ieslēgšanas secība tiek uzskatīta par vienu nepārtrauktu ciklu. Novērš mērcēšanas programmu (mašīna izslēdzas uz vairākām minūtēm mazgāšanas laikā) sadalīšanu divos atsevišķos ciklos. Ja vēlaties, lai tās tiktu uzskaitītas kā atsevišķi cikli, iestatiet to īsāku par atstarpi starp slodzēm, kas tiek veiktas vienā virzienā. Ierīces tipa noklusējuma iestatījumi aizsargā katras ierīces tipisko pauzi cikla laikā.", + "doc": "Izslēgšanās pauze, kas īsāka par šo, nesadala ciklu, tāpēc mērcēšanas pauzes paliek vienā mazgāšanā. Turiet to īsāku par pauzi starp secīgām mazgāšanām, citādi tās saplūst vienā ciklā.", "label": "Min. izslēgšanas atstarpe" }, "min_power": { - "doc": "Absolūtā minimālā jauda tiek uzskatīta par aktīvu. Rādījumi, kas ir zemāki par šo, tiek uzskatīti par 0 W (gaidīšanas režīmā), filtrējot viedo spraudņu un gaidstāves gaismas diožu fantoma slodzi.", + "doc": "Rādījumi zem šīs vērtības tiek uzskatīti par 0 W (gaidstāve). Atfiltrē viedo spraudņu un gaidstāves gaismas diožu dīkstāves patēriņu.", "label": "Minimālā jauda" }, "ml_training_enabled": { - "doc": "Periodiski izpētiet savus pārskatītos ciklus nakti un precizējiet modeļus šai konkrētajai iekārtai. Izmaiņas tiek saglabātas tikai tad, ja tās patiešām iegūst labākus rezultātus ilgstošajos ciklos, tāpēc tas var tikai palīdzēt vai palikt nemainīgs – nekad neatkāpties.", + "doc": "Laiku pa laikam pārtrenēt enerģijas modeli ar šīs mašīnas pabeigtajiem cikliem. Jauns modelis tiek paturēts tikai tad, ja testa ciklos tas pārspēj vienkāršo aplēsi.", "label": "Mācīties no šīs mašīnas" }, "ml_training_hour": { - "doc": "Vietējā diennakts stunda (0–23), lai veiktu nakts precizēšanu. Izvēlieties klusu stundu, piemēram, 2 (02:00).", + "doc": "Diennakts stunda (0-23) nakts apmācībai. Izvēlieties klusu stundu, piemēram, 2.", "label": "Mācīties stundā" }, "ml_training_interval_days": { - "doc": "Atkārtoti pārbaudiet uzlabojumus ne vairāk kā reizi šajās dienās.", + "doc": "Pārtrenēt ne biežāk kā reizi tik daudzās dienās.", "label": "Pārbaudīt ne biežāk kā ik" }, "ml_training_min_cycles": { - "doc": "Pirms precizēšanas pagaidiet, līdz ir ierakstīts vismaz tik daudz ciklu, lai būtu pietiekami daudz, no kā mācīties.", + "doc": "Pirms apmācības pagaidīt vismaz tik daudz ierakstītu ciklu.", "label": "Nepieciešami cikli vispirms" }, "name": { - "doc": "Parādāmais nosaukums, kas parādīts HA integrāciju sarakstā un ierīču reģistrā.", + "doc": "Šīs ierīces nosaukums Home Assistant.", "label": "Ierīces nosaukums" }, "no_update_active_timeout": { - "doc": "Ja darbības laikā tik ilgi netiek saņemti strāvas atjauninājumi, pieņemsim, ka kontaktdakša ir nokritusi bezsaistē un piespiedu kārtā apturiet, lai izvairītos no zombiju cikla. Noklusējuma 600 s pieļauj mākoņa vai tīkla aizkavi.", + "doc": "Ja cikla laikā tik ilgi nepienāk neviens jaudas atjauninājums, tiek pieņemts, ka spraudnis ir bezsaistē, un cikls tiek piespiedu kārtā apturēts. Noklusējuma 600 s pieļauj mākoņa vai tīkla aizkavi.", "label": "Laiks bez atjauninājuma" }, "notify_before_end_minutes": { @@ -1350,35 +1335,35 @@ "label": "Brīdinājums pirms beigām" }, "notify_channel": { - "doc": "Android paziņojumu kanāla nosaukums sākuma/tiešraides ziņojumiem (kontrolē katra kanāla skaņu un prioritāti mobilajā lietotnē). Blank izmanto pavadošās lietotnes noklusējuma iestatījumus.", + "doc": "Android kanāls sākuma un tiešraides ziņojumiem; kanāls nosaka skaņu un prioritāti. Tukšs: lietotnes noklusējums.", "label": "Android kanāls (starts/tiešraide)" }, "notify_cycle_timers": { - "doc": "Paziņojumi noteiktās cikla minūtēs (piemēram, lai pievienotu mīkstinātāju). Ziņojums atbalsta {device}, {program}, {minutes}. Iespējojiet automātisko pauzi, lai apturētu šajā brīdī un saņemtu interaktīvu paziņojumu ar pogu Atsākt; atsākt, izmantojot paneli, pauzes/atsākšanas pakalpojumu vai paziņojuma darbību.", + "doc": "Paziņojumi noteiktās cikla minūtēs (piem., lai pievienotu mīkstinātāju). Ziņojums atbalsta {device}, {program}, {minutes}. Ar automātisko pauzi cikls tur arī tiek pauzēts, un paziņojumam ir poga Turpināt.", "label": "Cikla taimeri" }, "notify_finish_channel": { - "doc": "Android paziņojumu kanāla nosaukums beigu ziņojumam. Tukšs atkārtoti izmanto sākuma/tiešraides kanālu.", + "doc": "Android kanāls beigu ziņojumam. Tukšs: izmanto sākuma/tiešraides kanālu.", "label": "Android kanāls (beigas)" }, "notify_finish_message": { - "doc": "Pamatteksts, ko nosūta, kad cikls beidzas. Veidnes mainīgie: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}. Šeit visnoderīgākie ir {time_finished} un {vs_typical}.", + "doc": "Ziņojuma teksts, kad cikls beidzas. Mainīgie: {device}, {program}, {duration} (minūtes), {duration_hm} (1 h 05 min), {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}, {status} (completed vai force_stopped; pārtraukti cikli neko nesūta). Ar jebkuru citu mainīgo tiek izmantots noklusējuma teksts.", "label": "Beigu ziņojums" }, "notify_finish_services": { - "doc": "paziņot.* pakalpojumi tiek izsaukti, kad cikls beidzas. Pievienojiet vienu katram mērķim; atstājiet tukšu, lai nepabeigtu paziņojumu.", + "doc": "notify.* pakalpojumi, ko izsaukt, kad cikls beidzas. Atstājiet tukšu, ja nevajag.", "label": "Beigu pakalpojumi" }, "notify_fire_events": { - "doc": "Aktivizējiet arī ha_washdata_* notikumus cikla sākumā/pabeigumā, lai jūs varētu izveidot savu automatizāciju.", + "doc": "Izraisīt arī ha_washdata_* notikumus cikla sākumā un beigās jūsu automatizācijām.", "label": "Aktivēt HA notikumus paziņojumiem" }, "notify_icon": { - "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.", + "doc": "Maza ikona paziņojumā (piem., mdi:washing-machine). Tiek sūtīta tikai uz mobile_app mērķiem, ne uz notify grupām. iOS ierīcēs tā aizstāj lietotnes ikonu, un tai vajag pavadošo lietotni 2026.8 vai jaunāku. Tukšs: platformas noklusējums.", "label": "Paziņojuma ikona" }, "notify_live_chronometer": { - "doc": "Statiska aprēķina vietā rādiet paziņojumā reāllaika atpakaļskaitīšanas taimeri (platformās, kas to atbalsta).", + "doc": "Rādīt paziņojumā reāllaika atpakaļskaitīšanas taimeri (kur atbalstīts), nevis fiksētu aplēsi.", "label": "Izmantot tiešraides hronometru" }, "notify_live_interval_seconds": { @@ -1386,143 +1371,135 @@ "label": "Tiešraides atjauninājumu intervāls" }, "notify_live_overrun_percent": { - "doc": "Ja cikls pārsniedz aprēķināto procentuālo daļu, nosūtiet pārsniegšanas brīdinājumu.", + "doc": "Tiešraides atjauninājumi apstājas, kad cikls pārsniedz paredzēto ilgumu par šo procentu (20 = līdz 1,2x paredzētā laika).", "label": "Tiešraides pārsnieguma % pirms brīdinājuma" }, "notify_live_services": { - "doc": "notify.* pakalpojumi pieprasīja tiešsaistes progresa atjauninājumus cikla laikā. Atstājiet tukšu, lai atspējotu tiešraides norises paziņojumus.", + "doc": "notify.* pakalpojumi tiešraides progresa atjauninājumiem cikla laikā. Atstājiet tukšu, lai tos izslēgtu.", "label": "Tiešraides progresa pakalpojumi" }, "notify_only_when_home": { - "doc": "Sūtiet paziņojumus tikai tad, ja vismaz viena no saistītajām personām (iepriekš) ir mājās.", + "doc": "Paziņot tikai tad, ja vismaz viena no iepriekš norādītajām personām ir mājās.", "label": "Paziņot tikai mājās atrodoties" }, "notify_people": { - "doc": "persona.* vienības, ko izmanto “Paziņot tikai tad, kad mājās”, lai izlemtu, vai kāds ir mājās.", + "doc": "person.* entītijas, ko pārbauda “Paziņot tikai mājās atrodoties”.", "label": "Personas (priekš Tikai mājās)" }, "notify_pre_complete_message": { - "doc": "Pirmsbeigu / gandrīz pabeigts brīdinājuma pamatteksts. Veidnes mainīgie: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Katra tiešraides progresa atjauninājuma teksts. Veidnes mainīgie: {device}, {program}, {minutes} (atlikušās minūtes). Ar jebkuru citu mainīgo tiek izmantots noklusējuma teksts.", "label": "Gandrīz pabeigšanas ziņojums" }, "notify_reminder_message": { - "doc": "Pamatteksts atgādinājumam par joprojām gaidāmo izkraušanu. Tukšs izmanto iebūvēto noklusējuma vērtību. Veidnes mainīgie: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Vienreizējā Brīdinājuma pirms beigām teksts. Veidnes mainīgie: {device}, {program}, {minutes} (iestatījums Brīdinājums pirms beigām). Ar jebkuru citu mainīgo tiek izmantots noklusējuma teksts.", "label": "Atgādinājuma ziņojums" }, "notify_start_message": { - "doc": "Pamatteksts, ko nosūta, kad cikls sākas. Veidnes mainīgie: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Teksts, ko nosūta, kad cikls sākas. Veidnes mainīgie: {device}, {program}. Ar jebkuru citu mainīgo tiek izmantots noklusējuma teksts.", "label": "Starta ziņojums" }, "notify_start_services": { - "doc": "paziņot.* pakalpojumi tiek izsaukti, kad sākas cikls. Pievienojiet vienu katram mērķim (tālrunim, informācijas panelim utt.); atstājiet tukšu, lai netiktu paziņots par sākumu.", + "doc": "notify.* pakalpojumi, ko izsaukt, kad cikls sākas (tālrunis, informācijas panelis, ...). Atstājiet tukšu, ja nevajag.", "label": "Starta pakalpojumi" }, "notify_timeout_seconds": { - "doc": "Automātiski noraidīt paziņojumu pēc šīm sekundēm (platformās, kas to atbalsta). 0 saglabā to, līdz tiek manuāli noraidīts.", + "doc": "Aizvērt paziņojumu pēc tik daudz sekundēm (kur atbalstīts). 0 to patur, līdz jūs to aizverat.", "label": "Automātiskā noņemšana pēc" }, "notify_title": { - "doc": "Paziņojuma nosaukums. Veidnes mainīgie: {device}, {program}, {duration}, {energy}, {cost}, {date}, {time}, {minutes}.", + "doc": "Paziņojuma nosaukums. Aizvieto tikai {device}; ar jebkuru citu mainīgo tiek izmantots noklusējuma nosaukums.", "label": "Paziņojuma virsraksts" }, "notify_unload_delay_minutes": { - "doc": "Dažas minūtes pēc cikla beigām, pirms tiek nosūtīts joprojām gaidošais atgādinājums \"izlādēt iekārtu\". Iestatiet 0, lai atspējotu atgādinājumu.", + "doc": "Minūtes pēc cikla beigām līdz atgādinājumam \"izkraut mašīnu\". 0 to atspējo.", "label": "Izkraušanas atgādinājuma aizkave" }, "notify_unload_repeat": { - "doc": "Atkārtoti nosūta izkraušanas atgādinājumu ik pēc „Izkraušanas atgādinājuma aizkave“ minūtēm, līdz izkraušana tiek apstiprināta vai paziņojumā pieskaraties „Pārtraukt atgādināšanu“. Nepieciešams veids, kā apstiprināt izkraušanu: Durvju sensora entītija, Izkraušanas apstiprinājuma entītija vai zemāk esošā opcija „Apstiprināt izkraušanu manuāli“. Atmešanas poga darbojas Home Assistant mobilās (companion) lietotnes paziņojumos.", + "doc": "Atkārtoti sūtīt izkraušanas atgādinājumu ik pēc „Izkraušanas atgādinājuma aizkave“ minūtēm, līdz izkraušana ir apstiprināta vai jūs pieskaraties „Pārtraukt atgādināšanu“ (mobilajā lietotnē). Nepieciešama Durvju sensora entītija, Izkraušanas apstiprinājuma entītija vai „Apstiprināt izkraušanu manuāli“.", "label": "Atkārtot, līdz izkrauts" }, "off_delay": { - "doc": "Laiks gaidīt, kad jauda samazinās, pirms tiek paziņots, ka cikls ir pabeigts. Ja šajā logā tiek atsākta barošana, cikls turpinās nemanāmi – tas pārslēdz pauzes starp mazgāšanas posmiem. Trauku mazgājamām mašīnām ir garas žāvēšanas fāzes (izslēdziet strāvu uz 20–60 minūtēm), tāpēc izslēgšanas aizkavei ir jābūt lielākai par to, lai visa mazgāšana un žāvēšana būtu viens cikls.", + "doc": "Cik ilgi jaudai jāpaliek zemai, pirms cikls ir pabeigts. Ja jauda atgriežas šajā laikā, cikls turpinās. Trauku mazgājamās mašīnas žāvējot var būt dīkstāvē 20-60 min, tāpēc iestatiet to garāku.", "label": "Izslēgšanas aizkave" }, "pause_cuts_power": { - "doc": "Kad cikls ir apturēts, izslēdziet arī tālāk esošo Switch Entity. Tikai ierīcēm, kuru kontaktdakšu var droši nogriezt cikla vidū.", + "doc": "Kad cikls ir pauzēts, izslēgt arī zemāk norādīto Slēdža entītiju. Tikai ierīcēm, kas var droši zaudēt strāvu cikla vidū.", "label": "Pauze arī izslēdz jaudu (ar slēdzi)" }, "power_off_delay": { - "doc": "Cik ilgi jaudai pēc cikla pabeigšanas jāpaliek zem Barošanas izslēgšanas sliekšņa, pirms stāvoklis atgriežas uz Izslēgts. Izmanto tikai tad, ja Barošanas izslēgšanas slieksnis pārsniedz 0. Tiek pārbaudīts fona ritmā, tāpēc faktiskā aizkave tiek noapaļota uz augšu līdz nākamajai stāvokļa derīguma beigu atzīmei.", + "doc": "Cik ilgi jaudai pēc cikla jāpaliek zem Barošanas izslēgšanas sliekšņa, pirms stāvoklis atgriežas uz Izslēgts. Izmanto tikai tad, ja šis slieksnis ir virs 0. Tiek pārbaudīts periodiski, tāpēc faktiskā gaidīšana var būt nedaudz ilgāka.", "label": "Barošanas izslēgšanas aizkave" }, "power_off_threshold_w": { - "doc": "Neobligāta, uz jaudu balstīta Izslēgts stāvokļa noteikšana. Ja vērtība pārsniedz 0 un cikls ir pabeigts, un jauda saglabājas zem šī līmeņa Barošanas izslēgšanas aizkaves laikā, mašīna tiek uzskatīta par izslēgtu, un stāvoklis atgriežas uz Izslēgts. Lai atspējotu, atstājiet 0 (noklusējums). Iestatiet to virs patiesā izslēgtā līmeņa, bet zem Apstāšanās sliekšņa un mašīnas pabeigtā-bet-ieslēgtā gaidstāves patēriņa; ja tā nav zem Apstāšanās sliekšņa, tā tiek ignorēta. Kad iespējota, atgriešanās uz Izslēgts aizstāj Progresa atiestatīšanas aizkavi, tāpēc pabeigta mašīna paliek stāvoklī Pabeigts/Tīrs, līdz tā tiek faktiski izslēgta.", + "doc": "Ja pēc cikla jauda Barošanas izslēgšanas aizkaves laikā ir zem šīs vērtības, stāvoklis kļūst Izslēgts (to vairs nedara Progresa atiestatīšanas aizkave). Iestatiet to virs izslēgtas ierīces patēriņa, zem gaidstāves un Apstāšanās sliekšņa. 0 to atspējo (noklusējums).", "label": "Barošanas izslēgšanas slieksnis" }, "power_sensor": { - "doc": "Sensora entītija, kas ziņo par šīs ierīces jaudu vatos (piem., sensor.washer_power). Visa cikla noteikšana ir balstīta uz šo signālu.", + "doc": "Sensors, kas ziņo šīs ierīces pašreizējo jaudu vatos (piem., sensor.washer_power). Uz tā balstās visa noteikšana.", "label": "Jaudas sensors" }, - "profile_duration_tolerance": { - "doc": "+/- josla ap profila vidējo ilgumu, ko izmanto atbilstības noteikšanas laikā. 0,25 nozīmē, ka 60 minūšu profils atbilst 45–75 minūšu cikliem.", - "label": "Profila ilguma tolerance" - }, "profile_match_interval": { - "doc": "Cik bieži ir jāmēģina izveidot profila saskaņošanu skriešanas cikla laikā. Noklusējuma 300 s (5 minūtes) līdzsvaro noteikšanas ātrumu un centrālo procesoru.", + "doc": "Cik bieži saskaņot darbojošos ciklu ar profiliem. Noklusējums 300 s (5 min).", "label": "Atbilstības intervāls" }, "profile_match_max_duration_ratio": { - "doc": "Maksimālais cikla garums attiecībā pret profilu. 1.3 nozīmē, ka ciklam ir jābūt mazākam par 130% no profila ilguma, lai tas atbilstu.", + "doc": "Programma tiek atmesta, tiklīdz cikls ilgst vairāk nekā tās tipiskais ilgums, reizināts ar šo vērtību. 1,8: cikls, kas ir par 80% garāks par programmu, ar to vairs netiek saskaņots.", "label": "Maks. ilguma attiecība" }, "profile_match_min_duration_ratio": { - "doc": "Minimālais cikla garums attiecībā pret profilu. 0,9 nozīmē, ka ciklam ir jābūt vismaz 90% no profila ilguma, lai tas atbilstu.", + "doc": "Programma tiek ņemta vērā tikai tad, kad cikls ir ilgis šo daļu no tās tipiskā ilguma. 0,10: 2 stundu programma kļūst par kandidātu pēc 12 minūtēm. Netiek piemērots cikla pirmajās 15 minūtēs.", "label": "Min. ilguma attiecība" }, "profile_match_threshold": { - "doc": "Minimālais līdzības rādītājs (0-1), kas nepieciešams cikla beigās, lai pieņemtu programmas identifikāciju. Paaugstiniet to, lai samazinātu nepareizu identifikāciju; samaziniet to, ja jūsu mašīnas programmas netiek saskaņotas. Noklusējuma vērtība 0,4 ir konservatīvs sākumpunkts.", + "doc": "Reāllaika atbilstības rezultāts, kas ciklam vajadzīgs, pirms Viedā pabeigšana vai pretgrumbu pabeigšana drīkst to beigt agrāk. Tas nemaina rādīto programmu. Paaugstiniet to, ja vājas atbilstības beidz ciklus agri; samaziniet, ja skaidras atbilstības gaida izslēgšanas aizkavi.", "label": "Atbilstības slieksnis" }, "profile_unmatch_threshold": { - "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.", + "doc": "Ja reāllaika atbilstības rezultāts nokrītas zem šī, pašreizējais programmas minējums tiek notīrīts. Turiet to nedaudz zem Atbilstības sliekšņa, lai īss kritums to neatiestatītu.", "label": "Neatbilstības slieksnis" }, "progress_reset_delay": { - "doc": "Pēc pabeigšanas saglabājiet progresu 100% līmenī tik ilgi, lai informācijas paneļos būtu redzams paziņojums Pabeigts pirms atiestatīšanas uz dīkstāvi.", + "doc": "Pēc cikla turēt progresu 100% tik ilgi, pirms atiestatīšanas uz dīkstāvi.", "label": "Progresa atiestatīšanas aizkave" }, "pump_stuck_duration": { - "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).", + "doc": "Cik ilgi sūknis drīkst darboties bez pārtraukuma, pirms tiek izraisīts iestrēguša sūkņa notikums.", "label": "Iestrēgušā sūkņa ilgums" }, "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.", + "doc": "Minimālais laiks starp apstrādātajiem rādījumiem. Ātrāk pienākušie rādījumi tiek izlaisti (zemi rādījumi cikla laikā vienmēr tiek saglabāti). Iestatiet to vienādu ar spraudņa ziņošanas intervālu vai mazāku; lielāka vērtība tikai izmet rādījumus.", "label": "Paraugu ņemšanas intervāls" }, "save_debug_traces": { - "doc": "Saglabājiet pilnas jaudas izsekošanu un atbilstošos atkļūdošanas datus katram ciklam. Noderīgs problēmu novēršanai, bet palielina krātuves apjomu.", + "doc": "Kopā ar katru ciklu saglabāt arī saskaņotāja atkļūdošanas datus (katra kandidāta rezultātus) problēmu novēršanai. Pievieno apmēram 20 KB katram ciklam.", "label": "Saglabāt atkļūdošanas pēdas" }, "smart_termination_duration_ratio": { - "doc": "Cik tālu atbilstošās programmas paredzētajā ilgumā ciklam jābūt nonākušam, pirms vieda pabeigšana drīkst to pabeigt agrāk, kad jauda samazinās. Paredzētais ilgums ir programmas vidējais rādītājs, tāpēc ierīcēs, kuru darbības laiks stipri mainās - veļas mašīnas ar aukstu ziemas un siltu vasaras ieplūstošo ūdeni, žāvētāji ar sensora žāvēšanu, no slodzes atkarīgas programmas - apmēram puse no visiem cikliem beidzas īsāk par šo vidējo un nekad nesaņem ātro pabeigšanu, beidzoties tikai caur rezerves noildzi, dažas minūtes vēlāk. Samaziniet to (piem., 0,85) šajās mašīnās, lai agrā pabeigšana tomēr nostrādātu; palieliniet to virzienā uz 1,0, lai būtu piesardzīgāks. Atstājiet tukšu noklusējuma vērtībai (0,98 vai 0,99 trauku mazgājamām mašīnām). Tā var pabeigt ciklu tikai agrāk, nekad ne vēlāk, un nekad nenostrādā pie neskaidras vai zemas pārliecības atbilstības.", + "doc": "Programmas vidējā ilguma daļa, kas ciklam jāsasniedz, pirms Viedā pabeigšana drīkst to beigt agrāk. Ja darbības laiks mainās (ūdens temperatūra, sensora žāvēšana, veļas daudzums), samaziniet to (piem., 0,85). Atstājiet tukšu, lai izmantotu 0,98 (trauku mazgājamām mašīnām 0,99).", "label": "Viedas pabeigšanas attiecība" }, - "smoothing_window": { - "doc": "Cik daudz neapstrādāts jaudas signāls ir izlīdzināts. Zems (2) ir atsaucīgs, bet trokšņains; augsts (5) izlīdzina tapas, bet palielina nobīdi.", - "label": "Izlīdzināšanas logs" - }, "start_duration_threshold": { - "doc": "Jaudai ir jāpaliek virs sākuma sliekšņa tik ilgi, lai apstiprinātu īstu palaišanu, neļaujot sekundes ieslēgšanas/izslēgšanas slēdžiem sākt ciklu.", + "doc": "Jaudai tik ilgi jāpaliek virs Starta sliekšņa, pirms cikls sākas, lai īsi ieslēgšanās/izslēgšanās mirkļi tiktu ignorēti.", "label": "Starta ilgums" }, "start_energy_threshold": { - "doc": "Enerģija (jauda x laiks) ierīcei ir jāpatērē pirms DARBAS. Īsam lieljaudas smailei ir ļoti zema enerģija, un tā tiek ignorēta, novēršot viltus iedarbināšanu.", + "doc": "Enerģija, kas ierīcei jāpatērē, pirms cikls tiek uzskatīts par darbojošos. Īss jaudas pīķis patērē maz enerģijas, tāpēc ciklu nesāk.", "label": "Starta enerģija" }, "start_threshold_w": { - "doc": "Jaudai jāpalielinās virs šī līmeņa, lai apstiprinātu, ka cikls ir sācies. Pārāk zema tā iestatīšana izraisa viltus palaišanu no gaidstāves strāvas; pārāk augstas un lēnas palaišanas programmas (aukstā uzpilde) tiek izlaistas. Ieteikuma dzinējs to nosaka tieši virs mašīnas novērotās zemākās aktīvās jaudas.", + "doc": "Jaudai jāpārsniedz šī vērtība, lai sāktos cikls. Pārāk zema: gaidstāves jauda izraisa viltus startus. Pārāk augsta: lēni startējošas programmas (auksta uzpilde) tiek palaistas garām.", "label": "Starta slieksnis" }, "stop_threshold_w": { - "doc": "Pirms izslēgšanas aizkaves atpakaļskaitīšanas sākuma jaudai ir jāsamazinās zem šī līmeņa. Iestatiet to zem sākuma sliekšņa – atstarpe starp tām ir histerēzes josla, kas novērš mirgošanu. Ja iestatīts pārāk augsts, mazjaudas fāzes (skalošanas aizturēšana, pretburzīšanās) kļūdaini aktivizē beigu secību.", + "doc": "Jaudai jānokrītas zem šīs vērtības, pirms sāk skaitīt izslēgšanas aizkavi. Turiet to zem Starta sliekšņa. Ja tā ir pārāk augsta, zemas jaudas fāzes (skalošanas pauzes, pretgrumbu režīms) kļūdaini sāk beigu atskaiti.", "label": "Apstāšanās slieksnis" }, "switch_entity": { - "doc": "Papildu slēdzis tiek izslēgts pauzes laikā un atkal ieslēgts, kad tiek atsākts, un tiek izmantots kopā ar funkciju “Pauze arī pārtrauc strāvu”.", + "doc": "Neobligāts slēdzis, ko izslēdz pauzes laikā un atkal ieslēdz, turpinot, ja ieslēgts “Pauze arī izslēdz jaudu”.", "label": "Slēdža entītija" }, "watchdog_interval": { - "doc": "Cik bieži fona sargsuns pārbauda, vai sensori nav apstājušies un vai nav pagājis noildzes laiks. Noklusējums 30 s.", + "doc": "Cik bieži fona pārbaude meklē apstājušos sensorus un noildzes. Noklusējums 30 s.", "label": "Sargsuna intervāls" }, "notify_milestone_message": { @@ -1530,127 +1507,82 @@ "label": "Atskaites punkta ziņojums" }, "notify_milestones": { - "doc": "Ar komatu atdalīti ciklu skaiti, kuri, tos sasniedzot, izraisa vienreizēju svinību paziņojumu (piem., 50, 100, 500, 1000). Tukšs atspējo atskaites punktu paziņojumus.", + "doc": "Ciklu skaiti, kas izraisa vienreizēju svinību paziņojumu, atdalīti ar komatu (piem., 50, 100, 500, 1000). Tukšs tos atspējo.", "label": "Ciklu atskaites punkti" }, "notify_quiet_end_hour": { - "doc": "Netraucēšanas loga beigas (0-23). Aizturētie paziņojumi tiek piegādāti šajā stundā. Atstājiet tukšu, lai atspējotu.", + "doc": "Netraucēšanas loga beigas (0-23); tad tiek nosūtīti aizturētie paziņojumi. Tukšs to atspējo.", "label": "Kluso stundu beigas" }, "notify_quiet_start_hour": { - "doc": "Netraucēšanas loga sākums (0-23). Beigu, atgādinājuma un tīrās veļas paziņojumi, kas nostrādātu kluso stundu laikā, tiek aizturēti un piegādāti, kad logs beidzas. Atstājiet tukšu, lai atspējotu. Atbalsta logus, kas šķērso pusnakti (piem., sākums 22, beigas 7).", + "doc": "Netraucēšanas loga sākums (0-23). Beigu, atgādinājuma un tīrās veļas paziņojumi šajā logā tiek aizturēti līdz tā beigām. Var šķērsot pusnakti (piem., 22 līdz 7). Tukšs to atspējo.", "label": "Kluso stundu sākums" }, "peak_rate_message": { - "doc": "Neobligāts pielāgots teksts maksimālā tarifa padomam, ko pievieno sākuma paziņojumam. Veidnes mainīgie: {device}, {price}. Tukšs izmanto iebūvēto noklusējuma vērtību.", + "doc": "Pielāgots teksts maksimālā tarifa padomam. Veidnes mainīgie: {device}, {price}. Tukšs: noklusējums.", "label": "Maksimālā tarifa ziņojums" }, "peak_rate_threshold": { - "doc": "Kad cikls sākas un pašreizējā cena par kWh ir vienāda vai lielāka par šo vērtību, sākuma paziņojumam pievieno maksimālā tarifa padomu. 0 vai tukšs atspējo padomu.", + "doc": "Ja cikla sākumā cena par kWh ir vienāda vai lielāka par šo, sākuma paziņojumam pievieno maksimālā tarifa padomu. 0 vai tukšs to atspējo.", "label": "Maksimālā tarifa slieksnis (par kWh)" }, - "dtw_bandwidth": { - "label": "Formas saskaņošanas pielaide", - "doc": "Cik lielā mērā saskaņotājs var laikā deformēt ciklu, lai tas atbilstu saglabātās programmas formai. Augstāka vērtība pieļauj lielākas ātruma svārstības; pārāk augsta var saskaņot nepareizu programmu." - }, - "corr_weight": { - "label": "Formas un līmeņa svars", - "doc": "Līdzsvars starp līknes formu un absolūto jaudas līmeni, vērtējot saskaņošanu. Augstāka vērtība dod priekšroku formai; zemāka dod priekšroku jaudas līmeņa saskaņošanai." - }, - "duration_weight": { - "label": "Ilguma svars", - "doc": "Cik stipri tiek dota priekšroka programmai, kuras tipiskais ilgums atbilst šim ciklam." - }, - "energy_weight": { - "label": "Enerģijas svars", - "doc": "Cik stipri tiek dota priekšroka programmai, kuras tipiskais enerģijas patēriņš atbilst šim ciklam." - }, "store_brand": { "label": "Ierīces zīmols", - "doc": "Neobligāti. Ierīces zīmols, kas izvēlēts no kopienas kataloga. Izmanto atbilstošu atsauces ierakstu atrašanai un kopīgošanai. Atstājiet tukšu, ja neizmantojat tiešsaistes funkcijas." + "doc": "Neobligāti. Jūsu ierīces zīmols no kopienas kataloga atsauces ierakstu meklēšanai un kopīgošanai. Atstājiet tukšu, ja neizmantojat tiešsaistes funkcijas." }, "store_model": { "label": "Ierīces modelis", - "doc": "Neobligāti. Ierīces modelis, kas izvēlēts no kopienas kataloga, kad ir iestatīts zīmols. Ja jūsu modeļa sarakstā nav, varat to pievienot katalogam." + "doc": "Neobligāti. Jūsu ierīces modelis no kopienas kataloga (vispirms izvēlieties zīmolu). Ja tā sarakstā nav, varat to pievienot." }, "show_contributor": { "doc": "Rādīt \"autors: \" attiecinājumu kopienas ierīcēs un atskaites ciklos." }, - "enable_phase_matching": { - "label": "Uz fāzēm balstīts atlikušais laiks", - "doc": "Sadaliet katru notiekošo ciklu fāzēs (sildīšana, mazgāšana, izgriešana) un plānojiet atlikušo laiku katrai fāzei, sajaucot ar klasisko aprēķinu - cikla sākumā paļaujoties uz fāžu plānojumu, bet beigās uz klasisko aprēķinu. Tas pielāgo atpakaļskaitīšanu tam, cik ilgi jūsu mašīna patiešām sildās un darbojas, kas visvairāk pamanāms cikla pirmajā pusē. Izslēgts = tikai klasiskais aprēķins. Tiek ietekmēts tikai atlikušā laika attēlojums; programmu saskaņošana un ciklu noteikšana paliek nemainīga." - }, - "keep_min_score": { - "label": "Min. atbilstības rezultāts", - "doc": "Zemākais līdzības rezultāts, kas kandidātam vajadzīgs, lai paliktu saskaņošanas procesā. Noklusējums 0,1 ir apzināti ļaujošs – tas pieļauj pat vājus kandidātus un paļaujas uz vēlākajiem posmiem labākā atbilstības atrašanai. Palieliniet, lai agrāk filtrētu maz ticamos profilus; samaziniet, lai paplašinātu sākotnējo kandidātu kopumu." - }, - "dtw_blend": { - "label": "DTW sajaukums", - "doc": "Cik daudz DTW izlīdzināšanas rezultāts aizstāj 2. posma pamatrezultātu. 0 = izmantojiet tikai 2. posma rezultātu, 1 = izmantojiet tikai DTW rezultātu, 0,5 (noklusējums) = vienāds sajaukums. Palieliniet, lai vairāk paļautos uz laika izlīdzināšanu, ja programmām ir līdzīgi jaudas līmeņi, bet atšķirīgas laika shēmas." - }, - "dtw_ensemble_w": { - "label": "DTW ensemble miks", - "doc": "Ensemble-DTW režīmā (noklusējums) tas ir svars skalētajam L1 salīdzinājumā ar atvasināto DTW (DDTW). 1,0 = tikai skalētais L1, 0 = tikai DDTW, 0,7 = noklusējums. Skalētā varianta salīdzina jaudas līmeņus; atvasinātā varianta reaģē uz to, kā jauda mainās laika gaitā. Ensemble apvieno abus." - }, - "dtw_ddtw_scale": { - "label": "Atv. DTW skāle", - "doc": "Atvasinātās DTW vērtēšanas pusiesātinājuma attālums. Rezultāts uz pusēm samazinās, kad DDTW attālums ir vienāds ar šo vērtību. Mazāks = jutīgāks pret formas atšķirībām. Noklusējums 30 ir kalibrēts tipiskajiem sadzīves tehnikas jaudas profiliem. Ietekmē tikai ensemble un ddtw DTW režīmus." - }, - "dtw_refine_top_n": { - "label": "DTW precizēšanas skaits", - "doc": "Cik daudz 2. posma labāko kandidātu DTW pārvērtē. DTW ir dārgāks, tāpēc tas tiek piemērots tikai N labākajiem. Noklusējums 5. Palieliniet (līdz 7-9), ja pareizais profils dažreiz pēc 2. posma sasniedz tikai 4. vai 5. vietu – DTW var to glābt. Zemāka vērtība nedaudz paātrina saskaņošanu." - }, - "duration_scale": { - "label": "Ilguma asums", - "doc": "4. posma ilguma atbilstības soda asums. Tas ir logaritmiskais koeficients, pie kura atbilstības rezultāts uz pusēm samazinās. Mazāks = bargāks: ilguma neatbilstība kaitē vairāk. Noklusējums 0,175 atbilst aptuveni 18% ilguma tolerancei pie pus-svara. Izmantojiet kopā ar Ilguma svaru." - }, - "energy_scale": { - "label": "Enerģijas asums", - "doc": "4. posma enerģijas atbilstības soda asums. Mazāks = bargāks: enerģijas neatbilstība kaitē vairāk. Noklusējums 0,25 ir apzināti piedodošāks nekā ilguma asums, jo enerģija mainās atkarībā no slodzes. Izmantojiet kopā ar Enerģijas svaru." - }, "power_profile_interval_min": { "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." + "doc": "power_profile atribūta (vidējie vati katrā laika nišā) spaiņa lielums plānotājiem, piemēram, EMHASS un tibber_prices. Mazāks saglabā īsus pīķus; lielāks izlīdzina līkni. Noklusējums 15 min. 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." + "doc": "Tikai iOS. Atjaunināt tiešraides progresu klusi, ar akumulatoru taupošu prioritāti, ko iOS var apvienot. Pirmais atjauninājums un sākuma/beigu paziņojumi joprojām brīdina. Izslēgts: brīdina katrs atjauninājums." }, "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." + "doc": "Tikai Android. Saglabāt tiešraides progresa paziņojumu pēc pieskāriena. Izslēgts: pieskāriens to aizver." }, "notify_live_click_action": { "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." + "doc": "Ko atver pieskāriens šīs ierīces paziņojumam. Tukšs: WashData panelis ar šo ierīci. Ievadiet informācijas paneļa ceļu (piem., /lovelace/laundry) vai URL, vai “none”, ja pieskārienam nav jādara nekas." }, "door_opens_at_end": { "label": "Durvis automātiski atveras beigās", - "doc": "Trauku mazgājamajām mašīnām, kuras cikla beigās atver durvis žāvēšanai (AirDry un līdzīgas). Ieslēgts, durvju atvēršana darbojošā ciklā vairs nepārtrauc to uz visiem laikiem; tā vietā, ja durvis paliek atvērtas norādīto uzturēšanās laiku zemāk, WashData uzskata ciklu par pabeigtu. Īsa atvēršana (pievienojot trauku) tiek ignorēta. Nepieciešams durvju sensora objekts." + "doc": "Trauku mazgājamām mašīnām, kas beigās atver durvis žāvēšanai (piem., AirDry). Ja durvis paliek atvērtas zemāk norādīto laiku, cikls beidzas, nevis tiek pauzēts; īsa atvēršana tiek ignorēta. Nepieciešama Durvju sensora entītija." }, "door_end_dwell_seconds": { "label": "Durvju atvēršanas beigu uzturēšanās laiks", - "doc": "Cik ilgi durvis jāpaliek atvērtām, lai WashData beigtu ciklu, kad ir ieslēgts iestatījums \"Durvis automātiski atveras beigās\". Pietiekami ilgi, lai ignorētu ātru trauka pievienošanu (noklusējums 60 s), pietiekami īsi, lai ātri beigtu, kad mašīna atver durvis." + "doc": "Ja ieslēgts \"Durvis automātiski atveras beigās\": cik ilgi durvīm jāpaliek atvērtām, pirms cikls beidzas. Pietiekami ilgi, lai ignorētu trauka pievienošanu (noklusējums 60 s)." }, "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." + "doc": "Kuri ciklu veidi veido katras programmas līkni un saskaņošanu. Atzīmes noņemšana neko neizdzēš; noderīgi, ja neuzticaties importētajiem datiem. Statistikā vienmēr tiek skaitīti tikai šīs mašīnas cikli. Visu atzīmju noņemšana tiek ignorēta." }, "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." + "doc": "Rēķināt katru cikla brīdi pēc tajā brīdī spēkā esošās cenas, nevis pēc cenas beigās. Nepieciešama cenas entītija. Apstrādāt vēsturi (Diagnostika) pārrēķina vecāku ciklu izmaksas." }, "unload_confirm_entity": { "label": "Izkraušanas apstiprinājuma entītija", - "doc": "Neobligāta entītija, kuras aktivizēšana nozīmē, ka saturs ir izņemts; paredzēta iekārtām, kurām nevar uzstādīt durvju sensoru. Der jebkura: Zigbee vai NFC poga, input_button palīgrīks, kustību sensors iekārtas priekšā, aina. Par apstiprinājumu tiek uzskatītas visas stāvokļa izmaiņas, izņemot izslēgšanu un kļūšanu nepieejamai. Šīs entītijas iestatīšana ieslēdz izkraušanas atgādinājumu arī ierīcei bez Durvju sensora entītijas." + "doc": "Neobligāti. Jebkura entītija, kas apstiprina, ka saturs ir izņemts, iekārtām bez durvju sensora (poga, input_button, kustību sensors, aina). Skaitās katra stāvokļa maiņa, izņemot izslēgšanu vai nepieejamību. Ieslēdz arī izkraušanas atgādinājumu." }, "unload_track_without_door": { "label": "Apstiprināt izkraušanu manuāli", - "doc": "Izmantot izkraušanas atgādinājumu ierīcei, kurai nav ne Durvju sensora entītijas, ne Izkraušanas apstiprinājuma entītijas, un apstiprināt izkraušanu ar savu automatizāciju: nospiediet pogas entītiju „Atzīmēt kā izkrautu“ vai izsauciet pakalpojumu ha_washdata.mark_unloaded. Tiek ignorēts, ja ir iestatīta Durvju sensora entītija, kas šo signālu jau nodrošina." + "doc": "Izmantot izkraušanas atgādinājumu bez durvju sensora vai apstiprinājuma entītijas. Apstipriniet izkraušanu ar savu automatizāciju, izmantojot pogu „Atzīmēt kā izkrautu“ vai pakalpojumu ha_washdata.mark_unloaded. Tiek ignorēts, ja ir iestatīta Durvju sensora entītija." }, "notify_icon_color": { "label": "Paziņojuma krāsa", - "doc": "Akcenta krāsa šīs ierīces paziņojumiem, lai veļas mašīnu, žāvētāju un trauku mazgājamo mašīnu varētu atšķirt ar vienu acu uzmetienu. iOS iekrāso ikonu un Live Activity progresa joslu bloķēšanas ekrānā un Dynamic Island. Android tiek nosūtīta tā pati krāsa, taču tas, ko ar to izdara, ir atkarīgs no tālruņa: daži iekrāso ikonu, bet daudzi (jo īpaši Samsung One UI) to pilnībā ignorē. Abas platformas saprot hex kodu (piem., #03A9F4). Atstājiet tukšu, lai izmantotu platformas noklusējuma krāsu." + "doc": "Akcenta krāsa šīs ierīces paziņojumiem, lai veļas mašīnu, žāvētāju un trauku mazgājamo mašīnu būtu viegli atšķirt. iOS ierīcēs tā aizpilda apli aiz ikonas un iekrāso Live Activity joslu. Daudzi Android tālruņi (īpaši Samsung) to ignorē. Izmantojiet hex, piem., #03A9F4. Tukšs: platformas noklusējums." + }, + "share_name": { + "doc": "Rādīt jūsu konta vārdu kā kopīgoto ciklu augšupielādētāju. Izslēgts: kopīgo bez vārda." } }, "setting_group": { @@ -1678,9 +1610,6 @@ "door_pause": { "label": "Durvis un pauze" }, - "dtw_refinement": { - "label": "DTW precizēšana" - }, "duration_gates": { "label": "Ilguma vārti" }, @@ -1757,13 +1686,18 @@ "rebuilding": "Pārbūvē…", "analyzing": "Analizē…", "resetting": "Atiestata…", - "reverting": "Atgriež…", "trimming": "Apgriež…", "splitting": "Sadala…", + "stalled": "Iestrēdzis", "deleting": "Dzēš…", "imported": "Importēts", "preparing": "Sagatavo…", - "unlabelling": "Noņem marķējumu…" + "unlabelling": "Noņem marķējumu…", + "working": "Notiek apstrāde…", + "match_uncertain": "Nenoteikti: {top} vai {runner}", + "match_uncertain_single": "Nenoteikti: varbūt {top}", + "match_sure": "~{pct}% pārliecība", + "match_uncertain_tip": "WashData vēl nav izlēmis par programmu. Skaitlis rāda, cik bieži vadošais minējums, {top}, iepriekšējos ciklos bija pareizs, kad tas vadīja ar tik lielu pārsvaru. Tikai informācijai." }, "tab": { "advanced": "Papildu", @@ -1795,13 +1729,17 @@ "toast": { "catalog_refreshed": "Kopienas katalogs atsvaidzināts", "contrib_device_changed": "Pievienots kopienas veikalā, bet, kamēr veidlapa bija atvērta, mainījās atlasītā ierīce. Atlasiet to sadaļā Iestatījumi.", + "task_name_required": "Piešķiriet uzdevumam nosaukumu", + "task_added": "Uzdevums pievienots", + "task_save_failed": "Neizdevās saglabāt uzdevumu: {error}", + "task_removed": "Uzdevums noņemts", + "maint_logged": "Atzīmēts kā izpildīts: {task}", "access_saved": "Piekļuves kontrole saglabāta", "all_wiped": "Visi dati ir izdzēsti", "analysis_complete_none": "Analīze pabeigta: nav jaunu ieteikumu", "auto_label_complete": "Automātiskā etiķete ir pabeigta", "automation_deleted": "Automatizācija izdzēsta", "correction_submitted": "Labojums iesniegts", - "cycle_deleted": "Cikls izdzēsts", "cycle_force_stopped": "Cikls piespiedu kārtā apturēts", "cycle_labelled": "Cikls marķēts", "cycle_paused": "Cikls apturēts", @@ -1817,11 +1755,9 @@ "group_deleted": "Grupa izdzēsta", "group_name_required": "Grupas nosaukums ir nepieciešams", "group_saved": "Grupa saglabāta", - "import_successful": "Importēšana ir veiksmīga; integrācijas pārlādēšana", "json_required": "Ir nepieciešami JSON dati", "legacy_removed": "Mantotās darbības ir noņemtas", "logs_exported": "Žurnāli eksportēti", - "matching_reverted": "Atbilstošie svari tika atjaunoti uz noklusējuma vērtībām", "min_2_profiles": "Atlasiet vismaz 2 profilus grupai", "ml_training_no_improvement": "Apmācība pabeigta: bāzes līmenis saglabāts (nav uzlabojumu)", "models_reverted": "Ierīcē esošie modeļi tika atjaunoti uz sākotnējo stāvokli", @@ -1831,8 +1767,8 @@ "pg_preset_loaded": "Priekšiestatījums \"{name}\" ielādēts", "pg_preset_saved": "Priekšiestatījums \"{name}\" saglabāts", "pg_sugg_loaded": "Ielādētas {n} ieteiktās vērtības - palaidiet izmēģinājumu laukumu salīdzināšanai", - "pg_sugg_ml_loaded": "Ielādētas {n} ML kalibrētas vērtības - palaidiet izmēģinājumu laukumu salīdzināšanai", "phase_name_required": "Nepieciešams fāzes nosaukums", + "phase_reset": "Atjaunota iebūvētā fāze \"{name}\"", "phase_updated": "Fāze atjaunināta", "phases_saved": "Fāzes saglabātas", "preferences_saved": "Preferences saglabātas", @@ -1879,14 +1815,10 @@ "store_name_required": "Ievadiet profila nosaukumu", "store_import_failed": "Importēšana neizdevās: {error}", "store_imported": "Importēts profilā {profile}", - "store_program_required": "Ievadiet programmas nosaukumu", "store_no_appliance": "Vispirms iestatījumos norādiet ierīces zīmolu un modeli.", "store_share_failed": "Kopīgošana neizdevās: {error}", "store_shared": "Kopīgots kopienas veikalā - gaida pārskatīšanu.", "appliance_added": "Ierīce pievienota - gaida apstiprinājumu", - "thanks_confirming": "Paldies par apstiprinājumu", - "device_approved": "Kopiena apstiprināja", - "rating_saved": "Kvalitātes vērtējums saglabāts", "brand_added": "Zīmols pievienots - gaida apstiprinājumu", "profile_added": "Profils pievienots - gaida apstiprinājumu", "saved_except_conflicts": "Saglabāts. Labojiet izceltās pretrunas, lai saglabātu pārējo.", @@ -1909,11 +1841,52 @@ "odometer_saved": "Kopējais ciklu skaits saglabāts", "odometer_save_failed": "Neizdevās saglabāt kopējo skaitu: {error}", "store_pick_profile_share": "Izvēlieties profilu, kurā kopīgot", - "remove_failed": "Noņemšana neizdevās: {error}" + "remove_failed": "Noņemšana neizdevās: {error}", + "analysis_complete": "Analīze pabeigta: ieteikumi: {count}", + "analysis_failed": "Analīze neizdevās: {error}", + "auto_detect_failed": "Automātiskā noteikšana neizdevās: {error}", + "auto_label_failed": "Automātiskā iezīmēšana neizdevās: {error}", + "could_not_load_cycle": "Neizdevās ielādēt ciklu: {error}", + "create_failed": "Izveide neizdevās: {error}", + "cycles_deleted": "Dzēsti cikli: {count}", + "debug_cleared": "Notīrītas atkļūdošanas pēdas: {count}", + "discard_failed": "Izmešana neizdevās: {error}", + "error": "Kļūda: {error}", + "export_failed": "Eksportēšana neizdevās: {error}", + "import_failed": "Importēšana neizdevās: {error}", + "invalid_json": "\"{key}\" nav derīgs JSON - izlabojiet to vai notīriet lauku pirms saglabāšanas.", + "label_failed": "Iezīmēšana neizdevās: {error}", + "ml_training_promoted": "Apmācība pabeigta: aktivizēti modeļi: {count}", + "pause_failed": "Pauzēšana neizdevās: {error}", + "phase_created": "Fāze \"{name}\" izveidota", + "phase_deleted": "Fāze \"{name}\" dzēsta", + "profile_created": "Profils \"{name}\" izveidots", + "rename_failed": "Pārdēvēšana neizdevās: {error}", + "save_failed": "Saglabāšana neizdevās: {error}", + "split_complete": "Sadalīts ciklos: {count}", + "start_failed": "Palaišana neizdevās: {error}", + "stop_failed": "Apturēšana neizdevās: {error}", + "update_failed": "Atjaunināšana neizdevās: {error}", + "store_download_skipped": "Izlaisti ieraksti: {n} (pārāk īsi, ar pārāk daudziem pārtraukumiem vai jau ir jūsu ierīcē).", + "store_import_low_quality": "Šis ieraksts ir pārāk īss, tajā ir pārāk daudz pārtraukumu vai tas ir neticams, tāpēc to nevar importēt.", + "store_import_duplicate": "Šis ieraksts jau ir jūsu ierīcē.", + "device_approved": "Kopiena apstiprināja", + "thanks_confirming": "Paldies par apstiprinājumu", + "rating_saved": "Kvalitātes vērtējums saglabāts", + "store_download_partial": "Neizdevās lejupielādēt programmas: {n}. Mēģiniet vēlāk vēlreiz.", + "import_no_restore_point": "Importēts, taču atjaunošanas punktu neizdevās saglabāt, tāpēc šo importēšanu nevar atsaukt", + "import_undo_failed": "Neizdevās atsaukt importēšanu: {error}", + "import_undone": "Importēšana atsaukta: atjaunoti {profiles} profili un {cycles} cikli", + "auto_label_cancelled": "Automātiskā marķēšana atcelta pēc {count} ciklu marķēšanas.", + "auto_label_cancelled_zero": "Automātiskā marķēšana atcelta pēc {count} ciklu marķēšanas.", + "auto_label_cancelled_one": "Automātiskā marķēšana atcelta pēc {count} cikla marķēšanas.", + "auto_label_cancelled_other": "Automātiskā marķēšana atcelta pēc {count} ciklu marķēšanas.", + "auto_label_done": "Automātiskā marķēšana pabeigta: marķēti {count} cikli, bez marķējuma palika {skipped}.", + "auto_label_done_zero": "Automātiskā marķēšana pabeigta: marķēti {count} cikli, bez marķējuma palika {skipped}.", + "auto_label_done_one": "Automātiskā marķēšana pabeigta: marķēts {count} cikls, bez marķējuma palika {skipped}.", + "auto_label_done_other": "Automātiskā marķēšana pabeigta: marķēti {count} cikli, bez marķējuma palika {skipped}." }, "suggestion": { - "both_agree": "WashData iesaka", - "calibrated_label": "Kalibrēts", "observed_label": "Novērotais", "impact": { "auto_label_confidence": { @@ -1928,10 +1901,6 @@ "higher": "Aizver tikai ciklus, kas patērēja ievērojamu enerģiju", "lower": "Aizver arī īsākus vai zemas enerģijas ciklus" }, - "end_repeat_count": { - "higher": "Prasa beigu signālu atkārtot vairāk reižu – uzticamāks, nedaudz lēnāks", - "lower": "Beidz pēc mazākiem atkārtojumiem – ātrāka atklāšana, lielāks viltus beigu risks" - }, "learning_confidence": { "higher": "Mācās tikai no ļoti pārliecinošām atbilstībām – lēnāks, bet uzticamāks", "lower": "Mācās no vairākiem cikliem; modelis var būt trokšņaināks" @@ -1970,40 +1939,25 @@ } }, "reason": { - "auto_label_confidence": "15. procentiles ticamība {cycles} automātiskajiem marķējumiem, kurus lietotājs nekad nelaboja ({p15}).", "completion_min_seconds": "Puse no p05 tīro ciklu ilguma ({minutes} min) {cycles} ciklos; filtrē viltus ciklus.{excl}", - "dead_zone_batch": "Saglabāts īss (mediānais palaišanas nestabilitātes logs {cycles} ciklos = {s}s), lai patiesās beigas tiktu noteiktas ātri.", - "dead_zone_from_dip": "Pamatots ar agrīnu jaudas kritumu, kas konstatēts pie {s}s.", - "end_energy_default": "Noklusējuma ieteicamā bāzlīnija cikla beigu trokšņa slieksnim.", - "end_energy_false": "p95 viltus beigu enerģija ({p95}Wh) {cycles} ciklos, ar apakšējo robežu 0.2% no mediānās cikla enerģijas ({floor}Wh).", - "end_energy_no_false": "Nav viltus beigu {cycles} ciklos; ar apakšējo robežu 0.2% no mediānās cikla enerģijas ({floor}Wh).", - "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ā.", + "match_interval_floored": "Īsākā programma ({shortest}s) ierobežotu šo vērtību līdz {cap}s, kas ir zem {minimum}s minimuma, tāpēc tiek izmantots minimums: {persistence} secīgas atbilstības aizņem {budget}s, vairāk nekā {pct}% no šīs programmas. Saskaņošana notiek tikai tad, kad pienāk rādījums (mediāna={median}s), tāpēc tas 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.", "min_off_gap": "Pamatots ar {gaps} novērotiem starpciklu intervāliem (p05={p05}s). Ierīces apakšējā robeža: {floor}s.", "min_off_gap_bridge": "Aprēķināts, lai pārsegtu garāko kluso posmu cikla iekšienē: {span}s + 60s buferis, no {spans} pārsegtiem intervāliem {cycles} tīros ciklos (paliek zem jūsu īsākā intervāla starp divām secīgām palaišanas reizēm, kas ir {ceiling}s).", "min_power": "40% no p05 zemākās aktīvās jaudas ({p05}W) {cycles} tīros ciklos, saglabājot izslēgšanas slieksni zem reālā patēriņa.{excl}", - "ml_auto_label": "Zemākā ticamība, ko kvalitātes modelis vēl novērtēja kā tīru (p10 no {cycles} tīriem cikliem = {p10}).", - "ml_end_repeat": "{false}/{total} ciklu ({pct}%) bija pauze, ko beigu detektors novērtēja >50%; pieprasīt {val} beigu apstiprinājumu(s).", - "ml_off_delay": "Beigu detektora apstiprinātās pauzes: p95 {p95}s + 60s buferis, no {pauses} modeļa pārbaudītām pauzēm {cycles} ciklos (apakšējā robeža {floor}s).", "no_update_timeout": "Pamatots ar novēroto atjaunināšanas biežumu (p95={p95}s) * 20 (min. 60s).", "off_delay_cadence": "Pamatots ar novēroto atjaunināšanas biežumu (p95={p95}s) * 5", "off_delay_device_floor": "Izmantots ierīcei {device} raksturīgais drošais minimums ({floor}s).", "off_delay_generic_floor": "Izmantots vispārīgs drošais minimums ({floor}s).", "off_delay_pauses": "Aprēķināts, lai pārsniegtu reālās pauzes: p95 cikla iekšējā pauze {p95}s + 60s buferis, no {pauses} pauzēm {cycles} tīros ciklos (apakšējā robeža {floor}s).", - "profile_match_threshold": "p10 ticamība {cycles} pareiziem automātiskajiem marķējumiem ({p10}); drošais reāllaika apstiprināšanas slieksnis.", - "sampling_interval": "Mediānais atjaunināšanas intervāls, kas novērots {cycles} tīros ciklos ({si}s).{excl}", - "smoothing_window": "Aprēķināts, lai izlīdzinātu ~30s rādījumu pie {si}s nolases intervāla ({samples} paraugi).", - "start_duration": "Saglabāts īss (~viens {si}s nolases intervāls), lai noteikšana sāktos pēc iespējas agrāk, vienlaikus ignorējot atsevišķu paraugu izlēcienus.", - "thr_batch": "Saglabāts nedaudz virs p05 zemākās aktīvās jaudas {cycles} ciklos ({p05}W), lai sākums tiktu uztverts pēc iespējas agrāk un apturēšanas slieksnis paliktu zem mašīnas zemākās darbības jaudas.", - "tol_per_profile": "p75 no katra profila ilguma dispersijas {profiles} profilos ({cycles} cikli); stabili profili netiek sodīti.", - "tol_pooled": "Pamatots ar apvienoto ilguma dispersiju {cycles} nesen marķētiem cikliem (p95 novirze={dev}).", - "watchdog": "Saglabāts pēc iespējas zemāks, cik droši (tikai nedaudz virs p95 atjaunināšanas intervāla {p95}s un vismaz 2x paraugu ņemšanas intervāls {median}s, min. 30s), lai apstāšanās tiktu atklāta ātri bez viltus apturēšanas." + "standby_floor": "Katrs nesens cikls vēl joprojām patērēja {idle}W, kad beidzās, tādēļ slieksnis zem šīs vērtības nekad nevar konstatēt ierīces izslēgšanos. Saglabāts virs tās; {cycles} tīrie cikli ierakstā nekad nav nokrituši zem tā ilgāk par {pause}s.", + "thr_batch": "Nedaudz virs zemākās aktīvās jaudas (p05) {cycles} ciklos ({p05}W), lai starti tiktu uztverti agri un apstāšanās slieksnis paliktu zem zemākās darbības jaudas.", + "watchdog": "Saglabāts pēc iespējas zemāks, cik droši (tikai nedaudz virs p95 atjaunināšanas intervāla {p95}s un vismaz 2x paraugu ņemšanas intervāls {median}s, min. 30s), lai apstāšanās tiktu atklāta ātri bez viltus apturēšanas.", + "end_energy_floor": "Paaugstināts līdz minimumam, ko nosaka apstāšanās slieksnis ({stop} W) izslēgšanas aizkaves laikā ({off_delay} s); zem tā cikls var beigties tikai ar rezerves mehānismu.", + "max_duration_ratio_default": "Atjaunota noklusējuma vērtība: zemāka augšējā robeža noraida pareizo programmu ikreiz, kad cikls ilgst ilgāk nekā parasti.", + "min_duration_ratio_default": "Atjaunota noklusējuma vērtība: augstāka apakšējā robeža neļauj atpazīt notiekošu ciklu, līdz tas ir krietni pavirzījies uz priekšu." }, "exclusions": { "summary": "Izslēgti {total} kļūdaini atpazīti cikli: {parts}.", @@ -2021,7 +1975,8 @@ "no_power": "nav jaudas", "no_active_power": "nav aktīvas jaudas" } - } + }, + "now_value": "(tagad {v})" }, "trend": { "down": "Tendences uz leju", @@ -2029,6 +1984,10 @@ }, "maint": { "bearing_service": "Gultņu apkope", + "salt": "Papildināt sāli", + "rinse_aid": "Papildināt skalošanas līdzekli", + "lint_filter": "Tīrīt plūksnu filtru", + "condenser_clean": "Tīrīt kondensatoru", "descale": "Atkaļķošana", "drum_clean": "Tīrīt tromeli", "filter_clean": "Tīrīt filtru", @@ -2036,34 +1995,22 @@ }, "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", + "smart_termination_duration_ratio": "Programmas vidējā ilguma daļa, kas ciklam jāsasniedz, pirms Viedā pabeigšana drīkst to beigt agrāk; samaziniet, ja darbības laiks mainās", + "anti_crease_finalize_ratio": "Programmas vidējā ilguma daļa, kas ciklam jāsasniedz, pirms pretgrumbu aste drīkst to pabeigt; žāvētājiem samaziniet, 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_exit_power": "Jaudas līmenis pauzēs starp griešanas impulsiem; darbojas tikai virs Apstāšanās sliekšņa", "anti_wrinkle_idle_timeout": "Atļautais klusuma laiks starp diviem griešanās impulsiem, pirms pretgrumbu režīms beidzas", "anti_wrinkle_max_duration": "Impulss, kas garāks par šo, beidz pretgrumbu režīmu un sāk jaunu ciklu", "anti_wrinkle_max_power": "Impulss, kas pārsniedz šo, beidz pretgrumbu režīmu un sāk jaunu ciklu", "completion_min_seconds": "Īsākais darbības laiks, kas skaitās par īstu ciklu", - "end_repeat_count": "Zemi rādījumi pēc kārtas pirms beigām", "interrupted_min_seconds": "Īsi cikli tiek atzīmēti kā pārtraukti", "min_off_gap": "Atstarpe, kas nepieciešama divu ciklu atdalīšanai", "off_delay": "Zemas jaudas sekundes pirms cikla beigām", "start_duration_threshold": "Sekundes virs sliekšņa, lai apstiprinātu sākumu", "start_threshold_w": "Minimālais vatu skaits, lai skaitītos kā sācies", "stop_threshold_w": "Zem šīs vērtības iekārta skaitās izslēgta", - "dtw_bandwidth": "3. posms: Sakoe-Chiba laika locīšanas josla (0 = DTW izslēgts; noklusējums 0,2 = 20% no cikla garuma)", - "corr_weight": "2. posms: līdzsvars starp līknes formu (korelācija) un jaudas līmeni (MAE); noklusējums 0,45", - "duration_weight": "4. posms: cik stipri ilguma atbilstība ietekmē galīgo rezultātu (noklusējums 0,22)", - "energy_weight": "4. posms: cik stipri enerģijas atbilstība ietekmē galīgo rezultātu (noklusējums 0,22)", - "profile_match_min_duration_ratio": "1. posms: īsākais cikls kā profila ilguma daļa; noklusējums 0,1 (10%)", - "profile_match_max_duration_ratio": "1. posms: garākais cikls kā profila ilguma daļa; noklusējums 1,8 (180%)", - "keep_min_score": "2. posms: zemākais rezultāts, lai paliktu sacensībā; noklusējums 0,1 pieļauj vājus kandidātus, vēlākie posmi izvēlas labāko", - "dtw_blend": "3. posms: 0 = tikai pamatrezultāts, 1 = tikai DTW rezultāts, 0,5 = vienāds sajaukums (noklusējums)", - "dtw_ensemble_w": "3. posms (ensemble režīms): skalētā L1 svars pret atvasinātās DTW; noklusējums 0,7 dod priekšroku līmeņapziņai", - "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)", + "profile_match_min_duration_ratio": "Īsākais cikls kā profila ilguma daļa; noklusējums 0,1 (10%)", + "profile_match_max_duration_ratio": "Garākais cikls kā profila ilguma daļa; noklusējums 1,8 (180%)", "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": { @@ -2071,12 +2018,14 @@ "status_tip": "Cikla iznākums: Pabeigts (dabiskas beigas), Pārtraukts (straujš jaudas kritums), Piespiedu apturēts (manuāli) vai Nepieciešams pārskats (gaida atsauksmi).", "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ī. 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.", + "energy_tip": "Kopējā patērētā enerģija (kWh), aprēķināta no jaudas laikā.", + "cost_tip": "Šā cikla enerģijas izmaksas, fiksētas pabeigšanas brīdī. Ja ieslēgtas Laikā svērtās izmaksas, katrs brīdis tiek rēķināts pēc savas cenas; citādi pēc cenas beigās. Iestatiet cenu sadaļā Iestatījumi, lai to 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.", - "cost_dynamic_tip": "Laikā svērts: rēķināts pēc cikla laikā spēkā esošās cenas (faktiskā {price} {cur}/kWh)." + "cost_dynamic_tip": "Laikā svērts: rēķināts pēc cikla laikā spēkā esošās cenas (faktiskā {price} {cur}/kWh).", + "dbg_conf": "Pārl.", + "dbg_mae": "MAE", + "dbg_corr": "Korel." }, "stat": { "avg": "vidēji", @@ -2100,7 +2049,10 @@ "notify_finish": "Tiktu nosūtīts pabeigšanas paziņojums.", "notify_milestone": "Tiktu nosūtīts atskaites punkta paziņojums.", "notify_held": "Paziņojums tika aizturēts klusuma stundu dēļ.", - "finished": "Cikls sasniedza gala stāvokli un beidzās." + "finished": "Cikls sasniedza gala stāvokli un beidzās.", + "match_reverted": "Programma atgriezās noteikšanas stāvoklī: tās rezultāts nokritās krietni zem sava maksimuma.", + "verified_pause": "Reāllaika salāgošana apstiprināja (vai atcēla) pauzi programmas vidū. Kamēr tā ir spēkā, cikls netiek pabeigts.", + "stalled": "Cikls gaidstāves jaudā palika ilgāk, nekā šī programma tur jebkad pauzē, un darbs vēl nebija pabeigts. Rādīts kā Pauzēts (Iestrēdzis); cikls paliek atvērts." }, "store": { "search_ph2": "Meklēt pēc zīmola vai modeļa…", @@ -2109,11 +2061,7 @@ "programs_count": "Programmas: {n}", "browse": "Pārlūkot", "device": "Ierīce", - "favorites": "Izlase", - "search_ph": "Meklēt pēc zīmola vai modeļa…", "no_results": "Atbilstošas ierīces nav atrastas. Izmēģiniet citu meklēšanu.", - "n_programs": "{n} programmas", - "n_cycles": "{n} cikli", "no_programs": "Šai ierīcei vēl nav kopīgotu programmu.", "no_cycles": "Šai programmai vēl nav kopīgotu atsauces ciklu.", "anon": "anonīms", @@ -2125,19 +2073,21 @@ "merge_target": "Sapludināt ar profilu", "description": "Apraksts (neobligāti)", "contributed_by": "autors {name}", + "website": "Veikala vietne ↗", + "website_tip": "Atvērt kopienas veikala vietni", + "favorites": "Izlase", "rating_summary": "★ {avg} ({n})", "no_ratings": "Vēl nav vērtējumu", - "website": "Veikala vietne ↗", - "website_tip": "Atvērt kopienas veikala vietni" + "unreachable": "Neizdevās sasniegt kopienas veikalu. Pārbaudiet savienojumu un mēģiniet vēlreiz." }, "settings": { "gear": { - "title": "Iestatījumi" + "title": "Preferences" } }, "link": { - "manual": "Rokasgrāmata ↗", - "add_appliance": "Pievienojiet savu ierīci" + "maintenance_supporter": "Maintenance Supporter vietnē GitHub ↗", + "manual": "Rokasgrāmata ↗" }, "tip": { "add_brand": "Zīmola nav sarakstā? Pievienojiet to kopienas katalogam", @@ -2171,7 +2121,13 @@ "rebuild": { "envelopes": "Apvalklīkņu pārbūve" }, - "cancelling": "Atceļ..." + "cancelling": "Atceļ...", + "auto_label": { + "running": "Ciklu automātiskā marķēšana" + }, + "store_download": { + "running": "Lejupielāde no veikala" + } }, "setup": { "hdr": { @@ -2180,26 +2136,24 @@ }, "phase0": { "washer": "WashData jau konstatē jūsu ciklus. Ierakstiet pirmo ciklu, lai aktivizētu programmu nosaukumus un laika aprēķinus.", - "dishwasher": "WashData vēro. Trauku mazgājamām mašīnām ir sarežģīti cikli – ir ļoti ieteicams ierakstīt pirmo ciklu. Ja konstatētais cikls darbojas pārāk ilgi, pirms saglabāšanas kā profilu izmantojiet cikla redaktoru, lai to apgrieztu.", + "dishwasher": "WashData vēro. Trauku mazgājamās mašīnas cikli ir sarežģīti, tāpēc ierakstiet savu pirmo ciklu. Ja noteiktais cikls ir pārāk garš, apgrieziet to cikla redaktorā pirms saglabāšanas kā profilu.", "generic": "WashData vēro. Ierakstiet vai iezīmējiet konstatētu ciklu, lai sāktu veidot profilus." }, "phase1a": { - "labelled": "Labs sākums – jūsu pirmā programma ir saglabāta. Lai iegūtu tīrākos datus, apsveriet iespēju ierakstīt nākamo ciklu ar ierakstīšanas logrīku." + "labelled": "Labs sākums: jūsu pirmā programma ir saglabāta. Lai dati būtu vistīrākie, ierakstiet nākamo ciklu ar ierakstīšanas logrīku." }, "phase1b": { - "recorded": "Jūsu ieraksts ir saglabāts kā {profile_name}. Tagad ierakstiet vai iezīmējiet citas bieži izmantotās programmas, lai paplašinātu pārklājumu." + "recorded": "Jūsu ieraksts ir saglabāts kā {profile_name}. Tagad ierakstiet vai iezīmējiet citas bieži izmantotās programmas." }, "phase1c": { - "verify": "Jums ir {count} programmas no kopienas. Palaidiet ciklu, lai pārbaudītu, vai WashData to pareizi atpazīst – atbilstība uzlabosies, kad jūsu ierīce veidos savu vēsturi." + "verify": "Jums ir {count} programmas no kopienas. Palaidiet ciklu, lai pārbaudītu, vai WashData to atpazīst; saskaņošana uzlabojas, ierīcei veidojot savu vēsturi." }, "phase2": { - "cluster": "WashData ir konstatējis {count} ciklus, kas neatbilst nevienai saglabātai programmai – tie izskatās līdzīgi viens otram. Vai vēlaties tiem izveidot jaunu profilu?", + "cluster": "WashData ir redzējis {count} līdzīgus ciklus, kas neatbilst nevienai saglabātai programmai. Izveidot tiem jaunu profilu?", "unmatched": "Pēdējais cikls neatbilda nevienai saglabātai programmai. Vai tā ir jauna programma?" }, "phase3": { - "suggestions": "WashData ir iestatījumu ieteikumi, pamatojoties uz jūsu ciklu vēsturi – pārskatiet tos, lai uzlabotu noteikšanas precizitāti.", - "groups": "Daži jūsu profili izskatās kā viena un tā pati programma dažādās temperatūrās. Sakārtojiet tos grupā labākai atbilstībai.", - "phases": "Pievienojiet programmas fāzes profilam {profile_name}, lai iegūtu precīzākus laika atlikuma aprēķinus." + "suggestions": "WashData ir iestatījumu ieteikumi no jūsu ciklu vēstures. Pārskatiet tos, lai uzlabotu noteikšanu." }, "phase4": { "healthy": "Šī ierīce ir pilnīgi iestatīta ({profile_count} profili)." @@ -2212,12 +2166,58 @@ "create_from_cluster": "Izveidot profilu no šiem cikliem", "create_profile": "Izveidot tam profilu", "review_suggestions": "Pārskatīt ieteikumus", - "organise_profiles": "Kārtot profilus", - "configure_phases": "Konfigurēt fāzes", "skip_step": "Izlaist šo soli", "skip_forever": "Vairs nerādīt", "hide_guidance": "Slēpt norādījumus", "show_guidance": "Rādīt norādījumus" } + }, + "access": { + "edit": "Rediģēšana", + "full": "Pilna", + "inherit": "Mantot", + "none": "Nav (paslēpts)", + "read": "Lasīšana" + }, + "pg_label_reason": { + "no_winner": "Neviena programma neatbilda", + "below_floor": "Zem mācīšanās pārliecības", + "ambiguous": "Atzīmēta kā neskaidra", + "margin": "Pārāk tuvu otrajai vietai", + "unknown_profile": "Programma vairs nepastāv", + "not_evaluated": "Nav novērtēts" + }, + "diagram": { + "accum": "uzkr.", + "auto": "auto", + "below_off": "zem = izsl.", + "confirmed": "apstiprināts", + "energy_counts": "skaita enerģiju", + "feedback": "atsauksmes", + "gap_below_min": "atstarpe < min = viens cikls", + "heat_phase": "karsēšanas fāze", + "held_at_100": "noturēts 100%", + "ignored": "ignorēts", + "kept": "saglabāts", + "max": "maks.", + "min": "min.", + "minus_tol": "-tol.", + "no_match": "nav atbilstības", + "no_updates": "nav atjauninājumu", + "off_delay_wait": "izslēgšanas aizkave", + "off_gap": "izslēgšanas atstarpe", + "plus_tol": "+tol.", + "profile": "profils", + "reading_interval": "tipisks rādījumu intervāls", + "sensor_offline": "sensors bezsaistē: piespiedu apturēšana", + "si": "PI", + "spike_ignored": "smaile ignorēta", + "spike_ignored_short": "smaile: ignorēta", + "start": "starts", + "stop": "apstāšanās", + "tail_energy_resets": "beigu enerģija virs sliekšņa: taimeris atiestatās", + "thr": "slieksnis", + "too_short": "par īsu", + "tumble_kept": "trumuļa impulsi saglabāti" } } diff --git a/custom_components/ha_washdata/translations/panel/mk.json b/custom_components/ha_washdata/translations/panel/mk.json index dff67689..229fe0fa 100644 --- a/custom_components/ha_washdata/translations/panel/mk.json +++ b/custom_components/ha_washdata/translations/panel/mk.json @@ -3,6 +3,7 @@ "min": "мин", "entity_not_found": "Ентитетот не е пронајден", "running_long": "Трае предолго", + "stalled": "Заглавено", "remaining": "преостанато", "progress": "напредок", "show_time_remaining": "Прикажи преостанато време", @@ -48,16 +49,16 @@ } }, "badge": { - "quiet_tail_tip": "Кон крајот оваа програма стивнува околу {mins} мин, потоа повлекува околу {watts} Вт во тек на {secs} сек и завршува. Забележано кај {seen}/{measured} измерени циклуси: уредот не го прави тоа при секое пуштање.", + "quiet_tail_tip": "Кон крајот оваа програма стивнува околу {mins} мин, потоа повлекува околу {watts} Вт во тек на {secs} сек. Забележано во {seen} од {measured} измерени циклуси.", "quiet_tail": "~{mins} мин тишина на крајот", - "artifact_tip": "Откриени се {n} аномалии (на пр. вратата отворена во средината на циклусот) - отворени за да ги видите на графиконот", + "artifact_tip": "Откриени се {n} аномалии (на пр. вратата отворена во средината на циклусот) - отворете за да ги видите на графиконот", + "artifact_tip_one": "Откриена е {n} аномалија (на пр. вратата отворена во средината на циклусот) - отворете за да ја видите на графиконот", + "artifact_tip_other": "Откриени се {n} аномалии (на пр. вратата отворена во средината на циклусот) - отворете за да ги видите на графиконот", "auto": "(автоматски откриено)", "built_in_tag": "вграден", "declining": "↘ опаѓање", "energy_low": "Пониска енергија од вообичаено", "energy_spike": "Поголема енергија од вообичаено", - "fair_fit": "фер одговара", - "fair_fit_tip": "Умерена конзистентност на совпаѓање - некои циклуси доделени на овој профил имаат пониски оценки за доверба. Обележете повеќе циклуси или повторно снимајте го профилот за да ја подобрите точноста.", "feedback_requested": "Се бара повратна информација", "golden_cycle": "Снимен референтен циклус", "duration_outlier": "Циклуси со необична должина: {n}", @@ -66,27 +67,36 @@ "needs_review": "Треба преглед", "overrun": "Трчаше подолго од вообичаено", "poor_fit": "лошо вклопување", - "poor_fit_tip": "Неконзистентна историја на совпаѓање - размислете за повторно градење на овој профил", - "restart_gap_tip": "{n} HA пропуст при рестартирање: графиконот на моќност има дупка", + "restart_gap_tip": "{n} HA пропусти при рестартирање: графиконот на моќност има дупки", + "restart_gap_tip_one": "{n} HA пропуст при рестартирање: графиконот на моќност има дупка", + "restart_gap_tip_other": "{n} HA пропусти при рестартирање: графиконот на моќност има дупки", "reviewed": "Прегледано", "steady": "→ стабилно", "underrun": "Завршен побрзо од вообичаено", - "using_defaults": "Користење на стандардните испорачани вредности", - "using_tuned": "Користење на подесени тегови", "overrun_ratio": "({x}× од очекуваното)", "underrun_ratio": "({pct}% од очекуваното)", - "awaiting_tip": "Чека одобрување од заедницата", - "awaiting_n": "Чека одобрување · {n} потврдени", - "approved": "Одобрено", - "awaiting": "Чека одобрување", "imported_tip": "Увезено од продавницата на заедницата. Се користи само за совпаѓање, не се брои во статистиката.", "unmatchable": "не може да се совпадне", "not_importable": "неприменливо", "exists": "постои", "backfilled_tip": "Откриено во увезена историја на моќност. Влијае само на совпаѓањето на програмите, не се брои во статистиката.", - "armed": "(важи за следниот циклус)" + "armed": "(важи за следниот циклус)", + "awaiting_tip": "Чека одобрување од заедницата", + "awaiting_n": "Чека одобрување · {n} потврдени", + "awaiting": "Чека одобрување", + "approved": "Одобрено", + "matcher_unused": "Сè уште без совпаѓање со вашите циклуси", + "matcher_used": "Совпаднато со {n} ваши циклуси", + "matcher_used_one": "Совпаднато со {n} ваш циклус", + "matcher_used_other": "Совпаднато со {n} ваши циклуси", + "matcher_used_tip": "Циклуси на овој апарат што WashData сам ги совпаднал со оваа увезена програма. Увоз што никогаш не се совпаѓа можеби не одговара на вашиот апарат; можете да го отстраните. Се брои локално; ништо не се праќа во продавницата." }, "btn": { + "pg_preset_delete": "Избриши конфигурација", + "pg_preset_load": "Вчитај конфигурација", + "pg_preset_load_tip": "Замени ги вредностите во песочникот со оваа конфигурација (активните поставки остануваат недопрени)", + "pg_preset_save": "Зачувај како конфигурација", + "pg_preset_save_tip": "Зачувај ги сите вредности подолу како именувана конфигурација за овој уред", "reset_zoom": "Ресетирај зум", "set_brand_model": "Постави бренд и модел", "refresh_catalog": "Освежи го каталогот", @@ -97,12 +107,11 @@ "add_timer": "+ Додај тајмер", "apply_all": "Примени ги сите", "apply_label": "Примени етикета", - "apply_set_b": "Примени сет B", "apply_split": "Примени Сплит", "apply_trim": "Примени скратување", "auto_detect_split": "Автоматско откривање", "auto_label_cycles": "Циклуси за автоматско обележување", - "auto_label_cycles_tip": "Автоматски доделува имиња на профили на неозначени циклуси чијашто доверба за совпаѓање го надминува прагот", + "auto_label_cycles_tip": "Именувајте ги неозначените циклуси чија доверба за совпаѓање го надминува прагот.", "cancel": "Откажи", "clear_debug": "Исчистете ги податоците за отстранување грешки", "clear_splits": "Исчисти", @@ -112,8 +121,6 @@ "convert_to_automation": "Претворете во автоматизација", "correct": "Коригирај…", "create": "Креирај", - "create_from_cluster": "Направи профил од {count} циклуси", - "create_profile": "+ Направете профил", "delete": "Избриши", "delete_group": "Избриши група", "delete_group_tip": "Избришете ја само оваа група - профилите на членовите се чуваат", @@ -122,39 +129,30 @@ "discard_tip": "Отфрли го снимениот траг без зачувување", "dismiss": "Отфрли", "mute_suggestion": "Запрете да се предлага оваа поставка", - "pg_load_calibrated": "Вчитај калибрирани (ML) ({n})", - "pg_load_calibrated_tip": "Ги вчитува поставките калибрирани со ML како замени во песочникот", "pg_load_live": "Вчитај ги активните поставки", "pg_load_live_tip": "Прочитај ги повторно тековните поставки на интеграцијата и отфрли ги сите измени во песочникот", "pg_load_suggested": "Вчитај предлози ({n})", "pg_load_suggested_tip": "Ги вчитува тековните предлози на автоматскиот подесувач како замени во песочникот", - "pg_preset_delete": "Избриши конфигурација", - "pg_preset_load": "Вчитај конфигурација", - "pg_preset_load_tip": "Замени ги вредностите во песочникот со оваа конфигурација (активните поставки остануваат недопрени)", - "pg_preset_save": "Зачувај како конфигурација", - "pg_preset_save_tip": "Зачувај ги сите вредности подолу како именувана конфигурација за овој уред", "pg_publish_all": "Примени {n} во интеграцијата", "pg_publish_one": "Примени {label} во интеграцијата", "reset_muted": "Ресетирање на заглушените", + "reset_phase_tip": "Поништете ја вашата измена и вратете ја вградената фаза", "done": "Готово", "edit": "Уреди", "export": "Извоз", - "export_json": "Извези во JSON", "force_stop": "Присилно стопирање", "force_stop_tip": "Веднаш заврши го тековниот циклус и означи го како присилно запрен", "from_template": "Од шаблонот ▾", "ignore": "Игнорирај", "import_json": "Увезете од JSON", - "import_overwrite": "Увези (ги препишува податоците)", "inspect": "Увид", "label": "Етикета", - "load": "Вчитај", "load_more": "Вчитај повеќе", "manage": "Управувај", "merge": "Спојување", "new_automation": "+ Нова автоматизација", "new_group": "+ Нова група", - "new_group_tip": "Групирајте скоро идентични профили (иста форма/траење, различна температура или центрифугирање) за совпаѓањето сигурно да бира меѓу нив", + "new_group_tip": "Групирајте речиси исти профили (иста форма, различна температура или центрифуга) за совпаѓањето да може да ги разликува.", "new_phase": "+ Нова фаза", "new_profile": "+ Нов профил", "new_profile_tip": "Создадете нов профил на програма од постоечки означен циклус или снимање", @@ -162,23 +160,19 @@ "on_cycle_started": "На циклус започна", "pause_cycle": "Пауза", "pause_cycle_tip": "Паузирај го тековниот циклус – апаратот ќе продолжи од местото каде застанал", - "pg_apply_device": "Примени на уредот", - "pg_autofill": "Автоматско пополнување", - "play": "Пушти", "process": "Процес", "process_history": "Обработи сега", "process_recording": "Зачувај снимање", "process_tip": "Зачувај го снимениот траг како нов или постоечки профил", "rebuild": "Обновете ги пликовите", "rebuild_envelope": "Обновете го пликот", - "rebuild_tip": "Препресметај го очекуваниот плик на моќноста (мин./макс. лента) за сите профили од нивните означени циклуси – извршете после означување на нови циклуси или поправање на стари", + "rebuild_tip": "Повторно изградете го очекуваниот опсег на моќност на секој профил од неговите означени циклуси. Пуштете по означување или поправање циклуси.", "rec_start_tip": "Почнете снимање на трагот на моќноста на апаратот – стартувајте непосредно пред почеток на циклусот", "rec_stop": "Стоп", "rec_stop_tip": "Запрете го снимањето и задржете го снимениот траг за преглед", "record": "Започнете со снимање", "refresh": "Освежи", "refresh_diag_tip": "Повторно вчитајте ги статистиките за складирање", - "refresh_logs_tip": "Повторно вчитајте ги најновите записи на дневникот", "refresh_settings_tip": "Повторно вчитајте ги поставките од серверот", "relabel": "Повторно означи ({count})", "remove": "Отстрани", @@ -186,7 +180,6 @@ "reset": "Ресетирај", "reset_ml_models_tip": "Отфрлете ги фино подесените модели и вратете се на вградените. WashData може повторно да ги научи подоцна.", "reset_to_builtin": "Ресетирајте на вградени модели", - "reset_to_defaults": "Ресетирање на стандардните", "resume_cycle": "Продолжи", "resume_cycle_tip": "Продолжи го паузираниот циклус", "revert_settings": "Откажи промени", @@ -199,8 +192,6 @@ "run_analysis": "🔍 Изврши анализа на предлози", "run_analysis_tip": "Анализирајте ги вашите снимени циклуси сега и освежете ги предлозите за подесување", "run_auto_label": "Стартувај Auto-Label", - "run_both": "Изврши A наспроти B", - "run_simulation": "Изврши симулација", "save": "Зачувај", "save_access_control": "Зачувај контрола на пристап", "save_group": "Зачувај група", @@ -212,10 +203,8 @@ "save_settings": "Зачувај ги поставките", "select": "Изберете", "select_all": "Избери сè", - "select_last20": "Последни 20", "select_none": "Исчисти", "show_only": "Прикажи само", - "skip_setup": "Прескокни поставување", "split": "Сплит", "stop": "Стопирај", "submit_correction": "Поднесете корекција", @@ -224,30 +213,24 @@ "trim_split": "Скрати / Раздели", "undo": "Врати", "unlabel_selected": "Отстрани ја етикетата од избраните ({n})", - "unlabel_selected_tip": "Ја отстранува етикетата на профилот од избраните циклуси. Тие остануваат во вашата историја и може повторно да се означат подоцна - само престануваат да го обликуваат овој профил.", + "unlabel_selected_tip": "Отстранете ја етикетата од избраните циклуси. Остануваат во историјата, но престануваат да го обликуваат овој профил.", "use": "Користете", "wipe_all": "Избришете ги сите податоци", "delete_selected": "Избриши избрани ({n})", - "group_suggest": "Група {n}: {members}", "pg_apply_best": "Примени најдобро", - "pg_apply_to_settings": "Зачувај во поставките", "pg_apply_to_settings_tip": "Копирај ги тука изменетите вредности во активните поставки на овој уред", "import": "Увези", "disconnect": "Прекини врска", "connect_github": "Поврзи GitHub", - "save_appliance": "Зачувај уред", "share_to_store": "Сподели во продавницата", "share": "Сподели", - "confirm_appliance": "Потврди го овој уред", - "confirmed": "Потврдено", "browse_community_setups": "Прегледај поставки на заедницата", "download_device": "Преземи поставки на уредот", "share_device": "Сподели поставки на уредот", - "share_device_tip": "Споделете ги програмите и поставките на овој уред со заедницата", + "share_device_tip": "Споделете го овој апарат и неговите референтни циклуси во продавницата на заедницата, за сопствениците на истата машина да можат да ги преземат.", "share_n": "Сподели циклуси ({n})", "export_selected": "Извоз (избор на податоци)", "export_all": "Брз извоз на сè", - "import_raw": "Напредно: замени сè од JSON", "download_export": "Преземи извоз", "analyze_import": "Анализирај датотека", "import_selected": "Увези избрано", @@ -260,7 +243,15 @@ "save_total_cycles": "Зачувај го вкупниот број", "clear": "Исчисти", "add_brand": "Додај бренд", - "add_appliance": "Додај уред" + "log_done_task": "Означи како завршено: {task}", + "add_task": "Додади задача", + "add_appliance": "Додај уред", + "retry": "Обиди се повторно", + "confirm_appliance": "Потврди го овој уред", + "confirmed": "Потврдено", + "create_profile_from_gap": "Креирај профил", + "undo_import": "Поништи го последниот увоз", + "pg_rerun": "Ажурирај ја симулацијата" }, "note": { "off_delay": { @@ -268,10 +259,6 @@ } }, "conflict": { - "anti_wrinkle_exit": { - "exit": "Мора да е под Прагот за Стоп ({stop} Вт); инаку моќноста за излез од режимот против брчки се игнорира", - "stop": "Мора да е над Моќноста за Излез од Режимот Против Брчки ({exit} Вт)" - }, "anti_wrinkle_max": { "max": "Мора да е над Прагот за Почеток ({start} Вт); инаку ограничувањето на траењето против брчки се заобиколува", "start": "Мора да е под Макс. Моќноста Против Брчки ({max} Вт)" @@ -279,10 +266,10 @@ "attn_sub": "Поправете ги конфликтите пред зачувување", "attn_title": "Конфликти во поставките: {n}", "price_entity": { - "power_sensor": "Ова е Сензорот за моќност на уредот, а не цена. Ентитет на цена има предност пред статичната цена подолу, па циклусите би се пресметувале по вати за kWh - исчистете го ова поле.", - "energy_sensor": "Ова е Мерачот на енергија на уредот, а не цена. Ентитет на цена има предност пред статичната цена подолу, па циклусите би се пресметувале по показанието на мерачот за kWh - исчистете го ова поле.", - "device_class": "Класата на овој сензор е {dc}, а не цена по kWh. Ентитет на цена има предност пред статичната цена подолу - исчистете го ова поле или насочете го кон сензор за тарифа.", - "unit": "Овој сензор прикажува вредности во {unit}, што не е цена по kWh. Ентитет на цена има предност пред статичната цена подолу - исчистете го ова поле или насочете го кон сензор за тарифа." + "power_sensor": "Ова е Сензорот за моќност на уредот, а не цена. Циклусите би се пресметувале во вати по kWh; исчистете го ова поле.", + "energy_sensor": "Ова е Мерачот на енергија на уредот, а не цена. Циклусите би се пресметувале според читањето на мерачот по kWh; исчистете го ова поле.", + "device_class": "Овој сензор мери {dc}, а не цена по kWh. Исчистете го ова поле или изберете сензор за тарифа.", + "unit": "Овој сензор прикажува во {unit}, а не цена по kWh. Исчистете го ова поле или изберете сензор за тарифа." }, "settings_banner": "Конфликти во поставките: {n}. Проверете ги означените раздели и поправете ги пред зачувување.", "settings_banner_btn": "Оди на прв", @@ -317,8 +304,8 @@ "threshold": "Мора да е под Прагот за Стоп ({stop} Вт) за да стапи во сила" }, "pump_stuck": { - "duration": "Мора да е помал од Паузата Без Ажурирање ({to} с), за да алармот за заглавена пумпа се активира пред чуварот да го запре циклусот", - "timeout": "Мора да го надминува Времетраењето за Заглавена Пумпа ({ps} с), за да алармот се активира пред циклусот да биде присилно запрен" + "duration": "Мора да е помало од Паузата без ажурирање ({to} с), за алармот за заглавена пумпа да се активира прв", + "timeout": "Мора да го надминува Времетраењето за заглавена пумпа ({ps} с), за алармот за заглавена пумпа да се активира прв" }, "start_dur": { "sampling": "Интервалот на Земање Примероци го надминува Времетраењето на Старт ({sdt} с); единечни скокови може да отворат циклус", @@ -334,10 +321,15 @@ "watchdog": { "interval": "Треба да е барем 2x Интервалот на Земање Примероци ({si} с)", "sampling": "Интервалот на Земање Примероци треба да е највеќе половина од Интервалот на Чувар ({wi} с)" + }, + "end_energy": { + "energy": "Престрого за Праг за стоп ({w} Вт) во текот на Одложување на исклучување ({d} с); циклусот може да заврши само по резервен пат", + "stop": "Крајна енергија ({e} Вт·ч во текот на {d} с) дозволува само {p} Вт" } }, "hdr": { - "ab_comparison": "Споредба на поставки A/B", + "custom_tasks": "Ваши задачи", + "maintenance_supporter": "Ви треба повеќе?", "access_control": "Контрола на пристап", "add_maintenance": "Додади настан за одржување", "automation_delete": "Избришете ја оваа автоматизација", @@ -346,28 +338,22 @@ "automation_review_this_cycle": "Прегледајте го овој циклус", "automations": "Автомати", "clear_debug": "Исчистете ги трагите за отстранување грешки", - "cycle_simulator": "Симулатор на циклуси", "diagnostics_pref": "Дијагностика", "display": "Приказ", - "dtw_inspector": "DTW инспектор", "export_import": "Извоз / увоз", - "getting_started": "Започнување", "live_power": "Жива моќ", "logs": "Дневници", - "logs_diagnostics": "Дијагностика", "maintenance": "Акции за одржување", "maintenance_log": "Дневник за одржување", "maintenance_reminders": "Потсетници за сервис", "manual_recording": "Рачно снимање", - "manual_recording_tip": "Намерно стартувајте циклус додека WashData ја снима трагата на моќноста. Стартувајте непосредно пред почетокот на апаратот, запрете кога ќе заврши, па Обработете за да го зачувате како именуван профил.", + "manual_recording_tip": "Снимете циклус намерно: притиснете „Започнете со снимање“ непосредно пред апаратот да тргне и „Стоп“ кога ќе заврши, потоа „Процес“ за да го зачувате како профил.", "ml_learned": "Што научи WashData", - "ml_matching_tuning": "Фино подесување што одговара на програмата", "ml_settings_card": "Поставки", "ml_smart_learning": "Паметно учење", "my_preferences": "Мои преференции", "no_automations": "Сè уште нема автоматизација на овој уред.", "panel_settings": "Поставки на панелот", - "param_sweep": "Прочешлување на параметар", "pg_settings_source": "Извор на поставките", "phase_catalog": "Фазен каталог", "playground": "Песочник", @@ -376,11 +362,8 @@ "status": "Статус", "status_graph": "График на статус", "storage_stats": "Статистика за складирање", - "test_history": "Проверка врз историјата", "toggle_sidebar": "Вклучете ја страничната лента на Home Assistant", - "tools_and_data": "Алатки и податоци", "wipe_history": "Избришете ја историјата", - "recommendations": "Препораки ({n})", "pg_detection_params": "Поставки за откривање", "pg_outcome": "Резултат од симулацијата", "pg_across_cycles": "Низ сите ваши циклуси", @@ -388,7 +371,8 @@ "online_account": "Продавница на заедницата и онлајн функции", "import_power_history": "Увоз на историја на моќност", "service_status": "Статус на сервисот", - "online_account_tab": "Онлајн и заедница" + "online_account_tab": "Онлајн и заедница", + "live_match_debug": "Дијагностика на совпаѓањето во живо" }, "health": { "fair": "Прифатлив квалитет на профилот", @@ -396,11 +380,16 @@ "poor": "⚠ Лош квалитет на профилот" }, "lbl": { - "envelope_position_tip": "Позиција на сопствената снимена крива на совпаднатата програма, {pct}%. Се мери со порамнување на овој циклус со профилот наместо со броење време, па останува точна кога пуштањето е подолго или пократко од вообичаеното. Се освежува додека уредот е тивок, па во текот на активна фаза може да доцни.", + "envelope_position_tip": "Позиција на снимената крива на совпаднатата програма: {pct}%. Се одредува со порамнување на циклусот со профилот, па останува точна кога работата е подолга или пократка. Се освежува кога апаратот е тивок, па може да доцни.", "envelope_position": "крива {pct}%", "aria_playground_chart3": "Интерактивен график на моќноста на циклусот: посочете за да видите време/моќност, лизгајте или користете два прста за зумирање, влечете за поместување", "device_unresolved": "Недостапен уред ({id})", "drag_to_resize": "Повлечете за промена на големината", + "task_name": "Задача", + "every_cycles": "Интервал (циклуси)", + "every_days": "Интервал (денови)", + "new_task_name": "Нова задача", + "days_since_service": "{since} / {total} дена", "actions": "Акции", "activity": "Активност", "administrators": "Администраторите", @@ -408,10 +397,8 @@ "aria_cycle_chart": "Крива на моќта на циклусот", "aria_envelope_chart": "Графикон на обвивката на моќта на профилот", "aria_phase_chart": "Графикон на уредувачот на фази", - "aria_playground_chart": "Крива на моќта на циклусот со подвижни прагови на детекција", "aria_power_chart": "Графикон на потрошувачка на моќ", "aria_spaghetti_chart": "Преклопени криви на моќта на циклусите", - "aria_sweep_chart": "Графикон со резултати од скенирање на параметри", "artifact_anomaly": "Аномалија", "artifact_high_power": "Висока моќ", "artifact_interruption": "Прекин", @@ -425,87 +412,61 @@ "cohesion_good": "кохезија {pct}%", "cohesion_low": "⚠ ниска кохезија {pct}%", "compare_profiles": "Споредете со профили", - "concurrency": "Паралелност", "confidence": "Доверба", "confidence_threshold": "Праг на доверба", "correct_duration": "Точно времетраење (мин., изборно)", "correct_profile": "Точен профил", - "correlation": "Корелација", "cost": "Цена", "create_new_profile": "+ Направете нов профил…", "current_phase": "Тековна фаза", - "curves": "Криви", "cycle": "Циклус", "cycle_date_display": "Приказ на датум на циклус", - "cycle_health": "Здравје на циклусот", - "cycle_index": "индекс на циклус →", - "cycles": "Циклуси", "cycles_count": "Циклуси", "cycles_lc": "циклуси", - "dashed": "испрекината", "date": "Датум", "debug_traces": "Дебагирање траги", - "default": "Стандардно", "default_access_level": "Стандардно ниво за корисници кои не се наведени подолу", "default_other": "Стандардно (други уреди)", "default_tab": "Стандардна картичка при отворање на панелот", "font_size": "Големина на фонт на панелот", - "delta": "Δ", "description": "Опис", - "dtw_blend_w": "Тежина на мешање", - "dtw_blended": "Мешана оценка", - "dtw_ddtw": "DDTW оценка", - "dtw_l1": "L1 оценка", - "dtw_matched_profile": "(совпаднат профил на циклусот)", "duration": "Времетраење", - "duration_agreement": "Согласување на времетраење", - "emphasis": "Нагласување", "enable_access_control": "Овозможи контрола на пристап по корисник", "energy": "Енергија", - "energy_agreement": "Согласување на енергија", - "event_timeline": "Временска линија на настани", "event_type": "Тип на настан", "expected": "Очекувано", + "suspected_profile": "{name} (претпоставка)", + "runner_up_profile": "{name} (втора најдобра)", "expected_duration": "Очекувано времетраење (мин.)", "file_kb": "Датотека (kB)", - "final_score": "Конечна оценка", "from": "Од", "gap_s": "Јаз (и)", "group_name": "Назив на групата", "head_trim": "Скратување на почетокот (с)", - "health": "Здравје", "hide_tabs": "Скриј јазичиња за не-администратори", - "in_use": "Во употреба", "json_data": "JSON податоци", "label": "Совпаден програм", "last": "Последни", "last_checked": "Последно проверено:", "load_from_file": "Вчитај од датотека", - "mae_score": "MAE оценка", "manual_duration": "Рачно времетраење (мин., изборно)", "match": "Совпаѓање", "members": "Членови", - "metric": "Метрика", "mode_existing_profile": "Додај во постоечкиот профил", "mode_new_profile": "Креирај нов профил", "models_fine_tuned": "(фино подесени модели: {count})", - "n_classic_suggestions": "{n} класично", - "n_ml_suggestions": "{n} ML", "n_selected": "{n} избрани", - "n_tuning_suggestions": "{n} предлог за подесување", - "na": "н/д", + "n_tuning_suggestions": "{n} предлози за подесување", + "n_tuning_suggestions_one": "{n} предлог за подесување", + "n_tuning_suggestions_other": "{n} предлози за подесување", "new_profile_name": "Ново име на профил", "notes": "Забелешки", - "of": "од", - "outcome": "Исход", "panel_default_tab": "Стандардна картичка", "panel_language": "Јазик на таблата", "per_cycle_short": "циклус", - "pg_autofill_tip": "Автоматско пополнување на опсегот од тековната вредност ({lo}–{hi})", "pg_best_value": "Пронајдена најдобра вредност", "pg_current_value": "Тековна вредност:", "pg_drag_hint": "Влечете ја линијата на графиконот", - "pg_dtw": "DTW", "pg_ev_ending": "Завршување", "pg_ev_running": "Работи", "pg_group_advanced": "Гранични случаи", @@ -513,63 +474,39 @@ "pg_group_timing": "Правила за време", "pg_idle": "Неактивен", "pg_anti_wrinkle": "Против брчки", - "pg_last_run": "Последно извршување: {date}", - "pg_match_score": "Оценка на совпаѓање", - "pg_pct_of": "{p}% од {total}", "pg_poor_match": "Лошо совпаѓање", "pg_preset": "Конфигурација на песочникот", "pg_preset_name": "Име на новата конфигурација", "pg_preset_none": "Изберете конфигурација…", "pg_strong_match": "Силно совпаѓање", - "pg_tile_ambiguous": "Двосмислени", - "pg_tile_ambiguous_tip": "Циклуси со речиси изедначено совпаѓање меѓу две програми", - "pg_tile_detected": "Откриени", - "pg_tile_detected_tip": "Циклуси каде што моќноста го надмина прагот за старт доволно долго за да се регистрира како работа", - "pg_tile_matched": "Совпаднати", - "pg_tile_matched_tip": "Циклуси правилно припишани на позната програма", - "pg_time_left": "Преостанато време", "pg_weak_match": "Слабо совпаѓање", "phase_name": "Име на фазата", "name": "Име", "phase_timeline": "Временска линија на фази", "power": "Моќ", "profile": "Профил", - "profile_index": "индекс на профил →", "profile_name": "Име на профилот", "program": "Програма", - "program_tip": "Заменете го профилот кој е поврзан со тековниот циклус. Автоматското откривање му овозможува на интеграцијата автоматски да го избере најдоброто совпаѓање. Закачете одреден програм за принудно совпаѓање кога автоматското откривање е погрешно или знаете што работи. Изберете програма пред да го стартувате апаратот и таа ќе се примени штом ќе започне следниот циклус.", + "program_tip": "Изберете со која програма се совпаѓа тековниот циклус. Автоматското откривање го избира најдоброто совпаѓање. Закачете програма кога автоматското откривање греши; програма избрана пред стартот важи за следниот циклус.", "progress": "Напредок", "raw_socket": "Суров приклучок", - "read_only": "Само за читање", "ref_cycle": "Референтен циклус (опционално)", - "refresh_interval": "Интервал (и) на освежување", "remaining": "Преостанати", "reminder_every": "Потсети на секои (циклуси)", "remove_label": "- Отстрани ја етикетата -", "rename_profile": "Преименувајте го профилот", - "replay_duration": "Репродукција", "resulting_profile": "Резултат на профилот", "save_mode": "Режим за зачувување", - "score": "Оценка", - "score_breakdown": "Разложување на оценката", "seconds_from_start": "Секунди од почеток", "select_profile": "Изберете Профил", "setting": "Поставка", - "settings_a": "Поставки A (тековни)", - "settings_a_short": "A", "settings_advanced": "Напредно", - "settings_b": "Поставки B (тест)", - "settings_b_short": "B", "settings_basic": "Основно", "settings_detail_level": "Ниво на детали на поставките", - "settings_overrides": "Заменети поставки", "show_debug": "Прикажи картичка за отстранување грешки при совпаѓање во живо на страницата Статус (доверба, нејасност, најдобри кандидати)", "show_expected": "Прикажи очекувано преклопување на кривата (совпаднат профил, портокалова)", "show_raw": "Прикажи прекинувач за необработени отчитувања на приклучокот во живиот графикон на моќноста", "sparkline": "Неодамнешен тренд на времетраење на циклусите", - "stage2": "Етапа 2 – основна сличност", - "stage3": "Етапа 3 – DTW", - "stage4": "Етапа 4 – согласување", "status": "Статус", "tail_trim": "Скратување на крајот (с)", "tap_to_dismiss": "допрете го графиконот за да затворите", @@ -580,7 +517,6 @@ "ungrouped": "Негрупирани", "unlabelled": "Неозначено", "unlabelled_paren": "(неозначен)", - "warp_path": "Патека на порамнување", "input": "Влез:", "start": "Почеток", "end": "Крај", @@ -605,9 +541,8 @@ "n_shown": ", прикажани {n}", "cycles_title": "Циклуси ({n})", "pg_detecting": "Откривање", - "pg_best_marker": "Најдобро: {v}", "pg_time_left_model": "Преостанато време (модел)", - "pg_time_left_model_tip": "Проценка на преостанатото време од моделот (проценувач на фази + ML), а не статичко одбројување", + "pg_time_left_model_tip": "Проценето според фазите на програмата, а не фиксно одбројување", "phase": "Фаза", "pg_mode_history": "Тест врз историја", "pg_ev_detected": "Откриено", @@ -647,18 +582,8 @@ "pg_mode_optimize": "Оптимизирај", "pg_group_matching": "Совпаѓање на програми", "pg_zoom_hint": "двоен клик го ресетира зумот", - "aria_playground_chart2": "Интерактивен график на моќноста на циклусот: посочете за да видите време/моќност, лизгајте за зумирање, влечете за поместување", "pg_alert_timeout_end": "Завршено поради истек на времето, а не според предвидување", "pg_alert_indefinite": "Би работел бесконечно", - "pg_alert_stress_cap": "Достигнат заштитен лимит", - "pg_alert_stress_ok": "Откривање на мирување: циклусот е запрен", - "pg_alert_stress_warn": "Потрошувачката во мирување е над прагот за запирање", - "pg_stress_group": "Тест на откривање на мирување", - "pg_stress_idle_auto": "Auto", - "pg_stress_idle_w": "Ниво на мирување (W)", - "pg_stress_idle_w_desc": "Замени го автоматски откривениот минимален праг на потрошувачка во режим на мирување (оставете празно за автоматско откривање).", - "pg_stress_toggle": "Тестирај откривање на мирување", - "pg_stress_toggle_desc": "Симулира дека уредот останува при потрошувачката во режим на мирување по завршувањето на снимањето -- прикажува дали и кога WashData ќе го запре циклусот.", "task_pg_history": "Тест на историја", "task_pg_sweep": "Оптимизирај", "task_reprocess": "Повторна обработка", @@ -668,16 +593,9 @@ "pg_recent_runs": "Неодамнешни извршувања", "pg_load_run": "Повторно вчитај го ова извршување", "pg_match_confidence": "Сигурност на совпаѓањето", - "pg_envelope_fit": "Вклопување во обвивката", - "pg_envelope_fit_tip": "Колку добро циклусот се вклопува во зачуваната обвивка на моќноста на овој профил, поинаков поглед од сигурноста на совпаѓањето.", "n_imported_note": ", {n} увезени", "enable_online": "Овозможи онлајн функции", - "store_brand": "Бренд на уредот", - "store_model": "Модел на уредот", "anonymous": "Анонимно", - "rate_quality": "Оцени го квалитетот", - "rate_n": "{n} ѕвезди", - "approved_only": "Само одобрени", "adopt_settings": "Прифати поставки", "flags": "Ознаки", "include_phase_map": "Вклучи карта на фазите", @@ -694,7 +612,6 @@ "cat_custom_phases": "Прилагодени фази", "cat_profile_groups": "Групи профили", "cat_settings": "Поставки за детекција и препознавање", - "cat_matching_config": "Фино прилагодување на препознавањето", "cat_ml_models": "ML модели", "cat_feedback": "Повратни информации и ознаки за преглед", "cat_suggestions": "Предлози", @@ -714,7 +631,6 @@ "conflict_overwrite": "Презапиши", "hist_csv_data": "CSV податоци", "hist_from_recorder": "Или прочитај ја од Home Assistant", - "days": "дена", "hist_keep": "Зачувај го овој циклус", "hist_looks_complete": "завршен", "peak_power_short": "Врв", @@ -735,7 +651,40 @@ "zoom_hint_touch_zoomed": "зумирајте со два прста · двоен допир за ресетирање", "cycles_since_service": "{since} / {total} циклуси", "total_cycles_run": "циклуси вкупно", - "correct_total_cycles": "Поправи го вкупниот број" + "correct_total_cycles": "Поправи го вкупниот број", + "hist_reason_already_recorded": "се преклопува со циклус што WashData веќе го снимил", + "share_consent": "Потврдувам дека овие циклуси завршија нормално, без прекин", + "show_internal": "Прикажи внатрешни поставки", + "show_internal_tip": "Поставки што WashData ги нагодува наместо вас (земање примероци, чувар, честота на совпаѓање, прагови). Се прикажуваат кога овој уред има сопствена вредност, конфликт или предлог.", + "rate_n": "{n} ѕвезди", + "rate_quality": "Оцени го квалитетот", + "pg_autolabel": "Автоматско означување", + "pg_label_review": "Бара преглед", + "pg_ev_match_reverted": "Совпаѓањето е поништено", + "pg_ev_verified_pause": "Потврдена пауза", + "n_samples": "{n} примероци", + "n_samples_one": "{n} примерок", + "n_samples_other": "{n} примероци", + "n_cycles": "{n} циклуси", + "n_cycles_one": "{n} циклус", + "n_cycles_other": "{n} циклуси", + "energy_per_cycle": "{v}/циклус", + "energy_total": "вкупно {v}", + "automation_off": "(исклучено)", + "ref_cycle_none": "Нема", + "share_name": "Прикажи го моето име во споделеното", + "cat_backfill_cycles": "Циклуси најдени во увезена историја на моќност", + "pg_compare_profile": "Спореди со профил", + "pg_obj_early": "Стапка на рани завршувања", + "pg_obj_endlag": "Доцнење на завршувањето (медијана)", + "pg_obj_split": "Стапка на поделени циклуси", + "pg_sweep_cycles": "Последни циклуси", + "cycle_context": "Историја од recorder околу циклусот", + "cycle_context_off": "Исклучено", + "cycle_context_minutes": "{min} мин", + "cycle_context_series": "Историја од recorder", + "before_start": "Пред почетокот", + "after_end": "По крајот" }, "log": { "all_levels": "Сите нивоа", @@ -745,11 +694,9 @@ }, "ml": { "builtin_models": "● Користење на вградени модели", - "fit_fair": "Прифатливо", "fit_good": "Добро", "fit_slight": "Мало", "fit_strong": "Силен", - "fit_weak": "Слаби", "fit_word": "одговара", "personalized": "● Персонализирано за оваа машина", "better_than_baseline": "{pct}% подобро од основната проценка ({metric})", @@ -757,21 +704,29 @@ "trend_declining_tip": "Одговарањето на овој модел ослабе низ последните повторни проверки; прегледувањето на повеќе циклуси може да му помогне повторно да научи.", "trend_steady_tip": "Одговарањето на овој модел остана приближно стабилно низ последните повторни проверки.", "cap_blurb": { - "end": "Препознавање кога циклусот навистина завршил", - "live_match": "Побрзо препознавање на активната програма", - "quality": "Откривање на погрешно препознати или оштетени циклуси", - "remaining_time": "Предвидување на преостанатото време", "total_energy": "Предвидување на вкупната потрошувачка на енергија и трошокот" }, "cap_label": { - "end": "Препознавање на крајот на циклусот", - "live_match": "Препознавање на програмата", - "quality": "Проверка на квалитетот на циклусот", - "remaining_time": "Проценка на преостанатото време", "total_energy": "Проценка на потрошувачката на енергија" }, - "metric_auc": "AUC {auc} на издвоени податоци", - "metric_mae": "грешка {model} наспроти основната вредност {baseline}" + "metric_mae": "грешка {model} наспроти основната вредност {baseline}", + "fine_tuned_at": "фино подесено {when}", + "held_out_count": "измерено на {count} издвоени циклуси", + "held_out_count_one": "измерено на {count} издвоен циклус", + "held_out_count_other": "измерено на {count} издвоени циклуси", + "last_run_not_promoted": "Последна проверка ({when}): {reason}", + "never": "никогаш", + "skip_reason": { + "fit_failed": "Обучувањето не успеа: {error}", + "holdout_too_small": "Премалку циклуси за фер тестирање на нов модел: за тестирање можеа да се издвојат {held_out}, а потребни се {min}.", + "insufficient_rows": "Сè уште нема доволно употребливи циклуси: досега {count} означени, чисти циклуси.", + "insufficient_rows_one": "Сè уште нема доволно употребливи циклуси: досега {count} означен, чист циклус.", + "insufficient_rows_other": "Сè уште нема доволно употребливи циклуси: досега {count} означени, чисти циклуси.", + "not_better_than_incumbent": "Новиот модел не беше подобар од тој што се користи (грешка {model} наспроти {incumbent}), па тековниот е задржан.", + "not_better_than_naive": "Новиот модел не беше јасно подобар од едноставната проценка (грешка {model} наспроти {naive}).", + "unknown": "Ништо ново не е научено." + }, + "trained_unknown": "во непознато време" }, "modal": { "auto_label": "Циклуси за автоматско обележување", @@ -780,7 +735,6 @@ "cycle": "Циклус", "edit_group": "Уреди група на профили", "edit_phase": "Фаза на уредување", - "import_config": "Увезена конфигурација", "label_cycle": "Циклус на етикети", "merge_cycles": "Спојување на {n} циклуси", "new_group": "Нова група на профили", @@ -795,6 +749,8 @@ "delete_automation_title": "Избриши автоматизација", "delete_automation_msg": "Да се избрише автоматизацијата „{name}“ од Home Assistant? Ова не може да се врати.", "remove_legacy_title": "Отстрани застарени дејства", + "reset_phase_msg": "Да се поништи вашата измена на „{name}“ и да се врати вградената фаза? Програмите ги задржуваат своите опсези.", + "reset_phase_title": "Ресетирај фаза", "remove_legacy_msg": "Да се отстранат застарените прилагодени дејства? Тие ќе престанат да се активираат при настани на циклусот. Ова не може да се врати од панелот.", "discard_recording_title": "Отфрли снимка", "discard_recording_msg": "Да се отфрли зачуваната снимка? Ова не може да се врати.", @@ -815,32 +771,44 @@ "store_share_device": "Сподели поставки на уредот", "export_select": "Извоз - избор на податоци", "import_wizard": "Увоз - избор на податоци", - "history_import": "Увоз на историја на моќност" + "history_import": "Увоз на историја на моќност", + "apply_suggestions_title": "Примени предлози", + "remove_task_title": "Отстрани задача", + "remove_task_msg": "Да се отстрани потсетникот „{task}“? Неговите записи во дневникот за одржување се зачувуваат.", + "apply_suggestions_msg": "Да се променат овие поставки ({n})? Интеграцијата повторно се вчитува по зачувувањето.", + "discard_settings_title": "Да се отфрлат незачуваните промени?", + "discard_settings_msg": "Незачувани промени во поставките: {n}. Да се напуштат Поставките и промените да се отфрлат?", + "profile_title": "Профил · {name}", + "undo_import_msg": "Податоците и поставките на овој уред се враќаат во состојбата пред последниот увоз ({when}). Циклусите снимени оттогаш и направените промени ќе бидат изгубени.", + "undo_import_ok": "Поништи го увозот", + "undo_import_title": "Поништи го последниот увоз" }, "msg": { "pg_canvas_empty3": "Изберете циклус погоре и притиснете Стартувај за да го симулирате. Потоа посочете за да ги видите вредностите, лизгајте или користете два прста за зумирање и влечете за поместување.", + "pg_preset_delete_confirm": "Да се избрише конфигурацијата „{name}“?", + "pg_preset_limit": "Достигнат е лимитот на конфигурации ({n})", + "pg_preset_overwrite": "Да се пребрише конфигурацијата „{name}“?", "tail_trim_hint": "Отстранете толку секунди од крајот", - "store_sibling_hint": "Нема ништо споделено за вашиот точен модел? Близок модел од истиот бренд обично е добра почетна точка.", - "store_declare_appliance": "Кажете му на WashData кој уред го поседувате и оваа картичка ќе ги прикаже конфигурациите што другите ги споделиле за него. Можете и да напишете бренд погоре за да го разгледате каталогот.", - "refresh_catalog_hint": "Списоците со брендови и уреди на заедницата се чуваат во кеш за да остане продавницата на заедницата во рамките на своето дневно ограничување. Освежете за да ги преземете записите што други ги додале или одобриле.", + "store_sibling_hint": "Нема ништо споделено за вашиот модел? Сроден модел од истата марка обично е добар почеток.", + "store_declare_appliance": "Кажете му на WashData каков апарат имате за да ги видите поставките што другите ги споделиле за него. Или внесете марка погоре и разгледајте.", + "refresh_catalog_hint": "Листите на марки и апарати од заедницата се кешираат поради дневниот лимит на продавницата. Освежете за да ги видите записите што другите ги додале или одобриле.", "head_trim_hint": "Отстранете толку секунди од почетокот", "appliance_monitor": "Монитор на апаратот", "artifact_dip_detail": "Падна под вообичаениот опсег на моќност за ~{n}с.", "artifact_footer": "Нагласено на графиконот погоре. Ова се минливи артефакти (на пр. вратата отворена во средината на циклусот), а не нужно проблеми.", "artifact_header": "{n} откриени аномалии за време на овој циклус", - "artifact_pause_detail": "Моќноста падна до скоро нула за ~{n}с потоа продолжи – веројатно вратата беше отворена во средината на циклусот или циклусот беше паузиран.", + "artifact_header_one": "{n} откриена аномалија за време на овој циклус", + "artifact_header_other": "{n} откриени аномалии за време на овој циклус", + "artifact_pause_detail": "Моќноста падна речиси на нула за ~{n}s, а потоа се врати. Веројатно е отворена вратата или циклусот е паузиран.", "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": "Преклопени профили (бледо)", - "compare_profiles_tip": "Преклопете ги другите пликови на профили на табелата погоре за да видите кој најдобро одговара на овој циклус.", + "compare_profiles_tip": "Преклопете други профили на графикот за да видите кој најдобро одговара на овој циклус.", "compare_selected_cycles": "Избрани циклуси (цврсти) - прикажи / скриј", - "consider_new_profile": "Размислете за создавање нов профил.", - "coverage_gap": "последните циклуси немаат соодветни профили ({pct}% од последните 30).", - "coverage_gap_similar_cycles": "{count} слични необележани циклуси пронајдени – направете профил за да започнете да ги совпаѓате.", "cycles_deleted": "Избришани се {count} циклуси", "enough_data": "Доволно податоци за учење ({current}/{min} циклуси).", "export_description": "Изберете точно кои профили, циклуси, поставки и друго да се извезат во JSON, или анализирајте датотека и увезете само деловите што ги сакате.", @@ -848,30 +816,26 @@ "feedback_prompt": "Потврдете дека е точно, поправете ја програмата или игнорирајте.", "feedback_relabel_hint": "Повторното етикетирање на овој циклус исто така го решава.", "filter_by_profile": "Филтрирај по профил…", - "group_modal_help": "Групирајте програми со иста форма што се разликуваат по температура/вртење (времетраењето може да варира). Со совпаѓање се добиваат бодови на групата како еден кандидат, а потоа се избира членот кој најдобро одговара. Изберете најмалку 2; преклопот покажува колку се слични.", + "group_modal_help": "Групирајте програми со иста форма што се разликуваат по температура или центрифуга. Совпаѓањето ја оценува групата како целина, потоа го избира членот што најдобро одговара. Изберете најмалку 2; преклопот покажува колку се слични.", "group_not_cohesive": "Овие профили не се доволно слични за да се групираат веродостојно, така што совпаѓањето ги третира поединечно додека не го отстраните оддалеченото или не ја поделите групата.", "group_preview_hint": "Означете 2 или повеќе членови за преглед и споредба на нивните криви на моќност.", - "import_intro": "Вчитајте извезена датотека или залепете JSON носивост подолу.", - "legacy_actions_title": "{count} наследено приспособено дејство сè уште работи", + "legacy_actions_title": "{count} наследени приспособени дејства сè уште работат", + "legacy_actions_title_one": "{count} наследено приспособено дејство сè уште работи", + "legacy_actions_title_other": "{count} наследени приспособени дејства сè уште работат", "live_chart_loading": "Табелата за напојување во живо се појавува кога пристигнуваат читањата.", "loading": "Се вчитува…", "loading_curve": "Се вчитува кривата…", "loading_settings": "Се вчитуваат поставките…", "log_buffer_hint": "Најновото прво · ги зачува последните 500 записи ha_washdata од рестартирањето · повлечете го долниот раб за да ја промените големината.", - "maintenance_advisory": "Зголеменото времетраење/енергија може да укаже на потребното одржување на апаратот (на пр. бигор, чистење на филтерот).", "maintenance_due": "Треба одржување: {items}", - "maintenance_intro": "Запишувајте го одржувањето што го вршите на овој апарат и добивајте потсетници кога секоја задача повторно треба да се изврши.", + "maintenance_intro": "Запишувајте го сервисирањето на овој апарат и добијте потсетник кога секоја задача повторно ќе дојде на ред.", "maintenance_load_error": "Не може да се вчитаат податоците за одржување: {error}", "maintenance_requires_access": "Одржувањето и извозот/увозот бараат целосен пристап.", - "manual_duration_hint": "Просечната/очекуваната должина на циклусот на профилот, што се користи за проценки на преостанатото време. Уредете за да го поставите; оставајќи го непроменет ја задржува моменталната вредност.", - "matching_tuning_intro": "Кога учи, WashData приспособува и колку одговарањето на програмата тежи обликот наспроти времетраењето и енергијата.", + "manual_duration_hint": "Очекуваната должина на циклусот на профилот, се користи за проценка на преостанатото време. Оставете непроменето за да ја задржите тековната вредност.", "merge_intro": "Избраните циклуси се комбинираат во еден (хронолошки редослед; празнини исполнети со 0 W). Изберете го добиениот профил.", "ml_intro": "WashData се испорачува со паметни модели кои работат надвор од кутијата.", "ml_learned_intro": "Модели фино подесени на оваа машина.", - "ml_loading": "се вчитува ML…", "ml_settings_intro": "Два независни прекинувачи: едниот ги применува моделите додека работи циклусот, другиот дозволува WashData фино да ги прилагоди на вашата машина со текот на времето.", - "name_first_program": "Имате доволно циклуси – именувајте ја вашата прва програма за да започне совпаѓањето.", - "near_duplicate_cluster": "Откриена е скоро дупликат профилна група. Групирањето овозможува совпаѓањето со сигурност да се избере помеѓу сличните (на пр. иста програма на различна температура/вртење).", "no_cycles_match": "Ниту еден циклус не одговара на тековниот филтер.", "no_cycles_profile": "Нема циклуси за овој профил.", "no_cycles_selected": "Прво изберете барем еден циклус.", @@ -896,79 +860,60 @@ "no_suggestions": "Нема активни предлози.", "notify_services_hint": "Користете ID-ови на сервисите {entity} (одвоени со запирки за повеќе). Варијабли на шаблонот: {vars}.", "old_actions_warning": "Конфигуриран со стариот уредувач на дејства (сега отстранет). Сè уште пукаат на настани од циклусот, но веќе не може да се уредуваат овде. Претворете ги во нормална автоматизација или отстранете ги.", - "onboarding_progress": "{n} / 3 набљудувани циклуси", - "onboarding_watching": "Користете го апаратот како вообичаено – WashData набљудува. По 3 циклуси, ќе започне совпаѓањето на програми.", "pending_feedback": "Во очекување на повратни информации за откривање", "performance_trend": "Тренд на изведба ({n} циклуси)", - "pg_analysis_empty": "Вчитајте циклус за да видите анализа на совпаѓање.", "pg_apply_confirm": "Да се примени најдобрата вредност: {label} = {value}?", - "pg_canvas_empty": "Изберете циклус погоре, потоа притиснете „Вчитај“ за да ја видите неговата крива на моќност.", - "pg_chart_caption": "Стапка на откривање/совпаѓање по вредности на {param}", "pg_ctrl_intro": "Вредностите тргнуваат од активните поставки на интеграцијата за овој уред. Измените остануваат во песочникот сè додека не ги примените.", "pg_live_unavailable": "Активните поставки не се достапни, прикажани се стандардните вредности.", "pg_load_live_confirm": "Да се отфрлат измените во песочникот ({n}) и повторно да се вчитаат тековните поставки на интеграцијата?", - "pg_match_rate": "{pct}% совпаѓања на {total} циклуси", "pg_matches_live": "Се совпаѓа со активните поставки", "pg_n_changed": "Промени наспроти активните поставки: {n}", "pg_sugg_none": "Сите предлози веќе одговараат на тековните поставки", - "pg_preset_delete_confirm": "Да се избрише конфигурацијата „{name}“?", - "pg_preset_limit": "Достигнат е лимитот на конфигурации ({n})", - "pg_preset_overwrite": "Да се пребрише конфигурацијата „{name}“?", "pg_publish_one_confirm": "Да се зачува {label} = {value} во поставките на овој уред?", "pg_restart_note": "Рестартирајте го Home Assistant за да ги овозможите алатките за симулација.", - "pg_sim_progress": "{done} / {total} циклуси", - "pg_starting": "Започнување…", - "pg_stress_synthetic": "симулацијата на мирување почнува тука", - "pg_sweep_intro": "Што ако {param} беше поинаков? Тестирајте {steps} вредности врз вашите последни {cycles} циклуси за да ја пронајдете поставката каде што најмногу циклуси се совпаѓаат правилно.", - "pg_sweep_step": "Чекор {done} / {total}", - "pg_verdict_bad": "Бара внимание: многу циклуси остануваат неоткриени.", - "pg_verdict_good": "Добро наместено: повеќето циклуси правилно се откриваат и совпаѓаат.", - "pg_verdict_ok": "Прифатливо: некои циклуси се пропуштени. Обидете се да го намалите прагот за старт.", "phase_catalog_intro": "Именувани сегменти од циклусот (предмиење, загревање, центрифугирање...). Доделете ги на профил од неговата контролна табла.", "phase_ranges_intro": "Опсегот на фази (минути од почетокот на циклусот) се преклопени на просечната крива. Уредете ги вредностите за да ги прегледате во живо.", - "playground_intro": "Безбеден песочник за повторно пуштање минати циклуси со различни поставки, споредба на две конфигурации една до друга и увид во тоа како еден циклус се порамнува со профил. Ништо тука не ги менува вашите активни податоци сè додека изрично не го примените.", - "playground_needs_restart": "Рестартирајте го Home Assistant за да ги овозможите алатките на Песочникот.", - "preferences_admin": "Преференци, панел и контрола на пристап", - "preferences_adv": "Преференци", + "playground_intro": "Пробајте поставки врз вистинските податоци од вашите циклуси. Ништо тука не ги менува активните поставки додека не ги примените.", "prefs_personal": "Овие се однесуваат само на вашата сметка на „Помошник за дома“.", "font_size_hint": "Направете сè во овој панел поголемо или помало. Се применува на вашата сметка на овој уред.", - "process_history_hint": "Повторно стартувајте го совпаѓањето на сите зачувани циклуси, освежете ги предлозите за подесување, повторно обучете ги ML моделите (ако се овозможени), препресметајте ги трошоците според запишаните цени на енергијата и препресметајте го здравјето на циклусите. Стартувајте го ова по серија прегледи.", + "process_history_hint": "Повторно го пушта совпаѓањето на сите зачувани циклуси, ги освежува предлозите, повторно ги тренира ML моделите (ако се вклучени), ги пресметува трошоците на циклусите според снимените цени и повторно ја пресметува состојбата на циклусите. Пуштете по серија прегледи.", "profile_deleted": "Профилот е избришан", - "profile_poor_health_detail": "Циклусите доделени на овој профил имаат неконзистентни форми или мала доверба. Размислете за обнова на пликот или прегледување на означените циклуси.", "profiles_intro": "Кликнете на профил за статистика, фази и чистење. Групирајте скоро идентични програми (иста форма/траење, различна температура или центрифугирање) така што сигурно се совпаѓаат помеѓу нив.", - "rbac_hint": "Кога е исклучен, секој корисник на Home Assistant има целосен пристап (стандардно). Администраторите секогаш имаат целосен пристап и можат да управуваат со секого.", - "recent_logs": "Неодамнешни записи ha_washdata", + "rbac_hint": "Исклучено: секој корисник на Home Assistant има целосен пристап (стандардно). Администраторите секогаш имаат целосен пристап и можат да управуваат со сите.", "recording_in_progress": "Снимањето е во тек", "reminders_intro": "Прикажи потсетник во панелот толку циклуси по последниот сервис. Оставете празно или 0 за да го исклучите потсетникот.", "restart_gap_footer": "Означено на графиконот. Податоците за моќност недостасуваат за овие интервали; совпаѓањето користеше само вистински очитувања.", - "restart_gap_header": "{n} HA пропуст при рестартирање за време на овој циклус", + "restart_gap_header": "{n} HA пропусти при рестартирање за време на овој циклус", + "restart_gap_header_one": "{n} HA пропуст при рестартирање за време на овој циклус", + "restart_gap_header_other": "{n} HA пропусти при рестартирање за време на овој циклус", "restart_gap_item": "{dur} пропуст", - "review_confirm_help": "Потврдете дали овој циклус е правилно откриен. Вашите рецензии го обучуваат моделот на вашата машина - колку повеќе циклуси потврдувате, толку подобро се совпаѓаат и бодовите за здравјето. Доволно е брзо/добро/лошо.", + "review_confirm_help": "Дали овој циклус е правилно откриен? Потврдувањето циклуси го подобрува совпаѓањето за вашата машина. Доволно е брзо Добро/Лошо.", "review_in_settings": "Преглед во Поставки", "review_notes_placeholder": "Забелешки (изборно)", "review_notes_tip": "Белешки со слободен текст за ваша сопствена референца. Не се користи со совпаѓање или обука.", - "review_profile_tip": "Програмата за овој циклус е означена како. Ако програмата за автоматско откривање е погрешна, поправете ја овде - етикетирањето учи совпаѓање за идните циклуси.", - "review_quality_tip": "Колку е чист овој циклус. Добро = пример за учебник за оваа програма; Лошо = откриено, но бучно или нетипично; Неупотреблив = погрешно откриен (споен, скратен или лажен). Вози на здравствениот резултат и кои циклуси се дозволени за обука на моделот.", - "review_recorded_tip": "Означете го ова како рачно избран референтен циклус за неговата програма - иста улога како рачно снимен циклус. Референтните циклуси секогаш се чуваат, се засноваат соодветниот шаблон и никогаш не се испуштаат со чистење. (Ова е „златното“/снимено знаме; и двете се иста работа.)", - "review_tags_tip": "Изборни знаменца што опишуваат што тргнало наопаку со овој циклус, така што обуката и чистењето можат да одговорат за тоа.", + "review_profile_tip": "Програмата со која е означен овој циклус. Ако е погрешна, поправете ја тука; етикетите го учат совпаѓањето.", + "review_quality_tip": "Колку е чист овој циклус. Добро: учебничко извршување на оваа програма. Лошо: откриен, но бучен или необичен. Неупотребливи: погрешно откриен (споен, прекинат или лажен). Влијае на оценката за состојба и на тоа кои циклуси го тренираат моделот.", + "review_recorded_tip": "Означете како рачно избран референтен циклус на неговата програма, како рачно снимен циклус. Референтните циклуси секогаш се чуваат, го сочинуваат шаблонот за совпаѓање и чистењето никогаш не ги отстранува.", + "review_tags_tip": "Опционални ознаки за тоа што тргнало наопаку, за тренирањето и чистењето да можат да го земат предвид.", "review_to_cycles": "Отворете го редот за преглед на циклуси", "samples_decimated": "Прикажани {shown} од {total} примероци (проретчени за приказ; врвовите се задржани). Широк процеп тука е проретчување, а не податоци што недостасуваат.", - "saving_triggers_reload": "Зачувувањето активира повторно вчитување на интеграцијата. Ентитетите на HA може накратко да се покажат како недостапни.", + "saving_triggers_reload": "Зачувувањето ја вчитува интеграцијата повторно. Ентитетите можат накратко да бидат недостапни.", "search_placeholder": "Поставки за пребарување…", "see_recorder": "Погледнете го додатокот за рекордер подолу", "setting_changed": "Променето од {old} на {new} на {date}", "settings_basic_note": "Прикажани се основните поставки. Префрлете се на Напредно за целосната листа.", - "shape_drift_advisory": "⚠ Форма дрифт", - "shape_drift_detail": "Обрасецот на моќноста за овој профил се промени со текот на времето – можно е трошење на апаратот или потребно е одржување (на пр. одкаменување, чистење на филтерот).", "show_all_settings": "Прикажи ги сите поставки", - "showing_suggestions": "Се прикажува поставката {count} со предлози.", - "split_intro": "Кликнете на графиконот за да додадете или отстраните точка на разделување или автоматско откривање со празнини во мирување. Секој добиен сегмент може да добие свој профил.", - "storage_diagnostics": "Статистика за складирање, одржување, извоз/увоз", + "showing_suggestions": "Се прикажуваат {count} поставки со предлози.", + "showing_suggestions_one": "Се прикажува {count} поставка со предлози.", + "showing_suggestions_other": "Се прикажуваат {count} поставки со предлози.", + "split_intro": "Кликнете на графикот за да додадете или отстраните точка на поделба, или пронајдете ја автоматски според паузите на мирување. Секој дел може да добие свој профил.", "sug_staged": "Поставено {key} = {val}. Зачувајте за примена.", "sug_muted": "Оваа поставка нема да се предлага повторно", "sug_mute_failed": "Не можеше да се заглуши предлогот", "sug_unmuted_all": "Заглушените предлози се ресетирани", - "n_suggestions_muted": "{count} заглушени; авто-подесувачот нема да ги предлага овие.", + "n_suggestions_muted": "{count} заглушени предлози; авто-подесувачот нема да ги предлага.", + "n_suggestions_muted_one": "{count} заглушен предлог; авто-подесувачот нема да го предлага.", + "n_suggestions_muted_other": "{count} заглушени предлози; авто-подесувачот нема да ги предлага.", "toast_auto_detect_enabled": "Автоматско откривање овозможено", "toast_auto_label_complete": "Автоматско означување завршено", "toast_auto_label_failed": "Автоматско означување неуспешно: {error}", @@ -984,7 +929,6 @@ "toast_no_automation": "Тука не може да се создаде автоматизација", "toast_profile_name_required": "Потребно е име на профил", "toast_program_set": "Поставена програма: {program}", - "toast_rebuild_failed": "Реконструкцијата неуспешна: {error}", "toast_resume_failed": "Продолжувањето неуспешно: {error}", "toast_revert_failed": "Враќањето неуспешно: {error}", "toast_reverted": "{key} е вратено", @@ -995,95 +939,80 @@ "trend_energy_up": "Енергетскиот тренд нагоре ({pct}%/циклус) - неодамнешен просек {avg}", "trim_destructive_confirm": "Ова задржува само {pct}% од циклусот и не може да се врати. Да се продолжи?", "trim_intro": "Повлечете ги црвените рачки или внесете вредности. Сè што е надвор од прозорецот е отстрането.", - "tuning_suggestions_available": "{count} предлог за подесување е достапен од набљудуваните циклуси. Тие се појавуваат покрај соодветните полиња.", + "tuning_suggestions_available": "{count} предлози за подесување се достапни од набљудуваните циклуси. Тие се појавуваат покрај соодветните полиња.", + "tuning_suggestions_available_one": "{count} предлог за подесување е достапен од набљудуваните циклуси. Се појавува покрај соодветното поле.", + "tuning_suggestions_available_other": "{count} предлози за подесување се достапни од набљудуваните циклуси. Тие се појавуваат покрај соодветните полиња.", "unsure_detected_prefix": "WashData не е сигурен дека е откриен", - "updated": "Ажурирано", "warmup_badge": "Сè уште учи ({done}/{needed} циклуси)", - "warmup_detail": "На овој профил му требаат {needed} означени циклуси пред да започне автоматското совпаѓање. Секој потврден циклус помага во учењето.", + "warmup_detail": "Совпаѓањето веќе работи. WashData бара да ги потврдите првите {needed} циклуси на овој профил пред сам да почне да означува.", "wipe_history_warning": "Трајно избришете ги сите циклуси и профили. Не може да се врати.", "compare_overlay_tip": "Преклопете ги пликовите на научените профили за да видите на која програма личи секој циклус.", - "duration_trend_up_tip": "Времетраењето расте ({pct}%/циклус)", - "duration_trend_down_tip": "Времетраењето опаѓа ({pct}%/циклус)", - "energy_trend_up_tip": "Енергијата расте ({pct}%/циклус)", - "energy_trend_down_tip": "Енергијата опаѓа ({pct}%/циклус)", - "performance_trending": "Тренд на изведба", - "pg_select_cycle_hint": "Изберете циклус и кликнете ↺ за вчитување", - "pg_sweep_progress": "Чекор {done} / {total}", - "pg_analysis_hint": "Изберете циклус и кликнете ↺ за вчитување на анализата.", "diagnostics_load_failed": "Не успеа да се вчита дијагностиката: {error}", - "manual_duration_ref_hint": "Се користи само кога не е избран референтен циклус. Референтниот циклус го одредува времетраењето според сопствената должина.", + "manual_duration_ref_hint": "Се користи само без референтен циклус; референтниот циклус го поставува траењето според својата должина.", + "duration_learned_hint": "Пресметано од циклусите на оваа програма (без отстапувањата) и се ажурира по секој нов циклус. За да го промените, скратете или отстранете ги циклусите што отстапуваат.", "toast_convert_rolled_back": "Конверзијата не успеа и беше вратена (не остана ниту една автоматизација): {error}", "toast_convert_orphan": "Автоматизацијата беше создадена, но чистењето на старите акции не успеа. Не обидувајте се повторно: рачно отстранете ги старите акции за да избегнете дуплирана автоматизација.", - "advisory_duration_trend_up": "Циклусите на '{name}' стануваат сè подолги (околу +{pct}% по циклус). Ако однесувањето на уредот се променило, повторно снимете го или обновете го овој профил.", - "advisory_energy_trend_up": "'{name}' троши сè повеќе енергија (околу +{pct}% по циклус) - вреди да го проверите уредот ако тоа е неочекувано.", - "advisory_poor_health": "Профилот '{name}' има ниска оценка на совпаѓање - неговите неодамнешни циклуси значително варираат или слабо се препознаваат. Прегледајте ги неговите циклуси или повторно снимете го профилот за да останат точни препознавањето и проценките на времето.", - "advisory_shape_drift": "Профилот '{name}' значително отстапи од својата првобитна форма на потрошувачка на моќност. Уредот можеби со текот на времето го променил своето однесување (на пр. бигор, абење). Размислете за повторно снимање на овој профил со неодамнешните циклуси.", - "advisory_shape_drift_corr": "Профилот '{name}' значително отстапи од својата првобитна форма на потрошувачка на моќност (корелација {corr}). Уредот можеби со текот на времето го променил своето однесување (на пр. бигор, абење). Размислете за повторно снимање на овој профил со неодамнешните циклуси.", - "advisory_unmatchable": "Профилот '{name}' никогаш не може да се совпадне: зад него не стои ниту еден циклус со податоци за моќност, па WashData нема со што да го спореди тековниот циклус. Обележете циклус од оваа програма (или снимете нов) за да се врати во совпаѓањето.", - "advisory_unmatchable_no_duration": "Профилот '{name}' никогаш не може да се совпадне: има циклус со податоци за моќност, но без употребливо времетраење, па WashData нема референтна должина според која би се измерило совпаѓањето. Снимете ја оваа програма повторно или поставете го нејзиното очекувано времетраење за да се врати во совпаѓањето.", + "advisory_duration_trend_up": "Циклусите на '{name}' стануваат сè подолги (околу +{pct}% по циклус). Ако апаратот се променил, повторно снимете го или изградете го овој профил.", + "advisory_energy_trend_up": "'{name}' троши повеќе енергија со секој циклус (околу +{pct}% по циклус). Проверете го апаратот ако тоа е неочекувано.", + "advisory_poor_health": "'{name}' лошо одговара: неговите последни циклуси многу се разликуваат или слабо се совпаѓаат. Прегледајте ги неговите циклуси или повторно снимете го, за совпаѓањето и проценките да останат точни.", + "advisory_shape_drift": "'{name}' отстапил од првичната форма на моќноста, можеби поради бигор или абење. Размислете за повторно снимање со неодамнешни циклуси.", + "advisory_shape_drift_corr": "'{name}' отстапил од првичната форма на моќноста (корелација {corr}), можеби поради бигор или абење. Размислете за повторно снимање со неодамнешни циклуси.", + "advisory_unmatchable": "'{name}' никогаш нема да се совпадне: зад него нема ниту еден циклус со податоци за моќност. Означете или снимете циклус од оваа програма за да го поправите тоа.", + "advisory_unmatchable_no_duration": "'{name}' никогаш нема да се совпадне: неговиот циклус нема употребливо траење. Повторно снимете ја оваа програма или поставете го нејзиното очекувано траење.", "pg_no_alerts": "Во ова извршување не се откриени проблеми.", "pg_override_active": "Се користат вашите изменети поставки наместо тековните", "pg_history_empty": "Притиснете „Изврши“ за да ги репродуцирате циклусите.", "pg_history_summary": "{detected}/{total} откриени · {correct} совпаднати правилно", "pg_sweep_no_metric": "Нема доволно податоци за оценка на оваа цел.", "pg_sweep_intro2": "Најдете ја поставката што најдобро ја постигнува целта врз вашите последни циклуси. Ништо не се менува додека не ја примените.", - "pg_running_history": "Повторно изведување на вашите циклуси…", - "pg_running_sweep": "Тестирање на вредности низ сите ваши циклуси…", "pg_row_load_hint": "Вчитај го овој циклус во графикот погоре", - "pg_history_intro2": "Повторно изведете ги неодамнешните циклуси преку вистинскиот детектор и модулот за совпаѓање со поставките погоре. Кликнете на кој било ред за да го вчитате тој циклус во графикот; изменете поставка за да видите споредба пред и потоа.", + "pg_history_intro2": "Пуштете ги неодамнешните циклуси низ вистинскиот детектор и совпаѓање со поставките погоре. Кликнете ред за да го вчитате во графикот; сменете поставка за да споредите пред и потоа.", "pg_simulating": "Симулирање на циклус…", - "pg_canvas_empty2": "Изберете циклус погоре и притиснете Стартувај за да го симулирате. Потоа посочете за да ги видите вредностите, лизгајте за зумирање и влечете за поместување.", "pg_apply_settings_confirm": "Да се зачуваат ли овие {n} поставки на овој уред? {list}", - "pg_stress_terminated_detail": "Се стабилизира на ~{idle}W мирување -- циклусот заврши {h} ч {m} мин подоцна преку {reason}.", - "pg_stress_above_threshold_detail": "Потрошувачката во мирување ~{idle}W е еднаква или над ефективниот праг за запирање ({stop}W) -- циклусот никогаш не беше регистриран како мирен. За да го поправите ова, зголемете го stop_threshold_w.", - "pg_stress_hit_cap_detail": "Циклусот работеше {h} ч {m} мин без да запре -- принудно запрен од заштитниот лимит. Потрошувачка во мирување {idle}W наспроти прагот за запирање {stop}W.", "no_logs_match": "Ниту еден запис од дневникот не одговара на овие филтри.", "pg_analysis_hint2": "Изберете циклус и притиснете Изврши за да се вчита анализата на совпаѓање.", - "pg_analysis_empty2": "Притиснете Изврши за да ја видите анализата на совпаѓање.", - "imported_readonly": "Увезено од продавницата на заедницата. Се прикажува за референца и совпаѓање. Не се брои во вашата статистика и не може да се уредува.", + "imported_readonly": "Увезено од продавницата на заедницата. Служи како референца и за совпаѓање, не се брои во статистиката и не може да се уредува.", "merge_no_imports": "Увезените снимки не можат да се спојат со вистински циклус. Отштиклирајте ги за да ги споите.", - "store_enable_hint": "Овозможете ги онлајн функциите во поставките за да прелистувате и увезувате референтни циклуси од заедницата.", - "advisory_duration_outlier": "Циклуси означени како '{name}' ({n}) се премногу далеку од нејзината вообичаена должина за да можат некогаш да се совпаднат со неа (најголемо отстапување: {ratio}x). Веројатно се погрешно означени или едно снимање зафатило две извршувања. Означете ги повторно или поделете ги за да престанат да ја искривуваат проценката на времето за оваа програма.", + "store_enable_hint": "Вклучете ги онлајн функциите во Преференции > Онлајн и заедница за да прелистувате и увезувате референтни циклуси од заедницата.", + "advisory_duration_outlier": "Циклусите со етикета ‘{name}’ ({n}) се предалеку од неговата вообичаена должина за некогаш да се совпаднат (најлош: {ratio}x). Веројатно се погрешно означени или содржат две извршувања. Означете ги повторно или поделете ги за да не ја искривуваат проценката на времето.", "standby_above_stop_title": "Сè уште повлекува {idle} Вт кога циклусот заврши", "standby_above_stop_sub": "{n} од последните {total} циклуси. Одложувањето на исклучување почнува да тече дури кога моќноста ќе падне под Прагот за стоп ({stop} Вт), па циклусите завршуваат доцна или треба да се запрат рачно.", - "online_intro": "Споделувајте и преземајте референтни циклуси со други корисници на WashData. Сите онлајн функции се по избор и стандардно се исклучени.", "store_share_intro": "Прикачете го овој референтен циклус за да можат да го користат други со истиот уред. Се прегледува пред да стане јавен.", - "online_intro_global": "Прелистувајте и споделувајте референтни снимки со други корисници на WashData и потврдувајте записи за уреди. Едно поврзување важи за целата ваша интеграција WashData; брендот и моделот на уредот се поставуваат за секој уред во разделот „Основно“. Сите онлајн функции се по избор и стандардно се исклучени.", + "online_intro_global": "Прелистувајте и споделувајте референтни снимки со други корисници на WashData. Една врска важи за сите ваши уреди WashData; марката и моделот се поставуваат за секој уред под „Основно“. Онлајн функциите се исклучени додека не ги вклучите.", "online_unavailable": "Онлајн функциите не се достапни на овој сервер.", "store_picker_offline": "Овозможете ги онлајн функциите од иконата за поставки (запченик) за да избирате од каталогот на заедницата.", "pick_brand_first": "Прво изберете бренд на уредот.", - "model_not_found": "Сè уште не е во каталогот.", - "connect_to_confirm": "Поврзете се од иконата за поставки (запченик) за да потврдувате или оценувате.", + "custom_task_limit": "Го достигнавте максималниот број задачи ({max}).", + "custom_tasks_intro": "Додадете сè друго за што сакате потсетник. Задачата треба да се изврши кога ќе се достигне кој било од интервалите; оставете еден празен или 0 за да се користи само другиот.", + "maintenance_due_sensor_hint": "Бинарниот сензор „Треба одржување“ е вклучен додека има задача за извршување; користете го во сопствените автоматизации за известувања.", + "maintenance_supporter_intro": "Maintenance Supporter е посебна интеграција што може многу повеќе: кој било апарат или задача, распореди според време, користење или сензор, известувања и историја. Може да го користи бројачот на циклуси на WashData.", "adopt_settings_hint": "Примени ги поставките за откривање и совпаѓање на споделениот уред на овој уред", - "include_settings_hint": "Вклучи прагови за откривање, тежини за совпаѓање и други параметри во споделениот пакет", + "include_settings_hint": "Споделете ги праговите за откривање и совпаѓање на овој уред (без известувања, ентитети и цена на енергија). Другите сами избираат дали да ги применат.", "onboard_download": "Преземи готови поставки од продавницата на заедницата", - "share_consent": "Споделувате вистински податоци од вашиот уред. Не споделувајте ако вашите обрасци за користење се приватни.", "share_device_none": "Сè уште нема поставени уреди. Прво додајте уред.", "share_guideline_naming": "Користете јасни имиња на програмите (напр. 'Cotton 40', 'Eco 60') за да можат другите да ги препознаат", - "share_guideline_quality": "Споделувајте само циклуси што завршиле нормално -- без прекини среде циклусот, отворања на вратата или трепкања во напојувањето.", + "share_guideline_quality": "Споделувајте само циклуси што завршиле нормално: без прекини, отворање на вратата и прекини на струјата.", "share_guideline_review": "Прегледајте ги профилите пред споделување -- отстранете ги оние кои изгледаат погрешно", "share_guidelines_title": "Пред споделување", - "store_download_device_intro": "Преземете поставки на уредот од заедницата и применете ги на нов или постоечки уред", + "store_download_device_intro": "Додајте ги на вашиот уред сите споделени програми и нивните референтни циклуси. Вашите сопствени циклуси и статистика остануваат недопрени.", "store_share_device_intro": "Прикачете {brand} {model} со референтните циклуси што ќе ги изберете. Други со истиот уред можат да ги преземат вашите програми. Записите се прегледуваат пред да станат јавни.", "share_profile_no_cycles": "Нема референтни циклуси - означете циклус со ⭐ во картичката Циклуси за да го вклучите овој профил", - "advisory_phase_inconsistent": "Изгледа дека '{name}' меша различни програми или температури - неговите циклуси се загреваат многу различно долго. Поделбата на посебни профили (на пр. по температура) ќе ги подобри совпаѓањето и проценките на времето.", - "advisory_phase_inconsistent_title": "⚠ Можеби измешани програми", - "export_select_intro": "Означете што точно да се вклучи. Изборот на профили без нивните циклуси сепак извезува препознатлива програма (нејзината научена форма се пренесува заедно со неа).", - "import_analyze_hint": "Вчитајте извезена датотека (или залепете го нејзиниот JSON). WashData ја анализира и покажува што точно може да се увезе пред да се промени нешто.", + "export_select_intro": "Штиклирајте што да се вклучи. Профилите без своите циклуси сепак извезуваат препознатлива програма.", + "import_analyze_hint": "Вчитајте извезена датотека (или залепете го нејзиниот JSON). WashData покажува што може да се увезе пред нешто да се промени.", "analyzing": "Анализирање…", - "device_type_mismatch_warn": "Овој извоз е од друг тип уред ({src} наместо {local}). Програмите и циклусите сепак можат да се увезат како референтни податоци, но поставките специфични за уредот и увозот на реална историја се оневозможени.", + "device_type_mismatch_warn": "Овој извоз е од друг тип апарат ({src} vs {local}). Програмите и циклусите сепак може да се увезат како референтни податоци, но без поставките на уредот и вистинската историја.", "merge_hint": "Увезените ставки се додаваат; ништо локално не се губи. Конфликтите на имиња се решаваат подолу.", "replace_warn": "Секоја означена категорија се брише и се заменува со податоците од датотеката. Неозначените категории остануваат непроменети.", "dest_reference_hint": "Увезените циклуси само го подобруваат препознавањето на програми и никогаш не влијаат на статистиката за користење/енергија.", - "dest_real_history_hint": "Увезените циклуси се сметаат за сопствена историја на овој уред и ја хранат статистиката за енергија/користење. Користете за преместување на еден уред на нова инсталација.", - "import_history_description": "Имавте паметен приклучок и пред WashData? Поставете извоз од историјата на неговиот сензор за моќност или прочитајте ја директно од Home Assistant, и вообичаената детекција ќе помине низ неа, така што минатите циклуси ќе се појават во списокот „Циклуси“ подготвени за именување.", - "hist_input_hint": "Поставете CSV преземен од панелот „Историја“ (ентитет, состојба, последна промена) или дозволете WashData да ја прочита историјата на сензорот директно. Потоа детекцијата поминува низ неа исто како и во живо, а вие избирате кои од најдените циклуси да ги зачувате.", - "hist_recorder_hint": "Ги чита податоците од избраниот датум до сега. Home Assistant стандардно чува детална историја 10 дена, а потоа само часовни просеци, кои се премногу груби за детекција на циклуси - изберете поран датум само ако вашиот recorder е поставен да чува повеќе.", - "hist_scanning": "Вашата историја се пушта низ детекторот. Ова се извршува во заднина - можете да го затворите овој дијалог и да се вратите подоцна.", + "dest_real_history_hint": "Увезените циклуси се бројат како сопствена историја и статистика на овој уред. Користете при преместување на апарат на нова инсталација.", + "import_history_description": "Имавте паметен приклучок пред WashData? Прикачете ја неговата историја на моќност или прочитајте ја од Home Assistant. Откривањето минува низ податоците, а старите циклуси се појавуваат во листата „Циклуси“, подготвени за именување.", + "hist_input_hint": "Прикачете CSV од панелот „Историја“ (ентитет, состојба, последна промена) или дозволете WashData директно да ја прочита историјата на сензорот. Откривањето во живо минува низ податоците, а вие избирате кои циклуси да ги задржите.", + "hist_recorder_hint": "Чита од избраниот датум до сега. Home Assistant стандардно чува детална историја 10 дена; постарите часовни просеци се премногу груби за откривање циклуси.", + "hist_scanning": "Вашата историја минува низ детекторот во позадина. Можете да го затворите овој прозорец и да се вратите подоцна.", "hist_imported_count": "Увезени циклуси: {n}.", "hist_duplicates": "Веќе увезени и прескокнати: {n}.", "hist_capped": "Достигнато е ограничувањето на увезени циклуси по уред; останатите не се зачувани.", - "hist_next_step": "Тие се во списокот „Циклуси“, означени како увезена историја. Отворете еден и користете „Етикета“ за да ја наведете неговата програма.", + "hist_next_step": "Тие се во листата „Циклуси“, означени како увезена историја. Отворете еден и со копчето „Етикета“ именувајте ја неговата програма.", "hist_rows_read": "Прочитани читања: {n}", "hist_breaks": "Празнини каде сензорот бил недостапен: {n}", "hist_other_entity": "Прескокнати читања за други ентитети: {n}", @@ -1095,25 +1024,72 @@ "hist_scan_capped": "Прикажани се само првите кандидати (најдени: {n}).", "hist_recorder_empty": "Home Assistant нема детална историја за овој сензор во тој период.", "hist_scan_failed": "Скенирањето не успеа.", - "hist_scan_expired": "Тоа скенирање повеќе не е достапно. Скенирајте повторно.", + "hist_scan_expired": "Ова скенирање веќе не е достапно. Скенирајте повторно.", "hist_import_failed": "Увозот не успеа.", - "imported_history_readonly": "Откриено во увезена историја на моќност. Влијае на совпаѓањето на програмите, но не се брои во вашата статистика и не може да се скратува или разделува. Користете „Етикета“ за да ја наведете програмата.", - "odometer_intro": "Овој вкупен број само расте. Бришењето на запис за циклус не го менува и продолжува да брои и над ограничувањето на зачуваната историја, така што потсетниците за сервис остануваат точни.", - "odometer_correct_hint": "Поставете го ова ако WashData сними циклус што никогаш не се случил или ако апаратот веќе извршувал циклуси пред да се инсталира WashData.", + "imported_history_readonly": "Пронајдено во увезена историја на моќност. Помага на совпаѓањето, но не се брои во статистиката и не може да се скрати или подели. Означете го за да ја именувате програмата.", + "odometer_intro": "Овој збир само расте. Бришењето циклус не го намалува, па потсетниците за сервис остануваат точни.", + "odometer_correct_hint": "Поставете го ова ако WashData избројал циклус што не се случил или апаратот работел циклуси пред инсталирањето на WashData.", "no_reminders_set": "Сè уште нема поставени потсетници за сервис.", "toast_program_armed": "Поставена програма за следниот циклус: {program}", "toast_range_required": "Поставете валиден опсег Од/До", - "no_profiles_option": "(нема профили, додајте нов)" + "no_profiles_option": "(нема профили, додајте нов)", + "compare_cycles_title": "Спореди циклуси ({count})", + "edit_builtin_phase": "Ова е вградена фаза. Зачувувањето создава сопствена копија; избришете ја копијата за да го вратите оригиналот.", + "fetch_error": "Вчитувањето на податоците не успеа.", + "connection_lost": "Врската е прекината - се прикажуваат податоци од {time}.", + "pg_notify_partial": "Маркерите прикажуваат само известувања за старт, речиси готово, завршување, пресвртници и тивки часови. Ажурирањата во живо, потсетниците за истовар и предупредувањата за пречекорување не се симулираат.", + "connect_to_confirm": "Поврзете се од иконата за поставки (запченик) за да потврдувате или оценувате.", + "coverage_gap_cluster": "Неодамнешните неозначени циклуси ({count}) од околу {min} мин изгледаат како една програма што сè уште не сте ја креирале.", + "pg_label_yes": "На крајот на циклусот би бил означен како „{name}“", + "pg_label_no": "Би бил испратен на преглед: {reason}", + "live_match_debug_tip": "Доверба: колку точно тековната крива на моќност се совпаѓа со најдобриот профил кандидат (0-100%). Двосмислено: резултатите на двата најдобри кандидати се разликуваат за помалку од 5%, па етикетата е несигурна до крајот на циклусот.", + "automation_alias_started": "{device}: циклусот започна", + "automation_alias_finished": "{device}: циклусот заврши", + "automation_desc_started": "Се извршува кога ќе започне циклус на {device}. Додајте свои дејства (известувања, светла, ...).", + "automation_desc_finished": "Се извршува кога ќе заврши циклус на {device}. Додајте свои дејства (известувања, светла, ...).", + "feedback_detected": "WashData откри: {name}", + "import_undo_hint": "Се чува точка за враќање од пред последниот увоз ({when}): {profiles} профил(и) и {cycles} циклус(и).", + "replace_undo_hint": "Најпрво се зачувува точка за враќање, па овој увоз можете да го поништите од Извоз / увоз.", + "pg_in_sample_note": "Овие циклуси помогнаа да се изградат профилите со кои се споредуваат, па резултатите се оптимистички: кај нови циклуси очекувајте малку помалку точни совпаѓања.", + "hist_tail_trimmed": "Скратени се {min} мин чекање по завршувањето на циклусот, исто како што прави активната интеграција", + "hist_truncated": "запрено на ограничувањето на читања ({n}), па остатокот од историјата не е скениран", + "hist_warn_kw": "Највисокото читање е само {w}. Изгледа дека овој сензор јавува киловати; на WashData му требаат вати, па не може да се откријат циклуси.", + "hist_warn_naive_time": "{n} временски печати немаат временска зона и се прочитани како UTC. Ако датотеката користи локално време, циклусите се поместени за вашата разлика од UTC, а читањата околу промената на часот може да се изгубат.", + "hist_warn_naive_time_one": "{n} временски печат нема временска зона и е прочитан како UTC. Ако датотеката користи локално време, циклусите се поместени за вашата разлика од UTC, а читањата околу промената на часот може да се изгубат.", + "hist_warn_naive_time_other": "{n} временски печати немаат временска зона и се прочитани како UTC. Ако датотеката користи локално време, циклусите се поместени за вашата разлика од UTC, а читањата околу промената на часот може да се изгубат.", + "pg_compare_profile_tip": "Го избира само профилот исцртан врз графикот. Симулацијата секогаш сама ја совпаѓа програмата со циклусот, како и активната интеграција.", + "pg_outcome_stale": "Поставките се променија откако се изврши оваа симулација. Сè уште го прикажува стариот резултат.", + "pg_sweep_guarded": "Завршува повеќе циклуси прерано, дели повеќе од нив или пропушта циклуси што тековната поставка ги фаќа, па никогаш не се препорачува.", + "pg_sweep_keep_current": "Задржи ја тековната вредност ({value}, {metric}): ниедна вредност не беше подобра за барем еден циклус.", + "pg_sweep_n_cycles": "на {count} циклуси", + "pg_sweep_n_cycles_one": "на {count} циклус", + "pg_sweep_n_cycles_other": "на {count} циклуси", + "pg_task_busy": "За овој уред веќе тече „Тест врз историја“ или „Оптимизирај“. Почекајте да заврши или откажете го.", + "cycle_context_loading": "Се вчитува историјата од recorder…", + "cycle_context_note": "Сиво: историјата на сензорот за моќност од recorder околу овој циклус. Не е дел од циклусот и не влијае на траењето, енергијата или совпаѓањето.", + "cycle_context_none": "Во recorder нема отчитувања околу овој циклус (Home Assistant стандардно чува 10 дена историја).", + "cycle_context_tip": "Само за приказ: ја прикажува историјата на сензорот за моќност од recorder околу овој циклус, ако сè уште се чува. За да ги додавате неуспешните обиди за старт во идните циклуси, користете Воведни секунди на кривата (Поставки > Откривање > Почеток на циклус), што ги менува зачуваните траења.", + "invalid_threshold_pair": "Прагот за стоп ({stop} Вт) мора да биде под Прагот за почеток ({start} Вт). Ништо не е зачувано; сменете ги двете вредности заедно." }, "phase_desc": { "anti_crease": "Повремени кратки превртувања по завршувањето за да се намалат брчките.", "anti_wrinkle": "Периодично вртење после циклус за намалување на брчките.", + "baking": "Грејач на висока температура за кората и средината на лебот.", + "cooking": "Главна фаза на готвење со вклучени грејач и вентилатор.", "cool_down": "Превртување без топлина блиску до крајот на циклусот.", + "cool_down_af": "Ладење само со вентилатор по загревањето.", "drain_and_switch": "Преоден период од режим на перење во сушење.", "dry": "Фаза на сушење со помош на грејач и/или преостаната топлина.", "drying": "Главен загреан период на превртување.", "heat_up": "Почетното загревање на грејачот пред да започне целосното сушење.", + "keep_warm": "Слабо загревање за храната да остане топла.", + "keep_warm_bm": "Слабо загревање за лебот да остане топол по печењето.", + "kneading": "Моторот го меша и меси тестото. Висока потрошувачка на моќност.", + "pause_af": "Кратка пауза за протресување или проверка.", + "pre_heat": "Почетно загревање на комората пред вистинското готвење.", "pre_rinse": "Почетно прскање пред миење со детергент.", + "proving": "Втасување на слаба топлина: квасецот ферментира и тестото нараснува.", + "resting": "Кратка пауза со мала моќност меѓу месењата за глутенот да се опушти.", "pre_wash": "Почетно натопување или предтретман пред главното перење.", "rinse": "Фаза на плакнење со чиста вода. Оваа фаза може да се повтори повеќе пати.", "sanitize": "Фаза за чистење со висока температура за програми за дезинфекција.", @@ -1126,6 +1102,7 @@ "wash_dw": "Главно миење со детергент со загревање." }, "placeholder": { + "custom_task": "пр. Исчисти ја гумата на вратата", "add": "додај…", "group_name": "пр. Памук 2:47", "json_buttons": "[{\"action\":\"ID\",\"title\":\"Label\"}]", @@ -1152,11 +1129,11 @@ }, "section": { "dishwasher": { - "intro": "Ракување со крајот на циклусот за машини за миење садови, кои обично завршуваат со долга фаза на сушење речиси без потрошувачка на енергија пред кратко финално испумпување.", + "intro": "Ракување со крајот за машини за миење садови, кои често долго и тивко сушат пред кратко завршно испуштање.", "label": "Машина за миење садови" }, "anti_wrinkle": { - "intro": "Режимот против брчки/набирање открива импулси со ниска моќност по главната фаза и ги прикачува на завршениот циклус.", + "intro": "Ги задржува кратките импулси на барабанот со ниска моќност по главната фаза на машината за сушење со завршениот циклус.", "label": "Против брчки" }, "basic": { @@ -1164,7 +1141,7 @@ "label": "Основно" }, "delay": { - "intro": "Откривањето на одложениот старт идентификува кога апаратот е вклучен но сè уште не започнал циклус.", + "intro": "Открива апарат што е вклучен и чека одложен старт.", "label": "Одложен старт" }, "detection": { @@ -1188,161 +1165,153 @@ "label": "Тајмирање и чувар" }, "triggers": { - "intro": "Опционални надворешни сигнали: тригер за крај, сензор за врата, прекинувач за пауза и потсетник за истовар.", + "intro": "Опционални додатоци: тригер за крај, сензор за врата, прекинувач за пауза и потсетник за истовар.", "label": "Тригери и врати" - }, - "phase_eta": { - "label": "Преостанато време", - "intro": "Проценка на преостанатото време, свесна за фазите, за машини чија должина на циклусот зависи од температурата или центрифугирањето." } }, "setting": { "dishwasher_end_spike_quiet_release": { - "doc": "Откако циклусот ќе го надмине очекуваното траење, колку долго машината за миење садови мора да остане неактивна (под «Праг за стоп») пред WashData да престане да чека финално испумпување и да го заврши циклусот. Зголемете го ако вашата машина има долга тивка фаза на сушење пред доцно финално испумпување што се пропушта - поширок прозорец овозможува наученото траење да ги следи сезонските промени (постудена влезна вода = подолги циклуси) наместо да се заклучи на старата просечна вредност. Тоа само го скратува чекањето во однос на внатрешниот лимит од 30 минути за чекање на финалниот скок (испумпување на вода), но никогаш не го продолжува.", + "doc": "По очекуваното траење: колку долго машината за миење садови мора да остане под Прагот за стоп пред WashData да престане да чека завршно испуштање. Зголемете ако доцното испуштање по долго сушење се пропушта. Чекањето никогаш не надминува 30 минути.", "label": "Тивко завршување на пасивното сушење" }, "anti_crease_finalize_ratio": { - "doc": "Колку длабоко во очекуваното траење на совпаднатата програма мора да навлезе циклусот пред WashData да смее да заклучи дека тивкото превртување на барабанот што го гледа е завршната опашка против брчки и да го заврши циклусот таму. Очекуваното траење е просекот на програмата, па кај машина за сушење чие време на работа со сензор за влажност зависи од полнењето, повеќето циклуси никогаш не достигнуваат 98% од тој просек, па опашката останува сè до резервниот тајмаут - често 15 до 40 минути откако машината навистина застанала. Кај таква машина за сушење намалете ја оваа вредност (пр. 0.75). Машините за перење оставете ги на стандардната вредност: машината за перење го поминува најголемиот дел од циклусот под поставката Макс. моќност против брчки, па овој дел е единственото што ја одвојува тивката фаза среде перењето од опашката, а неговото намалување може да го преполови перењето. Оставете празно за стандардната вредност (0.98). Ова е поинаква поставка од Соодносот на паметно завршување, кој управува со посебна проверка за предвремено завршување.", + "doc": "Дел од просечното траење на програмата што циклусот мора да го достигне пред вртењето на барабанот со ниска моќност да се брои како опашка против брчки. Намалете го (на пр. 0,75) кај машини за сушење што се прилагодуваат на количината алишта. Кај машини за перење оставете стандардно (0,98), инаку перењето може да заврши на половина.", "label": "Сооднос на завршување против брчки" }, "anti_wrinkle_enabled": { - "doc": "Препознајте ги кратките пулсирања со мала моќност што ги испушта машината за сушење по главната топлинска фаза и држете ги прикачени на завршениот циклус наместо да ги читате како нови циклуси.", + "doc": "Задржете ги кратките импулси на барабанот со ниска моќност по главната фаза на загревање на машината за сушење со завршениот циклус, наместо да се читаат како нови циклуси.", "label": "Овозможи откривање против брчки" }, "anti_wrinkle_exit_power": { - "doc": "Моќта мора да падне под ова помеѓу импулсите за режимот против брчки да остане активен.", + "doc": "Во режимот против брчки моќноста под оваа вредност се смета за мирување меѓу импулсите на вртење, а мирување подолго од Макс. пауза меѓу импулси го завршува режимот. Ако Прагот за стоп е повисок, се користи тој, па поставката има ефект само над Прагот за стоп.", "label": "Праг на моќност за излез" }, "anti_wrinkle_idle_timeout": { "label": "Макс. пауза меѓу импулси", - "doc": "Колку долго машината смее да остане тивка меѓу два импулса на превртување на барабанот пред режимот против брчки да заврши. Поставете ја над најдолгата пауза што вашата машина за сушење ја остава меѓу импулсите, инаку секој нареден импулс се чита како лажен старт." + "doc": "Колку долго машината за сушење смее да мирува меѓу импулсите на барабанот пред режимот против брчки да заврши. Поставете над најдолгата пауза на вашата машина, инаку следните импулси се читаат како лажни стартови." }, "anti_wrinkle_max_duration": { - "doc": "Пулсовите подолги од ова се третираат како вистински циклус, а не како средство против брчки.", + "doc": "Импулс подолг од ова се брои како вистински циклус, а не како вртење против брчки.", "label": "Макс. траење" }, "anti_wrinkle_max_power": { - "doc": "Пулсот над оваа моќност се третира како вистински нов циклус, а не како ролна против брчки. Поставете веднаш над моќноста на пулсот на ролна.", + "doc": "Импулс над оваа моќност се брои како вистински нов циклус, а не како вртење на барабанот. Поставете малку над моќноста на импулсите.", "label": "Макс. моќност против брчки" }, "auto_label_confidence": { - "doc": "Ако резултатот на совпаѓањето на крајот на циклусот е еднаков или поголем од ова, програмата автоматски се означува без никакво барање за потврда. Подигнете го за да бара поголема сигурност пред автоматско означување; спуштете го за да се автоматизира повеќе. Работи заедно со Довербата при учење под неа.", + "doc": "Оценка на совпаѓање на крајот на циклусот при која или над која програмата се означува без прашање. Зголемете за поголема сигурност; намалете за автоматски да се означат повеќе циклуси. Држете над Доверба при учење.", "label": "Доверба за автоматско означување" }, "auto_maintenance": { - "doc": "Извршете ноќно одржување: обновете ги пликовите на профилот, пресметувајте го здравјето на циклусот, исечете ги трагите од отстранување грешки и задржете ги најновите циклуси.", + "doc": "Ноќно чистење: означува постари циклуси што сега јасно одговараат на програма, повторно ги гради кривите на програмите, ги освежува ознаките за аномалии и состојбата на циклусите и ги отстранува трагите за дебагирање ако „Зачувај траги за дебагирање“ е исклучено. Циклусите и нивните записи на моќност секогаш се чуваат.", "label": "Автоматско одржување (ноќно чистење)" }, "completion_min_seconds": { - "doc": "Циклусите пократки од ова се отфрлаат како циклуси на духови (тестни, отворање на вратата за додавање чорап).", + "doc": "Циклусите пократки од ова (пробни извршувања, отворање на вратата за додавање чорап) се зачувуваат како прекинати и не добиваат известување за завршување.", "label": "Мин. траење на циклус" }, "curve_preroll_seconds": { - "doc": "Колку секунди отчитувања од прекинати обиди за стартување смее WashData да пренесе на почетокот на кривата на циклусот. Кривата на циклусот вообичаено започнува со обидот за стартување што конечно успева, па кај машина што се обидува неколку пати пред да се смири - избор на програма, заклучување на вратата, прво полнење со вода - првите минути на вистинска активност недостасуваат на секоја крива. Поставете ја оваа вредност малку над најдолгиот таков залет што го има вашата машина (300 s е за повеќето повеќе од доволно). 0 ја исклучува функцијата. Имајте предвид дека вклучувањето го поместува запишаниот почеток порано, па циклусите запишани пред и по промената носат различни траења за истата програма; научените просеци ќе отстапуваат додека постарите циклуси не истечат.", + "doc": "Ги додава читањата од неуспешните обиди за старт на почетокот на циклусот, за машини што пробуваат пред стартот (заклучување на вратата, прво полнење). 300 с се доволни за повеќето; 0 = исклучено. Снимените почетоци се поместуваат порано, па научените траења некое време се менуваат.", "label": "Воведни секунди на кривата" }, "delay_confirm_seconds": { - "doc": "Напојувањето мора да остане толку долго во опсегот на подготвеност пред апаратот да се третира како што чека да стартува наместо да работи.", + "doc": "Моќноста мора толку долго да остане во опсегот на мирување пред апаратот да се смета дека чека старт.", "label": "Секунди за потврда" }, "delay_start_detect_enabled": { - "doc": "Откријте кога апаратот е вклучен и чека (одложен почеток / мирување), но не го започнал својот циклус, така што цртањето во режим на подготвеност не се смета за работен циклус.", + "doc": "Откривајте апарат што е вклучен и чека одложен старт, за неговата потрошувачка во мирување да не се смета за работа на циклус.", "label": "Овозможи откривање на одложен старт" }, "delay_timeout_hours": { - "doc": "Престанете да чекате во режимот за одложено стартување после толку часови и вратете се во мирување, така што машината што останала напојувана, но никогаш не стартувала, не чека вечно.", + "doc": "По толку часа престанете да чекате одложен старт и вратете се во мирување.", "label": "Часови за истекување" }, "device_type": { - "doc": "Класа на апарати. Поставува разумни поставки за откривање (прагови, исклучено одложување, крајно ракување) прилагодени за тој тип на апарат; променете го само ако уредот првично бил поставен како погрешен тип.", + "doc": "Ги поставува стандардните вредности за откривање (прагови, одложување на исклучување, ракување со крајот) за овој вид апарат. Менувајте само ако уредот е поставен со погрешен тип.", "label": "Тип на уред" }, "door_sensor_entity": { - "doc": "Изборен бинарен сензор за врата. Се користи за откривање кога апаратот е отворен/растоварен по циклус.", + "doc": "Опционален сензор за врата. Покажува кога апаратот е отворен за истовар по циклус.", "label": "Ентитет на сензор за врата" }, "duration_tolerance": { - "doc": "Толеранција за проценките на преостанатото време (повратни информации за учење, не совпаѓање). Ако вистинското времетраење е во рамките на +/-X% од проценката, тоа се смета за добро совпаѓање.", + "doc": "Само за повратни информации при учење: циклус во рамки на +/-X% од проценетото траење се брои како добра проценка. Не влијае на совпаѓањето.", "label": "Толеранција на проценката" }, "enable_ml_models": { - "doc": "Додека трае циклусот, дозволете им на моделите да ги усовршат резултатите во живо: постабилна проценка на преостанатото време и енергија/трошок, како и заштита против предвремено запирање при откривање на крајот (може само да го одложи финишот, никогаш да не заврши порано и е ограничен). Ги користи вашите фино подесени модели кога се достапни, инаку вградените. Исклучено = само класична логика базирана на моќност (сеуште сигурна).", + "doc": "Користете модел трениран на оваа машина за предвидување на енергијата и трошокот на тековниот циклус и дозволете добро позната програма што невообичаено рано запрела да заврши без целото одложување на исклучување. Исклучено: само логика според моќноста.", "label": "Примени паметни модели за време на циклус" }, "end_energy_threshold": { - "doc": "За време на одбројувањето без одложување, акумулираната енергија (вати x време) се споредува со овој праг. Ако се надмине, одбројувањето се ресетира - држејќи ги опашките против туткање и опашките за сушење во машина за миење садови прикачени на циклусот наместо да ги скратите. Зголемете го ако циклусите завршуваат премногу рано за време на ладењето; спуштете го ако откривањето е бавно.", + "doc": "Енергија дозволена за време на одложувањето на исклучување. Над тоа циклусот продолжува, па вртењата и завршното сушење остануваат дел од него. Намалете ако циклусите завршуваат за време на опашката; зголемете ако завршените циклуси и понатаму чекаат.", "label": "Крајна енергија" }, - "end_repeat_count": { - "doc": "Број на последователни отчитувања под прагот што се потребни пред да заврши циклусот. 1 е во ред за повеќето приклучоци. Зголемете на 2-3 ако вашиот паметен приклучок повремено пријавува примерок со лажна нула во средината на циклусот и вашите циклуси завршуваат предвреме.", - "label": "Броење на повторување при крај" - }, "energy_price_entity": { - "doc": "Сензор со тековната цена на струја по kWh (на пр. динамична тарифа). Мора да биде цена, а не сопствениот ентитет за моќност или енергија на приклучникот: има предност пред статичната цена подолу, па мерач на kWh овде го пресметува секој циклус по показанието на мерачот наместо по вашата тарифа. Кога е вклучен Временски пондериран трошок, секој циклус се наплаќа по цената што важела во секој момент од неговото работење; во спротивно на него се замрзнува цената што важела кога завршил.", + "doc": "Сензор со тековната цена по kWh (на пр. динамична тарифа). Има предност пред статичната цена подолу, па мора да биде цена, а не сензорот за моќност или енергија на приклучокот.", "label": "Ентитет на цена на енергија" }, "energy_price_static": { - "doc": "Фиксна цена по kWh што се користи за бројките на трошоците кога не е поставен ентитет на цена во живо погоре.", + "doc": "Фиксна цена по kWh, се користи кога не е поставен ентитет на цена.", "label": "Статична цена на енергија (по kWh)" }, "energy_sensor": { - "doc": "Изборен кумулативен бројач на енергија (total_increasing kWh/Wh, на пр. сопствениот бројач за вкупната потрошувачка на приклучокот). Кога е поставен, енергијата пријавена за секој циклус се зема од разликата во отчитувањата на овој бројач меѓу почетокот и крајот, што го избегнува потценувањето што се добива при интегрирање на бавно известувачки сензор за моќност. Се враќа на интегрираната вредност ако отчитувањето недостасува, неговата единица е непозната или разликата не е позитивна. Оставете празно за да продолжите да го интегрирате сензорот за моќност.", + "doc": "Опционален броител на kWh или Wh (total_increasing, на пр. сопствениот мерач на приклучокот). Енергијата на циклусот тогаш се чита од него, што е попрецизно од пресметка од бавен сензор за моќност. Ако неговото читање е неупотребливо, се користи пресметаната вредност.", "label": "Ентитет на мерач на енергија" }, "expose_debug_entities": { - "doc": "Објавувајте дополнителни дијагностички HA ентитети (доверба во совпаѓањето, двосмисленост, внатрешни состојби). Исклучено го одржува списокот со ентитети чист за нормална употреба.", + "doc": "Додајте дијагностички ентитети (доверба за совпаѓање, двосмисленост, внатрешна состојба). Исклучено: листата на ентитети останува уредна.", "label": "Изложи ентитети за дебагирање" }, "external_end_trigger": { - "doc": "Бинарен сензор чија промена на состојбата го означува крајот на циклусот (на пр. контакт „завршен“ на апаратот или придружна интеграција).", + "doc": "Бинарен сензор чија промена го означува крајот на циклусот (на пр. контакт „завршено“ или друга интеграција).", "label": "Ентитет на надворешен тригер" }, "external_end_trigger_enabled": { - "doc": "Дозволете надворешен бинарен сензор да го сигнализира крајот на циклусот, покрај вграденото откривање базирано на енергија.", + "doc": "Дозволете бинарен сензор да го сигнализира крајот на циклусот, покрај откривањето според моќноста.", "label": "Овозможи надворешен тригер за крај" }, "external_end_trigger_inverted": { - "doc": "Третирајте го исклучувањето на сензорот за активирање (наместо ВКЛУЧЕНО) како сигнал за крајот на циклусот.", + "doc": "Сметајте го ИСКЛУЧУВАЊЕТО на сензорот (а не вклучувањето) за сигнал за крај.", "label": "Инвертирај надворешен тригер (тригер при ИСКЛУЧУВАЊЕ)" }, "learning_confidence": { - "doc": "Ако резултатот на совпаѓањето е помеѓу оваа вредност и Довербата за автоматско означување, известување за повратни информации бара од вас да ја потврдите препознаената програма. Под овој резултат совпаѓањето е премногу несигурно за да се прикаже. Мора да се чува под Довербата за автоматско означување.", + "doc": "Најниска оценка на совпаѓање за означување завршен циклус. Јасниот победник се означува; тесниот резултат оди во редот за преглед во „Циклуси“. Под оваа вредност ништо не се означува ниту се прашува. Држете ја под Довербата за автоматско означување.", "label": "Доверба при учење" }, "linked_device": { - "doc": "Изборно вгнездувајте го овој уред WashData под друг уред (на пр. паметниот приклучок) во регистарот на уреди HA, прикажан како „Поврзан преку ...“.", + "doc": "Опционално. Прикажете го овој уред WashData под друг уред (на пр. паметниот приклучок) во Home Assistant, како „Поврзано преку ...“.", "label": "Групирај под уред" }, "min_off_gap": { - "doc": "Ако машината се исклучува помалку од ова време, секвенцата за вклучување/исклучување/вклучување се третира како еден континуиран циклус. Спречува програмите за натопување (машината се исклучува неколку минути при перење) да се поделат на два одделни циклуси. Поставете го пократко од јазот помеѓу вчитувањата одназад, ако сакате тие да се бројат како посебни циклуси. Стандардните типови на уред ја штитат типичната пауза во циклусот за секој апарат.", + "doc": "Исклучување пократко од ова не го дели циклусот, па паузите за натопување остануваат во едно перење. Држете пократко од паузата меѓу последователни полнења, инаку тие се спојуваат во еден циклус.", "label": "Мин. јаз при исклучување" }, "min_power": { - "doc": "Апсолутна минимална моќност се смета за активна. Читањата под ова се третираат како 0 W (во мирување), филтрирање на фантомското оптоварување на паметните приклучоци и LED диоди во мирување.", + "doc": "Читањата под оваа вредност се бројат како 0 W (мирување). Ја филтрира потрошувачката во мирување на паметните приклучоци и LED светлата за мирување.", "label": "Минимална моќност" }, "ml_training_enabled": { - "doc": "Периодично проучувајте ги прегледуваните циклуси преку ноќ и дотерувајте ги моделите на оваа специфична машина. Промената се задржува само кога навистина постигнува подобри резултати на долготрајните циклуси, така што ова може само да помогне или да остане исто - никогаш да не се уназадувате.", + "doc": "Одвреме-навреме повторно тренирајте го моделот за енергија врз завршените циклуси на оваа машина. Новиот модел се задржува само ако на тест циклусите е подобар од едноставната проценка.", "label": "Учи од оваа машина" }, "ml_training_hour": { - "doc": "Локален час од денот (0-23) за да се изврши фино подесување преку ноќ. Изберете мирен час како 2 (02:00 часот).", + "doc": "Час од денот (0-23) за ноќното тренирање. Изберете мирен час, на пр. 2.", "label": "Учи во час" }, "ml_training_interval_days": { - "doc": "Повторно проверете дали има подобрувања најмногу еднаш во овој многу дена.", + "doc": "Повторно тренирајте најмногу еднаш на толку дена.", "label": "Проверувај најмногу на секои" }, "ml_training_min_cycles": { - "doc": "Почекајте барем толкав број циклуси да бидат снимени пред фино подесување, за да има доволно за учење.", + "doc": "Пред тренирање почекајте најмалку толку снимени циклуси.", "label": "Прво потребни циклуси" }, "name": { - "doc": "Името на прикажување прикажано во списокот со интеграции на HA и во регистарот на уреди.", + "doc": "Име на овој уред прикажано во Home Assistant.", "label": "Назив на уред" }, "no_update_active_timeout": { - "doc": "Ако не пристигне ажурирање напојување за толку долго додека работи, претпоставете дека приклучокот испаднал офлајн и присилно прекинете за да избегнете циклус на зомби. Стандардните 600 секунди овозможуваат доцнење во облак или мрежа.", + "doc": "Ако за време на циклус толку долго не стигне ниедно ажурирање на моќноста, се претпоставува дека приклучокот е офлајн и циклусот принудно се запира. Стандардните 600 с ги покриваат доцнењата на облакот или mesh мрежата.", "label": "Пауза без ажурирање" }, "notify_before_end_minutes": { @@ -1350,35 +1319,35 @@ "label": "Предупредување пред крај" }, "notify_channel": { - "doc": "Име на каналот за известување на Android за пораки за почеток/во живо (го контролира звукот и приоритетот по канал на мобилната апликација). Blank ја користи стандардната придружна апликација.", + "doc": "Android канал за пораките за старт и во живо; каналот ги одредува звукот и приоритетот. Празно: стандардниот канал на апликацијата.", "label": "Android канал (старт/живо)" }, "notify_cycle_timers": { - "doc": "Известувања во одредени минути во циклусот (на пр. за додавање омекнувач). Пораката поддржува {device}, {program}, {minutes}. Овозможете автоматско паузирање за да паузирате во тој момент и добивате интерактивно известување со копчето Продолжи; продолжи преку панелот, услугата за пауза/продолжување или дејството за известување.", + "doc": "Известувања во зададени минути од циклусот (на пр. за додавање омекнувач). Пораката поддржува {device}, {program}, {minutes}. Со автоматска пауза циклусот исто така се паузира таму, а известувањето добива копче „Продолжи“.", "label": "Тајмери за циклус" }, "notify_finish_channel": { - "doc": "Името на каналот за известување на Android за завршната порака. Blank повторно го користи каналот за старт/жив.", + "doc": "Android канал за пораката за завршување. Празно: се користи каналот за старт и во живо.", "label": "Android канал (крај)" }, "notify_finish_message": { - "doc": "Тело што се испраќа кога циклусот ќе заврши. Шаблонски променливи: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}. {time_finished} и {vs_typical} се најкорисни тука.", + "doc": "Текст што се праќа кога циклусот ќе заврши. Променливи: {device}, {program}, {duration} (минути), {duration_hm} (1 h 05 min), {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}, {status} (completed или force_stopped; прекинатите циклуси не праќаат ништо). За секоја друга променлива се користи стандардниот текст.", "label": "Порака за крај" }, "notify_finish_services": { - "doc": "известувај.* услугите се повикуваат кога ќе заврши циклусот. Додадете по еден по цел; оставете празно известување без завршница.", + "doc": "notify.* услуги што се повикуваат кога циклусот ќе заврши. Оставете празно за ниедна.", "label": "Услуги за крај" }, "notify_fire_events": { - "doc": "Исто така, активирајте ha_washdata_* настани на почетокот/завршувањето на циклусот за да можете да изградите свои автоматизации.", + "doc": "Испраќајте и настани ha_washdata_* при старт и завршување на циклусот за сопствени автоматизации.", "label": "Активирај HA настани за известувања" }, "notify_icon": { - "doc": "Малата икона прикажана на известувањето (на пр. mdi:washing-machine). Android ја црта во статусната лента; iOS ја прикажува наместо иконата на апликацијата и бара придружна апликација 2026.8 или понова. Оставете празно за стандардната икона на платформата.", + "doc": "Мала икона на известувањето (на пр. mdi:washing-machine). Се испраќа само до mobile_app цели, не до notify групи. На iOS ја заменува иконата на апликацијата и бара мобилна апликација 2026.8 или понова. Празно: стандардната икона на платформата.", "label": "Икона на известување" }, "notify_live_chronometer": { - "doc": "Прикажи тајмер за одбројување што се ажурира во живо во известувањето (на платформите што го поддржуваат) наместо статична проценка.", + "doc": "Прикажете одбројување во живо во известувањето (каде што е поддржано) наместо фиксна проценка.", "label": "Користи хронометар во живо" }, "notify_live_interval_seconds": { @@ -1386,139 +1355,131 @@ "label": "Интервал за ажурирање во живо" }, "notify_live_overrun_percent": { - "doc": "Ако циклусот ја надмине неговата проценка за повеќе од овој процент, испратете предупредување за пречекорување.", + "doc": "Ажурирањата во живо престануваат кога циклусот ќе го надмине очекуваното траење за овој процент (20 = до 1,2x од очекуваното време).", "label": "% на прекорачување во живо пред предупредување" }, "notify_live_services": { - "doc": "известувај.* услугите повикани за ажурирање на напредокот во живо додека трае циклусот. Оставете празно за да ги оневозможите известувањата за напредок во живо.", + "doc": "notify.* услуги за ажурирања на напредокот во живо додека трае циклусот. Оставете празно за да ги исклучите.", "label": "Услуги за напредок во живо" }, "notify_only_when_home": { - "doc": "Испраќајте известувања само кога барем едно од поврзаните луѓе (горе) е дома.", + "doc": "Известувајте само кога барем едно од лицата погоре е дома.", "label": "Известувај само кога сте дома" }, "notify_people": { - "doc": "лице.* ентитети што ги користи „Извести само кога дома“ за да одлучи дали некој е дома.", + "doc": "person.* ентитети што ги проверува „Известувај само кога сте дома“.", "label": "Луѓе (за Само дома)" }, "notify_pre_complete_message": { - "doc": "Тело на предупредувањето пред-крај / речиси готово. Шаблонски променливи: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Текст на секое ажурирање на напредокот во живо. Шаблонски променливи: {device}, {program}, {minutes} (преостанати минути). За секоја друга променлива се користи стандардниот текст.", "label": "Порака за скоро крај" }, "notify_reminder_message": { - "doc": "Тело на потсетникот за истоварување што сè уште чека. Ако е празно, се користи вградената стандардна вредност. Шаблонски променливи: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Текст на еднократното Предупредување пред крај. Шаблонски променливи: {device}, {program}, {minutes} (поставката Предупредување пред крај). За секоја друга променлива се користи стандардниот текст.", "label": "Порака за потсетник" }, "notify_start_message": { - "doc": "Тело што се испраќа кога циклусот ќе започне. Шаблонски променливи: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Текст што се испраќа кога циклусот ќе започне. Шаблонски променливи: {device}, {program}. За секоја друга променлива се користи стандардниот текст.", "label": "Порака за почеток" }, "notify_start_services": { - "doc": "известувај.* услуги се повикуваат кога започнува циклус. Додадете по еден по цел (телефон, контролна табла, итн.); оставете празно известување без почеток.", + "doc": "notify.* услуги што се повикуваат кога циклусот ќе почне (телефон, контролна табла, ...). Оставете празно за ниедна.", "label": "Услуги за почеток" }, "notify_timeout_seconds": { - "doc": "Автоматски отфрли го известувањето по овие многу секунди (на платформите што го поддржуваат). 0 го задржува додека не се отфрли рачно.", + "doc": "Затворете го известувањето по толку секунди (каде што е поддржано). 0 го задржува додека не го затворите.", "label": "Автоматско отфрлање по" }, "notify_title": { - "doc": "Наслов на известување. Шаблонски променливи: {device}, {program}, {duration}, {energy}, {cost}, {date}, {time}, {minutes}.", + "doc": "Наслов на известувањето. Се заменува само {device}; за секоја друга променлива се користи стандардниот наслов.", "label": "Наслов на известување" }, "notify_unload_delay_minutes": { - "doc": "Неколку минути по завршувањето на циклусот пред да го испратите потсетникот за „растоварување на машината“ што се уште чека. Поставете 0 за да го оневозможите потсетникот.", + "doc": "Минути по крајот на циклусот пред потсетникот „истоварете ја машината“. 0 го исклучува.", "label": "Задоцнување на потсетникот за истовар" }, "notify_unload_repeat": { - "doc": "Продолжува да го испраќа потсетникот за истовар на секои \"Задоцнување на потсетникот за истовар\" минути, додека истоварот не се потврди или не притиснете \"Престани да потсетуваш\" во известувањето. Потребен е начин за потврда на истоварот: Ентитет на сензор за врата, Ентитет за потврда на истовар или опцијата \"Рачно потврди истовар\" подолу. Копчето за отфрлање работи на известувањата на мобилната апликација на Home Assistant (companion).", + "doc": "Повторно праќајте го потсетникот за истовар на секои „Задоцнување на потсетникот за истовар“ минути додека истоварот не се потврди или не допрете „Престани да потсетуваш“ (мобилна апликација). Потребен е Ентитет на сензор за врата, Ентитет за потврда на истовар или „Рачно потврди истовар“.", "label": "Повторувај до истовар" }, "off_delay": { - "doc": "Време е да се чека откако ќе падне напојувањето пред да се прогласи дека циклусот е завршен. Ако напојувањето се обнови во овој прозорец, циклусот продолжува беспрекорно - ова ги премостува паузите помеѓу фазите на перење. Машините за миење садови имаат долги фази на сушење (исклучување на струја 20-60 мин), така што доцнењето на исклучување мора да го надмине тоа за да се задржи целото перење+сушење како еден циклус.", + "doc": "Колку долго моќноста мора да остане ниска пред циклусот да заврши. Ако моќноста се врати во тоа време, циклусот продолжува. Машините за миење садови може да мируваат 20-60 мин при сушење, па поставете подолго.", "label": "Одложување на исклучување" }, "pause_cuts_power": { - "doc": "Кога циклусот е паузиран, исклучете го и Switch Entity подолу. Само за уреди чиј приклучок може безбедно да се исече во средината на циклусот.", + "doc": "Кога циклусот е паузиран, исклучете го и Ентитетот на прекинувач подолу. Само за апарати што безбедно можат да останат без напојување среде циклус.", "label": "Паузата исто така ја исклучува моќноста (преку прекинувач)" }, "power_off_delay": { - "doc": "Колку долго моќноста мора да остане под Прагот за исклучување на напојувањето откако циклусот ќе заврши пред состојбата да се врати на „Исклучено“. Се користи само кога Прагот за исклучување на напојувањето е над 0. Се проверува на позадинскиот ритам, така што ефективното одложување се заокружува нагоре до следното отчукување за истек на состојбата.", + "doc": "Колку долго моќноста по циклусот мора да остане под Прагот за исклучување на напојувањето пред состојбата да се врати на „Исклучено“. Се користи само кога тој праг е над 0. Се проверува периодично, па вистинското чекање може да биде малку подолго.", "label": "Одложување на исклучување на напојувањето" }, "power_off_threshold_w": { - "doc": "Опционално откривање на исклучена состојба врз основа на моќноста. Кога е над 0, откако циклусот ќе заврши и моќноста остане под ова ниво во текот на Одложувањето на исклучување на напојувањето, машината се третира како исклучена и состојбата се враќа на „Исклучено“. Оставете на 0 за да го оневозможите (стандардно). Поставете го над вистинското ниво на исклучено напојување, а под Прагот за стоп и под потрошувачката на вашата машина во мирување (завршена, но сè уште вклучена); ако не е под Прагот за стоп, се игнорира. Кога е овозможено, го заменува Одложувањето на ресет на напредок за враќање на „Исклучено“, така што завршената машина останува на „Завршено/Чиста“ додека навистина не се исклучи.", + "doc": "По циклусот моќност под оваа вредност во текот на Одложувањето на исклучување на напојувањето ја поставува состојбата на „Исклучено“, наместо Одложувањето на ресет на напредок. Поставете над потрошувачката на исклучениот апарат, а под мирувањето и Прагот за стоп. 0 го исклучува (стандардно).", "label": "Праг за исклучување на напојувањето" }, "power_sensor": { - "doc": "Ентитетот на сензорот што известува за електрична енергија во вати за овој апарат (на пр. sensor.washer_power). Откривањето на целиот циклус се заснова на овој сигнал.", + "doc": "Сензорот што ја јавува тековната моќност на овој апарат во вати (на пр. sensor.washer_power). Целото откривање се заснова на него.", "label": "Сензор за моќност" }, - "profile_duration_tolerance": { - "doc": "Опсегот +/- околу просечното времетраење на профилот што се користи за време на совпаѓањето. 0,25 значи дека профилот од 60 минути одговара на циклусите од 45-75 минути.", - "label": "Толеранција на траење на профилот" - }, "profile_match_interval": { - "doc": "Колку често се обидувате да се совпаднете со профилот за време на циклусот на трчање. Стандардните 300 секунди (5 минути) ги балансираат брзината на откривање и процесорот.", + "doc": "Колку често тековниот циклус се совпаѓа со профилите. Стандардно 300 с (5 мин).", "label": "Интервал за совпаѓање" }, "profile_match_max_duration_ratio": { - "doc": "Максимална должина на циклусот во однос на профилот. 1.3 значи дека циклусот мора да биде под 130% од времетраењето на профилот за да се совпадне.", + "doc": "Програмата отпаѓа кога циклусот трае подолго од овој множител на нејзиното вообичаено траење. 1,8: циклус 80% подолг од програмата повеќе не се совпаѓа со неа.", "label": "Макс. сооднос на траење" }, "profile_match_min_duration_ratio": { - "doc": "Минимална должина на циклусот во однос на профилот. 0,9 значи дека циклусот мора да биде најмалку 90% од времетраењето на профилот за да се совпадне.", + "doc": "Програмата се разгледува дури откако циклусот ќе помине толкав дел од нејзиното вообичаено траење. 0,10: програма од 2 часа станува кандидат по 12 минути. Не важи во првите 15 минути од циклусот.", "label": "Мин. сооднос на траење" }, "profile_match_threshold": { - "doc": "Потребен е минимален резултат на сличност (0-1) на крајот на циклусот за да се прифати идентификација на програмата. Подигнете го за да ги намалите погрешните идентификации; намалете го ако програмите на вашата машина не се совпаѓаат. Стандардно 0.4 е конзервативна почетна точка.", + "doc": "Оценка на совпаѓање во живо што му треба на циклусот пред Паметното завршување или завршувањето против брчки да може да го заврши порано. Не ја менува прикажаната програма. Зголемете ако слаби совпаѓања прерано ги завршуваат циклусите; намалете ако јасни совпаѓања го чекаат одложувањето на исклучување.", "label": "Праг за совпаѓање" }, "profile_unmatch_threshold": { - "doc": "Ако совпаѓањето во живо среде циклусот падне под овој резултат, привремената идентификација се брише. Чувајте го малку под Прагот за совпаѓање за краток пад во сличноста да не го врати приказот назад на „без совпаѓање“.", + "doc": "Ако оценката на совпаѓање во живо падне под оваа вредност, тековната претпоставка за програмата се брише. Држете малку под Прагот за совпаѓање, за краток пад да не ја ресетира.", "label": "Праг за несовпаѓање" }, "progress_reset_delay": { - "doc": "По завршувањето, задржете го напредокот на 100% толку долго, така што „Завршено“ е видливо на контролните табли пред да се ресетира на „Неактивен“.", + "doc": "По циклусот држете го напредокот на 100% толку долго пред ресет на „Неактивен“.", "label": "Одложување на ресет на напредок" }, "pump_stuck_duration": { - "doc": "Секунди пумпата може да работи непрекинато пред да биде означена како евентуално заглавена (го активира настанот на заглавената пумпа).", + "doc": "Колку долго пумпата смее да работи без пауза пред да се активира настанот за заглавена пумпа.", "label": "Времетраење за заглавена пумпа" }, "sampling_interval": { - "doc": "Очекувано време помеѓу отчитувањата на сензорот - се користи за правилна големина на прозорецот за измазнување и правилно започнување на отскокнување. Секое ажурирање на сензорот се доловува без оглед на оваа вредност; ги калибрира само пресметките низводно. Моторот за предлози ја мери вистинската каденца на вашиот сензор од минатите циклуси и го поставува тоа автоматски.", + "doc": "Најкратко време меѓу обработените читања. Читањата што стигнуваат порано се прескокнуваат (ниските читања за време на циклус секогаш се чуваат). Поставете го интервалот на јавување на приклучокот или помалку; повисока вредност само отфрла читања.", "label": "Интервал на земање примероци" }, "save_debug_traces": { - "doc": "Складирајте ја целосната трага на енергија и соодветните податоци за отстранување грешки за секој циклус. Корисно за решавање проблеми, но ја зголемува големината на складирањето.", + "doc": "Со секој циклус зачувувајте ги и податоците за дебагирање на совпаѓањето (оценките на сите кандидати), за решавање проблеми. Додава околу 20 KB по циклус.", "label": "Зачувај траги за дебагирање" }, - "smoothing_window": { - "doc": "Колку е измазнет сигналот за сирова моќност. Ниско (2) реагира, но бучно; високото (5) ги измазнува шила, но додава заостанување.", - "label": "Прозорец за изглавнување" - }, "start_duration_threshold": { - "doc": "Напојувањето мора да остане над прагот на стартување толку долго за да се потврди вистинскиот почеток, спречувајќи ги прекинувачите за вклучување/исклучување во дел од секундата да започнат циклус.", + "doc": "Моќноста мора толку долго да остане над Прагот за почеток пред циклусот да започне, за кратките вклучувања и исклучувања да се игнорираат.", "label": "Времетраење на старт" }, "start_energy_threshold": { - "doc": "Енергија (моќност x време) апаратот мора да троши пред да РАБОТИ. Краток скок со висока моќност има многу мала енергија и се игнорира, спречувајќи лажни стартови.", + "doc": "Енергија што апаратот мора да ја потроши пред циклусот да се смета за активен. Краток скок на моќноста троши малку енергија, па не започнува циклус.", "label": "Старт енергија" }, "start_threshold_w": { - "doc": "Моќта мора да се искачи над ова ниво за да потврди дека циклусот започнал. Премногу ниско поставување предизвикува лажни стартувања од напојувањето во мирување; се пропуштаат премногу високи програми и бавно стартување (ладно полнење). Предлог моторот го поставува ова веднаш над забележаната најниска активна моќност на машината.", + "doc": "Моќноста мора да порасне над оваа вредност за да започне циклус. Прениско: потрошувачката во мирување предизвикува лажни стартови. Превисоко: програмите што бавно стартуваат (ладно полнење) се пропуштаат.", "label": "Праг за почеток" }, "stop_threshold_w": { - "doc": "Напојувањето мора да падне под ова ниво пред да започне одбројувањето за исклучување. Поставете го под прагот за почеток - јазот меѓу нив е лентата за хистереза што го спречува треперењето. Ако е поставено премногу високо, фазите со мала моќност (држење на плакнење, против туткање) лажно ја активираат крајната низа.", + "doc": "Моќноста мора да падне под оваа вредност пред да почне одбројувањето на одложувањето на исклучување. Држете под Прагот за почеток. Ако е превисоко, фазите со ниска моќност (паузи при плакнење, против брчки) по грешка го пуштаат одбројувањето на крајот.", "label": "Праг за стоп" }, "switch_entity": { - "doc": "Изборниот прекинувач се исклучува при пауза и повторно се вклучува при продолжување, што се користи заедно со „Паузата исто така го намалува напојувањето“.", + "doc": "Опционален прекинувач што се исклучува при пауза и повторно се вклучува при продолжување, заедно со „Паузата исто така ја исклучува моќноста“.", "label": "Ентитет на прекинувач" }, "watchdog_interval": { - "doc": "Колку често чуварот на заднина проверува дали има застојани сензори и изминати тајмаути. Стандардно 30 секунди.", + "doc": "Колку често позадинската проверка бара заглавени сензори и истечени временски ограничувања. Стандардно 30 с.", "label": "Интервал на чувар" }, "notify_milestone_message": { @@ -1526,131 +1487,86 @@ "label": "Порака за пресвртница" }, "notify_milestones": { - "doc": "Броеви на циклуси одделени со запирки што активираат еднократно честитачко известување кога ќе се достигнат (пр. 50, 100, 500, 1000). Празно ги оневозможува известувањата за пресвртници.", + "doc": "Броеви на циклуси што активираат еднократно известување со честитка, одделени со запирки (на пр. 50, 100, 500, 1000). Празно ги исклучува.", "label": "Пресвртници на циклуси" }, "notify_quiet_end_hour": { - "doc": "Крај на прозорецот „не вознемирувај“ (0-23). Задржаните известувања се испорачуваат во овој час. Оставете празно за да се оневозможи.", + "doc": "Крај на прозорецот „не вознемирувај“ (0-23); задржаните известувања тогаш се праќаат. Празно го исклучува.", "label": "Крај на тивки часови" }, "notify_quiet_start_hour": { - "doc": "Почеток на прозорецот „не вознемирувај“ (0-23). Известувањата за завршување, потсетниците и известувањата за чиста облека што би се активирале за време на тивките часови се задржуваат и се испорачуваат кога прозорецот ќе заврши. Оставете празно за да се оневозможи. Поддржува прозорци што ја преминуваат полноќ (пр. почеток во 22, крај во 7).", + "doc": "Почеток на прозорецот „не вознемирувај“ (0-23). Известувањата за завршување, потсетниците и известувањата за чисти алишта во овој прозорец се задржуваат до неговиот крај. Може да премине преку полноќ (на пр. од 22 до 7). Празно го исклучува.", "label": "Почеток на тивки часови" }, "peak_rate_message": { - "doc": "Изборен прилагоден текст за советот за врвна тарифа што се додава на известувањето при старт. Шаблонски променливи: {device}, {price}. Ако е празно, се користи вградената стандардна вредност.", + "doc": "Сопствен текст на советот за врвна тарифа. Променливи на шаблонот: {device}, {price}. Празно: стандардниот текст.", "label": "Порака за врвна тарифа" }, "peak_rate_threshold": { - "doc": "Кога ќе започне циклус и тековната цена по kWh е на или над оваа вредност, додади совет за врвна тарифа на известувањето при старт. 0 или празно го оневозможува советот.", + "doc": "Ако цената по kWh на стартот на циклусот е еднаква на оваа вредност или повисока, додајте совет за врвна тарифа во известувањето за старт. 0 или празно го исклучува.", "label": "Праг за врвна тарифа (по kWh)" }, - "dtw_bandwidth": { - "label": "Толеранција на совпаѓање по форма", - "doc": "Колку модулот за совпаѓање смее временски да го растегне циклусот за да одговара на формата на зачувана програма. Повисока вредност поднесува повеќе варијации во брзината; премногу висока ризикува да совпадне погрешна програма." - }, - "corr_weight": { - "label": "Тежина форма наспроти ниво", - "doc": "Рамнотежа помеѓу формата на кривата и апсолутното ниво на моќност при оценување на совпаѓањето. Повисока вредност дава предност на формата; пониска дава предност на согласувањето на нивото на моќност." - }, - "duration_weight": { - "label": "Тежина на времетраењето", - "doc": "Колку силно се претпочита програма чиешто типично времетраење се совпаѓа со овој циклус." - }, - "energy_weight": { - "label": "Тежина на енергијата", - "doc": "Колку силно се претпочита програма чијашто типична потрошувачка на енергија се совпаѓа со овој циклус." - }, "store_brand": { "label": "Бренд на уредот", - "doc": "Опционално. Брендот на уредот, избран од каталогот на заедницата. Се користи за наоѓање и споделување на соодветни референтни снимки. Оставете празно ако не ги користите онлајн функциите." + "doc": "Опционално. Марката на вашиот апарат од каталогот на заедницата, се користи за наоѓање и споделување референтни снимки. Оставете празно ако не користите онлајн функции." }, "store_model": { "label": "Модел на уредот", - "doc": "Опционално. Моделот на уредот, избран од каталогот на заедницата откако е поставен бренд. Ако вашиот модел не е на списокот, можете да го додадете во каталогот." + "doc": "Опционално. Моделот на вашиот апарат од каталогот на заедницата (прво изберете марка). Ако го нема во листата, можете да го додадете." }, "show_contributor": { "doc": "Прикажи го името на соработникот на профилите преземени од продавницата на заедницата" }, "smart_termination_duration_ratio": { - "doc": "Колку длабоко во очекуваното траење на совпаднатата програма мора да навлезе циклусот пред Паметното завршување да може да го заврши предвреме штом падне моќноста. Очекуваното траење е просекот на програмата, па кај уреди со многу променливо време на работа - машини за перење при студена зимска и топла летна влезна вода, машини за сушење со сензор за влажност, програми што зависат од полнењето - околу половина од сите циклуси завршуваат пократко од тој просек и никогаш не го добиваат брзото завршување, туку завршуваат дури преку резервниот тајмаут со неколку минути задоцнување. Намалете ја оваа вредност (пр. 0.85) кај такви машини за да се активира сепак предвременото завршување; зголемете ја кон 1.0 за поконзервативно однесување. Оставете празно за стандардната вредност (0.98 или 0.99 за мијалници). Може само да заврши циклус порано, никогаш подоцна, и никогаш не се активира при двосмислено или недоволно сигурно совпаѓање.", + "doc": "Дел од просечното траење на програмата што циклусот мора да го достигне пред Паметното завршување да може да го заврши порано. Ако времињата варираат (температура на водата, сушење со сензор, количина алишта), намалете го (на пр. 0,85). Оставете празно за 0,98 (0,99 кај машини за миење садови).", "label": "Сооднос на паметно завршување" }, - "enable_phase_matching": { - "label": "Преостанато време според фазите", - "doc": "Го дели секој активен циклус на фази (загревање, перење, центрифугирање) и го распределува преостанатото време по фази, во комбинација со класичната проценка - потпирајќи се на распределбата по фази на почетокот на циклусот, а на класичната проценка кон крајот. Ова го прилагодува одбројувањето на тоа колку навистина се загрева и работи вашата машина, што е најзабележливо во првата половина од циклусот. Исклучено = само класичната проценка. Влијае само на приказот на преостанатото време; совпаѓањето на програми и откривањето на циклуси остануваат непроменети." - }, - "keep_min_score": { - "label": "Мин. резултат на совпаѓање", - "doc": "Минимален резултат на сличност потребен на кандидатот за да остане во трката на совпаѓање. Стандардната вредност 0,1 е намерно попустлива - прифаќа дури и слаби кандидати и се потпира на подоцнежните фази за наоѓање на најдоброто совпаѓање. Зголемете за порано отфрлање на малку веројатни профили; намалете за проширување на почетниот сет на кандидати." - }, - "dtw_blend": { - "label": "Мешање на деформација", - "doc": "Колку DTW резултатот на порамнување го заменува основниот резултат на Фаза 2. 0 = користи само резултат на Фаза 2, 1 = користи само DTW резултат, 0,5 (стандардно) = еднакво мешање. Зголемете за поголемо потпирање на warp порамнување кога програмите имаат слични нивоа на моќност но различни временски обрасци." - }, - "dtw_ensemble_w": { - "label": "Микс на комбинација DTW", - "doc": "Во комбинираниот DTW режим (стандардниот), тежина на скалиран L1 наспроти изводен DTW (DDTW). 1,0 = само скалиран L1, 0 = само DDTW, 0,7 = стандардно. Скалираната варијанта L1 ги споредува нивоата на моќност; изводната варијанта реагира на промените на моќноста низ времето. Комбинираниот режим ги спојува двете." - }, - "dtw_ddtw_scale": { - "label": "Скала на изводна деформација", - "doc": "Растојание на пол-заситување за оценување со изводен DTW. Резултатот се преполовува кога растојанието DDTW е еднакво на оваа вредност. Помало = поосетливо на разлики во форма. Стандардната вредност 30 е калибрирана за типични траги на моќност на домашни апарати. Влијае само на комбинираниот и ddtw режим на DTW." - }, - "dtw_refine_top_n": { - "label": "Број на прецизирани кандидати", - "doc": "Колку врвни кандидати на Фаза 2 DTW повторно ги оценува. DTW е поскап па се применува само за N најдобри кандидати. Стандардно 5. Зголемете (на 7-9) ако правилниот профил понекогаш достигнува само 4. или 5. место по Фаза 2 - DTW може да го спаси. Намалувањето малку го забрзува совпаѓањето." - }, - "duration_scale": { - "label": "Острина на траење", - "doc": "Острина на казната за несовпаѓање на траење во Фаза 4. Ова е лог-однос при кој резултатот на согласност се преполовува. Помал = построг: несовпаѓањето на траење повеќе го намалува резултатот. Стандардната вредност 0,175 одговара на толеранција на траење од околу 18% при пол-тежина. Употребувајте заедно со Тежината на траење." - }, - "energy_scale": { - "label": "Острина на енергија", - "doc": "Острина на казната за несовпаѓање на енергија во Фаза 4. Помал = построг: несовпаѓањето на енергија повеќе го намалува резултатот. Стандардната вредност 0,25 е намерно попустлива отколку Острината на траење бидејќи енергијата варира со оптоварувањето. Употребувајте заедно со Тежината на енергија." - }, "power_profile_interval_min": { "label": "Интервал на профилот на моќност", - "doc": "Големина на сегментот за атрибутот на сензорот power_profile по профил (рамна низа просечни вати по слот, користена од надворешни планери како EMHASS и tibber_prices). Помали сегменти ги задржуваат кратките врвови на моќноста остри; поголемите ја израмнуваат формата. Стандардно 15 мин. Само за читање; не влијае на откривањето." + "doc": "Големина на интервалот на атрибутот power_profile (просечни вати по слот) што го користат планери како EMHASS и tibber_prices. Помал ги чува кратките скокови; поголем ја измазнува кривата. Стандардно 15 мин. Не влијае на откривањето." }, "notify_live_silent": { "label": "Тивки ажурирања во живо", - "doc": "Само за iOS. Освежувајте го напредокот во живо тивко: секое ажурирање пристигнува без звук и вибрации, со понизок приоритет што штеди батерија и што iOS може накратко да го групира. Првото ажурирање во циклусот (она што ја стартува Live Activity), како и известувањата за почеток и крај, и понатаму се огласуваат како и обично. Исклучете го ова за да добивате сигнал при секое ажурирање." + "doc": "Само iOS. Ажурирајте го напредокот во живо тивко, со приоритет што штеди батерија и што iOS може да го групира. Првото ажурирање и известувањата за старт и завршување сè уште предупредуваат. Исклучено: секое ажурирање предупредува." }, "notify_live_sticky": { "label": "Задржи живото известување при допир", - "doc": "Само за Android. Нека известувањето за напредокот во живо биде постојано (прикачено) за да не се отфрли со допир. Исклучено го задржува стандарното однесување каде допирот го отфрлува." + "doc": "Само Android. Известувањето за напредок во живо останува по допир. Исклучено: допирот го затвора." }, "notify_live_click_action": { "label": "Цел при допир на известувањето", - "doc": "Каде се отвора допирот на известување од овој уред, на Android и iOS. Празно го отвора панелот WashData на овој уред. Внесете патека до контролна табла (на пр. /lovelace/laundry) или целосен URL за да го испратите на друго место, или „none“ за воопшто да нема цел при допир." + "doc": "Што отвора допирот на известувањето на овој апарат. Празно: панелот WashData за овој апарат. Внесете патека до контролна табла (на пр. /lovelace/laundry) или URL, или „none“ за допирот да не отвора ништо." }, "door_opens_at_end": { "label": "Вратата се отвора автоматски на крај", - "doc": "За машини за миење садови чија врата се отвора на крајот на циклусот за сушење (AirDry и слично). Со вклучено, отворање на вратата за време на тековниот циклус повеќе не го паузира засекогаш; наместо тоа, ако вратата остане отворена за времето на задржување подолу, WashData го третира циклусот како завршен. Кратко отворање (додавање предмет) се игнорира. Потребен е ентитет на сензор за врата." + "doc": "За машини за миење садови што на крајот ја отвораат вратата за сушење (на пр. AirDry). Врата отворена за времето подолу го завршува циклусот наместо да го паузира; кратко отворање се игнорира. Потребен е Ентитет на сензор за врата." }, "door_end_dwell_seconds": { "label": "Задржување при отворена врата", - "doc": "Колку долго вратата мора да остане отворена пред WashData да го заврши циклусот, кога е вклучено \"Вратата се отвора автоматски на крај\". Доволно долго за да се игнора брзото додавање сад (стандардно 60 с), доволно кратко за брзо завршување откако машината ја отвора вратата." + "doc": "Кога е вклучено „Вратата се отвора автоматски на крај“: колку долго вратата мора да остане отворена пред циклусот да заврши. Доволно долго за да се игнорира додавање чинија (стандардно 60 с)." }, "profile_evidence_sources": { "label": "Циклуси што обликуваат програма", - "doc": "Кои циклуси се користат за изградба на кривата на моќност на секоја програма и за совпаѓање на завршен циклус со неа. Ако одзначите некој вид, тој престанува да ги обликува вашите програми, но ништо не се брише - циклусите остануваат во списокот Циклуси и сѐ уште можат да се означат или отстранат. Корисно ако не им верувате на увезените податоци. Статистиката не се менува: во неа секогаш се бројат само циклусите што овој уред навистина ги извршил. Одзначувањето на сè се игнорира, бидејќи програма без ниту еден циклус никогаш не би можела да се совпадне." + "doc": "Кои видови циклуси ги обликуваат кривата и совпаѓањето на секоја програма. Отштиклирањето вид не брише ништо; корисно ако не им верувате на увезените податоци. Статистиката секогаш ги брои само циклусите на оваа машина. Отштиклирањето на сите се игнорира." }, "energy_price_dynamic": { "label": "Временски пондериран трошок", - "doc": "Го наплаќа секој циклус по цената што важела во секој момент од неговото работење, наместо по единствената цена што важела кога завршил. Важи само за ентитет на цена (статичната цена не може да се менува). Не бара дополнително поставување: цената се следи во живо додека циклусот трае, а „Обработка на историјата“ во Дијагностика повторно ги пресметува трошоците на постарите циклуси од recorder. Исклучено = класичното однесување, една цена замрзната на крајот." + "doc": "Пресметувајте го секој циклус според цената во секој момент од неговата работа, наместо според цената при завршување. Потребен е ентитет на цена. „Обработка на историјата“ (Дијагностика) ги пресметува постарите циклуси." }, "unload_confirm_entity": { "label": "Ентитет за потврда на истовар", - "doc": "Изборен ентитет чиешто активирање значи дека содржината е извадена, за машини што не можат да имаат сензор за врата. Работи кој било вид: копче Zigbee или NFC, помошник input_button, сензор за движење пред машината, сцена. Секоја промена на состојбата се смета за потврда, освен исклучување и преминување во недостапна состојба. Поставувањето на ова го овозможува потсетникот за истовар и на уред без Ентитет на сензор за врата." + "doc": "Опционално. Кој било ентитет што потврдува дека содржината е извадена, за машини без сензор за врата (копче, input_button, сензор за движење, сцена). Се брои секоја промена на состојбата освен исклучување и недостапност. Го вклучува и потсетникот за истовар." }, "unload_track_without_door": { "label": "Рачно потврди истовар", - "doc": "Извршувајте го потсетникот за истовар на уред без Ентитет на сензор за врата и без Ентитет за потврда на истовар, а истоварот потврдете го од сопствена автоматизација: притиснете го ентитетот копче \"Означи како истоварено\" или повикајте ја услугата ha_washdata.mark_unloaded. Се игнорира кога е поставен Ентитет на сензор за врата, кој веќе го дава тој сигнал." + "doc": "Пуштајте го потсетникот за истовар без сензор за врата или ентитет за потврда. Потврдете го истоварот од сопствена автоматизација со копчето „Означи како истоварено“ или услугата ha_washdata.mark_unloaded. Се игнорира кога е поставен Ентитет на сензор за врата." }, "notify_icon_color": { "label": "Боја на известување", - "doc": "Акцентна боја за известувањата на овој уред, за да се разликуваат машината за перење, машината за сушење и машината за миење садови на прв поглед. iOS ја бои иконата и лентата за напредок на Live Activity на заклучениот екран и во Dynamic Island. Android ја добива истата боја, но што ќе направи со неа зависи од телефонот: некои ја бојат иконата, а многу од нив (особено Samsung One UI) целосно ја игнорираат. И двете платформи разбираат Hex (на пр. #03A9F4). Оставете празно за стандардната боја на платформата." + "doc": "Акцентна боја на известувањата на овој апарат, за машината за перење, машината за сушење и машината за миење садови лесно да се разликуваат. На iOS го пополнува кругот зад иконата и ја обојува лентата на Live Activity. Многу телефони со Android (особено Samsung) ја игнорираат. Користете hex, на пр. #03A9F4. Празно: стандардната боја на платформата." + }, + "share_name": { + "doc": "Прикажете го името на вашата сметка како автор на циклусите што ги споделувате. Исклучено: се споделува без име." } }, "setting_group": { @@ -1678,9 +1594,6 @@ "door_pause": { "label": "Врата и пауза" }, - "dtw_refinement": { - "label": "DTW прецизирање" - }, "duration_gates": { "label": "Порти за времетраење" }, @@ -1757,13 +1670,18 @@ "rebuilding": "Обновување…", "analyzing": "Анализирање…", "resetting": "Ресетирање…", - "reverting": "Враќање…", "trimming": "Скратување…", "splitting": "Разделување…", + "stalled": "Заглавено", "deleting": "Бришење…", "imported": "Увезено", "preparing": "Подготовка…", - "unlabelling": "Отстранување на етикетата…" + "unlabelling": "Отстранување на етикетата…", + "working": "Се обработува…", + "match_uncertain": "Несигурно: {top} или {runner}", + "match_uncertain_single": "Несигурно: можеби {top}", + "match_sure": "~{pct}% сигурност", + "match_uncertain_tip": "WashData сè уште не избрал програма. Бројот покажува колку често водечката претпоставка, {top}, била точна во минатите циклуси при ваква предност. Само информативно." }, "tab": { "advanced": "Напредно", @@ -1794,13 +1712,17 @@ }, "toast": { "catalog_refreshed": "Каталогот на заедницата е освежен", + "task_name_required": "Дајте ѝ име на задачата", + "task_added": "Задачата е додадена", + "task_save_failed": "Задачата не може да се зачува: {error}", + "task_removed": "Задачата е отстранета", + "maint_logged": "Означено како завршено: {task}", "access_saved": "Контролата за пристап е зачувана", "all_wiped": "Сите податоци се избришани", "analysis_complete_none": "Анализата е завршена: нема нови предлози", "auto_label_complete": "Автоматската етикета е завршена", "automation_deleted": "Автоматизацијата е избришана", "correction_submitted": "Исправката е поднесена", - "cycle_deleted": "Циклусот е избришан", "cycle_force_stopped": "Циклусот со сила е запрен", "cycle_labelled": "Циклус означен", "cycle_paused": "Циклусот е паузиран", @@ -1816,11 +1738,9 @@ "group_deleted": "Групата е избришана", "group_name_required": "Името на групата е потребно", "group_saved": "Групата е зачувана", - "import_successful": "Увозот е успешен; интеграција повторно вчитување", "json_required": "Потребни се податоци за JSON", "legacy_removed": "Дејствата за наследство се отстранети", "logs_exported": "Извезени дневници", - "matching_reverted": "Соодветните тежини се вратија на стандардни", "min_2_profiles": "Изберете најмалку 2 профили за група", "ml_training_no_improvement": "Обуката е завршена: основната линија е задржана (без подобрување)", "models_reverted": "Моделите на уредот се вратија на основната линија", @@ -1830,8 +1750,8 @@ "pg_preset_loaded": "Конфигурацијата „{name}“ е вчитана", "pg_preset_saved": "Конфигурацијата „{name}“ е зачувана", "pg_sugg_loaded": "Вчитани {n} предложени вредности - стартувај го песочникот за споредба", - "pg_sugg_ml_loaded": "Вчитани {n} ML калибрирани вредности - стартувај го песочникот за споредба", "phase_name_required": "Потребно е име на фаза", + "phase_reset": "Фазата „{name}“ е вратена на вградената верзија", "phase_updated": "Фазата е ажурирана", "phases_saved": "Фазите се зачувани", "preferences_saved": "Поставките се зачувани", @@ -1878,15 +1798,11 @@ "store_name_required": "Внесете име на профил", "store_import_failed": "Увозот не успеа: {error}", "store_imported": "Увезено во {profile}", - "store_program_required": "Внесете име на програма", "store_no_appliance": "Прво поставете го брендот и моделот на уредот во поставките.", "store_share_failed": "Споделувањето не успеа: {error}", "store_shared": "Споделено во продавницата на заедницата, чека преглед.", "contrib_device_changed": "Додадено во продавницата на заедницата, но избраниот уред се смени додека формуларот беше отворен. Изберете го во поставките.", "appliance_added": "Уредот е додаден, чека одобрување", - "thanks_confirming": "Благодариме за потврдата", - "device_approved": "Одобрено од заедницата", - "rating_saved": "Оцената за квалитет е зачувана", "brand_added": "Брендот е додаден, чека одобрување", "profile_added": "Профилот е додаден, чека одобрување", "saved_except_conflicts": "Зачувано. Поправете ги означените конфликти за да го зачувате останатото.", @@ -1909,11 +1825,50 @@ "odometer_saved": "Вкупниот број циклуси е зачуван", "odometer_save_failed": "Вкупниот број не можеше да се зачува: {error}", "store_pick_profile_share": "Изберете профил во кој да споделите", - "remove_failed": "Отстранувањето не успеа: {error}" + "remove_failed": "Отстранувањето не успеа: {error}", + "analysis_complete": "Анализата е завршена: предлози: {count}", + "analysis_failed": "Анализата не успеа: {error}", + "auto_detect_failed": "Автоматското откривање не успеа: {error}", + "auto_label_failed": "Автоматското етикетирање не успеа: {error}", + "could_not_load_cycle": "Циклусот не може да се вчита: {error}", + "create_failed": "Создавањето не успеа: {error}", + "cycles_deleted": "Избришани циклуси: {count}", + "debug_cleared": "Исчистени траги за дебагирање: {count}", + "discard_failed": "Отфрлањето не успеа: {error}", + "error": "Грешка: {error}", + "export_failed": "Извозот не успеа: {error}", + "import_failed": "Увозот не успеа: {error}", + "invalid_json": "„{key}“ не е валиден JSON - поправете го или исчистете го полето пред зачувување.", + "label_failed": "Етикетирањето не успеа: {error}", + "ml_training_promoted": "Обуката е завршена: активирани модели: {count}", + "pause_failed": "Паузирањето не успеа: {error}", + "phase_created": "Фазата „{name}“ е создадена", + "phase_deleted": "Фазата „{name}“ е избришана", + "profile_created": "Профилот „{name}“ е создаден", + "rename_failed": "Преименувањето не успеа: {error}", + "save_failed": "Зачувувањето не успеа: {error}", + "split_complete": "Поделено на циклуси: {count}", + "start_failed": "Стартувањето не успеа: {error}", + "stop_failed": "Запирањето не успеа: {error}", + "update_failed": "Ажурирањето не успеа: {error}", + "store_download_skipped": "Прескокнати снимки: {n} (премногу кратки, со премногу празнини или веќе се на вашиот уред).", + "store_import_low_quality": "Оваа снимка е премногу кратка, со премногу празнини или неверодостојна за да се увезе.", + "store_import_duplicate": "Оваа снимка веќе е на вашиот уред.", + "device_approved": "Одобрено од заедницата", + "thanks_confirming": "Благодариме за потврдата", + "rating_saved": "Оцената за квалитет е зачувана", + "store_download_partial": "Неуспешно преземени програми: {n}; обидете се повторно подоцна.", + "import_no_restore_point": "Увезено, но не можеше да се зачува точка за враќање, па овој увоз не може да се поништи", + "import_undo_failed": "Увозот не можеше да се поништи: {error}", + "import_undone": "Увозот е поништен: вратени се {profiles} профил(и) и {cycles} циклус(и)", + "auto_label_cancelled": "Автоматското означување е откажано по означувањето на {count} циклуси.", + "auto_label_cancelled_one": "Автоматското означување е откажано по означувањето на {count} циклус.", + "auto_label_cancelled_other": "Автоматското означување е откажано по означувањето на {count} циклуси.", + "auto_label_done": "Автоматското означување заврши: означени се {count} циклуси, неозначени останаа: {skipped}.", + "auto_label_done_one": "Автоматското означување заврши: означен е {count} циклус, неозначени останаа: {skipped}.", + "auto_label_done_other": "Автоматското означување заврши: означени се {count} циклуси, неозначени останаа: {skipped}." }, "suggestion": { - "both_agree": "WashData препорачува", - "calibrated_label": "Калибрирано", "observed_label": "Набљудувано", "impact": { "auto_label_confidence": { @@ -1928,10 +1883,6 @@ "higher": "Затвора само циклуси со значителна потрошувачка на енергија", "lower": "Затвора и пократки или понискоенергетски циклуси" }, - "end_repeat_count": { - "higher": "Бара сигналот за крај да се повтори повеќе пати – посигурно, малку побавно", - "lower": "Завршува по помалку повторувања – побрзо откривање, поголем ризик од лажен крај" - }, "learning_confidence": { "higher": "Учи само од многу сигурни совпаѓања – побавно, но посигурно", "lower": "Учи од повеќе циклуси; моделот може да биде позашумен" @@ -1970,40 +1921,25 @@ } }, "reason": { - "auto_label_confidence": "Доверба на 15-от перцентил за {cycles} автоматски ознаки што корисникот никогаш не ги коригирал ({p15}).", "completion_min_seconds": "Половина од траењето на чист циклус p05 ({minutes} min) низ {cycles} циклуси; ги филтрира лажните циклуси.{excl}", - "dead_zone_batch": "Задржано кратко (медијана на прозорецот на нестабилност при стартување низ {cycles} циклуси = {s}s) за вистинскиот крај да се препознае брзо.", - "dead_zone_from_dip": "Врз основа на ран пад на моќноста откриен на {s}s.", - "end_energy_default": "Препорачана појдовна вредност по стандард за прагот на шум на крајот на циклусот.", - "end_energy_false": "Енергија при лажен крај p95 ({p95}Wh) низ {cycles} циклуси, со долна граница од 0,2 % од медијаната на енергијата на циклусот ({floor}Wh).", - "end_energy_no_false": "Нема лажни краеви низ {cycles} циклуси; со долна граница од 0,2 % од медијаната на енергијата на циклусот ({floor}Wh).", - "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), па минимумот не носи дополнително оптоварување.", + "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": "Задржано што е можно пониско за програмата да се препознае рано во циклусот; праговите на доверба и двосмисленост спречуваат прерани потврди.", "min_off_gap": "Врз основа на {gaps} забележани интервали меѓу циклусите (p05={p05}s). Долна граница на уредот: {floor}s.", "min_off_gap_bridge": "Димензионирано да го премости најдолгиот тивок период во рамките на циклусот: {span}s + 60s резерва, од {spans} премостени интервали низ {cycles} чисти циклуси (останува под вашиот најкраток размак меѓу две последователни вклучувања, кој изнесува {ceiling}s).", "min_power": "40 % од најниската работна моќност p05 ({p05}W) низ {cycles} чисти циклуси, за прагот на исклучување да остане под реалната потрошувачка.{excl}", - "ml_auto_label": "Најниската доверба што моделот за квалитет сепак ја оценил како чиста (p10 од {cycles} чисти циклуси = {p10}).", - "ml_end_repeat": "{false}/{total} циклуси ({pct}%) имале пауза што детекторот за крај ја оценил над 50 %; барај {val} потврда(и) за крај.", - "ml_off_delay": "Паузи потврдени од детекторот за крај: p95 {p95}s + 60s резерва, од {pauses} паузи проверени од моделот низ {cycles} циклуси (долна граница {floor}s).", "no_update_timeout": "Врз основа на забележаната зачестеност на ажурирање (p95={p95}s) * 20 (мин. 60s).", "off_delay_cadence": "Врз основа на забележаната зачестеност на ажурирање (p95={p95}s) * 5", "off_delay_device_floor": "Употребен е безбеден минимум специфичен за уредот {device} ({floor}s).", "off_delay_generic_floor": "Употребен е општ безбеден минимум ({floor}s).", "off_delay_pauses": "Димензионирано да ги надживее вистинските паузи: пауза во рамките на циклусот p95 {p95}s + 60s резерва, од {pauses} паузи низ {cycles} чисти циклуси (долна граница {floor}s).", - "profile_match_threshold": "Доверба p10 на {cycles} точни автоматски ознаки ({p10}); безбедна долна граница за потврда во живо.", - "sampling_interval": "Медијана на интервалот на ажурирање забележана низ {cycles} чисти циклуси ({si}s).{excl}", - "smoothing_window": "Димензионирано да измазни ~30s отчитувања при семплирање од {si}s ({samples} примероци).", - "start_duration": "Задржано кратко (~еден интервал на семплирање од {si}s) за детекцијата да започне што е можно порано, а сепак да ги игнорира поединечните скокови.", - "thr_batch": "Задржано веднаш над најниската работна моќност p05 низ {cycles} циклуси ({p05}W) за стартот да се фати што е можно порано, а прагот на запирање да остане под најниската работна моќност на машината.", - "tol_per_profile": "p75 на варијансата на траењето по профил низ {profiles} профили ({cycles} циклуси); доследните профили не се казнуваат.", - "tol_pooled": "Врз основа на здружената варијанса на траењето на {cycles} неодамнешни означени циклуси (p95 отстапување={dev}).", - "watchdog": "Задржано што е можно пониско додека е безбедно (малку над интервалот на ажурирање p95 од {p95}s и барем 2x интервалот на земање примероци од {median}s, мин. 30s) за застоите брзо да се фатат без лажни запирања." + "standby_floor": "Секој скорешен циклус сè уште трошел {idle}W при завршувањето, затоа прагот под таа вредност никогаш не може да го регистрира исклучувањето на уредот. Задржан е над неа; {cycles} чисти циклуси во евиденцијата никогаш не паднале под него за повеќе од {pause}с.", + "thr_batch": "Малку над најниската работна моќност (p05) низ {cycles} циклуси ({p05}W), за стартовите да се фатат рано, а прагот за стоп да остане под најниската работна моќност.", + "watchdog": "Задржано што е можно пониско додека е безбедно (малку над интервалот на ажурирање p95 од {p95}s и барем 2x интервалот на земање примероци од {median}s, мин. 30s) за застоите брзо да се фатат без лажни запирања.", + "end_energy_floor": "Зголемено до минимумот што произлегува од прагот за стоп ({stop} Вт) во текот на одложувањето на исклучување ({off_delay} с); под него циклусот може да заврши само по резервен пат.", + "max_duration_ratio_default": "Вратено на стандардната вредност: пониска горна граница ја отфрла точната програма секогаш кога циклусот трае подолго од вообичаено.", + "min_duration_ratio_default": "Вратено на стандардната вредност: повисока долна граница спречува циклусот во тек да биде препознаен сè додека не напредне значително." }, "exclusions": { "summary": "Исклучени се {total} погрешно откриени циклуси: {parts}.", @@ -2021,7 +1957,8 @@ "no_power": "без моќност", "no_active_power": "без активна моќност" } - } + }, + "now_value": "(моментално {v})" }, "trend": { "down": "Во тренд надолу", @@ -2029,6 +1966,10 @@ }, "maint": { "bearing_service": "Сервис на лежишта", + "salt": "Дополни сол", + "rinse_aid": "Дополни средство за плакнење", + "lint_filter": "Исчисти го филтерот за мовчиња", + "condenser_clean": "Исчисти го кондензаторот", "descale": "Отстранување на бигор", "drum_clean": "Исчисти го барабанот", "filter_clean": "Исчисти го филтерот", @@ -2036,10 +1977,9 @@ }, "pg_desc": { "dishwasher_end_spike_quiet_release": "Машина за миење садови: секунди неактивност по очекуваното траење пред да се ослободи чекањето за испумпување на крајот од циклусот", - "smart_termination_duration_ratio": "Дел од очекуваното траење на совпаднатата програма што циклусот мора да го достигне пред Паметното завршување да може да го заврши предвреме; намалете го за машини што зависат од полнењето или температурата", - "anti_crease_finalize_ratio": "Дел од очекуваното траење на совпаднатата програма што циклусот мора да го достигне пред завршната опашка на превртување против брчки да смее да се финализира; намалете го кај машини за сушење со долга опашка на превртување, машините за перење оставете ги на стандардната вредност", + "smart_termination_duration_ratio": "Дел од просечното траење на програмата што циклусот мора да го достигне пред Паметното завршување да може да го заврши порано; намалете ако времињата варираат", + "anti_crease_finalize_ratio": "Дел од просечното траење на програмата што циклусот мора да го достигне пред опашката против брчки да може да го заврши; намалете кај машини за сушење, кај машини за перење оставете стандардно", "completion_min_seconds": "Најкратко извршување што се смета за вистински циклус", - "end_repeat_count": "Ниски отчитувања по ред пред завршување", "interrupted_min_seconds": "Кратки циклуси означени како прекинати", "min_off_gap": "Празнина потребна за раздвојување на два циклуси", "off_delay": "Секунди ниска моќност пред крајот на циклусот", @@ -2047,23 +1987,12 @@ "start_threshold_w": "Минимум вати за да се смета за започнато", "stop_threshold_w": "Под ова, машината се смета за исклучена", "anti_wrinkle_enabled": "Апсорбирајте ги импулсите на превртување на барабанот по главната фаза, наместо да ги читате како нови циклуси", - "anti_wrinkle_exit_power": "Моќноста меѓу импулсите мора да падне под ова за режимот против брчки да остане активен", + "anti_wrinkle_exit_power": "Ниво на мирување меѓу импулсите на вртење; има ефект само над Прагот за стоп", "anti_wrinkle_idle_timeout": "Дозволена тишина меѓу два импулса на превртување на барабанот пред режимот против брчки да заврши", "anti_wrinkle_max_duration": "Импулс подолг од ова го завршува режимот против брчки и отвора нов циклус", "anti_wrinkle_max_power": "Импулс над ова го завршува режимот против брчки и отвора нов циклус", - "dtw_bandwidth": "Фаза 3: warp лента на Sakoe-Chiba (0 = DTW исклучен; стандардно 0,2 = 20% од должината на циклусот)", - "corr_weight": "Фаза 2: рамнотежа помеѓу обликот на кривата (корелација) и нивото на моќност (MAE); стандардно 0,45", - "duration_weight": "Фаза 4: колку силно согласноста на траење влијае на конечниот резултат (стандардно 0,22)", - "energy_weight": "Фаза 4: колку силно согласноста на енергија влијае на конечниот резултат (стандардно 0,22)", - "profile_match_min_duration_ratio": "Фаза 1: минимална должина на циклус како дел од траењето на профилот; стандардно 0,1 (10%)", - "profile_match_max_duration_ratio": "Фаза 1: максимална должина на циклус како дел од траењето на профилот; стандардно 1,8 (180%)", - "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)", - "dtw_refine_top_n": "Фаза 3: кандидати кои DTW повторно ги оценува; зголемете на 7-9 ако правилниот профил заземе 4.-5. место (стандардно 5)", - "duration_scale": "Фаза 4: лог-однос каде согласноста на траење се преполовува; помал = построга казна (стандардно 0,175)", - "energy_scale": "Фаза 4: лог-однос каде согласноста на енергија се преполовува; помал = построга казна (стандардно 0,25)", + "profile_match_min_duration_ratio": "Минимална должина на циклус како дел од траењето на профилот; стандардно 0,1 (10%)", + "profile_match_max_duration_ratio": "Максимална должина на циклус како дел од траењето на профилот; стандардно 1,8 (180%)", "curve_preroll_seconds": "Секунди отчитувања од прекинати обиди за стартување пренесени на почетокот на кривата на циклусот; 0 = исклучено" }, "col": { @@ -2071,12 +2000,14 @@ "status_tip": "Исход на циклусот: Завршен (природен крај), Прекинат (нагло паѓање на моќноста), Присилно стопиран (рачно) или Треба преглед (се чека повратна информација).", "date_tip": "Датум и време кога започна циклусот.", "duration_tip": "Вкупно време на извршување на циклусот од почеток до крај.", - "energy_tip": "Вкупно потрошена енергија (kWh). Се пресметува со интегрирање на моќноста во текот на времето.", - "cost_tip": "Трошок за енергија за овој циклус, замрзнат при завршување. Кога е вклучен Временски пондериран трошок, кривата на моќноста се наплаќа по цената што важи во секој момент; во спротивно енергија x единствената цена што важи на крајот. Поставете цена во Поставки за да се пополни.", + "energy_tip": "Вкупно потрошена енергија (kWh), пресметана од моќноста низ времето.", + "cost_tip": "Трошок за енергија на овој циклус, утврден при завршување. Со вклучен Временски пондериран трошок секој момент се пресметува по својата цена; инаку по цената на крајот. Поставете цена во Поставки за ова да се пополни.", "confidence_tip": "Доверба на совпаѓањето на профилот (0–100%). Колку прецизно кривата на моќноста на циклусот се совпадна со идентификуваната програма.", - "health_tip": "ML здравје на циклусот (повисоко = подобро). Кликнете на циклус за да го инспектирате и прегледате.", "flags_tip": "Ознаки на заедницата и показатели за квалитет на податоците", - "cost_dynamic_tip": "Временски пондерирано: наплатено по цената што важела во текот на циклусот (ефективно {price} {cur}/kWh)." + "cost_dynamic_tip": "Временски пондерирано: наплатено по цената што важела во текот на циклусот (ефективно {price} {cur}/kWh).", + "dbg_conf": "Довер.", + "dbg_mae": "MAE", + "dbg_corr": "Корел." }, "stat": { "avg": "просек", @@ -2100,7 +2031,10 @@ "notify_finish": "Ќе се испратеше известување за завршување.", "notify_milestone": "Ќе се испратеше известување за пресвртница.", "notify_held": "Известувањето беше задржано поради тивките часови.", - "finished": "Циклусот достигна крајна состојба и заврши." + "finished": "Циклусот достигна крајна состојба и заврши.", + "match_reverted": "Програмата се врати на откривање: нејзиниот резултат падна далеку под нејзиниот сопствен врв.", + "verified_pause": "Порамнувањето во живо потврди (или укина) пауза во рамките на програмата. Додека паузата трае, циклусот не се завршува.", + "stalled": "Циклусот остана на потрошувачка во мирување подолго отколку што оваа програма некогаш паузира таму, а работата не е завршена. Прикажано како Паузирано (Заглавено); циклусот останува отворен." }, "store": { "search_ph2": "Пребарај по марка или модел…", @@ -2109,11 +2043,7 @@ "programs_count": "Програми: {n}", "browse": "Прелистување", "device": "Уред", - "favorites": "Омилени", - "search_ph": "Пребарување по бренд или модел…", "no_results": "Не се пронајдени соодветни уреди. Обидете се со друго пребарување.", - "n_programs": "{n} програми", - "n_cycles": "{n} циклуси", "no_programs": "Сè уште нема споделени програми за овој уред.", "no_cycles": "Сè уште нема споделени референтни циклуси за оваа програма.", "anon": "анонимен", @@ -2125,19 +2055,21 @@ "merge_target": "Спој во профил", "description": "Опис (опционално)", "contributed_by": "од {name}", + "website": "Веб-страница на продавницата ↗", + "website_tip": "Отвори ја веб-страницата на продавницата на заедницата", + "favorites": "Омилени", "rating_summary": "★ {avg} ({n})", "no_ratings": "Сè уште нема оцени", - "website": "Веб-страница на продавницата ↗", - "website_tip": "Отвори ја веб-страницата на продавницата на заедницата" + "unreachable": "Не може да се поврзе со продавницата на заедницата. Проверете ја врската и обидете се повторно." }, "settings": { "gear": { - "title": "Поставки" + "title": "Преференции" } }, "link": { - "manual": "Прирачник ↗", - "add_appliance": "Додајте го вашиот уред" + "maintenance_supporter": "Maintenance Supporter на GitHub ↗", + "manual": "Прирачник ↗" }, "tip": { "add_brand": "Брендот го нема на списокот? Додајте го во каталогот на заедницата", @@ -2171,6 +2103,12 @@ }, "rebuild": { "envelopes": "Повторно градење на обвивки" + }, + "auto_label": { + "running": "Автоматско обележување на циклуси" + }, + "store_download": { + "running": "Преземање од продавницата" } }, "setup": { @@ -2180,26 +2118,24 @@ }, "phase0": { "washer": "WashData веќе ги открива вашите циклуси. Снимете го првиот циклус за да ги овозможите имињата на програмите и проценките на времето.", - "dishwasher": "WashData набљудува. Машините за миење садови имаат сложени циклуси – снимањето на првиот циклус е изразито препорачано. Ако открит циклус трае предолго, користете го уредувачот на циклуси за скратување пред зачувување како профил.", + "dishwasher": "WashData набљудува. Циклусите на машината за миење садови се сложени, па снимете го првиот циклус. Ако откриениот циклус трае предолго, скратете го во уредувачот на циклуси пред да го зачувате како профил.", "generic": "WashData набљудува. Снимете или означете откриен циклус за да започнете со градење профили." }, "phase1a": { - "labelled": "Добар почеток – вашата прва програма е зачувана. За најчисти податоци, размислете за снимање на следниот циклус со додатокот за снимање." + "labelled": "Добар почеток: вашата прва програма е зачувана. За најчисти податоци снимете го следниот циклус со рачно снимање." }, "phase1b": { - "recorded": "Вашата снимка е зачувана како {profile_name}. Сега снимете или означете ги другите чести програми за да ја проширите покриеноста." + "recorded": "Вашата снимка е зачувана како {profile_name}. Сега снимете ги или означете ги другите чести програми." }, "phase1c": { - "verify": "Имате {count} програми од заедницата. Стартувајте циклус за да проверите дали WashData ги препознава правилно – совпаѓањето ќе се подобрува додека вашиот уред ја гради сопствената историја." + "verify": "Имате програми од заедницата ({count}). Пуштете циклус за да проверите дали WashData го препознава; совпаѓањето се подобрува како уредот гради сопствена историја." }, "phase2": { - "cluster": "WashData видел {count} циклуси кои не одговараат на ниедна зачувана програма – меѓусебно си наликуваат. Сакате ли за нив да создадете нов профил?", + "cluster": "WashData виде слични циклуси ({count}) што не одговараат на ниедна зачувана програма. Да се создаде нов профил за нив?", "unmatched": "Вашиот последен циклус не одговараше на ниедна зачувана програма. Дали е тоа нова програма?" }, "phase3": { - "suggestions": "WashData има препораки за поставки засновани на историјата на вашите циклуси – прегледајте ги за подобрување на точноста на откривање.", - "groups": "Некои од вашите профили изгледаат како иста програма на различни температури. Организирајте ги во група за подобро совпаѓање.", - "phases": "Додадете фази на програмата кон {profile_name} за попрецизни проценки на преостанатото време." + "suggestions": "WashData има предлози за поставки од историјата на вашите циклуси. Прегледајте ги за подобро откривање." }, "phase4": { "healthy": "Овој уред е целосно поставен ({profile_count} профили)." @@ -2212,12 +2148,58 @@ "create_from_cluster": "Создај профил од овие циклуси", "create_profile": "Создај за него профил", "review_suggestions": "Прегледај препораки", - "organise_profiles": "Организирај профили", - "configure_phases": "Конфигурирај фази", "skip_step": "Прескокни го овој чекор", "skip_forever": "Не прикажувај повеќе", "hide_guidance": "Скриј насоки", "show_guidance": "Прикажи насоки" } + }, + "access": { + "edit": "Уредување", + "full": "Целосен", + "inherit": "Наследи", + "none": "Нема (скриено)", + "read": "Читање" + }, + "pg_label_reason": { + "no_winner": "Ниту една програма не одговара", + "below_floor": "Под прагот „Доверба при учење“", + "ambiguous": "Означено како двосмислено", + "margin": "Премногу блиску до втората најдобра програма", + "unknown_profile": "Програмата повеќе не постои", + "not_evaluated": "Не е оценето" + }, + "diagram": { + "accum": "акум.", + "auto": "авто", + "below_off": "под = искл.", + "confirmed": "потврдено", + "energy_counts": "енергијата се брои", + "feedback": "повратна инф.", + "gap_below_min": "јаз под мин. = еден циклус", + "heat_phase": "фаза на загревање", + "held_at_100": "останува на 100%", + "ignored": "се игнорира", + "kept": "задржан", + "max": "макс.", + "min": "мин.", + "minus_tol": "-тол", + "no_match": "нема совпаѓање", + "no_updates": "нема ажурирања", + "off_delay_wait": "одложување на исклучување", + "off_gap": "јаз искл.", + "plus_tol": "+тол", + "profile": "профил", + "reading_interval": "типичен интервал на отчитување", + "sensor_offline": "сензор офлајн: присилно стопирање", + "si": "ИП", + "spike_ignored": "скокот се игнорира", + "spike_ignored_short": "скок: се игнорира", + "start": "почеток", + "stop": "стоп", + "tail_energy_resets": "енергија на крајот над прагот: тајмерот се ресетира", + "thr": "праг", + "too_short": "прекраток", + "tumble_kept": "импулсите на барабанот се задржуваат" } } diff --git a/custom_components/ha_washdata/translations/panel/nb.json b/custom_components/ha_washdata/translations/panel/nb.json index ea6a1af8..32f9b12f 100644 --- a/custom_components/ha_washdata/translations/panel/nb.json +++ b/custom_components/ha_washdata/translations/panel/nb.json @@ -3,6 +3,7 @@ "min": "min", "entity_not_found": "Entitet ikke funnet", "running_long": "Kjører lenger", + "stalled": "Stoppet opp", "remaining": "gjenstår", "progress": "fremdrift", "show_time_remaining": "Vis gjenstående tid", @@ -48,16 +49,16 @@ } }, "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_tip": "Mot slutten blir dette programmet stille i omtrent {mins} min og trekker deretter rundt {watts} W i {secs} s. Sett i {seen} av {measured} målte sykluser.", "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", + "artifact_tip": "{n} avvik oppdaget (f.eks. dør åpnet midt i syklusen) - åpne for å se dem på grafen", + "artifact_tip_one": "{n} avvik oppdaget (f.eks. dør åpnet midt i syklusen) - åpne for å se det på grafen", + "artifact_tip_other": "{n} avvik oppdaget (f.eks. dør åpnet midt i syklusen) - åpne for å se dem på grafen", "auto": "(automatisk oppdaget)", "built_in_tag": "innebygd", "declining": "↘ synkende", "energy_low": "Lavere energi enn vanlig", "energy_spike": "Høyere energi enn vanlig", - "fair_fit": "akseptabel kvalitet", - "fair_fit_tip": "Moderat matchkonsistens – noen sykluser tilordnet denne profilen har lavere konfidensscore. Merk flere sykluser eller registrer profilen på nytt for å forbedre nøyaktigheten.", "feedback_requested": "Tilbakemelding forespurt", "golden_cycle": "Registrert referansesyklus", "duration_outlier": "{n} syklus(er) med avvikende lengde", @@ -66,27 +67,38 @@ "needs_review": "Trenger gjennomgang", "overrun": "Løp lenger enn vanlig", "poor_fit": "dårlig kvalitet", - "poor_fit_tip": "Inkonsekvent matchhistorikk – vurder å gjenoppbygge denne profilen", - "restart_gap_tip": "{n} HA-omstartsgap: strømsporet har et hull", + "restart_gap_tip": "{n} HA-omstartsgap: strømsporet har hull", + "restart_gap_tip_one": "{n} HA-omstartsgap: strømsporet har et hull", + "restart_gap_tip_other": "{n} HA-omstartsgap: strømsporet har hull", "reviewed": "Gjennomgått", "steady": "→ stødig", "underrun": "Ble ferdig raskere enn vanlig", - "using_defaults": "Bruker standardverdier for levering", - "using_tuned": "Bruker avstemte vekter", "overrun_ratio": "({x}x forventet)", "underrun_ratio": "({pct}% av forventet)", - "awaiting_tip": "Venter på godkjenning fra fellesskapet", - "awaiting_n": "Venter på godkjenning · {n} bekreftet", - "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.", - "armed": "(gjelder neste syklus)" + "armed": "(gjelder neste syklus)", + "awaiting_tip": "Venter på godkjenning fra fellesskapet", + "awaiting_n": "Venter på godkjenning · {n} bekreftet", + "awaiting": "Venter på godkjenning", + "approved": "Godkjent", + "matcher_unused": "Ennå ikke gjenkjent i syklusene dine", + "matcher_used": "Gjenkjent i {n} av syklusene dine", + "matcher_used_one": "Gjenkjent i {n} av syklusene dine", + "matcher_used_other": "Gjenkjent i {n} av syklusene dine", + "matcher_used_tip": "Sykluser på dette apparatet som WashData selv har matchet med dette importerte programmet. En import som aldri matcher, passer kanskje ikke til apparatet ditt; du kan fjerne den. Telles lokalt; ingenting sendes til fellesskapsbutikken." }, "btn": { + "log_done_task": "Loggfør som utført: {task}", + "add_task": "Legg til oppgave", + "pg_preset_delete": "Slett forhåndsinnstilling", + "pg_preset_load": "Last inn forhåndsinnstilling", + "pg_preset_load_tip": "Erstatt verdiene i sandkassen med denne forhåndsinnstillingen (de aktive innstillingene berøres ikke)", + "pg_preset_save": "Lagre som forhåndsinnstilling", + "pg_preset_save_tip": "Lagre alle verdiene nedenfor som en navngitt forhåndsinnstilling for denne enheten", "reset_zoom": "Tilbakestill zoom", "set_brand_model": "Angi merke og modell", "refresh_catalog": "Oppdater katalog", @@ -97,12 +109,11 @@ "add_timer": "+ Legg til tidtaker", "apply_all": "Bruk alle", "apply_label": "Påfør etikett", - "apply_set_b": "Bruk sett B", "apply_split": "Påfør Split", "apply_trim": "Bruk beskjæring", "auto_detect_split": "Automatisk gjenkjenning", "auto_label_cycles": "Automatiske etikettsykluser", - "auto_label_cycles_tip": "Tilordne automatisk profilnavn til umerkede sykluser der matchkonfidensen overstiger terskelen", + "auto_label_cycles_tip": "Navngi umerkede sykluser der matchkonfidensen når terskelen.", "cancel": "Avbryt", "clear_debug": "Fjern feilsøkingsdata", "clear_splits": "Tøm", @@ -112,8 +123,6 @@ "convert_to_automation": "Konverter til automatisering", "correct": "Korriger…", "create": "Opprett", - "create_from_cluster": "Opprett profil fra {count} sykluser", - "create_profile": "+ Opprett profil", "delete": "Slett", "delete_group": "Slett gruppe", "delete_group_tip": "Slett kun denne gruppen - medlemsprofilene beholdes", @@ -124,22 +133,19 @@ "done": "Ferdig", "edit": "Rediger", "export": "Eksporter", - "export_json": "Eksporter til JSON", "force_stop": "Tving stopp", "force_stop_tip": "Avslutt gjeldende syklus umiddelbart og marker den som tvangsstoppet", "from_template": "Fra mal ▾", "ignore": "Overse", "import_json": "Importer fra JSON", - "import_overwrite": "Importer (overskriver data)", "inspect": "Undersøk", "label": "Merk", - "load": "Last inn", "load_more": "Last inn mer", "manage": "Administrer", "merge": "Slå sammen", "new_automation": "+ Ny automatisering", "new_group": "+ Ny gruppe", - "new_group_tip": "Grupper nesten identiske profiler (samme form/varighet, forskjellig temperatur eller spinn) slik at matcher pålitelig velger mellom dem", + "new_group_tip": "Grupper nesten identiske profiler (samme form, annen temperatur eller sentrifugering) slik at matchingen kan skille dem.", "new_phase": "+ Ny fase", "new_profile": "+ Ny profil", "new_profile_tip": "Opprett en ny programprofil fra en eksisterende merket syklus eller opptak", @@ -147,36 +153,25 @@ "on_cycle_started": "På syklus startet", "pause_cycle": "Pause", "pause_cycle_tip": "Sett den kjørende syklusen på pause – apparatet fortsetter der det slapp", - "pg_apply_device": "Bruk på enhet", - "pg_autofill": "Autofyll", - "pg_load_calibrated": "Last inn kalibrerte (ML) ({n})", - "pg_load_calibrated_tip": "Laster inn ML-kalibrerte innstillinger som sandkasse-overstyringer", "pg_load_live": "Last inn aktive innstillinger", "pg_load_live_tip": "Les inn integrasjonens gjeldende innstillinger på nytt og forkast alle endringer i sandkassen", "pg_load_suggested": "Last inn forslag ({n})", "pg_load_suggested_tip": "Laster inn auto-tunerens gjeldende forslag som sandkasse-overstyringer", - "pg_preset_delete": "Slett forhåndsinnstilling", - "pg_preset_load": "Last inn forhåndsinnstilling", - "pg_preset_load_tip": "Erstatt verdiene i sandkassen med denne forhåndsinnstillingen (de aktive innstillingene berøres ikke)", - "pg_preset_save": "Lagre som forhåndsinnstilling", - "pg_preset_save_tip": "Lagre alle verdiene nedenfor som en navngitt forhåndsinnstilling for denne enheten", "pg_publish_all": "Lagre {n} i integrasjonen", "pg_publish_one": "Lagre {label} i integrasjonen", - "play": "Spill av", "process": "Behandl", "process_history": "Behandl nå", "process_recording": "Lagre opptak", "process_tip": "Lagre det innspilte sporet som en ny eller eksisterende profil", "rebuild": "Gjenoppbygg konvolutter", "rebuild_envelope": "Gjenoppbygg konvolutt", - "rebuild_tip": "Beregn den forventede effektkonvolutten (min/maks-bånd) for alle profiler fra deres merkede sykluser på nytt – kjør etter merking av nye sykluser eller korrigering av gamle", + "rebuild_tip": "Bygg opp hver profils forventede effektbånd på nytt fra de merkede syklusene. Kjør etter merking eller korrigering av sykluser.", "rec_start_tip": "Begynn å spille inn apparatets effektspor – start rett før du kjører en syklus", "rec_stop": "Stopp", "rec_stop_tip": "Stopp opptaket og behold det fangede sporet for gjennomgang", "record": "Start opptak", "refresh": "Oppdater", "refresh_diag_tip": "Last inn lagringsstatistikk på nytt", - "refresh_logs_tip": "Last inn de nyeste loggpostene på nytt", "refresh_settings_tip": "Last inn innstillinger fra serveren på nytt", "relabel": "Merk på nytt ({count})", "remove": "Fjern", @@ -184,7 +179,6 @@ "reset": "Tilbakestill", "reset_ml_models_tip": "Kast de finjusterte modellene og gå tilbake til de innebygde. WashData kan lære dem på nytt senere.", "reset_to_builtin": "Tilbakestill til innebygde modeller", - "reset_to_defaults": "Tilbakestill til standardinnstillinger", "resume_cycle": "Gjenoppta", "resume_cycle_tip": "Fortsett den pausede syklusen", "revert_settings": "Forkast endringer", @@ -197,8 +191,6 @@ "run_analysis": "🔍 Kjør forslagsanalyse", "run_analysis_tip": "Analyser de innspilte syklusene dine nå og oppdater innstillingsforslag", "run_auto_label": "Kjør Auto-Label", - "run_both": "Kjør A mot B", - "run_simulation": "Kjør simulering", "save": "Lagre", "save_access_control": "Lagre tilgangskontroll", "save_group": "Lagre gruppe", @@ -210,10 +202,8 @@ "save_settings": "Lagre innstillinger", "select": "Velg", "select_all": "Velg alle", - "select_last20": "Siste 20", "select_none": "Tøm", "show_only": "Vis kun", - "skip_setup": "Hopp over oppsett", "split": "Splitt", "stop": "Stopp", "submit_correction": "Send inn rettelse", @@ -222,36 +212,32 @@ "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.", + "unlabel_selected_tip": "Fjern merkingen fra de valgte syklusene. De blir liggende i historikken din, men former ikke lenger denne profilen.", "use": "Bruk", "wipe_all": "Tørk alle data", + "pg_rerun": "Oppdater simulering", "delete_selected": "Slett valgte ({n})", - "group_suggest": "Gruppe {n}: {members}", "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", "import": "Importer", "disconnect": "Koble fra", "connect_github": "Koble til GitHub", - "save_appliance": "Lagre apparat", "share_to_store": "Del i butikken", "share": "Del", - "confirm_appliance": "Bekreft dette apparatet", - "confirmed": "Bekreftet", "browse_community_setups": "Se fellesskapsoppsett", "download_device": "Last ned dette oppsettet", "share_device": "⬆ Del dette apparatet", - "share_device_tip": "Del dette apparatet og dets innspilte referansesykluser til fellesskapsbutikken slik at andre med samme maskin kan ta dem i bruk", + "share_device_tip": "Del dette apparatet og referansesyklusene til fellesskapsbutikken, slik at eiere av samme maskin kan ta dem i bruk.", "share_n": "Del {n} syklus/sykluser", "export_selected": "Eksporter (velg data)", "export_all": "Rask eksport av alt", - "import_raw": "Avansert: erstatt alt fra JSON", "download_export": "Last ned eksport", "analyze_import": "Analyser fil", "import_selected": "Importer valgte", "back": "Tilbake", "mute_suggestion": "Slutt å foreslå denne innstillingen", "reset_muted": "Tilbakestill dempede forslag", + "reset_phase_tip": "Angre endringen din og gjenopprett den innebygde fasen", "import_power_history": "Importer effekthistorikk", "hist_read_recorder": "Les fra Home Assistant", "hist_scan": "Søk etter sykluser", @@ -260,7 +246,12 @@ "save_total_cycles": "Lagre total", "clear": "Tøm", "add_brand": "Legg til merke", - "add_appliance": "Legg til apparat" + "add_appliance": "Legg til apparat", + "retry": "Prøv igjen", + "confirm_appliance": "Bekreft dette apparatet", + "confirmed": "Bekreftet", + "create_profile_from_gap": "Opprett profil", + "undo_import": "Angre siste import" }, "note": { "off_delay": { @@ -268,10 +259,6 @@ } }, "conflict": { - "anti_wrinkle_exit": { - "exit": "Må være under stoppeterskelen ({stop} W); ellers ignoreres anti-krøll-avslutningseffekten", - "stop": "Må være over anti-krøll-avslutningseffekten ({exit} W)" - }, "anti_wrinkle_max": { "max": "Må være over startterskelen ({start} W); ellers omgås anti-krøll-varighetsgrensen", "start": "Må være under maksimal anti-krøll-effekt ({max} W)" @@ -309,14 +296,14 @@ "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." + "power_sensor": "Dette er apparatets Strømsensor, ikke en pris. Sykluser ville blitt beregnet i watt per kWh; tøm dette feltet.", + "energy_sensor": "Dette er apparatets Energimåler, ikke en pris. 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. Tøm dette feltet, eller velg en tariffsensor.", + "unit": "Denne sensoren måler i {unit}, ikke en pris per kWh. Tøm dette feltet, eller velg 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" + "duration": "Må være under Tid uten oppdatering ({to} s) slik at alarmen for fastsittende pumpe kan utløses først", + "timeout": "Må være over Pumpe fastsittingsvarighet ({ps} s) slik at alarmen for fastsittende pumpe kan utløses først" }, "start_dur": { "sampling": "Samplingsintervall overskrider startvarigheten ({sdt} s); enkeltmålingsstøt kan åpne en syklus", @@ -334,10 +321,15 @@ "sampling": "Samplingsintervall bør være høyst halvparten av vaktbikkjeintervallet ({wi} s)" }, "settings_banner": "Innstillingskonflikter: {n}. Sjekk de fremhevede seksjonene og rett dem opp før du lagrer.", - "settings_banner_btn": "Gå til første" + "settings_banner_btn": "Gå til første", + "end_energy": { + "energy": "For streng for Stoppterskel ({w} W) i løpet av Av-forsinkelse ({d} s); syklusen kan bare avsluttes via en reservevei", + "stop": "Sluttenergi ({e} Wh på {d} s) tillater bare {p} W" + } }, "hdr": { - "ab_comparison": "Sammenligning av innstillinger A/B", + "custom_tasks": "Dine egne oppgaver", + "maintenance_supporter": "Trenger du mer?", "access_control": "Tilgangskontroll", "add_maintenance": "Legg til vedlikeholdshendelse", "automation_delete": "Slett denne automatiseringen", @@ -346,28 +338,22 @@ "automation_review_this_cycle": "Gjennomgå denne syklusen", "automations": "Automatiseringer", "clear_debug": "Fjern feilsøkingsspor", - "cycle_simulator": "Syklussimulator", "diagnostics_pref": "Diagnostikk", "display": "Visning", - "dtw_inspector": "DTW-inspektør", "export_import": "Eksporter / Importer", - "getting_started": "Kom i gang", "live_power": "Live effekt", "logs": "Logger", - "logs_diagnostics": "Diagnostikk", "maintenance": "Vedlikeholdstiltak", "maintenance_log": "Vedlikeholdslogg", "maintenance_reminders": "Servicepåminnelser", "manual_recording": "Manuelt opptak", - "manual_recording_tip": "Kjør en syklus med vilje mens WashData spiller inn effektsporet. Klikk Start opptak rett før apparatet starter, Stopp når det er ferdig, og deretter Behandl for å lagre det som en navngitt profil.", + "manual_recording_tip": "Ta opp en syklus med vilje: trykk Start rett før apparatet starter og Stopp når det er ferdig, og deretter Behandl for å lagre den som en profil.", "ml_learned": "Hva WashData har lært", - "ml_matching_tuning": "Programtilpasset finjustering", "ml_settings_card": "Innstillinger", "ml_smart_learning": "Smart læring", "my_preferences": "Mine preferanser", "no_automations": "Ingen automatiseringer refererer til denne enheten ennå.", "panel_settings": "Panelinnstillinger", - "param_sweep": "Parametersveip", "pg_settings_source": "Kilde for innstillingene", "phase_catalog": "Fasekatalog", "playground": "Sandkasse", @@ -376,11 +362,8 @@ "status": "Status", "status_graph": "Statusgraf", "storage_stats": "Lagringsstatistikk", - "test_history": "Test på historikk", "toggle_sidebar": "Veksle Home Assistant-sidepanelet", - "tools_and_data": "Verktøy og data", "wipe_history": "Tørk historikk", - "recommendations": "Anbefalinger ({n})", "pg_detection_params": "Deteksjonsinnstillinger", "pg_outcome": "Simuleringsresultat", "pg_across_cycles": "På tvers av syklusene dine", @@ -388,7 +371,8 @@ "online_account": "Fellesskapsbutikk og nettfunksjoner", "import_power_history": "Importer effekthistorikk", "service_status": "Servicestatus", - "online_account_tab": "Nett & fellesskap" + "online_account_tab": "Nett & fellesskap", + "live_match_debug": "Feilsøking av direkte match" }, "health": { "fair": "Akseptabel profilkvalitet", @@ -396,7 +380,12 @@ "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.", + "task_name": "Oppgave", + "every_cycles": "Hver (sykluser)", + "every_days": "Hver (dager)", + "new_task_name": "Ny oppgave", + "days_since_service": "{since} / {total} dager", + "envelope_position_tip": "Posisjon på det matchede programmets registrerte kurve: {pct} %. Funnet ved å justere syklusen mot profilen, så den stemmer også når en kjøring er lengre eller kortere. Oppdateres mens apparatet er stille, så den kan henge etter.", "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})", @@ -408,10 +397,8 @@ "aria_cycle_chart": "Syklusens effektkurve", "aria_envelope_chart": "Diagram over profilens effektomhylling", "aria_phase_chart": "Diagram for faseredigering", - "aria_playground_chart": "Syklusens effektkurve med flyttbare deteksjonsterskler", "aria_power_chart": "Diagram over effektforbruk", "aria_spaghetti_chart": "Overlagrede effektkurver for sykluser", - "aria_sweep_chart": "Diagram over resultater fra parametersveip", "artifact_anomaly": "Anomali", "artifact_high_power": "Høy effekt", "artifact_interruption": "Avbrudd", @@ -425,155 +412,105 @@ "cohesion_good": "kohesjon {pct} %", "cohesion_low": "⚠ lav kohesjon {pct} %", "compare_profiles": "Sammenlign med profiler", - "concurrency": "Samtidighet", "confidence": "Konfidens", "confidence_threshold": "Konfidensgrense", "correct_duration": "Riktig varighet (min, valgfritt)", "correct_profile": "Riktig profil", - "correlation": "Korrelasjon", "cost": "Kostnad", "create_new_profile": "+ Opprett ny profil...", "current_phase": "Gjeldende fase", - "curves": "Kurver", "cycle": "Syklus", "cycle_date_display": "Visning av syklusdato", - "cycle_health": "Syklushelse", - "cycle_index": "syklusindeks →", - "cycles": "Sykluser", + "cycle_context": "Recorder-historikk rundt syklusen", + "cycle_context_off": "Av", + "cycle_context_minutes": "{min} min", + "cycle_context_series": "Recorder-historikk", + "before_start": "Før start", + "after_end": "Etter slutt", "cycles_count": "Sykluser", "cycles_lc": "sykluser", - "dashed": "stiplet", "date": "Dato", "debug_traces": "Feilsøk spor", - "default": "Misligholde", "default_access_level": "Standardnivå for brukere som ikke er oppført nedenfor", "default_other": "Standard (andre enheter)", "default_tab": "Standardfane når panelet åpnes", - "delta": "Δ", "description": "Beskrivelse", - "dtw_blend_w": "Blandingsvekt", - "dtw_blended": "Blandet poengsum", - "dtw_ddtw": "DDTW-poengsum", - "dtw_l1": "L1-poengsum", - "dtw_matched_profile": "(syklusens samsvarende profil)", "duration": "Varighet", - "duration_agreement": "Varighetssamsvar", - "emphasis": "Vektlegging", "enable_access_control": "Aktiver tilgangskontroll per bruker", "energy": "Energi", - "energy_agreement": "Energisamsvar", - "event_timeline": "Hendelsestidslinje", "event_type": "Hendelsestype", "expected": "Forventet", + "suspected_profile": "{name} (antatt)", + "runner_up_profile": "{name} (nummer to)", "expected_duration": "Forventet varighet (min)", "file_kb": "Fil (kB)", - "final_score": "Endelig poengsum", "from": "Fra", "gap_s": "Gap (s)", "group_name": "Gruppenavn", "head_trim": "Beskjær start (s)", - "health": "Helse", "hide_tabs": "Skjul faner for ikke-administratorer", - "in_use": "I bruk", "json_data": "JSON-data", "label": "Matchet program", "last": "Siste", "last_checked": "Sist sjekket:", "load_from_file": "Last fra fil", - "mae_score": "MAE-poengsum", "manual_duration": "Manuell varighet (min, valgfritt)", "match": "Treff", "members": "Medlemmer", - "metric": "Metrikk", "mode_existing_profile": "Legg til i eksisterende profil", "mode_new_profile": "Opprett ny profil", "models_fine_tuned": "(finjusterte modeller: {count})", - "n_classic_suggestions": "{n} klassisk", - "n_ml_suggestions": "{n} ML", "n_selected": "{n} valgt", "n_tuning_suggestions": "{n} justeringsforslag", - "na": "–", + "n_tuning_suggestions_one": "{n} justeringsforslag", + "n_tuning_suggestions_other": "{n} justeringsforslag", "new_profile_name": "Nytt profilnavn", "notes": "Merknader", - "of": "av", - "outcome": "Utfall", "panel_default_tab": "Standardfane", "panel_language": "Panelspråk", "per_cycle_short": "syklus", - "pg_autofill_tip": "Autofyll område fra gjeldende verdi ({lo}–{hi})", "pg_best_value": "Beste verdi funnet", "pg_current_value": "Gjeldende verdi:", "pg_drag_hint": "Dra linjen på grafen", - "pg_dtw": "DTW", "pg_ev_ending": "Avslutter", "pg_ev_running": "Kjører", "pg_group_advanced": "Spesialtilfeller", "pg_group_detection": "Deteksjonsutløsere", "pg_group_timing": "Tidsregler", "pg_idle": "Inaktiv", + "pg_poor_match": "Dårlig treff", "pg_preset": "Forhåndsinnstilling for sandkassen", "pg_preset_name": "Navn på ny forhåndsinnstilling", "pg_preset_none": "Velg en forhåndsinnstilling…", - "pg_stress_group": "Test av inaktiv avslutning", - "pg_stress_idle_auto": "Auto", - "pg_stress_idle_w": "Inaktivt nivå (W)", - "pg_stress_idle_w_desc": "Overstyr den automatisk oppdagede standby-gulvverdien (la stå tomt for automatisk).", - "pg_stress_toggle": "Test inaktiv avslutning", - "pg_stress_toggle_desc": "Simulerer at apparatet holder seg på standby-forbruket etter at opptaket slutter - viser om og når WashData stopper syklusen.", - "pg_last_run": "Siste kjøring: {date}", - "pg_match_score": "Treffscore", - "pg_pct_of": "{p}% av {total}", - "pg_poor_match": "Dårlig treff", "pg_strong_match": "Sterkt treff", - "pg_tile_ambiguous": "Tvetydig", - "pg_tile_ambiguous_tip": "Sykluser med nesten uavgjort mellom to programmer", - "pg_tile_detected": "Oppdaget", - "pg_tile_detected_tip": "Sykluser der effekten krysset startterskelen lenge nok til å registreres som kjørende", - "pg_tile_matched": "Matchet", - "pg_tile_matched_tip": "Sykluser som korrekt er tilskrevet et kjent program", - "pg_time_left": "Tid igjen", "pg_weak_match": "Svakt treff", "phase_name": "Fasenavn", "name": "Navn", "phase_timeline": "Fasetidslinje", "power": "Effekt", "profile": "Profil", - "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. Velg ett før du starter apparatet, så brukes det så snart neste syklus begynner.", + "program_tip": "Velg hvilket program den gjeldende syklusen matches med. Automatisk gjenkjenning velger beste match. Fest et program når automatisk gjenkjenning tar feil; velges det før start, gjelder det for neste syklus.", "progress": "Framgang", "raw_socket": "Rå strøm", - "read_only": "Skrivebeskyttet", "ref_cycle": "Referansesyklus (valgfritt)", - "refresh_interval": "Oppdateringsintervall (s)", "remaining": "Gjenværende", "reminder_every": "Påminn hver (sykluser)", "remove_label": "- Fjern etiketten -", "rename_profile": "Gi nytt navn til profil", - "replay_duration": "Avspilling", "resulting_profile": "Resulterende profil", "save_mode": "Lagre modus", - "score": "Poengsum", - "score_breakdown": "Poengfordeling", "seconds_from_start": "Sekunder fra start", "select_profile": "Velg Profil", "setting": "Innstilling", - "settings_a": "Innstillinger A (gjeldende)", - "settings_a_short": "A", "settings_advanced": "Avansert", - "settings_b": "Innstillinger B (test)", - "settings_b_short": "B", "settings_basic": "Grunnleggende", "settings_detail_level": "Detaljnivå for innstillinger", - "settings_overrides": "Overstyringer", "show_debug": "Vis direkte match-feilsøkingskort på statussiden (konfidens, tvetydighet, toppkandidater)", "show_expected": "Vis forventet kurveoverlegg (matchet profil, oransje)", "show_raw": "Vis veksler for råe stikkontaktverdier i sanntids effektgraf", "sparkline": "Nylig trend for syklusvarighet", - "stage2": "Trinn 2 – kjernelikhet", - "stage3": "Trinn 3 – DTW", - "stage4": "Trinn 4 – samsvar", "status": "Status", "tail_trim": "Beskjær slutt (s)", "tap_to_dismiss": "trykk på grafen for å lukke", @@ -584,7 +521,6 @@ "ungrouped": "Ugruppert", "unlabelled": "Umerket", "unlabelled_paren": "(umerket)", - "warp_path": "Justeringsbane", "input": "Inndata:", "start": "Start", "end": "Slutt", @@ -609,9 +545,8 @@ "n_shown": ", {n} vist", "cycles_title": "Sykluser ({n})", "pg_detecting": "Oppdager", - "pg_best_marker": "Beste: {v}", "pg_time_left_model": "Tid igjen (modell)", - "pg_time_left_model_tip": "Modellberegnet gjenværende tid (faseberegner + ML-blanding), ikke en statisk nedtelling", + "pg_time_left_model_tip": "Beregnet ut fra programmets faser, ikke en fast nedtelling", "phase": "Fase", "pg_mode_history": "Test på historikk", "pg_ev_detected": "Oppdaget", @@ -651,12 +586,8 @@ "pg_mode_optimize": "Optimaliser", "pg_group_matching": "Programgjenkjenning", "pg_zoom_hint": "dobbeltklikk for å tilbakestille zoom", - "aria_playground_chart2": "Interaktiv effektgraf for syklusen: hold pekeren over for å lese av tid/effekt, rull for å zoome, dra for å panorere", "pg_alert_timeout_end": "Avsluttet ved tidsavbrudd, ikke ved forutsigelse", "pg_alert_indefinite": "Ville kjøre i det uendelige", - "pg_alert_stress_cap": "Sikkerhetsgrense nådd", - "pg_alert_stress_ok": "Inaktiv avslutning: syklus stoppet", - "pg_alert_stress_warn": "Standby-forbruk over stoppterskel", "task_pg_history": "Test på historikk", "task_pg_sweep": "Optimaliser", "task_reprocess": "Behandler på nytt", @@ -666,16 +597,9 @@ "pg_recent_runs": "Nylige kjøringer", "pg_load_run": "Last inn denne kjøringen på nytt", "pg_match_confidence": "Treffsikkerhet", - "pg_envelope_fit": "Envelope-tilpasning", - "pg_envelope_fit_tip": "Hvor tett syklusen holder seg innenfor denne profilens lagrede effekt-envelope, en annen innfallsvinkel enn treffsikkerhet.", "n_imported_note": ", {n} importert", "enable_online": "Aktiver nettfunksjoner", - "store_brand": "Apparatets merke", - "store_model": "Apparatets modell", "anonymous": "Anonym", - "rate_quality": "Vurder kvalitet", - "rate_n": "{n} stjerner", - "approved_only": "Kun godkjente", "adopt_settings": "Ta også over innstillinger", "flags": "Flagg", "include_phase_map": "Inkluder fasekart", @@ -692,7 +616,6 @@ "cat_custom_phases": "Egendefinerte faser", "cat_profile_groups": "Profilgrupper", "cat_settings": "Deteksjons- & gjenkjenningsinnstillinger", - "cat_matching_config": "Finjustering av gjenkjenning", "cat_ml_models": "ML-modeller", "cat_feedback": "Tilbakemelding & vurderingsmerker", "cat_suggestions": "Forslag", @@ -715,7 +638,6 @@ "hist_csv_data": "CSV-data", "hist_from_recorder": "Eller les den fra Home Assistant", "hist_since": "Siden", - "days": "dager", "hist_keep": "Behold denne syklusen", "hist_looks_complete": "komplett", "peak_power_short": "Topp", @@ -735,7 +657,34 @@ "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" + "correct_total_cycles": "Korriger totalen", + "hist_reason_already_recorded": "overlapper en syklus WashData allerede har registrert", + "share_consent": "Jeg bekrefter at disse syklusene ble fullført normalt uten avbrudd", + "show_internal": "Vis interne innstillinger", + "show_internal_tip": "Innstillinger som WashData justerer for deg (sampling, vokter, samsvarsfrekvens, terskler). Vises når denne enheten har sin egen verdi, en konflikt eller et forslag.", + "rate_n": "{n} stjerner", + "rate_quality": "Vurder kvalitet", + "pg_autolabel": "Auto-merking", + "pg_label_review": "Ber om gjennomgang", + "pg_ev_match_reverted": "Treff tilbakestilt", + "pg_ev_verified_pause": "Bekreftet pause", + "n_samples": "{n} målinger", + "n_samples_one": "{n} måling", + "n_samples_other": "{n} målinger", + "n_cycles": "{n} sykluser", + "n_cycles_one": "{n} syklus", + "n_cycles_other": "{n} sykluser", + "energy_per_cycle": "{v}/syklus", + "energy_total": "{v} totalt", + "automation_off": "(av)", + "ref_cycle_none": "Ingen", + "share_name": "Vis navnet mitt på det jeg deler", + "pg_compare_profile": "Sammenlign med profil", + "pg_obj_endlag": "Sluttforsinkelse (median)", + "pg_obj_early": "Rate for tidlig slutt", + "pg_obj_split": "Oppdelingsrate", + "pg_sweep_cycles": "Siste sykluser", + "cat_backfill_cycles": "Sykluser funnet i importert effekthistorikk" }, "log": { "all_levels": "Alle nivåer", @@ -745,11 +694,9 @@ }, "ml": { "builtin_models": "● Bruke innebygde modeller", - "fit_fair": "Akseptabel", "fit_good": "God", "fit_slight": "Litt", "fit_strong": "Sterk", - "fit_weak": "Svak", "fit_word": "tilpasning", "personalized": "● Personlig tilpasset denne maskinen", "better_than_baseline": "{pct}% bedre enn grunnlinjeestimatet ({metric})", @@ -757,30 +704,39 @@ "trend_declining_tip": "Denne modellens tilpasning har blitt dårligere ved de siste ny-sjekkene. Å gjennomgå flere sykluser kan hjelpe den å lære på nytt.", "trend_steady_tip": "Denne modellens tilpasning har holdt seg omtrent stabil ved de siste ny-sjekkene.", "cap_blurb": { - "end": "Å vite når en syklus faktisk er ferdig", - "live_match": "Å identifisere det pågående programmet raskere", - "quality": "Å oppdage feildetekterte eller ødelagte sykluser", - "remaining_time": "Å forutsi hvor lang tid som er igjen", "total_energy": "Å forutsi total energi og kostnad" }, "cap_label": { - "end": "Deteksjon av syklusslutt", - "live_match": "Programgjenkjenning", - "quality": "Kvalitetssjekk av syklus", - "remaining_time": "Estimat av gjenstående tid", "total_energy": "Energiestimat" }, - "metric_auc": "AUC {auc} på valideringsdata", - "metric_mae": "feil {model} mot baseline {baseline}" + "metric_mae": "feil {model} mot baseline {baseline}", + "fine_tuned_at": "finjustert {when}", + "never": "aldri", + "trained_unknown": "på et ukjent tidspunkt", + "held_out_count": "målt på {count} tilbakeholdte valideringssykluser", + "held_out_count_one": "målt på {count} tilbakeholdt valideringssyklus", + "held_out_count_other": "målt på {count} tilbakeholdte valideringssykluser", + "last_run_not_promoted": "Siste sjekk ({when}): {reason}", + "skip_reason": { + "insufficient_rows": "Ikke nok brukbare sykluser ennå: {count} merkede, rene sykluser så langt.", + "insufficient_rows_one": "Ikke nok brukbare sykluser ennå: {count} merket, ren syklus så langt.", + "insufficient_rows_other": "Ikke nok brukbare sykluser ennå: {count} merkede, rene sykluser så langt.", + "fit_failed": "Treningen mislyktes: {error}", + "unknown": "Ingenting nytt ble lært.", + "holdout_too_small": "For få sykluser til å teste en ny modell rettferdig: {held_out} kunne settes av til testing, {min} trengs.", + "not_better_than_naive": "Den nye modellen var ikke tydelig bedre enn det enkle anslaget (feil {model} mot {naive}).", + "not_better_than_incumbent": "Den nye modellen var ikke bedre enn den som er i bruk (feil {model} mot {incumbent}), så den nåværende ble beholdt." + } }, "modal": { + "remove_task_title": "Fjern oppgave", + "remove_task_msg": "Fjerne påminnelsen \"{task}\"? Oppføringene i vedlikeholdsloggen beholdes.", "auto_label": "Automatiske etikettsykluser", "correct_feedback": "Korriger tilbakemelding", "create_profile": "Opprett profil", "cycle": "Syklus", "edit_group": "Rediger profilgruppe", "edit_phase": "Rediger fase", - "import_config": "Importer konfigurasjon", "label_cycle": "Merk syklus", "merge_cycles": "Slå sammen {n} sykluser", "new_group": "Ny profilgruppe", @@ -795,6 +751,8 @@ "delete_automation_title": "Slett automasjon", "delete_automation_msg": "Slette automasjonen \"{name}\" fra Home Assistant? Dette kan ikke angres.", "remove_legacy_title": "Fjern eldre handlinger", + "reset_phase_msg": "Angre endringen din av \"{name}\" og gjenopprette den innebygde fasen? Programmer beholder områdene sine.", + "reset_phase_title": "Tilbakestill fase", "remove_legacy_msg": "Fjerne de eldre egendefinerte handlingene? De vil ikke lenger utløses ved syklushendelser. Dette kan ikke angres fra panelet.", "discard_recording_title": "Forkast opptak", "discard_recording_msg": "Forkaste det lagrede opptaket? Dette kan ikke angres.", @@ -815,63 +773,77 @@ "store_share_device": "Del dette apparatet", "export_select": "Eksport - velg data", "import_wizard": "Import - velg data", - "history_import": "Importer effekthistorikk" + "history_import": "Importer effekthistorikk", + "apply_suggestions_title": "Bruk forslag", + "apply_suggestions_msg": "Endre disse {n} innstillingene? Integrasjonen lastes inn på nytt etter lagring.", + "discard_settings_title": "Forkaste ulagrede endringer?", + "discard_settings_msg": "Ulagrede innstillingsendringer: {n}. Forlate Innstillinger og forkaste dem?", + "profile_title": "Profil · {name}", + "undo_import_msg": "Denne enhetens data og innstillinger går tilbake til slik de var før siste import ({when}). Sykluser og endringer siden da går tapt.", + "undo_import_ok": "Angre import", + "undo_import_title": "Angre siste import" }, "msg": { + "custom_task_limit": "Du har nådd grensen på {max} oppgaver.", + "custom_tasks_intro": "Legg til alt annet du vil bli påminnet om. En oppgave forfaller når et av intervallene er nådd; la det ene stå tomt eller på 0 for å bare bruke det andre.", + "maintenance_due_sensor_hint": "Den binære sensoren Vedlikehold forfaller er på så lenge en oppgave forfaller, til dine egne varslingsautomasjoner.", + "maintenance_supporter_intro": "Maintenance Supporter er en egen integrasjon som gjør mye mer: hvilket som helst apparat eller oppgave, intervaller etter tid, bruk eller en sensor, varsler og historikk. Den kan bruke syklustelleren i WashData.", "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.", + "pg_preset_delete_confirm": "Slette forhåndsinnstillingen \"{name}\"?", + "pg_preset_limit": "Grensen for forhåndsinnstillinger er nådd ({n})", + "pg_preset_overwrite": "Overskrive forhåndsinnstillingen \"{name}\"?", "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.", - "refresh_catalog_hint": "Fellesskapets lister over merker og apparater mellomlagres for at fellesskapsbutikken skal holde seg innenfor sin daglige grense. Oppdater for å hente bidrag som andre har lagt til eller godkjent.", + "store_sibling_hint": "Ingenting delt for modellen din? En beslektet modell fra samme merke er vanligvis en god start.", + "store_declare_appliance": "Fortell WashData hvilket apparat du har, for å se oppsett andre har delt for det. Eller skriv et merke ovenfor for å se deg rundt.", + "refresh_catalog_hint": "Fellesskapets lister over merker og apparater mellomlagres for å spare butikkens daglige budsjett. Oppdater for å se oppføringer som andre har lagt til eller godkjent.", "head_trim_hint": "Fjern så mange sekunder fra starten", "appliance_monitor": "Apparatmonitor", "artifact_dip_detail": "Falt under det vanlige effektbåndet i ~{n}s.", "artifact_footer": "Fremhevet på grafen over. Dette er forbigående artefakter (f.eks. døren åpnet midt i syklusen), ikke nødvendigvis problemer.", "artifact_header": "{n} uregelmessigheter oppdaget i løpet av denne syklusen", - "artifact_pause_detail": "Effekten falt til nær null i ~{n}s og gjenopptok deretter – sannsynligvis ble døren åpnet midt i syklusen eller syklusen ble satt på pause.", + "artifact_header_one": "{n} uregelmessighet oppdaget i løpet av denne syklusen", + "artifact_header_other": "{n} uregelmessigheter oppdaget i løpet av denne syklusen", + "artifact_pause_detail": "Effekten falt til nær null i ~{n}s og kom deretter tilbake. Døren ble sannsynligvis åpnet, eller syklusen ble satt på pause.", "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 fjern deretter merkingen deres (de blir liggende i historikken, men påvirker ikke lenger denne profilen), eller slett dem helt.", + "cleanup_intro": "Alle merkede sykluser lagt over hverandre. Kryss av for uteliggere, og fjern deretter merkingen (de blir i historikken, men former ikke lenger denne profilen), eller slett dem.", "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)", - "compare_profiles_tip": "Legg over andre profilkonvolutter på diagrammet ovenfor for å se hvilken som passer best til denne syklusen.", + "compare_profiles_tip": "Legg andre profiler over grafen for å se hvilken som passer best til denne syklusen.", "compare_selected_cycles": "Valgte sykluser (fast) – vis / skjul", - "consider_new_profile": "Vurder å opprette en ny profil.", - "coverage_gap": "nylige sykluser har ingen samsvarende profil ({pct}% av siste 30).", - "coverage_gap_similar_cycles": "{count} like umerkede sykluser funnet – opprett en profil for å begynne å matche dem.", "cycles_deleted": "{count} syklus(er) slettet", + "cycle_context_loading": "Laster inn recorder-historikk…", + "cycle_context_note": "Grå: strømsensorens recorder-historikk rundt denne syklusen. Den er ikke en del av syklusen og påvirker ikke varighet, energi eller matching.", + "cycle_context_none": "Recorderen har ingen målinger rundt denne syklusen (Home Assistant beholder 10 dager med historikk som standard).", + "cycle_context_tip": "Bare visning: viser strømsensorens recorder-historikk rundt denne syklusen, hvis den fortsatt finnes. For å legge mislykkede startforsøk til fremtidige sykluser, bruk Kurveopptakt (Innstillinger > Deteksjon > Syklusstart), som endrer lagrede varigheter.", "enough_data": "Nok data til å lære av ({current}/{min} sykluser).", "export_description": "Velg nøyaktig hvilke profiler, sykluser, innstillinger og mer som skal eksporteres til JSON, eller analyser en fil og importer bare delene du vil ha.", "feedback_cycles_pending": "Til gjennomgang: {n}", "feedback_prompt": "Bekreft at det var riktig, korriger programmet eller ignorer.", "feedback_relabel_hint": "Å merke denne syklusen på nytt løser også gjennomgangen.", "filter_by_profile": "Filtrer etter profil…", - "group_modal_help": "Grupper programmer med samme form som varierer i temperatur/spinn (varigheten kan variere). Matching scorer gruppen som én kandidat, og velger deretter det best passende medlemmet. Velg minst 2; overlegget viser hvor like de er.", + "group_modal_help": "Grupper programmer med samme form som skiller seg i temperatur eller sentrifugering. Matchingen scorer gruppen som én og velger deretter det best passende medlemmet. Velg minst 2; overlegget viser hvor like de er.", "group_not_cohesive": "Disse profilene er ikke like nok til å gruppere pålitelig, så matching behandler dem individuelt inntil du fjerner uteliggeren eller deler gruppen.", "group_preview_hint": "Kryss av 2 eller flere medlemmer for å forhåndsvise og sammenligne effektkurvene.", - "import_intro": "Last inn en eksportert fil eller lim inn en JSON-nyttelast nedenfor.", - "legacy_actions_title": "{count} eldre tilpasset handling kjører fortsatt", + "legacy_actions_title": "{count} eldre tilpassede handlinger kjører fortsatt", + "legacy_actions_title_one": "{count} eldre tilpasset handling kjører fortsatt", + "legacy_actions_title_other": "{count} eldre tilpassede handlinger kjører fortsatt", "live_chart_loading": "Direkte strømdiagram vises når avlesningene kommer.", "loading": "Laster inn …", "loading_curve": "Laster inn kurven …", "loading_settings": "Laster inn innstillinger …", "log_buffer_hint": "Nyeste først · bufre de siste 500 ha_washdata-postene siden omstart · dra den nederste kanten for å endre størrelse.", - "maintenance_advisory": "Økende varighet/energi kan indikere nødvendig vedlikehold av apparatet (f.eks. avkalking, filterrengjøring).", "maintenance_due": "Vedlikehold forfaller: {items}", - "maintenance_intro": "Loggfør vedlikeholdet du utfører på dette apparatet, og få påminnelser når hver oppgave forfaller igjen.", + "maintenance_intro": "Loggfør service på dette apparatet, og få påminnelse når hver oppgave forfaller igjen.", "maintenance_load_error": "Kunne ikke laste vedlikeholdsdata: {error}", "maintenance_requires_access": "Vedlikehold og eksport/import krever full tilgang.", - "manual_duration_hint": "Profilens gjennomsnittlige/forventede sykluslengde, brukt for gjenværende tidsanslag. Rediger for å angi det; ved å forlate den uendret beholder den gjeldende verdien.", - "matching_tuning_intro": "Ved læring justerer WashData også hvor mye programtilpasning veier form kontra varighet og energi.", + "manual_duration_hint": "Profilens forventede sykluslengde, brukt til anslag for gjenværende tid. La den stå uendret for å beholde gjeldende verdi.", "merge_intro": "De valgte syklusene er kombinert til én (kronologisk rekkefølge; hull fylt med 0 W). Velg den resulterende profilen.", "ml_intro": "WashData leveres med smarte modeller som fungerer ut av esken.", "ml_learned_intro": "Modeller finjustert til denne maskinen.", - "ml_loading": "laster inn ML...", "ml_settings_intro": "To uavhengige brytere: den ene bruker modellene mens en syklus kjører, den andre lar WashData finjustere dem til maskinen din over tid.", - "name_first_program": "Du har nok sykluser – gi det første programmet et navn for å starte samsvarssøket.", - "near_duplicate_cluster": "nesten duplikatprofilklynge oppdaget. Gruppering lar matching pålitelig velge mellom like-like (f.eks. samme program ved forskjellig temperatur/spinn).", "no_cycles_match": "Ingen sykluser samsvarer med gjeldende filter.", "no_cycles_profile": "Ingen sykluser for denne profilen.", "no_cycles_selected": "Velg minst én syklus først.", @@ -896,73 +868,52 @@ "no_suggestions": "Ingen aktive forslag.", "notify_services_hint": "Bruk {entity}-tjeneste-ID-er (kommaseparert for flere). Malvariabler: {vars}.", "old_actions_warning": "Konfigurert med den gamle handlingsredigereren (nå fjernet). De utløses fortsatt ved syklushendelser, men kan ikke lenger redigeres her. Konverter dem til en vanlig automatisering, eller fjern dem.", - "onboarding_progress": "{n} / 3 sykluser observert", - "onboarding_watching": "Bruk apparatet som normalt – WashData følger med. Etter 3 sykluser starter programsamsvaret.", "pending_feedback": "Venter på tilbakemelding på gjenkjenning", "performance_trend": "Ytelsestrend ({n} sykluser)", - "pg_analysis_empty": "Last inn en syklus for å se treffanalyse.", "pg_apply_confirm": "Bruke beste verdi: {label} = {value}?", - "pg_canvas_empty": "Velg en syklus ovenfor, og trykk deretter Last inn for å se effektkurven.", - "pg_chart_caption": "Deteksjons-/treffrate på tvers av verdier for {param}", "pg_ctrl_intro": "Verdiene tar utgangspunkt i denne enhetens aktive innstillinger i integrasjonen. Endringer blir værende i sandkassen til du lagrer dem i integrasjonen.", "pg_live_unavailable": "Aktive innstillinger er ikke tilgjengelige, viser standardverdier.", "pg_load_live_confirm": "Forkaste {n} endring(er) i sandkassen og laste inn integrasjonens gjeldende innstillinger på nytt?", - "pg_match_rate": "{pct}% treffrate på {total} sykluser", "pg_matches_live": "Samsvarer med de aktive innstillingene", "pg_n_changed": "{n} endret sammenlignet med de aktive innstillingene", "pg_sugg_none": "Alle forslag samsvarer allerede med de aktive innstillingene", - "pg_preset_delete_confirm": "Slette forhåndsinnstillingen \"{name}\"?", - "pg_preset_limit": "Grensen for forhåndsinnstillinger er nådd ({n})", - "pg_preset_overwrite": "Overskrive forhåndsinnstillingen \"{name}\"?", "pg_publish_one_confirm": "Lagre {label} = {value} i innstillingene for denne enheten?", "pg_restart_note": "Start Home Assistant på nytt for å aktivere simuleringsverktøyene.", - "pg_sim_progress": "{done} / {total} sykluser", - "pg_starting": "Starter…", - "pg_stress_synthetic": "inaktiv simulering starter her", - "pg_sweep_intro": "Hva om {param} var annerledes? Test {steps} verdier på tvers av dine siste {cycles} sykluser for å finne innstillingen der flest sykluser blir riktig matchet.", - "pg_sweep_step": "Trinn {done} / {total}", - "pg_verdict_bad": "Trenger oppmerksomhet: mange sykluser blir ikke oppdaget.", - "pg_verdict_good": "Godt innstilt: de fleste sykluser blir riktig identifisert og matchet.", - "pg_verdict_ok": "Akseptabelt: noen sykluser ble oversett. Prøv å senke startterskelen.", "phase_catalog_intro": "Navngitte segmenter av en syklus (forvask, oppvarming, sentrifugering...). Tilordne dem til en profil fra kontrollpanelet.", "phase_ranges_intro": "Faseområder (minutter fra syklusstart) er lagt over gjennomsnittskurven. Rediger verdier for å forhåndsvise live.", - "playground_intro": "En trygg sandkasse for å spille av tidligere sykluser med ulike innstillinger, sammenligne to oppsett side om side og se hvordan en syklus justeres mot en profil. Ingenting her endrer de aktive dataene dine før du uttrykkelig tar det i bruk.", - "playground_needs_restart": "Start Home Assistant på nytt for å aktivere Sandkasse-verktøyene.", - "preferences_admin": "Preferanser, panel og tilgangskontroll", - "preferences_adv": "Preferanser", + "playground_intro": "Prøv innstillinger på de ekte syklusdataene dine. Ingenting her endrer de aktive innstillingene før du tar det i bruk.", "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), 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.", + "process_history_hint": "Kjør matching på nytt for alle lagrede sykluser, oppdater forslag, tren ML-modeller på nytt (hvis aktivert), beregn kostnader på nytt ut fra registrerte priser, og beregn syklushelse på nytt. Kjør det etter en runde med vurderinger.", "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.", - "rbac_hint": "Når den er av, har alle Home Assistant-brukere full tilgang (standard). Administratorer har alltid full tilgang og kan administrere alle.", - "recent_logs": "Nylige ha_washdata-poster", + "rbac_hint": "Av: alle Home Assistant-brukere har full tilgang (standard). Administratorer har alltid full tilgang og kan administrere alle.", "recording_in_progress": "Opptak pågår", "reminders_intro": "Vis en påminnelse i panelet så mange sykluser etter forrige service. La stå tomt eller 0 for å slå av en påminnelse.", "restart_gap_footer": "Fremhevet på grafen. Strømdata mangler for disse intervallene; matching brukte bare faktiske avlesninger.", "restart_gap_header": "{n} HA-omstartsgap under denne syklusen", + "restart_gap_header_one": "{n} HA-omstartsgap under denne syklusen", + "restart_gap_header_other": "{n} HA-omstartsgap under denne syklusen", "restart_gap_item": "{dur} gap", - "review_confirm_help": "Bekreft om denne syklusen ble oppdaget riktig. Dine vurderinger trener modellen på maskinen din - jo flere sykluser du bekrefter, jo bedre matching og helsepoeng får du. En rask god/dårlig er nok.", + "review_confirm_help": "Ble denne syklusen oppdaget riktig? Bekreftede sykluser forbedrer matchingen for maskinen din. En rask God/Dårlig er nok.", "review_in_settings": "Se gjennom i Innstillinger", "review_notes_placeholder": "Merknader (valgfritt)", "review_notes_tip": "Fritekstnotater for din egen referanse. Ikke brukt av matching eller trening.", - "review_profile_tip": "Programmet denne syklusen er merket som. Hvis det automatisk oppdagede programmet var feil, korriger det her - merking lærer samsvar for fremtidige sykluser.", - "review_quality_tip": "Hvor ren denne syklusen er. Bra = et lærebokeksempel på dette programmet; Dårlig = oppdaget, men støyende eller atypisk; Ubrukelig = feiloppdaget (sammenslått, avkortet eller falsk). Driver helsepoeng og hvilke sykluser som er tillatt å trene modellen.", - "review_recorded_tip": "Merk dette som en håndplukket referansesyklus for programmet - samme rolle som en manuelt registrert syklus. Referansesykluser beholdes alltid, setter den matchende malen, og blir aldri droppet ved opprydding. (Dette er det \"gyldne\"/innspilte flagget; begge er det samme.)", - "review_tags_tip": "Valgfrie flagg som beskriver hva som gikk galt med denne syklusen, slik at trening og opprydding kan forklare det.", - "review_to_cycles": "Åpne Cycles review-køen", + "review_profile_tip": "Programmet denne syklusen er merket som. Hvis det er feil, korriger det her; merkinger lærer matchingen.", + "review_quality_tip": "Hvor ren denne syklusen er. God: et lærebokeksempel på dette programmet. Dårlig: oppdaget, men støyende eller uvanlig. Ubrukelig: feiloppdaget (slått sammen, avkortet eller falsk). Påvirker helsepoengene og hvilke sykluser som trener modellen.", + "review_recorded_tip": "Merk dette som en håndplukket referansesyklus for programmet, som en manuelt registrert syklus. Referansesykluser beholdes alltid, danner grunnlaget for matchmalen og fjernes aldri ved opprydding.", + "review_tags_tip": "Valgfrie flagg for hva som gikk galt, slik at trening og opprydding kan ta hensyn til det.", + "review_to_cycles": "Åpne vurderingskøen under Sykluser", "samples_decimated": "Viser {shown} av {total} prøver (tynnet ut for visning; topper beholdes). Et bredt hull her er uttynning, ikke manglende data.", - "saving_triggers_reload": "Lagring utløser en ny integrasjon. HA-enheter kan kort vises som utilgjengelige.", + "saving_triggers_reload": "Lagring laster integrasjonen inn på nytt. Entiteter kan kort vises som utilgjengelige.", "search_placeholder": "Søkeinnstillinger...", "see_recorder": "Se opptaker-widget nedenfor", "setting_changed": "Endret fra {old} til {new} den {date}", "settings_basic_note": "Viser de viktigste innstillingene. Bytt til Avansert for hele listen.", - "shape_drift_advisory": "⚠ Formavvik", - "shape_drift_detail": "Effektmønsteret for denne profilen har endret seg over tid – mulig slitasje på apparatet eller vedlikehold nødvendig (f.eks. avkalking, filterrengjøring).", "show_all_settings": "Vis alle innstillinger", - "showing_suggestions": "Viser {count}-innstilling med forslag.", - "split_intro": "Klikk på grafen for å legge til eller fjerne et delt punkt, eller automatisk oppdage ved tomgangshull. Hvert resulterende segment kan få sin egen profil.", - "storage_diagnostics": "Lagringsstatistikk, vedlikehold, eksport/import", + "showing_suggestions": "Viser {count} innstillinger med forslag.", + "showing_suggestions_one": "Viser {count} innstilling med forslag.", + "showing_suggestions_other": "Viser {count} innstillinger med forslag.", + "split_intro": "Klikk på grafen for å legge til eller fjerne et delingspunkt, eller gjenkjenn automatisk ut fra tomgangspauser. Hver del kan få sin egen profil.", "sug_staged": "Angitt {key} = {val}. Lagre for å bruke.", "toast_auto_detect_enabled": "Autodeteksjon aktivert", "toast_auto_label_complete": "Automatisk merking fullført", @@ -979,7 +930,6 @@ "toast_no_automation": "Kan ikke opprette automatisering her", "toast_profile_name_required": "Profilnavn er påkrevd", "toast_program_set": "Program valgt: {program}", - "toast_rebuild_failed": "Gjenoppbygging mislyktes: {error}", "toast_resume_failed": "Gjenopptaking mislyktes: {error}", "toast_revert_failed": "Tilbakestilling mislyktes: {error}", "toast_reverted": "{key} tilbakestilt", @@ -990,100 +940,83 @@ "trend_energy_up": "Energi på vei opp ({pct} %/syklus) – nylig gjennomsnittlig {avg}", "trim_destructive_confirm": "Kun {pct}% av syklusen beholdes. Dette kan ikke angres. Fortsette?", "trim_intro": "Dra de røde håndtakene, eller skriv inn verdier. Alt utenfor vinduet fjernes.", - "tuning_suggestions_available": "{count} justeringsforslag tilgjengelig fra observerte sykluser. De vises ved siden av de relevante feltene.", + "tuning_suggestions_available": "{count} justeringsforslag tilgjengelige fra observerte sykluser. De vises ved siden av de relevante feltene.", + "tuning_suggestions_available_one": "{count} justeringsforslag tilgjengelig fra observerte sykluser. Det vises ved siden av det relevante feltet.", + "tuning_suggestions_available_other": "{count} justeringsforslag tilgjengelige fra observerte sykluser. De vises ved siden av de relevante feltene.", "unsure_detected_prefix": "WashData er usikker på om det er oppdaget", - "updated": "Oppdatert", "warmup_badge": "Lærer fortsatt ({done}/{needed} sykluser)", - "warmup_detail": "Denne profilen trenger {needed} merkede sykluser før automatisk matching begynner. Hver bekreftede syklus hjelper den med å lære.", + "warmup_detail": "Matchingen fungerer allerede. WashData ber deg bekrefte de første {needed} syklusene for denne profilen før den merker på egen hånd.", "wipe_history_warning": "Slett alle sykluser og profiler permanent. Kan ikke angres.", "compare_overlay_tip": "Legg innlærte profilomhyllinger oppå hverandre for å se hvilket program hver syklus ligner på.", - "duration_trend_up_tip": "Varighet øker ({pct}%/syklus)", - "duration_trend_down_tip": "Varighet synker ({pct}%/syklus)", - "energy_trend_up_tip": "Energi øker ({pct}%/syklus)", - "energy_trend_down_tip": "Energi synker ({pct}%/syklus)", - "performance_trending": "Ytelsestrend", - "pg_select_cycle_hint": "Velg en syklus og klikk på ↺ for å laste inn", - "pg_sweep_progress": "Trinn {done} / {total}", - "pg_analysis_hint": "Velg en syklus og klikk på ↺ for å laste inn analysen.", "diagnostics_load_failed": "Kunne ikke laste inn diagnostikk: {error}", - "manual_duration_ref_hint": "Brukes bare når ingen referansesyklus er valgt. En referansesyklus setter varigheten ut fra sin egen lengde.", + "manual_duration_ref_hint": "Brukes bare uten referansesyklus; en referansesyklus setter varigheten ut fra sin egen lengde.", + "duration_learned_hint": "Beregnet fra dette programmets sykluser (uten uteliggere) og oppdatert etter hver ny syklus. Vil du endre den, kan du beskjære eller fjerne syklusene som avviker.", "toast_convert_rolled_back": "Konverteringen mislyktes og ble rullet tilbake (ingen automatisering ble igjen): {error}", "toast_convert_orphan": "Automatiseringen ble opprettet, men tømming av de gamle handlingene mislyktes. Ikke prøv igjen: fjern de gamle handlingene manuelt for å unngå en duplisert automatisering.", - "advisory_duration_trend_up": "Syklusene for '{name}' blir gradvis lengre (omtrent +{pct}% per syklus). Hvis apparatets oppførsel har endret seg, ta opp eller bygg opp denne profilen på nytt.", - "advisory_energy_trend_up": "'{name}' trekker gradvis mer energi (omtrent +{pct}% per syklus) - verdt å sjekke apparatet hvis det er uventet.", - "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.", + "advisory_duration_trend_up": "Syklusene for '{name}' blir stadig lengre (omtrent +{pct}% per syklus). Hvis apparatet har endret seg, ta opp eller bygg opp denne profilen på nytt.", + "advisory_energy_trend_up": "'{name}' bruker mer energi for hver syklus (omtrent +{pct}% per syklus). Sjekk apparatet hvis det er uventet.", + "advisory_poor_health": "'{name}' passer dårlig: de siste syklusene varierer mye eller matcher svakt. Gå gjennom syklusene, eller ta den opp på nytt slik at matching og anslag forblir nøyaktige.", + "advisory_shape_drift": "'{name}' har drevet bort fra sin opprinnelige effektkurve, kanskje på grunn av kalk eller slitasje. Vurder å ta den opp på nytt med nye sykluser.", + "advisory_shape_drift_corr": "'{name}' har drevet bort fra sin opprinnelige effektkurve (korrelasjon {corr}), kanskje på grunn av kalk eller slitasje. Vurder å ta den opp på nytt med nye sykluser.", + "advisory_unmatchable": "'{name}' kan aldri matches: det finnes ingen syklus med effektdata bak den. Merk eller ta opp en syklus av dette programmet for å fikse det.", + "advisory_unmatchable_no_duration": "'{name}' kan aldri matches: syklusen har ingen brukbar varighet. Ta opp dette programmet på nytt, eller angi forventet varighet.", "pg_no_alerts": "Ingen problemer oppdaget i denne kjøringen.", "pg_override_active": "Bruker dine redigerte innstillinger mot gjeldende", "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.", "pg_sweep_intro2": "Finn innstillingen som best oppfyller et mål på tvers av de siste syklusene dine. Ingenting endres før du tar det i bruk.", - "pg_running_history": "Spiller av syklusene dine på nytt…", - "pg_running_sweep": "Tester verdier på tvers av syklusene dine…", "pg_row_load_hint": "Last inn denne syklusen i grafen ovenfor", - "pg_history_intro2": "Spill av de siste syklusene dine gjennom den ekte detektoren og gjenkjenningen med innstillingene ovenfor. Klikk på en rad for å laste inn den syklusen i grafen; endre en innstilling for å se en før/etter-sammenligning.", + "pg_history_intro2": "Spill av nylige sykluser gjennom den ekte detektoren og matcheren med innstillingene ovenfor. Klikk på en rad for å laste den inn i grafen; endre en innstilling for å sammenligne før og etter.", "pg_simulating": "Simulerer syklus…", - "pg_canvas_empty2": "Velg en syklus ovenfor og trykk på Kjør for å simulere den. Hold deretter pekeren over for å lese av verdier, rull for å zoome, og dra for å panorere.", "pg_apply_settings_confirm": "Lagre disse {n} innstilling(er) på denne enheten? {list}", - "pg_stress_terminated_detail": "Stabiliserte seg på ~{idle}W inaktiv -- syklusen endte {h} t {m} min senere via {reason}.", - "pg_stress_above_threshold_detail": "Standby-forbruket på ~{idle}W er på eller over den effektive stoppterskelen ({stop}W) -- syklusen ble aldri registrert som inaktiv. Øk stop_threshold_w for å rette dette.", - "pg_stress_hit_cap_detail": "Syklusen kjørte {h} t {m} min uten å stoppe -- tvangsstoppet av sikkerhetsgrensen. Standby-forbruk {idle}W mot stoppterskel {stop}W.", "no_logs_match": "Ingen loggoppføringer samsvarer med disse filtrene.", "pg_analysis_hint2": "Velg en syklus og trykk Kjør for å laste inn treffanalysen.", - "pg_analysis_empty2": "Trykk Kjør for å se treffanalysen.", - "imported_readonly": "Importert fra fellesskapsbutikken. Vises for referanse og matching. Telles ikke med i statistikken din og kan ikke redigeres.", + "imported_readonly": "Importert fra fellesskapsbutikken. Brukes som referanse og til matching, telles ikke med i statistikken din og kan ikke redigeres.", "merge_no_imports": "Importerte opptak kan ikke slås sammen med en ekte syklus. Fjern merkingen for å slå sammen.", - "store_enable_hint": "Aktiver nettfunksjoner i Innstillinger for å bla gjennom og importere fellesskapets referansesykluser.", - "advisory_duration_outlier": "{n} syklus(er) merket '{name}' ligger for langt fra programmets vanlige lengde til noen gang å kunne matche det (verst: {ratio}x). De er sannsynligvis feilmerket, eller ett opptak fanget to kjøringer. Merk dem på nytt, eller del dem opp, slik at de slutter å forskyve tidsestimatet for dette programmet.", + "store_enable_hint": "Slå på nettfunksjoner under Preferanser > Nett & fellesskap for å bla gjennom og importere fellesskapets referansesykluser.", + "advisory_duration_outlier": "{n} syklus(er) merket '{name}' ligger for langt fra den vanlige lengden til noen gang å matche (verst: {ratio}x). De er sannsynligvis feilmerket eller inneholder to kjøringer. Merk dem på nytt eller del dem opp, slik at de ikke lenger forskyver tidsanslaget.", "standby_above_stop_title": "Trakk fortsatt {idle} W da syklusen ble avsluttet", "standby_above_stop_sub": "{n} av de siste {total} syklusene. Av-forsinkelsen starter først når effekten er under Stoppterskel ({stop} W), så sykluser avsluttes for sent eller må stoppes for hånd.", - "online_intro": "Del referansesykluser med andre WashData-brukere, og last dem ned. Alle nettfunksjoner er valgfrie og av som standard.", "store_share_intro": "Last opp denne referansesyklusen slik at andre med samme apparat kan bruke den. Den gjennomgås før den vises offentlig.", - "online_intro_global": "Bla gjennom og del referanseopptak med andre WashData-brukere, og bekreft apparatoppføringer. Én tilkobling gjelder for hele WashData-integrasjonen din; apparatets merke og modell angis per enhet under Grunnleggende. Alle nettfunksjoner er valgfrie og av som standard.", + "online_intro_global": "Bla gjennom og del referanseopptak med andre WashData-brukere. Én tilkobling dekker alle WashData-enhetene dine; merke og modell angis per enhet under Grunnleggende. Nettfunksjoner er av til du slår dem på.", "online_unavailable": "Nettfunksjoner er ikke tilgjengelige på denne serveren.", "store_picker_offline": "Aktiver nettfunksjoner i innstillingstannhjulet for å velge fra fellesskapskatalogen.", "pick_brand_first": "Velg et apparatmerke først.", - "model_not_found": "Ikke i katalogen ennå.", - "connect_to_confirm": "Koble til i innstillingstannhjulet for å bekrefte eller vurdere.", "adopt_settings_hint": "Overskriver deteksjons- og matchgrensene for dette apparatet med de delte. Dine varslinger, entiteter og energipris endres aldri.", - "include_settings_hint": "Deler deteksjons- og matchgrensene for dette apparatet (ikke dine varslinger, entiteter eller energipris). Brukere velger selv om de vil bruke dem.", + "include_settings_hint": "Del denne enhetens deteksjons- og matchterskler (ikke varsler, entiteter eller energipris). Andre velger selv om de vil bruke dem.", "onboard_download": "Nytt apparat? Ta i bruk et ferdig oppsett (programmer, referansesykluser og faser) fra en annen WashData-bruker med samme apparat.", - "share_consent": "Jeg bekrefter at disse syklusene ble fullført normalt uten avbrudd", "share_device_none": "Ingen delbare sykluser ennå. Marker en innspilt eller håndplukket syklus som referansesyklus (⭐) under fanen Sykluser først.", "share_guideline_naming": "Gi hver profil nøyaktig det navnet som vises på apparatets dreieknapp eller display (f.eks. 'Cotton 40', 'Eco 60').", - "share_guideline_quality": "Del bare sykluser som ble fullført normalt -- ingen avbrudd midt i syklusen, åpning av dør eller strømbortfall.", + "share_guideline_quality": "Del bare sykluser som ble fullført normalt: ingen avbrudd, døråpninger eller strømbrudd.", "share_guideline_review": "Din opplasting starter som ventende og vises offentlig når nok fellesskapsmedlemmer bekrefter den.", "share_guidelines_title": "Før du deler", - "store_download_device_intro": "Ta over alle delte programmer og deres referansesykluser til apparatet ditt. Dine egne innspilte sykluser og statistikk påvirkes ikke.", + "store_download_device_intro": "Legg til alle delte programmer og referansesyklusene deres på enheten din. Dine egne sykluser og din statistikk påvirkes ikke.", "store_share_device_intro": "Last opp {brand} {model} med referansesyklusene du velger. Andre med samme apparat kan ta i bruk programmene dine. Bidrag gjennomgås før de vises offentlig.", "share_profile_no_cycles": "Ingen referansesykluser -- marker en syklus som ⭐ under fanen Sykluser for å inkludere denne profilen", - "advisory_phase_inconsistent": "'{name}' ser ut til å blande ulike programmer eller temperaturer - syklusene varmer opp i svært ulike tidsrom. Hvis du deler den opp i separate profiler (f.eks. per temperatur), blir samsvar og tidsestimater bedre.", - "advisory_phase_inconsistent_title": "⚠ Muligens blandede programmer", - "export_select_intro": "Kryss av nøyaktig hva som skal inkluderes. Å velge profiler uten syklusene deres eksporterer likevel et gjenkjennbart program (den lærte formen følger med).", - "import_analyze_hint": "Last inn en eksportert fil (eller lim inn JSON-en). WashData analyserer den og viser nøyaktig hva som kan importeres før noe endres.", + "export_select_intro": "Kryss av for det som skal med. Profiler uten syklusene sine eksporterer likevel et program som kan matches.", + "import_analyze_hint": "Last inn en eksportert fil (eller lim inn JSON-en). WashData viser hva som kan importeres før noe endres.", "analyzing": "Analyserer…", - "device_type_mismatch_warn": "Denne eksporten er fra en annen apparattype ({src} vs {local}). Programmer og sykluser kan fortsatt importeres som referansedata, men apparatspesifikke innstillinger og import av ekte historikk er deaktivert.", + "device_type_mismatch_warn": "Denne eksporten er fra en annen apparattype ({src} vs {local}). Programmer og sykluser kan fortsatt importeres som referansedata, men ikke enhetsinnstillinger eller ekte historikk.", "merge_hint": "Importerte elementer legges til; ingenting lokalt går tapt. Navnekonflikter løses nedenfor.", "replace_warn": "Hver avkrysset kategori slettes og erstattes fra filen. Ikke-avkryssede kategorier forblir uendret.", "dest_reference_hint": "Importerte sykluser forbedrer bare programgjenkjenning og påvirker aldri bruks-/energistatistikk.", - "dest_real_history_hint": "Importerte sykluser teller som dette apparatets egen historikk og mater energi-/bruksstatistikk. Bruk dette til å flytte ett apparat til en ny installasjon.", + "dest_real_history_hint": "Importerte sykluser teller som dette apparatets egen historikk og statistikk. Bruk dette til å flytte et apparat til en ny installasjon.", "sug_muted": "Vil ikke foreslå denne innstillingen igjen", "sug_mute_failed": "Kunne ikke dempe forslaget", "sug_unmuted_all": "Dempede forslag tilbakestilt", - "n_suggestions_muted": "{count} dempet; autotuner vil ikke foreslå disse.", + "n_suggestions_muted": "{count} forslag dempet; autotuneren vil ikke foreslå disse.", + "n_suggestions_muted_one": "{count} forslag dempet; autotuneren vil ikke foreslå det.", + "n_suggestions_muted_other": "{count} forslag dempet; autotuneren vil ikke foreslå disse.", "font_size_hint": "Gjør alt i dette panelet større eller mindre. Gjelder for kontoen din på denne enheten.", - "import_history_description": "Hadde du allerede en smartplugg før du tok i bruk WashData? Last opp en historikkeksport fra effektsensoren, eller la den leses rett fra Home Assistant, og den vanlige deteksjonen kjører over den slik at tidligere sykluser dukker opp i Sykluser-listen din, klare til å navngis.", - "hist_input_hint": "Last opp en CSV-fil lastet ned fra Historikk-panelet (entitet, tilstand, sist endret), eller la WashData lese sensorens historikk direkte. Deteksjonen kjører deretter over den akkurat som live, og du velger hvilke av syklusene den finner du vil beholde.", - "hist_recorder_hint": "Leser fra datoen du velger og fram til nå. Home Assistant beholder detaljert historikk i 10 dager som standard og deretter bare timesgjennomsnitt, som er for grove å oppdage sykluser fra - velg en dato lenger tilbake bare hvis recorderen din er satt til å beholde mer.", - "hist_scanning": "Historikken din spilles gjennom deteksjonen. Dette kjører i bakgrunnen - du kan lukke denne dialogen og komme tilbake til den senere.", + "import_history_description": "Hadde du en smartplugg før WashData? Last opp effekthistorikken, eller les den fra Home Assistant. Deteksjonen kjører over den, og tidligere sykluser dukker opp i Sykluser-listen din, klare til å navngis.", + "hist_input_hint": "Last opp en CSV fra Historikk-panelet (entitet, tilstand, sist endret), eller la WashData lese sensorens historikk direkte. Live-deteksjonen kjører over den, og du velger hvilke sykluser som skal beholdes.", + "hist_recorder_hint": "Leser fra valgt dato og fram til nå. Home Assistant beholder detaljert historikk i 10 dager som standard; eldre timesgjennomsnitt er for grove for syklusdeteksjon.", + "hist_scanning": "Historikken din kjøres gjennom detektoren i bakgrunnen. Du kan lukke denne dialogen og komme tilbake senere.", "hist_imported_count": "{n} sykluser importert.", "hist_duplicates": "{n} var allerede importert og ble hoppet over.", "hist_capped": "Grensen per enhet for importerte sykluser ble nådd; resten ble ikke lagret.", - "hist_next_step": "De ligger i Sykluser-listen din, merket som importert historikk. Åpne en og bruk Merk for å navngi programmet den hører til.", + "hist_next_step": "De ligger i Sykluser-listen din, merket som importert historikk. Åpne en og bruk Merk for å navngi programmet.", "hist_rows_read": "{n} avlesninger lest", "hist_entity_substituted": "leste {used} (denne enheten er konfigurert for {wanted})", "hist_breaks": "{n} hull der sensoren var utilgjengelig", @@ -1097,55 +1030,86 @@ "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.", - "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.", + "imported_history_readonly": "Funnet i importert effekthistorikk. Den hjelper matchingen, men telles ikke med i statistikken din og kan ikke beskjæres eller deles opp. Merk den for å navngi programmet.", + "odometer_intro": "Denne totalen går bare opp. Å slette en syklus senker den ikke, så servicepåminnelser forblir riktige.", + "odometer_correct_hint": "Angi dette hvis WashData telte en kjøring som aldri fant sted, eller apparatet kjørte 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}", "toast_range_required": "Angi et gyldig Fra/Til-område.", - "no_profiles_option": "(ingen profiler ennå, legg til en)" + "no_profiles_option": "(ingen profiler ennå, legg til en)", + "compare_cycles_title": "Sammenlign {count} sykluser", + "edit_builtin_phase": "Dette er en innebygd fase. Når du lagrer, opprettes en egendefinert kopi; slett kopien for å gjenopprette originalen.", + "fetch_error": "Kunne ikke laste inn data.", + "connection_lost": "Tilkoblingen ble brutt - viser data fra {time}.", + "pg_notify_partial": "Markører viser bare varsler for start, nesten ferdig, ferdig, milepæler og stilletid. Live-oppdateringer, utlastingspåminnelser og varsler om overskridelse simuleres ikke.", + "connect_to_confirm": "Koble til i innstillingstannhjulet for å bekrefte eller vurdere.", + "coverage_gap_cluster": "{count} nylige umerkede sykluser på omtrent {min} min ser ut som ett program du ikke har opprettet ennå.", + "pg_label_yes": "Ville blitt merket \"{name}\" ved syklusens slutt", + "pg_label_no": "Ville bedt om gjennomgang: {reason}", + "live_match_debug_tip": "Konfidens: hvor godt den nåværende effektkurven samsvarer med den beste kandidatprofilen (0-100%). Tvetydig: de to beste kandidatene scorer innenfor 5% av hverandre - merket er usikkert til syklusen er ferdig.", + "automation_alias_started": "{device}: syklus startet", + "automation_alias_finished": "{device}: syklus ferdig", + "automation_desc_started": "Kjører når en {device}-syklus starter. Legg til handlingene dine (varsel, lys, ...).", + "automation_desc_finished": "Kjører når en {device}-syklus er ferdig. Legg til handlingene dine (varsel, lys, ...).", + "feedback_detected": "WashData oppdaget: {name}", + "pg_outcome_stale": "Innstillingene er endret siden denne simuleringen kjørte. Den viser fortsatt det gamle resultatet.", + "pg_compare_profile_tip": "Velger bare profilen som tegnes over grafen. Simuleringen gjenkjenner alltid syklusen på egen hånd, slik integrasjonen gjør under drift.", + "pg_task_busy": "En kjøring av Test på historikk eller Optimaliser pågår allerede for denne enheten. Vent på den, eller avbryt den.", + "pg_sweep_guarded": "Avslutter flere sykluser for tidlig, deler opp flere av dem eller går glipp av sykluser som den nåværende innstillingen fanger opp, så den anbefales aldri.", + "pg_sweep_n_cycles": "over {count} sykluser", + "pg_sweep_n_cycles_one": "over {count} syklus", + "pg_sweep_n_cycles_other": "over {count} sykluser", + "pg_sweep_keep_current": "Behold den nåværende verdien ({value}, {metric}): ingen verdi gjorde det bedre med minst én syklus.", + "hist_truncated": "stoppet ved grensen på {n} avlesninger, så resten av historikken ble ikke gjennomgått", + "hist_warn_kw": "Den høyeste målingen er bare {w}. Denne sensoren ser ut til å rapportere kilowatt; WashData trenger watt, så ingen sykluser kan oppdages.", + "hist_warn_naive_time": "{n} tidsstempler har ingen tidssone og ble lest som UTC. Hvis filen bruker lokal tid, er syklusene forskjøvet med UTC-forskyvningen din, og målinger rundt et sommertidsskifte kan gå tapt.", + "hist_warn_naive_time_one": "{n} tidsstempel har ingen tidssone og ble lest som UTC. Hvis filen bruker lokal tid, er syklusene forskjøvet med UTC-forskyvningen din, og målinger rundt et sommertidsskifte kan gå tapt.", + "hist_warn_naive_time_other": "{n} tidsstempler har ingen tidssone og ble lest som UTC. Hvis filen bruker lokal tid, er syklusene forskjøvet med UTC-forskyvningen din, og målinger rundt et sommertidsskifte kan gå tapt.", + "hist_tail_trimmed": "{min} min venting etter at syklusen var ferdig ble klippet bort, slik integrasjonen gjør under drift", + "invalid_threshold_pair": "Stoppterskel ({stop} W) må være under Startterskel ({start} W). Ingenting ble lagret; endre begge samtidig.", + "import_undo_hint": "Et gjenopprettingspunkt fra før siste import ({when}) er beholdt: {profiles} profil(er) og {cycles} syklus(er).", + "replace_undo_hint": "Et gjenopprettingspunkt lagres først, så du kan angre denne importen fra Eksporter / Importer.", + "pg_in_sample_note": "Disse syklusene var med på å bygge profilene de matches mot, så resultatene er optimistiske: forvent litt færre riktige match på nye sykluser." }, "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", - "smart_termination_duration_ratio": "Andel av det gjenkjente programmets forventede varighet som en syklus må nå før smart avslutning kan avslutte den tidlig; senk den for last- eller temperaturavhengige maskiner", + "smart_termination_duration_ratio": "Andel av programmets gjennomsnittlige varighet som en syklus må nå før smart avslutning kan avslutte den tidlig; senk den hvis kjøretidene varierer", "completion_min_seconds": "Korteste kjøring som teller som en ekte syklus", - "end_repeat_count": "Lave avlesninger på rad før avslutning", "interrupted_min_seconds": "Korte sykluser merkes som avbrutt", "min_off_gap": "Mellomrom som kreves for å skille to sykluser", "off_delay": "Sekunder med lav effekt før syklusen avsluttes", "start_duration_threshold": "Sekunder over terskelen for å bekrefte start", "start_threshold_w": "Minimum watt for å telle som startet", "stop_threshold_w": "Under dette telles maskinen som av", - "dtw_bandwidth": "Trinn 3: Sakoe-Chiba-tidsfordreiningsband (0 = DTW av; standard 0,2 = 20% av sykellengde)", - "corr_weight": "Trinn 2: balanse mellom kurvform (korrelasjon) og effektnivå (MAE); standard 0,45", - "duration_weight": "Trinn 4: hvor sterkt varighetsoverensstemmelse påvirker sluttscoren (standard 0,22)", - "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,8 (180%)", - "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)", - "dtw_refine_top_n": "Trinn 3: kandidater DTW beregner på nytt; øk til 7-9 hvis riktig profil rangerer 4.-5. (standard 5)", - "duration_scale": "Trinn 4: log-forhold der varighetsoverensstemmelse halveres; lavere = strengere straff (standard 0,175)", - "energy_scale": "Trinn 4: log-forhold der energioverensstemmelse halveres; lavere = strengere straff (standard 0,25)", + "profile_match_min_duration_ratio": "Korteste sykluslengde som andel av profilens varighet; standard 0,1 (10%)", + "profile_match_max_duration_ratio": "Lengste sykluslengde som andel av profilens varighet; standard 1,8 (180%)", "anti_wrinkle_idle_timeout": "Tillatt stille tid mellom to trommelpulser før anti-rynke slutter", "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_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", + "anti_wrinkle_exit_power": "Stille nivå mellom trommelpulser; virker bare over Stoppterskelen", + "anti_crease_finalize_ratio": "Andel av programmets gjennomsnittlige varighet som en syklus må nå før anti-rynke-etterløpet kan avslutte den; senk den på tørketromler, la vaskemaskiner stå på standard", "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.", "anti_wrinkle": "Periodisk tumbling etter syklus for å redusere rynker.", + "baking": "Varmeelement på høy temperatur aktivt for å danne skorpe og krumme.", + "cooking": "Hovedtilberedning med varmeelement og vifte aktive.", "cool_down": "Tumling uten varme nær syklusslutt.", + "cool_down_af": "Avkjøling med bare vifte etter oppvarmingen.", "drain_and_switch": "Overgangsperiode fra vaske- til tørkemodus.", "dry": "Tørketrinn ved bruk av varmeapparat og/eller restvarme.", "drying": "Hovedoppvarmet tumleperiode.", "heat_up": "Første oppvarming av varmeren før full tørking begynner.", + "keep_warm": "Lav varme for å holde maten varm.", + "keep_warm_bm": "Lav varme for å holde brødet varmt etter stekingen.", + "kneading": "Motoren blander og elter deigen. Høyt strømforbruk.", + "pause_af": "Kort pause for å riste eller sjekke maten.", + "pre_heat": "Første oppvarming av kammeret før selve tilberedningen.", "pre_rinse": "Spray første gang før vaskemiddelvask.", + "proving": "Heving på lav varme så gjæren virker og deigen vokser.", + "resting": "Kort pause på lav effekt mellom eltingene så glutenet slapper av.", "pre_wash": "Innledende bløtlegging eller forbehandling før hovedvask.", "rinse": "Renvannsskylletrinn. Denne fasen kan gjentas flere ganger.", "sanitize": "Høytemperaturrengjøringstrinnet for desinfiseringsprogrammer.", @@ -1158,6 +1122,7 @@ "wash_dw": "Hovedvaskemiddel med oppvarming." }, "placeholder": { + "custom_task": "f.eks. rengjør dørpakning", "add": "legg til…", "group_name": "f.eks. Bomull 2:47", "json_buttons": "[{\"action\":\"ID\",\"title\":\"Label\"}]", @@ -1185,10 +1150,10 @@ "section": { "dishwasher": { "label": "Oppvaskmaskin", - "intro": "Håndtering av syklusslutt for oppvaskmaskiner, som vanligvis avsluttes med en lang, nesten lydløs tørkefase før en kort siste tømming." + "intro": "Slutthåndtering for oppvaskmaskiner, som ofte tørker stille lenge før en kort siste tømming." }, "anti_wrinkle": { - "intro": "Anti-rynke-/anti-krøll-modus oppdager lavomsatt tumle-pulser etter hoveddelen og holder dem festet til den ferdige syklusen.", + "intro": "Holder en tørketrommels trommelpulser med lav effekt etter hovedfasen sammen med den ferdige syklusen.", "label": "Anti-rynke" }, "basic": { @@ -1196,7 +1161,7 @@ "label": "Grunnleggende" }, "delay": { - "intro": "Forsinket-start-deteksjon identifiserer når et apparat er påslått men ennå ikke har begynt syklusen.", + "intro": "Oppdager et apparat som er på og venter på en forsinket start.", "label": "Forsinket start" }, "detection": { @@ -1220,161 +1185,153 @@ "label": "Timing og vokter" }, "triggers": { - "intro": "Valgfrie eksterne signaler: en slutt-utløser, en dørensor, en pausebryter og utlastingspåminnelsen.", + "intro": "Valgfrie ekstrafunksjoner: slutt-utløser, dørsensor, pausebryter og utlastingspåminnelse.", "label": "Utløsere og dører" - }, - "phase_eta": { - "label": "Tid som gjenstår", - "intro": "Fasebevisst tid som gjenstår, for maskiner der syklusens lengde avhenger av temperatur eller sentrifugering." } }, "setting": { "dishwasher_end_spike_quiet_release": { "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." + "doc": "Etter forventet varighet: hvor lenge oppvaskmaskinen må holde seg under Stoppterskel før WashData slutter å vente på siste tømming. Hev den hvis en sen tømming etter lang tørking blir oversett. Ventetiden er aldri over 30 minutter." }, "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." + "doc": "Andel av programmets gjennomsnittlige varighet som en syklus må nå før trommelingen med lav effekt teller som anti-rynke-etterløpet. Senk den (f.eks. 0,75) på tørketromler med fuktsensor. La vaskemaskiner stå på standard (0,98), ellers kan en vask slutte halvveis." }, "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.", + "doc": "Hold en tørketrommels korte trommelpulser med lav effekt etter hovedvarmefasen sammen med den ferdige syklusen, i stedet for å lese dem som nye sykluser.", "label": "Aktiver anti-rynke-deteksjon" }, "anti_wrinkle_exit_power": { - "doc": "Strømmen må falle under dette mellom pulsene for at anti-rynkemodus skal forbli aktiv.", + "doc": "I anti-rynkemodus regnes effekt under dette som stille mellom trommelpulser, og stillhet lenger enn Maks. pulspause avslutter modusen. Stoppterskelen brukes når den er høyere, så dette endrer bare noe når det settes over Stoppterskelen.", "label": "Utgangseffektterskel" }, "anti_wrinkle_idle_timeout": { - "doc": "Hvor lenge maskinen kan forbli stille mellom to trommelpulser før anti-rynkemodus slutter. Sett den høyere enn den lengste pausen tørketrommelen din lar stå mellom to pulser, ellers leses hver senere puls som en falsk start.", + "doc": "Hvor lenge tørketrommelen kan være stille mellom trommelpulser før anti-rynkemodus slutter. Sett den over tørketrommelens lengste pause, ellers leses senere pulser som falske starter.", "label": "Maks. pulspause" }, "anti_wrinkle_max_duration": { - "doc": "Pulser lengre enn dette behandles som en ekte syklus i stedet for en anti-rynke tumbling.", + "doc": "En lengre puls teller som en ekte syklus, ikke som anti-rynke-tromling.", "label": "Maks. varighet" }, "anti_wrinkle_max_power": { - "doc": "En puls over denne kraften blir behandlet som en virkelig ny syklus, ikke en anti-rynke tumbling. Sett like over tumle-pulseffekten.", + "doc": "En puls over denne effekten teller som en ekte ny syklus, ikke som tromling. Sett den like over effekten til trommelpulsene dine.", "label": "Maks. anti-rynke effekt" }, "auto_label_confidence": { - "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.", + "doc": "Matchpoeng ved syklusslutt der programmet merkes uten å spørre, hvis poengene er på eller over denne. Hev den for mer sikkerhet; senk den for å merke flere sykluser automatisk. Hold den over Lærings-konfidensgrad.", "label": "Auto-merking konfidensgrad" }, "auto_maintenance": { - "doc": "Kjør nattlig rengjøring: gjenoppbygg profilkonvolutter, beregn syklushelsen på nytt, beskjær feilsøkingsspor og behold de siste syklusene.", + "doc": "Nattlig opprydding: merk gamle sykluser som nå tydelig matcher et program, bygg opp programkurver på nytt, oppdater avviksmarkeringer og syklushelse, og fjern feilsøkingsspor hvis Lagre feilsøkingsspor er av. Sykluser og effektsporene deres beholdes alltid.", "label": "Automatisk vedlikehold (nattlig opprydding)" }, "completion_min_seconds": { - "doc": "Sykluser som er kortere enn dette, forkastes som spøkelsessykluser (prøvekjøringer, åpne døren for å legge til en sokk).", + "doc": "Sykluser kortere enn dette (prøvekjøringer, åpne døren for å legge inn en sokk) lagres som Avbrutt og får ingen ferdigmelding.", "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." + "doc": "Legger målinger fra mislykkede startforsøk til forrest i en syklus, for maskiner som prøver seg fram før start (dørlås, første vannfylling). 300 s dekker de fleste; 0 = av. Registrerte starter flyttes tidligere, så lærte varigheter glir en stund." }, "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.", + "doc": "Effekten må holde seg i standby-båndet så lenge før apparatet teller som ventende på start.", "label": "Bekreftelsessekunder" }, "delay_start_detect_enabled": { - "doc": "Oppdag når apparatet er slått på og venter (forsinket start / standby), men ikke har begynt syklusen, så standby-trekning blir ikke forvekslet med en kjørende syklus.", + "doc": "Oppdag et apparat som er på og venter på en forsinket start, slik at standbyforbruket ikke forveksles med en kjørende syklus.", "label": "Aktiver deteksjon av forsinket start" }, "delay_timeout_hours": { - "doc": "Slutt å vente i forsinket startmodus etter disse mange timene og gå tilbake til tomgang, så en maskin som er slått på, men aldri startet, ikke venter for alltid.", + "doc": "Gi opp å vente på en forsinket start etter så mange timer, og gå tilbake til tomgang.", "label": "Tidsavbruddsimer" }, "device_type": { - "doc": "Apparatklasse. Angir fornuftige deteksjonsstandarder (terskler, av-forsinkelse, slutthåndtering) innstilt for den apparattypen; endre den bare hvis enheten opprinnelig ble satt opp som feil type.", + "doc": "Angir deteksjonsstandarder (terskler, av-forsinkelse, slutthåndtering) for denne typen apparat. Endre den bare hvis enheten ble satt opp som feil type.", "label": "Enhetstype" }, "door_sensor_entity": { - "doc": "Valgfri dør binær sensor. Brukes til å oppdage når apparatet har blitt åpnet/avlastet etter en syklus.", - "label": "Dørensorenhet" + "doc": "Valgfri dørsensor. Viser når apparatet ble åpnet for å tømmes etter en syklus.", + "label": "Dørsensorenhet" }, "duration_tolerance": { - "doc": "Toleranse for anslag som gjenstår av tid (læringsfeedback, ikke matching). Hvis den faktiske varigheten er innenfor +/-X % av estimatet, teller det som et godt samsvar.", + "doc": "Bare læringsfeedback: en syklus innenfor +/-X % av anslått varighet teller som et godt anslag. Påvirker ikke matching.", "label": "Estimattoleranse" }, "enable_ml_models": { - "doc": "Mens en syklus kjører, la modellene avgrense live-resultatene: et jevnere tid som gjenstår og energi/kostnadsestimat, og en anti-prematur-stopp-vakt ved sluttdeteksjon (den kan bare forsinke en finish, aldri avslutte en tidlig, og er begrenset). Bruker dine finjusterte modeller når de er tilgjengelige, ellers de innebygde. Av = kun den klassiske strømbaserte logikken (fortsatt pålitelig).", + "doc": "Bruk en modell trent på denne maskinen til å forutsi en kjørende syklus' energi og kostnad, og la et velkjent program som stopper uvanlig tidlig, avslutte uten full av-forsinkelse. Av: bare effektbasert logikk.", "label": "Bruk smarte modeller under syklus" }, "end_energy_threshold": { - "doc": "Under nedtellingen med avbruddsforsinkelse sammenlignes akkumulert energi (watt x tid) med denne terskelen. Hvis den overskrides, nullstilles nedtellingen - og holder anti-krøll-trommel og oppvaskmaskinens tørkehaler festet til syklusen i stedet for å klippe dem korte. Hev den hvis syklusene avsluttes for tidlig under nedkjøling; senke den hvis deteksjonen er treg.", + "doc": "Energi som tillates under av-forsinkelsen. Mer enn dette, og syklusen fortsetter, slik at tromlinger og tørkehaler forblir en del av den. Senk den hvis sykluser slutter under en hale; hev den hvis ferdige sykluser fortsetter å vente.", "label": "Sluttenergi" }, - "end_repeat_count": { - "doc": "Antall påfølgende avlesninger under stoppterskel som kreves før syklusen avsluttes. 1 er greit for de fleste plugger. Øk til 2-3 hvis smartpluggen av og til rapporterer en prøve med falsk null midt i syklusen og syklusene dine avsluttes for tidlig.", - "label": "Slutt gjentaksantall" - }, "energy_price_entity": { - "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.", + "doc": "Sensor med gjeldende pris per kWh (f.eks. en dynamisk tariff). Den går foran den statiske prisen nedenfor, så den må være en pris, ikke pluggens strøm- eller energisensor.", "label": "Energiprisenhet" }, "energy_price_static": { - "doc": "Fast pris per kWh brukt for kostnadstall når ingen levende prisenhet er satt ovenfor.", + "doc": "Fast pris per kWh, brukt når ingen prisenhet er angitt.", "label": "Statisk energipris (per kWh)" }, "energy_sensor": { - "doc": "Valgfri kumulativ energiteller (total_increasing i kWh/Wh, f.eks. støpselets egen levetidsmåler). Når den er angitt, hentes hver syklus' rapporterte energi fra forskjellen på denne telleren mellom start og slutt, noe som unngår undertellingen du får ved å integrere en tregt rapporterende strømsensor. Faller tilbake til den integrerte verdien hvis avlesningen mangler, enheten er ukjent, eller forskjellen ikke er positiv. La feltet stå tomt for å fortsette å integrere strømsensoren.", + "doc": "Valgfri kWh- eller Wh-teller (total_increasing, f.eks. pluggens egen måler). Syklusenergien leses da fra den, noe som er mer nøyaktig enn å beregne den fra en treg strømsensor. Hvis målingen er ubrukelig, brukes den beregnede verdien.", "label": "Energimålerenhet" }, "expose_debug_entities": { - "doc": "Publiser ekstra diagnostiske HA-enheter (match konfidens, tvetydighet, tilstandsinternal). Av holder enhetslisten ren for normal bruk.", + "doc": "Legg til diagnostiske entiteter (matchkonfidens, tvetydighet, intern tilstand). Av holder entitetslisten ryddig.", "label": "Vis feilsøkingsenheter" }, "external_end_trigger": { - "doc": "Binær sensor hvis tilstandsendring markerer syklusslutt (f.eks. en \"ferdig\" kontakt med apparatet eller en følgeintegrasjon).", + "doc": "Binær sensor der en endring markerer syklusslutt (f.eks. en \"ferdig\"-kontakt eller en annen integrasjon).", "label": "Ekstern utløserenhet" }, "external_end_trigger_enabled": { - "doc": "La en ekstern binær sensor signalisere slutten av en syklus, i tillegg til den innebygde strømbaserte deteksjonen.", + "doc": "La en binær sensor signalisere slutten på en syklus, i tillegg til den effektbaserte deteksjonen.", "label": "Aktiver ekstern slutt-utløser" }, "external_end_trigger_inverted": { - "doc": "Betrakt triggersensoren som slår seg AV (i stedet for PÅ) som sluttsignalet.", + "doc": "Behandle at sensoren slår seg AV (ikke PÅ) som sluttsignalet.", "label": "Inverter ekstern utløser (utløs ved AV)" }, "learning_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.", + "doc": "Laveste matchpoeng for å merke en ferdig syklus. En klar vinner merkes; et jevnt tilfelle går til vurderingskøen under Sykluser. Under dette merkes eller spørres ingenting. Hold den under Auto-merking konfidensgrad.", "label": "Lærings-konfidensgrad" }, "linked_device": { - "doc": "Eventuelt kan denne WashData-enheten legges under en annen enhet (f.eks. smartpluggen) i HA-enhetsregisteret, vist som \"Tilkoblet via ...\".", + "doc": "Valgfritt. Vis denne WashData-enheten under en annen enhet (f.eks. smartpluggen) i Home Assistant, som \"Tilkoblet via ...\".", "label": "Grupper under enhet" }, "min_off_gap": { - "doc": "Hvis maskinen slår seg av i kortere tid enn denne tiden, behandles på/av/på-sekvensen som én kontinuerlig syklus. Forhindrer bløtleggingsprogrammer (maskinen slår seg av i flere minutter midt i vask) fra å deles i to separate sykluser. Sett den kortere enn gapet mellom rygg-til-rygg-lastene hvis du vil at de skal telles som separate sykluser. Standardinnstillinger for enhetstype beskytter den typiske pausen i syklusen for hvert apparat.", + "doc": "En av-periode kortere enn dette deler ikke en syklus, så bløtleggingspauser forblir i én vask. Hold den kortere enn pausen mellom vasker rett etter hverandre, ellers slås de sammen til én syklus.", "label": "Min. av-intervall" }, "min_power": { - "doc": "Absolutt minimumseffekt anses som aktiv. Avlesninger under dette behandles som 0 W (standby), og filtrerer bort fantombelastningen til smartplugger og standby-lysdioder.", + "doc": "Målinger under dette teller som 0 W (standby). Filtrerer bort tomgangsforbruket til smartplugger og standby-lysdioder.", "label": "Minimum effekt" }, "ml_training_enabled": { - "doc": "Studer regelmessig de gjennomgåtte syklusene dine over natten og finjuster modellene til denne spesifikke maskinen. En endring beholdes bare når den virkelig scorer bedre på utholdte sykluser, så dette kan bare hjelpe eller forbli det samme - aldri gå tilbake.", + "doc": "Tren energimodellen på nytt med jevne mellomrom på denne maskinens ferdige sykluser. En ny modell beholdes bare hvis den slår det enkle anslaget på testsykluser.", "label": "Lær fra denne maskinen" }, "ml_training_hour": { - "doc": "Lokal tid på døgnet (0-23) for å gjøre finjusteringen over natten. Velg en rolig time som 2 (02:00).", + "doc": "Time på døgnet (0-23) for nattlig trening. Velg en rolig time, f.eks. 2.", "label": "Lær ved time" }, "ml_training_interval_days": { - "doc": "Sjekk på nytt for forbedringer maksimalt én gang per så mange dager.", + "doc": "Tren på nytt høyst én gang hver så mange dager.", "label": "Sjekk høyst hvert" }, "ml_training_min_cycles": { - "doc": "Vent til minst så mange sykluser er registrert før du finjusterer, så det er nok å lære av.", + "doc": "Vent på minst så mange registrerte sykluser før trening.", "label": "Sykluser som trengs først" }, "name": { - "doc": "Visningsnavn vist i HA-integreringslisten og enhetsregisteret.", + "doc": "Navnet som vises i Home Assistant for denne enheten.", "label": "Enhetsnavn" }, "no_update_active_timeout": { - "doc": "Hvis det ikke kommer noen strømoppdateringer på så lenge mens du kjører, anta at støpselet har falt frakoblet og tvinget til å stoppe for å unngå en zombiesyklus. Standard 600 s tillater sky eller mesh lag.", + "doc": "Hvis ingen effektoppdatering kommer på så lenge under en syklus, antas pluggen å være frakoblet, og syklusen tvangsstoppes. Standarden på 600 s gir rom for forsinkelser i sky eller mesh.", "label": "Tid uten oppdatering" }, "notify_before_end_minutes": { @@ -1382,35 +1339,35 @@ "label": "Forhåndsvarsling" }, "notify_channel": { - "doc": "Android-varslingskanalnavn for start-/livemeldinger (styrer lyd per kanal og prioritet på mobilappen). Blank bruker standardappen for følgeappen.", + "doc": "Android-kanal for start- og livemeldinger; kanalen bestemmer lyd og prioritet. Tom: appens standard.", "label": "Android-kanal (start/live)" }, "notify_cycle_timers": { - "doc": "Varsler på bestemte minutter i en syklus (f.eks. for å legge til mykner). Meldingen støtter {device}, {program}, {minutes}. Aktiver Auto-pause for å pause på det tidspunktet og motta et interaktivt varsel med en Fortsett-knapp; gjenoppta via panelet, pause/gjenoppta-tjenesten eller varslingshandlingen.", + "doc": "Varsler ved bestemte minutter inn i en syklus (f.eks. for å tilsette tøymykner). Meldingen støtter {device}, {program}, {minutes}. Med Auto-pause settes syklusen også på pause der, og varselet får en Fortsett-knapp.", "label": "Syklustidtakere" }, "notify_finish_channel": { - "doc": "Android-varslingskanalnavn for sluttmeldingen. Blank gjenbruker start/live-kanalen.", + "doc": "Android-kanal for sluttmeldingen. Tom: start-/livekanalen gjenbrukes.", "label": "Android-kanal (slutt)" }, "notify_finish_message": { - "doc": "Meldingstekst som sendes når en syklus er ferdig. Malvariabler: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}. {time_finished} og {vs_typical} er mest nyttige her.", + "doc": "Tekst som sendes når en syklus er ferdig. Variabler: {device}, {program}, {duration} (minutter), {duration_hm} (1 h 05 min), {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}, {status} (completed eller force_stopped; avbrutte sykluser sender ingenting). Med andre variabler brukes standardteksten.", "label": "Sluttmelding" }, "notify_finish_services": { - "doc": "varsle.* tjenester som kalles når en syklus er ferdig. Legg til en per mål; la stå tomt for ingen ferdigmelding.", + "doc": "notify.*-tjenester som kalles når en syklus er ferdig. La stå tomt for ingen.", "label": "Sluttjenester" }, "notify_fire_events": { - "doc": "Avfyr også ha_washdata_*-hendelser på syklusstart/slutt slik at du kan bygge dine egne automatiseringer.", + "doc": "Utløs også ha_washdata_*-hendelser ved syklusstart og -slutt, til dine egne automasjoner.", "label": "Aktiver HA-hendelser for varsler" }, "notify_icon": { - "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.", + "doc": "Lite ikon på varselet (f.eks. mdi:washing-machine). Sendes bare til mobile_app-mål, ikke til notify-grupper. På iOS erstatter det appikonet og krever companion-appen 2026.8 eller nyere. Tom: plattformens standard.", "label": "Varselikon" }, "notify_live_chronometer": { - "doc": "Vis en live-oppdatering nedtellingstidtaker i varselet (på plattformer som støtter det) i stedet for et statisk estimat.", + "doc": "Vis en løpende nedtelling i varselet (der det støttes) i stedet for et fast anslag.", "label": "Bruk direktekronometer" }, "notify_live_interval_seconds": { @@ -1418,143 +1375,135 @@ "label": "Direkteoppdateringsintervall" }, "notify_live_overrun_percent": { - "doc": "Hvis en syklus går forbi estimatet med mer enn denne prosentandelen, send et overløpsvarsel.", + "doc": "Live-oppdateringer stopper når en syklus går denne prosentandelen over forventet varighet (20 = opptil 1,2x forventet tid).", "label": "Direkteoverskridelse % før varsel" }, "notify_live_services": { - "doc": "notify.* tjenester kalt for live fremdriftsoppdateringer mens en syklus kjører. La stå tomt for å deaktivere varsler om fremdrift i sanntid.", + "doc": "notify.*-tjenester for live fremdriftsoppdateringer mens en syklus kjører. La stå tomt for å slå dem av.", "label": "Direktefremdriftstjenester" }, "notify_only_when_home": { - "doc": "Send kun varsler når minst én av de tilknyttede personene (over) er hjemme.", + "doc": "Varsle bare når minst én av personene ovenfor er hjemme.", "label": "Varsle kun når hjemme" }, "notify_people": { - "doc": "person.*-enheter brukt av «Varsle bare når hjemme» for å avgjøre om noen er hjemme.", + "doc": "person.*-entiteter som «Varsle kun når hjemme» sjekker.", "label": "Personer (for Kun hjemme)" }, "notify_pre_complete_message": { - "doc": "Teksten i varselet før slutt / nesten ferdig. Malvariabler: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Tekst i hver live fremdriftsoppdatering. Malvariabler: {device}, {program}, {minutes} (minutter igjen). Med andre variabler brukes standardteksten.", "label": "Nesten ferdig-melding" }, "notify_reminder_message": { - "doc": "Teksten i påminnelsen om at tøyet fortsatt venter på å tas ut. Tomt bruker den innebygde standarden. Malvariabler: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Tekst i Forhåndsvarsling, som sendes én gang. Malvariabler: {device}, {program}, {minutes} (innstillingen Forhåndsvarsling). Med andre variabler brukes standardteksten.", "label": "Påminnelsesmelding" }, "notify_start_message": { - "doc": "Meldingstekst som sendes når en syklus starter. Malvariabler: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Tekst som sendes når en syklus starter. Malvariabler: {device}, {program}. Med andre variabler brukes standardteksten.", "label": "Startmelding" }, "notify_start_services": { - "doc": "varsle.* tjenester som kalles når en syklus starter. Legg til en per mål (telefon, dashbord osv.); la stå tomt for ingen startvarsel.", + "doc": "notify.*-tjenester som kalles når en syklus starter (telefon, dashbord, ...). La stå tomt for ingen.", "label": "Starttjenester" }, "notify_timeout_seconds": { - "doc": "Avvis automatisk varselet etter så mange sekunder (på plattformer som støtter det). 0 beholder den til den avvises manuelt.", + "doc": "Lukk varselet etter så mange sekunder (der det støttes). 0 beholder det til du lukker det.", "label": "Automatisk avvisning etter" }, "notify_title": { - "doc": "Meldingstittel. Malvariabler: {device}, {program}, {duration}, {energy}, {cost}, {date}, {time}, {minutes}.", + "doc": "Meldingstittel. Bare {device} settes inn; med andre variabler brukes standardtittelen.", "label": "Varselstittel" }, "notify_unload_delay_minutes": { - "doc": "Minutter etter at en syklus avsluttes før du sender den fortsatt ventende påminnelsen om \"last av maskinen\". Sett 0 for å deaktivere påminnelsen.", + "doc": "Minutter etter at en syklus slutter før påminnelsen \"tøm maskinen\". 0 slår den av.", "label": "Utlastingspåminnelsesforsinkelse" }, "notify_unload_repeat": { - "doc": "Fortsetter å sende utlastingspåminnelsen på nytt hvert \"Utlastingspåminnelsesforsinkelse\" minutt til utlastingen er bekreftet eller du trykker på \"Stopp påminnelser\" i varselet. Krever en måte å bekrefte utlastingen på: en Dørensorenhet, en Utlastingsbekreftelsesenhet eller alternativet \"Bekreft utlasting manuelt\" nedenfor. Avvis-knappen fungerer på varsler fra Home Assistant companion-appen (mobil).", + "doc": "Send utlastingspåminnelsen på nytt hvert \"Utlastingspåminnelsesforsinkelse\" minutt til utlastingen er bekreftet eller du trykker \"Stopp påminnelser\" (mobilapp). Krever en Dørsensorenhet, en Utlastingsbekreftelsesenhet eller \"Bekreft utlasting manuelt\".", "label": "Gjenta til utlastet" }, "off_delay": { - "doc": "På tide å vente etter at strømmen faller før du erklærer at syklusen er fullført. Hvis strømmen gjenopptas innenfor dette vinduet, fortsetter syklusen sømløst - dette slår bro mellom pauser mellom vasketrinn. Oppvaskmaskiner har lange tørkefaser (strøm av i 20-60 min) så avslåingsforsinkelsen må overstige det for å holde hele vask+tørr som én syklus.", + "doc": "Hvor lenge effekten må holde seg lav før syklusen er ferdig. Kommer effekten tilbake innen denne tiden, fortsetter syklusen. Oppvaskmaskiner kan være stille i 20-60 min under tørking, så sett den lengre enn det.", "label": "Av-forsinkelse" }, "pause_cuts_power": { - "doc": "Når en syklus er satt på pause, slå også av Switch Entity nedenfor. Kun for apparater hvis støpsel trygt kan kuttes midt i syklusen.", + "doc": "Når en syklus settes på pause, slå også av Bryterenhet nedenfor. Bare for apparater som trygt kan miste strøm midt i en syklus.", "label": "Pause kutter også strøm (via bryter)" }, "power_off_delay": { - "doc": "Hvor lenge effekten må holde seg under Avslåingsterskel etter at en syklus er fullført før tilstanden går tilbake til Av. Brukes bare når Avslåingsterskel er over 0. Kontrolleres i bakgrunnstakten, så den effektive forsinkelsen rundes opp til neste tilstandsutløpstakt.", + "doc": "Hvor lenge effekten må holde seg under Avslåingsterskel etter en syklus før tilstanden går tilbake til Av. Brukes bare når den terskelen er over 0. Sjekkes periodisk, så den faktiske ventetiden kan bli litt lengre.", "label": "Avslåingsforsinkelse" }, "power_off_threshold_w": { - "doc": "Valgfri effektbasert registrering av avslåing. Når verdien er over 0, regnes maskinen som slått av og tilstanden går tilbake til Av så snart en syklus er fullført og effekten holder seg under dette nivået i hele Avslåingsforsinkelsen. La stå på 0 for å deaktivere (standard). Sett verdien over det reelle avslåtte nullnivået og under Stoppterskel samt under maskinens standbyforbruk i tilstanden ferdig-men-på; hvis den ikke er under Stoppterskel, blir den ignorert. Når den er aktivert, erstatter den Fremdriftsreset-forsinkelse for tilbakegang til Av, slik at en ferdig maskin forblir i Ferdig/Rengjøre til den faktisk blir slått av.", + "doc": "Etter en syklus setter effekt under denne i Avslåingsforsinkelse tilstanden til Av, i stedet for at Fremdriftsreset-forsinkelse gjør det. Sett den over forbruket i avslått tilstand, under standby og Stoppterskel. 0 slår den av (standard).", "label": "Avslåingsterskel" }, "power_sensor": { - "doc": "Sensorenheten rapporterer levende effekt i watt for dette apparatet (f.eks. sensor.washer_power). All syklusdeteksjon er basert på dette signalet.", + "doc": "Sensoren som rapporterer dette apparatets gjeldende effekt i watt (f.eks. sensor.washer_power). All deteksjon bygger på den.", "label": "Strømsensor" }, - "profile_duration_tolerance": { - "doc": "+/--båndet rundt en profilgjennomsnittlig varighet brukt under matching. 0,25 betyr at en 60 min profil matcher 45-75 min sykluser.", - "label": "Profil varighetstoleranse" - }, "profile_match_interval": { - "doc": "Hvor ofte du skal forsøke profiltilpasning under en løpesyklus. Standard 300 s (5 minutter) balanserer deteksjonshastighet og CPU.", + "doc": "Hvor ofte en kjørende syklus matches mot profilene. Standard 300 s (5 min).", "label": "Samsvarsintervall" }, "profile_match_max_duration_ratio": { - "doc": "Maksimal sykluslengde i forhold til profilen. 1.3 betyr at en syklus må være under 130 % av profilvarigheten for å matche.", + "doc": "Et program forkastes når syklusen har gått lenger enn denne faktoren ganger den typiske varigheten. 1,8: en syklus 80 % lengre enn programmet matcher det ikke lenger.", "label": "Maks. varighetsforhold" }, "profile_match_min_duration_ratio": { - "doc": "Minimum sykluslengde i forhold til profilen. 0,9 betyr at en syklus må være minst 90 % av profilvarigheten for å matche.", + "doc": "Et program vurderes først når syklusen har gått denne andelen av den typiske varigheten. 0,10: et program på 2 timer blir kandidat etter 12 minutter. Brukes ikke i en syklus' første 15 minutter.", "label": "Min. varighetsforhold" }, "profile_match_threshold": { - "doc": "Minimum likhetspoeng (0-1) kreves ved syklusslutt for å godta en programidentifikasjon. Hev den for å redusere feil identifikasjon; senk den hvis maskinens programmer ikke blir matchet. Standard 0,4 er et konservativt utgangspunkt.", + "doc": "Live-matchpoeng en syklus trenger før smart avslutning eller anti-rynke-avslutningen kan avslutte den tidlig. Endrer ikke det viste programmet. Hev den hvis svake match avslutter sykluser for tidlig; senk den hvis tydelige match venter hele av-forsinkelsen.", "label": "Samsvarsterskel" }, "profile_unmatch_threshold": { - "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.", + "doc": "Hvis live-matchpoengene faller under denne, fjernes det gjeldende programgjettet. Hold den litt under Samsvarsterskel, slik at et kort fall ikke tilbakestiller det.", "label": "Ikke-samsvarsterskel" }, "progress_reset_delay": { - "doc": "Etter å ha fullført, hold fremdriften på 100 % så lenge, slik at Fullført er synlig på dashbord før du tilbakestiller til inaktiv.", + "doc": "Etter en syklus, hold fremdriften på 100 % så lenge før tilbakestilling til Inaktiv.", "label": "Fremdriftsreset-forsinkelse" }, "pump_stuck_duration": { - "doc": "Sekunder kan en pumpe kjøre kontinuerlig før den blir flagget som mulig fastlåst (utløser hendelsen med fast pumpe).", + "doc": "Hvor lenge en pumpe kan gå sammenhengende før hendelsen for fastsittende pumpe utløses.", "label": "Pumpe fastsittingsvarighet" }, "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.", + "doc": "Minste tid mellom behandlede målinger. Målinger som kommer tidligere, hoppes over (lave målinger under en syklus beholdes alltid). Sett den til pluggens rapporteringsintervall eller lavere; en høyere verdi kaster bare målinger.", "label": "Sampling-intervall" }, "save_debug_traces": { - "doc": "Lagre full effektsporing og samsvarende feilsøkingsdata for hver syklus. Nyttig for feilsøking, men øker lagringsstørrelsen.", + "doc": "Lagre også matcherens feilsøkingsdata (poengsummene til alle kandidater) med hver syklus, for feilsøking. Bruker omtrent 20 KB per syklus.", "label": "Lagre feilsøkingsspor" }, "smart_termination_duration_ratio": { - "doc": "Hvor langt inn i det gjenkjente programmets forventede varighet en syklus må være før smart avslutning kan avslutte den tidlig når effekten faller. Den forventede varigheten er programmets gjennomsnitt, så på apparater med sterkt varierende kjøretid - vaskemaskiner med kaldt vinter- kontra varmt sommerinnløpsvann, tørketromler med fuktsensor, lastavhengige programmer - avslutter omtrent halvparten av alle kjøringer kortere enn dette gjennomsnittet og får aldri den raske avslutningen, men avsluttes først minutter senere via reserve-tidsavbruddet. Senk denne verdien (f.eks. 0,85) på slike maskiner slik at den tidlige avslutningen fortsatt utløses; øk den mot 1,0 for å være mer forsiktig. La feltet stå tomt for standarden (0,98, eller 0,99 for oppvaskmaskiner). Den kan bare avslutte en syklus tidligere, aldri senere, og utløses aldri ved en tvetydig eller usikker gjenkjenning.", + "doc": "Andel av programmets gjennomsnittlige varighet som en syklus må nå før smart avslutning kan avslutte den tidlig. Hvis kjøretidene varierer (vanntemperatur, fuktsensor, mengde), senk den (f.eks. 0,85). La stå tomt for 0,98 (0,99 på oppvaskmaskiner).", "label": "Forhold for smart avslutning" }, - "smoothing_window": { - "doc": "Hvor mye råeffektsignalet jevnes ut. Lav (2) er responsiv, men støyende; høy (5) jevner ut pigger, men legger til etterslep.", - "label": "Utjevningsvindu" - }, "start_duration_threshold": { - "doc": "Strømmen må holde seg over startterskelen så lenge for å bekrefte en reell start, og forhindre at av/på-brytere på et brøkdel sekund starter en syklus.", + "doc": "Effekten må holde seg over Startterskel så lenge før en syklus starter, slik at korte av/på-utslag ignoreres.", "label": "Startvarighet" }, "start_energy_threshold": { - "doc": "Energi (strøm x tid) apparatet må forbruke før KJØRING. En kort topp med høy effekt har svært lav energi og ignoreres, og forhindrer falske starter.", + "doc": "Energi apparatet må bruke før syklusen teller som i gang. En kort effekttopp bruker lite energi, så den starter ingen syklus.", "label": "Startenergi" }, "start_threshold_w": { - "doc": "Strømmen må stige over dette nivået for å bekrefte at en syklus har startet. Hvis du setter den for lavt, forårsaker feilstarter fra standby-strøm; for høye og saktestartende programmer (kaldfylling) savnes. Forslagsmotoren setter dette like over maskinens observerte laveste aktive effekt.", + "doc": "Effekten må stige over denne for å starte en syklus. For lav: standbyeffekt gir falske starter. For høy: saktestartende programmer (kaldtvannsfylling) blir oversett.", "label": "Startterskel" }, "stop_threshold_w": { - "doc": "Strømmen må falle under dette nivået før nedtellingen starter. Sett den under startterskelen - gapet mellom dem er hysteresebåndet som forhindrer flimmer. Hvis satt for høyt, utløser laveffektfaser (skyllestopp, anti-krøll) sluttsekvensen feilaktig.", + "doc": "Effekten må falle under denne før av-forsinkelsen begynner å telle. Hold den under Startterskel. For høy, og faser med lav effekt (skyllepauser, anti-rynke) starter sluttnedtellingen ved en feil.", "label": "Stoppterskel" }, "switch_entity": { - "doc": "Valgfri bryter slått av på pause og på igjen når du fortsetter, brukt sammen med \"Pause kutter også strømmen\".", + "doc": "Valgfri bryter som slås av ved pause og på igjen ved gjenopptak, med \"Pause kutter også strøm\".", "label": "Bryterenhet" }, "watchdog_interval": { - "doc": "Hvor ofte bakgrunnsvakthunden sjekker for stansede sensorer og medgåtte tidsavbrudd. Standard 30 s.", + "doc": "Hvor ofte bakgrunnssjekken ser etter fastlåste sensorer og tidsavbrudd. Standard 30 s.", "label": "Vokter-intervall" }, "notify_milestone_message": { @@ -1562,127 +1511,82 @@ "label": "Milepælmelding" }, "notify_milestones": { - "doc": "Kommaseparerte syklusantall som utløser et engangs feiringsvarsel når de nås (f.eks. 50, 100, 500, 1000). Tomt deaktiverer milepælvarsler.", + "doc": "Syklusantall som utløser et engangs feiringsvarsel, kommaseparert (f.eks. 50, 100, 500, 1000). Tomt slår dem av.", "label": "Syklusmilepæler" }, "notify_quiet_end_hour": { - "doc": "Slutten på ikke-forstyrr-vinduet (0-23). Tilbakeholdte varsler leveres på dette klokkeslettet. La stå tomt for å deaktivere.", + "doc": "Slutten på ikke-forstyrr-vinduet (0-23); tilbakeholdte varsler sendes da. Tomt slår det av.", "label": "Slutt på stilletid" }, "notify_quiet_start_hour": { - "doc": "Starten på et ikke-forstyrr-vindu (0-23). Varsler om ferdig, påminnelser og rent-tøy-varsler som ellers ville utløses i stilletiden, holdes tilbake og leveres når vinduet slutter. La stå tomt for å deaktivere. Støtter vinduer som krysser midnatt (f.eks. start 22, slutt 7).", + "doc": "Starten på et ikke-forstyrr-vindu (0-23). Ferdig-, påminnelses- og rent-tøy-varsler i dette vinduet holdes tilbake til det slutter. Kan gå over midnatt (f.eks. 22 til 7). Tomt slår det av.", "label": "Start på stilletid" }, "peak_rate_message": { - "doc": "Valgfri egendefinert tekst for topptarifftipset som legges til i startvarselet. Malvariabler: {device}, {price}. Tomt bruker den innebygde standarden.", + "doc": "Egen tekst for topptarifftipset. Malvariabler: {device}, {price}. Tomt bruker standarden.", "label": "Melding om topptariff" }, "peak_rate_threshold": { - "doc": "Når en syklus starter og gjeldende pris per kWh er på eller over denne verdien, legges et topptarifftips til i startvarselet. 0 eller tomt deaktiverer tipset.", + "doc": "Hvis prisen per kWh er på eller over denne når en syklus starter, legges et topptarifftips til i startvarselet. 0 eller tomt slår det av.", "label": "Terskel for topptariff (per kWh)" }, - "dtw_bandwidth": { - "label": "Toleranse for formgjenkjenning", - "doc": "Hvor mye gjenkjenningen kan forvrenge en syklus i tid for å passe til formen på et lagret program. Høyere tillater mer hastighetsvariasjon; for høyt risikerer å gjenkjenne feil program." - }, - "corr_weight": { - "label": "Vekt: form kontra nivå", - "doc": "Balanse mellom kurveform og absolutt effektnivå når en gjenkjenning vurderes. Høyere favoriserer formen; lavere favoriserer å treffe effektnivået." - }, - "duration_weight": { - "label": "Vekt på varighet", - "doc": "Hvor sterkt et program med typisk kjøretid som passer til denne syklusen, foretrekkes." - }, - "energy_weight": { - "label": "Vekt på energi", - "doc": "Hvor sterkt et program med typisk energiforbruk som passer til denne syklusen, foretrekkes." - }, "store_brand": { "label": "Apparatets merke", - "doc": "Valgfritt. Apparatets merke, valgt fra fellesskapskatalogen. Brukes til å finne og dele matchende referanseopptak. La stå tomt hvis du ikke bruker nettfunksjoner." + "doc": "Valgfritt. Apparatets merke fra fellesskapskatalogen, brukt til å finne og dele referanseopptak. La stå tomt hvis du ikke bruker nettfunksjoner." }, "store_model": { "label": "Apparatets modell", - "doc": "Valgfritt. Apparatets modell, valgt fra fellesskapskatalogen når et merke er angitt. Hvis modellen din ikke er oppført, kan du legge den til i katalogen." + "doc": "Valgfritt. Apparatets modell fra fellesskapskatalogen (velg et merke først). Hvis den ikke er oppført, kan du legge den til." }, "show_contributor": { "doc": "Viser tilskrivningen 'av ' på fellesskapsapparater og referansesykluser." }, - "enable_phase_matching": { - "label": "Fasebevisst tid som gjenstår", - "doc": "Del hver pågående syklus inn i faser (oppvarming, vask, sentrifugering) og budsjetter tiden som gjenstår per fase, blandet med det klassiske estimatet - tidlig i syklusen støtter den seg på fasebudsjettet og mot slutten på det klassiske estimatet. Dette tilpasser nedtellingen til hvor lenge maskinen din faktisk varmer opp og kjører, noe som er mest merkbart i første halvdel av en syklus. Av = bare det klassiske estimatet. Bare visningen av tid som gjenstår påvirkes; programsamsvar og syklusdeteksjon er uendret." - }, - "keep_min_score": { - "label": "Min. matchingspoeng", - "doc": "Laveste likhetspoeng en kandidat trenger for å bli i matcheprosessen. Standard 0,1 er bevisst tillåtende – det tillater selv svake kandidater og stoler på at senere trinn finner best samsvar. Øk for å luke ut usannsynlige profiler tidligere; senk for å utvide den innledende kandidatpoolen." - }, - "dtw_blend": { - "label": "DTW-blanding", - "doc": "Hvor mye DTW-justeringsscoren erstatter kjerneresultatet fra Trinn 2. 0 = bruk bare Trinn 2-scoren, 1 = bruk bare DTW-scoren, 0,5 (standard) = lik blanding. Øk for å stole mer på tidsjustering når programmer har like effektnivåer men ulike timingmønstre." - }, - "dtw_ensemble_w": { - "label": "DTW-ensemblemiks", - "doc": "I ensemble-DTW-modus (standard) er dette vekten på skalert L1 kontra derivativ DTW (DDTW). 1,0 = bare skalert L1, 0 = bare DDTW, 0,7 = standard. Den skalerte varianten sammenligner effektnivåer; den deriverte reagerer på hvordan effekten endres over tid. Ensemble blander begge." - }, - "dtw_ddtw_scale": { - "label": "Derivativ DTW-skala", - "doc": "Halvmetningsavstand for derivativ DTW-scoring. Scoren halveres når DDTW-avstanden er lik denne verdien. Lavere = mer følsom for formforskjeller. Standard 30 er kalibrert for typiske husholdningsapparaters effektprofiler. Påvirker bare ensemble- og ddtw-DTW-modi." - }, - "dtw_refine_top_n": { - "label": "DTW-forfiningsantall", - "doc": "Hvor mange toppkandidater fra Trinn 2 som beregnes på nytt av DTW. DTW er mer kostbart, så det brukes bare på de N beste. Standard 5. Øk (til 7-9) hvis riktig profil noen ganger bare når 4. eller 5. plass etter Trinn 2 – DTW kan redde den. Lavere verdi gir litt raskere matching." - }, - "duration_scale": { - "label": "Varighetsskarphet", - "doc": "Skarphet i Trinn 4 sitt straff for varighetsoverensstemmelse. Det er log-forholdet der overensstemmelsesscoren halveres. Lavere = strengere: en varighetsavvik straffes mer. Standard 0,175 tilsvarer omtrent 18% varighetstolerans ved halvvekt. Par med Varighetsvekt." - }, - "energy_scale": { - "label": "Energiskarphet", - "doc": "Skarphet i Trinn 4 sitt straff for energioverensstemmelse. Lavere = strengere: et energiavvik straffes mer. Standard 0,25 er bevisst mer tillåtende enn varighetsskarphet fordi energi varierer med last. Par med Energivekt." - }, "power_profile_interval_min": { "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." + "doc": "Bucket-størrelse for attributtet power_profile (gjennomsnittlig watt per slot) som planleggere som EMHASS og tibber_prices bruker. Mindre beholder korte topper; større glatter kurven. Standard 15 min. Påvirker ikke deteksjonen." }, "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." + "doc": "Kun iOS. Oppdater live-fremdriften stille, med en batterisparende prioritet som iOS kan samle opp. Den første oppdateringen og start-/sluttvarslene varsler fortsatt. Av: hver oppdatering varsler." }, "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." + "doc": "Kun Android. Behold live-fremdriftsvarselet ved trykk. Av: et trykk lukker det." }, "notify_live_click_action": { "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." + "doc": "Hva et trykk på varselet fra dette apparatet åpner. Tom: WashData-panelet for dette apparatet. Angi en dashbord-sti (f.eks. /lovelace/laundry) eller URL, eller \"none\" for ingen trykkhandling." }, "door_opens_at_end": { "label": "Dør åpnes automatisk ved avslutning", - "doc": "For oppvaskmaskiner som spretter opp døren på slutten av syklusen for tørking (AirDry og lignende). Med dette aktivert vil en døråpning under en aktiv syklus ikke sette den på pause for alltid; hvis døren forblir åpen i den konfigurerte oppholdstiden, behandler WashData syklusen som ferdig. En kort åpning (legge til et element) ignoreres. Krever en dørsensorenhet." + "doc": "For oppvaskmaskiner som åpner døren på slutten for å tørke (f.eks. AirDry). En dør som står åpen i ventetiden nedenfor, avslutter syklusen i stedet for å sette den på pause; en kort åpning ignoreres. Krever en Dørsensorenhet." }, "door_end_dwell_seconds": { "label": "Dørens åpningstid ved avslutning", - "doc": "Hvor lenge døren må forbli åpen før WashData avslutter syklusen, når \"Dør åpnes automatisk ved avslutning\" er aktivert. Lang nok til å ignorere rask tillegging av et fat (standard 60 s), kort nok til å avslutte raskt når maskinen spretter opp døren." + "doc": "Med \"Dør åpnes automatisk ved avslutning\" på: hvor lenge døren må stå åpen før syklusen slutter. Lenge nok til å ignorere at en tallerken settes inn (standard 60 s)." }, "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." + "doc": "Hvilke typer sykluser som former hvert programs kurve og matching. Å fjerne haken for en type sletter ingenting; nyttig hvis du ikke stoler på importerte data. Statistikken teller alltid bare denne maskinens sykluser. Å fjerne alle ignoreres." }, "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." + "doc": "Beregn hver syklus til prisen i hvert øyeblikk den kjørte, i stedet for prisen da den sluttet. Krever en prisenhet. Behandle historikk (Diagnostikk) beregner eldre sykluser på nytt." }, "unload_confirm_entity": { "label": "Utlastingsbekreftelsesenhet", - "doc": "Valgfri enhet der en aktivering betyr at vasken er tatt ut, for maskiner som ikke kan ha en dørsensor. Alle typer fungerer: en Zigbee- eller NFC-knapp, en input_button-hjelper, en bevegelsessensor foran maskinen, en scene. Enhver tilstandsendring teller som en bekreftelse, bortsett fra å bli slått av og å bli utilgjengelig. Å sette denne aktiverer også utlastingspåminnelsen på en enhet uten Dørensorenhet." + "doc": "Valgfritt. En hvilken som helst entitet som bekrefter at vasken er tatt ut, for maskiner uten dørsensor (en knapp, input_button, bevegelsessensor, scene). Hver tilstandsendring teller, unntatt å slå seg av eller bli utilgjengelig. Aktiverer også utlastingspåminnelsen." }, "unload_track_without_door": { "label": "Bekreft utlasting manuelt", - "doc": "Kjør utlastingspåminnelsen på en enhet uten Dørensorenhet og uten Utlastingsbekreftelsesenhet, og bekreft utlastingen fra din egen automasjon i stedet: trykk på knappenheten \"Merk som utlastet\", eller kall handlingen ha_washdata.mark_unloaded. Ignoreres når en Dørensorenhet er angitt, siden den allerede gir signalet." + "doc": "Kjør utlastingspåminnelsen uten dørsensor eller bekreftelsesentitet. Bekreft utlastingen fra din egen automasjon med knappen \"Merk som utlastet\" eller tjenesten ha_washdata.mark_unloaded. Ignoreres når en Dørsensorenhet er angitt." }, "notify_icon_color": { "label": "Varselfarge", - "doc": "Aksentfarge for varslene fra dette apparatet, slik at en vaskemaskin, en tørketrommel og en oppvaskmaskin er lette å skille fra hverandre. iOS farger ikonet og fremdriftslinjen i Liveaktiviteten på låst skjerm og i Dynamic Island. Android får den samme fargen, men hva den gjør med den, er opp til telefonen: noen farger ikonet, og mange (særlig Samsung One UI) ignorerer den helt. Begge forstår hex-verdier (f.eks. #03A9F4). La stå tomt for plattformens standard." + "doc": "Aksentfarge for varslene fra dette apparatet, slik at vaskemaskin, tørketrommel og oppvaskmaskin er lette å skille. På iOS fyller den sirkelen bak ikonet og farger linjen i Liveaktiviteten. Mange Android-telefoner (særlig Samsung) ignorerer den. Bruk hex, f.eks. #03A9F4. Tom: plattformens standard." + }, + "share_name": { + "doc": "Vis kontonavnet ditt som opplaster av sykluser du deler. Av: delt uten navn." } }, "setting_group": { @@ -1710,9 +1614,6 @@ "door_pause": { "label": "Dør og pause" }, - "dtw_refinement": { - "label": "DTW-foredling" - }, "duration_gates": { "label": "Varighetsporter" }, @@ -1789,17 +1690,20 @@ "rebuilding": "Bygger opp igjen…", "analyzing": "Analyserer…", "resetting": "Tilbakestiller…", - "reverting": "Tilbakefører…", "trimming": "Beskjærer…", "splitting": "Deler opp…", + "stalled": "Stoppet opp", "deleting": "Sletter…", "imported": "Importert", "preparing": "Forbereder…", - "unlabelling": "Fjerner merking…" + "unlabelling": "Fjerner merking…", + "working": "Arbeider…", + "match_uncertain": "Usikker: {top} eller {runner}", + "match_uncertain_single": "Usikker: kanskje {top}", + "match_sure": "~{pct}% sikker", + "match_uncertain_tip": "WashData har ennå ikke bestemt seg for et program. Tallet viser hvor ofte den ledende gjetningen, {top}, var riktig i tidligere sykluser når den ledet med så mye. Bare til informasjon." }, "suggestion": { - "both_agree": "WashData anbefaler", - "calibrated_label": "Kalibrert", "observed_label": "Observert", "impact": { "auto_label_confidence": { @@ -1814,10 +1718,6 @@ "higher": "Avslutter bare sykluser med betydelig energiforbruk", "lower": "Avslutter også kortere eller energisvake sykluser" }, - "end_repeat_count": { - "higher": "Sluttsignal må gjentas flere ganger – mer robust, litt tregere", - "lower": "Avslutter etter færre repetisjoner – raskere deteksjon, mer risiko for falsk avslutning" - }, "learning_confidence": { "higher": "Lærer bare fra svært sikre treff – langsommere men mer pålitelig", "lower": "Lærer fra flere sykluser – modellen kan bli mer unøyaktig" @@ -1856,40 +1756,25 @@ } }, "reason": { - "auto_label_confidence": "Konfidens ved 15. persentil for {cycles} automatiske etiketter som brukeren aldri korrigerte ({p15}).", "completion_min_seconds": "Halvparten av p05-varigheten for rene sykluser ({minutes} min) på tvers av {cycles} sykluser; filtrerer bort spøkelsessykluser.{excl}", - "dead_zone_batch": "Holdt kort (median for oppstarts-ustabilitetsvindu på tvers av {cycles} sykluser = {s}s) slik at en reell slutt oppdages raskt.", - "dead_zone_from_dip": "Basert på et tidlig effektfall oppdaget ved {s}s.", - "end_energy_default": "Standardanbefalt baseline for støygrensen ved syklusslutt.", - "end_energy_false": "p95 for energi ved falske slutter ({p95}Wh) på tvers av {cycles} sykluser, med gulv på 0.2% av medianens syklusenergi ({floor}Wh).", - "end_energy_no_false": "Ingen falske slutter på tvers av {cycles} sykluser; med gulv på 0.2% av medianens syklusenergi ({floor}Wh).", - "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.", + "match_interval_floored": "Det korteste programmet ({shortest}s) ville begrense dette til {cap}s, under minimumet på {minimum}s, så minimumet brukes: {persistence} treff på rad tar {budget}s, over {pct}% av det programmet. Matching kjører bare når en måling kommer (median={median}s), så det 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.", "min_off_gap": "Basert på {gaps} observerte mellomrom mellom sykluser (p05={p05}s). Enhetsgulv: {floor}s.", "min_off_gap_bridge": "Dimensjonert for å bygge bro over den lengste stille perioden i en syklus: {span}s + 60s buffer, fra {spans} dekkede mellomrom på tvers av {cycles} rene sykluser (holder seg under ditt korteste mellomrom på {ceiling}s mellom to kjøringer).", "min_power": "40% av den laveste aktive effekten ved p05 ({p05}W) på tvers av {cycles} rene sykluser, noe som holder av-terskelen under reelt forbruk.{excl}", - "ml_auto_label": "Laveste konfidens som kvalitetsmodellen fortsatt vurderte som ren (p10 av {cycles} rene sykluser = {p10}).", - "ml_end_repeat": "{false}/{total} sykluser ({pct}%) hadde en pause som sluttdetektoren vurderte til >50%; krever {val} sluttbekreftelse(r).", - "ml_off_delay": "Pauser bekreftet av sluttdetektoren: p95 {p95}s + 60s buffer, fra {pauses} modellverifiserte pauser på tvers av {cycles} sykluser (gulv {floor}s).", "no_update_timeout": "Basert på observert oppdateringskadens (p95={p95}s) * 20 (min 60s).", "off_delay_cadence": "Basert på observert oppdateringskadens (p95={p95}s) * 5", "off_delay_device_floor": "Brukte et enhetsspesifikt sikkert minimum for {device} ({floor}s).", "off_delay_generic_floor": "Brukte et generisk sikkert minimum ({floor}s).", "off_delay_pauses": "Dimensjonert for å vare lenger enn reelle pauser: p95 for pause i syklusen {p95}s + 60s buffer, fra {pauses} pauser på tvers av {cycles} rene sykluser (gulv {floor}s).", - "profile_match_threshold": "p10-konfidens for {cycles} korrekte automatiske etiketter ({p10}); sikkert gulv for direkte gjenkjenning.", - "sampling_interval": "Median for oppdateringsintervallet observert på tvers av {cycles} rene sykluser ({si}s).{excl}", - "smoothing_window": "Dimensjonert for å jevne ut ~30s med avlesninger ved {si}s sampling ({samples} prøver).", - "start_duration": "Holdt kort (~ett samplingsintervall på {si}s) slik at deteksjonen starter så tidlig som mulig, men fortsatt ignorerer enkelttopper.", - "thr_batch": "Holdt like over den laveste aktive effekten ved p05 på tvers av {cycles} sykluser ({p05}W) slik at en start fanges så tidlig som mulig og stoppterskelen holder seg under maskinens laveste driftseffekt.", - "tol_per_profile": "p75 for varighetsvariansen per profil på tvers av {profiles} profiler ({cycles} sykluser); stramme profiler straffes ikke.", - "tol_pooled": "Basert på samlet varighetsvarians for {cycles} nyeste merkede sykluser (p95 avvik={dev}).", - "watchdog": "Holdt så lavt som forsvarlig (like over p95-oppdateringsmellomrommet på {p95}s og minst 2x samplingsintervallet på {median}s, min. 30s) slik at stans fanges raskt uten falske stopp." + "standby_floor": "Alle nylige sykluser trakk fortsatt {idle}W da de ble avsluttet, slik at en terskel under dette aldri kan registrere at apparatet slår seg av. Holdt over dette nivået; de {cycles} rene syklusene i oversikten pauserte aldri under det i mer enn {pause}s.", + "thr_batch": "Like over den laveste aktive effekten (p05) på tvers av {cycles} sykluser ({p05}W), slik at starter fanges tidlig og stoppterskelen holder seg under den laveste driftseffekten.", + "watchdog": "Holdt så lavt som forsvarlig (like over p95-oppdateringsmellomrommet på {p95}s og minst 2x samplingsintervallet på {median}s, min. 30s) slik at stans fanges raskt uten falske stopp.", + "end_energy_floor": "Hevet til den nedre grensen som stoppterskelen ({stop} W) i løpet av av-forsinkelsen ({off_delay} s) gir; under den kan syklusen bare avsluttes via en reservevei.", + "max_duration_ratio_default": "Tilbake til standardverdien: et lavere tak avviser riktig program hver gang en kjøring varer lenger enn vanlig.", + "min_duration_ratio_default": "Tilbake til standardverdien: et høyere gulv hindrer at en pågående syklus gjenkjennes før den er godt i gang." }, "exclusions": { "summary": "Ekskluderte {total} feildetekterte syklus(er): {parts}.", @@ -1907,7 +1792,8 @@ "no_power": "ingen effekt", "no_active_power": "ingen aktiv effekt" } - } + }, + "now_value": "(nå {v})" }, "tab": { "advanced": "Avansert", @@ -1937,6 +1823,11 @@ "wrong_profile": "Feil profil" }, "toast": { + "task_name_required": "Gi oppgaven et navn", + "task_added": "Oppgave lagt til", + "task_save_failed": "Kunne ikke lagre oppgaven: {error}", + "task_removed": "Oppgave fjernet", + "maint_logged": "Loggført som utført: {task}", "catalog_refreshed": "Fellesskapskatalogen er oppdatert", "access_saved": "Tilgangskontroll lagret", "all_wiped": "Alle data slettet", @@ -1944,7 +1835,6 @@ "auto_label_complete": "Auto-etikett fullført", "automation_deleted": "Automatisering slettet", "correction_submitted": "Retting sendt inn", - "cycle_deleted": "Syklus slettet", "cycle_force_stopped": "Syklus tvunget stoppet", "cycle_labelled": "Syklus merket", "cycle_paused": "Syklus stoppet", @@ -1960,11 +1850,9 @@ "group_deleted": "Gruppe slettet", "group_name_required": "Gruppenavn er påkrevd", "group_saved": "Gruppen er lagret", - "import_successful": "Importen var vellykket; integrering omlasting", "json_required": "JSON-data kreves", "legacy_removed": "Eldre handlinger er fjernet", "logs_exported": "Logger eksportert", - "matching_reverted": "Matchende vekter er tilbakestilt til standardverdier", "min_2_profiles": "Velg minst 2 profiler for en gruppe", "ml_training_no_improvement": "Trening fullført: grunnlinje beholdt (ingen forbedring)", "models_reverted": "Modeller på enheten har gått tilbake til grunnlinjen", @@ -1974,8 +1862,8 @@ "pg_preset_loaded": "Forhåndsinnstillingen \"{name}\" er lastet inn", "pg_preset_saved": "Forhåndsinnstillingen \"{name}\" er lagret", "pg_sugg_loaded": "{n} foreslåtte verdi(er) lest inn - kjør sandkassen for å sammenligne", - "pg_sugg_ml_loaded": "{n} ML-kalibrerte verdi(er) lest inn - kjør sandkassen for å sammenligne", "phase_name_required": "Fasenavn er påkrevd", + "phase_reset": "\"{name}\" tilbakestilt til den innebygde fasen", "phase_updated": "Fase oppdatert", "phases_saved": "Faser lagret", "preferences_saved": "Preferansene er lagret", @@ -2022,14 +1910,10 @@ "store_name_required": "Skriv inn et profilnavn", "store_import_failed": "Import mislyktes: {error}", "store_imported": "Importert til {profile}", - "store_program_required": "Skriv inn et programnavn", "store_no_appliance": "Angi apparatets merke og modell i Innstillinger først.", "store_share_failed": "Deling mislyktes: {error}", "store_shared": "Delt i fellesskapsbutikken, venter på gjennomgang.", "appliance_added": "Apparat lagt til, venter på godkjenning", - "thanks_confirming": "Takk for bekreftelsen", - "device_approved": "Godkjent av fellesskapet", - "rating_saved": "Kvalitetsvurdering lagret", "brand_added": "Merke lagt til, venter på godkjenning", "profile_added": "Profil lagt til, venter på godkjenning", "saved_except_conflicts": "Lagret. Løs de merkede konfliktene for å lagre resten.", @@ -2053,13 +1937,58 @@ "odometer_save_failed": "Kunne ikke lagre totalen: {error}", "contrib_device_changed": "Lagt til i fellesskapsbutikken, men apparatet ble byttet mens skjemaet var åpent. Velg det i Innstillinger.", "store_pick_profile_share": "Velg en profil å dele til", - "remove_failed": "Fjerning mislyktes: {error}" + "remove_failed": "Fjerning mislyktes: {error}", + "analysis_complete": "Analysen er fullført: {count} forslag", + "analysis_failed": "Analyse mislyktes: {error}", + "auto_detect_failed": "Automatisk gjenkjenning mislyktes: {error}", + "auto_label_failed": "Automatisk merking mislyktes: {error}", + "auto_label_done": "Auto-merking fullført: {count} sykluser merket, {skipped} ikke merket.", + "auto_label_done_one": "Auto-merking fullført: {count} syklus merket, {skipped} ikke merket.", + "auto_label_done_other": "Auto-merking fullført: {count} sykluser merket, {skipped} ikke merket.", + "auto_label_cancelled": "Auto-merking avbrutt etter at {count} sykluser ble merket.", + "auto_label_cancelled_one": "Auto-merking avbrutt etter at {count} syklus ble merket.", + "auto_label_cancelled_other": "Auto-merking avbrutt etter at {count} sykluser ble merket.", + "could_not_load_cycle": "Kunne ikke laste inn syklus: {error}", + "create_failed": "Oppretting mislyktes: {error}", + "cycles_deleted": "{count} syklus(er) slettet", + "debug_cleared": "{count} feilsøkingsspor fjernet", + "discard_failed": "Forkasting mislyktes: {error}", + "error": "Feil: {error}", + "export_failed": "Eksport mislyktes: {error}", + "import_failed": "Import mislyktes: {error}", + "invalid_json": "\"{key}\" er ikke gyldig JSON - rett det eller tøm feltet før du lagrer.", + "label_failed": "Merking mislyktes: {error}", + "ml_training_promoted": "Trening fullført: {count} modell(er) aktivert", + "pause_failed": "Pause mislyktes: {error}", + "phase_created": "Fase \"{name}\" opprettet", + "phase_deleted": "Fase \"{name}\" slettet", + "profile_created": "Profil \"{name}\" opprettet", + "rename_failed": "Omdøping mislyktes: {error}", + "save_failed": "Lagring mislyktes: {error}", + "split_complete": "Delt i {count} sykluser", + "start_failed": "Start mislyktes: {error}", + "stop_failed": "Stopp mislyktes: {error}", + "update_failed": "Oppdatering mislyktes: {error}", + "store_download_skipped": "{n} opptak hoppet over: for korte, med for mange hull eller allerede på apparatet ditt.", + "store_import_low_quality": "Dette opptaket er for kort, har for mange hull eller er urealistisk og kan ikke importeres.", + "store_import_duplicate": "Dette opptaket er allerede på apparatet ditt.", + "device_approved": "Godkjent av fellesskapet", + "thanks_confirming": "Takk for bekreftelsen", + "rating_saved": "Kvalitetsvurdering lagret", + "store_download_partial": "{n} program(mer) kunne ikke lastes ned; prøv igjen senere.", + "import_no_restore_point": "Importert, men det kunne ikke lagres et gjenopprettingspunkt, så denne importen kan ikke angres", + "import_undo_failed": "Kunne ikke angre importen: {error}", + "import_undone": "Import angret: {profiles} profil(er) og {cycles} syklus(er) gjenopprettet" }, "trend": { "down": "Trender nedover", "up": "Trender opp" }, "maint": { + "salt": "Fyll på salt", + "rinse_aid": "Fyll på glansemiddel", + "lint_filter": "Rengjør lofilter", + "condenser_clean": "Rengjør kondensator", "bearing_service": "Lagerservice", "descale": "Avkalking", "drum_clean": "Rengjør trommel", @@ -2071,12 +2000,14 @@ "status_tip": "Syklusresultat: Fullført (naturlig slutt), Avbrutt (brått effektfall), Tvangsstoppet (manuelt) eller Trenger gjennomgang (tilbakemelding venter).", "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, 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.", + "energy_tip": "Totalt energiforbruk (kWh), beregnet fra effekt over tid.", + "cost_tip": "Energikostnad for denne syklusen, låst ved fullføring. Med Tidsvektet kostnad på beregnes hvert øyeblikk til sin egen pris; ellers til prisen ved slutten. Angi en pris under Innstillinger for å fylle ut dette.", "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.", - "cost_dynamic_tip": "Tidsvektet: beregnet til prisen som gjaldt under syklusen (effektivt {price} {cur}/kWh)." + "cost_dynamic_tip": "Tidsvektet: beregnet til prisen som gjaldt under syklusen (effektivt {price} {cur}/kWh).", + "dbg_conf": "Konf.", + "dbg_mae": "MAE", + "dbg_corr": "Korr." }, "stat": { "avg": "gj.sn.", @@ -2100,7 +2031,10 @@ "notify_finish": "Et ferdigvarsel ville bli sendt.", "notify_milestone": "Et milepælvarsel ville bli sendt.", "notify_held": "Et varsel ble holdt tilbake på grunn av stilletimer.", - "finished": "Syklusen nådde en sluttilstand og stoppet." + "finished": "Syklusen nådde en sluttilstand og stoppet.", + "match_reverted": "Programmet falt tilbake til oppdaging: poengsummen falt langt under sin egen topp.", + "verified_pause": "Den direkte justeringen bekreftet (eller opphevet) en pause inne i programmet. Så lenge den gjelder, avsluttes ikke syklusen.", + "stalled": "Syklusen lå på standbyforbruk lenger enn dette programmet noen gang tar pause der, med arbeid igjen. Vises som Pause (Stoppet opp); syklusen forblir åpen." }, "store": { "search_ph2": "Søk etter merke eller modell…", @@ -2109,11 +2043,7 @@ "programs_count": "Programmer: {n}", "browse": "Bla gjennom", "device": "Enhet", - "favorites": "Favoritter", - "search_ph": "Søk etter merke eller modell…", "no_results": "Ingen samsvarende apparater funnet. Prøv et annet søk.", - "n_programs": "{n} programmer", - "n_cycles": "{n} sykluser", "no_programs": "Ingen delte programmer for dette apparatet ennå.", "no_cycles": "Ingen referansesykluser delt for dette programmet ennå.", "anon": "anonym", @@ -2125,19 +2055,21 @@ "merge_target": "Slå sammen med profil", "description": "Beskrivelse (valgfritt)", "contributed_by": "av {name}", + "website": "Fellesskapsbutikkens nettsted ↗", + "website_tip": "Åpne fellesskapsbutikkens nettsted", + "favorites": "Favoritter", "rating_summary": "★ {avg} ({n})", "no_ratings": "Ingen vurderinger ennå", - "website": "Fellesskapsbutikkens nettsted ↗", - "website_tip": "Åpne fellesskapsbutikkens nettsted" + "unreachable": "Kunne ikke nå fellesskapsbutikken. Sjekk tilkoblingen og prøv igjen." }, "settings": { "gear": { - "title": "Innstillinger" + "title": "Preferanser" } }, "link": { - "manual": "Håndbok ↗", - "add_appliance": "Legg til apparatet ditt" + "maintenance_supporter": "Maintenance Supporter på GitHub ↗", + "manual": "Håndbok ↗" }, "tip": { "add_brand": "Merke ikke oppført? Legg det til i fellesskapskatalogen", @@ -2171,7 +2103,13 @@ "rebuild": { "envelopes": "Bygger opp envelopes på nytt" }, - "cancelling": "Avbryter..." + "cancelling": "Avbryter...", + "auto_label": { + "running": "Merker sykluser automatisk" + }, + "store_download": { + "running": "Laster ned fra butikken" + } }, "setup": { "hdr": { @@ -2180,26 +2118,24 @@ }, "phase0": { "washer": "WashData oppdager allerede syklusene dine. Spill inn den første syklusen for å aktivere programnavn og tidsanslag.", - "dishwasher": "WashData følger med. Oppvaskmaskiner har komplekse sykluser – det anbefales på det sterkeste å spille inn den første syklusen. Hvis en oppdaget syklus kjører for lenge, bruk syklusredigereren til å trimme den før du lagrer den som en profil.", + "dishwasher": "WashData følger med. Oppvasksykluser er komplekse, så ta opp den første syklusen din. Hvis en oppdaget syklus varer for lenge, beskjær den i syklusredigereren før du lagrer den som en profil.", "generic": "WashData følger med. Spill inn eller merk en oppdaget syklus for å begynne å bygge opp profiler." }, "phase1a": { - "labelled": "Bra start – det første programmet ditt er lagret. For de reneste dataene kan du vurdere å spille inn neste syklus med opptakswidgeten." + "labelled": "God start: det første programmet ditt er lagret. For de reneste dataene, ta opp neste syklus med opptakeren." }, "phase1b": { - "recorded": "Opptaket ditt ble lagret som {profile_name}. Spill nå inn eller merk de andre vanlige programmene dine for å bygge opp dekning." + "recorded": "Opptaket ditt ble lagret som {profile_name}. Ta nå opp eller merk de andre vanlige programmene dine." }, "phase1c": { - "verify": "Du har {count} programmer fra fellesskapet. Kjør en syklus for å bekrefte at WashData gjenkjenner den riktig – gjenkjenningen forbedres etter hvert som enheten bygger opp sin egen historikk." + "verify": "Du har {count} programmer fra fellesskapet. Kjør en syklus for å sjekke at WashData gjenkjenner den; matchingen blir bedre etter hvert som enheten bygger opp sin egen historikk." }, "phase2": { - "cluster": "WashData har sett {count} sykluser som ikke samsvarer med noe lagret program – de ligner på hverandre. Vil du opprette en ny profil for dem?", + "cluster": "WashData har sett {count} like sykluser som ikke matcher noe lagret program. Opprette en ny profil for dem?", "unmatched": "Den siste syklusen din samsvarte ikke med noe lagret program. Er det et nytt program?" }, "phase3": { - "suggestions": "WashData har innstillingsanbefalinger basert på syklushistorikken din – gå gjennom dem for å forbedre deteksjonsnøyaktigheten.", - "groups": "Noen av profilene dine ser ut som det samme programmet ved forskjellige temperaturer. Organiser dem i en gruppe for bedre gjenkjenning.", - "phases": "Legg til programfaser i {profile_name} for mer nøyaktige anslag på gjenværende tid." + "suggestions": "WashData har innstillingsforslag basert på syklushistorikken din. Gå gjennom dem for å forbedre deteksjonen." }, "phase4": { "healthy": "Denne enheten er fullt konfigurert ({profile_count} profiler)." @@ -2212,12 +2148,58 @@ "create_from_cluster": "Opprett profil fra disse syklusene", "create_profile": "Opprett en profil for den", "review_suggestions": "Se gjennom anbefalinger", - "organise_profiles": "Organiser profiler", - "configure_phases": "Konfigurer faser", "skip_step": "Hopp over dette trinnet", "skip_forever": "Ikke vis igjen", "hide_guidance": "Skjul veiledning", "show_guidance": "Vis veiledning" } + }, + "access": { + "edit": "Rediger", + "full": "Full", + "inherit": "Arv", + "none": "Ingen (skjult)", + "read": "Les" + }, + "pg_label_reason": { + "no_winner": "Ingen programmer passet", + "below_floor": "Under lærings-konfidensgraden", + "ambiguous": "Merket som tvetydig", + "margin": "For nær nummer to", + "unknown_profile": "Programmet finnes ikke lenger", + "not_evaluated": "Ikke vurdert" + }, + "diagram": { + "accum": "akkumulert", + "auto": "auto", + "below_off": "under = av", + "confirmed": "bekreftet", + "energy_counts": "energi teller", + "feedback": "tilbakemelding", + "gap_below_min": "gap under min = én syklus", + "heat_phase": "varmefase", + "held_at_100": "holdt på 100%", + "ignored": "ignorert", + "kept": "beholdt", + "max": "maks", + "min": "min", + "minus_tol": "-tol", + "no_match": "ingen treff", + "no_updates": "ingen oppdateringer", + "off_delay_wait": "av-forsinkelse", + "off_gap": "av-gap", + "plus_tol": "+tol", + "profile": "profil", + "reading_interval": "typisk måleintervall", + "sensor_offline": "sensor frakoblet: tvangsstopp", + "si": "SI", + "spike_ignored": "topp ignorert", + "spike_ignored_short": "topp: ignorert", + "start": "start", + "stop": "stopp", + "tail_energy_resets": "restenergi > terskel: timer nullstilles", + "thr": "tersk.", + "too_short": "for kort", + "tumble_kept": "tumlepulser beholdt" } } diff --git a/custom_components/ha_washdata/translations/panel/nl.json b/custom_components/ha_washdata/translations/panel/nl.json index 4e88c8d2..9ed6b247 100644 --- a/custom_components/ha_washdata/translations/panel/nl.json +++ b/custom_components/ha_washdata/translations/panel/nl.json @@ -3,6 +3,7 @@ "min": "min", "entity_not_found": "Entiteit niet gevonden", "running_long": "Loopt langer", + "stalled": "Vastgelopen", "remaining": "resterend", "progress": "voortgang", "show_time_remaining": "Resterende tijd tonen", @@ -48,16 +49,16 @@ } }, "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_tip": "Tegen het einde wordt dit programma ongeveer {mins} min stil en trekt daarna ongeveer {watts} W gedurende {secs} s. Gezien in {seen} van {measured} gemeten cycli.", "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", + "artifact_tip": "{n} afwijkingen gedetecteerd (bijvoorbeeld de deur werd halverwege de cyclus geopend) - open om ze in de grafiek te zien", + "artifact_tip_one": "{n} afwijking gedetecteerd (bijvoorbeeld de deur werd halverwege de cyclus geopend) - open om deze in de grafiek te zien", + "artifact_tip_other": "{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", "declining": "↘ afnemend", "energy_low": "Lager energieverbruik dan normaal", "energy_spike": "Hoger energieverbruik dan normaal", - "fair_fit": "acceptabele kwaliteit", - "fair_fit_tip": "Matige matchconsistentie: sommige cycli die aan dit profiel zijn toegewezen, hebben lagere betrouwbaarheidsscores. Label meer cycli of neem het profiel opnieuw op om de nauwkeurigheid te verbeteren.", "feedback_requested": "Feedback gevraagd", "golden_cycle": "Geregistreerde referentiecyclus", "duration_outlier": "{n} cyclus/cycli met afwijkende duur", @@ -66,27 +67,38 @@ "needs_review": "Moet worden beoordeeld", "overrun": "Langer gelopen dan normaal", "poor_fit": "slechte kwaliteit", - "poor_fit_tip": "Inconsistente matchgeschiedenis: overweeg om dit profiel opnieuw op te bouwen", - "restart_gap_tip": "{n} HA-herstart-gat: het vermogensspoor heeft een gat", + "restart_gap_tip": "{n} HA-herstart-gaten: het vermogensspoor heeft gaten", + "restart_gap_tip_one": "{n} HA-herstart-gat: het vermogensspoor heeft een gat", + "restart_gap_tip_other": "{n} HA-herstart-gaten: het vermogensspoor heeft gaten", "reviewed": "Beoordeeld", "steady": "→ stabiel", "underrun": "Sneller klaar dan normaal", - "using_defaults": "Standaardwaarden voor verzending gebruiken", - "using_tuned": "Gebruik van afgestemde gewichten", "overrun_ratio": "({x}x verwacht)", "underrun_ratio": "({pct}% van verwacht)", - "awaiting_tip": "Wacht op goedkeuring door de community", - "awaiting_n": "Wacht op goedkeuring · {n} bevestigd", - "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.", - "armed": "(geldt voor de volgende cyclus)" + "armed": "(geldt voor de volgende cyclus)", + "awaiting_tip": "Wacht op goedkeuring door de community", + "awaiting_n": "Wacht op goedkeuring · {n} bevestigd", + "awaiting": "Wacht op goedkeuring", + "approved": "Goedgekeurd", + "matcher_unused": "Nog niet herkend in je cycli", + "matcher_used": "Herkend in {n} van je cycli", + "matcher_used_one": "Herkend in {n} van je cycli", + "matcher_used_other": "Herkend in {n} van je cycli", + "matcher_used_tip": "Cycli op dit apparaat die WashData zelf aan dit geïmporteerde programma heeft gekoppeld. Een import die nooit wordt herkend, past misschien niet bij uw apparaat; u kunt hem verwijderen. Lokaal geteld; er wordt niets naar de communitystore gestuurd." }, "btn": { + "log_done_task": "Als gedaan registreren: {task}", + "add_task": "Taak toevoegen", + "pg_preset_delete": "Voorinstelling verwijderen", + "pg_preset_load": "Voorinstelling laden", + "pg_preset_load_tip": "De waarden in de speeltuin vervangen door deze voorinstelling (de actieve instellingen blijven ongewijzigd)", + "pg_preset_save": "Opslaan als voorinstelling", + "pg_preset_save_tip": "Alle onderstaande waarden opslaan als voorinstelling met een naam voor dit apparaat", "reset_zoom": "Zoom resetten", "set_brand_model": "Merk en model instellen", "refresh_catalog": "Catalogus vernieuwen", @@ -97,12 +109,11 @@ "add_timer": "+ Timer toevoegen", "apply_all": "Alles toepassen", "apply_label": "Etiket toepassen", - "apply_set_b": "Set B toepassen", "apply_split": "Splitsing toepassen", "apply_trim": "Bijsnijden toepassen", "auto_detect_split": "Automatische detectie", "auto_label_cycles": "Cycli automatisch labelen", - "auto_label_cycles_tip": "Wijs automatisch profielnamen toe aan ongelabelde cycli waarvan de matchbetrouwbaarheid de drempel haalt", + "auto_label_cycles_tip": "Geef ongelabelde cycli een naam als hun matchbetrouwbaarheid de drempel haalt.", "cancel": "Annuleren", "clear_debug": "Wis foutopsporingsgegevens", "clear_splits": "Wissen", @@ -112,8 +123,6 @@ "convert_to_automation": "Converteren naar automatisering", "correct": "Corrigeren…", "create": "Creëren", - "create_from_cluster": "Profiel aanmaken van {count} cycli", - "create_profile": "+ Profiel aanmaken", "delete": "Verwijderen", "delete_group": "Groep verwijderen", "delete_group_tip": "Verwijder alleen deze groep - de ledenprofielen blijven behouden", @@ -124,22 +133,19 @@ "done": "Klaar", "edit": "Bewerken", "export": "Exporteren", - "export_json": "Exporteren naar JSON", "force_stop": "Forceer stop", "force_stop_tip": "Huidige cyclus onmiddellijk beëindigen en markeren als geforceerd gestopt", "from_template": "Van sjabloon ▾", "ignore": "Negeren", "import_json": "Importeren vanuit JSON", - "import_overwrite": "Importeren (overschrijft gegevens)", "inspect": "Inspecteren", "label": "Labelen", - "load": "Laden", "load_more": "Meer laden", "manage": "Beheren", "merge": "Samenvoegen", "new_automation": "+ Nieuwe automatisering", "new_group": "+ Nieuwe groep", - "new_group_tip": "Groepeer bijna identieke profielen (zelfde vorm/duur, verschillende temperatuur of centrifugeersnelheid) zodat de matcher betrouwbaar tussen hen kan kiezen", + "new_group_tip": "Groepeer bijna identieke profielen (zelfde vorm, andere temperatuur of centrifuge) zodat de herkenning ze uit elkaar kan houden.", "new_phase": "+ Nieuwe Fase", "new_profile": "+ Nieuw profiel", "new_profile_tip": "Maak een nieuw programma profiel van een bestaande gelabelde cyclus of opname", @@ -147,36 +153,25 @@ "on_cycle_started": "Op cyclus gestart", "pause_cycle": "Pauze", "pause_cycle_tip": "De actieve cyclus pauzeren – het apparaat hervat waar het was gestopt", - "pg_apply_device": "Toepassen op apparaat", - "pg_autofill": "Automatisch invullen", - "pg_load_calibrated": "Gekalibreerde (ML) laden ({n})", - "pg_load_calibrated_tip": "Laadt ML-gekalibreerde instellingen als speeltuin-overschrijvingen", "pg_load_live": "Actieve instellingen laden", "pg_load_live_tip": "De huidige instellingen van de integratie opnieuw inlezen en alle wijzigingen in de speeltuin weggooien", "pg_load_suggested": "Suggesties laden ({n})", "pg_load_suggested_tip": "Laadt de huidige suggesties van de auto-tuner als speeltuin-overschrijvingen", - "pg_preset_delete": "Voorinstelling verwijderen", - "pg_preset_load": "Voorinstelling laden", - "pg_preset_load_tip": "De waarden in de speeltuin vervangen door deze voorinstelling (de actieve instellingen blijven ongewijzigd)", - "pg_preset_save": "Opslaan als voorinstelling", - "pg_preset_save_tip": "Alle onderstaande waarden opslaan als voorinstelling met een naam voor dit apparaat", "pg_publish_all": "{n} opslaan in integratie", "pg_publish_one": "{label} opslaan in de integratie", - "play": "Afspelen", "process": "Verwerken", "process_history": "Verwerk nu", "process_recording": "Opname opslaan", "process_tip": "Sla de geregistreerde trace op als een nieuw of bestaand profiel", "rebuild": "Enveloppen opnieuw opbouwen", "rebuild_envelope": "Envelop opnieuw opbouwen", - "rebuild_tip": "Herbereken de verwachte vermogensenvelop (min/max-band) voor alle profielen op basis van hun gelabelde cycli – voer uit na het labelen van nieuwe cycli of het corrigeren van bestaande", + "rebuild_tip": "Bouw de verwachte vermogensband van elk profiel opnieuw op uit de gelabelde cycli. Voer uit na het labelen of corrigeren van cycli.", "rec_start_tip": "Begin met het opnemen van het vermogensspoor van het apparaat – start vlak voor het uitvoeren van een cyclus", "rec_stop": "Stop", "rec_stop_tip": "Stop de opname en bewaar de vastgelegde trace voor beoordeling", "record": "Begin met opnemen", "refresh": "Vernieuwen", "refresh_diag_tip": "Opslagstatistieken opnieuw laden", - "refresh_logs_tip": "Meest recente logrecords opnieuw laden", "refresh_settings_tip": "Instellingen opnieuw laden van de server", "relabel": "Opnieuw labelen ({count})", "remove": "Verwijderen", @@ -184,7 +179,6 @@ "reset": "Opnieuw instellen", "reset_ml_models_tip": "Gooi de verfijnde modellen weg en ga terug naar de ingebouwde modellen. WashData kan ze later opnieuw leren.", "reset_to_builtin": "Reset naar ingebouwde modellen", - "reset_to_defaults": "Terugzetten naar standaardwaarden", "resume_cycle": "Hervatten", "resume_cycle_tip": "Hervat de gepauzeerde cyclus", "revert_settings": "Wijzigingen ongedaan maken", @@ -197,8 +191,6 @@ "run_analysis": "🔍 Voer suggestieanalyse uit", "run_analysis_tip": "Analyseer nu uw geregistreerde cycli en vernieuw de afstemmingssuggesties", "run_auto_label": "Voer Auto-Label uit", - "run_both": "A vs B uitvoeren", - "run_simulation": "Simulatie uitvoeren", "save": "Opslaan", "save_access_control": "Toegangscontrole opslaan", "save_group": "Groep opslaan", @@ -210,10 +202,8 @@ "save_settings": "Instellingen opslaan", "select": "Selecteer", "select_all": "Alles selecteren", - "select_last20": "Laatste 20", "select_none": "Wissen", "show_only": "Alleen weergeven", - "skip_setup": "Installatie overslaan", "split": "Splitsen", "stop": "Stoppen", "submit_correction": "Correctie indienen", @@ -222,36 +212,32 @@ "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.", + "unlabel_selected_tip": "Verwijder het label van de geselecteerde cycli. Ze blijven in uw geschiedenis, maar vormen dit profiel niet meer.", "use": "Gebruik", "wipe_all": "Wis alle gegevens", + "pg_rerun": "Simulatie bijwerken", "delete_selected": "Selectie verwijderen ({n})", - "group_suggest": "Groep {n}: {members}", "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", "import": "Importeren", "disconnect": "Verbinding verbreken", "connect_github": "Verbinden met GitHub", - "save_appliance": "Apparaat opslaan", "share_to_store": "Delen in store", "share": "Delen", - "confirm_appliance": "Dit apparaat bevestigen", - "confirmed": "Bevestigd", "browse_community_setups": "Community-setups doorzoeken", "download_device": "Deze instelling downloaden", "share_device": "⬆ Dit apparaat delen", - "share_device_tip": "Dit apparaat en zijn opgenomen referentiecycli delen in de communitystore, zodat andere gebruikers met hetzelfde apparaat ze kunnen overnemen", + "share_device_tip": "Deel dit apparaat en zijn referentiecycli in de communitystore, zodat eigenaren van dezelfde machine ze kunnen overnemen.", "share_n": "{n} cyclus/cycli delen", "export_selected": "Exporteren (gegevens kiezen)", "export_all": "Alles snel exporteren", - "import_raw": "Geavanceerd: alles vervangen vanuit JSON", "download_export": "Export downloaden", "analyze_import": "Bestand analyseren", "import_selected": "Selectie importeren", "back": "Terug", "mute_suggestion": "Deze instelling niet meer voorstellen", "reset_muted": "Gedempte suggesties herstellen", + "reset_phase_tip": "Bewerking ongedaan maken en de ingebouwde fase herstellen", "import_power_history": "Vermogensgeschiedenis importeren", "hist_read_recorder": "Uit Home Assistant lezen", "hist_scan": "Zoeken naar cycli", @@ -260,7 +246,12 @@ "save_total_cycles": "Totaal opslaan", "clear": "Wissen", "add_brand": "Merk toevoegen", - "add_appliance": "Apparaat toevoegen" + "add_appliance": "Apparaat toevoegen", + "retry": "Opnieuw proberen", + "confirm_appliance": "Dit apparaat bevestigen", + "confirmed": "Bevestigd", + "create_profile_from_gap": "Profiel aanmaken", + "undo_import": "Laatste import ongedaan maken" }, "note": { "off_delay": { @@ -268,10 +259,6 @@ } }, "conflict": { - "anti_wrinkle_exit": { - "exit": "Moet onder de stopdrempel ({stop} W) liggen; anders wordt het anti-kreuk-uitschakelverbruik genegeerd", - "stop": "Moet boven het anti-kreuk-uitschakelverbruik ({exit} W) liggen" - }, "anti_wrinkle_max": { "max": "Moet boven de startdrempel ({start} W) liggen; anders wordt de anti-kreuk-maximale duur omzeild", "start": "Moet onder het maximale anti-kreuk-vermogen ({max} W) liggen" @@ -309,14 +296,14 @@ "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." + "power_sensor": "Dit is de Vermogenssensor van het apparaat, geen prijs. Cycli zouden worden afgerekend in watt per kWh; maak dit veld leeg.", + "energy_sensor": "Dit is de Energiemeter van het apparaat, geen prijs. Cycli zouden worden afgerekend tegen de meterstand per kWh; maak dit veld leeg.", + "device_class": "Deze sensor meet {dc}, geen prijs per kWh. Maak dit veld leeg of kies een tariefsensor.", + "unit": "Deze sensor meet in {unit}, geen prijs per kWh. 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" + "duration": "Moet kleiner zijn dan Time-out zonder update ({to} s) zodat het vastgelopen-alarm eerst kan afgaan", + "timeout": "Moet groter zijn dan Duur pomp vastzitten ({ps} s) zodat het vastgelopen-alarm eerst kan afgaan" }, "start_dur": { "sampling": "Bemonsteringsinterval overschrijdt de startduur ({sdt} s); pieken met één meting kunnen een cyclus openen", @@ -334,10 +321,15 @@ "sampling": "Bemonsteringsinterval moet maximaal de helft van het watchdog-interval ({wi} s) zijn" }, "settings_banner": "Instellingsconflicten: {n}. Controleer de gemarkeerde secties en los ze op voordat u opslaat.", - "settings_banner_btn": "Ga naar eerste" + "settings_banner_btn": "Ga naar eerste", + "end_energy": { + "energy": "Te streng voor Stopdrempel ({w} W) gedurende Uitschakelvertraging ({d} s); de cyclus kan alleen via een terugvalpad eindigen", + "stop": "Eindenergie ({e} Wh in {d} s) staat slechts {p} W toe" + } }, "hdr": { - "ab_comparison": "Vergelijking van instellingen A/B", + "custom_tasks": "Je eigen taken", + "maintenance_supporter": "Meer nodig?", "access_control": "Toegangscontrole", "add_maintenance": "Onderhoudsgebeurtenis toevoegen", "automation_delete": "Verwijder deze automatisering", @@ -346,28 +338,22 @@ "automation_review_this_cycle": "Bekijk deze cyclus", "automations": "Automatiseringen", "clear_debug": "Wis foutopsporingssporen", - "cycle_simulator": "Cyclussimulator", "diagnostics_pref": "Diagnostiek", "display": "Weergave", - "dtw_inspector": "DTW-inspecteur", "export_import": "Exporteren / importeren", - "getting_started": "Aan de slag", "live_power": "Levende kracht", "logs": "Logboeken", - "logs_diagnostics": "Diagnostiek", "maintenance": "Onderhoudsacties", "maintenance_log": "Onderhoudslogboek", "maintenance_reminders": "Serviceherinneringen", "manual_recording": "Handmatige opname", - "manual_recording_tip": "Voer bewust een cyclus uit terwijl WashData het vermogensspoor opneemt. Klik op Begin met opnemen net voor het apparaat start, op Stop wanneer het klaar is, en vervolgens op Proces om het op te slaan als een benoemd profiel.", + "manual_recording_tip": "Neem bewust een cyclus op: druk op Start vlak voordat het apparaat begint en op Stop als het klaar is, en daarna op Verwerken om hem als profiel op te slaan.", "ml_learned": "Wat WashData heeft geleerd", - "ml_matching_tuning": "Verfijning van programma-matching", "ml_settings_card": "Instellingen", "ml_smart_learning": "Slim leren", "my_preferences": "Mijn voorkeuren", "no_automations": "Er zijn nog geen automatiseringen die naar dit apparaat verwijzen.", "panel_settings": "Paneelinstellingen", - "param_sweep": "Parameterscan", "pg_settings_source": "Herkomst van de instellingen", "phase_catalog": "Fasecatalogus", "playground": "Speeltuin", @@ -376,11 +362,8 @@ "status": "Status", "status_graph": "Statusgrafiek", "storage_stats": "Opslagstatistieken", - "test_history": "Testen op geschiedenis", "toggle_sidebar": "Schakel de Home Assistant-zijbalk in of uit", - "tools_and_data": "Hulpmiddelen en gegevens", "wipe_history": "Geschiedenis wissen", - "recommendations": "Aanbevelingen ({n})", "pg_detection_params": "Detectie-instellingen", "pg_outcome": "Simulatieresultaat", "pg_across_cycles": "Over al je cycli", @@ -388,7 +371,8 @@ "online_account": "Communitystore en onlinefuncties", "import_power_history": "Vermogensgeschiedenis importeren", "service_status": "Onderhoudsstatus", - "online_account_tab": "Online & community" + "online_account_tab": "Online & community", + "live_match_debug": "Foutopsporing live-match" }, "health": { "fair": "Acceptabele profielkwaliteit", @@ -396,7 +380,12 @@ "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.", + "task_name": "Taak", + "every_cycles": "Om de (cycli)", + "every_days": "Om de (dagen)", + "new_task_name": "Nieuwe taak", + "days_since_service": "{since} / {total} dagen", + "envelope_position_tip": "Positie op de opgenomen curve van het herkende programma: {pct}%. Bepaald door de cyclus op het profiel uit te lijnen, dus het klopt ook als een run langer of korter is. Wordt bijgewerkt terwijl het apparaat stil is, dus kan achterlopen.", "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})", @@ -408,10 +397,8 @@ "aria_cycle_chart": "Vermogenscurve van de cyclus", "aria_envelope_chart": "Vermogensenvelop van het profiel", "aria_phase_chart": "Grafiek van de fase-editor", - "aria_playground_chart": "Vermogenscurve van de cyclus met versleepbare detectiedrempels", "aria_power_chart": "Vermogensgrafiek", "aria_spaghetti_chart": "Over elkaar gelegde vermogenscurves van cycli", - "aria_sweep_chart": "Grafiek met resultaten van de parametersweep", "artifact_anomaly": "Anomalie", "artifact_high_power": "Hoog vermogen", "artifact_interruption": "Onderbreking", @@ -425,155 +412,105 @@ "cohesion_good": "cohesie {pct}%", "cohesion_low": "⚠ lage cohesie {pct}%", "compare_profiles": "Vergelijk met profielen", - "concurrency": "Gelijktijdigheid", "confidence": "Vertrouwen", "confidence_threshold": "Vertrouwensdrempel", "correct_duration": "Correcte duur (min, optioneel)", "correct_profile": "Correct profiel", - "correlation": "Correlatie", "cost": "Kosten", "create_new_profile": "+ Nieuw profiel aanmaken…", "current_phase": "Huidige fase", - "curves": "Curven", "cycle": "Cyclus", "cycle_date_display": "Weergave cyclusdatum", - "cycle_health": "Gezondheid van de cyclus", - "cycle_index": "cyclusindex →", - "cycles": "Cycli", + "cycle_context": "Recordergeschiedenis rond de cyclus", + "cycle_context_off": "Uit", + "cycle_context_minutes": "{min} min", + "cycle_context_series": "Recordergeschiedenis", + "before_start": "Vóór start", + "after_end": "Na einde", "cycles_count": "Cycli", "cycles_lc": "cycli", - "dashed": "gestreept", "date": "Datum", "debug_traces": "Sporen debuggen", - "default": "Standaard", "default_access_level": "Standaardniveau voor gebruikers die hieronder niet worden vermeld", "default_other": "Standaard (andere apparaten)", "default_tab": "Standaardtabblad bij het openen van het paneel", - "delta": "Δ", "description": "Beschrijving", - "dtw_blend_w": "Mengingsgewicht", - "dtw_blended": "Gemengde score", - "dtw_ddtw": "DDTW-score", - "dtw_l1": "L1-score", - "dtw_matched_profile": "(gematcht profiel van de cyclus)", "duration": "Duur", - "duration_agreement": "Duurovereenkomst", - "emphasis": "Nadruk", "enable_access_control": "Schakel toegangscontrole per gebruiker in", "energy": "Energie", - "energy_agreement": "Energieovereenkomst", - "event_timeline": "Gebeurtenistijdlijn", "event_type": "Gebeurtenistype", "expected": "Verwacht", + "suspected_profile": "{name} (vermoedelijk)", + "runner_up_profile": "{name} (nummer twee)", "expected_duration": "Verwachte duur (min)", "file_kb": "Bestand (kB)", - "final_score": "Eindscore", "from": "Van", "gap_s": "Tussenruimte (s)", "group_name": "Groepsnaam", "head_trim": "Begin bijsnijden (s)", - "health": "Gezondheid", "hide_tabs": "Verberg tabbladen voor niet-beheerders", - "in_use": "In gebruik", "json_data": "JSON-gegevens", "label": "Overeenkomst", "last": "Laatste", "last_checked": "Laatst gecontroleerd:", "load_from_file": "Laden uit bestand", - "mae_score": "MAE-score", "manual_duration": "Handmatige duur (min, optioneel)", "match": "Match", "members": "Leden", - "metric": "Metriek", "mode_existing_profile": "Toevoegen aan bestaand profiel", "mode_new_profile": "Nieuw profiel maken", "models_fine_tuned": "(verfijnde modellen: {count})", - "n_classic_suggestions": "{n} klassiek", - "n_ml_suggestions": "{n} ML", "n_selected": "{n} geselecteerd", - "n_tuning_suggestions": "{n} afstemmingssuggestie", - "na": "n.v.t.", + "n_tuning_suggestions": "{n} afstemmingssuggesties", + "n_tuning_suggestions_one": "{n} afstemmingssuggestie", + "n_tuning_suggestions_other": "{n} afstemmingssuggesties", "new_profile_name": "Nieuwe profielnaam", "notes": "Opmerkingen", - "of": "van", - "outcome": "Uitkomst", "panel_default_tab": "Standaard tabblad", "panel_language": "Paneeltaal", "per_cycle_short": "cyclus", - "pg_autofill_tip": "Bereik automatisch invullen vanaf huidige waarde ({lo}–{hi})", "pg_best_value": "Beste gevonden waarde", "pg_current_value": "Huidige waarde:", "pg_drag_hint": "Sleep de lijn op de grafiek", - "pg_dtw": "DTW", "pg_ev_ending": "Afsluiten", "pg_ev_running": "Actief", "pg_group_advanced": "Randgevallen", "pg_group_detection": "Detectietriggers", "pg_group_timing": "Timingregels", "pg_idle": "Inactief", + "pg_poor_match": "Slechte match", "pg_preset": "Speeltuin-voorinstelling", "pg_preset_name": "Naam van nieuwe voorinstelling", "pg_preset_none": "Kies een voorinstelling…", - "pg_stress_group": "Test voor inactieve beëindiging", - "pg_stress_idle_auto": "Auto", - "pg_stress_idle_w": "Inactief vermogen (W)", - "pg_stress_idle_w_desc": "Overschrijf de automatisch gedetecteerde standby-drempelwaarde (laat leeg voor automatisch).", - "pg_stress_toggle": "Inactieve beëindiging testen", - "pg_stress_toggle_desc": "Simuleert dat het apparaat na het einde van de opname op zijn standby-verbruik blijft - toont of en wanneer WashData de cyclus stopt.", - "pg_last_run": "Laatste run: {date}", - "pg_match_score": "Matchscore", - "pg_pct_of": "{p}% van {total}", - "pg_poor_match": "Slechte match", "pg_strong_match": "Sterke match", - "pg_tile_ambiguous": "Dubbelzinnig", - "pg_tile_ambiguous_tip": "Cycli met een bijna gelijkspel tussen twee programma's", - "pg_tile_detected": "Gedetecteerd", - "pg_tile_detected_tip": "Cycli waarbij het vermogen lang genoeg boven de startdrempel kwam om als actief te worden geregistreerd", - "pg_tile_matched": "Herkend", - "pg_tile_matched_tip": "Cycli die correct aan een bekend programma zijn toegewezen", - "pg_time_left": "Resterende tijd", "pg_weak_match": "Zwakke match", "phase_name": "Fasenaam", "name": "Naam", "phase_timeline": "Fasetijdlijn", "power": "Vermogen", "profile": "Profiel", - "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. Kies er een voordat u het apparaat start, dan wordt het toegepast zodra de volgende cyclus begint.", + "program_tip": "Kies aan welk programma de huidige cyclus wordt gekoppeld. Automatische detectie kiest de beste match. Zet een programma vast als de automatische detectie fout zit; kies er een vóór de start en het geldt voor de volgende cyclus.", "progress": "Voortgang", "raw_socket": "Ruwe stroom", - "read_only": "Alleen-lezen", "ref_cycle": "Referentiecyclus (optioneel)", - "refresh_interval": "Vernieuwingsinterval (s)", "remaining": "Resterend", "reminder_every": "Herinneren om de (cycli)", "remove_label": "- Etiket verwijderen -", "rename_profile": "Naam van profiel wijzigen", - "replay_duration": "Herhaling", "resulting_profile": "Resulterend profiel", "save_mode": "Bewaarmodus", - "score": "Score", - "score_breakdown": "Score-uitsplitsing", "seconds_from_start": "Seconden vanaf het begin", "select_profile": "Selecteer Profiel", "setting": "Instelling", - "settings_a": "Instellingen A (huidig)", - "settings_a_short": "A", "settings_advanced": "Geavanceerd", - "settings_b": "Instellingen B (test)", - "settings_b_short": "B", "settings_basic": "Basis", "settings_detail_level": "Detailniveau van instellingen", - "settings_overrides": "Overschreven instellingen", "show_debug": "Toon live match-foutopsporingskaart op de statuspagina (vertrouwen, dubbelzinnigheid, topkandidaten)", "show_expected": "Verwachte curve-overlay weergeven (overeenkomend profiel, oranje)", "show_raw": "Toon schakelaar voor onbewerkte stopcontactwaarden in live vermogensgrafiek", "sparkline": "Recente trend van cyclusduur", - "stage2": "Stap 2 – kerngelijkenis", - "stage3": "Stap 3 – DTW", - "stage4": "Stap 4 – overeenkomst", "status": "Status", "tail_trim": "Einde bijsnijden (s)", "tap_to_dismiss": "tik op de grafiek om te sluiten", @@ -584,7 +521,6 @@ "ungrouped": "Niet-gegroepeerd", "unlabelled": "Zonder etiket", "unlabelled_paren": "(ongelabeld)", - "warp_path": "Uitlijningspad", "input": "Invoer:", "start": "Start", "end": "Einde", @@ -609,9 +545,8 @@ "n_shown": ", {n} weergegeven", "cycles_title": "Cycli ({n})", "pg_detecting": "Detecteren", - "pg_best_marker": "Beste: {v}", "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", + "pg_time_left_model_tip": "Geschat op basis van de fasen van het programma, geen vaste aftelling", "phase": "Fase", "pg_mode_history": "Op geschiedenis testen", "pg_ev_detected": "Gedetecteerd", @@ -651,12 +586,8 @@ "pg_mode_optimize": "Optimaliseren", "pg_group_matching": "Programmaherkenning", "pg_zoom_hint": "dubbelklik om de zoom te resetten", - "aria_playground_chart2": "Interactieve vermogensgrafiek van de cyclus: beweeg de muis eroverheen om tijd/vermogen af te lezen, scroll om te zoomen, sleep om te verschuiven", "pg_alert_timeout_end": "Beëindigd door time-out, niet door voorspelling", "pg_alert_indefinite": "Zou eindeloos blijven draaien", - "pg_alert_stress_cap": "Veiligheidslimiet bereikt", - "pg_alert_stress_ok": "Inactieve beëindiging: cyclus gestopt", - "pg_alert_stress_warn": "Standby-verbruik boven stoptempel", "task_pg_history": "Testen op geschiedenis", "task_pg_sweep": "Optimaliseren", "task_reprocess": "Opnieuw verwerken", @@ -666,16 +597,9 @@ "pg_recent_runs": "Recente uitvoeringen", "pg_load_run": "Deze uitvoering opnieuw laden", "pg_match_confidence": "Matchbetrouwbaarheid", - "pg_envelope_fit": "Envelop-passing", - "pg_envelope_fit_tip": "Hoe strak de cyclus binnen de opgeslagen vermogensenvelop van dit profiel blijft, een andere invalshoek dan matchbetrouwbaarheid.", "n_imported_note": ", {n} geïmporteerd", "enable_online": "Onlinefuncties inschakelen", - "store_brand": "Merk van apparaat", - "store_model": "Model van apparaat", "anonymous": "Anoniem", - "rate_quality": "Kwaliteit beoordelen", - "rate_n": "{n} sterren", - "approved_only": "Alleen goedgekeurd", "adopt_settings": "Instellingen ook overnemen", "flags": "Vlaggen", "include_phase_map": "Fasekaart opnemen", @@ -692,7 +616,6 @@ "cat_custom_phases": "Aangepaste fasen", "cat_profile_groups": "Profielgroepen", "cat_settings": "Detectie- & herkenningsinstellingen", - "cat_matching_config": "Herkenningsafstemming", "cat_ml_models": "ML-modellen", "cat_feedback": "Feedback & beoordelingslabels", "cat_suggestions": "Suggesties", @@ -715,7 +638,6 @@ "hist_csv_data": "CSV-gegevens", "hist_from_recorder": "Of lees het uit Home Assistant", "hist_since": "Sinds", - "days": "dagen", "hist_keep": "Deze cyclus bewaren", "hist_looks_complete": "volledig", "peak_power_short": "Piek", @@ -735,7 +657,34 @@ "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" + "correct_total_cycles": "Totaal corrigeren", + "hist_reason_already_recorded": "overlapt met een cyclus die WashData al heeft vastgelegd", + "share_consent": "Ik bevestig dat deze cycli normaal zijn voltooid zonder onderbreking", + "show_internal": "Interne instellingen tonen", + "show_internal_tip": "Instellingen die WashData voor u afstemt (sampling, waakhond, matchfrequentie, drempels). Getoond als dit apparaat een eigen waarde, een conflict of een suggestie heeft.", + "rate_n": "{n} sterren", + "rate_quality": "Kwaliteit beoordelen", + "pg_autolabel": "Auto-label", + "pg_label_review": "Vraagt om beoordeling", + "pg_ev_match_reverted": "Match teruggedraaid", + "pg_ev_verified_pause": "Bevestigde pauze", + "n_samples": "{n} metingen", + "n_samples_one": "{n} meting", + "n_samples_other": "{n} metingen", + "n_cycles": "{n} cycli", + "n_cycles_one": "{n} cyclus", + "n_cycles_other": "{n} cycli", + "energy_per_cycle": "{v}/cyclus", + "energy_total": "{v} totaal", + "automation_off": "(uit)", + "ref_cycle_none": "Geen", + "share_name": "Mijn naam tonen bij wat ik deel", + "pg_compare_profile": "Vergelijk met profiel", + "pg_obj_endlag": "Eindvertraging (mediaan)", + "pg_obj_early": "Vroeg-einde-percentage", + "pg_obj_split": "Splitspercentage", + "pg_sweep_cycles": "Recente cycli", + "cat_backfill_cycles": "Cycli gevonden in geïmporteerde vermogensgeschiedenis" }, "log": { "all_levels": "Alle niveaus", @@ -745,11 +694,9 @@ }, "ml": { "builtin_models": "● Ingebouwde modellen gebruiken", - "fit_fair": "Acceptabel", "fit_good": "Goed", "fit_slight": "Licht", "fit_strong": "Sterk", - "fit_weak": "Zwak", "fit_word": "fit", "personalized": "● Gepersonaliseerd voor deze machine", "better_than_baseline": "{pct}% beter dan de basisschatting ({metric})", @@ -757,30 +704,39 @@ "trend_declining_tip": "De pasvorm van dit model is bij recente hercontroles achteruitgegaan. Meer cycli beoordelen kan het helpen opnieuw te leren.", "trend_steady_tip": "De pasvorm van dit model is bij recente hercontroles ongeveer stabiel gebleven.", "cap_blurb": { - "end": "Weten wanneer een cyclus echt is afgelopen", - "live_match": "Het lopende programma sneller herkennen", - "quality": "Verkeerd gedetecteerde of beschadigde cycli opsporen", - "remaining_time": "Voorspellen hoe lang het nog duurt", "total_energy": "Totaal energieverbruik en kosten voorspellen" }, "cap_label": { - "end": "Detectie van cycluseinde", - "live_match": "Programmaherkenning", - "quality": "Kwaliteitscontrole van de cyclus", - "remaining_time": "Schatting van de resterende tijd", "total_energy": "Schatting van het energieverbruik" }, - "metric_auc": "AUC {auc} op validatiegegevens", - "metric_mae": "fout {model} vs. referentie {baseline}" + "metric_mae": "fout {model} vs. referentie {baseline}", + "fine_tuned_at": "verfijnd: {when}", + "never": "nooit", + "trained_unknown": "op een onbekend tijdstip", + "held_out_count": "gemeten op {count} achtergehouden validatiecycli", + "held_out_count_one": "gemeten op {count} achtergehouden validatiecyclus", + "held_out_count_other": "gemeten op {count} achtergehouden validatiecycli", + "last_run_not_promoted": "Laatste controle ({when}): {reason}", + "skip_reason": { + "insufficient_rows": "Nog niet genoeg bruikbare cycli: tot nu toe {count} gelabelde, schone cycli.", + "insufficient_rows_one": "Nog niet genoeg bruikbare cycli: tot nu toe {count} gelabelde, schone cyclus.", + "insufficient_rows_other": "Nog niet genoeg bruikbare cycli: tot nu toe {count} gelabelde, schone cycli.", + "fit_failed": "Training mislukt: {error}", + "unknown": "Er is niets nieuws geleerd.", + "holdout_too_small": "Te weinig cycli om een nieuw model eerlijk te testen: {held_out} konden apart worden gehouden om te testen, {min} zijn nodig.", + "not_better_than_naive": "Het nieuwe model was niet duidelijk beter dan de eenvoudige schatting (fout {model} vs. {naive}).", + "not_better_than_incumbent": "Het nieuwe model was niet beter dan het model dat in gebruik is (fout {model} vs. {incumbent}), dus het huidige is behouden." + } }, "modal": { + "remove_task_title": "Taak verwijderen", + "remove_task_msg": "De herinnering \"{task}\" verwijderen? De bijbehorende items in het onderhoudslogboek blijven bewaard.", "auto_label": "Automatische labelcycli", "correct_feedback": "Correcte feedback", "create_profile": "Profiel aanmaken", "cycle": "Cyclus", "edit_group": "Profielgroep bewerken", "edit_phase": "Fase bewerken", - "import_config": "Configuratie importeren", "label_cycle": "Labelcyclus", "merge_cycles": "Voeg {n} cycli samen", "new_group": "Nieuwe profielgroep", @@ -795,6 +751,8 @@ "delete_automation_title": "Automatisering verwijderen", "delete_automation_msg": "De automatisering \"{name}\" uit Home Assistant verwijderen? Dit kan niet ongedaan worden gemaakt.", "remove_legacy_title": "Verouderde acties verwijderen", + "reset_phase_msg": "De bewerking van \"{name}\" ongedaan maken en de ingebouwde fase herstellen? Programma's behouden hun bereiken.", + "reset_phase_title": "Fase herstellen", "remove_legacy_msg": "De verouderde aangepaste acties verwijderen? Ze worden niet meer geactiveerd bij cyclusgebeurtenissen. Dit kan niet vanuit het paneel ongedaan worden gemaakt.", "discard_recording_title": "Opname verwijderen", "discard_recording_msg": "De opgeslagen opname verwijderen? Dit kan niet ongedaan worden gemaakt.", @@ -815,63 +773,77 @@ "store_share_device": "Dit apparaat delen", "export_select": "Export - gegevens kiezen", "import_wizard": "Import - gegevens kiezen", - "history_import": "Vermogensgeschiedenis importeren" + "history_import": "Vermogensgeschiedenis importeren", + "apply_suggestions_title": "Suggesties toepassen", + "apply_suggestions_msg": "Deze {n} instellingen wijzigen? De integratie wordt na het opslaan herladen.", + "discard_settings_title": "Niet-opgeslagen wijzigingen weggooien?", + "discard_settings_msg": "Niet-opgeslagen instellingswijzigingen: {n}. Instellingen verlaten en de wijzigingen weggooien?", + "profile_title": "Profiel · {name}", + "undo_import_msg": "De gegevens en instellingen van dit apparaat gaan terug naar hoe ze waren vóór de laatste import ({when}). Cycli en wijzigingen van daarna gaan verloren.", + "undo_import_ok": "Import ongedaan maken", + "undo_import_title": "Laatste import ongedaan maken" }, "msg": { + "custom_task_limit": "Je hebt de limiet van {max} taken bereikt.", + "custom_tasks_intro": "Voeg alles toe waaraan je nog herinnerd wilt worden. Een taak is aan de beurt zodra een van beide intervallen is bereikt; laat er een leeg of op 0 om alleen de andere te gebruiken.", + "maintenance_due_sensor_hint": "De binaire sensor Onderhoud nodig staat aan zolang er een taak aan de beurt is, voor uw eigen meldingsautomatiseringen.", + "maintenance_supporter_intro": "Maintenance Supporter is een aparte integratie die veel meer kan: elk apparaat of elke taak, intervallen op basis van tijd, gebruik of een sensor, meldingen en een geschiedenis. Het kan de cyclusteller van WashData gebruiken.", "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.", + "pg_preset_delete_confirm": "De voorinstelling \"{name}\" verwijderen?", + "pg_preset_limit": "Maximum aantal voorinstellingen bereikt ({n})", + "pg_preset_overwrite": "De voorinstelling \"{name}\" overschrijven?", "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.", - "refresh_catalog_hint": "De merken- en apparatenlijsten van de community worden in de cache opgeslagen zodat de communitystore binnen zijn daglimiet blijft. Vernieuw om vermeldingen op te halen die anderen hebben toegevoegd of goedgekeurd.", + "store_sibling_hint": "Niets gedeeld voor uw model? Een verwant model van hetzelfde merk is meestal een goed begin.", + "store_declare_appliance": "Laat WashData weten welk apparaat u hebt om setups te zien die anderen ervoor hebben gedeeld. Of typ hierboven een merk om rond te kijken.", + "refresh_catalog_hint": "Merk- en apparatenlijsten van de community worden in de cache bewaard om het daglimiet van de communitystore te sparen. Vernieuw om vermeldingen te zien die anderen hebben toegevoegd of goedgekeurd.", "head_trim_hint": "Dit aantal seconden aan het begin verwijderen", "appliance_monitor": "Apparaatmonitor", "artifact_dip_detail": "Daalde onder de gebruikelijke vermogensband voor ~{n}s.", "artifact_footer": "Gemarkeerd in de bovenstaande grafiek. Dit zijn tijdelijke artefacten (bijvoorbeeld de deur die halverwege de cyclus wordt geopend), en niet noodzakelijkerwijs problemen.", "artifact_header": "{n} afwijkingen gedetecteerd tijdens deze cyclus", - "artifact_pause_detail": "Vermogen daalde tot bijna nul voor ~{n}s en hervatte vervolgens – waarschijnlijk werd de deur halverwege de cyclus geopend of de cyclus gepauzeerd.", + "artifact_header_one": "{n} afwijking gedetecteerd tijdens deze cyclus", + "artifact_header_other": "{n} afwijkingen gedetecteerd tijdens deze cyclus", + "artifact_pause_detail": "Het vermogen zakte ~{n}s tot bijna nul en kwam daarna terug. Waarschijnlijk is de deur geopend of de cyclus gepauzeerd.", "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": "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.", + "cleanup_intro": "Alle gelabelde cycli over elkaar heen. Vink uitschieters aan en verwijder dan hun label (ze blijven in de geschiedenis, maar vormen dit profiel niet meer) of verwijder ze.", "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)", - "compare_profiles_tip": "Leg andere profielenveloppen op het schema hierboven om te zien welke het beste bij deze cyclus past.", + "compare_profiles_tip": "Leg andere profielen over de grafiek om te zien welk het best bij deze cyclus past.", "compare_selected_cycles": "Geselecteerde cycli (vast) – tonen/verbergen", - "consider_new_profile": "Overweeg een nieuw profiel aan te maken.", - "coverage_gap": "recente cycli hebben geen overeenkomend profiel ({pct}% van de laatste 30).", - "coverage_gap_similar_cycles": "{count} vergelijkbare niet-gelabelde cycli gevonden – maak een profiel aan om ze te beginnen matchen.", "cycles_deleted": "{count} cyclus(sen) verwijderd", + "cycle_context_loading": "Recordergeschiedenis laden…", + "cycle_context_note": "Grijs: de recordergeschiedenis van de vermogenssensor rond deze cyclus. Die hoort niet bij de cyclus en heeft geen invloed op duur, energie of herkenning.", + "cycle_context_none": "De recorder bevat geen metingen rond deze cyclus (Home Assistant bewaart standaard 10 dagen geschiedenis).", + "cycle_context_tip": "Alleen weergave: toont de recordergeschiedenis van de vermogenssensor rond deze cyclus, als die nog bewaard is. Om mislukte startpogingen aan toekomstige cycli toe te voegen, gebruikt u Curve-aanloop (Instellingen > Detectie > Cyclusstart); dat verandert de opgeslagen duur.", "enough_data": "Voldoende gegevens om van te leren ({current}/{min} cycli).", "export_description": "Kies precies welke profielen, cycli, instellingen en meer u naar JSON wilt exporteren, of analyseer een bestand en importeer alleen de onderdelen die u wilt.", "feedback_cycles_pending": "Te beoordelen: {n}", "feedback_prompt": "Bevestig dat het goed was, corrigeer het programma of negeer het.", "feedback_relabel_hint": "Deze cyclus opnieuw labelen lost de beoordeling ook op.", "filter_by_profile": "Filteren op profiel…", - "group_modal_help": "Groepeer programma's met dezelfde vorm die verschillen in temperatuur/centrifugeren (duur kan variëren). Matching scoort de groep als één kandidaat en kiest vervolgens het best passende lid. Kies er minimaal 2; de overlay laat zien hoezeer ze op elkaar lijken.", + "group_modal_help": "Groepeer programma's met dezelfde vorm die verschillen in temperatuur of centrifuge. De herkenning scoort de groep als één geheel en kiest dan het best passende lid. Kies er minstens 2; de overlay toont hoe gelijk ze zijn.", "group_not_cohesive": "Deze profielen lijken niet voldoende op elkaar om betrouwbaar te kunnen worden gegroepeerd, dus bij het matchen worden ze individueel behandeld totdat u de uitbijter verwijdert of de groep opsplitst.", "group_preview_hint": "Selecteer 2 of meer leden om hun vermogenscurven te bekijken en te vergelijken.", - "import_intro": "Laad een geëxporteerd bestand of plak hieronder een JSON-payload.", - "legacy_actions_title": "{count} verouderde aangepaste actie wordt nog steeds uitgevoerd", + "legacy_actions_title": "{count} verouderde aangepaste acties worden nog steeds uitgevoerd", + "legacy_actions_title_one": "{count} verouderde aangepaste actie wordt nog steeds uitgevoerd", + "legacy_actions_title_other": "{count} verouderde aangepaste acties worden nog steeds uitgevoerd", "live_chart_loading": "Een live vermogensgrafiek verschijnt zodra de metingen binnenkomen.", "loading": "Laden…", "loading_curve": "Curve laden…", "loading_settings": "Instellingen laden…", "log_buffer_hint": "Nieuwste eerst · buffert de laatste 500 ha_washdata-records sinds het opnieuw opstarten · sleep de onderrand om het formaat te wijzigen.", - "maintenance_advisory": "Het verhogen van de duur/energie kan erop wijzen dat er onderhoud aan het apparaat nodig is (bijvoorbeeld ontkalken, filterreiniging).", "maintenance_due": "Onderhoud nodig: {items}", - "maintenance_intro": "Registreer het onderhoud dat je aan dit apparaat uitvoert en word herinnerd wanneer elke taak weer aan de beurt is.", + "maintenance_intro": "Registreer onderhoud aan dit apparaat en krijg een herinnering wanneer elke taak weer aan de beurt is.", "maintenance_load_error": "Kan onderhoudsgegevens niet laden: {error}", "maintenance_requires_access": "Onderhoud en export/import vereisen volledige toegang.", - "manual_duration_hint": "De gemiddelde/verwachte cycluslengte van het profiel, gebruikt voor schattingen van de resterende tijd. Bewerk om het in te stellen; Door het ongewijzigd te laten, blijft de huidige waarde behouden.", - "matching_tuning_intro": "Tijdens het leren past WashData ook aan hoeveel programmamatching de vorm versus duur en energie weegt.", + "manual_duration_hint": "De verwachte cyclusduur van het profiel, gebruikt voor de resterende tijd. Laat ongewijzigd om de huidige waarde te houden.", "merge_intro": "De geselecteerde cycli worden gecombineerd tot één (chronologische volgorde; gaten gevuld met 0 W). Kies het resulterende profiel.", "ml_intro": "WashData wordt geleverd met slimme modellen die out-of-the-box werken.", "ml_learned_intro": "Modellen die op deze machine zijn afgestemd.", - "ml_loading": "ML laden...", "ml_settings_intro": "Twee onafhankelijke schakelaars: de ene past de modellen toe terwijl een cyclus loopt, de andere laat WashData ze in de loop van de tijd nauwkeurig afstemmen op uw machine.", - "name_first_program": "Je hebt genoeg cycli – geef je eerste programma een naam om te beginnen met matchen.", - "near_duplicate_cluster": "bijna dubbel profielcluster gedetecteerd. Door te groeperen kan er op betrouwbare wijze worden gekozen tussen look-alikes (bijvoorbeeld hetzelfde programma op verschillende temperatuur/centrifugeren).", "no_cycles_match": "Er zijn geen cycli die overeenkomen met het huidige filter.", "no_cycles_profile": "Geen cycli voor dit profiel.", "no_cycles_selected": "Selecteer eerst minstens één cyclus.", @@ -896,73 +868,52 @@ "no_suggestions": "Geen actieve suggesties.", "notify_services_hint": "Gebruik {entity}-service-ID's (kommagescheiden voor meerdere). Sjabloonvariabelen: {vars}.", "old_actions_warning": "Geconfigureerd met de oude actie-editor (nu verwijderd). Ze vuren nog steeds op cyclusevenementen, maar kunnen hier niet langer worden bewerkt. Converteer ze naar een normale automatisering of verwijder ze.", - "onboarding_progress": "{n} / 3 cycli waargenomen", - "onboarding_watching": "Gebruik je apparaat normaal – WashData kijkt mee. Na 3 cycli begint het matchen van programma's.", "pending_feedback": "In afwachting van detectiefeedback", "performance_trend": "Prestatietrend ({n} cycli)", - "pg_analysis_empty": "Laad een cyclus om de matchanalyse te bekijken.", "pg_apply_confirm": "Beste waarde toepassen: {label} = {value}?", - "pg_canvas_empty": "Selecteer hierboven een cyclus en druk vervolgens op Laden om de vermogenscurve te bekijken.", - "pg_chart_caption": "Detectie-/matchpercentage over de waarden van {param}", "pg_ctrl_intro": "De waarden beginnen bij de actieve instellingen van dit apparaat in de integratie. Wijzigingen blijven in de speeltuin totdat je ze opslaat in de integratie.", "pg_live_unavailable": "Actieve instellingen niet beschikbaar, standaardwaarden worden getoond.", "pg_load_live_confirm": "{n} wijziging(en) in de speeltuin weggooien en de huidige instellingen van de integratie opnieuw laden?", - "pg_match_rate": "{pct}% matchpercentage op {total} cycli", "pg_matches_live": "Gelijk aan de actieve instellingen", "pg_n_changed": "{n} gewijzigd t.o.v. de actieve instellingen", "pg_sugg_none": "Alle suggesties komen al overeen met de huidige instellingen", - "pg_preset_delete_confirm": "De voorinstelling \"{name}\" verwijderen?", - "pg_preset_limit": "Maximum aantal voorinstellingen bereikt ({n})", - "pg_preset_overwrite": "De voorinstelling \"{name}\" overschrijven?", "pg_publish_one_confirm": "{label} = {value} opslaan in de instellingen van dit apparaat?", "pg_restart_note": "Start Home Assistant opnieuw op om de simulatiehulpmiddelen in te schakelen.", - "pg_sim_progress": "{done} / {total} cycli", - "pg_starting": "Bezig met starten…", - "pg_stress_synthetic": "inactieve simulatie begint hier", - "pg_sweep_intro": "Wat als {param} anders was? Test {steps} waarden over je laatste {cycles} cycli om de instelling te vinden waarbij de meeste cycli correct worden herkend.", - "pg_sweep_step": "Stap {done} / {total}", - "pg_verdict_bad": "Vereist aandacht: veel cycli worden niet gedetecteerd.", - "pg_verdict_good": "Goed afgestemd: de meeste cycli worden correct herkend en gematcht.", - "pg_verdict_ok": "Acceptabel: sommige cycli gemist. Probeer de startdrempel te verlagen.", "phase_catalog_intro": "Benoemde segmenten van een cyclus (Voorwas, Verwarming, Centrifugeren…). Wijs ze toe aan een profiel vanuit het configuratiescherm.", "phase_ranges_intro": "Fasebereiken (minuten vanaf het begin van de cyclus) over de gemiddelde curve heen. Bewerk waarden om live een voorbeeld te bekijken.", - "playground_intro": "Een veilige speeltuin om eerdere cycli met andere instellingen opnieuw af te spelen, twee configuraties naast elkaar te vergelijken en te bekijken hoe een cyclus wordt uitgelijnd met een profiel. Niets hier verandert je actieve gegevens totdat je het expliciet toepast.", - "playground_needs_restart": "Start Home Assistant opnieuw op om de Speeltuin-tools in te schakelen.", - "preferences_admin": "Voorkeuren, paneel- en toegangscontrole", - "preferences_adv": "Voorkeuren", + "playground_intro": "Probeer instellingen uit op uw echte cyclusgegevens. Niets hier verandert uw live instellingen totdat u het toepast.", "prefs_personal": "Deze zijn alleen van toepassing op uw Home Assistant-account.", - "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.", + "process_history_hint": "Voer de herkenning opnieuw uit op alle opgeslagen cycli, vernieuw suggesties, train ML-modellen opnieuw (indien ingeschakeld), herbereken de kosten met de geregistreerde prijzen en herbereken de cyclusgezondheid. Voer dit uit 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.", - "rbac_hint": "Indien uitgeschakeld heeft elke Home Assistant-gebruiker volledige toegang (standaard). Beheerders hebben altijd volledige toegang en kunnen iedereen beheren.", - "recent_logs": "Recente ha_washdata-records", + "rbac_hint": "Uit: elke Home Assistant-gebruiker heeft volledige toegang (standaard). Beheerders hebben altijd volledige toegang en kunnen iedereen beheren.", "recording_in_progress": "Opname bezig", "reminders_intro": "Toon een herinnering in het paneel dit aantal cycli na de laatste servicebeurt. Laat leeg of 0 om een herinnering uit te schakelen.", "restart_gap_footer": "Gemarkeerd op de grafiek. Vermogensgegevens ontbreken voor deze intervallen; matching gebruikte alleen echte metingen.", - "restart_gap_header": "{n} HA-herstart-gat tijdens deze cyclus", + "restart_gap_header": "{n} HA-herstart-gaten tijdens deze cyclus", + "restart_gap_header_one": "{n} HA-herstart-gat tijdens deze cyclus", + "restart_gap_header_other": "{n} HA-herstart-gaten tijdens deze cyclus", "restart_gap_item": "{dur} gat", - "review_confirm_help": "Controleer of deze cyclus correct is gedetecteerd. Uw beoordelingen trainen het model op uw machine: hoe meer cycli u bevestigt, hoe beter de matching en de gezondheidsscore worden. Een snelle goed/slecht is genoeg.", + "review_confirm_help": "Is deze cyclus goed gedetecteerd? Bevestigde cycli verbeteren de herkenning voor uw machine. Een snelle Goed/Slecht is genoeg.", "review_in_settings": "Controleer in Instellingen", "review_notes_placeholder": "Opmerkingen (optioneel)", "review_notes_tip": "Vrije tekstnotities voor uw eigen referentie. Wordt niet gebruikt bij matching of training.", - "review_profile_tip": "Het programma waarmee deze cyclus wordt aangeduid als. Als het automatisch gedetecteerde programma fout was, corrigeer dit dan hier. Labeling leert matching voor toekomstige cycli.", - "review_quality_tip": "Hoe schoon is deze cyclus. Goed = een schoolvoorbeeld van dit programma; Slecht = gedetecteerd maar luidruchtig of atypisch; Onbruikbaar = verkeerd gedetecteerd (samengevoegd, afgekapt of onecht). Stuurt de gezondheidsscore aan en welke cycli het model mogen trainen.", - "review_recorded_tip": "Markeer dit als een zorgvuldig uitgekozen referentiecyclus voor het programma - dezelfde rol als een handmatig geregistreerde cyclus. Referentiecycli worden altijd bewaard, voorzien van de overeenkomende sjabloon en worden nooit verwijderd door opschoning. (Dit is de \"gouden\"/opgenomen vlag; beide zijn hetzelfde.)", - "review_tags_tip": "Optionele vlaggen die beschrijven wat er mis is gegaan in deze cyclus, zodat training en opruiming hiervan een verklaring kunnen zijn.", + "review_profile_tip": "Het programma waarmee deze cyclus is gelabeld. Is het fout, corrigeer het dan hier; labels leren de herkenning.", + "review_quality_tip": "Hoe schoon deze cyclus is. Goed: een schoolvoorbeeld van dit programma. Slecht: gedetecteerd, maar rommelig of ongewoon. Onbruikbaar: verkeerd gedetecteerd (samengevoegd, afgekapt of onecht). Beïnvloedt de gezondheidsscore en welke cycli het model trainen.", + "review_recorded_tip": "Markeer dit als handgekozen referentiecyclus voor zijn programma, net als een handmatig opgenomen cyclus. Referentiecycli blijven altijd bewaard, vormen de basis van het herkenningssjabloon en worden nooit door opruimen verwijderd.", + "review_tags_tip": "Optionele vlaggen voor wat er misging, zodat training en opruimen er rekening mee kunnen houden.", "review_to_cycles": "Open de wachtrij voor het beoordelen van cycli", "samples_decimated": "{shown} van {total} metingen weergegeven (uitgedund voor weergave; pieken behouden). Een brede leemte hier is uitdunning, geen ontbrekende gegevens.", - "saving_triggers_reload": "Opslaan activeert een herladen van de integratie. HA-entiteiten kunnen kort worden weergegeven als niet beschikbaar.", + "saving_triggers_reload": "Opslaan herlaadt de integratie. Entiteiten kunnen kort als niet beschikbaar worden getoond.", "search_placeholder": "Zoekinstellingen…", "see_recorder": "Zie recorderwidget hieronder", "setting_changed": "Gewijzigd van {old} naar {new} op {date}", "settings_basic_note": "Essentiële instellingen worden getoond. Schakel over naar Geavanceerd voor de volledige lijst.", - "shape_drift_advisory": "⚠ Vormafwijking", - "shape_drift_detail": "Het vermogenspatroon voor dit profiel is in de loop van de tijd verschoven – mogelijk slijtage van het apparaat of onderhoud nodig (bijv. ontkalken, filterreiniging).", "show_all_settings": "Toon alle instellingen", - "showing_suggestions": "Instelling {count} met suggesties weergegeven.", - "split_intro": "Klik op de grafiek om een splitspunt toe te voegen of te verwijderen, of om automatisch gaten in de grafiek te detecteren. Elk resulterend segment kan zijn eigen profiel krijgen.", - "storage_diagnostics": "Opslagstatistieken, onderhoud, export/import", + "showing_suggestions": "{count} instellingen met suggesties weergegeven.", + "showing_suggestions_one": "{count} instelling met suggesties weergegeven.", + "showing_suggestions_other": "{count} instellingen met suggesties weergegeven.", + "split_intro": "Klik op de grafiek om een splitspunt toe te voegen of te verwijderen, of detecteer automatisch op stille pauzes. Elk deel kan een eigen profiel krijgen.", "sug_staged": "{key} = {val} ingesteld. Sla op om toe te passen.", "toast_auto_detect_enabled": "Automatische detectie ingeschakeld", "toast_auto_label_complete": "Automatisch labelen voltooid", @@ -979,7 +930,6 @@ "toast_no_automation": "Hier kan geen automatisering worden aangemaakt", "toast_profile_name_required": "Profielnaam is vereist", "toast_program_set": "Programma ingesteld: {program}", - "toast_rebuild_failed": "Herbouwen mislukt: {error}", "toast_resume_failed": "Hervatten mislukt: {error}", "toast_revert_failed": "Terugzetten mislukt: {error}", "toast_reverted": "{key} teruggezet", @@ -990,100 +940,83 @@ "trend_energy_up": "Energie stijgt ({pct}%/cyclus) – recent gemiddelde {avg}", "trim_destructive_confirm": "Hiermee wordt slechts {pct}% van de cyclus behouden. Dit kan niet ongedaan worden gemaakt. Doorgaan?", "trim_intro": "Versleep de rode handgrepen of voer waarden in. Alles buiten het venster wordt verwijderd.", - "tuning_suggestions_available": "{count} afstemmingssuggestie beschikbaar op basis van waargenomen cycli. Ze verschijnen naast de relevante velden.", + "tuning_suggestions_available": "{count} afstemmingssuggesties beschikbaar op basis van waargenomen cycli. Ze verschijnen naast de relevante velden.", + "tuning_suggestions_available_one": "{count} afstemmingssuggestie beschikbaar op basis van waargenomen cycli. Deze verschijnt naast het relevante veld.", + "tuning_suggestions_available_other": "{count} afstemmingssuggesties beschikbaar op basis van waargenomen cycli. Ze verschijnen naast de relevante velden.", "unsure_detected_prefix": "WashData weet niet zeker of het is gedetecteerd", - "updated": "Bijgewerkt", "warmup_badge": "Nog aan het leren ({done}/{needed} cycli)", - "warmup_detail": "Dit profiel heeft {needed} gelabelde cycli nodig voordat automatisch matchen begint. Elke bevestigde cyclus helpt het te leren.", + "warmup_detail": "De herkenning werkt al. WashData vraagt u de eerste {needed} cycli van dit profiel te bevestigen voordat het zelf labelt.", "wipe_history_warning": "Verwijder definitief alle cycli en profielen. Kan niet ongedaan worden gemaakt.", "compare_overlay_tip": "Leg geleerde profielenvelopen over elkaar om te zien op welk programma elke cyclus lijkt.", - "duration_trend_up_tip": "Duur omhoog ({pct}%/cyclus)", - "duration_trend_down_tip": "Duur omlaag ({pct}%/cyclus)", - "energy_trend_up_tip": "Energie omhoog ({pct}%/cyclus)", - "energy_trend_down_tip": "Energie omlaag ({pct}%/cyclus)", - "performance_trending": "Prestatietrend", - "pg_select_cycle_hint": "Selecteer een cyclus en klik op ↺ om te laden", - "pg_sweep_progress": "Stap {done} / {total}", - "pg_analysis_hint": "Selecteer een cyclus en klik op ↺ om de analyse te laden.", "diagnostics_load_failed": "Kon diagnostiek niet laden: {error}", - "manual_duration_ref_hint": "Wordt alleen gebruikt wanneer er geen referentiecyclus is geselecteerd. Een referentiecyclus stelt de duur in op basis van de eigen lengte.", + "manual_duration_ref_hint": "Alleen gebruikt zonder referentiecyclus; een referentiecyclus bepaalt de duur op basis van zijn eigen lengte.", + "duration_learned_hint": "Berekend uit de cycli van dit programma (uitschieters niet meegeteld) en na elke nieuwe cyclus bijgewerkt. Wilt u dit wijzigen, snijd dan de afwijkende cycli bij of verwijder ze.", "toast_convert_rolled_back": "Conversie mislukt en teruggedraaid (geen automatisering achtergelaten): {error}", "toast_convert_orphan": "De automatisering is aangemaakt, maar het wissen van de oude acties is mislukt. Probeer het niet opnieuw: verwijder de oude acties handmatig om een dubbele automatisering te voorkomen.", - "advisory_duration_trend_up": "De cycli van '{name}' worden steeds langer (ongeveer +{pct}% per cyclus). Als het gedrag van het apparaat is veranderd, neem dit profiel dan opnieuw op of bouw het opnieuw op.", - "advisory_energy_trend_up": "'{name}' verbruikt steeds meer energie (ongeveer +{pct}% per cyclus) - controleer het apparaat als dat onverwacht is.", - "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.", + "advisory_duration_trend_up": "De cycli van '{name}' worden steeds langer (ongeveer +{pct}% per cyclus). Als het apparaat is veranderd, neem dit profiel dan opnieuw op of bouw het opnieuw op.", + "advisory_energy_trend_up": "'{name}' verbruikt elke cyclus meer energie (ongeveer +{pct}% per cyclus). Controleer het apparaat als dat onverwacht is.", + "advisory_poor_health": "'{name}' past slecht: de recente cycli variëren sterk of komen zwak overeen. Bekijk de cycli of neem het opnieuw op zodat herkenning en schattingen nauwkeurig blijven.", + "advisory_shape_drift": "'{name}' is afgeweken van zijn oorspronkelijke vermogenscurve, mogelijk door kalk of slijtage. Overweeg het opnieuw op te nemen met recente cycli.", + "advisory_shape_drift_corr": "'{name}' is afgeweken van zijn oorspronkelijke vermogenscurve (correlatie {corr}), mogelijk door kalk of slijtage. Overweeg het opnieuw op te nemen met recente cycli.", + "advisory_unmatchable": "'{name}' kan nooit worden herkend: er zit geen cyclus met vermogensdata achter. Label of neem een cyclus van dit programma op om dit op te lossen.", + "advisory_unmatchable_no_duration": "'{name}' kan nooit worden herkend: de cyclus heeft geen bruikbare duur. Neem dit programma opnieuw op of stel de verwachte duur in.", "pg_no_alerts": "Geen problemen gedetecteerd in deze run.", "pg_override_active": "Gebruikt je bewerkte instellingen t.o.v. de huidige", "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.", "pg_sweep_intro2": "Vind de instelling die een doel het best haalt over je recente cycli. Er verandert niets totdat je het toepast.", - "pg_running_history": "Je cycli worden opnieuw afgespeeld…", - "pg_running_sweep": "Waarden worden over je cycli getest…", "pg_row_load_hint": "Deze cyclus in de grafiek hierboven laden", - "pg_history_intro2": "Laat je recente cycli opnieuw door de echte detector en herkenning lopen met de bovenstaande instellingen. Klik op een rij om die cyclus in de grafiek te laden; wijzig een instelling om een voor/na-vergelijking te zien.", + "pg_history_intro2": "Speel recente cycli opnieuw af door de echte detector en herkenning met de instellingen hierboven. Klik op een rij om hem in de grafiek te laden; wijzig een instelling om voor en na te vergelijken.", "pg_simulating": "Cyclus simuleren…", - "pg_canvas_empty2": "Kies hierboven een cyclus en druk op Uitvoeren om deze te simuleren. Beweeg vervolgens de muis erover om waarden af te lezen, scroll om te zoomen en sleep om te verschuiven.", "pg_apply_settings_confirm": "Deze {n} instelling(en) opslaan voor dit apparaat? {list}", - "pg_stress_terminated_detail": "Gestabiliseerd op ~{idle}W standby -- cyclus eindigde {h} u {m} min later via {reason}.", - "pg_stress_above_threshold_detail": "Standby-verbruik ~{idle}W ligt op of boven de effectieve stopdrempel ({stop}W) -- de cyclus is nooit als inactief geregistreerd. Verhoog stop_threshold_w om dit op te lossen.", - "pg_stress_hit_cap_detail": "Cyclus liep {h} u {m} min zonder te stoppen -- geforceerd gestopt door de veiligheidslimiet. Standby-verbruik {idle}W versus stopdrempel {stop}W.", "no_logs_match": "Geen logrecords komen overeen met deze filters.", "pg_analysis_hint2": "Kies een cyclus en druk op Uitvoeren om de matchanalyse te laden.", - "pg_analysis_empty2": "Druk op Uitvoeren om de matchanalyse te zien.", - "imported_readonly": "Geïmporteerd uit de communitystore. Weergegeven ter referentie en voor matchen. Telt niet mee in je statistieken en kan niet worden bewerkt.", + "imported_readonly": "Geïmporteerd uit de communitystore. Gebruikt als referentie en voor herkenning, telt niet mee in uw statistieken en kan niet worden bewerkt.", "merge_no_imports": "Geïmporteerde opnamen kunnen niet worden samengevoegd met een echte cyclus. Deselecteer ze om samen te voegen.", - "store_enable_hint": "Schakel onlinefuncties in bij Instellingen om referentiecycli van de community te bekijken en te importeren.", - "advisory_duration_outlier": "{n} cyclus/cycli met het label '{name}' wijken te ver af van de gebruikelijke duur om er ooit mee te matchen (ergste geval: {ratio}x). Waarschijnlijk zijn ze verkeerd gelabeld, of heeft één opname twee runs vastgelegd. Label ze opnieuw of splits ze, zodat ze de tijdschatting van dit programma niet langer vertekenen.", + "store_enable_hint": "Zet onlinefuncties aan bij Voorkeuren > Online & community om referentiecycli van de community te bekijken en te importeren.", + "advisory_duration_outlier": "{n} cyclus/cycli met label '{name}' wijken te ver af van de gebruikelijke lengte om ooit te matchen (slechtste: {ratio}x). Ze zijn waarschijnlijk verkeerd gelabeld of bevatten twee runs. Label ze opnieuw of splits ze, zodat ze de tijdschatting niet meer vertekenen.", "standby_above_stop_title": "Verbruikte nog {idle} W toen de cyclus eindigde", "standby_above_stop_sub": "{n} van de laatste {total} cycli. De uitschakelvertraging start pas zodra het vermogen onder de Stopdrempel ({stop} W) ligt, waardoor cycli te laat eindigen of met de hand gestopt moeten worden.", - "online_intro": "Deel referentiecycli met andere WashData-gebruikers en download ze. Alle onlinefuncties zijn optioneel en standaard uitgeschakeld.", "store_share_intro": "Upload deze referentiecyclus zodat anderen met hetzelfde apparaat hem kunnen gebruiken. Hij wordt beoordeeld voordat hij openbaar verschijnt.", - "online_intro_global": "Bekijk en deel referentieopnamen met andere WashData-gebruikers en bevestig apparaatvermeldingen. Eén verbinding geldt voor je hele WashData-integratie; merk en model van het apparaat worden per apparaat ingesteld onder Basis. Alle onlinefuncties zijn optioneel en standaard uitgeschakeld.", + "online_intro_global": "Bekijk en deel referentieopnamen met andere WashData-gebruikers. Eén verbinding geldt voor al uw WashData-apparaten; merk en model stelt u per apparaat in onder Basis. Onlinefuncties staan uit totdat u ze inschakelt.", "online_unavailable": "Onlinefuncties zijn niet beschikbaar op deze server.", "store_picker_offline": "Schakel onlinefuncties in via het instellingentandwiel om te kiezen uit de communitycatalogus.", "pick_brand_first": "Kies eerst een merk van het apparaat.", - "model_not_found": "Nog niet in de catalogus.", - "connect_to_confirm": "Verbind via het instellingentandwiel om te bevestigen of te beoordelen.", "adopt_settings_hint": "Overschrijft de detectie- en matchdrempels van dit apparaat met de gedeelde. Uw meldingen, entiteiten en energieprijs worden nooit gewijzigd.", - "include_settings_hint": "Deelt de detectie- en matchdrempels van dit apparaat (niet uw meldingen, entiteiten of energieprijs). Overnemers kiezen zelf of ze ze toepassen.", + "include_settings_hint": "Deel de detectie- en matchdrempels van dit apparaat (geen meldingen, entiteiten of energieprijs). Anderen kiezen zelf of ze ze toepassen.", "onboard_download": "Nieuw apparaat? Neem een kant-en-klare instelling (programma's, referentiecycli en fasen) over van een andere WashData-gebruiker met hetzelfde apparaat.", - "share_consent": "Ik bevestig dat deze cycli normaal zijn voltooid zonder onderbreking", "share_device_none": "Nog geen deelbare cycli. Markeer eerst een opgenomen of handmatig geselecteerde cyclus als referentiecyclus (⭐) op het tabblad Cycli.", "share_guideline_naming": "Geef elk profiel precies de naam zoals weergegeven op de kiesschijf of het display van het apparaat (bijv. 'Cotton 40', 'Eco 60').", - "share_guideline_quality": "Deel alleen cycli die normaal zijn voltooid -- geen onderbrekingen halverwege, deur geopend of stroomuitval.", + "share_guideline_quality": "Deel alleen cycli die normaal zijn voltooid: geen onderbrekingen, geopende deur of stroomhaperingen.", "share_guideline_review": "Uw upload begint als in behandeling en verschijnt openbaar zodra genoeg communityleden het bevestigen.", "share_guidelines_title": "Voordat u deelt", - "store_download_device_intro": "Neem elk gedeeld programma en zijn referentiecycli over op uw apparaat. Uw eigen opgenomen cycli en statistieken worden niet beïnvloed.", + "store_download_device_intro": "Voeg elk gedeeld programma en zijn referentiecycli toe aan uw apparaat. Uw eigen cycli en statistieken blijven ongewijzigd.", "store_share_device_intro": "Upload {brand} {model} met de referentiecycli die u selecteert. Anderen met hetzelfde apparaat kunnen uw programma's overnemen. Vermeldingen worden beoordeeld voordat ze openbaar verschijnen.", "share_profile_no_cycles": "Geen referentiecycli -- markeer een cyclus als ⭐ op het tabblad Cycli om dit profiel op te nemen", - "advisory_phase_inconsistent": "'{name}' lijkt verschillende programma's of temperaturen te combineren - de cycli verwarmen gedurende sterk uiteenlopende tijden. Door het op te splitsen in aparte profielen (bijv. per temperatuur) verbeteren de matching en tijdschattingen.", - "advisory_phase_inconsistent_title": "⚠ Mogelijk gemengde programma's", - "export_select_intro": "Vink precies aan wat u wilt opnemen. Als u profielen zonder hun cycli selecteert, wordt toch een herkenbaar programma geëxporteerd (de geleerde vorm gaat mee).", - "import_analyze_hint": "Laad een geëxporteerd bestand (of plak de JSON ervan). WashData analyseert het en toont precies wat kan worden geïmporteerd voordat er iets verandert.", + "export_select_intro": "Vink aan wat u wilt meenemen. Profielen zonder hun cycli leveren toch een herkenbaar programma op.", + "import_analyze_hint": "Laad een geëxporteerd bestand (of plak de JSON). WashData toont wat kan worden geïmporteerd voordat er iets verandert.", "analyzing": "Analyseren…", - "device_type_mismatch_warn": "Deze export komt van een ander apparaattype ({src} vs {local}). Programma's en cycli kunnen nog steeds als referentiegegevens worden geïmporteerd, maar apparaatspecifieke instellingen en het importeren van echte geschiedenis zijn uitgeschakeld.", + "device_type_mismatch_warn": "Deze export komt van een ander apparaattype ({src} vs {local}). Programma's en cycli kunnen nog als referentiegegevens worden geïmporteerd, maar apparaatinstellingen en echte geschiedenis niet.", "merge_hint": "Geïmporteerde items worden toegevoegd; er gaat lokaal niets verloren. Naamconflicten worden hieronder opgelost.", "replace_warn": "Elke aangevinkte categorie wordt gewist en vervangen vanuit het bestand. Niet-aangevinkte categorieën blijven ongewijzigd.", "dest_reference_hint": "Geïmporteerde cycli verbeteren alleen de programmaherkenning en hebben nooit invloed op de gebruiks-/energiestatistieken.", - "dest_real_history_hint": "Geïmporteerde cycli tellen als de eigen geschiedenis van dit apparaat en voeden de energie-/gebruiksstatistieken. Gebruik dit om één apparaat naar een nieuwe installatie te verplaatsen.", + "dest_real_history_hint": "Geïmporteerde cycli tellen als eigen geschiedenis en statistieken van dit apparaat. Gebruik dit om een apparaat naar een nieuwe installatie te verhuizen.", "sug_muted": "Deze instelling wordt niet meer voorgesteld", "sug_mute_failed": "Instelling kon niet worden gedempt", "sug_unmuted_all": "Gedempte suggesties hersteld", - "n_suggestions_muted": "{count} gedempt; de autotuner stelt deze niet meer voor.", + "n_suggestions_muted": "{count} suggesties gedempt; de autotuner stelt deze niet meer voor.", + "n_suggestions_muted_one": "{count} suggestie gedempt; de autotuner stelt deze niet meer voor.", + "n_suggestions_muted_other": "{count} suggesties gedempt; de autotuner stelt deze niet meer voor.", "font_size_hint": "Maak alles in dit paneel groter of kleiner. Geldt voor uw account op dit apparaat.", - "import_history_description": "Had je al een slimme stekker voordat je WashData ging gebruiken? Upload een geschiedenisexport van de vermogenssensor, of laat die rechtstreeks uit Home Assistant lezen, en de normale detectie loopt eroverheen zodat oude cycli in je Cycli-lijst verschijnen, klaar om benoemd te worden.", - "hist_input_hint": "Upload een CSV die je uit het Geschiedenis-paneel hebt gedownload (entiteit, status, laatst gewijzigd), of laat WashData de geschiedenis van de sensor direct lezen. De detectie loopt er dan precies zo over als live, en jij kiest welke van de gevonden cycli je bewaart.", - "hist_recorder_hint": "Leest vanaf de datum die je kiest tot nu. Home Assistant bewaart standaard 10 dagen gedetailleerde geschiedenis en daarna alleen uurgemiddelden, die te grof zijn om cycli in te detecteren - kies alleen een datum verder terug als je recorder is ingesteld om meer te bewaren.", - "hist_scanning": "Je geschiedenis wordt door de detectie gehaald. Dit gebeurt op de achtergrond - je kunt dit venster sluiten en later terugkomen.", + "import_history_description": "Had u al een slimme stekker vóór WashData? Upload de vermogensgeschiedenis of lees die uit Home Assistant. De detectie loopt erover en oude cycli verschijnen in uw Cycli-lijst, klaar om te benoemen.", + "hist_input_hint": "Upload een CSV uit het Geschiedenis-paneel (entiteit, status, laatst gewijzigd), of laat WashData de geschiedenis van de sensor direct lezen. De live detectie loopt erover en u kiest welke cycli u bewaart.", + "hist_recorder_hint": "Leest vanaf de gekozen datum tot nu. Home Assistant bewaart standaard 10 dagen gedetailleerde geschiedenis; oudere uurgemiddelden zijn te grof voor cyclusdetectie.", + "hist_scanning": "Uw geschiedenis gaat op de achtergrond door de detector. U kunt dit venster sluiten en later terugkomen.", "hist_imported_count": "{n} cycli geïmporteerd.", "hist_duplicates": "{n} waren al geïmporteerd en zijn overgeslagen.", "hist_capped": "De limiet per apparaat voor geïmporteerde cycli is bereikt; de rest is niet opgeslagen.", - "hist_next_step": "Ze staan in je Cycli-lijst, gemarkeerd als geïmporteerde geschiedenis. Open er een en gebruik Labelen om het bijbehorende programma te benoemen.", + "hist_next_step": "Ze staan in uw Cycli-lijst, gemarkeerd als geïmporteerde geschiedenis. Open er een en gebruik Labelen om het programma te benoemen.", "hist_rows_read": "{n} metingen gelezen", "hist_entity_substituted": "{used} gelezen (dit apparaat is geconfigureerd voor {wanted})", "hist_breaks": "{n} gaten waarin de sensor niet beschikbaar was", @@ -1097,55 +1030,86 @@ "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.", - "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.", + "imported_history_readonly": "Gevonden in geïmporteerde vermogensgeschiedenis. Helpt bij de herkenning, maar telt niet mee in uw statistieken en kan niet worden bijgesneden of gesplitst. Label het om het programma te benoemen.", + "odometer_intro": "Dit totaal gaat alleen omhoog. Een cyclus verwijderen verlaagt het niet, zodat onderhoudsherinneringen kloppen.", + "odometer_correct_hint": "Stel dit in als WashData een run heeft geteld die nooit heeft plaatsgevonden, of als het apparaat al cycli draaide voordat WashData werd geïnstalleerd.", "no_reminders_set": "Er zijn nog geen serviceherinneringen ingesteld.", "toast_program_armed": "Programma klaargezet voor de volgende cyclus: {program}", "toast_range_required": "Stel een geldig Van/Tot-bereik in.", - "no_profiles_option": "(nog geen profielen, voeg er een toe)" + "no_profiles_option": "(nog geen profielen, voeg er een toe)", + "compare_cycles_title": "{count} cycli vergelijken", + "edit_builtin_phase": "Dit is een ingebouwde fase. Opslaan maakt een eigen kopie; verwijder de kopie om het origineel te herstellen.", + "fetch_error": "Laden van gegevens mislukt.", + "connection_lost": "Verbinding verbroken - gegevens van {time} worden getoond.", + "pg_notify_partial": "Markeringen tonen alleen meldingen voor start, bijna klaar, einde, mijlpalen en stille uren. Live updates, uitlaadherinneringen en uitloopwaarschuwingen worden niet gesimuleerd.", + "connect_to_confirm": "Verbind via het instellingentandwiel om te bevestigen of te beoordelen.", + "coverage_gap_cluster": "{count} recente ongelabelde cycli van ongeveer {min} min lijken één programma te zijn dat je nog niet hebt aangemaakt.", + "pg_label_yes": "Zou aan het einde van de cyclus als \"{name}\" worden gelabeld", + "pg_label_no": "Zou om beoordeling vragen: {reason}", + "live_match_debug_tip": "Vertrouwen: hoe goed de huidige vermogenscurve overeenkomt met het beste kandidaatprofiel (0-100%). Dubbelzinnig: de twee beste kandidaten scoren binnen 5% van elkaar - het label is onzeker tot de cyclus klaar is.", + "automation_alias_started": "{device}: cyclus gestart", + "automation_alias_finished": "{device}: cyclus voltooid", + "automation_desc_started": "Wordt uitgevoerd wanneer een {device}-cyclus start. Voeg uw acties toe (melding, lampen, ...).", + "automation_desc_finished": "Wordt uitgevoerd wanneer een {device}-cyclus klaar is. Voeg uw acties toe (melding, lampen, ...).", + "feedback_detected": "Gedetecteerd door WashData: {name}", + "pg_outcome_stale": "De instellingen zijn gewijzigd sinds deze simulatie is uitgevoerd. Ze toont nog het oude resultaat.", + "pg_compare_profile_tip": "Kiest alleen het profiel dat over de grafiek wordt getekend. De simulatie herkent de cyclus altijd zelf, net als de integratie tijdens gebruik.", + "pg_task_busy": "Voor dit apparaat loopt al een Op geschiedenis testen of Optimaliseren. Wacht tot die klaar is of annuleer hem.", + "pg_sweep_guarded": "Beëindigt meer cycli te vroeg, splitst er meer of mist cycli die de huidige instelling wel vangt, en wordt daarom nooit aanbevolen.", + "pg_sweep_n_cycles": "over {count} cycli", + "pg_sweep_n_cycles_one": "over {count} cyclus", + "pg_sweep_n_cycles_other": "over {count} cycli", + "pg_sweep_keep_current": "Behoud de huidige waarde ({value}, {metric}): geen enkele waarde deed het minstens één cyclus beter.", + "hist_truncated": "gestopt bij de limiet van {n} metingen, dus de rest van de geschiedenis is niet doorzocht", + "hist_warn_kw": "De hoogste meting is maar {w}. Deze sensor lijkt kilowatt te melden; WashData heeft watt nodig, dus er kunnen geen cycli worden gedetecteerd.", + "hist_warn_naive_time": "{n} tijdstempels hebben geen tijdzone en zijn als UTC gelezen. Als het bestand lokale tijd gebruikt, zijn cycli verschoven met uw UTC-verschil en kunnen metingen rond een zomertijdwissel wegvallen.", + "hist_warn_naive_time_one": "{n} tijdstempel heeft geen tijdzone en is als UTC gelezen. Als het bestand lokale tijd gebruikt, zijn cycli verschoven met uw UTC-verschil en kunnen metingen rond een zomertijdwissel wegvallen.", + "hist_warn_naive_time_other": "{n} tijdstempels hebben geen tijdzone en zijn als UTC gelezen. Als het bestand lokale tijd gebruikt, zijn cycli verschoven met uw UTC-verschil en kunnen metingen rond een zomertijdwissel wegvallen.", + "hist_tail_trimmed": "{min} min wachten na het einde van de cyclus is weggeknipt, zoals de integratie dat tijdens gebruik ook doet", + "invalid_threshold_pair": "Stopdrempel ({stop} W) moet lager zijn dan Startdrempel ({start} W). Er is niets opgeslagen; wijzig beide samen.", + "import_undo_hint": "Er wordt een herstelpunt van vóór de laatste import ({when}) bewaard: {profiles} profiel(en) en {cycles} cyclus(sen).", + "replace_undo_hint": "Eerst wordt een herstelpunt opgeslagen, zodat deze import via Exporteren / importeren ongedaan kan worden gemaakt.", + "pg_in_sample_note": "Deze cycli hebben de profielen helpen opbouwen waarmee ze worden vergeleken, dus de resultaten zijn optimistisch: verwacht bij nieuwe cycli iets minder juiste matches." }, "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", - "smart_termination_duration_ratio": "Deel van de verwachte duur van het herkende programma dat een cyclus moet bereiken voordat de slimme beëindiging hem vroegtijdig mag afsluiten; verlaag deze voor belasting- of temperatuurafhankelijke machines", + "smart_termination_duration_ratio": "Deel van de gemiddelde programmaduur dat een cyclus moet halen voordat de slimme beëindiging hem vroeg mag afsluiten; verlaag als de looptijden variëren", "completion_min_seconds": "Kortste run die als een echte cyclus telt", - "end_repeat_count": "Opeenvolgende lage metingen voor het einde", "interrupted_min_seconds": "Korte cycli gemarkeerd als onderbroken", "min_off_gap": "Vereist interval om twee cycli te scheiden", "off_delay": "Seconden laag vermogen voordat de cyclus eindigt", "start_duration_threshold": "Seconden boven de drempel om de start te bevestigen", "start_threshold_w": "Minimaal aantal watt om als gestart te tellen", "stop_threshold_w": "Hieronder telt de machine als uit", - "dtw_bandwidth": "Stap 3: Sakoe-Chiba-vervormingsband (0 = DTW uit; standaard 0,2 = 20% van de cycluslengte)", - "corr_weight": "Stap 2: balans tussen curvevorm (correlatie) en vermogensniveau (MAE); standaard 0,45", - "duration_weight": "Stap 4: hoe sterk de duurovereenkomst de eindscore beïnvloedt (standaard 0,22)", - "energy_weight": "Stap 4: hoe sterk de energieovereenkomst de eindscore beïnvloedt (standaard 0,22)", - "profile_match_min_duration_ratio": "Stap 1: minimale cycluslengte als fractie van de profielduur; standaard 0,1 (10%)", - "profile_match_max_duration_ratio": "Stap 1: maximale cycluslengte als fractie van de profielduur; standaard 1,8 (180%)", - "keep_min_score": "Stap 2: minimale score om in de race te blijven; standaard 0,1 laat zwakke kandidaten toe, latere stappen kiezen de beste", - "dtw_blend": "Stap 3: 0 = alleen kernscore, 1 = alleen DTW-score, 0,5 = gelijke mix (standaard)", - "dtw_ensemble_w": "Stap 3 (ensemble-modus): gewicht op geschaalde L1 vs afgeleid DTW; standaard 0,7 geeft voorkeur aan niveauafhankelijke herkenning", - "dtw_ddtw_scale": "Stap 3: DDTW-halfsaturatieafstand; kleiner = gevoeliger voor vorm (standaard 30)", - "dtw_refine_top_n": "Stap 3: kandidaten die door DTW opnieuw beoordeeld worden; verhoog naar 7-9 als het juiste profiel op de 4e-5e plaats staat (standaard 5)", - "duration_scale": "Stap 4: log-verhouding waarbij de duurovereenkomst halveert; kleiner = strengere straf (standaard 0,175)", - "energy_scale": "Stap 4: log-verhouding waarbij de energieovereenkomst halveert; kleiner = strengere straf (standaard 0,25)", + "profile_match_min_duration_ratio": "Minimale cycluslengte als fractie van de profielduur; standaard 0,1 (10%)", + "profile_match_max_duration_ratio": "Maximale cycluslengte als fractie van de profielduur; standaard 1,8 (180%)", "anti_wrinkle_idle_timeout": "Toegestane stille tijd tussen twee tuimelpulsen voordat anti-kreuk eindigt", "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_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", + "anti_wrinkle_exit_power": "Stil niveau tussen tuimelpulsen; werkt alleen boven de Stopdrempel", + "anti_crease_finalize_ratio": "Deel van de gemiddelde programmaduur dat een cyclus moet halen voordat de anti-kreuknaloop hem mag afronden; verlaag bij drogers, laat wasmachines op de standaard", "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.", "anti_wrinkle": "Periodiek tuimelen na de cyclus om rimpels te verminderen.", + "baking": "Verwarmingselement op hoge temperatuur actief voor korst en kruim.", + "cooking": "Hoofdbereiding met verwarming en ventilator actief.", "cool_down": "Tuimelen zonder warmte tegen het einde van de cyclus.", + "cool_down_af": "Afkoelen met alleen de ventilator na het verwarmen.", "drain_and_switch": "Overgangsperiode van wassen naar drogen.", "dry": "Droogfase met behulp van verwarming en/of restwarmte.", "drying": "Belangrijkste verwarmde tuimelen periode.", "heat_up": "Eerste opwarming van de verwarming voordat het volledig drogen begint.", + "keep_warm": "Lage warmte om het eten warm te houden.", + "keep_warm_bm": "Lage warmte om het brood na het bakken warm te houden.", + "kneading": "De motor mengt en kneedt het deeg. Hoog verbruik.", + "pause_af": "Korte pauze om te schudden of te controleren.", + "pre_heat": "Eerste opwarming van de bakruimte vóór het eigenlijke bereiden.", "pre_rinse": "Eerste spray-down vóór het wassen met wasmiddel.", + "proving": "Rijsperiode op lage warmte zodat de gist werkt en het deeg rijst.", + "resting": "Korte pauze op laag vermogen tussen het kneden door, zodat het gluten ontspant.", "pre_wash": "Eerste inweken of voorbehandeling vóór de hoofdwas.", "rinse": "Spoelfase met schoon water. Deze fase kan zich meerdere keren herhalen.", "sanitize": "Reinigingsfase op hoge temperatuur voor ontsmettingsprogramma's.", @@ -1158,6 +1122,7 @@ "wash_dw": "Hoofdwasmiddel met verwarming." }, "placeholder": { + "custom_task": "bijv. deurrubber reinigen", "add": "toevoegen…", "group_name": "bijv. Katoen 2:47", "json_buttons": "[{\"action\":\"ID\",\"title\":\"Label\"}]", @@ -1185,10 +1150,10 @@ "section": { "dishwasher": { "label": "Vaatwasser", - "intro": "Afhandeling van het einde van de cyclus voor vaatwassers, die doorgaans eindigen met een lange, vrijwel stille droogfase vóór een kort laatste wegpompen." + "intro": "Eindafhandeling voor vaatwassers, die vaak lang stil drogen vóór een kort laatste wegpompen." }, "anti_wrinkle": { - "intro": "De anti-kreuk-/anti-vouw-modus detecteert lage-vermogen trommelimpulsen na de hoofdfase en houdt ze bevestigd aan de voltooide cyclus.", + "intro": "Houdt de trommelpulsen met laag vermogen van een droger na de hoofdfase bij de voltooide cyclus.", "label": "Anti-kreuk" }, "basic": { @@ -1196,7 +1161,7 @@ "label": "Basis" }, "delay": { - "intro": "Detectie van vertraagde start identificeert wanneer een apparaat is ingeschakeld maar nog niet begonnen is met zijn cyclus.", + "intro": "Detecteert een apparaat dat aan staat en wacht op een uitgestelde start.", "label": "Vertraagde start" }, "detection": { @@ -1220,161 +1185,153 @@ "label": "Timing & Waakhond" }, "triggers": { - "intro": "Optionele externe signalen: een eindtrigger, een deursensor, een pauzeknop en de uitlaadherinnering.", + "intro": "Optionele extra's: eindtrigger, deursensor, pauzeschakelaar en uitlaadherinnering.", "label": "Triggers & Deuren" - }, - "phase_eta": { - "label": "Resterende tijd", - "intro": "Fasebewuste resterende tijd, voor machines waarvan de cyclusduur afhangt van temperatuur of centrifugeren." } }, "setting": { "dishwasher_end_spike_quiet_release": { "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." + "doc": "Na de verwachte duur: hoe lang de vaatwasser onder de Stopdrempel moet blijven voordat WashData niet meer op het laatste wegpompen wacht. Verhoog als een laat wegpompen na lang drogen wordt gemist. Het wachten duurt nooit langer dan 30 minuten." }, "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." + "doc": "Deel van de gemiddelde programmaduur dat een cyclus moet halen voordat het trommelen met laag vermogen als anti-kreuknaloop telt. Verlaag het (bijv. 0,75) bij drogers met beladingsdetectie. Laat wasmachines op de standaard (0,98), anders kan een was halverwege eindigen." }, "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.", + "doc": "Houd de korte trommelpulsen met laag vermogen van een droger na de hoofdverwarmingsfase bij de voltooide cyclus, in plaats van ze als nieuwe cycli te lezen.", "label": "Anti-kreuk detectie inschakelen" }, "anti_wrinkle_exit_power": { - "doc": "Het vermogen moet tussen de pulsen onder dit niveau liggen, anders blijft de antirimpelmodus actief.", + "doc": "In anti-kreukmodus telt vermogen onder deze waarde als stil tussen tuimelpulsen, en langer stil dan de Max. pulspauze beëindigt de modus. Is de Stopdrempel hoger, dan wordt die gebruikt; deze waarde doet dus alleen iets boven de Stopdrempel.", "label": "Drempel uitgangsverwarming" }, "anti_wrinkle_idle_timeout": { - "doc": "Hoe lang het apparaat tussen twee tuimelpulsen stil mag blijven voordat de anti-kreukmodus eindigt. Stel dit hoger in dan de langste pauze die uw droger tussen twee pulsen laat, anders wordt elke latere puls als valse start gelezen.", + "doc": "Hoe lang de droger tussen trommelpulsen stil mag zijn voordat de anti-kreukmodus eindigt. Stel het hoger in dan de langste pauze van uw droger, anders worden latere pulsen als valse starts gelezen.", "label": "Max. pulspauze" }, "anti_wrinkle_max_duration": { - "doc": "Pulsaties langer dan dit worden behandeld als een echte cyclus in plaats van als een anti-rimpel-tuimelen.", + "doc": "Een langere puls telt als een echte cyclus, niet als anti-kreuktrommelen.", "label": "Max. duur" }, "anti_wrinkle_max_power": { - "doc": "Een puls boven dit vermogen wordt beschouwd als een echte nieuwe cyclus, niet als een anti-rimpeltuimeling. Ingesteld net boven het vermogen van de tuimelpuls.", + "doc": "Een puls boven dit vermogen telt als een echte nieuwe cyclus, niet als trommelen. Stel het net boven het vermogen van uw trommelpulsen in.", "label": "Max. anti-kreuk vermogen" }, "auto_label_confidence": { - "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.", + "doc": "Matchscore aan het einde van de cyclus vanaf waar het programma zonder vragen wordt gelabeld. Verhoog voor meer zekerheid; verlaag om meer cycli automatisch te labelen. Houd het boven Leerbetrouwbaarheid.", "label": "Auto-label betrouwbaarheid" }, "auto_maintenance": { - "doc": "Voer een nachtelijke huishouding uit: bouw profielenveloppen opnieuw op, bereken de cyclusstatus opnieuw, verwijder debug-sporen en bewaar de meest recente cycli.", + "doc": "Nachtelijke opruiming: label oude cycli die nu duidelijk bij een programma passen, bouw programmacurves opnieuw op, vernieuw afwijkingsmarkeringen en cyclusgezondheid, en verwijder foutopsporingssporen als Foutopsporingssporen opslaan uit staat. Cycli en hun vermogensverloop blijven altijd bewaard.", "label": "Automatisch onderhoud (nachtelijke opruiming)" }, "completion_min_seconds": { - "doc": "Cycli korter dan dit worden weggegooid als spookcycli (testruns, de deur openen om een sok toe te voegen).", + "doc": "Cycli korter dan dit (testruns, de deur openen om een sok toe te voegen) worden opgeslagen als Onderbroken en krijgen geen eindmelding.", "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." + "doc": "Voegt metingen van mislukte startpogingen toe aan het begin van een cyclus, voor machines die proberen voordat ze starten (deurvergrendeling, eerste vulling). 300 s volstaat meestal; 0 = uit. Opgenomen starts schuiven naar voren, dus geleerde duren schuiven een tijdje mee." }, "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.", + "doc": "Het vermogen moet zo lang in de standby-band blijven voordat het apparaat als wachtend op de start telt.", "label": "Bevestigingsseconden" }, "delay_start_detect_enabled": { - "doc": "Detecteer wanneer het apparaat is ingeschakeld en wacht (uitgestelde start/stand-by) maar nog niet met zijn cyclus is begonnen. Stand-byverbruik wordt dus niet aangezien voor een lopende cyclus.", + "doc": "Detecteer een apparaat dat aan staat en wacht op een uitgestelde start, zodat het standbyverbruik niet voor een lopende cyclus wordt aangezien.", "label": "Vertraagde start detectie inschakelen" }, "delay_timeout_hours": { - "doc": "Stop na zoveel uren met wachten in de uitgestelde startmodus en keer terug naar stationair, zodat een machine die nog steeds is ingeschakeld maar nooit is gestart, niet eeuwig wacht.", + "doc": "Stop na zoveel uur met wachten op een uitgestelde start en keer terug naar inactief.", "label": "Time-out uren" }, "device_type": { - "doc": "Apparaatklasse. Stelt verstandige detectiestandaarden in (drempels, uitschakelvertraging, eindafhandeling) afgestemd op dat apparaattype; wijzig dit alleen als het apparaat oorspronkelijk als het verkeerde type was ingesteld.", + "doc": "Stelt detectiestandaarden in (drempels, uitschakelvertraging, eindafhandeling) voor dit soort apparaat. Wijzig het alleen als het apparaat als verkeerd type is ingesteld.", "label": "Apparaattype" }, "door_sensor_entity": { - "doc": "Optionele binaire deursensor. Wordt gebruikt om te detecteren wanneer het apparaat na een cyclus is geopend/uitgeladen.", + "doc": "Optionele deursensor. Toont wanneer het apparaat na een cyclus is geopend om uit te laden.", "label": "Deursensorentiteit" }, "duration_tolerance": { - "doc": "Tolerantie voor resterende tijdschattingen (leerfeedback, niet matching). Als de werkelijke duur binnen +/-X% van de schatting ligt, telt dit als een goede match.", + "doc": "Alleen leerfeedback: een cyclus binnen +/-X% van de geschatte duur telt als een goede schatting. Heeft geen invloed op herkenning.", "label": "Schattingstolerantie" }, "enable_ml_models": { - "doc": "Terwijl een cyclus loopt, kunnen de modellen de live resultaten verfijnen: een stabielere schatting van de resterende tijd en energie/kosten, en een anti-voortijdige stop-bewaking bij einddetectie (deze kan een finish alleen maar uitstellen, nooit eerder eindigen, en is begrensd). Gebruikt uw verfijnde modellen indien beschikbaar, anders de ingebouwde modellen. Uit = alleen de klassieke, op energie gebaseerde logica (nog steeds betrouwbaar).", + "doc": "Gebruik een op deze machine getraind model om energie en kosten van een lopende cyclus te voorspellen, en laat een bekend programma dat ongewoon vroeg stopt eindigen zonder de volledige uitschakelvertraging. Uit: alleen logica op basis van vermogen.", "label": "Slimme modellen gebruiken tijdens een cyclus" }, "end_energy_threshold": { - "doc": "Tijdens het aftellen van de uitschakelvertraging wordt de verzamelde energie (watt x tijd) vergeleken met deze drempel. Bij overschrijding wordt het aftellen gereset, waardoor anti-kreukdroogtrommels en vaatwasserdroogstukken aan het programma blijven gekoppeld in plaats van ze kort te knippen. Verhoog deze als de cycli tijdens het afkoelen te vroeg eindigen; verlaag deze als de detectie traag is.", + "doc": "Toegestane energie tijdens de uitschakelvertraging. Meer dan dit en de cyclus loopt door, zodat trommelen en droognalopen erbij blijven horen. Verlaag als cycli tijdens een naloop eindigen; verhoog als voltooide cycli blijven wachten.", "label": "Eindenergie" }, - "end_repeat_count": { - "doc": "Aantal opeenvolgende metingen onder de stopdrempel vereist voordat de cyclus eindigt. 1 is prima voor de meeste stekkers. Verhoog naar 2-3 als uw slimme stekker halverwege de cyclus af en toe een vals-nulmonster rapporteert en uw cycli voortijdig eindigen.", - "label": "Eindherhalingsaantal" - }, "energy_price_entity": { - "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.", + "doc": "Sensor met de actuele prijs per kWh (bijv. een dynamisch tarief). Gaat vóór de statische prijs hieronder, dus het moet een prijs zijn, niet de vermogens- of energiesensor van de stekker.", "label": "Energieprijsentiteit" }, "energy_price_static": { - "doc": "Vaste prijs per kWh gebruikt voor kostencijfers wanneer er hierboven geen live prijsentiteit is ingesteld.", + "doc": "Vaste prijs per kWh, gebruikt als er geen prijsentiteit is ingesteld.", "label": "Vaste energieprijs (per kWh)" }, "energy_sensor": { - "doc": "Optionele cumulatieve energieteller (total_increasing in kWh/Wh, bijvoorbeeld de eigen levensduurteller van de stekker). Indien ingesteld wordt de gerapporteerde energie van elke cyclus genomen uit het verschil van deze teller tussen begin en einde, wat de onderregistratie voorkomt die je krijgt bij het integreren van een traag rapporterende vermogenssensor. Valt terug op de geïntegreerde waarde als de meting ontbreekt, de eenheid onbekend is of het verschil niet positief is. Laat leeg om de vermogenssensor te blijven integreren.", + "doc": "Optionele kWh- of Wh-teller (total_increasing, bijv. de eigen meter van de stekker). De cyclusenergie wordt dan daaruit gelezen, wat nauwkeuriger is dan berekenen uit een trage vermogenssensor. Is de meting onbruikbaar, dan wordt de berekende waarde gebruikt.", "label": "Energiemeterentiteit" }, "expose_debug_entities": { - "doc": "Publiceer extra diagnostische HA-entiteiten (matchvertrouwen, ambiguïteit, staatsinternals). Uit houdt de entiteitenlijst schoon voor normaal gebruik.", + "doc": "Voeg diagnostische entiteiten toe (matchbetrouwbaarheid, ambiguïteit, interne staat). Uit houdt de entiteitenlijst overzichtelijk.", "label": "Foutopsporingsentiteiten blootstellen" }, "external_end_trigger": { - "doc": "Binaire sensor waarvan de statusverandering het einde van de cyclus markeert (bijvoorbeeld een apparaat \"klaar\" contact of een begeleidende integratie).", + "doc": "Binaire sensor waarvan de wijziging het einde van de cyclus markeert (bijv. een \"klaar\"-contact of een andere integratie).", "label": "Externe triggerentiteit" }, "external_end_trigger_enabled": { - "doc": "Laat een externe binaire sensor het einde van een cyclus signaleren, naast de ingebouwde op stroom gebaseerde detectie.", + "doc": "Laat een binaire sensor het einde van een cyclus melden, naast de detectie op basis van vermogen.", "label": "Externe eindtrigger inschakelen" }, "external_end_trigger_inverted": { - "doc": "Beschouw het uitschakelen van de triggersensor (in plaats van AAN) als het signaal einde cyclus.", + "doc": "Gebruik het UIT gaan (niet AAN) van de sensor als eindsignaal.", "label": "Externe trigger omkeren (activeer bij UIT)" }, "learning_confidence": { - "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.", + "doc": "Laagste matchscore om een voltooide cyclus te labelen. Een duidelijke winnaar wordt gelabeld; een nipte uitslag gaat naar de beoordelingswachtrij in Cycli. Daaronder wordt niets gelabeld of gevraagd. Houd het onder Auto-label betrouwbaarheid.", "label": "Leerbetrouwbaarheid" }, "linked_device": { - "doc": "Nest dit WashData-apparaat optioneel onder een ander apparaat (bijvoorbeeld de slimme plug) in het HA-apparaatregister, weergegeven als \"Verbonden via ...\".", + "doc": "Optioneel. Toon dit WashData-apparaat in Home Assistant onder een ander apparaat (bijv. de slimme stekker), als \"Verbonden via ...\".", "label": "Groepeer onder apparaat" }, "min_off_gap": { - "doc": "Als de machine korter dan deze tijd wordt uitgeschakeld, wordt de aan/uit/aan-reeks behandeld als één continue cyclus. Voorkomt dat weekprogramma's (de machine wordt tijdens het wassen enkele minuten uitgeschakeld) in twee afzonderlijke cycli worden opgesplitst. Stel deze korter in dan de afstand tussen uw rug-aan-rugladingen als u wilt dat deze als afzonderlijke cycli worden geteld. Standaardinstellingen voor het apparaattype beschermen de typische pauze binnen de cyclus voor elk apparaat.", + "doc": "Een uit-periode korter dan dit splitst een cyclus niet, zodat weekpauzes in één was blijven. Houd het korter dan de pauze tussen opeenvolgende ladingen, anders smelten die samen tot één cyclus.", "label": "Min. uitschakelinterval" }, "min_power": { - "doc": "Absoluut minimaal vermogen dat als actief wordt beschouwd. Aflezingen daaronder worden behandeld als 0 W (stand-by), waardoor de fantoombelasting van slimme stekkers en stand-by-LED's wordt weggefilterd.", + "doc": "Metingen hieronder tellen als 0 W (stand-by). Filtert het ruststroomverbruik van slimme stekkers en stand-by-LED's weg.", "label": "Minimaal vermogen" }, "ml_training_enabled": { - "doc": "Bestudeer uw beoordeelde cycli regelmatig van de ene op de andere dag en stem de modellen af op deze specifieke machine. Een verandering wordt alleen behouden als deze daadwerkelijk beter scoort op aanhoudende cycli, dus dit kan alleen maar helpen of hetzelfde blijven - nooit achteruitgaan.", + "doc": "Train het energiemodel af en toe opnieuw op de voltooide cycli van deze machine. Een nieuw model wordt alleen bewaard als het de eenvoudige schatting verslaat op testcycli.", "label": "Leren van deze machine" }, "ml_training_hour": { - "doc": "Lokaal uur van de dag (0-23) voor de nachtelijke fijnafstemming. Kies een rustig uur, bijvoorbeeld 2 (02:00 uur).", + "doc": "Uur van de dag (0-23) voor de nachtelijke training. Kies een rustig uur, bijv. 2.", "label": "Leren om uur" }, "ml_training_interval_days": { - "doc": "Controleer maximaal één keer per zoveel dagen opnieuw of er verbeteringen zijn.", + "doc": "Train hooguit eens per zoveel dagen opnieuw.", "label": "Controleer maximaal elke" }, "ml_training_min_cycles": { - "doc": "Wacht tot er minstens zoveel cycli zijn opgenomen voordat je gaat finetunen, zodat er genoeg is om van te leren.", + "doc": "Wacht tot er minstens zoveel cycli zijn opgenomen voordat er wordt getraind.", "label": "Eerst benodigde cycli" }, "name": { - "doc": "Weergavenaam weergegeven in de HA-integratielijst en het apparaatregister.", + "doc": "Naam van dit apparaat in Home Assistant.", "label": "Apparaatnaam" }, "no_update_active_timeout": { - "doc": "Als er gedurende deze tijd geen stroomupdates arriveren tijdens het gebruik, ga er dan van uit dat de stekker offline is gevallen en forceer een stop om een zombiecyclus te voorkomen. Standaard 600 s maakt cloud- of mesh-vertraging mogelijk.", + "doc": "Komt er tijdens een cyclus zo lang geen vermogensupdate binnen, dan wordt aangenomen dat de stekker offline is en wordt de cyclus geforceerd gestopt. De standaard 600 s houdt rekening met cloud- of mesh-vertraging.", "label": "Time-out zonder update" }, "notify_before_end_minutes": { @@ -1382,35 +1339,35 @@ "label": "Pre-eindalert" }, "notify_channel": { - "doc": "Android-meldingskanaalnaam voor start-/liveberichten (controleert het geluid en de prioriteit per kanaal op de mobiele app). Blank gebruikt standaard de bijbehorende app.", + "doc": "Android-kanaal voor start- en liveberichten; het kanaal bepaalt geluid en prioriteit. Leeg: standaard van de app.", "label": "Android-kanaal (start/live)" }, "notify_cycle_timers": { - "doc": "Meldingen op specifieke minuten in een cyclus (bijvoorbeeld om wasverzachter toe te voegen). Bericht ondersteunt {device}, {program}, {minutes}. Schakel Auto-pauze in om op dat punt te pauzeren en een interactieve melding te ontvangen met een knop Hervatten; hervatten via het paneel, de pauze-/hervattingsservice of de meldingsactie.", + "doc": "Meldingen op vaste minuten in een cyclus (bijv. om wasverzachter toe te voegen). Bericht ondersteunt {device}, {program}, {minutes}. Met Automatische pauze pauzeert de cyclus daar ook en krijgt de melding een knop Hervatten.", "label": "Cyclustimers" }, "notify_finish_channel": { - "doc": "Android-meldingskanaalnaam voor het eindbericht. Blank hergebruikt het start-/livekanaal.", + "doc": "Android-kanaal voor het eindbericht. Leeg: het start-/livekanaal wordt hergebruikt.", "label": "Android-kanaal (eind)" }, "notify_finish_message": { - "doc": "Berichttekst die wordt verzonden wanneer een cyclus is voltooid. Sjabloonvariabelen: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}. {time_finished} en {vs_typical} zijn hier het nuttigst.", + "doc": "Tekst die wordt verstuurd als een cyclus klaar is. Variabelen: {device}, {program}, {duration} (minuten), {duration_hm} (1 h 05 min), {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}, {status} (completed of force_stopped; onderbroken cycli sturen niets). Bij elke andere variabele wordt de standaardtekst gebruikt.", "label": "Eindbericht" }, "notify_finish_services": { - "doc": "notificeren.* services die worden aangeroepen wanneer een cyclus is voltooid. Voeg één per doel toe; laat leeg als er geen eindmelding is.", + "doc": "notify.*-services die worden aangeroepen als een cyclus klaar is. Laat leeg voor geen.", "label": "Eindservices" }, "notify_fire_events": { - "doc": "Activeer ook ha_washdata_*-gebeurtenissen bij het starten/beëindigen van de cyclus, zodat u uw eigen automatiseringen kunt bouwen.", + "doc": "Vuur ook ha_washdata_*-gebeurtenissen af bij start en einde van een cyclus, voor uw eigen automatiseringen.", "label": "HA-gebeurtenissen afvuren voor meldingen" }, "notify_icon": { - "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.", + "doc": "Klein pictogram op de melding (bijv. mdi:washing-machine). Wordt alleen naar mobile_app-doelen gestuurd, niet naar notify-groepen. Op iOS vervangt het het app-pictogram en is companion-app 2026.8 of nieuwer nodig. Leeg: platformstandaard.", "label": "Meldingspictogram" }, "notify_live_chronometer": { - "doc": "Toon een live bijgewerkte afteltimer in de melding (op platforms die dit ondersteunen) in plaats van een statische schatting.", + "doc": "Toon een lopende afteltimer in de melding (waar ondersteund) in plaats van een vaste schatting.", "label": "Live chronometer gebruiken" }, "notify_live_interval_seconds": { @@ -1418,143 +1375,135 @@ "label": "Live update-interval" }, "notify_live_overrun_percent": { - "doc": "Als een cyclus de schatting met meer dan dit percentage overschrijdt, verzendt u een overschrijdingswaarschuwing.", + "doc": "Live updates stoppen zodra een cyclus dit percentage langer duurt dan verwacht (20 = tot 1,2x de verwachte tijd).", "label": "Live overschrijding % voor alert" }, "notify_live_services": { - "doc": "notificeren.* services vroegen om live voortgangsupdates terwijl een cyclus loopt. Laat dit leeg om live-voortgangsmeldingen uit te schakelen.", + "doc": "notify.*-services voor live voortgangsupdates tijdens een cyclus. Laat leeg om ze uit te zetten.", "label": "Live voortgangsservices" }, "notify_only_when_home": { - "doc": "Stuur alleen meldingen als minimaal één van de gekoppelde personen (hierboven) thuis is.", + "doc": "Alleen melden als minstens één van de personen hierboven thuis is.", "label": "Alleen melden als thuis" }, "notify_people": { - "doc": "person.* entiteiten die worden gebruikt door \"Alleen melden wanneer thuis\" om te beslissen of er iemand thuis is.", + "doc": "person.*-entiteiten die \"Alleen melden als thuis\" controleert.", "label": "Personen (voor Alleen thuis)" }, "notify_pre_complete_message": { - "doc": "Tekst van de melding vóór het einde / bijna klaar. Sjabloonvariabelen: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Tekst van elke live voortgangsupdate. Sjabloonvariabelen: {device}, {program}, {minutes} (resterende minuten). Bij elke andere variabele wordt de standaardtekst gebruikt.", "label": "Bijna klaar-bericht" }, "notify_reminder_message": { - "doc": "Tekst van de herinnering dat er nog moet worden uitgeladen. Leeg gebruikt de ingebouwde standaard. Sjabloonvariabelen: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Tekst van de eenmalige Pre-eindalert. Sjabloonvariabelen: {device}, {program}, {minutes} (de instelling Pre-eindalert). Bij elke andere variabele wordt de standaardtekst gebruikt.", "label": "Herinneringsbericht" }, "notify_start_message": { - "doc": "Berichttekst die wordt verzonden wanneer een cyclus start. Sjabloonvariabelen: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Tekst die wordt verstuurd als een cyclus start. Sjabloonvariabelen: {device}, {program}. Bij elke andere variabele wordt de standaardtekst gebruikt.", "label": "Startbericht" }, "notify_start_services": { - "doc": "notificeren.* services die worden aangeroepen wanneer een cyclus start. Voeg één toe per doel (telefoon, dashboard, etc.); laat leeg als er geen startmelding is.", + "doc": "notify.*-services die worden aangeroepen als een cyclus start (telefoon, dashboard, ...). Laat leeg voor geen.", "label": "Startservices" }, "notify_timeout_seconds": { - "doc": "Sluit de melding automatisch na dit aantal seconden (op platforms die dit ondersteunen). 0 behoudt het totdat het handmatig wordt verwijderd.", + "doc": "Sluit de melding na zoveel seconden (waar ondersteund). 0 houdt hem tot u hem sluit.", "label": "Automatisch verwijderen na" }, "notify_title": { - "doc": "Titel van melding. Sjabloonvariabelen: {device}, {program}, {duration}, {energy}, {cost}, {date}, {time}, {minutes}.", + "doc": "Titel van melding. Alleen {device} wordt ingevuld; bij elke andere variabele wordt de standaardtitel gebruikt.", "label": "Meldingstitel" }, "notify_unload_delay_minutes": { - "doc": "Minuten nadat een cyclus is geëindigd voordat de nog wachtende herinnering voor het leegmaken van de machine wordt verzonden. Stel 0 in om de herinnering uit te schakelen.", + "doc": "Minuten na het einde van een cyclus voor de herinnering \"machine uitladen\". 0 schakelt hem uit.", "label": "Vertraging uitlaadherinnering" }, "notify_unload_repeat": { - "doc": "Blijft de uitlaadherinnering elke \"Vertraging uitlaadherinnering\" minuten opnieuw verzenden totdat het uitladen is bevestigd of u in de melding op \"Stop met herinneren\" tikt. Vereist een manier om het uitladen te bevestigen: een Deursensorentiteit, een Uitlaadbevestigingsentiteit of de optie \"Uitladen handmatig bevestigen\" hieronder. De sluitknop werkt bij meldingen van de Home Assistant companion-app (mobiel).", + "doc": "Stuur de uitlaadherinnering elke \"Vertraging uitlaadherinnering\" minuten opnieuw tot het uitladen is bevestigd of u op \"Stop met herinneren\" tikt (mobiele app). Vereist een Deursensorentiteit, een Uitlaadbevestigingsentiteit of \"Uitladen handmatig bevestigen\".", "label": "Herhalen tot uitgeladen" }, "off_delay": { - "doc": "Tijd om te wachten nadat de stroom is uitgevallen voordat de cyclus als voltooid wordt verklaard. Als de stroom binnen dit venster wordt hervat, gaat de cyclus naadloos verder - dit overbrugt pauzes tussen wasfasen. Vaatwassers hebben lange droogfasen (uitschakeling gedurende 20-60 minuten), dus de uitschakelvertraging moet groter zijn dan die om de hele was+droogcyclus als één cyclus te laten verlopen.", + "doc": "Hoe lang het vermogen laag moet blijven voordat de cyclus klaar is. Komt het vermogen binnen deze tijd terug, dan loopt de cyclus door. Vaatwassers kunnen tijdens het drogen 20-60 min stil zijn, dus stel het langer in.", "label": "Uitschakelvertraging" }, "pause_cuts_power": { - "doc": "Wanneer een cyclus wordt gepauzeerd, schakel dan ook onderstaande Schakelentiteit uit. Alleen voor apparaten waarvan de stekker halverwege de cyclus veilig kan worden doorgesneden.", + "doc": "Schakel bij het pauzeren van een cyclus ook de Schakelentiteit hieronder uit. Alleen voor apparaten die halverwege een cyclus veilig stroomloos kunnen.", "label": "Pauze schakelt ook vermogen uit (via schakelaar)" }, "power_off_delay": { - "doc": "Hoe lang het vermogen na het voltooien van een cyclus onder de Uitschakeldrempel moet blijven voordat de status terugkeert naar Uit. Wordt alleen gebruikt wanneer de Uitschakeldrempel boven 0 is. Wordt gecontroleerd op de achtergrondcadans, dus de effectieve vertraging wordt naar boven afgerond tot de volgende tick waarop de status verloopt.", + "doc": "Hoe lang het vermogen na een cyclus onder de Uitschakeldrempel moet blijven voordat de staat terug naar Uit gaat. Alleen gebruikt als die drempel boven 0 ligt. Wordt periodiek gecontroleerd, dus het echte wachten kan iets langer zijn.", "label": "Uitschakelvertraging" }, "power_off_threshold_w": { - "doc": "Optionele, op vermogen gebaseerde Uit-detectie. Bij een waarde boven 0 wordt de machine als uitgeschakeld beschouwd en keert de status terug naar Uit zodra een cyclus is voltooid en het vermogen gedurende de Uitschakelvertraging onder dit niveau blijft. Laat op 0 staan om dit uit te schakelen (de standaard). Stel de waarde in boven het werkelijke uitgeschakelde nulniveau en onder de Stopdrempel en onder het standby-verbruik van uw machine in de toestand voltooid-maar-aan; als de waarde niet onder de Stopdrempel ligt, wordt deze genegeerd. Indien ingeschakeld vervangt deze waarde de Voortgangsreset vertraging voor het terugkeren naar Uit, zodat een voltooide machine in Voltooid/Schoon blijft totdat deze daadwerkelijk wordt uitgeschakeld.", + "doc": "Na een cyclus zet vermogen onder deze waarde gedurende de Uitschakelvertraging de staat op Uit, in plaats van de Voortgangsreset vertraging. Stel het in boven het verbruik in uitgeschakelde toestand, onder stand-by en de Stopdrempel. 0 schakelt het uit (standaard).", "label": "Uitschakeldrempel" }, "power_sensor": { - "doc": "De sensorentiteit die het live vermogen in watt voor dit apparaat rapporteert (bijvoorbeeld sensor.washer_power). Alle cyclusdetectie is gebaseerd op dit signaal.", + "doc": "De sensor die het actuele vermogen van dit apparaat in watt meldt (bijv. sensor.washer_power). Alle detectie is erop gebaseerd.", "label": "Vermogenssensor" }, - "profile_duration_tolerance": { - "doc": "De +/- band rond een gemiddelde profielduur die wordt gebruikt tijdens het matchen. 0,25 betekent dat een profiel van 60 minuten overeenkomt met cycli van 45-75 minuten.", - "label": "Profiel duurtoleratie" - }, "profile_match_interval": { - "doc": "Hoe vaak profielmatching moet worden geprobeerd tijdens een hardloopcyclus. Standaard 300 s (5 minuten) balanceert detectiesnelheid en CPU.", + "doc": "Hoe vaak een lopende cyclus met de profielen wordt vergeleken. Standaard 300 s (5 min).", "label": "Afstemmingsinterval" }, "profile_match_max_duration_ratio": { - "doc": "Maximale cycluslengte ten opzichte van het profiel. 1.3 betekent dat een cyclus minder dan 130% van de profielduur moet bedragen om te matchen.", + "doc": "Een programma valt af zodra de cyclus langer loopt dan dit veelvoud van de typische duur. 1,8: een cyclus die 80% langer loopt dan het programma, matcht er niet meer mee.", "label": "Max. duurverhouding" }, "profile_match_min_duration_ratio": { - "doc": "Minimale cycluslengte ten opzichte van het profiel. 0,9 betekent dat een cyclus minimaal 90% van de profielduur moet bedragen om te matchen.", + "doc": "Een programma telt pas mee als de cyclus dit deel van de typische duur heeft gelopen. 0,10: een programma van 2 uur wordt na 12 minuten kandidaat. Niet toegepast in de eerste 15 minuten van een cyclus.", "label": "Min. duurverhouding" }, "profile_match_threshold": { - "doc": "Minimale gelijkheidsscore (0-1) vereist aan het einde van de cyclus om een programma-identificatie te accepteren. Verhoog het om verkeerde identificaties te verminderen; verlaag het als de programma's van uw machine niet overeenkomen. Standaard 0,4 is een conservatief uitgangspunt.", + "doc": "Live matchscore die een cyclus nodig heeft voordat de slimme beëindiging of de anti-kreukafronding hem vroeg mag beëindigen. Verandert het getoonde programma niet. Verhoog als zwakke matches cycli te vroeg beëindigen; verlaag als duidelijke matches de uitschakelvertraging afwachten.", "label": "Afstemmingsdrempel" }, "profile_unmatch_threshold": { - "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.", + "doc": "Zakt de live matchscore hieronder, dan wordt de huidige programmagok gewist. Houd het iets onder de Afstemmingsdrempel, zodat een korte dip hem niet reset.", "label": "Niet-afstemmingsdrempel" }, "progress_reset_delay": { - "doc": "Nadat u klaar bent, houdt u de voortgang zo lang op 100% vast, zodat Voltooid zichtbaar is op dashboards voordat u terugzet naar Inactief.", + "doc": "Houd na een cyclus de voortgang zo lang op 100% voordat er wordt teruggezet naar Inactief.", "label": "Voortgangsreset vertraging" }, "pump_stuck_duration": { - "doc": "Seconden dat een pomp continu kan draaien voordat deze wordt gemarkeerd als mogelijk vastgelopen (de gebeurtenis Vastgelopen pomp wordt geactiveerd).", + "doc": "Hoe lang een pomp onafgebroken mag draaien voordat de gebeurtenis Vastgelopen pomp afgaat.", "label": "Duur pomp vastzitten" }, "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.", + "doc": "Minimale tijd tussen verwerkte metingen. Metingen die eerder binnenkomen, worden overgeslagen (lage metingen tijdens een cyclus blijven altijd). Stel het in op het meldinterval van uw stekker of lager; een hogere waarde gooit alleen metingen weg.", "label": "Samplinginterval" }, "save_debug_traces": { - "doc": "Bewaar de volledige vermogenstrace en overeenkomende foutopsporingsgegevens voor elke cyclus. Handig voor het oplossen van problemen, maar vergroot de opslaggrootte.", + "doc": "Bewaar ook de foutopsporingsgegevens van de herkenning (de scores van elke kandidaat) bij elke cyclus, voor probleemoplossing. Kost ongeveer 20 KB per cyclus.", "label": "Foutopsporingssporen opslaan" }, "smart_termination_duration_ratio": { - "doc": "Hoe ver een cyclus in de verwachte duur van het herkende programma moet zijn voordat de slimme beëindiging hem vroegtijdig mag afsluiten zodra het vermogen daalt. De verwachte duur is het gemiddelde van het programma, dus bij apparaten waarvan de looptijd sterk varieert - wasmachines met koud toevoerwater in de winter versus warm in de zomer, drogers met vochtsensor, belastingafhankelijke programma's - eindigt ongeveer de helft van alle draaien korter dan dat gemiddelde en krijgt nooit de snelle beëindiging, maar eindigt pas minuten later via de fallback-time-out. Verlaag deze waarde (bijv. 0,85) op zulke machines zodat de vroegtijdige beëindiging toch wordt geactiveerd; verhoog hem richting 1,0 om voorzichtiger te zijn. Laat leeg voor de standaard (0,98, of 0,99 voor vaatwassers). Hij kan een cyclus alleen eerder beëindigen, nooit later, en wordt nooit geactiveerd bij een dubbelzinnige of onzekere herkenning.", + "doc": "Deel van de gemiddelde programmaduur dat een cyclus moet halen voordat de slimme beëindiging hem vroeg mag afsluiten. Variëren de looptijden (watertemperatuur, vochtsensor, beladingsgrootte), verlaag het dan (bijv. 0,85). Laat leeg voor 0,98 (0,99 bij vaatwassers).", "label": "Verhouding slimme beëindiging" }, - "smoothing_window": { - "doc": "Hoeveel het ruwe stroomsignaal wordt afgevlakt. Laag (2) is responsief maar luidruchtig; hoog (5) verzacht pieken maar voegt vertraging toe.", - "label": "Afvlakkingsvenster" - }, "start_duration_threshold": { - "doc": "Het vermogen moet zo lang boven de startdrempel blijven om een echte start te bevestigen, waardoor wordt voorkomen dat aan/uit-schakelaars van een fractie van een seconde een cyclus starten.", + "doc": "Het vermogen moet zo lang boven de Startdrempel blijven voordat een cyclus start, zodat korte aan/uit-pieken worden genegeerd.", "label": "Startduur" }, "start_energy_threshold": { - "doc": "Energie (vermogen x tijd) die het apparaat moet verbruiken voordat het WERKT. Een korte piek met hoog vermogen heeft een zeer lage energie en wordt genegeerd, waardoor valse starts worden voorkomen.", + "doc": "Energie die het apparaat moet verbruiken voordat de cyclus als lopend telt. Een korte piek verbruikt weinig energie en start dus geen cyclus.", "label": "Startenergie" }, "start_threshold_w": { - "doc": "Het vermogen moet boven dit niveau stijgen om te bevestigen dat een cyclus is begonnen. Als u deze te laag instelt, worden valse starts veroorzaakt door de standby-stroom; te hoge en langzaam startende programma's (koud vullen) worden gemist. De suggestie-engine stelt dit net boven het waargenomen laagste actieve vermogen van de machine in.", + "doc": "Het vermogen moet hierboven stijgen om een cyclus te starten. Te laag: stand-byverbruik geeft valse starts. Te hoog: langzaam startende programma's (koud vullen) worden gemist.", "label": "Startdrempel" }, "stop_threshold_w": { - "doc": "Het vermogen moet onder dit niveau dalen voordat het aftellen van de uitschakelvertraging begint. Stel deze in onder de startdrempel. De ruimte daartussen is de hysteresisband die flikkering voorkomt. Als de fases met laag vermogen (spoelstops, anti-kreuk) te hoog zijn ingesteld, wordt ten onrechte de eindsequentie geactiveerd.", + "doc": "Het vermogen moet hieronder zakken voordat de uitschakelvertraging begint te tellen. Houd het onder de Startdrempel. Te hoog, en fasen met laag vermogen (spoelpauzes, anti-kreuk) starten per ongeluk het aftellen naar het einde.", "label": "Stopdrempel" }, "switch_entity": { - "doc": "Optionele schakelaar die wordt uitgeschakeld bij pauze en weer ingeschakeld bij hervatten, gebruikt in combinatie met \"Pauze schakelt ook de stroom uit\".", + "doc": "Optionele schakelaar die bij pauze uit en bij hervatten weer aan gaat, met \"Pauze schakelt ook vermogen uit\".", "label": "Schakelentiteit" }, "watchdog_interval": { - "doc": "Hoe vaak de achtergrondwaakhond controleert op vastgelopen sensoren en verstreken time-outs. Standaard 30 sec.", + "doc": "Hoe vaak de achtergrondcontrole zoekt naar vastgelopen sensoren en time-outs. Standaard 30 s.", "label": "Waakhondinterval" }, "notify_milestone_message": { @@ -1562,127 +1511,82 @@ "label": "Mijlpaalbericht" }, "notify_milestones": { - "doc": "Door komma's gescheiden aantallen cycli die een eenmalige felicitatiemelding activeren wanneer ze worden bereikt (bijv. 50, 100, 500, 1000). Leeg schakelt mijlpaalmeldingen uit.", + "doc": "Aantallen cycli die een eenmalige felicitatiemelding geven, gescheiden door komma's (bijv. 50, 100, 500, 1000). Leeg schakelt ze uit.", "label": "Cyclusmijlpalen" }, "notify_quiet_end_hour": { - "doc": "Einde van het niet-storen-venster (0-23). Vastgehouden meldingen worden op dit uur bezorgd. Laat leeg om uit te schakelen.", + "doc": "Einde van het niet-storen-venster (0-23); vastgehouden meldingen worden dan verstuurd. Leeg schakelt het uit.", "label": "Einde stille uren" }, "notify_quiet_start_hour": { - "doc": "Begin van een niet-storen-venster (0-23). Meldingen over voltooiing, herinneringen en schone-was-meldingen die tijdens de stille uren zouden afgaan, worden vastgehouden en bezorgd wanneer het venster eindigt. Laat leeg om uit te schakelen. Ondersteunt vensters die middernacht overschrijden (bijv. start 22, einde 7).", + "doc": "Begin van een niet-storen-venster (0-23). Eind-, herinnerings- en schone-was-meldingen in dit venster worden vastgehouden tot het eindigt. Kan over middernacht gaan (bijv. 22 tot 7). Leeg schakelt het uit.", "label": "Begin stille uren" }, "peak_rate_message": { - "doc": "Optionele aangepaste tekst voor de piektarief-tip die aan de startmelding wordt toegevoegd. Sjabloonvariabelen: {device}, {price}. Leeg gebruikt de ingebouwde standaard.", + "doc": "Eigen tekst voor de piektarief-tip. Sjabloonvariabelen: {device}, {price}. Leeg gebruikt de standaard.", "label": "Bericht bij piektarief" }, "peak_rate_threshold": { - "doc": "Wanneer een cyclus start en de huidige prijs per kWh op of boven deze waarde ligt, wordt een piektarief-tip aan de startmelding toegevoegd. 0 of leeg schakelt de tip uit.", + "doc": "Ligt de prijs per kWh bij de start van een cyclus op of boven deze waarde, dan krijgt de startmelding een piektarief-tip. 0 of leeg schakelt dit uit.", "label": "Drempel piektarief (per kWh)" }, - "dtw_bandwidth": { - "label": "Tolerantie voor vormherkenning", - "doc": "Hoeveel de herkenning een cyclus in de tijd mag vervormen om bij de vorm van een opgeslagen programma te passen. Hoger staat meer snelheidsvariatie toe; te hoog riskeert het verkeerde programma te herkennen." - }, - "corr_weight": { - "label": "Gewicht vorm versus niveau", - "doc": "Balans tussen curvevorm en absoluut vermogensniveau bij het beoordelen van een herkenning. Hoger geeft voorkeur aan de vorm; lager geeft voorkeur aan het vermogensniveau." - }, - "duration_weight": { - "label": "Gewicht van duur", - "doc": "Hoe sterk de voorkeur uitgaat naar een programma waarvan de typische looptijd bij deze cyclus past." - }, - "energy_weight": { - "label": "Gewicht van energie", - "doc": "Hoe sterk de voorkeur uitgaat naar een programma waarvan het typische energieverbruik bij deze cyclus past." - }, "store_brand": { "label": "Merk van apparaat", - "doc": "Optioneel. Het merk van het apparaat, gekozen uit de communitycatalogus. Wordt gebruikt om passende referentieopnamen te vinden en te delen. Laat leeg als je geen onlinefuncties gebruikt." + "doc": "Optioneel. Het merk van uw apparaat uit de communitycatalogus, om referentieopnamen te vinden en te delen. Laat leeg als u geen onlinefuncties gebruikt." }, "store_model": { "label": "Model van apparaat", - "doc": "Optioneel. Het model van het apparaat, gekozen uit de communitycatalogus zodra een merk is ingesteld. Als je model niet vermeld staat, kun je het toevoegen aan de catalogus." + "doc": "Optioneel. Het model van uw apparaat uit de communitycatalogus (kies eerst een merk). Staat het er niet bij, dan kunt u het toevoegen." }, "show_contributor": { "doc": "Toont de vermelding 'door ' op community-apparaten en referentiecycli." }, - "enable_phase_matching": { - "label": "Fasebewuste resterende tijd", - "doc": "Verdeel elke lopende cyclus in fasen (verwarmen, wassen, centrifugeren) en begroot de resterende tijd per fase, gemengd met de klassieke schatting - vroeg in de cyclus leunend op het fasebudget en tegen het einde op de klassieke schatting. Dit stemt het aftellen af op hoe lang uw machine daadwerkelijk verwarmt en draait, wat het meest merkbaar is in de eerste helft van een cyclus. Uit = alleen de klassieke schatting. Alleen de weergave van de resterende tijd wordt beïnvloed; programma-matching en cyclusdetectie blijven ongewijzigd." - }, - "keep_min_score": { - "label": "Min. matchscore", - "doc": "Minimale gelijkenisscore die een kandidaat nodig heeft om in de race te blijven. De standaard 0,1 is bewust permissief - het laat zelfs zwakke kandidaten toe en vertrouwt op latere stappen om de beste overeenkomst te vinden. Verhoog het om onwaarschijnlijke profielen eerder te verwijderen; verlaag het om de initiële kandidatenpool te verbreden." - }, - "dtw_blend": { - "label": "Warp-mengfactor", - "doc": "Hoeveel de tijdvervormingsscore (DTW) de kernscore van stap 2 vervangt. 0 = gebruik alleen de stap 2-score, 1 = gebruik alleen de DTW-score, 0,5 (standaard) = gelijke mix. Verhoog het om meer te vertrouwen op warp-uitlijning wanneer programma's vergelijkbare vermogensniveaus maar verschillende tijdpatronen hebben." - }, - "dtw_ensemble_w": { - "label": "Warp-ensemblemix", - "doc": "In ensemble DTW-modus (de standaard), het gewicht op geschaalde L1 versus afgeleid DTW (DDTW). 1,0 = alleen geschaalde L1, 0 = alleen DDTW, 0,7 = standaard. De geschaalde L1-variant vergelijkt vermogensniveaus; de afgeleide variant reageert op hoe vermogen verandert over de tijd. Ensemble mengt beide." - }, - "dtw_ddtw_scale": { - "label": "Afgeleide warp-schaal", - "doc": "Halfsaturatieafstand voor de afgeleide DTW-beoordeling. De score halveert wanneer de DDTW-afstand gelijk is aan deze waarde. Kleiner = gevoeliger voor vormverschillen. Standaard 30 is gekalibreerd op typische vermogenssporen van apparaten. Beïnvloedt alleen ensemble- en ddtw DTW-modi." - }, - "dtw_refine_top_n": { - "label": "Warp-verfijningsaantal", - "doc": "Hoeveel top stap 2-kandidaten opnieuw worden beoordeeld door DTW. DTW is duurder en wordt daarom alleen toegepast op de beste N kandidaten. Standaard 5. Verhoog het (naar 7-9) als het juiste profiel na stap 2 soms alleen de 4e of 5e plaats bereikt - DTW kan het redden. Verlagen versnelt het matchen iets." - }, - "duration_scale": { - "label": "Duur-scherpte", - "doc": "Scherpte van de duurovereenkomststraf in stap 4. Dit is de log-verhouding waarbij de overeenkomstscore halveert. Kleiner = strenger: een duurafwijking kost meer punten. Standaard 0,175 komt overeen met ongeveer 18% duurtolerантie op half gewicht. Combineer met Duurgewicht." - }, - "energy_scale": { - "label": "Energie-scherpte", - "doc": "Scherpte van de energieovereenkomststraf. Kleiner = strenger: een energiemismatch kost meer punten. Standaard 0,25 is bewust meer vergevingsgezind dan Duurscherpte, omdat energie varieert met de belasting. Combineer met Energiegewicht." - }, "power_profile_interval_min": { "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." + "doc": "Bucketgrootte van het attribuut power_profile (gemiddelde watt per slot) dat planners zoals EMHASS en tibber_prices gebruiken. Kleiner houdt korte pieken; groter maakt de curve gladder. Standaard 15 min. Heeft geen invloed op 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." + "doc": "Alleen iOS. Werk de live voortgang stil bij, met een batterijsparende prioriteit die iOS kan bundelen. De eerste update en de start-/eindmeldingen geven nog steeds een signaal. Uit: elke update geeft een signaal." }, "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." + "doc": "Alleen Android. Houd de live voortgangsmelding vast bij een tik. Uit: een tik sluit hem." }, "notify_live_click_action": { "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." + "doc": "Wat een tik op de melding van dit apparaat opent. Leeg: het WashData-paneel bij dit apparaat. Voer een dashboardpad (bijv. /lovelace/laundry) of URL in, of \"none\" voor geen tikactie." }, "door_opens_at_end": { "label": "Deur opent automatisch aan het einde", - "doc": "Voor vaatwassers die aan het einde van de cyclus de deur openen om te drogen (AirDry en vergelijkbaar). Als dit is ingeschakeld, pauzeert een deur openen tijdens een actieve cyclus de cyclus niet permanent; als de deur de ingestelde verwijltijd lang open blijft, beschouwt WashData de cyclus als voltooid. Een korte opening (om een artikel toe te voegen) wordt genegeerd. Vereist een deursensorentiteit." + "doc": "Voor vaatwassers die aan het einde de deur openen om te drogen (bijv. AirDry). Een deur die de verblijftijd hieronder open blijft, beëindigt de cyclus in plaats van hem te pauzeren; kort openen wordt genegeerd. Vereist een Deursensorentiteit." }, "door_end_dwell_seconds": { "label": "Deuropen-eindverwijltijd", - "doc": "Hoe lang de deur open moet blijven voordat WashData de cyclus beëindigt, wanneer \"Deur opent automatisch aan het einde\" is ingeschakeld. Lang genoeg om het snel toevoegen van een bord te negeren (standaard 60 s), kort genoeg om de cyclus snel te beëindigen zodra de machine de deur opent." + "doc": "Met \"Deur opent automatisch aan het einde\" aan: hoe lang de deur open moet blijven voordat de cyclus eindigt. Lang genoeg om het bijzetten van een bord te negeren (standaard 60 s)." }, "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." + "doc": "Welke soorten cycli de curve en herkenning van elk programma vormen. Een soort uitvinken verwijdert niets; handig als u geïmporteerde data niet vertrouwt. Statistieken tellen altijd alleen de cycli van deze machine. Alles uitvinken wordt genegeerd." }, "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." + "doc": "Reken elke cyclus af tegen de prijs op elk moment dat hij liep, in plaats van de prijs bij het einde. Vereist een prijsentiteit. Geschiedenis verwerken (Diagnostiek) herberekent oudere cycli." }, "unload_confirm_entity": { "label": "Uitlaadbevestigingsentiteit", - "doc": "Optionele entiteit waarvan de activering betekent dat de lading eruit is gehaald, voor machines die geen deursensor kunnen hebben. Elk type werkt: een Zigbee- of NFC-knop, een input_button-helper, een bewegingssensor voor de machine, een scène. Elke statuswijziging telt als bevestiging, behalve uitschakelen en niet beschikbaar worden. Door dit in te stellen wordt de uitlaadherinnering ook ingeschakeld op een apparaat zonder Deursensorentiteit." + "doc": "Optioneel. Elke entiteit die bevestigt dat de lading is uitgehaald, voor machines zonder deursensor (een knop, input_button, bewegingssensor, scène). Elke statuswijziging telt, behalve uitschakelen of niet beschikbaar. Schakelt ook de uitlaadherinnering in." }, "unload_track_without_door": { "label": "Uitladen handmatig bevestigen", - "doc": "Voert de uitlaadherinnering uit op een apparaat zonder Deursensorentiteit en zonder Uitlaadbevestigingsentiteit, waarbij u het uitladen in plaats daarvan vanuit uw eigen automatisering bevestigt: druk op de knopentiteit \"Markeren als uitgeladen\" of roep de actie ha_washdata.mark_unloaded aan. Wordt genegeerd wanneer er een Deursensorentiteit is ingesteld, die het signaal al levert." + "doc": "Gebruik de uitlaadherinnering zonder deursensor of bevestigingsentiteit. Bevestig het uitladen vanuit uw eigen automatisering met de knop \"Markeren als uitgeladen\" of de service ha_washdata.mark_unloaded. Genegeerd als een Deursensorentiteit is ingesteld." }, "notify_icon_color": { "label": "Meldingskleur", - "doc": "Accentkleur voor de meldingen van dit apparaat, zodat een wasmachine, een droger en een vaatwasser in één oogopslag te onderscheiden zijn. iOS kleurt het pictogram en de voortgangsbalk van de Live-activiteit op het toegangsscherm en in de Dynamic Island. Android krijgt dezelfde kleur doorgestuurd, maar wat ermee gebeurt, bepaalt de telefoon: sommige kleuren het pictogram en veel toestellen (zeker Samsung One UI) negeren de kleur volledig. Beide begrijpen hex-waarden (bijvoorbeeld #03A9F4). Laat dit leeg voor de platformstandaard." + "doc": "Accentkleur voor de meldingen van dit apparaat, zodat wasmachine, droger en vaatwasser makkelijk te onderscheiden zijn. Op iOS vult het de cirkel achter het pictogram en kleurt het de balk van de Live Activity. Veel Android-telefoons (vooral Samsung) negeren het. Gebruik hex, bijv. #03A9F4. Leeg: platformstandaard." + }, + "share_name": { + "doc": "Toon uw accountnaam als uploader van cycli die u deelt. Uit: gedeeld zonder naam." } }, "setting_group": { @@ -1710,9 +1614,6 @@ "door_pause": { "label": "Deur & pauze" }, - "dtw_refinement": { - "label": "DTW verfijning" - }, "duration_gates": { "label": "Duurpoorten" }, @@ -1789,17 +1690,20 @@ "rebuilding": "Opnieuw opbouwen…", "analyzing": "Analyseren…", "resetting": "Resetten…", - "reverting": "Terugzetten…", "trimming": "Bijsnijden…", "splitting": "Splitsen…", + "stalled": "Vastgelopen", "deleting": "Verwijderen…", "imported": "Geïmporteerd", "preparing": "Voorbereiden…", - "unlabelling": "Label verwijderen…" + "unlabelling": "Label verwijderen…", + "working": "Bezig…", + "match_uncertain": "Onzeker: {top} of {runner}", + "match_uncertain_single": "Onzeker: misschien {top}", + "match_sure": "~{pct}% zeker", + "match_uncertain_tip": "WashData heeft nog geen programma gekozen. Het getal toont hoe vaak de koploper, {top}, in eerdere cycli juist was als hij zo ver voorlag. Alleen ter informatie." }, "suggestion": { - "both_agree": "WashData raadt aan", - "calibrated_label": "Gekalibreerd", "observed_label": "Gemeten", "impact": { "auto_label_confidence": { @@ -1814,10 +1718,6 @@ "higher": "Sluit alleen cycli af met aanzienlijk energieverbruik", "lower": "Sluit ook kortere of energiearme cycli af" }, - "end_repeat_count": { - "higher": "Eindsignaal moet vaker herhalen – robuuster, iets langzamer", - "lower": "Stopt na minder herhalingen – snellere detectie, meer kans op vals einde" - }, "learning_confidence": { "higher": "Leert alleen van zeer zekere overeenkomsten – langzamer maar betrouwbaarder", "lower": "Leert van meer cycli – model kan minder nauwkeurig worden" @@ -1856,40 +1756,25 @@ } }, "reason": { - "auto_label_confidence": "Betrouwbaarheid op het 15e percentiel van {cycles} automatische labels die de gebruiker nooit heeft gecorrigeerd ({p15}).", "completion_min_seconds": "De helft van de p05-duur van schone cycli ({minutes} min) over {cycles} cycli; filtert spookcycli eruit.{excl}", - "dead_zone_batch": "Kort gehouden (mediane instabiliteitsvenster bij het opstarten over {cycles} cycli = {s}s) zodat een echt einde snel wordt gedetecteerd.", - "dead_zone_from_dip": "Op basis van een vroege vermogensdip die is gedetecteerd bij {s}s.", - "end_energy_default": "Aanbevolen standaardwaarde voor de ruisdrempel aan het einde van de cyclus.", - "end_energy_false": "p95 van de energie bij valse einden ({p95}Wh) over {cycles} cycli, met een ondergrens van 0.2% van de mediane cyclusenergie ({floor}Wh).", - "end_energy_no_false": "Geen valse einden over {cycles} cycli; met een ondergrens van 0.2% van de mediane cyclusenergie ({floor}Wh).", - "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.", + "match_interval_floored": "Het kortste programma ({shortest}s) zou dit begrenzen op {cap}s, onder het minimum van {minimum}s, dus wordt het minimum gebruikt: {persistence} opeenvolgende matches duren {budget}s, meer dan {pct}% van dat programma. De herkenning draait alleen als er een meting binnenkomt (mediaan={median}s), dus dit 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.", "min_off_gap": "Op basis van {gaps} waargenomen tussenpozen tussen cycli (p05={p05}s). Ondergrens van het apparaat: {floor}s.", "min_off_gap_bridge": "Zo gedimensioneerd dat de langste stille periode binnen een cyclus wordt overbrugd: {span}s + 60s buffer, uit {spans} overbrugde tussenpozen over {cycles} schone cycli (blijft onder uw kortste tussenpoos van {ceiling}s tussen twee opeenvolgende ladingen).", "min_power": "40% van het laagste actieve vermogen op p05 ({p05}W) over {cycles} schone cycli, zodat de uitschakeldrempel onder het werkelijke verbruik blijft.{excl}", - "ml_auto_label": "Laagste betrouwbaarheid die het kwaliteitsmodel nog als schoon beoordeelde (p10 van {cycles} schone cycli = {p10}).", - "ml_end_repeat": "{false}/{total} cycli ({pct}%) hadden een pauze die de einddetector >50% scoorde; vereis {val} eindbevestiging(en).", - "ml_off_delay": "Door de einddetector bevestigde pauzes: p95 {p95}s + 60s buffer, uit {pauses} door het model geverifieerde pauzes over {cycles} cycli (ondergrens {floor}s).", "no_update_timeout": "Op basis van de waargenomen updatecadans (p95={p95}s) * 20 (min. 60s).", "off_delay_cadence": "Op basis van de waargenomen updatecadans (p95={p95}s) * 5", "off_delay_device_floor": "Apparaatspecifiek veilig minimum gebruikt voor {device} ({floor}s).", "off_delay_generic_floor": "Generiek veilig minimum gebruikt ({floor}s).", "off_delay_pauses": "Zo gedimensioneerd dat echte pauzes worden overbrugd: p95 van de pauze binnen een cyclus {p95}s + 60s buffer, uit {pauses} pauzes over {cycles} schone cycli (ondergrens {floor}s).", - "profile_match_threshold": "p10-betrouwbaarheid van {cycles} correcte automatische labels ({p10}); veilige ondergrens voor live-bevestiging.", - "sampling_interval": "Mediaan-updateinterval waargenomen over {cycles} schone cycli ({si}s).{excl}", - "smoothing_window": "Zo gedimensioneerd dat ~30s aan metingen bij {si}s-bemonstering wordt afgevlakt ({samples} metingen).", - "start_duration": "Kort gehouden (~één bemonsteringsinterval van {si}s) zodat de detectie zo vroeg mogelijk begint, maar losse pieken van één meting toch worden genegeerd.", - "thr_batch": "Net boven het laagste actieve vermogen op p05 over {cycles} cycli ({p05}W) gehouden zodat een start zo vroeg mogelijk wordt opgemerkt en de stopdrempel onder het laagste draaivermogen van de machine blijft.", - "tol_per_profile": "p75 van de duurvariantie per profiel over {profiles} profielen ({cycles} cycli); strakke profielen worden niet benadeeld.", - "tol_pooled": "Op basis van de gebundelde duurvariantie van {cycles} recent gelabelde cycli (p95-afwijking={dev}).", - "watchdog": "Zo laag als veilig gehouden (net boven de p95-updatekloof van {p95}s en minstens 2x het meetinterval van {median}s, min. 30s) zodat vastlopers snel worden opgemerkt zonder valse stops." + "standby_floor": "Elke recente cyclus verbruikte nog {idle}W toen die eindigde, zodat een drempel eronder nooit kan registreren dat het apparaat uitschakelt. Boven die waarde gehouden; de {cycles} schone cycli in het overzicht hebben er nooit meer dan {pause}s onder gepauzeerd.", + "thr_batch": "Net boven het laagste actieve vermogen (p05) over {cycles} cycli ({p05}W), zodat starts vroeg worden opgemerkt en de stopdrempel onder het laagste draaivermogen blijft.", + "watchdog": "Zo laag als veilig gehouden (net boven de p95-updatekloof van {p95}s en minstens 2x het meetinterval van {median}s, min. 30s) zodat vastlopers snel worden opgemerkt zonder valse stops.", + "end_energy_floor": "Verhoogd tot de ondergrens die volgt uit de stopdrempel ({stop} W) gedurende de uitschakelvertraging ({off_delay} s); daaronder kan de cyclus alleen via een terugvalpad eindigen.", + "max_duration_ratio_default": "Terug naar de standaardwaarde: een lager plafond wijst het juiste programma af zodra een cyclus langer duurt dan gebruikelijk.", + "min_duration_ratio_default": "Terug naar de standaardwaarde: een hogere ondergrens zorgt ervoor dat een lopende cyclus pas wordt herkend als hij al ver gevorderd is." }, "exclusions": { "summary": "{total} verkeerd gedetecteerde cyclus(sen) uitgesloten: {parts}.", @@ -1907,7 +1792,8 @@ "no_power": "geen vermogen", "no_active_power": "geen actief vermogen" } - } + }, + "now_value": "(nu {v})" }, "tab": { "advanced": "Geavanceerd", @@ -1937,6 +1823,11 @@ "wrong_profile": "Verkeerd profiel" }, "toast": { + "task_name_required": "Geef de taak een naam", + "task_added": "Taak toegevoegd", + "task_save_failed": "Kan taak niet opslaan: {error}", + "task_removed": "Taak verwijderd", + "maint_logged": "Geregistreerd als gedaan: {task}", "catalog_refreshed": "Communitycatalogus vernieuwd", "access_saved": "Toegangscontrole opgeslagen", "all_wiped": "Alle gegevens gewist", @@ -1944,7 +1835,6 @@ "auto_label_complete": "Automatisch labelen voltooid", "automation_deleted": "Automatisering verwijderd", "correction_submitted": "Correctie ingediend", - "cycle_deleted": "Cyclus verwijderd", "cycle_force_stopped": "Cyclus geforceerd gestopt", "cycle_labelled": "Cyclus gelabeld", "cycle_paused": "Cyclus onderbroken", @@ -1960,11 +1850,9 @@ "group_deleted": "Groep verwijderd", "group_name_required": "Groepsnaam is vereist", "group_saved": "Groep opgeslagen", - "import_successful": "Importeren succesvol; integratie herladen", "json_required": "JSON-gegevens zijn vereist", "legacy_removed": "Verouderde acties verwijderd", "logs_exported": "Logboeken geëxporteerd", - "matching_reverted": "Overeenkomende gewichten zijn teruggezet naar de standaardwaarden", "min_2_profiles": "Selecteer minimaal 2 profielen voor een groep", "ml_training_no_improvement": "Training voltooid: basislijn behouden (geen verbetering)", "models_reverted": "Modellen op het apparaat zijn teruggekeerd naar de basislijn", @@ -1974,8 +1862,8 @@ "pg_preset_loaded": "Voorinstelling \"{name}\" geladen", "pg_preset_saved": "Voorinstelling \"{name}\" opgeslagen", "pg_sugg_loaded": "{n} gesuggereerde waarde(n) geladen - voer de speeltuin uit om te vergelijken", - "pg_sugg_ml_loaded": "{n} ML-gekalibreerde waarde(n) geladen - voer de speeltuin uit om te vergelijken", "phase_name_required": "Fasenaam is vereist", + "phase_reset": "\"{name}\" hersteld naar de ingebouwde fase", "phase_updated": "Fase bijgewerkt", "phases_saved": "Fasen opgeslagen", "preferences_saved": "Voorkeuren opgeslagen", @@ -2022,14 +1910,10 @@ "store_name_required": "Voer een profielnaam in", "store_import_failed": "Importeren mislukt: {error}", "store_imported": "Geïmporteerd in {profile}", - "store_program_required": "Voer een programmanaam in", "store_no_appliance": "Stel eerst het merk en model van je apparaat in bij Instellingen.", "store_share_failed": "Delen mislukt: {error}", "store_shared": "Gedeeld in de communitystore, in afwachting van beoordeling.", "appliance_added": "Apparaat toegevoegd, wacht op goedkeuring", - "thanks_confirming": "Bedankt voor het bevestigen", - "device_approved": "Goedgekeurd door de community", - "rating_saved": "Kwaliteitsbeoordeling opgeslagen", "brand_added": "Merk toegevoegd, wacht op goedkeuring", "profile_added": "Profiel toegevoegd, wacht op goedkeuring", "saved_except_conflicts": "Opgeslagen. Los de gemarkeerde conflicten op om de rest op te slaan.", @@ -2053,13 +1937,58 @@ "odometer_save_failed": "Kan totaal niet opslaan: {error}", "contrib_device_changed": "Toegevoegd aan de communitystore, maar er is van apparaat gewisseld terwijl het formulier openstond. Selecteer het bij Instellingen.", "store_pick_profile_share": "Kies een profiel om naar te delen", - "remove_failed": "Verwijderen mislukt: {error}" + "remove_failed": "Verwijderen mislukt: {error}", + "analysis_complete": "Analyse voltooid: {count} suggestie(s)", + "analysis_failed": "Analyse mislukt: {error}", + "auto_detect_failed": "Automatische detectie mislukt: {error}", + "auto_label_failed": "Automatisch labelen mislukt: {error}", + "auto_label_done": "Automatisch labelen voltooid: {count} cycli gelabeld, {skipped} ongelabeld gelaten.", + "auto_label_done_one": "Automatisch labelen voltooid: {count} cyclus gelabeld, {skipped} ongelabeld gelaten.", + "auto_label_done_other": "Automatisch labelen voltooid: {count} cycli gelabeld, {skipped} ongelabeld gelaten.", + "auto_label_cancelled": "Automatisch labelen geannuleerd nadat {count} cycli zijn gelabeld.", + "auto_label_cancelled_one": "Automatisch labelen geannuleerd nadat {count} cyclus is gelabeld.", + "auto_label_cancelled_other": "Automatisch labelen geannuleerd nadat {count} cycli zijn gelabeld.", + "could_not_load_cycle": "Kan cyclus niet laden: {error}", + "create_failed": "Aanmaken mislukt: {error}", + "cycles_deleted": "Verwijderde cycli: {count}", + "debug_cleared": "Gewiste debugsporen: {count}", + "discard_failed": "Verwerpen mislukt: {error}", + "error": "Fout: {error}", + "export_failed": "Exporteren mislukt: {error}", + "import_failed": "Importeren mislukt: {error}", + "invalid_json": "\"{key}\" is geen geldige JSON - corrigeer het of maak het veld leeg voordat u opslaat.", + "label_failed": "Labelen mislukt: {error}", + "ml_training_promoted": "Training voltooid: {count} model(len) geactiveerd", + "pause_failed": "Pauzeren mislukt: {error}", + "phase_created": "Fase \"{name}\" aangemaakt", + "phase_deleted": "Fase \"{name}\" verwijderd", + "profile_created": "Profiel \"{name}\" aangemaakt", + "rename_failed": "Hernoemen mislukt: {error}", + "save_failed": "Opslaan mislukt: {error}", + "split_complete": "Gesplitst in {count} cycli", + "start_failed": "Starten mislukt: {error}", + "stop_failed": "Stoppen mislukt: {error}", + "update_failed": "Bijwerken mislukt: {error}", + "store_download_skipped": "{n} opname(s) overgeslagen: te kort, met te veel gaten of al op uw apparaat.", + "store_import_low_quality": "Deze opname is te kort, bevat te veel gaten of is onaannemelijk en kan niet worden geïmporteerd.", + "store_import_duplicate": "Deze opname staat al op uw apparaat.", + "device_approved": "Goedgekeurd door de community", + "thanks_confirming": "Bedankt voor het bevestigen", + "rating_saved": "Kwaliteitsbeoordeling opgeslagen", + "store_download_partial": "{n} programma('s) kon(den) niet worden gedownload; probeer het later opnieuw.", + "import_no_restore_point": "Geïmporteerd, maar er kon geen herstelpunt worden opgeslagen, dus deze import kan niet ongedaan worden gemaakt", + "import_undo_failed": "Kan de import niet ongedaan maken: {error}", + "import_undone": "Import ongedaan gemaakt: {profiles} profiel(en) en {cycles} cyclus(sen) hersteld" }, "trend": { "down": "Trend naar beneden", "up": "Trend omhoog" }, "maint": { + "salt": "Zout bijvullen", + "rinse_aid": "Glansmiddel bijvullen", + "lint_filter": "Pluizenfilter reinigen", + "condenser_clean": "Condensor reinigen", "bearing_service": "Lageronderhoud", "descale": "Ontkalken", "drum_clean": "Trommel reinigen", @@ -2071,12 +2000,14 @@ "status_tip": "Cyclusresultaat: Voltooid (natuurlijk einde), Onderbroken (abrupte vermogensdaling), Geforceerd gestopt (handmatig) of Beoordeling nodig (feedback in behandeling).", "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, 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.", + "energy_tip": "Totaal verbruikte energie (kWh), berekend uit het vermogen over de tijd.", + "cost_tip": "Energiekosten van deze cyclus, vastgelegd bij het einde. Met Tijdgewogen kosten aan wordt elk moment tegen zijn eigen prijs afgerekend; anders tegen de prijs aan het einde. 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.", - "cost_dynamic_tip": "Tijdgewogen: afgerekend tegen de prijs die tijdens de cyclus gold (effectief {price} {cur}/kWh)." + "cost_dynamic_tip": "Tijdgewogen: afgerekend tegen de prijs die tijdens de cyclus gold (effectief {price} {cur}/kWh).", + "dbg_conf": "Vertr.", + "dbg_mae": "MAE", + "dbg_corr": "Corr." }, "stat": { "avg": "gem.", @@ -2100,7 +2031,10 @@ "notify_finish": "Er zou een voltooiingsmelding worden verzonden.", "notify_milestone": "Er zou een mijlpaalmelding worden verzonden.", "notify_held": "Een melding werd vastgehouden vanwege de stille uren.", - "finished": "De cyclus bereikte een eindtoestand en stopte." + "finished": "De cyclus bereikte een eindtoestand en stopte.", + "match_reverted": "Het programma viel terug naar detecteren: zijn score zakte ruim onder zijn eigen piek.", + "verified_pause": "De live uitlijning bevestigde een pauze binnen het programma (of hief die op). Zolang die geldt, wordt de cyclus niet beëindigd.", + "stalled": "De cyclus bleef langer op stand-byverbruik dan dit programma daar ooit pauzeert, terwijl er nog werk te doen was. Getoond als Gepauzeerd (Vastgelopen); de cyclus blijft open." }, "store": { "search_ph2": "Zoeken op merk of model…", @@ -2109,11 +2043,7 @@ "programs_count": "Programma's: {n}", "browse": "Bladeren", "device": "Apparaat", - "favorites": "Favorieten", - "search_ph": "Zoeken op merk of model…", "no_results": "Geen passende apparaten gevonden. Probeer een andere zoekopdracht.", - "n_programs": "{n} programma's", - "n_cycles": "{n} cycli", "no_programs": "Nog geen gedeelde programma's voor dit apparaat.", "no_cycles": "Nog geen referentiecycli gedeeld voor dit programma.", "anon": "anoniem", @@ -2125,19 +2055,21 @@ "merge_target": "Samenvoegen in profiel", "description": "Beschrijving (optioneel)", "contributed_by": "door {name}", + "website": "Communitystore-website ↗", + "website_tip": "Open de communitystore-website", + "favorites": "Favorieten", "rating_summary": "★ {avg} ({n})", "no_ratings": "Nog geen beoordelingen", - "website": "Communitystore-website ↗", - "website_tip": "Open de communitystore-website" + "unreachable": "Kan de communitystore niet bereiken. Controleer uw verbinding en probeer het opnieuw." }, "settings": { "gear": { - "title": "Instellingen" + "title": "Voorkeuren" } }, "link": { - "manual": "Handleiding ↗", - "add_appliance": "Je apparaat toevoegen" + "maintenance_supporter": "Maintenance Supporter op GitHub ↗", + "manual": "Handleiding ↗" }, "tip": { "add_brand": "Merk niet vermeld? Voeg het toe aan de communitycatalogus", @@ -2171,7 +2103,13 @@ "rebuild": { "envelopes": "Enveloppen opnieuw opbouwen" }, - "cancelling": "Annuleren..." + "cancelling": "Annuleren...", + "auto_label": { + "running": "Cycli automatisch labelen" + }, + "store_download": { + "running": "Downloaden uit de store" + } }, "setup": { "hdr": { @@ -2180,26 +2118,24 @@ }, "phase0": { "washer": "WashData detecteert uw cycli al. Neem uw eerste cyclus op om programmanamen en tijdschattingen in te schakelen.", - "dishwasher": "WashData kijkt mee. Vaatwassers hebben complexe cycli – het opnemen van uw eerste cyclus wordt sterk aanbevolen. Als een gedetecteerde cyclus te lang duurt, gebruik dan de cycluseditor om hem bij te snijden vóór het opslaan als profiel.", + "dishwasher": "WashData kijkt mee. Vaatwascycli zijn complex, dus neem uw eerste cyclus op. Duurt een gedetecteerde cyclus te lang, snijd hem dan bij in de cycluseditor voordat u hem als profiel opslaat.", "generic": "WashData kijkt mee. Neem een cyclus op of label een gedetecteerde cyclus om te beginnen met het opbouwen van profielen." }, "phase1a": { - "labelled": "Goed begin – uw eerste programma is opgeslagen. Voor de zuiverste gegevens kunt u uw volgende cyclus opnemen met de recorder-widget." + "labelled": "Goed begin: uw eerste programma is opgeslagen. Neem voor de zuiverste gegevens uw volgende cyclus op met de recorder." }, "phase1b": { - "recorded": "Uw opname is opgeslagen als {profile_name}. Neem nu uw andere veelgebruikte programma's op of label ze om de dekking uit te breiden." + "recorded": "Uw opname is opgeslagen als {profile_name}. Neem nu uw andere veelgebruikte programma's op of label ze." }, "phase1c": { - "verify": "U heeft {count} programma's van de community. Voer een cyclus uit om te verifiëren dat WashData deze correct herkent – de herkenning verbetert naarmate uw apparaat zijn eigen geschiedenis opbouwt." + "verify": "U hebt {count} programma's van de community. Draai een cyclus om te controleren of WashData hem herkent; de herkenning verbetert naarmate uw apparaat eigen geschiedenis opbouwt." }, "phase2": { - "cluster": "WashData heeft {count} cycli gezien die niet overeenkomen met een opgeslagen programma – ze lijken op elkaar. Wilt u een nieuw profiel voor ze aanmaken?", + "cluster": "WashData heeft {count} vergelijkbare cycli gezien die bij geen opgeslagen programma passen. Een nieuw profiel voor ze aanmaken?", "unmatched": "Uw laatste cyclus kwam niet overeen met een opgeslagen programma. Is het een nieuw programma?" }, "phase3": { - "suggestions": "WashData heeft instellingsaanbevelingen op basis van uw cyclusgeschiedenis – bekijk ze om de detectienauwkeurigheid te verbeteren.", - "groups": "Sommige van uw profielen lijken op hetzelfde programma bij verschillende temperaturen. Organiseer ze in een groep voor betere herkenning.", - "phases": "Voeg programmasfases toe aan {profile_name} voor nauwkeurigere schattingen van de resterende tijd." + "suggestions": "WashData heeft instellingssuggesties op basis van uw cyclusgeschiedenis. Bekijk ze om de detectie te verbeteren." }, "phase4": { "healthy": "Dit apparaat is volledig ingesteld ({profile_count} profielen)." @@ -2212,12 +2148,58 @@ "create_from_cluster": "Profiel maken van deze cycli", "create_profile": "Maak er een profiel voor", "review_suggestions": "Aanbevelingen bekijken", - "organise_profiles": "Profielen organiseren", - "configure_phases": "Fasen configureren", "skip_step": "Sla deze stap over", "skip_forever": "Niet meer weergeven", "hide_guidance": "Begeleiding verbergen", "show_guidance": "Begeleiding weergeven" } + }, + "access": { + "edit": "Bewerken", + "full": "Volledig", + "inherit": "Overnemen", + "none": "Geen (verborgen)", + "read": "Lezen" + }, + "pg_label_reason": { + "no_winner": "Geen programma paste", + "below_floor": "Onder de leerbetrouwbaarheid", + "ambiguous": "Als dubbelzinnig gemarkeerd", + "margin": "Te dicht bij de nummer twee", + "unknown_profile": "Programma bestaat niet meer", + "not_evaluated": "Niet geëvalueerd" + }, + "diagram": { + "accum": "cumulatief", + "auto": "auto", + "below_off": "eronder = uit", + "confirmed": "bevestigd", + "energy_counts": "energie telt", + "feedback": "feedback", + "gap_below_min": "interval onder min = één cyclus", + "heat_phase": "verwarmingsfase", + "held_at_100": "op 100% gehouden", + "ignored": "genegeerd", + "kept": "behouden", + "max": "max", + "min": "min", + "minus_tol": "-tol", + "no_match": "geen match", + "no_updates": "geen updates", + "off_delay_wait": "uitschakelvertraging", + "off_gap": "uit-interval", + "plus_tol": "+tol", + "profile": "profiel", + "reading_interval": "typisch meetinterval", + "sensor_offline": "sensor offline: geforceerde stop", + "si": "SI", + "spike_ignored": "piek genegeerd", + "spike_ignored_short": "piek: genegeerd", + "start": "start", + "stop": "stop", + "tail_energy_resets": "restenergie boven drempel: timer reset", + "thr": "drempel", + "too_short": "te kort", + "tumble_kept": "tuimelpulsen behouden" } } diff --git a/custom_components/ha_washdata/translations/panel/pl.json b/custom_components/ha_washdata/translations/panel/pl.json index 16dff5e3..b5c468ba 100644 --- a/custom_components/ha_washdata/translations/panel/pl.json +++ b/custom_components/ha_washdata/translations/panel/pl.json @@ -3,6 +3,7 @@ "min": "min", "entity_not_found": "Nie znaleziono encji", "running_long": "Działa długo", + "stalled": "Utknął", "remaining": "pozostało", "progress": "postęp", "show_time_remaining": "Pokaż pozostały czas", @@ -48,16 +49,18 @@ } }, "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_tip": "Pod koniec ten program cichnie na około {mins} min, a potem pobiera około {watts} W przez {secs} s. Widoczne w {seen} z {measured} zmierzonych cykli.", "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", + "artifact_tip_one": "Wykryto {n} anomalię (np. drzwi otwarte w połowie cyklu) – otwórz, aby zobaczyć ją na wykresie", + "artifact_tip_few": "Wykryto {n} anomalie (np. drzwi otwarte w połowie cyklu) – otwórz, aby zobaczyć je na wykresie", + "artifact_tip_many": "Wykryto {n} anomalii (np. drzwi otwarte w połowie cyklu) – otwórz, aby zobaczyć je na wykresie", + "artifact_tip_other": "Wykryto {n} anomalii (np. drzwi otwarte w połowie cyklu) – otwórz, aby zobaczyć je na wykresie", "auto": "(automatycznie wykryty)", "built_in_tag": "wbudowany", "declining": "↘ maleje", "energy_low": "Niższe zużycie energii niż zwykle", "energy_spike": "Wyższe zużycie energii niż zwykle", - "fair_fit": "akceptowalne dopasowanie", - "fair_fit_tip": "Umiarkowana spójność dopasowania – niektóre cykle przypisane do tego profilu mają niższy poziom pewności. Oznacz więcej cykli lub ponownie zapisz profil, aby poprawić dokładność.", "feedback_requested": "Poproszono o opinię", "golden_cycle": "Zarejestrowany cykl referencyjny", "duration_outlier": "Cykle o nietypowej długości: {n}", @@ -66,27 +69,40 @@ "needs_review": "Wymaga recenzji", "overrun": "Trwało dłużej niż zwykle", "poor_fit": "słabe dopasowanie", - "poor_fit_tip": "Niespójna historia dopasowań - rozważ przebudowę tego profilu", - "restart_gap_tip": "{n} przerwa restartu HA: przebieg mocy ma lukę", + "restart_gap_tip": "{n} przerwy restartu HA: przebieg mocy ma luki", + "restart_gap_tip_one": "{n} przerwa restartu HA: przebieg mocy ma lukę", + "restart_gap_tip_few": "{n} przerwy restartu HA: przebieg mocy ma luki", + "restart_gap_tip_many": "{n} przerw restartu HA: przebieg mocy ma luki", + "restart_gap_tip_other": "{n} przerwy restartu HA: przebieg mocy ma luki", "reviewed": "Zrecenzowano", "steady": "→ stały", "underrun": "Zakończono szybciej niż zwykle", - "using_defaults": "Używanie dostarczonych ustawień domyślnych", - "using_tuned": "Używanie dostrojonych wag", "overrun_ratio": "({x}x oczekiwanego)", "underrun_ratio": "({pct}% oczekiwanego)", - "awaiting_tip": "Oczekuje na zatwierdzenie przez społeczność", - "awaiting_n": "Oczekuje na zatwierdzenie · potwierdzono {n}", - "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.", - "armed": "(dotyczy następnego cyklu)" + "armed": "(dotyczy następnego cyklu)", + "awaiting_tip": "Oczekuje na zatwierdzenie przez społeczność", + "awaiting_n": "Oczekuje na zatwierdzenie · potwierdzono {n}", + "awaiting": "Oczekuje na zatwierdzenie", + "approved": "Zatwierdzone", + "matcher_unused": "Jeszcze nie dopasowano do Twoich cykli", + "matcher_used": "Dopasowano do {n} Twoich cykli", + "matcher_used_one": "Dopasowano do {n} Twojego cyklu", + "matcher_used_few": "Dopasowano do {n} Twoich cykli", + "matcher_used_many": "Dopasowano do {n} Twoich cykli", + "matcher_used_other": "Dopasowano do {n} cyklu", + "matcher_used_tip": "Cykle na tym urządzeniu, które WashData samodzielnie dopasował do tego zaimportowanego programu. Import, który nigdy nie zostaje dopasowany, może nie pasować do Twojego urządzenia; możesz go usunąć. Liczone lokalnie; nic nie jest wysyłane do sklepu." }, "btn": { + "pg_preset_delete": "Usuń preset", + "pg_preset_load": "Wczytaj preset", + "pg_preset_load_tip": "Zastąp wartości w piaskownicy tym presetem (aktywne ustawienia pozostają nietknięte)", + "pg_preset_save": "Zapisz jako preset", + "pg_preset_save_tip": "Zapisz wszystkie poniższe wartości jako nazwany preset dla tego urządzenia", "reset_zoom": "Resetuj powiększenie", "set_brand_model": "Ustaw markę i model", "refresh_catalog": "Odśwież katalog", @@ -97,12 +113,11 @@ "add_timer": "+ Dodaj timer", "apply_all": "Zastosuj wszystko", "apply_label": "Zastosuj etykietę", - "apply_set_b": "Zastosuj zestaw B", "apply_split": "Zastosuj podział", "apply_trim": "Zastosuj przycięcie", "auto_detect_split": "Automatyczne wykrywanie", "auto_label_cycles": "Cykle automatycznego etykietowania", - "auto_label_cycles_tip": "Automatycznie przypisz nazwy profili do nieoznakowanych cykli, których pewność dopasowania przekracza próg", + "auto_label_cycles_tip": "Nadaj nazwy nieoznaczonym cyklom, których pewność dopasowania przekracza próg.", "cancel": "Anuluj", "clear_debug": "Wyczyść dane debugowania", "clear_splits": "Wyczyść", @@ -112,8 +127,6 @@ "convert_to_automation": "Przekształć w automatyzację", "correct": "Popraw…", "create": "Utwórz", - "create_from_cluster": "Utwórz profil z {count} cykli", - "create_profile": "+ Utwórz profil", "delete": "Usuń", "delete_group": "Usuń grupę", "delete_group_tip": "Usuń tylko tę grupę – profile członków zostaną zachowane", @@ -122,39 +135,30 @@ "discard_tip": "Odrzuć nagranie bez zapisywania", "dismiss": "Odrzuć", "mute_suggestion": "Przestań sugerować to ustawienie", - "pg_load_calibrated": "Wczytaj skalibrowane (ML) ({n})", - "pg_load_calibrated_tip": "Załaduj ustawienia skalibrowane przez ML jako nadpisania w piaskownicy", "pg_load_live": "Wczytaj aktywne ustawienia", "pg_load_live_tip": "Wczytaj ponownie bieżące ustawienia integracji i odrzuć wszystkie zmiany z piaskownicy", "pg_load_suggested": "Wczytaj sugerowane ({n})", "pg_load_suggested_tip": "Załaduj bieżące sugestie automatycznego dostrajania jako nadpisania w piaskownicy", - "pg_preset_delete": "Usuń preset", - "pg_preset_load": "Wczytaj preset", - "pg_preset_load_tip": "Zastąp wartości w piaskownicy tym presetem (aktywne ustawienia pozostają nietknięte)", - "pg_preset_save": "Zapisz jako preset", - "pg_preset_save_tip": "Zapisz wszystkie poniższe wartości jako nazwany preset dla tego urządzenia", "pg_publish_all": "Zapisz w integracji ({n})", "pg_publish_one": "Zapisz {label} w integracji", "reset_muted": "Zresetuj wyciszone", + "reset_phase_tip": "Cofnij swoją edycję i przywróć wbudowaną fazę", "done": "Zrobione", "edit": "Edytuj", "export": "Eksportuj", - "export_json": "Eksportuj do JSON", "force_stop": "Wymuś zatrzymanie", "force_stop_tip": "Natychmiast zakończ bieżący cykl i oznacz go jako wymuszony do zatrzymania", "from_template": "Z szablonu ▾", "ignore": "Ignoruj", "import_json": "Importuj z JSON", - "import_overwrite": "Importuj (nadpisuje dane)", "inspect": "Zbadaj", "label": "Oznacz", - "load": "Wczytaj", "load_more": "Wczytaj więcej", "manage": "Zarządzaj", "merge": "Scal", "new_automation": "+ Nowa automatyzacja", "new_group": "+ Nowa grupa", - "new_group_tip": "Grupuj niemal identyczne profile (ten sam kształt/czas trwania, różna temperatura lub wirowanie), aby dopasowanie niezawodnie wybierało między nimi", + "new_group_tip": "Grupuj niemal identyczne profile (ten sam kształt, inna temperatura lub wirowanie), aby dopasowanie umiało je rozróżnić.", "new_phase": "+ Nowa faza", "new_profile": "+ Nowy profil", "new_profile_tip": "Utwórz nowy profil programu z istniejącego oznakowanego cyklu lub nagrania", @@ -162,23 +166,19 @@ "on_cycle_started": "Po rozpoczęciu cyklu", "pause_cycle": "Pauza", "pause_cycle_tip": "Wstrzymaj bieżący cykl – urządzenie wznowi pracę od miejsca, w którym zostało zatrzymane", - "pg_apply_device": "Zastosuj do urządzenia", - "pg_autofill": "Wypełnij automatycznie", - "play": "Odtwórz", "process": "Przetwórz", "process_history": "Przetwarzaj teraz", "process_recording": "Zapisz nagranie", "process_tip": "Zapisz zarejestrowany przebieg jako nowy lub istniejący profil", "rebuild": "Odbuduj koperty", "rebuild_envelope": "Odbuduj kopertę", - "rebuild_tip": "Przelicz oczekiwaną obwiednię mocy (pasmo min/maks) dla wszystkich profili z ich oznakowanych cykli – uruchom po oznaczeniu nowych cykli lub korekcie starych", + "rebuild_tip": "Odbuduj oczekiwane pasmo mocy każdego profilu z jego oznaczonych cykli. Uruchom po oznaczeniu lub poprawieniu cykli.", "rec_start_tip": "Rozpocznij rejestrowanie przebiegu mocy urządzenia – uruchom tuż przed rozpoczęciem cyklu", "rec_stop": "Zatrzymaj", "rec_stop_tip": "Zatrzymaj nagrywanie i zachowaj zarejestrowany przebieg do przeglądu", "record": "Rozpocznij nagrywanie", "refresh": "Odśwież", "refresh_diag_tip": "Przeładuj statystyki przechowywania", - "refresh_logs_tip": "Przeładuj najnowsze rekordy dziennika", "refresh_settings_tip": "Przeładuj ustawienia z serwera", "relabel": "Zmień etykietę ({count})", "remove": "Usuń", @@ -186,7 +186,6 @@ "reset": "Resetuj", "reset_ml_models_tip": "Odrzuć modele dopracowane i wróć do wbudowanych. WashData może później nauczyć się ich ponownie.", "reset_to_builtin": "Zresetuj do modeli wbudowanych", - "reset_to_defaults": "Przywróć ustawienia domyślne", "resume_cycle": "Wznów cykl", "resume_cycle_tip": "Wznów wstrzymany cykl", "revert_settings": "Odrzuć zmiany", @@ -199,8 +198,6 @@ "run_analysis": "🔍 Przeprowadź analizę sugestii", "run_analysis_tip": "Przeanalizuj już teraz swoje zarejestrowane cykle i odśwież sugestie dotyczące tuningu", "run_auto_label": "Uruchom automatyczne etykietowanie", - "run_both": "Uruchom A vs B", - "run_simulation": "Uruchom symulację", "save": "Zapisz", "save_access_control": "Zapisz kontrolę dostępu", "save_group": "Zapisz grupę", @@ -212,10 +209,8 @@ "save_settings": "Zapisz ustawienia", "select": "Wybierz", "select_all": "Zaznacz wszystko", - "select_last20": "Ostatnie 20", "select_none": "Wyczyść", "show_only": "Pokaż tylko", - "skip_setup": "Pomiń konfigurację", "split": "Podziel", "stop": "Zatrzymaj", "submit_correction": "Prześlij poprawkę", @@ -224,30 +219,24 @@ "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.", + "unlabel_selected_tip": "Usuń etykietę z zaznaczonych cykli. Zostaną w historii, ale przestaną kształtować ten profil.", "use": "Użyj", "wipe_all": "Wyczyść wszystkie dane", "delete_selected": "Usuń zaznaczone ({n})", - "group_suggest": "Grupa {n}: {members}", "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", "import": "Importuj", "disconnect": "Rozłącz", "connect_github": "Połącz z GitHub", - "save_appliance": "Zapisz urządzenie", "share_to_store": "Udostępnij w sklepie", "share": "Udostępnij", - "confirm_appliance": "Potwierdź to urządzenie", - "confirmed": "Potwierdzone", "browse_community_setups": "Przeglądaj konfiguracje społeczności", "download_device": "Pobierz konfigurację urządzenia", "share_device": "Udostępnij konfigurację urządzenia", - "share_device_tip": "Udostępnij programy i ustawienia tego urządzenia społeczności", + "share_device_tip": "Udostępnij to urządzenie i jego cykle referencyjne w sklepie społeczności, aby właściciele tej samej maszyny mogli z nich skorzystać.", "share_n": "Udostępnij cykle: {n}", "export_selected": "Eksport (wybierz dane)", "export_all": "Szybki eksport wszystkiego", - "import_raw": "Zaawansowane: zastąp wszystko z pliku JSON", "download_export": "Pobierz eksport", "analyze_import": "Analizuj plik", "import_selected": "Importuj wybrane", @@ -260,7 +249,15 @@ "save_total_cycles": "Zapisz łączną liczbę", "clear": "Wyczyść", "add_brand": "Dodaj markę", - "add_appliance": "Dodaj urządzenie" + "log_done_task": "Oznacz jako wykonane: {task}", + "add_task": "Dodaj zadanie", + "add_appliance": "Dodaj urządzenie", + "retry": "Ponów", + "confirm_appliance": "Potwierdź to urządzenie", + "confirmed": "Potwierdzone", + "create_profile_from_gap": "Utwórz profil", + "undo_import": "Cofnij ostatni import", + "pg_rerun": "Aktualizuj symulację" }, "note": { "off_delay": { @@ -268,10 +265,6 @@ } }, "conflict": { - "anti_wrinkle_exit": { - "exit": "Musi być poniżej Progu Zatrzymania ({stop} W); w przeciwnym razie moc wyjścia antygniotowego jest ignorowana", - "stop": "Musi być powyżej Progu Wyjścia Mocy antygniotowej ({exit} W)" - }, "anti_wrinkle_max": { "max": "Musi być powyżej Progu Startu ({start} W); w przeciwnym razie limit czasu trwania ochrony antygniotowej jest pomijany", "start": "Musi być poniżej Maks. Mocy Antygniotowej ({max} W)" @@ -309,14 +302,14 @@ "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." + "power_sensor": "To jest Czujnik Mocy urządzenia, a nie cena. Cykle byłyby wyceniane w watach za kWh; wyczyść to pole.", + "energy_sensor": "To jest Licznik Energii urządzenia, a nie cena. Cykle byłyby wyceniane według wskazania licznika za kWh; wyczyść to pole.", + "device_class": "Ten czujnik mierzy {dc}, a nie cenę za kWh. Wyczyść to pole lub wybierz czujnik taryfy.", + "unit": "Ten czujnik podaje wartości w {unit}, a nie cenę za kWh. Wyczyść to pole lub wybierz 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" + "duration": "Musi być mniejszy niż Limit Czasu Braku Aktualizacji ({to} s), aby alarm zacięcia zadziałał pierwszy", + "timeout": "Musi przekraczać Czas Zacięcia Pompy ({ps} s), aby alarm zacięcia zadziałał pierwszy" }, "start_dur": { "sampling": "Interwał Próbkowania przekracza Czas Trwania Startu ({sdt} s); pojedynczy impuls może otworzyć cykl", @@ -334,10 +327,15 @@ "sampling": "Interwał Próbkowania powinien wynosić co najwyżej połowę Interwału Watchdog ({wi} s)" }, "settings_banner": "Konflikty ustawień: {n}. Sprawdź podświetlone sekcje i popraw je przed zapisaniem.", - "settings_banner_btn": "Przejdź do pierwszego" + "settings_banner_btn": "Przejdź do pierwszego", + "end_energy": { + "energy": "Zbyt restrykcyjne dla Progu Zatrzymania ({w} W) w czasie Opóźnienia Wyłączenia ({d} s); cykl może się zakończyć tylko ścieżką awaryjną", + "stop": "Energia Zakończenia ({e} Wh w ciągu {d} s) dopuszcza tylko {p} W" + } }, "hdr": { - "ab_comparison": "Porównanie ustawień A/B", + "custom_tasks": "Własne zadania", + "maintenance_supporter": "Potrzebujesz więcej?", "access_control": "Kontrola dostępu", "add_maintenance": "Dodaj zdarzenie konserwacji", "automation_delete": "Usuń tę automatyzację", @@ -346,28 +344,22 @@ "automation_review_this_cycle": "Przejrzyj ten cykl", "automations": "Automatyka", "clear_debug": "Wyczyść ślady debugowania", - "cycle_simulator": "Symulator cykli", "diagnostics_pref": "Diagnostyka", "display": "Wyświetlacz", - "dtw_inspector": "Inspektor DTW", "export_import": "Eksportuj/Importuj", - "getting_started": "Pierwsze kroki", "live_power": "Moc na żywo", "logs": "Dzienniki", - "logs_diagnostics": "Diagnostyka", "maintenance": "Działania konserwacyjne", "maintenance_log": "Dziennik konserwacji", "maintenance_reminders": "Przypomnienia serwisowe", "manual_recording": "Nagrywanie ręczne", - "manual_recording_tip": "Uruchom cykl celowo, gdy WashData rejestruje przebieg mocy. Naciśnij Start tuż przed uruchomieniem urządzenia, Stop po zakończeniu, a następnie Przetwórz, aby zapisać jako nazwany profil.", + "manual_recording_tip": "Nagraj cykl celowo: naciśnij Rozpocznij nagrywanie tuż przed uruchomieniem urządzenia i Zatrzymaj po zakończeniu, a potem Przetwórz, aby zapisać go jako profil.", "ml_learned": "Czego nauczył się WashData", - "ml_matching_tuning": "Dostrajanie dopasowania programu", "ml_settings_card": "Ustawienia", "ml_smart_learning": "Inteligentne uczenie się", "my_preferences": "Moje preferencje", "no_automations": "Żadna automatyzacja nie odwołuje się jeszcze do tego urządzenia.", "panel_settings": "Ustawienia panelu", - "param_sweep": "Przemiatanie parametru", "pg_settings_source": "Źródło ustawień", "phase_catalog": "Katalog faz", "playground": "Piaskownica", @@ -376,11 +368,8 @@ "status": "Status", "status_graph": "Wykres stanu", "storage_stats": "Statystyki przechowywania", - "test_history": "Testuj na historii", "toggle_sidebar": "Przełącz pasek boczny Asystenta Home", - "tools_and_data": "Narzędzia i dane", "wipe_history": "Wyczyść historię", - "recommendations": "Zalecenia ({n})", "pg_detection_params": "Ustawienia wykrywania", "pg_outcome": "Wynik symulacji", "pg_across_cycles": "We wszystkich Twoich cyklach", @@ -388,7 +377,8 @@ "online_account": "Sklep społecznościowy i funkcje online", "import_power_history": "Import historii mocy", "service_status": "Status serwisu", - "online_account_tab": "Online i społeczność" + "online_account_tab": "Online i społeczność", + "live_match_debug": "Debugowanie dopasowania na żywo" }, "health": { "fair": "Akceptowalna jakość profilu", @@ -396,11 +386,16 @@ "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_tip": "Pozycja na zarejestrowanej krzywej dopasowanego programu: {pct}%. Wyznaczana przez dopasowanie cyklu do profilu, więc pozostaje poprawna, gdy przebieg jest dłuższy lub krótszy. Odświeża się, gdy urządzenie jest ciche, więc może się opóźniać.", "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", + "task_name": "Zadanie", + "every_cycles": "Co ile cykli", + "every_days": "Co ile dni", + "new_task_name": "Nowe zadanie", + "days_since_service": "{since} / {total} dni", "actions": "Działania", "activity": "Działalność", "administrators": "Administratorzy", @@ -408,10 +403,8 @@ "aria_cycle_chart": "Krzywa mocy cyklu", "aria_envelope_chart": "Obwiednia mocy profilu", "aria_phase_chart": "Wykres edytora faz", - "aria_playground_chart": "Krzywa mocy cyklu z przeciąganymi progami wykrywania", "aria_power_chart": "Wykres mocy", "aria_spaghetti_chart": "Nałożone krzywe mocy cykli", - "aria_sweep_chart": "Wykres wyników przemiatania parametru", "artifact_anomaly": "Anomalia", "artifact_high_power": "Wysoka moc", "artifact_interruption": "Przerwanie", @@ -425,151 +418,103 @@ "cohesion_good": "spójność {pct}%", "cohesion_low": "⚠ niska spójność {pct}%", "compare_profiles": "Porównaj z profilami", - "concurrency": "Współbieżność", "confidence": "Zaufanie", "confidence_threshold": "Próg ufności", "correct_duration": "Prawidłowy czas trwania (min, opcjonalnie)", "correct_profile": "Prawidłowy profil", - "correlation": "Korelacja", "cost": "Koszt", "create_new_profile": "+ Utwórz nowy profil…", "current_phase": "Bieżąca faza", - "curves": "Krzywe", "cycle": "Cykl", "cycle_date_display": "Wyświetlanie daty cyklu", - "cycle_health": "Zdrowie na rowerze", - "cycle_index": "indeks cyklu →", - "cycles": "Cykle", "cycles_count": "Cykle", "cycles_lc": "cykli", - "dashed": "przerywana", "date": "Data", "debug_traces": "Debuguj ślady", - "default": "Domyślny", "default_access_level": "Domyślny poziom dla użytkowników niewymienionych poniżej", "default_other": "Domyślne (inne urządzenia)", "default_tab": "Domyślna zakładka podczas otwierania panelu", "font_size": "Rozmiar czcionki panelu", - "delta": "Δ", "description": "Opis", - "dtw_blend_w": "Waga mieszania", - "dtw_blended": "Wynik mieszany", - "dtw_ddtw": "Wynik DDTW", - "dtw_l1": "Wynik L1", - "dtw_matched_profile": "(dopasowany profil cyklu)", "duration": "Czas trwania", - "duration_agreement": "Zgodność czasu trwania", - "emphasis": "Podkreślenie", "enable_access_control": "Włącz kontrolę dostępu dla poszczególnych użytkowników", "energy": "Energia", - "energy_agreement": "Zgodność energii", - "event_timeline": "Oś czasu zdarzeń", "event_type": "Typ zdarzenia", "expected": "Oczekiwane", + "suspected_profile": "{name} (przypuszczalny)", + "runner_up_profile": "{name} (drugi najlepszy)", "expected_duration": "Oczekiwany czas trwania (min)", "file_kb": "Plik (kB)", - "final_score": "Wynik końcowy", "from": "Od", "gap_s": "Luka (y)", "group_name": "Nazwa grupy", "head_trim": "Przycięcie początku (s)", - "health": "Zdrowie", "hide_tabs": "Ukryj karty dla osób niebędących administratorami", - "in_use": "W użyciu", "json_data": "Dane JSON", "label": "Dopasowany program", "last": "Ostatnie", "last_checked": "Ostatnio sprawdzane:", "load_from_file": "Załaduj z pliku", - "mae_score": "Wynik MAE", "manual_duration": "Ręczny czas trwania (min, opcjonalnie)", "match": "Dopasowanie", "members": "Członkowie", - "metric": "Metryka", "mode_existing_profile": "Dodaj do istniejącego profilu", "mode_new_profile": "Utwórz nowy profil", "models_fine_tuned": "(dostrojone modele: {count})", - "n_classic_suggestions": "{n} klasyczne", - "n_ml_suggestions": "{n} ML", "n_selected": "Wybrano {n}", - "n_tuning_suggestions": "{n} sugestia dostrajania", - "na": "nd.", + "n_tuning_suggestions": "{n} sugestii dostrajania", + "n_tuning_suggestions_one": "{n} sugestia dostrajania", + "n_tuning_suggestions_few": "{n} sugestie dostrajania", + "n_tuning_suggestions_many": "{n} sugestii dostrajania", + "n_tuning_suggestions_other": "{n} sugestii dostrajania", "new_profile_name": "Nowa nazwa profilu", "notes": "Notatki", - "of": "z", - "outcome": "Wynik", "panel_default_tab": "Zakładka domyślna", "panel_language": "Język panelu", "per_cycle_short": "cykl", - "pg_autofill_tip": "Wypełnij automatycznie zakres na podstawie bieżącej wartości ({lo}–{hi})", "pg_best_value": "Znaleziona najlepsza wartość", "pg_current_value": "Bieżąca wartość:", "pg_drag_hint": "Przeciągnij linię na wykresie", "pg_anti_wrinkle": "Antygniotowy", - "pg_dtw": "DTW", "pg_ev_ending": "Kończenie", "pg_ev_running": "Działa", "pg_group_advanced": "Przypadki brzegowe", "pg_group_detection": "Wyzwalacze wykrywania", "pg_group_timing": "Reguły czasowe", "pg_idle": "Bezczynny", - "pg_last_run": "Ostatnie uruchomienie: {date}", - "pg_match_score": "Wynik dopasowania", - "pg_pct_of": "{p}% z {total}", "pg_poor_match": "Kiepskie dopasowanie", "pg_preset": "Preset piaskownicy", "pg_preset_name": "Nazwa nowego presetu", "pg_preset_none": "Wybierz preset…", "pg_strong_match": "Silne dopasowanie", - "pg_tile_ambiguous": "Niejednoznaczny", - "pg_tile_ambiguous_tip": "Cykle z niemal równym dopasowaniem do dwóch programów", - "pg_tile_detected": "Wykryto", - "pg_tile_detected_tip": "Cykle, w których moc przekroczyła próg startu na tyle długo, by zostać zarejestrowana jako działanie", - "pg_tile_matched": "Dopasowano", - "pg_tile_matched_tip": "Cykle poprawnie przypisane do znanego programu", - "pg_time_left": "Pozostały czas", "pg_weak_match": "Słabe dopasowanie", "phase_name": "Nazwa fazy", "name": "Nazwa", "phase_timeline": "Oś czasu faz", "power": "Moc", "profile": "Profil", - "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. Wybierz program przed uruchomieniem urządzenia, a zostanie zastosowany, gdy tylko rozpocznie się następny cykl.", + "program_tip": "Wybierz, do którego programu dopasowany jest bieżący cykl. Automatyczne wykrywanie wybiera najlepsze dopasowanie. Przypnij program, gdy automatyczne wykrywanie się myli; program wybrany przed startem dotyczy następnego cyklu.", "progress": "Postęp", "raw_socket": "Surowe gniazdo", - "read_only": "Tylko do odczytu", "ref_cycle": "Cykl referencyjny (opcjonalnie)", - "refresh_interval": "Interwał odświeżania (s)", "remaining": "Pozostały", "reminder_every": "Przypominaj co (cykle)", "remove_label": "- Usuń etykietę -", "rename_profile": "Zmień nazwę profilu", - "replay_duration": "Odtwarzanie", "resulting_profile": "Wynikowy profil", "save_mode": "Tryb oszczędzania", - "score": "Wynik", - "score_breakdown": "Rozbicie wyniku", "seconds_from_start": "Sekundy od początku", "select_profile": "Wybierz opcję Profil", "setting": "Ustawienie", - "settings_a": "Ustawienia A (bieżące)", - "settings_a_short": "A", "settings_advanced": "Zaawansowane", - "settings_b": "Ustawienia B (test)", - "settings_b_short": "B", "settings_basic": "Podstawowe", "settings_detail_level": "Poziom szczegółowości ustawień", - "settings_overrides": "Zastąpienia ustawień", "show_debug": "Pokaż kartę debugowania dopasowania na żywo na stronie Status (pewność, niejednoznaczność, najlepsi kandydaci)", "show_expected": "Pokaż oczekiwaną nakładkę krzywej (dopasowany profil, pomarańczowy)", "show_raw": "Pokaż przełącznik surowych odczytów gniazdka na wykresie mocy na żywo", "sparkline": "Ostatni trend czasu trwania cykli", - "stage2": "Etap 2 – podstawowe podobieństwo", - "stage3": "Etap 3 – DTW", - "stage4": "Etap 4 – zgodność", "status": "Status", "tail_trim": "Przycięcie końca (s)", "tap_to_dismiss": "dotknij wykresu, aby zamknąć", @@ -580,7 +525,6 @@ "ungrouped": "Rozgrupowane", "unlabelled": "Nieoznakowane", "unlabelled_paren": "(bez etykiety)", - "warp_path": "Ścieżka dopasowania", "input": "Wejście:", "start": "Początek", "end": "Koniec", @@ -605,9 +549,8 @@ "n_shown": ", pokazano {n}", "cycles_title": "Cykle ({n})", "pg_detecting": "Wykrywanie", - "pg_best_marker": "Najlepszy: {v}", "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", + "pg_time_left_model_tip": "Szacowane na podstawie faz programu, a nie stałe odliczanie", "phase": "Faza", "pg_mode_history": "Testuj na historii", "pg_ev_detected": "Wykryto", @@ -647,18 +590,8 @@ "pg_mode_optimize": "Optymalizuj", "pg_group_matching": "Dopasowywanie programów", "pg_zoom_hint": "kliknij dwukrotnie, aby zresetować powiększenie", - "aria_playground_chart2": "Interaktywny wykres mocy cyklu: najedź, aby odczytać czas/moc, przewiń, aby powiększyć, przeciągnij, aby przesunąć", "pg_alert_timeout_end": "Zakończono z powodu limitu czasu, nie na podstawie prognozy", "pg_alert_indefinite": "Działałby w nieskończoność", - "pg_alert_stress_cap": "Osiągnięto limit bezpieczeństwa", - "pg_alert_stress_ok": "Wykrycie stanu bezczynności: cykl zatrzymany", - "pg_alert_stress_warn": "Pobór mocy w trybie bezczynności powyżej progu zatrzymania", - "pg_stress_group": "Test zatrzymania w stanie bezczynności", - "pg_stress_idle_auto": "Auto", - "pg_stress_idle_w": "Poziom bezczynności (W)", - "pg_stress_idle_w_desc": "Zastąp automatycznie wykrytą wartość minimalną w trybie czuwania (pozostaw puste dla automatycznego wykrycia).", - "pg_stress_toggle": "Testuj wykrywanie stanu bezczynności", - "pg_stress_toggle_desc": "Symuluje, że urządzenie utrzymuje pobór mocy w trybie czuwania po zakończeniu nagrywania -- pokazuje, czy i kiedy WashData zatrzyma cykl.", "task_pg_history": "Test na historii", "task_pg_sweep": "Optymalizuj", "task_reprocess": "Ponowne przetwarzanie", @@ -668,16 +601,9 @@ "pg_recent_runs": "Ostatnie uruchomienia", "pg_load_run": "Wczytaj ponownie to uruchomienie", "pg_match_confidence": "Pewność dopasowania", - "pg_envelope_fit": "Zgodność z obwiednią", - "pg_envelope_fit_tip": "Jak dokładnie cykl mieści się w zapisanej obwiedni mocy tego profilu, inne spojrzenie niż pewność dopasowania.", "n_imported_note": ", {n} zaimportowanych", "enable_online": "Włącz funkcje online", - "store_brand": "Marka urządzenia", - "store_model": "Model urządzenia", "anonymous": "Anonimowo", - "rate_quality": "Oceń jakość", - "rate_n": "{n} gwiazdek", - "approved_only": "Tylko zatwierdzone", "adopt_settings": "Przyjmij ustawienia", "flags": "Flagi", "include_phase_map": "Dołącz mapę faz", @@ -694,7 +620,6 @@ "cat_custom_phases": "Fazy niestandardowe", "cat_profile_groups": "Grupy profili", "cat_settings": "Ustawienia wykrywania i rozpoznawania", - "cat_matching_config": "Strojenie rozpoznawania", "cat_ml_models": "Modele ML", "cat_feedback": "Etykiety opinii i weryfikacji", "cat_suggestions": "Sugestie", @@ -714,7 +639,6 @@ "conflict_overwrite": "Nadpisz", "hist_csv_data": "Dane CSV", "hist_from_recorder": "Albo wczytaj ją z Home Assistant", - "days": "dni", "hist_keep": "Zachowaj ten cykl", "hist_looks_complete": "zakończony", "peak_power_short": "Szczyt", @@ -735,7 +659,44 @@ "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ę" + "correct_total_cycles": "Popraw łączną liczbę", + "hist_reason_already_recorded": "nakłada się na cykl już zarejestrowany przez WashData", + "share_consent": "Potwierdzam, że te cykle przebiegły normalnie do końca, bez przerwania", + "show_internal": "Pokaż ustawienia wewnętrzne", + "show_internal_tip": "Ustawienia, które WashData stroi za Ciebie (próbkowanie, watchdog, częstotliwość dopasowania, progi). Widoczne, gdy to urządzenie ma własną wartość, konflikt lub sugestię.", + "rate_n": "{n} gwiazdek", + "rate_quality": "Oceń jakość", + "pg_autolabel": "Automatyczne oznaczanie", + "pg_label_review": "Prosi o przegląd", + "pg_ev_match_reverted": "Dopasowanie cofnięte", + "pg_ev_verified_pause": "Potwierdzona pauza", + "n_samples": "{n} próbek", + "n_samples_one": "{n} próbka", + "n_samples_few": "{n} próbki", + "n_samples_many": "{n} próbek", + "n_samples_other": "{n} próbki", + "n_cycles": "{n} cykli", + "n_cycles_one": "{n} cykl", + "n_cycles_few": "{n} cykle", + "n_cycles_many": "{n} cykli", + "n_cycles_other": "{n} cyklu", + "energy_per_cycle": "{v}/cykl", + "energy_total": "łącznie {v}", + "automation_off": "(wyłączona)", + "ref_cycle_none": "Brak", + "share_name": "Pokazuj moją nazwę w udostępnianych treściach", + "cat_backfill_cycles": "Cykle znalezione w zaimportowanej historii mocy", + "pg_compare_profile": "Porównaj z profilem", + "pg_obj_early": "Wskaźnik wczesnych końców", + "pg_obj_endlag": "Opóźnienie końca (mediana)", + "pg_obj_split": "Wskaźnik podzielonych cykli", + "pg_sweep_cycles": "Ostatnie cykle", + "cycle_context": "Historia z recordera wokół cyklu", + "cycle_context_off": "Wyłączone", + "cycle_context_minutes": "{min} min", + "cycle_context_series": "Historia z recordera", + "before_start": "Przed startem", + "after_end": "Po zakończeniu" }, "log": { "all_levels": "Wszystkie poziomy", @@ -745,11 +706,9 @@ }, "ml": { "builtin_models": "● Korzystanie z wbudowanych modeli", - "fit_fair": "Akceptowalny", "fit_good": "Dobry", "fit_slight": "Niewielki", "fit_strong": "Mocny", - "fit_weak": "Słaby", "fit_word": "dopasowanie", "personalized": "● Spersonalizowane dla tego urządzenia", "better_than_baseline": "{pct}% lepiej niż szacunek bazowy ({metric})", @@ -757,21 +716,33 @@ "trend_declining_tip": "Dopasowanie tego modelu pogorszyło się w ostatnich ponownych sprawdzeniach, przejrzenie większej liczby cykli może pomóc mu ponownie się nauczyć.", "trend_steady_tip": "Dopasowanie tego modelu pozostało mniej więcej stabilne w ostatnich ponownych sprawdzeniach.", "cap_blurb": { - "end": "Wykrywanie, kiedy cykl faktycznie się zakończył", - "live_match": "Szybsze rozpoznawanie uruchomionego programu", - "quality": "Wykrywanie błędnie rozpoznanych lub uszkodzonych cykli", - "remaining_time": "Przewidywanie, ile czasu pozostało", "total_energy": "Przewidywanie całkowitej energii i kosztu" }, "cap_label": { - "end": "Wykrywanie końca cyklu", - "live_match": "Rozpoznawanie programu", - "quality": "Kontrola jakości cyklu", - "remaining_time": "Szacowanie pozostałego czasu", "total_energy": "Szacowanie energii" }, - "metric_auc": "AUC {auc} na danych walidacyjnych", - "metric_mae": "błąd {model} względem modelu bazowego {baseline}" + "metric_mae": "błąd {model} względem modelu bazowego {baseline}", + "fine_tuned_at": "dostrojono {when}", + "held_out_count": "zmierzono na {count} cyklach walidacyjnych", + "held_out_count_one": "zmierzono na {count} cyklu walidacyjnym", + "held_out_count_few": "zmierzono na {count} cyklach walidacyjnych", + "held_out_count_many": "zmierzono na {count} cyklach walidacyjnych", + "held_out_count_other": "zmierzono na {count} cyklu walidacyjnego", + "last_run_not_promoted": "Ostatnie sprawdzenie ({when}): {reason}", + "never": "nigdy", + "skip_reason": { + "fit_failed": "Trenowanie nie powiodło się: {error}", + "holdout_too_small": "Za mało cykli, by rzetelnie przetestować nowy model: do testów można było odłożyć {held_out}, a potrzeba {min}.", + "insufficient_rows": "Wciąż za mało użytecznych cykli: jak dotąd {count} oznaczonych, czystych cykli.", + "insufficient_rows_one": "Wciąż za mało użytecznych cykli: jak dotąd {count} oznaczony, czysty cykl.", + "insufficient_rows_few": "Wciąż za mało użytecznych cykli: jak dotąd {count} oznaczone, czyste cykle.", + "insufficient_rows_many": "Wciąż za mało użytecznych cykli: jak dotąd {count} oznaczonych, czystych cykli.", + "insufficient_rows_other": "Wciąż za mało użytecznych cykli: jak dotąd {count} oznaczonego, czystego cyklu.", + "not_better_than_incumbent": "Nowy model nie był lepszy od używanego (błąd {model} wobec {incumbent}), więc zachowano obecny.", + "not_better_than_naive": "Nowy model nie był wyraźnie lepszy od prostego oszacowania (błąd {model} wobec {naive}).", + "unknown": "Nie nauczono się niczego nowego." + }, + "trained_unknown": "w nieznanym momencie" }, "modal": { "auto_label": "Cykle automatycznego etykietowania", @@ -780,7 +751,6 @@ "cycle": "Cykl", "edit_group": "Edytuj grupę profili", "edit_phase": "Edytuj fazę", - "import_config": "Importuj konfigurację", "label_cycle": "Cykl etykiet", "merge_cycles": "Połącz {n} cykli", "new_group": "Nowa grupa profili", @@ -795,6 +765,8 @@ "delete_automation_title": "Usuń automatyzację", "delete_automation_msg": "Usunąć automatyzację \"{name}\" z Home Assistant? Tej operacji nie można cofnąć.", "remove_legacy_title": "Usuń starsze akcje", + "reset_phase_msg": "Cofnąć edycję fazy \"{name}\" i przywrócić wbudowaną fazę? Programy zachowają swoje zakresy.", + "reset_phase_title": "Resetuj fazę", "remove_legacy_msg": "Usunąć starsze niestandardowe akcje? Przestaną się uruchamiać przy zdarzeniach cyklu. Nie można tego cofnąć z panelu.", "discard_recording_title": "Odrzuć nagranie", "discard_recording_msg": "Odrzucić zapisane nagranie? Tej operacji nie można cofnąć.", @@ -815,32 +787,46 @@ "store_share_device": "Udostępnij konfigurację urządzenia", "export_select": "Eksport - wybierz dane", "import_wizard": "Import - wybierz dane", - "history_import": "Import historii mocy" + "history_import": "Import historii mocy", + "apply_suggestions_title": "Zastosuj sugestie", + "remove_task_title": "Usuń zadanie", + "remove_task_msg": "Usunąć przypomnienie \"{task}\"? Jego wpisy w dzienniku konserwacji zostaną zachowane.", + "apply_suggestions_msg": "Zmienić te ustawienia ({n})? Integracja zostanie przeładowana po zapisaniu.", + "discard_settings_title": "Odrzucić niezapisane zmiany?", + "discard_settings_msg": "Niezapisane zmiany ustawień: {n}. Opuścić Ustawienia i je odrzucić?", + "profile_title": "Profil · {name}", + "undo_import_msg": "Dane i ustawienia tego urządzenia wrócą do stanu sprzed ostatniego importu ({when}). Cykle zarejestrowane i zmiany wprowadzone od tego czasu zostaną utracone.", + "undo_import_ok": "Cofnij import", + "undo_import_title": "Cofnij ostatni import" }, "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ąć.", + "pg_preset_delete_confirm": "Usunąć preset \"{name}\"?", + "pg_preset_limit": "Osiągnięto limit presetów ({n})", + "pg_preset_overwrite": "Nadpisać preset \"{name}\"?", "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ć.", - "refresh_catalog_hint": "Listy marek i urządzeń społeczności są przechowywane w pamięci podręcznej, aby wspólny sklep nie przekroczył swojego dziennego limitu zapytań. Odśwież, aby pobrać pozycje dodane lub zatwierdzone przez innych.", + "store_sibling_hint": "Nic nie udostępniono dla Twojego modelu? Pokrewny model tej samej marki to zwykle dobry początek.", + "store_declare_appliance": "Wskaż WashData, jakie urządzenie posiadasz, aby zobaczyć konfiguracje udostępnione dla niego przez innych. Możesz też wpisać markę powyżej, aby się rozejrzeć.", + "refresh_catalog_hint": "Listy marek i urządzeń społeczności są buforowane, aby oszczędzać dzienny limit sklepu. Odśwież, aby zobaczyć pozycje dodane lub zatwierdzone przez innych.", "head_trim_hint": "Ile sekund usunąć z początku", "appliance_monitor": "Monitor urządzenia", "artifact_dip_detail": "Moc spadła poniżej normalnego zakresu przez ~{n}s.", "artifact_footer": "Zaznaczono na powyższym wykresie. Są to artefakty przejściowe (np. otwarcie drzwi w połowie cyklu), niekoniecznie problemy.", "artifact_header": "W tym cyklu wykryto {n} anomalii", - "artifact_pause_detail": "Moc spadła prawie do zera przez ~{n}s, a następnie wzrosła – prawdopodobnie drzwi zostały otwarte w trakcie cyklu lub cykl był wstrzymany.", + "artifact_header_one": "W tym cyklu wykryto {n} anomalię", + "artifact_header_few": "W tym cyklu wykryto {n} anomalie", + "artifact_header_many": "W tym cyklu wykryto {n} anomalii", + "artifact_header_other": "W tym cyklu wykryto {n} anomalii", + "artifact_pause_detail": "Moc spadła prawie do zera na ~{n}s, a potem wróciła. Prawdopodobnie otwarto drzwi lub wstrzymano cykl.", "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": "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.", + "cleanup_intro": "Wszystkie oznaczone cykle nałożone na siebie. Zaznacz odstające, a potem usuń im etykietę (zostaną w historii, ale przestaną kształtować ten profil) lub je usuń.", "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)", - "compare_profiles_tip": "Nałóż inne koperty profili na powyższy wykres, aby zobaczyć, która z nich najlepiej pasuje do tego cyklu.", + "compare_profiles_tip": "Nałóż inne profile na wykres, aby zobaczyć, który najlepiej pasuje do tego cyklu.", "compare_selected_cycles": "Wybrane cykle (pełne) – pokaż / ukryj", - "consider_new_profile": "Rozważ utworzenie nowego profilu.", - "coverage_gap": "ostatnie cykle nie mają pasującego profilu ({pct}% z ostatnich 30).", - "coverage_gap_similar_cycles": "Znaleziono {count} podobnych nieoznaczonych cykli – utwórz profil, aby zacząć je dopasowywać.", "cycles_deleted": "Usunięto cykle: {count}", "enough_data": "Wystarczająco danych do nauki ({current}/{min} cykli).", "export_description": "Wybierz dokładnie, które profile, cykle, ustawienia i inne dane wyeksportować do pliku JSON, albo przeanalizuj plik i zaimportuj tylko wybrane części.", @@ -848,30 +834,28 @@ "feedback_prompt": "Potwierdź, że wszystko było w porządku, popraw program lub zignoruj.", "feedback_relabel_hint": "Ponowne oznaczenie tego cyklu również go rozwiązuje.", "filter_by_profile": "Filtruj według profilu…", - "group_modal_help": "Grupuj programy o tym samym kształcie, różniące się temperaturą/wirowaniem (czas trwania może się różnić). Dopasowanie ocenia grupę jako jednego kandydata, a następnie wybiera najlepiej dopasowanego członka. Wybierz co najmniej 2; nakładka pokazuje, jak bardzo są podobni.", + "group_modal_help": "Grupuj programy o tym samym kształcie, różniące się temperaturą lub wirowaniem. Dopasowanie ocenia grupę jako całość, a potem wybiera najlepiej pasującego członka. Wybierz co najmniej 2; nakładka pokazuje, jak bardzo są podobne.", "group_not_cohesive": "Te profile nie są na tyle podobne, aby można było je bezpiecznie grupować, więc dopasowywanie traktuje je indywidualnie, dopóki nie usuniesz wartości odstającej lub nie podzielisz grupy.", "group_preview_hint": "Zaznacz co najmniej 2 członków, aby podejrzeć i porównać ich krzywe mocy.", - "import_intro": "Załaduj wyeksportowany plik lub wklej poniżej ładunek JSON.", - "legacy_actions_title": "{count} starsze działanie niestandardowe nadal działa", + "legacy_actions_title": "Starsze działania niestandardowe nadal działające: {count}", + "legacy_actions_title_one": "{count} starsze działanie niestandardowe nadal działa", + "legacy_actions_title_few": "{count} starsze działania niestandardowe nadal działają", + "legacy_actions_title_many": "{count} starszych działań niestandardowych nadal działa", + "legacy_actions_title_other": "Starsze działania niestandardowe nadal działające: {count}", "live_chart_loading": "Wykres mocy na żywo pojawia się po otrzymaniu odczytów.", "loading": "Załadunek…", "loading_curve": "Ładowanie krzywej…", "loading_settings": "Ładowanie ustawień…", "log_buffer_hint": "Najpierw najnowsze · buforuje ostatnie 500 rekordów ha_washdata od ponownego uruchomienia · przeciągnij dolną krawędź, aby zmienić rozmiar.", - "maintenance_advisory": "Zwiększenie czasu trwania/energii może wskazywać na konieczność konserwacji urządzenia (np. odkamieniania, czyszczenia filtra).", "maintenance_due": "Wymagana konserwacja: {items}", - "maintenance_intro": "Zapisuj czynności serwisowe wykonywane przy tym urządzeniu i otrzymuj przypomnienia, gdy każde zadanie znów będzie wymagane.", + "maintenance_intro": "Zapisuj serwis tego urządzenia i otrzymuj przypomnienia, gdy każde zadanie znów będzie wymagane.", "maintenance_load_error": "Nie można wczytać danych konserwacji: {error}", "maintenance_requires_access": "Konserwacja i eksport/import wymagają pełnego dostępu.", - "manual_duration_hint": "Średnia/oczekiwana długość cyklu profilu, używana do szacunków pozostałego czasu. Edytuj, aby to ustawić; pozostawienie go bez zmian powoduje zachowanie bieżącej wartości.", - "matching_tuning_intro": "Podczas nauki WashData dostosowuje również wpływ dopasowania programu na kształt w porównaniu z czasem trwania i energią.", + "manual_duration_hint": "Oczekiwana długość cyklu profilu, używana do szacowania pozostałego czasu. Pozostaw bez zmian, aby zachować bieżącą wartość.", "merge_intro": "Wybrane cykle są łączone w jeden (kolejność chronologiczna; przerwy wypełnione 0 W). Wybierz wynikowy profil.", "ml_intro": "WashData jest dostarczana z inteligentnymi modelami, które działają od razu po wyjęciu z pudełka.", "ml_learned_intro": "Modele dopasowane do tej maszyny.", - "ml_loading": "ładowanie ML…", "ml_settings_intro": "Dwa niezależne przełączniki: jeden stosuje modele w trakcie trwania cyklu, drugi pozwala WashData dostosować je do Twojej maszyny w miarę upływu czasu.", - "name_first_program": "Masz wystarczająco dużo cykli – nadaj nazwę pierwszemu programowi, aby rozpocząć dopasowywanie.", - "near_duplicate_cluster": "Wykryto prawie zduplikowany klaster profili. Grupowanie umożliwia niezawodne wybieranie podobnych programów (np. tego samego programu w innej temperaturze/wirowaniu).", "no_cycles_match": "Żaden cykl nie pasuje do bieżącego filtra.", "no_cycles_profile": "Brak cykli dla tego profilu.", "no_cycles_selected": "Najpierw wybierz co najmniej jeden cykl.", @@ -896,79 +880,66 @@ "no_suggestions": "Brak aktywnych sugestii.", "notify_services_hint": "Użyj identyfikatorów usług {entity} (oddzielonych przecinkami dla kilku). Zmienne szablonu: {vars}.", "old_actions_warning": "Skonfigurowano przy użyciu starego edytora akcji (teraz usuniętego). Nadal uruchamiają się w przypadku wydarzeń cyklicznych, ale nie można ich już tutaj edytować. Przekształć je w normalną automatyzację lub usuń.", - "onboarding_progress": "Zaobserwowano {n} / 3 cykli", - "onboarding_watching": "Używaj urządzenia normalnie – WashData obserwuje. Po 3 cyklach rozpocznie się dopasowywanie programów.", "pending_feedback": "Oczekuje na informację zwrotną dotyczącą wykrycia", "performance_trend": "Trend wydajności ({n} cykli)", - "pg_analysis_empty": "Wczytaj cykl, aby zobaczyć analizę dopasowania.", "pg_apply_confirm": "Zastosować najlepszą wartość: {label} = {value}?", - "pg_canvas_empty": "Wybierz cykl powyżej, a następnie naciśnij Wczytaj, aby zobaczyć jego przebieg mocy.", - "pg_chart_caption": "Współczynnik wykrywania/dopasowania dla wartości {param}", "pg_ctrl_intro": "Wartości pochodzą z aktywnych ustawień integracji dla tego urządzenia. Zmiany pozostają w piaskownicy, dopóki nie zapiszesz ich w integracji.", "pg_live_unavailable": "Aktywne ustawienia niedostępne, wyświetlane są wartości domyślne.", "pg_load_live_confirm": "Odrzucić zmiany w piaskownicy ({n}) i wczytać ponownie bieżące ustawienia integracji?", - "pg_match_rate": "{pct}% skuteczności dopasowania na {total} cyklach", "pg_matches_live": "Zgodne z aktywnymi ustawieniami", "pg_n_changed": "Zmienione względem aktywnych ustawień: {n}", "pg_sugg_none": "Wszystkie sugestie już odpowiadają bieżącym ustawieniom", - "pg_preset_delete_confirm": "Usunąć preset \"{name}\"?", - "pg_preset_limit": "Osiągnięto limit presetów ({n})", - "pg_preset_overwrite": "Nadpisać preset \"{name}\"?", "pg_publish_one_confirm": "Zapisać {label} = {value} w ustawieniach tego urządzenia?", "pg_restart_note": "Uruchom ponownie Home Assistant, aby włączyć narzędzia symulacji.", - "pg_sim_progress": "{done} / {total} cykli", - "pg_starting": "Rozpoczynanie…", - "pg_stress_synthetic": "symulacja bezczynności zaczyna się tutaj", - "pg_sweep_intro": "Co by było, gdyby {param} był inny? Przetestuj {steps} wartości na swoich ostatnich {cycles} cyklach, aby znaleźć ustawienie, przy którym najwięcej cykli jest poprawnie dopasowanych.", - "pg_sweep_step": "Krok {done} / {total}", - "pg_verdict_bad": "Wymaga uwagi: wiele cykli pozostaje niewykrytych.", - "pg_verdict_good": "Dobrze dostrojone: większość cykli jest poprawnie rozpoznawana i dopasowywana.", - "pg_verdict_ok": "Akceptowalnie: część cykli pominięto. Spróbuj obniżyć próg startu.", "phase_catalog_intro": "Nazwane segmenty cyklu (pranie wstępne, podgrzewanie, wirowanie…). Przypisz je do profilu z panelu sterowania.", "phase_ranges_intro": "Zakresy faz (minuty od początku cyklu) nałożone na krzywą średnią. Edytuj wartości, aby wyświetlić podgląd na żywo.", - "playground_intro": "Bezpieczna piaskownica do odtwarzania wcześniejszych cykli z innymi ustawieniami, porównywania dwóch konfiguracji obok siebie i sprawdzania, jak cykl dopasowuje się do profilu. Nic tutaj nie zmienia Twoich aktywnych danych, dopóki wyraźnie tego nie zastosujesz.", - "playground_needs_restart": "Uruchom ponownie Home Assistant, aby włączyć narzędzia Piaskownicy.", - "preferences_admin": "Preferencje, panel i kontrola dostępu", - "preferences_adv": "Preferencje", + "playground_intro": "Wypróbuj ustawienia na rzeczywistych danych swoich cykli. Nic tutaj nie zmienia aktywnych ustawień, dopóki ich nie zastosujesz.", "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), przelicz koszty cykli według zapisanych cen energii i przelicz kondycję cykli. Uruchom to po serii przeglądów.", + "process_history_hint": "Ponownie uruchom dopasowanie na wszystkich zapisanych cyklach, odśwież sugestie, ponownie wytrenuj modele ML (jeśli włączone), przelicz koszty cykli według zapisanych cen 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.", - "rbac_hint": "Gdy opcja jest wyłączona, każdy użytkownik Home Assistant ma pełny dostęp (domyślnie). Administratorzy zawsze mają pełny dostęp i mogą zarządzać wszystkimi.", - "recent_logs": "Najnowsze rekordy ha_washdata", + "rbac_hint": "Wyłączone: każdy użytkownik Home Assistant ma pełny dostęp (domyślnie). Administratorzy zawsze mają pełny dostęp i mogą zarządzać wszystkimi.", "recording_in_progress": "Trwa nagrywanie", "reminders_intro": "Pokaż przypomnienie w panelu po tylu cyklach od ostatniego serwisu. Pozostaw puste lub 0, aby wyłączyć przypomnienie.", "restart_gap_footer": "Zaznaczone na wykresie. Dane mocy są niedostępne dla tych przedziałów; dopasowanie używało tylko rzeczywistych odczytów.", - "restart_gap_header": "{n} przerwa restartu HA podczas tego cyklu", + "restart_gap_header": "{n} przerwy restartu HA podczas tego cyklu", + "restart_gap_header_one": "{n} przerwa restartu HA podczas tego cyklu", + "restart_gap_header_few": "{n} przerwy restartu HA podczas tego cyklu", + "restart_gap_header_many": "{n} przerw restartu HA podczas tego cyklu", + "restart_gap_header_other": "{n} przerwy restartu HA podczas tego cyklu", "restart_gap_item": "{dur} przerwa", - "review_confirm_help": "Sprawdź, czy ten cykl został prawidłowo wykryty. Twoje recenzje szkolą model na Twojej maszynie – im więcej cykli potwierdzisz, tym lepsze dopasowanie i punktacja kondycji. Wystarczy szybkie dobre/złe.", + "review_confirm_help": "Czy ten cykl został wykryty poprawnie? Potwierdzanie cykli poprawia dopasowanie dla Twojej maszyny. Wystarczy szybkie Dobry/Zły.", "review_in_settings": "Przejrzyj w Ustawieniach", "review_notes_placeholder": "Notatki (opcjonalnie)", "review_notes_tip": "Notatki tekstowe do własnego użytku. Nieużywane podczas dopasowywania lub treningu.", - "review_profile_tip": "Program tego cyklu jest oznaczony jako. Jeśli automatycznie wykryty program był błędny, popraw go tutaj - etykietowanie uczy dopasowywania dla przyszłych cykli.", - "review_quality_tip": "Jak czysty jest ten cykl. Dobry = podręcznikowy przykład tego programu; Zły = wykryty, ale głośny lub nietypowy; Nieużyteczny = błędnie wykryty (połączony, obcięty lub fałszywy). Określa wynik kondycji i liczbę cykli, które mogą trenować model.", - "review_recorded_tip": "Oznacz to jako ręcznie wybrany cykl referencyjny dla swojego programu – pełni tę samą rolę, co cykl nagrany ręcznie. Cykle referencyjne są zawsze zachowywane, zaszczepiają pasujący szablon i nigdy nie są usuwane podczas czyszczenia. (To jest „złota”/nagrana flaga; obie są tym samym.)", - "review_tags_tip": "Opcjonalne flagi opisujące, co poszło nie tak w tym cyklu, więc szkolenie i czyszczenie mogą to wyjaśnić.", + "review_profile_tip": "Program, którym oznaczono ten cykl. Jeśli jest błędny, popraw go tutaj; etykiety uczą dopasowanie.", + "review_quality_tip": "Jak czysty jest ten cykl. Dobry: podręcznikowy przebieg tego programu. Zły: wykryty, ale zaszumiony lub nietypowy. Nie nadaje się do użytku: błędnie wykryty (połączony, ucięty lub fałszywy). Wpływa na wynik kondycji i na to, które cykle trenują model.", + "review_recorded_tip": "Oznacz jako ręcznie wybrany cykl referencyjny jego programu, tak jak cykl nagrany ręcznie. Cykle referencyjne są zawsze zachowywane, tworzą szablon dopasowania i nigdy nie są usuwane przy czyszczeniu.", + "review_tags_tip": "Opcjonalne flagi opisujące, co poszło nie tak, aby trenowanie i czyszczenie mogły to uwzględnić.", "review_to_cycles": "Otwórz kolejkę przeglądu Cykli", "samples_decimated": "Wyświetlanie {shown} z {total} próbek (przerzedzone do wyświetlania; szczyty zachowane). Szeroka przerwa oznacza tu przerzedzenie, a nie brakujące dane.", - "saving_triggers_reload": "Zapisanie powoduje przeładowanie integracji. Jednostki HA mogą przez chwilę być wyświetlane jako niedostępne.", + "saving_triggers_reload": "Zapisanie przeładowuje integrację. Encje mogą przez chwilę być niedostępne.", "search_placeholder": "Ustawienia wyszukiwania…", "see_recorder": "Zobacz widżet rejestratora poniżej", "setting_changed": "Zmieniono z {old} na {new} dnia {date}", "settings_basic_note": "Wyświetlane są podstawowe ustawienia. Przełącz na Zaawansowane, aby zobaczyć pełną listę.", - "shape_drift_advisory": "⚠ Dryf kształtu", - "shape_drift_detail": "Wzorzec mocy dla tego profilu zmienił się z biegiem czasu – możliwe zużycie urządzenia lub konieczna konserwacja (np. odkamienianie, czyszczenie filtra).", "show_all_settings": "Pokaż wszystkie ustawienia", - "showing_suggestions": "Wyświetlam ustawienie {count} z sugestiami.", - "split_intro": "Kliknij wykres, aby dodać lub usunąć punkt podziału albo automatycznie wykryć na podstawie pustych przerw. Każdy powstały segment może otrzymać swój własny profil.", - "storage_diagnostics": "Statystyki przechowywania, konserwacja, eksport/import", + "showing_suggestions": "Wyświetlam {count} ustawienia z sugestiami.", + "showing_suggestions_one": "Wyświetlam {count} ustawienie z sugestiami.", + "showing_suggestions_few": "Wyświetlam {count} ustawienia z sugestiami.", + "showing_suggestions_many": "Wyświetlam {count} ustawień z sugestiami.", + "showing_suggestions_other": "Wyświetlam {count} ustawienia z sugestiami.", + "split_intro": "Kliknij wykres, aby dodać lub usunąć punkt podziału, albo wykryj podział automatycznie po przerwach bezczynności. Każda część może dostać własny profil.", "sug_staged": "Ustawiono {key} = {val}. Zapisz, aby zastosować.", "sug_muted": "To ustawienie nie będzie więcej sugerowane", "sug_mute_failed": "Nie udało się wyciszyć sugestii", "sug_unmuted_all": "Wyciszone sugestie przywrócone", - "n_suggestions_muted": "{count} wyciszonych; automatyczny tuner nie będzie proponować tych ustawień.", + "n_suggestions_muted": "Wyciszono {count} sugestii; automatyczny tuner nie będzie ich proponować.", + "n_suggestions_muted_one": "Wyciszono {count} sugestię; automatyczny tuner nie będzie jej proponować.", + "n_suggestions_muted_few": "Wyciszono {count} sugestie; automatyczny tuner nie będzie ich proponować.", + "n_suggestions_muted_many": "Wyciszono {count} sugestii; automatyczny tuner nie będzie ich proponować.", + "n_suggestions_muted_other": "Wyciszono {count} sugestii; automatyczny tuner nie będzie ich proponować.", "toast_auto_detect_enabled": "Automatyczne wykrywanie włączone", "toast_auto_label_complete": "Automatyczne oznaczanie zakończone", "toast_auto_label_failed": "Automatyczne oznaczanie nie powiodło się: {error}", @@ -984,7 +955,6 @@ "toast_no_automation": "Nie można tu utworzyć automatyzacji", "toast_profile_name_required": "Wymagana jest nazwa profilu", "toast_program_set": "Ustawiony program: {program}", - "toast_rebuild_failed": "Odbudowanie nie powiodło się: {error}", "toast_resume_failed": "Wznowienie nie powiodło się: {error}", "toast_revert_failed": "Przywracanie nie powiodło się: {error}", "toast_reverted": "Cofnięto: {key}", @@ -995,95 +965,82 @@ "trend_energy_up": "Trend wzrostowy w zakresie energii ({pct}%/cykl) – ostatnia średnia {avg}", "trim_destructive_confirm": "To zachowa tylko {pct}% cyklu i tej operacji nie można cofnąć. Kontynuować?", "trim_intro": "Przeciągnij czerwone uchwyty lub wprowadź wartości. Wszystko poza oknem jest usuwane.", - "tuning_suggestions_available": "Sugestia dostrojenia {count} dostępna z obserwowanych cykli. Pojawiają się obok odpowiednich pól.", + "tuning_suggestions_available": "Dostępne sugestie dostrojenia na podstawie obserwowanych cykli: {count}. Pojawiają się obok odpowiednich pól.", + "tuning_suggestions_available_one": "Dostępna jest {count} sugestia dostrojenia na podstawie obserwowanych cykli. Pojawia się obok odpowiedniego pola.", + "tuning_suggestions_available_few": "Dostępne są {count} sugestie dostrojenia na podstawie obserwowanych cykli. Pojawiają się obok odpowiednich pól.", + "tuning_suggestions_available_many": "Dostępnych jest {count} sugestii dostrojenia na podstawie obserwowanych cykli. Pojawiają się obok odpowiednich pól.", + "tuning_suggestions_available_other": "Dostępne sugestie dostrojenia na podstawie obserwowanych cykli: {count}. Pojawiają się obok odpowiednich pól.", "unsure_detected_prefix": "WashData nie jest pewien, czy został wykryty", - "updated": "Zaktualizowano", "warmup_badge": "Wciąż się uczy ({done}/{needed} cykli)", - "warmup_detail": "Ten profil potrzebuje {needed} oznaczonych cykli, zanim rozpocznie się automatyczne dopasowywanie. Każdy potwierdzony cykl pomaga mu się uczyć.", + "warmup_detail": "Dopasowanie już działa. WashData prosi o potwierdzenie pierwszych {needed} cykli tego profilu, zanim zacznie oznaczać samodzielnie.", "wipe_history_warning": "Usuń trwale wszystkie cykle i profile. Nie można tego cofnąć.", "compare_overlay_tip": "Nałóż wyuczone obwiednie profili, aby zobaczyć, który program przypomina każdy cykl.", - "duration_trend_up_tip": "Czas trwania rośnie ({pct}%/cykl)", - "duration_trend_down_tip": "Czas trwania spada ({pct}%/cykl)", - "energy_trend_up_tip": "Energia rośnie ({pct}%/cykl)", - "energy_trend_down_tip": "Energia spada ({pct}%/cykl)", - "performance_trending": "Trend wydajności", - "pg_select_cycle_hint": "Wybierz cykl i kliknij ↺, aby wczytać", - "pg_sweep_progress": "Krok {done} / {total}", - "pg_analysis_hint": "Wybierz cykl i kliknij ↺, aby wczytać analizę.", "diagnostics_load_failed": "Nie udało się wczytać diagnostyki: {error}", - "manual_duration_ref_hint": "Używane tylko wtedy, gdy nie wybrano cyklu referencyjnego. Cykl referencyjny ustawia czas trwania na podstawie własnej długości.", + "manual_duration_ref_hint": "Używane tylko bez cyklu referencyjnego; cykl referencyjny ustawia czas trwania według własnej długości.", + "duration_learned_hint": "Wyliczone z cykli tego programu (bez wartości odstających) i aktualizowane po każdym nowym. Aby to zmienić, przytnij lub usuń cykle, które odbiegają.", "toast_convert_rolled_back": "Konwersja nie powiodła się i została wycofana (nie pozostawiono żadnej automatyzacji): {error}", "toast_convert_orphan": "Automatyzacja została utworzona, ale wyczyszczenie starych akcji nie powiodło się. Nie próbuj ponownie: usuń stare akcje ręcznie, aby uniknąć zduplikowanej automatyzacji.", - "advisory_duration_trend_up": "Cykle '{name}' trwają coraz dłużej (około +{pct}% na cykl). Jeśli zachowanie urządzenia się zmieniło, nagraj ponownie lub przebuduj ten profil.", - "advisory_energy_trend_up": "'{name}' pobiera coraz więcej energii (około +{pct}% na cykl) - warto sprawdzić urządzenie, jeśli jest to nieoczekiwane.", - "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.", + "advisory_duration_trend_up": "Cykle '{name}' trwają coraz dłużej (około +{pct}% na cykl). Jeśli urządzenie się zmieniło, nagraj ponownie lub odbuduj ten profil.", + "advisory_energy_trend_up": "'{name}' zużywa więcej energii z każdym cyklem (około +{pct}% na cykl). Sprawdź urządzenie, jeśli to nieoczekiwane.", + "advisory_poor_health": "'{name}' słabo pasuje: jego ostatnie cykle mocno się różnią lub są słabo dopasowane. Przejrzyj jego cykle lub nagraj go ponownie, aby dopasowanie i szacunki pozostały dokładne.", + "advisory_shape_drift": "'{name}' odbiegł od pierwotnego kształtu krzywej mocy, być może przez kamień lub zużycie. Rozważ ponowne nagranie na podstawie ostatnich cykli.", + "advisory_shape_drift_corr": "'{name}' odbiegł od pierwotnego kształtu krzywej mocy (korelacja {corr}), być może przez kamień lub zużycie. Rozważ ponowne nagranie na podstawie ostatnich cykli.", + "advisory_unmatchable": "'{name}' nigdy nie zostanie dopasowany: nie stoi za nim żaden cykl z danymi o mocy. Oznacz lub nagraj cykl tego programu, aby to naprawić.", + "advisory_unmatchable_no_duration": "'{name}' nigdy nie zostanie dopasowany: jego cykl nie ma użytecznego czasu trwania. Nagraj ten program ponownie lub ustaw jego oczekiwany czas trwania.", "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_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.", "pg_sweep_intro2": "Znajdź ustawienie, które najlepiej spełnia cel dla Twoich ostatnich cykli. Nic się nie zmieni, dopóki go nie zastosujesz.", - "pg_running_history": "Odtwarzanie Twoich cykli…", - "pg_running_sweep": "Testowanie wartości w Twoich cyklach…", "pg_row_load_hint": "Wczytaj ten cykl na wykresie powyżej", - "pg_history_intro2": "Odtwórz swoje ostatnie cykle przez rzeczywisty detektor i mechanizm dopasowywania programów z ustawieniami powyżej. Kliknij dowolny wiersz, aby wczytać dany cykl na wykresie; zmień ustawienie, aby zobaczyć porównanie przed zmianą i po niej.", + "pg_history_intro2": "Odtwórz ostatnie cykle przez rzeczywisty detektor i mechanizm dopasowania z ustawieniami powyżej. Kliknij wiersz, aby wczytać go na wykres; zmień ustawienie, aby porównać przed i po.", "pg_simulating": "Symulowanie cyklu…", - "pg_canvas_empty2": "Wybierz cykl powyżej i naciśnij Uruchom, aby go zasymulować. Następnie najedź, aby odczytać wartości, przewiń, aby powiększyć, i przeciągnij, aby przesunąć.", "pg_apply_settings_confirm": "Zapisać te ustawienia ({n}) w tym urządzeniu? {list}", - "pg_stress_terminated_detail": "Ustabilizował się na ~{idle}W w trybie bezczynności -- cykl zakończył się {h} godz {m} min później przez {reason}.", - "pg_stress_above_threshold_detail": "Pobór mocy w trybie bezczynności ~{idle}W jest równy lub wyższy od efektywnego progu zatrzymania ({stop}W) -- cykl nigdy nie został rozpoznany jako nieaktywny. Zwiększ stop_threshold_w, aby to naprawić.", - "pg_stress_hit_cap_detail": "Cykl działał {h} godz {m} min bez zatrzymania -- wymuszono zatrzymanie przez limit bezpieczeństwa. Pobór mocy w trybie bezczynności {idle}W wobec progu zatrzymania {stop}W.", "no_logs_match": "Żadne wpisy dziennika nie pasują do tych filtrów.", "pg_analysis_hint2": "Wybierz cykl i naciśnij Uruchom, aby wczytać analizę dopasowania.", - "pg_analysis_empty2": "Naciśnij Uruchom, aby zobaczyć analizę dopasowania.", - "imported_readonly": "Zaimportowano ze sklepu społecznościowego. Wyświetlane w celach referencyjnych i do dopasowywania. Nie liczy się do Twoich statystyk i nie można go edytować.", + "imported_readonly": "Zaimportowany ze sklepu społeczności. Służy jako odniesienie i do dopasowania, nie liczy się do statystyk i nie można go edytować.", "merge_no_imports": "Zaimportowanych nagrań nie można scalić z rzeczywistym cyklem. Odznacz je, aby scalić.", - "store_enable_hint": "Włącz funkcje online w Ustawieniach, aby przeglądać i importować cykle referencyjne społeczności.", - "advisory_duration_outlier": "Cykle oznaczone jako '{name}' ({n}) są zbyt odległe od jego zwykłej długości, aby kiedykolwiek mogły zostać z nim dopasowane (największe odchylenie: {ratio}x). Prawdopodobnie są błędnie oznaczone albo jedno nagranie objęło dwa uruchomienia. Oznacz je ponownie lub podziel, aby przestały zniekształcać szacowany czas tego programu.", + "store_enable_hint": "Włącz funkcje online w Preferencje > Online i społeczność, aby przeglądać i importować cykle referencyjne społeczności.", + "advisory_duration_outlier": "Cykle oznaczone jako ‘{name}’ ({n}) odbiegają od jego zwykłej długości zbyt mocno, aby kiedykolwiek zostały dopasowane (najgorszy: {ratio}x). Prawdopodobnie są błędnie oznaczone lub zawierają dwa przebiegi. Oznacz je ponownie lub podziel, aby przestały zaburzać szacowany czas.", "standby_above_stop_title": "Nadal pobiera {idle} W w momencie zakończenia cyklu", "standby_above_stop_sub": "{n} z ostatnich {total} cykli. Opóźnienie Wyłączenia zaczyna biec dopiero wtedy, gdy moc spadnie poniżej Progu Zatrzymania ({stop} W), więc cykle kończą się z opóźnieniem albo trzeba je zatrzymywać ręcznie.", - "online_intro": "Udostępniaj i pobieraj cykle referencyjne z innymi użytkownikami WashData. Wszystkie funkcje online są dobrowolne i domyślnie wyłączone.", "store_share_intro": "Prześlij ten cykl referencyjny, aby inni z tym samym urządzeniem mogli go używać. Zostanie sprawdzony przed publicznym udostępnieniem.", - "online_intro_global": "Przeglądaj i udostępniaj nagrania referencyjne innym użytkownikom WashData oraz potwierdzaj wpisy urządzeń. Jedno połączenie dotyczy całej integracji WashData; markę i model urządzenia ustawia się osobno dla każdego urządzenia w sekcji Podstawowe. Wszystkie funkcje online są dobrowolne i domyślnie wyłączone.", + "online_intro_global": "Przeglądaj i udostępniaj nagrania referencyjne innym użytkownikom WashData. Jedno połączenie obejmuje wszystkie Twoje urządzenia WashData; markę i model ustawia się dla każdego urządzenia w sekcji Podstawowe. Funkcje online są wyłączone, dopóki ich nie włączysz.", "online_unavailable": "Funkcje online nie są dostępne na tym serwerze.", "store_picker_offline": "Włącz funkcje online w kółku zębatym ustawień, aby wybierać z katalogu społeczności.", "pick_brand_first": "Najpierw wybierz markę urządzenia.", - "model_not_found": "Nie ma go jeszcze w katalogu.", - "connect_to_confirm": "Połącz się w kółku zębatym ustawień, aby potwierdzić lub ocenić.", + "custom_task_limit": "Osiągnięto limit zadań ({max}).", + "custom_tasks_intro": "Dodaj wszystko inne, o czym chcesz otrzymywać przypomnienia. Zadanie staje się wymagane, gdy upłynie którykolwiek z interwałów; pozostaw jeden pusty lub 0, aby używać tylko drugiego.", + "maintenance_due_sensor_hint": "Czujnik binarny Wymagana konserwacja jest włączony, gdy którekolwiek zadanie jest wymagane; możesz go użyć we własnych automatyzacjach powiadomień.", + "maintenance_supporter_intro": "Maintenance Supporter to osobna integracja, która potrafi znacznie więcej: dowolne urządzenie lub zadanie, harmonogramy według czasu, użycia lub czujnika, powiadomienia i historia. Może korzystać z licznika cykli WashData.", "adopt_settings_hint": "Zastosuj ustawienia wykrywania i dopasowywania udostępnionego urządzenia do tego urządzenia", - "include_settings_hint": "Dołącz progi wykrywania, wagi dopasowywania i inne parametry do udostępnianego pakietu", + "include_settings_hint": "Udostępnij progi wykrywania i dopasowania tego urządzenia (bez powiadomień, encji i ceny energii). Inni sami zdecydują, czy je zastosować.", "onboard_download": "Pobierz gotową konfigurację ze sklepu społeczności", - "share_consent": "Udostępniasz rzeczywiste dane ze swojego urządzenia. Nie udostępniaj, jeśli Twoje wzorce użytkowania są prywatne.", "share_device_none": "Nie skonfigurowano jeszcze żadnych urządzeń. Najpierw dodaj urządzenie.", "share_guideline_naming": "Używaj czytelnych nazw programów (np. 'Cotton 40', 'Eco 60'), żeby inni mogli je zidentyfikować", - "share_guideline_quality": "Udostępniaj tylko cykle, które zakończyły się normalnie: bez przerw w połowie cyklu, otwierania drzwi ani chwilowych zaników zasilania.", + "share_guideline_quality": "Udostępniaj tylko cykle zakończone normalnie: bez przerw, otwierania drzwi i chwilowych zaników zasilania.", "share_guideline_review": "Przejrzyj swoje profile przed udostępnieniem -- usuń te, które wyglądają błędnie", "share_guidelines_title": "Przed udostępnieniem", - "store_download_device_intro": "Pobierz konfigurację urządzenia ze społeczności i zastosuj ją do nowego lub istniejącego urządzenia", + "store_download_device_intro": "Dodaj do swojego urządzenia wszystkie udostępnione programy i ich cykle referencyjne. Twoje własne cykle i statystyki pozostaną bez zmian.", "store_share_device_intro": "Prześlij {brand} {model} wraz z wybranymi cyklami referencyjnymi. Inne osoby z tym samym urządzeniem mogą przyjąć Twoje programy. Wpisy są sprawdzane przed publikacją.", "share_profile_no_cycles": "Brak cykli referencyjnych. Aby dołączyć ten profil, oznacz cykl jako ⭐ na karcie Cykle", - "advisory_phase_inconsistent": "Wygląda na to, że '{name}' miesza różne programy lub temperatury - jego cykle nagrzewają się przez bardzo różne czasy. Podzielenie go na osobne profile (np. dla każdej temperatury) poprawi dopasowanie i szacowanie czasu.", - "advisory_phase_inconsistent_title": "⚠ Prawdopodobnie pomieszane programy", - "export_select_intro": "Zaznacz dokładnie to, co ma zostać uwzględnione. Wybranie profili bez ich cykli i tak eksportuje rozpoznawalny program (jego wyuczony kształt jest przenoszony razem z nim).", - "import_analyze_hint": "Wczytaj wyeksportowany plik (lub wklej jego JSON). WashData go przeanalizuje i pokaże dokładnie, co można zaimportować, zanim cokolwiek się zmieni.", + "export_select_intro": "Zaznacz, co uwzględnić. Profile bez swoich cykli i tak eksportują rozpoznawalny program.", + "import_analyze_hint": "Wczytaj wyeksportowany plik (lub wklej jego JSON). WashData pokaże, co można zaimportować, zanim cokolwiek się zmieni.", "analyzing": "Analizowanie…", - "device_type_mismatch_warn": "Ten eksport pochodzi z innego typu urządzenia ({src} vs {local}). Programy i cykle można nadal zaimportować jako dane referencyjne, ale ustawienia specyficzne dla urządzenia oraz import rzeczywistej historii są wyłączone.", + "device_type_mismatch_warn": "Ten eksport pochodzi z innego typu urządzenia ({src} vs {local}). Programy i cykle można nadal zaimportować jako dane referencyjne, ale bez ustawień urządzenia i rzeczywistej historii.", "merge_hint": "Zaimportowane elementy są dodawane; nic lokalnego nie zostaje utracone. Konflikty nazw rozwiązuje się poniżej.", "replace_warn": "Każda zaznaczona kategoria zostaje wyczyszczona i zastąpiona danymi z pliku. Niezaznaczone kategorie pozostają nietknięte.", "dest_reference_hint": "Zaimportowane cykle jedynie poprawiają rozpoznawanie programów i nigdy nie wpływają na statystyki zużycia/energii.", - "dest_real_history_hint": "Zaimportowane cykle liczą się jako własna historia tego urządzenia i zasilają statystyki energii/zużycia. Użyj przy przenoszeniu jednego urządzenia do nowej instalacji.", - "import_history_description": "Twoja inteligentna wtyczka działała już przed WashData? Wgraj eksport historii jej czujnika mocy albo wczytaj ją wprost z Home Assistant - normalne wykrywanie przebiegnie po tych danych, więc dawne cykle pojawią się na liście Cykle, gotowe do nazwania.", - "hist_input_hint": "Wgraj plik CSV pobrany z panelu Historia (encja, stan, ostatnia zmiana) albo pozwól WashData wczytać historię czujnika bezpośrednio. Wykrywanie przebiegnie dokładnie tak samo jak na żywo, a Ty wybierasz, które z odnalezionych cykli zachować.", - "hist_recorder_hint": "Odczytuje dane od wybranej daty do teraz. Home Assistant domyślnie przechowuje szczegółową historię przez 10 dni, a potem już tylko średnie godzinowe, zbyt zgrubne do wykrywania cykli - wcześniejszą datę wybieraj tylko wtedy, gdy Twój recorder jest ustawiony na dłuższe przechowywanie.", - "hist_scanning": "Odtwarzamy Twoją historię przez detektor. Działa to w tle - możesz zamknąć to okno i wrócić później.", + "dest_real_history_hint": "Zaimportowane cykle liczą się jako własna historia i statystyki tego urządzenia. Użyj tego przy przenoszeniu urządzenia do nowej instalacji.", + "import_history_description": "Miałeś inteligentną wtyczkę przed WashData? Wgraj jej historię mocy lub odczytaj ją z Home Assistant. Wykrywanie przebiegnie po tych danych, a dawne cykle pojawią się na liście Cykle, gotowe do nazwania.", + "hist_input_hint": "Wgraj plik CSV z panelu Historia (encja, stan, ostatnia zmiana) albo pozwól WashData odczytać historię czujnika bezpośrednio. Wykrywanie na żywo przebiegnie po tych danych, a Ty wybierzesz, które cykle zachować.", + "hist_recorder_hint": "Odczytuje dane od wybranej daty do teraz. Home Assistant domyślnie przechowuje szczegółową historię przez 10 dni; starsze średnie godzinowe są zbyt zgrubne do wykrywania cykli.", + "hist_scanning": "Twoja historia przechodzi przez detektor w tle. Możesz zamknąć to okno i wrócić później.", "hist_imported_count": "Zaimportowane cykle: {n}.", "hist_duplicates": "Już wcześniej zaimportowane i pominięte: {n}.", "hist_capped": "Osiągnięto limit zaimportowanych cykli na urządzenie; reszta nie została zapisana.", - "hist_next_step": "Są na liście Cykle, oznaczone jako zaimportowana historia. Otwórz jeden z nich i przyciskiem Oznacz nadaj mu nazwę programu, do którego należy.", + "hist_next_step": "Są na liście Cykle, oznaczone jako zaimportowana historia. Otwórz jeden i użyj Oznacz, aby nadać nazwę programu.", "hist_rows_read": "Wczytane odczyty: {n}", "hist_breaks": "Przerwy, w których czujnik był niedostępny: {n}", "hist_other_entity": "Pominięte odczyty innych encji: {n}", @@ -1095,25 +1052,76 @@ "hist_scan_capped": "Pokazywane są tylko pierwsze kandydatury (znaleziono: {n}).", "hist_recorder_empty": "Home Assistant nie ma szczegółowej historii tego czujnika w tym okresie.", "hist_scan_failed": "Skanowanie nie powiodło się.", - "hist_scan_expired": "To skanowanie nie jest już dostępne. Uruchom je ponownie.", + "hist_scan_expired": "To skanowanie nie jest już dostępne. Zeskanuj 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.", - "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.", + "imported_history_readonly": "Wykryty w zaimportowanej historii mocy. Pomaga w dopasowaniu, ale nie liczy się do statystyk i nie można go przyciąć ani podzielić. Oznacz go, aby nadać nazwę programu.", + "odometer_intro": "Ta suma tylko rośnie. Usunięcie cyklu jej nie zmniejsza, więc przypomnienia serwisowe pozostają prawidłowe.", + "odometer_correct_hint": "Ustaw to, jeśli WashData policzył cykl, który się nie odbył, albo urządzenie wykonało cykle przed instalacją WashData.", "no_reminders_set": "Nie ustawiono jeszcze żadnych przypomnień serwisowych.", "toast_program_armed": "Program ustawiony na następny cykl: {program}", "toast_range_required": "Ustaw prawidłowy zakres Od/Do", - "no_profiles_option": "(brak profili, dodaj nowy)" + "no_profiles_option": "(brak profili, dodaj nowy)", + "compare_cycles_title": "Porównaj cykle ({count})", + "edit_builtin_phase": "To jest wbudowana faza. Zapisanie tworzy własną kopię; usuń kopię, aby przywrócić oryginał.", + "fetch_error": "Nie udało się załadować danych.", + "connection_lost": "Utracono połączenie - wyświetlane są dane z godz. {time}.", + "pg_notify_partial": "Znaczniki pokazują tylko powiadomienia o starcie, prawie gotowe, zakończeniu, kamieniach milowych i cichych godzinach. Aktualizacje na żywo, przypomnienia o rozładunku i alerty przekroczenia czasu nie są symulowane.", + "connect_to_confirm": "Połącz się w kółku zębatym ustawień, aby potwierdzić lub ocenić.", + "coverage_gap_cluster": "Ostatnie nieoznakowane cykle ({count}) trwające około {min} min wyglądają na jeden program, którego jeszcze nie utworzono.", + "pg_label_yes": "Na końcu cyklu zostałby oznaczony jako \"{name}\"", + "pg_label_no": "Trafiłby do przeglądu: {reason}", + "live_match_debug_tip": "Zaufanie: jak dokładnie bieżąca krzywa mocy pasuje do najlepszego profilu-kandydata (0-100%). Niejednoznaczne: wyniki dwóch najlepszych kandydatów różnią się o mniej niż 5%, więc etykieta jest niepewna aż do zakończenia cyklu.", + "automation_alias_started": "{device}: cykl rozpoczęty", + "automation_alias_finished": "{device}: cykl zakończony", + "automation_desc_started": "Uruchamiana, gdy rozpoczyna się cykl urządzenia {device}. Dodaj własne akcje (powiadomienia, światła, ...).", + "automation_desc_finished": "Uruchamiana, gdy kończy się cykl urządzenia {device}. Dodaj własne akcje (powiadomienia, światła, ...).", + "feedback_detected": "WashData wykrył: {name}", + "import_undo_hint": "Zachowano punkt przywracania sprzed ostatniego importu ({when}): {profiles} profil(e) i {cycles} cykl(e).", + "replace_undo_hint": "Najpierw zapisywany jest punkt przywracania, więc ten import można cofnąć w sekcji Eksportuj/Importuj.", + "pg_in_sample_note": "Te cykle pomogły zbudować profile, z którymi są porównywane, więc wyniki są optymistyczne: przy nowych cyklach spodziewaj się nieco mniej poprawnych dopasowań.", + "hist_tail_trimmed": "Przycięto {min} min oczekiwania po zakończeniu cyklu, tak samo jak robi to działająca integracja", + "hist_truncated": "zatrzymano na limicie odczytów ({n}), więc reszta historii nie została przeskanowana", + "hist_warn_kw": "Najwyższy odczyt to tylko {w}. Ten czujnik chyba podaje kilowaty; WashData potrzebuje watów, więc nie da się wykryć cykli.", + "hist_warn_naive_time": "{n} znaczników czasu nie ma strefy czasowej i zostało odczytanych jako UTC. Jeśli plik używa czasu lokalnego, cykle są przesunięte o Twoją różnicę względem UTC, a odczyty w pobliżu zmiany czasu mogą zostać pominięte.", + "hist_warn_naive_time_one": "{n} znacznik czasu nie ma strefy czasowej i został odczytany jako UTC. Jeśli plik używa czasu lokalnego, cykle są przesunięte o Twoją różnicę względem UTC, a odczyty w pobliżu zmiany czasu mogą zostać pominięte.", + "hist_warn_naive_time_few": "{n} znaczniki czasu nie mają strefy czasowej i zostały odczytane jako UTC. Jeśli plik używa czasu lokalnego, cykle są przesunięte o Twoją różnicę względem UTC, a odczyty w pobliżu zmiany czasu mogą zostać pominięte.", + "hist_warn_naive_time_many": "{n} znaczników czasu nie ma strefy czasowej i zostało odczytanych jako UTC. Jeśli plik używa czasu lokalnego, cykle są przesunięte o Twoją różnicę względem UTC, a odczyty w pobliżu zmiany czasu mogą zostać pominięte.", + "hist_warn_naive_time_other": "{n} znacznika czasu nie ma strefy czasowej i zostało odczytane jako UTC. Jeśli plik używa czasu lokalnego, cykle są przesunięte o Twoją różnicę względem UTC, a odczyty w pobliżu zmiany czasu mogą zostać pominięte.", + "pg_compare_profile_tip": "Wybiera tylko profil nakładany na wykres. Symulacja zawsze sama dopasowuje program do cyklu, tak jak działająca integracja.", + "pg_outcome_stale": "Ustawienia zmieniły się od uruchomienia tej symulacji. Nadal pokazuje ona stary wynik.", + "pg_sweep_guarded": "Kończy więcej cykli przedwcześnie, dzieli więcej z nich lub pomija cykle wykrywane przy obecnym ustawieniu, więc nigdy nie jest zalecane.", + "pg_sweep_keep_current": "Zachowaj obecną wartość ({value}, {metric}): żadna wartość nie była lepsza o co najmniej jeden cykl.", + "pg_sweep_n_cycles": "na {count} cyklach", + "pg_sweep_n_cycles_one": "na {count} cyklu", + "pg_sweep_n_cycles_few": "na {count} cyklach", + "pg_sweep_n_cycles_many": "na {count} cyklach", + "pg_sweep_n_cycles_other": "na {count} cyklu", + "pg_task_busy": "Dla tego urządzenia trwa już przebieg „Testuj na historii” lub „Optymalizuj”. Poczekaj na jego zakończenie lub go anuluj.", + "cycle_context_loading": "Wczytywanie historii z recordera…", + "cycle_context_note": "Szary: historia czujnika mocy z recordera wokół tego cyklu. Nie jest częścią cyklu i nie wpływa na czas trwania, energię ani dopasowanie.", + "cycle_context_none": "Recorder nie przechowuje żadnych odczytów z okresu wokół tego cyklu (Home Assistant domyślnie przechowuje historię przez 10 dni).", + "cycle_context_tip": "Tylko podgląd: pokazuje historię czujnika mocy z recordera wokół tego cyklu, jeśli jest jeszcze przechowywana. Aby dołączać nieudane próby startu do przyszłych cykli, użyj ustawienia Wstępny zapis krzywej (Ustawienia > Wykrywanie > Start Cyklu), które zmienia zapisane czasy trwania.", + "invalid_threshold_pair": "Próg Zatrzymania ({stop} W) musi być niższy niż Próg Startu ({start} W). Nic nie zostało zapisane; zmień obie wartości razem." }, "phase_desc": { "anti_crease": "Sporadyczne krótkie obroty bębna po zakończeniu w celu zapobiegania gnieceniu tkanin.", "anti_wrinkle": "Okresowe obroty bębna po zakończeniu cyklu w celu zapobiegania gnieceniu tkanin.", + "baking": "Grzałka pracuje w wysokiej temperaturze, tworząc skórkę i miąższ.", + "cooking": "Główna faza przygotowania z włączoną grzałką i wentylatorem.", "cool_down": "Obracanie bez ogrzewania pod koniec cyklu.", + "cool_down_af": "Chłodzenie samym wentylatorem po grzaniu.", "drain_and_switch": "Okres przejścia z trybu prania do trybu suszenia.", "dry": "Etap suszenia przy użyciu grzejnika i/lub ciepła resztkowego.", "drying": "Główny okres podgrzewania.", "heat_up": "Wstępne rozgrzewanie grzejnika przed rozpoczęciem pełnego suszenia.", + "keep_warm": "Lekkie grzanie, by jedzenie pozostało ciepłe.", + "keep_warm_bm": "Lekkie grzanie, by chleb pozostał ciepły po upieczeniu.", + "kneading": "Silnik miesza i wyrabia ciasto. Duży pobór mocy.", + "pause_af": "Krótka przerwa na potrząśnięcie lub sprawdzenie.", + "pre_heat": "Wstępne nagrzewanie komory przed właściwym przygotowaniem.", "pre_rinse": "Wstępne spryskanie przed praniem w detergencie.", + "proving": "Wyrastanie w niskiej temperaturze: drożdże fermentują, a ciasto rośnie.", + "resting": "Krótka przerwa przy małej mocy między wyrabianiem, by gluten się rozluźnił.", "pre_wash": "Wstępne namoczenie lub obróbka wstępna przed praniem zasadniczym.", "rinse": "Etap płukania czystą wodą. Faza ta może powtarzać się wielokrotnie.", "sanitize": "Etap czyszczenia w wysokiej temperaturze w programach odkażania.", @@ -1126,6 +1134,7 @@ "wash_dw": "Pranie w detergencie głównym z ogrzewaniem." }, "placeholder": { + "custom_task": "np. Wyczyść uszczelkę drzwi", "add": "dodaj…", "group_name": "np. Bawełna 2:47", "json_buttons": "[{\"action\":\"ID\",\"title\":\"Label\"}]", @@ -1153,10 +1162,10 @@ "section": { "dishwasher": { "label": "Zmywarka", - "intro": "Obsługa końca cyklu w zmywarkach, które zazwyczaj kończą długą, niemal cichą fazą suszenia przed krótkim końcowym odpompowaniem wody." + "intro": "Obsługa końca cyklu w zmywarkach, które często długo i cicho suszą przed krótkim końcowym odpompowaniem." }, "anti_wrinkle": { - "intro": "Tryb antygniotowy wykrywa impulsy bębnowania przy niskiej mocy po fazie głównej i utrzymuje je przyłączone do zakończonego cyklu.", + "intro": "Dołącza do zakończonego cyklu impulsy obracania bębna o niskiej mocy, które suszarka wykonuje po fazie głównej.", "label": "Antygniotowy" }, "basic": { @@ -1164,7 +1173,7 @@ "label": "Podstawowe" }, "delay": { - "intro": "Wykrywanie opóźnionego startu identyfikuje, gdy urządzenie jest zasilane, ale jeszcze nie rozpoczęło cyklu.", + "intro": "Wykrywa urządzenie, które jest włączone i czeka na opóźniony start.", "label": "Opóźniony Start" }, "detection": { @@ -1188,157 +1197,149 @@ "label": "Czas i Watchdog" }, "triggers": { - "intro": "Opcjonalne zewnętrzne sygnały: wyzwalacz zakończenia, czujnik drzwi, przełącznik pauzy i przypomnienie o rozładunku.", + "intro": "Opcjonalne dodatki: wyzwalacz zakończenia, czujnik drzwi, przełącznik pauzy i przypomnienie o rozładunku.", "label": "Wyzwalacze i Drzwi" - }, - "phase_eta": { - "label": "Pozostały czas", - "intro": "Pozostały czas uwzględniający fazy, dla urządzeń, których długość cyklu zależy od temperatury lub wirowania." } }, "setting": { "dishwasher_end_spike_quiet_release": { "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." + "doc": "Po upływie oczekiwanego czasu trwania: jak długo zmywarka musi pozostać poniżej Progu Zatrzymania, zanim WashData przestanie czekać na końcowe odpompowanie. Zwiększ, jeśli późne odpompowanie po długim suszeniu jest pomijane. Oczekiwanie nigdy nie przekracza 30 minut." }, "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.", + "doc": "Ułamek średniego czasu trwania programu, który cykl musi osiągnąć, zanim obracanie bębna o niskiej mocy zostanie uznane za ogon antygniotowy. Obniż (np. 0,75) w suszarkach, które dostosowują czas do wsadu. W pralkach zostaw wartość domyślną (0,98), inaczej pranie może się skończyć w połowie.", "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.", + "doc": "Dołączaj krótkie impulsy obracania bębna o niskiej mocy po głównej fazie grzania suszarki do zakończonego cyklu, zamiast traktować je jako nowe cykle.", "label": "Włącz Wykrywanie Antygniotowe" }, "anti_wrinkle_exit_power": { - "doc": "Moc musi spaść poniżej tej wartości pomiędzy impulsami, aby tryb przeciwzmarszczkowy pozostał aktywny.", + "doc": "W trybie antygniotowym moc poniżej tej wartości oznacza przerwę między impulsami obrotów, a przerwa dłuższa niż Maks. Odstęp Impulsów kończy tryb. Jeśli Próg Zatrzymania jest wyższy, używany jest on, więc to ustawienie działa tylko powyżej Progu Zatrzymania.", "label": "Próg Wyjścia Mocy" }, "anti_wrinkle_idle_timeout": { - "doc": "Jak długo maszyna może pozostać cicha między dwoma impulsami bębna, zanim tryb antygniotowy się zakończy. Ustaw powyżej najdłuższej przerwy, jaką suszarka zostawia między impulsami, w przeciwnym razie każdy kolejny impuls zostanie odczytany jako fałszywy start.", + "doc": "Jak długo suszarka może być cicha między impulsami obracania, zanim tryb antygniotowy się zakończy. Ustaw powyżej najdłuższej przerwy suszarki, inaczej kolejne impulsy zostaną odczytane jako fałszywe starty.", "label": "Maks. Odstęp Impulsów" }, "anti_wrinkle_max_duration": { - "doc": "Impulsy dłuższe niż ten traktowane są jako prawdziwy cykl, a nie przeciwzmarszczkowy upadek.", + "doc": "Impuls dłuższy niż ten czas liczy się jako prawdziwy cykl, a nie obracanie antygniotowe.", "label": "Maks. Czas Trwania" }, "anti_wrinkle_max_power": { - "doc": "Impuls powyżej tej mocy traktowany jest jako prawdziwy nowy cykl, a nie przeciwzmarszczkowy upadek. Ustaw tuż powyżej mocy impulsu opadającego.", + "doc": "Impuls powyżej tej mocy liczy się jako prawdziwy nowy cykl, a nie obracanie bębna. Ustaw tuż powyżej mocy impulsów obracania.", "label": "Maks. Moc Antygniotowa" }, "auto_label_confidence": { - "doc": "Jeśli wynik dopasowania na koniec cyklu jest równy lub wyższy, program jest automatycznie oznaczany bez monitu o potwierdzenie. Podnieś, aby wymagać większej pewności przed automatycznym etykietowaniem; opuść go, aby bardziej zautomatyzować. Działa w połączeniu z Learning Confidence poniżej.", + "doc": "Wynik dopasowania na koniec cyklu, od którego program jest oznaczany bez pytania. Podnieś dla większej pewności; obniż, aby automatycznie oznaczać więcej cykli. Utrzymuj powyżej Pewności Uczenia.", "label": "Pewność Automatycznego Etykietowania" }, "auto_maintenance": { - "doc": "Wykonuj nocne porządki: odbuduj obwiednie profili, ponownie oblicz kondycję cykli, usuń ślady debugowania i zachowaj najnowsze cykle.", + "doc": "Nocne porządki: oznacz starsze cykle, które teraz wyraźnie pasują do programu, odbuduj krzywe programów, odśwież oznaczenia anomalii i kondycję cykli oraz usuń ślady debugowania, jeśli Zapisuj Ślady Debugowania jest wyłączone. Cykle i ich przebiegi mocy są zawsze zachowywane.", "label": "Automatyczna Konserwacja (nocne czyszczenie)" }, "completion_min_seconds": { - "doc": "Cykle krótsze niż ta są odrzucane jako cykle widma (przebiegi próbne, otwarcie drzwi w celu dodania skarpetki).", + "doc": "Cykle krótsze niż ten czas (przebiegi próbne, otwarcie drzwi, by dorzucić skarpetkę) są zapisywane jako przerwane i nie dostają powiadomienia o zakończeniu.", "label": "Minimalny Czas Trwania Cyklu" }, "delay_confirm_seconds": { - "doc": "Zasilanie musi pozostać w paśmie czuwania przez tak długi czas, zanim urządzenie zostanie potraktowane jako oczekujące na uruchomienie, a nie działające.", + "doc": "Moc musi tyle czasu pozostawać w paśmie czuwania, zanim urządzenie zostanie uznane za czekające na start.", "label": "Sekundy Potwierdzenia" }, "delay_start_detect_enabled": { - "doc": "Wykryj, kiedy urządzenie jest włączone i czeka (opóźniony start / tryb czuwania), ale nie rozpoczęło swojego cyklu, dzięki czemu pobieranie w trybie gotowości nie jest mylone z cyklem pracy.", + "doc": "Wykrywaj urządzenie, które jest włączone i czeka na opóźniony start, aby jego pobór w czuwaniu nie był brany za pracujący cykl.", "label": "Włącz Wykrywanie Opóźnionego Startu" }, "delay_timeout_hours": { - "doc": "Po tylu godzinach przestań czekać w trybie opóźnionego startu i wróć do stanu bezczynności, aby maszyna pozostawiona pod napięciem, ale nigdy nie uruchomiona, nie czekała wiecznie.", + "doc": "Przestań czekać na opóźniony start po tylu godzinach i wróć do stanu bezczynności.", "label": "Czas Oczekiwania (godziny)" }, "device_type": { - "doc": "Klasa urządzenia. Ustawia rozsądne domyślne ustawienia wykrywania (progi, opóźnienie wyłączenia, obsługa końcówek) dostosowane do tego typu urządzenia; zmień go tylko wtedy, gdy urządzenie zostało pierwotnie skonfigurowane jako niewłaściwego typu.", + "doc": "Ustawia domyślne parametry wykrywania (progi, opóźnienie wyłączenia, obsługa końca) dla tego rodzaju urządzenia. Zmieniaj tylko, jeśli urządzenie skonfigurowano z niewłaściwym typem.", "label": "Typ Urządzenia" }, "door_sensor_entity": { - "doc": "Opcjonalny czujnik binarny drzwi. Służy do wykrywania, kiedy urządzenie zostało otwarte/rozładowane po cyklu.", + "doc": "Opcjonalny czujnik drzwi. Pokazuje, kiedy urządzenie zostało otwarte w celu rozładunku po cyklu.", "label": "Encja Czujnika Drzwi" }, "duration_tolerance": { - "doc": "Tolerancja dla szacunków pozostałego czasu (uczenie się informacji zwrotnej, brak dopasowania). Jeśli rzeczywisty czas trwania mieści się w granicach +/-X% szacunkowej wartości, liczy się to jako dobre dopasowanie.", + "doc": "Dotyczy tylko informacji zwrotnej uczenia: cykl w granicach +/-X% szacowanego czasu trwania liczy się jako dobre oszacowanie. Nie wpływa na dopasowanie.", "label": "Tolerancja Szacowania" }, "enable_ml_models": { - "doc": "Podczas trwania cyklu pozwól modelom udoskonalić wyniki na żywo: bardziej stabilne oszacowanie pozostałego czasu i energii/kosztów oraz zabezpieczenie przed przedwczesnym zatrzymaniem w przypadku wykrycia końca (może zawsze opóźnić zakończenie, nigdy nie zakończyć wcześniej i jest ograniczone). Używa Twoich dopracowanych modeli, jeśli są dostępne, w przeciwnym razie wbudowanych. Off = tylko klasyczna logika oparta na zasilaniu (nadal niezawodna).", + "doc": "Używaj modelu wytrenowanego na tej maszynie do przewidywania energii i kosztu trwającego cyklu oraz pozwól dobrze znanemu programowi, który kończy się nietypowo wcześnie, zakończyć się bez pełnego opóźnienia wyłączenia. Wyłączone: tylko logika oparta na mocy.", "label": "Stosuj inteligentne modele podczas cyklu" }, "end_energy_threshold": { - "doc": "Podczas odliczania opóźnienia wyłączenia skumulowana energia (waty x czas) jest porównywana z tym progiem. Jeśli zostanie przekroczony, odliczanie zostanie zresetowane – utrzymując bębny zapobiegające zagnieceniom i końcówki suszące w zmywarce dołączone do cyklu, zamiast je skracać. Podnieś go, jeśli cykle kończą się zbyt wcześnie podczas schładzania; obniż ją, jeśli wykrywanie jest powolne.", + "doc": "Energia dozwolona w czasie opóźnienia wyłączenia. Powyżej tej wartości cykl trwa dalej, więc obroty bębna i końcowe suszenie pozostają jego częścią. Obniż, jeśli cykle kończą się w trakcie ogona; podnieś, jeśli zakończone cykle wciąż czekają.", "label": "Energia Zakończenia" }, - "end_repeat_count": { - "doc": "Liczba kolejnych odczytów poniżej progu wymaganych przed zakończeniem cyklu. 1 jest odpowiedni dla większości wtyczek. Zwiększ do 2-3, jeśli inteligentna wtyczka czasami zgłasza w połowie cyklu próbkę fałszywie zerową, a cykle kończą się przedwcześnie.", - "label": "Liczba Powtórzeń Zakończenia" - }, "energy_price_entity": { - "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.", + "doc": "Czujnik z bieżącą ceną za kWh (np. taryfa dynamiczna). Ma pierwszeństwo przed ceną statyczną poniżej, więc musi to być cena, a nie czujnik mocy lub energii wtyczki.", "label": "Encja Ceny Energii" }, "energy_price_static": { - "doc": "Stała cena za kWh używana do danych o kosztach, gdy powyżej nie określono żadnej jednostki ceny rzeczywistej.", + "doc": "Stała cena za kWh, używana, gdy nie ustawiono encji ceny.", "label": "Statyczna Cena Energii (za kWh)" }, "energy_sensor": { - "doc": "Opcjonalny skumulowany licznik energii (total_increasing kWh/Wh, np. własny licznik całkowitego zużycia wtyczki). Po ustawieniu energia raportowana dla każdego cyklu jest pobierana z różnicy wskazań tego licznika między początkiem a końcem, co pozwala uniknąć zaniżania wynikającego z całkowania wolno raportującego czujnika mocy. W razie braku odczytu, nieznanej jednostki lub niedodatniej różnicy następuje powrót do wartości całkowanej. Pozostaw puste, aby nadal całkować czujnik mocy.", + "doc": "Opcjonalny licznik kWh lub Wh (total_increasing, np. własny licznik wtyczki). Energia cyklu jest wtedy odczytywana z niego, co jest dokładniejsze niż obliczanie z wolnego czujnika mocy. Jeśli jego odczyt jest nieużyteczny, używana jest wartość obliczona.", "label": "Encja Licznika Energii" }, "expose_debug_entities": { - "doc": "Publikuj dodatkowe jednostki diagnostyczne HA (pewność dopasowania, niejednoznaczność, elementy wewnętrzne stanu). Opcja Off utrzymuje listę encji w czystości podczas normalnego użytkowania.", + "doc": "Dodaj encje diagnostyczne (pewność dopasowania, niejednoznaczność, stan wewnętrzny). Wyłączone: lista encji pozostaje czysta.", "label": "Ujawnij Encje Debugowania" }, "external_end_trigger": { - "doc": "Czujnik binarny, którego zmiana stanu oznacza koniec cyklu (np. „zakończony” styk urządzenia lub integracja towarzysząca).", + "doc": "Czujnik binarny, którego zmiana oznacza koniec cyklu (np. styk „zakończono” lub inna integracja).", "label": "Encja Zewnętrznego Wyzwalacza" }, "external_end_trigger_enabled": { - "doc": "Pozwól zewnętrznemu czujnikowi binarnemu zasygnalizować koniec cyklu, oprócz wbudowanej detekcji opartej na mocy.", + "doc": "Pozwól czujnikowi binarnemu sygnalizować koniec cyklu, oprócz wykrywania na podstawie mocy.", "label": "Włącz Zewnętrzny Wyzwalacz Zakończenia" }, "external_end_trigger_inverted": { - "doc": "Traktuj wyłączenie czujnika wyzwalającego (a nie włączenie) jako sygnał zakończenia cyklu.", + "doc": "Traktuj wyłączenie czujnika (a nie włączenie) jako sygnał końca.", "label": "Odwróć Zewnętrzny Wyzwalacz (wyzwalaj przy WYŁ)" }, "learning_confidence": { - "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.", + "doc": "Najniższy wynik dopasowania, przy którym zakończony cykl dostaje etykietę. Wyraźny zwycięzca jest etykietowany; wynik na styk trafia do kolejki przeglądu w Cyklach. Poniżej tej wartości nic nie jest etykietowane ani nie ma pytania. Utrzymuj poniżej Pewności Automatycznego Etykietowania.", "label": "Pewność Uczenia" }, "linked_device": { - "doc": "Opcjonalnie zagnieżdż to urządzenie WashData pod innym urządzeniem (np. inteligentną wtyczką) w rejestrze urządzeń HA, pokazanym jako „Połączono przez…”.", + "doc": "Opcjonalnie. Pokaż to urządzenie WashData pod innym urządzeniem (np. inteligentną wtyczką) w Home Assistant jako „Połączono przez…”.", "label": "Grupuj pod Urządzeniem" }, "min_off_gap": { - "doc": "Jeśli urządzenie wyłączy się na krócej niż ten czas, sekwencja włączenia/wyłączenia/włączenia jest traktowana jako jeden ciągły cykl. Zapobiega podziałowi programów namaczania (urządzenie wyłącza się na kilka minut w połowie prania) na dwa oddzielne cykle. Ustaw go na krótszy niż przerwa między obciążeniami, jeśli chcesz, aby były one liczone jako oddzielne cykle. Domyślne ustawienia typu urządzenia chronią typową przerwę w cyklu dla każdego urządzenia.", + "doc": "Przerwa krótsza niż ten czas nie dzieli cyklu, więc pauzy namaczania pozostają w jednym praniu. Utrzymuj krótszą niż przerwa między kolejnymi wsadami, inaczej połączą się w jeden cykl.", "label": "Minimalna Przerwa Wyłączenia" }, "min_power": { - "doc": "Absolutna minimalna moc uważana za aktywną. Odczyty poniżej są traktowane jako 0 W (tryb czuwania), odfiltrowując obciążenie fantomowe inteligentnych wtyczek i diod LED stanu czuwania.", + "doc": "Odczyty poniżej tej wartości liczą się jako 0 W (czuwanie). Odfiltrowuje pobór jałowy inteligentnych wtyczek i diod czuwania.", "label": "Minimalna Moc" }, "ml_training_enabled": { - "doc": "Okresowo analizuj sprawdzone cykle przez noc i dostosowuj modele do konkretnej maszyny. Zmiana zostaje utrzymana tylko wtedy, gdy rzeczywiście osiąga lepsze wyniki w przetrzymywanych cyklach, więc może to tylko pomóc lub pozostać bez zmian – nigdy nie cofać się.", + "doc": "Od czasu do czasu trenuj ponownie model energii na zakończonych cyklach tej maszyny. Nowy model zostaje tylko wtedy, gdy na cyklach testowych wypada lepiej niż proste oszacowanie.", "label": "Ucz się od tej maszyny" }, "ml_training_hour": { - "doc": "Lokalna godzina dnia (0-23), w której należy dokonać dostrajania w nocy. Wybierz spokojną godzinę, na przykład 2 (02:00).", + "doc": "Godzina (0-23) nocnego trenowania. Wybierz spokojną godzinę, np. 2.", "label": "Ucz się o godzinie" }, "ml_training_interval_days": { - "doc": "Sprawdzaj ponownie, czy nie ma ulepszeń, najwyżej raz na tę liczbę dni.", + "doc": "Trenuj ponownie najwyżej raz na tyle dni.", "label": "Sprawdzaj co najwyżej co" }, "ml_training_min_cycles": { - "doc": "Przed dostrojeniem poczekaj, aż zarejestrowanych zostanie co najmniej tyle cykli, aby było z czego się uczyć.", + "doc": "Przed trenowaniem poczekaj na co najmniej tyle zarejestrowanych cykli.", "label": "Wymagane minimum cykli" }, "name": { - "doc": "Nazwa wyświetlana na liście integracji HA i rejestrze urządzeń.", + "doc": "Nazwa tego urządzenia wyświetlana w Home Assistant.", "label": "Nazwa Urządzenia" }, "no_update_active_timeout": { - "doc": "Jeśli przez tak długi czas nie zostaną dostarczone żadne aktualizacje zasilania, załóż, że wtyczka została odłączona od sieci i wymuś zatrzymanie, aby uniknąć cyklu zombie. Domyślna wartość 600 s pozwala na opóźnienie w chmurze lub siatce.", + "doc": "Jeśli przez ten czas w trakcie cyklu nie nadejdzie żadna aktualizacja mocy, przyjmij, że wtyczka jest offline, i wymuś zatrzymanie cyklu. Domyślne 600 s uwzględnia opóźnienia chmury lub sieci mesh.", "label": "Limit Czasu Braku Aktualizacji" }, "notify_before_end_minutes": { @@ -1346,35 +1347,35 @@ "label": "Alert Przed Zakończeniem" }, "notify_channel": { - "doc": "Nazwa kanału powiadomień Androida dla wiadomości startowych/na żywo (steruje dźwiękiem i priorytetem poszczególnych kanałów w aplikacji mobilnej). Blank używa domyślnej aplikacji towarzyszącej.", + "doc": "Kanał Androida dla wiadomości o starcie i na żywo; kanał ustala dźwięk i priorytet. Puste: domyślny kanał aplikacji.", "label": "Kanał Android (start/na żywo)" }, "notify_cycle_timers": { - "doc": "Powiadomienia w określonych minutach cyklu (np. o konieczności dodania zmiękczacza). Wiadomość obsługuje {device}, {program}, {minutes}. Włącz automatyczną pauzę, aby wstrzymać w tym momencie i otrzymać interaktywne powiadomienie za pomocą przycisku Wznów; wznów poprzez panel, usługę wstrzymania/wznowienia lub akcję powiadomienia.", + "doc": "Powiadomienia w ustalonych minutach cyklu (np. aby dodać płyn do płukania). Wiadomość obsługuje {device}, {program}, {minutes}. Z automatyczną pauzą cykl również się tam wstrzymuje, a powiadomienie dostaje przycisk Wznów.", "label": "Timery Cyklu" }, "notify_finish_channel": { - "doc": "Nazwa kanału powiadomień Androida dla wiadomości końcowej. Blank ponownie wykorzystuje kanał startowy/na żywo.", + "doc": "Kanał Androida dla wiadomości o zakończeniu. Puste: używany jest kanał startu i na żywo.", "label": "Kanał Android (zakończenie)" }, "notify_finish_message": { - "doc": "Treść wysyłana po zakończeniu cyklu. Zmienne szablonu: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}. {time_finished} i {vs_typical} są tutaj najbardziej przydatne.", + "doc": "Treść wysyłana po zakończeniu cyklu. Zmienne: {device}, {program}, {duration} (minuty), {duration_hm} (1 h 05 min), {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}, {status} (completed lub force_stopped; przerwane cykle nic nie wysyłają). Przy każdej innej zmiennej używany jest tekst domyślny.", "label": "Wiadomość Zakończenia" }, "notify_finish_services": { - "doc": "notify.* usługi wywoływane po zakończeniu cyklu. Dodaj po jednym na cel; pozostaw puste, aby nie było powiadomienia o zakończeniu.", + "doc": "Usługi notify.* wywoływane po zakończeniu cyklu. Pozostaw puste, aby nic nie wysyłać.", "label": "Usługi Zakończenia" }, "notify_fire_events": { - "doc": "Odpalaj także zdarzenia ha_washdata_* na początku/zakończeniu cyklu, dzięki czemu możesz budować własne automatyzacje.", + "doc": "Wysyłaj też zdarzenia ha_washdata_* przy starcie i zakończeniu cyklu dla własnych automatyzacji.", "label": "Wysyłaj Zdarzenia HA dla Powiadomień" }, "notify_icon": { - "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.", + "doc": "Mała ikona w powiadomieniu (np. mdi:washing-machine). Wysyłana tylko do celów mobile_app, nie do grup notify. W iOS zastępuje ikonę aplikacji i wymaga aplikacji mobilnej 2026.8 lub nowszej. Puste: domyślna ikona platformy.", "label": "Ikona Powiadomienia" }, "notify_live_chronometer": { - "doc": "Wyświetlaj w powiadomieniu licznik czasu aktualizacji na żywo (na platformach, które je obsługują) zamiast statycznego oszacowania.", + "doc": "Pokazuj w powiadomieniu odliczanie na żywo (gdzie to obsługiwane) zamiast stałego oszacowania.", "label": "Używaj Chronometru na Żywo" }, "notify_live_interval_seconds": { @@ -1382,143 +1383,135 @@ "label": "Interwał Aktualizacji na Żywo" }, "notify_live_overrun_percent": { - "doc": "Jeśli cykl przekroczy szacunkową wartość o więcej niż ten procent, wyślij alert o przekroczeniu.", + "doc": "Aktualizacje na żywo kończą się, gdy cykl przekroczy oczekiwany czas trwania o ten procent (20 = do 1,2x oczekiwanego czasu).", "label": "% Przekroczenia przed Alertem" }, "notify_live_services": { - "doc": "notify.* usługi wymagały aktualizacji postępu na żywo podczas trwania cyklu. Pozostaw puste, aby wyłączyć powiadomienia o postępie na żywo.", + "doc": "Usługi notify.* dla aktualizacji postępu na żywo w trakcie cyklu. Pozostaw puste, aby je wyłączyć.", "label": "Usługi Aktualizacji na Żywo" }, "notify_only_when_home": { - "doc": "Wysyłaj powiadomienia tylko wtedy, gdy co najmniej jedna z połączonych osób (powyżej) jest w domu.", + "doc": "Powiadamiaj tylko, gdy co najmniej jedna z osób powyżej jest w domu.", "label": "Powiadamiaj Tylko Gdy Jestem w Domu" }, "notify_people": { - "doc": "osoba.* podmioty wykorzystywane przez funkcję „Powiadamiaj tylko, gdy jesteś w domu” w celu ustalenia, czy ktoś jest w domu.", + "doc": "Encje person.*, które sprawdza „Powiadamiaj Tylko Gdy Jestem w Domu”.", "label": "Osoby (dla Tylko Gdy Jestem w Domu)" }, "notify_pre_complete_message": { - "doc": "Treść alertu przed zakończeniem / prawie gotowe. Zmienne szablonu: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Treść każdej aktualizacji postępu na żywo. Zmienne szablonu: {device}, {program}, {minutes} (pozostałe minuty). Przy każdej innej zmiennej używany jest tekst domyślny.", "label": "Wiadomość Przed Zakończeniem" }, "notify_reminder_message": { - "doc": "Treść przypomnienia o wciąż oczekującym rozładunku. Puste używa wbudowanej wartości domyślnej. Zmienne szablonu: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Treść jednorazowego Alertu Przed Zakończeniem. Zmienne szablonu: {device}, {program}, {minutes} (ustawienie Alert Przed Zakończeniem). Przy każdej innej zmiennej używany jest tekst domyślny.", "label": "Wiadomość Przypomnienia" }, "notify_start_message": { - "doc": "Treść wysyłana po rozpoczęciu cyklu. Zmienne szablonu: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Treść wysyłana po rozpoczęciu cyklu. Zmienne szablonu: {device}, {program}. Przy każdej innej zmiennej używany jest tekst domyślny.", "label": "Wiadomość Startu" }, "notify_start_services": { - "doc": "notify.* usługi wywoływane po rozpoczęciu cyklu. Dodaj po jednym na cel (telefon, pulpit nawigacyjny itp.); pozostaw puste, aby nie było powiadomienia o rozpoczęciu.", + "doc": "Usługi notify.* wywoływane po rozpoczęciu cyklu (telefon, pulpit, ...). Pozostaw puste, aby nic nie wysyłać.", "label": "Usługi Startu" }, "notify_timeout_seconds": { - "doc": "Automatycznie odrzuć powiadomienie po tylu sekundach (na platformach, które je obsługują). 0 przechowuje go do momentu ręcznego usunięcia.", + "doc": "Zamknij powiadomienie po tylu sekundach (gdzie to obsługiwane). 0 zostawia je, dopóki go nie zamkniesz.", "label": "Automatyczne Zamknięcie Po" }, "notify_title": { - "doc": "Tytuł powiadomienia. Zmienne szablonu: {device}, {program}, {duration}, {energy}, {cost}, {date}, {time}, {minutes}.", + "doc": "Tytuł powiadomienia. Podstawiane jest tylko {device}; przy każdej innej zmiennej używany jest tytuł domyślny.", "label": "Tytuł Powiadomienia" }, "notify_unload_delay_minutes": { - "doc": "Minuty po zakończeniu cyklu przed wysłaniem wciąż oczekującego przypomnienia o „rozładowaniu maszyny”. Ustaw 0, aby wyłączyć przypomnienie.", + "doc": "Minuty po zakończeniu cyklu przed przypomnieniem „rozładuj maszynę”. 0 je wyłącza.", "label": "Opóźnienie Przypomnienia o Rozładunku" }, "notify_unload_repeat": { - "doc": "Ponownie wysyła przypomnienie o rozładunku co „Opóźnienie Przypomnienia o Rozładunku” minut, dopóki rozładunek nie zostanie potwierdzony lub nie dotkniesz „Przestań przypominać” w powiadomieniu. Wymaga sposobu potwierdzenia rozładunku: Encji Czujnika Drzwi, Encji Potwierdzenia Rozładunku albo opcji „Ręczne potwierdzanie rozładunku” poniżej. Przycisk odrzucenia działa w powiadomieniach aplikacji mobilnej Home Assistant (companion app).", + "doc": "Ponawiaj przypomnienie o rozładunku co „Opóźnienie Przypomnienia o Rozładunku” minut, aż rozładunek zostanie potwierdzony lub dotkniesz „Przestań przypominać” (aplikacja mobilna). Wymaga Encji Czujnika Drzwi, Encji Potwierdzenia Rozładunku lub „Ręczne potwierdzanie rozładunku”.", "label": "Powtarzaj do rozładowania" }, "off_delay": { - "doc": "Czas oczekiwania po spadku mocy przed ogłoszeniem zakończenia cyklu. Jeżeli w tym oknie zasilanie zostanie przywrócone, cykl będzie przebiegał płynnie – eliminując przerwy pomiędzy etapami prania. Zmywarki mają długie fazy suszenia (wyłączenie zasilania na 20–60 minut), więc opóźnienie wyłączenia musi być dłuższe, aby całe pranie+suszenie trwało w jednym cyklu.", + "doc": "Jak długo moc musi pozostać niska, zanim cykl zostanie zakończony. Powrót mocy w tym czasie kontynuuje cykl. Zmywarki podczas suszenia mogą być bezczynne 20-60 min, więc ustaw dłuższy czas.", "label": "Opóźnienie Wyłączenia" }, "pause_cuts_power": { - "doc": "Kiedy cykl zostanie wstrzymany, wyłącz także Jednostkę Przełączającą poniżej. Tylko do urządzeń, których wtyczkę można bezpiecznie odciąć w połowie cyklu.", + "doc": "Gdy cykl jest wstrzymany, wyłącz też Encję Przełącznika poniżej. Tylko dla urządzeń, które mogą bezpiecznie stracić zasilanie w trakcie cyklu.", "label": "Pauza Odcina Zasilanie (przez przełącznik)" }, "power_off_delay": { - "doc": "Jak długo moc musi pozostawać poniżej „Próg Wyłączenia Zasilania” po zakończeniu cyklu, zanim stan wróci do „Wyłączony”. Używane tylko wtedy, gdy „Próg Wyłączenia Zasilania” jest większy niż 0. Sprawdzane w rytmie działania w tle, więc efektywne opóźnienie jest zaokrąglane w górę do następnego taktu wygaśnięcia stanu.", + "doc": "Jak długo moc musi pozostać poniżej Progu Wyłączenia Zasilania po cyklu, zanim stan wróci do Wyłączony. Używane tylko, gdy ten próg jest większy niż 0. Sprawdzane okresowo, więc rzeczywiste oczekiwanie może być nieco dłuższe.", "label": "Opóźnienie Wyłączenia Zasilania" }, "power_off_threshold_w": { - "doc": "Opcjonalne wykrywanie stanu „Wyłączony” na podstawie mocy. Gdy wartość jest większa niż 0, po zakończeniu cyklu, jeśli moc pozostaje poniżej tego poziomu przez czas „Opóźnienie Wyłączenia Zasilania”, urządzenie jest traktowane jako wyłączone, a stan wraca do „Wyłączony”. Pozostaw 0, aby wyłączyć (wartość domyślna). Ustaw powyżej rzeczywistego poziomu wyłączonego urządzenia, a poniżej „Próg Zatrzymania” oraz poniżej poboru w trybie czuwania po zakończeniu; jeśli wartość nie jest niższa niż „Próg Zatrzymania”, jest ignorowana. Po włączeniu zastępuje „Opóźnienie Resetowania Postępu” przy powrocie do „Wyłączony”, więc zakończone urządzenie pozostaje w stanie „Zakończony”/„Czysty”, dopóki nie zostanie faktycznie wyłączone.", + "doc": "Po cyklu moc poniżej tej wartości przez Opóźnienie Wyłączenia Zasilania ustawia stan na Wyłączony, zamiast robić to po Opóźnieniu Resetowania Postępu. Ustaw powyżej poboru wyłączonego urządzenia, a poniżej czuwania i Progu Zatrzymania. 0 wyłącza (domyślnie).", "label": "Próg Wyłączenia Zasilania" }, "power_sensor": { - "doc": "Jednostka czujnika zgłaszająca moc bieżącą tego urządzenia w watach (np. sensor.washer_power). Cała detekcja cykli opiera się na tym sygnale.", + "doc": "Czujnik podający bieżącą moc tego urządzenia w watach (np. sensor.washer_power). Na nim opiera się całe wykrywanie.", "label": "Czujnik Mocy" }, - "profile_duration_tolerance": { - "doc": "Pasmo +/- wokół średniego czasu trwania profilu używanego podczas dopasowywania. Wartość 0,25 oznacza, że profil 60-minutowy odpowiada cyklom 45-75-minutowym.", - "label": "Tolerancja Czasu Profilu" - }, "profile_match_interval": { - "doc": "Jak często podejmować próby dopasowania profilu podczas cyklu biegowego. Domyślnie 300 s (5 minut) równoważy szybkość wykrywania i procesor.", + "doc": "Jak często dopasowywać trwający cykl do profili. Domyślnie 300 s (5 min).", "label": "Interwał Dopasowania" }, "profile_match_max_duration_ratio": { - "doc": "Maksymalna długość cyklu w stosunku do profilu. 1.3 oznacza, że cykl musi trwać krócej niż 130% czasu trwania profilu, aby uzyskać zgodność.", + "doc": "Program jest odrzucany, gdy cykl trwa dłużej niż ta wielokrotność jego typowego czasu trwania. 1,8: cykl o 80% dłuższy niż program przestaje być do niego dopasowywany.", "label": "Maksymalna Proporcja Czasu Trwania" }, "profile_match_min_duration_ratio": { - "doc": "Minimalna długość cyklu w stosunku do profilu. Wartość 0,9 oznacza, że aby cykl był zgodny, musi wynosić co najmniej 90% czasu trwania profilu.", + "doc": "Program jest brany pod uwagę dopiero, gdy cykl trwa już taką część jego typowego czasu trwania. 0,10: program 2-godzinny staje się kandydatem po 12 minutach. Nie stosuje się w pierwszych 15 minutach cyklu.", "label": "Minimalna Proporcja Czasu Trwania" }, "profile_match_threshold": { - "doc": "Minimalny wynik podobieństwa (0-1) wymagany na koniec cyklu, aby zaakceptować identyfikację programu. Podnieś go, aby ograniczyć błędne identyfikacje; obniż go, jeśli programy twojego komputera nie są dopasowane. Wartość domyślna 0,4 to konserwatywny punkt wyjścia.", + "doc": "Wynik dopasowania na żywo, którego cykl potrzebuje, zanim Inteligentne zakończenie lub zakończenie antygniotowe może go zakończyć wcześniej. Nie zmienia wyświetlanego programu. Podnieś, jeśli słabe dopasowania kończą cykle za wcześnie; obniż, jeśli wyraźne dopasowania czekają przez opóźnienie wyłączenia.", "label": "Próg Dopasowania" }, "profile_unmatch_threshold": { - "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.", + "doc": "Jeśli wynik dopasowania na żywo spadnie poniżej tej wartości, bieżące przypuszczenie programu jest czyszczone. Utrzymuj nieco poniżej Progu Dopasowania, aby krótki spadek go nie resetował.", "label": "Próg Niedopasowania" }, "progress_reset_delay": { - "doc": "Po zakończeniu utrzymaj postęp na poziomie 100% przez tak długi czas, aby na pulpitach nawigacyjnych pojawił się komunikat Ukończono, zanim przejdziesz do stanu bezczynności.", + "doc": "Po cyklu utrzymuj postęp na 100% przez ten czas, zanim nastąpi reset do stanu Bezczynny.", "label": "Opóźnienie Resetowania Postępu" }, "pump_stuck_duration": { - "doc": "Przez kilka sekund pompa może pracować nieprzerwanie, zanim zostanie oznaczona jako prawdopodobnie zablokowana (wywołane zostanie zdarzenie zablokowania pompy).", + "doc": "Jak długo pompa może pracować bez przerwy, zanim zostanie wywołane zdarzenie zaciętej pompy.", "label": "Czas Zacięcia Pompy" }, "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.", + "doc": "Minimalny czas między przetwarzanymi odczytami. Odczyty, które nadejdą szybciej, są pomijane (niskie odczyty w trakcie cyklu są zawsze zachowywane). Ustaw interwał raportowania wtyczki lub mniej; wyższa wartość tylko odrzuca odczyty.", "label": "Interwał Próbkowania" }, "save_debug_traces": { - "doc": "Przechowuj ślad pełnej mocy i pasujące dane debugowania dla każdego cyklu. Przydatne do rozwiązywania problemów, ale zwiększa rozmiar pamięci.", + "doc": "Zapisuj też przy każdym cyklu dane debugowania mechanizmu dopasowania (wyniki każdego kandydata), do rozwiązywania problemów. Dodaje około 20 KB na cykl.", "label": "Zapisuj Ślady Debugowania" }, "smart_termination_duration_ratio": { - "doc": "Jak daleko w oczekiwanym czasie trwania dopasowanego programu musi znajdować się cykl, zanim Inteligentne zakończenie może go zakończyć wcześniej po spadku mocy. Oczekiwany czas trwania to średnia programu, więc w urządzeniach, których czas pracy bardzo się waha - pralki z zimną wodą dopływową zimą wobec ciepłej latem, suszarki z suszeniem czujnikowym, programy zależne od wsadu - około połowa wszystkich uruchomień kończy się krócej niż ta średnia i nigdy nie doczeka się szybkiego zakończenia, kończąc dopiero przez awaryjny limit czasu, kilka minut później. W takich maszynach obniż tę wartość (np. 0,85), aby wcześniejsze zakończenie nadal się uruchamiało; zwiększ ją w stronę 1,0, aby zachować większą ostrożność. Pozostaw puste dla wartości domyślnej (0,98, a dla zmywarek 0,99). Może jedynie zakończyć cykl wcześniej, nigdy później, i nigdy nie uruchamia się przy niejednoznacznym dopasowaniu lub dopasowaniu o niskiej pewności.", + "doc": "Ułamek średniego czasu trwania programu, który cykl musi osiągnąć, zanim Inteligentne zakończenie może go zakończyć wcześniej. Jeśli czasy pracy się wahają (temperatura wody, suszenie z czujnikiem, wielkość wsadu), obniż go (np. 0,85). Pozostaw puste dla 0,98 (0,99 w zmywarkach).", "label": "Współczynnik inteligentnego zakończenia" }, - "smoothing_window": { - "doc": "W jakim stopniu surowy sygnał mocy jest wygładzany. Niski (2) reaguje, ale jest głośny; wysoki (5) wygładza skoki, ale dodaje opóźnienia.", - "label": "Okno Wygładzania" - }, "start_duration_threshold": { - "doc": "Moc musi pozostać powyżej progu rozruchu przez tak długi czas, aby potwierdzić prawdziwy start i zapobiec rozpoczęciu cyklu przez przełączniki włączania/wyłączania trwające ułamek sekundy.", + "doc": "Moc musi tyle czasu pozostawać powyżej Progu Startu, zanim cykl się rozpocznie, aby krótkie włączenia i wyłączenia były ignorowane.", "label": "Czas Trwania Startu" }, "start_energy_threshold": { - "doc": "Energia (moc x czas), jaką urządzenie musi zużyć przed PRACĄ. Krótki skok dużej mocy ma bardzo niską energię i jest ignorowany, co zapobiega fałszywym startom.", + "doc": "Energia, którą urządzenie musi zużyć, zanim cykl zostanie uznany za pracujący. Krótki skok mocy zużywa mało energii, więc nie rozpoczyna cyklu.", "label": "Energia Startu" }, "start_threshold_w": { - "doc": "Moc musi wzrosnąć powyżej tego poziomu, aby potwierdzić rozpoczęcie cyklu. Ustawienie zbyt niskiego poziomu powoduje falowe starty z zasilania w trybie gotowości; pominięte są programy zbyt wysokie i wolno uruchamiające się (napełnianie na zimno). Silnik sugestii ustawia tę wartość tuż powyżej zaobserwowanej najniższej mocy czynnej maszyny.", + "doc": "Moc musi wzrosnąć powyżej tej wartości, aby rozpocząć cykl. Za nisko: pobór w czuwaniu powoduje fałszywe starty. Za wysoko: wolno startujące programy (zimne napełnianie) są pomijane.", "label": "Próg Startu" }, "stop_threshold_w": { - "doc": "Moc musi spaść poniżej tego poziomu, zanim rozpocznie się odliczanie opóźnienia wyłączenia. Ustaw go poniżej progu początkowego – odstęp między nimi to pasmo histerezy zapobiegające migotaniu. W przypadku ustawienia zbyt wysokiego, fazy o małej mocy (zatrzymanie płukania, zapobieganie zagnieceniom) błędnie uruchamiają sekwencję końcową.", + "doc": "Moc musi spaść poniżej tej wartości, zanim zacznie się odliczanie opóźnienia wyłączenia. Utrzymuj poniżej Progu Startu. Przy zbyt wysokiej wartości fazy o niskiej mocy (przerwy w płukaniu, tryb antygniotowy) omyłkowo uruchamiają odliczanie końca.", "label": "Próg Zatrzymania" }, "switch_entity": { - "doc": "Opcjonalny przełącznik wyłączany podczas pauzy i włączany ponownie po wznowieniu, używany razem z opcją „Pauza powoduje również odcięcie zasilania”.", + "doc": "Opcjonalny przełącznik wyłączany przy pauzie i włączany przy wznowieniu, razem z „Pauza Odcina Zasilanie (przez przełącznik)”.", "label": "Encja Przełącznika" }, "watchdog_interval": { - "doc": "Jak często funkcja nadzorująca działająca w tle sprawdza, czy czujniki nie uległy zablokowaniu i nie upłynęły limity czasu. Domyślnie 30 s.", + "doc": "Jak często kontrola w tle szuka zablokowanych czujników i przekroczonych limitów czasu. Domyślnie 30 s.", "label": "Interwał Watchdog" }, "notify_milestone_message": { @@ -1526,131 +1519,86 @@ "label": "Wiadomość o kamieniu milowym" }, "notify_milestones": { - "doc": "Rozdzielone przecinkami liczby cykli, które po osiągnięciu wyzwalają jednorazowe powiadomienie z gratulacjami (np. 50, 100, 500, 1000). Puste wyłącza powiadomienia o kamieniach milowych.", + "doc": "Liczby cykli, które wyzwalają jednorazowe powiadomienie z gratulacjami, oddzielone przecinkami (np. 50, 100, 500, 1000). Puste je wyłącza.", "label": "Kamienie milowe cykli" }, "notify_quiet_end_hour": { - "doc": "Koniec okna „nie przeszkadzać” (0–23). Wstrzymane powiadomienia są dostarczane o tej godzinie. Pozostaw puste, aby wyłączyć.", + "doc": "Koniec okna „nie przeszkadzać” (0-23); wstrzymane powiadomienia są wtedy wysyłane. Puste je wyłącza.", "label": "Koniec cichych godzin" }, "notify_quiet_start_hour": { - "doc": "Początek okna „nie przeszkadzać” (0–23). Powiadomienia o zakończeniu, przypomnienia i powiadomienia o czystym praniu, które wypadłyby w cichych godzinach, są wstrzymywane i dostarczane po zakończeniu okna. Pozostaw puste, aby wyłączyć. Obsługuje okna przekraczające północ (np. początek 22, koniec 7).", + "doc": "Początek okna „nie przeszkadzać” (0-23). Powiadomienia o zakończeniu, przypomnienia i powiadomienia o czystym praniu w tym oknie są wstrzymywane do jego końca. Może przechodzić przez północ (np. 22 do 7). Puste je wyłącza.", "label": "Początek cichych godzin" }, "peak_rate_message": { - "doc": "Opcjonalny własny tekst wskazówki o taryfie szczytowej dołączanej do powiadomienia o starcie. Zmienne szablonu: {device}, {price}. Puste używa wbudowanej wartości domyślnej.", + "doc": "Własny tekst wskazówki o taryfie szczytowej. Zmienne szablonu: {device}, {price}. Puste: tekst domyślny.", "label": "Wiadomość o taryfie szczytowej" }, "peak_rate_threshold": { - "doc": "Gdy cykl się rozpoczyna, a bieżąca cena za kWh jest równa tej wartości lub wyższa, do powiadomienia o starcie dołączana jest wskazówka o taryfie szczytowej. 0 lub puste wyłącza wskazówkę.", + "doc": "Jeśli cena za kWh na starcie cyklu jest równa tej wartości lub wyższa, dodaj wskazówkę o taryfie szczytowej do powiadomienia o starcie. 0 lub puste ją wyłącza.", "label": "Próg taryfy szczytowej (za kWh)" }, - "dtw_bandwidth": { - "label": "Tolerancja dopasowania kształtu", - "doc": "Jak bardzo mechanizm dopasowywania może zniekształcić cykl w czasie, aby dopasować go do kształtu zapisanego programu. Wyższa wartość toleruje większe wahania tempa; zbyt wysoka grozi dopasowaniem niewłaściwego programu." - }, - "corr_weight": { - "label": "Waga kształtu względem poziomu", - "doc": "Równowaga między kształtem krzywej a bezwzględnym poziomem mocy przy ocenie dopasowania. Wyższa preferuje kształt; niższa preferuje zgodność poziomu mocy." - }, - "duration_weight": { - "label": "Waga czasu trwania", - "doc": "Jak silnie preferowany jest program, którego typowy czas trwania odpowiada temu cyklowi." - }, - "energy_weight": { - "label": "Waga energii", - "doc": "Jak silnie preferowany jest program, którego typowe zużycie energii odpowiada temu cyklowi." - }, "store_brand": { "label": "Marka urządzenia", - "doc": "Opcjonalne. Marka urządzenia wybrana z katalogu społeczności. Używana do wyszukiwania i udostępniania pasujących nagrań referencyjnych. Pozostaw puste, jeśli nie korzystasz z funkcji online." + "doc": "Opcjonalnie. Marka urządzenia z katalogu społeczności, używana do wyszukiwania i udostępniania nagrań referencyjnych. Pozostaw puste, jeśli nie korzystasz z funkcji online." }, "store_model": { "label": "Model urządzenia", - "doc": "Opcjonalne. Model urządzenia wybrany z katalogu społeczności po ustawieniu marki. Jeśli Twojego modelu nie ma na liście, możesz go dodać do katalogu." + "doc": "Opcjonalnie. Model urządzenia z katalogu społeczności (najpierw wybierz markę). Jeśli go nie ma na liście, możesz go dodać." }, "show_contributor": { "doc": "Pokaż nazwę współtwórcy na profilach pobranych ze sklepu społeczności" }, - "enable_phase_matching": { - "label": "Pozostały czas uwzględniający fazy", - "doc": "Dzieli każdy trwający cykl na fazy (podgrzewanie, pranie, wirowanie) i planuje pozostały czas dla każdej fazy, łącząc go z klasycznym szacowaniem - opierając się na budżecie faz na początku cyklu, a na klasycznym szacowaniu pod koniec. Personalizuje to odliczanie do tego, jak długo urządzenie faktycznie się nagrzewa i pracuje, co jest najbardziej zauważalne w pierwszej połowie cyklu. Wyłączone = tylko klasyczne szacowanie. Wpływa to wyłącznie na wyświetlanie pozostałego czasu; dopasowanie programów i wykrywanie cykli pozostają bez zmian." - }, - "keep_min_score": { - "label": "Min. wynik dopasowania", - "doc": "Minimalny wynik podobieństwa potrzebny kandydatowi, by pozostać w wyścigu dopasowań. Domyślna wartość 0,1 jest celowo łagodna - dopuszcza nawet słabych kandydatów i polega na późniejszych etapach w wyborze najlepszego dopasowania. Zwiększ, by wcześniej odrzucać mało prawdopodobne profile; zmniejsz, by poszerzyć początkową pulę kandydatów." - }, - "dtw_blend": { - "label": "Mieszanie znieksztalcenia", - "doc": "Jak bardzo wynik wyrównania DTW zastępuje wynik główny Etapu 2. 0 = używaj tylko wyniku Etapu 2, 1 = używaj tylko wyniku DTW, 0,5 (domyślnie) = równe mieszanie. Zwiększ, by bardziej polegać na wyrównaniu, gdy programy mają podobne poziomy mocy, ale różne wzorce czasowe." - }, - "dtw_ensemble_w": { - "label": "Mix kombinacji DTW", - "doc": "W trybie kombinacji DTW (domyślnym), waga skalowanego L1 względem pochodnego DTW (DDTW). 1,0 = tylko skalowane L1, 0 = tylko DDTW, 0,7 = domyślnie. Wariant skalowany L1 porównuje poziomy mocy; wariant pochodny reaguje na zmiany mocy w czasie. Kombinacja łączy oba." - }, - "dtw_ddtw_scale": { - "label": "Skala pochodnej DTW", - "doc": "Odległość półnasycenia dla oceniania pochodnym DTW. Wynik spada do połowy, gdy odległość DDTW równa się tej wartości. Mniejsza = większa wrażliwość na różnice kształtu. Domyślna wartość 30 jest skalibrowana do typowych przebiegów mocy urządzeń. Dotyczy tylko trybów DTW kombinacji i ddtw." - }, - "dtw_refine_top_n": { - "label": "Liczba doprecyzowanych kandydatow", - "doc": "Ile najlepszych kandydatów Etapu 2 jest ponownie ocenianych przez DTW. DTW jest kosztowniejszy, więc stosowany jest tylko do N najlepszych kandydatów. Domyślnie 5. Zwiększ (do 7-9), jeśli właściwy profil czasem zajmuje tylko 4. lub 5. miejsce po Etapie 2 - DTW może go uratować. Zmniejszenie nieznacznie przyspiesza dopasowanie." - }, - "duration_scale": { - "label": "Ostrość czasu trwania", - "doc": "Ostrość kary za niezgodność czasu trwania w Etapie 4. To logarytm stosunku, przy którym wynik zgodności spada do połowy. Mniejszy = surowszy: niezgodność czasu trwania bardziej obniża wynik. Domyślna wartość 0,175 odpowiada tolerancji ok. 18% przy połowie wagi. Używaj razem z Wagą Czasu Trwania." - }, - "energy_scale": { - "label": "Ostrość energii", - "doc": "Ostrość kary za niezgodność energii w Etapie 4. Mniejszy = surowszy: niezgodność energii bardziej obniża wynik. Domyślna wartość 0,25 jest celowo bardziej łagodna niż Ostrość Czasu Trwania, ponieważ energia zmienia się z obciążeniem. Używaj razem z Wagą Energii." - }, "power_profile_interval_min": { "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." + "doc": "Rozmiar przedziału atrybutu power_profile (średnie waty na slot) używanego przez planery takie jak EMHASS i tibber_prices. Mniejszy zachowuje krótkie skoki; większy wygładza krzywą. Domyślnie 15 min. 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." + "doc": "Tylko iOS. Aktualizuj postęp na żywo po cichu, z priorytetem oszczędzającym baterię, który iOS może grupować. Pierwsza aktualizacja oraz powiadomienia o starcie i zakończeniu nadal powiadamiają. Wyłączone: każda aktualizacja powiadamia." }, "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." + "doc": "Tylko Android. Zachowaj powiadomienie o postępie na żywo po dotknięciu. Wyłączone: dotknięcie je zamyka." }, "notify_live_click_action": { "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." + "doc": "Co otwiera dotknięcie powiadomienia tego urządzenia. Puste: panel WashData tego urządzenia. Wpisz ścieżkę pulpitu (np. /lovelace/laundry) lub URL albo „none”, aby dotknięcie nic nie otwierało." }, "door_opens_at_end": { "label": "Drzwi otwierają się automatycznie na końcu", - "doc": "Dla zmywarek, które na końcu cyklu otwierają drzwi w celu suszenia (AirDry i podobne). Po włączeniu otwarcie drzwi podczas działającego cyklu nie zatrzymuje go na zawsze; jeśli drzwi pozostaną otwarte przez czas oczekiwania poniżej, WashData uznaje cykl za zakończony. Krótkie otwarcie (dodawanie naczynia) jest ignorowane. Wymaga encji czujnika drzwi." + "doc": "Dla zmywarek, które na końcu otwierają drzwi, aby suszyć (np. AirDry). Drzwi otwarte przez czas oczekiwania poniżej kończą cykl zamiast go wstrzymywać; krótkie otwarcie jest ignorowane. Wymaga Encji Czujnika Drzwi." }, "door_end_dwell_seconds": { "label": "Czas otwarcia drzwi do zakończenia cyklu", - "doc": "Jak długo drzwi muszą pozostać otwarte, zanim WashData zakończy cykl, gdy włączone jest \"Drzwi otwierają się automatycznie na końcu\". Wystarczająco długo, aby zignorować szybkie dodawanie naczyń (domyślnie 60 s), wystarczająco krótko, aby szybko zakończyć, gdy maszyna otworzy drzwi." + "doc": "Przy włączonym „Drzwi otwierają się automatycznie na końcu”: jak długo drzwi muszą pozostać otwarte, zanim cykl się zakończy. Na tyle długo, aby zignorować dołożenie naczynia (domyślnie 60 s)." }, "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." + "doc": "Które rodzaje cykli kształtują krzywą i dopasowanie każdego programu. Odznaczenie rodzaju niczego nie usuwa; przydatne, jeśli nie ufasz zaimportowanym danym. Statystyki zawsze liczą tylko cykle tej maszyny. Odznaczenie wszystkich jest ignorowane." }, "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." + "doc": "Naliczaj każdy cykl według ceny obowiązującej w każdej chwili jego trwania, a nie ceny z chwili zakończenia. Wymaga encji ceny. Przetwarzanie historii (Diagnostyka) przelicza koszty starszych cykli." }, "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.", + "doc": "Dołącza odczyty z nieudanych prób startu na początek cyklu, dla maszyn, które wykonują próby przed startem (blokada drzwi, pierwsze napełnianie). 300 s wystarcza w większości przypadków; 0 = wyłączone. Zapisany początek przesuwa się wcześniej, więc wyuczone czasy trwania przez jakiś czas się zmieniają.", "label": "Wstępny zapis krzywej" }, "unload_confirm_entity": { "label": "Encja Potwierdzenia Rozładunku", - "doc": "Opcjonalna encja, której aktywacja oznacza, że wsad został wyjęty, dla maszyn, w których nie da się zamontować czujnika drzwi. Działa dowolny rodzaj: przycisk Zigbee lub NFC, pomocnik input_button, czujnik ruchu przed maszyną, scena. Każda zmiana stanu liczy się jako potwierdzenie, z wyjątkiem wyłączenia i przejścia w stan niedostępny. Ustawienie tej opcji włącza też przypomnienie o rozładunku na urządzeniu bez Encji Czujnika Drzwi." + "doc": "Opcjonalnie. Dowolna encja, która potwierdza wyjęcie wsadu, dla maszyn bez czujnika drzwi (przycisk, input_button, czujnik ruchu, scena). Liczy się każda zmiana stanu poza wyłączeniem i niedostępnością. Włącza też przypomnienie o rozładunku." }, "unload_track_without_door": { "label": "Ręczne potwierdzanie rozładunku", - "doc": "Uruchamia przypomnienie o rozładunku na urządzeniu bez Encji Czujnika Drzwi i bez Encji Potwierdzenia Rozładunku, a rozładunek potwierdzasz z własnej automatyzacji: naciśnij encję przycisku „Oznacz jako rozładowane” lub wywołaj usługę ha_washdata.mark_unloaded. Ignorowane, gdy ustawiona jest Encja Czujnika Drzwi, która już dostarcza ten sygnał." + "doc": "Uruchamiaj przypomnienie o rozładunku bez czujnika drzwi i encji potwierdzenia. Potwierdź rozładunek z własnej automatyzacji przyciskiem „Oznacz jako rozładowane” lub usługą ha_washdata.mark_unloaded. Ignorowane, gdy ustawiono Encję Czujnika Drzwi." }, "notify_icon_color": { "label": "Kolor Powiadomienia", - "doc": "Kolor akcentu powiadomień tego urządzenia, aby pralkę, suszarkę i zmywarkę można było odróżnić na pierwszy rzut oka. iOS koloruje ikonę oraz pasek postępu Live Activity na ekranie blokady i w Dynamic Island. Android otrzymuje ten sam kolor, ale to telefon decyduje, co z nim zrobi: niektóre kolorują ikonę, a wiele (zwłaszcza Samsung One UI) całkowicie go ignoruje. Obie platformy rozumieją Hex (np. #03A9F4). Pozostaw puste, aby użyć domyślnego koloru platformy." + "doc": "Kolor akcentu powiadomień tego urządzenia, aby łatwo odróżnić pralkę, suszarkę i zmywarkę. W iOS wypełnia kółko za ikoną i koloruje pasek Live Activity. Wiele telefonów z Androidem (zwłaszcza Samsung) go ignoruje. Użyj zapisu hex, np. #03A9F4. Puste: domyślny kolor platformy." + }, + "share_name": { + "doc": "Pokazuj nazwę swojego konta jako autora udostępnianych cykli. Wyłączone: udostępniane bez nazwy." } }, "setting_group": { @@ -1678,9 +1626,6 @@ "door_pause": { "label": "Drzwi i Pauza" }, - "dtw_refinement": { - "label": "Uściślenie DTW" - }, "duration_gates": { "label": "Bramki Czasowe" }, @@ -1757,17 +1702,20 @@ "rebuilding": "Przebudowywanie…", "analyzing": "Analizowanie…", "resetting": "Resetowanie…", - "reverting": "Przywracanie…", "trimming": "Przycinanie…", "splitting": "Dzielenie…", + "stalled": "Utknął", "deleting": "Usuwanie…", "imported": "Zaimportowano", "preparing": "Przygotowywanie…", - "unlabelling": "Usuwanie etykiety…" + "unlabelling": "Usuwanie etykiety…", + "working": "Przetwarzanie…", + "match_uncertain": "Niepewne: {top} lub {runner}", + "match_uncertain_single": "Niepewne: może {top}", + "match_sure": "~{pct}% pewności", + "match_uncertain_tip": "WashData nie wybrał jeszcze programu. Liczba pokazuje, jak często wiodący kandydat, {top}, okazywał się trafny w poprzednich cyklach przy takiej przewadze. Tylko informacyjnie." }, "suggestion": { - "both_agree": "WashData zaleca", - "calibrated_label": "Skalibrowano", "observed_label": "Zmierzono", "impact": { "auto_label_confidence": { @@ -1782,10 +1730,6 @@ "higher": "Zamyka tylko cykle o znacznym zużyciu energii", "lower": "Zamyka również krótsze lub mniej energochłonne cykle" }, - "end_repeat_count": { - "higher": "Wymaga więcej powtórzeń sygnału końca – bardziej niezawodne, nieco wolniejsze", - "lower": "Kończy po mniejszej liczbie powtórzeń – szybsze wykrywanie, większe ryzyko fałszywego zakończenia" - }, "learning_confidence": { "higher": "Uczy się tylko z bardzo pewnych dopasowań – wolniejsze, ale bardziej niezawodne", "lower": "Uczy się z większej liczby cykli; model może być mniej precyzyjny" @@ -1824,40 +1768,25 @@ } }, "reason": { - "auto_label_confidence": "Pewność na 15. percentylu dla {cycles} etykiet automatycznych, których użytkownik nigdy nie poprawił ({p15}).", "completion_min_seconds": "Połowa czasu trwania p05 poprawnego cyklu ({minutes} min) dla {cycles} cykli; odfiltrowuje fałszywe cykle.{excl}", - "dead_zone_batch": "Utrzymane krótkie (mediana okna niestabilności rozruchu dla {cycles} cykli = {s}s), aby rzeczywisty koniec był szybko wykrywany.", - "dead_zone_from_dip": "Na podstawie wczesnego spadku mocy wykrytego przy {s}s.", - "end_energy_default": "Domyślna zalecana wartość bazowa dla progu szumu na końcu cyklu.", - "end_energy_false": "Energia p95 fałszywych zakończeń ({p95}Wh) dla {cycles} cykli, z dolną granicą 0.2% mediany energii cyklu ({floor}Wh).", - "end_energy_no_false": "Brak fałszywych zakończeń dla {cycles} cykli; dolna granica 0.2% mediany energii cyklu ({floor}Wh).", - "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.", + "match_interval_floored": "Najkrótszy program ({shortest}s) ograniczyłby tę wartość do {cap}s, poniżej minimum {minimum}s, więc użyto minimum: {persistence} kolejnych dopasowań zajmuje {budget}s, ponad {pct}% tego programu. Dopasowanie uruchamia się tylko po nadejściu odczytu (mediana={median}s), więc nic to nie kosztuje.", "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.", "min_off_gap": "Na podstawie {gaps} zaobserwowanych przerw między cyklami (p05={p05}s). Minimum urządzenia: {floor}s.", "min_off_gap_bridge": "Dobrane tak, aby pokryć najdłuższy cichy odcinek wewnątrz cyklu: {span}s + zapas 60s, z {spans} pokrytych przerw w {cycles} poprawnych cyklach (pozostaje poniżej {ceiling}s, czyli najkrótszej przerwy, jaką zostawiasz między kolejnymi uruchomieniami).", "min_power": "40% najniższej mocy czynnej p05 ({p05}W) w {cycles} poprawnych cyklach, tak aby próg wyłączenia pozostał poniżej rzeczywistego poboru.{excl}", - "ml_auto_label": "Najniższa pewność, przy której model jakości nadal uznawał cykl za poprawny (p10 z {cycles} poprawnych cykli = {p10}).", - "ml_end_repeat": "{false}/{total} cykli ({pct}%) miało pauzę, którą detektor końca ocenił powyżej 50%; wymagaj {val} potwierdzeń zakończenia.", - "ml_off_delay": "Pauzy potwierdzone przez detektor końca: p95 {p95}s + zapas 60s, z {pauses} pauz zweryfikowanych przez model w {cycles} cyklach (minimum {floor}s).", "no_update_timeout": "Na podstawie obserwowanej częstotliwości aktualizacji (p95={p95}s) * 20 (min. 60s).", "off_delay_cadence": "Na podstawie obserwowanej częstotliwości aktualizacji (p95={p95}s) * 5", "off_delay_device_floor": "Użyto bezpiecznego minimum specyficznego dla urządzenia {device} ({floor}s).", "off_delay_generic_floor": "Użyto ogólnego bezpiecznego minimum ({floor}s).", "off_delay_pauses": "Dobrane tak, aby przetrwać rzeczywiste pauzy: pauza w cyklu p95 {p95}s + zapas 60s, z {pauses} pauz w {cycles} poprawnych cyklach (minimum {floor}s).", - "profile_match_threshold": "Pewność p10 dla {cycles} poprawnych etykiet automatycznych ({p10}); bezpieczny próg potwierdzenia na żywo.", - "sampling_interval": "Mediana interwału aktualizacji zaobserwowana w {cycles} poprawnych cyklach ({si}s).{excl}", - "smoothing_window": "Dobrane do wygładzania ~30s odczytów przy próbkowaniu {si}s ({samples} próbek).", - "start_duration": "Utrzymane krótkie (~jeden interwał próbkowania {si}s), aby wykrywanie rozpoczynało się jak najwcześniej, jednocześnie ignorując pojedyncze skoki.", - "thr_batch": "Utrzymane tuż powyżej najniższej mocy czynnej p05 w {cycles} cyklach ({p05}W), aby start był wykrywany jak najwcześniej, a próg zatrzymania pozostawał poniżej najniższej mocy pracy urządzenia.", - "tol_per_profile": "p75 wariancji czasu trwania na profil w {profiles} profilach ({cycles} cykli); spójne profile nie są karane.", - "tol_pooled": "Na podstawie łącznej wariancji czasu trwania {cycles} ostatnich oznaczonych cykli (odchylenie p95={dev}).", - "watchdog": "Utrzymane tak nisko, jak to bezpieczne (tuż powyżej przerwy aktualizacji p95 wynoszącej {p95}s i co najmniej 2x powyżej interwału próbkowania {median}s, min. 30s), aby zawieszenia były szybko wykrywane bez fałszywych zatrzymań." + "standby_floor": "Każdy ostatni cykl nadal pobierał {idle}W w chwili zakończenia, więc próg poniżej tej wartości nigdy nie wykryje wyłączenia urządzenia. Utrzymany powyżej niej; {cycles} czystych cykli w historii nigdy nie spadło poniżej niego na dłużej niż {pause}s.", + "thr_batch": "Tuż powyżej najniższej mocy czynnej (p05) w {cycles} cyklach ({p05}W), aby starty były wykrywane wcześnie, a próg zatrzymania pozostał poniżej najniższej mocy pracy.", + "watchdog": "Utrzymane tak nisko, jak to bezpieczne (tuż powyżej przerwy aktualizacji p95 wynoszącej {p95}s i co najmniej 2x powyżej interwału próbkowania {median}s, min. 30s), aby zawieszenia były szybko wykrywane bez fałszywych zatrzymań.", + "end_energy_floor": "Podniesiono do minimum wynikającego z progu zatrzymania ({stop} W) w czasie opóźnienia wyłączenia ({off_delay} s); poniżej niego cykl może się zakończyć tylko ścieżką awaryjną.", + "max_duration_ratio_default": "Przywrócono wartość domyślną: niższy górny limit odrzuca właściwy program za każdym razem, gdy cykl trwa dłużej niż zwykle.", + "min_duration_ratio_default": "Przywrócono wartość domyślną: wyższy dolny limit uniemożliwia rozpoznanie trwającego cyklu, dopóki nie jest on mocno zaawansowany." }, "exclusions": { "summary": "Wykluczono {total} błędnie wykrytych cykli: {parts}.", @@ -1875,7 +1804,8 @@ "no_power": "bez mocy", "no_active_power": "bez aktywnej mocy" } - } + }, + "now_value": "(obecnie {v})" }, "tab": { "advanced": "Zaawansowany", @@ -1906,13 +1836,17 @@ }, "toast": { "catalog_refreshed": "Katalog społeczności odświeżony", + "task_name_required": "Nadaj zadaniu nazwę", + "task_added": "Dodano zadanie", + "task_save_failed": "Nie można zapisać zadania: {error}", + "task_removed": "Usunięto zadanie", + "maint_logged": "Oznaczono jako wykonane: {task}", "access_saved": "Kontrola dostępu zapisana", "all_wiped": "Wszystkie dane zostały usunięte", "analysis_complete_none": "Analiza ukończona: brak nowych sugestii", "auto_label_complete": "Automatyczne etykietowanie zakończone", "automation_deleted": "Automatyzacja usunięta", "correction_submitted": "Poprawka złożona", - "cycle_deleted": "Cykl usunięty", "cycle_force_stopped": "Cykl wymuszony zatrzymany", "cycle_labelled": "Cykl oznaczony", "cycle_paused": "Cykl wstrzymany", @@ -1928,11 +1862,9 @@ "group_deleted": "Grupa usunięta", "group_name_required": "Nazwa grupy jest wymagana", "group_saved": "Grupa zapisana", - "import_successful": "Import udany; przeładowanie integracji", "json_required": "Wymagane są dane JSON", "legacy_removed": "Usunięto starsze działania", "logs_exported": "Logi wyeksportowane", - "matching_reverted": "Dopasowane wagi przywrócono do wartości domyślnych", "min_2_profiles": "Wybierz co najmniej 2 profile dla grupy", "ml_training_no_improvement": "Trening ukończony: poziom bazowy utrzymany (brak poprawy)", "models_reverted": "Modele na urządzeniu przywrócono do stanu bazowego", @@ -1942,8 +1874,8 @@ "pg_preset_loaded": "Preset \"{name}\" wczytany", "pg_preset_saved": "Preset \"{name}\" zapisany", "pg_sugg_loaded": "Załadowano {n} sugerowanych wartości - uruchom piaskownicę, aby porównać", - "pg_sugg_ml_loaded": "Załadowano {n} wartości skalibrowanych przez ML - uruchom piaskownicę, aby porównać", "phase_name_required": "Nazwa fazy jest wymagana", + "phase_reset": "Przywrócono wbudowaną wersję fazy \"{name}\"", "phase_updated": "Faza zaktualizowana", "phases_saved": "Fazy zapisane", "preferences_saved": "Preferencje zostały zapisane", @@ -1990,15 +1922,11 @@ "store_name_required": "Wprowadź nazwę profilu", "store_import_failed": "Import nie powiódł się: {error}", "store_imported": "Zaimportowano do {profile}", - "store_program_required": "Wprowadź nazwę programu", "store_no_appliance": "Najpierw ustaw markę i model urządzenia w Ustawieniach.", "store_share_failed": "Udostępnianie nie powiodło się: {error}", "store_shared": "Udostępniono w sklepie społecznościowym, oczekuje na weryfikację.", "contrib_device_changed": "Dodano do sklepu społecznościowego, ale wybrane urządzenie zmieniło się, gdy formularz był otwarty. Wybierz je w Ustawieniach.", "appliance_added": "Urządzenie dodane, oczekuje na zatwierdzenie", - "thanks_confirming": "Dziękujemy za potwierdzenie", - "device_approved": "Zatwierdzone przez społeczność", - "rating_saved": "Ocena jakości zapisana", "brand_added": "Marka dodana, oczekuje na zatwierdzenie", "profile_added": "Profil dodany, oczekuje na zatwierdzenie", "saved_except_conflicts": "Zapisano. Popraw podświetlone konflikty, aby zapisać pozostałe.", @@ -2021,7 +1949,52 @@ "odometer_saved": "Łączna liczba cykli zapisana", "odometer_save_failed": "Nie udało się zapisać łącznej liczby: {error}", "store_pick_profile_share": "Wybierz profil do udostępnienia", - "remove_failed": "Usuwanie nie powiodło się: {error}" + "remove_failed": "Usuwanie nie powiodło się: {error}", + "analysis_complete": "Analiza ukończona: sugestie: {count}", + "analysis_failed": "Analiza nie powiodła się: {error}", + "auto_detect_failed": "Automatyczne wykrywanie nie powiodło się: {error}", + "auto_label_failed": "Automatyczne oznaczanie nie powiodło się: {error}", + "could_not_load_cycle": "Nie można załadować cyklu: {error}", + "create_failed": "Tworzenie nie powiodło się: {error}", + "cycles_deleted": "Usunięte cykle: {count}", + "debug_cleared": "Wyczyszczone ślady debugowania: {count}", + "discard_failed": "Odrzucenie nie powiodło się: {error}", + "error": "Błąd: {error}", + "export_failed": "Eksport nie powiódł się: {error}", + "import_failed": "Import nie powiódł się: {error}", + "invalid_json": "\"{key}\" nie jest prawidłowym JSON - popraw go lub wyczyść pole przed zapisaniem.", + "label_failed": "Oznaczanie nie powiodło się: {error}", + "ml_training_promoted": "Trening ukończony: wdrożone modele: {count}", + "pause_failed": "Wstrzymanie nie powiodło się: {error}", + "phase_created": "Utworzono fazę \"{name}\"", + "phase_deleted": "Usunięto fazę \"{name}\"", + "profile_created": "Utworzono profil \"{name}\"", + "rename_failed": "Zmiana nazwy nie powiodła się: {error}", + "save_failed": "Zapisywanie nie powiodło się: {error}", + "split_complete": "Podzielono na cykle: {count}", + "start_failed": "Uruchomienie nie powiodło się: {error}", + "stop_failed": "Zatrzymanie nie powiodło się: {error}", + "update_failed": "Aktualizacja nie powiodła się: {error}", + "store_download_skipped": "Pominięto nagrań: {n} (zbyt krótkie, ze zbyt wieloma przerwami lub już są na Twoim urządzeniu).", + "store_import_low_quality": "To nagranie jest zbyt krótkie, ma zbyt wiele przerw lub jest niewiarygodne, więc nie można go zaimportować.", + "store_import_duplicate": "To nagranie jest już na Twoim urządzeniu.", + "device_approved": "Zatwierdzone przez społeczność", + "thanks_confirming": "Dziękujemy za potwierdzenie", + "rating_saved": "Ocena jakości zapisana", + "store_download_partial": "Nie udało się pobrać programów: {n}; spróbuj ponownie później.", + "import_no_restore_point": "Zaimportowano, ale nie udało się zapisać punktu przywracania, więc tego importu nie można cofnąć", + "import_undo_failed": "Nie udało się cofnąć importu: {error}", + "import_undone": "Import cofnięty: przywrócono {profiles} profil(e) i {cycles} cykl(e)", + "auto_label_cancelled": "Automatyczne oznaczanie anulowano po oznaczeniu {count} cykli.", + "auto_label_cancelled_one": "Automatyczne oznaczanie anulowano po oznaczeniu {count} cyklu.", + "auto_label_cancelled_few": "Automatyczne oznaczanie anulowano po oznaczeniu {count} cykli.", + "auto_label_cancelled_many": "Automatyczne oznaczanie anulowano po oznaczeniu {count} cykli.", + "auto_label_cancelled_other": "Automatyczne oznaczanie anulowano po oznaczeniu {count} cyklu.", + "auto_label_done": "Automatyczne oznaczanie zakończone: oznaczono {count} cykli, bez etykiety pozostało: {skipped}.", + "auto_label_done_one": "Automatyczne oznaczanie zakończone: oznaczono {count} cykl, bez etykiety pozostało: {skipped}.", + "auto_label_done_few": "Automatyczne oznaczanie zakończone: oznaczono {count} cykle, bez etykiety pozostało: {skipped}.", + "auto_label_done_many": "Automatyczne oznaczanie zakończone: oznaczono {count} cykli, bez etykiety pozostało: {skipped}.", + "auto_label_done_other": "Automatyczne oznaczanie zakończone: oznaczono {count} cyklu, bez etykiety pozostało: {skipped}." }, "trend": { "down": "Trend w dół", @@ -2029,6 +2002,10 @@ }, "maint": { "bearing_service": "Serwis łożysk", + "salt": "Uzupełnij sól", + "rinse_aid": "Uzupełnij nabłyszczacz", + "lint_filter": "Wyczyść filtr kłaczków", + "condenser_clean": "Wyczyść skraplacz", "descale": "Odkamienianie", "drum_clean": "Wyczyść bęben", "filter_clean": "Wyczyść filtr", @@ -2036,34 +2013,22 @@ }, "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ą", + "smart_termination_duration_ratio": "Ułamek średniego czasu trwania programu, który cykl musi osiągnąć, zanim Inteligentne zakończenie może go zakończyć wcześniej; obniż, jeśli czasy pracy się wahają", + "anti_crease_finalize_ratio": "Ułamek średniego czasu trwania programu, który cykl musi osiągnąć, zanim ogon antygniotowy może go zakończyć; obniż w suszarkach, w pralkach zostaw 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_exit_power": "Poziom mocy w przerwach między impulsami obrotów; działa tylko powyżej Progu Zatrzymania", "anti_wrinkle_idle_timeout": "Dozwolony czas ciszy między dwoma impulsami bębna, zanim tryb antygniotowy się zakończy", "anti_wrinkle_max_duration": "Dłuższy impuls kończy tryb antygniotowy i otwiera nowy cykl", "anti_wrinkle_max_power": "Impuls powyżej tej wartości kończy tryb antygniotowy i otwiera nowy cykl", "completion_min_seconds": "Najkrótszy przebieg liczony jako prawdziwy cykl", - "end_repeat_count": "Kolejne niskie odczyty przed zakończeniem", "interrupted_min_seconds": "Krótkie cykle oznaczane jako przerwane", "min_off_gap": "Przerwa wymagana do rozdzielenia dwóch cykli", "off_delay": "Sekundy niskiej mocy przed zakończeniem cyklu", "start_duration_threshold": "Sekundy powyżej progu, by potwierdzić start", "start_threshold_w": "Minimalna moc w watach, by uznać za rozpoczęty", "stop_threshold_w": "Poniżej tej wartości urządzenie liczone jest jako wyłączone", - "dtw_bandwidth": "Etap 3: pasmo zniekształcenia Sakoe-Chiba (0 = DTW wyłączony; domyślnie 0,2 = 20% długości cyklu)", - "corr_weight": "Etap 2: balans między kształtem krzywej (korelacja) a poziomem mocy (MAE); domyślnie 0,45", - "duration_weight": "Etap 4: jak mocno zgodność czasu trwania wpływa na wynik końcowy (domyślnie 0,22)", - "energy_weight": "Etap 4: jak mocno zgodność energii wpływa na wynik końcowy (domyślnie 0,22)", - "profile_match_min_duration_ratio": "Etap 1: minimalna długość cyklu jako ułamek czasu trwania profilu; domyślnie 0,1 (10%)", - "profile_match_max_duration_ratio": "Etap 1: maksymalna długość cyklu jako ułamek czasu trwania profilu; domyślnie 1,8 (180%)", - "keep_min_score": "Etap 2: minimalny wynik, by pozostać w wyścigu; domyślnie 0,1 dopuszcza słabe dopasowania, późniejsze etapy wybierają najlepsze", - "dtw_blend": "Etap 3: 0 = tylko wynik główny, 1 = tylko wynik DTW, 0,5 = równe mieszanie (domyślnie)", - "dtw_ensemble_w": "Etap 3 (tryb kombinacji): waga skalowanego L1 względem pochodnego DTW; domyślnie 0,7 faworyzuje poziom", - "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)", + "profile_match_min_duration_ratio": "Minimalna długość cyklu jako ułamek czasu trwania profilu; domyślnie 0,1 (10%)", + "profile_match_max_duration_ratio": "Maksymalna długość cyklu jako ułamek czasu trwania profilu; domyślnie 1,8 (180%)", "curve_preroll_seconds": "Sekundy odczytów z przerwanych prób startu dołączane na początku krzywej cyklu; 0 = wyłączone" }, "col": { @@ -2071,12 +2036,14 @@ "status_tip": "Wynik cyklu: Zakończony (naturalny koniec), Przerwany (nagły spadek mocy), Wymuszone zatrzymanie (ręczne) lub Wymaga sprawdzenia (oczekuje opinii).", "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, 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.", + "energy_tip": "Całkowita zużyta energia (kWh), obliczona z mocy w czasie.", + "cost_tip": "Koszt energii tego cyklu, ustalony w chwili zakończenia. Przy włączonym Koszcie ważonym czasem każda chwila jest naliczana po swojej cenie; w przeciwnym razie po cenie z końca. Ustaw cenę w Ustawieniach, aby to wypełnić.", "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", - "cost_dynamic_tip": "Ważone czasem: naliczone według ceny obowiązującej w trakcie cyklu (efektywnie {price} {cur}/kWh)." + "cost_dynamic_tip": "Ważone czasem: naliczone według ceny obowiązującej w trakcie cyklu (efektywnie {price} {cur}/kWh).", + "dbg_conf": "Zauf.", + "dbg_mae": "MAE", + "dbg_corr": "Kor." }, "stat": { "avg": "śr.", @@ -2100,7 +2067,10 @@ "notify_finish": "Zostałoby wysłane powiadomienie o zakończeniu.", "notify_milestone": "Zostałoby wysłane powiadomienie o osiągnięciu etapu.", "notify_held": "Powiadomienie zostało wstrzymane z powodu godzin ciszy.", - "finished": "Cykl osiągnął stan końcowy i zakończył się." + "finished": "Cykl osiągnął stan końcowy i zakończył się.", + "match_reverted": "Program wrócił do wykrywania: jego wynik spadł znacznie poniżej własnego szczytu.", + "verified_pause": "Wyrównanie na żywo potwierdziło (lub zniosło) pauzę wewnątrz programu. Dopóki pauza trwa, cykl nie zostanie zakończony.", + "stalled": "Cykl pozostawał na poborze czuwania dłużej, niż ten program kiedykolwiek tam pauzuje, a praca nie była skończona. Pokazany jako Wstrzymany (Utknął); cykl pozostaje otwarty." }, "store": { "search_ph2": "Szukaj według marki lub modelu…", @@ -2109,11 +2079,7 @@ "programs_count": "Programy: {n}", "browse": "Przeglądaj", "device": "Urządzenie", - "favorites": "Ulubione", - "search_ph": "Szukaj według marki lub modelu…", "no_results": "Nie znaleziono pasujących urządzeń. Spróbuj innego wyszukiwania.", - "n_programs": "{n} programów", - "n_cycles": "{n} cykli", "no_programs": "Brak udostępnionych programów dla tego urządzenia.", "no_cycles": "Brak udostępnionych cykli referencyjnych dla tego programu.", "anon": "anonimowo", @@ -2125,19 +2091,21 @@ "merge_target": "Scal z profilem", "description": "Opis (opcjonalnie)", "contributed_by": "od {name}", + "website": "Strona sklepu ↗", + "website_tip": "Otwórz stronę sklepu społecznościowego", + "favorites": "Ulubione", "rating_summary": "★ {avg} ({n})", "no_ratings": "Brak ocen", - "website": "Strona sklepu ↗", - "website_tip": "Otwórz stronę sklepu społecznościowego" + "unreachable": "Nie udało się połączyć ze sklepem społecznościowym. Sprawdź połączenie i spróbuj ponownie." }, "settings": { "gear": { - "title": "Ustawienia" + "title": "Preferencje" } }, "link": { - "manual": "Instrukcja ↗", - "add_appliance": "Dodaj swoje urządzenie" + "maintenance_supporter": "Maintenance Supporter na GitHubie ↗", + "manual": "Instrukcja ↗" }, "tip": { "add_brand": "Marki nie ma na liście? Dodaj ją do katalogu społeczności", @@ -2171,7 +2139,13 @@ "rebuild": { "envelopes": "Przeliczanie obwiedni" }, - "cancelling": "Anulowanie..." + "cancelling": "Anulowanie...", + "auto_label": { + "running": "Automatyczne etykietowanie cykli" + }, + "store_download": { + "running": "Pobieranie ze sklepu" + } }, "setup": { "hdr": { @@ -2180,26 +2154,24 @@ }, "phase0": { "washer": "WashData już wykrywa Twoje cykle. Nagraj pierwszy cykl, aby włączyć nazwy programów i szacunki czasu.", - "dishwasher": "WashData obserwuje. Zmywarki mają złożone cykle – nagranie pierwszego cyklu jest zdecydowanie zalecane. Jeśli wykryty cykl trwa zbyt długo, użyj edytora cyklu, aby go przyciąć przed zapisaniem jako profil.", + "dishwasher": "WashData obserwuje. Cykle zmywarki są złożone, więc nagraj pierwszy cykl. Jeśli wykryty cykl trwa za długo, przytnij go w edytorze cyklu przed zapisaniem jako profil.", "generic": "WashData obserwuje. Nagraj lub oznacz wykryty cykl, aby zacząć tworzyć profile." }, "phase1a": { - "labelled": "Dobry początek – Twój pierwszy program jest zapisany. Aby uzyskać najczystsze dane, rozważ nagranie następnego cyklu za pomocą widżetu rejestratora." + "labelled": "Dobry początek: Twój pierwszy program jest zapisany. Aby mieć najczystsze dane, nagraj następny cykl rejestratorem." }, "phase1b": { - "recorded": "Nagranie zostało zapisane jako {profile_name}. Teraz nagraj lub oznacz inne popularne programy, aby rozszerzyć pokrycie." + "recorded": "Nagranie zapisano jako {profile_name}. Teraz nagraj lub oznacz pozostałe często używane programy." }, "phase1c": { - "verify": "Masz {count} programów ze społeczności. Uruchom cykl, aby sprawdzić, czy WashData rozpoznaje go poprawnie – dopasowanie będzie się poprawiać w miarę jak urządzenie buduje własną historię." + "verify": "Masz programy ze społeczności ({count}). Uruchom cykl, aby sprawdzić, czy WashData go rozpoznaje; dopasowanie poprawia się, gdy urządzenie buduje własną historię." }, "phase2": { - "cluster": "WashData zauważył {count} cykli, które nie pasują do żadnego zapisanego programu – wyglądają podobnie do siebie. Chcesz utworzyć dla nich nowy profil?", + "cluster": "WashData zauważył podobne cykle ({count}), które nie pasują do żadnego zapisanego programu. Utworzyć dla nich nowy profil?", "unmatched": "Twój ostatni cykl nie pasował do żadnego zapisanego programu. Czy to nowy program?" }, "phase3": { - "suggestions": "WashData ma zalecenia dotyczące ustawień oparte na historii Twoich cykli – przejrzyj je, aby poprawić dokładność wykrywania.", - "groups": "Niektóre Twoje profile wyglądają jak ten sam program w różnych temperaturach. Zorganizuj je w grupę dla lepszego dopasowania.", - "phases": "Dodaj fazy programu do {profile_name} dla dokładniejszych szacunków pozostałego czasu." + "suggestions": "WashData ma sugestie ustawień na podstawie historii Twoich cykli. Przejrzyj je, aby poprawić wykrywanie." }, "phase4": { "healthy": "To urządzenie jest w pełni skonfigurowane ({profile_count} profili)." @@ -2212,12 +2184,58 @@ "create_from_cluster": "Utwórz profil z tych cykli", "create_profile": "Utwórz dla niego profil", "review_suggestions": "Przejrzyj sugestie", - "organise_profiles": "Zorganizuj profile", - "configure_phases": "Skonfiguruj fazy", "skip_step": "Pomiń ten krok", "skip_forever": "Nie pokazuj ponownie", "hide_guidance": "Ukryj wskazówki", "show_guidance": "Pokaż wskazówki" } + }, + "access": { + "edit": "Edycja", + "full": "Pełny", + "inherit": "Dziedzicz", + "none": "Brak (ukryte)", + "read": "Odczyt" + }, + "pg_label_reason": { + "no_winner": "Żaden program nie pasuje", + "below_floor": "Poniżej progu \"Pewność Uczenia\"", + "ambiguous": "Oznaczone jako niejednoznaczne", + "margin": "Zbyt blisko drugiego najlepszego programu", + "unknown_profile": "Program już nie istnieje", + "not_evaluated": "Nie oceniono" + }, + "diagram": { + "accum": "akum.", + "auto": "auto", + "below_off": "poniżej = wył.", + "confirmed": "potwierdzono", + "energy_counts": "energia się liczy", + "feedback": "opinia", + "gap_below_min": "przerwa poniżej min. = jeden cykl", + "heat_phase": "faza grzania", + "held_at_100": "pozostaje na 100%", + "ignored": "ignorowane", + "kept": "zachowane", + "max": "maks.", + "min": "min.", + "minus_tol": "-tol", + "no_match": "brak dopasowania", + "no_updates": "brak aktualizacji", + "off_delay_wait": "opóźnienie wyłączenia", + "off_gap": "przerwa wył.", + "plus_tol": "+tol", + "profile": "profil", + "reading_interval": "typowy interwał odczytów", + "sensor_offline": "czujnik offline: wymuszone zatrzymanie", + "si": "IP", + "spike_ignored": "skok zignorowany", + "spike_ignored_short": "skok: zignorowany", + "start": "start", + "stop": "stop", + "tail_energy_resets": "energia końcówki powyżej progu: licznik się zeruje", + "thr": "próg", + "too_short": "za krótki", + "tumble_kept": "impulsy bębna zachowane" } } diff --git a/custom_components/ha_washdata/translations/panel/pt-BR.json b/custom_components/ha_washdata/translations/panel/pt-BR.json index 16911063..0bbfd40b 100644 --- a/custom_components/ha_washdata/translations/panel/pt-BR.json +++ b/custom_components/ha_washdata/translations/panel/pt-BR.json @@ -1,15 +1,16 @@ { "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_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. Visto em {seen} de {measured} ciclos medidos.", "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", + "artifact_tip": "{n} anomalias detectadas (por exemplo, porta aberta no meio do ciclo) - abra para vê-las no gráfico", + "artifact_tip_one": "{n} anomalia detectada (por exemplo, porta aberta no meio do ciclo) - abra para vê-la no gráfico", + "artifact_tip_many": "{n} anomalias detectadas (por exemplo, porta aberta no meio do ciclo) - abra para vê-las no gráfico", + "artifact_tip_other": "{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", "declining": "↘ em declínio", "energy_low": "Energia abaixo do habitual", "energy_spike": "Energia acima do habitual", - "fair_fit": "qualidade aceitável", - "fair_fit_tip": "Consistência de correspondência moderada – alguns ciclos atribuídos a este perfil têm pontuações de confiança mais baixas. Rotule mais ciclos ou regrave o perfil para melhorar a precisão.", "feedback_requested": "Feedback solicitado", "golden_cycle": "Ciclo de referência gravado", "improving": "↗ melhorando", @@ -17,28 +18,41 @@ "needs_review": "Precisa de revisão", "overrun": "Demorou mais tempo que o normal", "poor_fit": "qualidade fraca", - "poor_fit_tip": "Histórico de correspondência inconsistente – considere reconstruir este perfil", - "restart_gap_tip": "{n} lacuna de reinicialização do HA: o rastro de potência tem uma lacuna", + "restart_gap_tip": "{n} lacunas de reinicialização do HA: o rastro de potência tem lacunas", + "restart_gap_tip_one": "{n} lacuna de reinicialização do HA: o rastro de potência tem uma lacuna", + "restart_gap_tip_many": "{n} lacunas de reinicialização do HA: o rastro de potência tem lacunas", + "restart_gap_tip_other": "{n} lacunas de reinicialização do HA: o rastro de potência tem lacunas", "reviewed": "Revisado", "steady": "→ estável", "underrun": "Terminou mais rápido que o normal", - "using_defaults": "Usando configurações padrão", - "using_tuned": "Usando pesos ajustados", "overrun_ratio": "({x}x o esperado)", "underrun_ratio": "({pct}% do esperado)", - "awaiting_tip": "Aguardando aprovação da comunidade", - "awaiting_n": "Aguardando aprovação · {n} confirmados", - "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.", "armed": "(aplica-se ao próximo ciclo)", - "duration_outlier": "{n} ciclo(s) com duração incomum" + "duration_outlier": "{n} ciclo(s) com duração incomum", + "awaiting_tip": "Aguardando aprovação da comunidade", + "awaiting_n": "Aguardando aprovação · {n} confirmados", + "awaiting": "Aguardando aprovação", + "approved": "Aprovado", + "matcher_unused": "Ainda não correspondeu a nenhum dos seus ciclos", + "matcher_used": "Corresponde a {n} dos seus ciclos", + "matcher_used_one": "Corresponde a {n} dos seus ciclos", + "matcher_used_many": "Corresponde a {n} dos seus ciclos", + "matcher_used_other": "Corresponde a {n} dos seus ciclos", + "matcher_used_tip": "Ciclos deste aparelho que o WashData associou sozinho a este programa importado. Uma importação que nunca corresponde pode não servir para o seu aparelho; você pode removê-la. Contado localmente; nada é enviado para a loja da comunidade." }, "btn": { + "log_done_task": "Registrar como feito: {task}", + "add_task": "Adicionar tarefa", + "pg_preset_delete": "Excluir predefinição", + "pg_preset_load": "Carregar predefinição", + "pg_preset_load_tip": "Substituir os valores da Área de testes por esta predefinição (as configurações ativas não são alteradas)", + "pg_preset_save": "Salvar como predefinição", + "pg_preset_save_tip": "Salvar todos os valores abaixo como uma predefinição nomeada para este dispositivo", "reset_zoom": "Redefinir zoom", "set_brand_model": "Definir marca e modelo", "refresh_catalog": "Atualizar catálogo", @@ -49,12 +63,11 @@ "add_timer": "+ Adicionar temporizador", "apply_all": "Aplicar todos", "apply_label": "Aplicar rótulo", - "apply_set_b": "Aplicar conjunto B", "apply_split": "Aplicar divisão", "apply_trim": "Aplicar recorte", "auto_detect_split": "Detectar automaticamente", "auto_label_cycles": "Rotular ciclos automaticamente", - "auto_label_cycles_tip": "Atribuir automaticamente nomes de perfil a ciclos sem rótulo cuja confiança de correspondência supere o limite", + "auto_label_cycles_tip": "Dar nome aos ciclos sem rótulo cuja confiança de correspondência supere o limite.", "cancel": "Cancelar", "clear_debug": "Limpar dados de depuração", "clear_splits": "Limpar", @@ -64,8 +77,6 @@ "convert_to_automation": "Converter em automação", "correct": "Corrigir…", "create": "Criar", - "create_from_cluster": "Criar perfil a partir de {count} ciclos", - "create_profile": "+ Criar perfil", "delete": "Excluir", "delete_group": "Excluir grupo", "delete_group_tip": "Excluir apenas este grupo – os perfis dos membros são mantidos", @@ -76,22 +87,19 @@ "done": "Concluído", "edit": "Editar", "export": "Exportar", - "export_json": "Exportar para JSON", "force_stop": "Forçar parada", "force_stop_tip": "Encerrar imediatamente o ciclo atual e marcá-lo como parado forçadamente", "from_template": "A partir de modelo ▾", "ignore": "Ignorar", "import_json": "Importar de JSON", - "import_overwrite": "Importar (substitui dados)", "inspect": "Inspecionar", "label": "Rotular", - "load": "Carregar", "load_more": "Carregar mais", "manage": "Gerenciar", "merge": "Mesclar", "new_automation": "+ Nova automação", "new_group": "+ Novo grupo", - "new_group_tip": "Agrupar perfis quase idênticos (mesma forma/duração, temperatura ou centrifugação diferentes) para que o comparador os distinga de forma confiável", + "new_group_tip": "Agrupar perfis quase idênticos (mesmo formato, temperatura ou centrifugação diferente) para que a correspondência consiga diferenciá-los.", "new_phase": "+ Nova fase", "new_profile": "+ Novo perfil", "new_profile_tip": "Criar um novo perfil de programa a partir de um ciclo rotulado existente ou de uma gravação", @@ -99,36 +107,25 @@ "on_cycle_started": "Ao iniciar o ciclo", "pause_cycle": "Pausar", "pause_cycle_tip": "Pausar o ciclo em andamento – o eletrodoméstico retomará onde parou", - "pg_apply_device": "Aplicar ao dispositivo", - "pg_autofill": "Preencher automaticamente", - "pg_load_calibrated": "Carregar Calibrado (ML) ({n})", - "pg_load_calibrated_tip": "Carregar as configurações calibradas por ML como substituições na Área de testes", "pg_load_live": "Carregar as configurações ativas", "pg_load_live_tip": "Reler as configurações atuais da integração e descartar todas as edições feitas na Área de testes", "pg_load_suggested": "Carregar sugestões ({n})", "pg_load_suggested_tip": "Carregar as sugestões atuais do ajustador automático como substituições na Área de testes", - "pg_preset_delete": "Excluir predefinição", - "pg_preset_load": "Carregar predefinição", - "pg_preset_load_tip": "Substituir os valores da Área de testes por esta predefinição (as configurações ativas não são alteradas)", - "pg_preset_save": "Salvar como predefinição", - "pg_preset_save_tip": "Salvar todos os valores abaixo como uma predefinição nomeada para este dispositivo", "pg_publish_all": "Salvar {n} configuração(ões) na integração", "pg_publish_one": "Salvar {label} na integração", - "play": "Reproduzir", "process": "Processar", "process_history": "Processar agora", "process_recording": "Salvar gravação", "process_tip": "Salvar o rastro gravado como um perfil novo ou existente", "rebuild": "Reconstruir envelopes", "rebuild_envelope": "Reconstruir envelope", - "rebuild_tip": "Recalcular o envelope de potência esperado (faixa mín/máx) para todos os perfis a partir dos seus ciclos rotulados – executar após rotular novos ciclos ou corrigir os existentes", + "rebuild_tip": "Reconstruir a faixa de potência esperada de cada perfil a partir dos seus ciclos rotulados. Execute após rotular ou corrigir ciclos.", "rec_start_tip": "Iniciar a gravação do rastro de potência do eletrodoméstico – iniciar logo antes de executar um ciclo", "rec_stop": "Parar", "rec_stop_tip": "Parar a gravação e manter o rastro capturado para revisão", "record": "Iniciar gravação", "refresh": "Atualizar", "refresh_diag_tip": "Recarregar estatísticas de armazenamento", - "refresh_logs_tip": "Recarregar os registros de log mais recentes", "refresh_settings_tip": "Recarregar as configurações do servidor", "relabel": "Reetiquetar ({count})", "remove": "Remover", @@ -136,7 +133,6 @@ "reset": "Redefinir", "reset_ml_models_tip": "Descartar os modelos ajustados e voltar aos integrados. O WashData pode reaprendê-los mais tarde.", "reset_to_builtin": "Redefinir para modelos integrados", - "reset_to_defaults": "Redefinir para padrão", "resume_cycle": "Retomar ciclo", "resume_cycle_tip": "Retomar o ciclo pausado", "revert_settings": "Descartar alterações", @@ -149,8 +145,6 @@ "run_analysis": "🔍 Executar análise de sugestões", "run_analysis_tip": "Analisar os ciclos gravados agora e atualizar as sugestões de ajuste", "run_auto_label": "Executar rotulagem automática", - "run_both": "Executar A vs B", - "run_simulation": "Executar simulação", "save": "Salvar", "save_access_control": "Salvar controle de acesso", "save_group": "Salvar grupo", @@ -162,10 +156,8 @@ "save_settings": "Salvar configurações", "select": "Selecionar", "select_all": "Selecionar tudo", - "select_last20": "Últimos 20", "select_none": "Limpar", "show_only": "Mostrar apenas", - "skip_setup": "Pular configuração", "split": "Dividir", "stop": "Parar", "submit_correction": "Enviar correção", @@ -174,36 +166,31 @@ "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.", + "unlabel_selected_tip": "Remover o rótulo dos ciclos selecionados. Eles continuam no seu histórico, mas deixam de moldar este perfil.", "use": "Usar", "wipe_all": "Apagar todos os dados", "delete_selected": "Excluir selecionados ({n})", - "group_suggest": "Grupo {n}: {members}", "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", "import": "Importar", "disconnect": "Desconectar", "connect_github": "Conectar GitHub", - "save_appliance": "Salvar eletrodoméstico", "share_to_store": "Compartilhar na loja", "share": "Compartilhar", - "confirm_appliance": "Confirmar este eletrodoméstico", - "confirmed": "Confirmado", "browse_community_setups": "Explorar configurações da comunidade", "download_device": "Baixar esta configuração", "share_device": "⬆ Compartilhar este dispositivo", - "share_device_tip": "Compartilhe este eletrodoméstico e seus ciclos de referência gravados na loja da comunidade para que outros usuários com o mesmo aparelho possam adotá-los", + "share_device_tip": "Compartilhar este eletrodoméstico e seus ciclos de referência na loja da comunidade, para que donos da mesma máquina possam adotá-los.", "share_n": "Compartilhar {n} ciclo(s)", "export_selected": "Exportar (escolher dados)", "export_all": "Exportar tudo rapidamente", - "import_raw": "Avançado: substituir tudo a partir de JSON", "download_export": "Baixar exportação", "analyze_import": "Analisar arquivo", "import_selected": "Importar seleção", "back": "Voltar", "mute_suggestion": "Parar de sugerir esta configuração", "reset_muted": "Redefinir silenciados", + "reset_phase_tip": "Desfazer sua edição e restaurar a fase integrada", "import_power_history": "Importar histórico de potência", "hist_read_recorder": "Ler do Home Assistant", "hist_scan": "Procurar ciclos", @@ -212,13 +199,15 @@ "save_total_cycles": "Salvar total", "clear": "Limpar", "add_brand": "Adicionar marca", - "add_appliance": "Adicionar eletrodoméstico" + "add_appliance": "Adicionar eletrodoméstico", + "retry": "Tentar novamente", + "confirm_appliance": "Confirmar este eletrodoméstico", + "confirmed": "Confirmado", + "create_profile_from_gap": "Criar perfil", + "undo_import": "Desfazer a última importação", + "pg_rerun": "Atualizar simulação" }, "conflict": { - "anti_wrinkle_exit": { - "exit": "Deve ser abaixo do Limiar de parada ({stop} W); caso contrário, a potência de saída antiamassado é ignorada", - "stop": "Deve ser acima da Potência de saída antiamassado ({exit} W)" - }, "anti_wrinkle_max": { "max": "Deve ser acima do Limiar de início ({start} W); caso contrário, o limite de duração antiamassado é ignorado", "start": "Deve ser abaixo da Potência máxima antiamassado ({max} W)" @@ -226,10 +215,10 @@ "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." + "power_sensor": "Este é o Sensor de Potência do dispositivo, não um preço. Os ciclos seriam cobrados em watts por kWh; limpe este campo.", + "energy_sensor": "Esta é a Entidade do Medidor de Energia do dispositivo, não um preç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. Limpe este campo ou escolha um sensor de tarifa.", + "unit": "Este sensor mede em {unit}, não em preço por kWh. Limpe este campo ou escolha 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", @@ -264,8 +253,8 @@ "threshold": "Deve ser abaixo do Limiar de parada ({stop} W) para ter efeito" }, "pump_stuck": { - "duration": "Deve ser menor que o Tempo limite sem atualização ({to} s) para que o alarme de bomba presa dispare antes do watchdog encerrar o ciclo", - "timeout": "Deve exceder a Duração de bomba presa ({ps} s) para que o alarme dispare antes de o ciclo ser forçado a parar" + "duration": "Deve ser menor que o Timeout Sem Atualização ({to} s) para que o alarme de bomba travada possa disparar primeiro", + "timeout": "Deve ser maior que a Duração da Bomba Travada ({ps} s) para que o alarme de bomba travada possa disparar primeiro" }, "start_dur": { "sampling": "O Intervalo de amostragem ultrapassa a Duração de início ({sdt} s); picos de amostra única podem abrir um ciclo", @@ -281,10 +270,15 @@ "watchdog": { "interval": "Deve ser pelo menos 2x o Intervalo de amostragem ({si} s)", "sampling": "O Intervalo de amostragem deve ser no máximo metade do Intervalo watchdog ({wi} s)" + }, + "end_energy": { + "energy": "Restritivo demais para o Limite de Parada ({w} W) durante o Atraso de Desligamento ({d} s); o ciclo só pode terminar por um mecanismo alternativo", + "stop": "A Energia de Fim ({e} Wh em {d} s) só permite {p} W" } }, "hdr": { - "ab_comparison": "Comparação de configurações A/B", + "custom_tasks": "Suas próprias tarefas", + "maintenance_supporter": "Precisa de mais?", "access_control": "Controle de acesso", "add_maintenance": "Adicionar evento de manutenção", "automation_delete": "Excluir esta automação", @@ -293,28 +287,22 @@ "automation_review_this_cycle": "Revisar este ciclo", "automations": "Automações", "clear_debug": "Limpar rastros de depuração", - "cycle_simulator": "Simulador de ciclos", "diagnostics_pref": "Diagnósticos", "display": "Exibição", - "dtw_inspector": "Inspetor DTW", "export_import": "Exportar / Importar", - "getting_started": "Primeiros passos", "live_power": "Potência em tempo real", "logs": "Registros", - "logs_diagnostics": "Diagnósticos", "maintenance": "Ações de manutenção", "maintenance_log": "Registro de manutenção", "maintenance_reminders": "Lembretes de manutenção", "manual_recording": "Gravação manual", - "manual_recording_tip": "Execute um ciclo intencionalmente enquanto o WashData registra o rastro de potência. Inicie logo antes do eletrodoméstico ligar, Pare quando terminar, depois Processe para salvá-lo como um perfil com nome.", + "manual_recording_tip": "Grave um ciclo de propósito: pressione Início logo antes de o eletrodoméstico começar e Parar quando terminar, depois Processar para salvá-lo como perfil.", "ml_learned": "O que o WashData aprendeu", - "ml_matching_tuning": "Ajuste fino da correspondência de programas", "ml_settings_card": "Configurações", "ml_smart_learning": "Aprendizado inteligente", "my_preferences": "Minhas preferências", "no_automations": "Ainda não há automações que referenciem este dispositivo.", "panel_settings": "Configurações do painel", - "param_sweep": "Varredura de parâmetros", "pg_settings_source": "Origem das configurações", "phase_catalog": "Catálogo de fases", "playground": "Área de testes", @@ -323,11 +311,8 @@ "status": "Status", "status_graph": "Gráfico de status", "storage_stats": "Estatísticas de armazenamento", - "test_history": "Testar no histórico", "toggle_sidebar": "Alternar barra lateral do Home Assistant", - "tools_and_data": "Ferramentas e dados", "wipe_history": "Apagar histórico", - "recommendations": "Recomendações ({n})", "pg_detection_params": "Configurações de detecção", "pg_outcome": "Resultado da simulação", "pg_across_cycles": "Em todos os seus ciclos", @@ -335,7 +320,8 @@ "online_account": "Loja da comunidade e recursos online", "import_power_history": "Importar histórico de potência", "service_status": "Status da manutenção", - "online_account_tab": "Online e comunidade" + "online_account_tab": "Online e comunidade", + "live_match_debug": "Depuração da correspondência em tempo real" }, "health": { "fair": "Qualidade de perfil aceitável", @@ -343,7 +329,12 @@ "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.", + "task_name": "Tarefa", + "every_cycles": "A cada (ciclos)", + "every_days": "A cada (dias)", + "new_task_name": "Nova tarefa", + "days_since_service": "{since} / {total} dias", + "envelope_position_tip": "Posição na curva gravada do programa correspondente: {pct}%. Obtida alinhando o ciclo ao perfil, então continua correta quando um ciclo é mais longo ou mais curto. Atualiza enquanto o aparelho está em silêncio, então pode ficar atrasada.", "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})", @@ -355,10 +346,8 @@ "aria_cycle_chart": "Curva de potência do ciclo", "aria_envelope_chart": "Envelope de potência do perfil", "aria_phase_chart": "Gráfico do editor de fases", - "aria_playground_chart": "Curva de potência do ciclo com limites de detecção arrastáveis", "aria_power_chart": "Gráfico de potência", "aria_spaghetti_chart": "Curvas de potência de ciclos sobrepostas", - "aria_sweep_chart": "Gráfico de resultados da varredura de parâmetros", "artifact_anomaly": "Anomalia", "artifact_high_power": "Potência alta", "artifact_interruption": "Interrupção", @@ -372,150 +361,107 @@ "cohesion_good": "coesão {pct}%", "cohesion_low": "⚠ coesão baixa {pct}%", "compare_profiles": "Comparar com perfis", - "concurrency": "Simultaneidade", "confidence": "Confiança", "confidence_threshold": "Limite de confiança", "correct_duration": "Duração corrigida (min, opcional)", "correct_profile": "Perfil correto", - "correlation": "Correlação", "cost": "Custo", "create_new_profile": "+ Criar novo perfil…", "current_phase": "Fase atual", - "curves": "Curvas", "cycle": "Ciclo", "cycle_date_display": "Exibição da data do ciclo", - "cycle_health": "Saúde do ciclo", - "cycle_index": "índice do ciclo →", - "cycles": "Ciclos", + "cycle_context": "Histórico do recorder em torno do ciclo", + "cycle_context_off": "Desligado", + "cycle_context_minutes": "{min} min", + "cycle_context_series": "Histórico do recorder", + "before_start": "Antes do início", + "after_end": "Depois do fim", "cycles_count": "Ciclos", "cycles_lc": "ciclos", - "dashed": "tracejada", "date": "Data", "debug_traces": "Rastros de depuração", - "default": "Padrão", "default_access_level": "Nível padrão para usuários não listados abaixo", "default_other": "Padrão (outros dispositivos)", "default_tab": "Aba padrão ao abrir o painel", - "delta": "Δ", "description": "Descrição", - "dtw_blend_w": "Peso de mistura", - "dtw_blended": "Pontuação combinada", - "dtw_ddtw": "Pontuação DDTW", - "dtw_l1": "Pontuação L1", - "dtw_matched_profile": "(perfil correspondente do ciclo)", "duration": "Duração", - "duration_agreement": "Concordância de duração", - "emphasis": "Destaque", "enable_access_control": "Ativar controle de acesso por usuário", "energy": "Energia", - "energy_agreement": "Concordância de energia", - "event_timeline": "Linha do tempo de eventos", "event_type": "Tipo de evento", "expected": "Esperado", + "suspected_profile": "{name} (provável)", + "runner_up_profile": "{name} (segundo candidato)", "expected_duration": "Duração esperada (min)", "file_kb": "Arquivo (kB)", - "final_score": "Pontuação final", "from": "De", "gap_s": "Intervalo (s)", "group_name": "Nome do grupo", "head_trim": "Recorte inicial (s)", - "health": "Saúde", "hide_tabs": "Ocultar abas para não administradores", - "in_use": "Em uso", "json_data": "Dados JSON", "label": "Programa correspondente", "last": "Últimos", "last_checked": "Última verificação:", "load_from_file": "Carregar de arquivo", - "mae_score": "Pontuação MAE", "manual_duration": "Duração manual (min, opcional)", "match": "Correspondência", "members": "Membros", - "metric": "Métrica", "mode_existing_profile": "Adicionar a perfil existente", "mode_new_profile": "Criar novo perfil", "models_fine_tuned": "(modelos ajustados: {count})", - "n_classic_suggestions": "{n} clássico", - "n_ml_suggestions": "{n} ML", "n_selected": "{n} selecionados", - "n_tuning_suggestions": "{n} sugestão de ajuste", - "na": "n/d", + "n_tuning_suggestions": "{n} sugestões de ajuste", + "n_tuning_suggestions_one": "{n} sugestão de ajuste", + "n_tuning_suggestions_many": "{n} sugestões de ajuste", + "n_tuning_suggestions_other": "{n} sugestões de ajuste", "new_profile_name": "Nome do novo perfil", "notes": "Observações", - "of": "de", - "outcome": "Resultado", "panel_default_tab": "Aba padrão", "panel_language": "Idioma do painel", "per_cycle_short": "ciclo", "pg_anti_wrinkle": "Antiamasso", - "pg_autofill_tip": "Preencher automaticamente a faixa a partir do valor atual ({lo}–{hi})", "pg_best_value": "Melhor valor encontrado", "pg_current_value": "Valor atual:", "pg_drag_hint": "Arraste a linha no gráfico", - "pg_dtw": "DTW", "pg_ev_ending": "Encerrando", "pg_ev_running": "Em execução", "pg_group_advanced": "Casos extremos", "pg_group_detection": "Gatilhos de detecção", "pg_group_timing": "Regras de temporização", "pg_idle": "Ocioso", - "pg_last_run": "Última execução: {date}", - "pg_match_score": "Pontuação de correspondência", - "pg_pct_of": "{p}% de {total}", "pg_poor_match": "Correspondência ruim", "pg_preset": "Predefinição da Área de testes", "pg_preset_name": "Nome da nova predefinição", "pg_preset_none": "Selecione uma predefinição…", "pg_strong_match": "Correspondência forte", - "pg_tile_ambiguous": "Ambíguo", - "pg_tile_ambiguous_tip": "Ciclos com empate quase perfeito entre dois programas", - "pg_tile_detected": "Detectado", - "pg_tile_detected_tip": "Ciclos em que a potência ultrapassou o limite de início por tempo suficiente para registrar como em execução", - "pg_tile_matched": "Correspondido", - "pg_tile_matched_tip": "Ciclos atribuídos corretamente a um programa conhecido", - "pg_time_left": "Tempo restante", "pg_weak_match": "Correspondência fraca", "phase_name": "Nome da fase", "name": "Nome", "phase_timeline": "Linha do tempo de fases", "power": "Potência", "profile": "Perfil", - "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. Escolha um antes de ligar o eletrodoméstico e ele será aplicado assim que o próximo ciclo começar.", + "program_tip": "Escolha a qual programa o ciclo atual é associado. Detecção automática escolhe a melhor correspondência. Fixe um programa quando a detecção automática errar; se você escolher antes de iniciar, ele vale para o próximo ciclo.", "progress": "Progresso", "raw_socket": "Socket bruto", - "read_only": "Somente leitura", "ref_cycle": "Ciclo de referência (opcional)", - "refresh_interval": "Intervalo de atualização (s)", "remaining": "Restante", "reminder_every": "Lembrar a cada (ciclos)", "remove_label": "- Remover rótulo -", "rename_profile": "Renomear perfil", - "replay_duration": "Reprodução", "resulting_profile": "Perfil resultante", "save_mode": "Modo de salvamento", - "score": "Pontuação", - "score_breakdown": "Detalhamento da pontuação", "seconds_from_start": "Segundos desde o início", "select_profile": "Selecionar perfil", "setting": "Configuração", - "settings_a": "Configurações A (atuais)", - "settings_a_short": "A", "settings_advanced": "Avançado", - "settings_b": "Configurações B (teste)", - "settings_b_short": "B", "settings_basic": "Básico", "settings_detail_level": "Nível de detalhe das configurações", - "settings_overrides": "Substituições de configurações", "show_debug": "Mostrar cartão de depuração de correspondência ao vivo na página de Status (confiança, ambiguidade, principais candidatos)", "show_expected": "Mostrar sobreposição da curva esperada (perfil correspondente, laranja)", "show_raw": "Mostrar o interruptor de sinal bruto da tomada no gráfico de potência ao vivo", "sparkline": "Tendência recente da duração dos ciclos", - "stage2": "Etapa 2 – similaridade principal", - "stage3": "Etapa 3 – DTW", - "stage4": "Etapa 4 – concordância", "status": "Status", "tail_trim": "Recorte final (s)", "tap_to_dismiss": "toque no gráfico para fechar", @@ -526,7 +472,6 @@ "ungrouped": "Sem grupo", "unlabelled": "Sem rótulo", "unlabelled_paren": "(sem rótulo)", - "warp_path": "Caminho de alinhamento", "input": "Entrada:", "start": "Início", "end": "Fim", @@ -551,9 +496,8 @@ "n_shown": ", {n} mostrados", "cycles_title": "Ciclos ({n})", "pg_detecting": "Detectando", - "pg_best_marker": "Melhor: {v}", "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", + "pg_time_left_model_tip": "Estimado a partir das fases do programa, não uma contagem regressiva fixa", "phase": "Fase", "pg_mode_history": "Testar no histórico", "pg_ev_detected": "Detectado", @@ -576,9 +520,6 @@ "pg_alert_unmatched": "Sem correspondência", "pg_alert_ambiguous": "Correspondência ambígua", "pg_alert_energy": "Anomalia de energia", - "pg_alert_stress_cap": "Limite de segurança atingido", - "pg_alert_stress_ok": "Encerramento em repouso: ciclo parado", - "pg_alert_stress_warn": "Consumo em espera acima do limite de parada", "pg_newly_correct": "agora correto", "pg_regressed": "regrediu", "pg_end_timing_changed": "momento do fim alterado", @@ -588,12 +529,6 @@ "pg_sweep_param": "Parâmetro", "pg_objective": "Objetivo", "pg_steps": "Etapas", - "pg_stress_group": "Teste de encerramento em repouso", - "pg_stress_idle_auto": "Auto", - "pg_stress_idle_w": "Nível em espera (W)", - "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_obj_match": "Precisão de correspondência", "pg_obj_endtiming": "Precisão do momento de fim", "pg_obj_falseend": "Taxa de fins falsos", @@ -602,7 +537,6 @@ "pg_mode_optimize": "Otimizar", "pg_group_matching": "Correspondência de programa", "pg_zoom_hint": "clique duas vezes para redefinir o zoom", - "aria_playground_chart2": "Gráfico interativo de potência do ciclo: passe o cursor para ler tempo/potência, role para ampliar, arraste para deslocar", "pg_alert_timeout_end": "Encerrado por tempo limite, não por previsão", "pg_alert_indefinite": "Ficaria funcionando indefinidamente", "task_pg_history": "Testar no histórico", @@ -614,16 +548,9 @@ "pg_recent_runs": "Execuções recentes", "pg_load_run": "Recarregar esta execução", "pg_match_confidence": "Confiança da correspondência", - "pg_envelope_fit": "Ajuste ao envelope", - "pg_envelope_fit_tip": "O quanto o ciclo permanece dentro do envelope de potência salvo deste perfil, uma perspectiva diferente da confiança da correspondência.", "n_imported_note": ", {n} importados", "enable_online": "Ativar recursos online", - "store_brand": "Marca do eletrodoméstico", - "store_model": "Modelo do eletrodoméstico", "anonymous": "Anônimo", - "rate_quality": "Avaliar a qualidade", - "rate_n": "{n} estrelas", - "approved_only": "Apenas aprovados", "adopt_settings": "Também adotar configurações", "flags": "Sinalizadores", "include_phase_map": "Incluir mapa de fases", @@ -640,7 +567,6 @@ "cat_custom_phases": "Fases personalizadas", "cat_profile_groups": "Grupos de perfis", "cat_settings": "Configurações de detecção e reconhecimento", - "cat_matching_config": "Ajuste do reconhecimento", "cat_ml_models": "Modelos de ML", "cat_feedback": "Feedback e rótulos de revisão", "cat_suggestions": "Sugestões", @@ -662,7 +588,6 @@ "hist_csv_data": "Dados CSV", "hist_from_recorder": "Ou ler direto do Home Assistant", "hist_since": "Desde", - "days": "dias", "hist_keep": "Manter este ciclo", "hist_looks_complete": "completo", "peak_power_short": "Pico", @@ -682,7 +607,36 @@ "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" + "correct_total_cycles": "Corrigir o total", + "hist_reason_already_recorded": "se sobrepõe a um ciclo que o WashData já registrou", + "share_consent": "Confirmo que estes ciclos terminaram normalmente, sem interrupção", + "show_internal": "Mostrar configurações internas", + "show_internal_tip": "Configurações que o WashData ajusta para você (amostragem, watchdog, cadência de correspondência, limites). Exibidas quando este dispositivo tem um valor próprio, um conflito ou uma sugestão.", + "rate_n": "{n} estrelas", + "rate_quality": "Avaliar a qualidade", + "pg_autolabel": "Rotulagem automática", + "pg_label_review": "Solicita revisão", + "pg_ev_match_reverted": "Correspondência revertida", + "pg_ev_verified_pause": "Pausa verificada", + "n_samples": "{n} amostras", + "n_samples_one": "{n} amostra", + "n_samples_many": "{n} amostras", + "n_samples_other": "{n} amostras", + "n_cycles": "{n} ciclos", + "n_cycles_one": "{n} ciclo", + "n_cycles_many": "{n} ciclos", + "n_cycles_other": "{n} ciclos", + "energy_per_cycle": "{v}/ciclo", + "energy_total": "{v} no total", + "automation_off": "(desativada)", + "ref_cycle_none": "Nenhum", + "share_name": "Mostrar meu nome no que eu compartilho", + "cat_backfill_cycles": "Ciclos encontrados em um histórico de potência importado", + "pg_compare_profile": "Comparar com perfil", + "pg_obj_early": "Taxa de fins antecipados", + "pg_obj_endlag": "Atraso do fim (mediana)", + "pg_obj_split": "Taxa de divisões", + "pg_sweep_cycles": "Ciclos recentes" }, "log": { "all_levels": "Todos os níveis", @@ -692,11 +646,9 @@ }, "ml": { "builtin_models": "● Usando modelos integrados", - "fit_fair": "Aceitável", "fit_good": "Bom", "fit_slight": "Leve", "fit_strong": "Forte", - "fit_weak": "Fraco", "fit_word": "ajuste", "personalized": "● Personalizado para esta máquina", "better_than_baseline": "{pct}% melhor que a estimativa de referência ({metric})", @@ -704,30 +656,41 @@ "trend_declining_tip": "O ajuste deste modelo piorou ao longo das verificações recentes: revisar mais ciclos pode ajudá-lo a reaprender.", "trend_steady_tip": "O ajuste deste modelo se manteve praticamente estável ao longo das verificações recentes.", "cap_blurb": { - "end": "Saber quando um ciclo realmente terminou", - "live_match": "Identificar mais cedo o programa em execução", - "quality": "Detectar ciclos mal identificados ou corrompidos", - "remaining_time": "Prever quanto tempo falta", "total_energy": "Prever a energia total e o custo" }, "cap_label": { - "end": "Detecção de fim de ciclo", - "live_match": "Identificação de programa", - "quality": "Verificação de qualidade do ciclo", - "remaining_time": "Estimativa de tempo restante", "total_energy": "Estimativa de energia" }, - "metric_auc": "AUC {auc} em dados de validação", - "metric_mae": "erro {model} vs. linha de base {baseline}" + "metric_mae": "erro {model} vs. linha de base {baseline}", + "fine_tuned_at": "ajustado em {when}", + "held_out_count": "medido em {count} ciclos de validação", + "held_out_count_one": "medido em {count} ciclo de validação", + "held_out_count_many": "medido em {count} ciclos de validação", + "held_out_count_other": "medido em {count} ciclos de validação", + "last_run_not_promoted": "Última verificação ({when}): {reason}", + "never": "nunca", + "skip_reason": { + "fit_failed": "O treinamento falhou: {error}", + "holdout_too_small": "Poucos ciclos para testar um novo modelo de forma justa: {held_out} puderam ser separados para teste, são necessários {min}.", + "insufficient_rows": "Ainda não há ciclos utilizáveis suficientes: até agora, {count} ciclos rotulados e limpos.", + "insufficient_rows_one": "Ainda não há ciclos utilizáveis suficientes: até agora, {count} ciclo rotulado e limpo.", + "insufficient_rows_many": "Ainda não há ciclos utilizáveis suficientes: até agora, {count} ciclos rotulados e limpos.", + "insufficient_rows_other": "Ainda não há ciclos utilizáveis suficientes: até agora, {count} ciclos rotulados e limpos.", + "not_better_than_incumbent": "O novo modelo não era melhor do que o que está em uso (erro {model} vs. {incumbent}), então o atual foi mantido.", + "not_better_than_naive": "O novo modelo não era claramente melhor do que a estimativa simples (erro {model} vs. {naive}).", + "unknown": "Nada de novo foi aprendido." + }, + "trained_unknown": "em um momento desconhecido" }, "modal": { + "remove_task_title": "Remover tarefa", + "remove_task_msg": "Remover o lembrete \"{task}\"? As entradas dele no registro de manutenção são mantidas.", "auto_label": "Rotulagem automática de ciclos", "correct_feedback": "Corrigir feedback", "create_profile": "Criar perfil", "cycle": "Ciclo", "edit_group": "Editar grupo de perfis", "edit_phase": "Editar fase", - "import_config": "Importar configuração", "label_cycle": "Rotular ciclo", "merge_cycles": "Mesclar {n} ciclos", "new_group": "Novo grupo de perfis", @@ -742,6 +705,8 @@ "delete_automation_title": "Excluir automação", "delete_automation_msg": "Excluir a automação \"{name}\" do Home Assistant? Esta ação não pode ser desfeita.", "remove_legacy_title": "Remover ações antigas", + "reset_phase_msg": "Desfazer sua edição de \"{name}\" e restaurar a fase integrada? Os programas mantêm seus intervalos.", + "reset_phase_title": "Redefinir fase", "remove_legacy_msg": "Remover as ações personalizadas antigas? Elas deixarão de ser acionadas nos eventos de ciclo. Esta ação não pode ser desfeita a partir do painel.", "discard_recording_title": "Descartar a gravação", "discard_recording_msg": "Descartar a gravação salva? Esta ação não pode ser desfeita.", @@ -762,63 +727,79 @@ "store_share_device": "Compartilhar este dispositivo", "export_select": "Exportar - escolher dados", "import_wizard": "Importar - escolher dados", - "history_import": "Importar histórico de potência" + "history_import": "Importar histórico de potência", + "apply_suggestions_title": "Aplicar sugestões", + "apply_suggestions_msg": "Alterar estas {n} configurações? A integração é recarregada após salvar.", + "discard_settings_title": "Descartar alterações não salvas?", + "discard_settings_msg": "Alterações de configuração não salvas: {n}. Sair de Configurações e descartá-las?", + "profile_title": "Perfil · {name}", + "undo_import_msg": "Os dados e configurações deste dispositivo voltam ao estado anterior à última importação ({when}). Os ciclos e alterações posteriores são perdidos.", + "undo_import_ok": "Desfazer importação", + "undo_import_title": "Desfazer a última importação" }, "msg": { + "custom_task_limit": "Você atingiu o limite de {max} tarefas.", + "custom_tasks_intro": "Adicione qualquer outra coisa da qual você queira ser lembrado. Uma tarefa fica pendente quando qualquer um dos intervalos é atingido; deixe um em branco ou em 0 para usar apenas o outro.", + "maintenance_due_sensor_hint": "O sensor binário Manutenção pendente fica ligado enquanto houver alguma tarefa pendente, para suas próprias automações de notificação.", + "maintenance_supporter_intro": "O Maintenance Supporter é uma integração separada que faz muito mais: qualquer eletrodoméstico ou tarefa, prazos por tempo, uso ou um sensor, notificações e histórico. Ele pode usar o contador de ciclos do WashData.", "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.", + "pg_preset_delete_confirm": "Excluir a predefinição \"{name}\"?", + "pg_preset_limit": "Limite de predefinições atingido ({n})", + "pg_preset_overwrite": "Sobrescrever a predefinição \"{name}\"?", "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.", - "refresh_catalog_hint": "As listas de marcas e eletrodomésticos da comunidade ficam em cache para manter a loja compartilhada dentro do seu limite diário. Atualize para obter as entradas adicionadas ou aprovadas por outras pessoas.", + "store_sibling_hint": "Nada compartilhado para o seu modelo? Um modelo parecido da mesma marca costuma ser um bom começo.", + "store_declare_appliance": "Informe ao WashData qual eletrodoméstico você tem para ver as configurações que outros compartilharam para ele. Ou digite uma marca acima para explorar.", + "refresh_catalog_hint": "As listas de marcas e eletrodomésticos da comunidade ficam em cache para economizar a cota diária da loja. Atualize para ver as entradas que outros adicionaram ou aprovaram.", "head_trim_hint": "Número de segundos a remover do início", "appliance_monitor": "Monitor de eletrodoméstico", "artifact_dip_detail": "Caiu abaixo da banda de potência habitual por ~{n}s.", "artifact_footer": "Destacadas no gráfico acima. São artefatos transitórios (por exemplo, a porta foi aberta no meio do ciclo), não necessariamente problemas.", "artifact_header": "{n} anomalias detectadas durante este ciclo", - "artifact_pause_detail": "A potência caiu quase a zero por ~{n}s e depois retomou – provavelmente a porta foi aberta no meio do ciclo ou o ciclo foi pausado.", + "artifact_header_one": "{n} anomalia detectada durante este ciclo", + "artifact_header_many": "{n} anomalias detectadas durante este ciclo", + "artifact_header_other": "{n} anomalias detectadas durante este ciclo", + "artifact_pause_detail": "A potência caiu quase a zero por ~{n}s e depois voltou. Provavelmente a porta foi aberta ou o ciclo foi pausado.", "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 depois remova o rótulo deles (permanecem no histórico, mas deixam de influenciar este perfil) ou exclua-os de vez.", + "cleanup_intro": "Todos os ciclos rotulados sobrepostos. Marque os valores discrepantes e depois remova o rótulo deles (continuam no histórico, mas deixam de moldar este perfil) ou exclua-os.", "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)", - "compare_profiles_tip": "Sobreponha outros envelopes de perfil no gráfico acima para ver qual deles melhor se adapta a este ciclo.", + "compare_profiles_tip": "Sobreponha outros perfis no gráfico para ver qual se adapta melhor a este ciclo.", "compare_selected_cycles": "Ciclos selecionados (sólido) – mostrar / ocultar", - "consider_new_profile": "Considere criar um novo perfil.", - "coverage_gap": "os ciclos recentes não têm perfil correspondente ({pct}% dos últimos 30).", - "coverage_gap_similar_cycles": "{count} ciclos similares sem rótulo encontrados – crie um perfil para começar a correspondê-los.", "cycles_deleted": "{count} ciclo(s) excluído(s)", + "cycle_context_loading": "Carregando o histórico do recorder…", + "cycle_context_note": "Cinza: o histórico do recorder do sensor de potência em torno deste ciclo. Ele não faz parte do ciclo e não afeta a duração, a energia nem a correspondência.", + "cycle_context_none": "O recorder não tem leituras em torno deste ciclo (o Home Assistant mantém 10 dias de histórico por padrão).", + "cycle_context_tip": "Apenas exibição: mostra o histórico do recorder do sensor de potência em torno deste ciclo, se ainda existir. Para adicionar tentativas de partida com falha a ciclos futuros, use Pré-início da curva (Configurações > Detecção > Início do Ciclo), que altera as durações salvas.", "enough_data": "Dados suficientes para aprender ({current}/{min} ciclos).", "export_description": "Escolha exatamente quais perfis, ciclos, configurações e mais exportar para JSON, ou analise um arquivo e importe apenas as partes que quiser.", "feedback_cycles_pending": "Para revisar: {n}", "feedback_prompt": "Confirme que estava correto, corrija o programa ou ignore.", "feedback_relabel_hint": "Rotular novamente este ciclo também o resolve.", "filter_by_profile": "Filtrar por perfil…", - "group_modal_help": "Agrupe programas com o mesmo formato que diferem em temperatura/centrifugação (as durações podem variar). A correspondência avalia o grupo como um candidato e depois escolhe o membro mais adequado. Escolha pelo menos 2; a sobreposição mostra o grau de semelhança.", + "group_modal_help": "Agrupe programas com o mesmo formato que diferem em temperatura ou centrifugação. A correspondência pontua o grupo como um só e depois escolhe o membro que melhor se ajusta. Escolha pelo menos 2; a sobreposição mostra o quanto são parecidos.", "group_not_cohesive": "Esses perfis não são similares o suficiente para serem agrupados de forma confiável, então a correspondência os trata individualmente até que você remova o valor discrepante ou divida o grupo.", "group_preview_hint": "Selecione 2 ou mais membros para visualizar e comparar suas curvas de potência.", - "import_intro": "Carregue um arquivo exportado ou cole um payload JSON abaixo.", - "legacy_actions_title": "{count} ação personalizada legada ainda em execução", + "legacy_actions_title": "{count} ações personalizadas legadas ainda em execução", + "legacy_actions_title_one": "{count} ação personalizada legada ainda em execução", + "legacy_actions_title_many": "{count} ações personalizadas legadas ainda em execução", + "legacy_actions_title_other": "{count} ações personalizadas legadas ainda em execução", "live_chart_loading": "O gráfico de potência ao vivo aparece à medida que chegam leituras.", "loading": "Carregando…", "loading_curve": "Carregando curva…", "loading_settings": "Carregando configurações…", "log_buffer_hint": "Mais recente primeiro · armazena em buffer os últimos 500 registros ha_washdata desde a reinicialização · arraste a borda inferior para redimensionar.", - "maintenance_advisory": "O aumento da duração/energia pode indicar a necessidade de manutenção do eletrodoméstico (por exemplo, descalcificação, limpeza do filtro).", "maintenance_due": "Manutenção pendente: {items}", - "maintenance_intro": "Registre as manutenções que você realiza neste eletrodoméstico e receba lembretes quando cada tarefa vencer novamente.", + "maintenance_intro": "Registre as manutenções deste eletrodoméstico e receba um lembrete quando cada tarefa vencer novamente.", "maintenance_load_error": "Não foi possível carregar os dados de manutenção: {error}", "maintenance_requires_access": "A manutenção e a exportação/importação requerem acesso total.", - "manual_duration_hint": "A duração média/esperada do ciclo do perfil, usada para estimativas de tempo restante. Edite para definir; deixá-la inalterada mantém o valor atual.", - "matching_tuning_intro": "Durante o aprendizado, o WashData também ajusta o peso dado ao formato em relação à duração e energia na correspondência de programas.", + "manual_duration_hint": "A duração esperada do ciclo do perfil, usada nas estimativas de tempo restante. Deixe inalterada para manter o valor atual.", "merge_intro": "Os ciclos selecionados são mesclados em um só (em ordem cronológica; as lacunas são preenchidas com 0 W). Escolha o perfil resultante.", "ml_intro": "O WashData vem com modelos inteligentes que funcionam imediatamente.", "ml_learned_intro": "Modelos ajustados a esta máquina.", - "ml_loading": "carregando ML…", "ml_settings_intro": "Dois botões independentes: um aplica os modelos durante um ciclo, o outro permite ao WashData ajustá-los à sua máquina ao longo do tempo.", - "name_first_program": "Você já tem ciclos suficientes – dê um nome ao seu primeiro programa para começar a correspondência.", - "near_duplicate_cluster": "cluster de perfis quase duplicados detectado. O agrupamento permite que a correspondência escolha de forma confiável entre perfis semelhantes (por exemplo, o mesmo programa em temperatura/centrifugação diferente).", "no_cycles_match": "Nenhum ciclo corresponde ao filtro atual.", "no_cycles_profile": "Nenhum ciclo para este perfil.", "no_cycles_selected": "Selecione pelo menos um ciclo primeiro.", @@ -843,73 +824,54 @@ "no_suggestions": "Nenhuma sugestão ativa.", "notify_services_hint": "Use os IDs de serviço de {entity} (separados por vírgula para múltiplos). Variáveis de template: {vars}.", "old_actions_warning": "Configurado com o antigo editor de ações (agora removido). Ainda são acionadas em eventos de ciclo, mas não podem mais ser editadas aqui. Converta-as em uma automação normal ou remova-as.", - "onboarding_progress": "{n} / 3 ciclos observados", - "onboarding_watching": "Use o eletrodoméstico normalmente – o WashData está observando. Após 3 ciclos, a correspondência de programas começará.", "pending_feedback": "Feedback de detecção pendente", "performance_trend": "Tendência de desempenho ({n} ciclos)", - "pg_analysis_empty": "Carregue um ciclo para ver a análise de correspondência.", "pg_apply_confirm": "Aplicar o melhor valor: {label} = {value}?", - "pg_canvas_empty": "Selecione um ciclo acima e pressione Carregar para ver o rastro de potência.", - "pg_chart_caption": "Taxa de detecção/correspondência ao longo dos valores de {param}", "pg_ctrl_intro": "Os valores partem das configurações ativas deste dispositivo na integração. As edições ficam na Área de testes até você salvá-las na integração.", "pg_live_unavailable": "Configurações ativas indisponíveis, exibindo os valores padrão.", "pg_load_live_confirm": "Descartar {n} edição(ões) da Área de testes e recarregar as configurações atuais da integração?", - "pg_match_rate": "{pct}% de taxa de correspondência em {total} ciclos", "pg_matches_live": "Corresponde às configurações ativas", "pg_n_changed": "{n} valor(es) diferente(s) das configurações ativas", "pg_sugg_none": "Todas as sugestões já correspondem às configurações atuais", - "pg_preset_delete_confirm": "Excluir a predefinição \"{name}\"?", - "pg_preset_limit": "Limite de predefinições atingido ({n})", - "pg_preset_overwrite": "Sobrescrever a predefinição \"{name}\"?", "pg_publish_one_confirm": "Salvar {label} = {value} nas configurações deste dispositivo?", "pg_restart_note": "Reinicie o Home Assistant para ativar as ferramentas de simulação.", - "pg_sim_progress": "{done} / {total} ciclos", - "pg_starting": "Iniciando…", - "pg_stress_synthetic": "a simulação de espera começa aqui", - "pg_sweep_intro": "E se {param} fosse diferente? Teste {steps} valores nos seus últimos {cycles} ciclos para encontrar a configuração em que o maior número de ciclos é correspondido corretamente.", - "pg_sweep_step": "Etapa {done} / {total}", - "pg_verdict_bad": "Requer atenção: muitos ciclos estão passando sem detecção.", - "pg_verdict_good": "Bem ajustado: a maioria dos ciclos é identificada e correspondida corretamente.", - "pg_verdict_ok": "Aceitável: alguns ciclos não foram detectados. Tente reduzir o limite de início.", "phase_catalog_intro": "Segmentos nomeados de um ciclo (Pré-lavagem, Aquecimento, Centrifugação…). Atribua-os a um perfil a partir do seu painel de controle.", "phase_ranges_intro": "Intervalos de fase (minutos desde o início do ciclo) sobrepostos na curva média. Edite os valores para pré-visualizar em tempo real.", - "playground_intro": "Um ambiente seguro para reproduzir ciclos anteriores com configurações diferentes, comparar duas configurações lado a lado e ver como um ciclo se alinha a um perfil. Nada aqui altera os seus dados ativos até que você o aplique explicitamente.", - "playground_needs_restart": "Reinicie o Home Assistant para ativar as ferramentas da Área de testes.", - "preferences_admin": "Preferências, painel e controle de acesso", - "preferences_adv": "Preferências", + "playground_intro": "Teste configurações nos dados reais dos seus ciclos. Nada aqui altera suas configurações ativas até você aplicar.", "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), 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.", + "process_history_hint": "Executa novamente a correspondência em todos os ciclos salvos, atualiza as sugestões, retreina os modelos ML (se ativados), recalcula o custo com os preços registrados e recalcula a saúde dos ciclos. Execute após uma série 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.", - "rbac_hint": "Quando desativado, todos os usuários do Home Assistant têm acesso total (padrão). Os administradores sempre têm acesso total e podem gerenciar todos.", - "recent_logs": "Registros ha_washdata recentes", + "rbac_hint": "Desligado: todo usuário do Home Assistant tem acesso total (padrão). Os administradores sempre têm acesso total e podem gerenciar todos.", "recording_in_progress": "Gravação em andamento", "reminders_intro": "Mostrar um lembrete no painel este número de ciclos após a última manutenção. Deixe em branco ou 0 para desativar um lembrete.", "restart_gap_footer": "Destacado no gráfico. Dados de potência estão ausentes para esses intervalos; o reconhecimento usou apenas leituras reais.", - "restart_gap_header": "{n} lacuna de reinicialização do HA durante este ciclo", + "restart_gap_header": "{n} lacunas de reinicialização do HA durante este ciclo", + "restart_gap_header_one": "{n} lacuna de reinicialização do HA durante este ciclo", + "restart_gap_header_many": "{n} lacunas de reinicialização do HA durante este ciclo", + "restart_gap_header_other": "{n} lacunas de reinicialização do HA durante este ciclo", "restart_gap_item": "{dur} lacuna", - "review_confirm_help": "Confirme se este ciclo foi detectado corretamente. Suas revisões treinam o modelo na sua máquina – quanto mais ciclos confirmar, melhor ficam a correspondência e a pontuação de saúde. Um Bom/Ruim rápido é suficiente.", + "review_confirm_help": "Este ciclo foi detectado corretamente? Confirmar ciclos melhora a correspondência para a sua máquina. Um rápido Bom/Ruim é suficiente.", "review_in_settings": "Revisar em Configurações", "review_notes_placeholder": "Observações (opcional)", "review_notes_tip": "Notas em texto livre para sua própria referência. Não são usadas pela correspondência ou pelo treinamento.", - "review_profile_tip": "O programa com que este ciclo está rotulado. Se o programa detectado automaticamente estava errado, corrija-o aqui – a rotulagem ensina a correspondência para ciclos futuros.", - "review_quality_tip": "A qualidade deste ciclo. Bom = um exemplo perfeito deste programa; Ruim = detectado mas ruidoso ou atípico; Inutilizável = detectado incorretamente (mesclado, truncado ou espúrio). Determina a pontuação de saúde e quais ciclos são permitidos para treinar o modelo.", - "review_recorded_tip": "Marque este ciclo como referência escolhida a dedo para seu programa – o mesmo papel de um ciclo gravado manualmente. Os ciclos de referência são sempre mantidos, alimentam o modelo de correspondência e nunca são removidos pela limpeza. (Esta é a bandeira \"dourada\"/gravada; ambas são a mesma coisa.)", - "review_tags_tip": "Etiquetas opcionais que descrevem o que deu errado com este ciclo, para que o treinamento e a limpeza possam levar isso em conta.", + "review_profile_tip": "O programa com que este ciclo está rotulado. Se estiver errado, corrija aqui; os rótulos ensinam a correspondência.", + "review_quality_tip": "O quanto este ciclo é limpo. Bom: um exemplo clássico deste programa. Ruim: detectado, mas ruidoso ou incomum. Inutilizável: detectado errado (mesclado, cortado ou espúrio). Afeta a pontuação de saúde e quais ciclos treinam o modelo.", + "review_recorded_tip": "Marque este ciclo como referência escolhida a dedo para seu programa, como um ciclo gravado manualmente. Ciclos de referência são sempre mantidos, servem de base ao modelo de correspondência e nunca são removidos pela limpeza.", + "review_tags_tip": "Marcações opcionais sobre o que deu errado, para que o treinamento e a limpeza levem isso em conta.", "review_to_cycles": "Abrir a fila de revisão de Ciclos", "samples_decimated": "Mostrando {shown} de {total} amostras (reduzidas para exibição; picos mantidos). Um intervalo grande aqui se deve à redução, não a dados faltantes.", - "saving_triggers_reload": "Salvar aciona um recarregamento da integração. As entidades do HA podem ficar brevemente indisponíveis.", + "saving_triggers_reload": "Salvar recarrega a integração. As entidades podem aparecer brevemente como indisponíveis.", "search_placeholder": "Pesquisar configurações…", "see_recorder": "Veja o widget de gravação abaixo", "setting_changed": "Alterado de {old} para {new} em {date}", "settings_basic_note": "Mostrando as configurações essenciais. Mude para Avançado para ver a lista completa.", - "shape_drift_advisory": "⚠ Desvio de forma", - "shape_drift_detail": "O padrão de potência para este perfil mudou ao longo do tempo – possível desgaste do eletrodoméstico ou manutenção necessária (por exemplo, descalcificação, limpeza do filtro).", "show_all_settings": "Mostrar todas as configurações", - "showing_suggestions": "Mostrando a configuração {count} com sugestões.", - "split_intro": "Clique no gráfico para adicionar ou remover um ponto de divisão, ou detecte automaticamente por intervalos de inatividade. Cada segmento resultante pode ter seu próprio perfil.", - "storage_diagnostics": "Estatísticas de armazenamento, manutenção, exportação/importação", + "showing_suggestions": "Mostrando {count} configurações com sugestões.", + "showing_suggestions_one": "Mostrando {count} configuração com sugestões.", + "showing_suggestions_many": "Mostrando {count} configurações com sugestões.", + "showing_suggestions_other": "Mostrando {count} configurações com sugestões.", + "split_intro": "Clique no gráfico para adicionar ou remover um ponto de divisão, ou detecte automaticamente por intervalos de inatividade. Cada parte pode ter o seu próprio perfil.", "sug_staged": "Definido {key} = {val}. Salve para aplicar.", "toast_auto_detect_enabled": "Detecção automática ativada", "toast_auto_label_complete": "Rotulagem automática concluída", @@ -926,7 +888,6 @@ "toast_no_automation": "Não é possível criar automação aqui", "toast_profile_name_required": "O nome do perfil é obrigatório", "toast_program_set": "Programa definido: {program}", - "toast_rebuild_failed": "Reconstrução falhou: {error}", "toast_resume_failed": "Retomada falhou: {error}", "toast_revert_failed": "Reversão falhou: {error}", "toast_reverted": "{key} revertido", @@ -937,97 +898,82 @@ "trend_energy_up": "Energia com tendência crescente ({pct}%/ciclo) – média recente {avg}", "trim_destructive_confirm": "Isso mantém apenas {pct}% do ciclo e esta ação não pode ser desfeita. Continuar?", "trim_intro": "Arraste as alças vermelhas ou insira valores. Tudo fora da janela é removido.", - "tuning_suggestions_available": "{count} sugestão de ajuste disponível a partir dos ciclos observados. Aparecem ao lado dos campos relevantes.", + "tuning_suggestions_available": "{count} sugestões de ajuste disponíveis a partir dos ciclos observados. Aparecem ao lado dos campos relevantes.", + "tuning_suggestions_available_one": "{count} sugestão de ajuste disponível a partir dos ciclos observados. Aparece ao lado do campo relevante.", + "tuning_suggestions_available_many": "{count} sugestões de ajuste disponíveis a partir dos ciclos observados. Aparecem ao lado dos campos relevantes.", + "tuning_suggestions_available_other": "{count} sugestões de ajuste disponíveis a partir dos ciclos observados. Aparecem ao lado dos campos relevantes.", "unsure_detected_prefix": "O WashData não tem certeza se detectou", - "updated": "Atualizado", "warmup_badge": "Ainda aprendendo ({done}/{needed} ciclos)", - "warmup_detail": "Este perfil precisa de {needed} ciclos rotulados antes de a correspondência automática começar. Cada ciclo confirmado ajuda-o a aprender.", + "warmup_detail": "A correspondência já funciona. O WashData pede que você confirme os primeiros {needed} ciclos deste perfil antes de rotular sozinho.", "wipe_history_warning": "Excluir permanentemente todos os ciclos e perfis. Não pode ser desfeito.", "compare_overlay_tip": "Sobreponha os envelopes dos perfis aprendidos para ver a que programa cada ciclo se assemelha.", - "duration_trend_up_tip": "Duração aumentando ({pct}%/ciclo)", - "duration_trend_down_tip": "Duração diminuindo ({pct}%/ciclo)", - "energy_trend_up_tip": "Energia aumentando ({pct}%/ciclo)", - "energy_trend_down_tip": "Energia diminuindo ({pct}%/ciclo)", - "performance_trending": "Tendência de desempenho", - "pg_select_cycle_hint": "Selecione um ciclo e clique em ↺ para carregar", - "pg_sweep_progress": "Passo {done} / {total}", - "pg_analysis_hint": "Selecione um ciclo e clique em ↺ para carregar a análise.", "diagnostics_load_failed": "Não foi possível carregar os diagnósticos: {error}", - "manual_duration_ref_hint": "Usado apenas quando nenhum ciclo de referência está selecionado. Um ciclo de referência define a duração a partir do seu próprio comprimento.", + "manual_duration_ref_hint": "Usado apenas sem ciclo de referência; um ciclo de referência define a duração pelo seu próprio comprimento.", + "duration_learned_hint": "Calculada a partir dos ciclos deste programa (sem valores discrepantes) e atualizada após cada novo ciclo. Para mudá-la, recorte ou exclua os ciclos que destoam.", "toast_convert_rolled_back": "A conversão falhou e foi revertida (nenhuma automação foi deixada): {error}", "toast_convert_orphan": "A automação foi criada, mas a limpeza das ações antigas falhou. Não tente novamente: remova as ações antigas manualmente para evitar uma automação duplicada.", - "advisory_duration_trend_up": "Os ciclos de '{name}' estão ficando cada vez mais longos (cerca de +{pct}% por ciclo). Se o comportamento do aparelho mudou, regrave ou reconstrua este perfil.", - "advisory_energy_trend_up": "'{name}' está consumindo cada vez mais energia (cerca de +{pct}% por ciclo) - vale a pena verificar o aparelho se isso for inesperado.", - "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.", + "advisory_duration_trend_up": "Os ciclos de '{name}' estão ficando cada vez mais longos (cerca de +{pct}% por ciclo). Se o aparelho mudou, grave novamente ou reconstrua este perfil.", + "advisory_energy_trend_up": "'{name}' consome mais energia a cada ciclo (cerca de +{pct}% por ciclo). Verifique o aparelho se isso for inesperado.", + "advisory_poor_health": "'{name}' se ajusta mal: seus ciclos recentes variam muito ou têm correspondência fraca. Revise os ciclos ou grave-o novamente para manter a correspondência e as estimativas precisas.", + "advisory_shape_drift": "'{name}' se afastou da sua curva de potência original, talvez por calcário ou desgaste. Considere gravá-lo novamente com ciclos recentes.", + "advisory_shape_drift_corr": "'{name}' se afastou da sua curva de potência original (correlação {corr}), talvez por calcário ou desgaste. Considere gravá-lo novamente com ciclos recentes.", + "advisory_unmatchable": "'{name}' nunca poderá ter correspondência: não há nenhum ciclo com dados de potência por trás dele. Rotule ou grave um ciclo deste programa para resolver.", + "advisory_unmatchable_no_duration": "'{name}' nunca poderá ter correspondência: o ciclo dele não tem uma duração utilizável. Grave este programa novamente ou defina a duração esperada.", "pg_no_alerts": "Nenhum problema detectado nesta execução.", "pg_override_active": "Usando suas configurações editadas vs. as atuais", "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.", "pg_sweep_intro2": "Encontre a configuração que melhor cumpre um objetivo nos seus ciclos recentes. Nada muda até você aplicá-la.", - "pg_running_history": "Reproduzindo seus ciclos…", - "pg_running_sweep": "Testando valores em todos os seus ciclos…", "pg_row_load_hint": "Carregar este ciclo no gráfico acima", - "pg_history_intro2": "Reproduza seus ciclos recentes pelo detector e pelo sistema de correspondência reais com as configurações acima. Clique em qualquer linha para carregar esse ciclo no gráfico; edite uma configuração para ver uma comparação antes/depois.", + "pg_history_intro2": "Reproduza ciclos recentes pelo detector e pelo comparador reais com as configurações acima. Clique em uma linha para carregá-la no gráfico; altere uma configuração para comparar antes e depois.", "pg_simulating": "Simulando o ciclo…", - "pg_canvas_empty2": "Escolha um ciclo acima e pressione Executar para simulá-lo. Depois passe o cursor para ler os valores, role para ampliar e arraste para deslocar.", "pg_apply_settings_confirm": "Salvar estas {n} configuração(ões) neste dispositivo? {list}", - "pg_stress_terminated_detail": "Estabilizou em ~{idle}W em espera -- o ciclo terminou {h}h {m}m depois via {reason}.", - "pg_stress_above_threshold_detail": "O consumo em espera ~{idle}W é igual ou superior ao limite de parada efetivo ({stop}W) -- o ciclo nunca foi considerado inativo. Aumente stop_threshold_w para corrigir.", - "pg_stress_hit_cap_detail": "O ciclo durou {h}h {m}m sem parar -- interrompido à força pelo limite de segurança. Consumo em espera {idle}W em relação ao limite de parada {stop}W.", "no_logs_match": "Nenhum registro corresponde a estes filtros.", "pg_analysis_hint2": "Selecione um ciclo e pressione Executar para carregar a análise de correspondência.", - "pg_analysis_empty2": "Pressione Executar para ver a análise de correspondência.", - "imported_readonly": "Importado da loja da comunidade. Exibido para referência e correspondência. Não é contabilizado nas suas estatísticas e não pode ser editado.", + "imported_readonly": "Importado da loja da comunidade. Usado como referência e para correspondência, não conta nas suas estatísticas e não pode ser editado.", "merge_no_imports": "As gravações importadas não podem ser mescladas em um ciclo real. Desmarque-as para mesclar.", - "store_enable_hint": "Ative os recursos online em Configurações para navegar e importar ciclos de referência da comunidade.", - "online_intro": "Compartilhe e baixe ciclos de referência com outros usuários do WashData. Todos os recursos online são opcionais e ficam desativados por padrão.", + "store_enable_hint": "Ative os recursos online em Preferências > Online e comunidade para navegar e importar ciclos de referência da comunidade.", "store_share_intro": "Envie este ciclo de referência para que outras pessoas com o mesmo eletrodoméstico possam usá-lo. Ele é revisado antes de aparecer publicamente.", - "online_intro_global": "Navegue e compartilhe gravações de referência com outros usuários do WashData e confirme entradas de eletrodomésticos. Uma única conexão se aplica a toda a sua integração WashData; a marca e o modelo do eletrodoméstico são definidos por dispositivo em Básico. Todos os recursos online são opcionais e ficam desativados por padrão.", + "online_intro_global": "Navegue e compartilhe gravações de referência com outros usuários do WashData. Uma conexão serve para todos os seus dispositivos WashData; marca e modelo são definidos por dispositivo em Básico. Os recursos online ficam desligados até você ativá-los.", "online_unavailable": "Os recursos online não estão disponíveis neste servidor.", "store_picker_offline": "Ative os recursos online no ícone de configurações para escolher no catálogo da comunidade.", "pick_brand_first": "Escolha primeiro uma marca de eletrodoméstico.", - "model_not_found": "Ainda não está no catálogo.", - "connect_to_confirm": "Conecte-se no ícone de configurações para confirmar ou avaliar.", "adopt_settings_hint": "Substituir os limites de detecção e reconhecimento deste dispositivo pelos compartilhados. Suas notificações, entidades e preço de energia nunca são alterados.", - "include_settings_hint": "Compartilhe os limites de detecção e reconhecimento deste dispositivo (não suas notificações, entidades ou preço de energia). Os adotantes escolhem se desejam aplicá-los.", + "include_settings_hint": "Compartilhe os limites de detecção e correspondência deste dispositivo (não as notificações, entidades ou preço da energia). Os outros escolhem se vão aplicá-los.", "onboard_download": "Novo dispositivo? Adote uma configuração pronta (programas, ciclos de referência e fases) de outro usuário do WashData com o mesmo eletrodoméstico.", - "share_consent": "Confirmo que estes ciclos foram concluídos normalmente, sem interrupções", "share_device_none": "Nenhum ciclo compartilhável ainda. Marque um ciclo gravado ou selecionado como ciclo de referência (⭐) na aba Ciclos primeiro.", "share_guideline_naming": "Nomeie cada perfil exatamente como aparece no mostrador ou display do eletrodoméstico (ex.: 'Cotton 40', 'Eco 60').", - "share_guideline_quality": "Compartilhe apenas ciclos concluídos normalmente -- sem interrupções no meio do ciclo, abertura de porta ou falhas de energia.", + "share_guideline_quality": "Compartilhe apenas ciclos concluídos normalmente: sem interrupções, abertura de porta ou falhas de energia.", "share_guideline_review": "Seu envio começa como pendente e aparece publicamente assim que membros suficientes da comunidade o confirmarem.", "share_guidelines_title": "Antes de compartilhar", - "store_download_device_intro": "Adote todos os programas compartilhados e seus ciclos de referência neste dispositivo. Seus próprios ciclos gravados e estatísticas não são afetados.", + "store_download_device_intro": "Adicionar todos os programas compartilhados e seus ciclos de referência ao seu dispositivo. Seus próprios ciclos e estatísticas não são afetados.", "store_share_device_intro": "Envie {brand} {model} com os ciclos de referência que você selecionar. Outros usuários com o mesmo eletrodoméstico podem adotar seus programas. As entradas são revisadas antes de aparecerem publicamente.", "share_profile_no_cycles": "Nenhum ciclo de referência -- marque um ciclo como ⭐ na aba Ciclos para incluir este perfil", - "advisory_phase_inconsistent": "'{name}' parece misturar diferentes programas ou temperaturas - seus ciclos aquecem por períodos de tempo muito diferentes. Dividi-lo em perfis separados (por ex. por temperatura) melhorará a correspondência e as estimativas de tempo.", - "advisory_phase_inconsistent_title": "⚠ Possíveis programas misturados", - "export_select_intro": "Marque exatamente o que incluir. Selecionar perfis sem os ciclos deles ainda exporta um programa reconhecível (a forma aprendida vai junto).", - "import_analyze_hint": "Carregue um arquivo exportado (ou cole o JSON dele). O WashData o analisa e mostra exatamente o que pode ser importado antes de qualquer alteração.", + "export_select_intro": "Marque o que incluir. Perfis sem seus ciclos ainda exportam um programa reconhecível.", + "import_analyze_hint": "Carregue um arquivo exportado (ou cole o JSON dele). O WashData mostra o que pode ser importado antes de qualquer alteração.", "analyzing": "Analisando…", - "device_type_mismatch_warn": "Esta exportação é de um tipo de eletrodoméstico diferente ({src} em vez de {local}). Os programas e ciclos ainda podem ser importados como dados de referência, mas as configurações específicas do dispositivo e a importação para o histórico real estão desativadas.", + "device_type_mismatch_warn": "Esta exportação é de um tipo de eletrodoméstico diferente ({src} em vez de {local}). Os programas e ciclos ainda podem ser importados como dados de referência, mas não as configurações do dispositivo nem o histórico real.", "merge_hint": "Os itens importados são adicionados; nada local é perdido. Os conflitos de nomes são resolvidos abaixo.", "replace_warn": "Cada categoria marcada é apagada e substituída pelo conteúdo do arquivo. As categorias não marcadas ficam intactas.", "dest_reference_hint": "Os ciclos importados só melhoram o reconhecimento de programas e nunca afetam as estatísticas de uso/energia.", - "dest_real_history_hint": "Os ciclos importados contam como histórico próprio deste dispositivo e alimentam as estatísticas de energia/uso. Use para mover um eletrodoméstico para uma nova instalação.", + "dest_real_history_hint": "Os ciclos importados contam como histórico e estatísticas próprios deste dispositivo. Use para mover um eletrodoméstico para uma nova instalação.", "sug_muted": "Não vai sugerir esta configuração novamente", "sug_mute_failed": "Não foi possível silenciar a sugestão", "sug_unmuted_all": "Sugestões silenciadas redefinidas", - "n_suggestions_muted": "{count} silenciadas; o ajuste automático não irá propô-las.", + "n_suggestions_muted": "{count} sugestões silenciadas; o ajuste automático não irá propô-las.", + "n_suggestions_muted_one": "{count} sugestão silenciada; o ajuste automático não irá propô-la.", + "n_suggestions_muted_many": "{count} sugestões silenciadas; o ajuste automático não irá propô-las.", + "n_suggestions_muted_other": "{count} sugestões silenciadas; o ajuste automático não irá propô-las.", "font_size_hint": "Amplie ou reduza o texto neste painel. Aplica-se à sua conta neste dispositivo.", - "import_history_description": "Você já tinha uma tomada inteligente antes do WashData? Carregue uma exportação do histórico do sensor de potência dela, ou leia direto do Home Assistant: a detecção normal é executada sobre esses dados, então os ciclos antigos aparecem na sua lista de Ciclos, prontos para nomear.", - "hist_input_hint": "Carregue um CSV baixado do painel Histórico (entidade, estado, última alteração), ou deixe o WashData ler o histórico do sensor diretamente. A detecção é executada em seguida exatamente como em tempo real, e você escolhe quais dos ciclos encontrados quer manter.", - "hist_recorder_hint": "Lê desde a data que você escolher até agora. O Home Assistant mantém o histórico detalhado por 10 dias por padrão e, depois disso, apenas médias por hora, grosseiras demais para detectar ciclos - escolha uma data mais antiga somente se o recorder do Home Assistant estiver configurado para guardar mais.", - "hist_scanning": "Seu histórico está sendo reproduzido no detector. Isso é executado em segundo plano: você pode fechar esta janela e voltar depois.", + "import_history_description": "Você já tinha uma tomada inteligente antes do WashData? Envie o histórico de potência dela ou leia-o do Home Assistant. A detecção percorre o histórico e os ciclos passados aparecem na sua lista de Ciclos, prontos para receber nome.", + "hist_input_hint": "Envie um CSV do painel Histórico (entidade, estado, última alteração), ou deixe o WashData ler o histórico do sensor diretamente. A detecção ao vivo percorre o histórico e você escolhe quais ciclos manter.", + "hist_recorder_hint": "Lê desde a data escolhida até agora. O Home Assistant mantém histórico detalhado por 10 dias por padrão; médias por hora mais antigas são grosseiras demais para detectar ciclos.", + "hist_scanning": "Seu histórico está passando pelo detector em segundo plano. Você pode fechar esta janela e voltar mais tarde.", "hist_imported_count": "{n} ciclos importados.", "hist_duplicates": "{n} já tinham sido importados e foram ignorados.", "hist_capped": "O limite de ciclos importados por dispositivo foi atingido; os restantes não foram salvos.", - "hist_next_step": "Eles estão na sua lista de Ciclos, marcados como histórico importado. Abra um e use Rotular para indicar o programa a que ele pertence.", + "hist_next_step": "Eles estão na sua lista de Ciclos, marcados como histórico importado. Abra um e use Rotular para dar nome ao programa.", "hist_rows_read": "{n} leituras lidas", "hist_breaks": "{n} intervalos em que o sensor estava indisponível", "hist_other_entity": "{n} leituras de outras entidades ignoradas", @@ -1041,26 +987,71 @@ "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.", - "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.", + "imported_history_readonly": "Encontrado em um histórico de potência importado. Ajuda a correspondência, mas não conta nas suas estatísticas e não pode ser cortado nem dividido. Rotule-o para dar nome ao programa.", + "odometer_intro": "Este total só aumenta. Excluir um ciclo não o reduz, para que os lembretes de manutenção continuem corretos.", + "odometer_correct_hint": "Defina este valor se o WashData contou um ciclo que nunca aconteceu, ou se o eletrodoméstico já fez 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}", "standby_above_stop_title": "Ainda consumindo {idle} W quando o ciclo terminou", "standby_above_stop_sub": "{n} dos últimos {total} ciclos. O Atraso de Desligamento só começa quando a potência fica abaixo do Limite de Parada ({stop} W), então os ciclos terminam tarde ou precisam ser parados na mão.", - "advisory_duration_outlier": "{n} ciclo(s) rotulados como '{name}' estão longe demais da duração habitual dele para algum dia corresponderem (pior caso: {ratio}x). Provavelmente estão rotulados errado, ou uma gravação capturou duas execuções. Rotule-os novamente ou divida-os para que parem de distorcer a estimativa de tempo deste programa.", + "advisory_duration_outlier": "{n} ciclo(s) rotulados como '{name}' estão longe demais da duração habitual dele para algum dia corresponder (pior caso: {ratio}x). Provavelmente estão rotulados errado ou contêm dois ciclos. Rotule-os novamente ou divida-os para que parem de distorcer a estimativa de tempo.", "toast_range_required": "Defina um intervalo De/Até válido", - "no_profiles_option": "(sem perfis, adicione um)" + "no_profiles_option": "(sem perfis, adicione um)", + "compare_cycles_title": "Comparar {count} ciclos", + "edit_builtin_phase": "Esta é uma fase integrada. Salvar cria uma cópia personalizada; exclua a cópia para restaurar o original.", + "fetch_error": "Falha ao carregar os dados.", + "connection_lost": "Conexão perdida - exibindo dados das {time}.", + "pg_notify_partial": "Os marcadores mostram apenas notificações de início, quase pronto, fim, marcos e horário silencioso. Atualizações ao vivo, lembretes de descarga e alertas de tempo excedido não são simulados.", + "connect_to_confirm": "Conecte-se no ícone de configurações para confirmar ou avaliar.", + "coverage_gap_cluster": "{count} ciclos recentes sem rótulo de cerca de {min} min parecem ser um programa que você ainda não criou.", + "pg_label_yes": "Seria rotulado como \"{name}\" no fim do ciclo", + "pg_label_no": "Solicitaria revisão: {reason}", + "live_match_debug_tip": "Confiança: o quanto a curva de potência atual corresponde ao perfil candidato principal (0-100%). Ambíguo: os dois melhores candidatos têm pontuações a menos de 5% um do outro; o rótulo fica incerto até o ciclo terminar.", + "automation_alias_started": "{device}: ciclo iniciado", + "automation_alias_finished": "{device}: ciclo finalizado", + "automation_desc_started": "Executada quando um ciclo de {device} começa. Adicione suas ações (notificar, luzes, ...).", + "automation_desc_finished": "Executada quando um ciclo de {device} termina. Adicione suas ações (notificar, luzes, ...).", + "feedback_detected": "O WashData detectou: {name}", + "import_undo_hint": "Um ponto de restauração de antes da última importação ({when}) é mantido: {profiles} perfil(is) e {cycles} ciclo(s).", + "replace_undo_hint": "Um ponto de restauração é salvo primeiro, então você pode desfazer esta importação em Exportar / Importar.", + "pg_in_sample_note": "Esses ciclos ajudaram a construir os perfis com os quais são comparados, então os resultados são otimistas: espere um pouco menos de correspondências certas em ciclos novos.", + "hist_tail_trimmed": "Foram cortados {min} min de espera após o fim do ciclo, como faz a integração em tempo real", + "invalid_threshold_pair": "O Limite de Parada ({stop} W) deve ficar abaixo do Limite de Início ({start} W). Nada foi salvo; altere os dois juntos.", + "hist_truncated": "parou no limite de {n} leituras, então o restante do histórico não foi analisado", + "hist_warn_kw": "A leitura mais alta é de apenas {w}. Este sensor parece informar quilowatts; o WashData precisa de watts, então nenhum ciclo pode ser detectado.", + "hist_warn_naive_time": "{n} carimbos de data/hora não têm fuso horário e foram lidos como UTC. Se o arquivo usar o horário local, os ciclos ficam deslocados pela sua diferença em relação ao UTC e leituras perto de uma mudança de horário de verão podem se perder.", + "hist_warn_naive_time_one": "{n} carimbo de data/hora não tem fuso horário e foi lido como UTC. Se o arquivo usar o horário local, os ciclos ficam deslocados pela sua diferença em relação ao UTC e leituras perto de uma mudança de horário de verão podem se perder.", + "hist_warn_naive_time_many": "{n} carimbos de data/hora não têm fuso horário e foram lidos como UTC. Se o arquivo usar o horário local, os ciclos ficam deslocados pela sua diferença em relação ao UTC e leituras perto de uma mudança de horário de verão podem se perder.", + "hist_warn_naive_time_other": "{n} carimbos de data/hora não têm fuso horário e foram lidos como UTC. Se o arquivo usar o horário local, os ciclos ficam deslocados pela sua diferença em relação ao UTC e leituras perto de uma mudança de horário de verão podem se perder.", + "pg_compare_profile_tip": "Escolhe apenas o perfil desenhado sobre o gráfico. A simulação sempre busca por conta própria a correspondência do ciclo, como faz a integração em tempo real.", + "pg_outcome_stale": "As configurações mudaram desde que esta simulação foi executada. Ela ainda mostra o resultado antigo.", + "pg_sweep_guarded": "Encerra mais ciclos antes da hora, divide mais ciclos ou deixa passar ciclos que a configuração atual detecta, então este valor nunca é recomendado.", + "pg_sweep_keep_current": "Mantenha o valor atual ({value}, {metric}): nenhum valor foi melhor por pelo menos um ciclo.", + "pg_sweep_n_cycles": "em {count} ciclos", + "pg_sweep_n_cycles_one": "em {count} ciclo", + "pg_sweep_n_cycles_many": "em {count} ciclos", + "pg_sweep_n_cycles_other": "em {count} ciclos", + "pg_task_busy": "Já há uma execução de Testar no histórico ou Otimizar em andamento para este dispositivo. Aguarde até que termine ou cancele." }, "phase_desc": { "anti_crease": "Giros curtos ocasionais após a conclusão para reduzir amassados.", "anti_wrinkle": "Giro periódico pós-ciclo para reduzir amassados.", + "baking": "Resistência de alta temperatura ativa para formar a casca e o miolo.", + "cooking": "Fase principal de cozimento com resistência e ventilador ativos.", "cool_down": "Giro sem calor perto do fim do ciclo.", + "cool_down_af": "Resfriamento só com o ventilador após o aquecimento.", "drain_and_switch": "Período de transição do modo de lavagem para o modo de secagem.", "dry": "Etapa de secagem com aquecedor e/ou calor residual.", "drying": "Período principal de giro com calor.", "heat_up": "Aquecimento inicial do aquecedor antes do início da secagem completa.", + "keep_warm": "Fase de calor baixo para manter a comida quente.", + "keep_warm_bm": "Fase de calor baixo para manter o pão quente depois de assar.", + "kneading": "O motor mistura e sova a massa. Consumo alto.", + "pause_af": "Pausa curta para chacoalhar ou conferir a comida.", + "pre_heat": "Aquecimento inicial da câmara antes do cozimento completo.", "pre_rinse": "Pulverização inicial antes da lavagem com detergente.", + "proving": "Fermentação com calor baixo para o fermento agir e a massa crescer.", + "resting": "Pausa curta de baixa potência entre as sovas para relaxar o glúten.", "pre_wash": "Imersão inicial ou pré-tratamento antes da lavagem principal.", "rinse": "Etapa de enxágue com água limpa. Esta fase pode se repetir várias vezes.", "sanitize": "Etapa de limpeza em alta temperatura para programas de higienização.", @@ -1073,6 +1064,7 @@ "wash_dw": "Lavagem principal com detergente e aquecimento." }, "placeholder": { + "custom_task": "ex. limpar a borracha da porta", "add": "adicionar…", "group_name": "ex. Algodão 2:47", "json_buttons": "[{\"action\":\"ID\",\"title\":\"Label\"}]", @@ -1100,10 +1092,10 @@ "section": { "dishwasher": { "label": "Lava-louças", - "intro": "Tratamento de fim de ciclo para lava-louças, que normalmente terminam com uma longa fase de secagem quase silenciosa antes de uma breve descarga final." + "intro": "Tratamento do fim para lava-louças, que muitas vezes secam em silêncio por muito tempo antes de um breve escoamento final." }, "anti_wrinkle": { - "intro": "O modo antiamasso detecta pulsos de giro de baixa potência após a fase principal e os mantém anexados ao ciclo terminado.", + "intro": "Mantém os pulsos de giro de baixa potência de uma secadora após a fase principal dentro do ciclo concluído.", "label": "Antiamasso" }, "basic": { @@ -1111,7 +1103,7 @@ "label": "Básico" }, "delay": { - "intro": "A detecção de início retardado identifica quando um aparelho está ligado mas ainda não iniciou seu ciclo.", + "intro": "Detecta um eletrodoméstico ligado que aguarda um início programado.", "label": "Início Retardado" }, "detection": { @@ -1135,161 +1127,153 @@ "label": "Temporização e Watchdog" }, "triggers": { - "intro": "Sinais externos opcionais: um gatilho de fim, um sensor de porta, um interruptor de pausa e o lembrete de descarga.", + "intro": "Extras opcionais: gatilho de fim, sensor de porta, interruptor de pausa e lembrete de descarga.", "label": "Gatilhos e Portas" - }, - "phase_eta": { - "label": "Tempo restante", - "intro": "Tempo restante com base nas fases, para máquinas cuja duração do ciclo depende da temperatura ou da centrifugação." } }, "setting": { "dishwasher_end_spike_quiet_release": { "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." + "doc": "Após a duração esperada: quanto tempo a lava-louças deve ficar abaixo do Limite de Parada antes de o WashData parar de esperar pelo escoamento final. Aumente se um escoamento tardio após uma secagem longa for perdido. A espera nunca passa de 30 minutos." }, "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." + "doc": "Fração da duração média do programa que um ciclo deve atingir antes de o giro de baixa potência contar como cauda antiamasso. Reduza (ex.: 0,75) em secadoras com sensor de carga. Deixe as lavadoras no padrão (0,98), ou uma lavagem pode terminar pela metade." }, "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.", + "doc": "Manter os pulsos curtos de giro de baixa potência de uma secadora após a fase principal de aquecimento dentro do ciclo concluído, em vez de lê-los como novos ciclos.", "label": "Ativar Detecção Antiamasso" }, "anti_wrinkle_exit_power": { - "doc": "A potência deve cair abaixo disso entre os pulsos para que o modo antirrugas permaneça ativo.", + "doc": "No modo antiamasso, a potência abaixo deste valor conta como silêncio entre pulsos de giro, e um silêncio maior que o Intervalo Máximo Entre Pulsos encerra o modo. Se o Limite de Parada for mais alto, ele é usado, então isto só muda algo acima do Limite de Parada.", "label": "Limite de Saída de Potência" }, "anti_wrinkle_idle_timeout": { - "doc": "Por quanto tempo a máquina pode permanecer em silêncio entre dois pulsos de giro antes de o modo antiamasso terminar. Defina-o acima do maior intervalo que sua secadora deixa entre pulsos; caso contrário, cada pulso posterior é lido como um início falso.", + "doc": "Quanto tempo a secadora pode ficar em silêncio entre pulsos de giro antes de o modo antiamasso terminar. Defina acima da pausa mais longa da sua secadora, ou os pulsos seguintes serão lidos como falsas partidas.", "label": "Intervalo Máximo Entre Pulsos" }, "anti_wrinkle_max_duration": { - "doc": "Pulsos mais longos do que isso são tratados como um ciclo real, e não como uma queda anti-rugas.", + "doc": "Um pulso mais longo que isso conta como um ciclo real, não como giro antiamasso.", "label": "Duração Máxima" }, "anti_wrinkle_max_power": { - "doc": "Uma pulsação acima dessa potência é tratada como um verdadeiro novo ciclo, e não como um tombo antirrugas. Defina logo acima da potência do pulso de queda.", + "doc": "Um pulso acima desta potência conta como um novo ciclo real, não como giro. Defina logo acima da potência dos seus pulsos de giro.", "label": "Potência Máxima Antiamasso" }, "auto_label_confidence": { - "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.", + "doc": "Pontuação de correspondência no fim do ciclo a partir da qual o programa é rotulado sem perguntar. Aumente para ter mais certeza; reduza para rotular mais ciclos automaticamente. Mantenha acima da Confiança de Aprendizado.", "label": "Confiança de Rotulagem Automática" }, "auto_maintenance": { - "doc": "Execute a limpeza noturna: recrie envelopes de perfil, recalcule a integridade do ciclo, remova rastros de depuração e retenha os ciclos mais recentes.", + "doc": "Limpeza noturna: rotula ciclos antigos que agora correspondem claramente a um programa, reconstrói as curvas dos programas, atualiza as marcas de anomalia e a saúde dos ciclos, e remove os rastros de depuração se Salvar Rastros de Depuração estiver desligado. Os ciclos e seus rastros de potência são sempre mantidos.", "label": "Manutenção Automática (limpeza noturna)" }, "completion_min_seconds": { - "doc": "Ciclos mais curtos que isso são descartados como ciclos fantasmas (testes, abertura da porta para adicionar uma meia).", + "doc": "Ciclos mais curtos que isso (testes, abrir a porta para colocar uma meia) são salvos como Interrompido e não recebem notificação de fim.", "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." + "doc": "Adiciona ao início de um ciclo as leituras de tentativas de partida com falha, para máquinas que testam antes de começar (trava da porta, primeiro enchimento). 300 s bastam para a maioria; 0 = desligado. Os inícios registrados ficam mais cedo, então as durações aprendidas se desviam por um tempo." }, "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.", + "doc": "A potência deve ficar na faixa de espera por este tempo antes de o eletrodoméstico contar como aguardando o início.", "label": "Segundos de Confirmação" }, "delay_start_detect_enabled": { - "doc": "Detecta quando o aparelho está ligado e aguardando (início retardado/standby), mas ainda não iniciou seu ciclo, para que o standby não seja confundido com um ciclo em execução.", + "doc": "Detectar um eletrodoméstico ligado que aguarda um início programado, para que o consumo em espera não seja confundido com um ciclo em andamento.", "label": "Ativar Detecção de Início Retardado" }, "delay_timeout_hours": { - "doc": "Pare de esperar no modo de partida retardada após tantas horas e retorne ao modo inativo, para que uma máquina deixada ligada, mas nunca iniciada, não espere para sempre.", + "doc": "Desistir de aguardar um início programado após este número de horas e voltar a ocioso.", "label": "Horas de Timeout" }, "device_type": { - "doc": "Classe de eletrodomésticos. Define padrões de detecção sensatos (limites, atraso de desativação, tratamento final) ajustados para esse tipo de dispositivo; altere-o apenas se o dispositivo tiver sido originalmente configurado como o tipo errado.", + "doc": "Define os padrões de detecção (limites, atraso de desligamento, tratamento do fim) para este tipo de eletrodoméstico. Só altere se o dispositivo foi configurado com o tipo errado.", "label": "Tipo de Dispositivo" }, "door_sensor_entity": { - "doc": "Sensor binário de porta opcional. Usado para detectar quando o aparelho foi aberto/descarregado após um ciclo.", + "doc": "Sensor de porta opcional. Mostra quando o eletrodoméstico foi aberto para descarregar após um ciclo.", "label": "Entidade do Sensor de Porta" }, "duration_tolerance": { - "doc": "Tolerância para estimativas de tempo restante (feedback de aprendizagem, não correspondência). Se a duração real estiver dentro de +/-X% da estimativa, isso será considerado uma boa correspondência.", + "doc": "Apenas para aprendizado: um ciclo dentro de +/-X% da duração estimada conta como uma boa estimativa. Não afeta a correspondência.", "label": "Tolerância de Estimativa" }, "enable_ml_models": { - "doc": "Enquanto um ciclo é executado, deixe os modelos refinarem os resultados em tempo real: uma estimativa de tempo restante e de energia/custo mais estável, e uma proteção anti-parada prematura na detecção de final (ela só pode atrasar um término, nunca terminar um mais cedo e é limitada). Usa seus modelos ajustados quando disponíveis, caso contrário, os integrados. Desligado = apenas a lógica clássica baseada em energia (ainda confiável).", + "doc": "Usar um modelo treinado nesta máquina para prever a energia e o custo de um ciclo em andamento, e permitir que um programa bem conhecido que para cedo demais termine sem todo o atraso de desligamento. Desligado: apenas lógica baseada em potência.", "label": "Aplicar modelos inteligentes durante um ciclo" }, "end_energy_threshold": { - "doc": "Durante a contagem regressiva do atraso de desligamento, a energia acumulada (watts x tempo) é comparada a esse limite. Se excedido, a contagem regressiva é reiniciada - mantendo os rolos anti-rugas e os restos de secagem da máquina de lavar louça presos ao ciclo, em vez de cortá-los. Aumente se os ciclos terminarem muito cedo durante o resfriamento; abaixe-o se a detecção for lenta.", + "doc": "Energia permitida durante o atraso de desligamento. Acima disso o ciclo continua, para que giros e caudas de secagem continuem fazendo parte dele. Reduza se os ciclos terminarem durante uma cauda; aumente se ciclos concluídos continuarem esperando.", "label": "Energia de Fim" }, - "end_repeat_count": { - "doc": "Número de leituras consecutivas abaixo do limite de parada necessárias antes do término do ciclo. 1 é adequado para a maioria dos plugues. Aumente para 2-3 se o seu plugue inteligente ocasionalmente relatar uma amostra de falso zero no meio do ciclo e seus ciclos estiverem terminando prematuramente.", - "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). 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.", + "doc": "Sensor com o preço atual por kWh (ex.: uma tarifa dinâmica). Ele tem prioridade sobre o preço fixo abaixo, então precisa ser um preço, não o sensor de potência ou de energia da tomada.", "label": "Entidade de Preço de Energia" }, "energy_price_static": { - "doc": "Preço fixo por kWh usado para valores de custos quando nenhuma entidade de preço real é definida acima.", + "doc": "Preço fixo por kWh, usado quando nenhuma entidade de preço está definida.", "label": "Preço Estático de Energia (por kWh)" }, "energy_sensor": { - "doc": "Contador de energia cumulativo opcional (total_increasing em kWh/Wh, por ex. o medidor de consumo total da própria tomada). Quando definido, a energia informada de cada ciclo é obtida a partir da diferença deste contador entre o início e o fim, o que evita a subcontagem resultante da integração de um sensor de potência que informa lentamente. Recorre ao valor integrado se a leitura estiver ausente, se sua unidade for desconhecida ou se o delta não for positivo. Deixe em branco para continuar integrando o sensor de potência.", + "doc": "Contador opcional em kWh ou Wh (total_increasing, ex.: o próprio medidor da tomada). A energia do ciclo passa a ser lida dele, o que é mais preciso que calculá-la a partir de um sensor de potência lento. Se a leitura não servir, usa-se o valor calculado.", "label": "Entidade do Medidor de Energia" }, "expose_debug_entities": { - "doc": "Publique entidades de HA de diagnóstico extras (confiança de correspondência, ambiguidade, estados internos). Off mantém a lista de entidades limpa para uso normal.", + "doc": "Adicionar entidades de diagnóstico (confiança de correspondência, ambiguidade, estado interno). Desligado mantém a lista de entidades limpa.", "label": "Expor Entidades de Depuração" }, "external_end_trigger": { - "doc": "Sensor binário cuja mudança de estado marca o fim do ciclo (por exemplo, um contato \"terminado\" do aparelho ou uma integração complementar).", + "doc": "Sensor binário cuja mudança marca o fim do ciclo (ex.: um contato \"concluído\" ou outra integração).", "label": "Entidade do Gatilho Externo" }, "external_end_trigger_enabled": { - "doc": "Deixe um sensor binário externo sinalizar o fim de um ciclo, além da detecção integrada baseada em energia.", + "doc": "Permitir que um sensor binário sinalize o fim de um ciclo, além da detecção baseada em potência.", "label": "Ativar Gatilho de Fim Externo" }, "external_end_trigger_inverted": { - "doc": "Trate o sensor de disparo desligado (em vez de ligado) como o sinal de fim de ciclo.", + "doc": "Tratar a passagem do sensor para desligado (e não para ligado) como sinal de fim.", "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 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.", + "doc": "Pontuação de correspondência mínima para rotular um ciclo concluído. Um vencedor claro é rotulado; um caso apertado vai para a fila de revisão em Ciclos. Abaixo disso, nada é rotulado nem perguntado. Mantenha abaixo da Confiança de Rotulagem Automática.", "label": "Confiança de Aprendizado" }, "linked_device": { - "doc": "Opcionalmente, aninhe este dispositivo WashData em outro dispositivo (por exemplo, o plugue inteligente) no registro do dispositivo HA, mostrado como \"Conectado via ...\".", + "doc": "Opcional. Mostrar este dispositivo WashData sob outro dispositivo (ex.: o plugue inteligente) no Home Assistant, como \"Conectado via ...\".", "label": "Agrupar sob Dispositivo" }, "min_off_gap": { - "doc": "Se a máquina desligar por menos que esse tempo, a sequência liga/desliga/liga é tratada como um ciclo contínuo. Evita que os programas de imersão (a máquina desliga durante vários minutos a meio da lavagem) sejam divididos em dois ciclos separados. Defina-o mais curto do que o intervalo entre as cargas consecutivas se quiser que elas sejam contadas como ciclos separados. Os padrões de tipo de dispositivo protegem a pausa intra-ciclo típica para cada dispositivo.", + "doc": "Um período desligado mais curto que isso não divide um ciclo, então as pausas de molho ficam em uma só lavagem. Mantenha abaixo da pausa entre cargas seguidas, ou elas se juntam em um só ciclo.", "label": "Intervalo Mínimo de Desligamento" }, "min_power": { - "doc": "Potência mínima absoluta considerada ativa. As leituras abaixo disso são tratadas como 0 W (standby), filtrando a carga fantasma de plugues inteligentes e LEDs de standby.", + "doc": "Leituras abaixo disso contam como 0 W (espera). Filtra o consumo em repouso de plugues inteligentes e LEDs de espera.", "label": "Potência Mínima" }, "ml_training_enabled": { - "doc": "Estude periodicamente seus ciclos revisados durante a noite e ajuste os modelos para esta máquina específica. Uma mudança só é mantida quando realmente obtém resultados melhores em ciclos prolongados, portanto, isso só pode ajudar ou permanecer igual - nunca regredir.", + "doc": "Retreinar de tempos em tempos o modelo de energia com os ciclos concluídos desta máquina. Um modelo novo só é mantido se superar a estimativa simples em ciclos de teste.", "label": "Aprender com esta máquina" }, "ml_training_hour": { - "doc": "Hora local do dia (0-23) para fazer o ajuste fino noturno. Escolha um horário tranquilo, como 2 (02:00).", + "doc": "Hora do dia (0-23) para o treino noturno. Escolha um horário tranquilo, ex.: 2.", "label": "Aprender na hora" }, "ml_training_interval_days": { - "doc": "Verifique novamente se há melhorias no máximo uma vez a cada um desses dias.", + "doc": "Retreinar no máximo uma vez a cada este número de dias.", "label": "Verificar no máximo a cada" }, "ml_training_min_cycles": { - "doc": "Espere até que pelo menos esse número de ciclos tenha sido registrado antes do ajuste fino, para que haja o suficiente para aprender.", + "doc": "Esperar por pelo menos este número de ciclos gravados antes de treinar.", "label": "Ciclos necessários primeiro" }, "name": { - "doc": "Nome de exibição mostrado na lista de integrações de alta disponibilidade e no registro do dispositivo.", + "doc": "Nome exibido no Home Assistant para este dispositivo.", "label": "Nome do Dispositivo" }, "no_update_active_timeout": { - "doc": "Se nenhuma atualização de energia chegar por tanto tempo durante a execução, presuma que o plugue caiu off-line e force a parada para evitar um ciclo zumbi. O padrão de 600 s permite atraso na nuvem ou na malha.", + "doc": "Se nenhuma atualização de potência chegar por este tempo durante um ciclo, considera-se que o plugue ficou offline e o ciclo é parado à força. O padrão de 600 s dá margem para atrasos da nuvem ou da rede mesh.", "label": "Timeout Sem Atualização" }, "notify_before_end_minutes": { @@ -1297,35 +1281,35 @@ "label": "Alerta Pré-Fim" }, "notify_channel": { - "doc": "Nome do canal de notificação do Android para mensagens iniciais/ao vivo (controla o som e a prioridade por canal no aplicativo móvel). Em branco usa o aplicativo complementar padrão.", + "doc": "Canal Android para mensagens de início e ao vivo; o canal define som e prioridade. Em branco: padrão do aplicativo.", "label": "Canal Android (início/ao vivo)" }, "notify_cycle_timers": { - "doc": "Notificações em minutos específicos de um ciclo (por exemplo, para adicionar amaciante). A mensagem suporta {device}, {program}, {minutes}. Ative a pausa automática para pausar nesse ponto e receber uma notificação interativa com um botão Continuar; retomar por meio do painel, do serviço de pausa/retomada ou da ação de notificação.", + "doc": "Notificações em minutos definidos de um ciclo (ex.: para colocar amaciante). A mensagem suporta {device}, {program}, {minutes}. Com Pausa automática, o ciclo também pausa ali e a notificação ganha um botão Retomar.", "label": "Temporizadores de Ciclo" }, "notify_finish_channel": { - "doc": "Nome do canal de notificação do Android para a mensagem final. Em branco reutiliza o canal inicial/ao vivo.", + "doc": "Canal Android para a mensagem de fim. Em branco: reutiliza o canal de início/ao vivo.", "label": "Canal Android (fim)" }, "notify_finish_message": { - "doc": "Corpo enviado quando um ciclo termina. Variáveis de modelo: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}. {time_finished} e {vs_typical} são os mais úteis aqui.", + "doc": "Corpo enviado quando um ciclo termina. Variáveis: {device}, {program}, {duration} (minutos), {duration_hm} (1 h 05 min), {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}, {status} (completed ou force_stopped; ciclos interrompidos não enviam nada). Com qualquer outra variável, é usado o texto padrão.", "label": "Mensagem de Fim" }, "notify_finish_services": { - "doc": "notify.* serviços chamados quando um ciclo termina. Adicione um por alvo; deixe em branco para nenhuma notificação de conclusão.", + "doc": "Serviços notify.* a chamar quando um ciclo termina. Deixe vazio para nenhum.", "label": "Serviços de Fim" }, "notify_fire_events": { - "doc": "Também dispare eventos ha_washdata_* no início/término do ciclo para que você possa construir suas próprias automações.", + "doc": "Disparar também eventos ha_washdata_* no início e no fim do ciclo, para suas próprias automações.", "label": "Disparar Eventos HA para Notificações" }, "notify_icon": { - "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.", + "doc": "Ícone pequeno da notificação (ex.: mdi:washing-machine). Enviado só para destinos mobile_app, não para grupos notify. No iOS substitui o ícone do aplicativo e requer o aplicativo companion 2026.8 ou mais recente. Em branco: padrão da plataforma.", "label": "Ícone da Notificação" }, "notify_live_chronometer": { - "doc": "Mostre um cronômetro de contagem regressiva com atualização ao vivo na notificação (em plataformas que o suportam) em vez de uma estimativa estática.", + "doc": "Mostrar uma contagem regressiva em andamento na notificação (onde houver suporte) em vez de uma estimativa fixa.", "label": "Usar Cronômetro ao Vivo" }, "notify_live_interval_seconds": { @@ -1333,143 +1317,135 @@ "label": "Intervalo de Atualização ao Vivo" }, "notify_live_overrun_percent": { - "doc": "Se um ciclo ultrapassar sua estimativa em mais do que essa porcentagem, envie um alerta de ultrapassagem.", + "doc": "As atualizações ao vivo param quando um ciclo passa da duração prevista nesta porcentagem (20 = até 1,2x o tempo previsto).", "label": "% de Excesso Antes do Alerta" }, "notify_live_services": { - "doc": "notify.* serviços chamados para atualizações de progresso ao vivo enquanto um ciclo é executado. Deixe em branco para desativar as notificações de progresso ao vivo.", + "doc": "Serviços notify.* para atualizações de progresso ao vivo durante um ciclo. Deixe vazio para desligá-las.", "label": "Serviços de Progresso ao Vivo" }, "notify_only_when_home": { - "doc": "Envie notificações apenas quando pelo menos uma das pessoas vinculadas (acima) estiver em casa.", + "doc": "Notificar apenas quando pelo menos uma das pessoas acima estiver em casa.", "label": "Notificar Apenas Quando em Casa" }, "notify_people": { - "doc": "person.* entidades usadas por \"Notify Only When Home\" para decidir se alguém está em casa.", + "doc": "Entidades person.* que \"Notificar Apenas Quando em Casa\" verifica.", "label": "Pessoas (para Apenas Quando em Casa)" }, "notify_pre_complete_message": { - "doc": "Corpo do alerta de pré-fim / quase concluído. Variáveis de modelo: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Texto de cada atualização de progresso ao vivo. Variáveis de modelo: {device}, {program}, {minutes} (minutos restantes). Com qualquer outra variável, é usado o texto padrão.", "label": "Mensagem Pré-Conclusão" }, "notify_reminder_message": { - "doc": "Corpo do lembrete de que a roupa ainda aguarda para ser retirada. Em branco usa o padrão interno. Variáveis de modelo: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Texto do Alerta Pré-Fim, enviado uma única vez. Variáveis de modelo: {device}, {program}, {minutes} (a configuração Alerta Pré-Fim). Com qualquer outra variável, é usado o texto padrão.", "label": "Mensagem de Lembrete" }, "notify_start_message": { - "doc": "Corpo enviado quando um ciclo começa. Variáveis de modelo: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Corpo enviado quando um ciclo começa. Variáveis de modelo: {device}, {program}. Com qualquer outra variável, é usado o texto padrão.", "label": "Mensagem de Início" }, "notify_start_services": { - "doc": "notify.* serviços chamados quando um ciclo é iniciado. Adicione um por destino (telefone, painel, etc.); deixe em branco para nenhuma notificação de início.", + "doc": "Serviços notify.* a chamar quando um ciclo começa (celular, dashboard, ...). Deixe vazio para nenhum.", "label": "Serviços de Início" }, "notify_timeout_seconds": { - "doc": "Ignore automaticamente a notificação após esses segundos (em plataformas que a suportam). 0 o mantém até ser descartado manualmente.", + "doc": "Fechar a notificação após este número de segundos (onde houver suporte). 0 mantém até você fechar.", "label": "Dispensar Automaticamente Após" }, "notify_title": { - "doc": "Título da notificação. Variáveis de modelo: {device}, {program}, {duration}, {energy}, {cost}, {date}, {time}, {minutes}.", + "doc": "Título da notificação. Só {device} é substituído; com qualquer outra variável, é usado o título padrão.", "label": "Título da Notificação" }, "notify_unload_delay_minutes": { - "doc": "Minutos após o término de um ciclo, antes de enviar o lembrete de \"descarregar a máquina\" ainda em espera. Defina 0 para desativar o lembrete.", + "doc": "Minutos após o fim de um ciclo até o lembrete \"descarregar a máquina\". 0 desativa.", "label": "Atraso de Lembrete de Descarga" }, "notify_unload_repeat": { - "doc": "Continua reenviando o lembrete de descarga a cada \"Atraso de Lembrete de Descarga\" minutos até a descarga ser confirmada ou você tocar em \"Parar de lembrar\" na notificação. Requer uma forma de confirmar a descarga: uma Entidade do Sensor de Porta, uma Entidade de Confirmação de Descarga ou a opção \"Confirmar a Descarga Manualmente\" abaixo. O botão de dispensar funciona nas notificações do aplicativo complementar (móvel) do Home Assistant.", + "doc": "Reenviar o lembrete de descarga a cada \"Atraso de Lembrete de Descarga\" minutos até a descarga ser confirmada ou você tocar em \"Parar de lembrar\" (aplicativo móvel). Requer uma Entidade do Sensor de Porta, uma Entidade de Confirmação de Descarga ou \"Confirmar a Descarga Manualmente\".", "label": "Repetir Até Confirmar a Descarga" }, "off_delay": { - "doc": "Tempo de espera após a queda de energia antes de declarar o término do ciclo. Se a energia for retomada dentro desta janela, o ciclo continua perfeitamente - isso faz a ponte entre as etapas de lavagem. As máquinas de lavar louça têm longas fases de secagem (desligadas por 20-60 minutos), portanto o atraso no desligamento deve exceder esse valor para manter toda a lavagem + secagem em um único ciclo.", + "doc": "Quanto tempo a potência deve ficar baixa antes de o ciclo terminar. Se a potência voltar dentro desse tempo, o ciclo continua. Lava-louças podem ficar paradas por 20-60 min durante a secagem, então defina acima disso.", "label": "Atraso de Desligamento" }, "pause_cuts_power": { - "doc": "Quando um ciclo for pausado, desligue também a Entidade Switch abaixo. Apenas para aparelhos cuja ficha possa ser cortada com segurança a meio do ciclo.", + "doc": "Ao pausar um ciclo, desligar também a Entidade do Switch abaixo. Apenas para eletrodomésticos que podem perder energia no meio do ciclo com segurança.", "label": "Pausa Também Corta Energia (via switch)" }, "power_off_delay": { - "doc": "Por quanto tempo a potência deve permanecer abaixo do Limite de Desligamento após o término de um ciclo antes de o estado voltar para Desligado. Usado apenas quando o Limite de Desligamento é maior que 0. É verificado conforme a cadência em segundo plano, de modo que o atraso efetivo é arredondado para cima até o próximo tick de expiração de estado.", + "doc": "Quanto tempo a potência deve ficar abaixo do Limite de Desligamento após um ciclo antes de o estado voltar para Desligado. Só é usado quando esse limite é maior que 0. É verificado periodicamente, então a espera real pode ser um pouco maior.", "label": "Atraso de Desligamento" }, "power_off_threshold_w": { - "doc": "Detecção opcional do estado Desligado com base na potência. Quando maior que 0, assim que um ciclo termina e a potência permanece abaixo deste nível durante o Atraso de Desligamento, a máquina é considerada desligada e o estado volta para Desligado. Deixe em 0 para desativar (padrão). Defina-o acima do consumo real com a máquina desligada e abaixo do Limite de Parada e do consumo em espera da sua máquina (concluída mas ainda ligada); se não for inferior ao Limite de Parada, é ignorado. Quando ativado, substitui o Atraso de Redefinição de Progresso para o retorno a Desligado, de modo que uma máquina concluída permanece em Concluído/Limpar até ser realmente desligada.", + "doc": "Após um ciclo, potência abaixo deste valor durante o Atraso de Desligamento coloca o estado em Desligado, em vez de o Atraso de Redefinição de Progresso fazer isso. Defina acima do consumo desligado e abaixo do consumo em espera e do Limite de Parada. 0 desativa (padrão).", "label": "Limite de Desligamento" }, "power_sensor": { - "doc": "A entidade do sensor que relata a potência ativa em watts para este aparelho (por exemplo, sensor.washer_power). Toda a detecção de ciclo é baseada neste sinal.", + "doc": "O sensor que informa a potência atual deste eletrodoméstico em watts (ex.: sensor.washer_power). Toda a detecção se baseia nele.", "label": "Sensor de Potência" }, - "profile_duration_tolerance": { - "doc": "A banda +/- em torno da duração média de um perfil usada durante a correspondência. 0,25 significa que um perfil de 60 minutos corresponde a ciclos de 45 a 75 minutos.", - "label": "Tolerância de Duração do Perfil" - }, "profile_match_interval": { - "doc": "Com que frequência tentar a correspondência de perfil durante um ciclo de execução. O padrão de 300 s (5 minutos) equilibra a velocidade de detecção e a CPU.", + "doc": "Com que frequência um ciclo em andamento é comparado com os perfis. Padrão 300 s (5 min).", "label": "Intervalo de Correspondência" }, "profile_match_max_duration_ratio": { - "doc": "Duração máxima do ciclo em relação ao perfil. 1.3 significa que um ciclo deve ter menos de 130% da duração do perfil para corresponder.", + "doc": "Um programa é descartado quando o ciclo passa deste múltiplo da sua duração habitual. 1,8: um ciclo 80% mais longo que o programa não corresponde mais a ele.", "label": "Proporção de Duração Máxima" }, "profile_match_min_duration_ratio": { - "doc": "Duração mínima do ciclo em relação ao perfil. 0,9 significa que um ciclo deve ter pelo menos 90% da duração do perfil para corresponder.", + "doc": "Um programa só é considerado quando o ciclo atingiu esta fração da sua duração habitual. 0,10: um programa de 2 horas vira candidato aos 12 minutos. Não se aplica nos primeiros 15 minutos de um ciclo.", "label": "Proporção de Duração Mínima" }, "profile_match_threshold": { - "doc": "Pontuação mínima de similaridade (0-1) exigida no final do ciclo para aceitar a identificação do programa. Aumente-o para reduzir identificações erradas; abaixe-o se os programas da sua máquina não estiverem correspondendo. O padrão 0,4 é um ponto de partida conservador.", + "doc": "Pontuação de correspondência ao vivo de que um ciclo precisa antes de o Encerramento inteligente ou o fim antiamasso poderem finalizá-lo mais cedo. Não altera o programa exibido. Aumente se correspondências fracas finalizam ciclos cedo demais; reduza se correspondências claras esperam todo o atraso de desligamento.", "label": "Limite de Correspondência" }, "profile_unmatch_threshold": { - "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.", + "doc": "Se a pontuação de correspondência ao vivo cair abaixo deste valor, o palpite atual de programa é limpo. Mantenha um pouco abaixo do Limite de Correspondência para que uma queda breve não o redefina.", "label": "Limite de Não Correspondência" }, "progress_reset_delay": { - "doc": "Depois de terminar, mantenha o progresso em 100% por esse tempo para que Concluído fique visível nos painéis antes de redefinir para Inativo.", + "doc": "Após um ciclo, manter o progresso em 100% por este tempo antes de voltar a Ocioso.", "label": "Atraso de Redefinição de Progresso" }, "pump_stuck_duration": { - "doc": "Segundos, uma bomba pode funcionar continuamente antes de ser sinalizada como possivelmente travada (dispara o evento de bomba travada).", + "doc": "Quanto tempo uma bomba pode funcionar sem parar antes de disparar o evento de bomba travada.", "label": "Duração da Bomba Travada" }, "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.", + "doc": "Tempo mínimo entre leituras processadas. Leituras que chegam antes são ignoradas (leituras baixas durante um ciclo são sempre mantidas). Defina no intervalo de envio do seu plugue ou menos; um valor maior só descarta leituras.", "label": "Intervalo de Amostragem" }, "save_debug_traces": { - "doc": "Armazene o rastreamento de energia completo e os dados de depuração correspondentes para cada ciclo. Útil para solução de problemas, mas aumenta o tamanho do armazenamento.", + "doc": "Salvar também com cada ciclo os dados de depuração do comparador (as pontuações de cada candidato), para solução de problemas. Adiciona cerca de 20 KB por ciclo.", "label": "Salvar Rastros de Depuração" }, "smart_termination_duration_ratio": { - "doc": "Até que ponto um ciclo deve ter avançado na duração esperada do programa correspondente antes de o Encerramento inteligente poder encerrá-lo mais cedo assim que a potência cai. A duração esperada é a média do programa, então em aparelhos cujo tempo de funcionamento varia muito - lavadoras conforme a água de entrada fria no inverno ou morna no verão, secadoras com sensor de umidade, programas que dependem da carga - cerca de metade das execuções termina mais cedo do que essa média e nunca recebe o fim rápido, terminando apenas pelo tempo limite de reserva com minutos de atraso. Reduza este valor (p. ex. 0,85) nessas máquinas para que o fim antecipado continue disparando; aumente-o para perto de 1,0 para ser mais conservador. Deixe vazio para o valor padrão (0,98, ou 0,99 para lava-louças). Ele só pode encerrar um ciclo mais cedo, nunca mais tarde, e nunca dispara em uma correspondência ambígua ou de baixa confiança.", + "doc": "Fração da duração média do programa que um ciclo deve atingir antes de o Encerramento inteligente poder finalizá-lo mais cedo. Se os tempos de execução variarem (temperatura da água, secagem por sensor, tamanho da carga), reduza (ex.: 0,85). Deixe vazio para 0,98 (0,99 em lava-louças).", "label": "Proporção de encerramento inteligente" }, - "smoothing_window": { - "doc": "Quanto o sinal de potência bruta é suavizado. Baixo (2) é responsivo, mas barulhento; alto (5) suaviza os picos, mas adiciona atraso.", - "label": "Janela de Suavização" - }, "start_duration_threshold": { - "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.", + "doc": "A potência deve ficar acima do Limite de Início por este tempo antes de um ciclo começar, para ignorar picos breves de liga/desliga.", "label": "Duração de Início" }, "start_energy_threshold": { - "doc": "Energia (potência x tempo) que o aparelho deve consumir antes de FUNCIONAR. Um breve pico de alta potência tem energia muito baixa e é ignorado, evitando falsas partidas.", + "doc": "Energia que o eletrodoméstico precisa consumir antes de o ciclo contar como em andamento. Um pico breve de potência consome pouca energia, então não inicia um ciclo.", "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. 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.", + "doc": "A potência deve subir acima deste valor para iniciar um ciclo. Baixo demais: o consumo em espera causa falsas partidas. Alto demais: programas de partida lenta (enchimento a frio) são perdidos.", "label": "Limite de Início" }, "stop_threshold_w": { - "doc": "A energia deve cair abaixo deste nível antes que a contagem regressiva do atraso de desligamento comece. Defina-o abaixo do limite inicial - a lacuna entre eles é a banda de histerese que evita oscilações. Se definido para um valor demasiado elevado, as fases de baixa potência (paragens de enxaguamento, anti-rugas) acionam falsamente a sequência final.", + "doc": "A potência deve cair abaixo deste valor antes de o atraso de desligamento começar a contar. Mantenha abaixo do Limite de Início. Se for alto demais, fases de baixa potência (pausas de enxágue, antiamasso) iniciam por engano a contagem para o fim.", "label": "Limite de Parada" }, "switch_entity": { - "doc": "Interruptor opcional desligado na pausa e ligado novamente ao reiniciar, usado junto com \"Pausa também corta energia\".", + "doc": "Interruptor opcional que desliga na pausa e liga de novo ao retomar, com \"Pausa Também Corta Energia\".", "label": "Entidade do Switch" }, "watchdog_interval": { - "doc": "Com que frequência o watchdog em segundo plano verifica sensores paralisados e tempos limite decorridos. Padrão 30 seg.", + "doc": "Com que frequência a verificação em segundo plano procura sensores travados e tempos limite expirados. Padrão 30 s.", "label": "Intervalo do Watchdog" }, "notify_milestone_message": { @@ -1477,127 +1453,82 @@ "label": "Mensagem de marco" }, "notify_milestones": { - "doc": "Contagens de ciclos separadas por vírgulas que acionam uma notificação de comemoração única quando são atingidas (ex. 50, 100, 500, 1000). Em branco desativa as notificações de marcos.", + "doc": "Contagens de ciclos que disparam uma notificação única de comemoração, separadas por vírgula (ex.: 50, 100, 500, 1000). Em branco desativa.", "label": "Marcos de ciclos" }, "notify_quiet_end_hour": { - "doc": "Fim da janela de não perturbe (0-23). As notificações retidas são entregues nesta hora. Deixe em branco para desativar.", + "doc": "Fim da janela de não perturbe (0-23); as notificações retidas são enviadas nesse horário. Em branco desativa.", "label": "Fim do horário silencioso" }, "notify_quiet_start_hour": { - "doc": "Início de uma janela de não perturbe (0-23). As notificações de conclusão, lembretes e de roupa lavada que ocorreriam durante o horário silencioso são retidas e entregues quando a janela termina. Deixe em branco para desativar. Suporta janelas que cruzam a meia-noite (ex. início 22, fim 7).", + "doc": "Início de uma janela de não perturbe (0-23). Notificações de fim, lembrete e roupa limpa nesta janela ficam retidas até ela terminar. Pode passar da meia-noite (ex.: 22 a 7). Em branco desativa.", "label": "Início do horário silencioso" }, "peak_rate_message": { - "doc": "Texto personalizado opcional para a dica de tarifa de pico anexada à notificação de início. Variáveis de modelo: {device}, {price}. Em branco usa o padrão interno.", + "doc": "Texto próprio para o aviso de tarifa de pico. Variáveis de modelo: {device}, {price}. Em branco usa o padrão.", "label": "Mensagem de tarifa de pico" }, "peak_rate_threshold": { - "doc": "Quando um ciclo começa e o preço atual por kWh é igual ou superior a este valor, uma dica de tarifa de pico é anexada à notificação de início. 0 ou em branco desativa a dica.", + "doc": "Se o preço por kWh for igual ou maior que este valor quando um ciclo começa, adiciona um aviso de tarifa de pico à notificação de início. 0 ou em branco desativa.", "label": "Limite de tarifa de pico (por kWh)" }, - "dtw_bandwidth": { - "label": "Tolerância de correspondência de forma", - "doc": "Quanta deformação temporal o sistema de correspondência pode aplicar a um ciclo para ajustá-lo à forma de um programa salvo. Um valor mais alto tolera maior variação de velocidade; alto demais corre o risco de corresponder ao programa errado." - }, - "corr_weight": { - "label": "Peso da forma em relação ao nível", - "doc": "Equilíbrio entre a forma da curva e o nível de potência absoluto ao pontuar uma correspondência. Um valor mais alto favorece a forma; um mais baixo favorece a correspondência do nível de potência." - }, - "duration_weight": { - "label": "Peso da duração", - "doc": "O quanto é preferido um programa cuja duração típica corresponde a este ciclo." - }, - "energy_weight": { - "label": "Peso da energia", - "doc": "O quanto é preferido um programa cujo consumo de energia típico corresponde a este ciclo." - }, "store_brand": { "label": "Marca do eletrodoméstico", - "doc": "Opcional. A marca do eletrodoméstico, escolhida no catálogo da comunidade. Usada para encontrar e compartilhar gravações de referência correspondentes. Deixe em branco se não usar os recursos online." + "doc": "Opcional. A marca do seu eletrodoméstico no catálogo da comunidade, para encontrar e compartilhar gravações de referência. Deixe em branco se não usar os recursos online." }, "store_model": { "label": "Modelo do eletrodoméstico", - "doc": "Opcional. O modelo do eletrodoméstico, escolhido no catálogo da comunidade depois de definida a marca. Se o seu modelo não estiver na lista, você pode adicioná-lo ao catálogo." + "doc": "Opcional. O modelo do seu eletrodoméstico no catálogo da comunidade (escolha uma marca antes). Se ele não estiver na lista, você pode adicioná-lo." }, "show_contributor": { "doc": "Mostrar a atribuição \"por \" em eletrodomésticos e ciclos de referência da comunidade." }, - "enable_phase_matching": { - "label": "Tempo restante com base nas fases", - "doc": "Divide cada ciclo em andamento em fases (aquecimento, lavagem, centrifugação) e distribui o tempo restante por fase, combinado com a estimativa clássica - apoiando-se na repartição por fases no início do ciclo e na estimativa clássica perto do fim. Isso personaliza a contagem regressiva de acordo com o tempo que sua máquina realmente aquece e funciona, o que é mais perceptível na primeira metade de um ciclo. Desativado = apenas a estimativa clássica. Apenas a exibição do tempo restante é afetada; a correspondência de programas e a detecção de ciclos permanecem inalteradas." - }, - "keep_min_score": { - "label": "Score mín de correspondência", - "doc": "Pontuação de similaridade mínima que um candidato precisa para permanecer na disputa. O padrão 0,1 é deliberadamente permissivo - admite até candidatos fracos e depende de etapas posteriores 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", - "doc": "O quanto o score de alinhamento por deformação temporal (DTW) substitui o score central da etapa 2. 0 = usar apenas o score da etapa 2, 1 = usar apenas o score DTW, 0,5 (padrão) = mistura equilibrada. Aumente para confiar mais no alinhamento Warp quando os programas têm níveis de potência semelhantes mas padrões temporais diferentes." - }, - "dtw_ensemble_w": { - "label": "Mix ensemble Warp", - "doc": "No modo DTW ensemble (o padrão), o peso em L1 escalado versus DTW derivativo (DDTW). 1,0 = apenas L1 escalado, 0 = apenas DDTW, 0,7 = padrão. A variante L1 escalada compara os níveis de potência; a variante derivativa reage a como a potência muda ao longo do tempo. Ensemble combina ambos." - }, - "dtw_ddtw_scale": { - "label": "Escala Warp derivada", - "doc": "Distância de meia saturação para a pontuação DTW derivativa. A pontuação é reduzida à metade quando a distância DDTW é igual a este valor. Menor = mais sensível às diferenças de forma. O padrão 30 está calibrado para rastros de potência típicos de eletrodomésticos. Afeta apenas os modos DTW ensemble e ddtw." - }, - "dtw_refine_top_n": { - "label": "Contagem de refinamento Warp", - "doc": "Quantos candidatos principais da etapa 2 são reavaliados pelo DTW. O DTW é mais caro, por isso é aplicado apenas aos N melhores candidatos. Padrão 5. Aumente (para 7-9) se o perfil correto às vezes só chega ao 4.º ou 5.º lugar após a etapa 2 - o DTW pode recuperá-lo. Reduzir acelera ligeiramente a correspondência." - }, - "duration_scale": { - "label": "Nitidez de duração", - "doc": "Precisão da penalidade de concordância de duração na etapa 4. Este é o log-razão em que o score de concordância é reduzido à metade. Menor = mais rigoroso: uma discordância de duração penaliza mais. O padrão 0,175 corresponde a cerca de 18% de tolerância de duração a meio peso. Use em conjunto com o Peso de duração." - }, - "energy_scale": { - "label": "Nitidez de energia", - "doc": "Precisão da penalidade de concordância de energia. Menor = mais rigoroso: uma discordância de energia penaliza mais. O padrão 0,25 é intencionalmente mais permissivo do que a Precisão de duração, porque a energia varia com a carga. Use em conjunto com o Peso de energia." - }, "power_profile_interval_min": { "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." + "doc": "Tamanho do intervalo do atributo power_profile (watts médios por intervalo) usado por planejadores como EMHASS e tibber_prices. Menor mantém picos curtos; maior suaviza a curva. Padrão 15 min. 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." + "doc": "Apenas iOS. Atualizar o progresso ao vivo em silêncio, com uma prioridade que economiza bateria e que o iOS pode agrupar. A primeira atualização e as notificações de início e fim continuam alertando. Desligado: cada atualização alerta." }, "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." + "doc": "Apenas Android. Manter a notificação de progresso ao vivo ao tocar. Desligado: um toque a fecha." }, "notify_live_click_action": { "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." + "doc": "O que um toque na notificação deste eletrodoméstico abre. Em branco: o painel WashData neste eletrodoméstico. Informe um caminho de dashboard (ex.: /lovelace/laundry) ou uma URL, ou \"none\" para nenhuma ação ao tocar." }, "door_opens_at_end": { "label": "Porta Abre Automaticamente ao Final", - "doc": "Para máquinas de lavar louça que abrem a porta automaticamente ao final do ciclo para secar (AirDry e similares). Com esta opção ativa, a abertura de porta durante um ciclo em execução não o pausa indefinidamente; em vez disso, se a porta permanecer aberta pelo tempo de espera configurado abaixo, o WashData considera o ciclo como concluído. Uma abertura breve (para adicionar um item) é ignorada. Requer uma Entidade de Sensor de Porta." + "doc": "Para lava-louças que abrem a porta no fim para secar (ex.: AirDry). Uma porta que fica aberta pelo tempo de espera abaixo termina o ciclo em vez de pausá-lo; uma abertura breve é ignorada. Requer uma Entidade do Sensor de Porta." }, "door_end_dwell_seconds": { "label": "Tempo de Espera da Porta Aberta", - "doc": "Por quanto tempo a porta deve permanecer aberta antes de o WashData encerrar o ciclo, quando a opção \"Porta Abre Automaticamente ao Final\" está ativa. Longo o suficiente para ignorar a adição rápida de um prato (padrão 60 s), curto o suficiente para encerrar prontamente assim que a máquina abre a porta." + "doc": "Com \"Porta Abre Automaticamente ao Final\" ligado: quanto tempo a porta deve ficar aberta antes de o ciclo terminar. O suficiente para ignorar a colocação de um prato (padrão 60 s)." }, "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." + "doc": "Quais tipos de ciclos moldam a curva e a correspondência de cada programa. Desmarcar um tipo não apaga nada; útil se você não confia nos dados importados. As estatísticas sempre contam apenas os ciclos desta máquina. Desmarcar todos é ignorado." }, "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." + "doc": "Cobrar cada ciclo pelo preço de cada momento em que funcionou, em vez do preço no fim. Requer uma entidade de preço. Processar histórico (Diagnósticos) recalcula os ciclos antigos." }, "unload_confirm_entity": { "label": "Entidade de Confirmação de Descarga", - "doc": "Entidade opcional cuja ativação significa que a carga foi retirada, para máquinas que não podem ter um sensor de porta. Serve qualquer tipo: um botão Zigbee ou NFC, um auxiliar input_button, um sensor de movimento na frente da máquina, uma cena. Qualquer mudança de estado conta como confirmação, exceto desligar e ficar indisponível. Definir isso também ativa o lembrete de descarga em um dispositivo sem Entidade do Sensor de Porta." + "doc": "Opcional. Qualquer entidade que confirme que a carga foi retirada, para máquinas sem sensor de porta (um botão, input_button, sensor de movimento, cena). Toda mudança de estado conta, exceto desligar ou ficar indisponível. Também ativa o lembrete de descarga." }, "unload_track_without_door": { "label": "Confirmar a Descarga Manualmente", - "doc": "Executa o lembrete de descarga em um dispositivo sem Entidade do Sensor de Porta e sem Entidade de Confirmação de Descarga, e você confirma a descarga pela sua própria automação: pressione a entidade de botão \"Marcar como descarregado\" ou chame o serviço ha_washdata.mark_unloaded. É ignorado quando uma Entidade do Sensor de Porta está definida, pois ela já fornece o sinal." + "doc": "Usar o lembrete de descarga sem sensor de porta nem entidade de confirmação. Confirme a descarga pela sua própria automação com o botão \"Marcar como descarregado\" ou o serviço ha_washdata.mark_unloaded. Ignorado quando há uma Entidade do Sensor de Porta definida." }, "notify_icon_color": { "label": "Cor da Notificação", - "doc": "Cor de destaque para as notificações deste eletrodoméstico, para distinguir de relance uma lavadora, uma secadora e uma lava-louças. O iOS colore o ícone e a barra de progresso da Atividade ao Vivo na Tela Bloqueada e na Dynamic Island. O Android recebe a mesma cor, mas o que ele faz com ela depende do celular: alguns colorem o ícone e muitos (em especial o Samsung One UI) a ignoram por completo. Ambos entendem o formato hexadecimal (por exemplo, #03A9F4). Deixe em branco para o padrão da plataforma." + "doc": "Cor de destaque para as notificações deste eletrodoméstico, para diferenciar facilmente lavadora, secadora e lava-louças. No iOS preenche o círculo atrás do ícone e colore a barra da Live Activity. Muitos celulares Android (principalmente Samsung) ignoram. Use hex, ex.: #03A9F4. Em branco: padrão da plataforma." + }, + "share_name": { + "doc": "Mostrar o nome da sua conta como autor dos ciclos que você compartilha. Desligado: compartilhados sem nome." } }, "setting_group": { @@ -1625,9 +1556,6 @@ "door_pause": { "label": "Porta e Pausa" }, - "dtw_refinement": { - "label": "Refinamento DTW" - }, "duration_gates": { "label": "Portas de Duração" }, @@ -1704,13 +1632,18 @@ "rebuilding": "Reconstruindo…", "analyzing": "Analisando…", "resetting": "Redefinindo…", - "reverting": "Revertendo…", "trimming": "Recortando…", "splitting": "Dividindo…", + "stalled": "Travado", "deleting": "Excluindo…", "imported": "Importado", "preparing": "Preparando…", - "unlabelling": "Removendo rótulo…" + "unlabelling": "Removendo rótulo…", + "working": "Processando…", + "match_uncertain": "Incerto: {top} ou {runner}", + "match_uncertain_single": "Incerto: talvez {top}", + "match_sure": "~{pct}% de certeza", + "match_uncertain_tip": "O WashData ainda não se decidiu por um programa. O número mostra com que frequência a opção principal, {top}, acertou em ciclos anteriores quando estava tão à frente. Apenas informativo." }, "tab": { "advanced": "Avançado", @@ -1740,6 +1673,11 @@ "wrong_profile": "Perfil errado" }, "toast": { + "task_name_required": "Dê um nome à tarefa", + "task_added": "Tarefa adicionada", + "task_save_failed": "Não foi possível salvar a tarefa: {error}", + "task_removed": "Tarefa removida", + "maint_logged": "Registrado como feito: {task}", "catalog_refreshed": "Catálogo da comunidade atualizado", "access_saved": "Controle de acesso salvo", "all_wiped": "Todos os dados apagados", @@ -1747,7 +1685,6 @@ "auto_label_complete": "Rotulagem automática concluída", "automation_deleted": "Automação excluída", "correction_submitted": "Correção enviada", - "cycle_deleted": "Ciclo excluído", "cycle_force_stopped": "Ciclo parado forçadamente", "cycle_labelled": "Ciclo rotulado", "cycle_paused": "Ciclo pausado", @@ -1763,11 +1700,9 @@ "group_deleted": "Grupo excluído", "group_name_required": "O nome do grupo é obrigatório", "group_saved": "Grupo salvo", - "import_successful": "Importação bem-sucedida; a integração está recarregando", "json_required": "Os dados JSON são obrigatórios", "legacy_removed": "Ações legadas removidas", "logs_exported": "Registros exportados", - "matching_reverted": "Pesos de correspondência revertidos para o padrão", "min_2_profiles": "Selecione pelo menos 2 perfis para um grupo", "ml_training_no_improvement": "Treinamento concluído: base mantida (sem melhoria)", "models_reverted": "Modelos no dispositivo revertidos para a base", @@ -1777,8 +1712,8 @@ "pg_preset_loaded": "Predefinição \"{name}\" carregada", "pg_preset_saved": "Predefinição \"{name}\" salva", "pg_sugg_loaded": "{n} valor(es) sugerido(s) carregado(s) - execute a Área de testes para comparar", - "pg_sugg_ml_loaded": "{n} valor(es) calibrado(s) por ML carregado(s) - execute a Área de testes para comparar", "phase_name_required": "O nome da fase é obrigatório", + "phase_reset": "Fase \"{name}\" restaurada para a versão integrada", "phase_updated": "Fase atualizada", "phases_saved": "Fases salvas", "preferences_saved": "Preferências salvas", @@ -1825,15 +1760,11 @@ "store_name_required": "Digite um nome de perfil", "store_import_failed": "Falha na importação: {error}", "store_imported": "Importado para {profile}", - "store_program_required": "Digite um nome de programa", "store_no_appliance": "Defina primeiro a marca e o modelo do seu eletrodoméstico em Configurações.", "store_share_failed": "Falha no compartilhamento: {error}", "store_shared": "Compartilhado na loja da comunidade, aguardando revisão.", "appliance_added": "Eletrodoméstico adicionado, aguardando aprovação", "contrib_device_changed": "Adicionado à loja da comunidade, mas o dispositivo selecionado mudou enquanto o formulário estava aberto. Selecione-o em Configurações.", - "thanks_confirming": "Obrigado por confirmar", - "device_approved": "Aprovado pela comunidade", - "rating_saved": "Avaliação de qualidade salva", "brand_added": "Marca adicionada, aguardando aprovação", "profile_added": "Perfil adicionado, aguardando aprovação", "saved_except_conflicts": "Salvo. Corrija os conflitos destacados para salvar o restante.", @@ -1856,11 +1787,52 @@ "odometer_saved": "Total de ciclos salvo", "odometer_save_failed": "Não foi possível salvar o total: {error}", "store_pick_profile_share": "Escolha um perfil no qual compartilhar", - "remove_failed": "Falha ao remover: {error}" + "remove_failed": "Falha ao remover: {error}", + "analysis_complete": "Análise concluída: {count} sugestão(ões)", + "analysis_failed": "Falha na análise: {error}", + "auto_detect_failed": "Falha na detecção automática: {error}", + "auto_label_failed": "Falha na rotulagem automática: {error}", + "could_not_load_cycle": "Não foi possível carregar o ciclo: {error}", + "create_failed": "Falha ao criar: {error}", + "cycles_deleted": "{count} ciclo(s) excluído(s)", + "debug_cleared": "{count} rastros de depuração limpos", + "discard_failed": "Falha ao descartar: {error}", + "error": "Erro: {error}", + "export_failed": "Falha ao exportar: {error}", + "import_failed": "Falha ao importar: {error}", + "invalid_json": "\"{key}\" não é um JSON válido; corrija ou limpe o campo antes de salvar.", + "label_failed": "Falha ao rotular: {error}", + "ml_training_promoted": "Treinamento concluído: {count} modelo(s) promovido(s)", + "pause_failed": "Falha ao pausar: {error}", + "phase_created": "Fase \"{name}\" criada", + "phase_deleted": "Fase \"{name}\" excluída", + "profile_created": "Perfil \"{name}\" criado", + "rename_failed": "Falha ao renomear: {error}", + "save_failed": "Falha ao salvar: {error}", + "split_complete": "Dividido em {count} ciclos", + "start_failed": "Falha ao iniciar: {error}", + "stop_failed": "Falha ao parar: {error}", + "update_failed": "Falha ao atualizar: {error}", + "store_download_skipped": "{n} gravação(ões) ignorada(s): muito curtas, com lacunas demais ou já presentes no seu dispositivo.", + "store_import_low_quality": "Esta gravação é muito curta, tem lacunas demais ou é implausível para ser importada.", + "store_import_duplicate": "Esta gravação já está no seu dispositivo.", + "device_approved": "Aprovado pela comunidade", + "thanks_confirming": "Obrigado por confirmar", + "rating_saved": "Avaliação de qualidade salva", + "store_download_partial": "Não foi possível baixar {n} programa(s); tente novamente mais tarde.", + "import_no_restore_point": "Importado, mas não foi possível salvar um ponto de restauração, então esta importação não pode ser desfeita", + "import_undo_failed": "Não foi possível desfazer a importação: {error}", + "import_undone": "Importação desfeita: {profiles} perfil(is) e {cycles} ciclo(s) restaurados", + "auto_label_cancelled": "Rotulagem automática cancelada após rotular {count} ciclos.", + "auto_label_cancelled_one": "Rotulagem automática cancelada após rotular {count} ciclo.", + "auto_label_cancelled_many": "Rotulagem automática cancelada após rotular {count} ciclos.", + "auto_label_cancelled_other": "Rotulagem automática cancelada após rotular {count} ciclos.", + "auto_label_done": "Rotulagem automática concluída: {count} ciclos rotulados, {skipped} sem rótulo.", + "auto_label_done_one": "Rotulagem automática concluída: {count} ciclo rotulado, {skipped} sem rótulo.", + "auto_label_done_many": "Rotulagem automática concluída: {count} ciclos rotulados, {skipped} sem rótulo.", + "auto_label_done_other": "Rotulagem automática concluída: {count} ciclos rotulados, {skipped} sem rótulo." }, "suggestion": { - "both_agree": "WashData recomenda", - "calibrated_label": "Calibrado", "observed_label": "Observado", "impact": { "auto_label_confidence": { @@ -1875,10 +1847,6 @@ "higher": "Encerra apenas ciclos que consumiram energia substancial", "lower": "Encerra também ciclos curtos ou de baixa energia" }, - "end_repeat_count": { - "higher": "Exige que o sinal de fim se repita mais vezes – mais robusto, ligeiramente mais lento", - "lower": "Termina após menos repetições – detecção mais rápida, maior risco de fim falso" - }, "learning_confidence": { "higher": "Aprende apenas com correspondências muito confiantes – mais lento mas mais confiável", "lower": "Aprende com mais ciclos; o modelo pode ser menos preciso" @@ -1917,40 +1885,25 @@ } }, "reason": { - "auto_label_confidence": "Confiança no percentil 15 de {cycles} rótulos automáticos que o usuário nunca corrigiu ({p15}).", "completion_min_seconds": "Metade da duração p05 dos ciclos limpos ({minutes} min) em {cycles} ciclos; filtra ciclos fantasma.{excl}", - "dead_zone_batch": "Mantido curto (janela mediana de instabilidade inicial em {cycles} ciclos = {s}s) para que um fim real seja detectado prontamente.", - "dead_zone_from_dip": "Com base na queda inicial de potência detectada em {s}s.", - "end_energy_default": "Linha de base recomendada padrão para o filtro de ruído de fim de ciclo.", - "end_energy_false": "Energia p95 de falsos fins ({p95}Wh) em {cycles} ciclos, com piso em 0.2% da energia mediana do ciclo ({floor}Wh).", - "end_energy_no_false": "Sem falsos fins em {cycles} ciclos; com piso em 0.2% da energia mediana do ciclo ({floor}Wh).", - "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.", + "match_interval_floored": "O programa mais curto ({shortest}s) limitaria isto a {cap}s, abaixo do mínimo de {minimum}s, então usa-se o mínimo: {persistence} correspondências seguidas levam {budget}s, mais de {pct}% desse programa. A correspondência só roda quando chega uma leitura (mediana={median}s), então 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.", "min_off_gap": "Com base em {gaps} intervalos entre ciclos observados (p05={p05}s). Piso do dispositivo: {floor}s.", "min_off_gap_bridge": "Dimensionado para cobrir o período de silêncio mais longo dentro de um ciclo: {span}s + margem de 60s, de {spans} intervalos cobertos em {cycles} ciclos limpos (fica abaixo do seu intervalo mínimo de {ceiling}s entre cargas consecutivas).", "min_power": "40% da menor potência ativa p05 ({p05}W) em {cycles} ciclos limpos, mantendo o limiar de desligamento abaixo do consumo real.{excl}", - "ml_auto_label": "Menor confiança que o modelo de qualidade ainda classificou como limpa (p10 de {cycles} ciclos limpos = {p10}).", - "ml_end_repeat": "{false}/{total} ciclos ({pct}%) tiveram uma pausa que o detector de fim pontuou em >50%; exigir {val} confirmação(ões) de fim.", - "ml_off_delay": "Pausas confirmadas pelo detector de fim: p95 {p95}s + margem de 60s, de {pauses} pausas verificadas pelo modelo em {cycles} ciclos (piso {floor}s).", "no_update_timeout": "Com base na cadência de atualização observada (p95={p95}s) * 20 (mín. 60s).", "off_delay_cadence": "Com base na cadência de atualização observada (p95={p95}s) * 5", "off_delay_device_floor": "Usado o mínimo seguro específico do dispositivo para {device} ({floor}s).", "off_delay_generic_floor": "Usado o mínimo seguro genérico ({floor}s).", "off_delay_pauses": "Dimensionado para durar mais que pausas reais: pausa intraciclo p95 {p95}s + margem de 60s, de {pauses} pausas em {cycles} ciclos limpos (piso {floor}s).", - "profile_match_threshold": "Confiança p10 de {cycles} rótulos automáticos corretos ({p10}); piso seguro para confirmação ao vivo.", - "sampling_interval": "Intervalo mediano de atualização observado em {cycles} ciclos limpos ({si}s).{excl}", - "smoothing_window": "Dimensionado para suavizar ~30s de leituras a uma amostragem de {si}s ({samples} amostras).", - "start_duration": "Mantido curto (~um intervalo de amostra de {si}s) para que a detecção comece o mais cedo possível, ignorando ainda picos de amostra única.", - "thr_batch": "Mantido logo acima da menor potência ativa p05 em {cycles} ciclos ({p05}W) para que um início seja captado o mais cedo possível e o limiar de parada permaneça abaixo da menor potência de funcionamento da máquina.", - "tol_per_profile": "p75 da variância de duração por perfil em {profiles} perfis ({cycles} ciclos); perfis consistentes não são penalizados.", - "tol_pooled": "Com base na variância de duração agregada de {cycles} ciclos rotulados recentes (desvio p95={dev}).", - "watchdog": "Mantido tão baixo quanto é seguro (logo acima do intervalo de atualização p95 de {p95}s e pelo menos 2x o intervalo de amostragem de {median}s, mín. 30s) para que travamentos sejam detectados rapidamente sem paradas falsas." + "standby_floor": "Cada ciclo recente ainda consumia {idle}W quando terminou, então um limite abaixo disso nunca poderá detectar o desligamento do eletrodoméstico. Mantido acima; os {cycles} ciclos limpos registrados nunca ficaram abaixo desse valor por mais de {pause}s.", + "thr_batch": "Logo acima da menor potência ativa (p05) em {cycles} ciclos ({p05}W), para pegar as partidas cedo e manter o limite de parada abaixo da menor potência de funcionamento.", + "watchdog": "Mantido tão baixo quanto é seguro (logo acima do intervalo de atualização p95 de {p95}s e pelo menos 2x o intervalo de amostragem de {median}s, mín. 30s) para que travamentos sejam detectados rapidamente sem paradas falsas.", + "end_energy_floor": "Elevado ao mínimo implicado pelo limite de parada ({stop} W) durante o atraso de desligamento ({off_delay} s); abaixo dele, o ciclo só pode terminar por um mecanismo alternativo.", + "max_duration_ratio_default": "De volta ao padrão: um teto mais baixo rejeita o programa certo sempre que um ciclo dura mais que o normal.", + "min_duration_ratio_default": "De volta ao padrão: um piso mais alto impede que um ciclo em andamento seja reconhecido até que esteja bem adiantado." }, "exclusions": { "summary": "Excluído(s) {total} ciclo(s) mal detectado(s): {parts}.", @@ -1968,13 +1921,18 @@ "no_power": "sem energia", "no_active_power": "sem energia ativa" } - } + }, + "now_value": "(agora {v})" }, "trend": { "down": "Tendência de queda", "up": "Tendência de alta" }, "maint": { + "salt": "Reabastecer o sal", + "rinse_aid": "Reabastecer o secante", + "lint_filter": "Limpar o filtro de fiapos", + "condenser_clean": "Limpar o condensador", "bearing_service": "Manutenção de rolamentos", "descale": "Descalcificar", "drum_clean": "Limpar o tambor", @@ -1983,34 +1941,22 @@ }, "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", + "smart_termination_duration_ratio": "Fração da duração média do programa que um ciclo deve atingir antes de o Encerramento inteligente poder finalizá-lo mais cedo; reduza se os tempos de execução variarem", + "anti_crease_finalize_ratio": "Fração da duração média do programa que um ciclo deve atingir antes de a cauda antiamasso poder finalizá-lo; reduza em secadoras, deixe as lavadoras no 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_exit_power": "Nível considerado silêncio entre pulsos de giro; só tem efeito acima do Limite de Parada", "anti_wrinkle_idle_timeout": "Tempo de silêncio permitido entre dois pulsos de giro antes de o modo antiamasso terminar", "anti_wrinkle_max_duration": "Um pulso mais longo do que isso encerra o modo antiamasso e abre um novo ciclo", "anti_wrinkle_max_power": "Um pulso acima disso encerra o modo antiamasso e abre um novo ciclo", "completion_min_seconds": "Menor duração que conta como ciclo real", - "end_repeat_count": "Leituras baixas seguidas antes de terminar", "interrupted_min_seconds": "Ciclos curtos marcados como interrompidos", "min_off_gap": "Intervalo necessário para separar dois ciclos", "off_delay": "Segundos de baixa potência antes de o ciclo terminar", "start_duration_threshold": "Segundos acima do limite para confirmar o início", "start_threshold_w": "Watts mínimos para contar como iniciado", "stop_threshold_w": "Abaixo disso, a máquina conta como desligada", - "dtw_bandwidth": "Etapa 3: banda de deformação Sakoe-Chiba (0 = DTW desativado; padrão 0,2 = 20% do comprimento do ciclo)", - "corr_weight": "Etapa 2: equilíbrio entre a forma da curva (correlação) e o nível de potência (MAE); padrão 0,45", - "duration_weight": "Etapa 4: o quanto a concordância de duração afeta a pontuação final (padrão 0,22)", - "energy_weight": "Etapa 4: o quanto a concordância de energia afeta a pontuação final (padrão 0,22)", - "profile_match_min_duration_ratio": "Etapa 1: duração mínima do ciclo como fração da duração do perfil; padrão 0,1 (10%)", - "profile_match_max_duration_ratio": "Etapa 1: duração máxima do ciclo como fração da duração do perfil; padrão 1,8 (180%)", - "keep_min_score": "Etapa 2: pontuação mínima para permanecer na disputa; o padrão 0,1 admite candidatos fracos, etapas posteriores escolhem o melhor", - "dtw_blend": "Etapa 3: 0 = apenas pontuação central, 1 = apenas pontuação DTW, 0,5 = mistura equilibrada (padrão)", - "dtw_ensemble_w": "Etapa 3 (modo ensemble): peso em L1 escalado vs DTW derivativo; o padrão 0,7 favorece a sensibilidade ao nível", - "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)", + "profile_match_min_duration_ratio": "Duração mínima do ciclo como fração da duração do perfil; padrão 0,1 (10%)", + "profile_match_max_duration_ratio": "Duração máxima do ciclo como fração da duração do perfil; padrão 1,8 (180%)", "curve_preroll_seconds": "Segundos de leituras de tentativas de início interrompidas incorporados no começo da curva de um ciclo; 0 = desativado" }, "col": { @@ -2018,12 +1964,14 @@ "status_tip": "Resultado do ciclo: Concluído (fim natural), Interrompido (queda abrupta de potência), Parado forçadamente (manual) ou Precisa de revisão (feedback pendente).", "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. 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.", + "energy_tip": "Energia total consumida (kWh), calculada a partir da potência ao longo do tempo.", + "cost_tip": "Custo de energia deste ciclo, fixado no fim. Com Custo Ponderado no Tempo ligado, cada momento é cobrado pelo seu próprio preço; caso contrário, pelo preço no fim. Defina um preço em Configurações para preencher isto.", "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.", - "cost_dynamic_tip": "Ponderado no tempo: cobrado pelo preço em vigor durante o ciclo (preço efetivo {price} {cur}/kWh)." + "cost_dynamic_tip": "Ponderado no tempo: cobrado pelo preço em vigor durante o ciclo (preço efetivo {price} {cur}/kWh).", + "dbg_conf": "Conf.", + "dbg_mae": "MAE", + "dbg_corr": "Corr." }, "stat": { "avg": "méd.", @@ -2047,7 +1995,10 @@ "notify_finish": "Uma notificação de conclusão seria enviada.", "notify_milestone": "Uma notificação de etapa seria enviada.", "notify_held": "Uma notificação foi retida durante o horário de silêncio.", - "finished": "O ciclo atingiu um estado final e foi encerrado." + "finished": "O ciclo atingiu um estado final e foi encerrado.", + "match_reverted": "O programa voltou para a detecção: sua pontuação caiu muito abaixo do próprio pico.", + "verified_pause": "O alinhamento ao vivo confirmou (ou liberou) uma pausa dentro do programa. Enquanto ela se mantiver, o ciclo não é encerrado.", + "stalled": "O ciclo ficou no consumo de espera por mais tempo do que este programa jamais pausa nesse ponto, com trabalho pendente. Exibido como Pausado (Travado); o ciclo continua aberto." }, "store": { "search_ph2": "Pesquisar por marca ou modelo…", @@ -2056,11 +2007,7 @@ "programs_count": "Programas: {n}", "browse": "Navegar", "device": "Dispositivo", - "favorites": "Favoritos", - "search_ph": "Pesquisar por marca ou modelo…", "no_results": "Nenhum eletrodoméstico correspondente encontrado. Tente outra pesquisa.", - "n_programs": "{n} programas", - "n_cycles": "{n} ciclos", "no_programs": "Ainda não há programas compartilhados para este eletrodoméstico.", "no_cycles": "Ainda não há ciclos de referência compartilhados para este programa.", "anon": "anônimo", @@ -2072,19 +2019,21 @@ "merge_target": "Mesclar no perfil", "description": "Descrição (opcional)", "contributed_by": "por {name}", + "website": "Site da loja ↗", + "website_tip": "Abrir o site da loja da comunidade", + "favorites": "Favoritos", "rating_summary": "★ {avg} ({n})", "no_ratings": "Ainda sem avaliações", - "website": "Site da loja ↗", - "website_tip": "Abrir o site da loja da comunidade" + "unreachable": "Não foi possível acessar a loja da comunidade. Verifique sua conexão e tente novamente." }, "settings": { "gear": { - "title": "Configurações" + "title": "Preferências" } }, "link": { - "manual": "Manual ↗", - "add_appliance": "Adicione seu eletrodoméstico" + "maintenance_supporter": "Maintenance Supporter no GitHub ↗", + "manual": "Manual ↗" }, "tip": { "add_brand": "Marca não listada? Adicione-a ao catálogo da comunidade", @@ -2118,7 +2067,13 @@ "rebuild": { "envelopes": "Reconstruindo envelopes" }, - "cancelling": "Cancelando..." + "cancelling": "Cancelando...", + "auto_label": { + "running": "Rotulando ciclos automaticamente" + }, + "store_download": { + "running": "Baixando da loja" + } }, "setup": { "hdr": { @@ -2127,26 +2082,24 @@ }, "phase0": { "washer": "O WashData já está detectando seus ciclos. Grave o primeiro ciclo para ativar os nomes dos programas e as estimativas de tempo.", - "dishwasher": "O WashData está monitorando. As máquinas de lavar louça têm ciclos complexos – é altamente recomendável gravar o primeiro ciclo. Se um ciclo detectado demorar muito, use o editor de ciclos para recortá-lo antes de salvá-lo como perfil.", + "dishwasher": "O WashData está observando. Ciclos de lava-louças são complexos, então grave seu primeiro ciclo. Se um ciclo detectado durar demais, corte-o no editor de ciclos antes de salvá-lo como perfil.", "generic": "O WashData está monitorando. Grave ou rotule um ciclo detectado para começar a criar perfis." }, "phase1a": { - "labelled": "Ótimo começo – seu primeiro programa está salvo. Para dados mais precisos, considere gravar o próximo ciclo com o widget de gravação." + "labelled": "Bom começo: seu primeiro programa está salvo. Para dados mais limpos, grave seu próximo ciclo com o gravador." }, "phase1b": { - "recorded": "Sua gravação foi salva como {profile_name}. Agora grave ou rotule seus outros programas habituais para ampliar a cobertura." + "recorded": "Sua gravação foi salva como {profile_name}. Agora grave ou rotule seus outros programas comuns." }, "phase1c": { - "verify": "Você tem {count} programas da comunidade. Execute um ciclo para verificar se o WashData os reconhece corretamente – a correspondência vai melhorar à medida que o dispositivo acumula seu próprio histórico." + "verify": "Você tem {count} programas da comunidade. Execute um ciclo para verificar se o WashData o reconhece; a correspondência melhora conforme seu dispositivo cria o próprio histórico." }, "phase2": { - "cluster": "O WashData encontrou {count} ciclos que não correspondem a nenhum programa salvo – eles se parecem entre si. Deseja criar um novo perfil para eles?", + "cluster": "O WashData viu {count} ciclos parecidos que não correspondem a nenhum programa salvo. Criar um novo perfil para eles?", "unmatched": "Seu último ciclo não correspondeu a nenhum programa salvo. É um novo programa?" }, "phase3": { - "suggestions": "O WashData tem recomendações de configuração baseadas no histórico de ciclos – revise-as para melhorar a precisão da detecção.", - "groups": "Alguns dos seus perfis parecem o mesmo programa em temperaturas diferentes. Organize-os em um grupo para uma correspondência mais confiável.", - "phases": "Adicione fases de programa a {profile_name} para estimativas mais precisas do tempo restante." + "suggestions": "O WashData tem sugestões de configuração com base no seu histórico de ciclos. Revise-as para melhorar a detecção." }, "phase4": { "healthy": "Este dispositivo está totalmente configurado ({profile_count} perfis)." @@ -2159,8 +2112,6 @@ "create_from_cluster": "Criar perfil a partir destes ciclos", "create_profile": "Criar um perfil para este programa", "review_suggestions": "Revisar sugestões", - "organise_profiles": "Organizar perfis", - "configure_phases": "Configurar fases", "skip_step": "Pular esta etapa", "skip_forever": "Não mostrar novamente", "hide_guidance": "Ocultar guia", @@ -2171,6 +2122,7 @@ "min": "min", "entity_not_found": "Entidade não encontrada", "running_long": "Em atraso", + "stalled": "Travado", "remaining": "restante", "progress": "progresso", "show_time_remaining": "Mostrar tempo restante", @@ -2219,5 +2171,53 @@ "off_delay": { "effective": "Um ciclo concluído espera na verdade {eff} s, e não {delay} s: o Intervalo Mínimo de Desligamento ({gap} s) é maior e tem prioridade." } + }, + "access": { + "edit": "Edição", + "full": "Total", + "inherit": "Herdar", + "none": "Nenhum (oculto)", + "read": "Leitura" + }, + "pg_label_reason": { + "no_winner": "Nenhum programa corresponde", + "below_floor": "Abaixo da Confiança de Aprendizado", + "ambiguous": "Correspondência marcada como ambígua", + "margin": "Muito próximo do segundo candidato", + "unknown_profile": "O programa não existe mais", + "not_evaluated": "Não avaliado" + }, + "diagram": { + "accum": "acum.", + "auto": "auto", + "below_off": "abaixo = desligado", + "confirmed": "confirmado", + "energy_counts": "energia contada", + "feedback": "revisão", + "gap_below_min": "intervalo abaixo do mín. = um ciclo", + "heat_phase": "fase de aquecimento", + "held_at_100": "mantido em 100%", + "ignored": "ignorado", + "kept": "mantido", + "max": "máx.", + "min": "mín.", + "minus_tol": "-tol", + "no_match": "sem corresp.", + "no_updates": "sem atualizações", + "off_delay_wait": "espera até desligar", + "off_gap": "intervalo", + "plus_tol": "+tol", + "profile": "perfil", + "reading_interval": "intervalo típico de leitura", + "sensor_offline": "sensor offline: parada forçada", + "si": "IA", + "spike_ignored": "pico ignorado", + "spike_ignored_short": "pico: ignorado", + "start": "início", + "stop": "parada", + "tail_energy_resets": "cauda > limite: temporizador reinicia", + "thr": "limite", + "too_short": "curto demais", + "tumble_kept": "giros mantidos" } } diff --git a/custom_components/ha_washdata/translations/panel/pt.json b/custom_components/ha_washdata/translations/panel/pt.json index 90bbb434..d0e33250 100644 --- a/custom_components/ha_washdata/translations/panel/pt.json +++ b/custom_components/ha_washdata/translations/panel/pt.json @@ -1,15 +1,16 @@ { "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_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. Visto em {seen} de {measured} ciclos medidos.", "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", + "artifact_tip": "{n} anomalias detectadas (por exemplo, porta aberta a meio do ciclo) - abra para as ver no gráfico", + "artifact_tip_one": "{n} anomalia detectada (por exemplo, porta aberta a meio do ciclo) - abra para a ver no gráfico", + "artifact_tip_many": "{n} anomalias detectadas (por exemplo, porta aberta a meio do ciclo) - abra para as ver no gráfico", + "artifact_tip_other": "{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", "declining": "↘ a declinar", "energy_low": "Energia abaixo do habitual", "energy_spike": "Energia acima do habitual", - "fair_fit": "qualidade aceitável", - "fair_fit_tip": "Consistência de correspondência moderada – alguns ciclos atribuídos a este perfil têm pontuações de confiança mais baixas. Rotule mais ciclos ou re-grave o perfil para melhorar a precisão.", "feedback_requested": "Feedback solicitado", "golden_cycle": "Ciclo de referência gravado", "improving": "↗ a melhorar", @@ -17,28 +18,41 @@ "needs_review": "Precisa de revisão", "overrun": "Demorou mais do que o habitual", "poor_fit": "qualidade fraca", - "poor_fit_tip": "Histórico de correspondência inconsistente – considere reconstruir este perfil", - "restart_gap_tip": "{n} lacuna de reinício do HA: o traçado de potência tem uma lacuna", + "restart_gap_tip": "{n} lacunas de reinício do HA: o traçado de potência tem lacunas", + "restart_gap_tip_one": "{n} lacuna de reinício do HA: o traçado de potência tem uma lacuna", + "restart_gap_tip_many": "{n} lacunas de reinício do HA: o traçado de potência tem lacunas", + "restart_gap_tip_other": "{n} lacunas de reinício do HA: o traçado de potência tem lacunas", "reviewed": "Revisto", "steady": "→ estável", "underrun": "Terminou mais rápido do que o habitual", - "using_defaults": "A usar predefinições", - "using_tuned": "A usar pesos ajustados", "overrun_ratio": "({x}x o esperado)", "underrun_ratio": "({pct}% do esperado)", - "awaiting_tip": "A aguardar aprovação da comunidade", - "awaiting_n": "A aguardar aprovação · {n} confirmados", - "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.", "armed": "(aplica-se ao próximo ciclo)", - "duration_outlier": "{n} ciclo(s) com duração invulgar" + "duration_outlier": "{n} ciclo(s) com duração invulgar", + "awaiting_tip": "A aguardar aprovação da comunidade", + "awaiting_n": "A aguardar aprovação · {n} confirmados", + "awaiting": "A aguardar aprovação", + "approved": "Aprovado", + "matcher_unused": "Ainda sem correspondência com os seus ciclos", + "matcher_used": "Corresponde a {n} dos seus ciclos", + "matcher_used_one": "Corresponde a {n} dos seus ciclos", + "matcher_used_many": "Corresponde a {n} dos seus ciclos", + "matcher_used_other": "Corresponde a {n} dos seus ciclos", + "matcher_used_tip": "Ciclos deste eletrodoméstico que o WashData associou por si só a este programa importado. Uma importação que nunca corresponde pode não se adequar ao seu eletrodoméstico; pode removê-la. Contado localmente; nada é enviado para a loja da comunidade." }, "btn": { + "log_done_task": "Registar como feito: {task}", + "add_task": "Adicionar tarefa", + "pg_preset_delete": "Eliminar preset", + "pg_preset_load": "Carregar preset", + "pg_preset_load_tip": "Substituir os valores da Área de testes por este preset (as definições ativas não são alteradas)", + "pg_preset_save": "Guardar como preset", + "pg_preset_save_tip": "Guardar todos os valores abaixo como um preset com nome para este dispositivo", "reset_zoom": "Repor zoom", "set_brand_model": "Definir marca e modelo", "refresh_catalog": "Actualizar catálogo", @@ -49,12 +63,11 @@ "add_timer": "+ Adicionar temporizador", "apply_all": "Aplicar todos", "apply_label": "Aplicar rótulo", - "apply_set_b": "Aplicar conjunto B", "apply_split": "Aplicar divisão", "apply_trim": "Aplicar corte", "auto_detect_split": "Detectar automaticamente", "auto_label_cycles": "Rotular ciclos automaticamente", - "auto_label_cycles_tip": "Atribuir automaticamente nomes de perfil a ciclos sem rótulo cuja confiança de correspondência ultrapasse o limiar", + "auto_label_cycles_tip": "Dar nome aos ciclos sem rótulo cuja confiança de correspondência ultrapassa o limiar.", "cancel": "Cancelar", "clear_debug": "Limpar dados de depuração", "clear_splits": "Limpar", @@ -64,8 +77,6 @@ "convert_to_automation": "Converter em automatização", "correct": "Corrigir…", "create": "Criar", - "create_from_cluster": "Criar perfil a partir de {count} ciclos", - "create_profile": "+ Criar perfil", "delete": "Eliminar", "delete_group": "Eliminar grupo", "delete_group_tip": "Eliminar apenas este grupo – os perfis dos membros são mantidos", @@ -76,22 +87,19 @@ "done": "Concluído", "edit": "Editar", "export": "Exportar", - "export_json": "Exportar para JSON", "force_stop": "Forçar paragem", "force_stop_tip": "Terminar imediatamente o ciclo atual e marcá-lo como parado forçosamente", "from_template": "A partir de modelo ▾", "ignore": "Ignorar", "import_json": "Importar de JSON", - "import_overwrite": "Importar (substitui dados)", "inspect": "Inspecionar", "label": "Rotular", - "load": "Carregar", "load_more": "Carregar mais", "manage": "Gerir", "merge": "Combinar", "new_automation": "+ Nova automatização", "new_group": "+ Novo grupo", - "new_group_tip": "Agrupar perfis quase idênticos (mesma forma/duração, temperatura ou centrifugação diferentes) para que o comparador os distinga de forma fiável", + "new_group_tip": "Agrupar perfis quase idênticos (mesma forma, temperatura ou centrifugação diferente) para que a correspondência os consiga distinguir.", "new_phase": "+ Nova fase", "new_profile": "+ Novo perfil", "new_profile_tip": "Criar um novo perfil de programa a partir de um ciclo rotulado existente ou de uma gravação", @@ -99,36 +107,25 @@ "on_cycle_started": "Ao iniciar o ciclo", "pause_cycle": "Pausar", "pause_cycle_tip": "Pausar o ciclo em curso – o aparelho retomará onde parou", - "pg_apply_device": "Aplicar ao dispositivo", - "pg_autofill": "Preenchimento automático", - "pg_load_calibrated": "Carregar Calibrado (ML) ({n})", - "pg_load_calibrated_tip": "Carregar as definições calibradas por ML como substituições na Área de testes", "pg_load_live": "Carregar as definições ativas", "pg_load_live_tip": "Voltar a ler as definições atuais da integração e descartar todas as edições feitas na Área de testes", "pg_load_suggested": "Carregar sugestões ({n})", "pg_load_suggested_tip": "Carregar as sugestões atuais do ajustador automático como substituições na Área de testes", - "pg_preset_delete": "Eliminar preset", - "pg_preset_load": "Carregar preset", - "pg_preset_load_tip": "Substituir os valores da Área de testes por este preset (as definições ativas não são alteradas)", - "pg_preset_save": "Guardar como preset", - "pg_preset_save_tip": "Guardar todos os valores abaixo como um preset com nome para este dispositivo", "pg_publish_all": "Guardar {n} definição(ões) na integração", "pg_publish_one": "Guardar {label} na integração", - "play": "Reproduzir", "process": "Processar", "process_history": "Processar agora", "process_recording": "Guardar gravação", "process_tip": "Guardar o traço gravado como um perfil novo ou existente", "rebuild": "Reconstruir envelopes", "rebuild_envelope": "Reconstruir envelope", - "rebuild_tip": "Recalcular o envelope de potência esperado (banda mín/máx) para todos os perfis a partir dos seus ciclos rotulados – executar após rotular novos ciclos ou corrigir os existentes", + "rebuild_tip": "Reconstruir a banda de potência esperada de cada perfil a partir dos seus ciclos rotulados. Execute após rotular ou corrigir ciclos.", "rec_start_tip": "Iniciar a gravação do traço de potência do aparelho – iniciar imediatamente antes de executar um ciclo", "rec_stop": "Parar", "rec_stop_tip": "Parar a gravação e manter o traço capturado para revisão", "record": "Iniciar gravação", "refresh": "Actualizar", "refresh_diag_tip": "Recarregar estatísticas de armazenamento", - "refresh_logs_tip": "Recarregar os registos de log mais recentes", "refresh_settings_tip": "Recarregar as definições do servidor", "relabel": "Reetiquetar ({count})", "remove": "Remover", @@ -136,7 +133,6 @@ "reset": "Repor", "reset_ml_models_tip": "Descartar os modelos ajustados e voltar aos incorporados. O WashData pode reaprendê-los mais tarde.", "reset_to_builtin": "Repor modelos incorporados", - "reset_to_defaults": "Repor predefinições", "resume_cycle": "Retomar ciclo", "resume_cycle_tip": "Retomar o ciclo em pausa", "revert_settings": "Descartar alterações", @@ -149,8 +145,6 @@ "run_analysis": "🔍 Executar análise de sugestões", "run_analysis_tip": "Analisar os ciclos gravados agora e actualizar as sugestões de ajuste", "run_auto_label": "Executar rotulagem automática", - "run_both": "Executar A vs B", - "run_simulation": "Executar simulação", "save": "Guardar", "save_access_control": "Guardar controlo de acesso", "save_group": "Guardar grupo", @@ -162,10 +156,8 @@ "save_settings": "Guardar definições", "select": "Seleccionar", "select_all": "Selecionar tudo", - "select_last20": "Últimos 20", "select_none": "Limpar", "show_only": "Mostrar apenas", - "skip_setup": "Ignorar configuração", "split": "Dividir", "stop": "Parar", "submit_correction": "Submeter correcção", @@ -174,36 +166,31 @@ "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.", + "unlabel_selected_tip": "Remover o rótulo dos ciclos selecionados. Continuam no seu histórico, mas deixam de moldar este perfil.", "use": "Usar", "wipe_all": "Apagar todos os dados", "delete_selected": "Eliminar selecionados ({n})", - "group_suggest": "Grupo {n}: {members}", "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", "import": "Importar", "disconnect": "Desconectar", "connect_github": "Conectar GitHub", - "save_appliance": "Guardar electrodoméstico", "share_to_store": "Partilhar na loja", "share": "Partilhar", - "confirm_appliance": "Confirmar este electrodoméstico", - "confirmed": "Confirmado", "browse_community_setups": "Explorar configurações da comunidade", "download_device": "Transferir esta configuração", "share_device": "⬆ Partilhar este dispositivo", - "share_device_tip": "Partilhar este electrodoméstico e os seus ciclos de referência gravados na loja da comunidade para que outros utilizadores com o mesmo aparelho os possam adotar", + "share_device_tip": "Partilhar este eletrodoméstico e os seus ciclos de referência na loja da comunidade, para que donos da mesma máquina os possam adotar.", "share_n": "Partilhar {n} ciclo(s)", "export_selected": "Exportar (escolher dados)", "export_all": "Exportar tudo rapidamente", - "import_raw": "Avançado: substituir tudo a partir de JSON", "download_export": "Transferir exportação", "analyze_import": "Analisar ficheiro", "import_selected": "Importar seleção", "back": "Voltar", "mute_suggestion": "Não sugerir mais esta definição", "reset_muted": "Repor silenciados", + "reset_phase_tip": "Anular a sua edição e repor a fase incorporada", "import_power_history": "Importar histórico de potência", "hist_read_recorder": "Ler do Home Assistant", "hist_scan": "Procurar ciclos", @@ -212,13 +199,15 @@ "save_total_cycles": "Guardar total", "clear": "Limpar", "add_brand": "Adicionar marca", - "add_appliance": "Adicionar electrodoméstico" + "add_appliance": "Adicionar electrodoméstico", + "retry": "Tentar novamente", + "confirm_appliance": "Confirmar este electrodoméstico", + "confirmed": "Confirmado", + "create_profile_from_gap": "Criar perfil", + "undo_import": "Anular a última importação", + "pg_rerun": "Atualizar simulação" }, "conflict": { - "anti_wrinkle_exit": { - "exit": "Deve ser inferior ao Limiar de paragem ({stop} W); caso contrário, a potência de saída anti-vincos é ignorada", - "stop": "Deve ser superior à Potência de saída anti-vincos ({exit} W)" - }, "anti_wrinkle_max": { "max": "Deve ser superior ao Limiar de arranque ({start} W); caso contrário, o limite de duração anti-vincos é ignorado", "start": "Deve ser inferior à Potência máxima anti-vincos ({max} W)" @@ -226,10 +215,10 @@ "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." + "power_sensor": "Este é o Sensor de Potência do dispositivo, não um preço. Os ciclos seriam cobrados em watts por kWh; limpe este campo.", + "energy_sensor": "Esta é a Entidade do Contador de Energia do dispositivo, não um preço. 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. Limpe este campo ou escolha um sensor de tarifa.", + "unit": "Este sensor mede em {unit}, não em preço por kWh. Limpe este campo ou escolha 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", @@ -264,8 +253,8 @@ "threshold": "Deve ser inferior ao Limiar de paragem ({stop} W) para ter efeito" }, "pump_stuck": { - "duration": "Deve ser inferior ao Tempo de espera sem actualização ({to} s) para que o alarme de bomba encravada dispare antes do watchdog terminar o ciclo", - "timeout": "Deve exceder a Duração de bomba encravada ({ps} s) para que o alarme dispare antes de o ciclo ser forçado a parar" + "duration": "Tem de ser inferior ao Timeout Sem Actualização ({to} s) para que o alarme de bomba presa possa disparar primeiro", + "timeout": "Tem de exceder a Duração de Bomba Presa ({ps} s) para que o alarme de bomba presa possa disparar primeiro" }, "start_dur": { "sampling": "O Intervalo de amostragem excede a Duração de arranque ({sdt} s); picos de amostra única podem abrir um ciclo", @@ -281,10 +270,15 @@ "watchdog": { "interval": "Deve ser pelo menos 2x o Intervalo de amostragem ({si} s)", "sampling": "O Intervalo de amostragem deve ser no máximo metade do Intervalo watchdog ({wi} s)" + }, + "end_energy": { + "energy": "Demasiado restritivo para o Limiar de Paragem ({w} W) durante o Atraso de Desligamento ({d} s); o ciclo só pode terminar por um mecanismo de recurso", + "stop": "A Energia de Fim ({e} Wh em {d} s) só permite {p} W" } }, "hdr": { - "ab_comparison": "Comparação de definições A/B", + "custom_tasks": "As suas próprias tarefas", + "maintenance_supporter": "Precisa de mais?", "access_control": "Controlo de acesso", "add_maintenance": "Adicionar evento de manutenção", "automation_delete": "Eliminar esta automatização", @@ -293,28 +287,22 @@ "automation_review_this_cycle": "Rever este ciclo", "automations": "Automatizações", "clear_debug": "Limpar traços de depuração", - "cycle_simulator": "Simulador de ciclos", "diagnostics_pref": "Diagnósticos", "display": "Visualização", - "dtw_inspector": "Inspetor DTW", "export_import": "Exportar / Importar", - "getting_started": "Introdução", "live_power": "Potência em tempo real", "logs": "Registos", - "logs_diagnostics": "Diagnósticos", "maintenance": "Acções de manutenção", "maintenance_log": "Registo de manutenção", "maintenance_reminders": "Lembretes de manutenção", "manual_recording": "Gravação manual", - "manual_recording_tip": "Execute um ciclo intencionalmente enquanto o WashData regista o traço de potência. Inicie imediatamente antes de o aparelho arrancar, Pare quando terminar, depois Processe para o guardar como um perfil com nome.", + "manual_recording_tip": "Grave um ciclo de propósito: prima Início mesmo antes de o eletrodoméstico arrancar e Parar quando terminar, e depois Processar para o guardar como perfil.", "ml_learned": "O que o WashData aprendeu", - "ml_matching_tuning": "Ajuste fino da correspondência de programas", "ml_settings_card": "Definições", "ml_smart_learning": "Aprendizagem inteligente", "my_preferences": "As minhas preferências", "no_automations": "Ainda não há automatizações que referenciem este dispositivo.", "panel_settings": "Definições do painel", - "param_sweep": "Varrimento de parâmetro", "pg_settings_source": "Origem das definições", "phase_catalog": "Catálogo de fases", "playground": "Área de testes", @@ -323,11 +311,8 @@ "status": "Estado", "status_graph": "Gráfico de estado", "storage_stats": "Estatísticas de armazenamento", - "test_history": "Testar no histórico", "toggle_sidebar": "Alternar barra lateral do Home Assistant", - "tools_and_data": "Ferramentas e dados", "wipe_history": "Apagar histórico", - "recommendations": "Recomendações ({n})", "pg_detection_params": "Definições de deteção", "pg_outcome": "Resultado da simulação", "pg_across_cycles": "Em todos os seus ciclos", @@ -335,7 +320,8 @@ "online_account": "Loja da comunidade e funcionalidades online", "import_power_history": "Importar histórico de potência", "service_status": "Estado da manutenção", - "online_account_tab": "Online e comunidade" + "online_account_tab": "Online e comunidade", + "live_match_debug": "Depuração da correspondência em tempo real" }, "health": { "fair": "Qualidade de perfil aceitável", @@ -343,7 +329,12 @@ "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.", + "task_name": "Tarefa", + "every_cycles": "A cada (ciclos)", + "every_days": "A cada (dias)", + "new_task_name": "Nova tarefa", + "days_since_service": "{since} / {total} dias", + "envelope_position_tip": "Posição na curva gravada do programa correspondente: {pct}%. Obtida alinhando o ciclo com o perfil, por isso continua correta quando um ciclo é mais longo ou mais curto. Atualiza enquanto o eletrodoméstico está em silêncio, por isso pode ficar atrasada.", "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})", @@ -355,10 +346,8 @@ "aria_cycle_chart": "Curva de potência do ciclo", "aria_envelope_chart": "Envelope de potência do perfil", "aria_phase_chart": "Gráfico do editor de fases", - "aria_playground_chart": "Curva de potência do ciclo com limiares de deteção arrastáveis", "aria_power_chart": "Gráfico de potência", "aria_spaghetti_chart": "Curvas de potência de ciclos sobrepostas", - "aria_sweep_chart": "Gráfico de resultados do varrimento de parâmetros", "artifact_anomaly": "Anomalia", "artifact_high_power": "Potência elevada", "artifact_interruption": "Interrupção", @@ -372,150 +361,107 @@ "cohesion_good": "coesão {pct}%", "cohesion_low": "⚠ coesão baixa {pct}%", "compare_profiles": "Comparar com perfis", - "concurrency": "Concorrência", "confidence": "Confiança", "confidence_threshold": "Limiar de confiança", "correct_duration": "Duração corrigida (min, opcional)", "correct_profile": "Perfil correcto", - "correlation": "Correlação", "cost": "Custo", "create_new_profile": "+ Criar novo perfil…", "current_phase": "Fase atual", - "curves": "Curvas", "cycle": "Ciclo", "cycle_date_display": "Apresentação da data do ciclo", - "cycle_health": "Saúde do ciclo", - "cycle_index": "índice do ciclo →", - "cycles": "Ciclos", + "cycle_context": "Histórico do recorder em torno do ciclo", + "cycle_context_off": "Desligado", + "cycle_context_minutes": "{min} min", + "cycle_context_series": "Histórico do recorder", + "before_start": "Antes do início", + "after_end": "Depois do fim", "cycles_count": "Ciclos", "cycles_lc": "ciclos", - "dashed": "tracejada", "date": "Data", "debug_traces": "Traços de depuração", - "default": "Predefinição", "default_access_level": "Nível predefinido para utilizadores não listados abaixo", "default_other": "Predefinição (outros dispositivos)", "default_tab": "Separador predefinido ao abrir o painel", - "delta": "Δ", "description": "Descrição", - "dtw_blend_w": "Peso de mistura", - "dtw_blended": "Pontuação combinada", - "dtw_ddtw": "Pontuação DDTW", - "dtw_l1": "Pontuação L1", - "dtw_matched_profile": "(perfil correspondente do ciclo)", "duration": "Duração", - "duration_agreement": "Concordância de duração", - "emphasis": "Destaque", "enable_access_control": "Activar controlo de acesso por utilizador", "energy": "Energia", - "energy_agreement": "Concordância de energia", - "event_timeline": "Cronologia de eventos", "event_type": "Tipo de evento", "expected": "Esperado", + "suspected_profile": "{name} (provável)", + "runner_up_profile": "{name} (segundo candidato)", "expected_duration": "Duração esperada (min)", "file_kb": "Ficheiro (kB)", - "final_score": "Pontuação final", "from": "De", "gap_s": "Intervalo (s)", "group_name": "Nome do grupo", "head_trim": "Corte inicial (s)", - "health": "Saúde", "hide_tabs": "Ocultar separadores para não administradores", - "in_use": "Em uso", "json_data": "Dados JSON", "label": "Programa correspondente", "last": "Últimos", "last_checked": "Última verificação:", "load_from_file": "Carregar de ficheiro", - "mae_score": "Pontuação MAE", "manual_duration": "Duração manual (min, opcional)", "match": "Correspondência", "members": "Membros", - "metric": "Métrica", "mode_existing_profile": "Adicionar a perfil existente", "mode_new_profile": "Criar novo perfil", "models_fine_tuned": "(modelos ajustados: {count})", - "n_classic_suggestions": "{n} clássico", - "n_ml_suggestions": "{n} ML", "n_selected": "{n} selecionados", - "n_tuning_suggestions": "{n} sugestão de ajuste", - "na": "n/d", + "n_tuning_suggestions": "{n} sugestões de ajuste", + "n_tuning_suggestions_one": "{n} sugestão de ajuste", + "n_tuning_suggestions_many": "{n} sugestões de ajuste", + "n_tuning_suggestions_other": "{n} sugestões de ajuste", "new_profile_name": "Nome do novo perfil", "notes": "Notas", - "of": "de", - "outcome": "Resultado", "panel_default_tab": "Separador predefinido", "panel_language": "Idioma do painel", "per_cycle_short": "ciclo", "pg_anti_wrinkle": "Anti-vincos", - "pg_autofill_tip": "Preencher automaticamente o intervalo a partir do valor atual ({lo}–{hi})", "pg_best_value": "Melhor valor encontrado", "pg_current_value": "Valor atual:", "pg_drag_hint": "Arraste a linha no gráfico", - "pg_dtw": "DTW", "pg_ev_ending": "A terminar", "pg_ev_running": "Em execução", "pg_group_advanced": "Casos extremos", "pg_group_detection": "Acionadores de deteção", "pg_group_timing": "Regras de temporização", "pg_idle": "Inactivo", - "pg_last_run": "Última execução: {date}", - "pg_match_score": "Pontuação de correspondência", - "pg_pct_of": "{p}% de {total}", "pg_poor_match": "Correspondência má", "pg_preset": "Preset da Área de testes", "pg_preset_name": "Nome do novo preset", "pg_preset_none": "Selecione um preset…", "pg_strong_match": "Correspondência forte", - "pg_tile_ambiguous": "Ambíguo", - "pg_tile_ambiguous_tip": "Ciclos com um quase empate entre dois programas", - "pg_tile_detected": "Detetado", - "pg_tile_detected_tip": "Ciclos em que a potência ultrapassou o limiar de início tempo suficiente para ser registada como em execução", - "pg_tile_matched": "Reconhecido", - "pg_tile_matched_tip": "Ciclos corretamente atribuídos a um programa conhecido", - "pg_time_left": "Tempo restante", "pg_weak_match": "Correspondência fraca", "phase_name": "Nome da fase", "name": "Nome", "phase_timeline": "Cronologia de fases", "power": "Potência", "profile": "Perfil", - "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. Escolha um antes de ligar o aparelho e será aplicado assim que o próximo ciclo começar.", + "program_tip": "Escolha a que programa o ciclo atual é associado. Deteção automática escolhe a melhor correspondência. Fixe um programa quando a deteção automática errar; se o escolher antes de iniciar, aplica-se ao próximo ciclo.", "progress": "Progresso", "raw_socket": "Socket bruto", - "read_only": "Só de leitura", "ref_cycle": "Ciclo de referência (opcional)", - "refresh_interval": "Intervalo de actualização (s)", "remaining": "Restante", "reminder_every": "Lembrar a cada (ciclos)", "remove_label": "- Remover rótulo -", "rename_profile": "Renomear perfil", - "replay_duration": "Reprodução", "resulting_profile": "Perfil resultante", "save_mode": "Modo de gravação", - "score": "Pontuação", - "score_breakdown": "Detalhe da pontuação", "seconds_from_start": "Segundos desde o início", "select_profile": "Seleccionar perfil", "setting": "Definição", - "settings_a": "Definições A (atuais)", - "settings_a_short": "A", "settings_advanced": "Avançado", - "settings_b": "Definições B (teste)", - "settings_b_short": "B", "settings_basic": "Básico", "settings_detail_level": "Nível de detalhe das definições", - "settings_overrides": "Substituições de definições", "show_debug": "Mostrar cartão de depuração de correspondência ao vivo na página de Estado (confiança, ambiguidade, principais candidatos)", "show_expected": "Mostrar sobreposição da curva esperada (perfil correspondente, laranja)", "show_raw": "Mostrar o interruptor de sinal bruto da tomada no gráfico de potência em tempo real", "sparkline": "Tendência recente da duração dos ciclos", - "stage2": "Etapa 2 – semelhança principal", - "stage3": "Etapa 3 – DTW", - "stage4": "Etapa 4 – concordância", "status": "Estado", "tail_trim": "Corte final (s)", "tap_to_dismiss": "toque no gráfico para fechar", @@ -526,7 +472,6 @@ "ungrouped": "Sem grupo", "unlabelled": "Sem rótulo", "unlabelled_paren": "(sem rótulo)", - "warp_path": "Caminho de alinhamento", "input": "Entrada:", "start": "Início", "end": "Fim", @@ -551,9 +496,8 @@ "n_shown": ", {n} mostrados", "cycles_title": "Ciclos ({n})", "pg_detecting": "A detetar", - "pg_best_marker": "Melhor: {v}", "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", + "pg_time_left_model_tip": "Estimado a partir das fases do programa, não uma contagem decrescente fixa", "phase": "Fase", "pg_mode_history": "Testar no histórico", "pg_ev_detected": "Detetado", @@ -576,9 +520,6 @@ "pg_alert_unmatched": "Sem correspondência", "pg_alert_ambiguous": "Correspondência ambígua", "pg_alert_energy": "Anomalia de energia", - "pg_alert_stress_cap": "Limite de segurança atingido", - "pg_alert_stress_ok": "Terminação em repouso: ciclo parado", - "pg_alert_stress_warn": "Consumo em repouso acima do limiar de paragem", "pg_newly_correct": "agora correto", "pg_regressed": "regrediu", "pg_end_timing_changed": "momento do fim alterado", @@ -588,12 +529,6 @@ "pg_sweep_param": "Parâmetro", "pg_objective": "Objetivo", "pg_steps": "Passos", - "pg_stress_group": "Teste de terminação em repouso", - "pg_stress_idle_auto": "Auto", - "pg_stress_idle_w": "Nível em repouso (W)", - "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_obj_match": "Precisão de correspondência", "pg_obj_endtiming": "Precisão do momento de fim", "pg_obj_falseend": "Taxa de fins falsos", @@ -602,7 +537,6 @@ "pg_mode_optimize": "Otimizar", "pg_group_matching": "Correspondência de programa", "pg_zoom_hint": "faça duplo clique para repor o zoom", - "aria_playground_chart2": "Gráfico interativo da potência do ciclo: passe o cursor para ler tempo/potência, percorra para ampliar, arraste para deslocar", "pg_alert_timeout_end": "Terminado por tempo limite, não por previsão", "pg_alert_indefinite": "Funcionaria indefinidamente", "task_pg_history": "Testar no histórico", @@ -614,16 +548,9 @@ "pg_recent_runs": "Execuções recentes", "pg_load_run": "Recarregar esta execução", "pg_match_confidence": "Confiança da correspondência", - "pg_envelope_fit": "Ajuste ao envelope", - "pg_envelope_fit_tip": "Até que ponto o ciclo se mantém dentro do envelope de potência guardado deste perfil, uma perspetiva diferente da confiança da correspondência.", "n_imported_note": ", {n} importados", "enable_online": "Ativar funcionalidades online", - "store_brand": "Marca do electrodoméstico", - "store_model": "Modelo do electrodoméstico", "anonymous": "Anónimo", - "rate_quality": "Avaliar a qualidade", - "rate_n": "{n} estrelas", - "approved_only": "Apenas aprovados", "adopt_settings": "Adotar também as definições", "flags": "Indicadores", "include_phase_map": "Incluir mapa de fases", @@ -640,7 +567,6 @@ "cat_custom_phases": "Fases personalizadas", "cat_profile_groups": "Grupos de perfis", "cat_settings": "Definições de deteção e reconhecimento", - "cat_matching_config": "Ajuste do reconhecimento", "cat_ml_models": "Modelos de ML", "cat_feedback": "Comentários e etiquetas de revisão", "cat_suggestions": "Sugestões", @@ -662,7 +588,6 @@ "hist_csv_data": "Dados CSV", "hist_from_recorder": "Ou ler diretamente do Home Assistant", "hist_since": "Desde", - "days": "dias", "hist_keep": "Manter este ciclo", "hist_looks_complete": "completo", "peak_power_short": "Pico", @@ -682,7 +607,36 @@ "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" + "correct_total_cycles": "Corrigir o total", + "hist_reason_already_recorded": "sobrepõe-se a um ciclo que o WashData já registou", + "share_consent": "Confirmo que estes ciclos terminaram normalmente, sem interrupções", + "show_internal": "Mostrar definições internas", + "show_internal_tip": "Definições que o WashData afina por si (amostragem, watchdog, cadência de correspondência, limiares). Mostradas quando este dispositivo tem um valor próprio, um conflito ou uma sugestão.", + "rate_n": "{n} estrelas", + "rate_quality": "Avaliar a qualidade", + "pg_autolabel": "Rotulagem automática", + "pg_label_review": "Pede revisão", + "pg_ev_match_reverted": "Correspondência revertida", + "pg_ev_verified_pause": "Pausa verificada", + "n_samples": "{n} amostras", + "n_samples_one": "{n} amostra", + "n_samples_many": "{n} amostras", + "n_samples_other": "{n} amostras", + "n_cycles": "{n} ciclos", + "n_cycles_one": "{n} ciclo", + "n_cycles_many": "{n} ciclos", + "n_cycles_other": "{n} ciclos", + "energy_per_cycle": "{v}/ciclo", + "energy_total": "{v} no total", + "automation_off": "(desativada)", + "ref_cycle_none": "Nenhum", + "share_name": "Mostrar o meu nome no que partilho", + "cat_backfill_cycles": "Ciclos encontrados num histórico de potência importado", + "pg_compare_profile": "Comparar com perfil", + "pg_obj_early": "Taxa de fins antecipados", + "pg_obj_endlag": "Atraso do fim (mediana)", + "pg_obj_split": "Taxa de divisões", + "pg_sweep_cycles": "Ciclos recentes" }, "log": { "all_levels": "Todos os níveis", @@ -692,11 +646,9 @@ }, "ml": { "builtin_models": "● A usar modelos incorporados", - "fit_fair": "Aceitável", "fit_good": "Bom", "fit_slight": "Ligeiro", "fit_strong": "Forte", - "fit_weak": "Fraco", "fit_word": "ajuste", "personalized": "● Personalizado para esta máquina", "better_than_baseline": "{pct}% melhor do que a estimativa de referência ({metric})", @@ -704,30 +656,41 @@ "trend_declining_tip": "O ajuste deste modelo piorou ao longo das verificações recentes: rever mais ciclos pode ajudá-lo a reaprender.", "trend_steady_tip": "O ajuste deste modelo manteve-se praticamente estável ao longo das verificações recentes.", "cap_blurb": { - "end": "Saber quando um ciclo terminou realmente", - "live_match": "Identificar mais cedo o programa em curso", - "quality": "Detetar ciclos mal detetados ou corrompidos", - "remaining_time": "Prever quanto tempo falta", "total_energy": "Prever a energia total e o custo" }, "cap_label": { - "end": "Deteção de fim de ciclo", - "live_match": "Reconhecimento do programa", - "quality": "Verificação da qualidade do ciclo", - "remaining_time": "Estimativa do tempo restante", "total_energy": "Estimativa da energia" }, - "metric_auc": "AUC {auc} em dados de validação", - "metric_mae": "erro {model} vs. referência {baseline}" + "metric_mae": "erro {model} vs. referência {baseline}", + "fine_tuned_at": "ajustado em {when}", + "held_out_count": "medido em {count} ciclos de validação", + "held_out_count_one": "medido em {count} ciclo de validação", + "held_out_count_many": "medido em {count} ciclos de validação", + "held_out_count_other": "medido em {count} ciclos de validação", + "last_run_not_promoted": "Última verificação ({when}): {reason}", + "never": "nunca", + "skip_reason": { + "fit_failed": "O treino falhou: {error}", + "holdout_too_small": "Ciclos insuficientes para testar um novo modelo de forma justa: foi possível reservar {held_out} para teste, são necessários {min}.", + "insufficient_rows": "Ainda não há ciclos utilizáveis suficientes: até agora, {count} ciclos rotulados e limpos.", + "insufficient_rows_one": "Ainda não há ciclos utilizáveis suficientes: até agora, {count} ciclo rotulado e limpo.", + "insufficient_rows_many": "Ainda não há ciclos utilizáveis suficientes: até agora, {count} ciclos rotulados e limpos.", + "insufficient_rows_other": "Ainda não há ciclos utilizáveis suficientes: até agora, {count} ciclos rotulados e limpos.", + "not_better_than_incumbent": "O novo modelo não era melhor do que o que está em uso (erro {model} vs. {incumbent}), pelo que o atual foi mantido.", + "not_better_than_naive": "O novo modelo não era claramente melhor do que a estimativa simples (erro {model} vs. {naive}).", + "unknown": "Não foi aprendido nada de novo." + }, + "trained_unknown": "num momento desconhecido" }, "modal": { + "remove_task_title": "Remover tarefa", + "remove_task_msg": "Remover o lembrete \"{task}\"? As suas entradas no registo de manutenção são mantidas.", "auto_label": "Rotulagem automática de ciclos", "correct_feedback": "Corrigir feedback", "create_profile": "Criar perfil", "cycle": "Ciclo", "edit_group": "Editar grupo de perfis", "edit_phase": "Editar fase", - "import_config": "Importar configuração", "label_cycle": "Rotular ciclo", "merge_cycles": "Combinar {n} ciclos", "new_group": "Novo grupo de perfis", @@ -742,6 +705,8 @@ "delete_automation_title": "Eliminar automação", "delete_automation_msg": "Eliminar a automação \"{name}\" do Home Assistant? Esta ação não pode ser anulada.", "remove_legacy_title": "Remover ações antigas", + "reset_phase_msg": "Anular a sua edição de \"{name}\" e repor a fase incorporada? Os programas mantêm os seus intervalos.", + "reset_phase_title": "Repor fase", "remove_legacy_msg": "Remover as ações personalizadas antigas? Deixarão de ser acionadas nos eventos de ciclo. Esta ação não pode ser anulada a partir do painel.", "discard_recording_title": "Descartar a gravação", "discard_recording_msg": "Descartar a gravação guardada? Esta ação não pode ser anulada.", @@ -762,63 +727,79 @@ "store_share_device": "Partilhar este dispositivo", "export_select": "Exportar - escolher dados", "import_wizard": "Importar - escolher dados", - "history_import": "Importar histórico de potência" + "history_import": "Importar histórico de potência", + "apply_suggestions_title": "Aplicar sugestões", + "apply_suggestions_msg": "Alterar estas {n} definições? A integração é recarregada depois de guardar.", + "discard_settings_title": "Descartar alterações não guardadas?", + "discard_settings_msg": "Alterações de definições não guardadas: {n}. Sair das Definições e descartá-las?", + "profile_title": "Perfil · {name}", + "undo_import_msg": "Os dados e definições deste dispositivo voltam ao estado anterior à última importação ({when}). Os ciclos e alterações posteriores perdem-se.", + "undo_import_ok": "Anular importação", + "undo_import_title": "Anular a última importação" }, "msg": { + "custom_task_limit": "Atingiu o limite de {max} tarefas.", + "custom_tasks_intro": "Adicione qualquer outra coisa de que pretenda ser lembrado. Uma tarefa fica pendente quando qualquer um dos intervalos é atingido; deixe um em branco ou a 0 para usar apenas o outro.", + "maintenance_due_sensor_hint": "O sensor binário Manutenção pendente está ligado enquanto houver alguma tarefa pendente, para as suas próprias automatizações de notificação.", + "maintenance_supporter_intro": "O Maintenance Supporter é uma integração separada que faz muito mais: qualquer eletrodoméstico ou tarefa, prazos por tempo, utilização ou um sensor, notificações e histórico. Pode usar o contador de ciclos do WashData.", "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.", + "pg_preset_delete_confirm": "Eliminar o preset \"{name}\"?", + "pg_preset_limit": "Limite de presets atingido ({n})", + "pg_preset_overwrite": "Substituir o preset \"{name}\"?", "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.", - "refresh_catalog_hint": "As listas de marcas e electrodomésticos da comunidade são guardadas em cache para manter a loja partilhada dentro do seu limite diário. Actualize para obter as entradas adicionadas ou aprovadas por outros utilizadores.", + "store_sibling_hint": "Nada partilhado para o seu modelo? Um modelo semelhante da mesma marca costuma ser um bom começo.", + "store_declare_appliance": "Indique ao WashData qual o eletrodoméstico que possui para ver as configurações que outros partilharam para ele. Ou escreva uma marca acima para explorar.", + "refresh_catalog_hint": "As listas de marcas e eletrodomésticos da comunidade ficam em cache para poupar o limite diário da loja. Atualize para ver as entradas que outros adicionaram ou aprovaram.", "head_trim_hint": "Número de segundos a remover do início", "appliance_monitor": "Monitor de electrodoméstico", "artifact_dip_detail": "Caiu abaixo da banda de potência habitual durante ~{n}s.", "artifact_footer": "Realçadas no gráfico acima. São artefactos transitórios (por exemplo, a porta foi aberta a meio do ciclo), não necessariamente problemas.", "artifact_header": "{n} anomalias detectadas durante este ciclo", - "artifact_pause_detail": "A potência caiu quase a zero durante ~{n}s e depois retomou – provavelmente a porta foi aberta a meio do ciclo ou o ciclo foi pausado.", + "artifact_header_one": "{n} anomalia detectada durante este ciclo", + "artifact_header_many": "{n} anomalias detectadas durante este ciclo", + "artifact_header_other": "{n} anomalias detectadas durante este ciclo", + "artifact_pause_detail": "A potência caiu quase a zero durante ~{n}s e depois voltou. Provavelmente a porta foi aberta ou o ciclo foi pausado.", "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 depois remova-lhes o rótulo (mantêm-se no histórico, mas deixam de influenciar este perfil) ou elimine-os por completo.", + "cleanup_intro": "Todos os ciclos rotulados sobrepostos. Assinale os valores aberrantes e depois remova-lhes o rótulo (ficam no histórico, mas deixam de moldar este perfil) ou elimine-os.", "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)", - "compare_profiles_tip": "Sobreponha outros envelopes de perfil no gráfico acima para ver qual deles melhor se adequa a este ciclo.", + "compare_profiles_tip": "Sobreponha outros perfis no gráfico para ver qual se adequa melhor a este ciclo.", "compare_selected_cycles": "Ciclos seleccionados (sólido) – mostrar / ocultar", - "consider_new_profile": "Considere criar um novo perfil.", - "coverage_gap": "os ciclos recentes não têm perfil correspondente ({pct}% dos últimos 30).", - "coverage_gap_similar_cycles": "{count} ciclos semelhantes sem rótulo encontrados – crie um perfil para começar a correspondê-los.", "cycles_deleted": "{count} ciclo(s) eliminado(s)", + "cycle_context_loading": "A carregar o histórico do recorder…", + "cycle_context_note": "Cinzento: o histórico do recorder do sensor de potência em torno deste ciclo. Não faz parte do ciclo e não afeta a duração, a energia nem a correspondência.", + "cycle_context_none": "O recorder não contém leituras em torno deste ciclo (o Home Assistant guarda 10 dias de histórico por predefinição).", + "cycle_context_tip": "Apenas visualização: mostra o histórico do recorder do sensor de potência em torno deste ciclo, se ainda existir. Para acrescentar tentativas de arranque falhadas a ciclos futuros, use Pré-arranque da curva (Definições > Detecção > Início do Ciclo), que altera as durações guardadas.", "enough_data": "Dados suficientes para aprender ({current}/{min} ciclos).", "export_description": "Escolha exatamente que perfis, ciclos, definições e mais exportar para JSON, ou analise um ficheiro e importe apenas as partes que quiser.", "feedback_cycles_pending": "Para rever: {n}", "feedback_prompt": "Confirme que estava correcto, corrija o programa ou ignore.", "feedback_relabel_hint": "Voltar a rotular este ciclo também o resolve.", "filter_by_profile": "Filtrar por perfil…", - "group_modal_help": "Agrupe programas com a mesma forma que diferem em temperatura/centrifugação (as durações podem variar). A correspondência pontua o grupo como um candidato e, em seguida, escolhe o membro mais adequado. Escolha pelo menos 2; a sobreposição mostra o grau de semelhança.", + "group_modal_help": "Agrupe programas com a mesma forma que diferem em temperatura ou centrifugação. A correspondência pontua o grupo como um só e depois escolhe o membro que melhor encaixa. Escolha pelo menos 2; a sobreposição mostra o quão semelhantes são.", "group_not_cohesive": "Estes perfis não são suficientemente semelhantes para serem agrupados de forma fiável, pelo que a correspondência os trata individualmente até remover o valor aberrante ou dividir o grupo.", "group_preview_hint": "Selecione 2 ou mais membros para pré-visualizar e comparar as suas curvas de potência.", - "import_intro": "Carregue um ficheiro exportado ou cole um payload JSON abaixo.", - "legacy_actions_title": "{count} acção personalizada legada ainda em execução", + "legacy_actions_title": "{count} ações personalizadas legadas ainda em execução", + "legacy_actions_title_one": "{count} ação personalizada legada ainda em execução", + "legacy_actions_title_many": "{count} ações personalizadas legadas ainda em execução", + "legacy_actions_title_other": "{count} ações personalizadas legadas ainda em execução", "live_chart_loading": "O gráfico de potência ao vivo aparece à medida que chegam leituras.", "loading": "A carregar…", "loading_curve": "A carregar curva…", "loading_settings": "A carregar definições…", "log_buffer_hint": "O mais recente primeiro · guarda em memória os últimos 500 registos ha_washdata desde o reinício · arraste a margem inferior para redimensionar.", - "maintenance_advisory": "O aumento da duração/energia pode indicar a necessidade de manutenção do electrodoméstico (por exemplo, descalcificação, limpeza do filtro).", "maintenance_due": "Manutenção pendente: {items}", - "maintenance_intro": "Registe as manutenções que efetua neste eletrodoméstico e receba lembretes quando cada tarefa voltar a estar em falta.", + "maintenance_intro": "Registe as manutenções deste eletrodoméstico e receba um lembrete quando cada tarefa voltar a estar pendente.", "maintenance_load_error": "Não foi possível carregar os dados de manutenção: {error}", "maintenance_requires_access": "A manutenção e a exportação/importação requerem acesso total.", - "manual_duration_hint": "A duração média/esperada do ciclo do perfil, usada para estimativas de tempo restante. Edite para a definir; deixá-la inalterada mantém o valor actual.", - "matching_tuning_intro": "Durante a aprendizagem, o WashData também ajusta o peso dado à forma em relação à duração e energia na correspondência de programas.", + "manual_duration_hint": "A duração esperada do ciclo do perfil, usada nas estimativas de tempo restante. Deixe inalterada para manter o valor atual.", "merge_intro": "Os ciclos seleccionados são combinados num só (por ordem cronológica; as lacunas são preenchidas com 0 W). Escolha o perfil resultante.", "ml_intro": "O WashData é fornecido com modelos inteligentes que funcionam imediatamente.", "ml_learned_intro": "Modelos ajustados a esta máquina.", - "ml_loading": "a carregar ML…", "ml_settings_intro": "Dois comutadores independentes: um aplica os modelos durante um ciclo, o outro permite ao WashData ajustá-los à sua máquina ao longo do tempo.", - "name_first_program": "Já tem ciclos suficientes – atribua um nome ao seu primeiro programa para começar a corresponder.", - "near_duplicate_cluster": "cluster de perfis quase duplicados detectado. O agrupamento permite que a correspondência escolha de forma fiável entre perfis semelhantes (por exemplo, o mesmo programa a diferentes temperaturas/centrifugações).", "no_cycles_match": "Nenhum ciclo corresponde ao filtro actual.", "no_cycles_profile": "Nenhum ciclo para este perfil.", "no_cycles_selected": "Selecione primeiro pelo menos um ciclo.", @@ -843,73 +824,54 @@ "no_suggestions": "Nenhuma sugestão activa.", "notify_services_hint": "Use os IDs de serviço {entity} (separados por vírgulas para múltiplos). Variáveis de modelo: {vars}.", "old_actions_warning": "Configurado com o antigo editor de acções (agora removido). Ainda são accionadas em eventos de ciclo, mas já não podem ser editadas aqui. Converta-as numa automatização normal ou remova-as.", - "onboarding_progress": "{n} / 3 ciclos observados", - "onboarding_watching": "Utilize o eletrodoméstico normalmente – o WashData está a observar. Após 3 ciclos, a correspondência de programas terá início.", "pending_feedback": "Feedback de detecção pendente", "performance_trend": "Tendência de desempenho ({n} ciclos)", - "pg_analysis_empty": "Carregue um ciclo para ver a análise de correspondência.", "pg_apply_confirm": "Aplicar o melhor valor: {label} = {value}?", - "pg_canvas_empty": "Selecione um ciclo acima e, em seguida, prima Carregar para ver a sua curva de potência.", - "pg_chart_caption": "Taxa de deteção/correspondência ao longo dos valores de {param}", "pg_ctrl_intro": "Os valores partem das definições ativas deste dispositivo na integração. As edições ficam na Área de testes até as guardar na integração.", "pg_live_unavailable": "Definições ativas indisponíveis, a mostrar os valores predefinidos.", "pg_load_live_confirm": "Descartar {n} edição(ões) da Área de testes e recarregar as definições atuais da integração?", - "pg_match_rate": "{pct}% de taxa de correspondência em {total} ciclos", "pg_matches_live": "Corresponde às definições ativas", "pg_n_changed": "{n} valor(es) diferente(s) das definições ativas", "pg_sugg_none": "Todas as sugestões já correspondem às definições atuais", - "pg_preset_delete_confirm": "Eliminar o preset \"{name}\"?", - "pg_preset_limit": "Limite de presets atingido ({n})", - "pg_preset_overwrite": "Substituir o preset \"{name}\"?", "pg_publish_one_confirm": "Guardar {label} = {value} nas definições deste dispositivo?", "pg_restart_note": "Reinicie o Home Assistant para ativar as ferramentas de simulação.", - "pg_sim_progress": "{done} / {total} ciclos", - "pg_starting": "A iniciar…", - "pg_stress_synthetic": "a simulação de repouso começa aqui", - "pg_sweep_intro": "E se {param} fosse diferente? Teste {steps} valores nos seus últimos {cycles} ciclos para encontrar a definição em que mais ciclos são reconhecidos corretamente.", - "pg_sweep_step": "Passo {done} / {total}", - "pg_verdict_bad": "Requer atenção: muitos ciclos não são detetados.", - "pg_verdict_good": "Bem afinado: a maioria dos ciclos é identificada e reconhecida corretamente.", - "pg_verdict_ok": "Aceitável: alguns ciclos não foram detetados. Tente baixar o limiar de início.", "phase_catalog_intro": "Segmentos nomeados de um ciclo (Pré-lavagem, Aquecimento, Centrifugação…). Atribua-os a um perfil a partir do seu painel de controlo.", "phase_ranges_intro": "Intervalos de fase (minutos desde o início do ciclo) sobrepostos na curva média. Edite os valores para pré-visualizar em tempo real.", - "playground_intro": "Um ambiente seguro para reproduzir ciclos anteriores com definições diferentes, comparar duas configurações lado a lado e ver como um ciclo se alinha a um perfil. Nada aqui altera os seus dados ativos até que o aplique explicitamente.", - "playground_needs_restart": "Reinicie o Home Assistant para ativar as ferramentas da Área de testes.", - "preferences_admin": "Preferências, painel e controlo de acesso", - "preferences_adv": "Preferências", + "playground_intro": "Experimente definições com os dados reais dos seus ciclos. Nada aqui altera as suas definições ativas até as aplicar.", "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), 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.", + "process_history_hint": "Volta a executar a correspondência em todos os ciclos guardados, atualiza as sugestões, retreina os modelos ML (se ativados), recalcula o custo a partir dos preços registados e recalcula a saúde dos ciclos. Execute após uma série 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.", - "rbac_hint": "Quando desactivado, todos os utilizadores do Home Assistant têm acesso total (predefinição). Os administradores têm sempre acesso total e podem gerir todos.", - "recent_logs": "Registos ha_washdata recentes", + "rbac_hint": "Desligado: todos os utilizadores do Home Assistant têm acesso total (predefinição). Os administradores têm sempre acesso total e podem gerir todos.", "recording_in_progress": "Gravação em curso", "reminders_intro": "Mostrar um lembrete no painel este número de ciclos após a última manutenção. Deixe em branco ou 0 para desativar um lembrete.", "restart_gap_footer": "Destacado no gráfico. Dados de potência estão em falta para estes intervalos; a correspondência usou apenas leituras reais.", - "restart_gap_header": "{n} lacuna de reinício do HA durante este ciclo", + "restart_gap_header": "{n} lacunas de reinício do HA durante este ciclo", + "restart_gap_header_one": "{n} lacuna de reinício do HA durante este ciclo", + "restart_gap_header_many": "{n} lacunas de reinício do HA durante este ciclo", + "restart_gap_header_other": "{n} lacunas de reinício do HA durante este ciclo", "restart_gap_item": "{dur} lacuna", - "review_confirm_help": "Confirme se este ciclo foi detectado correctamente. As suas revisões treinam o modelo na sua máquina – quanto mais ciclos confirmar, melhor ficam a correspondência e a pontuação de saúde. Um Bom/Mau rápido é suficiente.", + "review_confirm_help": "Este ciclo foi bem detetado? Confirmar ciclos melhora a correspondência para a sua máquina. Basta um rápido Bom/Mau.", "review_in_settings": "Rever nas Definições", "review_notes_placeholder": "Notas (opcional)", "review_notes_tip": "Notas em texto livre para sua referência pessoal. Não são usadas pela correspondência ou pelo treino.", - "review_profile_tip": "O programa com que este ciclo está rotulado. Se o programa detectado automaticamente estava errado, corrija-o aqui – a rotulagem ensina a correspondência para ciclos futuros.", - "review_quality_tip": "A qualidade deste ciclo. Bom = um exemplo perfeito deste programa; Mau = detectado mas ruidoso ou atípico; Inutilizável = detectado incorrectamente (combinado, truncado ou espúrio). Determina a pontuação de saúde e quais os ciclos permitidos para treinar o modelo.", - "review_recorded_tip": "Marque este ciclo como referência escolhida a dedo para o seu programa – o mesmo papel de um ciclo gravado manualmente. Os ciclos de referência são sempre mantidos, semeiam o modelo de correspondência e nunca são removidos pela limpeza. (Esta é a bandeira \"dourada\"/gravada; ambas são a mesma coisa.)", - "review_tags_tip": "Etiquetas opcionais que descrevem o que correu mal com este ciclo, para que o treino e a limpeza possam ter isso em conta.", + "review_profile_tip": "O programa com que este ciclo está rotulado. Se estiver errado, corrija-o aqui; os rótulos ensinam a correspondência.", + "review_quality_tip": "Quão limpo é este ciclo. Bom: um exemplo de manual deste programa. Mau: detetado, mas ruidoso ou invulgar. Inutilizável: mal detetado (fundido, cortado ou espúrio). Afeta a pontuação de saúde e quais os ciclos que treinam o modelo.", + "review_recorded_tip": "Marque este ciclo como referência escolhida a dedo para o seu programa, como um ciclo gravado manualmente. Os ciclos de referência são sempre mantidos, servem de base ao modelo de correspondência e nunca são removidos pela limpeza.", + "review_tags_tip": "Marcas opcionais sobre o que correu mal, para que o treino e a limpeza o possam ter em conta.", "review_to_cycles": "Abrir a fila de revisão de Ciclos", "samples_decimated": "A mostrar {shown} de {total} amostras (reduzidas para exibição; picos mantidos). Um intervalo grande aqui deve-se à redução, não a dados em falta.", - "saving_triggers_reload": "Guardar desencadeia um recarregamento da integração. As entidades HA podem ficar brevemente indisponíveis.", + "saving_triggers_reload": "Guardar recarrega a integração. As entidades podem aparecer brevemente como indisponíveis.", "search_placeholder": "Pesquisar definições…", "see_recorder": "Veja o widget de gravação abaixo", "setting_changed": "Alterado de {old} para {new} em {date}", "settings_basic_note": "A mostrar as definições essenciais. Mude para Avançado para ver a lista completa.", - "shape_drift_advisory": "⚠ Desvio de forma", - "shape_drift_detail": "O padrão de potência para este perfil mudou ao longo do tempo – possível desgaste do electrodoméstico ou manutenção necessária (por exemplo, descalcificação, limpeza do filtro).", "show_all_settings": "Mostrar todas as definições", - "showing_suggestions": "A mostrar a definição {count} com sugestões.", - "split_intro": "Clique no gráfico para adicionar ou remover um ponto de divisão, ou detecte automaticamente por intervalos de inactividade. Cada segmento resultante pode ter o seu próprio perfil.", - "storage_diagnostics": "Estatísticas de armazenamento, manutenção, exportação/importação", + "showing_suggestions": "A mostrar {count} definições com sugestões.", + "showing_suggestions_one": "A mostrar {count} definição com sugestões.", + "showing_suggestions_many": "A mostrar {count} definições com sugestões.", + "showing_suggestions_other": "A mostrar {count} definições com sugestões.", + "split_intro": "Clique no gráfico para adicionar ou remover um ponto de divisão, ou detete automaticamente por intervalos de inatividade. Cada parte pode ter o seu próprio perfil.", "sug_staged": "Definido {key} = {val}. Guarde para aplicar.", "toast_auto_detect_enabled": "Deteção automática ativada", "toast_auto_label_complete": "Rotulagem automática concluída", @@ -926,7 +888,6 @@ "toast_no_automation": "Não é possível criar automatização aqui", "toast_profile_name_required": "O nome do perfil é obrigatório", "toast_program_set": "Programa definido: {program}", - "toast_rebuild_failed": "Reconstrução falhou: {error}", "toast_resume_failed": "Retomar falhou: {error}", "toast_revert_failed": "Reversão falhou: {error}", "toast_reverted": "{key} revertido", @@ -937,97 +898,82 @@ "trend_energy_up": "Energia com tendência crescente ({pct}%/ciclo) – média recente {avg}", "trim_destructive_confirm": "Isto mantém apenas {pct}% do ciclo e esta ação não pode ser anulada. Continuar?", "trim_intro": "Arraste os puxadores vermelhos ou introduza valores. Tudo o que estiver fora da janela é removido.", - "tuning_suggestions_available": "{count} sugestão de ajuste disponível a partir dos ciclos observados. Aparecem junto dos campos relevantes.", + "tuning_suggestions_available": "{count} sugestões de ajuste disponíveis a partir dos ciclos observados. Aparecem junto dos campos relevantes.", + "tuning_suggestions_available_one": "{count} sugestão de ajuste disponível a partir dos ciclos observados. Aparece junto do campo relevante.", + "tuning_suggestions_available_many": "{count} sugestões de ajuste disponíveis a partir dos ciclos observados. Aparecem junto dos campos relevantes.", + "tuning_suggestions_available_other": "{count} sugestões de ajuste disponíveis a partir dos ciclos observados. Aparecem junto dos campos relevantes.", "unsure_detected_prefix": "O WashData não tem a certeza de que detectou", - "updated": "Actualizado", "warmup_badge": "Ainda a aprender ({done}/{needed} ciclos)", - "warmup_detail": "Este perfil precisa de {needed} ciclos rotulados antes de a correspondência automática começar. Cada ciclo confirmado ajuda-o a aprender.", + "warmup_detail": "A correspondência já funciona. O WashData pede-lhe que confirme os primeiros {needed} ciclos deste perfil antes de rotular por si.", "wipe_history_warning": "Eliminar permanentemente todos os ciclos e perfis. Não é possível desfazer.", "compare_overlay_tip": "Sobreponha os envelopes dos perfis aprendidos para ver a que programa cada ciclo se assemelha.", - "duration_trend_up_tip": "Duração a aumentar ({pct}%/ciclo)", - "duration_trend_down_tip": "Duração a diminuir ({pct}%/ciclo)", - "energy_trend_up_tip": "Energia a aumentar ({pct}%/ciclo)", - "energy_trend_down_tip": "Energia a diminuir ({pct}%/ciclo)", - "performance_trending": "Tendência de desempenho", - "pg_select_cycle_hint": "Selecione um ciclo e clique em ↺ para carregar", - "pg_sweep_progress": "Passo {done} / {total}", - "pg_analysis_hint": "Selecione um ciclo e clique em ↺ para carregar a análise.", "diagnostics_load_failed": "Não foi possível carregar os diagnósticos: {error}", - "manual_duration_ref_hint": "Usado apenas quando nenhum ciclo de referência está selecionado. Um ciclo de referência define a duração a partir do seu próprio comprimento.", + "manual_duration_ref_hint": "Só é usado sem ciclo de referência; um ciclo de referência define a duração a partir do seu próprio comprimento.", + "duration_learned_hint": "Calculada a partir dos ciclos deste programa (sem valores aberrantes) e atualizada após cada novo ciclo. Para a alterar, corte ou remova os ciclos que destoam.", "toast_convert_rolled_back": "A conversão falhou e foi revertida (não foi deixada nenhuma automatização): {error}", "toast_convert_orphan": "A automatização foi criada, mas a limpeza das ações antigas falhou. Não tente novamente: remova as ações antigas manualmente para evitar uma automatização duplicada.", - "advisory_duration_trend_up": "Os ciclos de '{name}' estão a demorar cada vez mais (cerca de +{pct}% por ciclo). Se o comportamento do aparelho mudou, volte a gravar ou reconstrua este perfil.", - "advisory_energy_trend_up": "'{name}' está a consumir cada vez mais energia (cerca de +{pct}% por ciclo) - vale a pena verificar o aparelho se isso for inesperado.", - "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.", + "advisory_duration_trend_up": "Os ciclos de '{name}' estão a ficar cada vez mais longos (cerca de +{pct}% por ciclo). Se o eletrodoméstico mudou, volte a gravar ou reconstrua este perfil.", + "advisory_energy_trend_up": "'{name}' consome mais energia a cada ciclo (cerca de +{pct}% por ciclo). Verifique o eletrodoméstico se isso for inesperado.", + "advisory_poor_health": "'{name}' encaixa mal: os ciclos recentes variam muito ou correspondem fracamente. Reveja os ciclos ou volte a gravá-lo para manter a correspondência e as estimativas precisas.", + "advisory_shape_drift": "'{name}' afastou-se da sua curva de potência original, talvez por calcário ou desgaste. Considere voltar a gravá-lo com ciclos recentes.", + "advisory_shape_drift_corr": "'{name}' afastou-se da sua curva de potência original (correlação {corr}), talvez por calcário ou desgaste. Considere voltar a gravá-lo com ciclos recentes.", + "advisory_unmatchable": "'{name}' nunca poderá ser correspondido: não tem nenhum ciclo com dados de potência associado. Rotule ou grave um ciclo deste programa para resolver.", + "advisory_unmatchable_no_duration": "'{name}' nunca poderá ser correspondido: o seu ciclo não tem uma duração utilizável. Volte a gravar este programa ou defina a duração esperada.", "pg_no_alerts": "Nenhum problema detetado nesta execução.", "pg_override_active": "A usar as suas definições editadas vs. as atuais", "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.", "pg_sweep_intro2": "Encontre a definição que melhor cumpre um objetivo nos seus ciclos recentes. Nada muda até a aplicar.", - "pg_running_history": "A reproduzir os seus ciclos…", - "pg_running_sweep": "A testar valores em todos os seus ciclos…", "pg_row_load_hint": "Carregar este ciclo no gráfico acima", - "pg_history_intro2": "Reproduza os seus ciclos recentes através do detetor e do sistema de correspondência reais com as definições acima. Clique numa linha para carregar esse ciclo no gráfico; edite uma definição para ver uma comparação antes/depois.", + "pg_history_intro2": "Reproduza ciclos recentes com o detetor e o comparador reais usando as definições acima. Clique numa linha para a carregar no gráfico; altere uma definição para comparar antes e depois.", "pg_simulating": "A simular o ciclo…", - "pg_canvas_empty2": "Escolha um ciclo acima e prima Executar para o simular. Depois passe o cursor para ler os valores, percorra para ampliar e arraste para deslocar.", "pg_apply_settings_confirm": "Guardar estas {n} definição(ões) neste dispositivo? {list}", - "pg_stress_terminated_detail": "Estabilizou em ~{idle}W em repouso -- o ciclo terminou {h}h {m}m depois através de {reason}.", - "pg_stress_above_threshold_detail": "O consumo em repouso ~{idle}W é igual ou superior ao limiar de paragem efetivo ({stop}W) -- o ciclo nunca foi considerado inativo. Aumente stop_threshold_w para corrigir.", - "pg_stress_hit_cap_detail": "O ciclo durou {h}h {m}m sem parar -- parado à força pelo limite de segurança. Consumo em repouso {idle}W face ao limiar de paragem {stop}W.", "no_logs_match": "Nenhum registo corresponde a estes filtros.", "pg_analysis_hint2": "Selecione um ciclo e prima Executar para carregar a análise de correspondência.", - "pg_analysis_empty2": "Prima Executar para ver a análise de correspondência.", - "imported_readonly": "Importado da loja da comunidade. Mostrado para referência e correspondência. Não é contabilizado nas suas estatísticas e não pode ser editado.", + "imported_readonly": "Importado da loja da comunidade. Usado como referência e para correspondência, não conta nas suas estatísticas e não pode ser editado.", "merge_no_imports": "As gravações importadas não podem ser combinadas com um ciclo real. Desmarque-as para combinar.", - "store_enable_hint": "Ative as funcionalidades online nas Definições para explorar e importar ciclos de referência da comunidade.", - "online_intro": "Partilhe e transfira ciclos de referência com outros utilizadores do WashData. Todas as funcionalidades online são opcionais e estão desativadas por predefinição.", + "store_enable_hint": "Ative as funcionalidades online em Preferências > Online e comunidade para explorar e importar ciclos de referência da comunidade.", "store_share_intro": "Carregue este ciclo de referência para que outras pessoas com o mesmo electrodoméstico o possam usar. É revisto antes de aparecer publicamente.", - "online_intro_global": "Explore e partilhe gravações de referência com outros utilizadores do WashData e confirme entradas de electrodomésticos. Uma única ligação aplica-se a toda a sua integração WashData; a marca e o modelo do electrodoméstico definem-se por dispositivo em Básico. Todas as funcionalidades online são opcionais e estão desativadas por predefinição.", + "online_intro_global": "Explore e partilhe gravações de referência com outros utilizadores do WashData. Uma ligação serve todos os seus dispositivos WashData; a marca e o modelo definem-se por dispositivo em Básico. As funções online estão desligadas até as ativar.", "online_unavailable": "As funcionalidades online não estão disponíveis neste servidor.", "store_picker_offline": "Ative as funcionalidades online no ícone de definições para escolher do catálogo da comunidade.", "pick_brand_first": "Escolha primeiro uma marca de electrodoméstico.", - "model_not_found": "Ainda não está no catálogo.", - "connect_to_confirm": "Conecte-se no ícone de definições para confirmar ou avaliar.", "adopt_settings_hint": "Substituir os limiares de deteção e reconhecimento deste dispositivo pelos partilhados. As suas notificações, entidades e preço de energia nunca são alterados.", - "include_settings_hint": "Partilhar os limiares de reconhecimento e correspondência deste dispositivo (não as suas notificações, entidades ou preço de energia). Quem adotar escolhe se os aplica.", + "include_settings_hint": "Partilhar os limiares de deteção e correspondência deste dispositivo (não as notificações, entidades ou preço da energia). Os outros escolhem se os aplicam.", "onboard_download": "Dispositivo novo? Adote uma configuração pronta (programas, ciclos de referência e fases) de outro utilizador WashData com o mesmo electrodoméstico.", - "share_consent": "Confirmo que estes ciclos correram até à conclusão normal sem interrupções", "share_device_none": "Ainda não há ciclos partilháveis. Marque primeiro um ciclo gravado ou selecionado a dedo como ciclo de referência (⭐) no separador Ciclos.", "share_guideline_naming": "Dê a cada perfil exatamente o nome apresentado no mostrador ou ecrã do aparelho (ex. 'Cotton 40', 'Eco 60').", - "share_guideline_quality": "Partilhe apenas ciclos que terminaram normalmente -- sem interrupções a meio do ciclo, eventos de abertura da porta ou falhas de corrente.", + "share_guideline_quality": "Partilhe apenas ciclos que terminaram normalmente: sem interrupções, aberturas de porta ou falhas de corrente.", "share_guideline_review": "O seu carregamento começa como pendente e aparece publicamente quando membros suficientes da comunidade o confirmam.", "share_guidelines_title": "Antes de partilhar", - "store_download_device_intro": "Adotar todos os programas partilhados e os seus ciclos de referência neste dispositivo. Os seus ciclos gravados e estatísticas não são afetados.", + "store_download_device_intro": "Adicionar todos os programas partilhados e os seus ciclos de referência ao seu dispositivo. Os seus próprios ciclos e estatísticas não são afetados.", "store_share_device_intro": "Carregar {brand} {model} com os ciclos de referência que selecionar. Outros utilizadores com o mesmo electrodoméstico podem adotar os seus programas. As entradas são revistas antes de aparecerem publicamente.", "share_profile_no_cycles": "Sem ciclos de referência -- marque um ciclo como ⭐ no separador Ciclos para incluir este perfil", - "advisory_phase_inconsistent": "'{name}' parece misturar diferentes programas ou temperaturas - os seus ciclos aquecem durante períodos de tempo muito diferentes. Dividi-lo em perfis separados (p. ex. por temperatura) melhorará a correspondência e as estimativas de tempo.", - "advisory_phase_inconsistent_title": "⚠ Possíveis programas misturados", - "export_select_intro": "Marque exatamente o que incluir. Selecionar perfis sem os seus ciclos exporta na mesma um programa reconhecível (a forma aprendida vai junto).", - "import_analyze_hint": "Carregue um ficheiro exportado (ou cole o seu JSON). O WashData analisa-o e mostra exatamente o que pode ser importado antes de qualquer alteração.", + "export_select_intro": "Assinale o que incluir. Perfis sem os seus ciclos exportam na mesma um programa reconhecível.", + "import_analyze_hint": "Carregue um ficheiro exportado (ou cole o seu JSON). O WashData mostra o que pode ser importado antes de alterar alguma coisa.", "analyzing": "A analisar…", - "device_type_mismatch_warn": "Esta exportação é de um tipo de eletrodoméstico diferente ({src} em vez de {local}). Os programas e ciclos ainda podem ser importados como dados de referência, mas as definições específicas do dispositivo e a importação para o histórico real estão desativadas.", + "device_type_mismatch_warn": "Esta exportação é de um tipo de eletrodoméstico diferente ({src} em vez de {local}). Os programas e ciclos ainda podem ser importados como dados de referência, mas não as definições do dispositivo nem o histórico real.", "merge_hint": "Os itens importados são adicionados; nada local é perdido. Os conflitos de nomes são resolvidos abaixo.", "replace_warn": "Cada categoria marcada é apagada e substituída pelo conteúdo do ficheiro. As categorias não marcadas ficam intactas.", "dest_reference_hint": "Os ciclos importados só melhoram o reconhecimento de programas e nunca afetam as estatísticas de utilização/energia.", - "dest_real_history_hint": "Os ciclos importados contam como histórico próprio deste dispositivo e alimentam as estatísticas de energia/utilização. Use para mover um eletrodoméstico para uma nova instalação.", + "dest_real_history_hint": "Os ciclos importados contam como histórico e estatísticas próprios deste dispositivo. Use para mudar um eletrodoméstico para uma nova instalação.", "sug_muted": "Esta definição não será sugerida novamente", "sug_mute_failed": "Não foi possível silenciar a sugestão", "sug_unmuted_all": "Sugestões silenciadas repostas", - "n_suggestions_muted": "{count} silenciadas; o ajustador automático não proporá estas definições.", + "n_suggestions_muted": "{count} sugestões silenciadas; o ajustador automático não as proporá.", + "n_suggestions_muted_one": "{count} sugestão silenciada; o ajustador automático não a proporá.", + "n_suggestions_muted_many": "{count} sugestões silenciadas; o ajustador automático não as proporá.", + "n_suggestions_muted_other": "{count} sugestões silenciadas; o ajustador automático não as proporá.", "font_size_hint": "Torna tudo neste painel maior ou menor. Aplica-se à sua conta neste dispositivo.", - "import_history_description": "Já tinha uma tomada inteligente antes do WashData? Carregue uma exportação do histórico do respetivo sensor de potência, ou leia-o diretamente do Home Assistant: a deteção normal é executada sobre esses dados, pelo que os ciclos passados aparecem na sua lista de Ciclos, prontos a nomear.", - "hist_input_hint": "Carregue um CSV descarregado do painel Histórico (entidade, estado, última alteração), ou deixe o WashData ler diretamente o histórico do sensor. A deteção é depois executada exatamente como em tempo real e escolhe quais dos ciclos encontrados quer manter.", - "hist_recorder_hint": "Lê desde a data que escolher até agora. O Home Assistant guarda o histórico detalhado durante 10 dias por predefinição e, depois disso, apenas médias horárias, demasiado grosseiras para detetar ciclos - escolha uma data mais antiga apenas se o recorder do Home Assistant estiver configurado para guardar mais.", - "hist_scanning": "O seu histórico está a ser reproduzido através do detetor. Isto é executado em segundo plano: pode fechar esta janela e voltar mais tarde.", + "import_history_description": "Já tinha uma tomada inteligente antes do WashData? Carregue o histórico de potência ou leia-o do Home Assistant. A deteção percorre-o e os ciclos passados aparecem na sua lista de Ciclos, prontos a receber nome.", + "hist_input_hint": "Carregue um CSV do painel Histórico (entidade, estado, última alteração), ou deixe o WashData ler diretamente o histórico do sensor. A deteção ao vivo percorre-o e pode escolher quais os ciclos a manter.", + "hist_recorder_hint": "Lê desde a data escolhida até agora. O Home Assistant guarda histórico detalhado durante 10 dias por predefinição; as médias horárias mais antigas são demasiado grosseiras para detetar ciclos.", + "hist_scanning": "O seu histórico está a passar pelo detetor em segundo plano. Pode fechar esta janela e voltar mais tarde.", "hist_imported_count": "{n} ciclos importados.", "hist_duplicates": "{n} já tinham sido importados e foram ignorados.", "hist_capped": "Foi atingido o limite de ciclos importados por dispositivo; os restantes não foram guardados.", - "hist_next_step": "Estão na sua lista de Ciclos, marcados como histórico importado. Abra um e use Rotular para indicar o programa a que pertence.", + "hist_next_step": "Estão na sua lista de Ciclos, marcados como histórico importado. Abra um e use Rotular para dar nome ao programa.", "hist_rows_read": "{n} leituras lidas", "hist_breaks": "{n} intervalos em que o sensor estava indisponível", "hist_other_entity": "{n} leituras de outras entidades ignoradas", @@ -1041,58 +987,91 @@ "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.", - "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.", + "imported_history_readonly": "Encontrado num histórico de potência importado. Ajuda a correspondência, mas não conta nas suas estatísticas e não pode ser cortado nem dividido. Rotule-o para dar nome ao programa.", + "odometer_intro": "Este total só aumenta. Eliminar um ciclo não o reduz, para que os lembretes de manutenção continuem corretos.", + "odometer_correct_hint": "Defina este valor se o WashData contou um ciclo que nunca aconteceu, ou se o eletrodoméstico já fez 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}", "standby_above_stop_title": "Ainda consome {idle} W quando o ciclo terminou", "standby_above_stop_sub": "{n} dos últimos {total} ciclos. O Atraso de Desligamento só começa quando a potência desce abaixo do Limiar de Paragem ({stop} W), pelo que os ciclos terminam tarde ou têm de ser parados à mão.", - "advisory_duration_outlier": "{n} ciclo(s) rotulados como '{name}' estão demasiado longe da sua duração habitual para alguma vez lhe corresponderem (pior caso: {ratio}x). Provavelmente estão mal rotulados, ou uma gravação captou duas execuções. Volte a rotulá-los ou divida-os para deixarem de distorcer a estimativa de tempo deste programa.", + "advisory_duration_outlier": "{n} ciclo(s) rotulados como '{name}' estão demasiado longe da sua duração habitual para alguma vez corresponderem (pior caso: {ratio}x). Provavelmente estão mal rotulados ou contêm dois ciclos. Volte a rotulá-los ou divida-os para que deixem de distorcer a estimativa de tempo.", "toast_range_required": "Defina um intervalo De/Até válido", - "no_profiles_option": "(sem perfis, adicione um)" + "no_profiles_option": "(sem perfis, adicione um)", + "compare_cycles_title": "Comparar {count} ciclos", + "edit_builtin_phase": "Esta é uma fase incorporada. Guardar cria uma cópia personalizada; elimine a cópia para repor o original.", + "fetch_error": "Falha ao carregar os dados.", + "connection_lost": "Ligação perdida - a mostrar dados das {time}.", + "pg_notify_partial": "Os marcadores mostram apenas notificações de início, quase concluído, fim, marcos e horas de silêncio. As atualizações ao vivo, os lembretes de descarga e os alertas de tempo excedido não são simulados.", + "connect_to_confirm": "Conecte-se no ícone de definições para confirmar ou avaliar.", + "coverage_gap_cluster": "{count} ciclos recentes sem rótulo de cerca de {min} min parecem ser um programa que ainda não criou.", + "pg_label_yes": "Seria rotulado como \"{name}\" no fim do ciclo", + "pg_label_no": "Pediria revisão: {reason}", + "live_match_debug_tip": "Confiança: o quanto a curva de potência atual corresponde ao perfil candidato principal (0-100%). Ambíguo: os dois melhores candidatos têm pontuações a menos de 5% um do outro; a etiqueta fica incerta até o ciclo terminar.", + "automation_alias_started": "{device}: ciclo iniciado", + "automation_alias_finished": "{device}: ciclo terminado", + "automation_desc_started": "Executada quando um ciclo de {device} começa. Adicione as suas ações (notificar, luzes, ...).", + "automation_desc_finished": "Executada quando um ciclo de {device} termina. Adicione as suas ações (notificar, luzes, ...).", + "feedback_detected": "O WashData detetou: {name}", + "import_undo_hint": "É mantido um ponto de restauro de antes da última importação ({when}): {profiles} perfil(is) e {cycles} ciclo(s).", + "replace_undo_hint": "Primeiro é guardado um ponto de restauro, para que possa anular esta importação em Exportar / Importar.", + "pg_in_sample_note": "Estes ciclos ajudaram a construir os perfis com que são comparados, por isso os resultados são otimistas: conte com um pouco menos de correspondências certas em ciclos novos.", + "hist_tail_trimmed": "Foram cortados {min} min de espera após o fim do ciclo, tal como faz a integração em tempo real", + "invalid_threshold_pair": "O Limiar de Paragem ({stop} W) tem de ser inferior ao Limiar de Início ({start} W). Nada foi guardado; altere ambos em conjunto.", + "hist_truncated": "parou no limite de {n} leituras, pelo que o resto do histórico não foi analisado", + "hist_warn_kw": "A leitura mais alta é de apenas {w}. Este sensor parece indicar quilowatts; o WashData precisa de watts, por isso não é possível detetar ciclos.", + "hist_warn_naive_time": "{n} carimbos de data/hora não têm fuso horário e foram lidos como UTC. Se o ficheiro usar a hora local, os ciclos ficam desfasados pela sua diferença em relação ao UTC e podem perder-se leituras perto de uma mudança da hora de verão.", + "hist_warn_naive_time_one": "{n} carimbo de data/hora não tem fuso horário e foi lido como UTC. Se o ficheiro usar a hora local, os ciclos ficam desfasados pela sua diferença em relação ao UTC e podem perder-se leituras perto de uma mudança da hora de verão.", + "hist_warn_naive_time_many": "{n} carimbos de data/hora não têm fuso horário e foram lidos como UTC. Se o ficheiro usar a hora local, os ciclos ficam desfasados pela sua diferença em relação ao UTC e podem perder-se leituras perto de uma mudança da hora de verão.", + "hist_warn_naive_time_other": "{n} carimbos de data/hora não têm fuso horário e foram lidos como UTC. Se o ficheiro usar a hora local, os ciclos ficam desfasados pela sua diferença em relação ao UTC e podem perder-se leituras perto de uma mudança da hora de verão.", + "pg_compare_profile_tip": "Escolhe apenas o perfil desenhado sobre o gráfico. A simulação procura sempre por si própria a correspondência do ciclo, tal como faz a integração em tempo real.", + "pg_outcome_stale": "As definições mudaram desde que esta simulação foi executada. Ainda mostra o resultado antigo.", + "pg_sweep_guarded": "Termina mais ciclos antes do tempo, divide mais ciclos ou deixa escapar ciclos que a definição atual deteta, pelo que este valor nunca é recomendado.", + "pg_sweep_keep_current": "Mantenha o valor atual ({value}, {metric}): nenhum valor foi melhor por, pelo menos, um ciclo.", + "pg_sweep_n_cycles": "em {count} ciclos", + "pg_sweep_n_cycles_one": "em {count} ciclo", + "pg_sweep_n_cycles_many": "em {count} ciclos", + "pg_sweep_n_cycles_other": "em {count} ciclos", + "pg_task_busy": "Já está em curso uma execução de Testar no histórico ou Otimizar para este dispositivo. Aguarde que termine ou cancele-a." }, "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", + "smart_termination_duration_ratio": "Fração da duração média do programa que um ciclo deve atingir antes de a Terminação inteligente o poder terminar mais cedo; reduza-a se os tempos de execução variarem", + "anti_crease_finalize_ratio": "Fração da duração média do programa que um ciclo deve atingir antes de a cauda anti-vincos o poder terminar; reduza-a em máquinas de secar, deixe as máquinas de lavar na predefinição", "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_exit_power": "Nível considerado silêncio entre pulsos de rotação; só tem efeito acima do Limiar de Paragem", "anti_wrinkle_idle_timeout": "Tempo de silêncio permitido entre dois pulsos de rotação antes de o modo anti-vincos terminar", "anti_wrinkle_max_duration": "Um pulso mais longo do que isto termina o modo anti-vincos e abre um novo ciclo", "anti_wrinkle_max_power": "Um pulso acima disto termina o modo anti-vincos e abre um novo ciclo", "completion_min_seconds": "Execução mais curta que conta como um ciclo real", - "end_repeat_count": "Leituras baixas consecutivas antes de terminar", "interrupted_min_seconds": "Ciclos curtos marcados como interrompidos", "min_off_gap": "Intervalo necessário para separar dois ciclos", "off_delay": "Segundos de baixa potência antes de o ciclo terminar", "start_duration_threshold": "Segundos acima do limiar para confirmar o início", "start_threshold_w": "Watts mínimos para contar como iniciado", "stop_threshold_w": "Abaixo disto, a máquina conta como desligada", - "dtw_bandwidth": "Fase 3: banda de deformação Sakoe-Chiba (0 = DTW desativado; predefinição 0,2 = 20% do comprimento do ciclo)", - "corr_weight": "Fase 2: equilíbrio entre a forma da curva (correlação) e o nível de potência (MAE); predefinição 0,45", - "duration_weight": "Fase 4: o quanto a concordância de duração afeta a pontuação final (predefinição 0,22)", - "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,8 (180%)", - "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)", + "profile_match_min_duration_ratio": "Duração mínima do ciclo como fração da duração do perfil; predefinição 0,1 (10%)", + "profile_match_max_duration_ratio": "Duração máxima do ciclo como fração da duração do perfil; predefinição 1,8 (180%)", "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.", "anti_wrinkle": "Rotação periódica pós-ciclo para reduzir vincos.", + "baking": "Resistência a alta temperatura ativa para formar a côdea e o miolo.", + "cooking": "Fase principal de cozedura com resistência e ventoinha ativas.", "cool_down": "Rotação sem calor perto do fim do ciclo.", + "cool_down_af": "Arrefecimento só com a ventoinha após o aquecimento.", "drain_and_switch": "Período de transição do modo de lavagem para o modo de secagem.", "dry": "Fase de secagem com aquecedor e/ou calor residual.", "drying": "Período principal de rotação com calor.", "heat_up": "Aquecimento inicial do aquecedor antes do início da secagem completa.", + "keep_warm": "Fase de calor baixo para manter a comida quente.", + "keep_warm_bm": "Fase de calor baixo para manter o pão quente após a cozedura.", + "kneading": "O motor mistura e amassa a massa. Consumo elevado.", + "pause_af": "Pausa curta para agitar ou verificar a comida.", + "pre_heat": "Aquecimento inicial da câmara antes da cozedura completa.", "pre_rinse": "Pulverização inicial antes da lavagem com detergente.", + "proving": "Levedação com calor baixo para o fermento atuar e a massa crescer.", + "resting": "Pausa curta de baixa potência entre amassaduras para relaxar o glúten.", "pre_wash": "Imersão inicial ou pré-tratamento antes da lavagem principal.", "rinse": "Fase de enxagúe com água limpa. Esta fase pode repetir-se várias vezes.", "sanitize": "Fase de limpeza a alta temperatura para programas de higienização.", @@ -1105,6 +1084,7 @@ "wash_dw": "Lavagem principal com detergente e aquecimento." }, "placeholder": { + "custom_task": "ex. limpar o vedante da porta", "add": "adicionar…", "group_name": "ex. Algodão 2:47", "json_buttons": "[{\"action\":\"ID\",\"title\":\"Label\"}]", @@ -1132,10 +1112,10 @@ "section": { "dishwasher": { "label": "Máquina de lavar louça", - "intro": "Gestão de fim de ciclo para máquinas de lavar louça, que normalmente terminam com uma longa fase de secagem quase silenciosa antes de uma breve descarga final." + "intro": "Tratamento do fim para máquinas de lavar loiça, que muitas vezes secam em silêncio durante muito tempo antes de um breve escoamento final." }, "anti_wrinkle": { - "intro": "O modo anti-vincos detecta pulsos de rotação a baixa potência após a fase principal e mantém-nos ligados ao ciclo terminado.", + "intro": "Mantém os impulsos de rotação de baixa potência de uma máquina de secar após a fase principal dentro do ciclo terminado.", "label": "Anti-Vincos" }, "basic": { @@ -1143,7 +1123,7 @@ "label": "Básico" }, "delay": { - "intro": "A detecção de início retardado identifica quando um electrodoméstico está ligado mas ainda não iniciou o ciclo.", + "intro": "Deteta um eletrodoméstico ligado que aguarda um início diferido.", "label": "Início Retardado" }, "detection": { @@ -1167,161 +1147,153 @@ "label": "Temporização e Watchdog" }, "triggers": { - "intro": "Sinais externos opcionais: um gatilho de fim, um sensor de porta, um interruptor de pausa e o lembrete de descarga.", + "intro": "Extras opcionais: gatilho de fim, sensor de porta, interruptor de pausa e lembrete de descarga.", "label": "Gatilhos e Portas" - }, - "phase_eta": { - "label": "Tempo restante", - "intro": "Tempo restante com base nas fases, para máquinas cuja duração do ciclo depende da temperatura ou da centrifugação." } }, "setting": { "dishwasher_end_spike_quiet_release": { "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." + "doc": "Após a duração esperada: quanto tempo a máquina de lavar loiça tem de ficar abaixo do Limiar de Paragem antes de o WashData deixar de esperar pelo escoamento final. Aumente-o se um escoamento tardio após uma secagem longa for perdido. A espera nunca passa dos 30 minutos." }, "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." + "doc": "Fração da duração média do programa que um ciclo deve atingir antes de a sua rotação de baixa potência contar como cauda anti-vincos. Reduza-a (p. ex. 0,75) em máquinas de secar com sensor de carga. Deixe as máquinas de lavar na predefinição (0,98), senão uma lavagem pode terminar a meio." }, "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.", + "doc": "Manter os curtos impulsos de rotação de baixa potência de uma máquina de secar após a fase principal de aquecimento dentro do ciclo terminado, em vez de os ler como novos ciclos.", "label": "Activar Detecção Anti-Vincos" }, "anti_wrinkle_exit_power": { - "doc": "A potência deve cair abaixo disso entre os pulsos para que o modo antirrugas permaneça ativo.", + "doc": "No modo anti-vincos, a potência abaixo deste valor conta como silêncio entre pulsos de rotação, e um silêncio mais longo do que o Intervalo Máximo Entre Pulsos termina o modo. Se o Limiar de Paragem for mais alto, é esse o usado, por isso isto só tem efeito acima do Limiar de Paragem.", "label": "Limiar de Saída de Potência" }, "anti_wrinkle_idle_timeout": { - "doc": "Durante quanto tempo a máquina pode permanecer em silêncio entre dois pulsos de rotação antes de o modo anti-vincos terminar. Defina-o acima do intervalo mais longo que a sua secadora deixa entre pulsos, caso contrário cada pulso posterior é lido como um falso arranque.", + "doc": "Quanto tempo a máquina de secar pode ficar em silêncio entre impulsos de rotação antes de o modo anti-vincos terminar. Defina-o acima da pausa mais longa da sua máquina, senão os impulsos seguintes são lidos como falsos arranques.", "label": "Intervalo Máximo Entre Pulsos" }, "anti_wrinkle_max_duration": { - "doc": "Pulsos mais longos do que isso são tratados como um ciclo real, e não como uma queda anti-rugas.", + "doc": "Um impulso mais longo do que isto conta como um ciclo real, não como rotação anti-vincos.", "label": "Duração Máxima" }, "anti_wrinkle_max_power": { - "doc": "Uma pulsação acima dessa potência é tratada como um verdadeiro novo ciclo, e não como um tombo antirrugas. Defina logo acima da potência do pulso de queda.", + "doc": "Um impulso acima desta potência conta como um novo ciclo real, não como rotação. Defina-o logo acima da potência dos seus impulsos de rotação.", "label": "Potência Máxima Anti-Vincos" }, "auto_label_confidence": { - "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.", + "doc": "Pontuação de correspondência no fim do ciclo a partir da qual o programa é rotulado sem perguntar. Aumente-a para mais certeza; reduza-a para rotular mais ciclos automaticamente. Mantenha-a acima da Confiança de Aprendizagem.", "label": "Confiança de Rotulagem Automática" }, "auto_maintenance": { - "doc": "Execute a limpeza noturna: recrie envelopes de perfil, recalcule a integridade do ciclo, remova rastros de depuração e retenha os ciclos mais recentes.", + "doc": "Limpeza noturna: rotula ciclos antigos que agora correspondem claramente a um programa, reconstrói as curvas dos programas, atualiza as marcas de anomalia e a saúde dos ciclos, e remove os traços de depuração se Guardar Traços de Depuração estiver desligado. Os ciclos e os seus traços de potência são sempre mantidos.", "label": "Manutenção Automática (limpeza nocturna)" }, "completion_min_seconds": { - "doc": "Ciclos mais curtos que isso são descartados como ciclos fantasmas (testes, abertura da porta para adicionar uma meia).", + "doc": "Os ciclos mais curtos do que isto (testes, abrir a porta para juntar uma meia) são guardados como Interrompido e não recebem notificação de fim.", "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." + "doc": "Acrescenta ao início de um ciclo as leituras de tentativas de arranque falhadas, para máquinas que tateiam antes de arrancar (bloqueio da porta, primeiro enchimento). 300 s chegam para a maioria; 0 = desligado. Os inícios registados passam a ser mais cedo, por isso as durações aprendidas desviam-se durante algum tempo." }, "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.", + "doc": "A potência tem de ficar na banda de espera este tempo antes de o eletrodoméstico contar como à espera de iniciar.", "label": "Segundos de Confirmação" }, "delay_start_detect_enabled": { - "doc": "Detecta quando o aparelho está ligado e aguardando (início retardado/standby), mas ainda não iniciou seu ciclo, para que o standby não seja confundido com um ciclo em execução.", + "doc": "Detetar um eletrodoméstico ligado que aguarda um início diferido, para que o seu consumo em espera não seja tomado por um ciclo em curso.", "label": "Activar Detecção de Início Retardado" }, "delay_timeout_hours": { - "doc": "Pare de esperar no modo de partida retardada após tantas horas e retorne ao modo inativo, para que uma máquina deixada ligada, mas nunca iniciada, não espere para sempre.", + "doc": "Desistir de esperar por um início diferido após este número de horas e voltar a inativo.", "label": "Horas de Timeout" }, "device_type": { - "doc": "Classe de eletrodomésticos. Define padrões de detecção sensatos (limites, atraso de desativação, tratamento final) ajustados para esse tipo de dispositivo; altere-o apenas se o dispositivo tiver sido originalmente configurado como o tipo errado.", + "doc": "Define os valores de deteção predefinidos (limiares, atraso de desligamento, tratamento do fim) para este tipo de eletrodoméstico. Só altere se o dispositivo foi configurado com o tipo errado.", "label": "Tipo de Dispositivo" }, "door_sensor_entity": { - "doc": "Sensor binário de porta opcional. Usado para detectar quando o aparelho foi aberto/descarregado após um ciclo.", + "doc": "Sensor de porta opcional. Mostra quando o eletrodoméstico foi aberto para descarregar após um ciclo.", "label": "Entidade do Sensor de Porta" }, "duration_tolerance": { - "doc": "Tolerância para estimativas de tempo restante (feedback de aprendizagem, não correspondência). Se a duração real estiver dentro de +/-X% da estimativa, isso será considerado uma boa correspondência.", + "doc": "Apenas para aprendizagem: um ciclo dentro de +/-X% da sua duração estimada conta como uma boa estimativa. Não afeta a correspondência.", "label": "Tolerância de Estimativa" }, "enable_ml_models": { - "doc": "Enquanto um ciclo é executado, deixe os modelos refinarem os resultados em tempo real: uma estimativa de tempo restante e de energia/custo mais estável, e uma proteção anti-parada prematura na detecção de final (ela só pode atrasar um término, nunca terminar um mais cedo e é limitada). Usa seus modelos ajustados quando disponíveis, caso contrário, os integrados. Desligado = apenas a lógica clássica baseada em energia (ainda confiável).", + "doc": "Usar um modelo treinado nesta máquina para prever a energia e o custo de um ciclo em curso, e deixar que um programa bem conhecido que para invulgarmente cedo termine sem todo o atraso de desligamento. Desligado: apenas lógica baseada na potência.", "label": "Aplicar modelos inteligentes durante um ciclo" }, "end_energy_threshold": { - "doc": "Durante a contagem regressiva do atraso de desligamento, a energia acumulada (watts x tempo) é comparada a esse limite. Se excedido, a contagem regressiva é reiniciada - mantendo os rolos anti-rugas e os restos de secagem da máquina de lavar louça presos ao ciclo, em vez de cortá-los. Aumente se os ciclos terminarem muito cedo durante o resfriamento; abaixe-o se a detecção for lenta.", + "doc": "Energia permitida durante o atraso de desligamento. Acima disto o ciclo continua, para que rotações e caudas de secagem continuem a fazer parte dele. Reduza-a se os ciclos terminarem durante uma cauda; aumente-a se ciclos terminados continuarem à espera.", "label": "Energia de Fim" }, - "end_repeat_count": { - "doc": "Número de leituras consecutivas abaixo do limite de parada necessárias antes do término do ciclo. 1 é adequado para a maioria dos plugues. Aumente para 2-3 se o seu plugue inteligente ocasionalmente relatar uma amostra de falso zero no meio do ciclo e seus ciclos estiverem terminando prematuramente.", - "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 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.", + "doc": "Sensor com o preço atual por kWh (p. ex. uma tarifa dinâmica). Tem prioridade sobre o preço fixo abaixo, por isso tem de ser um preço, não o sensor de potência ou de energia da tomada.", "label": "Entidade de Preço de Energia" }, "energy_price_static": { - "doc": "Preço fixo por kWh usado para valores de custos quando nenhuma entidade de preço real é definida acima.", + "doc": "Preço fixo por kWh, usado quando não há nenhuma entidade de preço definida.", "label": "Preço Estático de Energia (por kWh)" }, "energy_sensor": { - "doc": "Contador de energia cumulativo opcional (total_increasing em kWh/Wh, por ex. o contador de consumo total da própria tomada). Quando definido, a energia indicada de cada ciclo é obtida a partir da diferença deste contador entre o início e o fim, o que evita a subcontagem resultante da integração de um sensor de potência que reporta lentamente. Recorre ao valor integrado se a leitura estiver em falta, se a sua unidade for desconhecida ou se o delta não for positivo. Deixe em branco para continuar a integrar o sensor de potência.", + "doc": "Contador opcional em kWh ou Wh (total_increasing, p. ex. o próprio contador da tomada). A energia do ciclo passa a ser lida dele, o que é mais preciso do que calculá-la a partir de um sensor de potência lento. Se a leitura não servir, usa-se o valor calculado.", "label": "Entidade do Contador de Energia" }, "expose_debug_entities": { - "doc": "Publique entidades de HA de diagnóstico extras (confiança de correspondência, ambiguidade, estados internos). Off mantém a lista de entidades limpa para uso normal.", + "doc": "Adicionar entidades de diagnóstico (confiança de correspondência, ambiguidade, estado interno). Desligado mantém a lista de entidades limpa.", "label": "Expor Entidades de Depuração" }, "external_end_trigger": { - "doc": "Sensor binário cuja mudança de estado marca o fim do ciclo (por exemplo, um contato \"terminado\" do aparelho ou uma integração complementar).", + "doc": "Sensor binário cuja mudança marca o fim do ciclo (p. ex. um contacto \"concluído\" ou outra integração).", "label": "Entidade do Gatilho Externo" }, "external_end_trigger_enabled": { - "doc": "Deixe um sensor binário externo sinalizar o fim de um ciclo, além da detecção integrada baseada em energia.", + "doc": "Deixar um sensor binário sinalizar o fim de um ciclo, além da deteção baseada na potência.", "label": "Activar Gatilho de Fim Externo" }, "external_end_trigger_inverted": { - "doc": "Trate o sensor de disparo desligado (em vez de ligado) como o sinal de fim de ciclo.", + "doc": "Tratar a passagem do sensor a desligado (e não a ligado) como sinal de fim.", "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 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.", + "doc": "Pontuação de correspondência mínima para rotular um ciclo terminado. Um vencedor claro é rotulado; um caso renhido vai para a fila de revisão em Ciclos. Abaixo disto, nada é rotulado nem perguntado. Mantenha-a abaixo da Confiança de Rotulagem Automática.", "label": "Confiança de Aprendizagem" }, "linked_device": { - "doc": "Opcionalmente, aninhe este dispositivo WashData em outro dispositivo (por exemplo, o plugue inteligente) no registro do dispositivo HA, mostrado como \"Conectado via ...\".", + "doc": "Opcional. Mostrar este dispositivo WashData sob outro dispositivo (p. ex. a tomada inteligente) no Home Assistant, como \"Ligado através de ...\".", "label": "Agrupar sob Dispositivo" }, "min_off_gap": { - "doc": "Se a máquina desligar por menos que esse tempo, a sequência liga/desliga/liga é tratada como um ciclo contínuo. Evita que os programas de imersão (a máquina desliga durante vários minutos a meio da lavagem) sejam divididos em dois ciclos separados. Defina-o mais curto do que o intervalo entre as cargas consecutivas se quiser que elas sejam contadas como ciclos separados. Os padrões de tipo de dispositivo protegem a pausa intra-ciclo típica para cada dispositivo.", + "doc": "Um período desligado mais curto do que isto não divide um ciclo, por isso as pausas de molho ficam numa só lavagem. Mantenha-o abaixo da pausa entre cargas seguidas, senão fundem-se num só ciclo.", "label": "Intervalo Mínimo de Desligamento" }, "min_power": { - "doc": "Potência mínima absoluta considerada ativa. As leituras abaixo disso são tratadas como 0 W (standby), filtrando a carga fantasma de plugues inteligentes e LEDs de standby.", + "doc": "Leituras abaixo disto contam como 0 W (espera). Filtra o consumo em repouso das tomadas inteligentes e dos LED de espera.", "label": "Potência Mínima" }, "ml_training_enabled": { - "doc": "Estude periodicamente seus ciclos revisados durante a noite e ajuste os modelos para esta máquina específica. Uma mudança só é mantida quando realmente obtém resultados melhores em ciclos prolongados, portanto, isso só pode ajudar ou permanecer igual - nunca regredir.", + "doc": "Retreinar de vez em quando o modelo de energia com os ciclos terminados desta máquina. Um modelo novo só é mantido se superar a estimativa simples em ciclos de teste.", "label": "Aprender com esta máquina" }, "ml_training_hour": { - "doc": "Hora local do dia (0-23) para fazer o ajuste fino noturno. Escolha um horário tranquilo, como 2 (02:00).", + "doc": "Hora do dia (0-23) para o treino noturno. Escolha uma hora calma, p. ex. 2.", "label": "Aprender às horas" }, "ml_training_interval_days": { - "doc": "Verifique novamente se há melhorias no máximo uma vez a cada um desses dias.", + "doc": "Retreinar no máximo uma vez a cada este número de dias.", "label": "Verificar no máximo de" }, "ml_training_min_cycles": { - "doc": "Espere até que pelo menos esse número de ciclos tenha sido registrado antes do ajuste fino, para que haja o suficiente para aprender.", + "doc": "Esperar por pelo menos este número de ciclos gravados antes de treinar.", "label": "Ciclos necessários primeiro" }, "name": { - "doc": "Nome de exibição mostrado na lista de integrações de alta disponibilidade e no registro do dispositivo.", + "doc": "Nome mostrado no Home Assistant para este dispositivo.", "label": "Nome do Dispositivo" }, "no_update_active_timeout": { - "doc": "Se nenhuma atualização de energia chegar por tanto tempo durante a execução, presuma que o plugue caiu off-line e force a parada para evitar um ciclo zumbi. O padrão de 600 s permite atraso na nuvem ou na malha.", + "doc": "Se não chegar nenhuma atualização de potência durante este tempo num ciclo, assume-se que a tomada ficou offline e o ciclo é parado à força. A predefinição de 600 s dá margem a atrasos da nuvem ou da rede mesh.", "label": "Timeout Sem Actualização" }, "notify_before_end_minutes": { @@ -1329,35 +1301,35 @@ "label": "Alerta Pré-Fim" }, "notify_channel": { - "doc": "Nome do canal de notificação do Android para mensagens iniciais/ao vivo (controla o som e a prioridade por canal no aplicativo móvel). Em branco usa o aplicativo complementar padrão.", + "doc": "Canal Android para as mensagens de início e ao vivo; o canal define o som e a prioridade. Em branco: a predefinição da aplicação.", "label": "Canal Android (início/tempo real)" }, "notify_cycle_timers": { - "doc": "Notificações em minutos específicos de um ciclo (por exemplo, para adicionar amaciante). A mensagem suporta {device}, {program}, {minutes}. Ative a pausa automática para pausar nesse ponto e receber uma notificação interativa com um botão Continuar; retomar por meio do painel, do serviço de pausa/retomada ou da ação de notificação.", + "doc": "Notificações em minutos definidos de um ciclo (p. ex. para juntar amaciador). A mensagem suporta {device}, {program}, {minutes}. Com Pausa automática, o ciclo também pausa aí e a notificação recebe um botão Retomar.", "label": "Temporizadores de Ciclo" }, "notify_finish_channel": { - "doc": "Nome do canal de notificação do Android para a mensagem final. Em branco reutiliza o canal inicial/ao vivo.", + "doc": "Canal Android para a mensagem de fim. Em branco: reutiliza o canal de início/ao vivo.", "label": "Canal Android (fim)" }, "notify_finish_message": { - "doc": "Corpo enviado quando um ciclo termina. Variáveis do modelo: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}. {time_finished} e {vs_typical} são os mais úteis aqui.", + "doc": "Corpo enviado quando um ciclo termina. Variáveis: {device}, {program}, {duration} (minutos), {duration_hm} (1 h 05 min), {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}, {status} (completed ou force_stopped; os ciclos interrompidos não enviam nada). Com qualquer outra variável é usado o texto predefinido.", "label": "Mensagem de Fim" }, "notify_finish_services": { - "doc": "notify.* serviços chamados quando um ciclo termina. Adicione um por alvo; deixe em branco para nenhuma notificação de conclusão.", + "doc": "Serviços notify.* a chamar quando um ciclo termina. Deixe vazio para nenhum.", "label": "Serviços de Fim" }, "notify_fire_events": { - "doc": "Também dispare eventos ha_washdata_* no início/término do ciclo para que você possa construir suas próprias automações.", + "doc": "Disparar também eventos ha_washdata_* no início e no fim do ciclo, para as suas próprias automatizações.", "label": "Disparar Eventos HA para Notificações" }, "notify_icon": { - "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.", + "doc": "Ícone pequeno da notificação (p. ex. mdi:washing-machine). Enviado apenas para destinos mobile_app, não para grupos notify. No iOS substitui o ícone da aplicação e requer a aplicação companion 2026.8 ou mais recente. Em branco: a predefinição da plataforma.", "label": "Ícone da Notificação" }, "notify_live_chronometer": { - "doc": "Mostre um cronômetro de contagem regressiva com atualização ao vivo na notificação (em plataformas que o suportam) em vez de uma estimativa estática.", + "doc": "Mostrar uma contagem decrescente em curso na notificação (onde suportado) em vez de uma estimativa fixa.", "label": "Usar Cronómetro em Tempo Real" }, "notify_live_interval_seconds": { @@ -1365,143 +1337,135 @@ "label": "Intervalo de Actualização em Tempo Real" }, "notify_live_overrun_percent": { - "doc": "Se um ciclo ultrapassar sua estimativa em mais do que essa porcentagem, envie um alerta de ultrapassagem.", + "doc": "As atualizações ao vivo param quando um ciclo excede a duração prevista nesta percentagem (20 = até 1,2x o tempo previsto).", "label": "% de Sobrecarga Antes do Alerta" }, "notify_live_services": { - "doc": "notify.* serviços chamados para atualizações de progresso ao vivo enquanto um ciclo é executado. Deixe em branco para desativar as notificações de progresso ao vivo.", + "doc": "Serviços notify.* para as atualizações de progresso ao vivo durante um ciclo. Deixe vazio para as desligar.", "label": "Serviços de Progresso em Tempo Real" }, "notify_only_when_home": { - "doc": "Envie notificações apenas quando pelo menos uma das pessoas vinculadas (acima) estiver em casa.", + "doc": "Notificar apenas quando pelo menos uma das pessoas acima estiver em casa.", "label": "Notificar Apenas Quando em Casa" }, "notify_people": { - "doc": "person.* entidades usadas por \"Notify Only When Home\" para decidir se alguém está em casa.", + "doc": "Entidades person.* que \"Notificar Apenas Quando em Casa\" verifica.", "label": "Pessoas (para Apenas Quando em Casa)" }, "notify_pre_complete_message": { - "doc": "Corpo do alerta de pré-fim / quase concluído. Variáveis do modelo: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Texto de cada atualização de progresso ao vivo. Variáveis de modelo: {device}, {program}, {minutes} (minutos restantes). Com qualquer outra variável é usado o texto predefinido.", "label": "Mensagem Pré-Conclusão" }, "notify_reminder_message": { - "doc": "Corpo do lembrete de que a roupa continua à espera de ser retirada. Em branco usa a predefinição incorporada. Variáveis do modelo: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Texto do Alerta Pré-Fim, enviado uma única vez. Variáveis de modelo: {device}, {program}, {minutes} (a definição Alerta Pré-Fim). Com qualquer outra variável é usado o texto predefinido.", "label": "Mensagem de Lembrete" }, "notify_start_message": { - "doc": "Corpo enviado quando um ciclo começa. Variáveis do modelo: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Corpo enviado quando um ciclo começa. Variáveis de modelo: {device}, {program}. Com qualquer outra variável é usado o texto predefinido.", "label": "Mensagem de Início" }, "notify_start_services": { - "doc": "notify.* serviços chamados quando um ciclo é iniciado. Adicione um por destino (telefone, painel, etc.); deixe em branco para nenhuma notificação de início.", + "doc": "Serviços notify.* a chamar quando um ciclo começa (telemóvel, painel, ...). Deixe vazio para nenhum.", "label": "Serviços de Início" }, "notify_timeout_seconds": { - "doc": "Ignore automaticamente a notificação após esses segundos (em plataformas que a suportam). 0 o mantém até ser descartado manualmente.", + "doc": "Fechar a notificação após este número de segundos (onde suportado). 0 mantém-na até a fechar.", "label": "Dispensar Automaticamente Após" }, "notify_title": { - "doc": "Título da notificação. Variáveis de modelo: {device}, {program}, {duration}, {energy}, {cost}, {date}, {time}, {minutes}.", + "doc": "Título da notificação. Só {device} é substituído; com qualquer outra variável é usado o título predefinido.", "label": "Título da Notificação" }, "notify_unload_delay_minutes": { - "doc": "Minutos após o término de um ciclo, antes de enviar o lembrete de \"descarregar a máquina\" ainda em espera. Defina 0 para desativar o lembrete.", + "doc": "Minutos após o fim de um ciclo até ao lembrete \"descarregar a máquina\". 0 desativa-o.", "label": "Atraso de Lembrete de Descarga" }, "notify_unload_repeat": { - "doc": "Continua a reenviar o lembrete de descarga a cada \"Atraso de Lembrete de Descarga\" minutos até a descarga ser confirmada ou tocar em \"Parar de lembrar\" na notificação. Requer uma forma de confirmar a descarga: uma Entidade do Sensor de Porta, uma Entidade de Confirmação de Descarga ou a opção \"Confirmar a Descarga Manualmente\" abaixo. O botão de dispensar funciona nas notificações da aplicação complementar (móvel) do Home Assistant.", + "doc": "Reenviar o lembrete de descarga a cada \"Atraso de Lembrete de Descarga\" minutos até a descarga ser confirmada ou tocar em \"Parar de lembrar\" (aplicação móvel). Requer uma Entidade do Sensor de Porta, uma Entidade de Confirmação de Descarga ou \"Confirmar a Descarga Manualmente\".", "label": "Repetir Até Confirmar a Descarga" }, "off_delay": { - "doc": "Tempo de espera após a queda de energia antes de declarar o término do ciclo. Se a energia for retomada dentro desta janela, o ciclo continua perfeitamente - isso faz a ponte entre as etapas de lavagem. As máquinas de lavar louça têm longas fases de secagem (desligadas por 20-60 minutos), portanto o atraso no desligamento deve exceder esse valor para manter toda a lavagem + secagem em um único ciclo.", + "doc": "Quanto tempo a potência tem de ficar baixa antes de o ciclo terminar. Se a potência voltar dentro deste tempo, o ciclo continua. As máquinas de lavar loiça podem ficar paradas 20-60 min durante a secagem, por isso defina-o acima disso.", "label": "Atraso de Desligamento" }, "pause_cuts_power": { - "doc": "Quando um ciclo for pausado, desligue também a Entidade Switch abaixo. Apenas para aparelhos cuja ficha possa ser cortada com segurança a meio do ciclo.", + "doc": "Ao pausar um ciclo, desligar também a Entidade do Switch abaixo. Apenas para eletrodomésticos que podem perder a energia a meio do ciclo em segurança.", "label": "Pausa Também Corta Energia (via switch)" }, "power_off_delay": { - "doc": "Durante quanto tempo a potência deve manter-se abaixo do Limiar de Desligamento após o fim de um ciclo antes de o estado voltar a Desligado. Só é usado quando o Limiar de Desligamento é superior a 0. É verificado segundo a cadência em segundo plano, pelo que o atraso efetivo é arredondado para cima até ao próximo tick de expiração de estado.", + "doc": "Quanto tempo a potência tem de ficar abaixo do Limiar de Desligamento após um ciclo antes de o estado voltar a Desligado. Só é usado quando esse limiar é superior a 0. É verificado periodicamente, por isso a espera real pode ser um pouco maior.", "label": "Atraso de Desligamento" }, "power_off_threshold_w": { - "doc": "Deteção opcional do estado Desligado com base na potência. Quando superior a 0, assim que um ciclo termina e a potência se mantém abaixo deste nível durante o Atraso de Desligamento, a máquina é considerada desligada e o estado volta a Desligado. Deixe em 0 para desativar (predefinição). Defina-o acima do consumo real com a máquina desligada e abaixo do Limiar de Paragem e do consumo em espera da sua máquina (concluída mas ainda ligada); se não for inferior ao Limiar de Paragem, é ignorado. Quando ativado, substitui o Atraso de Reposição de Progresso para o regresso a Desligado, pelo que uma máquina concluída permanece em Concluído/Limpar até ser efetivamente desligada.", + "doc": "Após um ciclo, potência abaixo deste valor durante o Atraso de Desligamento põe o estado em Desligado, em vez de o fazer o Atraso de Reposição de Progresso. Defina-o acima do consumo desligado e abaixo do consumo em espera e do Limiar de Paragem. 0 desativa-o (predefinição).", "label": "Limiar de Desligamento" }, "power_sensor": { - "doc": "A entidade do sensor que relata a potência ativa em watts para este aparelho (por exemplo, sensor.washer_power). Toda a detecção de ciclo é baseada neste sinal.", + "doc": "O sensor que reporta a potência atual deste eletrodoméstico em watts (p. ex. sensor.washer_power). Toda a deteção se baseia nele.", "label": "Sensor de Potência" }, - "profile_duration_tolerance": { - "doc": "A banda +/- em torno da duração média de um perfil usada durante a correspondência. 0,25 significa que um perfil de 60 minutos corresponde a ciclos de 45 a 75 minutos.", - "label": "Tolerância de Duração do Perfil" - }, "profile_match_interval": { - "doc": "Com que frequência tentar a correspondência de perfil durante um ciclo de execução. O padrão de 300 s (5 minutos) equilibra a velocidade de detecção e a CPU.", + "doc": "Com que frequência um ciclo em curso é comparado com os perfis. Predefinição 300 s (5 min).", "label": "Intervalo de Correspondência" }, "profile_match_max_duration_ratio": { - "doc": "Duração máxima do ciclo em relação ao perfil. 1.3 significa que um ciclo deve ter menos de 130% da duração do perfil para corresponder.", + "doc": "Um programa é descartado quando o ciclo ultrapassa este múltiplo da sua duração habitual. 1,8: um ciclo 80% mais longo do que o programa já não lhe corresponde.", "label": "Rácio de Duração Máxima" }, "profile_match_min_duration_ratio": { - "doc": "Duração mínima do ciclo em relação ao perfil. 0,9 significa que um ciclo deve ter pelo menos 90% da duração do perfil para corresponder.", + "doc": "Um programa só é considerado quando o ciclo atingiu esta fração da sua duração habitual. 0,10: um programa de 2 horas torna-se candidato aos 12 minutos. Não se aplica nos primeiros 15 minutos de um ciclo.", "label": "Rácio de Duração Mínima" }, "profile_match_threshold": { - "doc": "Pontuação mínima de similaridade (0-1) exigida no final do ciclo para aceitar a identificação do programa. Aumente-o para reduzir identificações erradas; abaixe-o se os programas da sua máquina não estiverem correspondendo. O padrão 0,4 é um ponto de partida conservador.", + "doc": "Pontuação de correspondência ao vivo de que um ciclo precisa antes de a Terminação inteligente ou o fim anti-vincos o poderem terminar mais cedo. Não altera o programa mostrado. Aumente-a se correspondências fracas terminarem ciclos cedo demais; reduza-a se correspondências claras esperarem todo o atraso de desligamento.", "label": "Limiar de Correspondência" }, "profile_unmatch_threshold": { - "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.", + "doc": "Se a pontuação de correspondência ao vivo descer abaixo deste valor, a suposição atual de programa é limpa. Mantenha-a um pouco abaixo do Limiar de Correspondência para que uma descida breve não a reponha.", "label": "Limiar de Não Correspondência" }, "progress_reset_delay": { - "doc": "Depois de terminar, mantenha o progresso em 100% por esse tempo para que Concluído fique visível nos painéis antes de redefinir para Inativo.", + "doc": "Após um ciclo, manter o progresso a 100% durante este tempo antes de voltar a Inativo.", "label": "Atraso de Reposição de Progresso" }, "pump_stuck_duration": { - "doc": "Segundos, uma bomba pode funcionar continuamente antes de ser sinalizada como possivelmente travada (dispara o evento de bomba travada).", + "doc": "Quanto tempo uma bomba pode funcionar sem parar antes de disparar o evento de bomba presa.", "label": "Duração de Bomba Presa" }, "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.", + "doc": "Tempo mínimo entre leituras processadas. As leituras que chegam mais cedo são ignoradas (as leituras baixas durante um ciclo são sempre mantidas). Defina-o para o intervalo de reporte da sua tomada ou menos; um valor maior só descarta leituras.", "label": "Intervalo de Amostragem" }, "save_debug_traces": { - "doc": "Armazene o rastreamento de energia completo e os dados de depuração correspondentes para cada ciclo. Útil para solução de problemas, mas aumenta o tamanho do armazenamento.", + "doc": "Guardar também com cada ciclo os dados de depuração do comparador (as pontuações de cada candidato), para resolução de problemas. Acrescenta cerca de 20 KB por ciclo.", "label": "Guardar Traços de Depuração" }, "smart_termination_duration_ratio": { - "doc": "Até que ponto um ciclo deve ter avançado na duração esperada do programa correspondente antes de a Terminação inteligente o poder terminar mais cedo assim que a potência cai. A duração esperada é a média do programa, por isso em aparelhos cujo tempo de funcionamento varia muito - máquinas de lavar consoante a água de entrada fria no inverno ou morna no verão, secadores com sensor de humidade, programas que dependem da carga - cerca de metade das execuções termina mais cedo do que essa média e nunca beneficia do fim rápido, terminando apenas através do tempo limite de reserva com minutos de atraso. Reduza este valor (p. ex. 0,85) nessas máquinas para que o fim antecipado continue a disparar; aumente-o para perto de 1,0 para ser mais conservador. Deixe vazio para o valor predefinido (0,98, ou 0,99 para máquinas de lavar louça). Só pode terminar um ciclo mais cedo, nunca mais tarde, e nunca dispara numa correspondência ambígua ou de baixa confiança.", + "doc": "Fração da duração média do programa que um ciclo deve atingir antes de a Terminação inteligente o poder terminar mais cedo. Se os tempos de execução variarem (temperatura da água, secagem por sensor, tamanho da carga), reduza-a (p. ex. 0,85). Deixe vazio para 0,98 (0,99 em máquinas de lavar loiça).", "label": "Rácio de terminação inteligente" }, - "smoothing_window": { - "doc": "Quanto o sinal de potência bruta é suavizado. Baixo (2) é responsivo, mas barulhento; alto (5) suaviza os picos, mas adiciona atraso.", - "label": "Janela de Suavização" - }, "start_duration_threshold": { - "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.", + "doc": "A potência tem de ficar acima do Limiar de Início este tempo antes de um ciclo começar, para ignorar picos breves de liga/desliga.", "label": "Duração de Início" }, "start_energy_threshold": { - "doc": "Energia (potência x tempo) que o aparelho deve consumir antes de FUNCIONAR. Um breve pico de alta potência tem energia muito baixa e é ignorado, evitando falsas partidas.", + "doc": "Energia que o eletrodoméstico tem de consumir antes de o ciclo contar como em curso. Um pico breve de potência consome pouca energia, por isso não inicia um ciclo.", "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. 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.", + "doc": "A potência tem de subir acima deste valor para iniciar um ciclo. Demasiado baixo: o consumo em espera causa falsos arranques. Demasiado alto: programas de arranque lento (enchimento a frio) são perdidos.", "label": "Limiar de Início" }, "stop_threshold_w": { - "doc": "A energia deve cair abaixo deste nível antes que a contagem regressiva do atraso de desligamento comece. Defina-o abaixo do limite inicial - a lacuna entre eles é a banda de histerese que evita oscilações. Se definido para um valor demasiado elevado, as fases de baixa potência (paragens de enxaguamento, anti-rugas) acionam falsamente a sequência final.", + "doc": "A potência tem de descer abaixo deste valor antes de o atraso de desligamento começar a contar. Mantenha-o abaixo do Limiar de Início. Se for demasiado alto, fases de baixa potência (pausas de enxaguamento, anti-vincos) iniciam por engano a contagem para o fim.", "label": "Limiar de Paragem" }, "switch_entity": { - "doc": "Interruptor opcional desligado na pausa e ligado novamente ao reiniciar, usado junto com \"Pausa também corta energia\".", + "doc": "Interruptor opcional que desliga na pausa e volta a ligar ao retomar, com \"Pausa Também Corta Energia\".", "label": "Entidade do Switch" }, "watchdog_interval": { - "doc": "Com que frequência o watchdog em segundo plano verifica sensores paralisados e tempos limite decorridos. Padrão 30 seg.", + "doc": "Com que frequência a verificação em segundo plano procura sensores parados e tempos limite expirados. Predefinição 30 s.", "label": "Intervalo do Watchdog" }, "notify_milestone_message": { @@ -1509,127 +1473,82 @@ "label": "Mensagem de marco" }, "notify_milestones": { - "doc": "Contagens de ciclos separadas por vírgulas que acionam uma notificação de celebração única quando são atingidas (ex. 50, 100, 500, 1000). Em branco desativa as notificações de marcos.", + "doc": "Contagens de ciclos que disparam uma notificação única de celebração, separadas por vírgulas (p. ex. 50, 100, 500, 1000). Em branco desativa-as.", "label": "Marcos de ciclos" }, "notify_quiet_end_hour": { - "doc": "Fim da janela de não incomodar (0-23). As notificações retidas são entregues a esta hora. Deixe em branco para desativar.", + "doc": "Fim da janela de não incomodar (0-23); as notificações retidas são enviadas nessa altura. Em branco desativa-a.", "label": "Fim das horas de silêncio" }, "notify_quiet_start_hour": { - "doc": "Início de uma janela de não incomodar (0-23). As notificações de conclusão, lembretes e de roupa lavada que ocorreriam durante as horas de silêncio são retidas e entregues quando a janela termina. Deixe em branco para desativar. Suporta janelas que cruzam a meia-noite (ex. início 22, fim 7).", + "doc": "Início de uma janela de não incomodar (0-23). As notificações de fim, lembrete e roupa limpa nesta janela ficam retidas até ela terminar. Pode atravessar a meia-noite (p. ex. 22 a 7). Em branco desativa-a.", "label": "Início das horas de silêncio" }, "peak_rate_message": { - "doc": "Texto personalizado opcional para a dica de tarifa de pico anexada à notificação de início. Variáveis do modelo: {device}, {price}. Em branco usa a predefinição incorporada.", + "doc": "Texto próprio para o aviso de tarifa de ponta. Variáveis de modelo: {device}, {price}. Em branco usa a predefinição.", "label": "Mensagem de tarifa de pico" }, "peak_rate_threshold": { - "doc": "Quando um ciclo começa e o preço atual por kWh é igual ou superior a este valor, é anexada uma dica de tarifa de pico à notificação de início. 0 ou em branco desativa a dica.", + "doc": "Se o preço por kWh for igual ou superior a este valor quando um ciclo começa, acrescenta um aviso de tarifa de ponta à notificação de início. 0 ou em branco desativa-o.", "label": "Limite de tarifa de pico (por kWh)" }, - "dtw_bandwidth": { - "label": "Tolerância de correspondência de forma", - "doc": "Quanta deformação temporal o sistema de correspondência pode aplicar a um ciclo para o ajustar à forma de um programa guardado. Um valor mais alto tolera maior variação de velocidade; demasiado alto corre o risco de corresponder ao programa errado." - }, - "corr_weight": { - "label": "Peso da forma face ao nível", - "doc": "Equilíbrio entre a forma da curva e o nível de potência absoluto ao pontuar uma correspondência. Um valor mais alto privilegia a forma; um mais baixo privilegia a correspondência do nível de potência." - }, - "duration_weight": { - "label": "Peso da duração", - "doc": "Com que intensidade é preferido um programa cuja duração habitual corresponde a este ciclo." - }, - "energy_weight": { - "label": "Peso da energia", - "doc": "Com que intensidade é preferido um programa cujo consumo de energia habitual corresponde a este ciclo." - }, "store_brand": { "label": "Marca do electrodoméstico", - "doc": "Opcional. A marca do electrodoméstico, escolhida do catálogo da comunidade. Usada para encontrar e partilhar gravações de referência correspondentes. Deixe em branco se não usar as funcionalidades online." + "doc": "Opcional. A marca do seu eletrodoméstico no catálogo da comunidade, para encontrar e partilhar gravações de referência. Deixe em branco se não usar as funções online." }, "store_model": { "label": "Modelo do electrodoméstico", - "doc": "Opcional. O modelo do electrodoméstico, escolhido do catálogo da comunidade depois de definida a marca. Se o seu modelo não estiver listado, pode adicioná-lo ao catálogo." + "doc": "Opcional. O modelo do seu eletrodoméstico no catálogo da comunidade (escolha primeiro uma marca). Se não estiver na lista, pode adicioná-lo." }, "show_contributor": { "doc": "Mostrar a atribuição \"por \" nos electrodomésticos da comunidade e nos ciclos de referência." }, - "enable_phase_matching": { - "label": "Tempo restante com base nas fases", - "doc": "Divide cada ciclo em curso em fases (aquecimento, lavagem, centrifugação) e distribui o tempo restante por fase, combinado com a estimativa clássica - apoiando-se na repartição por fases no início do ciclo e na estimativa clássica perto do fim. Isto personaliza a contagem decrescente de acordo com o tempo que a sua máquina realmente aquece e funciona, o que é mais percetível na primeira metade de um ciclo. Desativado = apenas a estimativa clássica. Apenas a apresentação do tempo restante é afetada; a correspondência de programas e a deteção de ciclos permanecem inalteradas." - }, - "keep_min_score": { - "label": "Score mín de correspondência", - "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", - "doc": "O quanto o score de alinhamento por deformação temporal (DTW) substitui o score central da fase 2. 0 = usar apenas o score da fase 2, 1 = usar apenas o score DTW, 0,5 (predefinição) = mistura equilibrada. Aumente para confiar mais no alinhamento Warp quando os programas têm níveis de potência semelhantes mas padrões temporais diferentes." - }, - "dtw_ensemble_w": { - "label": "Mix ensemble Warp", - "doc": "No modo DTW ensemble (a predefinição), o peso em L1 escalado versus DTW derivativo (DDTW). 1,0 = apenas L1 escalado, 0 = apenas DDTW, 0,7 = predefinição. A variante L1 escalada compara os níveis de potência; a variante derivativa reage a como a potência muda ao longo do tempo. Ensemble combina ambos." - }, - "dtw_ddtw_scale": { - "label": "Escala Warp derivativa", - "doc": "Distância de semissaturação para a pontuação DTW derivativa. A pontuação é reduzida a metade quando a distância DDTW é igual a este valor. Menor = mais sensível às diferenças de forma. A predefinição 30 está calibrada para traços de potência típicos de eletrodomésticos. Afeta apenas os modos DTW ensemble e ddtw." - }, - "dtw_refine_top_n": { - "label": "Contagem de refinamento Warp", - "doc": "Quantos candidatos principais da fase 2 são reavaliados pelo DTW. O DTW é mais dispendioso, por isso é aplicado apenas aos N melhores candidatos. Predefinição 5. Aumente (para 7-9) se o perfil correto às vezes só chega ao 4.º ou 5.º lugar após a fase 2 - o DTW pode recuperá-lo. Reduzir acelera ligeiramente a correspondência." - }, - "duration_scale": { - "label": "Nitidez de duração", - "doc": "Precisão da penalidade de concordância de duração na fase 4. Este é o log-razão em que o score de concordância é reduzido a metade. Menor = mais rigoroso: uma discordância de duração penaliza mais. A predefinição 0,175 corresponde a cerca de 18% de tolerância de duração a meio peso. Use em conjunto com o Peso de duração." - }, - "energy_scale": { - "label": "Nitidez de energia", - "doc": "Precisão da penalidade de concordância de energia. Menor = mais rigoroso: uma discordância de energia penaliza mais. A predefinição 0,25 é intencionalmente mais permissiva do que a Precisão de duração, porque a energia varia com a carga. Use em conjunto com o Peso de energia." - }, "power_profile_interval_min": { "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." + "doc": "Tamanho do intervalo do atributo power_profile (watts médios por intervalo) usado por planeadores como EMHASS e tibber_prices. Mais pequeno mantém os picos curtos; maior suaviza a curva. Predefinição 15 min. 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." + "doc": "Apenas iOS. Atualizar o progresso ao vivo em silêncio, com uma prioridade que poupa bateria e que o iOS pode agrupar. A primeira atualização e as notificações de início e fim continuam a alertar. Desligado: cada atualização alerta." }, "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." + "doc": "Apenas Android. Manter a notificação de progresso ao vivo quando tocada. Desligado: um toque fecha-a." }, "notify_live_click_action": { "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." + "doc": "O que um toque na notificação deste eletrodoméstico abre. Em branco: o painel WashData neste eletrodoméstico. Introduza um caminho de painel (p. ex. /lovelace/laundry) ou um URL, ou \"none\" para nenhuma ação ao tocar." }, "door_opens_at_end": { "label": "Porta Abre Automaticamente no Fim", - "doc": "Para máquinas de lavar louça que abrem a porta automaticamente no fim do ciclo para secar (AirDry e similares). Com esta opção ativa, a abertura da porta durante um ciclo em execução deixa de o pausar indefinidamente; em vez disso, se a porta permanecer aberta durante o tempo de espera indicado abaixo, o WashData considera o ciclo concluído. Uma abertura breve (ao adicionar um item) é ignorada. Requer uma Entidade do Sensor de Porta." + "doc": "Para máquinas de lavar loiça que abrem a porta no fim para secar (p. ex. AirDry). Uma porta que fica aberta durante o tempo de espera abaixo termina o ciclo em vez de o pausar; uma abertura breve é ignorada. Requer uma Entidade do Sensor de Porta." }, "door_end_dwell_seconds": { "label": "Tempo de Espera com Porta Aberta para Fim", - "doc": "Quanto tempo a porta tem de permanecer aberta antes de o WashData terminar o ciclo, quando a opção \"Porta Abre Automaticamente no Fim\" está ativa. Suficientemente longo para ignorar a adição rápida de um prato (predefinição: 60 s), suficientemente curto para terminar prontamente quando a máquina abre a porta." + "doc": "Com «Porta Abre Automaticamente no Fim» ligado: quanto tempo a porta tem de ficar aberta antes de o ciclo terminar. O suficiente para ignorar que se junta um prato (predefinição 60 s)." }, "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." + "doc": "Que tipos de ciclos moldam a curva e a correspondência de cada programa. Desmarcar um tipo não apaga nada; útil se não confiar nos dados importados. As estatísticas contam sempre apenas os ciclos desta máquina. Desmarcar todos é ignorado." }, "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." + "doc": "Cobrar cada ciclo ao preço de cada momento em que funcionou, em vez do preço no fim. Requer uma entidade de preço. Processar histórico (Diagnósticos) recalcula os ciclos mais antigos." }, "unload_confirm_entity": { "label": "Entidade de Confirmação de Descarga", - "doc": "Entidade opcional cuja ativação significa que a carga foi retirada, para máquinas que não podem ter um sensor de porta. Serve qualquer tipo: um botão Zigbee ou NFC, um ajudante input_button, um sensor de movimento em frente à máquina, uma cena. Qualquer mudança de estado conta como confirmação, exceto desligar-se ou ficar indisponível. Definir isto também ativa o lembrete de descarga num dispositivo sem Entidade do Sensor de Porta." + "doc": "Opcional. Qualquer entidade que confirme que a carga foi retirada, para máquinas sem sensor de porta (um botão, input_button, sensor de movimento, cena). Qualquer mudança de estado conta, exceto desligar ou ficar indisponível. Também ativa o lembrete de descarga." }, "unload_track_without_door": { "label": "Confirmar a Descarga Manualmente", - "doc": "Executa o lembrete de descarga num dispositivo sem Entidade do Sensor de Porta e sem Entidade de Confirmação de Descarga, confirmando a descarga a partir da sua própria automação: prima a entidade de botão \"Marcar como descarregado\" ou chame o serviço ha_washdata.mark_unloaded. É ignorado quando está definida uma Entidade do Sensor de Porta, que já fornece o sinal." + "doc": "Usar o lembrete de descarga sem sensor de porta nem entidade de confirmação. Confirme a descarga a partir da sua própria automatização com o botão \"Marcar como descarregado\" ou o serviço ha_washdata.mark_unloaded. Ignorado quando há uma Entidade do Sensor de Porta definida." }, "notify_icon_color": { "label": "Cor da Notificação", - "doc": "Cor de destaque para as notificações deste eletrodoméstico, para distinguir num relance uma máquina de lavar roupa, uma máquina de secar e uma máquina de lavar louça. O iOS colore o ícone e a barra de progresso da Atividade em Direto no Ecrã Bloqueado e na Dynamic Island. O Android recebe a mesma cor, mas o que faz com ela depende do telemóvel: alguns colorem o ícone e muitos (em especial o Samsung One UI) ignoram-na por completo. Ambos entendem o formato hexadecimal (por exemplo, #03A9F4). Deixe em branco para a predefinição da plataforma." + "doc": "Cor de destaque para as notificações deste eletrodoméstico, para distinguir facilmente máquina de lavar, máquina de secar e máquina de lavar loiça. No iOS preenche o círculo atrás do ícone e pinta a barra da Live Activity. Muitos telemóveis Android (sobretudo Samsung) ignoram-na. Use hex, p. ex. #03A9F4. Em branco: a predefinição da plataforma." + }, + "share_name": { + "doc": "Mostrar o nome da sua conta como autor dos ciclos que partilha. Desligado: partilhados sem nome." } }, "setting_group": { @@ -1657,9 +1576,6 @@ "door_pause": { "label": "Porta e Pausa" }, - "dtw_refinement": { - "label": "Refinamento DTW" - }, "duration_gates": { "label": "Portas de Duração" }, @@ -1736,13 +1652,18 @@ "rebuilding": "A reconstruir…", "analyzing": "A analisar…", "resetting": "A repor…", - "reverting": "A reverter…", "trimming": "A cortar…", "splitting": "A dividir…", + "stalled": "Parado", "deleting": "A eliminar…", "imported": "Importado", "preparing": "A preparar…", - "unlabelling": "A remover rótulo…" + "unlabelling": "A remover rótulo…", + "working": "A processar…", + "match_uncertain": "Incerto: {top} ou {runner}", + "match_uncertain_single": "Incerto: talvez {top}", + "match_sure": "~{pct}% de certeza", + "match_uncertain_tip": "O WashData ainda não se decidiu por um programa. O número indica quantas vezes a opção principal, {top}, acertou em ciclos anteriores quando ia tão à frente. Apenas informativo." }, "tab": { "advanced": "Avançado", @@ -1772,6 +1693,11 @@ "wrong_profile": "Perfil errado" }, "toast": { + "task_name_required": "Dê um nome à tarefa", + "task_added": "Tarefa adicionada", + "task_save_failed": "Não foi possível guardar a tarefa: {error}", + "task_removed": "Tarefa removida", + "maint_logged": "Registado como feito: {task}", "catalog_refreshed": "Catálogo da comunidade actualizado", "access_saved": "Controlo de acesso guardado", "all_wiped": "Todos os dados apagados", @@ -1779,7 +1705,6 @@ "auto_label_complete": "Rotulagem automática concluída", "automation_deleted": "Automatização eliminada", "correction_submitted": "Correcção submetida", - "cycle_deleted": "Ciclo eliminado", "cycle_force_stopped": "Ciclo parado forçosamente", "cycle_labelled": "Ciclo rotulado", "cycle_paused": "Ciclo em pausa", @@ -1795,11 +1720,9 @@ "group_deleted": "Grupo eliminado", "group_name_required": "O nome do grupo é obrigatório", "group_saved": "Grupo guardado", - "import_successful": "Importação bem-sucedida; a integração está a recarregar", "json_required": "Os dados JSON são obrigatórios", "legacy_removed": "Acções legadas removidas", "logs_exported": "Registos exportados", - "matching_reverted": "Pesos de correspondência repostos para as predefinições", "min_2_profiles": "Seleccione pelo menos 2 perfis para um grupo", "ml_training_no_improvement": "Treino concluído: base mantida (sem melhoria)", "models_reverted": "Modelos no dispositivo revertidos para a base", @@ -1809,8 +1732,8 @@ "pg_preset_loaded": "Preset \"{name}\" carregado", "pg_preset_saved": "Preset \"{name}\" guardado", "pg_sugg_loaded": "{n} valor(es) sugerido(s) carregado(s) - execute a Área de testes para comparar", - "pg_sugg_ml_loaded": "{n} valor(es) calibrado(s) por ML carregado(s) - execute a Área de testes para comparar", "phase_name_required": "O nome da fase é obrigatório", + "phase_reset": "Fase \"{name}\" reposta na versão incorporada", "phase_updated": "Fase actualizada", "phases_saved": "Fases guardadas", "preferences_saved": "Preferências guardadas", @@ -1857,15 +1780,11 @@ "store_name_required": "Introduza um nome de perfil", "store_import_failed": "Falha na importação: {error}", "store_imported": "Importado para {profile}", - "store_program_required": "Introduza um nome de programa", "store_no_appliance": "Defina primeiro a marca e o modelo do seu electrodoméstico nas Definições.", "store_share_failed": "Falha na partilha: {error}", "store_shared": "Partilhado na loja da comunidade, a aguardar revisão.", "appliance_added": "Electrodoméstico adicionado, a aguardar aprovação", "contrib_device_changed": "Adicionado à loja da comunidade, mas o dispositivo seleccionado mudou enquanto o formulário estava aberto. Seleccione-o nas Definições.", - "thanks_confirming": "Obrigado por confirmar", - "device_approved": "Aprovado pela comunidade", - "rating_saved": "Avaliação de qualidade guardada", "brand_added": "Marca adicionada, a aguardar aprovação", "profile_added": "Perfil adicionado, a aguardar aprovação", "saved_except_conflicts": "Guardado. Corrija os conflitos assinalados para guardar o restante.", @@ -1888,11 +1807,52 @@ "odometer_saved": "Total de ciclos guardado", "odometer_save_failed": "Não foi possível guardar o total: {error}", "store_pick_profile_share": "Escolha um perfil onde partilhar", - "remove_failed": "Falha ao remover: {error}" + "remove_failed": "Falha ao remover: {error}", + "analysis_complete": "Análise concluída: {count} sugestão(ões)", + "analysis_failed": "Falha na análise: {error}", + "auto_detect_failed": "Falha na deteção automática: {error}", + "auto_label_failed": "Falha na rotulagem automática: {error}", + "could_not_load_cycle": "Não foi possível carregar o ciclo: {error}", + "create_failed": "Falha ao criar: {error}", + "cycles_deleted": "{count} ciclo(s) eliminado(s)", + "debug_cleared": "{count} traços de depuração limpos", + "discard_failed": "Falha ao descartar: {error}", + "error": "Erro: {error}", + "export_failed": "Falha ao exportar: {error}", + "import_failed": "Falha ao importar: {error}", + "invalid_json": "\"{key}\" não é JSON válido; corrija-o ou limpe o campo antes de guardar.", + "label_failed": "Falha ao rotular: {error}", + "ml_training_promoted": "Treino concluído: {count} modelo(s) promovido(s)", + "pause_failed": "Falha ao pausar: {error}", + "phase_created": "Fase \"{name}\" criada", + "phase_deleted": "Fase \"{name}\" eliminada", + "profile_created": "Perfil \"{name}\" criado", + "rename_failed": "Falha ao mudar o nome: {error}", + "save_failed": "Falha ao guardar: {error}", + "split_complete": "Dividido em {count} ciclos", + "start_failed": "Falha ao iniciar: {error}", + "stop_failed": "Falha ao parar: {error}", + "update_failed": "Falha ao atualizar: {error}", + "store_download_skipped": "{n} gravação(ões) ignorada(s): demasiado curtas, com demasiadas lacunas ou já presentes no seu dispositivo.", + "store_import_low_quality": "Esta gravação é demasiado curta, tem demasiadas lacunas ou é implausível para ser importada.", + "store_import_duplicate": "Esta gravação já está no seu dispositivo.", + "device_approved": "Aprovado pela comunidade", + "thanks_confirming": "Obrigado por confirmar", + "rating_saved": "Avaliação de qualidade guardada", + "store_download_partial": "Não foi possível transferir {n} programa(s); tente novamente mais tarde.", + "import_no_restore_point": "Importado, mas não foi possível guardar um ponto de restauro, pelo que esta importação não pode ser anulada", + "import_undo_failed": "Não foi possível anular a importação: {error}", + "import_undone": "Importação anulada: {profiles} perfil(is) e {cycles} ciclo(s) restaurados", + "auto_label_cancelled": "Rotulagem automática cancelada após rotular {count} ciclos.", + "auto_label_cancelled_one": "Rotulagem automática cancelada após rotular {count} ciclo.", + "auto_label_cancelled_many": "Rotulagem automática cancelada após rotular {count} ciclos.", + "auto_label_cancelled_other": "Rotulagem automática cancelada após rotular {count} ciclos.", + "auto_label_done": "Rotulagem automática concluída: {count} ciclos rotulados, {skipped} sem rótulo.", + "auto_label_done_one": "Rotulagem automática concluída: {count} ciclo rotulado, {skipped} sem rótulo.", + "auto_label_done_many": "Rotulagem automática concluída: {count} ciclos rotulados, {skipped} sem rótulo.", + "auto_label_done_other": "Rotulagem automática concluída: {count} ciclos rotulados, {skipped} sem rótulo." }, "suggestion": { - "both_agree": "WashData recomenda", - "calibrated_label": "Calibrado", "observed_label": "Observado", "impact": { "auto_label_confidence": { @@ -1907,10 +1867,6 @@ "higher": "Fecha apenas ciclos que consumiram energia substancial", "lower": "Também fecha ciclos curtos ou de baixa energia" }, - "end_repeat_count": { - "higher": "Exige que o sinal de fim se repita mais vezes – mais robusto, ligeiramente mais lento", - "lower": "Termina após menos repetições – deteção mais rápida, maior risco de fim falso" - }, "learning_confidence": { "higher": "Aprende apenas com correspondências muito confiantes – mais lento mas mais fiável", "lower": "Aprende com mais ciclos; o modelo pode ser menos preciso" @@ -1949,40 +1905,25 @@ } }, "reason": { - "auto_label_confidence": "Confiança no percentil 15 de {cycles} rótulos automáticos que o utilizador nunca corrigiu ({p15}).", "completion_min_seconds": "Metade da duração p05 dos ciclos limpos ({minutes} min) ao longo de {cycles} ciclos; filtra os ciclos fantasma.{excl}", - "dead_zone_batch": "Mantido curto (janela mediana de instabilidade no arranque ao longo de {cycles} ciclos = {s}s) para que um fim real seja detetado rapidamente.", - "dead_zone_from_dip": "Com base numa queda de potência precoce detetada aos {s}s.", - "end_energy_default": "Valor de referência recomendado por predefinição para o limiar de ruído no fim do ciclo.", - "end_energy_false": "Energia p95 dos fins falsos ({p95}Wh) ao longo de {cycles} ciclos, com um mínimo de 0.2% da energia mediana do ciclo ({floor}Wh).", - "end_energy_no_false": "Sem fins falsos ao longo de {cycles} ciclos; com um mínimo de 0.2% da energia mediana do ciclo ({floor}Wh).", - "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.", + "match_interval_floored": "O programa mais curto ({shortest}s) limitaria isto a {cap}s, abaixo do mínimo de {minimum}s, por isso usa-se o mínimo: {persistence} correspondências seguidas demoram {budget}s, mais de {pct}% desse programa. A correspondência só corre quando chega uma leitura (mediana={median}s), por isso não custa nada.", "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.", "min_off_gap": "Com base em {gaps} intervalos entre ciclos observados (p05={p05}s). Mínimo do dispositivo: {floor}s.", "min_off_gap_bridge": "Dimensionado para cobrir o período de silêncio mais longo dentro de um ciclo: {span}s + 60s de margem, a partir de {spans} intervalos cobertos ao longo de {cycles} ciclos limpos (mantém-se abaixo do seu intervalo mínimo de {ceiling}s entre cargas consecutivas).", "min_power": "40% da potência ativa mais baixa no p05 ({p05}W) ao longo de {cycles} ciclos limpos, mantendo o limiar de desligamento abaixo do consumo real.{excl}", - "ml_auto_label": "Confiança mais baixa que o modelo de qualidade ainda classificou como limpa (p10 de {cycles} ciclos limpos = {p10}).", - "ml_end_repeat": "{false}/{total} ciclos ({pct}%) tiveram uma pausa que o detetor de fim pontuou >50%; exigir {val} confirmação(ões) de fim.", - "ml_off_delay": "Pausas confirmadas pelo detetor de fim: p95 {p95}s + 60s de margem, a partir de {pauses} pausas verificadas pelo modelo ao longo de {cycles} ciclos (mínimo {floor}s).", "no_update_timeout": "Com base na cadência de atualização observada (p95={p95}s) * 20 (mín. 60s).", "off_delay_cadence": "Com base na cadência de atualização observada (p95={p95}s) * 5", "off_delay_device_floor": "Utilizado o mínimo seguro específico do dispositivo para {device} ({floor}s).", "off_delay_generic_floor": "Utilizado o mínimo seguro genérico ({floor}s).", "off_delay_pauses": "Dimensionado para durar mais do que as pausas reais: pausa intraciclo p95 {p95}s + 60s de margem, a partir de {pauses} pausas ao longo de {cycles} ciclos limpos (mínimo {floor}s).", - "profile_match_threshold": "Confiança p10 de {cycles} rótulos automáticos corretos ({p10}); mínimo seguro para confirmação em direto.", - "sampling_interval": "Intervalo de atualização mediano observado ao longo de {cycles} ciclos limpos ({si}s).{excl}", - "smoothing_window": "Dimensionado para suavizar ~30s de leituras com uma amostragem de {si}s ({samples} amostras).", - "start_duration": "Mantido curto (~um intervalo de amostragem de {si}s) para que a deteção comece o mais cedo possível, ignorando ainda assim os picos de uma única amostra.", - "thr_batch": "Mantido logo acima da potência ativa mais baixa no p05 ao longo de {cycles} ciclos ({p05}W) para que um arranque seja captado o mais cedo possível e o limiar de paragem se mantenha abaixo da potência de funcionamento mais baixa da máquina.", - "tol_per_profile": "p75 da variância de duração por perfil em {profiles} perfis ({cycles} ciclos); os perfis restritos não são penalizados.", - "tol_pooled": "Com base na variância de duração agrupada de {cycles} ciclos rotulados recentes (desvio p95={dev}).", - "watchdog": "Mantido o mais baixo que é seguro (ligeiramente acima do intervalo de atualização p95 de {p95}s e pelo menos 2x o intervalo de amostragem de {median}s, mín. 30s) para detetar rapidamente os bloqueios sem paragens falsas." + "standby_floor": "Cada ciclo recente ainda consumia {idle}W quando terminou, pelo que um limiar abaixo disso nunca poderá detetar o desligamento do eletrodoméstico. Mantido acima; os {cycles} ciclos limpos registados nunca ficaram abaixo desse valor por mais de {pause}s.", + "thr_batch": "Logo acima da potência ativa mais baixa (p05) em {cycles} ciclos ({p05}W), para apanhar os arranques cedo e manter o limiar de paragem abaixo da potência de funcionamento mais baixa.", + "watchdog": "Mantido o mais baixo que é seguro (ligeiramente acima do intervalo de atualização p95 de {p95}s e pelo menos 2x o intervalo de amostragem de {median}s, mín. 30s) para detetar rapidamente os bloqueios sem paragens falsas.", + "end_energy_floor": "Elevado ao mínimo implicado pelo limiar de paragem ({stop} W) durante o atraso de desligamento ({off_delay} s); abaixo dele, o ciclo só pode terminar por um mecanismo de recurso.", + "max_duration_ratio_default": "Reposto o valor predefinido: um limite superior mais baixo rejeita o programa certo sempre que um ciclo dura mais do que o habitual.", + "min_duration_ratio_default": "Reposto o valor predefinido: um limite inferior mais alto impede que um ciclo em curso seja reconhecido até estar bem adiantado." }, "exclusions": { "summary": "Excluído(s) {total} ciclo(s) mal detetado(s): {parts}.", @@ -2000,13 +1941,18 @@ "no_power": "sem energia", "no_active_power": "sem energia ativa" } - } + }, + "now_value": "(agora {v})" }, "trend": { "down": "Tendência descendente", "up": "Tendência ascendente" }, "maint": { + "salt": "Repor o sal", + "rinse_aid": "Repor o abrilhantador", + "lint_filter": "Limpar o filtro de cotão", + "condenser_clean": "Limpar o condensador", "bearing_service": "Manutenção de rolamentos", "descale": "Descalcificar", "drum_clean": "Limpar o tambor", @@ -2018,12 +1964,14 @@ "status_tip": "Resultado do ciclo: Concluído (fim natural), Interrompido (queda abrupta de potência), Parado forçosamente (manual) ou Precisa de revisão (feedback pendente).", "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. 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.", + "energy_tip": "Energia total consumida (kWh), calculada a partir da potência ao longo do tempo.", + "cost_tip": "Custo de energia deste ciclo, fixado no fim. Com Custo Ponderado no Tempo ligado, cada momento é cobrado ao seu próprio preço; caso contrário, ao preço no fim. Defina um preço em Definições para preencher isto.", "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.", - "cost_dynamic_tip": "Ponderado no tempo: cobrado ao preço em vigor durante o ciclo (preço efetivo {price} {cur}/kWh)." + "cost_dynamic_tip": "Ponderado no tempo: cobrado ao preço em vigor durante o ciclo (preço efetivo {price} {cur}/kWh).", + "dbg_conf": "Conf.", + "dbg_mae": "MAE", + "dbg_corr": "Corr." }, "stat": { "avg": "méd.", @@ -2047,7 +1995,10 @@ "notify_finish": "Seria enviada uma notificação de conclusão.", "notify_milestone": "Seria enviada uma notificação de etapa.", "notify_held": "Uma notificação foi retida durante as horas de silêncio.", - "finished": "O ciclo atingiu um estado final e terminou." + "finished": "O ciclo atingiu um estado final e terminou.", + "match_reverted": "O programa voltou à deteção: a sua pontuação caiu muito abaixo do seu próprio pico.", + "verified_pause": "O alinhamento em tempo real confirmou (ou libertou) uma pausa dentro do programa. Enquanto se mantiver, o ciclo não é terminado.", + "stalled": "O ciclo ficou em consumo de espera mais tempo do que este programa alguma vez pausa nesse ponto, com trabalho por fazer. Mostrado como Em pausa (Parado); o ciclo continua aberto." }, "store": { "search_ph2": "Pesquisar por marca ou modelo…", @@ -2056,11 +2007,7 @@ "programs_count": "Programas: {n}", "browse": "Explorar", "device": "Dispositivo", - "favorites": "Favoritos", - "search_ph": "Pesquisar por marca ou modelo…", "no_results": "Não foram encontrados electrodomésticos correspondentes. Tente outra pesquisa.", - "n_programs": "{n} programas", - "n_cycles": "{n} ciclos", "no_programs": "Ainda não há programas partilhados para este electrodoméstico.", "no_cycles": "Ainda não foram partilhados ciclos de referência para este programa.", "anon": "anónimo", @@ -2072,19 +2019,21 @@ "merge_target": "Combinar com o perfil", "description": "Descrição (opcional)", "contributed_by": "por {name}", + "website": "Site da loja ↗", + "website_tip": "Abrir o site da loja da comunidade", + "favorites": "Favoritos", "rating_summary": "★ {avg} ({n})", "no_ratings": "Ainda sem avaliações", - "website": "Site da loja ↗", - "website_tip": "Abrir o site da loja da comunidade" + "unreachable": "Não foi possível contactar a loja da comunidade. Verifique a sua ligação e tente novamente." }, "settings": { "gear": { - "title": "Definições" + "title": "Preferências" } }, "link": { - "manual": "Manual ↗", - "add_appliance": "Adicione o seu electrodoméstico" + "maintenance_supporter": "Maintenance Supporter no GitHub ↗", + "manual": "Manual ↗" }, "tip": { "add_brand": "Marca não listada? Adicione-a ao catálogo da comunidade", @@ -2118,7 +2067,13 @@ "rebuild": { "envelopes": "A reconstruir envelopes" }, - "cancelling": "A cancelar..." + "cancelling": "A cancelar...", + "auto_label": { + "running": "A rotular ciclos automaticamente" + }, + "store_download": { + "running": "A transferir da loja" + } }, "setup": { "hdr": { @@ -2127,26 +2082,24 @@ }, "phase0": { "washer": "O WashData já está a detectar os seus ciclos. Grave o primeiro ciclo para activar os nomes dos programas e as estimativas de tempo.", - "dishwasher": "O WashData está a monitorizar. As máquinas de lavar louça têm ciclos complexos – recomenda-se vivamente gravar o primeiro ciclo. Se um ciclo detectado demorar demasiado, utilize o editor de ciclos para o cortar antes de o guardar como perfil.", + "dishwasher": "O WashData está a observar. Os ciclos de máquinas de lavar loiça são complexos, por isso grave o seu primeiro ciclo. Se um ciclo detetado durar demasiado, corte-o no editor de ciclos antes de o guardar como perfil.", "generic": "O WashData está a monitorizar. Grave ou rotule um ciclo detectado para começar a criar perfis." }, "phase1a": { - "labelled": "Bom começo – o seu primeiro programa está guardado. Para dados mais precisos, considere gravar o próximo ciclo com o widget de gravação." + "labelled": "Bom começo: o seu primeiro programa está guardado. Para dados mais limpos, grave o próximo ciclo com o gravador." }, "phase1b": { - "recorded": "A sua gravação foi guardada como {profile_name}. Agora grave ou rotule os outros programas que utiliza habitualmente para aumentar a cobertura." + "recorded": "A sua gravação foi guardada como {profile_name}. Agora grave ou rotule os seus outros programas habituais." }, "phase1c": { - "verify": "Tem {count} programas da comunidade. Execute um ciclo para verificar se o WashData os reconhece correctamente – a correspondência melhorará à medida que o dispositivo acumula o seu próprio histórico." + "verify": "Tem {count} programas da comunidade. Execute um ciclo para verificar se o WashData o reconhece; a correspondência melhora à medida que o seu dispositivo cria o seu próprio histórico." }, "phase2": { - "cluster": "O WashData detectou {count} ciclos que não correspondem a nenhum programa guardado – parecem-se entre si. Quer criar um novo perfil para eles?", + "cluster": "O WashData viu {count} ciclos semelhantes que não correspondem a nenhum programa guardado. Criar um novo perfil para eles?", "unmatched": "O seu último ciclo não correspondeu a nenhum programa guardado. É um novo programa?" }, "phase3": { - "suggestions": "O WashData tem recomendações de definições baseadas no histórico de ciclos – reveja-as para melhorar a precisão de detecção.", - "groups": "Alguns dos seus perfis parecem o mesmo programa a temperaturas diferentes. Organize-os num grupo para uma melhor correspondência.", - "phases": "Adicione fases de programa a {profile_name} para estimativas mais precisas do tempo restante." + "suggestions": "O WashData tem sugestões de definições com base no seu histórico de ciclos. Reveja-as para melhorar a deteção." }, "phase4": { "healthy": "Este dispositivo está totalmente configurado ({profile_count} perfis)." @@ -2159,8 +2112,6 @@ "create_from_cluster": "Criar perfil a partir destes ciclos", "create_profile": "Criar um perfil para este programa", "review_suggestions": "Rever sugestões", - "organise_profiles": "Organizar perfis", - "configure_phases": "Configurar fases", "skip_step": "Ignorar este passo", "skip_forever": "Não voltar a mostrar", "hide_guidance": "Ocultar guia", @@ -2171,6 +2122,7 @@ "min": "min", "entity_not_found": "Entidade não encontrada", "running_long": "Em atraso", + "stalled": "Parado", "remaining": "restante", "progress": "progresso", "show_time_remaining": "Mostrar tempo restante", @@ -2219,5 +2171,53 @@ "off_delay": { "effective": "Um ciclo terminado espera na realidade {eff} s, e não {delay} s: o Intervalo Mínimo de Desligamento ({gap} s) é mais longo e tem prioridade." } + }, + "access": { + "edit": "Edição", + "full": "Total", + "inherit": "Herdar", + "none": "Nenhum (oculto)", + "read": "Leitura" + }, + "pg_label_reason": { + "no_winner": "Nenhum programa corresponde", + "below_floor": "Abaixo da Confiança de Aprendizagem", + "ambiguous": "Correspondência marcada como ambígua", + "margin": "Demasiado próximo do segundo candidato", + "unknown_profile": "O programa já não existe", + "not_evaluated": "Não avaliado" + }, + "diagram": { + "accum": "acum.", + "auto": "auto", + "below_off": "abaixo = desligado", + "confirmed": "confirmado", + "energy_counts": "energia contada", + "feedback": "revisão", + "gap_below_min": "intervalo abaixo do mín. = um ciclo", + "heat_phase": "fase de aquecimento", + "held_at_100": "mantido em 100%", + "ignored": "ignorado", + "kept": "mantido", + "max": "máx.", + "min": "mín.", + "minus_tol": "-tol", + "no_match": "sem corresp.", + "no_updates": "sem atualizações", + "off_delay_wait": "espera até desligar", + "off_gap": "intervalo", + "plus_tol": "+tol", + "profile": "perfil", + "reading_interval": "intervalo típico de leitura", + "sensor_offline": "sensor offline: paragem forçada", + "si": "IA", + "spike_ignored": "pico ignorado", + "spike_ignored_short": "pico: ignorado", + "start": "início", + "stop": "paragem", + "tail_energy_resets": "cauda > limiar: temporizador reinicia", + "thr": "limiar", + "too_short": "demasiado curto", + "tumble_kept": "rotações mantidas" } } diff --git a/custom_components/ha_washdata/translations/panel/ro.json b/custom_components/ha_washdata/translations/panel/ro.json index 01604983..0cb68e34 100644 --- a/custom_components/ha_washdata/translations/panel/ro.json +++ b/custom_components/ha_washdata/translations/panel/ro.json @@ -1,15 +1,16 @@ { "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_tip": "Spre final, acest program rămâne liniștit aproximativ {mins} min, apoi consumă aproximativ {watts} W timp de {secs} s. Observat în {seen} din {measured} cicluri măsurate.", "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", + "artifact_tip": "{n} de anomalii detectate (de exemplu, ușa deschisă la jumătatea ciclului) - deschideți pentru a le vedea pe grafic", + "artifact_tip_one": "{n} anomalie detectată (de exemplu, ușa deschisă la jumătatea ciclului) - deschideți pentru a o vedea pe grafic", + "artifact_tip_few": "{n} anomalii detectate (de exemplu, ușa deschisă la jumătatea ciclului) - deschideți pentru a le vedea pe grafic", + "artifact_tip_other": "{n} de 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", "declining": "↘ în scădere", "energy_low": "Energie mai mică decât de obicei", "energy_spike": "Energie mai mare decât de obicei", - "fair_fit": "calitate acceptabilă", - "fair_fit_tip": "Consistență moderată a potrivirii – unele cicluri atribuite acestui profil au scoruri de încredere mai mici. Etichetați mai multe cicluri sau reînregistrați profilul pentru a îmbunătăți acuratețea.", "feedback_requested": "S-a solicitat feedback", "golden_cycle": "Ciclu de referință înregistrat", "improving": "↗ în îmbunătățire", @@ -17,28 +18,41 @@ "needs_review": "Necesită revizuire", "overrun": "A durat mai mult decât de obicei", "poor_fit": "calitate slabă", - "poor_fit_tip": "Istoric de potrivire inconsecvent – luați în considerare reconstruirea acestui profil", - "restart_gap_tip": "{n} gol la repornirea HA: traseul de putere are o lipsă", + "restart_gap_tip": "{n} de goluri la repornirea HA: traseul de putere are lipsuri", + "restart_gap_tip_one": "{n} gol la repornirea HA: traseul de putere are o lipsă", + "restart_gap_tip_few": "{n} goluri la repornirea HA: traseul de putere are lipsuri", + "restart_gap_tip_other": "{n} de goluri la repornirea HA: traseul de putere are lipsuri", "reviewed": "Revizuit", "steady": "→ stabil", "underrun": "S-a terminat mai repede decât de obicei", - "using_defaults": "Folosind setările implicite", - "using_tuned": "Folosind ponderi ajustate", "overrun_ratio": "({x}x față de estimat)", "underrun_ratio": "({pct}% din estimat)", - "awaiting_tip": "În așteptarea aprobării comunității", - "awaiting_n": "În așteptarea aprobării · {n} confirmate", - "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.", "armed": "(se aplică ciclului următor)", - "duration_outlier": "{n} ciclu(ri) cu durată neobișnuită" + "duration_outlier": "{n} ciclu(ri) cu durată neobișnuită", + "awaiting_tip": "În așteptarea aprobării comunității", + "awaiting_n": "În așteptarea aprobării · {n} confirmate", + "awaiting": "În așteptarea aprobării", + "approved": "Aprobat", + "matcher_unused": "Încă nu se potrivește cu niciunul dintre ciclurile dvs.", + "matcher_used": "Potrivit cu {n} dintre ciclurile dvs.", + "matcher_used_one": "Potrivit cu {n} dintre ciclurile dvs.", + "matcher_used_few": "Potrivit cu {n} dintre ciclurile dvs.", + "matcher_used_other": "Potrivit cu {n} dintre ciclurile dvs.", + "matcher_used_tip": "Cicluri ale acestui aparat pe care WashData le-a asociat singur cu acest program importat. Un import care nu se potrivește niciodată s-ar putea să nu fie potrivit pentru aparatul dvs.; îl puteți elimina. Numărat local; nimic nu este trimis către magazinul comunității." }, "btn": { + "log_done_task": "Marcați ca efectuat: {task}", + "add_task": "Adăugați sarcină", + "pg_preset_delete": "Șterge presetarea", + "pg_preset_load": "Încarcă presetarea", + "pg_preset_load_tip": "Înlocuiește valorile din Zona de test cu această presetare (setările active rămân neatinse)", + "pg_preset_save": "Salvează ca presetare", + "pg_preset_save_tip": "Salvează toate valorile de mai jos ca presetare denumită pentru acest dispozitiv", "reset_zoom": "Resetați zoomul", "set_brand_model": "Stabiliți marca și modelul", "refresh_catalog": "Actualizați catalogul", @@ -49,12 +63,11 @@ "add_timer": "+ Adăugați cronometru", "apply_all": "Aplicați toate", "apply_label": "Aplicați eticheta", - "apply_set_b": "Aplicați setul B", "apply_split": "Aplicați divizarea", "apply_trim": "Aplicați tăierea", "auto_detect_split": "Detectare automată", "auto_label_cycles": "Etichetați ciclurile automat", - "auto_label_cycles_tip": "Atribuiți automat nume de profil ciclurilor neetichetate a căror încredere de potrivire depășește pragul", + "auto_label_cycles_tip": "Denumiți ciclurile neetichetate a căror încredere de potrivire depășește pragul.", "cancel": "Anulați", "clear_debug": "Curățați datele de depanare", "clear_splits": "Ștergeți", @@ -64,8 +77,6 @@ "convert_to_automation": "Convertiți în automatizare", "correct": "Corectați…", "create": "Creați", - "create_from_cluster": "Creați profil din {count} cicluri", - "create_profile": "+ Creați profil", "delete": "Ștergeți", "delete_group": "Ștergeți grupul", "delete_group_tip": "Ștergeți numai acest grup – profilurile membrilor sunt păstrate", @@ -76,22 +87,19 @@ "done": "Terminat", "edit": "Editați", "export": "Exportați", - "export_json": "Exportați în JSON", "force_stop": "Oprire forțată", "force_stop_tip": "Termina imediat ciclul curent și marchează-l ca oprit forțat", "from_template": "Din șablon ▾", "ignore": "Ignorați", "import_json": "Importați din JSON", - "import_overwrite": "Importați (suprascrie datele)", "inspect": "Inspectați", "label": "Etichetează", - "load": "Încărcați", "load_more": "Încărcați mai multe", "manage": "Gestionați", "merge": "Îmbinați", "new_automation": "+ Automatizare nouă", "new_group": "+ Grup nou", - "new_group_tip": "Grupați profilurile aproape identice (aceeași formă/durată, temperatură sau turație diferite) pentru ca motorul de potrivire să le distingă fiabil", + "new_group_tip": "Grupați profiluri aproape identice (aceeași formă, altă temperatură sau centrifugare) ca potrivirea să le poată deosebi.", "new_phase": "+ Fază nouă", "new_profile": "+ Profil nou", "new_profile_tip": "Creați un nou profil de program dintr-un ciclu etichetat existent sau o înregistrare", @@ -99,36 +107,25 @@ "on_cycle_started": "La pornirea ciclului", "pause_cycle": "Pauză", "pause_cycle_tip": "Puneți în pauză ciclul în curs – aparatul va relua de unde a rămas", - "pg_apply_device": "Aplicați pe dispozitiv", - "pg_autofill": "Completare automată", - "pg_load_calibrated": "Încarcă Calibrat (ML) ({n})", - "pg_load_calibrated_tip": "Încarcă setările calibrate de ML ca substituiri în Zona de test", "pg_load_live": "Încarcă setările active", "pg_load_live_tip": "Recitește setările actuale ale integrării și renunță la toate modificările făcute în Zona de test", "pg_load_suggested": "Încarcă sugestii ({n})", "pg_load_suggested_tip": "Încarcă sugestiile curente ale optimizatorului automat ca substituiri în Zona de test", - "pg_preset_delete": "Șterge presetarea", - "pg_preset_load": "Încarcă presetarea", - "pg_preset_load_tip": "Înlocuiește valorile din Zona de test cu această presetare (setările active rămân neatinse)", - "pg_preset_save": "Salvează ca presetare", - "pg_preset_save_tip": "Salvează toate valorile de mai jos ca presetare denumită pentru acest dispozitiv", "pg_publish_all": "Salvează {n} setări în integrare", "pg_publish_one": "Salvează {label} în integrare", - "play": "Redați", "process": "Procesați", "process_history": "Procesați acum", "process_recording": "Salvați înregistrarea", "process_tip": "Salvați urma înregistrată ca profil nou sau existent", "rebuild": "Reconstruiți plicurile", "rebuild_envelope": "Reconstruiți plicul", - "rebuild_tip": "Recalculați plicul de putere așteptat (banda min/max) pentru toate profilurile din ciclurile lor etichetate – rulați după etichetarea de noi cicluri sau corectarea celor vechi", + "rebuild_tip": "Reconstruiți banda de putere așteptată a fiecărui profil din ciclurile sale etichetate. Rulați după etichetarea sau corectarea ciclurilor.", "rec_start_tip": "Porniți înregistrarea urmei de putere a aparatului – porniți imediat înainte de a rula un ciclu", "rec_stop": "Opriți", "rec_stop_tip": "Opriți înregistrarea și păstrați urma capturată pentru revizuire", "record": "Porniți înregistrarea", "refresh": "Actualizați", "refresh_diag_tip": "Reîncărcați statisticile de stocare", - "refresh_logs_tip": "Reîncărcați cele mai recente înregistrări din jurnal", "refresh_settings_tip": "Reîncărcați setările de pe server", "relabel": "Reetichetați ({count})", "remove": "Eliminați", @@ -136,7 +133,6 @@ "reset": "Resetați", "reset_ml_models_tip": "Renunțați la modelele ajustate și reveniți la cele integrate. WashData le poate reînvăța ulterior.", "reset_to_builtin": "Resetați la modelele integrate", - "reset_to_defaults": "Resetați la valorile implicite", "resume_cycle": "Reluare ciclu", "resume_cycle_tip": "Reluați ciclul întrerupt", "revert_settings": "Renunță la modificări", @@ -149,8 +145,6 @@ "run_analysis": "🔍 Rulați analiza sugestiilor", "run_analysis_tip": "Analizați ciclurile înregistrate acum și actualizați sugestiile de ajustare", "run_auto_label": "Rulați etichetarea automată", - "run_both": "Rulați A vs B", - "run_simulation": "Rulați simularea", "save": "Salvați", "save_access_control": "Salvați controlul accesului", "save_group": "Salvați grupul", @@ -162,10 +156,8 @@ "save_settings": "Salvați setările", "select": "Selectați", "select_all": "Selectați tot", - "select_last20": "Ultimele 20", "select_none": "Deselectați", "show_only": "Afișați doar", - "skip_setup": "Omiteți configurarea", "split": "Divizați", "stop": "Opriți", "submit_correction": "Trimiteți corecția", @@ -174,36 +166,31 @@ "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.", + "unlabel_selected_tip": "Eliminați eticheta de pe ciclurile selectate. Rămân în istoricul dvs., dar nu mai modelează acest profil.", "use": "Utilizați", "wipe_all": "Ștergeți toate datele", "delete_selected": "Ștergeți selecția ({n})", - "group_suggest": "Grup {n}: {members}", "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", "import": "Importați", "disconnect": "Deconectați", "connect_github": "Conectați GitHub", - "save_appliance": "Salvați aparatul", "share_to_store": "Partajați în magazin", "share": "Partajați", - "confirm_appliance": "Confirmați acest aparat", - "confirmed": "Confirmat", "browse_community_setups": "Răsfoiți configurațiile comunității", "download_device": "Descărcați această configurație", "share_device": "⬆ Partajați acest dispozitiv", - "share_device_tip": "Partajați acest aparat și ciclurile sale de referință înregistrate în magazinul comunității, astfel încât alții cu același aparat să le poată adopta", + "share_device_tip": "Partajați acest aparat și ciclurile sale de referință în magazinul comunității, ca proprietarii aceluiași aparat să le poată adopta.", "share_n": "Partajați {n} ciclu(ri)", "export_selected": "Exportă (alege datele)", "export_all": "Exportă rapid tot", - "import_raw": "Avansat: înlocuiește tot din JSON", "download_export": "Descarcă exportul", "analyze_import": "Analizează fișierul", "import_selected": "Importă selecția", "back": "Înapoi", "mute_suggestion": "Nu mai sugerați această setare", "reset_muted": "Resetați cele ignorate", + "reset_phase_tip": "Anulați modificarea și restabiliți faza integrată", "import_power_history": "Importați istoricul de putere", "hist_read_recorder": "Citiți din Home Assistant", "hist_scan": "Căutați cicluri", @@ -212,13 +199,15 @@ "save_total_cycles": "Salvați totalul", "clear": "Ștergeți", "add_brand": "Adăugați marcă", - "add_appliance": "Adăugați aparat" + "add_appliance": "Adăugați aparat", + "retry": "Reîncercați", + "confirm_appliance": "Confirmați acest aparat", + "confirmed": "Confirmat", + "create_profile_from_gap": "Creați profil", + "undo_import": "Anulați ultimul import", + "pg_rerun": "Actualizează simularea" }, "conflict": { - "anti_wrinkle_exit": { - "exit": "Trebuie să fie sub Pragul de oprire ({stop} W); altfel puterea de ieșire anti-șifonare este ignorată", - "stop": "Trebuie să fie peste Puterea de ieșire anti-șifonare ({exit} W)" - }, "anti_wrinkle_max": { "max": "Trebuie să fie peste Pragul de pornire ({start} W); altfel limita de durată anti-șifonare este ignorată", "start": "Trebuie să fie sub Puterea maximă anti-șifonare ({max} W)" @@ -226,10 +215,10 @@ "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." + "power_sensor": "Acesta este Senzorul de Putere al dispozitivului, nu un preț. Ciclurile ar fi taxate în wați pe kWh; goliți acest câmp.", + "energy_sensor": "Aceasta este Entitatea contor de energie a dispozitivului, nu un preț. Ciclurile ar fi taxate la citirea contorului pe kWh; goliți acest câmp.", + "device_class": "Acest senzor măsoară {dc}, nu un preț pe kWh. Goliți acest câmp sau alegeți un senzor de tarif.", + "unit": "Acest senzor măsoară în {unit}, nu în preț pe kWh. Goliți acest câmp sau alegeț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", @@ -264,8 +253,8 @@ "threshold": "Trebuie să fie sub Pragul de oprire ({stop} W) pentru a avea efect" }, "pump_stuck": { - "duration": "Trebuie să fie mai mic decât Limita de timp fără actualizări ({to} s) astfel încât alarma pompei blocate să se declanșeze înainte ca watchdog-ul să oprească ciclul", - "timeout": "Trebuie să depășească Durata pompei blocate ({ps} s) astfel încât alarma să se declanșeze înainte ca ciclul să fie oprit forțat" + "duration": "Trebuie să fie sub Timeout Fără Actualizare ({to} s) ca alarma pompei blocate să se poată declanșa prima", + "timeout": "Trebuie să depășească Durata Pompei Blocate ({ps} s) ca alarma pompei blocate să se poată declanșa prima" }, "start_dur": { "sampling": "Intervalul de eșantionare depășește Durata de pornire ({sdt} s); vârfurile pe un singur eșantion pot deschide un ciclu", @@ -281,10 +270,15 @@ "watchdog": { "interval": "Ar trebui să fie de cel puțin 2x Intervalul de eșantionare ({si} s)", "sampling": "Intervalul de eșantionare ar trebui să fie cel mult jumătate din Intervalul watchdog ({wi} s)" + }, + "end_energy": { + "energy": "Prea strict pentru Pragul de Oprire ({w} W) pe durata Întârzierii la Oprire ({d} s); ciclul se poate încheia doar printr-o cale de rezervă", + "stop": "Energia de Sfârșit ({e} Wh în {d} s) permite doar {p} W" } }, "hdr": { - "ab_comparison": "Comparație setări A/B", + "custom_tasks": "Sarcinile dumneavoastră", + "maintenance_supporter": "Aveți nevoie de mai mult?", "access_control": "Control acces", "add_maintenance": "Adăugați eveniment de întreținere", "automation_delete": "Ștergeți această automatizare", @@ -293,28 +287,22 @@ "automation_review_this_cycle": "Revizuiți acest ciclu", "automations": "Automatizări", "clear_debug": "Curățați urmele de depanare", - "cycle_simulator": "Simulator de cicluri", "diagnostics_pref": "Diagnostice", "display": "Afișaj", - "dtw_inspector": "Inspector DTW", "export_import": "Export / Import", - "getting_started": "Primii pași", "live_power": "Putere în timp real", "logs": "Jurnale", - "logs_diagnostics": "Diagnostice", "maintenance": "Acțiuni de întreținere", "maintenance_log": "Jurnal de întreținere", "maintenance_reminders": "Mementouri de service", "manual_recording": "Înregistrare manuală", - "manual_recording_tip": "Rulați un ciclu intenționat în timp ce WashData înregistrează urma de putere. Porniți imediat înainte ca aparatul să pornească, Opriți când termină, apoi Procesați pentru a-l salva ca profil cu nume.", + "manual_recording_tip": "Înregistrați un ciclu intenționat: apăsați Început chiar înainte ca aparatul să pornească și Opriți când termină, apoi Procesați pentru a-l salva ca profil.", "ml_learned": "Ce a învățat WashData", - "ml_matching_tuning": "Ajustare fină a potrivirii programelor", "ml_settings_card": "Setări", "ml_smart_learning": "Învățare inteligentă", "my_preferences": "Preferințele mele", "no_automations": "Nicio automatizare nu face referire încă la acest dispozitiv.", "panel_settings": "Setări panou", - "param_sweep": "Scanare parametri", "pg_settings_source": "Sursa setărilor", "phase_catalog": "Catalog de faze", "playground": "Zonă de test", @@ -323,11 +311,8 @@ "status": "Stare", "status_graph": "Grafic de stare", "storage_stats": "Statistici stocare", - "test_history": "Testare pe istoric", "toggle_sidebar": "Comutați bara laterală Home Assistant", - "tools_and_data": "Instrumente și date", "wipe_history": "Ștergeți istoricul", - "recommendations": "Recomandări ({n})", "pg_detection_params": "Setări de detectare", "pg_outcome": "Rezultatul simulării", "pg_across_cycles": "În toate ciclurile tale", @@ -335,7 +320,8 @@ "online_account": "Magazinul comunității și funcții online", "import_power_history": "Importați istoricul de putere", "service_status": "Starea întreținerii", - "online_account_tab": "Online și comunitate" + "online_account_tab": "Online și comunitate", + "live_match_debug": "Depanare potrivire în timp real" }, "health": { "fair": "Calitate acceptabilă a profilului", @@ -343,7 +329,12 @@ "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.", + "task_name": "Sarcină", + "every_cycles": "La fiecare (cicluri)", + "every_days": "La fiecare (zile)", + "new_task_name": "Sarcină nouă", + "days_since_service": "{since} / {total} zile", + "envelope_position_tip": "Poziția pe curba înregistrată a programului potrivit: {pct}%. Obținută prin alinierea ciclului la profil, deci rămâne corectă și când un ciclu durează mai mult sau mai puțin. Se actualizează cât aparatul este liniștit, așa că poate rămâne în urmă.", "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})", @@ -355,10 +346,8 @@ "aria_cycle_chart": "Curba de putere a ciclului", "aria_envelope_chart": "Grafic al plicului de putere al profilului", "aria_phase_chart": "Grafic al editorului de faze", - "aria_playground_chart": "Curba de putere a ciclului cu praguri de detectare glisabile", "aria_power_chart": "Grafic al consumului de putere", "aria_spaghetti_chart": "Curbe de putere ale ciclurilor suprapuse", - "aria_sweep_chart": "Grafic cu rezultatele scanării parametrilor", "artifact_anomaly": "Anomalie", "artifact_high_power": "Putere ridicată", "artifact_interruption": "Întrerupere", @@ -372,150 +361,107 @@ "cohesion_good": "coeziune {pct}%", "cohesion_low": "⚠ coeziune scăzută {pct}%", "compare_profiles": "Comparați cu profiluri", - "concurrency": "Concurență", "confidence": "Încredere", "confidence_threshold": "Pragul de încredere", "correct_duration": "Durată corectată (min, opțional)", "correct_profile": "Profil corectat", - "correlation": "Corelație", "cost": "Cost", "create_new_profile": "+ Creați un profil nou…", "current_phase": "Faza curentă", - "curves": "Curbe", "cycle": "Ciclu", "cycle_date_display": "Afișarea datei ciclului", - "cycle_health": "Sănătatea ciclului", - "cycle_index": "index ciclu →", - "cycles": "Cicluri", + "cycle_context": "Istoricul recorder-ului în jurul ciclului", + "cycle_context_off": "Oprit", + "cycle_context_minutes": "{min} min", + "cycle_context_series": "Istoricul recorder-ului", + "before_start": "Înainte de pornire", + "after_end": "După sfârșit", "cycles_count": "Cicluri", "cycles_lc": "cicluri", - "dashed": "întrerupt", "date": "Dată", "debug_traces": "Urme de depanare", - "default": "Implicit", "default_access_level": "Nivelul implicit pentru utilizatorii care nu sunt listați mai jos", "default_other": "Implicit (alte dispozitive)", "default_tab": "Fila implicită la deschiderea panoului", - "delta": "Δ", "description": "Descriere", - "dtw_blend_w": "Pondere de amestecare", - "dtw_blended": "Scor amestecat", - "dtw_ddtw": "Scor DDTW", - "dtw_l1": "Scor L1", - "dtw_matched_profile": "(profilul potrivit al ciclului)", "duration": "Durată", - "duration_agreement": "Concordanță durată", - "emphasis": "Accent", "enable_access_control": "Activați controlul accesului per utilizator", "energy": "Energie", - "energy_agreement": "Concordanță energie", - "event_timeline": "Cronologie evenimente", "event_type": "Tip eveniment", "expected": "Așteptat", + "suspected_profile": "{name} (probabil)", + "runner_up_profile": "{name} (al doilea candidat)", "expected_duration": "Durată așteptată (min)", "file_kb": "Fișier (kB)", - "final_score": "Scor final", "from": "De la", "gap_s": "Pauză (s)", "group_name": "Numele grupului", "head_trim": "Tăiere inițială (s)", - "health": "Sănătate", "hide_tabs": "Ascundeți filele pentru non-administratori", - "in_use": "În uz", "json_data": "Date JSON", "label": "Program potrivit", "last": "Ultimele", "last_checked": "Ultima verificare:", "load_from_file": "Încărcați din fișier", - "mae_score": "Scor MAE", "manual_duration": "Durată manuală (min, opțional)", "match": "Potrivire", "members": "Membri", - "metric": "Metrică", "mode_existing_profile": "Adăugați la profilul existent", "mode_new_profile": "Creați un profil nou", "models_fine_tuned": "(modele ajustate: {count})", - "n_classic_suggestions": "{n} clasice", - "n_ml_suggestions": "{n} ML", "n_selected": "{n} selectate", - "n_tuning_suggestions": "{n} sugestie de reglaj", - "na": "n/a", + "n_tuning_suggestions": "{n} de sugestii de reglaj", + "n_tuning_suggestions_one": "{n} sugestie de reglaj", + "n_tuning_suggestions_few": "{n} sugestii de reglaj", + "n_tuning_suggestions_other": "{n} de sugestii de reglaj", "new_profile_name": "Numele noului profil", "notes": "Note", - "of": "din", - "outcome": "Rezultat", "panel_default_tab": "Fila implicită", "panel_language": "Limba panoului", "per_cycle_short": "ciclu", "pg_anti_wrinkle": "Anti-șifonare", - "pg_autofill_tip": "Completează automat intervalul din valoarea curentă ({lo}–{hi})", "pg_best_value": "Cea mai bună valoare găsită", "pg_current_value": "Valoarea curentă:", "pg_drag_hint": "Trageți linia pe grafic", - "pg_dtw": "DTW", "pg_ev_ending": "Se încheie", "pg_ev_running": "În funcțiune", "pg_group_advanced": "Cazuri limită", "pg_group_detection": "Declanșatoare de detectare", "pg_group_timing": "Reguli de temporizare", "pg_idle": "Inactiv", - "pg_last_run": "Ultima rulare: {date}", - "pg_match_score": "Scor de potrivire", - "pg_pct_of": "{p}% din {total}", "pg_poor_match": "Potrivire proastă", "pg_preset": "Presetare din Zona de test", "pg_preset_name": "Numele noii presetări", "pg_preset_none": "Selectează o presetare…", "pg_strong_match": "Potrivire puternică", - "pg_tile_ambiguous": "Ambigue", - "pg_tile_ambiguous_tip": "Cicluri cu un rezultat aproape egal între două programe", - "pg_tile_detected": "Detectate", - "pg_tile_detected_tip": "Cicluri în care puterea a depășit pragul de pornire suficient de mult pentru a fi înregistrată ca în funcțiune", - "pg_tile_matched": "Potrivite", - "pg_tile_matched_tip": "Cicluri atribuite corect unui program cunoscut", - "pg_time_left": "Timp rămas", "pg_weak_match": "Potrivire slabă", "phase_name": "Numele fazei", "name": "Nume", "phase_timeline": "Cronologie faze", "power": "Putere", "profile": "Profil", - "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ă. Alegeți unul înainte de a porni aparatul și va fi aplicat imediat ce începe ciclul următor.", + "program_tip": "Alegeți cu ce program este asociat ciclul curent. Detectarea automată alege cea mai bună potrivire. Fixați un program când detectarea automată greșește; ales înainte de pornire, se aplică ciclului următor.", "progress": "Progres", "raw_socket": "Socket brut", - "read_only": "Doar citire", "ref_cycle": "Ciclu de referință (opțional)", - "refresh_interval": "Interval de actualizare (s)", "remaining": "Rămas", "reminder_every": "Amintește la fiecare (cicluri)", "remove_label": "- Eliminați eticheta -", "rename_profile": "Redenumiți profilul", - "replay_duration": "Redare", "resulting_profile": "Profilul rezultat", "save_mode": "Mod de salvare", - "score": "Scor", - "score_breakdown": "Detalierea scorului", "seconds_from_start": "Secunde de la start", "select_profile": "Selectați profilul", "setting": "Setare", - "settings_a": "Setări A (curente)", - "settings_a_short": "A", "settings_advanced": "Avansat", - "settings_b": "Setări B (test)", - "settings_b_short": "B", "settings_basic": "De bază", "settings_detail_level": "Nivel de detaliu al setărilor", - "settings_overrides": "Suprascrieri de setări", "show_debug": "Afișați cardul de depanare a potrivirii live pe pagina de Stare (încredere, ambiguitate, principalii candidați)", "show_expected": "Afișați suprapunerea curbei așteptate (profil potrivit, portocaliu)", "show_raw": "Afișați comutatorul pentru semnalul brut al prizei în graficul de putere în timp real", "sparkline": "Tendința recentă a duratei ciclurilor", - "stage2": "Etapa 2 – similaritate de bază", - "stage3": "Etapa 3 – DTW", - "stage4": "Etapa 4 – concordanță", "status": "Stare", "tail_trim": "Tăiere finală (s)", "tap_to_dismiss": "atingeți graficul pentru a închide", @@ -526,7 +472,6 @@ "ungrouped": "Negrupat", "unlabelled": "Neetichetat", "unlabelled_paren": "(neetichetat)", - "warp_path": "Cale de deformare", "input": "Intrare:", "start": "Început", "end": "Sfârșit", @@ -551,9 +496,8 @@ "n_shown": ", {n} afișate", "cycles_title": "Cicluri ({n})", "pg_detecting": "Detectare", - "pg_best_marker": "Cel mai bun: {v}", "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ă", + "pg_time_left_model_tip": "Estimat după fazele programului, nu o numărătoare inversă fixă", "phase": "Fază", "pg_mode_history": "Testează pe istoric", "pg_ev_detected": "Detectat", @@ -576,9 +520,6 @@ "pg_alert_unmatched": "Fără potrivire", "pg_alert_ambiguous": "Potrivire ambiguă", "pg_alert_energy": "Anomalie de energie", - "pg_alert_stress_cap": "Limita de siguranță atinsă", - "pg_alert_stress_ok": "Terminare în repaus: ciclu oprit", - "pg_alert_stress_warn": "Consum în standby peste pragul de oprire", "pg_newly_correct": "acum corect", "pg_regressed": "regresat", "pg_end_timing_changed": "moment de încheiere schimbat", @@ -588,12 +529,6 @@ "pg_sweep_param": "Parametru", "pg_objective": "Obiectiv", "pg_steps": "Pași", - "pg_stress_group": "Test de terminare în repaus", - "pg_stress_idle_auto": "Auto", - "pg_stress_idle_w": "Nivel în repaus (W)", - "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_obj_match": "Acuratețea potrivirii", "pg_obj_endtiming": "Acuratețea momentului de încheiere", "pg_obj_falseend": "Rata de sfârșituri false", @@ -602,7 +537,6 @@ "pg_mode_optimize": "Optimizează", "pg_group_matching": "Potrivirea programului", "pg_zoom_hint": "dublu clic pentru a reseta zoomul", - "aria_playground_chart2": "Grafic interactiv al puterii ciclului: treci cu cursorul pentru a citi timpul/puterea, derulează pentru a mări, trage pentru a te deplasa", "pg_alert_timeout_end": "Încheiat la expirarea timpului, nu prin predicție", "pg_alert_indefinite": "Ar funcționa la nesfârșit", "task_pg_history": "Testare pe istoric", @@ -614,16 +548,9 @@ "pg_recent_runs": "Rulări recente", "pg_load_run": "Reîncarcă această rulare", "pg_match_confidence": "Încrederea în potrivire", - "pg_envelope_fit": "Încadrarea în anvelopă", - "pg_envelope_fit_tip": "Cât de bine se încadrează ciclul în anvelopa de putere salvată a acestui profil, o altă perspectivă decât încrederea în potrivire.", "n_imported_note": ", {n} importate", "enable_online": "Activați funcțiile online", - "store_brand": "Marca aparatului", - "store_model": "Modelul aparatului", "anonymous": "Anonim", - "rate_quality": "Evaluați calitatea", - "rate_n": "{n} stele", - "approved_only": "Doar aprobate", "adopt_settings": "Adoptați și setările", "flags": "Semnale", "include_phase_map": "Includeți harta fazelor", @@ -640,7 +567,6 @@ "cat_custom_phases": "Faze personalizate", "cat_profile_groups": "Grupuri de profiluri", "cat_settings": "Setări de detectare și recunoaștere", - "cat_matching_config": "Reglarea recunoașterii", "cat_ml_models": "Modele ML", "cat_feedback": "Feedback și etichete de revizuire", "cat_suggestions": "Sugestii", @@ -662,7 +588,6 @@ "hist_csv_data": "Date CSV", "hist_from_recorder": "Sau citiți-l din Home Assistant", "hist_since": "De la data", - "days": "zile", "hist_keep": "Păstrați acest ciclu", "hist_looks_complete": "complet", "peak_power_short": "Vârf", @@ -682,7 +607,36 @@ "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" + "correct_total_cycles": "Corectați totalul", + "hist_reason_already_recorded": "se suprapune cu un ciclu deja înregistrat de WashData", + "share_consent": "Confirm că aceste cicluri s-au încheiat normal, fără întreruperi", + "show_internal": "Afișează setările interne", + "show_internal_tip": "Setări pe care WashData le reglează pentru dvs. (eșantionare, watchdog, cadența potrivirii, praguri). Afișate când acest dispozitiv are o valoare proprie, un conflict sau o sugestie.", + "rate_n": "{n} stele", + "rate_quality": "Evaluați calitatea", + "pg_autolabel": "Etichetare automată", + "pg_label_review": "Solicită revizuire", + "pg_ev_match_reverted": "Potrivire anulată", + "pg_ev_verified_pause": "Pauză verificată", + "n_samples": "{n} de eșantioane", + "n_samples_one": "{n} eșantion", + "n_samples_few": "{n} eșantioane", + "n_samples_other": "{n} de eșantioane", + "n_cycles": "{n} de cicluri", + "n_cycles_one": "{n} ciclu", + "n_cycles_few": "{n} cicluri", + "n_cycles_other": "{n} de cicluri", + "energy_per_cycle": "{v}/ciclu", + "energy_total": "{v} în total", + "automation_off": "(dezactivată)", + "ref_cycle_none": "Niciunul", + "share_name": "Afișați-mi numele pe ceea ce partajez", + "cat_backfill_cycles": "Cicluri găsite într-un istoric de putere importat", + "pg_compare_profile": "Compară cu profilul", + "pg_obj_early": "Rata de sfârșituri anticipate", + "pg_obj_endlag": "Întârzierea sfârșitului (mediană)", + "pg_obj_split": "Rata de divizare", + "pg_sweep_cycles": "Cicluri recente" }, "log": { "all_levels": "Toate nivelurile", @@ -692,11 +646,9 @@ }, "ml": { "builtin_models": "● Folosind modele integrate", - "fit_fair": "Acceptabil", "fit_good": "Bun", "fit_slight": "Ușor", "fit_strong": "Puternic", - "fit_weak": "Slab", "fit_word": "potrivire", "personalized": "● Personalizat pentru această mașină", "better_than_baseline": "{pct}% mai bine decât estimarea de referință ({metric})", @@ -704,30 +656,41 @@ "trend_declining_tip": "Potrivirea acestui model s-a înrăutățit în ultimele reverificări: revizuirea mai multor cicluri îl poate ajuta să reînvețe.", "trend_steady_tip": "Potrivirea acestui model a rămas aproximativ stabilă în ultimele reverificări.", "cap_blurb": { - "end": "Să știe când un ciclu s-a încheiat cu adevărat", - "live_match": "Identificarea mai rapidă a programului în curs", - "quality": "Depistarea ciclurilor detectate greșit sau corupte", - "remaining_time": "Estimarea timpului rămas", "total_energy": "Estimarea energiei totale și a costului" }, "cap_label": { - "end": "Detectarea sfârșitului ciclului", - "live_match": "Recunoașterea programului", - "quality": "Verificarea calității ciclului", - "remaining_time": "Estimarea timpului rămas", "total_energy": "Estimarea energiei" }, - "metric_auc": "AUC {auc} pe datele de validare", - "metric_mae": "eroare: {model} față de referința {baseline}" + "metric_mae": "eroare: {model} față de referința {baseline}", + "fine_tuned_at": "ajustat pe {when}", + "held_out_count": "măsurat pe {count} de cicluri de validare", + "held_out_count_one": "măsurat pe {count} ciclu de validare", + "held_out_count_few": "măsurat pe {count} cicluri de validare", + "held_out_count_other": "măsurat pe {count} de cicluri de validare", + "last_run_not_promoted": "Ultima verificare ({when}): {reason}", + "never": "niciodată", + "skip_reason": { + "fit_failed": "Antrenarea a eșuat: {error}", + "holdout_too_small": "Prea puține cicluri pentru a testa corect un model nou: {held_out} au putut fi puse deoparte pentru testare, sunt necesare {min}.", + "insufficient_rows": "Încă nu există suficiente cicluri utilizabile: până acum, {count} de cicluri etichetate și curate.", + "insufficient_rows_one": "Încă nu există suficiente cicluri utilizabile: până acum, {count} ciclu etichetat și curat.", + "insufficient_rows_few": "Încă nu există suficiente cicluri utilizabile: până acum, {count} cicluri etichetate și curate.", + "insufficient_rows_other": "Încă nu există suficiente cicluri utilizabile: până acum, {count} de cicluri etichetate și curate.", + "not_better_than_incumbent": "Noul model nu a fost mai bun decât cel folosit (eroare {model} față de {incumbent}), așa că a fost păstrat cel actual.", + "not_better_than_naive": "Noul model nu a fost clar mai bun decât estimarea simplă (eroare {model} față de {naive}).", + "unknown": "Nu s-a învățat nimic nou." + }, + "trained_unknown": "la o dată necunoscută" }, "modal": { + "remove_task_title": "Eliminați sarcina", + "remove_task_msg": "Eliminați mementoul \"{task}\"? Înregistrările sale din jurnalul de întreținere se păstrează.", "auto_label": "Etichetare automată a ciclurilor", "correct_feedback": "Corectați feedback-ul", "create_profile": "Creați profil", "cycle": "Ciclu", "edit_group": "Editează grupul de profiluri", "edit_phase": "Editați faza", - "import_config": "Importați configurația", "label_cycle": "Etichetați ciclul", "merge_cycles": "Îmbinați {n} cicluri", "new_group": "Grup de profiluri nou", @@ -742,6 +705,8 @@ "delete_automation_title": "Ștergeți automatizarea", "delete_automation_msg": "Ștergeți automatizarea \"{name}\" din Home Assistant? Această acțiune nu poate fi anulată.", "remove_legacy_title": "Eliminați acțiunile vechi", + "reset_phase_msg": "Anulați modificarea fazei \"{name}\" și restabiliți faza integrată? Programele își păstrează intervalele.", + "reset_phase_title": "Resetați faza", "remove_legacy_msg": "Eliminați acțiunile personalizate vechi? Nu se vor mai declanșa la evenimentele de ciclu. Această acțiune nu poate fi anulată din panou.", "discard_recording_title": "Renunțați la înregistrare", "discard_recording_msg": "Renunțați la înregistrarea salvată? Această acțiune nu poate fi anulată.", @@ -762,63 +727,79 @@ "store_share_device": "Partajați acest dispozitiv", "export_select": "Exportă - alege datele", "import_wizard": "Importă - alege datele", - "history_import": "Importați istoricul de putere" + "history_import": "Importați istoricul de putere", + "apply_suggestions_title": "Aplicați sugestiile", + "apply_suggestions_msg": "Modificați aceste {n} setări? Integrarea se reîncarcă după salvare.", + "discard_settings_title": "Renunțați la modificările nesalvate?", + "discard_settings_msg": "Modificări nesalvate ale setărilor: {n}. Părăsiți Setări și renunțați la ele?", + "profile_title": "Profil · {name}", + "undo_import_msg": "Datele și setările acestui dispozitiv revin la starea de dinainte de ultimul import ({when}). Ciclurile și modificările de după se pierd.", + "undo_import_ok": "Anulați importul", + "undo_import_title": "Anulați ultimul import" }, "msg": { + "custom_task_limit": "Ați atins limita de {max} sarcini.", + "custom_tasks_intro": "Adăugați orice altceva doriți să vi se amintească. O sarcină devine scadentă când este atins oricare dintre intervale; lăsați unul gol sau 0 pentru a-l folosi doar pe celălalt.", + "maintenance_due_sensor_hint": "Senzorul binar Întreținere scadentă este activ cât timp există o sarcină scadentă, pentru propriile automatizări de notificare.", + "maintenance_supporter_intro": "Maintenance Supporter este o integrare separată care face mult mai mult: orice aparat sau sarcină, termene după timp, utilizare sau un senzor, notificări și un istoric. Poate folosi contorul de cicluri WashData.", "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.", + "pg_preset_delete_confirm": "Ștergi presetarea \"{name}\"?", + "pg_preset_limit": "Limita de presetări a fost atinsă ({n})", + "pg_preset_overwrite": "Suprascrii presetarea \"{name}\"?", "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.", - "refresh_catalog_hint": "Listele de mărci și aparate ale comunității sunt păstrate în cache pentru a menține magazinul partajat în limita zilnică. Actualizați pentru a prelua intrările adăugate sau aprobate de alții.", + "store_sibling_hint": "Nimic partajat pentru modelul dvs.? Un model înrudit de la aceeași marcă este de obicei un început bun.", + "store_declare_appliance": "Spuneți-i WashData ce aparat dețineți pentru a vedea configurațiile pe care alții le-au partajat pentru el. Sau scrieți o marcă mai sus pentru a explora.", + "refresh_catalog_hint": "Listele de mărci și aparate ale comunității sunt păstrate în cache pentru a economisi cota zilnică a magazinului. Reîmprospătați pentru a vedea intrările adăugate sau aprobate de alții.", "head_trim_hint": "Numărul de secunde de eliminat de la început", "appliance_monitor": "Monitor de aparate", "artifact_dip_detail": "A scăzut sub banda de putere obișnuită timp de ~{n}s.", "artifact_footer": "Evidențiate în graficul de mai sus. Acestea sunt artefacte tranzitorii (de exemplu, ușa deschisă la jumătatea ciclului), nu neapărat probleme.", - "artifact_header": "{n} anomalii detectate în timpul acestui ciclu", - "artifact_pause_detail": "Puterea a scăzut aproape la zero timp de ~{n}s, apoi a revenit – probabil ușa a fost deschisă în mijlocul ciclului sau ciclul a fost întrerupt.", + "artifact_header": "{n} de anomalii detectate în timpul acestui ciclu", + "artifact_header_one": "{n} anomalie detectată în timpul acestui ciclu", + "artifact_header_few": "{n} anomalii detectate în timpul acestui ciclu", + "artifact_header_other": "{n} de anomalii detectate în timpul acestui ciclu", + "artifact_pause_detail": "Puterea a scăzut aproape la zero timp de ~{n}s, apoi a revenit. Probabil ușa a fost deschisă sau ciclul a fost pus pe pauză.", "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, apoi eliminați-le eticheta (rămân în istoric, dar nu mai influențează acest profil) sau ștergeți-le definitiv.", + "cleanup_intro": "Toate ciclurile etichetate suprapuse. Bifați valorile aberante, apoi eliminați-le eticheta (rămân în istoric, dar nu mai modelează acest profil) sau ștergeți-le.", "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)", - "compare_profiles_tip": "Suprapuneți alte plicuri de profil pe graficul de mai sus pentru a vedea care dintre ele se potrivește cel mai bine acestui ciclu.", + "compare_profiles_tip": "Suprapuneți alte profiluri pe grafic pentru a vedea care se potrivește cel mai bine acestui ciclu.", "compare_selected_cycles": "Cicluri selectate (solid) – afișați / ascundeți", - "consider_new_profile": "Luați în considerare crearea unui profil nou.", - "coverage_gap": "ciclurile recente nu au un profil corespunzător ({pct}% din ultimele 30).", - "coverage_gap_similar_cycles": "{count} cicluri similare neetichetate găsite – creați un profil pentru a începe potrivirea lor.", "cycles_deleted": "{count} ciclu(ri) șterse", + "cycle_context_loading": "Se încarcă istoricul recorder-ului…", + "cycle_context_note": "Gri: istoricul recorder-ului pentru senzorul de putere în jurul acestui ciclu. Nu face parte din ciclu și nu afectează durata, energia sau potrivirea.", + "cycle_context_none": "Recorder-ul nu conține citiri în jurul acestui ciclu (Home Assistant păstrează implicit 10 zile de istoric).", + "cycle_context_tip": "Doar afișare: arată istoricul recorder-ului pentru senzorul de putere în jurul acestui ciclu, dacă mai este păstrat. Pentru a adăuga încercările de pornire eșuate la ciclurile viitoare, folosiți Preambulul curbei (Setări > Detectare > Pornire Ciclu), care modifică duratele salvate.", "enough_data": "Date suficiente pentru a învăța ({current}/{min} cicluri).", "export_description": "Alege exact ce profiluri, cicluri, setări și altele să exporți în JSON, sau analizează un fișier și importă doar părțile dorite.", "feedback_cycles_pending": "De revizuit: {n}", "feedback_prompt": "Confirmați că a fost corect, corectați programul sau ignorați.", "feedback_relabel_hint": "Și reetichetarea acestui ciclu îl rezolvă.", "filter_by_profile": "Filtrează după profil…", - "group_modal_help": "Grupați programe cu aceeași formă care diferă ca temperatură/centrifugare (duratele pot varia). Potrivirea evaluează grupul ca un singur candidat, apoi alege membrul cel mai potrivit. Alegeți cel puțin 2; suprapunerea arată cât de asemănătoare sunt.", + "group_modal_help": "Grupați programe cu aceeași formă care diferă prin temperatură sau centrifugare. Potrivirea evaluează grupul ca pe unul singur, apoi alege membrul cel mai potrivit. Alegeți cel puțin 2; suprapunerea arată cât de asemănătoare sunt.", "group_not_cohesive": "Aceste profiluri nu sunt suficient de similare pentru a fi grupate în mod fiabil, deci potrivirea le tratează individual până când eliminați valoarea aberantă sau împărțiți grupul.", "group_preview_hint": "Bifați 2 sau mai mulți membri pentru a previzualiza și compara curbele lor de putere.", - "import_intro": "Încărcați un fișier exportat sau lipiți un payload JSON mai jos.", - "legacy_actions_title": "{count} acțiune personalizată veche încă rulează", + "legacy_actions_title": "{count} de acțiuni personalizate vechi încă rulează", + "legacy_actions_title_one": "{count} acțiune personalizată veche încă rulează", + "legacy_actions_title_few": "{count} acțiuni personalizate vechi încă rulează", + "legacy_actions_title_other": "{count} de acțiuni personalizate vechi încă rulează", "live_chart_loading": "Graficul de putere în timp real apare pe măsură ce sosesc citirile.", "loading": "Se încarcă…", "loading_curve": "Se încarcă curba…", "loading_settings": "Se încarcă setările…", "log_buffer_hint": "Cel mai nou mai întâi · memorează ultimele 500 de înregistrări ha_washdata de la repornire · trageți marginea de jos pentru a redimensiona.", - "maintenance_advisory": "Creșterea duratei/energiei poate indica necesitatea întreținerii aparatului (de exemplu, detartrare, curățarea filtrului).", "maintenance_due": "Întreținere scadentă: {items}", - "maintenance_intro": "Înregistrați lucrările de service efectuate pe acest aparat și primiți un memento când fiecare sarcină devine din nou scadentă.", + "maintenance_intro": "Înregistrați lucrările de service la acest aparat și primiți un memento când fiecare sarcină devine din nou scadentă.", "maintenance_load_error": "Nu s-au putut încărca datele de întreținere: {error}", "maintenance_requires_access": "Întreținerea și exportul/importul necesită acces complet.", - "manual_duration_hint": "Durata medie/așteptată a ciclului profilului, utilizată pentru estimările timpului rămas. Editați pentru a o seta; lăsând-o neschimbată se păstrează valoarea curentă.", - "matching_tuning_intro": "În timpul învățării, WashData ajustează și ponderea acordată formei față de durată și energie în potrivirea programelor.", + "manual_duration_hint": "Durata așteptată a ciclului profilului, folosită pentru estimarea timpului rămas. Lăsați-o neschimbată pentru a păstra valoarea curentă.", "merge_intro": "Ciclurile selectate sunt combinate într-unul singur (în ordine cronologică; golurile sunt completate cu 0 W). Alegeți profilul rezultat.", "ml_intro": "WashData vine cu modele inteligente care funcționează imediat.", "ml_learned_intro": "Modele ajustate la această mașină.", - "ml_loading": "se încarcă ML…", "ml_settings_intro": "Două comutatoare independente: unul aplică modelele în timpul unui ciclu, celălalt permite WashData să le ajusteze la mașina dvs. în timp.", - "name_first_program": "Aveți suficiente cicluri – denumiți primul program pentru a începe potrivirea.", - "near_duplicate_cluster": "cluster de profiluri aproape duplicate detectat. Gruparea permite potrivirii să aleagă în mod fiabil între profiluri similare (de exemplu, același program la temperaturi/viteze de centrifugare diferite).", "no_cycles_match": "Niciun ciclu nu corespunde filtrului curent.", "no_cycles_profile": "Niciun ciclu pentru acest profil.", "no_cycles_selected": "Selectați mai întâi cel puțin un ciclu.", @@ -843,73 +824,54 @@ "no_suggestions": "Nicio sugestie activă.", "notify_services_hint": "Utilizați ID-urile de serviciu {entity} (separate prin virgulă pentru mai multe). Variabile șablon: {vars}.", "old_actions_warning": "Configurat cu vechiul editor de acțiuni (acum eliminat). Încă se declanșează la evenimentele de ciclu, dar nu mai pot fi editate aici. Convertiți-le într-o automatizare normală sau eliminați-le.", - "onboarding_progress": "{n} / 3 cicluri observate", - "onboarding_watching": "Utilizați aparatul în mod normal – WashData monitorizează. După 3 cicluri, va începe potrivirea programelor.", "pending_feedback": "Feedback de detecție în așteptare", "performance_trend": "Tendință de performanță ({n} cicluri)", - "pg_analysis_empty": "Încărcați un ciclu pentru a vedea analiza potrivirii.", "pg_apply_confirm": "Aplicați cea mai bună valoare: {label} = {value}?", - "pg_canvas_empty": "Selectați un ciclu de mai sus, apoi apăsați Încărcați pentru a vedea curba sa de putere.", - "pg_chart_caption": "Rata de detectare/potrivire în funcție de valorile {param}", "pg_ctrl_intro": "Valorile pornesc de la setările active ale acestui dispozitiv din integrare. Modificările rămân în Zona de test până când le salvezi în integrare.", "pg_live_unavailable": "Setările active nu sunt disponibile, se afișează valorile implicite.", "pg_load_live_confirm": "Renunți la {n} modificări din Zona de test și reîncarci setările actuale ale integrării?", - "pg_match_rate": "{pct}% rată de potrivire pe {total} cicluri", "pg_matches_live": "Identice cu setările active", "pg_n_changed": "{n} valori diferite față de setările active", "pg_sugg_none": "Toate sugestiile corespund deja setărilor actuale", - "pg_preset_delete_confirm": "Ștergi presetarea \"{name}\"?", - "pg_preset_limit": "Limita de presetări a fost atinsă ({n})", - "pg_preset_overwrite": "Suprascrii presetarea \"{name}\"?", "pg_publish_one_confirm": "Salvezi {label} = {value} în setările acestui dispozitiv?", "pg_restart_note": "Reporniți Home Assistant pentru a activa instrumentele de simulare.", - "pg_sim_progress": "{done} / {total} cicluri", - "pg_starting": "Se pornește…", - "pg_stress_synthetic": "simularea în repaus începe aici", - "pg_sweep_intro": "Ce-ar fi dacă {param} ar fi diferit? Testați {steps} valori pe ultimele {cycles} cicluri pentru a găsi setarea la care se potrivesc corect cele mai multe cicluri.", - "pg_sweep_step": "Pasul {done} / {total}", - "pg_verdict_bad": "Necesită atenție: multe cicluri rămân nedetectate.", - "pg_verdict_good": "Bine reglat: majoritatea ciclurilor sunt identificate și potrivite corect.", - "pg_verdict_ok": "Acceptabil: unele cicluri au fost ratate. Încercați să reduceți pragul de pornire.", "phase_catalog_intro": "Segmente denumite ale unui ciclu (Pre-spălare, Încălzire, Centrifugare…). Atribuiți-le unui profil din panoul de control al acestuia.", "phase_ranges_intro": "Intervale de fază (minute de la pornirea ciclului) suprapuse pe curba medie. Editați valorile pentru a previzualiza în timp real.", - "playground_intro": "Un spațiu sigur de testare pentru a reda ciclurile anterioare cu setări diferite, a compara două configurații una lângă alta și a inspecta cum se aliniază un ciclu la un profil. Nimic de aici nu vă modifică datele active până când nu aplicați în mod explicit.", - "playground_needs_restart": "Reporniți Home Assistant pentru a activa instrumentele Zonei de test.", - "preferences_admin": "Preferințe, panou și control acces", - "preferences_adv": "Preferințe", + "playground_intro": "Încercați setări pe datele reale ale ciclurilor dvs. Nimic de aici nu vă schimbă setările active până nu le aplicați.", "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), recalculați costul ciclurilor pe baza prețurilor energiei înregistrate și recalculați starea ciclurilor. Rulați după un lot de revizuiri.", + "process_history_hint": "Rulează din nou potrivirea pe toate ciclurile salvate, actualizează sugestiile, reantrenează modelele ML (dacă sunt activate), recalculează costul din prețurile înregistrate și recalculează sănătatea ciclurilor. Rulați după o serie 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.", - "rbac_hint": "Când este dezactivat, toți utilizatorii Home Assistant au acces complet (implicit). Administratorii au întotdeauna acces complet și pot gestiona toți utilizatorii.", - "recent_logs": "Înregistrări recente ha_washdata", + "rbac_hint": "Oprit: toți utilizatorii Home Assistant au acces complet (implicit). Administratorii au întotdeauna acces complet și pot gestiona pe toată lumea.", "recording_in_progress": "Înregistrare în curs", "reminders_intro": "Afișați un memento în panou după acest număr de cicluri de la ultimul service. Lăsați necompletat sau 0 pentru a dezactiva un memento.", "restart_gap_footer": "Evidențiat pe grafic. Datele de putere lipsesc pentru aceste intervale; potrivirea a folosit numai citiri reale.", - "restart_gap_header": "{n} gol la repornirea HA în timpul acestui ciclu", + "restart_gap_header": "{n} de goluri la repornirea HA în timpul acestui ciclu", + "restart_gap_header_one": "{n} gol la repornirea HA în timpul acestui ciclu", + "restart_gap_header_few": "{n} goluri la repornirea HA în timpul acestui ciclu", + "restart_gap_header_other": "{n} de goluri la repornirea HA în timpul acestui ciclu", "restart_gap_item": "{dur} gol", - "review_confirm_help": "Confirmați dacă acest ciclu a fost detectat corect. Recenziile dvs. antrenează modelul pe mașina dvs. – cu cât confirmați mai multe cicluri, cu atât obțineți o potrivire mai bună și un scor de sănătate mai bun. Un Bine/Rău rapid este suficient.", + "review_confirm_help": "A fost acest ciclu detectat corect? Confirmarea ciclurilor îmbunătățește potrivirea pentru mașina dvs. Ajunge un Bun/Rău rapid.", "review_in_settings": "Revizuiți în Setări", "review_notes_placeholder": "Note (opțional)", "review_notes_tip": "Note cu text liber pentru referință proprie. Nu sunt folosite de potrivire sau antrenament.", - "review_profile_tip": "Programul cu care acest ciclu este etichetat. Dacă programul detectat automat a fost greșit, corectați-l aici – etichetarea învață potrivirea pentru ciclurile viitoare.", - "review_quality_tip": "Cât de curat este acest ciclu. Bine = un exemplu model al acestui program; Rău = detectat dar zgomotos sau atipic; Inutilizabil = detectat greșit (combinat, trunchiat sau fals). Determină scorul de sănătate și ce cicluri sunt permise pentru antrenarea modelului.", - "review_recorded_tip": "Marcați acest lucru ca un ciclu de referință ales manual pentru programul său – același rol ca un ciclu înregistrat manual. Ciclurile de referință sunt întotdeauna păstrate, generează șablonul de potrivire și nu sunt niciodată eliminate de curățare. (Acesta este steagul \"de aur\"/înregistrat; ambele sunt același lucru.)", - "review_tags_tip": "Semnale opționale care descriu ce a mers prost cu acest ciclu, astfel încât antrenamentul și curățarea pot ține cont de acest lucru.", + "review_profile_tip": "Programul cu care este etichetat acest ciclu. Dacă este greșit, corectați-l aici; etichetele învață potrivirea.", + "review_quality_tip": "Cât de curat este acest ciclu. Bun: un exemplu clasic al acestui program. Rău: detectat, dar zgomotos sau neobișnuit. Inutilizabil: detectat greșit (unit, tăiat sau fals). Influențează scorul de sănătate și ce cicluri antrenează modelul.", + "review_recorded_tip": "Marcați-l ca ciclu de referință ales manual pentru programul său, ca un ciclu înregistrat manual. Ciclurile de referință sunt păstrate mereu, stau la baza șablonului de potrivire și nu sunt niciodată eliminate la curățare.", + "review_tags_tip": "Semnale opționale despre ce a mers prost, ca antrenarea și curățarea să țină cont de ele.", "review_to_cycles": "Deschideți coada de revizuire a Ciclurilor", "samples_decimated": "Se afișează {shown} din {total} eșantioane (reduse pentru afișare; vârfurile păstrate). Un spațiu mare aici se datorează reducerii, nu unor date lipsă.", - "saving_triggers_reload": "Salvarea declanșează o reîncărcare a integrării. Entitățile HA pot apărea pe scurt ca indisponibile.", + "saving_triggers_reload": "Salvarea reîncarcă integrarea. Entitățile pot apărea scurt ca indisponibile.", "search_placeholder": "Căutați setări…", "see_recorder": "Consultați widgetul de înregistrare de mai jos", "setting_changed": "Modificat de la {old} la {new} pe {date}", "settings_basic_note": "Se afișează setările esențiale. Comutați la Avansat pentru lista completă.", - "shape_drift_advisory": "⚠ Deriva formei", - "shape_drift_detail": "Tiparul de putere al acestui profil s-a schimbat în timp – posibilă uzură a aparatului sau întreținere necesară (ex. detartrare, curățarea filtrului).", "show_all_settings": "Afișați toate setările", - "showing_suggestions": "Se afișează setarea {count} cu sugestii.", - "split_intro": "Faceți clic pe grafic pentru a adăuga sau elimina un punct de divizare, sau detectați automat prin pauze de inactivitate. Fiecare segment rezultat poate primi propriul profil.", - "storage_diagnostics": "Statistici stocare, întreținere, export/import", + "showing_suggestions": "Se afișează {count} de setări cu sugestii.", + "showing_suggestions_one": "Se afișează {count} setare cu sugestii.", + "showing_suggestions_few": "Se afișează {count} setări cu sugestii.", + "showing_suggestions_other": "Se afișează {count} de setări cu sugestii.", + "split_intro": "Faceți clic pe grafic pentru a adăuga sau elimina un punct de divizare, sau detectați automat după pauzele de inactivitate. Fiecare parte poate primi propriul profil.", "sug_staged": "Setat {key} = {val}. Salvați pentru a aplica.", "toast_auto_detect_enabled": "Detecție automată activată", "toast_auto_label_complete": "Etichetare automată finalizată", @@ -926,7 +888,6 @@ "toast_no_automation": "Nu se poate crea automatizare aici", "toast_profile_name_required": "Numele profilului este obligatoriu", "toast_program_set": "Program setat: {program}", - "toast_rebuild_failed": "Reconstruire eșuată: {error}", "toast_resume_failed": "Reluare eșuată: {error}", "toast_revert_failed": "Revenire eșuată: {error}", "toast_reverted": "{key} a revenit la valoarea anterioară", @@ -937,97 +898,82 @@ "trend_energy_up": "Energia este în creștere ({pct}%/ciclu) – medie recentă {avg}", "trim_destructive_confirm": "Se păstrează doar {pct}% din ciclu, iar această acțiune nu poate fi anulată. Continuați?", "trim_intro": "Trageți mânerele roșii sau introduceți valori. Tot ceea ce este în afara ferestrei este eliminat.", - "tuning_suggestions_available": "{count} sugestie de ajustare disponibilă din ciclurile observate. Acestea apar lângă câmpurile relevante.", + "tuning_suggestions_available": "{count} de sugestii de ajustare disponibile din ciclurile observate. Acestea apar lângă câmpurile relevante.", + "tuning_suggestions_available_one": "{count} sugestie de ajustare disponibilă din ciclurile observate. Apare lângă câmpul relevant.", + "tuning_suggestions_available_few": "{count} sugestii de ajustare disponibile din ciclurile observate. Acestea apar lângă câmpurile relevante.", + "tuning_suggestions_available_other": "{count} de sugestii de ajustare disponibile din ciclurile observate. Acestea apar lângă câmpurile relevante.", "unsure_detected_prefix": "WashData nu este sigur că a detectat", - "updated": "Actualizat", "warmup_badge": "Încă învață ({done}/{needed} cicluri)", - "warmup_detail": "Acest profil are nevoie de {needed} cicluri etichetate înainte ca potrivirea automată să înceapă. Fiecare ciclu confirmat îl ajută să învețe.", + "warmup_detail": "Potrivirea funcționează deja. WashData vă cere să confirmați primele {needed} cicluri ale acestui profil înainte de a eticheta singur.", "wipe_history_warning": "Ștergeți definitiv toate ciclurile și profilurile. Nu poate fi anulat.", "compare_overlay_tip": "Suprapuneți plicurile profilurilor învățate pentru a vedea cărui program seamănă fiecare ciclu.", - "duration_trend_up_tip": "Durată în creștere ({pct}%/ciclu)", - "duration_trend_down_tip": "Durată în scădere ({pct}%/ciclu)", - "energy_trend_up_tip": "Energie în creștere ({pct}%/ciclu)", - "energy_trend_down_tip": "Energie în scădere ({pct}%/ciclu)", - "performance_trending": "Tendință de performanță", - "pg_select_cycle_hint": "Selectați un ciclu și faceți clic pe ↺ pentru a încărca", - "pg_sweep_progress": "Pasul {done} / {total}", - "pg_analysis_hint": "Selectați un ciclu și faceți clic pe ↺ pentru a încărca analiza.", "diagnostics_load_failed": "Nu s-au putut încărca diagnosticele: {error}", - "manual_duration_ref_hint": "Se folosește doar când nu este selectat niciun ciclu de referință. Un ciclu de referință stabilește durata în funcție de propria lungime.", + "manual_duration_ref_hint": "Folosit doar fără ciclu de referință; un ciclu de referință stabilește durata după propria lungime.", + "duration_learned_hint": "Calculată din ciclurile acestui program (fără valorile aberante) și actualizată după fiecare ciclu nou. Pentru a o schimba, tăiați sau eliminați ciclurile care deviază.", "toast_convert_rolled_back": "Conversia a eșuat și a fost anulată (nu a rămas nicio automatizare): {error}", "toast_convert_orphan": "Automatizarea a fost creată, dar ștergerea acțiunilor vechi a eșuat. Nu încercați din nou: eliminați manual acțiunile vechi pentru a evita o automatizare duplicată.", - "advisory_duration_trend_up": "Ciclurile '{name}' durează tot mai mult (aproximativ +{pct}% pe ciclu). Dacă comportamentul aparatului s-a schimbat, reînregistrați sau reconstruiți acest profil.", - "advisory_energy_trend_up": "'{name}' consumă tot mai multă energie (aproximativ +{pct}% pe ciclu) - merită să verificați aparatul dacă acest lucru este neașteptat.", - "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.", + "advisory_duration_trend_up": "Ciclurile '{name}' durează tot mai mult (aproximativ +{pct}% pe ciclu). Dacă aparatul s-a schimbat, reînregistrați sau reconstruiți acest profil.", + "advisory_energy_trend_up": "'{name}' consumă mai multă energie la fiecare ciclu (aproximativ +{pct}% pe ciclu). Verificați aparatul dacă acest lucru este neașteptat.", + "advisory_poor_health": "'{name}' se potrivește slab: ciclurile sale recente variază mult sau se potrivesc slab. Verificați-i ciclurile sau reînregistrați-l ca potrivirea și estimările să rămână precise.", + "advisory_shape_drift": "'{name}' s-a îndepărtat de curba sa de putere inițială, poate din cauza calcarului sau a uzurii. Luați în considerare reînregistrarea lui cu cicluri recente.", + "advisory_shape_drift_corr": "'{name}' s-a îndepărtat de curba sa de putere inițială (corelație {corr}), poate din cauza calcarului sau a uzurii. Luați în considerare reînregistrarea lui cu cicluri recente.", + "advisory_unmatchable": "'{name}' nu se poate potrivi niciodată: nu are în spate niciun ciclu cu date de putere. Etichetați sau înregistrați un ciclu al acestui program pentru a remedia.", + "advisory_unmatchable_no_duration": "'{name}' nu se poate potrivi niciodată: ciclul său nu are o durată utilizabilă. Reînregistrați acest program sau setați-i durata așteptată.", "pg_no_alerts": "Nicio problemă detectată în această rulare.", "pg_override_active": "Se folosesc setările editate față de cele actuale", "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.", "pg_sweep_intro2": "Găsiți setarea care îndeplinește cel mai bine un obiectiv pe ciclurile recente. Nimic nu se schimbă până nu o aplicați.", - "pg_running_history": "Se reiau ciclurile tale…", - "pg_running_sweep": "Se testează valori pe toate ciclurile tale…", "pg_row_load_hint": "Încarcă acest ciclu în graficul de mai sus", - "pg_history_intro2": "Reia ciclurile tale recente prin detectorul și sistemul de potrivire reale, cu setările de mai sus. Fă clic pe orice rând pentru a încărca acel ciclu în grafic; modifică o setare pentru a vedea o comparație înainte/după.", + "pg_history_intro2": "Rulați din nou ciclurile recente prin detectorul și sistemul de potrivire reale, cu setările de mai sus. Faceți clic pe un rând pentru a-l încărca în grafic; schimbați o setare pentru a compara înainte și după.", "pg_simulating": "Se simulează ciclul…", - "pg_canvas_empty2": "Alege un ciclu de mai sus și apasă Rulează pentru a-l simula. Apoi treci cu cursorul pentru a citi valorile, derulează pentru a mări și trage pentru a te deplasa.", "pg_apply_settings_confirm": "Salvezi aceste {n} setări pe acest dispozitiv? {list}", - "pg_stress_terminated_detail": "S-a stabilizat la ~{idle}W în standby -- ciclul s-a încheiat {h}h {m}m mai târziu prin {reason}.", - "pg_stress_above_threshold_detail": "Consumul în standby ~{idle}W este egal sau mai mare decât pragul de oprire efectiv ({stop}W) -- ciclul nu a fost niciodată considerat inactiv. Măriți stop_threshold_w pentru a corecta.", - "pg_stress_hit_cap_detail": "Ciclul a rulat {h}h {m}m fără să se oprească -- oprit forțat de limita de siguranță. Consum în standby {idle}W față de pragul de oprire {stop}W.", "no_logs_match": "Nicio înregistrare din jurnal nu corespunde acestor filtre.", "pg_analysis_hint2": "Selectați un ciclu și apăsați Rulați pentru a încărca analiza potrivirii.", - "pg_analysis_empty2": "Apăsați Rulați pentru a vedea analiza potrivirii.", - "imported_readonly": "Importat din magazinul comunității. Afișat pentru referință și potrivire. Nu este contabilizat în statisticile dvs. și nu poate fi editat.", + "imported_readonly": "Importat din magazinul comunității. Folosit ca referință și pentru potrivire, nu contează în statisticile dvs. și nu poate fi editat.", "merge_no_imports": "Înregistrările importate nu pot fi îmbinate într-un ciclu real. Deselectați-le pentru a le îmbina.", - "store_enable_hint": "Activați funcțiile online în Setări pentru a răsfoi și importa cicluri de referință ale comunității.", - "online_intro": "Partajați și descărcați cicluri de referință cu alți utilizatori WashData. Toate funcțiile online sunt opționale și dezactivate în mod implicit.", + "store_enable_hint": "Activați funcțiile online în Preferințe > Online și comunitate pentru a răsfoi și importa cicluri de referință ale comunității.", "store_share_intro": "Încărcați acest ciclu de referință pentru ca alți utilizatori cu același aparat să îl poată folosi. Este examinat înainte de a apărea public.", - "online_intro_global": "Răsfoiți și partajați înregistrări de referință cu alți utilizatori WashData și confirmați intrările de aparate. O singură conexiune se aplică întregii dvs. integrări WashData; marca și modelul aparatului se stabilesc per dispozitiv în De bază. Toate funcțiile online sunt opționale și dezactivate în mod implicit.", + "online_intro_global": "Răsfoiți și partajați înregistrări de referință cu alți utilizatori WashData. O conexiune acoperă toate dispozitivele dvs. WashData; marca și modelul se setează pe dispozitiv în De bază. Funcțiile online sunt oprite până le activați.", "online_unavailable": "Funcțiile online nu sunt disponibile pe acest server.", "store_picker_offline": "Activați funcțiile online din pictograma de setări pentru a alege din catalogul comunității.", "pick_brand_first": "Alegeți mai întâi o marcă de aparat.", - "model_not_found": "Încă nu este în catalog.", - "connect_to_confirm": "Conectați-vă din pictograma de setări pentru a confirma sau a evalua.", "adopt_settings_hint": "Suprascrieți pragurile de detectare și potrivire ale acestui dispozitiv cu cele partajate. Notificările, entitățile și prețul energiei nu sunt niciodată modificate.", - "include_settings_hint": "Partajați pragurile de recunoaștere și potrivire ale acestui dispozitiv (nu notificările, entitățile sau prețul energiei). Cei care adoptă aleg dacă să le aplice.", + "include_settings_hint": "Partajați pragurile de detectare și potrivire ale acestui dispozitiv (nu notificările, entitățile sau prețul energiei). Ceilalți aleg dacă le aplică.", "onboard_download": "Dispozitiv nou? Adoptați o configurație gata pregătită (programe, cicluri de referință și faze) de la un alt utilizator WashData cu același aparat.", - "share_consent": "Confirm că aceste cicluri au rulat până la finalizare normală fără întreruperi", "share_device_none": "Niciun ciclu de partajat încă. Marcați un ciclu înregistrat sau ales manual ca ciclu de referință (⭐) în fila Cicluri mai întâi.", "share_guideline_naming": "Denumiți fiecare profil exact așa cum apare pe cadranul sau afișajul aparatului (de ex. 'Cotton 40', 'Eco 60').", - "share_guideline_quality": "Partajați doar cicluri care s-au finalizat normal -- fără întreruperi la mijlocul ciclului, fără deschideri de ușă sau fluctuații de curent.", + "share_guideline_quality": "Partajați doar cicluri încheiate normal: fără întreruperi, deschideri de ușă sau căderi de tensiune.", "share_guideline_review": "Încărcarea dvs. începe ca în așteptare și apare public odată ce suficienți membri ai comunității o confirmă.", "share_guidelines_title": "Înainte de a partaja", - "store_download_device_intro": "Adoptați fiecare program partajat și ciclurile sale de referință pe dispozitivul dvs. Propriile cicluri înregistrate și statisticile nu sunt afectate.", + "store_download_device_intro": "Adăugați fiecare program partajat și ciclurile sale de referință pe dispozitivul dvs. Propriile cicluri și statistici nu sunt afectate.", "store_share_device_intro": "Încărcați {brand} {model} cu ciclurile de referință pe care le selectați. Alții cu același aparat vă pot adopta programele. Intrările sunt revizuite înainte de a apărea public.", "share_profile_no_cycles": "Niciun ciclu de referință -- marcați un ciclu ca ⭐ în fila Cicluri pentru a include acest profil", - "advisory_phase_inconsistent": "'{name}' pare să amestece programe sau temperaturi diferite - ciclurile sale se încălzesc pe durate foarte diferite. Împărțirea sa în profiluri separate (de ex. pe temperatură) va îmbunătăți potrivirea și estimările de timp.", - "advisory_phase_inconsistent_title": "⚠ Posibil programe amestecate", - "export_select_intro": "Bifează exact ce să incluzi. Selectarea profilurilor fără ciclurile lor exportă totuși un program recunoscibil (forma învățată îl însoțește).", - "import_analyze_hint": "Încarcă un fișier exportat (sau lipește JSON-ul acestuia). WashData îl analizează și arată exact ce poate fi importat înainte de orice modificare.", + "export_select_intro": "Bifați ce să includeți. Profilurile fără ciclurile lor exportă totuși un program recunoscibil.", + "import_analyze_hint": "Încărcați un fișier exportat (sau lipiți JSON-ul acestuia). WashData arată ce poate fi importat înainte de orice modificare.", "analyzing": "Se analizează…", - "device_type_mismatch_warn": "Acest export provine de la un alt tip de electrocasnic ({src} în loc de {local}). Programele și ciclurile pot fi totuși importate ca date de referință, dar setările specifice dispozitivului și importul în istoricul real sunt dezactivate.", + "device_type_mismatch_warn": "Acest export provine de la un alt tip de aparat ({src} în loc de {local}). Programele și ciclurile pot fi totuși importate ca date de referință, dar nu și setările dispozitivului sau istoricul real.", "merge_hint": "Elementele importate sunt adăugate; nimic local nu se pierde. Conflictele de nume se rezolvă mai jos.", "replace_warn": "Fiecare categorie bifată este ștearsă și înlocuită cu conținutul fișierului. Categoriile nebifate rămân neatinse.", "dest_reference_hint": "Ciclurile importate îmbunătățesc doar recunoașterea programelor și nu afectează niciodată statisticile de utilizare/energie.", - "dest_real_history_hint": "Ciclurile importate contează ca istoric propriu al acestui dispozitiv și alimentează statisticile de energie/utilizare. Folosește pentru a muta un electrocasnic pe o instalare nouă.", + "dest_real_history_hint": "Ciclurile importate contează ca istoric și statistici proprii ale acestui dispozitiv. Folosiți asta pentru a muta un aparat într-o instalare nouă.", "sug_muted": "Această setare nu va mai fi sugerată", "sug_mute_failed": "Nu s-a putut dezactiva sugestia", "sug_unmuted_all": "Sugestiile dezactivate au fost resetate", - "n_suggestions_muted": "{count} dezactivate; reglajul automat nu le va propune.", + "n_suggestions_muted": "{count} de sugestii dezactivate; reglajul automat nu le va propune.", + "n_suggestions_muted_one": "{count} sugestie dezactivată; reglajul automat nu o va propune.", + "n_suggestions_muted_few": "{count} sugestii dezactivate; reglajul automat nu le va propune.", + "n_suggestions_muted_other": "{count} de sugestii dezactivate; reglajul automat nu le va propune.", "font_size_hint": "Faceți tot din acest panou mai mare sau mai mic. Se aplică contului dvs. pe acest dispozitiv.", - "import_history_description": "Aveați deja o priză inteligentă înainte de WashData? Încărcați un export al istoricului senzorului său de putere sau citiți-l direct din Home Assistant: detectarea obișnuită rulează pe aceste date, astfel încât ciclurile anterioare apar în lista dvs. Cicluri, pregătite pentru a fi denumite.", - "hist_input_hint": "Încărcați un CSV descărcat din panoul Istoric (entitate, stare, ultima modificare) sau lăsați WashData să citească direct istoricul senzorului. Detectarea rulează apoi exact ca în timp real, iar dvs. alegeți care dintre ciclurile găsite să fie păstrate.", - "hist_recorder_hint": "Citește de la data pe care o alegeți până în prezent. Home Assistant păstrează implicit istoricul detaliat timp de 10 zile, iar după aceea doar medii orare, prea grosiere pentru a detecta cicluri - alegeți o dată mai veche doar dacă recorder-ul din Home Assistant este configurat să păstreze mai mult.", - "hist_scanning": "Istoricul dvs. este reluat prin detector. Această operațiune rulează în fundal: puteți închide această fereastră și reveni mai târziu.", + "import_history_description": "Aveați o priză inteligentă înainte de WashData? Încărcați istoricul ei de putere sau citiți-l din Home Assistant. Detectarea îl parcurge, iar ciclurile trecute apar în lista dvs. Cicluri, gata de denumit.", + "hist_input_hint": "Încărcați un CSV din panoul Istoric (entitate, stare, ultima modificare) sau lăsați WashData să citească direct istoricul senzorului. Detectarea în timp real îl parcurge și dvs. alegeți ce cicluri păstrați.", + "hist_recorder_hint": "Citește de la data aleasă până acum. Home Assistant păstrează implicit istoricul detaliat 10 zile; mediile orare mai vechi sunt prea grosiere pentru detectarea ciclurilor.", + "hist_scanning": "Istoricul dvs. trece prin detector în fundal. Puteți închide această fereastră și reveni mai târziu.", "hist_imported_count": "{n} cicluri importate.", "hist_duplicates": "{n} fuseseră deja importate și au fost omise.", "hist_capped": "Limita de cicluri importate pentru acest dispozitiv a fost atinsă; restul nu au fost salvate.", - "hist_next_step": "Se află în lista dvs. Cicluri, marcate ca istoric importat. Deschideți unul și folosiți Etichetează pentru a indica programul căruia îi aparține.", + "hist_next_step": "Se află în lista dvs. Cicluri, marcate ca istoric importat. Deschideți unul și folosiți Etichetează pentru a-i denumi programul.", "hist_rows_read": "{n} citiri preluate", "hist_breaks": "{n} întreruperi în care senzorul nu a fost disponibil", "hist_other_entity": "{n} citiri pentru alte entități ignorate", @@ -1041,26 +987,71 @@ "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.", - "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.", + "imported_history_readonly": "Găsit într-un istoric de putere importat. Ajută potrivirea, dar nu contează în statisticile dvs. și nu poate fi tăiat sau împărțit. Etichetați-l pentru a denumi programul.", + "odometer_intro": "Acest total doar crește. Ștergerea unui ciclu nu îl scade, ca mementourile de service să rămână corecte.", + "odometer_correct_hint": "Setați această valoare dacă WashData a numărat un ciclu care nu a avut loc sau dacă aparatul a rulat 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}", "standby_above_stop_title": "Încă consumă {idle} W când ciclul s-a încheiat", "standby_above_stop_sub": "{n} din ultimele {total} cicluri. Întârzierea la Oprire începe abia după ce puterea scade sub Pragul de Oprire ({stop} W), așa că ciclurile se termină târziu sau trebuie oprite manual.", - "advisory_duration_outlier": "{n} ciclu(ri) etichetate '{name}' sunt prea departe de durata sa obișnuită ca să se mai potrivească vreodată cu el (cel mai rău: {ratio}x). Probabil sunt etichetate greșit sau o înregistrare a surprins două rulări. Reetichetați-le sau împărțiți-le, ca să nu mai denatureze estimarea de timp a acestui program.", + "advisory_duration_outlier": "{n} ciclu(ri) etichetate '{name}' sunt prea departe de durata sa obișnuită pentru a se potrivi vreodată (cel mai rău: {ratio}x). Probabil sunt etichetate greșit sau conțin două rulări. Reetichetați-le sau împărțiți-le, ca să nu-i mai distorsioneze estimarea timpului.", "toast_range_required": "Setați un interval De la/Până la valid", - "no_profiles_option": "(niciun profil, adăugați unul)" + "no_profiles_option": "(niciun profil, adăugați unul)", + "compare_cycles_title": "Comparați {count} cicluri", + "edit_builtin_phase": "Aceasta este o fază integrată. Salvarea creează o copie personalizată; ștergeți copia pentru a restaura originalul.", + "fetch_error": "Încărcarea datelor a eșuat.", + "connection_lost": "Conexiunea s-a pierdut - se afișează datele de la ora {time}.", + "pg_notify_partial": "Marcajele arată doar notificările de pornire, aproape gata, final, etape și ore de liniște. Actualizările în timp real, mementourile de descărcare și alertele de depășire nu sunt simulate.", + "connect_to_confirm": "Conectați-vă din pictograma de setări pentru a confirma sau a evalua.", + "coverage_gap_cluster": "Ciclurile recente neetichetate ({count}) de aproximativ {min} min par să fie un singur program pe care nu l-ați creat încă.", + "pg_label_yes": "Ar fi etichetat \"{name}\" la sfârșitul ciclului", + "pg_label_no": "Ar solicita revizuire: {reason}", + "live_match_debug_tip": "Încredere: cât de bine se potrivește curba de putere curentă cu profilul candidat principal (0-100%). Ambiguu: cei mai buni doi candidați au scoruri la mai puțin de 5% unul de altul; eticheta rămâne incertă până la finalul ciclului.", + "automation_alias_started": "{device}: ciclu pornit", + "automation_alias_finished": "{device}: ciclu încheiat", + "automation_desc_started": "Rulează când {device} pornește un ciclu. Adăugați acțiunile dvs. (notificare, lumini, ...).", + "automation_desc_finished": "Rulează când {device} încheie un ciclu. Adăugați acțiunile dvs. (notificare, lumini, ...).", + "feedback_detected": "WashData a detectat: {name}", + "import_undo_hint": "Se păstrează un punct de restaurare de dinainte de ultimul import ({when}): {profiles} profil(uri) și {cycles} ciclu(ri).", + "replace_undo_hint": "Mai întâi se salvează un punct de restaurare, astfel încât puteți anula acest import din Export / Import.", + "pg_in_sample_note": "Aceste cicluri au ajutat la construirea profilurilor cu care sunt comparate, deci rezultatele sunt optimiste: așteptați-vă la ceva mai puține potriviri corecte pe cicluri noi.", + "hist_tail_trimmed": "Au fost eliminate {min} min de așteptare după sfârșitul ciclului, la fel cum face integrarea în timp real", + "invalid_threshold_pair": "Pragul de Oprire ({stop} W) trebuie să fie sub Pragul de Pornire ({start} W). Nu s-a salvat nimic; modificați-le pe amândouă împreună.", + "hist_truncated": "oprit la limita de {n} citiri, așa că restul istoricului nu a fost analizat", + "hist_warn_kw": "Cea mai mare citire este doar {w}. Acest senzor pare să raporteze kilowați; WashData are nevoie de wați, așa că nu se poate detecta niciun ciclu.", + "hist_warn_naive_time": "{n} de marcaje temporale nu au fus orar și au fost citite ca UTC. Dacă fișierul folosește ora locală, ciclurile sunt decalate cu diferența dvs. față de UTC, iar citirile din jurul trecerii la ora de vară se pot pierde.", + "hist_warn_naive_time_one": "{n} marcaj temporal nu are fus orar și a fost citit ca UTC. Dacă fișierul folosește ora locală, ciclurile sunt decalate cu diferența dvs. față de UTC, iar citirile din jurul trecerii la ora de vară se pot pierde.", + "hist_warn_naive_time_few": "{n} marcaje temporale nu au fus orar și au fost citite ca UTC. Dacă fișierul folosește ora locală, ciclurile sunt decalate cu diferența dvs. față de UTC, iar citirile din jurul trecerii la ora de vară se pot pierde.", + "hist_warn_naive_time_other": "{n} de marcaje temporale nu au fus orar și au fost citite ca UTC. Dacă fișierul folosește ora locală, ciclurile sunt decalate cu diferența dvs. față de UTC, iar citirile din jurul trecerii la ora de vară se pot pierde.", + "pg_compare_profile_tip": "Alege doar profilul desenat peste grafic. Simularea caută întotdeauna singură potrivirea ciclului, la fel ca integrarea în timp real.", + "pg_outcome_stale": "Setările s-au schimbat de la rularea acestei simulări. Încă arată rezultatul vechi.", + "pg_sweep_guarded": "Încheie prea devreme mai multe cicluri, divizează mai multe dintre ele sau ratează cicluri pe care setarea actuală le detectează, așa că această valoare nu este recomandată niciodată.", + "pg_sweep_keep_current": "Păstrați valoarea actuală ({value}, {metric}): nicio valoare nu a fost mai bună cu cel puțin un ciclu.", + "pg_sweep_n_cycles": "pe {count} de cicluri", + "pg_sweep_n_cycles_one": "pe {count} ciclu", + "pg_sweep_n_cycles_few": "pe {count} cicluri", + "pg_sweep_n_cycles_other": "pe {count} de cicluri", + "pg_task_busy": "O rulare Testează pe istoric sau Optimizează este deja în desfășurare pentru acest dispozitiv. Așteptați să se termine sau anulați-o." }, "phase_desc": { "anti_crease": "Tumblări scurte ocazionale după finalizare pentru a reduce șifonarea.", "anti_wrinkle": "Tumblare periodică post-ciclu pentru a reduce șifonarea.", + "baking": "Rezistență la temperatură înaltă activă pentru formarea cojii și a miezului.", + "cooking": "Faza principală de gătire, cu rezistența și ventilatorul active.", "cool_down": "Tumblare fără căldură aproape de sfârșitul ciclului.", + "cool_down_af": "Răcire doar cu ventilatorul după încălzire.", "drain_and_switch": "Perioada de tranziție de la modul de spălare la modul de uscare.", "dry": "Etapă de uscare folosind rezistența și/sau căldura reziduală.", "drying": "Perioada principală de tumblare cu căldură.", "heat_up": "Încălzirea inițială a rezistentei înainte de a începe uscarea completă.", + "keep_warm": "Etapă cu căldură redusă pentru a păstra mâncarea caldă.", + "keep_warm_bm": "Etapă cu căldură redusă pentru a păstra pâinea caldă după coacere.", + "kneading": "Motorul amestecă și frământă aluatul. Consum mare.", + "pause_af": "Pauză scurtă pentru scuturare sau verificare.", + "pre_heat": "Încălzirea inițială a camerei înainte de gătirea propriu-zisă.", "pre_rinse": "Pulverizare inițială înainte de spălarea cu detergent.", + "proving": "Dospire la căldură redusă, ca drojdia să fermenteze și aluatul să crească.", + "resting": "Pauză scurtă cu putere redusă între frământări, pentru relaxarea glutenului.", "pre_wash": "Înmuiere inițială sau pre-tratament înainte de spălarea principală.", "rinse": "Etapă de clătire cu apă curată. Această fază se poate repeta de mai multe ori.", "sanitize": "Etapă de curățare la temperatură înaltă pentru programele de igienizare.", @@ -1073,6 +1064,7 @@ "wash_dw": "Spălare principală cu detergent și încălzire." }, "placeholder": { + "custom_task": "de exemplu, curățare garnitura ușii", "add": "adaugă…", "group_name": "ex. Bumbac 2:47", "json_buttons": "[{\"action\":\"ID\",\"title\":\"Label\"}]", @@ -1100,10 +1092,10 @@ "section": { "dishwasher": { "label": "Mașină de spălat vase", - "intro": "Gestionarea sfârșitului de ciclu pentru mașinile de spălat vase, care de obicei se termină cu o fază lungă de uscare aproape silențioasă înainte de o scurtă evacuare finală." + "intro": "Gestionarea finalului pentru mașinile de spălat vase, care adesea usucă mult timp în liniște înainte de o scurtă evacuare finală." }, "anti_wrinkle": { - "intro": "Modul anti-șifonare detectează pulsuri de tumblare la putere redusă după faza principală și le menține atașate ciclului terminat.", + "intro": "Păstrează în ciclul încheiat impulsurile de rotire cu putere redusă ale unui uscător de după faza principală.", "label": "Anti-Șifonare" }, "basic": { @@ -1111,7 +1103,7 @@ "label": "De bază" }, "delay": { - "intro": "Detectarea startului întârziat identifică momentul în care un aparat este pornit dar nu și-a început ciclul.", + "intro": "Detectează un aparat pornit care așteaptă o pornire întârziată.", "label": "Start Întârziat" }, "detection": { @@ -1135,161 +1127,153 @@ "label": "Temporizare și Watchdog" }, "triggers": { - "intro": "Semnale externe opționale: un declanșator de sfârșit, un senzor de ușă, un comutator de pauză și reamintirea de descărcare.", + "intro": "Extra opționale: declanșator de final, senzor de ușă, comutator de pauză și memento de descărcare.", "label": "Declanșatoare și Uși" - }, - "phase_eta": { - "label": "Timp rămas", - "intro": "Timp rămas ținând cont de faze, pentru mașinile a căror durată de ciclu depinde de temperatură sau de centrifugare." } }, "setting": { "dishwasher_end_spike_quiet_release": { "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ă." + "doc": "După durata așteptată: cât timp trebuie să rămână mașina de spălat vase sub Pragul de Oprire înainte ca WashData să nu mai aștepte evacuarea finală. Creșteți-l dacă o evacuare târzie după o uscare lungă este ratată. Așteptarea nu depășește niciodată 30 de minute." }, "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." + "doc": "Fracțiunea din durata medie a programului pe care un ciclu trebuie să o atingă înainte ca rotirea sa cu putere redusă să conteze ca final anti-șifonare. Reduceți-o (de ex. 0,75) la uscătoarele cu senzor de încărcare. Lăsați mașinile de spălat la implicit (0,98), altfel o spălare se poate încheia la jumătate." }, "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.", + "doc": "Păstrați în ciclul încheiat impulsurile scurte de rotire cu putere redusă ale unui uscător de după faza principală de încălzire, în loc să fie citite ca cicluri noi.", "label": "Activați Detectarea Anti-Șifonare" }, "anti_wrinkle_exit_power": { - "doc": "Puterea trebuie să scadă sub aceasta între impulsuri pentru ca modul antirid să rămână activ.", + "doc": "În modul anti-șifonare, puterea sub această valoare contează ca liniște între impulsurile de rotire, iar o liniște mai lungă decât Interval Maxim Între Impulsuri încheie modul. Dacă Prag de Oprire este mai mare, se folosește acela, deci această setare contează doar peste Prag de Oprire.", "label": "Prag Putere de Ieșire" }, "anti_wrinkle_idle_timeout": { - "doc": "Cât timp poate rămâne aparatul în liniște între două impulsuri de rotire înainte ca modul anti-șifonare să se încheie. Setați-l peste cel mai lung interval pe care uscătorul îl lasă între impulsuri, altfel fiecare impuls ulterior este citit ca o pornire falsă.", + "doc": "Cât timp poate sta uscătorul liniștit între impulsurile de rotire înainte ca modul anti-șifonare să se încheie. Setați-l peste cea mai lungă pauză a uscătorului dvs., altfel impulsurile ulterioare sunt citite ca porniri false.", "label": "Interval Maxim Între Impulsuri" }, "anti_wrinkle_max_duration": { - "doc": "Pulsurile mai lungi decât aceasta sunt tratate ca un ciclu real, mai degrabă decât o răsturnare antirid.", + "doc": "Un impuls mai lung contează ca un ciclu real, nu ca rotire anti-șifonare.", "label": "Durată Maximă" }, "anti_wrinkle_max_power": { - "doc": "Un puls peste această putere este tratat ca un ciclu cu adevărat nou, nu ca o răsturnare antirid. Setați chiar deasupra puterii pulsului.", + "doc": "Un impuls peste această putere contează ca un ciclu nou real, nu ca rotire. Setați-l chiar peste puterea impulsurilor dvs. de rotire.", "label": "Putere Maximă Anti-Șifonare" }, "auto_label_confidence": { - "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.", + "doc": "Scorul de potrivire la finalul ciclului de la care programul este etichetat fără întrebare. Creșteți-l pentru mai multă certitudine; scădeți-l pentru a eticheta automat mai multe cicluri. Păstrați-l peste Încredere Învățare.", "label": "Încredere Etichetare Automată" }, "auto_maintenance": { - "doc": "Efectuați întreținerea de noapte: reconstruiți plicurile de profil, recalculați starea ciclului, eliminați urmele de depanare și păstrați cele mai recente cicluri.", + "doc": "Curățare de noapte: etichetează ciclurile vechi care acum se potrivesc clar unui program, reconstruiește curbele programelor, actualizează marcajele de anomalie și sănătatea ciclurilor și elimină urmele de depanare dacă Salvați Urmele de Depanare este oprit. Ciclurile și urmele lor de putere sunt păstrate mereu.", "label": "Întreținere Automată (curățare nocturnă)" }, "completion_min_seconds": { - "doc": "Ciclurile mai scurte decât aceasta sunt eliminate ca cicluri fantomă (testare, deschiderea ușii pentru a adăuga un șosetă).", + "doc": "Ciclurile mai scurte de atât (rulări de test, ușa deschisă pentru a mai pune o șosetă) sunt salvate ca Întrerupt și nu primesc notificare de final.", "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." + "doc": "Adaugă la începutul unui ciclu citirile din încercările de pornire eșuate, pentru mașinile care testează înainte de pornire (blocarea ușii, prima umplere). 300 s ajung pentru majoritatea; 0 = oprit. Pornirile înregistrate se mută mai devreme, așa că duratele învățate deviază o vreme." }, "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.", + "doc": "Puterea trebuie să rămână în banda de standby atât timp înainte ca aparatul să fie considerat în așteptarea pornirii.", "label": "Secunde de Confirmare" }, "delay_start_detect_enabled": { - "doc": "Detectați când aparatul este pornit și așteaptă (pornire întârziată / standby), dar nu și-a început ciclul, astfel încât retragerea în așteptare să nu fie confundată cu un ciclu de funcționare.", + "doc": "Detectați un aparat pornit care așteaptă o pornire întârziată, ca consumul său în standby să nu fie luat drept ciclu în desfășurare.", "label": "Activați Detectarea Startului Întârziat" }, "delay_timeout_hours": { - "doc": "Nu mai așteptați în modul de pornire întârziată după atâtea ore și reveniți la inactiv, astfel încât o mașină rămasă alimentată, dar care nu a pornit niciodată, nu așteaptă pentru totdeauna.", + "doc": "Renunțați la așteptarea unei porniri întârziate după acest număr de ore și reveniți la inactiv.", "label": "Ore de Timeout" }, "device_type": { - "doc": "Clasa de aparate. Setează valorile implicite sensibile de detecție (praguri, întârziere la oprire, manipulare finală) reglate pentru acel tip de aparat; schimbați-l numai dacă dispozitivul a fost configurat inițial ca tip greșit.", + "doc": "Stabilește valorile implicite de detectare (praguri, întârziere la oprire, gestionarea finalului) pentru acest tip de aparat. Schimbați-l doar dacă dispozitivul a fost configurat cu tipul greșit.", "label": "Tipul Dispozitivului" }, "door_sensor_entity": { - "doc": "Senzor binar de ușă opțional. Folosit pentru a detecta când aparatul a fost deschis/descărcat după un ciclu.", + "doc": "Senzor de ușă opțional. Arată când aparatul a fost deschis pentru descărcare după un ciclu.", "label": "Entitate Senzor Ușă" }, "duration_tolerance": { - "doc": "Toleranță pentru estimările timpului rămas (feedback de învățare, nepotrivire). Dacă durata reală se încadrează în +/-X% din estimare, se consideră o potrivire bună.", + "doc": "Doar pentru învățare: un ciclu în limita a +/-X% din durata estimată contează ca estimare bună. Nu afectează potrivirea.", "label": "Toleranță Estimare" }, "enable_ml_models": { - "doc": "În timp ce un ciclu rulează, lăsați modelele să rafineze rezultatele în direct: o estimare mai stabilă a timpului rămas și a energiei/costului și o protecție anti-oprire prematură la detectarea finalului (poate doar întârzia o finalizare, nu se oprește niciodată mai devreme și este limitată). Folosește modelele dvs. ajustate atunci când sunt disponibile, altfel cele încorporate. Off = doar logica clasică bazată pe putere (încă de încredere).", + "doc": "Folosiți un model antrenat pe această mașină pentru a prezice energia și costul unui ciclu în desfășurare și lăsați un program bine cunoscut care se oprește neobișnuit de devreme să se încheie fără întreaga întârziere la oprire. Oprit: doar logică bazată pe putere.", "label": "Aplicați modele inteligente în timpul unui ciclu" }, "end_energy_threshold": { - "doc": "În timpul numărătoarei inverse pentru oprire, energia acumulată (wați x timp) este comparată cu acest prag. Dacă este depășită, numărătoarea inversă se resetează - menținând robinetele anti-șifonare și cozile de uscare a mașinii de spălat vase atașate la ciclu în loc să le tăiați scurt. Ridicați-l dacă ciclurile se termină prea devreme în timpul răcirii; coborâți-l dacă detectarea este lentă.", + "doc": "Energia permisă în timpul întârzierii la oprire. Peste ea, ciclul continuă, astfel încât rotirile și finalurile de uscare rămân parte din el. Scădeți-o dacă ciclurile se încheie în timpul unui final; creșteți-o dacă ciclurile terminate tot așteaptă.", "label": "Energia de Sfârșit" }, - "end_repeat_count": { - "doc": "Numărul de citiri consecutive sub pragul de oprire necesare înainte de încheierea ciclului. 1 este în regulă pentru majoritatea prizei. Ridicați la 2-3 dacă mufa dvs. inteligentă raportează ocazional un eșantion fals zero la jumătatea ciclului și ciclurile dumneavoastră se termină prematur.", - "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). 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.", + "doc": "Senzor cu prețul curent pe kWh (de ex. un tarif dinamic). Are prioritate față de prețul fix de mai jos, deci trebuie să fie un preț, nu senzorul de putere sau de energie al prizei.", "label": "Entitate Preț Energie" }, "energy_price_static": { - "doc": "Preț fix pe kWh utilizat pentru cifrele de cost atunci când nu este stabilită mai sus nicio entitate de preț activ.", + "doc": "Preț fix pe kWh, folosit când nu este setată nicio entitate de preț.", "label": "Preț Static Energie (pe kWh)" }, "energy_sensor": { - "doc": "Contor de energie cumulativ opțional (total_increasing în kWh/Wh, de ex. propriul contor de consum total al prizei). Când este setat, energia raportată a fiecărui ciclu este preluată din diferența acestui contor între început și sfârșit, ceea ce evită subevaluarea care apare la integrarea unui senzor de putere care raportează lent. Revine la valoarea integrată dacă citirea lipsește, dacă unitatea sa este necunoscută sau dacă delta nu este pozitiv. Lăsați necompletat pentru a continua să integrați senzorul de putere.", + "doc": "Contor opțional în kWh sau Wh (total_increasing, de ex. contorul propriu al prizei). Energia ciclului este citită atunci din el, ceea ce e mai precis decât calculul dintr-un senzor de putere lent. Dacă citirea lui nu poate fi folosită, se folosește valoarea calculată.", "label": "Entitate contor de energie" }, "expose_debug_entities": { - "doc": "Publicați entități suplimentare de diagnosticare HA (încrederea potrivirii, ambiguitatea, elementele interne de stare). Off menține lista de entități curată pentru utilizare normală.", + "doc": "Adăugați entități de diagnostic (încredere de potrivire, ambiguitate, stare internă). Oprit păstrează lista de entități curată.", "label": "Expuneți Entitățile de Depanare" }, "external_end_trigger": { - "doc": "Senzor binar a cărui schimbare de stare marchează sfârșitul ciclului (de exemplu, un contact „terminat” al aparatului sau o integrare însoțitoare).", + "doc": "Senzor binar a cărui schimbare marchează finalul ciclului (de ex. un contact „gata” sau altă integrare).", "label": "Entitate Declanșator Extern" }, "external_end_trigger_enabled": { - "doc": "Lăsați un senzor binar extern să semnaleze sfârșitul unui ciclu, pe lângă detectarea încorporată bazată pe putere.", + "doc": "Lăsați un senzor binar să semnaleze finalul unui ciclu, pe lângă detectarea bazată pe putere.", "label": "Activați Declanșatorul Extern de Sfârșit" }, "external_end_trigger_inverted": { - "doc": "Tratați senzorul de declanșare oprit (mai degrabă decât pornit) ca semnal de sfârșit de ciclu.", + "doc": "Considerați trecerea senzorului pe OPRIT (nu pe PORNIT) drept semnal de final.", "label": "Inversați Declanșatorul Extern (declanșare la OPRIT)" }, "learning_confidence": { - "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ă.", + "doc": "Scorul minim de potrivire pentru a eticheta un ciclu încheiat. Un câștigător clar este etichetat; un caz strâns ajunge în coada de revizuire din Cicluri. Sub acest prag nu se etichetează și nu se întreabă nimic. Păstrați-l sub Încredere Etichetare Automată.", "label": "Încredere Învățare" }, "linked_device": { - "doc": "Opțional, plasați acest dispozitiv WashData sub alt dispozitiv (de exemplu, mufa inteligentă) în registrul dispozitivului HA, afișat ca „Conectat prin...”.", + "doc": "Opțional. Afișați acest dispozitiv WashData sub alt dispozitiv (de ex. priza inteligentă) în Home Assistant, ca „Conectat prin ...”.", "label": "Grupați sub Dispozitiv" }, "min_off_gap": { - "doc": "Dacă mașina se oprește mai puțin de acest timp, secvența de pornire/oprire/pornire este tratată ca un ciclu continuu. Împiedică programele de înmuiere (mașina se oprește timp de câteva minute la mijlocul spălării) să fie împărțite în două cicluri separate. Setați-l mai scurt decât distanța dintre sarcinile dvs. back-to-back dacă doriți ca acestea să fie socotite ca cicluri separate. Setările implicite ale tipului de dispozitiv protejează pauza tipică în cadrul ciclului pentru fiecare aparat.", + "doc": "O perioadă de oprire mai scurtă nu împarte un ciclu, așa că pauzele de înmuiere rămân într-o singură spălare. Păstrați-o mai scurtă decât pauza dintre încărcături succesive, altfel acestea se unesc într-un singur ciclu.", "label": "Pauză Minimă de Oprire" }, "min_power": { - "doc": "Puterea minimă absolută considerată activă. Citirile de mai jos sunt tratate ca 0 W (standby), eliminând încărcătura fantomă a prizei inteligente și a LED-urilor de așteptare.", + "doc": "Citirile sub această valoare contează ca 0 W (standby). Filtrează consumul în repaus al prizelor inteligente și al LED-urilor de standby.", "label": "Putere Minimă" }, "ml_training_enabled": { - "doc": "Studiați periodic ciclurile examinate peste noapte și reglați fin modelele la această mașină specifică. O schimbare este păstrată doar atunci când obține cu adevărat un scor mai bun în ciclurile susținute, așa că aceasta poate doar să ajute sau să rămână la fel - nu regresează niciodată.", + "doc": "Reantrenați din când în când modelul de energie pe ciclurile încheiate ale acestei mașini. Un model nou este păstrat doar dacă bate estimarea simplă pe ciclurile de test.", "label": "Învățați de la această mașină" }, "ml_training_hour": { - "doc": "Ora locală a zilei (0-23) pentru a face reglajul fin peste noapte. Alegeți o oră de liniște, cum ar fi 2 (02:00).", + "doc": "Ora din zi (0-23) pentru antrenarea de noapte. Alegeți o oră liniștită, de ex. 2.", "label": "Învățați la ora" }, "ml_training_interval_days": { - "doc": "Verificați din nou pentru îmbunătățiri cel mult o dată la atâtea zile.", + "doc": "Numărul minim de zile între două reantrenări.", "label": "Verificați cel mult la fiecare" }, "ml_training_min_cycles": { - "doc": "Așteptați până când cel puțin atât de multe cicluri au fost înregistrate înainte de reglare fină, astfel încât să aveți suficiente din care să învățați.", + "doc": "Așteptați cel puțin acest număr de cicluri înregistrate înainte de antrenare.", "label": "Cicluri necesare mai întâi" }, "name": { - "doc": "Numele afișat este afișat în lista de integrări HA și în registrul dispozitivului.", + "doc": "Numele afișat în Home Assistant pentru acest dispozitiv.", "label": "Numele Dispozitivului" }, "no_update_active_timeout": { - "doc": "Dacă nu sosesc actualizări de alimentare pentru atâta timp în timp ce rulați, presupuneți că mufa a fost oprită offline și opriți forțat pentru a evita un ciclu zombi. Valoarea implicită de 600 s permite întârzierea norului sau a rețelei.", + "doc": "Dacă în timpul unui ciclu nu sosește nicio actualizare de putere atât timp, se presupune că priza a ieșit offline și ciclul este oprit forțat. Valoarea implicită de 600 s lasă loc întârzierilor din cloud sau din rețeaua mesh.", "label": "Timeout Fără Actualizare" }, "notify_before_end_minutes": { @@ -1297,35 +1281,35 @@ "label": "Alertă Înainte de Sfârșit" }, "notify_channel": { - "doc": "Numele canalului de notificare Android pentru mesajele de pornire/live (controlează sunetul pe canal și prioritatea în aplicația mobilă). Blank utilizează aplicația însoțitoare implicită.", + "doc": "Canal Android pentru mesajele de pornire și în timp real; canalul stabilește sunetul și prioritatea. Gol: implicitul aplicației.", "label": "Canal Android (pornire/timp real)" }, "notify_cycle_timers": { - "doc": "Notificări la anumite minute dintr-un ciclu (de exemplu, pentru a adăuga balsam). Mesajul acceptă {device}, {program}, {minutes}. Activați Auto-pause pentru a întrerupe în acel moment și a primi o notificare interactivă cu un buton Reluare; reluați prin panou, serviciul de pauză/reluare sau acțiunea de notificare.", + "doc": "Notificări la anumite minute dintr-un ciclu (de ex. pentru a adăuga balsam). Mesajul acceptă {device}, {program}, {minutes}. Cu Pauză automată, ciclul se oprește și acolo pe pauză, iar notificarea primește un buton Reluare.", "label": "Temporizatoare Ciclu" }, "notify_finish_channel": { - "doc": "Numele canalului de notificare Android pentru mesajul final. Blank reutiliza canalul de pornire/live.", + "doc": "Canal Android pentru mesajul de final. Gol: refolosește canalul de pornire/timp real.", "label": "Canal Android (finalizare)" }, "notify_finish_message": { - "doc": "Corpul trimis când se termină un ciclu. Variabile de șablon: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}. {time_finished} și {vs_typical} sunt cele mai utile aici.", + "doc": "Corpul trimis când un ciclu se încheie. Variabile: {device}, {program}, {duration} (minute), {duration_hm} (1 h 05 min), {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}, {status} (completed sau force_stopped; ciclurile întrerupte nu trimit nimic). Cu orice altă variabilă se folosește textul implicit.", "label": "Mesaj de Finalizare" }, "notify_finish_services": { - "doc": "notifică.* servicii apelate când se termină un ciclu. Adăugați unul pe țintă; lăsați gol pentru nicio notificare de final.", + "doc": "Servicii notify.* apelate când un ciclu se încheie. Lăsați gol pentru niciunul.", "label": "Servicii de Finalizare" }, "notify_fire_events": { - "doc": "De asemenea, declanșați evenimente ha_washdata_* la începutul/terminarea ciclului, astfel încât să vă puteți construi propriile automatizări.", + "doc": "Declanșați și evenimente ha_washdata_* la pornirea și la finalul ciclului, pentru propriile automatizări.", "label": "Declanșați Evenimente HA pentru Notificări" }, "notify_icon": { - "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.", + "doc": "Pictogramă mică pe notificare (de ex. mdi:washing-machine). Se trimite doar către ținte mobile_app, nu către grupuri notify. Pe iOS înlocuiește pictograma aplicației și necesită aplicația companion 2026.8 sau mai nouă. Gol: implicitul platformei.", "label": "Pictogramă Notificare" }, "notify_live_chronometer": { - "doc": "Afișați un temporizator de numărătoare inversă de actualizare live în notificare (pe platformele care îl acceptă) în loc de o estimare statică.", + "doc": "Afișați o numărătoare inversă în desfășurare în notificare (unde este acceptată) în loc de o estimare fixă.", "label": "Utilizați Cronometru în Timp Real" }, "notify_live_interval_seconds": { @@ -1333,143 +1317,135 @@ "label": "Interval Actualizare în Timp Real" }, "notify_live_overrun_percent": { - "doc": "Dacă un ciclu trece peste estimarea sa cu mai mult de acest procent, trimiteți o alertă de depășire.", + "doc": "Actualizările în timp real se opresc când un ciclu depășește durata estimată cu acest procent (20 = până la 1,2x timpul estimat).", "label": "% Depășire Înainte de Alertă" }, "notify_live_services": { - "doc": "notify.* serviciile solicitate pentru actualizări live de progres în timp ce rulează un ciclu. Lăsați gol pentru a dezactiva notificările de progres live.", + "doc": "Servicii notify.* pentru actualizările de progres în timp real în timpul unui ciclu. Lăsați gol pentru a le opri.", "label": "Servicii Progres în Timp Real" }, "notify_only_when_home": { - "doc": "Trimiteți notificări numai atunci când cel puțin una dintre persoanele conectate (mai sus) este acasă.", + "doc": "Notificați doar când cel puțin una dintre persoanele de mai sus este acasă.", "label": "Notificați Doar Când Acasă" }, "notify_people": { - "doc": "persoană.* entități utilizate de „Notifică numai când acasă” pentru a decide dacă cineva este acasă.", + "doc": "Entități person.* pe care le verifică „Notificați Doar Când Acasă”.", "label": "Persoane (pentru Doar Când Acasă)" }, "notify_pre_complete_message": { - "doc": "Corpul alertei de aproape gata / pre-finalizare. Variabile de șablon: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Textul fiecărei actualizări de progres în timp real. Variabile de șablon: {device}, {program}, {minutes} (minute rămase). Cu orice altă variabilă se folosește textul implicit.", "label": "Mesaj Pre-Finalizare" }, "notify_reminder_message": { - "doc": "Corpul mementoului de descărcare care încă așteaptă. Necompletat folosește valoarea implicită. Variabile de șablon: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Textul Alertei Înainte de Sfârșit, trimisă o singură dată. Variabile de șablon: {device}, {program}, {minutes} (setarea Alertă Înainte de Sfârșit). Cu orice altă variabilă se folosește textul implicit.", "label": "Mesaj Reamintire" }, "notify_start_message": { - "doc": "Corpul trimis când începe un ciclu. Variabile de șablon: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Corpul trimis când pornește un ciclu. Variabile de șablon: {device}, {program}. Cu orice altă variabilă se folosește textul implicit.", "label": "Mesaj de Pornire" }, "notify_start_services": { - "doc": "notifică.* servicii apelate când începe un ciclu. Adăugați unul pentru fiecare țintă (telefon, tablou de bord etc.); lăsați gol pentru notificarea fără pornire.", + "doc": "Servicii notify.* apelate când un ciclu pornește (telefon, dashboard, ...). Lăsați gol pentru niciunul.", "label": "Servicii de Pornire" }, "notify_timeout_seconds": { - "doc": "Închideți automat notificarea după atâtea secunde (pe platformele care o acceptă). 0 îl păstrează până când este anulat manual.", + "doc": "Închideți notificarea după acest număr de secunde (unde este acceptat). 0 o păstrează până o închideți.", "label": "Ignorare Automată După" }, "notify_title": { - "doc": "Titlul notificarii. Variabile de șablon: {device}, {program}, {duration}, {energy}, {cost}, {date}, {time}, {minutes}.", + "doc": "Titlul notificării. Se înlocuiește doar {device}; cu orice altă variabilă se folosește titlul implicit.", "label": "Titlu Notificare" }, "notify_unload_delay_minutes": { - "doc": "La câteva minute după încheierea unui ciclu, înainte de a trimite mementoul încă în așteptare „descărcați mașina”. Setați 0 pentru a dezactiva mementoul.", + "doc": "Minute după finalul unui ciclu până la mementoul „descărcați mașina”. 0 îl dezactivează.", "label": "Întârziere Reamintire Descărcare" }, "notify_unload_repeat": { - "doc": "Continuă să retrimită mementoul de descărcare la fiecare \"Întârziere Reamintire Descărcare\" minute până când descărcarea este confirmată sau atingi \"Oprește mementourile\" în notificare. Necesită o modalitate de a confirma descărcarea: o Entitate Senzor Ușă, o Entitate Confirmare Descărcare sau opțiunea \"Confirmă Descărcarea Manual\" de mai jos. Butonul de respingere funcționează pe notificările aplicației complementare (mobile) Home Assistant.", + "doc": "Retrimiteți mementoul de descărcare la fiecare „Întârziere Reamintire Descărcare” minute până când descărcarea este confirmată sau atingeți „Oprește mementourile” (aplicația mobilă). Necesită o Entitate Senzor Ușă, o Entitate Confirmare Descărcare sau „Confirmă Descărcarea Manual”.", "label": "Repetă Până la Confirmarea Descărcării" }, "off_delay": { - "doc": "Timp de așteptat după căderea energiei înainte de a declara ciclul încheiat. Dacă alimentarea se reia în această fereastră, ciclul continuă fără întreruperi - acest lucru face legătura între etapele de spălare. Mașinile de spălat vase au faze lungi de uscare (oprire pentru 20-60 de minute), așa că întârzierea de oprire trebuie să depășească aceasta pentru a menține întreaga spălare+uscare ca un singur ciclu.", + "doc": "Cât timp trebuie să rămână puterea scăzută înainte ca ciclul să se încheie. Dacă puterea revine în acest timp, ciclul continuă. Mașinile de spălat vase pot sta 20-60 min în repaus la uscare, deci setați-o mai lungă de atât.", "label": "Întârziere la Oprire" }, "pause_cuts_power": { - "doc": "Când un ciclu este întrerupt, dezactivați, de asemenea, entitatea de comutare de mai jos. Doar pentru aparatele a căror priză poate fi tăiată în siguranță la jumătatea ciclului.", + "doc": "Când un ciclu este pus pe pauză, opriți și Entitatea Switch de mai jos. Doar pentru aparatele care pot rămâne fără curent la mijlocul ciclului în siguranță.", "label": "Pauza Întrerupe Alimentarea (prin switch)" }, "power_off_delay": { - "doc": "Cât timp trebuie să rămână puterea sub Prag Oprire Alimentare după terminarea unui ciclu înainte ca starea să revină la Oprit. Folosit doar când Prag Oprire Alimentare este peste 0. Este verificat conform cadenței din fundal, astfel încât întârzierea efectivă este rotunjită în sus până la următorul tick de expirare a stării.", + "doc": "Cât timp trebuie să rămână puterea sub Prag Oprire Alimentare după un ciclu înainte ca starea să revină la Oprit. Folosit doar când acel prag este peste 0. Se verifică periodic, așa că așteptarea reală poate fi puțin mai lungă.", "label": "Întârziere Oprire Alimentare" }, "power_off_threshold_w": { - "doc": "Detectare opțională a stării Oprit pe baza puterii. Când este peste 0, odată ce un ciclu s-a terminat și puterea rămâne sub acest nivel pe durata specificată de Întârziere Oprire Alimentare, mașina este considerată oprită, iar starea revine la Oprit. Lăsați la 0 pentru a dezactiva (valoarea implicită). Setați-l peste consumul real când mașina este oprită și sub Prag de Oprire, precum și sub consumul în standby al mașinii (terminată, dar încă pornită); dacă nu este sub Prag de Oprire, este ignorat. Când este activată, înlocuiește Întârziere Resetare Progres pentru revenirea la Oprit, astfel încât o mașină terminată rămâne în Terminat/Curat până când este oprită efectiv.", + "doc": "După un ciclu, o putere sub această valoare pe durata Întârziere Oprire Alimentare pune starea pe Oprit, în locul Întârziere Resetare Progres. Setați-o peste consumul în stare oprită, sub standby și sub Pragul de Oprire. 0 o dezactivează (implicit).", "label": "Prag Oprire Alimentare" }, "power_sensor": { - "doc": "Entitatea senzor care raportează puterea activă în wați pentru acest aparat (de exemplu, sensor.washer_power). Detectarea tuturor ciclurilor se bazează pe acest semnal.", + "doc": "Senzorul care raportează puterea curentă a acestui aparat în wați (de ex. sensor.washer_power). Toată detectarea se bazează pe el.", "label": "Senzor de Putere" }, - "profile_duration_tolerance": { - "doc": "Banda +/- din jurul unei durate medii de profil utilizată în timpul potrivirii. 0,25 înseamnă că un profil de 60 de minute se potrivește cu cicluri de 45-75 de minute.", - "label": "Toleranță Durată Profil" - }, "profile_match_interval": { - "doc": "Cât de des să încercați potrivirea profilului în timpul unui ciclu de rulare. Implicit 300 s (5 minute) echilibrează viteza de detectare și CPU.", + "doc": "Cât de des este comparat un ciclu în desfășurare cu profilurile. Implicit 300 s (5 min).", "label": "Interval de Potrivire" }, "profile_match_max_duration_ratio": { - "doc": "Lungimea maximă a ciclului în raport cu profilul. 1.3 înseamnă că un ciclu trebuie să fie sub 130% din durata profilului pentru a se potrivi.", + "doc": "Un program este eliminat după ce ciclul depășește acest multiplu al duratei sale obișnuite. 1,8: un ciclu cu 80% mai lung decât programul nu se mai potrivește cu el.", "label": "Raport Durată Maximă" }, "profile_match_min_duration_ratio": { - "doc": "Lungimea minimă a ciclului în raport cu profilul. 0,9 înseamnă că un ciclu trebuie să fie de cel puțin 90% din durata profilului pentru a se potrivi.", + "doc": "Un program este luat în calcul doar după ce ciclul a atins această fracțiune din durata sa obișnuită. 0,10: un program de 2 ore devine candidat după 12 minute. Nu se aplică în primele 15 minute ale unui ciclu.", "label": "Raport Durată Minimă" }, "profile_match_threshold": { - "doc": "Scorul minim de similaritate (0-1) necesar la sfârșitul ciclului pentru a accepta identificarea unui program. Ridicați-l pentru a reduce identificările greșite; reduceți-l dacă programele aparatului dvs. nu sunt potrivite. Implicit 0,4 este un punct de plecare conservator.", + "doc": "Scorul de potrivire în timp real de care are nevoie un ciclu înainte ca Terminarea inteligentă sau finalul anti-șifonare să îl poată încheia mai devreme. Nu schimbă programul afișat. Creșteți-l dacă potrivirile slabe încheie ciclurile prea devreme; scădeți-l dacă potrivirile clare așteaptă întreaga întârziere la oprire.", "label": "Prag de Potrivire" }, "profile_unmatch_threshold": { - "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.", + "doc": "Dacă scorul de potrivire în timp real scade sub această valoare, presupunerea curentă de program este ștearsă. Păstrați-l puțin sub Pragul de Potrivire, ca o scădere scurtă să nu o reseteze.", "label": "Prag de Nepotrivire" }, "progress_reset_delay": { - "doc": "După terminare, mențineți progresul la 100% pentru acest timp, astfel încât Finalizat să fie vizibil pe tablourile de bord înainte de a reveni la Inactiv.", + "doc": "După un ciclu, păstrați progresul la 100% atât timp înainte de revenirea la Inactiv.", "label": "Întârziere Resetare Progres" }, "pump_stuck_duration": { - "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ă).", + "doc": "Cât timp poate funcționa o pompă fără oprire înainte de declanșarea evenimentului de pompă blocată.", "label": "Durata Pompei Blocate" }, "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.", + "doc": "Timpul minim între citirile procesate. Citirile care sosesc mai devreme sunt sărite (citirile scăzute în timpul unui ciclu sunt păstrate mereu). Setați-l la intervalul de raportare al prizei dvs. sau mai jos; o valoare mai mare doar aruncă citiri.", "label": "Interval de Eșantionare" }, "save_debug_traces": { - "doc": "Stocați urmărirea completă a puterii și datele de depanare corespunzătoare pentru fiecare ciclu. Util pentru depanare, dar mărește dimensiunea de stocare.", + "doc": "Salvați și datele de depanare ale potrivirii (scorurile fiecărui candidat) cu fiecare ciclu, pentru depanare. Adaugă aproximativ 20 KB pe ciclu.", "label": "Salvați Urmele de Depanare" }, "smart_termination_duration_ratio": { - "doc": "Cât de mult trebuie să fi avansat un ciclu în durata așteptată a programului corespunzător înainte ca Terminarea inteligentă să îl poată încheia mai devreme odată ce puterea scade. Durata așteptată este media programului, așa că pe aparatele al căror timp de funcționare variază mult - mașini de spălat în funcție de apa de alimentare rece iarna sau caldă vara, uscătoare cu senzor de umiditate, programe care depind de încărcătură - aproximativ jumătate dintre rulări se termină mai devreme decât acea medie și nu beneficiază niciodată de încheierea rapidă, terminându-se doar prin expirarea de rezervă cu minute întârziere. Reduceți această valoare (de ex. 0,85) pe acele mașini pentru ca încheierea anticipată să se declanșeze totuși; măriți-o spre 1,0 pentru a fi mai conservator. Lăsați gol pentru valoarea implicită (0,98 sau 0,99 pentru mașinile de spălat vase). Poate doar să încheie un ciclu mai devreme, niciodată mai târziu, și nu se declanșează niciodată la o potrivire ambiguă sau cu încredere scăzută.", + "doc": "Fracțiunea din durata medie a programului pe care un ciclu trebuie să o atingă înainte ca Terminarea inteligentă să îl poată încheia mai devreme. Dacă duratele variază (temperatura apei, uscare cu senzor, mărimea încărcăturii), reduceți-o (de ex. 0,85). Lăsați gol pentru 0,98 (0,99 la mașinile de spălat vase).", "label": "Raport de terminare inteligentă" }, - "smoothing_window": { - "doc": "Cât de mult este netezit semnalul de putere brută. Scăzut (2) este receptiv, dar zgomotos; ridicat (5) netezește vârfurile, dar adaugă lag.", - "label": "Fereastră de Netezire" - }, "start_duration_threshold": { - "doc": "Puterea trebuie să rămână peste pragul de pornire atât de mult pentru a confirma o pornire reală, împiedicând comutatoarele de pornire/oprire în fracțiuni de secundă să înceapă un ciclu.", + "doc": "Puterea trebuie să rămână peste Pragul de Pornire atât timp înainte ca un ciclu să înceapă, ca scurtele clipiri pornit/oprit să fie ignorate.", "label": "Durata de Pornire" }, "start_energy_threshold": { - "doc": "Energia (putere x timp) pe care trebuie să o consume aparatul înainte de FUNCȚIONARE. Un vârf scurt de mare putere are energie foarte scăzută și este ignorat, prevenind pornirile false.", + "doc": "Energia pe care aparatul trebuie să o consume înainte ca ciclul să fie considerat în desfășurare. Un vârf scurt de putere consumă puțină energie, așa că nu pornește un ciclu.", "label": "Energia de Pornire" }, "start_threshold_w": { - "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.", + "doc": "Puterea trebuie să urce peste această valoare pentru a porni un ciclu. Prea mic: consumul în standby produce porniri false. Prea mare: programele cu pornire lentă (umplere la rece) sunt ratate.", "label": "Prag de Pornire" }, "stop_threshold_w": { - "doc": "Puterea trebuie să scadă sub acest nivel înainte să înceapă numărătoarea inversă pentru întârzierea opririi. Setați-l sub pragul de pornire - decalajul dintre ele este banda de histerezis care previne pâlpâirea. Dacă se setează prea mult, fazele de putere scăzută (reținere de clătire, anti-încredere) declanșează în mod fals secvența de final.", + "doc": "Puterea trebuie să scadă sub această valoare înainte ca întârzierea la oprire să înceapă să numere. Păstrați-l sub Pragul de Pornire. Dacă e prea mare, fazele cu putere redusă (pauze de clătire, anti-șifonare) pornesc din greșeală numărătoarea inversă de final.", "label": "Prag de Oprire" }, "switch_entity": { - "doc": "Comutator opțional dezactivat în pauză și reactivat la reluare, utilizat împreună cu „Pauză întrerupe și alimentarea”.", + "doc": "Comutator opțional care se oprește la pauză și repornește la reluare, împreună cu „Pauza Întrerupe Alimentarea”.", "label": "Entitate Switch" }, "watchdog_interval": { - "doc": "Cât de des verifică monitorul de fundal pentru senzori blocați și timeout-uri scurse. Implicit 30 s.", + "doc": "Cât de des verificarea din fundal caută senzori blocați și timeout-uri expirate. Implicit 30 s.", "label": "Interval Watchdog" }, "notify_milestone_message": { @@ -1477,127 +1453,82 @@ "label": "Mesaj de etapă importantă" }, "notify_milestones": { - "doc": "Numere de cicluri separate prin virgulă care declanșează o notificare unică de sărbătorire când sunt atinse (de exemplu, 50, 100, 500, 1000). Necompletat dezactivează notificările de etapă.", + "doc": "Numere de cicluri care declanșează o notificare unică de felicitare, separate prin virgulă (de ex. 50, 100, 500, 1000). Gol le dezactivează.", "label": "Etape de cicluri" }, "notify_quiet_end_hour": { - "doc": "Sfârșitul intervalului nu deranjați (0-23). Notificările reținute sunt livrate la această oră. Lăsați necompletat pentru a dezactiva.", + "doc": "Sfârșitul intervalului Nu deranjați (0-23); notificările reținute sunt trimise atunci. Gol îl dezactivează.", "label": "Sfârșitul orelor de liniște" }, "notify_quiet_start_hour": { - "doc": "Începutul unui interval nu deranjați (0-23). Notificările de finalizare, memento și rufe curate care s-ar declanșa în timpul orelor de liniște sunt reținute și livrate când se încheie intervalul. Lăsați necompletat pentru a dezactiva. Acceptă intervale care trec de miezul nopții (de exemplu, început 22, sfârșit 7).", + "doc": "Începutul unui interval Nu deranjați (0-23). Notificările de final, memento și rufe curate din acest interval sunt reținute până la sfârșitul lui. Poate trece peste miezul nopții (de ex. 22 la 7). Gol îl dezactivează.", "label": "Începutul orelor de liniște" }, "peak_rate_message": { - "doc": "Text personalizat opțional pentru sfatul privind tariful de vârf, adăugat la notificarea de pornire. Variabile de șablon: {device}, {price}. Necompletat folosește valoarea implicită.", + "doc": "Text propriu pentru sfatul de tarif de vârf. Variabile de șablon: {device}, {price}. Gol folosește textul implicit.", "label": "Mesaj tarif de vârf" }, "peak_rate_threshold": { - "doc": "Când începe un ciclu și prețul curent pe kWh este la această valoare sau peste, se adaugă un sfat privind tariful de vârf la notificarea de pornire. 0 sau necompletat dezactivează sfatul.", + "doc": "Dacă prețul pe kWh este egal cu sau peste această valoare la pornirea unui ciclu, notificarea de pornire primește un sfat de tarif de vârf. 0 sau gol îl dezactivează.", "label": "Prag tarif de vârf (pe kWh)" }, - "dtw_bandwidth": { - "label": "Toleranța potrivirii de formă", - "doc": "Cât de mult poate deforma în timp sistemul de potrivire un ciclu pentru a-l adapta la forma unui program salvat. O valoare mai mare tolerează variații de viteză mai mari; prea mare riscă să potrivească programul greșit." - }, - "corr_weight": { - "label": "Ponderea formă / nivel", - "doc": "Echilibrul dintre forma curbei și nivelul absolut de putere la evaluarea unei potriviri. O valoare mai mare favorizează forma; una mai mică favorizează potrivirea nivelului de putere." - }, - "duration_weight": { - "label": "Ponderea duratei", - "doc": "Cât de puternic este preferat un program a cărui durată obișnuită corespunde acestui ciclu." - }, - "energy_weight": { - "label": "Ponderea energiei", - "doc": "Cât de puternic este preferat un program al cărui consum obișnuit de energie corespunde acestui ciclu." - }, "store_brand": { "label": "Marca aparatului", - "doc": "Opțional. Marca aparatului, aleasă din catalogul comunității. Folosită pentru a găsi și partaja înregistrări de referință corespunzătoare. Lăsați necompletat dacă nu folosiți funcțiile online." + "doc": "Opțional. Marca aparatului dvs. din catalogul comunității, pentru a găsi și partaja înregistrări de referință. Lăsați gol dacă nu folosiți funcțiile online." }, "store_model": { "label": "Modelul aparatului", - "doc": "Opțional. Modelul aparatului, ales din catalogul comunității după ce este stabilită marca. Dacă modelul dvs. nu este listat, îl puteți adăuga în catalog." + "doc": "Opțional. Modelul aparatului dvs. din catalogul comunității (alegeți mai întâi o marcă). Dacă nu este în listă, îl puteți adăuga." }, "show_contributor": { "doc": "Afișați atribuirea \"de \" pe aparatele și ciclurile de referință ale comunității." }, - "enable_phase_matching": { - "label": "Timp rămas în funcție de faze", - "doc": "Împarte fiecare ciclu în curs în faze (încălzire, spălare, centrifugare) și repartizează timpul rămas pe fiecare fază, combinat cu estimarea clasică - bazându-se pe repartiția pe faze la începutul ciclului și pe estimarea clasică spre final. Acest lucru personalizează numărătoarea inversă în funcție de cât de mult încălzește și funcționează efectiv mașina, ceea ce se observă cel mai mult în prima jumătate a unui ciclu. Dezactivat = doar estimarea clasică. Este afectată doar afișarea timpului rămas; potrivirea programelor și detectarea ciclurilor rămân neschimbate." - }, - "keep_min_score": { - "label": "Scor Min. de Potrivire", - "doc": "Scorul minim de similaritate de care are nevoie un candidat pentru a rămâne în cursa de potrivire. Implicit 0.1 este deliberat permisiv - admite chiar și candidați slabi, lăsând etapele ulterioare să găsească cea mai bună potrivire. Măriți pentru a elimina mai devreme profilurile improbabile; micșorați pentru a lărgi grupul inițial de candidați." - }, - "dtw_blend": { - "label": "Amestec Deformare", - "doc": "Cât din scorul de aliniere prin deformare temporală (DTW) înlocuiește scorul de bază al Etapei 2. 0 = utilizați doar scorul Etapei 2, 1 = utilizați doar scorul DTW, 0.5 (implicit) = amestec egal. Măriți pentru a vă baza mai mult pe alinierea prin deformare când programele au niveluri de putere similare dar tipare de sincronizare diferite." - }, - "dtw_ensemble_w": { - "label": "Mix Ansamblu Deformare", - "doc": "În modul ansamblu DTW (implicit), ponderea pe L1 scalat față de DTW derivativ (DDTW). 1.0 = tot scalat-L1, 0 = tot DDTW, 0.7 = implicit. Varianta scalat-L1 compară nivelurile de putere; varianta derivativă reacționează la modul în care puterea se schimbă în timp. Ansamblul combină ambele." - }, - "dtw_ddtw_scale": { - "label": "Scară Deformare Derivativă", - "doc": "Distanța de semi-saturație pentru scorul DTW derivativ. Scorul se înjumătățește când distanța DDTW egalează această valoare. Mai mic = mai sensibil la diferențele de formă. Implicit 30 este calibrat pentru urme tipice de putere ale aparatelor. Afectează doar modurile DTW ansamblu și ddtw." - }, - "dtw_refine_top_n": { - "label": "Contor Rafinare DTW", - "doc": "Câți candidați de top ai Etapei 2 sunt rescorați de DTW. DTW este mai costisitor computațional, deci este aplicat doar la primii N candidați. Implicit 5. Măriți (la 7-9) dacă profilul corect ajunge uneori doar pe locul 4 sau 5 după Etapa 2 - DTW îl poate recupera. Micșorarea accelerează ușor potrivirea." - }, - "duration_scale": { - "label": "Precizie Durată", - "doc": "Rigoarea penalizării acordului de durată a Etapei 4. Acesta este raportul logaritmic la care scorul de acord se înjumătățește. Mai mic = mai strict: o nepotrivire de durată afectează mai mult. Implicit 0.175 corespunde unei toleranțe de durată de aproximativ 18% la jumătate de pondere. Utilizați împreună cu Ponderea Duratei." - }, - "energy_scale": { - "label": "Precizie Energie", - "doc": "Rigoarea penalizării acordului de energie a Etapei 4. Mai mic = mai strict: o nepotrivire de energie afectează mai mult. Implicit 0.25 este intenționat mai permisiv decât Rigoarea Duratei deoarece energia variază în funcție de sarcină. Utilizați împreună cu Ponderea Energiei." - }, "power_profile_interval_min": { "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." + "doc": "Mărimea intervalelor atributului power_profile (wați medii pe interval) folosit de planificatoare precum EMHASS și tibber_prices. Mai mic păstrează vârfurile scurte; mai mare netezește curba. Implicit 15 min. 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." + "doc": "Doar iOS. Actualizați progresul în timp real în liniște, cu o prioritate care economisește bateria și pe care iOS o poate grupa. Prima actualizare și notificările de pornire și final alertează în continuare. Oprit: fiecare actualizare alertează." }, "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." + "doc": "Doar Android. Păstrați notificarea de progres în timp real la atingere. Oprit: o atingere o închide." }, "notify_live_click_action": { "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." + "doc": "Ce deschide o atingere pe notificarea acestui aparat. Gol: panoul WashData pe acest aparat. Introduceți o cale de dashboard (de ex. /lovelace/laundry) sau un URL, ori „none” pentru nicio acțiune la atingere." }, "door_opens_at_end": { "label": "Ușa se deschide automat la final", - "doc": "Pentru mașinile de spălat vase care deschid ușa la sfârșitul ciclului pentru uscare (AirDry și similare). Cu această opțiune activată, deschiderea ușii în timpul unui ciclu activ nu îl mai pune în pauză indefinit; în schimb, dacă ușa rămâne deschisă pentru durata de așteptare de mai jos, WashData consideră ciclul terminat. O deschidere scurtă (pentru a adăuga un obiect) este ignorată. Necesită o entitate senzor de ușă." + "doc": "Pentru mașinile de spălat vase care deschid ușa la final pentru uscare (de ex. AirDry). O ușă lăsată deschisă pe durata de mai jos încheie ciclul în loc să îl pună pe pauză; o deschidere scurtă este ignorată. Necesită o Entitate Senzor Ușă." }, "door_end_dwell_seconds": { "label": "Durată de așteptare la deschiderea ușii", - "doc": "Cât timp trebuie să rămână ușa deschisă înainte ca WashData să termine ciclul, când opțiunea \"Ușa se deschide automat la final\" este activată. Destul de lungă pentru a ignora adăugarea rapidă a unui vas (implicit 60 s), destul de scurtă pentru a termina prompt odată ce mașina deschide ușa." + "doc": "Cu „Ușa se deschide automat la final” activat: cât timp trebuie să stea ușa deschisă înainte ca ciclul să se încheie. Suficient cât să ignore adăugarea unei farfurii (implicit 60 s)." }, "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ă." + "doc": "Ce tipuri de cicluri modelează curba și potrivirea fiecărui program. Debifarea unui tip nu șterge nimic; util dacă nu aveți încredere în datele importate. Statisticile numără mereu doar ciclurile acestei mașini. Debifarea tuturor este ignorată." }, "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." + "doc": "Taxați fiecare ciclu la prețul din fiecare moment în care a funcționat, în loc de prețul de la final. Necesită o entitate de preț. Procesare istoric (Diagnostice) recalculează ciclurile mai vechi." }, "unload_confirm_entity": { "label": "Entitate Confirmare Descărcare", - "doc": "Entitate opțională a cărei activare înseamnă că încărcătura a fost scoasă, pentru mașinile care nu pot avea un senzor de ușă. Merge orice tip: un buton Zigbee sau NFC, un ajutor input_button, un senzor de mișcare în fața mașinii, o scenă. Orice schimbare de stare contează ca o confirmare, cu excepția opririi și a trecerii în indisponibil. Setarea acesteia activează și mementoul de descărcare pe un dispozitiv fără Entitate Senzor Ușă." + "doc": "Opțional. Orice entitate care confirmă că încărcătura a fost scoasă, pentru mașinile fără senzor de ușă (un buton, input_button, senzor de mișcare, scenă). Contează orice schimbare de stare, cu excepția opririi sau a trecerii în indisponibil. Activează și mementoul de descărcare." }, "unload_track_without_door": { "label": "Confirmă Descărcarea Manual", - "doc": "Rulează mementoul de descărcare pe un dispozitiv fără Entitate Senzor Ușă și fără Entitate Confirmare Descărcare, iar descărcarea o confirmi din propria automatizare: apasă entitatea buton \"Marcați ca descărcat\" sau apelează serviciul ha_washdata.mark_unloaded. Se ignoră când este setată o Entitate Senzor Ușă, care oferă deja semnalul." + "doc": "Folosiți mementoul de descărcare fără senzor de ușă sau entitate de confirmare. Confirmați descărcarea din propria automatizare cu butonul „Marcați ca descărcat” sau cu serviciul ha_washdata.mark_unloaded. Ignorat când este setată o Entitate Senzor Ușă." }, "notify_icon_color": { "label": "Culoare Notificare", - "doc": "Culoare de accent pentru notificările acestui aparat, astfel încât o mașină de spălat rufe, un uscător și o mașină de spălat vase să poată fi deosebite dintr-o privire. iOS colorează pictograma și bara de progres a Activității live de pe ecranul blocat și din Dynamic Island. Android primește aceeași culoare, dar ce face cu ea depinde de telefon: unele colorează pictograma, iar multe (în special Samsung One UI) o ignoră complet. Formatul hexazecimal (de exemplu, #03A9F4) este înțeles de ambele. Lăsați necompletat pentru valoarea prestabilită a platformei." + "doc": "Culoare de accent pentru notificările acestui aparat, ca mașina de spălat, uscătorul și mașina de spălat vase să se deosebească ușor. Pe iOS umple cercul din spatele pictogramei și colorează bara Live Activity. Multe telefoane Android (mai ales Samsung) o ignoră. Folosiți hex, de ex. #03A9F4. Gol: implicitul platformei." + }, + "share_name": { + "doc": "Afișați numele contului dvs. ca autor al ciclurilor pe care le partajați. Oprit: partajate fără nume." } }, "setting_group": { @@ -1625,9 +1556,6 @@ "door_pause": { "label": "Ușă și Pauză" }, - "dtw_refinement": { - "label": "Rafinare DTW" - }, "duration_gates": { "label": "Porți Durată" }, @@ -1704,13 +1632,18 @@ "rebuilding": "Se reconstruiește…", "analyzing": "Se analizează…", "resetting": "Se resetează…", - "reverting": "Se revine…", "trimming": "Se taie…", "splitting": "Se divide…", + "stalled": "Blocat", "deleting": "Se șterge…", "imported": "Importat", "preparing": "Se pregătește…", - "unlabelling": "Se elimină eticheta…" + "unlabelling": "Se elimină eticheta…", + "working": "Se procesează…", + "match_uncertain": "Incert: {top} sau {runner}", + "match_uncertain_single": "Incert: poate {top}", + "match_sure": "~{pct}% certitudine", + "match_uncertain_tip": "WashData nu s-a decis încă asupra unui program. Cifra arată cât de des varianta din frunte, {top}, a fost corectă în ciclurile anterioare când conducea la o asemenea diferență. Doar informativ." }, "tab": { "advanced": "Avansat", @@ -1740,6 +1673,11 @@ "wrong_profile": "Profil greșit" }, "toast": { + "task_name_required": "Dați un nume sarcinii", + "task_added": "Sarcină adăugată", + "task_save_failed": "Sarcina nu a putut fi salvată: {error}", + "task_removed": "Sarcină eliminată", + "maint_logged": "Marcat ca efectuat: {task}", "catalog_refreshed": "Catalogul comunității a fost actualizat", "access_saved": "Controlul accesului a fost salvat", "all_wiped": "Toate datele au fost șterse", @@ -1747,7 +1685,6 @@ "auto_label_complete": "Etichetare automată finalizată", "automation_deleted": "Automatizare ștearsă", "correction_submitted": "Corecție trimisă", - "cycle_deleted": "Ciclu șters", "cycle_force_stopped": "Ciclu oprit forțat", "cycle_labelled": "Ciclu etichetat", "cycle_paused": "Ciclu în pauză", @@ -1763,11 +1700,9 @@ "group_deleted": "Grup șters", "group_name_required": "Numele grupului este obligatoriu", "group_saved": "Grup salvat", - "import_successful": "Import reușit; integrarea se reîncarcă", "json_required": "Datele JSON sunt obligatorii", "legacy_removed": "Acțiuni vechi eliminate", "logs_exported": "Jurnale exportate", - "matching_reverted": "Ponderile de potrivire revenite la valorile implicite", "min_2_profiles": "Selectați cel puțin 2 profiluri pentru un grup", "ml_training_no_improvement": "Antrenament finalizat: baza menținută (fără îmbunătățire)", "models_reverted": "Modelele de pe dispozitiv au revenit la baza inițială", @@ -1777,8 +1712,8 @@ "pg_preset_loaded": "Presetarea \"{name}\" a fost încărcată", "pg_preset_saved": "Presetarea \"{name}\" a fost salvată", "pg_sugg_loaded": "{n} valoare/valori sugerate incarcate - rulați Zona de test pentru comparație", - "pg_sugg_ml_loaded": "{n} valoare/valori calibrate ML incarcate - rulați Zona de test pentru comparație", "phase_name_required": "Numele fazei este obligatoriu", + "phase_reset": "Faza \"{name}\" a fost restabilită la versiunea integrată", "phase_updated": "Fază actualizată", "phases_saved": "Faze salvate", "preferences_saved": "Preferințe salvate", @@ -1825,15 +1760,11 @@ "store_name_required": "Introduceți un nume de profil", "store_import_failed": "Importul a eșuat: {error}", "store_imported": "Importat în {profile}", - "store_program_required": "Introduceți un nume de program", "store_no_appliance": "Stabiliți mai întâi marca și modelul aparatului dvs. în Setări.", "store_share_failed": "Partajarea a eșuat: {error}", "store_shared": "Partajat în magazinul comunității, în așteptarea examinării.", "appliance_added": "Aparat adăugat, în așteptarea aprobării", "contrib_device_changed": "Adăugat în magazinul comunității, dar dispozitivul selectat s-a schimbat în timp ce formularul era deschis. Selectați-l în Setări.", - "thanks_confirming": "Vă mulțumim pentru confirmare", - "device_approved": "Aprobat de comunitate", - "rating_saved": "Evaluarea calității a fost salvată", "brand_added": "Marcă adăugată, în așteptarea aprobării", "profile_added": "Profil adăugat, în așteptarea aprobării", "saved_except_conflicts": "Salvat. Remediați conflictele evidențiate pentru a salva restul.", @@ -1856,11 +1787,52 @@ "odometer_saved": "Totalul ciclurilor a fost salvat", "odometer_save_failed": "Nu s-a putut salva totalul: {error}", "store_pick_profile_share": "Alegeți un profil în care să partajați", - "remove_failed": "Eliminarea a eșuat: {error}" + "remove_failed": "Eliminarea a eșuat: {error}", + "analysis_complete": "Analiză finalizată, sugestii: {count}", + "analysis_failed": "Analiza a eșuat: {error}", + "auto_detect_failed": "Detectarea automată a eșuat: {error}", + "auto_label_failed": "Etichetarea automată a eșuat: {error}", + "could_not_load_cycle": "Ciclul nu a putut fi încărcat: {error}", + "create_failed": "Crearea a eșuat: {error}", + "cycles_deleted": "Cicluri șterse: {count}", + "debug_cleared": "Urme de depanare șterse: {count}", + "discard_failed": "Renunțarea a eșuat: {error}", + "error": "Eroare: {error}", + "export_failed": "Exportul a eșuat: {error}", + "import_failed": "Importul a eșuat: {error}", + "invalid_json": "\"{key}\" nu este un JSON valid; corectați-l sau goliți câmpul înainte de salvare.", + "label_failed": "Etichetarea a eșuat: {error}", + "ml_training_promoted": "Antrenare finalizată, modele promovate: {count}", + "pause_failed": "Punerea în pauză a eșuat: {error}", + "phase_created": "Faza \"{name}\" a fost creată", + "phase_deleted": "Faza \"{name}\" a fost ștearsă", + "profile_created": "Profilul \"{name}\" a fost creat", + "rename_failed": "Redenumirea a eșuat: {error}", + "save_failed": "Salvarea a eșuat: {error}", + "split_complete": "Împărțit în {count} cicluri", + "start_failed": "Pornirea a eșuat: {error}", + "stop_failed": "Oprirea a eșuat: {error}", + "update_failed": "Actualizarea a eșuat: {error}", + "store_download_skipped": "{n} înregistrare(i) omise: prea scurte, cu prea multe întreruperi sau deja pe dispozitivul dvs.", + "store_import_low_quality": "Această înregistrare este prea scurtă, are prea multe întreruperi sau este neverosimilă pentru a fi importată.", + "store_import_duplicate": "Această înregistrare este deja pe dispozitivul dvs.", + "device_approved": "Aprobat de comunitate", + "thanks_confirming": "Vă mulțumim pentru confirmare", + "rating_saved": "Evaluarea calității a fost salvată", + "store_download_partial": "{n} program(e) nu au putut fi descărcate; încercați din nou mai târziu.", + "import_no_restore_point": "Importat, dar nu s-a putut salva un punct de restaurare, așa că acest import nu poate fi anulat", + "import_undo_failed": "Importul nu a putut fi anulat: {error}", + "import_undone": "Import anulat: {profiles} profil(uri) și {cycles} ciclu(ri) restaurate", + "auto_label_cancelled": "Etichetarea automată a fost anulată după ce {count} de cicluri au fost etichetate.", + "auto_label_cancelled_one": "Etichetarea automată a fost anulată după ce {count} ciclu a fost etichetat.", + "auto_label_cancelled_few": "Etichetarea automată a fost anulată după ce {count} cicluri au fost etichetate.", + "auto_label_cancelled_other": "Etichetarea automată a fost anulată după ce {count} de cicluri au fost etichetate.", + "auto_label_done": "Etichetare automată finalizată: {count} de cicluri etichetate, neetichetate: {skipped}.", + "auto_label_done_one": "Etichetare automată finalizată: {count} ciclu etichetat, neetichetate: {skipped}.", + "auto_label_done_few": "Etichetare automată finalizată: {count} cicluri etichetate, neetichetate: {skipped}.", + "auto_label_done_other": "Etichetare automată finalizată: {count} de cicluri etichetate, neetichetate: {skipped}." }, "suggestion": { - "both_agree": "WashData recomandă", - "calibrated_label": "Calibrat", "observed_label": "Observat", "impact": { "auto_label_confidence": { @@ -1875,10 +1847,6 @@ "higher": "Închide doar ciclurile care au consumat energie substanțială", "lower": "Închide și cicluri scurte sau cu consum redus de energie" }, - "end_repeat_count": { - "higher": "Necesită repetarea semnalului de final de mai multe ori – mai robust, puțin mai lent", - "lower": "Se termină după mai puține repetări – detectare mai rapidă, risc mai mare de final fals" - }, "learning_confidence": { "higher": "Învață doar din potriviri foarte sigure – mai lent dar mai fiabil", "lower": "Învață din mai multe cicluri; modelul poate fi mai puțin precis" @@ -1917,40 +1885,25 @@ } }, "reason": { - "auto_label_confidence": "Încrederea la percentila 15 a {cycles} etichete automate pe care utilizatorul nu le-a corectat niciodată ({p15}).", "completion_min_seconds": "Jumătate din durata p05 a ciclurilor curate ({minutes} min) pe {cycles} cicluri; filtrează ciclurile false.{excl}", - "dead_zone_batch": "Păstrat scurt (fereastra mediană de instabilitate la pornire pe {cycles} cicluri = {s}s), astfel încât un sfârșit real să fie detectat prompt.", - "dead_zone_from_dip": "Pe baza unei scăderi timpurii de putere detectate la {s}s.", - "end_energy_default": "Valoare de referință recomandată implicit pentru pragul de zgomot la sfârșitul ciclului.", - "end_energy_false": "Energia p95 de sfârșit fals ({p95}Wh) pe {cycles} cicluri, limitată inferior la 0.2% din energia mediană a ciclului ({floor}Wh).", - "end_energy_no_false": "Niciun sfârșit fals pe {cycles} cicluri; limitată inferior la 0.2% din energia mediană a ciclului ({floor}Wh).", - "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.", + "match_interval_floored": "Cel mai scurt program ({shortest}s) ar limita această valoare la {cap}s, sub minimul de {minimum}s, așa că se folosește minimul: {persistence} potriviri consecutive durează {budget}s, peste {pct}% din acel program. Potrivirea rulează doar când sosește o citire (mediana={median}s), deci 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.", "min_off_gap": "Pe baza a {gaps} intervale între cicluri observate (p05={p05}s). Limita inferioară a dispozitivului: {floor}s.", "min_off_gap_bridge": "Dimensionat pentru a acoperi cea mai lungă perioadă de liniște din interiorul unui ciclu: {span}s + 60s marjă, din {spans} intervale acoperite pe {cycles} cicluri curate (rămâne sub intervalul dvs. minim de {ceiling}s dintre două încărcături consecutive).", "min_power": "40% din cea mai mică putere activă p05 ({p05}W) pe {cycles} cicluri curate, menținând pragul de oprire sub consumul real.{excl}", - "ml_auto_label": "Cea mai mică încredere pe care modelul de calitate a considerat-o încă drept curată (p10 din {cycles} cicluri curate = {p10}).", - "ml_end_repeat": "{false}/{total} cicluri ({pct}%) au avut o pauză pe care detectorul de sfârșit a evaluat-o la >50%; necesită {val} confirmare(i) de sfârșit.", - "ml_off_delay": "Pauze confirmate de detectorul de sfârșit: p95 {p95}s + 60s marjă, din {pauses} pauze verificate de model pe {cycles} cicluri (limită inferioară {floor}s).", "no_update_timeout": "Pe baza cadenței de actualizare observate (p95={p95}s) * 20 (min. 60s).", "off_delay_cadence": "Pe baza cadenței de actualizare observate (p95={p95}s) * 5", "off_delay_device_floor": "S-a folosit minimul sigur specific dispozitivului pentru {device} ({floor}s).", "off_delay_generic_floor": "S-a folosit minimul sigur generic ({floor}s).", "off_delay_pauses": "Dimensionat pentru a depăși pauzele reale: pauza p95 din interiorul ciclului {p95}s + 60s marjă, din {pauses} pauze pe {cycles} cicluri curate (limită inferioară {floor}s).", - "profile_match_threshold": "Încrederea p10 a {cycles} etichete automate corecte ({p10}); prag inferior sigur de confirmare în timp real.", - "sampling_interval": "Intervalul median de actualizare observat pe {cycles} cicluri curate ({si}s).{excl}", - "smoothing_window": "Dimensionat pentru a netezi ~30s de citiri la eșantionare de {si}s ({samples} eșantioane).", - "start_duration": "Păstrat scurt (~un interval de eșantionare de {si}s), astfel încât detectarea să înceapă cât mai devreme posibil, ignorând totuși vârfurile de un singur eșantion.", - "thr_batch": "Păstrat chiar deasupra celei mai mici puteri active p05 pe {cycles} cicluri ({p05}W), astfel încât o pornire să fie surprinsă cât mai devreme posibil, iar pragul de oprire să rămână sub cea mai mică putere de funcționare a mașinii.", - "tol_per_profile": "p75 al varianței duratei per profil pe {profiles} profiluri ({cycles} cicluri); profilurile stabile nu sunt penalizate.", - "tol_pooled": "Pe baza varianței combinate a duratei a {cycles} cicluri recente etichetate (deviație p95={dev}).", - "watchdog": "Păstrat cât mai mic în siguranță (puțin peste intervalul de actualizare p95 de {p95}s și cel puțin 2x intervalul de eșantionare de {median}s, min. 30s), astfel încât blocajele să fie surprinse rapid fără opriri false." + "standby_floor": "Fiecare ciclu recent consuma încă {idle}W la final, deci un prag mai mic nu va putea detecta niciodată că aparatul se oprește. Menținut deasupra; cele {cycles} cicluri curate înregistrate nu au scăzut niciodată sub acest prag mai mult de {pause}s.", + "thr_batch": "Chiar peste cea mai mică putere activă (p05) din {cycles} cicluri ({p05}W), ca pornirile să fie prinse devreme și pragul de oprire să rămână sub cea mai mică putere de funcționare.", + "watchdog": "Păstrat cât mai mic în siguranță (puțin peste intervalul de actualizare p95 de {p95}s și cel puțin 2x intervalul de eșantionare de {median}s, min. 30s), astfel încât blocajele să fie surprinse rapid fără opriri false.", + "end_energy_floor": "Ridicat la minimul implicat de pragul de oprire ({stop} W) pe durata întârzierii la oprire ({off_delay} s); sub acesta, ciclul se poate încheia doar printr-o cale de rezervă.", + "max_duration_ratio_default": "Revenire la valoarea implicită: o limită superioară mai mică respinge programul corect ori de câte ori un ciclu durează mai mult decât de obicei.", + "min_duration_ratio_default": "Revenire la valoarea implicită: o limită inferioară mai mare împiedică recunoașterea unui ciclu în desfășurare până când acesta este bine avansat." }, "exclusions": { "summary": "Excluse {total} cicluri detectate greșit: {parts}.", @@ -1968,13 +1921,18 @@ "no_power": "fără putere", "no_active_power": "fără putere activă" } - } + }, + "now_value": "(acum {v})" }, "trend": { "down": "Tendință descendentă", "up": "Tendință ascendentă" }, "maint": { + "salt": "Reumplere sare", + "rinse_aid": "Reumplere agent de clătire", + "lint_filter": "Curățare filtru de scame", + "condenser_clean": "Curățare condensator", "bearing_service": "Service rulmenți", "descale": "Detartrare", "drum_clean": "Curățare tambur", @@ -1983,34 +1941,22 @@ }, "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ă", + "smart_termination_duration_ratio": "Fracțiunea din durata medie a programului pe care un ciclu trebuie să o atingă înainte ca Terminarea inteligentă să îl poată încheia mai devreme; reduceți-o dacă duratele variază", + "anti_crease_finalize_ratio": "Fracțiunea din durata medie a programului pe care un ciclu trebuie să o atingă înainte ca finalul anti-șifonare să îl poată încheia; reduceți-o la uscătoare, lăsați mașinile de spălat la 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_exit_power": "Nivelul considerat liniște între impulsurile de rotire; are efect doar peste Prag de Oprire", "anti_wrinkle_idle_timeout": "Timp de liniște permis între două impulsuri de rotire înainte ca modul anti-șifonare să se încheie", "anti_wrinkle_max_duration": "Un impuls mai lung decât acesta încheie modul anti-șifonare și deschide un ciclu nou", "anti_wrinkle_max_power": "Un impuls peste această valoare încheie modul anti-șifonare și deschide un ciclu nou", "completion_min_seconds": "Cea mai scurtă rulare care contează ca ciclu real", - "end_repeat_count": "Citiri scăzute consecutive înainte de încheiere", "interrupted_min_seconds": "Ciclurile scurte sunt marcate ca întrerupte", "min_off_gap": "Pauza necesară pentru a separa două cicluri", "off_delay": "Secunde de putere scăzută înainte de încheierea ciclului", "start_duration_threshold": "Secunde peste prag pentru a confirma pornirea", "start_threshold_w": "Wați minimi pentru a fi considerat pornit", "stop_threshold_w": "Sub această valoare, mașina este considerată oprită", - "dtw_bandwidth": "Etapa 3: banda de deformare Sakoe-Chiba (0 = DTW dezactivat; implicit 0.2 = 20% din lungimea ciclului)", - "corr_weight": "Etapa 2: echilibru între forma curbei (corelație) și nivelul de putere (MAE); implicit 0.45", - "duration_weight": "Etapa 4: cât de puternic influențează acordul de durată scorul final (implicit 0.22)", - "energy_weight": "Etapa 4: cât de puternic influențează acordul de energie scorul final (implicit 0.22)", - "profile_match_min_duration_ratio": "Etapa 1: lungimea minimă a ciclului ca fracție din durata profilului; implicit 0.1 (10%)", - "profile_match_max_duration_ratio": "Etapa 1: lungimea maximă a ciclului ca fracție din durata profilului; implicit 1.8 (180%)", - "keep_min_score": "Etapa 2: prag minim de scor pentru a rămâne în cursă; implicit 0.1 admite potriviri slabe, etapele ulterioare aleg cel mai bun", - "dtw_blend": "Etapa 3: 0 = doar scor de bază, 1 = doar scor DTW, 0.5 = amestec egal (implicit)", - "dtw_ensemble_w": "Etapa 3 (modul ansamblu): ponderea pe L1 scalat față de DTW derivativ; implicit 0.7 favorizează nivelul de putere", - "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)", + "profile_match_min_duration_ratio": "Lungimea minimă a ciclului ca fracție din durata profilului; implicit 0,1 (10%)", + "profile_match_max_duration_ratio": "Lungimea maximă a ciclului ca fracție din durata profilului; implicit 1,8 (180%)", "curve_preroll_seconds": "Secunde de citiri din încercări de pornire întrerupte incluse la începutul curbei unui ciclu; 0 = dezactivat" }, "col": { @@ -2018,12 +1964,14 @@ "status_tip": "Rezultatul ciclului: Finalizat (sfârșit natural), Întrerupt (scădere bruscă de putere), Oprit forțat (manual) sau Necesită revizuire (feedback în așteptare).", "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. 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.", + "energy_tip": "Energia totală consumată (kWh), calculată din putere în timp.", + "cost_tip": "Costul energiei pentru acest ciclu, fixat la final. Cu Cost Ponderat în Timp activat, fiecare moment este taxat la propriul preț; altfel la prețul de la final. Setați un preț în Setări pentru a completa aceasta.", "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.", - "cost_dynamic_tip": "Ponderat în timp: taxat la prețul în vigoare în timpul ciclului (preț efectiv {price} {cur}/kWh)." + "cost_dynamic_tip": "Ponderat în timp: taxat la prețul în vigoare în timpul ciclului (preț efectiv {price} {cur}/kWh).", + "dbg_conf": "Încr.", + "dbg_mae": "MAE", + "dbg_corr": "Corel." }, "stat": { "avg": "medie", @@ -2047,7 +1995,10 @@ "notify_finish": "Ar fi trimisă o notificare de finalizare.", "notify_milestone": "Ar fi trimisă o notificare de etapă.", "notify_held": "O notificare a fost reținută pentru orele de liniște.", - "finished": "Ciclul a ajuns într-o stare finală și s-a încheiat." + "finished": "Ciclul a ajuns într-o stare finală și s-a încheiat.", + "match_reverted": "Programul a revenit la detectare: scorul său a scăzut mult sub propriul vârf.", + "verified_pause": "Alinierea în timp real a confirmat (sau a eliberat) o pauză în cadrul programului. Cât timp aceasta se menține, ciclul nu este încheiat.", + "stalled": "Ciclul a stat la consumul de standby mai mult decât face vreodată pauză acest program acolo, cu lucru rămas. Afișat ca Pauză (Blocat); ciclul rămâne deschis." }, "store": { "search_ph2": "Căutați după marcă sau model…", @@ -2056,11 +2007,7 @@ "programs_count": "Programe: {n}", "browse": "Răsfoiți", "device": "Dispozitiv", - "favorites": "Favorite", - "search_ph": "Căutați după marcă sau model…", "no_results": "Nu s-au găsit aparate corespunzătoare. Încercați o altă căutare.", - "n_programs": "{n} programe", - "n_cycles": "{n} cicluri", "no_programs": "Încă nu există programe partajate pentru acest aparat.", "no_cycles": "Încă nu au fost partajate cicluri de referință pentru acest program.", "anon": "anonim", @@ -2072,19 +2019,21 @@ "merge_target": "Îmbinați în profilul", "description": "Descriere (opțional)", "contributed_by": "de {name}", + "website": "Site-ul magazinului ↗", + "website_tip": "Deschide site-ul magazinului comunității", + "favorites": "Favorite", "rating_summary": "★ {avg} ({n})", "no_ratings": "Încă nicio evaluare", - "website": "Site-ul magazinului ↗", - "website_tip": "Deschide site-ul magazinului comunității" + "unreachable": "Magazinul comunității nu a putut fi accesat. Verificați conexiunea și încercați din nou." }, "settings": { "gear": { - "title": "Setări" + "title": "Preferințe" } }, "link": { - "manual": "Manual ↗", - "add_appliance": "Adăugați aparatul dvs." + "maintenance_supporter": "Maintenance Supporter pe GitHub ↗", + "manual": "Manual ↗" }, "tip": { "add_brand": "Marca nu este listată? Adăugați-o în catalogul comunității", @@ -2118,7 +2067,13 @@ "rebuild": { "envelopes": "Se reconstruiesc anvelopele" }, - "cancelling": "Se anulează..." + "cancelling": "Se anulează...", + "auto_label": { + "running": "Se etichetează automat ciclurile" + }, + "store_download": { + "running": "Se descarcă din magazin" + } }, "setup": { "hdr": { @@ -2127,26 +2082,24 @@ }, "phase0": { "washer": "WashData detectează deja ciclurile dvs. Înregistrați primul ciclu pentru a activa numele programelor și estimările de timp.", - "dishwasher": "WashData monitorizează. Mașinile de spălat vase au cicluri complexe – se recomandă insistent înregistrarea primului ciclu. Dacă un ciclu detectat durează prea mult, folosiți editorul de cicluri pentru a-l tăia înainte de a-l salva ca profil.", + "dishwasher": "WashData urmărește. Ciclurile mașinilor de spălat vase sunt complexe, așa că înregistrați primul ciclu. Dacă un ciclu detectat durează prea mult, tăiați-l în editorul de cicluri înainte de a-l salva ca profil.", "generic": "WashData monitorizează. Înregistrați sau etichetați un ciclu detectat pentru a începe să creați profiluri." }, "phase1a": { - "labelled": "Început bun – primul dvs. program este salvat. Pentru date mai curate, luați în considerare înregistrarea următorului ciclu cu widgetul de înregistrare." + "labelled": "Început bun: primul dvs. program este salvat. Pentru date cât mai curate, înregistrați următorul ciclu cu Înregistrare manuală." }, "phase1b": { - "recorded": "Înregistrarea dvs. a fost salvată ca {profile_name}. Acum înregistrați sau etichetați celelalte programe obișnuite pentru a extinde acoperirea." + "recorded": "Înregistrarea dvs. a fost salvată ca {profile_name}. Acum înregistrați sau etichetați celelalte programe pe care le folosiți des." }, "phase1c": { - "verify": "Aveți {count} programe din comunitate. Rulați un ciclu pentru a verifica dacă WashData îl recunoaște corect – potrivirea se va îmbunătăți pe măsură ce dispozitivul își construiește propriul istoric." + "verify": "Aveți {count} programe de la comunitate. Rulați un ciclu pentru a verifica dacă WashData îl recunoaște; potrivirea se îmbunătățește pe măsură ce dispozitivul își construiește propriul istoric." }, "phase2": { - "cluster": "WashData a observat {count} cicluri care nu corespund niciunui program salvat – acestea se aseamănă între ele. Doriți să creați un profil nou pentru ele?", + "cluster": "WashData a văzut {count} cicluri similare care nu se potrivesc cu niciun program salvat. Creați un profil nou pentru ele?", "unmatched": "Ultimul dvs. ciclu nu a corespuns niciunui program salvat. Este un program nou?" }, "phase3": { - "suggestions": "WashData are recomandări de setări bazate pe istoricul ciclurilor – revizuiți-le pentru a îmbunătăți acuratețea detectării.", - "groups": "Unele profiluri ale dvs. seamănă cu același program la temperaturi diferite. Organizați-le într-un grup pentru o potrivire mai bună.", - "phases": "Adăugați faze de program la {profile_name} pentru estimări mai precise ale timpului rămas." + "suggestions": "WashData are sugestii de setări pe baza istoricului ciclurilor dvs. Verificați-le pentru a îmbunătăți detectarea." }, "phase4": { "healthy": "Acest dispozitiv este complet configurat ({profile_count} profiluri)." @@ -2159,8 +2112,6 @@ "create_from_cluster": "Creați profil din aceste cicluri", "create_profile": "Creați un profil pentru acesta", "review_suggestions": "Revizuiți sugestiile", - "organise_profiles": "Organizați profilurile", - "configure_phases": "Configurați fazele", "skip_step": "Omiteți acest pas", "skip_forever": "Nu mai afișa", "hide_guidance": "Ascundeți ghidul", @@ -2171,6 +2122,7 @@ "min": "min", "entity_not_found": "Entitate negăsită", "running_long": "Întârziere", + "stalled": "Blocat", "remaining": "rămas", "progress": "progres", "show_time_remaining": "Afișează timpul rămas", @@ -2219,5 +2171,53 @@ "off_delay": { "effective": "Un ciclu încheiat așteaptă de fapt {eff} s, nu {delay} s: Pauza Minimă de Oprire ({gap} s) este mai lungă și are prioritate." } + }, + "access": { + "edit": "Editare", + "full": "Complet", + "inherit": "Moștenire", + "none": "Niciunul (ascuns)", + "read": "Citire" + }, + "pg_label_reason": { + "no_winner": "Niciun program nu se potrivește", + "below_floor": "Sub Încredere Învățare", + "ambiguous": "Potrivire marcată ca ambiguă", + "margin": "Prea aproape de al doilea candidat", + "unknown_profile": "Programul nu mai există", + "not_evaluated": "Neevaluat" + }, + "diagram": { + "accum": "acumul.", + "auto": "auto", + "below_off": "sub = oprit", + "confirmed": "confirmat", + "energy_counts": "energie numărată", + "feedback": "revizuire", + "gap_below_min": "pauză sub minim = un singur ciclu", + "heat_phase": "fază de încălzire", + "held_at_100": "menținut la 100%", + "ignored": "ignorat", + "kept": "păstrat", + "max": "max", + "min": "min", + "minus_tol": "-tol", + "no_match": "nepotrivit", + "no_updates": "fără actualizări", + "off_delay_wait": "întârziere oprire", + "off_gap": "pauză", + "plus_tol": "+tol", + "profile": "profil", + "reading_interval": "interval tipic de citire", + "sensor_offline": "senzor offline: oprire forțată", + "si": "IE", + "spike_ignored": "vârf ignorat", + "spike_ignored_short": "vârf: ignorat", + "start": "pornire", + "stop": "oprire", + "tail_energy_resets": "energie finală > prag: timer resetat", + "thr": "prag", + "too_short": "prea scurt", + "tumble_kept": "rotiri păstrate" } } diff --git a/custom_components/ha_washdata/translations/panel/ru.json b/custom_components/ha_washdata/translations/panel/ru.json index 35d547a8..1c962e7f 100644 --- a/custom_components/ha_washdata/translations/panel/ru.json +++ b/custom_components/ha_washdata/translations/panel/ru.json @@ -3,6 +3,7 @@ "min": "мин", "entity_not_found": "Объект не найден", "running_long": "Работает долго", + "stalled": "Завис", "remaining": "осталось", "progress": "прогресс", "show_time_remaining": "Показывать оставшееся время", @@ -48,16 +49,18 @@ } }, "badge": { - "quiet_tail_tip": "Ближе к концу эта программа затихает примерно на {mins} мин, затем потребляет около {watts} Вт в течение {secs} с и завершается. Замечено в {seen}/{measured} измеренных циклов: прибор делает так не при каждом запуске.", + "quiet_tail_tip": "Ближе к концу эта программа затихает примерно на {mins} мин, затем потребляет около {watts} Вт в течение {secs} с. Замечено в {seen} из {measured} измеренных циклов.", "quiet_tail": "~{mins} мин тишины в конце", - "artifact_tip": "Обнаружено {n} аномалий (например, дверь открылась в середине цикла) – откройте, чтобы увидеть их на графике", + "artifact_tip": "Обнаружено {n} аномалии (например, дверь открылась в середине цикла) – откройте, чтобы увидеть их на графике", + "artifact_tip_one": "Обнаружена {n} аномалия (например, дверь открылась в середине цикла) – откройте, чтобы увидеть её на графике", + "artifact_tip_few": "Обнаружены {n} аномалии (например, дверь открылась в середине цикла) – откройте, чтобы увидеть их на графике", + "artifact_tip_many": "Обнаружено {n} аномалий (например, дверь открылась в середине цикла) – откройте, чтобы увидеть их на графике", + "artifact_tip_other": "Обнаружено {n} аномалии (например, дверь открылась в середине цикла) – откройте, чтобы увидеть их на графике", "auto": "(авто-определение)", "built_in_tag": "встроенный", "declining": "↘ снижается", "energy_low": "Энергопотребление ниже обычного", "energy_spike": "Энергопотребление выше обычного", - "fair_fit": "приемлемое качество", - "fair_fit_tip": "Умеренная согласованность совпадений – некоторые циклы, назначенные этому профилю, имеют более низкие оценки достоверности. Пометьте больше циклов или перезапишите профиль, чтобы повысить точность.", "feedback_requested": "Запрошен отзыв", "golden_cycle": "Записанный эталонный цикл", "duration_outlier": "Циклы необычной длительности: {n}", @@ -66,27 +69,40 @@ "needs_review": "Требуется проверка", "overrun": "Работал дольше обычного", "poor_fit": "низкое качество", - "poor_fit_tip": "Непоследовательная история совпадений – рассмотрите возможность восстановления этого профиля", - "restart_gap_tip": "{n} пропуск при перезапуске HA: в графике мощности есть разрыв", + "restart_gap_tip": "{n} пропуска при перезапуске HA: в графике мощности есть разрывы", + "restart_gap_tip_one": "{n} пропуск при перезапуске HA: в графике мощности есть разрыв", + "restart_gap_tip_few": "{n} пропуска при перезапуске HA: в графике мощности есть разрывы", + "restart_gap_tip_many": "{n} пропусков при перезапуске HA: в графике мощности есть разрывы", + "restart_gap_tip_other": "{n} пропуска при перезапуске HA: в графике мощности есть разрывы", "reviewed": "Рассмотрено", "steady": "→ стабильно", "underrun": "Завершился быстрее обычного", - "using_defaults": "Использует стандартные настройки", - "using_tuned": "Использует настроенные веса", "overrun_ratio": "({x}× от ожидаемого)", "underrun_ratio": "({pct}% от ожидаемого)", - "awaiting_tip": "Ожидает одобрения сообществом", - "awaiting_n": "Ожидает одобрения · {n} подтверждено", - "approved": "Одобрено", - "awaiting": "Ожидает одобрения", "imported_tip": "Импортировано из магазина сообщества. Используется только для сопоставления, не учитывается в статистике.", "unmatchable": "не сопоставляется", "not_importable": "недоступно", "exists": "существует", "backfilled_tip": "Обнаружено в импортированной истории мощности. Влияет только на сопоставление программ, не учитывается в статистике.", - "armed": "(применится к следующему циклу)" + "armed": "(применится к следующему циклу)", + "awaiting_tip": "Ожидает одобрения сообществом", + "awaiting_n": "Ожидает одобрения · {n} подтверждено", + "awaiting": "Ожидает одобрения", + "approved": "Одобрено", + "matcher_unused": "Пока не сопоставлено ни с одним вашим циклом", + "matcher_used": "Сопоставлено с {n} вашими циклами", + "matcher_used_one": "Сопоставлено с {n} вашим циклом", + "matcher_used_few": "Сопоставлено с {n} вашими циклами", + "matcher_used_many": "Сопоставлено с {n} вашими циклами", + "matcher_used_other": "Сопоставлено с {n} вашими циклами", + "matcher_used_tip": "Циклы на этом приборе, которые WashData сам сопоставил с этой импортированной программой. Импорт, который ни разу не сопоставился, может не подходить вашему прибору; его можно удалить. Считается локально; в магазин ничего не отправляется." }, "btn": { + "pg_preset_delete": "Удалить пресет", + "pg_preset_load": "Загрузить пресет", + "pg_preset_load_tip": "Заменить значения в песочнице этим пресетом (активные настройки не изменятся)", + "pg_preset_save": "Сохранить как пресет", + "pg_preset_save_tip": "Сохранить все значения ниже как именованный пресет для этого устройства", "reset_zoom": "Сбросить масштаб", "set_brand_model": "Указать бренд и модель", "refresh_catalog": "Обновить каталог", @@ -97,12 +113,11 @@ "add_timer": "+ Добавить таймер", "apply_all": "Применить всё", "apply_label": "Применить метку", - "apply_set_b": "Применить набор B", "apply_split": "Применить разбивку", "apply_trim": "Применить обрезку", "auto_detect_split": "Авто-определение", "auto_label_cycles": "Автоматически пометить циклы", - "auto_label_cycles_tip": "Автоматически назначить имена профилей не помеченным циклам, чья достоверность совпадения превышает порог", + "auto_label_cycles_tip": "Назвать непомеченные циклы, у которых уверенность сопоставления выше порога.", "cancel": "Отмена", "clear_debug": "Очистить отладочные данные", "clear_splits": "Очистить", @@ -112,8 +127,6 @@ "convert_to_automation": "Преобразовать в автоматизацию", "correct": "Исправить…", "create": "Создать", - "create_from_cluster": "Создать профиль из {count} циклов", - "create_profile": "+ Создать профиль", "delete": "Удалить", "delete_group": "Удалить группу", "delete_group_tip": "Удалить только эту группу – профили участников останутся", @@ -122,39 +135,30 @@ "discard_tip": "Отменить запись без сохранения", "dismiss": "Закрыть", "mute_suggestion": "Не предлагать эту настройку", - "pg_load_calibrated": "Загрузить откалиброванные (ML) ({n})", - "pg_load_calibrated_tip": "Загрузить откалиброванные ML настройки как переопределения в песочнице", "pg_load_live": "Загрузить активные настройки", "pg_load_live_tip": "Перечитать текущие настройки интеграции и отменить все изменения в песочнице", "pg_load_suggested": "Загрузить рекомендуемые ({n})", "pg_load_suggested_tip": "Загрузить текущие рекомендации автонастройщика как переопределения в песочнице", - "pg_preset_delete": "Удалить пресет", - "pg_preset_load": "Загрузить пресет", - "pg_preset_load_tip": "Заменить значения в песочнице этим пресетом (активные настройки не изменятся)", - "pg_preset_save": "Сохранить как пресет", - "pg_preset_save_tip": "Сохранить все значения ниже как именованный пресет для этого устройства", "pg_publish_all": "Сохранить в настройки ({n})", "pg_publish_one": "Сохранить «{label}» в настройки интеграции", "reset_muted": "Сбросить заглушённые", + "reset_phase_tip": "Отменить ваши изменения и восстановить встроенную фазу", "done": "Готово", "edit": "Редактировать", "export": "Экспортировать", - "export_json": "Экспорт в JSON", "force_stop": "Принудительная остановка", "force_stop_tip": "Немедленно завершить текущий цикл и пометить его как принудительно остановленный", "from_template": "Из шаблона ▾", "ignore": "Игнорировать", "import_json": "Импорт из JSON", - "import_overwrite": "Импорт (перезаписать данные)", "inspect": "Проверить", "label": "Пометить", - "load": "Загрузить", "load_more": "Загрузить ещё", "manage": "Управление", "merge": "Объединить", "new_automation": "+ Новая автоматизация", "new_group": "+ Новая группа", - "new_group_tip": "Сгруппировать почти одинаковые профили (одинаковая форма/продолжительность, разная температура или отжим), чтобы система сопоставления надёжно выбирала между ними", + "new_group_tip": "Сгруппировать почти одинаковые профили (та же форма, другая температура или отжим), чтобы сопоставление могло их различать.", "new_phase": "+ Новая фаза", "new_profile": "+ Новый профиль", "new_profile_tip": "Создать новый профиль программы из существующего помеченного цикла или записи", @@ -162,23 +166,19 @@ "on_cycle_started": "При запуске цикла", "pause_cycle": "Пауза", "pause_cycle_tip": "Приостановить текущий цикл – прибор продолжит работу с того места, где остановился", - "pg_apply_device": "Применить к устройству", - "pg_autofill": "Автозаполнение", - "play": "Воспроизвести", "process": "Обработать", "process_history": "Обработать сейчас", "process_recording": "Сохранить запись", "process_tip": "Сохранить запись как новый или существующий профиль", "rebuild": "Перестроить огибающие", "rebuild_envelope": "Перестроить огибающую", - "rebuild_tip": "Пересчитать ожидаемую огибающую мощности (полоса мин./макс.) для всех профилей по помеченным циклам – запустить после пометки новых циклов или исправления старых", + "rebuild_tip": "Пересобрать ожидаемую полосу мощности каждого профиля по его помеченным циклам. Запускайте после пометки или исправления циклов.", "rec_start_tip": "Начать запись потребления мощности прибора – запустить непосредственно перед началом цикла", "rec_stop": "Стоп", "rec_stop_tip": "Остановить запись и сохранить захваченные данные для просмотра", "record": "Начать запись", "refresh": "Обновить", "refresh_diag_tip": "Обновить статистику хранилища", - "refresh_logs_tip": "Обновить последние записи журнала", "refresh_settings_tip": "Обновить настройки с сервера", "relabel": "Изменить метку ({count})", "remove": "Удалить", @@ -186,7 +186,6 @@ "reset": "Сбросить", "reset_ml_models_tip": "Откажитесь от доработанных моделей и вернитесь к встроенным. WashData может переучить их позже.", "reset_to_builtin": "Сбросить до встроенных моделей", - "reset_to_defaults": "Сбросить до значений по умолчанию", "resume_cycle": "Возобновить цикл", "resume_cycle_tip": "Возобновить приостановленный цикл", "revert_settings": "Отменить изменения", @@ -199,8 +198,6 @@ "run_analysis": "🔍 Запустить анализ предложений", "run_analysis_tip": "Проанализируйте свои записанные циклы прямо сейчас и обновите предложения по настройке", "run_auto_label": "Запустить авто-пометку", - "run_both": "Запустить A и B", - "run_simulation": "Запустить симуляцию", "save": "Сохранить", "save_access_control": "Сохранить управление доступом", "save_group": "Сохранить группу", @@ -212,10 +209,8 @@ "save_settings": "Сохранить настройки", "select": "Выбрать", "select_all": "Выбрать все", - "select_last20": "Последние 20", "select_none": "Снять выбор", "show_only": "Показать только", - "skip_setup": "Пропустить настройку", "split": "Разбить", "stop": "Остановить", "submit_correction": "Отправить исправление", @@ -224,30 +219,24 @@ "trim_split": "Обрезать / Разбить", "undo": "Отменить", "unlabel_selected": "Снять метку с выбранных ({n})", - "unlabel_selected_tip": "Снять метку профиля с выбранных циклов. Они останутся в вашей истории, и их можно пометить снова позже - просто перестанут формировать этот профиль.", + "unlabel_selected_tip": "Снять метку с выбранных циклов. Они останутся в истории, но перестанут формировать этот профиль.", "use": "Использовать", "wipe_all": "Удалить все данные", "delete_selected": "Удалить выбранные ({n})", - "group_suggest": "Группа {n}: {members}", "pg_apply_best": "Применить лучшее", - "pg_apply_to_settings": "Сохранить в настройки", "pg_apply_to_settings_tip": "Скопировать значения, изменённые здесь, в активные настройки этого устройства", "import": "Импортировать", "disconnect": "Отключить", "connect_github": "Подключить GitHub", - "save_appliance": "Сохранить устройство", "share_to_store": "Поделиться в магазине", "share": "Поделиться", - "confirm_appliance": "Подтвердить это устройство", - "confirmed": "Подтверждено", "browse_community_setups": "Обзор конфигураций сообщества", "download_device": "Скачать конфигурацию устройства", "share_device": "Поделиться конфигурацией устройства", - "share_device_tip": "Поделиться программами и настройками этого устройства с сообществом", + "share_device_tip": "Поделиться этим прибором и его эталонными циклами в магазине сообщества, чтобы владельцы такой же машины могли их взять.", "share_n": "Поделиться циклами: {n}", "export_selected": "Экспорт (выбрать данные)", "export_all": "Быстрый экспорт всего", - "import_raw": "Расширенно: заменить всё из JSON", "download_export": "Скачать экспорт", "analyze_import": "Анализировать файл", "import_selected": "Импортировать выбранное", @@ -260,7 +249,15 @@ "save_total_cycles": "Сохранить общее количество", "clear": "Очистить", "add_brand": "Добавить бренд", - "add_appliance": "Добавить устройство" + "log_done_task": "Отметить выполненным: {task}", + "add_task": "Добавить задачу", + "add_appliance": "Добавить устройство", + "retry": "Повторить", + "confirm_appliance": "Подтвердить это устройство", + "confirmed": "Подтверждено", + "create_profile_from_gap": "Создать профиль", + "undo_import": "Отменить последний импорт", + "pg_rerun": "Обновить симуляцию" }, "note": { "off_delay": { @@ -268,10 +265,6 @@ } }, "conflict": { - "anti_wrinkle_exit": { - "exit": "Должен быть ниже Порога Остановки ({stop} Вт); иначе мощность выхода из режима защиты от морщин игнорируется", - "stop": "Должен быть выше Порога Мощности Выхода из Режима Защиты от Морщин ({exit} Вт)" - }, "anti_wrinkle_max": { "max": "Должен быть выше Порога Запуска ({start} Вт); иначе ограничение длительности защиты от морщин обходится", "start": "Должен быть ниже Макс. Мощности Защиты от Морщин ({max} Вт)" @@ -279,10 +272,10 @@ "attn_sub": "Исправьте конфликты перед сохранением", "attn_title": "Конфликты настроек: {n}", "price_entity": { - "power_sensor": "Это Датчик Мощности устройства, а не цена. Объект цены имеет приоритет над фиксированной ценой ниже, поэтому циклы считались бы по ваттам за кВт·ч - очистите это поле.", - "energy_sensor": "Это Счётчик Энергии устройства, а не цена. Объект цены имеет приоритет над фиксированной ценой ниже, поэтому циклы считались бы по показанию счётчика за кВт·ч - очистите это поле.", - "device_class": "Класс этого сенсора - {dc}, а не цена за кВт·ч. Объект цены имеет приоритет над фиксированной ценой ниже - очистите это поле или укажите сенсор тарифа.", - "unit": "Этот сенсор выдаёт значения в {unit}, а это не цена за кВт·ч. Объект цены имеет приоритет над фиксированной ценой ниже - очистите это поле или укажите сенсор тарифа." + "power_sensor": "Это Датчик Мощности устройства, а не цена. Циклы считались бы в ваттах за кВт·ч; очистите это поле.", + "energy_sensor": "Это Счётчик Энергии устройства, а не цена. Циклы считались бы по показанию счётчика за кВт·ч; очистите это поле.", + "device_class": "Этот датчик измеряет {dc}, а не цену за кВт·ч. Очистите это поле или выберите датчик тарифа.", + "unit": "Этот датчик выдаёт значения в {unit}, а не цену за кВт·ч. Очистите это поле или выберите датчик тарифа." }, "settings_banner": "Конфликты настроек: {n}. Проверьте выделенные разделы и исправьте их перед сохранением.", "settings_banner_btn": "К первому", @@ -317,8 +310,8 @@ "threshold": "Должен быть ниже Порога Остановки ({stop} Вт), чтобы вступить в действие" }, "pump_stuck": { - "duration": "Должен быть меньше Таймаута Без Обновления ({to} с), чтобы сигнал застрявшего насоса срабатывал раньше watchdog-а", - "timeout": "Должен превышать Длительность Застрявания Насоса ({ps} с), чтобы сигнал застрявшего насоса срабатывал до принудительной остановки цикла" + "duration": "Должно быть меньше Таймаута Без Обновления ({to} с), чтобы сигнал застрявшего насоса сработал первым", + "timeout": "Должно превышать Длительность Застревания Насоса ({ps} с), чтобы сигнал застрявшего насоса сработал первым" }, "start_dur": { "sampling": "Интервал Выборки превышает Длительность Запуска ({sdt} с); одиночные выбросы могут открыть цикл", @@ -334,10 +327,15 @@ "watchdog": { "interval": "Должен быть не менее 2x Интервала Выборки ({si} с)", "sampling": "Интервал Выборки должен быть не более половины Интервала Watchdog ({wi} с)" + }, + "end_energy": { + "energy": "Слишком строго для Порога Остановки ({w} Вт) в течение Задержки Выключения ({d} с); цикл может завершиться только резервным путём", + "stop": "Энергия Завершения ({e} Вт·ч за {d} с) допускает только {p} Вт" } }, "hdr": { - "ab_comparison": "Сравнение настроек A/B", + "custom_tasks": "Свои задачи", + "maintenance_supporter": "Нужно больше?", "access_control": "Управление доступом", "add_maintenance": "Добавить событие обслуживания", "automation_delete": "Удалить эту автоматизацию", @@ -346,28 +344,22 @@ "automation_review_this_cycle": "Проверить этот цикл", "automations": "Автоматизации", "clear_debug": "Очистить отладочные данные", - "cycle_simulator": "Симулятор циклов", "diagnostics_pref": "Диагностика", "display": "Отображение", - "dtw_inspector": "Инспектор DTW", "export_import": "Экспорт / Импорт", - "getting_started": "Начало работы", "live_power": "Текущая мощность", "logs": "Журналы", - "logs_diagnostics": "Диагностика", "maintenance": "Действия по обслуживанию", "maintenance_log": "Журнал обслуживания", "maintenance_reminders": "Напоминания об обслуживании", "manual_recording": "Ручная запись", - "manual_recording_tip": "Запустите цикл намеренно, пока WashData записывает потребление мощности. Нажмите «Запись» перед запуском прибора, «Стоп» по завершении, затем «Обработать», чтобы сохранить как именованный профиль.", + "manual_recording_tip": "Запишите цикл намеренно: нажмите «Начать запись» прямо перед запуском прибора и «Стоп» по завершении, затем «Обработать», чтобы сохранить его как профиль.", "ml_learned": "Что WashData узнал", - "ml_matching_tuning": "Точная настройка распознавания программ", "ml_settings_card": "Настройки", "ml_smart_learning": "Умное обучение", "my_preferences": "Мои настройки", "no_automations": "Пока нет автоматизаций, ссылающихся на это устройство.", "panel_settings": "Настройки панели", - "param_sweep": "Перебор параметра", "pg_settings_source": "Источник настроек", "phase_catalog": "Каталог фаз", "playground": "Песочница", @@ -376,11 +368,8 @@ "status": "Статус", "status_graph": "График статуса", "storage_stats": "Статистика хранилища", - "test_history": "Проверка на истории", "toggle_sidebar": "Переключить боковую панель Домашнего помощника", - "tools_and_data": "Инструменты и данные", "wipe_history": "Очистить историю", - "recommendations": "Рекомендации ({n})", "pg_detection_params": "Настройки обнаружения", "pg_outcome": "Результат симуляции", "pg_across_cycles": "По всем вашим циклам", @@ -388,7 +377,8 @@ "online_account": "Магазин сообщества и онлайн-функции", "import_power_history": "Импорт истории мощности", "service_status": "Состояние обслуживания", - "online_account_tab": "Онлайн и сообщество" + "online_account_tab": "Онлайн и сообщество", + "live_match_debug": "Отладка текущего сопоставления" }, "health": { "fair": "Приемлемое качество профиля", @@ -396,11 +386,16 @@ "poor": "⚠ Низкое качество профиля" }, "lbl": { - "envelope_position_tip": "Положение на собственной записанной кривой сопоставленной программы, {pct}%. Измеряется совмещением этого цикла с профилем, а не подсчётом времени, поэтому остаётся верным, когда запуск длиннее или короче обычного. Обновляется, пока прибор молчит, поэтому во время активной фазы может отставать.", + "envelope_position_tip": "Положение на записанной кривой сопоставленной программы: {pct}%. Определяется совмещением цикла с профилем, поэтому остаётся верным при более длинном или коротком прогоне. Обновляется, когда прибор затихает, поэтому может отставать.", "envelope_position": "кривая {pct}%", "aria_playground_chart3": "Интерактивный график мощности цикла: наведите, чтобы увидеть время/мощность, прокрутите или используйте два пальца для масштабирования, перетащите для панорамирования", "device_unresolved": "Недоступное устройство ({id})", "drag_to_resize": "Перетащите, чтобы изменить размер", + "task_name": "Задача", + "every_cycles": "Интервал (циклы)", + "every_days": "Интервал (дни)", + "new_task_name": "Новая задача", + "days_since_service": "{since} / {total} дней", "actions": "Действия", "activity": "Активность", "administrators": "Администраторы", @@ -408,10 +403,8 @@ "aria_cycle_chart": "Кривая мощности цикла", "aria_envelope_chart": "График огибающей мощности профиля", "aria_phase_chart": "График редактора фаз", - "aria_playground_chart": "Кривая мощности цикла с перетаскиваемыми порогами обнаружения", "aria_power_chart": "График потребления мощности", "aria_spaghetti_chart": "Наложенные кривые мощности циклов", - "aria_sweep_chart": "График результатов перебора параметров", "artifact_anomaly": "Аномалия", "artifact_high_power": "Высокая мощность", "artifact_interruption": "Прерывание", @@ -425,87 +418,63 @@ "cohesion_good": "сплочённость {pct}%", "cohesion_low": "⚠ низкая сплочённость {pct}%", "compare_profiles": "Сравнить с профилями", - "concurrency": "Параллельность", "confidence": "Достоверность", "confidence_threshold": "Порог достоверности", "correct_duration": "Исправить продолжительность (мин, необязательно)", "correct_profile": "Исправить профиль", - "correlation": "Корреляция", "cost": "Стоимость", "create_new_profile": "+ Создать новый профиль…", "current_phase": "Текущая фаза", - "curves": "Кривые", "cycle": "Цикл", "cycle_date_display": "Отображение даты цикла", - "cycle_health": "Качество цикла", - "cycle_index": "индекс цикла →", - "cycles": "Циклы", "cycles_count": "Циклы", "cycles_lc": "циклов", - "dashed": "пунктир", "date": "Дата", "debug_traces": "Отладочные данные", - "default": "По умолчанию", "default_access_level": "Уровень доступа по умолчанию для пользователей, не указанных ниже", "default_other": "По умолчанию (другие устройства)", "default_tab": "Вкладка по умолчанию при открытии панели", "font_size": "Размер шрифта панели", - "delta": "Δ", "description": "Описание", - "dtw_blend_w": "Вес смешивания", - "dtw_blended": "Смешанный балл", - "dtw_ddtw": "Балл DDTW", - "dtw_l1": "Балл L1", - "dtw_matched_profile": "(сопоставленный профиль цикла)", "duration": "Продолжительность", - "duration_agreement": "Совпадение по длительности", - "emphasis": "Акцент", "enable_access_control": "Включить контроль доступа для каждого пользователя", "energy": "Энергия", - "energy_agreement": "Совпадение по энергии", - "event_timeline": "Хронология событий", "event_type": "Тип события", "expected": "Ожидаемо", + "suspected_profile": "{name} (предположительно)", + "runner_up_profile": "{name} (второй вариант)", "expected_duration": "Ожидаемая продолжительность (мин)", "file_kb": "Файл (кБ)", - "final_score": "Итоговый балл", "from": "От", "gap_s": "Пауза (с)", "group_name": "Название группы", "head_trim": "Обрезка начала (с)", - "health": "Качество", "hide_tabs": "Скрыть вкладки для не-администраторов", - "in_use": "Используется", "json_data": "Данные JSON", "label": "Сопоставленная программа", "last": "Последние", "last_checked": "Последняя проверка:", "load_from_file": "Загрузить из файла", - "mae_score": "Балл MAE", "manual_duration": "Ручная продолжительность (мин, необязательно)", "match": "Совпадение", "members": "Участники", - "metric": "Метрика", "mode_existing_profile": "Добавить к существующему профилю", "mode_new_profile": "Создать новый профиль", "models_fine_tuned": "(точно настроенные модели: {count})", - "n_classic_suggestions": "{n} классических", - "n_ml_suggestions": "{n} ML", "n_selected": "{n} выбрано", - "n_tuning_suggestions": "{n} предложение по настройке", - "na": "н/д", + "n_tuning_suggestions": "{n} предложения по настройке", + "n_tuning_suggestions_one": "{n} предложение по настройке", + "n_tuning_suggestions_few": "{n} предложения по настройке", + "n_tuning_suggestions_many": "{n} предложений по настройке", + "n_tuning_suggestions_other": "{n} предложения по настройке", "new_profile_name": "Новое имя профиля", "notes": "Заметки", - "of": "из", - "outcome": "Результат", "panel_default_tab": "Вкладка по умолчанию", "panel_language": "Язык панели", "per_cycle_short": "цикл", - "pg_autofill_tip": "Автозаполнить диапазон из текущего значения ({lo}–{hi})", "pg_best_value": "Найдено лучшее значение", "pg_current_value": "Текущее значение:", "pg_drag_hint": "Перетащите линию на графике", - "pg_dtw": "DTW", "pg_ev_ending": "Завершение", "pg_ev_running": "Работает", "pg_group_advanced": "Особые случаи", @@ -513,63 +482,39 @@ "pg_group_timing": "Правила времени", "pg_idle": "Ожидание", "pg_anti_wrinkle": "Защита от морщин", - "pg_last_run": "Последний запуск: {date}", - "pg_match_score": "Оценка совпадения", - "pg_pct_of": "{p}% из {total}", "pg_poor_match": "Плохое совпадение", "pg_preset": "Пресет песочницы", "pg_preset_name": "Имя нового пресета", "pg_preset_none": "Выберите пресет…", "pg_strong_match": "Сильное совпадение", - "pg_tile_ambiguous": "Неоднозначно", - "pg_tile_ambiguous_tip": "Циклы с почти равным совпадением двух программ", - "pg_tile_detected": "Обнаружено", - "pg_tile_detected_tip": "Циклы, в которых мощность превышала порог запуска достаточно долго, чтобы засчитаться как работа", - "pg_tile_matched": "Сопоставлено", - "pg_tile_matched_tip": "Циклы, правильно отнесённые к известной программе", - "pg_time_left": "Осталось времени", "pg_weak_match": "Слабое совпадение", "phase_name": "Имя фазы", "name": "Имя", "phase_timeline": "Хронология фаз", "power": "Мощность", "profile": "Профиль", - "profile_index": "индекс профиля →", "profile_name": "Имя профиля", "program": "Программа", - "program_tip": "Замените, какой профиль сопоставляется с текущим циклом. Авто-определение позволяет интеграции автоматически выбирать наилучшее совпадение. Закрепите конкретную программу, чтобы принудительно сопоставить её, если авто-определение ошибается или вы знаете, что запущено. Выберите программу до запуска прибора, и она будет применена, как только начнётся следующий цикл.", + "program_tip": "Выберите, с какой программой сопоставляется текущий цикл. Авто-определение выбирает лучшее совпадение. Закрепите программу, если авто-определение ошибается; программа, выбранная до запуска, применится к следующему циклу.", "progress": "Прогресс", "raw_socket": "Необработанный сокет", - "read_only": "Только чтение", "ref_cycle": "Эталонный цикл (необязательно)", - "refresh_interval": "Интервал обновления (с)", "remaining": "Осталось", "reminder_every": "Напоминать каждые (циклы)", "remove_label": "- Удалить метку -", "rename_profile": "Переименовать профиль", - "replay_duration": "Воспроизведение", "resulting_profile": "Результирующий профиль", "save_mode": "Режим сохранения", - "score": "Балл", - "score_breakdown": "Разбивка балла", "seconds_from_start": "Секунды от начала", "select_profile": "Выбрать профиль", "setting": "Настройка", - "settings_a": "Настройки A (текущие)", - "settings_a_short": "A", "settings_advanced": "Дополнительно", - "settings_b": "Настройки B (тест)", - "settings_b_short": "B", "settings_basic": "Основные", "settings_detail_level": "Уровень детализации настроек", - "settings_overrides": "Переопределения настроек", "show_debug": "Показать отладочную карточку на странице статуса (достоверность, неоднозначность, лучшие кандидаты)", "show_expected": "Показать ожидаемую кривую (сопоставленный профиль, оранжевый)", "show_raw": "Показать переключатель необработанных данных розетки на графике мощности в реальном времени", "sparkline": "Недавняя динамика длительности циклов", - "stage2": "Этап 2 – базовое сходство", - "stage3": "Этап 3 – DTW", - "stage4": "Этап 4 – совпадение", "status": "Статус", "tail_trim": "Обрезка конца (с)", "tap_to_dismiss": "коснитесь графика, чтобы закрыть", @@ -580,7 +525,6 @@ "ungrouped": "Без группы", "unlabelled": "Без метки", "unlabelled_paren": "(без метки)", - "warp_path": "Путь деформации", "input": "Ввод:", "start": "Начало", "end": "Конец", @@ -605,9 +549,8 @@ "n_shown": ", показано {n}", "cycles_title": "Циклы ({n})", "pg_detecting": "Обнаружение", - "pg_best_marker": "Лучшее: {v}", "pg_time_left_model": "Осталось времени (модель)", - "pg_time_left_model_tip": "Оценка оставшегося времени моделью (оценщик фаз + ML), а не статичный отсчёт", + "pg_time_left_model_tip": "Оценка по фазам программы, а не фиксированный обратный отсчёт", "phase": "Фаза", "pg_mode_history": "Проверка на истории", "pg_ev_detected": "Обнаружено", @@ -647,18 +590,8 @@ "pg_mode_optimize": "Оптимизировать", "pg_group_matching": "Сопоставление программ", "pg_zoom_hint": "двойной щелчок сбрасывает масштаб", - "aria_playground_chart2": "Интерактивный график мощности цикла: наведите, чтобы увидеть время/мощность, прокрутите для масштабирования, перетащите для панорамирования", "pg_alert_timeout_end": "Завершено по тайм-ауту, а не по прогнозу", "pg_alert_indefinite": "Работал бы бесконечно", - "pg_alert_stress_cap": "Достигнут защитный лимит", - "pg_alert_stress_ok": "Обнаружен режим ожидания: цикл остановлен", - "pg_alert_stress_warn": "Потребление в режиме ожидания выше порога остановки", - "pg_stress_group": "Тест обнаружения режима ожидания", - "pg_stress_idle_auto": "Auto", - "pg_stress_idle_w": "Уровень в режиме ожидания (Вт)", - "pg_stress_idle_w_desc": "Переопределить автоматически обнаруженный нижний порог потребления в режиме ожидания (оставьте пустым для автоматического определения).", - "pg_stress_toggle": "Тестировать обнаружение режима ожидания", - "pg_stress_toggle_desc": "Имитирует работу устройства в режиме ожидания после завершения записи -- показывает, остановит ли и когда WashData цикл.", "task_pg_history": "Тест на истории", "task_pg_sweep": "Оптимизация", "task_reprocess": "Повторная обработка", @@ -668,16 +601,9 @@ "pg_recent_runs": "Недавние запуски", "pg_load_run": "Перезагрузить этот запуск", "pg_match_confidence": "Достоверность совпадения", - "pg_envelope_fit": "Соответствие огибающей", - "pg_envelope_fit_tip": "Насколько точно цикл вписывается в сохранённую огибающую мощности этого профиля, иной взгляд, чем достоверность совпадения.", "n_imported_note": ", {n} импортировано", "enable_online": "Включить онлайн-функции", - "store_brand": "Бренд устройства", - "store_model": "Модель устройства", "anonymous": "Анонимно", - "rate_quality": "Оценить качество", - "rate_n": "{n} звёзд", - "approved_only": "Только одобренные", "adopt_settings": "Принять настройки", "flags": "Метки", "include_phase_map": "Включить карту фаз", @@ -694,7 +620,6 @@ "cat_custom_phases": "Пользовательские фазы", "cat_profile_groups": "Группы профилей", "cat_settings": "Настройки обнаружения и распознавания", - "cat_matching_config": "Тонкая настройка распознавания", "cat_ml_models": "ML-модели", "cat_feedback": "Отзывы и метки проверки", "cat_suggestions": "Предложения", @@ -714,7 +639,6 @@ "conflict_overwrite": "Перезаписать", "hist_csv_data": "Данные CSV", "hist_from_recorder": "Или считать из Home Assistant", - "days": "дн.", "hist_keep": "Сохранить этот цикл", "hist_looks_complete": "завершён", "peak_power_short": "Пик", @@ -735,7 +659,44 @@ "zoom_hint_touch_zoomed": "масштаб двумя пальцами · двойное касание для сброса", "cycles_since_service": "{since} / {total} циклов", "total_cycles_run": "циклов всего", - "correct_total_cycles": "Исправить общее количество" + "correct_total_cycles": "Исправить общее количество", + "hist_reason_already_recorded": "перекрывается с циклом, уже записанным WashData", + "share_consent": "Подтверждаю, что эти циклы прошли до нормального завершения без прерываний", + "show_internal": "Показать внутренние настройки", + "show_internal_tip": "Настройки, которые WashData подбирает за вас (выборка, watchdog, частота сопоставления, пороги). Показываются, если у этого устройства своё значение, конфликт или предложение.", + "rate_n": "{n} звёзд", + "rate_quality": "Оценить качество", + "pg_autolabel": "Автоматическая маркировка", + "pg_label_review": "Требует проверки", + "pg_ev_match_reverted": "Совпадение отменено", + "pg_ev_verified_pause": "Подтверждённая пауза", + "n_samples": "{n} показаний", + "n_samples_one": "{n} показание", + "n_samples_few": "{n} показания", + "n_samples_many": "{n} показаний", + "n_samples_other": "{n} показания", + "n_cycles": "{n} циклов", + "n_cycles_one": "{n} цикл", + "n_cycles_few": "{n} цикла", + "n_cycles_many": "{n} циклов", + "n_cycles_other": "{n} цикла", + "energy_per_cycle": "{v}/цикл", + "energy_total": "всего {v}", + "automation_off": "(выкл.)", + "ref_cycle_none": "Нет", + "share_name": "Показывать моё имя в публикациях", + "cat_backfill_cycles": "Циклы, найденные в импортированной истории мощности", + "pg_compare_profile": "Сравнить с профилем", + "pg_obj_early": "Частота ранних завершений", + "pg_obj_endlag": "Задержка завершения (медиана)", + "pg_obj_split": "Частота разбиений", + "pg_sweep_cycles": "Последние циклы", + "cycle_context": "История из recorder вокруг цикла", + "cycle_context_off": "Выключено", + "cycle_context_minutes": "{min} мин", + "cycle_context_series": "История из recorder", + "before_start": "До начала", + "after_end": "После окончания" }, "log": { "all_levels": "Все уровни", @@ -745,11 +706,9 @@ }, "ml": { "builtin_models": "● Использует встроенные модели", - "fit_fair": "Приемлемо", "fit_good": "Хорошо", "fit_slight": "Незначительное", "fit_strong": "Сильное", - "fit_weak": "Слабое", "fit_word": "соответствие", "personalized": "● Настроено под эту машину", "better_than_baseline": "{pct}% лучше базовой оценки ({metric})", @@ -757,21 +716,33 @@ "trend_declining_tip": "Соответствие этой модели ухудшилось за последние повторные проверки; проверка большего числа циклов может помочь ей переобучиться.", "trend_steady_tip": "Соответствие этой модели оставалось примерно стабильным за последние повторные проверки.", "cap_blurb": { - "end": "Определение момента, когда цикл действительно завершился", - "live_match": "Более раннее распознавание запущенной программы", - "quality": "Выявление ошибочно определённых или повреждённых циклов", - "remaining_time": "Прогноз оставшегося времени", "total_energy": "Прогноз общего расхода энергии и стоимости" }, "cap_label": { - "end": "Определение конца цикла", - "live_match": "Распознавание программы", - "quality": "Проверка качества цикла", - "remaining_time": "Оценка оставшегося времени", "total_energy": "Оценка энергопотребления" }, - "metric_auc": "AUC {auc} на отложенных данных", - "metric_mae": "ошибка {model} против базовой модели {baseline}" + "metric_mae": "ошибка {model} против базовой модели {baseline}", + "fine_tuned_at": "точно настроено {when}", + "held_out_count": "измерено на {count} отложенных циклах", + "held_out_count_one": "измерено на {count} отложенном цикле", + "held_out_count_few": "измерено на {count} отложенных циклах", + "held_out_count_many": "измерено на {count} отложенных циклах", + "held_out_count_other": "измерено на {count} отложенного цикла", + "last_run_not_promoted": "Последняя проверка ({when}): {reason}", + "never": "никогда", + "skip_reason": { + "fit_failed": "Ошибка обучения: {error}", + "holdout_too_small": "Слишком мало циклов, чтобы честно проверить новую модель: для проверки удалось отложить {held_out}, а нужно {min}.", + "insufficient_rows": "Пока недостаточно пригодных циклов: на данный момент {count} помеченных чистых циклов.", + "insufficient_rows_one": "Пока недостаточно пригодных циклов: на данный момент {count} помеченный чистый цикл.", + "insufficient_rows_few": "Пока недостаточно пригодных циклов: на данный момент {count} помеченных чистых цикла.", + "insufficient_rows_many": "Пока недостаточно пригодных циклов: на данный момент {count} помеченных чистых циклов.", + "insufficient_rows_other": "Пока недостаточно пригодных циклов: на данный момент {count} помеченного чистого цикла.", + "not_better_than_incumbent": "Новая модель оказалась не лучше используемой (ошибка {model} против {incumbent}), поэтому оставлена текущая.", + "not_better_than_naive": "Новая модель оказалась не заметно лучше простой оценки (ошибка {model} против {naive}).", + "unknown": "Ничего нового не выучено." + }, + "trained_unknown": "в неизвестное время" }, "modal": { "auto_label": "Автоматическая пометка циклов", @@ -780,7 +751,6 @@ "cycle": "Цикл", "edit_group": "Редактировать группу профилей", "edit_phase": "Редактировать фазу", - "import_config": "Импортировать конфигурацию", "label_cycle": "Пометить цикл", "merge_cycles": "Объединить {n} циклов", "new_group": "Новая группа профилей", @@ -795,6 +765,8 @@ "delete_automation_title": "Удалить автоматизацию", "delete_automation_msg": "Удалить автоматизацию «{name}» из Home Assistant? Это действие нельзя отменить.", "remove_legacy_title": "Удалить устаревшие действия", + "reset_phase_msg": "Отменить ваши изменения фазы «{name}» и восстановить встроенную фазу? Программы сохранят свои диапазоны.", + "reset_phase_title": "Сбросить фазу", "remove_legacy_msg": "Удалить устаревшие пользовательские действия? Они перестанут срабатывать при событиях цикла. Это нельзя отменить из панели.", "discard_recording_title": "Отклонить запись", "discard_recording_msg": "Отклонить сохранённую запись? Это действие нельзя отменить.", @@ -815,32 +787,46 @@ "store_share_device": "Поделиться конфигурацией устройства", "export_select": "Экспорт - выбор данных", "import_wizard": "Импорт - выбор данных", - "history_import": "Импорт истории мощности" + "history_import": "Импорт истории мощности", + "apply_suggestions_title": "Применить предложения", + "remove_task_title": "Удалить задачу", + "remove_task_msg": "Удалить напоминание «{task}»? Его записи в журнале обслуживания сохранятся.", + "apply_suggestions_msg": "Изменить эти настройки ({n})? После сохранения интеграция перезагрузится.", + "discard_settings_title": "Отменить несохранённые изменения?", + "discard_settings_msg": "Несохранённые изменения настроек: {n}. Покинуть Настройки и отменить их?", + "profile_title": "Профиль · {name}", + "undo_import_msg": "Данные и настройки этого устройства вернутся к состоянию до последнего импорта ({when}). Циклы, записанные с тех пор, и сделанные изменения будут потеряны.", + "undo_import_ok": "Отменить импорт", + "undo_import_title": "Отменить последний импорт" }, "msg": { "pg_canvas_empty3": "Выберите цикл выше и нажмите «Запустить», чтобы смоделировать его. Затем наведите указатель, чтобы увидеть значения, прокрутите или используйте два пальца для масштабирования и перетащите для панорамирования.", + "pg_preset_delete_confirm": "Удалить пресет «{name}»?", + "pg_preset_limit": "Достигнут предел числа пресетов ({n})", + "pg_preset_overwrite": "Перезаписать пресет «{name}»?", "tail_trim_hint": "Сколько секунд обрезать с конца", - "store_sibling_hint": "Для вашей точной модели ничего не опубликовано? Очень похожая модель того же бренда обычно является хорошей отправной точкой.", - "store_declare_appliance": "Укажите WashData, какое у вас устройство, и на этой вкладке появятся конфигурации, опубликованные для него другими пользователями. Можно также ввести бренд выше и просто посмотреть, что есть.", - "refresh_catalog_hint": "Списки брендов и устройств сообщества кэшируются, чтобы общий магазин не превышал свой дневной лимит запросов. Обновите, чтобы получить записи, добавленные или одобренные другими.", + "store_sibling_hint": "Для вашей модели ничего нет? Родственная модель той же марки обычно хорошее начало.", + "store_declare_appliance": "Укажите WashData, какой у вас прибор, чтобы увидеть настройки, которыми для него поделились другие. Или введите марку выше и осмотритесь.", + "refresh_catalog_hint": "Списки марок и приборов сообщества кэшируются, чтобы экономить дневной лимит магазина. Обновите, чтобы увидеть записи, которые другие добавили или одобрили.", "head_trim_hint": "Сколько секунд обрезать с начала", "appliance_monitor": "Монитор прибора", "artifact_dip_detail": "Мощность упала ниже обычного диапазона на ~{n}с.", "artifact_footer": "Выделено на графике выше. Это временные артефакты (например, дверь открылась в середине цикла), а не обязательно проблемы.", - "artifact_header": "В этом цикле обнаружено {n} аномалий", - "artifact_pause_detail": "Мощность упала почти до нуля на ~{n}с, затем возобновилась – вероятно, дверца была открыта в середине цикла или цикл был приостановлен.", + "artifact_header": "В этом цикле обнаружено {n} аномалии", + "artifact_header_one": "В этом цикле обнаружена {n} аномалия", + "artifact_header_few": "В этом цикле обнаружены {n} аномалии", + "artifact_header_many": "В этом цикле обнаружено {n} аномалий", + "artifact_header_other": "В этом цикле обнаружено {n} аномалии", + "artifact_pause_detail": "Мощность упала почти до нуля на ~{n}с, затем вернулась. Вероятно, открыли дверцу или поставили цикл на паузу.", "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": "Наложить профили (бледные)", - "compare_profiles_tip": "Наложите конверты других профилей на диаграмму выше, чтобы увидеть, какой из них лучше всего подходит для этого цикла.", + "compare_profiles_tip": "Наложите на график другие профили, чтобы увидеть, какой лучше всего подходит этому циклу.", "compare_selected_cycles": "Выбранные циклы (чёткие) – показать / скрыть", - "consider_new_profile": "Рассмотрите возможность создания нового профиля.", - "coverage_gap": "последние циклы не имеют соответствующего профиля ({pct}% из последних 30).", - "coverage_gap_similar_cycles": "Найдено {count} похожих непомеченных циклов – создайте профиль, чтобы начать их сопоставление.", "cycles_deleted": "Удалено циклов: {count}", "enough_data": "Достаточно данных для обучения ({current}/{min} циклов).", "export_description": "Выберите, какие именно профили, циклы, настройки и прочее экспортировать в JSON, или проанализируйте файл и импортируйте только нужные части.", @@ -848,30 +834,28 @@ "feedback_prompt": "Подтвердите правильность, исправьте программу или проигнорируйте.", "feedback_relabel_hint": "Повторная маркировка этого цикла также решает его.", "filter_by_profile": "Фильтр по профилю…", - "group_modal_help": "Групповые программы одинаковой формы, отличающиеся температурой/отжимом (продолжительность может варьироваться). При сопоставлении группа оценивается как один кандидат, а затем выбирается наиболее подходящий член. Выберите хотя бы 2; наложение показывает, насколько они похожи.", + "group_modal_help": "Сгруппируйте программы одной формы, которые отличаются температурой или отжимом. Сопоставление оценивает группу целиком, затем выбирает наиболее подходящего участника. Выберите не меньше 2; наложение показывает, насколько они похожи.", "group_not_cohesive": "Эти профили недостаточно похожи, чтобы их можно было надежно сгруппировать, поэтому сопоставление обрабатывает их индивидуально, пока вы не удалите выброс или не разделите группу.", "group_preview_hint": "Отметьте 2 или более участников, чтобы предварительно просмотреть и сравнить их кривые мощности.", - "import_intro": "Загрузите экспортированный файл или вставьте JSON ниже.", - "legacy_actions_title": "{count} устаревшее специальное действие все еще работает", + "legacy_actions_title": "{count} устаревшего специального действия все еще работает", + "legacy_actions_title_one": "{count} устаревшее специальное действие все еще работает", + "legacy_actions_title_few": "{count} устаревших специальных действия все еще работают", + "legacy_actions_title_many": "{count} устаревших специальных действий все еще работают", + "legacy_actions_title_other": "{count} устаревшего специального действия все еще работает", "live_chart_loading": "График мощности появится по мере поступления показаний.", "loading": "Загрузка…", "loading_curve": "Загрузка кривой…", "loading_settings": "Загрузка настроек…", "log_buffer_hint": "Сначала самые новые · буферизует последние 500 записей ha_washdata с момента перезапуска · перетащите нижний край, чтобы изменить размер.", - "maintenance_advisory": "Увеличение продолжительности/энергии может указывать на необходимость технического обслуживания устройства (например, удаление накипи, очистка фильтра).", "maintenance_due": "Требуется обслуживание: {items}", - "maintenance_intro": "Записывайте обслуживание этого прибора и получайте напоминания, когда придёт время выполнить каждую задачу снова.", + "maintenance_intro": "Записывайте обслуживание этого прибора и получайте напоминание, когда каждая задача снова подойдёт.", "maintenance_load_error": "Не удалось загрузить данные обслуживания: {error}", "maintenance_requires_access": "Обслуживание и экспорт/импорт требуют полного доступа.", - "manual_duration_hint": "Средняя/ожидаемая длина цикла профиля, используемая для оценки оставшегося времени. Измените, чтобы установить его; оставляя его без изменений, сохраняется текущее значение.", - "matching_tuning_intro": "При обучении WashData также регулирует, насколько сопоставление программ учитывает форму, продолжительность и энергию.", + "manual_duration_hint": "Ожидаемая длина цикла профиля, используется для оценки оставшегося времени. Не меняйте, чтобы сохранить текущее значение.", "merge_intro": "Выбранные циклы объединяются в один (хронологический порядок; пропуски заполняются 0 Вт). Выберите результирующий профиль.", "ml_intro": "WashData поставляется с умными моделями, которые работают сразу после установки.", "ml_learned_intro": "Модели, настроенные под эту машину.", - "ml_loading": "загрузка ML…", "ml_settings_intro": "Два независимых переключателя: один применяет модели во время работы цикла, другой позволяет WashData постепенно настраивать их под вашу машину.", - "name_first_program": "Циклов достаточно – назовите первую программу, чтобы начать сопоставление.", - "near_duplicate_cluster": "Обнаружен почти повторяющийся кластер профилей. Группировка позволяет надежно выбирать одинаковые программы (например, одна и та же программа при разной температуре/отжиме).", "no_cycles_match": "Нет циклов, соответствующих текущему фильтру.", "no_cycles_profile": "Нет циклов для этого профиля.", "no_cycles_selected": "Сначала выберите хотя бы один цикл.", @@ -896,79 +880,66 @@ "no_suggestions": "Нет активных предложений.", "notify_services_hint": "Используйте ID сервисов {entity} (через запятую для нескольких). Переменные шаблона: {vars}.", "old_actions_warning": "Настроен со старым редактором действий (теперь удален). Они по-прежнему срабатывают при событиях цикла, но их больше нельзя редактировать здесь. Преобразуйте их в обычную автоматику или удалите.", - "onboarding_progress": "Наблюдалось циклов: {n} / 3", - "onboarding_watching": "Используйте прибор как обычно – WashData наблюдает. После 3 циклов начнётся сопоставление программ.", "pending_feedback": "Ожидает отзыва об обнаружении", "performance_trend": "Тенденция производительности ({n} циклов)", - "pg_analysis_empty": "Загрузите цикл, чтобы увидеть анализ совпадения.", "pg_apply_confirm": "Применить лучшее значение: {label} = {value}?", - "pg_canvas_empty": "Выберите цикл выше, затем нажмите «Загрузить», чтобы увидеть его график мощности.", - "pg_chart_caption": "Доля обнаружения/совпадения по значениям {param}", "pg_ctrl_intro": "Значения берутся из активных настроек интеграции для этого устройства. Изменения остаются в песочнице, пока вы не сохраните их в настройки.", "pg_live_unavailable": "Активные настройки недоступны, показаны значения по умолчанию.", "pg_load_live_confirm": "Отменить изменения в песочнице ({n}) и загрузить текущие настройки интеграции?", - "pg_match_rate": "{pct}% совпадений на {total} циклах", "pg_matches_live": "Совпадает с активными настройками", "pg_n_changed": "Отличий от активных настроек: {n}", "pg_sugg_none": "Все рекомендации уже соответствуют текущим настройкам", - "pg_preset_delete_confirm": "Удалить пресет «{name}»?", - "pg_preset_limit": "Достигнут предел числа пресетов ({n})", - "pg_preset_overwrite": "Перезаписать пресет «{name}»?", "pg_publish_one_confirm": "Сохранить {label} = {value} в настройки этого устройства?", "pg_restart_note": "Перезапустите Home Assistant, чтобы включить инструменты симуляции.", - "pg_sim_progress": "{done} / {total} циклов", - "pg_starting": "Запуск…", - "pg_stress_synthetic": "здесь начинается симуляция режима ожидания", - "pg_sweep_intro": "Что если бы {param} было другим? Проверьте {steps} значений на ваших последних {cycles} циклах, чтобы найти настройку, при которой правильно сопоставляется больше всего циклов.", - "pg_sweep_step": "Шаг {done} / {total}", - "pg_verdict_bad": "Требует внимания: многие циклы остаются необнаруженными.", - "pg_verdict_good": "Хорошо настроено: большинство циклов правильно определяются и сопоставляются.", - "pg_verdict_ok": "Приемлемо: некоторые циклы пропущены. Попробуйте снизить порог запуска.", "phase_catalog_intro": "Именованные сегменты цикла (предстирка, нагрев, отжим…). Назначайте их профилю из его панели управления.", "phase_ranges_intro": "Диапазоны фаз (минуты от начала цикла) наложены на среднюю кривую. Изменяйте значения для предварительного просмотра.", - "playground_intro": "Безопасная песочница для повторного прогона прошлых циклов с другими настройками, сравнения двух конфигураций бок о бок и проверки того, как цикл выравнивается с профилем. Ничего здесь не меняет ваши активные данные, пока вы явно не примените изменения.", - "playground_needs_restart": "Перезапустите Home Assistant, чтобы включить инструменты песочницы.", - "preferences_admin": "Настройки, панель и управление доступом", - "preferences_adv": "Настройки", + "playground_intro": "Пробуйте настройки на реальных данных своих циклов. Ничто здесь не меняет действующие настройки, пока вы их не примените.", "prefs_personal": "Применяются только к вашей учётной записи Home Assistant.", "font_size_hint": "Сделайте всё на этой панели крупнее или мельче. Применяется к вашей учётной записи на этом устройстве.", - "process_history_hint": "Повторно запустите сопоставление для всех сохранённых циклов, обновите предложения по настройке, переобучите модели ML (если включено), пересчитайте стоимость циклов по записанным ценам на электроэнергию и пересчитайте качество циклов. Запускайте это после серии проверок.", + "process_history_hint": "Заново запускает сопоставление всех сохранённых циклов, обновляет предложения, переобучает модели ML (если включены), пересчитывает стоимость циклов по записанным ценам и пересчитывает состояние циклов. Запускайте после серии проверок.", "profile_deleted": "Профиль удалён", - "profile_poor_health_detail": "Циклы, назначенные этому профилю, имеют непоследовательную форму или низкую достоверность. Рассмотрите возможность восстановления конверта или пересмотра помеченных циклов.", "profiles_intro": "Нажмите на профиль для статистики, фаз и очистки. Группируйте почти одинаковые программы (одинаковая форма/продолжительность, разная температура или отжим), чтобы сопоставление надёжно выбирало между ними.", - "rbac_hint": "Когда выключено, каждый пользователь Home Assistant имеет полный доступ (по умолчанию). Администраторы всегда имеют полный доступ и могут управлять всеми.", - "recent_logs": "Последние записи ha_washdata", + "rbac_hint": "Выключено: у каждого пользователя Home Assistant полный доступ (по умолчанию). Администраторы всегда имеют полный доступ и могут управлять всеми.", "recording_in_progress": "Запись в процессе", "reminders_intro": "Показывать напоминание в панели через указанное число циклов после последнего обслуживания. Оставьте пустым или 0, чтобы отключить напоминание.", "restart_gap_footer": "Выделено на графике. Данные мощности отсутствуют для этих интервалов; сопоставление использовало только реальные показания.", - "restart_gap_header": "{n} пропуск при перезапуске HA во время этого цикла", + "restart_gap_header": "{n} пропуска при перезапуске HA во время этого цикла", + "restart_gap_header_one": "{n} пропуск при перезапуске HA во время этого цикла", + "restart_gap_header_few": "{n} пропуска при перезапуске HA во время этого цикла", + "restart_gap_header_many": "{n} пропусков при перезапуске HA во время этого цикла", + "restart_gap_header_other": "{n} пропуска при перезапуске HA во время этого цикла", "restart_gap_item": "{dur} пропуск", - "review_confirm_help": "Убедитесь, что этот цикл был обнаружен правильно. Ваши обзоры тренируют модель на вашей машине. Чем больше циклов вы подтвердите, тем лучше будет соответствие и оценка работоспособности. Достаточно быстрого ответа «Хорошо/Плохо».", + "review_confirm_help": "Цикл определён правильно? Подтверждение циклов улучшает сопоставление для вашей машины. Достаточно быстрого Хорошо/Плохо.", "review_in_settings": "Просмотр в настройках", "review_notes_placeholder": "Примечания (необязательно)", "review_notes_tip": "Свободные текстовые заметки для справки. Не используется при сопоставлении или обучении.", - "review_profile_tip": "Программа, под которой помечен этот цикл. Если автоматически определенная программа была неправильной, исправьте ее здесь – маркировка учит сопоставлению для будущих циклов.", - "review_quality_tip": "Насколько чист этот цикл. Хорошо = хрестоматийный пример этой программы; Плохо = обнаружено, но шумно или нетипично; Непригодный = обнаружен неправильно (объединен, усечен или поддельен). Управляет показателем работоспособности и тем, какие циклы разрешены для обучения модели.", - "review_recorded_tip": "Отметьте это как выбранный вручную эталонный цикл для своей программы – та же роль, что и цикл, записанный вручную. Ссылочные циклы всегда сохраняются, заполняют соответствующий шаблон и никогда не удаляются при очистке. (Это «золотой»/записанный флаг; оба – одно и то же.)", - "review_tags_tip": "Необязательные флаги, описывающие, что пошло не так в этом цикле, чтобы это можно было учесть при обучении и очистке.", + "review_profile_tip": "Программа, которой помечен этот цикл. Если она неверна, исправьте её здесь; метки обучают сопоставление.", + "review_quality_tip": "Насколько чист этот цикл. Хорошо: образцовый прогон этой программы. Плохо: определён, но с шумом или необычный. Непригодный: определён неверно (слит, обрезан или ложный). Влияет на оценку состояния и на то, какие циклы обучают модель.", + "review_recorded_tip": "Отметить как вручную выбранный эталонный цикл его программы, как цикл, записанный вручную. Эталонные циклы всегда сохраняются, задают шаблон сопоставления и никогда не удаляются при очистке.", + "review_tags_tip": "Необязательные флаги о том, что пошло не так, чтобы обучение и очистка могли это учесть.", "review_to_cycles": "Открыть очередь проверки циклов", "samples_decimated": "Показано {shown} из {total} отсчётов (прорежено для отображения; пики сохранены). Широкий разрыв здесь - это прореживание, а не пропуск данных.", - "saving_triggers_reload": "Сохранение вызывает перезагрузку интеграции. Объекты HA могут кратко отображаться как недоступные.", + "saving_triggers_reload": "Сохранение перезагружает интеграцию. Объекты могут ненадолго стать недоступными.", "search_placeholder": "Поиск настроек…", "see_recorder": "Смотрите виджет записи ниже", "setting_changed": "Изменено с {old} на {new} – {date}", "settings_basic_note": "Показаны основные настройки. Переключитесь на «Дополнительно» для полного списка.", - "shape_drift_advisory": "⚠ Смещение формы", - "shape_drift_detail": "Профиль мощности этого профиля изменился со временем – возможный износ прибора или необходимость технического обслуживания (например, удаление накипи, чистка фильтра).", "show_all_settings": "Показать все настройки", - "showing_suggestions": "Показана настройка {count} с предложениями.", - "split_intro": "Нажмите на график, чтобы добавить или удалить точку разбивки, или авто-определите по паузам. Каждый результирующий сегмент может получить свой профиль.", - "storage_diagnostics": "Статистика хранилища, обслуживание, экспорт/импорт", + "showing_suggestions": "Показано {count} настройки с предложениями.", + "showing_suggestions_one": "Показана {count} настройка с предложениями.", + "showing_suggestions_few": "Показаны {count} настройки с предложениями.", + "showing_suggestions_many": "Показано {count} настроек с предложениями.", + "showing_suggestions_other": "Показано {count} настройки с предложениями.", + "split_intro": "Нажмите на график, чтобы добавить или убрать точку разделения, или найдите их автоматически по паузам простоя. Каждая часть может получить свой профиль.", "sug_staged": "Установлено {key} = {val}. Сохраните для применения.", "sug_muted": "Эта настройка больше не будет предлагаться", "sug_mute_failed": "Не удалось заглушить предложение", "sug_unmuted_all": "Заглушённые предложения сброшены", - "n_suggestions_muted": "{count} заглушено; авто-настройщик не будет предлагать их.", + "n_suggestions_muted": "{count} предложения заглушено; авто-настройщик не будет их предлагать.", + "n_suggestions_muted_one": "{count} предложение заглушено; авто-настройщик не будет его предлагать.", + "n_suggestions_muted_few": "{count} предложения заглушены; авто-настройщик не будет их предлагать.", + "n_suggestions_muted_many": "{count} предложений заглушено; авто-настройщик не будет их предлагать.", + "n_suggestions_muted_other": "{count} предложения заглушено; авто-настройщик не будет их предлагать.", "toast_auto_detect_enabled": "Автоопределение включено", "toast_auto_label_complete": "Автоматическая маркировка завершена", "toast_auto_label_failed": "Автоматическая маркировка не выполнена: {error}", @@ -984,7 +955,6 @@ "toast_no_automation": "Здесь нельзя создать автоматизацию", "toast_profile_name_required": "Требуется название профиля", "toast_program_set": "Программа выбрана: {program}", - "toast_rebuild_failed": "Перестройка не выполнена: {error}", "toast_resume_failed": "Возобновление не выполнено: {error}", "toast_revert_failed": "Откат не выполнен: {error}", "toast_reverted": "Возвращено: {key}", @@ -995,95 +965,82 @@ "trend_energy_up": "Тенденция роста энергопотребления ({pct}%/цикл) – последнее среднее значение {avg}", "trim_destructive_confirm": "Останется только {pct}% цикла, и это действие нельзя отменить. Продолжить?", "trim_intro": "Перетащите красные маркеры или введите значения. Всё за пределами окна удаляется.", - "tuning_suggestions_available": "{count} предложение по настройке, доступное из наблюдаемых циклов. Они появляются рядом с соответствующими полями.", + "tuning_suggestions_available": "Доступно {count} предложения по настройке на основе наблюдаемых циклов. Они появляются рядом с соответствующими полями.", + "tuning_suggestions_available_one": "Доступно {count} предложение по настройке на основе наблюдаемых циклов. Оно появляется рядом с соответствующим полем.", + "tuning_suggestions_available_few": "Доступны {count} предложения по настройке на основе наблюдаемых циклов. Они появляются рядом с соответствующими полями.", + "tuning_suggestions_available_many": "Доступно {count} предложений по настройке на основе наблюдаемых циклов. Они появляются рядом с соответствующими полями.", + "tuning_suggestions_available_other": "Доступно {count} предложения по настройке на основе наблюдаемых циклов. Они появляются рядом с соответствующими полями.", "unsure_detected_prefix": "WashData не уверен, что обнаружил", - "updated": "Обновлено", "warmup_badge": "Ещё обучается ({done}/{needed} циклов)", - "warmup_detail": "Этому профилю требуется {needed} помеченных циклов до начала автоматического сопоставления. Каждый подтверждённый цикл помогает ему обучаться.", + "warmup_detail": "Сопоставление уже работает. WashData просит подтвердить первые {needed} циклов этого профиля, прежде чем помечать самостоятельно.", "wipe_history_warning": "Безвозвратно удалите все циклы и профили. Невозможно отменить.", "compare_overlay_tip": "Наложите огибающие изученных профилей, чтобы увидеть, на какую программу похож каждый цикл.", - "duration_trend_up_tip": "Продолжительность растёт ({pct}%/цикл)", - "duration_trend_down_tip": "Продолжительность падает ({pct}%/цикл)", - "energy_trend_up_tip": "Энергия растёт ({pct}%/цикл)", - "energy_trend_down_tip": "Энергия падает ({pct}%/цикл)", - "performance_trending": "Динамика производительности", - "pg_select_cycle_hint": "Выберите цикл и нажмите ↺ для загрузки", - "pg_sweep_progress": "Шаг {done} / {total}", - "pg_analysis_hint": "Выберите цикл и нажмите ↺ для загрузки анализа.", "diagnostics_load_failed": "Не удалось загрузить диагностику: {error}", - "manual_duration_ref_hint": "Используется только тогда, когда не выбран эталонный цикл. Эталонный цикл задаёт продолжительность по собственной длине.", + "manual_duration_ref_hint": "Используется только без эталонного цикла; эталонный цикл задаёт длительность по своей длине.", + "duration_learned_hint": "Рассчитано по циклам этой программы (без выбросов) и обновляется после каждого нового. Чтобы изменить, обрежьте или удалите неподходящие циклы.", "toast_convert_rolled_back": "Конвертация не выполнена и была отменена (автоматизация не оставлена): {error}", "toast_convert_orphan": "Автоматизация создана, но очистить старые действия не удалось. Не повторяйте попытку: удалите старые действия вручную, чтобы избежать дублирующей автоматизации.", - "advisory_duration_trend_up": "Циклы '{name}' становятся всё длиннее (примерно +{pct}% за цикл). Если поведение прибора изменилось, перезапишите или пересоберите этот профиль.", - "advisory_energy_trend_up": "'{name}' потребляет всё больше энергии (примерно +{pct}% за цикл) - стоит проверить прибор, если это неожиданно.", - "advisory_poor_health": "У '{name}' низкая оценка соответствия - его недавние циклы сильно различаются или плохо распознаются. Просмотрите его циклы или перезапишите профиль, чтобы распознавание и оценки времени оставались точными.", - "advisory_shape_drift": "'{name}' значительно отклонился от исходной формы кривой мощности. Возможно, поведение прибора со временем изменилось (например, накипь, износ). Рекомендуется перезаписать этот профиль по недавним циклам.", - "advisory_shape_drift_corr": "'{name}' значительно отклонился от исходной формы кривой мощности (корреляция {corr}). Возможно, поведение прибора со временем изменилось (например, накипь, износ). Рекомендуется перезаписать этот профиль по недавним циклам.", - "advisory_unmatchable": "'{name}' никогда не будет сопоставлен: за ним нет ни одного цикла с данными мощности, поэтому WashData не может сравнить с ним выполняемый цикл. Пометьте цикл этой программы (или запишите новый), чтобы вернуть профиль в сопоставление.", - "advisory_unmatchable_no_duration": "'{name}' никогда не будет сопоставлен: у него есть цикл с данными мощности, но без пригодной длительности, поэтому WashData не с чем сравнить длину цикла при сопоставлении. Запишите эту программу заново или задайте её ожидаемую длительность, чтобы вернуть профиль в сопоставление.", + "advisory_duration_trend_up": "Циклы '{name}' становятся всё длиннее (примерно +{pct}% за цикл). Если прибор изменился, перезапишите или пересоберите этот профиль.", + "advisory_energy_trend_up": "'{name}' с каждым циклом потребляет больше энергии (примерно +{pct}% за цикл). Проверьте прибор, если это неожиданно.", + "advisory_poor_health": "'{name}' плохо подходит: его последние циклы сильно различаются или слабо совпадают. Просмотрите его циклы или перезапишите его, чтобы сопоставление и оценки оставались точными.", + "advisory_shape_drift": "'{name}' отклонился от исходной формы кривой мощности, возможно из-за накипи или износа. Попробуйте перезаписать его по недавним циклам.", + "advisory_shape_drift_corr": "'{name}' отклонился от исходной формы кривой мощности (корреляция {corr}), возможно из-за накипи или износа. Попробуйте перезаписать его по недавним циклам.", + "advisory_unmatchable": "'{name}' никогда не сопоставится: за ним нет ни одного цикла с данными о мощности. Пометьте или запишите цикл этой программы, чтобы это исправить.", + "advisory_unmatchable_no_duration": "'{name}' никогда не сопоставится: у его цикла нет пригодной длительности. Перезапишите эту программу или задайте её ожидаемую длительность.", "pg_no_alerts": "В этом запуске проблем не обнаружено.", "pg_override_active": "Используются ваши изменённые настройки вместо текущих", "pg_history_empty": "Нажмите «Запустить», чтобы воспроизвести циклы.", "pg_history_summary": "{detected}/{total} обнаружено · {correct} сопоставлено верно", "pg_sweep_no_metric": "Недостаточно данных для оценки этой цели.", "pg_sweep_intro2": "Найдите настройку, которая лучше всего достигает цели на ваших последних циклах. Ничего не изменится, пока вы не примените.", - "pg_running_history": "Воспроизведение ваших циклов…", - "pg_running_sweep": "Проверка значений по всем вашим циклам…", "pg_row_load_hint": "Загрузить этот цикл на график выше", - "pg_history_intro2": "Воспроизведите недавние циклы через настоящий детектор и модуль сопоставления с указанными выше настройками. Нажмите на любую строку, чтобы загрузить этот цикл на график; измените настройку, чтобы увидеть сравнение до и после.", + "pg_history_intro2": "Прогоните недавние циклы через настоящий детектор и сопоставление с настройками выше. Нажмите на строку, чтобы загрузить её в график; измените настройку, чтобы сравнить до и после.", "pg_simulating": "Моделирование цикла…", - "pg_canvas_empty2": "Выберите цикл выше и нажмите «Запустить», чтобы смоделировать его. Затем наведите указатель, чтобы увидеть значения, прокрутите для масштабирования и перетащите для панорамирования.", "pg_apply_settings_confirm": "Сохранить эти настройки ({n}) в это устройство? {list}", - "pg_stress_terminated_detail": "Стабилизировалось на ~{idle}W в режиме ожидания -- цикл завершился {h} ч {m} мин позже через {reason}.", - "pg_stress_above_threshold_detail": "Потребление в режиме ожидания ~{idle}W равно или выше фактического порога остановки ({stop}W) -- цикл так и не был зафиксирован как неактивный. Чтобы исправить, увеличьте stop_threshold_w.", - "pg_stress_hit_cap_detail": "Цикл работал {h} ч {m} мин без остановки -- принудительно остановлен защитным лимитом. Потребление в режиме ожидания {idle}W по сравнению с порогом остановки {stop}W.", "no_logs_match": "Нет записей журнала, соответствующих этим фильтрам.", "pg_analysis_hint2": "Выберите цикл и нажмите Запустить, чтобы загрузить анализ совпадения.", - "pg_analysis_empty2": "Нажмите Запустить, чтобы увидеть анализ совпадения.", - "imported_readonly": "Импортировано из магазина сообщества. Показывается для справки и сопоставления. Не учитывается в вашей статистике и не может быть изменено.", + "imported_readonly": "Импортировано из магазина сообщества. Используется как эталон и для сопоставления, не учитывается в статистике и не редактируется.", "merge_no_imports": "Импортированные записи нельзя объединить с реальным циклом. Снимите с них выбор, чтобы объединить.", - "store_enable_hint": "Включите онлайн-функции в настройках, чтобы просматривать и импортировать эталонные циклы сообщества.", - "advisory_duration_outlier": "Циклы с меткой '{name}' ({n}) слишком далеки от её обычной длительности, чтобы когда-либо с ней совпасть (наибольшее отклонение: {ratio}x). Вероятно, они помечены неверно или одна запись охватила два запуска. Поменяйте метку или разделите их, чтобы они перестали искажать оценку времени для этой программы.", + "store_enable_hint": "Включите онлайн-функции в разделе Параметры > Онлайн и сообщество, чтобы просматривать и импортировать эталонные циклы сообщества.", + "advisory_duration_outlier": "Циклы с меткой ‘{name}’ ({n}) слишком далеки от его обычной длины, чтобы когда-либо сопоставиться (худший: {ratio}x). Вероятно, они помечены неверно или содержат два прогона. Перепометьте или разделите их, чтобы они не искажали оценку времени.", "standby_above_stop_title": "В момент завершения цикла всё ещё потребляется {idle} Вт", "standby_above_stop_sub": "{n} из последних {total} циклов. Задержка Выключения начинает отсчитываться только после того, как мощность опустится ниже Порога Остановки ({stop} Вт), поэтому циклы завершаются с опозданием или их приходится останавливать вручную.", - "online_intro": "Делитесь эталонными циклами с другими пользователями WashData и загружайте их. Все онлайн-функции подключаются по желанию и по умолчанию отключены.", "store_share_intro": "Загрузите этот эталонный цикл, чтобы другие пользователи с таким же устройством могли его использовать. Он проходит проверку перед публикацией.", - "online_intro_global": "Просматривайте эталонные записи и делитесь ими с другими пользователями WashData, а также подтверждайте записи об устройствах. Одно подключение действует для всей вашей интеграции WashData; бренд и модель устройства задаются для каждого устройства в разделе «Основные». Все онлайн-функции подключаются по желанию и по умолчанию отключены.", + "online_intro_global": "Просматривайте эталонные записи и делитесь ими с другими пользователями WashData. Одно подключение действует для всех ваших устройств WashData; марка и модель задаются для каждого устройства в разделе «Основные». Онлайн-функции выключены, пока вы их не включите.", "online_unavailable": "Онлайн-функции недоступны на этом сервере.", "store_picker_offline": "Включите онлайн-функции в меню настроек (шестерёнка), чтобы выбирать из каталога сообщества.", "pick_brand_first": "Сначала выберите бренд устройства.", - "model_not_found": "Пока нет в каталоге.", - "connect_to_confirm": "Подключитесь в меню настроек (шестерёнка), чтобы подтверждать или оценивать.", + "custom_task_limit": "Достигнут лимит задач ({max}).", + "custom_tasks_intro": "Добавьте всё, о чём ещё хотите получать напоминания. Задача требует выполнения, как только достигнут любой из интервалов; оставьте один пустым или 0, чтобы использовать только другой.", + "maintenance_due_sensor_hint": "Бинарный датчик «Требуется обслуживание» включён, пока есть задача к выполнению; используйте его в своих автоматизациях уведомлений.", + "maintenance_supporter_intro": "Maintenance Supporter является отдельной интеграцией, которая умеет гораздо больше: любой прибор или задача, расписание по времени, использованию или датчику, уведомления и история. Она может использовать счётчик циклов WashData.", "adopt_settings_hint": "Применить настройки обнаружения и сопоставления общего устройства к этому устройству", - "include_settings_hint": "Включить пороги обнаружения, веса сопоставления и другие параметры в общий пакет", + "include_settings_hint": "Поделиться порогами обнаружения и сопоставления этого устройства (без уведомлений, объектов и цены энергии). Другие сами решат, применять ли их.", "onboard_download": "Скачать готовую конфигурацию из магазина сообщества", - "share_consent": "Вы делитесь реальными данными вашего прибора. Не делитесь, если ваши сценарии использования являются личными.", "share_device_none": "Устройства ещё не настроены. Сначала добавьте устройство.", "share_guideline_naming": "Используйте понятные названия программ (например, 'Cotton 40', 'Eco 60'), чтобы другие могли их идентифицировать", - "share_guideline_quality": "Делитесь только циклами, которые завершились нормально: без прерываний в середине цикла, открытия дверцы или кратковременных сбоев питания.", + "share_guideline_quality": "Делитесь только циклами, которые завершились нормально: без прерываний, открывания дверцы и перебоев питания.", "share_guideline_review": "Просмотрите профили перед публикацией -- удалите те, которые выглядят неправильно", "share_guidelines_title": "Перед публикацией", - "store_download_device_intro": "Скачать конфигурацию устройства из сообщества и применить её к новому или существующему устройству", + "store_download_device_intro": "Добавить на ваше устройство все общие программы и их эталонные циклы. Ваши собственные циклы и статистика не затрагиваются.", "store_share_device_intro": "Загрузите {brand} {model} с выбранными вами эталонными циклами. Другие пользователи с таким же устройством смогут принять ваши программы. Записи проверяются перед публикацией.", "share_profile_no_cycles": "Нет эталонных циклов. Чтобы включить этот профиль, отметьте цикл как ⭐ на вкладке Циклы", - "advisory_phase_inconsistent": "Похоже, что '{name}' смешивает разные программы или температуры - его циклы нагреваются в течение очень разного времени. Разделение на отдельные профили (напр. по температуре) улучшит сопоставление и оценки времени.", - "advisory_phase_inconsistent_title": "⚠ Возможно, смешанные программы", - "export_select_intro": "Отметьте, что именно включить. Выбор профилей без их циклов всё равно экспортирует распознаваемую программу (её изученная форма передаётся вместе с ней).", - "import_analyze_hint": "Загрузите экспортированный файл (или вставьте его JSON). WashData проанализирует его и покажет, что именно можно импортировать, прежде чем что-либо изменится.", + "export_select_intro": "Отметьте, что включить. Профили без своих циклов всё равно экспортируют распознаваемую программу.", + "import_analyze_hint": "Загрузите экспортированный файл (или вставьте его JSON). WashData покажет, что можно импортировать, прежде чем что-либо изменится.", "analyzing": "Анализ…", - "device_type_mismatch_warn": "Этот экспорт получен от прибора другого типа ({src} вместо {local}). Программы и циклы всё равно можно импортировать как справочные данные, но настройки, зависящие от устройства, и импорт реальной истории отключены.", + "device_type_mismatch_warn": "Этот экспорт от другого типа прибора ({src} vs {local}). Программы и циклы всё равно можно импортировать как эталонные данные, но без настроек устройства и реальной истории.", "merge_hint": "Импортируемые элементы добавляются; ничего локального не теряется. Конфликты имён разрешаются ниже.", "replace_warn": "Каждая отмеченная категория очищается и заменяется данными из файла. Неотмеченные категории остаются без изменений.", "dest_reference_hint": "Импортированные циклы только улучшают распознавание программ и никогда не влияют на статистику использования/энергии.", - "dest_real_history_hint": "Импортированные циклы считаются собственной историей этого устройства и учитываются в статистике энергии/использования. Используйте для переноса одного прибора в новую установку.", - "import_history_description": "Умная розетка была у вас ещё до WashData? Загрузите экспорт истории её датчика мощности или считайте её прямо из Home Assistant, и обычное обнаружение пройдёт по этим данным, так что прошлые циклы появятся в списке «Циклы» и их можно будет назвать.", - "hist_input_hint": "Загрузите CSV, скачанный из панели «История» (объект, состояние, время изменения), или позвольте WashData считать историю датчика напрямую. Затем обнаружение пройдёт по этим данным точно так же, как в реальном времени, а вы выберете, какие из найденных циклов сохранить.", - "hist_recorder_hint": "Читает данные с выбранной даты по текущий момент. По умолчанию Home Assistant хранит подробную историю 10 дней, а после этого только часовые средние значения, которые слишком грубые для обнаружения циклов - выбирайте более раннюю дату, только если в настройках recorder задан больший срок хранения.", - "hist_scanning": "История воспроизводится через детектор. Это выполняется в фоне - можно закрыть это окно и вернуться позже.", + "dest_real_history_hint": "Импортированные циклы считаются собственной историей и статистикой этого устройства. Используйте при переносе прибора на новую установку.", + "import_history_description": "Умная розетка была у вас ещё до WashData? Загрузите её историю мощности или прочитайте её из Home Assistant. По ней пройдёт обнаружение, и прошлые циклы появятся в списке «Циклы», готовые к названию.", + "hist_input_hint": "Загрузите CSV из панели «История» (объект, состояние, время изменения) или позвольте WashData прочитать историю датчика напрямую. По ней пройдёт обнаружение как в реальном времени, а вы выберете, какие циклы оставить.", + "hist_recorder_hint": "Читает данные с выбранной даты до текущего момента. Home Assistant по умолчанию хранит подробную историю 10 дней; более старые часовые средние слишком грубы для обнаружения циклов.", + "hist_scanning": "Ваша история проходит через детектор в фоне. Можно закрыть это окно и вернуться позже.", "hist_imported_count": "Импортировано циклов: {n}.", "hist_duplicates": "Уже импортировано ранее и пропущено: {n}.", "hist_capped": "Достигнут лимит импортированных циклов для устройства; остальные не сохранены.", - "hist_next_step": "Они в списке «Циклы» с отметкой об импортированной истории. Откройте цикл и нажмите «Пометить», чтобы указать его программу.", + "hist_next_step": "Они в списке «Циклы» с отметкой импортированной истории. Откройте любой и кнопкой «Пометить» назовите его программу.", "hist_rows_read": "Прочитано показаний: {n}", "hist_breaks": "Пропусков, где датчик был недоступен: {n}", "hist_other_entity": "Показаний других объектов пропущено: {n}", @@ -1095,25 +1052,76 @@ "hist_scan_capped": "Показаны только первые кандидаты (найдено: {n}).", "hist_recorder_empty": "В Home Assistant нет подробной истории для этого датчика за указанный период.", "hist_scan_failed": "Сканирование не удалось.", - "hist_scan_expired": "Это сканирование больше недоступно. Выполните сканирование заново.", + "hist_scan_expired": "Это сканирование больше недоступно. Запустите сканирование снова.", "hist_import_failed": "Импорт не удался.", - "imported_history_readonly": "Обнаружено в импортированной истории мощности. Влияет на сопоставление программ, но не учитывается в вашей статистике; такой цикл нельзя обрезать или разделить. Нажмите «Пометить», чтобы указать программу.", - "odometer_intro": "Это общее количество только растёт. Удаление записи о цикле его не меняет, а подсчёт продолжается и за пределами лимита хранимой истории, поэтому напоминания об обслуживании остаются верными.", - "odometer_correct_hint": "Задайте значение, если WashData записал цикл, которого не было, или если прибор уже выполнял циклы до установки WashData.", + "imported_history_readonly": "Найдено в импортированной истории мощности. Помогает сопоставлению, но не учитывается в статистике, и его нельзя обрезать или разделить. Пометьте его, чтобы назвать программу.", + "odometer_intro": "Это число только растёт. Удаление цикла его не уменьшает, поэтому напоминания об обслуживании остаются верными.", + "odometer_correct_hint": "Задайте это, если WashData посчитал цикл, которого не было, или прибор работал до установки WashData.", "no_reminders_set": "Напоминания об обслуживании ещё не заданы.", "toast_program_armed": "Программа выбрана для следующего цикла: {program}", "toast_range_required": "Задайте корректный диапазон От/До", - "no_profiles_option": "(профилей пока нет, добавьте новый)" + "no_profiles_option": "(профилей пока нет, добавьте новый)", + "compare_cycles_title": "Сравнить циклы ({count})", + "edit_builtin_phase": "Это встроенная фаза. Сохранение создаёт собственную копию; удалите копию, чтобы вернуть оригинал.", + "fetch_error": "Не удалось загрузить данные.", + "connection_lost": "Соединение потеряно - показаны данные по состоянию на {time}.", + "pg_notify_partial": "Маркеры показывают только уведомления о старте, скором завершении, завершении, достижениях и тихих часах. Обновления в реальном времени, напоминания о выгрузке и предупреждения о превышении не моделируются.", + "connect_to_confirm": "Подключитесь в меню настроек (шестерёнка), чтобы подтверждать или оценивать.", + "coverage_gap_cluster": "Последние циклы без метки ({count}) длительностью около {min} мин похожи на одну программу, которую вы ещё не создали.", + "pg_label_yes": "В конце цикла получил бы метку «{name}»", + "pg_label_no": "Был бы отправлен на проверку: {reason}", + "live_match_debug_tip": "Достоверность: насколько точно текущая кривая мощности совпадает с лучшим профилем-кандидатом (0-100%). Неоднозначно: оценки двух лучших кандидатов различаются менее чем на 5%, поэтому метка остаётся неопределённой до завершения цикла.", + "automation_alias_started": "{device}: цикл запущен", + "automation_alias_finished": "{device}: цикл завершён", + "automation_desc_started": "Срабатывает при запуске цикла устройства {device}. Добавьте свои действия (уведомления, освещение, ...).", + "automation_desc_finished": "Срабатывает при завершении цикла устройства {device}. Добавьте свои действия (уведомления, освещение, ...).", + "feedback_detected": "WashData обнаружил: {name}", + "import_undo_hint": "Сохранена точка восстановления до последнего импорта ({when}): профилей: {profiles}, циклов: {cycles}.", + "replace_undo_hint": "Сначала сохраняется точка восстановления, поэтому этот импорт можно отменить в разделе «Экспорт / Импорт».", + "pg_in_sample_note": "Эти циклы помогли построить профили, с которыми они сравниваются, поэтому результаты оптимистичны: на новых циклах ожидайте чуть меньше верных совпадений.", + "hist_tail_trimmed": "Обрезано {min} мин ожидания после окончания цикла, так же как это делает работающая интеграция", + "hist_truncated": "остановлено на лимите показаний ({n}), поэтому остальная история не просмотрена", + "hist_warn_kw": "Максимальное показание всего {w}. Похоже, датчик передаёт киловатты; WashData нужны ватты, поэтому циклы обнаружить нельзя.", + "hist_warn_naive_time": "{n} отметок времени не содержат часового пояса и прочитаны как UTC. Если в файле местное время, циклы сдвинуты на ваше смещение от UTC, а показания около перевода часов могут потеряться.", + "hist_warn_naive_time_one": "{n} отметка времени не содержит часового пояса и прочитана как UTC. Если в файле местное время, циклы сдвинуты на ваше смещение от UTC, а показания около перевода часов могут потеряться.", + "hist_warn_naive_time_few": "{n} отметки времени не содержат часового пояса и прочитаны как UTC. Если в файле местное время, циклы сдвинуты на ваше смещение от UTC, а показания около перевода часов могут потеряться.", + "hist_warn_naive_time_many": "{n} отметок времени не содержат часового пояса и прочитаны как UTC. Если в файле местное время, циклы сдвинуты на ваше смещение от UTC, а показания около перевода часов могут потеряться.", + "hist_warn_naive_time_other": "{n} отметки времени не содержат часового пояса и прочитаны как UTC. Если в файле местное время, циклы сдвинуты на ваше смещение от UTC, а показания около перевода часов могут потеряться.", + "pg_compare_profile_tip": "Выбирает только профиль, отображаемый поверх графика. Симуляция всегда сама сопоставляет программу с циклом, как и работающая интеграция.", + "pg_outcome_stale": "Настройки изменились после запуска этой симуляции. Она всё ещё показывает старый результат.", + "pg_sweep_guarded": "Завершает больше циклов раньше времени, разбивает больше циклов или пропускает циклы, которые улавливает текущая настройка, поэтому никогда не рекомендуется.", + "pg_sweep_keep_current": "Оставить текущее значение ({value}, {metric}): ни одно значение не оказалось лучше хотя бы на один цикл.", + "pg_sweep_n_cycles": "на {count} циклах", + "pg_sweep_n_cycles_one": "на {count} цикле", + "pg_sweep_n_cycles_few": "на {count} циклах", + "pg_sweep_n_cycles_many": "на {count} циклах", + "pg_sweep_n_cycles_other": "на {count} цикла", + "pg_task_busy": "Для этого устройства уже идёт «Проверка на истории» или «Оптимизировать». Дождитесь окончания или отмените.", + "cycle_context_loading": "Загрузка истории из recorder…", + "cycle_context_note": "Серым: история датчика мощности из recorder вокруг этого цикла. Она не входит в цикл и не влияет на длительность, энергию и сопоставление.", + "cycle_context_none": "В recorder нет показаний вокруг этого цикла (по умолчанию Home Assistant хранит историю 10 дней).", + "cycle_context_tip": "Только для просмотра: показывает историю датчика мощности из recorder вокруг этого цикла, если она ещё хранится. Чтобы добавлять неудачные попытки запуска к будущим циклам, используйте Предзапись кривой (Настройки > Обнаружение > Запуск Цикла); это меняет сохранённые длительности.", + "invalid_threshold_pair": "Порог Остановки ({stop} Вт) должен быть ниже Порога Запуска ({start} Вт). Ничего не сохранено; измените оба значения вместе." }, "phase_desc": { "anti_crease": "Периодические короткие вращения барабана после завершения для уменьшения складок.", "anti_wrinkle": "Периодические короткие вращения барабана после завершения цикла для предотвращения сминания белья.", + "baking": "Нагрев на высокой температуре для корочки и мякиша.", + "cooking": "Основная фаза приготовления с нагревателем и вентилятором.", "cool_down": "Вращение без нагрева ближе к концу цикла.", + "cool_down_af": "Охлаждение только вентилятором после нагрева.", "drain_and_switch": "Переходный период от режима стирки к режиму сушки.", "dry": "Этап сушки с использованием нагревателя и/или остаточного тепла.", "drying": "Основной период нагреваемой сушки с вращением барабана.", "heat_up": "Первоначальный прогрев нагревателя перед началом полной сушки.", + "keep_warm": "Слабый нагрев, чтобы еда оставалась тёплой.", + "keep_warm_bm": "Слабый нагрев, чтобы хлеб оставался тёплым после выпечки.", + "kneading": "Мотор перемешивает и вымешивает тесто. Высокое потребление.", + "pause_af": "Короткая пауза, чтобы встряхнуть или проверить.", + "pre_heat": "Начальный прогрев камеры перед основным приготовлением.", "pre_rinse": "Первоначальное распыление перед стиркой моющим средством.", + "proving": "Расстойка при слабом нагреве: дрожжи бродят, тесто поднимается.", + "resting": "Короткая пауза на малой мощности между замесами, чтобы клейковина расслабилась.", "pre_wash": "Первоначальное замачивание или предварительная обработка перед основной стиркой.", "rinse": "Этап ополаскивания чистой водой. Эта фаза может повторяться несколько раз.", "sanitize": "Этап высокотемпературной очистки для программ санитарной обработки.", @@ -1126,6 +1134,7 @@ "wash_dw": "Основная моющая стирка с подогревом." }, "placeholder": { + "custom_task": "напр., Очистка уплотнения дверцы", "add": "добавить…", "group_name": "например, Хлопок 2:47", "json_buttons": "[{\"action\":\"ID\",\"title\":\"Label\"}]", @@ -1152,11 +1161,11 @@ }, "section": { "dishwasher": { - "intro": "Обработка завершения цикла для посудомоечных машин, которые обычно заканчиваются длительной фазой сушки почти без потребления энергии перед коротким финальным сливом.", + "intro": "Обработка завершения для посудомоечных машин, которые часто долго и тихо сушат перед коротким финальным сливом.", "label": "Посудомоечная машина" }, "anti_wrinkle": { - "intro": "Режим защиты от морщин определяет короткие импульсы вращения барабана на малой мощности после основной фазы и сохраняет их привязанными к завершённому циклу.", + "intro": "Оставляет короткие импульсы вращения барабана на малой мощности после основной фазы сушильной машины в составе завершённого цикла.", "label": "Защита от морщин" }, "basic": { @@ -1164,7 +1173,7 @@ "label": "Основное" }, "delay": { - "intro": "Обнаружение отложенного старта определяет, когда прибор включён, но ещё не начал цикл.", + "intro": "Обнаруживает прибор, который включён и ждёт отложенного старта.", "label": "Отложенный старт" }, "detection": { @@ -1188,157 +1197,149 @@ "label": "Таймер и Watchdog" }, "triggers": { - "intro": "Опциональные внешние сигналы: триггер завершения, датчик дверцы, переключатель паузы и напоминание о выгрузке.", + "intro": "Необязательные дополнения: триггер завершения, датчик дверцы, переключатель паузы и напоминание о выгрузке.", "label": "Триггеры и дверцы" - }, - "phase_eta": { - "label": "Оставшееся время", - "intro": "Оценка оставшегося времени с учётом фаз, для машин, длительность цикла которых зависит от температуры или отжима." } }, "setting": { "dishwasher_end_spike_quiet_release": { - "doc": "После того как цикл превышает ожидаемую продолжительность, сколько времени посудомоечная машина должна оставаться неактивной (ниже «Порог Остановки»), прежде чем WashData прекратит ждать финальный слив и завершит цикл. Увеличьте значение, если у вашей машины длительная тихая фаза сушки перед поздним финальным сливом, который пропускается - более широкое окно позволяет изученной продолжительности следовать за сезонными изменениями (более холодная входящая вода = более длинные циклы), а не фиксироваться на старом среднем значении. Оно только сокращает ожидание относительно внутреннего 30-минутного лимита ожидания завершающего всплеска (откачки воды), но никогда не увеличивает его.", + "doc": "После ожидаемой длительности: сколько посудомоечная машина должна оставаться ниже Порога Остановки, прежде чем WashData перестанет ждать финального слива. Увеличьте, если поздний слив после долгой сушки пропускается. Ожидание никогда не превышает 30 минут.", "label": "Тихое завершение пассивной сушки" }, "anti_crease_finalize_ratio": { - "doc": "Насколько глубоко в ожидаемую длительность сопоставленной программы должен зайти цикл, прежде чем WashData сможет счесть наблюдаемое вращение барабана на малой мощности хвостом защиты от морщин и завершить цикл на этом месте. Ожидаемая длительность - это среднее значение программы, поэтому на сушильной машине, время сушки по датчику влажности у которой зависит от загрузки, большинство запусков никогда не достигает 98% этого значения, и хвост тянется до самого резервного тайм-аута - часто через 15-40 минут после того, как машина фактически остановилась. На такой сушильной машине уменьшите значение (напр., 0.75). Для стиральных машин оставьте значение по умолчанию: стиральная машина проводит большую часть цикла ниже настройки Макс. Мощность Защиты от Морщин, поэтому эта доля - единственное, что отделяет тихую фазу середины стирки от хвоста, и её снижение может сократить стирку вдвое. Оставьте пустым для значения по умолчанию (0.98). Это не то же самое, что Коэффициент умного завершения, который управляет отдельной проверкой досрочного завершения.", + "doc": "Доля средней длительности программы, которую цикл должен пройти, прежде чем вращение барабана на малой мощности будет считаться хвостом защиты от морщин. Уменьшите (например, 0,75) для сушильных машин, подстраивающихся под загрузку. Для стиральных машин оставьте значение по умолчанию (0,98), иначе стирка может закончиться на середине.", "label": "Коэффициент завершения защиты от морщин" }, "anti_wrinkle_enabled": { - "doc": "Распознавайте короткие импульсы низкой мощности, которые сушильная машина излучает после основной фазы нагрева, и сохраняйте их привязанными к завершенному циклу, а не воспринимайте их как новые циклы.", + "doc": "Оставлять короткие импульсы вращения на малой мощности после основной фазы нагрева сушильной машины в составе завершённого цикла, а не считать их новыми циклами.", "label": "Включить Защиту от Морщин" }, "anti_wrinkle_exit_power": { - "doc": "Чтобы режим защиты от морщин оставался активным, мощность должна падать ниже этого значения между импульсами.", + "doc": "В режиме защиты от морщин мощность ниже этого значения считается паузой между импульсами вращения, а пауза дольше Макс. Паузы Между Импульсами завершает режим. Если Порог Остановки выше, используется он, поэтому настройка действует только выше Порога Остановки.", "label": "Порог Мощности Выхода" }, "anti_wrinkle_idle_timeout": { "label": "Макс. Пауза Между Импульсами", - "doc": "Как долго машина может оставаться в тишине между двумя импульсами вращения барабана, прежде чем режим защиты от морщин завершится. Установите выше самой длинной паузы, которую ваша сушильная машина оставляет между импульсами, иначе каждый последующий импульс будет воспринят как ложный запуск." + "doc": "Сколько сушильная машина может молчать между импульсами вращения, прежде чем режим защиты от морщин закончится. Задайте больше самой длинной паузы вашей машины, иначе следующие импульсы будут считаться ложными стартами." }, "anti_wrinkle_max_duration": { - "doc": "Пульс, превышающий этот, рассматривается как настоящий цикл, а не как борьба с морщинами.", + "doc": "Импульс длиннее этого считается настоящим циклом, а не вращением защиты от морщин.", "label": "Макс. Длительность" }, "anti_wrinkle_max_power": { - "doc": "Импульс выше этой мощности рассматривается как настоящий новый цикл, а не как борьба с морщинами. Установите чуть выше мощности вихревого импульса.", + "doc": "Импульс выше этой мощности считается настоящим новым циклом, а не вращением барабана. Задайте чуть выше мощности импульсов вращения.", "label": "Макс. Мощность Защиты от Морщин" }, "auto_label_confidence": { - "doc": "Если счет совпадения в конце цикла равен этому значению или превышает его, программа помечается автоматически без запроса подтверждения. Поднимите его, чтобы требовать большей уверенности перед автоматической маркировкой; опустите его, чтобы автоматизировать больше. Работает вместе с элементом «Уверенность в обучении», расположенным под ним.", + "doc": "Оценка совпадения в конце цикла, при которой или выше которой программа помечается без вопроса. Повысьте для большей уверенности; понизьте, чтобы автоматически помечать больше циклов. Держите выше Уверенности Обучения.", "label": "Уверенность Авто-Маркировки" }, "auto_maintenance": { - "doc": "Выполняйте ежевечернюю уборку: перестраивайте конверты профилей, пересчитывайте работоспособность циклов, удаляйте следы отладки и сохраняйте самые последние циклы.", + "doc": "Ночная уборка: помечает старые циклы, которые теперь явно соответствуют программе, пересобирает кривые программ, обновляет отметки аномалий и состояние циклов и удаляет отладочные данные, если «Сохранять Отладочные Данные» выключено. Циклы и их записи мощности всегда сохраняются.", "label": "Автоматическое Обслуживание (ночная уборка)" }, "completion_min_seconds": { - "doc": "Циклы короче этого значения отбрасываются как циклы-призраки (тестовые прогоны, открытие дверцы для добавления носка).", + "doc": "Циклы короче этого (пробные запуски, открыли дверцу, чтобы докинуть носок) сохраняются как прерванные и не получают уведомления о завершении.", "label": "Минимальная Длительность Цикла" }, "delay_confirm_seconds": { - "doc": "Электропитание должно оставаться в режиме ожидания в течение этого времени, прежде чем устройство будет считаться ожидающим запуска, а не работающим.", + "doc": "Мощность должна столько времени оставаться в полосе ожидания, прежде чем прибор будет считаться ожидающим старта.", "label": "Секунды Подтверждения" }, "delay_start_detect_enabled": { - "doc": "Обнаруживает, когда устройство включено и находится в режиме ожидания (отложенный запуск/режим ожидания), но еще не начало цикл, поэтому отключение в режиме ожидания не принимается за рабочий цикл.", + "doc": "Обнаруживать прибор, который включён и ждёт отложенного старта, чтобы его потребление в ожидании не принималось за работающий цикл.", "label": "Включить Обнаружение Отложенного Старта" }, "delay_timeout_hours": { - "doc": "Прекратите ожидание в режиме отложенного запуска по истечении этого количества часов и вернитесь в режим ожидания, чтобы машина, оставленная под напряжением, но так и не запущенная, не ждала вечно.", + "doc": "Через столько часов перестать ждать отложенного старта и вернуться в режим ожидания.", "label": "Часы Таймаута" }, "device_type": { - "doc": "Класс прибора. Устанавливает разумные настройки обнаружения по умолчанию (пороговые значения, задержка выключения, обработка завершения), настроенные для этого типа устройства; меняйте его только в том случае, если изначально устройство было настроено как неправильный тип.", + "doc": "Задаёт параметры обнаружения по умолчанию (пороги, задержка выключения, обработка завершения) для этого типа прибора. Меняйте, только если устройство настроено с неверным типом.", "label": "Тип Устройства" }, "door_sensor_entity": { - "doc": "Дополнительный бинарный датчик двери. Используется для определения того, когда прибор открывался/разгружался после цикла.", + "doc": "Необязательный датчик дверцы. Показывает, когда прибор открыли для выгрузки после цикла.", "label": "Объект Датчика Дверцы" }, "duration_tolerance": { - "doc": "Допуск к оценкам оставшегося времени (обратная связь обучения, а не сопоставление). Если фактическая продолжительность находится в пределах +/-X% от оценки, это считается хорошим совпадением.", + "doc": "Только для обратной связи обучения: цикл в пределах +/-X% от оценки длительности считается хорошей оценкой. Не влияет на сопоставление.", "label": "Допуск Оценки" }, "enable_ml_models": { - "doc": "Пока цикл выполняется, позвольте моделям уточнить результаты в реальном времени: более стабильную оценку оставшегося времени и энергии/затрат, а также защиту от преждевременной остановки при обнаружении окончания (она может только откладывать завершение, никогда не заканчивать его раньше и ограничена). Использует ваши точно настроенные модели, если они доступны, в противном случае – встроенные. Выкл. = только классическая силовая логика (все еще надежная).", + "doc": "Использовать модель, обученную на этой машине, для прогноза энергии и стоимости идущего цикла и позволять хорошо знакомой программе, остановившейся необычно рано, завершаться без полной задержки выключения. Выключено: только логика по мощности.", "label": "Применять умные модели во время цикла" }, "end_energy_threshold": { - "doc": "Во время обратного отсчета задержки отключения накопленная энергия (ватты x время) сравнивается с этим порогом. В случае превышения обратный отсчет сбрасывается: барабаны для предотвращения сминания и хвостики для сушки в посудомоечной машине остаются прикрепленными к циклу, а не обрезаются. Поднимите его, если циклы заканчиваются слишком рано во время охлаждения; опустите его, если обнаружение происходит медленно.", + "doc": "Энергия, допустимая во время задержки выключения. Если больше, цикл продолжается, и вращения и финальная сушка остаются его частью. Уменьшите, если циклы заканчиваются на хвосте; увеличьте, если завершённые циклы продолжают ждать.", "label": "Энергия Завершения" }, - "end_repeat_count": { - "doc": "Количество последовательных показаний ниже стоп-порога, необходимых до завершения цикла. 1 подходит для большинства вилок. Увеличьте значение до 2–3, если ваша интеллектуальная розетка иногда сообщает о ложном нулевом значении выборки в середине цикла, и ваши циклы заканчиваются преждевременно.", - "label": "Количество Повторений Завершения" - }, "energy_price_entity": { - "doc": "Сенсор с текущей ценой электроэнергии за кВт·ч (например, динамический тариф). Это должна быть цена, а не собственный объект мощности или энергии розетки: он имеет приоритет над фиксированной ценой ниже, поэтому счётчик кВт·ч, указанный здесь, считает каждый цикл по показанию счётчика вместо вашего тарифа. Когда включена «Стоимость, взвешенная по времени», каждый цикл рассчитывается по цене, действовавшей в каждый момент его работы; иначе за ним закрепляется цена, действовавшая на момент завершения.", + "doc": "Датчик текущей цены за кВт·ч (например, динамический тариф). Имеет приоритет над фиксированной ценой ниже, поэтому это должна быть цена, а не датчик мощности или энергии розетки.", "label": "Объект Цены на Электроэнергию" }, "energy_price_static": { - "doc": "Фиксированная цена за кВтч, используемая для расчета себестоимости, если выше не указан объект реальной цены.", + "doc": "Фиксированная цена за кВт·ч, используется, если объект цены не задан.", "label": "Фиксированная Цена Электроэнергии (за кВт·ч)" }, "energy_sensor": { - "doc": "Необязательный накопительный счётчик энергии (total_increasing kWh/Wh, например, собственный счётчик суммарного потребления розетки). Если задан, энергия, отображаемая для каждого цикла, берётся из разности показаний этого счётчика между началом и концом, что позволяет избежать занижения, возникающего при интегрировании медленно обновляющегося датчика мощности. Если показание отсутствует, единица измерения неизвестна или разность не положительна, используется интегрированное значение. Оставьте пустым, чтобы продолжать интегрировать датчик мощности.", + "doc": "Необязательный счётчик кВт·ч или Вт·ч (total_increasing, например собственный счётчик розетки). Энергия цикла тогда берётся из него, что точнее расчёта по медленному датчику мощности. Если его показание непригодно, используется расчётное значение.", "label": "Объект Счётчика Энергии" }, "expose_debug_entities": { - "doc": "Публикуйте дополнительные диагностические объекты высокой доступности (достоверность соответствия, неоднозначность, внутреннее состояние). Выкл. сохраняет список объектов чистым для нормального использования.", + "doc": "Добавить диагностические объекты (уверенность сопоставления, неоднозначность, внутреннее состояние). Выключено: список объектов остаётся чистым.", "label": "Открыть Отладочные Объекты" }, "external_end_trigger": { - "doc": "Бинарный датчик, изменение состояния которого указывает на окончание цикла (например, «завершенный» контакт устройства или интеграция сопутствующего устройства).", + "doc": "Бинарный датчик, изменение которого отмечает конец цикла (например, контакт «готово» или другая интеграция).", "label": "Объект Внешнего Триггера" }, "external_end_trigger_enabled": { - "doc": "Пусть внешний двоичный датчик сигнализирует об окончании цикла в дополнение к встроенному обнаружению на основе мощности.", + "doc": "Позволить бинарному датчику сообщать о конце цикла в дополнение к обнаружению по мощности.", "label": "Включить Внешний Триггер Завершения" }, "external_end_trigger_inverted": { - "doc": "Считайте выключение датчика триггера (а не включение) сигналом окончания цикла.", + "doc": "Считать сигналом конца ВЫКЛЮЧЕНИЕ датчика (а не включение).", "label": "Инвертировать Внешний Триггер (срабатывать на ВЫКЛ)" }, "learning_confidence": { - "doc": "Если оценка соответствия находится между этим значением и «Уверенностью Авто-Маркировки», уведомление обратной связи попросит вас подтвердить определённую программу. Ниже этой оценки соответствие слишком неопределённое, чтобы его показывать. Должно оставаться ниже «Уверенности Авто-Маркировки».", + "doc": "Минимальная оценка совпадения для маркировки завершённого цикла. Явный победитель маркируется; спорный случай попадает в очередь проверки во вкладке «Циклы». Ниже этого значения ничего не маркируется и не спрашивается. Держите ниже Уверенности Авто-Маркировки.", "label": "Уверенность Обучения" }, "linked_device": { - "doc": "При необходимости вложите это устройство WashData в другое устройство (например, интеллектуальную розетку) в реестре устройств высокой доступности, показанное как «Подключено через...».", + "doc": "Необязательно. Показывать это устройство WashData под другим устройством (например, умной розеткой) в Home Assistant как «Подключено через ...».", "label": "Группировать под Устройством" }, "min_off_gap": { - "doc": "Если машина выключается на время меньшее, чем это время, последовательность включения/выключения/включения рассматривается как один непрерывный цикл. Предотвращает разделение программ замачивания (машина выключается на несколько минут во время стирки) на два отдельных цикла. Установите его короче, чем промежуток между последовательными нагрузками, если вы хотите, чтобы они считались отдельными циклами. Значения по умолчанию для типа устройства защищают типичную паузу внутри цикла для каждого устройства.", + "doc": "Выключение короче этого не делит цикл, поэтому паузы замачивания остаются в одной стирке. Держите меньше паузы между загрузками подряд, иначе они сольются в один цикл.", "label": "Минимальная Пауза Выключения" }, "min_power": { - "doc": "Абсолютная минимальная мощность считается активной. Показания ниже этого значения считаются 0 Вт (режим ожидания), отфильтровывая фантомную нагрузку интеллектуальных розеток и светодиодов режима ожидания.", + "doc": "Показания ниже этого значения считаются 0 Вт (ожидание). Отсекает холостое потребление умных розеток и индикаторов ожидания.", "label": "Минимальная Мощность" }, "ml_training_enabled": { - "doc": "Периодически изучайте проверенные циклы в течение ночи и настраивайте модели для этой конкретной машины. Изменение сохраняется только в том случае, если оно действительно дает лучшие результаты в затянувшихся циклах, поэтому это может только помочь или остаться прежним, а не регрессировать.", + "doc": "Время от времени переобучать модель энергии на завершённых циклах этой машины. Новая модель остаётся, только если на тестовых циклах она лучше простой оценки.", "label": "Обучаться на этой машине" }, "ml_training_hour": { - "doc": "Местный час дня (0-23), чтобы выполнить ночную точную настройку. Выберите тихий час, например 2 (02:00).", + "doc": "Час суток (0-23) для ночного обучения. Выберите тихий час, например 2.", "label": "Обучаться в час" }, "ml_training_interval_days": { - "doc": "Повторно проверяйте наличие улучшений не чаще одного раза за указанное количество дней.", + "doc": "Переобучать не чаще одного раза за это число дней.", "label": "Проверять не чаще раза в" }, "ml_training_min_cycles": { - "doc": "Прежде чем приступить к точной настройке, подождите, пока будет записано хотя бы это количество циклов, чтобы было чему поучиться.", + "doc": "Перед обучением дождаться хотя бы такого числа записанных циклов.", "label": "Сначала нужно циклов" }, "name": { - "doc": "Отображаемое имя, отображаемое в списке интеграций высокой доступности и реестре устройств.", + "doc": "Имя этого устройства в Home Assistant.", "label": "Имя Устройства" }, "no_update_active_timeout": { - "doc": "Если во время работы в течение этого времени не поступают обновления питания, предположим, что вилка отключена от сети, и принудительно остановите ее, чтобы избежать цикла зомби. Значение по умолчанию 600 с учитывает задержку облака или сетки.", + "doc": "Если за это время во время цикла не пришло ни одного обновления мощности, считается, что розетка отключилась, и цикл принудительно останавливается. 600 с по умолчанию учитывают задержки облака или mesh-сети.", "label": "Таймаут Без Обновления" }, "notify_before_end_minutes": { @@ -1346,35 +1347,35 @@ "label": "Оповещение Перед Завершением" }, "notify_channel": { - "doc": "Название канала уведомлений Android для стартовых/живых сообщений (управляет звуком и приоритетом каждого канала в мобильном приложении). Пустой использует сопутствующее приложение по умолчанию.", + "doc": "Канал Android для сообщений о старте и в реальном времени; канал задаёт звук и приоритет. Пусто: канал приложения по умолчанию.", "label": "Канал Android (запуск/реальное время)" }, "notify_cycle_timers": { - "doc": "Уведомления в определенные минуты цикла (например, о добавлении смягчителя). Сообщение поддерживает {device}, {program}, {minutes}. Включите автоматическую паузу, чтобы сделать паузу в этот момент и получить интерактивное уведомление с помощью кнопки возобновления; возобновить работу через панель, службу паузы/возобновления или действие уведомления.", + "doc": "Уведомления на заданных минутах цикла (например, чтобы добавить кондиционер). Сообщение поддерживает {device}, {program}, {minutes}. С авто-паузой цикл там же ставится на паузу, а уведомление получает кнопку «Продолжить».", "label": "Таймеры Цикла" }, "notify_finish_channel": { - "doc": "Название канала уведомлений Android для сообщения о завершении. Пустой повторно использует стартовый/живой канал.", + "doc": "Канал Android для сообщения о завершении. Пусто: используется канал старта и реального времени.", "label": "Канал Android (завершение)" }, "notify_finish_message": { - "doc": "Тело, отправляемое при завершении цикла. Переменные шаблона: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}. {time_finished} и {vs_typical} наиболее полезны здесь.", + "doc": "Текст по завершении цикла. Переменные: {device}, {program}, {duration} (минуты), {duration_hm} (1 h 05 min), {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}, {status} (completed или force_stopped; прерванные циклы ничего не отправляют). При любой другой переменной используется текст по умолчанию.", "label": "Сообщение о Завершении" }, "notify_finish_services": { - "doc": "notify.* службы, вызываемые при завершении цикла. Добавьте по одному на каждую цель; оставьте пустым, чтобы не было уведомления о завершении.", + "doc": "Службы notify.*, вызываемые по завершении цикла. Оставьте пустым, чтобы не отправлять.", "label": "Службы Завершения" }, "notify_fire_events": { - "doc": "Также запускайте события ha_washdata_* в начале/окончании цикла, чтобы вы могли создавать свои собственные средства автоматизации.", + "doc": "Также генерировать события ha_washdata_* при старте и завершении цикла для своих автоматизаций.", "label": "Отправлять События HA для Уведомлений" }, "notify_icon": { - "doc": "Небольшой значок, отображаемый в уведомлении (например, mdi:washing-machine). Android рисует его в строке состояния; iOS показывает его вместо значка приложения, и для этого нужно сопутствующее приложение 2026.8 или новее. Оставьте пустым, чтобы использовать значок по умолчанию для платформы.", + "doc": "Маленький значок в уведомлении (например, mdi:washing-machine). Отправляется только на цели mobile_app, не в группы notify. В iOS заменяет значок приложения и требует мобильное приложение 2026.8 или новее. Пусто: значок платформы по умолчанию.", "label": "Иконка Уведомления" }, "notify_live_chronometer": { - "doc": "Показывать в уведомлении таймер обратного отсчета с обновлением в реальном времени (на платформах, которые его поддерживают) вместо статической оценки.", + "doc": "Показывать в уведомлении живой обратный отсчёт (где поддерживается) вместо фиксированной оценки.", "label": "Использовать Хронометр в Реальном Времени" }, "notify_live_interval_seconds": { @@ -1382,139 +1383,131 @@ "label": "Интервал Обновления в Реальном Времени" }, "notify_live_overrun_percent": { - "doc": "Если цикл превышает свою оценку более чем на этот процент, отправьте предупреждение о превышении.", + "doc": "Живые обновления прекращаются, когда цикл превышает ожидаемую длительность на этот процент (20 = до 1,2x ожидаемого времени).", "label": "% Превышения До Оповещения" }, "notify_live_services": { - "doc": "notify.* службы вызывают обновления хода выполнения в реальном времени во время выполнения цикла. Оставьте пустым, чтобы отключить уведомления о прогрессе в реальном времени.", + "doc": "Службы notify.* для обновлений прогресса в реальном времени во время цикла. Оставьте пустым, чтобы выключить их.", "label": "Службы Обновления в Реальном Времени" }, "notify_only_when_home": { - "doc": "Отправляйте уведомления только тогда, когда хотя бы один из связанных людей (выше) находится дома.", + "doc": "Уведомлять, только когда хотя бы один из людей выше дома.", "label": "Уведомлять Только Когда Дома" }, "notify_people": { - "doc": "person.* сущности, используемые функцией «Уведомлять только когда дома», чтобы определить, находится ли кто-нибудь дома.", + "doc": "Объекты person.*, которые проверяет «Уведомлять Только Когда Дома».", "label": "Люди (для Только Когда Дома)" }, "notify_pre_complete_message": { - "doc": "Тело оповещения «почти готово» / перед завершением. Переменные шаблона: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Текст каждого живого обновления прогресса. Переменные шаблона: {device}, {program}, {minutes} (осталось минут). При любой другой переменной используется текст по умолчанию.", "label": "Сообщение Перед Завершением" }, "notify_reminder_message": { - "doc": "Тело напоминания о том, что бельё ещё не выгружено. Пустое значение использует встроенное по умолчанию. Переменные шаблона: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Текст разового Оповещения Перед Завершением. Переменные шаблона: {device}, {program}, {minutes} (настройка Оповещение Перед Завершением). При любой другой переменной используется текст по умолчанию.", "label": "Напоминание" }, "notify_start_message": { - "doc": "Тело, отправляемое при запуске цикла. Переменные шаблона: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Текст при запуске цикла. Переменные шаблона: {device}, {program}. При любой другой переменной используется текст по умолчанию.", "label": "Сообщение о Запуске" }, "notify_start_services": { - "doc": "notify.* службы, вызываемые при запуске цикла. Добавьте по одному на каждую цель (телефон, панель управления и т. д.); оставьте пустым, чтобы не было уведомления о запуске.", + "doc": "Службы notify.*, вызываемые при старте цикла (телефон, дашборд, ...). Оставьте пустым, чтобы не отправлять.", "label": "Службы Запуска" }, "notify_timeout_seconds": { - "doc": "Автоматически закрывать уведомление по истечении этого количества секунд (на платформах, которые его поддерживают). 0 сохраняет его до тех пор, пока не будет закрыт вручную.", + "doc": "Закрыть уведомление через столько секунд (где поддерживается). 0 оставляет его, пока вы его не закроете.", "label": "Авто-Закрытие Через" }, "notify_title": { - "doc": "Название уведомления. Переменные шаблона: {device}, {program}, {duration}, {energy}, {cost}, {date}, {time}, {minutes}.", + "doc": "Заголовок уведомления. Подставляется только {device}; при любой другой переменной используется заголовок по умолчанию.", "label": "Заголовок Уведомления" }, "notify_unload_delay_minutes": { - "doc": "Через несколько минут после окончания цикла перед отправкой все еще ожидающего напоминания «разгрузить машину». Установите 0, чтобы отключить напоминание.", + "doc": "Минуты после окончания цикла до напоминания «выгрузите машину». 0 выключает его.", "label": "Задержка Напоминания о Выгрузке" }, "notify_unload_repeat": { - "doc": "Продолжать отправлять напоминание о выгрузке каждые «Задержка Напоминания о Выгрузке» минут, пока выгрузка не будет подтверждена или пока вы не нажмёте «Не напоминать» в уведомлении. Нужен способ подтвердить выгрузку: Объект Датчика Дверцы, Объект Подтверждения Выгрузки или параметр «Подтверждать Выгрузку Вручную» ниже. Кнопка отклонения работает в уведомлениях мобильного приложения Home Assistant (companion).", + "doc": "Повторять напоминание о выгрузке каждые «Задержка Напоминания о Выгрузке» минут, пока выгрузка не подтверждена или вы не нажмёте «Не напоминать» (мобильное приложение). Нужен Объект Датчика Дверцы, Объект Подтверждения Выгрузки или «Подтверждать Выгрузку Вручную».", "label": "Повторять, пока не выгружено" }, "off_delay": { - "doc": "Время ожидания после отключения питания, прежде чем объявить цикл завершенным. Если подача электроэнергии возобновится в пределах этого окна, цикл продолжится плавно – это устраняет паузы между этапами стирки. Посудомоечные машины имеют длительные фазы сушки (отключение питания на 20–60 минут), поэтому задержка выключения должна превышать это значение, чтобы вся стирка+сушка выполнялась как один цикл.", + "doc": "Сколько мощность должна оставаться низкой, прежде чем цикл завершится. Если мощность вернётся за это время, цикл продолжится. Посудомоечные машины могут простаивать 20-60 мин во время сушки, поэтому задайте больше.", "label": "Задержка Выключения" }, "pause_cuts_power": { - "doc": "Когда цикл приостановлен, также отключите объект Switch ниже. Только для приборов, вилку которых можно безопасно отрезать в середине цикла.", + "doc": "При паузе цикла также выключать Объект Переключателя ниже. Только для приборов, которые могут безопасно потерять питание посреди цикла.", "label": "Пауза Отключает Питание (через переключатель)" }, "power_off_delay": { - "doc": "Как долго мощность должна оставаться ниже «Порог Выключения Питания» после завершения цикла, прежде чем состояние вернётся к «Выключено». Используется только когда «Порог Выключения Питания» больше 0. Проверяется с фоновой периодичностью, поэтому фактическая задержка округляется вверх до следующего момента истечения состояния.", + "doc": "Сколько мощность после цикла должна оставаться ниже Порога Выключения Питания, прежде чем состояние вернётся в «Выключено». Используется, только если этот порог больше 0. Проверяется периодически, поэтому реальное ожидание может быть чуть дольше.", "label": "Задержка Выключения Питания" }, "power_off_threshold_w": { - "doc": "Необязательное определение состояния «Выключено» по мощности. Если значение больше 0, то после завершения цикла, когда мощность остаётся ниже этого уровня в течение времени «Задержка Выключения Питания», машина считается выключенной, и состояние возвращается к «Выключено». Оставьте 0, чтобы отключить (по умолчанию). Задайте значение выше реального уровня выключенного прибора, но ниже «Порог Остановки» и ниже потребления машины в режиме ожидания после завершения; если значение не ниже «Порог Остановки», оно игнорируется. Когда параметр включён, он заменяет «Задержка Сброса Прогресса» для возврата к «Выключено», поэтому завершившая работу машина остаётся в состоянии «Завершено»/«Чистый», пока её действительно не выключат.", + "doc": "После цикла мощность ниже этого значения в течение Задержки Выключения Питания переводит состояние в «Выключено» вместо Задержки Сброса Прогресса. Задайте выше потребления выключенного прибора, но ниже ожидания и Порога Остановки. 0 выключает (по умолчанию).", "label": "Порог Выключения Питания" }, "power_sensor": { - "doc": "Объект-датчик, сообщающий о активной мощности в ваттах для этого устройства (например, sensor.washer_power). Все обнаружение цикла основано на этом сигнале.", + "doc": "Датчик текущей мощности этого прибора в ваттах (например, sensor.washer_power). На нём основано всё обнаружение.", "label": "Датчик Мощности" }, - "profile_duration_tolerance": { - "doc": "Полоса +/- вокруг средней продолжительности профиля, используемая во время сопоставления. 0,25 означает, что 60-минутный профиль соответствует 45-75-минутным циклам.", - "label": "Допуск Длительности Профиля" - }, "profile_match_interval": { - "doc": "Как часто предпринимать попытки сопоставления профилей во время рабочего цикла. Значение по умолчанию 300 с (5 минут) обеспечивает баланс между скоростью обнаружения и процессором.", + "doc": "Как часто сопоставлять идущий цикл с профилями. По умолчанию 300 с (5 мин).", "label": "Интервал Совпадения" }, "profile_match_max_duration_ratio": { - "doc": "Максимальная длина цикла относительно профиля. 1.3 означает, что для соответствия цикл должен составлять менее 130 % от продолжительности профиля.", + "doc": "Программа отбрасывается, когда цикл длится дольше этого кратного её обычной длительности. 1,8: цикл на 80% длиннее программы больше с ней не сопоставляется.", "label": "Максимальное Соотношение Длительности" }, "profile_match_min_duration_ratio": { - "doc": "Минимальная длина цикла относительно профиля. 0,9 означает, что для соответствия цикл должен составлять не менее 90 % продолжительности профиля.", + "doc": "Программа рассматривается только после того, как цикл прошёл эту долю её обычной длительности. 0,10: 2-часовая программа становится кандидатом через 12 минут. Не действует в первые 15 минут цикла.", "label": "Минимальное Соотношение Длительности" }, "profile_match_threshold": { - "doc": "Минимальный балл сходства (0-1), необходимый в конце цикла для принятия идентификации программы. Поднимите его, чтобы уменьшить количество ошибочных идентификаций; опустите его, если программы вашей машины не совпадают. Значение по умолчанию 0,4 является консервативной отправной точкой.", + "doc": "Оценка совпадения в реальном времени, которая нужна циклу, прежде чем Умное завершение или завершение защиты от морщин смогут закончить его раньше. Не меняет показанную программу. Повысьте, если слабые совпадения заканчивают циклы рано; понизьте, если явные совпадения ждут задержку выключения.", "label": "Порог Совпадения" }, "profile_unmatch_threshold": { - "doc": "Если соответствие в реальном времени в середине цикла опускается ниже этой оценки, предварительное определение сбрасывается. Держите его немного ниже «Порога Совпадения», чтобы кратковременное снижение сходства не вернуло отображение к состоянию «нет совпадения».", + "doc": "Если оценка совпадения в реальном времени падает ниже этого значения, текущая догадка о программе сбрасывается. Держите немного ниже Порога Совпадения, чтобы краткий провал её не сбрасывал.", "label": "Порог Несовпадения" }, "progress_reset_delay": { - "doc": "После завершения удерживайте прогресс на уровне 100% в течение этого времени, чтобы значение «Завершено» было видно на информационных панелях, прежде чем перейти в режим ожидания.", + "doc": "После цикла держать прогресс на 100% это время, прежде чем сбросить в «Ожидание».", "label": "Задержка Сброса Прогресса" }, "pump_stuck_duration": { - "doc": "Секунды, в течение которых насос может работать непрерывно, прежде чем он будет помечен как возможно застрявший (вызовет событие застревания насоса).", + "doc": "Сколько насос может работать без перерыва, прежде чем сработает событие застрявшего насоса.", "label": "Длительность Застревания Насоса" }, "sampling_interval": { - "doc": "Ожидаемое время между показаниями датчика – используется для определения размера окна сглаживания и правильного запуска устранения дребезга. Каждое обновление датчика фиксируется независимо от этого значения; он только калибрует последующие вычисления. Механизм подсказок измеряет фактическую частоту вращения педалей вашего датчика на основе прошлых циклов и устанавливает ее автоматически.", + "doc": "Минимальное время между обрабатываемыми показаниями. Показания, пришедшие раньше, пропускаются (низкие показания во время цикла сохраняются всегда). Задайте интервал отчётов вашей розетки или меньше; большее значение только отбрасывает показания.", "label": "Интервал Выборки" }, "save_debug_traces": { - "doc": "Сохраняйте трассировку полной мощности и соответствующие данные отладки для каждого цикла. Полезно для устранения неполадок, но увеличивает размер хранилища.", + "doc": "Также сохранять с каждым циклом отладочные данные сопоставления (оценки всех кандидатов) для поиска проблем. Добавляет около 20 КБ на цикл.", "label": "Сохранять Отладочные Данные" }, - "smoothing_window": { - "doc": "Насколько сглаживается необработанный сигнал мощности. Низкий (2) отзывчивый, но шумный; высокий (5) сглаживает пики, но добавляет задержку.", - "label": "Окно Сглаживания" - }, "start_duration_threshold": { - "doc": "Мощность должна оставаться выше порогового значения запуска в течение этого времени, чтобы подтвердить реальный запуск, предотвращая запуск цикла с помощью переключателей включения/выключения, рассчитанных на долю секунды.", + "doc": "Мощность должна столько времени оставаться выше Порога Запуска, прежде чем начнётся цикл, чтобы короткие включения и выключения игнорировались.", "label": "Длительность Запуска" }, "start_energy_threshold": { - "doc": "Энергия (мощность x время), которую прибор должен потребить перед ЗАПУСКОМ. Короткий мощный всплеск имеет очень низкую энергию и игнорируется, что предотвращает фальстарт.", + "doc": "Энергия, которую прибор должен потребить, прежде чем цикл будет считаться запущенным. Короткий скачок мощности потребляет мало энергии и цикл не запускает.", "label": "Энергия Запуска" }, "start_threshold_w": { - "doc": "Мощность должна подняться выше этого уровня, чтобы подтвердить начало цикла. Установка слишком низкого значения приведет к ложному запуску резервного питания; слишком высокие и медленно запускающиеся программы (холодная заливка) пропускаются. Механизм предложения устанавливает это значение чуть выше наблюдаемой самой низкой активной мощности машины.", + "doc": "Мощность должна подняться выше этого значения, чтобы начался цикл. Слишком низко: потребление в ожидании вызывает ложные старты. Слишком высоко: медленно стартующие программы (холодный набор воды) пропускаются.", "label": "Порог Запуска" }, "stop_threshold_w": { - "doc": "Мощность должна упасть ниже этого уровня, прежде чем начнется обратный отсчет задержки выключения. Установите его ниже Start Threshold – промежуток между ними и есть полоса гистерезиса, предотвращающая мерцание. Если установлено слишком высокое значение, фазы низкой мощности (задержка полоскания, предотвращение сминания) ошибочно запускают последовательность завершения.", + "doc": "Мощность должна упасть ниже этого значения, прежде чем начнётся отсчёт задержки выключения. Держите ниже Порога Запуска. Если слишком высоко, фазы малой мощности (паузы полоскания, защита от морщин) по ошибке запускают отсчёт завершения.", "label": "Порог Остановки" }, "switch_entity": { - "doc": "Дополнительный переключатель, который выключается при паузе и снова включается при возобновлении, используется вместе с надписью «Пауза также отключает питание».", + "doc": "Необязательный переключатель, который выключается при паузе и снова включается при продолжении, вместе с «Пауза Отключает Питание».", "label": "Объект Переключателя" }, "watchdog_interval": { - "doc": "Как часто фоновый сторожевой таймер проверяет зависшие датчики и истекшие тайм-ауты. По умолчанию 30 с.", + "doc": "Как часто фоновая проверка ищет зависшие датчики и истёкшие таймауты. По умолчанию 30 с.", "label": "Интервал Watchdog" }, "notify_milestone_message": { @@ -1522,135 +1515,90 @@ "label": "Сообщение о вехе" }, "notify_milestones": { - "doc": "Количество циклов через запятую, при достижении которых срабатывает однократное праздничное уведомление (напр., 50, 100, 500, 1000). Пустое значение отключает уведомления о вехах.", + "doc": "Числа циклов, при которых приходит разовое праздничное уведомление, через запятую (например, 50, 100, 500, 1000). Пусто: выключено.", "label": "Вехи по циклам" }, "notify_quiet_end_hour": { - "doc": "Конец интервала «не беспокоить» (0-23). Отложенные уведомления доставляются в этот час. Оставьте пустым для отключения.", + "doc": "Конец окна «не беспокоить» (0-23); отложенные уведомления отправляются в это время. Пусто: выключено.", "label": "Конец тихих часов" }, "notify_quiet_start_hour": { - "doc": "Начало интервала «не беспокоить» (0-23). Уведомления о завершении, напоминания и уведомления о чистом белье, которые сработали бы в тихие часы, откладываются и доставляются по окончании интервала. Оставьте пустым для отключения. Поддерживаются интервалы, пересекающие полночь (напр., начало 22, конец 7).", + "doc": "Начало окна «не беспокоить» (0-23). Уведомления о завершении, напоминания и уведомления о чистом белье в этом окне откладываются до его конца. Может переходить через полночь (например, с 22 до 7). Пусто: выключено.", "label": "Начало тихих часов" }, "peak_rate_message": { - "doc": "Необязательный пользовательский текст подсказки о пиковом тарифе, добавляемой к уведомлению о запуске. Переменные шаблона: {device}, {price}. Пустое значение использует встроенное по умолчанию.", + "doc": "Свой текст подсказки о пиковом тарифе. Переменные шаблона: {device}, {price}. Пусто: текст по умолчанию.", "label": "Сообщение о пиковом тарифе" }, "peak_rate_threshold": { - "doc": "Когда цикл запускается и текущая цена за кВт·ч равна этому значению или выше, к уведомлению о запуске добавляется подсказка о пиковом тарифе. 0 или пусто отключает подсказку.", + "doc": "Если цена за кВт·ч на старте цикла равна этому значению или выше, добавить подсказку о пиковом тарифе в уведомление о старте. 0 или пусто: выключено.", "label": "Порог пикового тарифа (за кВт·ч)" }, - "dtw_bandwidth": { - "label": "Допуск сопоставления по форме", - "doc": "Насколько сильно модуль сопоставления может растянуть цикл по времени, чтобы подогнать его под форму сохранённой программы. Чем выше значение, тем больше допускается колебаний скорости; слишком высокое рискует сопоставить не ту программу." - }, - "corr_weight": { - "label": "Вес формы относительно уровня", - "doc": "Баланс между формой кривой и абсолютным уровнем мощности при оценке сопоставления. Более высокое значение отдаёт предпочтение форме; более низкое отдаёт предпочтение совпадению уровня мощности." - }, - "duration_weight": { - "label": "Вес длительности", - "doc": "Насколько сильно предпочтение отдаётся программе, типичная длительность которой совпадает с этим циклом." - }, - "energy_weight": { - "label": "Вес энергии", - "doc": "Насколько сильно предпочтение отдаётся программе, типичное энергопотребление которой совпадает с этим циклом." - }, "store_brand": { "label": "Бренд устройства", - "doc": "Необязательно. Бренд устройства, выбранный из каталога сообщества. Используется для поиска и публикации подходящих эталонных записей. Оставьте пустым, если не пользуетесь онлайн-функциями." + "doc": "Необязательно. Марка вашего прибора из каталога сообщества, используется для поиска эталонных записей и обмена ими. Оставьте пустым, если не пользуетесь онлайн-функциями." }, "store_model": { "label": "Модель устройства", - "doc": "Необязательно. Модель устройства, выбранная из каталога сообщества после указания бренда. Если вашей модели нет в списке, вы можете добавить её в каталог." + "doc": "Необязательно. Модель вашего прибора из каталога сообщества (сначала выберите марку). Если её нет в списке, можно добавить." }, "show_contributor": { "doc": "Показать имя автора на профилях, загруженных из магазина сообщества" }, "smart_termination_duration_ratio": { - "doc": "Насколько глубоко в ожидаемую длительность сопоставленной программы должен зайти цикл, прежде чем Умное завершение сможет завершить его досрочно при падении мощности. Ожидаемая длительность - это среднее значение программы, поэтому на приборах с сильно изменчивым временем работы - стиральные машины при холодной зимней и тёплой летней воде на входе, сушильные машины с датчиком влажности, программы, зависящие от загрузки - около половины всех запусков завершаются короче этого среднего и никогда не получают быстрого завершения, заканчиваясь только по резервному тайм-ауту с опозданием на несколько минут. Уменьшите это значение (напр., 0.85) на таких машинах, чтобы досрочное завершение всё же срабатывало; повышайте ближе к 1.0 для большей осторожности. Оставьте пустым для значения по умолчанию (0.98 или 0.99 для посудомоечных машин). Оно может только завершить цикл раньше, но никогда позже, и никогда не срабатывает при неоднозначном или недостаточно уверенном совпадении.", + "doc": "Доля средней длительности программы, которую цикл должен пройти, прежде чем Умное завершение сможет закончить его раньше. Если время работы колеблется (температура воды, сушка по датчику, размер загрузки), уменьшите её (например, 0,85). Пусто: 0,98 (0,99 для посудомоечных машин).", "label": "Коэффициент умного завершения" }, - "enable_phase_matching": { - "label": "Оставшееся время с учётом фаз", - "doc": "Разбивает каждый активный цикл на фазы (нагрев, стирка, отжим) и распределяет оставшееся время по фазам, сочетая это с классической оценкой - опираясь на распределение по фазам в начале цикла и на классическую оценку ближе к концу. Это персонализирует отсчёт под то, сколько на самом деле нагревается и работает ваша машина, что наиболее заметно в первой половине цикла. Выключено = только классическая оценка. Влияет только на отображение оставшегося времени; сопоставление программ и обнаружение цикла остаются без изменений." - }, - "keep_min_score": { - "label": "Мин. оценка совпадения", - "doc": "Минимальный балл сходства, необходимый кандидату для участия в соревновании по совпадению. Значение по умолчанию 0,1 намеренно мягкое - допускает даже слабых кандидатов и полагается на поздние этапы для нахождения наилучшего совпадения. Повысьте для более раннего отсева маловероятных профилей; понизьте для расширения начального пула кандидатов." - }, - "dtw_blend": { - "label": "Смешивание деформации", - "doc": "Насколько балл выравнивания DTW заменяет основной балл Этапа 2. 0 = используй только балл Этапа 2, 1 = используй только балл DTW, 0,5 (по умолчанию) = равное смешивание. Повысьте для большей опоры на выравнивание с деформацией, когда программы имеют схожие уровни мощности, но разные временные паттерны." - }, - "dtw_ensemble_w": { - "label": "Микс комбинации DTW", - "doc": "В режиме ансамбля DTW (по умолчанию), вес масштабированного L1 против производного DTW (DDTW). 1,0 = только масштабированный L1, 0 = только DDTW, 0,7 = по умолчанию. Масштабированный вариант L1 сравнивает уровни мощности; производный вариант реагирует на изменения мощности во времени. Ансамбль объединяет оба." - }, - "dtw_ddtw_scale": { - "label": "Масштаб производной деформации", - "doc": "Расстояние полунасыщения для оценки производным DTW. Балл уменьшается вдвое, когда расстояние DDTW равно этому значению. Меньше = чувствительнее к различиям формы. Значение по умолчанию 30 откалибровано для типичных кривых мощности бытовых приборов. Влияет только на режимы ensemble и ddtw DTW." - }, - "dtw_refine_top_n": { - "label": "Количество уточнённых кандидатов", - "doc": "Сколько ведущих кандидатов Этапа 2 DTW переоценивает. DTW дороже, поэтому применяется только к N лучшим кандидатам. По умолчанию 5. Повысьте (до 7-9), если правильный профиль иногда достигает только 4-го или 5-го места после Этапа 2 - DTW может его спасти. Снижение немного ускоряет сопоставление." - }, - "duration_scale": { - "label": "Резкость длительности", - "doc": "Резкость штрафа за несоответствие длительности на Этапе 4. Это логарифмическое отношение, при котором балл соответствия уменьшается вдвое. Меньше = строже: несоответствие длительности сильнее снижает балл. Значение по умолчанию 0,175 соответствует допуску длительности около 18% при половинном весе. Используйте вместе с Весом длительности." - }, - "energy_scale": { - "label": "Резкость энергии", - "doc": "Резкость штрафа за несоответствие энергии на Этапе 4. Меньше = строже: несоответствие энергии сильнее снижает балл. Значение по умолчанию 0,25 намеренно мягче, чем Резкость длительности, поскольку энергия варьируется с нагрузкой. Используйте вместе с Весом энергии." - }, "power_profile_interval_min": { "label": "Интервал профиля мощности", - "doc": "Размер ячейки для атрибута датчика power_profile на профиль (плоский массив средних ватт по слотам), используемого внешними планировщиками, такими как EMHASS и tibber_prices. Меньшие ячейки сохраняют чёткость коротких пиков мощности; большие ячейки сглаживают форму. По умолчанию 15 мин. Только при считывании; не влияет на обнаружение." + "doc": "Размер интервала атрибута power_profile (средние ватты на слот) для планировщиков вроде EMHASS и tibber_prices. Меньше сохраняет короткие пики; больше сглаживает кривую. По умолчанию 15 мин. Не влияет на обнаружение." }, "notify_live_silent": { "label": "Тихие живые обновления", - "doc": "Только для iOS. Обновлять ход выполнения тихо: каждое обновление приходит без звука и вибрации, с пониженным приоритетом для экономии заряда, поэтому iOS может ненадолго объединять их. Первое обновление цикла (то, которое запускает Live Activity), а также уведомления о начале и завершении по-прежнему уведомляют как обычно. Выключите, чтобы получать сигнал при каждом обновлении." + "doc": "Только iOS. Обновлять прогресс в реальном времени беззвучно, с экономящим батарею приоритетом, который iOS может группировать. Первое обновление и уведомления о старте и завершении по-прежнему со звуком. Выключено: звук при каждом обновлении." }, "notify_live_sticky": { "label": "Сохранять живое уведомление при нажатии", - "doc": "Только для Android. Сделать уведомление о ходе выполнения постоянным (закреплённым), чтобы нажатие не скрывало его. Выключено сохраняет поведение по умолчанию, при котором нажатие скрывает уведомление." + "doc": "Только Android. Не убирать уведомление о прогрессе после нажатия. Выключено: нажатие его закрывает." }, "notify_live_click_action": { "label": "Цель нажатия уведомления", - "doc": "Куда открывается нажатие на уведомление от этого прибора, на Android и iOS. Пустое значение открывает панель WashData на этом приборе. Укажите путь к панели управления (например, /lovelace/laundry) или полный URL, чтобы перейти в другое место, либо «none», чтобы нажатие ничего не открывало." + "doc": "Что открывает нажатие на уведомление этого прибора. Пусто: панель WashData этого прибора. Введите путь дашборда (например, /lovelace/laundry) или URL, или «none», чтобы нажатие ничего не открывало." }, "door_opens_at_end": { "label": "Дверца открывается автоматически в конце", - "doc": "Для посудомоечных машин, которые открывают дверцу в конце цикла для сушки (AirDry и аналогичные). При включении открытие дверцы во время работающего цикла больше не ставит его на паузу навсегда; вместо этого, если дверца остаётся открытой в течение времени выдержки ниже, WashData считает цикл завершённым. Кратковременное открытие (для загрузки посуды) игнорируется. Требуется объект датчика двери." + "doc": "Для посудомоечных машин, которые в конце открывают дверцу для сушки (например, AirDry). Дверца, открытая в течение времени ниже, завершает цикл, а не ставит его на паузу; короткое открытие игнорируется. Нужен Объект Датчика Дверцы." }, "door_end_dwell_seconds": { "label": "Выдержка при открытой дверце", - "doc": "Как долго дверца должна оставаться открытой до завершения цикла, когда включено \"Дверца открывается автоматически в конце\". Достаточно долго, чтобы игнорировать быструю загрузку посуды (по умолчанию 60 с), достаточно коротко для быстрого завершения после открытия дверцы машиной." + "doc": "При включённом «Дверца открывается автоматически в конце»: сколько дверца должна оставаться открытой, прежде чем цикл завершится. Достаточно долго, чтобы не реагировать на добавление тарелки (по умолчанию 60 с)." }, "profile_evidence_sources": { "label": "Циклы, формирующие программу", - "doc": "Какие циклы используются для построения кривой мощности каждой программы и для сопоставления с ней завершённого цикла. Если снять галочку с какого-то вида, он перестанет влиять на ваши программы, но ничего не удалится: циклы останутся в списке Циклы, их по-прежнему можно пометить или удалить. Полезно, если вы не доверяете импортированным данным. На статистику это не влияет: в ней всегда учитываются только циклы, которые эта машина действительно выполнила. Если снять все галочки, это будет проигнорировано, ведь программа без циклов никогда не смогла бы совпасть." + "doc": "Какие виды циклов формируют кривую и сопоставление каждой программы. Снятие отметки ничего не удаляет; полезно, если вы не доверяете импортированным данным. Статистика всегда считает только циклы этой машины. Снятие всех отметок игнорируется." }, "energy_price_dynamic": { "label": "Стоимость, взвешенная по времени", - "doc": "Рассчитывает стоимость каждого цикла по цене, действовавшей в каждый момент его работы, а не по единственной цене на момент завершения. Работает только с объектом цены (фиксированная цена меняться не может). Дополнительная настройка не нужна: пока цикл идёт, цена отслеживается в реальном времени, а «Обработка истории» в Диагностике пересчитывает стоимость старых циклов по данным recorder. Выключено = классическое поведение, одна цена, зафиксированная в конце." + "doc": "Считать стоимость каждого цикла по цене в каждый момент его работы, а не по цене на момент завершения. Нужен объект цены. «Обработка истории» (Диагностика) пересчитывает старые циклы." }, "curve_preroll_seconds": { - "doc": "Сколько секунд показаний от прерванных попыток запуска WashData может перенести в начало кривой цикла. Кривая цикла обычно начинается с той попытки запуска, которая в итоге удалась, поэтому на машине, которая делает несколько проб, прежде чем выйти на режим - выбор программы, блокировка люка, первый набор воды, - первые минуты реальной работы отсутствуют в каждой кривой. Задайте значение чуть больше самой длинной такой раскачки вашей машины (300 с с запасом хватает большинству). 0 отключает эту функцию. Учтите, что её включение сдвигает записанное начало раньше, поэтому циклы, записанные до изменения и после него, несут разную длительность для одной и той же программы; выученные средние будут смещаться, пока старые циклы не выйдут из истории.", + "doc": "Добавляет в начало цикла показания неудачных попыток запуска, для машин, которые пробуют перед стартом (блокировка дверцы, первый набор воды). 300 с хватает большинству; 0 = выключено. Записанное начало сдвигается раньше, поэтому выученные длительности какое-то время меняются.", "label": "Предзапись кривой" }, "unload_confirm_entity": { "label": "Объект Подтверждения Выгрузки", - "doc": "Необязательный объект, срабатывание которого означает, что содержимое извлечено; для машин, на которые нельзя установить датчик дверцы. Подойдёт любой: кнопка Zigbee или NFC, вспомогательный элемент input_button, датчик движения перед машиной, сцена. Подтверждением считается любое изменение состояния, кроме выключения и перехода в состояние «Недоступно». Установка этого объекта также включает напоминание о выгрузке на устройстве без Объекта Датчика Дверцы." + "doc": "Необязательно. Любой объект, подтверждающий, что загрузку вынули, для машин без датчика дверцы (кнопка, input_button, датчик движения, сцена). Считается любое изменение состояния, кроме выключения и недоступности. Также включает напоминание о выгрузке." }, "unload_track_without_door": { "label": "Подтверждать Выгрузку Вручную", - "doc": "Использовать напоминание о выгрузке на устройстве без Объекта Датчика Дверцы и без Объекта Подтверждения Выгрузки, подтверждая выгрузку из собственной автоматизации: нажмите объект-кнопку «Отметить как выгруженное» или вызовите службу ha_washdata.mark_unloaded. Игнорируется, если задан Объект Датчика Дверцы, который уже даёт такой сигнал." + "doc": "Включать напоминание о выгрузке без датчика дверцы и объекта подтверждения. Подтверждайте выгрузку из своей автоматизации кнопкой «Отметить как выгруженное» или службой ha_washdata.mark_unloaded. Игнорируется, если задан Объект Датчика Дверцы." }, "notify_icon_color": { "label": "Цвет Уведомления", - "doc": "Акцентный цвет уведомлений этого прибора, чтобы стиральную, сушильную и посудомоечную машину можно было различить с первого взгляда. iOS окрашивает значок и индикатор выполнения Live Activity на экране блокировки и в Dynamic Island. Android получает тот же цвет, но что с ним делать, решает сам телефон: некоторые окрашивают значок, а многие (особенно Samsung One UI) полностью его игнорируют. Обе платформы понимают Hex (например, #03A9F4). Оставьте пустым, чтобы использовать цвет по умолчанию для платформы." + "doc": "Цвет акцента уведомлений этого прибора, чтобы легко отличать стиральную, сушильную и посудомоечную машины. В iOS заполняет круг за значком и окрашивает полосу Live Activity. Многие телефоны Android (особенно Samsung) его игнорируют. Используйте hex, например #03A9F4. Пусто: цвет платформы по умолчанию." + }, + "share_name": { + "doc": "Показывать имя вашей учётной записи как автора циклов, которыми вы делитесь. Выключено: без имени." } }, "setting_group": { @@ -1678,9 +1626,6 @@ "door_pause": { "label": "Дверца и Пауза" }, - "dtw_refinement": { - "label": "Уточнение DTW" - }, "duration_gates": { "label": "Ворота Длительности" }, @@ -1757,13 +1702,18 @@ "rebuilding": "Перестроение…", "analyzing": "Анализ…", "resetting": "Сброс…", - "reverting": "Откат…", "trimming": "Обрезка…", "splitting": "Разбивка…", + "stalled": "Завис", "deleting": "Удаление…", "imported": "Импортировано", "preparing": "Подготовка…", - "unlabelling": "Снятие метки…" + "unlabelling": "Снятие метки…", + "working": "Выполняется…", + "match_uncertain": "Неуверенно: {top} или {runner}", + "match_uncertain_single": "Неуверенно: возможно, {top}", + "match_sure": "уверенность ~{pct}%", + "match_uncertain_tip": "WashData ещё не выбрал программу. Число показывает, как часто лидирующая догадка, {top}, оказывалась верной в прошлых циклах при таком отрыве. Только для сведения." }, "tab": { "advanced": "Дополнительно", @@ -1794,13 +1744,17 @@ }, "toast": { "catalog_refreshed": "Каталог сообщества обновлён", + "task_name_required": "Укажите название задачи", + "task_added": "Задача добавлена", + "task_save_failed": "Не удалось сохранить задачу: {error}", + "task_removed": "Задача удалена", + "maint_logged": "Отмечено как выполненное: {task}", "access_saved": "Управление доступом сохранено", "all_wiped": "Все данные удалены", "analysis_complete_none": "Анализ завершён: нет новых предложений", "auto_label_complete": "Автоматическая пометка завершена", "automation_deleted": "Автоматизация удалена", "correction_submitted": "Исправление отправлено", - "cycle_deleted": "Цикл удалён", "cycle_force_stopped": "Цикл принудительно остановлен", "cycle_labelled": "Цикл помечен", "cycle_paused": "Цикл приостановлен", @@ -1816,11 +1770,9 @@ "group_deleted": "Группа удалена", "group_name_required": "Требуется имя группы", "group_saved": "Группа сохранена", - "import_successful": "Импорт выполнен; интеграция перезагружается", "json_required": "Требуются данные JSON", "legacy_removed": "Устаревшие действия удалены", "logs_exported": "Журналы экспортированы", - "matching_reverted": "Веса сопоставления сброшены до значений по умолчанию", "min_2_profiles": "Выберите не менее 2 профилей для группы", "ml_training_no_improvement": "Обучение завершено: базовая модель сохранена (нет улучшений)", "models_reverted": "Модели устройства сброшены до базовых", @@ -1830,8 +1782,8 @@ "pg_preset_loaded": "Пресет «{name}» загружен", "pg_preset_saved": "Пресет «{name}» сохранён", "pg_sugg_loaded": "Загружено {n} рекомендуемых значений - запустите песочницу для сравнения", - "pg_sugg_ml_loaded": "Загружено {n} откалиброванных ML значений - запустите песочницу для сравнения", "phase_name_required": "Требуется имя фазы", + "phase_reset": "Фаза «{name}» возвращена к встроенной версии", "phase_updated": "Фаза обновлена", "phases_saved": "Фазы сохранены", "preferences_saved": "Настройки сохранены", @@ -1878,15 +1830,11 @@ "store_name_required": "Введите название профиля", "store_import_failed": "Не удалось импортировать: {error}", "store_imported": "Импортировано в {profile}", - "store_program_required": "Введите название программы", "store_no_appliance": "Сначала укажите бренд и модель устройства в настройках.", "store_share_failed": "Не удалось поделиться: {error}", "store_shared": "Опубликовано в магазине сообщества, ожидает проверки.", "contrib_device_changed": "Добавлено в магазин сообщества, но выбранное устройство изменилось, пока форма была открыта. Выберите его в настройках.", "appliance_added": "Устройство добавлено, ожидает одобрения", - "thanks_confirming": "Спасибо за подтверждение", - "device_approved": "Одобрено сообществом", - "rating_saved": "Оценка качества сохранена", "brand_added": "Бренд добавлен, ожидает одобрения", "profile_added": "Профиль добавлен, ожидает одобрения", "saved_except_conflicts": "Сохранено. Исправьте выделенные конфликты, чтобы сохранить остальное.", @@ -1909,11 +1857,54 @@ "odometer_saved": "Общее количество циклов сохранено", "odometer_save_failed": "Не удалось сохранить общее количество: {error}", "store_pick_profile_share": "Выберите профиль для публикации", - "remove_failed": "Не удалось удалить: {error}" + "remove_failed": "Не удалось удалить: {error}", + "analysis_complete": "Анализ завершён: предложений: {count}", + "analysis_failed": "Не удалось выполнить анализ: {error}", + "auto_detect_failed": "Не удалось выполнить авто-определение: {error}", + "auto_label_failed": "Не удалось автоматически пометить: {error}", + "could_not_load_cycle": "Не удалось загрузить цикл: {error}", + "create_failed": "Не удалось создать: {error}", + "cycles_deleted": "Удалено циклов: {count}", + "debug_cleared": "Очищено отладочных записей: {count}", + "discard_failed": "Не удалось отбросить: {error}", + "error": "Ошибка: {error}", + "export_failed": "Не удалось экспортировать: {error}", + "import_failed": "Не удалось импортировать: {error}", + "invalid_json": "«{key}» не является корректным JSON - исправьте его или очистите поле перед сохранением.", + "label_failed": "Не удалось пометить: {error}", + "ml_training_promoted": "Обучение завершено: активировано моделей: {count}", + "pause_failed": "Не удалось приостановить: {error}", + "phase_created": "Фаза «{name}» создана", + "phase_deleted": "Фаза «{name}» удалена", + "profile_created": "Профиль «{name}» создан", + "rename_failed": "Не удалось переименовать: {error}", + "save_failed": "Не удалось сохранить: {error}", + "split_complete": "Разбито на циклы: {count}", + "start_failed": "Не удалось запустить: {error}", + "stop_failed": "Не удалось остановить: {error}", + "update_failed": "Не удалось обновить: {error}", + "store_download_skipped": "Пропущено записей: {n} (слишком короткие, со слишком большими пропусками или уже есть на вашем устройстве).", + "store_import_low_quality": "Эта запись слишком короткая, содержит слишком большие пропуски или неправдоподобна, поэтому её нельзя импортировать.", + "store_import_duplicate": "Эта запись уже есть на вашем устройстве.", + "device_approved": "Одобрено сообществом", + "thanks_confirming": "Спасибо за подтверждение", + "rating_saved": "Оценка качества сохранена", + "store_download_partial": "Не загружено программ: {n}; повторите попытку позже.", + "import_no_restore_point": "Импорт выполнен, но точку восстановления сохранить не удалось, поэтому этот импорт нельзя отменить", + "import_undo_failed": "Не удалось отменить импорт: {error}", + "import_undone": "Импорт отменён: восстановлено профилей: {profiles}, циклов: {cycles}", + "auto_label_cancelled": "Автоматическая маркировка отменена после маркировки {count} циклов.", + "auto_label_cancelled_one": "Автоматическая маркировка отменена после маркировки {count} цикла.", + "auto_label_cancelled_few": "Автоматическая маркировка отменена после маркировки {count} циклов.", + "auto_label_cancelled_many": "Автоматическая маркировка отменена после маркировки {count} циклов.", + "auto_label_cancelled_other": "Автоматическая маркировка отменена после маркировки {count} цикла.", + "auto_label_done": "Автоматическая маркировка завершена: помечено {count} циклов, без метки осталось: {skipped}.", + "auto_label_done_one": "Автоматическая маркировка завершена: помечен {count} цикл, без метки осталось: {skipped}.", + "auto_label_done_few": "Автоматическая маркировка завершена: помечено {count} цикла, без метки осталось: {skipped}.", + "auto_label_done_many": "Автоматическая маркировка завершена: помечено {count} циклов, без метки осталось: {skipped}.", + "auto_label_done_other": "Автоматическая маркировка завершена: помечено {count} цикла, без метки осталось: {skipped}." }, "suggestion": { - "both_agree": "WashData рекомендует", - "calibrated_label": "Откалиброванное", "observed_label": "Наблюдаемое", "impact": { "auto_label_confidence": { @@ -1928,10 +1919,6 @@ "higher": "Закрывает только циклы, использовавшие значительное количество энергии", "lower": "Закрывает и короткие или маломощные циклы" }, - "end_repeat_count": { - "higher": "Требует повторения сигнала завершения больше раз – надёжнее, немного медленнее", - "lower": "Завершает после меньшего числа повторений – быстрее, выше риск ложного завершения" - }, "learning_confidence": { "higher": "Учится только на очень уверенных совпадениях – медленнее, но надёжнее", "lower": "Учится на большем числе циклов; модель может быть более шумной" @@ -1970,40 +1957,25 @@ } }, "reason": { - "auto_label_confidence": "Уверенность на 15-м процентиле для {cycles} автоматических меток, которые пользователь ни разу не исправлял ({p15}).", "completion_min_seconds": "Половина длительности p05 корректного цикла ({minutes} min) по {cycles} циклам; отфильтровывает ложные циклы.{excl}", - "dead_zone_batch": "Оставлено небольшим (медианное окно нестабильности при запуске по {cycles} циклам = {s}s), чтобы реальное завершение определялось быстро.", - "dead_zone_from_dip": "На основе ранней просадки мощности, обнаруженной на {s}s.", - "end_energy_default": "Рекомендуемое значение по умолчанию для порога шума в конце цикла.", - "end_energy_false": "Энергия p95 ложных завершений ({p95}Wh) по {cycles} циклам, с нижней границей 0.2% от медианной энергии цикла ({floor}Wh).", - "end_energy_no_false": "Ложных завершений за {cycles} циклов нет; нижняя граница - 0.2% от медианной энергии цикла ({floor}Wh).", - "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), поэтому минимум не даёт лишней нагрузки.", + "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": "Оставлено как можно ниже, чтобы программа распознавалась в начале цикла; пороги уверенности и неоднозначности предотвращают преждевременные подтверждения.", "min_off_gap": "На основе {gaps} наблюдаемых промежутков между циклами (p05={p05}s). Минимум устройства: {floor}s.", "min_off_gap_bridge": "Рассчитано так, чтобы перекрыть самый длинный тихий участок внутри цикла: {span}s + запас 60s, по {spans} перекрытым промежуткам за {cycles} корректных циклов (остаётся ниже вашего наименьшего промежутка между двумя запусками подряд, который составляет {ceiling}s).", "min_power": "40% от наименьшей активной мощности p05 ({p05}W) по {cycles} корректным циклам, чтобы порог отключения оставался ниже реального потребления.{excl}", - "ml_auto_label": "Наименьшая уверенность, при которой модель качества всё ещё считала цикл корректным (p10 по {cycles} корректным циклам = {p10}).", - "ml_end_repeat": "{false}/{total} циклов ({pct}%) имели паузу, которую детектор завершения оценил выше 50%; требуется {val} подтверждение(й) завершения.", - "ml_off_delay": "Паузы, подтверждённые детектором завершения: p95 {p95}s + запас 60s, по {pauses} паузам, проверенным моделью, за {cycles} циклов (минимум {floor}s).", "no_update_timeout": "На основе наблюдаемой частоты обновлений (p95={p95}s) * 20 (мин. 60s).", "off_delay_cadence": "На основе наблюдаемой частоты обновлений (p95={p95}s) * 5", "off_delay_device_floor": "Использован безопасный минимум для устройства {device} ({floor}s).", "off_delay_generic_floor": "Использован универсальный безопасный минимум ({floor}s).", "off_delay_pauses": "Рассчитано так, чтобы пережить реальные паузы: пауза внутри цикла p95 {p95}s + запас 60s, по {pauses} паузам за {cycles} корректных циклов (минимум {floor}s).", - "profile_match_threshold": "Уверенность p10 для {cycles} правильных автоматических меток ({p10}); безопасный порог подтверждения в реальном времени.", - "sampling_interval": "Медианный интервал обновления по {cycles} корректным циклам ({si}s).{excl}", - "smoothing_window": "Рассчитано на сглаживание ~30s показаний при частоте опроса {si}s ({samples} отсчётов).", - "start_duration": "Оставлено небольшим (~один интервал опроса {si}s), чтобы обнаружение начиналось как можно раньше, но при этом игнорировались одиночные всплески.", - "thr_batch": "Оставлено чуть выше наименьшей активной мощности p05 по {cycles} циклам ({p05}W), чтобы запуск фиксировался как можно раньше, а порог остановки оставался ниже минимальной рабочей мощности машины.", - "tol_per_profile": "p75 разброса длительности по профилям среди {profiles} профилей ({cycles} циклов); стабильные профили не штрафуются.", - "tol_pooled": "На основе объединённого разброса длительности {cycles} недавних размеченных циклов (отклонение p95={dev}).", - "watchdog": "Оставлено настолько низким, насколько это безопасно (чуть выше интервала обновления p95, равного {p95}s, и не менее 2x интервала выборки {median}s, мин. 30s), чтобы зависания выявлялись быстро без ложных остановок." + "standby_floor": "Каждый недавний цикл продолжал потреблять {idle}W в момент завершения, поэтому порог ниже этого значения никогда не сможет зафиксировать выключение прибора. Он оставлен выше неё; {cycles} чистых циклов в истории никогда не опускались ниже него более чем на {pause}с.", + "thr_batch": "Чуть выше самой низкой рабочей мощности (p05) по {cycles} циклам ({p05}W), чтобы старты ловились рано, а порог остановки оставался ниже самой низкой рабочей мощности.", + "watchdog": "Оставлено настолько низким, насколько это безопасно (чуть выше интервала обновления p95, равного {p95}s, и не менее 2x интервала выборки {median}s, мин. 30s), чтобы зависания выявлялись быстро без ложных остановок.", + "end_energy_floor": "Повышено до минимума, который следует из порога остановки ({stop} Вт) в течение задержки выключения ({off_delay} с); ниже него цикл может завершиться только резервным путём.", + "max_duration_ratio_default": "Возвращено значение по умолчанию: более низкий верхний предел отклоняет правильную программу всякий раз, когда цикл длится дольше обычного.", + "min_duration_ratio_default": "Возвращено значение по умолчанию: более высокий нижний предел не даёт распознать идущий цикл, пока он не продвинется достаточно далеко." }, "exclusions": { "summary": "Исключено {total} ошибочно определённых циклов: {parts}.", @@ -2021,7 +1993,8 @@ "no_power": "без мощности", "no_active_power": "без активной мощности" } - } + }, + "now_value": "(сейчас {v})" }, "trend": { "down": "Нисходящая тенденция", @@ -2029,6 +2002,10 @@ }, "maint": { "bearing_service": "Обслуживание подшипников", + "salt": "Пополнение соли", + "rinse_aid": "Пополнение ополаскивателя", + "lint_filter": "Очистка фильтра ворса", + "condenser_clean": "Очистка конденсатора", "descale": "Удаление накипи", "drum_clean": "Очистка барабана", "filter_clean": "Очистка фильтра", @@ -2036,34 +2013,22 @@ }, "pg_desc": { "dishwasher_end_spike_quiet_release": "Посудомоечная машина: секунды неактивности после ожидаемой длительности перед снятием ожидания финального слива в конце цикла", - "smart_termination_duration_ratio": "Доля ожидаемой длительности сопоставленной программы, которую должен достичь цикл, прежде чем Умное завершение сможет завершить его досрочно; уменьшите её для машин, зависящих от загрузки или температуры", + "smart_termination_duration_ratio": "Доля средней длительности программы, которую цикл должен пройти, прежде чем Умное завершение сможет закончить его раньше; уменьшите, если время работы колеблется", "completion_min_seconds": "Кратчайший запуск, считающийся настоящим циклом", - "end_repeat_count": "Сколько низких показаний подряд до завершения", "interrupted_min_seconds": "Короткие циклы помечаются как прерванные", "min_off_gap": "Промежуток для разделения двух циклов", "off_delay": "Секунды низкой мощности до завершения цикла", "start_duration_threshold": "Секунды выше порога для подтверждения старта", "start_threshold_w": "Минимум ватт, чтобы считать запущенным", "stop_threshold_w": "Ниже этого машина считается выключенной", - "anti_crease_finalize_ratio": "Доля ожидаемой длительности сопоставленной программы, которую должен достичь цикл, прежде чем можно завершить хвост вращения защиты от морщин; уменьшите её на сушильных машинах с длинным хвостом вращения, для стиральных машин оставьте значение по умолчанию", + "anti_crease_finalize_ratio": "Доля средней длительности программы, которую цикл должен пройти, прежде чем хвост защиты от морщин сможет его завершить; уменьшите для сушильных машин, для стиральных оставьте по умолчанию", "anti_wrinkle_enabled": "Поглощать импульсы вращения барабана после основной фазы, а не считать их новыми циклами", - "anti_wrinkle_exit_power": "Мощность между импульсами должна падать ниже этого, чтобы защита от морщин оставалась активной", + "anti_wrinkle_exit_power": "Уровень мощности в паузах между импульсами вращения; действует только выше Порога Остановки", "anti_wrinkle_idle_timeout": "Допустимая тишина между двумя импульсами вращения барабана, прежде чем защита от морщин завершится", "anti_wrinkle_max_duration": "Импульс длиннее этого завершает защиту от морщин и открывает новый цикл", "anti_wrinkle_max_power": "Импульс выше этого завершает защиту от морщин и открывает новый цикл", - "dtw_bandwidth": "Этап 3: полоса деформации Sakoe-Chiba (0 = DTW выключен; по умолчанию 0,2 = 20% длины цикла)", - "corr_weight": "Этап 2: баланс между формой кривой (корреляция) и уровнем мощности (MAE); по умолчанию 0,45", - "duration_weight": "Этап 4: насколько сильно совпадение по длительности влияет на итоговый балл (по умолчанию 0,22)", - "energy_weight": "Этап 4: насколько сильно совпадение по энергии влияет на итоговый балл (по умолчанию 0,22)", - "profile_match_min_duration_ratio": "Этап 1: минимальная длина цикла как доля длительности профиля; по умолчанию 0,1 (10%)", - "profile_match_max_duration_ratio": "Этап 1: максимальная длина цикла как доля длительности профиля; по умолчанию 1,8 (180%)", - "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)", - "dtw_refine_top_n": "Этап 3: кандидаты, переоцениваемые DTW; повысьте до 7-9, если правильный профиль занимает 4-5 место (по умолчанию 5)", - "duration_scale": "Этап 4: логарифмическое отношение, при котором совпадение по длительности уменьшается вдвое; меньше = более строгий штраф (по умолчанию 0,175)", - "energy_scale": "Этап 4: логарифмическое отношение, при котором совпадение по энергии уменьшается вдвое; меньше = более строгий штраф (по умолчанию 0,25)", + "profile_match_min_duration_ratio": "Минимальная длина цикла как доля длительности профиля; по умолчанию 0,1 (10%)", + "profile_match_max_duration_ratio": "Максимальная длина цикла как доля длительности профиля; по умолчанию 1,8 (180%)", "curve_preroll_seconds": "Секунды показаний от прерванных попыток запуска, переносимые в начало кривой цикла; 0 = выключено" }, "col": { @@ -2071,12 +2036,14 @@ "status_tip": "Результат цикла: Завершён (естественное окончание), Прерван (резкое падение мощности), Принудительно остановлен (вручную) или Требует проверки (ожидается отзыв).", "date_tip": "Дата и время начала цикла.", "duration_tip": "Общее время работы цикла от начала до конца.", - "energy_tip": "Общее потреблённое количество энергии (кВт·ч). Вычисляется интегрированием мощности по времени.", - "cost_tip": "Стоимость энергии для этого цикла, зафиксированная при завершении. Когда включена «Стоимость, взвешенная по времени», кривая мощности рассчитывается по цене, действующей в каждый момент; иначе энергия x единственная цена, действующая в конце. Задайте цену в Настройках, чтобы значение появилось.", + "energy_tip": "Всего потреблено энергии (кВт·ч), рассчитано по мощности во времени.", + "cost_tip": "Стоимость энергии этого цикла, зафиксированная при завершении. При включённой «Стоимости, взвешенной по времени» каждый момент оплачивается по своей цене; иначе по цене на конец. Задайте цену в настройках, чтобы это заполнилось.", "confidence_tip": "Достоверность совпадения профиля (0–100%). Насколько точно кривая мощности цикла совпала с определённой программой.", - "health_tip": "ML-качество цикла (выше = лучше). Нажмите на цикл, чтобы проверить и оценить его.", "flags_tip": "Метки сообщества и индикаторы качества данных", - "cost_dynamic_tip": "Взвешено по времени: рассчитано по цене, действовавшей во время цикла (эффективно {price} {cur}/кВт·ч)." + "cost_dynamic_tip": "Взвешено по времени: рассчитано по цене, действовавшей во время цикла (эффективно {price} {cur}/кВт·ч).", + "dbg_conf": "Дост.", + "dbg_mae": "MAE", + "dbg_corr": "Корр." }, "stat": { "avg": "сред.", @@ -2100,7 +2067,10 @@ "notify_finish": "Было бы отправлено уведомление о завершении.", "notify_milestone": "Было бы отправлено уведомление о промежуточном этапе.", "notify_held": "Уведомление было отложено из-за тихих часов.", - "finished": "Цикл достиг конечного состояния и завершился." + "finished": "Цикл достиг конечного состояния и завершился.", + "match_reverted": "Программа вернулась к обнаружению: её оценка упала намного ниже собственного пика.", + "verified_pause": "Выравнивание в реальном времени подтвердило (или сняло) паузу внутри программы. Пока пауза действует, цикл не завершается.", + "stalled": "Цикл держался на потреблении ожидания дольше, чем эта программа когда-либо делает там паузу, хотя работа не закончена. Показан как Пауза (Завис); цикл остаётся открытым." }, "store": { "search_ph2": "Поиск по бренду или модели…", @@ -2109,11 +2079,7 @@ "programs_count": "Программы: {n}", "browse": "Обзор", "device": "Устройство", - "favorites": "Избранное", - "search_ph": "Поиск по бренду или модели…", "no_results": "Подходящие устройства не найдены. Попробуйте другой запрос.", - "n_programs": "{n} программ", - "n_cycles": "{n} циклов", "no_programs": "Для этого устройства пока нет опубликованных программ.", "no_cycles": "Для этой программы пока нет опубликованных эталонных циклов.", "anon": "аноним", @@ -2125,19 +2091,21 @@ "merge_target": "Объединить с профилем", "description": "Описание (необязательно)", "contributed_by": "от {name}", + "website": "Сайт магазина ↗", + "website_tip": "Открыть сайт магазина сообщества", + "favorites": "Избранное", "rating_summary": "★ {avg} ({n})", "no_ratings": "Пока нет оценок", - "website": "Сайт магазина ↗", - "website_tip": "Открыть сайт магазина сообщества" + "unreachable": "Не удалось подключиться к магазину сообщества. Проверьте подключение и повторите попытку." }, "settings": { "gear": { - "title": "Настройки" + "title": "Параметры" } }, "link": { - "manual": "Руководство ↗", - "add_appliance": "Добавить ваше устройство" + "maintenance_supporter": "Maintenance Supporter на GitHub ↗", + "manual": "Руководство ↗" }, "tip": { "add_brand": "Бренда нет в списке? Добавьте его в каталог сообщества", @@ -2171,7 +2139,13 @@ "rebuild": { "envelopes": "Пересчёт огибающих" }, - "cancelling": "Отмена..." + "cancelling": "Отмена...", + "auto_label": { + "running": "Автоматическая пометка циклов" + }, + "store_download": { + "running": "Загрузка из магазина" + } }, "setup": { "hdr": { @@ -2180,26 +2154,24 @@ }, "phase0": { "washer": "WashData уже обнаруживает ваши циклы. Запишите первый цикл, чтобы включить названия программ и оценки времени.", - "dishwasher": "WashData наблюдает. Посудомоечные машины имеют сложные циклы – запись первого цикла настоятельно рекомендуется. Если обнаруженный цикл длится слишком долго, обрежьте его в редакторе циклов перед сохранением как профиль.", + "dishwasher": "WashData следит. Циклы посудомоечной машины сложные, поэтому запишите первый цикл. Если обнаруженный цикл слишком длинный, обрежьте его в редакторе цикла перед сохранением как профиля.", "generic": "WashData наблюдает. Запишите или пометьте обнаруженный цикл, чтобы начать создавать профили." }, "phase1a": { - "labelled": "Хорошее начало – ваша первая программа сохранена. Для наиболее чистых данных рассмотрите запись следующего цикла с помощью виджета записи." + "labelled": "Хорошее начало: ваша первая программа сохранена. Для самых чистых данных запишите следующий цикл через ручную запись." }, "phase1b": { - "recorded": "Ваша запись сохранена как {profile_name}. Теперь запишите или пометьте другие распространённые программы для расширения охвата." + "recorded": "Ваша запись сохранена как {profile_name}. Теперь запишите или пометьте другие частые программы." }, "phase1c": { - "verify": "У вас {count} программ от сообщества. Запустите цикл, чтобы убедиться, что WashData правильно их распознаёт – сопоставление будет улучшаться по мере того, как устройство накапливает собственную историю." + "verify": "У вас есть программы от сообщества ({count}). Запустите цикл, чтобы проверить, распознаёт ли его WashData; сопоставление улучшается по мере того, как устройство набирает свою историю." }, "phase2": { - "cluster": "WashData обнаружил {count} циклов, которые не соответствуют ни одной сохранённой программе – они похожи друг на друга. Создать для них новый профиль?", + "cluster": "WashData заметил похожие циклы ({count}), которые не подходят ни к одной сохранённой программе. Создать для них новый профиль?", "unmatched": "Ваш последний цикл не совпал ни с одной сохранённой программой. Это новая программа?" }, "phase3": { - "suggestions": "WashData имеет рекомендации по настройкам на основе истории ваших циклов – просмотрите их для повышения точности обнаружения.", - "groups": "Некоторые ваши профили похожи на одну и ту же программу при разных температурах. Объедините их в группу для улучшения сопоставления.", - "phases": "Добавьте фазы программы к {profile_name} для более точных оценок оставшегося времени." + "suggestions": "У WashData есть предложения по настройкам на основе истории ваших циклов. Просмотрите их, чтобы улучшить обнаружение." }, "phase4": { "healthy": "Это устройство полностью настроено ({profile_count} профилей)." @@ -2212,12 +2184,58 @@ "create_from_cluster": "Создать профиль из этих циклов", "create_profile": "Создать для него профиль", "review_suggestions": "Просмотреть предложения", - "organise_profiles": "Упорядочить профили", - "configure_phases": "Настроить фазы", "skip_step": "Пропустить этот шаг", "skip_forever": "Больше не показывать", "hide_guidance": "Скрыть подсказки", "show_guidance": "Показать подсказки" } + }, + "access": { + "edit": "Редактирование", + "full": "Полный", + "inherit": "Наследовать", + "none": "Нет (скрыто)", + "read": "Чтение" + }, + "pg_label_reason": { + "no_winner": "Ни одна программа не подошла", + "below_floor": "Ниже порога «Уверенность Обучения»", + "ambiguous": "Помечено как неоднозначное", + "margin": "Слишком малый отрыв от второй программы", + "unknown_profile": "Программа больше не существует", + "not_evaluated": "Не оценено" + }, + "diagram": { + "accum": "накоп.", + "auto": "авто", + "below_off": "ниже = выкл.", + "confirmed": "подтверждено", + "energy_counts": "энергия учитывается", + "feedback": "отзыв", + "gap_below_min": "пауза меньше мин. = один цикл", + "heat_phase": "фаза нагрева", + "held_at_100": "остаётся на 100%", + "ignored": "игнорируется", + "kept": "сохранён", + "max": "макс.", + "min": "мин.", + "minus_tol": "-доп", + "no_match": "нет совпадения", + "no_updates": "нет обновлений", + "off_delay_wait": "задержка выключения", + "off_gap": "пауза выкл.", + "plus_tol": "+доп", + "profile": "профиль", + "reading_interval": "типичный интервал показаний", + "sensor_offline": "датчик офлайн: принуд. остановка", + "si": "ИВ", + "spike_ignored": "всплеск игнорируется", + "spike_ignored_short": "всплеск: игнорируется", + "start": "запуск", + "stop": "остановка", + "tail_energy_resets": "энергия хвоста выше порога: таймер сбрасывается", + "thr": "порог", + "too_short": "слишком короткий", + "tumble_kept": "импульсы барабана сохраняются" } } diff --git a/custom_components/ha_washdata/translations/panel/sk.json b/custom_components/ha_washdata/translations/panel/sk.json index 9def0fca..4d59c6c1 100644 --- a/custom_components/ha_washdata/translations/panel/sk.json +++ b/custom_components/ha_washdata/translations/panel/sk.json @@ -3,6 +3,7 @@ "min": "min", "entity_not_found": "Entita sa nenašla", "running_long": "Beží dlho", + "stalled": "Zaseknuté", "remaining": "zostáva", "progress": "postup", "show_time_remaining": "Zobraziť zostávajúci čas", @@ -48,16 +49,18 @@ } }, "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_tip": "Ku koncu tento program stíchne asi na {mins} min a potom odoberá asi {watts} W počas {secs} s. Zaznamenané pri {seen} z {measured} zmeraných cyklov.", "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", + "artifact_tip": "Zistilo sa {n} anomálií (napr. dvere otvorené uprostred cyklu) – otvorte, aby ste ich videli na grafe", + "artifact_tip_one": "Zistila sa {n} anomália (napr. dvere otvorené uprostred cyklu) – otvorte, aby ste ju videli na grafe", + "artifact_tip_few": "Zistili sa {n} anomálie (napr. dvere otvorené uprostred cyklu) – otvorte, aby ste ich videli na grafe", + "artifact_tip_many": "Zistilo sa {n} anomálie (napr. dvere otvorené uprostred cyklu) – otvorte, aby ste ich videli na grafe", + "artifact_tip_other": "Zistilo sa {n} anomálií (napr. dvere otvorené uprostred cyklu) – otvorte, aby ste ich videli na grafe", "auto": "(automaticky zistené)", "built_in_tag": "vstavaný", "declining": "↘ klesajúci", "energy_low": "Nižšia spotreba energie ako zvyčajne", "energy_spike": "Vyššia spotreba energie ako zvyčajne", - "fair_fit": "prijateľná kvalita", - "fair_fit_tip": "Stredná konzistencia zhody – niektoré cykly priradené k tomuto profilu majú nižšie skóre spoľahlivosti. Označte viac cyklov alebo znova nahrajte profil, aby ste zlepšili presnosť.", "feedback_requested": "Vyžiadaná spätná väzba", "golden_cycle": "Zaznamenaný referenčný cyklus", "duration_outlier": "Cykly s neobvyklou dĺžkou: {n}", @@ -66,27 +69,40 @@ "needs_review": "Vyžaduje preskúmanie", "overrun": "Trvalo dlhšie ako zvyčajne", "poor_fit": "zlá kvalita", - "poor_fit_tip": "Nekonzistentná história zhôd – zvážte prestavbu tohto profilu", - "restart_gap_tip": "{n} medzera reštartu HA: priebeh výkonu má dieru", + "restart_gap_tip": "{n} medzier reštartu HA: priebeh výkonu má diery", + "restart_gap_tip_one": "{n} medzera reštartu HA: priebeh výkonu má dieru", + "restart_gap_tip_few": "{n} medzery reštartu HA: priebeh výkonu má diery", + "restart_gap_tip_many": "{n} medzery reštartu HA: priebeh výkonu má diery", + "restart_gap_tip_other": "{n} medzier reštartu HA: priebeh výkonu má diery", "reviewed": "Skontrolované", "steady": "→ stabilný", "underrun": "Skončil rýchlejšie ako zvyčajne", - "using_defaults": "Používa predvolené nastavenia", - "using_tuned": "Používa vyladené váhy", "overrun_ratio": "({x}x očakávaného)", "underrun_ratio": "({pct}% očakávaného)", - "awaiting_tip": "Čaká na schválenie komunitou", - "awaiting_n": "Čaká na schválenie · potvrdené {n}", - "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.", - "armed": "(použije sa na ďalší cyklus)" + "armed": "(použije sa na ďalší cyklus)", + "awaiting_tip": "Čaká na schválenie komunitou", + "awaiting_n": "Čaká na schválenie · potvrdené {n}", + "awaiting": "Čaká na schválenie", + "approved": "Schválené", + "matcher_unused": "Zatiaľ nepriradené žiadnemu z vašich cyklov", + "matcher_used": "Priradené k {n} vašim cyklom", + "matcher_used_one": "Priradené k {n} vášmu cyklu", + "matcher_used_few": "Priradené k {n} vašim cyklom", + "matcher_used_many": "Priradené k {n} cyklu", + "matcher_used_other": "Priradené k {n} vašim cyklom", + "matcher_used_tip": "Cykly na tomto spotrebiči, ktoré WashData sám priradil k tomuto importovanému programu. Import, ktorý sa nikdy nepriradí, nemusí zodpovedať vášmu spotrebiču; môžete ho odstrániť. Počítané lokálne; do obchodu sa nič neodosiela." }, "btn": { + "pg_preset_delete": "Odstrániť prednastavenie", + "pg_preset_load": "Načítať prednastavenie", + "pg_preset_load_tip": "Nahradiť hodnoty v testovacom prostredí týmto prednastavením (aktívne nastavenia zostanú bez zmeny)", + "pg_preset_save": "Uložiť ako prednastavenie", + "pg_preset_save_tip": "Uložiť všetky hodnoty nižšie ako pomenované prednastavenie pre toto zariadenie", "reset_zoom": "Obnoviť priblíženie", "set_brand_model": "Nastaviť značku a model", "refresh_catalog": "Obnoviť katalóg", @@ -97,12 +113,11 @@ "add_timer": "+ Pridať časovač", "apply_all": "Použiť všetko", "apply_label": "Použiť štítok", - "apply_set_b": "Použiť sadu B", "apply_split": "Použiť rozdelenie", "apply_trim": "Použiť orezanie", "auto_detect_split": "Automaticky zistiť", "auto_label_cycles": "Automaticky označiť cykly", - "auto_label_cycles_tip": "Automaticky priradiť názvy profilov neoznačeným cyklom, ktorých spoľahlivosť zhody prekračuje prah", + "auto_label_cycles_tip": "Pomenovať neoznačené cykly, ktorých spoľahlivosť zhody prekračuje prah.", "cancel": "Zrušiť", "clear_debug": "Vymazať ladiace dáta", "clear_splits": "Vymazať", @@ -112,8 +127,6 @@ "convert_to_automation": "Previesť na automatizáciu", "correct": "Opraviť…", "create": "Vytvoriť", - "create_from_cluster": "Vytvoriť profil z {count} cyklov", - "create_profile": "+ Vytvoriť profil", "delete": "Odstrániť", "delete_group": "Odstrániť skupinu", "delete_group_tip": "Odstrániť iba túto skupinu – profily členov sa zachovajú", @@ -122,39 +135,30 @@ "discard_tip": "Zahodiť zaznamenaný priebeh bez uloženia", "dismiss": "Zamietnuť", "mute_suggestion": "Prestať navrhovať toto nastavenie", - "pg_load_calibrated": "Načítať kalibrované (ML) ({n})", - "pg_load_calibrated_tip": "Načítať nastavenia kalibrované ML ako prepísania v testovacom prostredí", "pg_load_live": "Načítať aktívne nastavenia", "pg_load_live_tip": "Znovu načítať aktuálne nastavenia integrácie a zahodiť všetky úpravy v testovacom prostredí", "pg_load_suggested": "Načítať navrhované ({n})", "pg_load_suggested_tip": "Načítať aktuálne návrhy automatického ladenia ako prepísania v testovacom prostredí", - "pg_preset_delete": "Odstrániť prednastavenie", - "pg_preset_load": "Načítať prednastavenie", - "pg_preset_load_tip": "Nahradiť hodnoty v testovacom prostredí týmto prednastavením (aktívne nastavenia zostanú bez zmeny)", - "pg_preset_save": "Uložiť ako prednastavenie", - "pg_preset_save_tip": "Uložiť všetky hodnoty nižšie ako pomenované prednastavenie pre toto zariadenie", "pg_publish_all": "Uložiť do integrácie ({n})", "pg_publish_one": "Uložiť {label} do integrácie", "reset_muted": "Obnoviť stíšené", + "reset_phase_tip": "Vrátiť vašu úpravu a obnoviť vstavanú fázu", "done": "Hotovo", "edit": "Upraviť", "export": "Exportovať", - "export_json": "Exportovať do JSON", "force_stop": "Nútené zastavenie", "force_stop_tip": "Okamžite ukončiť aktuálny cyklus a označiť ho ako nútene zastavený", "from_template": "Zo šablóny ▾", "ignore": "Ignorovať", "import_json": "Importovať z JSON", - "import_overwrite": "Importovať (prepíše dáta)", "inspect": "Skontrolovať", "label": "Označiť", - "load": "Načítať", "load_more": "Načítať viac", "manage": "Spravovať", "merge": "Zlúčiť", "new_automation": "+ Nová automatizácia", "new_group": "+ Nová skupina", - "new_group_tip": "Zoskupiť takmer identické profily (rovnaký tvar/trvanie, rôzna teplota alebo odstreďovanie), aby porovnávanie spoľahlivo vyberalo medzi nimi", + "new_group_tip": "Zoskupiť takmer rovnaké profily (rovnaký tvar, iná teplota alebo odstreďovanie), aby ich párovanie vedelo rozlíšiť.", "new_phase": "+ Nová fáza", "new_profile": "+ Nový profil", "new_profile_tip": "Vytvoriť nový profil programu z existujúceho označeného cyklu alebo nahrávky", @@ -162,23 +166,19 @@ "on_cycle_started": "Pri spustení cyklu", "pause_cycle": "Pozastaviť", "pause_cycle_tip": "Pozastaviť bežiaci cyklus – spotrebič pokračuje tam, kde prestal", - "pg_apply_device": "Použiť na zariadenie", - "pg_autofill": "Automaticky vyplniť", - "play": "Prehrať", "process": "Spracovať", "process_history": "Spracovať teraz", "process_recording": "Uložiť nahrávku", "process_tip": "Uložiť zaznamenaný priebeh ako nový alebo existujúci profil", "rebuild": "Prebudovať obálky", "rebuild_envelope": "Prebudovať obálku", - "rebuild_tip": "Prepočítať očakávanú výkonovú obálku (pásmo min/max) pre všetky profily z ich označených cyklov – spustiť po označení nových cyklov alebo oprave starých", + "rebuild_tip": "Prepočítať očakávané výkonové pásmo každého profilu z jeho označených cyklov. Spustite po označení alebo oprave cyklov.", "rec_start_tip": "Začať nahrávanie priebehu výkonu spotrebiča – spustiť tesne pred začatím cyklu", "rec_stop": "Zastaviť", "rec_stop_tip": "Zastaviť nahrávanie a podržať zachytený priebeh na kontrolu", "record": "Začať nahrávanie", "refresh": "Obnoviť", "refresh_diag_tip": "Znovu načítať štatistiky úložiska", - "refresh_logs_tip": "Znovu načítať najnovšie záznamy denníka", "refresh_settings_tip": "Znovu načítať nastavenia zo servera", "relabel": "Zmeniť štítok ({count})", "remove": "Odstrániť", @@ -186,7 +186,6 @@ "reset": "Resetovať", "reset_ml_models_tip": "Zahoďte doladené modely a vráťte sa k vstavaným. WashData sa ich môže neskôr znova naučiť.", "reset_to_builtin": "Obnoviť na vstavané modely", - "reset_to_defaults": "Obnoviť predvolené hodnoty", "resume_cycle": "Pokračovať v cykle", "resume_cycle_tip": "Obnoviť pozastavený cyklus", "revert_settings": "Zahodiť zmeny", @@ -199,8 +198,6 @@ "run_analysis": "🔍 Spustiť analýzu návrhov", "run_analysis_tip": "Teraz analyzujte svoje zaznamenané cykly a obnovte návrhy ladenia", "run_auto_label": "Spustiť automatické označovanie", - "run_both": "Spustiť A vs B", - "run_simulation": "Spustiť simuláciu", "save": "Uložiť", "save_access_control": "Uložiť kontrolu prístupu", "save_group": "Uložiť skupinu", @@ -212,10 +209,8 @@ "save_settings": "Uložiť nastavenia", "select": "Vybrať", "select_all": "Vybrať všetko", - "select_last20": "Posledných 20", "select_none": "Zrušiť výber", "show_only": "Zobraziť iba", - "skip_setup": "Preskočiť nastavenie", "split": "Rozdeliť", "stop": "Zastaviť", "submit_correction": "Odoslať opravu", @@ -224,30 +219,24 @@ "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.", + "unlabel_selected_tip": "Odstrániť štítok z vybraných cyklov. Zostanú v histórii, ale prestanú tvoriť tento profil.", "use": "Použiť", "wipe_all": "Vymazať všetky dáta", "delete_selected": "Odstrániť vybrané ({n})", - "group_suggest": "Skupina {n}: {members}", "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", "import": "Importovať", "disconnect": "Odpojiť", "connect_github": "Pripojiť GitHub", - "save_appliance": "Uložiť spotrebič", "share_to_store": "Zdieľať do obchodu", "share": "Zdieľať", - "confirm_appliance": "Potvrdiť tento spotrebič", - "confirmed": "Potvrdené", "browse_community_setups": "Prechádzať nastavenia komunity", "download_device": "Stiahnuť nastavenie zariadenia", "share_device": "Zdieľať nastavenie zariadenia", - "share_device_tip": "Zdieľať programy a nastavenia tohto zariadenia s komunitou", + "share_device_tip": "Zdieľať tento spotrebič a jeho referenčné cykly v komunitnom obchode, aby ich mohli prevziať majitelia rovnakého stroja.", "share_n": "Zdieľať cykly: {n}", "export_selected": "Export (vybrať údaje)", "export_all": "Rýchly export všetkého", - "import_raw": "Pokročilé: nahradiť všetko zo súboru JSON", "download_export": "Stiahnuť export", "analyze_import": "Analyzovať súbor", "import_selected": "Importovať vybrané", @@ -260,7 +249,15 @@ "save_total_cycles": "Uložiť celkový počet", "clear": "Vymazať", "add_brand": "Pridať značku", - "add_appliance": "Pridať spotrebič" + "log_done_task": "Zapísať ako hotové: {task}", + "add_task": "Pridať úlohu", + "add_appliance": "Pridať spotrebič", + "retry": "Skúsiť znova", + "confirm_appliance": "Potvrdiť tento spotrebič", + "confirmed": "Potvrdené", + "create_profile_from_gap": "Vytvoriť profil", + "undo_import": "Vrátiť späť posledný import", + "pg_rerun": "Aktualizovať simuláciu" }, "note": { "off_delay": { @@ -268,10 +265,6 @@ } }, "conflict": { - "anti_wrinkle_exit": { - "exit": "Musí byť pod Prahom zastavenia ({stop} W); inak je výkon ukončenia ochrany pred krčením ignorovaný", - "stop": "Musí byť nad Prahom výkonu ukončenia ochrany pred krčením ({exit} W)" - }, "anti_wrinkle_max": { "max": "Musí byť nad Prahom spustenia ({start} W); inak je limit trvania ochrany pred krčením obídený", "start": "Musí byť pod Max. výkonom ochrany pred krčením ({max} W)" @@ -309,14 +302,14 @@ "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." + "power_sensor": "Toto je Senzor výkonu zariadenia, nie cena. Cykly by sa účtovali vo wattoch za kWh; vymažte toto pole.", + "energy_sensor": "Toto je Merač energie zariadenia, nie cena. Cykly by sa účtovali podľa stavu merača za kWh; vymažte toto pole.", + "device_class": "Tento senzor meria {dc}, nie cenu za kWh. Vymažte toto pole alebo vyberte senzor tarify.", + "unit": "Tento senzor meria v {unit}, nie cenu za kWh. Vymažte toto pole alebo vyberte 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" + "duration": "Musí byť menší ako Časový limit bez aktualizácie ({to} s), aby sa alarm zaseknutia spustil skôr", + "timeout": "Musí presiahnuť Trvanie zaseknutia čerpadla ({ps} s), aby sa alarm zaseknutia spustil skôr" }, "start_dur": { "sampling": "Interval vzorkovania presahuje Trvanie spustenia ({sdt} s); impulz z jednej vzorky môže otvoriť cyklus", @@ -334,10 +327,15 @@ "sampling": "Interval vzorkovania by mal byť najviac polovica Intervalu watchdog ({wi} s)" }, "settings_banner": "Konflikty nastavení: {n}. Skontrolujte zvýraznené sekcie a opravte ich pred uložením.", - "settings_banner_btn": "Prejsť na prvý" + "settings_banner_btn": "Prejsť na prvý", + "end_energy": { + "energy": "Príliš prísne pre Prah zastavenia ({w} W) počas Oneskorenia vypnutia ({d} s); cyklus môže skončiť len záložnou cestou", + "stop": "Energia ukončenia ({e} Wh za {d} s) povoľuje len {p} W" + } }, "hdr": { - "ab_comparison": "Porovnanie nastavení A/B", + "custom_tasks": "Vlastné úlohy", + "maintenance_supporter": "Potrebujete viac?", "access_control": "Kontrola prístupu", "add_maintenance": "Pridať udalosť údržby", "automation_delete": "Odstrániť túto automatizáciu", @@ -346,28 +344,22 @@ "automation_review_this_cycle": "Skontrolovať tento cyklus", "automations": "Automatizácie", "clear_debug": "Vymazať ladiace dáta", - "cycle_simulator": "Simulátor cyklov", "diagnostics_pref": "Diagnostika", "display": "Zobrazenie", - "dtw_inspector": "Inšpektor DTW", "export_import": "Export / Import", - "getting_started": "Začíname", "live_power": "Aktuálny výkon", "logs": "Denníky", - "logs_diagnostics": "Diagnostika", "maintenance": "Akcie údržby", "maintenance_log": "Denník údržby", "maintenance_reminders": "Pripomienky servisu", "manual_recording": "Manuálne nahrávanie", - "manual_recording_tip": "Spustite cyklus zámerne, kým WashData zaznamenáva priebeh výkonu. Stlačte Štart tesne pred spustením spotrebiča, Zastaviť po dokončení, potom Spracovať na uloženie ako pomenovaný profil.", + "manual_recording_tip": "Nahrajte cyklus zámerne: stlačte Začať nahrávanie tesne pred spustením spotrebiča a Zastaviť po dokončení, potom Spracovať na uloženie ako profil.", "ml_learned": "Čo sa WashData naučilo", - "ml_matching_tuning": "Dolaďovanie rozpoznávania programov", "ml_settings_card": "Nastavenia", "ml_smart_learning": "Inteligentné učenie", "my_preferences": "Moje predvoľby", "no_automations": "Žiadne automatizácie zatiaľ neodkazujú na toto zariadenie.", "panel_settings": "Nastavenia panela", - "param_sweep": "Prehľadávanie parametra", "pg_settings_source": "Zdroj nastavení", "phase_catalog": "Katalóg fáz", "playground": "Testovacie prostredie", @@ -376,11 +368,8 @@ "status": "Stav", "status_graph": "Graf stavu", "storage_stats": "Štatistiky úložiska", - "test_history": "Otestovať na histórii", "toggle_sidebar": "Prepnúť bočný panel Asistenta domácnosti", - "tools_and_data": "Nástroje a dáta", "wipe_history": "Vymazať históriu", - "recommendations": "Odporúčania ({n})", "pg_detection_params": "Nastavenia detekcie", "pg_outcome": "Výsledok simulácie", "pg_across_cycles": "Naprieč vašimi cyklami", @@ -388,7 +377,8 @@ "online_account": "Komunitný obchod a online funkcie", "import_power_history": "Import histórie výkonu", "service_status": "Stav servisu", - "online_account_tab": "Online a komunita" + "online_account_tab": "Online a komunita", + "live_match_debug": "Ladenie zhody v reálnom čase" }, "health": { "fair": "Prijateľná kvalita profilu", @@ -396,11 +386,16 @@ "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_tip": "Poloha na zaznamenanej krivke priradeného programu: {pct}%. Určuje sa zarovnaním cyklu s profilom, takže sedí aj pri dlhšom alebo kratšom behu. Obnovuje sa, keď je spotrebič v pokoji, takže 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ť", + "task_name": "Úloha", + "every_cycles": "Interval (cykly)", + "every_days": "Interval (dni)", + "new_task_name": "Nová úloha", + "days_since_service": "{since} / {total} dní", "actions": "Akcie", "activity": "Aktivita", "administrators": "Administrátori", @@ -408,10 +403,8 @@ "aria_cycle_chart": "Krivka výkonu cyklu", "aria_envelope_chart": "Obálka výkonu profilu", "aria_phase_chart": "Graf editora fáz", - "aria_playground_chart": "Krivka výkonu cyklu s posúvateľnými prahmi detekcie", "aria_power_chart": "Graf výkonu", "aria_spaghetti_chart": "Prekryté krivky výkonu cyklov", - "aria_sweep_chart": "Graf výsledkov prebehu parametra", "artifact_anomaly": "Anomália", "artifact_high_power": "Vysoký výkon", "artifact_interruption": "Prerušenie", @@ -425,151 +418,103 @@ "cohesion_good": "súdržnosť {pct}%", "cohesion_low": "⚠ nízka súdržnosť {pct}%", "compare_profiles": "Porovnať s profilmi", - "concurrency": "Súbežnosť", "confidence": "Spoľahlivosť", "confidence_threshold": "Prah spoľahlivosti", "correct_duration": "Opraviť trvanie (min, voliteľné)", "correct_profile": "Opraviť profil", - "correlation": "Korelácia", "cost": "Náklady", "create_new_profile": "+ Vytvoriť nový profil…", "current_phase": "Aktuálna fáza", - "curves": "Krivky", "cycle": "Cyklus", "cycle_date_display": "Zobrazenie dátumu cyklu", - "cycle_health": "Kvalita cyklu", - "cycle_index": "index cyklu →", - "cycles": "Cykly", "cycles_count": "Cykly", "cycles_lc": "cyklov", - "dashed": "prerušovaná", "date": "Dátum", "debug_traces": "Ladiace záznamy", - "default": "Predvolené", "default_access_level": "Predvolená úroveň pre používateľov, ktorí nie sú uvedení nižšie", "default_other": "Predvolené (iné zariadenia)", "default_tab": "Predvolená karta pri otváraní panela", "font_size": "Veľkosť písma panela", - "delta": "Δ", "description": "Popis", - "dtw_blend_w": "Váha zmiešania", - "dtw_blended": "Zmiešané skóre", - "dtw_ddtw": "Skóre DDTW", - "dtw_l1": "Skóre L1", - "dtw_matched_profile": "(priradený profil cyklu)", "duration": "Trvanie", - "duration_agreement": "Zhoda trvania", - "emphasis": "Dôraz", "enable_access_control": "Povoliť kontrolu prístupu pre každého používateľa", "energy": "Energia", - "energy_agreement": "Zhoda energie", - "event_timeline": "Časová os udalostí", "event_type": "Typ udalosti", "expected": "Očakávané", + "suspected_profile": "{name} (odhad)", + "runner_up_profile": "{name} (druhý najlepší)", "expected_duration": "Očakávané trvanie (min)", "file_kb": "Súbor (kB)", - "final_score": "Konečné skóre", "from": "Od", "gap_s": "Medzera (s)", "group_name": "Názov skupiny", "head_trim": "Orezanie začiatku (s)", - "health": "Kvalita", "hide_tabs": "Skryť karty pre ne-administrátorov", - "in_use": "Používa sa", "json_data": "Dáta JSON", "label": "Priradený program", "last": "Posledné", "last_checked": "Posledná kontrola:", "load_from_file": "Načítať zo súboru", - "mae_score": "Skóre MAE", "manual_duration": "Manuálne trvanie (min, voliteľné)", "match": "Zhoda", "members": "Členovia", - "metric": "Metrika", "mode_existing_profile": "Pridať k existujúcemu profilu", "mode_new_profile": "Vytvoriť nový profil", "models_fine_tuned": "(doladené modely: {count})", - "n_classic_suggestions": "{n} klasické", - "n_ml_suggestions": "{n} ML", "n_selected": "{n} vybraných", - "n_tuning_suggestions": "{n} návrh ladenia", - "na": "neuv.", + "n_tuning_suggestions": "{n} návrhov ladenia", + "n_tuning_suggestions_one": "{n} návrh ladenia", + "n_tuning_suggestions_few": "{n} návrhy ladenia", + "n_tuning_suggestions_many": "{n} návrhu ladenia", + "n_tuning_suggestions_other": "{n} návrhov ladenia", "new_profile_name": "Nový názov profilu", "notes": "Poznámky", - "of": "z", - "outcome": "Výsledok", "panel_default_tab": "Predvolená karta", "panel_language": "Jazyk panelu", "per_cycle_short": "cyklus", - "pg_autofill_tip": "Automaticky vyplniť rozsah z aktuálnej hodnoty ({lo}–{hi})", "pg_best_value": "Nájdená najlepšia hodnota", "pg_current_value": "Aktuálna hodnota:", "pg_drag_hint": "Potiahnite čiaru v grafe", "pg_anti_wrinkle": "Ochrana pred krčením", - "pg_dtw": "DTW", "pg_ev_ending": "Ukončovanie", "pg_ev_running": "Beží", "pg_group_advanced": "Hraničné prípady", "pg_group_detection": "Spúšťače detekcie", "pg_group_timing": "Pravidlá časovania", "pg_idle": "Nečinné", - "pg_last_run": "Posledné spustenie: {date}", - "pg_match_score": "Skóre zhody", - "pg_pct_of": "{p}% z {total}", "pg_poor_match": "Zlá zhoda", "pg_preset": "Prednastavenie testovacieho prostredia", "pg_preset_name": "Názov nového prednastavenia", "pg_preset_none": "Vyberte prednastavenie…", "pg_strong_match": "Silná zhoda", - "pg_tile_ambiguous": "Nejednoznačné", - "pg_tile_ambiguous_tip": "Cykly s takmer rovnakým výsledkom medzi dvoma programami", - "pg_tile_detected": "Zistené", - "pg_tile_detected_tip": "Cykly, kde výkon prekročil prah spustenia dostatočne dlho, aby sa zaznamenal ako beh", - "pg_tile_matched": "Priradené", - "pg_tile_matched_tip": "Cykly správne priradené k známemu programu", - "pg_time_left": "Zostávajúci čas", "pg_weak_match": "Slabá zhoda", "phase_name": "Názov fázy", "name": "Názov", "phase_timeline": "Časová os fáz", "power": "Výkon", "profile": "Profil", - "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ží. Vyberte program pred spustením spotrebiča a použije sa hneď, ako začne ďalší cyklus.", + "program_tip": "Vyberte, ku ktorému programu sa aktuálny cyklus páruje. Automatická detekcia vyberie najlepšiu zhodu. Pripnite program, keď sa automatická detekcia mýli; program vybraný pred spustením platí pre ďalší cyklus.", "progress": "Priebeh", "raw_socket": "Surový socket", - "read_only": "Len na čítanie", "ref_cycle": "Referenčný cyklus (voliteľné)", - "refresh_interval": "Interval obnovy (s)", "remaining": "Zostatok", "reminder_every": "Pripomenúť každých (cyklov)", "remove_label": "- Odstrániť štítok -", "rename_profile": "Premenovať profil", - "replay_duration": "Prehrávanie", "resulting_profile": "Výsledný profil", "save_mode": "Režim uloženia", - "score": "Skóre", - "score_breakdown": "Rozbor skóre", "seconds_from_start": "Sekundy od začiatku", "select_profile": "Vybrať profil", "setting": "Nastavenie", - "settings_a": "Nastavenia A (aktuálne)", - "settings_a_short": "A", "settings_advanced": "Rozšírené", - "settings_b": "Nastavenia B (test)", - "settings_b_short": "B", "settings_basic": "Základné", "settings_detail_level": "Úroveň detailov nastavení", - "settings_overrides": "Prepísania nastavení", "show_debug": "Zobraziť ladiaci panel na stránke stavu (spoľahlivosť, nejednoznačnosť, najlepší kandidáti)", "show_expected": "Zobraziť očakávanú krivku (zhodný profil, oranžový)", "show_raw": "Zobraziť prepínač surových dát zásuvky v živom grafe výkonu", "sparkline": "Nedávny trend trvania cyklov", - "stage2": "Etapa 2 – základná podobnosť", - "stage3": "Etapa 3 – DTW", - "stage4": "Etapa 4 – zhoda", "status": "Stav", "tail_trim": "Orezanie konca (s)", "tap_to_dismiss": "ťuknutím na graf zatvoríte", @@ -580,7 +525,6 @@ "ungrouped": "Nezoskupené", "unlabelled": "Bez štítku", "unlabelled_paren": "(bez štítku)", - "warp_path": "Cesta deformácie", "input": "Vstup:", "start": "Začiatok", "end": "Koniec", @@ -605,9 +549,8 @@ "n_shown": ", zobrazených {n}", "cycles_title": "Cykly ({n})", "pg_detecting": "Rozpoznávanie", - "pg_best_marker": "Najlepší: {v}", "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", + "pg_time_left_model_tip": "Odhad podľa fáz programu, nie pevné odpočítavanie", "phase": "Fáza", "pg_mode_history": "Otestovať na histórii", "pg_ev_detected": "Zistené", @@ -647,18 +590,8 @@ "pg_mode_optimize": "Optimalizovať", "pg_group_matching": "Priraďovanie programov", "pg_zoom_hint": "dvojitým kliknutím obnovíte priblíženie", - "aria_playground_chart2": "Interaktívny graf výkonu cyklu: prejdením myšou zobrazíte čas/výkon, kolieskom priblížite, ťahaním posuniete", "pg_alert_timeout_end": "Ukončené časovým limitom, nie predikciou", "pg_alert_indefinite": "Bežalo by donekonečna", - "pg_alert_stress_cap": "Dosiahnutý bezpečnostný limit", - "pg_alert_stress_ok": "Detekcia nečinnosti: cyklus zastavený", - "pg_alert_stress_warn": "Príkon v nečinnosti nad prahom zastavenia", - "pg_stress_group": "Test detekcie nečinnosti", - "pg_stress_idle_auto": "Auto", - "pg_stress_idle_w": "Úroveň nečinnosti (W)", - "pg_stress_idle_w_desc": "Prepísať automaticky zistený spodný limit pohotovostného príkonu (ponechajte prázdne pre automatické zistenie).", - "pg_stress_toggle": "Testovať detekciu nečinnosti", - "pg_stress_toggle_desc": "Simuluje, že spotrebič zostane pri svojom pohotovostnom príkone po skončení záznamu -- zobrazí, či a kedy WashData cyklus zastaví.", "task_pg_history": "Test na histórii", "task_pg_sweep": "Optimalizovať", "task_reprocess": "Opätovné spracovanie", @@ -668,16 +601,9 @@ "pg_recent_runs": "Nedávne spustenia", "pg_load_run": "Znovu načítať toto spustenie", "pg_match_confidence": "Spoľahlivosť zhody", - "pg_envelope_fit": "Súlad s obálkou", - "pg_envelope_fit_tip": "Ako presne cyklus zapadá do uloženej výkonovej obálky tohto profilu, iný pohľad než spoľahlivosť zhody.", "n_imported_note": ", {n} importované", "enable_online": "Povoliť online funkcie", - "store_brand": "Značka spotrebiča", - "store_model": "Model spotrebiča", "anonymous": "Anonymný", - "rate_quality": "Ohodnotiť kvalitu", - "rate_n": "{n} hviezdičiek", - "approved_only": "Iba schválené", "adopt_settings": "Prevziať nastavenia", "flags": "Príznaky", "include_phase_map": "Zahrnúť mapu fáz", @@ -694,7 +620,6 @@ "cat_custom_phases": "Vlastné fázy", "cat_profile_groups": "Skupiny profilov", "cat_settings": "Nastavenia detekcie a rozpoznávania", - "cat_matching_config": "Ladenie rozpoznávania", "cat_ml_models": "Modely ML", "cat_feedback": "Spätná väzba a hodnotiace štítky", "cat_suggestions": "Návrhy", @@ -714,7 +639,6 @@ "conflict_overwrite": "Prepísať", "hist_csv_data": "Údaje CSV", "hist_from_recorder": "Alebo ju načítajte z Home Assistant", - "days": "dní", "hist_keep": "Zachovať tento cyklus", "hist_looks_complete": "dokončený", "peak_power_short": "Špička", @@ -735,7 +659,44 @@ "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" + "correct_total_cycles": "Opraviť celkový počet", + "hist_reason_already_recorded": "prekrýva sa s cyklom, ktorý WashData už zaznamenal", + "share_consent": "Potvrdzujem, že tieto cykly prebehli normálne až do konca bez prerušenia", + "show_internal": "Zobraziť interné nastavenia", + "show_internal_tip": "Nastavenia, ktoré WashData ladí za vás (vzorkovanie, watchdog, frekvencia párovania, prahy). Zobrazia sa, keď má toto zariadenie vlastnú hodnotu, konflikt alebo návrh.", + "rate_n": "{n} hviezdičiek", + "rate_quality": "Ohodnotiť kvalitu", + "pg_autolabel": "Automatické označenie", + "pg_label_review": "Vyžiada kontrolu", + "pg_ev_match_reverted": "Zhoda zrušená", + "pg_ev_verified_pause": "Overené pozastavenie", + "n_samples": "{n} vzoriek", + "n_samples_one": "{n} vzorka", + "n_samples_few": "{n} vzorky", + "n_samples_many": "{n} vzorky", + "n_samples_other": "{n} vzoriek", + "n_cycles": "{n} cyklov", + "n_cycles_one": "{n} cyklus", + "n_cycles_few": "{n} cykly", + "n_cycles_many": "{n} cyklu", + "n_cycles_other": "{n} cyklov", + "energy_per_cycle": "{v}/cyklus", + "energy_total": "celkom {v}", + "automation_off": "(vypnuté)", + "ref_cycle_none": "Žiadny", + "share_name": "Zobrazovať moje meno pri zdieľanom obsahu", + "cat_backfill_cycles": "Cykly nájdené v importovanej histórii výkonu", + "pg_compare_profile": "Porovnať s profilom", + "pg_obj_early": "Miera predčasných ukončení", + "pg_obj_endlag": "Oneskorenie konca (medián)", + "pg_obj_split": "Miera rozdelených cyklov", + "pg_sweep_cycles": "Nedávne cykly", + "cycle_context": "História z recordera okolo cyklu", + "cycle_context_off": "Vypnuté", + "cycle_context_minutes": "{min} min", + "cycle_context_series": "História z recordera", + "before_start": "Pred začiatkom", + "after_end": "Po skončení" }, "log": { "all_levels": "Všetky úrovne", @@ -745,11 +706,9 @@ }, "ml": { "builtin_models": "● Používa vstavané modely", - "fit_fair": "Prijateľný", "fit_good": "Dobrý", "fit_slight": "Nepatrný", "fit_strong": "Silný", - "fit_weak": "Slabý", "fit_word": "zhoda", "personalized": "● Prispôsobený tomuto stroju", "better_than_baseline": "{pct}% lepšie ako základný odhad ({metric})", @@ -757,21 +716,33 @@ "trend_declining_tip": "Prispôsobenie tohto modelu sa v posledných opakovaných kontrolách zhoršilo, kontrola viacerých cyklov mu môže pomôcť znovu sa naučiť.", "trend_steady_tip": "Prispôsobenie tohto modelu zostalo v posledných opakovaných kontrolách približne stabilné.", "cap_blurb": { - "end": "Rozpoznanie, kedy sa cyklus skutočne skončil", - "live_match": "Skoršie rozpoznanie spusteného programu", - "quality": "Odhaľovanie chybne rozpoznaných alebo poškodených cyklov", - "remaining_time": "Predpoveď zostávajúceho času", "total_energy": "Predpoveď celkovej spotreby energie a nákladov" }, "cap_label": { - "end": "Rozpoznanie konca cyklu", - "live_match": "Rozpoznávanie programu", - "quality": "Kontrola kvality cyklu", - "remaining_time": "Odhad zostávajúceho času", "total_energy": "Odhad spotreby energie" }, - "metric_auc": "AUC {auc} na validačných dátach", - "metric_mae": "chyba {model} oproti základnému modelu {baseline}" + "metric_mae": "chyba {model} oproti základnému modelu {baseline}", + "fine_tuned_at": "doladené {when}", + "held_out_count": "merané na {count} validačných cykloch", + "held_out_count_one": "merané na {count} validačnom cykle", + "held_out_count_few": "merané na {count} validačných cykloch", + "held_out_count_many": "merané na {count} validačného cyklu", + "held_out_count_other": "merané na {count} validačných cykloch", + "last_run_not_promoted": "Posledná kontrola ({when}): {reason}", + "never": "nikdy", + "skip_reason": { + "fit_failed": "Trénovanie zlyhalo: {error}", + "holdout_too_small": "Príliš málo cyklov na férové otestovanie nového modelu: na testovanie bolo možné vyčleniť {held_out}, potrebných je {min}.", + "insufficient_rows": "Zatiaľ málo použiteľných cyklov: doteraz {count} označených a čistých cyklov.", + "insufficient_rows_one": "Zatiaľ málo použiteľných cyklov: doteraz {count} označený a čistý cyklus.", + "insufficient_rows_few": "Zatiaľ málo použiteľných cyklov: doteraz {count} označené a čisté cykly.", + "insufficient_rows_many": "Zatiaľ málo použiteľných cyklov: doteraz {count} označeného a čistého cyklu.", + "insufficient_rows_other": "Zatiaľ málo použiteľných cyklov: doteraz {count} označených a čistých cyklov.", + "not_better_than_incumbent": "Nový model nebol lepší ako ten používaný (chyba {model} oproti {incumbent}), preto sa ponechal súčasný.", + "not_better_than_naive": "Nový model nebol zreteľne lepší ako jednoduchý odhad (chyba {model} oproti {naive}).", + "unknown": "Nenaučilo sa nič nové." + }, + "trained_unknown": "v neznámom čase" }, "modal": { "auto_label": "Automatické označovanie cyklov", @@ -780,7 +751,6 @@ "cycle": "Cyklus", "edit_group": "Upraviť skupinu profilov", "edit_phase": "Upraviť fázu", - "import_config": "Importovať konfiguráciu", "label_cycle": "Označiť cyklus", "merge_cycles": "Zlúčiť {n} cyklov", "new_group": "Nová skupina profilov", @@ -795,6 +765,8 @@ "delete_automation_title": "Odstrániť automatizáciu", "delete_automation_msg": "Odstrániť automatizáciu \"{name}\" z Home Assistant? Túto akciu nie je možné vrátiť späť.", "remove_legacy_title": "Odstrániť staršie akcie", + "reset_phase_msg": "Vrátiť vašu úpravu fázy \"{name}\" a obnoviť vstavanú fázu? Programy si zachovajú svoje rozsahy.", + "reset_phase_title": "Resetovať fázu", "remove_legacy_msg": "Odstrániť staršie vlastné akcie? Prestanú sa spúšťať pri udalostiach cyklu. Z panela to nie je možné vrátiť späť.", "discard_recording_title": "Zahodiť záznam", "discard_recording_msg": "Zahodiť uložený záznam? Túto akciu nie je možné vrátiť späť.", @@ -815,32 +787,46 @@ "store_share_device": "Zdieľať nastavenie zariadenia", "export_select": "Export - vybrať údaje", "import_wizard": "Import - vybrať údaje", - "history_import": "Import histórie výkonu" + "history_import": "Import histórie výkonu", + "apply_suggestions_title": "Použiť návrhy", + "remove_task_title": "Odstrániť úlohu", + "remove_task_msg": "Odstrániť pripomienku \"{task}\"? Jej záznamy v denníku údržby zostanú zachované.", + "apply_suggestions_msg": "Zmeniť tieto nastavenia ({n})? Integrácia sa po uložení znovu načíta.", + "discard_settings_title": "Zahodiť neuložené zmeny?", + "discard_settings_msg": "Neuložené zmeny nastavení: {n}. Opustiť Nastavenia a zmeny zahodiť?", + "profile_title": "Profil · {name}", + "undo_import_msg": "Údaje a nastavenia tohto zariadenia sa vrátia do stavu pred posledným importom ({when}). Cykly zaznamenané a zmeny vykonané odvtedy sa stratia.", + "undo_import_ok": "Vrátiť import späť", + "undo_import_title": "Vrátiť späť posledný import" }, "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.", + "pg_preset_delete_confirm": "Odstrániť prednastavenie \"{name}\"?", + "pg_preset_limit": "Dosiahnutý limit prednastavení ({n})", + "pg_preset_overwrite": "Prepísať prednastavenie \"{name}\"?", "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.", - "refresh_catalog_hint": "Zoznamy značiek a spotrebičov z komunity sa ukladajú do medzipamäte, aby zdieľaný obchod neprekročil svoj denný limit dotazov. Obnovením načítate položky, ktoré ostatní pridali alebo schválili.", + "store_sibling_hint": "Pre váš model nie je nič zdieľané? Príbuzný model rovnakej značky je zvyčajne dobrý začiatok.", + "store_declare_appliance": "Povedzte WashData, aký spotrebič máte, a uvidíte nastavenia, ktoré preň zdieľali ostatní. Alebo zadajte značku vyššie a poobzerajte sa.", + "refresh_catalog_hint": "Zoznamy značiek a spotrebičov z komunity sa ukladajú do medzipamäte kvôli dennému limitu obchodu. Obnovením uvidíte položky, ktoré ostatní pridali alebo schválili.", "head_trim_hint": "Koľko sekúnd odstrániť zo začiatku", "appliance_monitor": "Monitor spotrebiča", "artifact_dip_detail": "Poklesol pod bežné pásmo výkonu počas ~{n} s.", "artifact_footer": "Zvýraznené na grafe vyššie. Sú to prechodné artefakty (napr. dvere otvorené uprostred cyklu), nie nevyhnutne problémy.", - "artifact_header": "Počet anomálií zistených počas tohto cyklu: {n}", - "artifact_pause_detail": "Výkon poklesol takmer na nulu počas ~{n} s a potom sa obnovil – pravdepodobne boli otvorené dvere uprostred cyklu alebo bol cyklus pozastavený.", + "artifact_header": "Počas tohto cyklu sa zistilo {n} anomálií", + "artifact_header_one": "Počas tohto cyklu sa zistila {n} anomália", + "artifact_header_few": "Počas tohto cyklu sa zistili {n} anomálie", + "artifact_header_many": "Počas tohto cyklu sa zistilo {n} anomálie", + "artifact_header_other": "Počas tohto cyklu sa zistilo {n} anomálií", + "artifact_pause_detail": "Výkon klesol takmer na nulu na ~{n}s a potom sa obnovil. Pravdepodobne boli otvorené dvierka alebo bol cyklus pozastavený.", "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 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.", + "cleanup_intro": "Všetky označené cykly cez seba. Zaškrtnite odľahlé a potom im odoberte štítok (zostanú v histórii, ale prestanú tvoriť tento profil) alebo ich 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é)", - "compare_profiles_tip": "Prekryte ostatné profilové obálky v tabuľke vyššie, aby ste videli, ktorá z nich najlepšie vyhovuje tomuto cyklu.", + "compare_profiles_tip": "Prekryte graf ďalšími profilmi a zistite, ktorý k tomuto cyklu sedí najlepšie.", "compare_selected_cycles": "Vybraté cykly (tučné) – zobraziť / skryť", - "consider_new_profile": "Zvážte vytvorenie nového profilu.", - "coverage_gap": "nedávne cykly nemajú žiadny zodpovedajúci profil ({pct} % z posledných 30).", - "coverage_gap_similar_cycles": "Nájdených {count} podobných neoznačených cyklov – vytvorte profil, aby sa začali párovať.", "cycles_deleted": "Odstránených cyklov: {count}", "enough_data": "Dostatok dát na učenie ({current}/{min} cyklov).", "export_description": "Vyberte presne, ktoré profily, cykly, nastavenia a ďalšie údaje exportovať do súboru JSON, alebo analyzujte súbor a importujte len tie časti, ktoré chcete.", @@ -848,30 +834,28 @@ "feedback_prompt": "Potvrďte správnosť, opravte program alebo ignorujte.", "feedback_relabel_hint": "Opätovné označenie tohto cyklu ho tiež vyrieši.", "filter_by_profile": "Filtrovať podľa profilu…", - "group_modal_help": "Skupinové programy s rovnakým tvarom, ktoré sa líšia teplotou/odstreďovaním (trvanie sa môže líšiť). Porovnanie ohodnotí skupinu ako jedného kandidáta a potom vyberie najlepšieho člena. Vyberte aspoň 2; prekrytie ukazuje, nakoľko sú si podobné.", + "group_modal_help": "Zoskupte programy s rovnakým tvarom, ktoré sa líšia teplotou alebo odstreďovaním. Párovanie hodnotí skupinu ako celok a potom vyberie najlepšie sediaceho člena. Vyberte aspoň 2; prekrytie ukazuje, ako sú si podobné.", "group_not_cohesive": "Tieto profily nie sú dostatočne podobné na to, aby sa dali spoľahlivo zoskupiť, takže zhoda s nimi zaobchádza individuálne, kým neodstránite odľahlú hodnotu alebo nerozdelíte skupinu.", "group_preview_hint": "Zaškrtnite aspoň 2 členov na náhľad a porovnanie ich výkonových kriviek.", - "import_intro": "Načítajte exportovaný súbor alebo prilepte JSON nižšie.", - "legacy_actions_title": "Stará vlastná akcia {count} je stále spustená", + "legacy_actions_title": "{count} starých vlastných akcií je stále spustených", + "legacy_actions_title_one": "{count} stará vlastná akcia je stále spustená", + "legacy_actions_title_few": "{count} staré vlastné akcie sú stále spustené", + "legacy_actions_title_many": "Stále spustené staré vlastné akcie: {count}", + "legacy_actions_title_other": "{count} starých vlastných akcií je stále spustených", "live_chart_loading": "Graf výkonu sa zobrazí po príchode hodnôt.", "loading": "Načítava sa…", "loading_curve": "Načítava sa krivka…", "loading_settings": "Načítavajú sa nastavenia…", "log_buffer_hint": "Najnovšie prvé · uloží do vyrovnávacej pamäte posledných 500 záznamov ha_washdata od reštartu · potiahnutím spodného okraja zmeníte veľkosť.", - "maintenance_advisory": "Zvyšujúca sa doba trvania/energia môže naznačovať potrebu údržby spotrebiča (napr. odstraňovanie vodného kameňa, čistenie filtra).", "maintenance_due": "Údržba je splatná: {items}", - "maintenance_intro": "Zaznamenávajte servis, ktorý na tomto spotrebiči vykonávate, a nechajte sa upozorniť, keď je každá úloha opäť na rade.", + "maintenance_intro": "Zaznamenávajte servis tohto spotrebiča a nechajte si pripomenúť, keď bude každá úloha opäť na rade.", "maintenance_load_error": "Nepodarilo sa načítať údaje o údržbe: {error}", "maintenance_requires_access": "Údržba a export/import vyžadujú plný prístup.", - "manual_duration_hint": "Priemerná/očakávaná dĺžka cyklu profilu, ktorá sa používa na odhady zostávajúceho času. Upravte ho; ponechaním nezmenenej zachová aktuálnu hodnotu.", - "matching_tuning_intro": "Pri učení WashData tiež upravuje, koľko váhy má pri porovnávaní programov tvar oproti trvaniu a energii.", + "manual_duration_hint": "Očakávaná dĺžka cyklu profilu, používaná na odhad zostávajúceho času. Ponechajte bez zmeny na zachovanie aktuálnej hodnoty.", "merge_intro": "Vybraté cykly sú zlúčené do jedného (chronologicky; medzery sú vyplnené 0 W). Vyberte výsledný profil.", "ml_intro": "WashData je dodávaný s inteligentnými modelmi, ktoré fungujú ihneď po inštalácii.", "ml_learned_intro": "Modely doladené pre tento stroj.", - "ml_loading": "načítava sa ML…", "ml_settings_intro": "Dva nezávislé prepínače: jeden používa modely počas behu cyklu, druhý umožňuje WashData časom ich doladiť na váš stroj.", - "name_first_program": "Máte dostatok cyklov – pomenujte svoj prvý program a spustite priraďovanie.", - "near_duplicate_cluster": "zistený takmer duplicitný klaster profilov. Zoskupenie umožňuje zhodu spoľahlivo vybrať medzi podobnými (napr. rovnaký program pri inej teplote/odstreďovaní).", "no_cycles_match": "Žiadne cykly nezodpovedajú aktuálnemu filtru.", "no_cycles_profile": "Pre tento profil nie sú žiadne cykly.", "no_cycles_selected": "Najprv vyberte aspoň jeden cyklus.", @@ -896,79 +880,66 @@ "no_suggestions": "Žiadne aktívne návrhy.", "notify_services_hint": "Použite ID služieb {entity} (oddelené čiarkami pre viacero). Premenné šablóny: {vars}.", "old_actions_warning": "Nakonfigurované pomocou starého editora akcií (teraz odstránený). Stále sa spúšťajú pri udalostiach cyklu, ale už ich tu nemožno upravovať. Premeňte ich na normálnu automatizáciu alebo ich odstráňte.", - "onboarding_progress": "Pozorovaných cyklov: {n} / 3", - "onboarding_watching": "Používajte spotrebič bežne – WashData sleduje. Po 3 cykloch sa spustí priraďovanie programov.", "pending_feedback": "Čaká sa na spätnú väzbu k detekcii", "performance_trend": "Trend výkonnosti ({n} cyklov)", - "pg_analysis_empty": "Načítajte cyklus na zobrazenie analýzy zhody.", "pg_apply_confirm": "Použiť najlepšiu hodnotu: {label} = {value}?", - "pg_canvas_empty": "Vyberte cyklus vyššie a potom stlačte Načítať na zobrazenie jeho priebehu výkonu.", - "pg_chart_caption": "Miera detekcie/zhody v závislosti od hodnôt {param}", "pg_ctrl_intro": "Hodnoty vychádzajú z aktívnych nastavení integrácie pre toto zariadenie. Úpravy zostávajú v testovacom prostredí, kým ich neuložíte do integrácie.", "pg_live_unavailable": "Aktívne nastavenia nie sú k dispozícii, zobrazujú sa predvolené hodnoty.", "pg_load_live_confirm": "Zahodiť úpravy v testovacom prostredí ({n}) a znovu načítať aktuálne nastavenia integrácie?", - "pg_match_rate": "{pct}% miera zhody na {total} cykloch", "pg_matches_live": "Zhoduje sa s aktívnymi nastaveniami", "pg_n_changed": "Zmenené oproti aktívnym nastaveniam: {n}", "pg_sugg_none": "Všetky návrhy sa už zhodujú s aktuálnymi nastaveniami", - "pg_preset_delete_confirm": "Odstrániť prednastavenie \"{name}\"?", - "pg_preset_limit": "Dosiahnutý limit prednastavení ({n})", - "pg_preset_overwrite": "Prepísať prednastavenie \"{name}\"?", "pg_publish_one_confirm": "Uložiť {label} = {value} do nastavení tohto zariadenia?", "pg_restart_note": "Reštartujte Home Assistant, aby ste povolili nástroje simulácie.", - "pg_sim_progress": "{done} / {total} cyklov", - "pg_starting": "Spúšťanie…", - "pg_stress_synthetic": "simulácia nečinnosti začína tu", - "pg_sweep_intro": "Čo keby bol {param} iný? Otestujte {steps} hodnôt na vašich posledných {cycles} cykloch a nájdite nastavenie, pri ktorom je správne priradených najviac cyklov.", - "pg_sweep_step": "Krok {done} / {total}", - "pg_verdict_bad": "Vyžaduje pozornosť: mnoho cyklov zostáva nezistených.", - "pg_verdict_good": "Dobre vyladené: väčšina cyklov je správne rozpoznaná a priradená.", - "pg_verdict_ok": "Prijateľné: niektoré cykly neboli zachytené. Skúste znížiť prah spustenia.", "phase_catalog_intro": "Pomenované segmenty cyklu (predpranie, ohrev, odstreďovanie…). Priraďte ich profilu z jeho ovládacieho panela.", "phase_ranges_intro": "Rozsahy fáz (minúty od začiatku cyklu) naložené na priemernú krivku. Upravujte hodnoty na živý náhľad.", - "playground_intro": "Bezpečné testovacie prostredie na prehrávanie minulých cyklov s rôznymi nastaveniami, porovnávanie dvoch konfigurácií vedľa seba a skúmanie, ako sa cyklus zarovnáva s profilom. Nič tu nezmení vaše aktívne údaje, kým to výslovne nepoužijete.", - "playground_needs_restart": "Reštartujte Home Assistant, aby ste povolili nástroje testovacieho prostredia.", - "preferences_admin": "Predvoľby, panel a kontrola prístupu", - "preferences_adv": "Predvoľby", + "playground_intro": "Vyskúšajte nastavenia na skutočných údajoch svojich cyklov. Nič tu nezmení živé nastavenia, kým ich nepoužijete.", "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 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.", + "process_history_hint": "Znova spustí párovanie pre všetky uložené cykly, obnoví návrhy, pretrénuje modely ML (ak sú povolené), prepočíta náklady cyklov podľa zaznamenaných cien a prepočíta zdravie cyklov. Spustite 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.", - "rbac_hint": "Keď je vypnuté, každý používateľ Home Assistant má plný prístup (predvolené). Administrátori majú vždy plný prístup a môžu spravovať všetkých.", - "recent_logs": "Posledné záznamy ha_washdata", + "rbac_hint": "Vypnuté: každý používateľ Home Assistant má plný prístup (predvolené). Administrátori majú vždy plný prístup a môžu spravovať všetkých.", "recording_in_progress": "Nahrávanie prebieha", "reminders_intro": "Zobraziť pripomienku v paneli po tomto počte cyklov od posledného servisu. Ponechajte prázdne alebo 0 na vypnutie pripomienky.", "restart_gap_footer": "Zvýraznené v grafe. Údaje o výkone chýbajú pre tieto intervaly; párovanie použilo iba skutočné merania.", - "restart_gap_header": "{n} medzera reštartu HA počas tohto cyklu", + "restart_gap_header": "{n} medzier reštartu HA počas tohto cyklu", + "restart_gap_header_one": "{n} medzera reštartu HA počas tohto cyklu", + "restart_gap_header_few": "{n} medzery reštartu HA počas tohto cyklu", + "restart_gap_header_many": "{n} medzery reštartu HA počas tohto cyklu", + "restart_gap_header_other": "{n} medzier reštartu HA počas tohto cyklu", "restart_gap_item": "{dur} medzera", - "review_confirm_help": "Potvrďte, či bol tento cyklus rozpoznaný správne. Vaše recenzie trénujú model na vašom stroji – čím viac cyklov potvrdíte, tým lepšie bude porovnávanie a hodnotenie zdravia. Stačí rýchle dobré/zlé.", + "review_confirm_help": "Bol tento cyklus rozpoznaný správne? Potvrdzovanie cyklov zlepšuje párovanie pre váš stroj. Stačí rýchle Dobrý/Zlý.", "review_in_settings": "Skontrolovať v nastaveniach", "review_notes_placeholder": "Poznámky (voliteľné)", "review_notes_tip": "Poznámky s voľným textom pre vašu vlastnú potrebu. Nepoužíva sa priraďovaním alebo tréningom.", - "review_profile_tip": "Program, pod ktorým je tento cyklus označený. Ak bol automaticky zistený program nesprávny, opravte ho tu – označovanie učí párovanie pre budúce cykly.", - "review_quality_tip": "Aký čistý je tento cyklus. Dobrý = učebnicový príklad tohto programu; Zlý = detekovaný, ale hlučný alebo atypický; Nepoužiteľný = nesprávne zistený (zlúčený, skrátený alebo falošný). Riadi skóre zdravia a ktoré cykly sú povolené na trénovanie modelu.", - "review_recorded_tip": "Označte to ako ručne vybraný referenčný cyklus pre svoj program – rovnakú úlohu ako manuálne zaznamenaný cyklus. Referenčné cykly sa vždy uchovávajú, nasadzujú zodpovedajúcu šablónu a pri čistení sa nikdy nezrušia. (Toto je zlatá/zaznamenaná vlajka; obe sú to isté.)", - "review_tags_tip": "Voliteľné príznaky popisujúce, čo sa v tomto cykle pokazilo, aby to tréning a čistenie mohli zohľadniť.", + "review_profile_tip": "Program, ktorým je tento cyklus označený. Ak je nesprávny, opravte ho tu; štítky učia párovanie.", + "review_quality_tip": "Aký čistý je tento cyklus. Dobrý: učebnicový beh tohto programu. Zlý: rozpoznaný, ale zašumený alebo nezvyčajný. Nepoužiteľný: nesprávne rozpoznaný (zlúčený, skrátený alebo falošný). Ovplyvňuje skóre zdravia a ktoré cykly trénujú model.", + "review_recorded_tip": "Označiť ako ručne vybraný referenčný cyklus jeho programu, rovnako ako ručne nahraný cyklus. Referenčné cykly sa vždy zachovajú, tvoria šablónu párovania a čistenie ich nikdy neodstráni.", + "review_tags_tip": "Voliteľné príznaky, čo sa pokazilo, aby s tým tréning a čistenie mohli počítať.", "review_to_cycles": "Otvoriť front kontroly cyklov", "samples_decimated": "Zobrazených {shown} z {total} vzoriek (preriedené pre zobrazenie; špičky zachované). Široká medzera tu znamená preriedenie, nie chýbajúce dáta.", - "saving_triggers_reload": "Uloženie spustí opätovné načítanie integrácie. Entity HA sa môžu nakrátko zobraziť ako nedostupné.", + "saving_triggers_reload": "Uloženie znova načíta integráciu. Entity sa môžu nakrátko zobraziť ako nedostupné.", "search_placeholder": "Hľadať nastavenia…", "see_recorder": "Pozrite si widget nahrávania nižšie", "setting_changed": "Zmenené z {old} na {new} dňa {date}", "settings_basic_note": "Zobrazujú sa základné nastavenia. Prepnite na Rozšírené pre úplný zoznam.", - "shape_drift_advisory": "⚠ Posun tvaru", - "shape_drift_detail": "Vzor výkonu tohto profilu sa časom zmenil – možné opotrebenie spotrebiča alebo potreba údržby (napr. odstraňovanie vodného kameňa, čistenie filtra).", "show_all_settings": "Zobraziť všetky nastavenia", - "showing_suggestions": "Zobrazuje sa nastavenie {count} s návrhmi.", - "split_intro": "Kliknite na graf a pridajte alebo odstráňte bod rozdelenia, alebo automaticky zistite nečinné medzery. Každý výsledný segment môže mať vlastný profil.", - "storage_diagnostics": "Štatistiky úložiska, údržba, export/import", + "showing_suggestions": "Zobrazuje sa {count} nastavení s návrhmi.", + "showing_suggestions_one": "Zobrazuje sa {count} nastavenie s návrhmi.", + "showing_suggestions_few": "Zobrazujú sa {count} nastavenia s návrhmi.", + "showing_suggestions_many": "Zobrazuje sa {count} nastavenia s návrhmi.", + "showing_suggestions_other": "Zobrazuje sa {count} nastavení s návrhmi.", + "split_intro": "Kliknutím na graf pridáte alebo odstránite bod rozdelenia, alebo ho nájdite automaticky podľa medzier nečinnosti. Každá časť môže dostať vlastný profil.", "sug_staged": "Nastavené {key} = {val}. Uložte pre použitie.", "sug_muted": "Toto nastavenie sa už nebude navrhovať", "sug_mute_failed": "Nastavenie sa nepodarilo stíšiť", "sug_unmuted_all": "Stíšené návrhy obnovené", - "n_suggestions_muted": "{count} stíšených; automatický ladiaci nástroj tieto nastavenia nebude navrhovať.", + "n_suggestions_muted": "{count} návrhov je stíšených; automatický ladiaci nástroj ich nebude navrhovať.", + "n_suggestions_muted_one": "{count} návrh je stíšený; automatický ladiaci nástroj ho nebude navrhovať.", + "n_suggestions_muted_few": "{count} návrhy sú stíšené; automatický ladiaci nástroj ich nebude navrhovať.", + "n_suggestions_muted_many": "Stíšené návrhy: {count}; automatický ladiaci nástroj ich nebude navrhovať.", + "n_suggestions_muted_other": "{count} návrhov je stíšených; automatický ladiaci nástroj ich nebude navrhovať.", "toast_auto_detect_enabled": "Automatická detekcia zapnutá", "toast_auto_label_complete": "Automatické označovanie dokončené", "toast_auto_label_failed": "Automatické označovanie zlyhalo: {error}", @@ -984,7 +955,6 @@ "toast_no_automation": "Tu nie je možné vytvoriť automatizáciu", "toast_profile_name_required": "Vyžaduje sa názov profilu", "toast_program_set": "Program nastavený: {program}", - "toast_rebuild_failed": "Prebudovanie zlyhalo: {error}", "toast_resume_failed": "Pokračovanie zlyhalo: {error}", "toast_revert_failed": "Vrátenie zmien zlyhalo: {error}", "toast_reverted": "{key} vrátené späť", @@ -995,95 +965,82 @@ "trend_energy_up": "Energetický trend stúpa ({pct} %/cyklus) – posledný priemer {avg}", "trim_destructive_confirm": "Týmto zostane len {pct}% cyklu a túto akciu nie je možné vrátiť späť. Pokračovať?", "trim_intro": "Potiahnite červené rukoväti alebo zadajte hodnoty. Všetko mimo okna sa odstráni.", - "tuning_suggestions_available": "{count} návrh ladenia dostupný z pozorovaných cyklov. Zobrazujú sa vedľa príslušných polí.", + "tuning_suggestions_available": "{count} návrhov ladenia je k dispozícii na základe pozorovaných cyklov. Zobrazujú sa vedľa príslušných polí.", + "tuning_suggestions_available_one": "{count} návrh ladenia je k dispozícii na základe pozorovaných cyklov. Zobrazuje sa vedľa príslušného poľa.", + "tuning_suggestions_available_few": "{count} návrhy ladenia sú k dispozícii na základe pozorovaných cyklov. Zobrazujú sa vedľa príslušných polí.", + "tuning_suggestions_available_many": "{count} návrhu ladenia je k dispozícii na základe pozorovaných cyklov. Zobrazujú sa vedľa príslušných polí.", + "tuning_suggestions_available_other": "{count} návrhov ladenia je k dispozícii na základe pozorovaných cyklov. Zobrazujú sa vedľa príslušných polí.", "unsure_detected_prefix": "WashData si nie je istý, či zistil", - "updated": "Aktualizované", "warmup_badge": "Ešte sa učí ({done}/{needed} cyklov)", - "warmup_detail": "Tento profil potrebuje {needed} označených cyklov, kým sa spustí automatické párovanie. Každý potvrdený cyklus mu pomáha učiť sa.", + "warmup_detail": "Párovanie už funguje. WashData vás žiada o potvrdenie prvých {needed} cyklov tohto profilu, kým začne označovať samo.", "wipe_history_warning": "Natrvalo odstrániť všetky cykly a profily. Nedá sa vrátiť späť.", "compare_overlay_tip": "Prekryte naučené obálky profilov, aby ste videli, ktorý program každý cyklus pripomína.", - "duration_trend_up_tip": "Trvanie rastie ({pct}%/cyklus)", - "duration_trend_down_tip": "Trvanie klesá ({pct}%/cyklus)", - "energy_trend_up_tip": "Energia rastie ({pct}%/cyklus)", - "energy_trend_down_tip": "Energia klesá ({pct}%/cyklus)", - "performance_trending": "Trend výkonu", - "pg_select_cycle_hint": "Vyberte cyklus a kliknutím na ↺ ho načítajte", - "pg_sweep_progress": "Krok {done} / {total}", - "pg_analysis_hint": "Vyberte cyklus a kliknutím na ↺ načítajte analýzu.", "diagnostics_load_failed": "Nepodarilo sa načítať diagnostiku: {error}", - "manual_duration_ref_hint": "Použije sa iba vtedy, keď nie je vybraný žiadny referenčný cyklus. Referenčný cyklus nastaví trvanie podľa vlastnej dĺžky.", + "manual_duration_ref_hint": "Použije sa len bez referenčného cyklu; referenčný cyklus nastaví trvanie podľa svojej dĺžky.", + "duration_learned_hint": "Vypočítané z cyklov tohto programu (bez odľahlých hodnôt) a aktualizuje sa po každom novom. Ak to chcete zmeniť, orežte alebo odstráňte cykly, ktoré nesedia.", "toast_convert_rolled_back": "Konverzia zlyhala a bola vrátená späť (nezostala žiadna automatizácia): {error}", "toast_convert_orphan": "Automatizácia bola vytvorená, ale vymazanie starých akcií zlyhalo. Neskúšajte to znova: staré akcie odstráňte ručne, aby ste predišli duplicitnej automatizácii.", - "advisory_duration_trend_up": "Cykly '{name}' trvajú čoraz dlhšie (približne +{pct}% na cyklus). Ak sa správanie spotrebiča zmenilo, nahrajte alebo prebudujte tento profil znova.", - "advisory_energy_trend_up": "'{name}' odoberá čoraz viac energie (približne +{pct}% na cyklus) - ak je to nečakané, oplatí sa spotrebič skontrolovať.", - "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.", + "advisory_duration_trend_up": "Cykly '{name}' trvajú čoraz dlhšie (asi +{pct}% na cyklus). Ak sa spotrebič zmenil, nahrajte alebo prepočítajte tento profil znova.", + "advisory_energy_trend_up": "'{name}' spotrebuje s každým cyklom viac energie (asi +{pct}% na cyklus). Ak je to nečakané, skontrolujte spotrebič.", + "advisory_poor_health": "'{name}' sedí zle: jeho posledné cykly sa veľmi líšia alebo sa slabo zhodujú. Prezrite jeho cykly alebo ho nahrajte znova, aby párovanie a odhady zostali presné.", + "advisory_shape_drift": "'{name}' sa odchýlil od pôvodného tvaru krivky výkonu, možno pre vodný kameň alebo opotrebenie. Zvážte nové nahranie z posledných cyklov.", + "advisory_shape_drift_corr": "'{name}' sa odchýlil od pôvodného tvaru krivky výkonu (korelácia {corr}), možno pre vodný kameň alebo opotrebenie. Zvážte nové nahranie z posledných cyklov.", + "advisory_unmatchable": "'{name}' sa nikdy nepriradí: nestojí za ním žiadny cyklus s údajmi o výkone. Opravíte to označením alebo nahraním cyklu tohto programu.", + "advisory_unmatchable_no_duration": "'{name}' sa nikdy nepriradí: jeho cyklus nemá použiteľnú dobu trvania. Nahrajte tento program znova alebo nastavte jeho očakávanú dobu trvania.", "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_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.", "pg_sweep_intro2": "Nájdite nastavenie, ktoré najlepšie spĺňa cieľ naprieč vašimi nedávnymi cyklami. Kým ho nepoužijete, nič sa nezmení.", - "pg_running_history": "Prehrávanie vašich cyklov…", - "pg_running_sweep": "Testovanie hodnôt naprieč vašimi cyklami…", "pg_row_load_hint": "Načítať tento cyklus do grafu vyššie", - "pg_history_intro2": "Prehrajte svoje nedávne cykly cez skutočný detektor a nástroj na priraďovanie programov s nastaveniami vyššie. Kliknutím na ľubovoľný riadok načítate daný cyklus do grafu; úpravou nastavenia zobrazíte porovnanie pred úpravou a po nej.", + "pg_history_intro2": "Prehrajte nedávne cykly skutočným detektorom a párovaním s nastaveniami vyššie. Kliknutím na riadok ho načítate do grafu; zmenou nastavenia porovnáte pred a po.", "pg_simulating": "Simulácia cyklu…", - "pg_canvas_empty2": "Vyberte cyklus vyššie a stlačením Spustiť ho nasimulujte. Potom prejdením myšou zobrazíte hodnoty, kolieskom priblížite a ťahaním posuniete.", "pg_apply_settings_confirm": "Uložiť týchto {n} nastavení do tohto zariadenia? {list}", - "pg_stress_terminated_detail": "Ustálil sa na ~{idle}W v nečinnosti -- cyklus sa skončil o {h} h {m} min neskôr cez {reason}.", - "pg_stress_above_threshold_detail": "Príkon v nečinnosti ~{idle}W je rovný alebo vyšší než efektívny prah zastavenia ({stop}W) -- cyklus sa nikdy nezaregistroval ako pokojový. Opravte to zvýšením stop_threshold_w.", - "pg_stress_hit_cap_detail": "Cyklus bežal {h} h {m} min bez zastavenia -- vynútene zastavený bezpečnostným limitom. Príkon v nečinnosti {idle}W oproti prahu zastavenia {stop}W.", "no_logs_match": "Týmto filtrom nezodpovedajú žiadne záznamy protokolu.", "pg_analysis_hint2": "Vyberte cyklus a stlačte Spustiť na načítanie analýzy zhody.", - "pg_analysis_empty2": "Stlačte Spustiť na zobrazenie analýzy zhody.", - "imported_readonly": "Importované z komunitného obchodu. Zobrazuje sa pre referenciu a porovnávanie. Nezapočítava sa do vašich štatistík a nedá sa upravovať.", + "imported_readonly": "Importované z komunitného obchodu. Slúži ako referencia a na párovanie, nezapočítava sa do štatistík a nedá sa upravovať.", "merge_no_imports": "Importované záznamy nemožno zlúčiť so skutočným cyklom. Zrušte ich výber, aby ste ich mohli zlúčiť.", - "store_enable_hint": "Povoľte online funkcie v Nastaveniach, aby ste mohli prehliadať a importovať referenčné cykly komunity.", - "advisory_duration_outlier": "Cykly označené ako '{name}' ({n}) sú príliš vzdialené od jeho obvyklej dĺžky, než aby sa s ním dali niekedy zhodovať (najväčšia odchýlka: {ratio}x). Pravdepodobne sú nesprávne označené alebo jeden záznam zachytil dva behy. Označte ich znova alebo ich rozdeľte, aby prestali skresľovať odhad času tohto programu.", + "store_enable_hint": "Zapnite online funkcie v časti Predvoľby > Online a komunita, aby ste mohli prehliadať a importovať referenčné cykly komunity.", + "advisory_duration_outlier": "Cykly označené ako ‘{name}’ ({n}) sú príliš ďaleko od jeho obvyklej dĺžky, aby sa niekedy priradili (najhorší: {ratio}x). Pravdepodobne sú nesprávne označené alebo obsahujú dva behy. Označte ich znova alebo rozdeľte, aby prestali skresľovať odhad času.", "standby_above_stop_title": "Pri ukončení cyklu stále odoberá {idle} W", "standby_above_stop_sub": "{n} z posledných {total} cyklov. Oneskorenie vypnutia sa začne počítať až vtedy, keď príkon klesne pod Prah zastavenia ({stop} W), takže cykly končia neskoro alebo ich treba zastaviť ručne.", - "online_intro": "Zdieľajte a sťahujte referenčné cykly s ostatnými používateľmi WashData. Všetky online funkcie sú dobrovoľné a predvolene vypnuté.", "store_share_intro": "Nahrajte tento referenčný cyklus, aby ho mohli používať ostatní s rovnakým spotrebičom. Pred zverejnením bude skontrolovaný.", - "online_intro_global": "Prehliadajte a zdieľajte referenčné záznamy s ostatnými používateľmi WashData a potvrdzujte záznamy spotrebičov. Jedno pripojenie platí pre celú vašu integráciu WashData; značka a model spotrebiča sa nastavujú pre každé zariadenie v sekcii Základné. Všetky online funkcie sú dobrovoľné a predvolene vypnuté.", + "online_intro_global": "Prehliadajte a zdieľajte referenčné záznamy s ostatnými používateľmi WashData. Jedno pripojenie platí pre všetky vaše zariadenia WashData; značka a model sa nastavujú pre každé zariadenie v sekcii Základné. Online funkcie sú vypnuté, kým ich nezapnete.", "online_unavailable": "Online funkcie nie sú na tomto serveri dostupné.", "store_picker_offline": "Povoľte online funkcie v ozubenom koliesku nastavení, aby ste mohli vyberať z katalógu komunity.", "pick_brand_first": "Najprv vyberte značku spotrebiča.", - "model_not_found": "Zatiaľ nie je v katalógu.", - "connect_to_confirm": "Pripojte sa v ozubenom koliesku nastavení na potvrdenie alebo hodnotenie.", + "custom_task_limit": "Dosiahli ste maximálny počet úloh ({max}).", + "custom_tasks_intro": "Pridajte čokoľvek ďalšie, na čo chcete dostať pripomienku. Úloha je na rade, keď uplynie ktorýkoľvek z intervalov; jeden nechajte prázdny alebo 0, aby platil len ten druhý.", + "maintenance_due_sensor_hint": "Binárny senzor Údržba na rade je zapnutý, kým je niektorá úloha na rade; použite ho vo vlastných automatizáciách upozornení.", + "maintenance_supporter_intro": "Maintenance Supporter je samostatná integrácia, ktorá dokáže oveľa viac: ľubovoľný spotrebič alebo úloha, plánovanie podľa času, používania alebo senzora, upozornenia a história. Dokáže využívať počítadlo cyklov WashData.", "adopt_settings_hint": "Použiť nastavenia detekcie a porovnávania zdieľaného zariadenia na toto zariadenie", - "include_settings_hint": "Zahrnúť prahy detekcie, váhy porovnávania a ďalšie parametre do zdieľaného balíčka", + "include_settings_hint": "Zdieľať prahy detekcie a párovania tohto zariadenia (nie upozornenia, entity ani cenu energie). Ostatní sa rozhodnú, či ich použijú.", "onboard_download": "Stiahnuť hotové nastavenie z obchodu komunity", - "share_consent": "Zdieľate skutočné dáta zo svojho spotrebiča. Nezdieľajte, ak sú vaše vzorce používania súkromné.", "share_device_none": "Zatiaľ nie sú nastavené žiadne zariadenia. Najprv pridajte zariadenie.", "share_guideline_naming": "Používajte zrozumiteľné názvy programov (napr. 'Cotton 40', 'Eco 60'), aby ich ostatní mohli identifikovať", - "share_guideline_quality": "Zdieľajte iba cykly, ktoré prebehli normálne: bez prerušení uprostred cyklu, otvorenia dvierok alebo krátkych výpadkov napájania.", + "share_guideline_quality": "Zdieľajte len cykly, ktoré skončili normálne: bez prerušení, otvorenia dvierok a výpadkov napájania.", "share_guideline_review": "Pred zdieľaním skontrolujte svoje profily -- odstráňte tie, ktoré vyzerajú zle", "share_guidelines_title": "Pred zdieľaním", - "store_download_device_intro": "Stiahnuť nastavenie zariadenia komunity a použiť ho na nové alebo existujúce zariadenie", + "store_download_device_intro": "Pridať do zariadenia všetky zdieľané programy a ich referenčné cykly. Vaše vlastné cykly a štatistiky sa nezmenia.", "store_share_device_intro": "Nahrajte {brand} {model} s referenčnými cyklami, ktoré vyberiete. Ostatní s rovnakým spotrebičom môžu vaše programy prevziať. Záznamy sú pred zverejnením skontrolované.", "share_profile_no_cycles": "Žiadne referenčné cykly. Ak chcete zahrnúť tento profil, označte cyklus ako ⭐ na karte Cykly", - "advisory_phase_inconsistent": "Zdá sa, že '{name}' mieša rôzne programy alebo teploty - jeho cykly sa ohrievajú veľmi rôzne dlho. Rozdelenie na samostatné profily (napr. podľa teploty) zlepší zhodu aj odhady času.", - "advisory_phase_inconsistent_title": "⚠ Možno zmiešané programy", - "export_select_intro": "Zaškrtnite presne to, čo sa má zahrnúť. Výber profilov bez ich cyklov aj tak exportuje rozpoznateľný program (jeho naučená krivka sa prenesie spolu s ním).", - "import_analyze_hint": "Načítajte exportovaný súbor (alebo vložte jeho JSON). WashData ho analyzuje a presne ukáže, čo možno importovať, skôr než dôjde k akejkoľvek zmene.", + "export_select_intro": "Zaškrtnite, čo zahrnúť. Profily bez svojich cyklov stále exportujú rozpoznateľný program.", + "import_analyze_hint": "Načítajte exportovaný súbor (alebo vložte jeho JSON). WashData ukáže, čo možno importovať, skôr než sa čokoľvek zmení.", "analyzing": "Analyzujem…", - "device_type_mismatch_warn": "Tento export pochádza z iného typu spotrebiča ({src} vs {local}). Programy a cykly možno stále importovať ako referenčné údaje, ale nastavenia špecifické pre zariadenie a import skutočnej histórie sú zakázané.", + "device_type_mismatch_warn": "Tento export pochádza z iného typu spotrebiča ({src} vs {local}). Programy a cykly možno stále importovať ako referenčné údaje, ale nie nastavenia zariadenia ani skutočnú históriu.", "merge_hint": "Importované položky sa pridajú; nič miestne sa nestratí. Konflikty názvov sa riešia nižšie.", "replace_warn": "Každá zaškrtnutá kategória sa vymaže a nahradí údajmi zo súboru. Nezaškrtnuté kategórie zostanú bez zmeny.", "dest_reference_hint": "Importované cykly len zlepšujú rozpoznávanie programov a nikdy neovplyvňujú štatistiky spotreby/energie.", - "dest_real_history_hint": "Importované cykly sa počítajú ako vlastná história tohto zariadenia a prispievajú do štatistík energie/spotreby. Použite pri presune jedného spotrebiča do novej inštalácie.", - "import_history_description": "Mali ste inteligentnú zásuvku už pred WashData? Nahrajte export histórie jej senzora výkonu alebo ju načítajte priamo z Home Assistant a prebehne nad ňou bežná detekcia, takže sa staršie cykly objavia v zozname Cykly pripravené na pomenovanie.", - "hist_input_hint": "Nahrajte CSV stiahnuté z panela História (entita, stav, posledná zmena), alebo nechajte WashData načítať históriu senzora priamo. Detekcia potom prebehne presne tak ako naživo a vy vyberiete, ktoré z nájdených cyklov si ponecháte.", - "hist_recorder_hint": "Načíta údaje od zvoleného dátumu až doteraz. Home Assistant štandardne uchováva podrobnú históriu 10 dní a potom už len hodinové priemery, ktoré sú na detekciu cyklov príliš hrubé - skorší dátum voľte len vtedy, ak máte recorder nastavený na dlhšie uchovávanie.", - "hist_scanning": "Prehrávame vašu históriu cez detektor. Beží to na pozadí - tento dialóg môžete zavrieť a vrátiť sa k nemu neskôr.", + "dest_real_history_hint": "Importované cykly sa počítajú ako vlastná história a štatistiky tohto zariadenia. Použite pri presune spotrebiča do novej inštalácie.", + "import_history_description": "Mali ste inteligentnú zásuvku už pred WashData? Nahrajte jej históriu výkonu alebo ju načítajte z Home Assistant. Prebehne nad ňou detekcia a staršie cykly sa objavia v zozname Cykly pripravené na pomenovanie.", + "hist_input_hint": "Nahrajte CSV z panela História (entita, stav, posledná zmena), alebo nechajte WashData načítať históriu senzora priamo. Prebehne nad ňou živá detekcia a vy vyberiete, ktoré cykly ponechať.", + "hist_recorder_hint": "Načíta údaje od zvoleného dátumu doteraz. Home Assistant štandardne uchováva podrobnú históriu 10 dní; staršie hodinové priemery sú na detekciu cyklov príliš hrubé.", + "hist_scanning": "Vaša história prechádza detektorom na pozadí. Tento dialóg môžete zavrieť a vrátiť sa neskôr.", "hist_imported_count": "Importované cykly: {n}.", "hist_duplicates": "Už predtým importované a preskočené: {n}.", "hist_capped": "Bol dosiahnutý limit importovaných cyklov na zariadenie; zvyšok sa neuložil.", - "hist_next_step": "Nájdete ich v zozname Cykly, označené ako importovaná história. Otvorte niektorý z nich a tlačidlom Označiť pomenujte program, ku ktorému patrí.", + "hist_next_step": "Nájdete ich v zozname Cykly, označené ako importovaná história. Otvorte niektorý a tlačidlom Označiť pomenujte jeho program.", "hist_rows_read": "Prečítané merania: {n}", "hist_breaks": "Medzery, keď bol senzor nedostupný: {n}", "hist_other_entity": "Ignorované merania iných entít: {n}", @@ -1095,25 +1052,76 @@ "hist_scan_capped": "Zobrazujú sa len prví kandidáti (nájdených bolo {n}).", "hist_recorder_empty": "Home Assistant nemá pre tento senzor v danom období žiadnu podrobnú históriu.", "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_scan_expired": "Toto skenovanie už nie je dostupné. Skenujte 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.", - "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.", + "imported_history_readonly": "Nájdené v importovanej histórii výkonu. Pomáha párovaniu, ale nezapočítava sa do štatistík a nedá sa orezať ani rozdeliť. Označením pomenujte program.", + "odometer_intro": "Tento súčet len rastie. Odstránenie cyklu ho nezníži, takže pripomienky servisu zostávajú správne.", + "odometer_correct_hint": "Nastavte, ak WashData započítalo cyklus, ktorý neprebehol, alebo spotrebič 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}", "toast_range_required": "Nastavte platný rozsah Od/Do", - "no_profiles_option": "(zatiaľ žiadne profily, pridajte nový)" + "no_profiles_option": "(zatiaľ žiadne profily, pridajte nový)", + "compare_cycles_title": "Porovnať cykly ({count})", + "edit_builtin_phase": "Toto je vstavaná fáza. Uložením vznikne vlastná kópia; odstránením kópie obnovíte originál.", + "fetch_error": "Načítanie údajov zlyhalo.", + "connection_lost": "Spojenie stratené - zobrazujú sa údaje k {time}.", + "pg_notify_partial": "Značky ukazujú len upozornenia na štart, takmer hotovo, dokončenie, míľniky a tiché hodiny. Živé aktualizácie, pripomienky vyloženia a upozornenia na prekročenie sa nesimulujú.", + "connect_to_confirm": "Pripojte sa v ozubenom koliesku nastavení na potvrdenie alebo hodnotenie.", + "coverage_gap_cluster": "Nedávne neoznačené cykly ({count}) s dĺžkou približne {min} min vyzerajú ako jeden program, ktorý ste ešte nevytvorili.", + "pg_label_yes": "Na konci cyklu by bol označený ako \"{name}\"", + "pg_label_no": "Vyžiadala by sa kontrola: {reason}", + "live_match_debug_tip": "Spoľahlivosť: ako presne aktuálna krivka príkonu zodpovedá najlepšiemu kandidátskemu profilu (0-100 %). Nejednoznačné: dvaja najlepší kandidáti majú skóre v rozmedzí 5 % od seba, takže štítok je neistý až do konca cyklu.", + "automation_alias_started": "{device}: cyklus spustený", + "automation_alias_finished": "{device}: cyklus dokončený", + "automation_desc_started": "Spustí sa, keď začne cyklus zariadenia {device}. Pridajte svoje akcie (oznámenia, svetlá, ...).", + "automation_desc_finished": "Spustí sa, keď skončí cyklus zariadenia {device}. Pridajte svoje akcie (oznámenia, svetlá, ...).", + "feedback_detected": "WashData rozpoznal: {name}", + "import_undo_hint": "Uchováva sa bod obnovenia spred posledného importu ({when}): {profiles} profilov a {cycles} cyklov.", + "replace_undo_hint": "Najprv sa uloží bod obnovenia, takže tento import môžete vrátiť späť v časti Export / Import.", + "pg_in_sample_note": "Tieto cykly pomohli vytvoriť profily, s ktorými sa porovnávajú, takže výsledky sú optimistické: pri nových cykloch čakajte o niečo menej správnych priradení.", + "hist_tail_trimmed": "Orezalo sa {min} min čakania po skončení cyklu, rovnako ako to robí bežiaca integrácia", + "hist_truncated": "zastavené na limite počtu meraní ({n}), zvyšok histórie sa preto neprehľadal", + "hist_warn_kw": "Najvyššia hodnota je len {w}. Tento senzor zrejme hlási kilowatty; WashData potrebuje watty, takže nemožno rozpoznať žiadne cykly.", + "hist_warn_naive_time": "{n} časových značiek nemá časové pásmo a bolo načítaných ako UTC. Ak súbor používa miestny čas, cykly sú posunuté o váš posun voči UTC a hodnoty okolo zmeny času môžu byť vynechané.", + "hist_warn_naive_time_one": "{n} časová značka nemá časové pásmo a bola načítaná ako UTC. Ak súbor používa miestny čas, cykly sú posunuté o váš posun voči UTC a hodnoty okolo zmeny času môžu byť vynechané.", + "hist_warn_naive_time_few": "{n} časové značky nemajú časové pásmo a boli načítané ako UTC. Ak súbor používa miestny čas, cykly sú posunuté o váš posun voči UTC a hodnoty okolo zmeny času môžu byť vynechané.", + "hist_warn_naive_time_many": "{n} časovej značky nemá časové pásmo a bolo načítané ako UTC. Ak súbor používa miestny čas, cykly sú posunuté o váš posun voči UTC a hodnoty okolo zmeny času môžu byť vynechané.", + "hist_warn_naive_time_other": "{n} časových značiek nemá časové pásmo a bolo načítaných ako UTC. Ak súbor používa miestny čas, cykly sú posunuté o váš posun voči UTC a hodnoty okolo zmeny času môžu byť vynechané.", + "pg_compare_profile_tip": "Vyberá iba profil vykreslený cez graf. Simulácia vždy priraďuje program k cyklu sama, rovnako ako bežiaca integrácia.", + "pg_outcome_stale": "Od spustenia tejto simulácie sa nastavenia zmenili. Stále zobrazuje starý výsledok.", + "pg_sweep_guarded": "Ukončí viac cyklov predčasne, viac ich rozdelí alebo vynechá cykly, ktoré súčasné nastavenie zachytí, preto sa nikdy neodporúča.", + "pg_sweep_keep_current": "Ponechať súčasnú hodnotu ({value}, {metric}): žiadna hodnota nebola lepšia aspoň o jeden cyklus.", + "pg_sweep_n_cycles": "na {count} cykloch", + "pg_sweep_n_cycles_one": "na {count} cykle", + "pg_sweep_n_cycles_few": "na {count} cykloch", + "pg_sweep_n_cycles_many": "na {count} cyklu", + "pg_sweep_n_cycles_other": "na {count} cykloch", + "pg_task_busy": "Pre toto zariadenie už beží „Otestovať na histórii“ alebo „Optimalizovať“. Počkajte na dokončenie alebo beh zrušte.", + "cycle_context_loading": "Načítava sa história z recordera…", + "cycle_context_note": "Sivou: história senzora výkonu z recordera okolo tohto cyklu. Nie je súčasťou cyklu a neovplyvňuje trvanie, energiu ani párovanie.", + "cycle_context_none": "Recorder okolo tohto cyklu neuchováva žiadne merania (Home Assistant štandardne uchováva históriu 10 dní).", + "cycle_context_tip": "Len na zobrazenie: ukazuje históriu senzora výkonu z recordera okolo tohto cyklu, ak je ešte uložená. Ak chcete pridávať neúspešné pokusy o spustenie do budúcich cyklov, použite Nábeh krivky (Nastavenia > Detekcia > Začiatok cyklu), ktorý mení uložené doby trvania.", + "invalid_threshold_pair": "Prah zastavenia ({stop} W) musí byť nižší ako Prah spustenia ({start} W). Nič sa neuložilo; zmeňte obe hodnoty spolu." }, "phase_desc": { "anti_crease": "Príležitostné krátke vírenia po dokončení na predchádzanie krčeniu oblečenia.", "anti_wrinkle": "Pravidelné vírenie po cykle na predchádzanie krčeniu oblečenia.", + "baking": "Ohrievač pri vysokej teplote vytvára kôrku a striedku.", + "cooking": "Hlavná fáza prípravy so zapnutým ohrevom a ventilátorom.", "cool_down": "Vírenie bez tepla blízko konca cyklu.", + "cool_down_af": "Chladenie iba ventilátorom po ohreve.", "drain_and_switch": "Obdobie prechodu z režimu prania do režimu sušenia.", "dry": "Fáza sušenia pomocou ohrievača a/alebo zvyškového tepla.", "drying": "Hlavné vyhrievané obdobie sušenia s vírením.", "heat_up": "Počiatočné zahriatie ohrievača pred úplným sušením.", + "keep_warm": "Mierny ohrev, aby jedlo zostalo teplé.", + "keep_warm_bm": "Mierny ohrev, aby chlieb po upečení zostal teplý.", + "kneading": "Motor mieša a miesi cesto. Vysoký príkon.", + "pause_af": "Krátka pauza na pretrasenie alebo kontrolu.", + "pre_heat": "Úvodný ohrev komory pred samotnou prípravou.", "pre_rinse": "Počiatočné striekanie pred umývaním čistiaceho prostriedku.", + "proving": "Kysnutie pri miernom teple: kvasnice kvasia a cesto naberá objem.", + "resting": "Krátka pauza s nízkym príkonom medzi miesením, aby sa lepok uvoľnil.", "pre_wash": "Počiatočné namáčanie alebo predbežná úprava pred hlavným praním.", "rinse": "Fáza oplachovania čistou vodou. Táto fáza sa môže opakovať viackrát.", "sanitize": "Vysokoteplotný čistiaci stupeň pre dezinfekčné programy.", @@ -1126,6 +1134,7 @@ "wash_dw": "Hlavné pranie čistiaceho prostriedku s ohrevom." }, "placeholder": { + "custom_task": "napr. Vyčistiť tesnenie dvierok", "add": "pridať…", "group_name": "napr. Bavlna 2:47", "json_buttons": "[{\"action\":\"ID\",\"title\":\"Label\"}]", @@ -1153,10 +1162,10 @@ "section": { "dishwasher": { "label": "Umývačka riadu", - "intro": "Spracovanie konca cyklu pre umývačky riadu, ktoré zvyčajne končia dlhou takmer tichou fázou sušenia pred krátkym záverečným odčerpaním vody." + "intro": "Spracovanie konca pri umývačkách riadu, ktoré často dlho a ticho sušia pred krátkym záverečným odčerpaním." }, "anti_wrinkle": { - "intro": "Režim ochrany pred krčením tkaniny deteguje pulzy tumblingu pri nízkej spotrebe po hlavnej fáze a udržuje ich priradené k dokončenému cyklu.", + "intro": "Ponechá krátke pulzy bubna s nízkym výkonom po hlavnej fáze sušičky pri dokončenom cykle.", "label": "Ochrana pred krčením" }, "basic": { @@ -1164,7 +1173,7 @@ "label": "Základné" }, "delay": { - "intro": "Detekcia odloženého štartu identifikuje, keď je spotrebič zapnutý, ale ešte nezačal cyklus.", + "intro": "Rozpozná spotrebič, ktorý je zapnutý a čaká na odložený štart.", "label": "Odložený štart" }, "detection": { @@ -1188,157 +1197,149 @@ "label": "Časovanie a watchdog" }, "triggers": { - "intro": "Voliteľné externé signály: spúšť ukončenia, senzor dverí, prepínač pauzy a pripomienka vyloženia.", + "intro": "Voliteľné doplnky: spúšť ukončenia, senzor dverí, prepínač pauzy a pripomienka vyloženia.", "label": "Spúšte a dvere" - }, - "phase_eta": { - "label": "Zostávajúci čas", - "intro": "Zostávajúci čas zohľadňujúci fázy, pre spotrebiče, ktorých dĺžka cyklu závisí od teploty alebo odstreďovania." } }, "setting": { "dishwasher_end_spike_quiet_release": { "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." + "doc": "Po očakávanej dobe trvania: ako dlho musí umývačka zostať pod Prahom zastavenia, než WashData prestane čakať na záverečné odčerpanie. Zvýšte, ak sa neskoré odčerpanie po dlhom sušení nezachytí. Čakanie nikdy nepresiahne 30 minút." }, "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.", + "doc": "Zlomok priemernej doby trvania programu, ktorý musí cyklus dosiahnuť, než sa otáčanie bubna s nízkym výkonom počíta ako chvost ochrany pred krčením. Pri sušičkách riadených náplňou ho znížte (napr. 0,75). Pri práčkach ponechajte predvolenú hodnotu (0,98), inak môže pranie skončiť v polovici.", "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.", + "doc": "Ponechať krátke pulzy bubna s nízkym výkonom po hlavnej fáze ohrevu sušičky pri dokončenom cykle, namiesto ich čítania ako nových cyklov.", "label": "Povoliť detekciu ochrany pred krčením" }, "anti_wrinkle_exit_power": { - "doc": "Aby režim proti vráskam zostal aktívny, musí výkon medzi impulzmi klesnúť pod túto hodnotu.", + "doc": "V režime ochrany pred krčením sa výkon pod touto hodnotou počíta ako pokoj medzi impulzmi otáčania a pokoj dlhší ako Max. medzera medzi impulzmi režim ukončí. Ak je Prah zastavenia vyšší, použije sa on, takže nastavenie má vplyv len nad Prahom zastavenia.", "label": "Prah výkonu ukončenia" }, "anti_wrinkle_idle_timeout": { - "doc": "Ako dlho môže stroj medzi dvoma impulzmi bubna zostať v pokoji, než sa režim ochrany pred krčením skončí. Nastavte ju nad najdlhšiu medzeru, ktorú vaša sušička medzi impulzmi ponecháva, inak sa každý ďalší impulz vyhodnotí ako falošné spustenie.", + "doc": "Ako dlho môže sušička medzi pulzmi bubna zostať v pokoji, než režim ochrany pred krčením skončí. Nastavte nad najdlhšiu medzeru vašej sušičky, inak sa ďalšie pulzy vyhodnotia ako falošné štarty.", "label": "Max. medzera medzi impulzmi" }, "anti_wrinkle_max_duration": { - "doc": "Dlhšie pulzy sa považujú za skutočný cyklus a nie za protivráskové.", + "doc": "Pulz dlhší ako tento čas sa počíta ako skutočný cyklus, nie ako otáčanie ochrany pred krčením.", "label": "Max. trvanie" }, "anti_wrinkle_max_power": { - "doc": "Pulz nad touto silou sa považuje za skutočný nový cyklus, nie ako protivráskový pád. Nastavte tesne nad silou pulzného bubna.", + "doc": "Pulz nad týmto výkonom sa počíta ako skutočný nový cyklus, nie ako otáčanie bubna. Nastavte tesne nad výkon pulzov bubna.", "label": "Max. výkon ochrany pred krčením" }, "auto_label_confidence": { - "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.", + "doc": "Skóre zhody na konci cyklu, od ktorého sa program označí bez otázky. Zvýšte pre väčšiu istotu; znížte, aby sa automaticky označovalo viac cyklov. Držte nad Spoľahlivosťou učenia.", "label": "Spoľahlivosť autooznačovania" }, "auto_maintenance": { - "doc": "Spustite nočnú upratovaciu službu: prestavte obálky profilu, prepočítajte stav cyklu, odstráňte stopy ladenia a uchovávajte najnovšie cykly.", + "doc": "Nočné upratovanie: označí staršie cykly, ktoré teraz jasne zodpovedajú programu, prepočíta krivky programov, obnoví značky anomálií a zdravie cyklov a odstráni ladiace záznamy, ak je vypnuté Uložiť ladiace záznamy. Cykly a ich záznamy výkonu sa vždy zachovajú.", "label": "Automatická údržba (nočné čistenie)" }, "completion_min_seconds": { - "doc": "Cykly kratšie ako toto sú vyradené ako cykly duchov (testovacie cykly, otvorenie dvierok na pridanie ponožky).", + "doc": "Cykly kratšie ako tento čas (skúšobné behy, otvorenie dvierok kvôli pridaniu ponožky) sa uložia ako prerušené a nedostanú upozornenie o dokončení.", "label": "Min. trvanie cyklu" }, "delay_confirm_seconds": { - "doc": "Napájanie musí zostať v pohotovostnom pásme dlho predtým, ako sa bude spotrebič považovať za čakajúci na spustenie a nie za bežiaci.", + "doc": "Výkon musí zostať v pohotovostnom pásme počas tohto času, než sa spotrebič počíta ako čakajúci na štart.", "label": "Sekundy potvrdenia" }, "delay_start_detect_enabled": { - "doc": "Zistíte, kedy je spotrebič zapnutý a čaká (oneskorený štart/pohotovostný režim), ale nezačal svoj cyklus, takže odber v pohotovostnom režime sa nepovažuje za prebiehajúci cyklus.", + "doc": "Rozpoznať spotrebič, ktorý je zapnutý a čaká na odložený štart, aby sa jeho pohotovostný odber nepovažoval za bežiaci cyklus.", "label": "Povoliť detekciu odloženého štartu" }, "delay_timeout_hours": { - "doc": "Po týchto hodinách prestaňte čakať v režime odloženého štartu a vráťte sa do režimu nečinnosti, takže stroj, ktorý zostal napájaný, ale nebol spustený, nebude čakať večne.", + "doc": "Po tomto počte hodín prestať čakať na odložený štart a vrátiť sa do nečinnosti.", "label": "Hodiny časového limitu" }, "device_type": { - "doc": "Trieda spotrebičov. Nastaví citlivé predvolené hodnoty detekcie (prahové hodnoty, oneskorenie vypnutia, spracovanie konca) vyladené pre daný typ zariadenia; zmeniť iba v prípade, že zariadenie bolo pôvodne nastavené ako nesprávny typ.", + "doc": "Nastaví predvolené hodnoty detekcie (prahy, oneskorenie vypnutia, spracovanie konca) pre tento druh spotrebiča. Meňte len vtedy, keď bolo zariadenie nastavené ako nesprávny typ.", "label": "Typ zariadenia" }, "door_sensor_entity": { - "doc": "Voliteľný dverný binárny senzor. Používa sa na zistenie, kedy bol spotrebič otvorený/vyložený po cykle.", + "doc": "Voliteľný senzor dverí. Ukazuje, kedy bol spotrebič po cykle otvorený kvôli vyloženiu.", "label": "Entita senzora dverí" }, "duration_tolerance": { - "doc": "Tolerancia pre odhady zostávajúceho času (učenie sa spätnej väzby, nesúlad). Ak je skutočné trvanie v rozmedzí +/-X % od odhadu, počíta sa to ako dobrá zhoda.", + "doc": "Len pre spätnú väzbu učenia: cyklus v rozmedzí +/-X % odhadovanej doby sa počíta ako dobrý odhad. Neovplyvňuje párovanie.", "label": "Tolerancia odhadu" }, "enable_ml_models": { - "doc": "Kým cyklus beží, nechajte modely spresniť aktuálne výsledky: stabilnejší zostávajúci čas a odhad energie/nákladov a ochrana proti predčasnému zastaveniu pri detekcii konca (môže vždy len oddialiť koniec, nikdy ho neskončiť skôr a je ohraničená). Používa vaše vyladené modely, ak sú k dispozícii, inak vstavané. Vypnuté = len klasická logika založená na výkone (stále spoľahlivá).", + "doc": "Použiť model natrénovaný na tomto stroji na predpoveď energie a nákladov bežiaceho cyklu a nechať dobre známy program, ktorý skončí nezvyčajne skoro, dokončiť bez celého oneskorenia vypnutia. Vypnuté: len logika založená na výkone.", "label": "Použiť inteligentné modely počas cyklu" }, "end_energy_threshold": { - "doc": "Počas odpočítavania oneskorenia vypnutia sa akumulovaná energia (watty x čas) porovnáva s touto prahovou hodnotou. Ak sa prekročí, odpočítavanie sa vynuluje - bubny proti pokrčeniu a sušiace konce umývačky riadu sú pripojené k cyklu namiesto ich skrátenia. Zdvihnite ho, ak sa cykly počas ochladzovania skončia príliš skoro; znížte ju, ak je detekcia pomalá.", + "doc": "Energia povolená počas oneskorenia vypnutia. Pri vyššej spotrebe cyklus pokračuje, takže otáčanie bubna a záverečné sušenie zostanú jeho súčasťou. Znížte, ak cykly končia počas chvosta; zvýšte, ak dokončené cykly stále čakajú.", "label": "Energia ukončenia" }, - "end_repeat_count": { - "doc": "Počet po sebe idúcich odčítaní pod prahovou hodnotou zastavenia požadovaných pred koncom cyklu. 1 je v poriadku pre väčšinu zástrčiek. Zvýšte na 2-3, ak vaša inteligentná zásuvka občas hlási vzorku s falošnou nulou uprostred cyklu a vaše cykly sa predčasne končia.", - "label": "Počet opakovaní ukončenia" - }, "energy_price_entity": { - "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í.", + "doc": "Senzor s aktuálnou cenou za kWh (napr. dynamická tarifa). Má prednosť pred statickou cenou nižšie, takže musí ísť o cenu, nie o senzor výkonu alebo energie zásuvky.", "label": "Entita ceny energie" }, "energy_price_static": { - "doc": "Pevná cena za kWh použitá pre číselné údaje o nákladoch, keď vyššie nie je nastavená žiadna aktuálna cena.", + "doc": "Pevná cena za kWh, použitá, keď nie je nastavená entita ceny.", "label": "Statická cena energie (za kWh)" }, "energy_sensor": { - "doc": "Voliteľné kumulatívne počítadlo energie (total_increasing kWh/Wh, napr. vlastný celoživotný merač zásuvky). Keď je nastavené, energia hlásená za každý cyklus sa odvodzuje z rozdielu tohto počítadla medzi začiatkom a koncom, čo zabraňuje podhodnoteniu, ktoré vzniká integráciou pomaly hlásiaceho senzora výkonu. Vráti sa k integrovanej hodnote, ak údaj chýba, jeho jednotka je neznáma alebo rozdiel nie je kladný. Nechajte prázdne, ak chcete naďalej integrovať senzor výkonu.", + "doc": "Voliteľné počítadlo kWh alebo Wh (total_increasing, napr. vlastný merač zásuvky). Energia cyklu sa potom číta z neho, čo je presnejšie ako výpočet z pomalého senzora výkonu. Ak je jeho hodnota nepoužiteľná, použije sa vypočítaná.", "label": "Entita merača energie" }, "expose_debug_entities": { - "doc": "Zverejniť ďalšie diagnostické entity HA (dôvera v zhodu, nejednoznačnosť, vnútorné údaje). Vypnuté udržiava zoznam entít čistý na bežné použitie.", + "doc": "Pridať diagnostické entity (spoľahlivosť zhody, nejednoznačnosť, vnútorný stav). Vypnuté: zoznam entít zostane prehľadný.", "label": "Zobraziť ladiace entity" }, "external_end_trigger": { - "doc": "Binárny snímač, ktorého zmena stavu označuje koniec cyklu (napr. „dokončený“ kontakt spotrebiča alebo integrácia spoločníka).", + "doc": "Binárny senzor, ktorého zmena označuje koniec cyklu (napr. kontakt „dokončené“ alebo iná integrácia).", "label": "Entita externého spúšte" }, "external_end_trigger_enabled": { - "doc": "Nechajte externý binárny senzor signalizovať koniec cyklu, navyše k vstavanej detekcii založenej na výkone.", + "doc": "Nechať binárny senzor signalizovať koniec cyklu, navyše k detekcii podľa výkonu.", "label": "Povoliť externý spúšť ukončenia" }, "external_end_trigger_inverted": { - "doc": "Snímač spúšte, ktorý sa vypína (a nie ZAP), berte ako signál konca cyklu.", + "doc": "Brať VYPNUTIE senzora (nie zapnutie) ako signál konca.", "label": "Invertovať externý spúšť (spustiť pri VYPNUTÍ)" }, "learning_confidence": { - "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.", + "doc": "Najnižšie skóre zhody na označenie dokončeného cyklu. Jasný víťaz sa označí; tesný výsledok ide do frontu kontroly v Cykloch. Pod touto hodnotou sa nič neoznačí ani sa nepýta. Držte ju pod Spoľahlivosťou autooznačovania.", "label": "Spoľahlivosť učenia" }, "linked_device": { - "doc": "Toto zariadenie WashData môžete voliteľne vložiť pod iné zariadenie (napr. inteligentnú zásuvku) v registri zariadenia HA, ktorý sa zobrazí ako „Pripojené cez ...“.", + "doc": "Voliteľné. Zobraziť toto zariadenie WashData pod iným zariadením (napr. inteligentnou zásuvkou) v Home Assistant ako „Pripojené cez ...“.", "label": "Zoskupiť pod zariadenie" }, "min_off_gap": { - "doc": "Ak sa stroj vypne na kratšiu dobu, postupnosť zapnutia/vypnutia/zapnutia sa považuje za jeden nepretržitý cyklus. Zabraňuje rozdeleniu programov namáčania (práčka sa vypne na niekoľko minút uprostred prania) do dvoch samostatných cyklov. Ak chcete, aby sa počítali ako samostatné cykly, nastavte ju kratšiu ako je medzera medzi záťažami. Predvolené nastavenia typu zariadenia chránia typickú prestávku v rámci cyklu pre každý spotrebič.", + "doc": "Vypnutie kratšie ako tento čas cyklus nerozdelí, takže pauzy namáčania zostanú v jednom praní. Držte kratšie ako medzera medzi po sebe idúcimi náplňami, inak sa zlúčia do jedného cyklu.", "label": "Min. medzera vypnutia" }, "min_power": { - "doc": "Absolútny minimálny výkon považovaný za aktívny. Hodnoty nižšie sa považujú za 0 W (pohotovostný režim), čím sa odfiltruje fantómová záťaž inteligentných zástrčiek a pohotovostných LED diód.", + "doc": "Hodnoty pod touto hranicou sa počítajú ako 0 W (pohotovosť). Odfiltruje pokojový odber inteligentných zásuviek a pohotovostných LED.", "label": "Minimálny výkon" }, "ml_training_enabled": { - "doc": "Pravidelne cez noc študujte svoje skontrolované cykly a dolaďujte modely pre tento konkrétny stroj. Zmena sa zachová iba vtedy, keď skutočne dosiahne lepšie výsledky v odložených cykloch, takže to môže len pomôcť alebo zostať rovnaké – nikdy sa nevrátiť späť.", + "doc": "Občas pretrénovať model energie na dokončených cykloch tohto stroja. Nový model sa ponechá len vtedy, keď na testovacích cykloch prekoná jednoduchý odhad.", "label": "Učiť sa z tohto stroja" }, "ml_training_hour": { - "doc": "Miestna hodina dňa (0-23), aby sa vykonalo nočné jemné doladenie. Vyberte pokojnú hodinu, napríklad 2 (02:00).", + "doc": "Hodina dňa (0-23) pre nočný tréning. Vyberte pokojnú hodinu, napr. 2.", "label": "Učiť sa v hodine" }, "ml_training_interval_days": { - "doc": "Znova skontrolujte vylepšenia maximálne raz za toľko dní.", + "doc": "Pretrénovať najviac raz za tento počet dní.", "label": "Kontrolovať maximálne každých" }, "ml_training_min_cycles": { - "doc": "Pred jemným doladením počkajte, kým sa zaznamená aspoň toľko cyklov, aby ste sa mali z čoho poučiť.", + "doc": "Pred tréningom počkať aspoň na tento počet zaznamenaných cyklov.", "label": "Najprv potrebné cykly" }, "name": { - "doc": "Zobrazovaný názov zobrazený v zozname integrácií HA a registri zariadení.", + "doc": "Názov tohto zariadenia zobrazený v Home Assistant.", "label": "Názov zariadenia" }, "no_update_active_timeout": { - "doc": "Ak počas behu nedostanú žiadne aktualizácie napájania tak dlho, predpokladajme, že zástrčka spadla offline a vynútene zastavte, aby ste predišli cyklu zombie. Predvolených 600 s umožňuje oneskorenie cloudu alebo siete.", + "doc": "Ak počas cyklu počas tohto času nepríde žiadna aktualizácia výkonu, predpokladá sa, že zásuvka je offline, a cyklus sa vynútene zastaví. Predvolených 600 s počíta s oneskorením cloudu alebo mesh siete.", "label": "Časový limit bez aktualizácie" }, "notify_before_end_minutes": { @@ -1346,35 +1347,35 @@ "label": "Upozornenie pred koncom" }, "notify_channel": { - "doc": "Názov kanála upozornení systému Android pre správy spustenia/živé správy (ovláda zvuk jednotlivých kanálov a prioritu v mobilnej aplikácii). Blank používa predvolenú sprievodnú aplikáciu.", + "doc": "Kanál Android pre správy o štarte a živom priebehu; kanál určuje zvuk a prioritu. Prázdne: predvolený kanál aplikácie.", "label": "Android kanál (štart/živý)" }, "notify_cycle_timers": { - "doc": "Upozornenia v konkrétnych minútach cyklu (napr. na pridanie aviváže). Správa podporuje {device}, {program}, {minutes}. Povoľte automatické pozastavenie, aby sa v tomto bode pozastavilo a dostali interaktívne upozornenie pomocou tlačidla Pokračovať; obnoviť prostredníctvom panela, služby pozastavenia/obnovenia alebo akcie upozornenia.", + "doc": "Upozornenia v nastavených minútach cyklu (napr. na pridanie aviváže). Správa podporuje {device}, {program}, {minutes}. S automatickou pauzou sa cyklus tu aj pozastaví a upozornenie dostane tlačidlo Pokračovať.", "label": "Časovače cyklu" }, "notify_finish_channel": { - "doc": "Názov kanála upozornení systému Android pre záverečnú správu. Prázdne znova použije štartovací/živý kanál.", + "doc": "Kanál Android pre správu o dokončení. Prázdne: použije sa kanál štartu a živého priebehu.", "label": "Android kanál (dokončenie)" }, "notify_finish_message": { - "doc": "Telo odoslané po skončení cyklu. Premenné šablóny: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}. {time_finished} a {vs_typical} sú tu najužitočnejšie.", + "doc": "Text odoslaný po dokončení cyklu. Premenné: {device}, {program}, {duration} (minúty), {duration_hm} (1 h 05 min), {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}, {status} (completed alebo force_stopped; prerušené cykly nič neposielajú). Pri akejkoľvek inej premennej sa použije predvolený text.", "label": "Správa pri dokončení" }, "notify_finish_services": { - "doc": "notify.* služby volané po skončení cyklu. Add one per target; ponechajte prázdne, ak nechcete ukončiť oznámenie.", + "doc": "Služby notify.* volané po dokončení cyklu. Ponechajte prázdne pre žiadne.", "label": "Služby pri dokončení" }, "notify_fire_events": { - "doc": "Taktiež spúšťajte udalosti ha_washdata_* pri spustení/dokončení cyklu, aby ste si mohli vytvoriť svoju vlastnú automatizáciu.", + "doc": "Tiež vyvolať udalosti ha_washdata_* pri štarte a dokončení cyklu pre vlastné automatizácie.", "label": "Spúšťať HA udalosti pre upozornenia" }, "notify_icon": { - "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.", + "doc": "Malá ikona v upozornení (napr. mdi:washing-machine). Posiela sa len na ciele mobile_app, nie na skupiny notify. V iOS nahradí ikonu aplikácie a vyžaduje mobilnú aplikáciu 2026.8 alebo novšiu. Prázdne: predvolená ikona platformy.", "label": "Ikona upozornenia" }, "notify_live_chronometer": { - "doc": "V upozornení (na platformách, ktoré ho podporujú) zobrazte namiesto statického odhadu odpočítavací časovač s aktuálnou verziou.", + "doc": "Zobraziť v upozornení živé odpočítavanie (kde je podporované) namiesto pevného odhadu.", "label": "Použiť živý chronometer" }, "notify_live_interval_seconds": { @@ -1382,143 +1383,135 @@ "label": "Interval živých aktualizácií" }, "notify_live_overrun_percent": { - "doc": "Ak cyklus prekročí svoj odhad o viac ako toto percento, pošlite upozornenie na prekročenie.", + "doc": "Živé aktualizácie sa zastavia, keď cyklus prekročí očakávané trvanie o toto percento (20 = až 1,2-násobok očakávaného času).", "label": "% prekročenia pred upozornením" }, "notify_live_services": { - "doc": "notify.* služby si vyžiadali aktuálne aktualizácie priebehu počas behu cyklu. Ak chcete zakázať upozornenia na aktuálny priebeh, nechajte prázdne.", + "doc": "Služby notify.* pre živé aktualizácie priebehu počas cyklu. Ponechajte prázdne na vypnutie.", "label": "Služby živého priebehu" }, "notify_only_when_home": { - "doc": "Upozornenia posielajte iba vtedy, keď je aspoň jedna z prepojených osôb (vyššie) doma.", + "doc": "Upozorňovať len vtedy, keď je aspoň jedna z osôb vyššie doma.", "label": "Upozorniť iba keď doma" }, "notify_people": { - "doc": "person.* entity, ktoré používa „Upozorniť iba keď domov“ na rozhodnutie, či je niekto doma.", + "doc": "Entity person.*, ktoré kontroluje „Upozorniť iba keď doma“.", "label": "Osoby (pre Iba keď doma)" }, "notify_pre_complete_message": { - "doc": "Telo upozornenia pred koncom / takmer hotové. Premenné šablóny: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Text každej živej aktualizácie priebehu. Premenné šablóny: {device}, {program}, {minutes} (zostávajúce minúty). Pri akejkoľvek inej premennej sa použije predvolený text.", "label": "Správa pred dokončením" }, "notify_reminder_message": { - "doc": "Telo pripomienky, že bielizeň stále čaká na vybratie. Prázdne pole použije vstavanú predvolenú hodnotu. Premenné šablóny: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Text jednorazového Upozornenia pred koncom. Premenné šablóny: {device}, {program}, {minutes} (nastavenie Upozornenie pred koncom). Pri akejkoľvek inej premennej sa použije predvolený text.", "label": "Správa pripomienky" }, "notify_start_message": { - "doc": "Telo odoslané pri spustení cyklu. Premenné šablóny: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Text odoslaný pri spustení cyklu. Premenné šablóny: {device}, {program}. Pri akejkoľvek inej premennej sa použije predvolený text.", "label": "Správa pri štarte" }, "notify_start_services": { - "doc": "notify.* služby volané pri spustení cyklu. Pridajte jeden na každý cieľ (telefón, prístrojová doska atď.); ponechajte prázdne, ak nechcete upozorniť na začiatok.", + "doc": "Služby notify.* volané pri štarte cyklu (telefón, nástenka, ...). Ponechajte prázdne pre žiadne.", "label": "Služby pri štarte" }, "notify_timeout_seconds": { - "doc": "Automaticky zavrieť upozornenie po uplynutí tohto počtu sekúnd (na platformách, ktoré ho podporujú). 0 si ho ponechá, kým sa manuálne nezatvorí.", + "doc": "Zavrieť upozornenie po tomto počte sekúnd (kde je podporované). 0 ho ponechá, kým ho nezavriete.", "label": "Automaticky zatvoriť po" }, "notify_title": { - "doc": "Názov oznámenia. Premenné šablóny: {device}, {program}, {duration}, {energy}, {cost}, {date}, {time}, {minutes}.", + "doc": "Nadpis upozornenia. Dosadzuje sa len {device}; pri akejkoľvek inej premennej sa použije predvolený nadpis.", "label": "Nadpis upozornenia" }, "notify_unload_delay_minutes": { - "doc": "Minúty po skončení cyklu pred odoslaním stále čakajúcej pripomienky „vyložiť stroj“. Ak chcete pripomienku deaktivovať, nastavte 0.", + "doc": "Minúty po skončení cyklu pred pripomienkou „vyložte stroj“. 0 ju vypne.", "label": "Oneskorenie pripomienky vyloženia" }, "notify_unload_repeat": { - "doc": "Znova odosiela pripomienku vyloženia každých „Oneskorenie pripomienky vyloženia“ minút, kým sa vyloženie nepotvrdí alebo v oznámení neťuknete na „Prestať pripomínať“. Vyžaduje spôsob potvrdenia vyloženia: Entitu senzora dverí, Entitu potvrdenia vyloženia alebo nižšie uvedenú možnosť „Ručné potvrdenie vyloženia“. Tlačidlo na zrušenie funguje pri oznámeniach mobilnej aplikácie Home Assistant (companion app).", + "doc": "Opakovať pripomienku vyloženia každých „Oneskorenie pripomienky vyloženia“ minút, kým sa vyloženie nepotvrdí alebo neťuknete na „Prestať pripomínať“ (mobilná aplikácia). Vyžaduje Entitu senzora dverí, Entitu potvrdenia vyloženia alebo „Ručné potvrdenie vyloženia“.", "label": "Opakovať do vyloženia" }, "off_delay": { - "doc": "Čas čakania po poklese napájania pred vyhlásením cyklu za ukončený. Ak sa v rámci tohto okna obnoví napájanie, cyklus plynule pokračuje – premosťuje to prestávky medzi fázami umývania. Umývačky riadu majú dlhé fázy sušenia (vypnutie na 20-60 minút), takže oneskorenie vypnutia musí prekročiť túto hodnotu, aby celé umývanie + suché zostalo ako jeden cyklus.", + "doc": "Ako dlho musí výkon zostať nízky, než je cyklus dokončený. Návrat výkonu počas tohto času cyklus predĺži. Umývačky môžu byť pri sušení 20-60 min nečinné, takže nastavte dlhší čas.", "label": "Oneskorenie vypnutia" }, "pause_cuts_power": { - "doc": "Keď je cyklus pozastavený, vypnite tiež nižšie uvedenú prepínaciu entitu. Len pre spotrebiče, ktorých zástrčku možno bezpečne odstrihnúť uprostred cyklu.", + "doc": "Pri pozastavení cyklu vypnúť aj Entitu prepínača nižšie. Len pre spotrebiče, ktoré môžu bezpečne stratiť napájanie uprostred cyklu.", "label": "Pauza tiež preruší napájanie (cez prepínač)" }, "power_off_delay": { - "doc": "Ako dlho musí príkon zostať pod hodnotou „Prah vypnutia napájania“ po dokončení cyklu, kým sa stav vráti na „Vypnuté“. Používa sa iba vtedy, keď je „Prah vypnutia napájania“ väčší než 0. Kontroluje sa v pravidelnom intervale na pozadí, takže sa skutočné oneskorenie zaokrúhľuje nahor na najbližší tik vypršania stavu.", + "doc": "Ako dlho musí výkon po cykle zostať pod Prahom vypnutia napájania, než sa stav vráti na Vypnuté. Použije sa len vtedy, keď je tento prah nad 0. Kontroluje sa pravidelne, takže skutočné čakanie môže byť o niečo dlhšie.", "label": "Oneskorenie vypnutia napájania" }, "power_off_threshold_w": { - "doc": "Voliteľná detekcia stavu „Vypnuté“ na základe príkonu. Keď je hodnota väčšia než 0, po dokončení cyklu, ak príkon zostane pod touto úrovňou počas času „Oneskorenie vypnutia napájania“, spotrebič sa považuje za vypnutý a stav sa vráti na „Vypnuté“. Ponechajte 0 na vypnutie (predvolené). Nastavte ju nad skutočnú úroveň vypnutého spotrebiča a pod „Prah zastavenia“ aj pod pohotovostný odber dokončeného, ale stále zapnutého spotrebiča; ak nie je nižšia než „Prah zastavenia“, ignoruje sa. Keď je zapnutá, nahrádza „Oneskorenie obnovenia priebehu“ pri návrate na „Vypnuté“, takže dokončený spotrebič zostane v stave „Dokončené“/„Čistý“, kým sa skutočne nevypne.", + "doc": "Po cykle výkon pod touto hodnotou počas Oneskorenia vypnutia napájania nastaví stav na Vypnuté, namiesto Oneskorenia obnovenia priebehu. Nastavte nad odber vypnutého spotrebiča, pod pohotovosť a Prah zastavenia. 0 vypne (predvolené).", "label": "Prah vypnutia napájania" }, "power_sensor": { - "doc": "Entita snímača, ktorá uvádza živý výkon vo wattoch pre tento spotrebič (napr. sensor.washer_power). Celá detekcia cyklu je založená na tomto signáli.", + "doc": "Senzor, ktorý hlási aktuálny výkon tohto spotrebiča vo wattoch (napr. sensor.washer_power). Celá detekcia vychádza z neho.", "label": "Senzor výkonu" }, - "profile_duration_tolerance": { - "doc": "Pásmo +/- okolo priemerného trvania profilu použitého počas párovania. 0,25 znamená, že 60-minútový profil zodpovedá 45-75-minútovým cyklom.", - "label": "Tolerancia trvania profilu" - }, "profile_match_interval": { - "doc": "Ako často sa pokúšať o párovanie profilu počas bežiaceho cyklu. Predvolených 300 s (5 minút) vyvažuje rýchlosť detekcie a CPU.", + "doc": "Ako často párovať bežiaci cyklus s profilmi. Predvolené 300 s (5 min).", "label": "Interval zhody" }, "profile_match_max_duration_ratio": { - "doc": "Maximálna dĺžka cyklu vzhľadom na profil. 1.3 znamená, že cyklus musí byť kratší ako 130 % trvania profilu, aby sa zhodoval.", + "doc": "Program sa vyradí, keď cyklus beží dlhšie ako tento násobok jeho typickej doby trvania. 1,8: cyklus o 80 % dlhší ako program mu už nezodpovedá.", "label": "Max. pomer trvania" }, "profile_match_min_duration_ratio": { - "doc": "Minimálna dĺžka cyklu vzhľadom na profil. 0,9 znamená, že cyklus musí trvať aspoň 90 % trvania profilu, aby sa zhodoval.", + "doc": "Program sa zvažuje až potom, čo cyklus prebehol túto časť jeho typickej doby trvania. 0,10: dvojhodinový program sa stane kandidátom po 12 minútach. Neplatí počas prvých 15 minút cyklu.", "label": "Min. pomer trvania" }, "profile_match_threshold": { - "doc": "Minimálne skóre podobnosti (0-1) požadované na konci cyklu na akceptovanie identifikácie programu. Zvýšte ho, aby ste znížili počet nesprávnych identifikácií; znížte ju, ak sa programy vášho zariadenia nezhodujú. Predvolená hodnota 0,4 je konzervatívny východiskový bod.", + "doc": "Živé skóre zhody, ktoré cyklus potrebuje, než ho Inteligentné ukončenie alebo dokončenie ochrany pred krčením smie ukončiť skôr. Nemení zobrazený program. Zvýšte, ak slabé zhody ukončujú cykly predčasne; znížte, ak jasné zhody čakajú celé oneskorenie vypnutia.", "label": "Prah zhody" }, "profile_unmatch_threshold": { - "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.", + "doc": "Ak živé skóre zhody klesne pod túto hodnotu, aktuálny odhad programu sa vymaže. Držte trochu pod Prahom zhody, aby ho krátky pokles nevynuloval.", "label": "Prah nezhody" }, "progress_reset_delay": { - "doc": "Po dokončení podržte priebeh na 100 % na túto dlhú dobu, aby sa pred resetovaním do stavu nečinnosti na paneloch zobrazilo Dokončené.", + "doc": "Po cykle držať priebeh na 100 % počas tohto času, než sa resetuje na Nečinný.", "label": "Oneskorenie obnovenia priebehu" }, "pump_stuck_duration": { - "doc": "Sekundy môže čerpadlo bežať nepretržite predtým, ako je označené ako možné zablokovanie (spustí udalosť zaseknutého čerpadla).", + "doc": "Ako dlho smie čerpadlo bežať bez prestávky, než sa spustí udalosť zaseknutého čerpadla.", "label": "Trvanie zaseknutia čerpadla" }, "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.", + "doc": "Minimálny čas medzi spracovanými hodnotami. Hodnoty, ktoré prídu skôr, sa preskočia (nízke hodnoty počas cyklu sa vždy ponechajú). Nastavte interval hlásenia vašej zásuvky alebo menej; vyššia hodnota len zahadzuje údaje.", "label": "Interval vzorkovania" }, "save_debug_traces": { - "doc": "Uložte úplnú stopu výkonu a zodpovedajúce údaje ladenia pre každý cyklus. Užitočné pri riešení problémov, ale zvyšuje veľkosť úložiska.", + "doc": "Ukladať ku každému cyklu aj ladiace údaje párovania (skóre všetkých kandidátov) na riešenie problémov. Pridáva asi 20 KB na cyklus.", "label": "Uložiť ladiace záznamy" }, "smart_termination_duration_ratio": { - "doc": "Ako ďaleko v očakávanej dobe trvania priradeného programu musí byť cyklus, než ho Inteligentné ukončenie môže po poklese príkonu ukončiť skôr. Očakávaná doba trvania je priemer programu, takže pri spotrebičoch, ktorých doba behu veľmi kolíše - práčky so studenou prívodnou vodou v zime oproti teplej v lete, sušičky so senzorovým sušením, programy závislé od náplne - skončí približne polovica všetkých behov skôr než tento priemer a nikdy sa rýchleho ukončenia nedočká, takže skončí až o niekoľko minút neskôr cez záložný časový limit. Pri takýchto strojoch túto hodnotu znížte (napr. 0,85), aby sa skoršie ukončenie napriek tomu spustilo; pre opatrnejšie správanie ju zvýšte smerom k 1,0. Ponechajte prázdne pre predvolenú hodnotu (0,98, pri umývačkách riadu 0,99). Cyklus môže vždy len ukončiť skôr, nikdy neskôr, a nikdy sa nespustí pri nejednoznačnej zhode alebo zhode s nízkou spoľahlivosťou.", + "doc": "Zlomok priemernej doby trvania programu, ktorý musí cyklus dosiahnuť, než ho Inteligentné ukončenie smie ukončiť skôr. Ak doby behu kolíšu (teplota vody, senzorové sušenie, veľkosť náplne), znížte ho (napr. 0,85). Ponechajte prázdne pre 0,98 (0,99 pri umývačkách).", "label": "Pomer inteligentného ukončenia" }, - "smoothing_window": { - "doc": "Ako veľmi je vyhladený signál surového výkonu. Nízka (2) reaguje, ale je hlučná; vysoká (5) vyhladzuje hroty, ale pridáva oneskorenie.", - "label": "Okno vyhladzovania" - }, "start_duration_threshold": { - "doc": "Napájanie musí zostať nad prahom spustenia tak dlho, aby sa potvrdil skutočný štart, čím sa zabráni spusteniu cyklu prepínačom zapnutia/vypnutia v zlomku sekundy.", + "doc": "Výkon musí zostať nad Prahom spustenia počas tohto času, než cyklus začne, aby sa krátke zapnutia a vypnutia ignorovali.", "label": "Trvanie spustenia" }, "start_energy_threshold": { - "doc": "Energiu (výkon x čas), ktorú musí spotrebič spotrebovať pred BEŽOU. Krátky vysokovýkonný hrot má veľmi nízku energiu a ignoruje sa, čím sa zabráni chybným štartom.", + "doc": "Energia, ktorú musí spotrebič spotrebovať, než sa cyklus počíta ako bežiaci. Krátka špička výkonu spotrebuje málo energie, takže cyklus nespustí.", "label": "Energia spustenia" }, "start_threshold_w": { - "doc": "Výkon sa musí zvýšiť nad túto úroveň, aby sa potvrdilo spustenie cyklu. Nastavenie príliš nízke spôsobuje nesprávne spustenie z pohotovostného režimu; chýbajú príliš vysoké a pomaly sa spúšťajúce programy (studená náplň). Nástroj návrhov to nastaví tesne nad pozorovaný najnižší aktívny výkon stroja.", + "doc": "Výkon musí stúpnuť nad túto hodnotu, aby cyklus začal. Príliš nízko: pohotovostný odber spôsobí falošné štarty. Príliš vysoko: pomaly štartujúce programy (studené napúšťanie) sa prehliadnu.", "label": "Prah spustenia" }, "stop_threshold_w": { - "doc": "Výkon musí klesnúť pod túto úroveň pred začiatkom odpočítavania oneskorenia. Nastavte ho pod hodnotu Start Threshold - medzera medzi nimi je pásmo hysterézie, ktoré zabraňuje blikaniu. Ak je nastavenie príliš vysoké, fázy s nízkym výkonom (zastavenie plákania, ochrana proti pokrčeniu) falošne spúšťajú koncovú sekvenciu.", + "doc": "Výkon musí klesnúť pod túto hodnotu, než začne odpočítavanie oneskorenia vypnutia. Držte pod Prahom spustenia. Ak je príliš vysoko, fázy s nízkym výkonom (pauzy pri plákaní, ochrana pred krčením) omylom spustia odpočítavanie konca.", "label": "Prah zastavenia" }, "switch_entity": { - "doc": "Voliteľný prepínač vypnutý pri pauze a opätovnom zapnutí pri opätovnom spustení, ktorý sa používa spolu s funkciou „Pauza tiež znižuje napájanie“.", + "doc": "Voliteľný prepínač, ktorý sa pri pauze vypne a pri pokračovaní znova zapne, spolu s „Pauza tiež preruší napájanie“.", "label": "Entita prepínača" }, "watchdog_interval": { - "doc": "Ako často strážny pes na pozadí kontroluje zablokované senzory a uplynuté časové limity. Predvolených 30 s.", + "doc": "Ako často kontrola na pozadí hľadá zaseknuté senzory a vypršané časové limity. Predvolené 30 s.", "label": "Interval watchdog" }, "notify_milestone_message": { @@ -1526,131 +1519,86 @@ "label": "Správa o míľniku" }, "notify_milestones": { - "doc": "Počty cyklov oddelené čiarkami, ktoré po dosiahnutí spustia jednorazové oslavné upozornenie (napr. 50, 100, 500, 1000). Prázdne pole vypne upozornenia na míľniky.", + "doc": "Počty cyklov, ktoré spustia jednorazové oslavné upozornenie, oddelené čiarkami (napr. 50, 100, 500, 1000). Prázdne ich vypne.", "label": "Míľniky cyklov" }, "notify_quiet_end_hour": { - "doc": "Koniec okna nerušiť (0-23). Zadržané upozornenia sa doručia v túto hodinu. Ponechajte prázdne na vypnutie.", + "doc": "Koniec okna nerušiť (0-23); zadržané upozornenia sa odošlú v tomto čase. Prázdne ho vypne.", "label": "Koniec tichých hodín" }, "notify_quiet_start_hour": { - "doc": "Začiatok okna nerušiť (0-23). Upozornenia na dokončenie, pripomienky a upozornenia na čistú bielizeň, ktoré by sa spustili počas tichých hodín, sa zadržia a doručia po skončení okna. Ponechajte prázdne na vypnutie. Podporuje okná prechádzajúce cez polnoc (napr. začiatok 22, koniec 7).", + "doc": "Začiatok okna nerušiť (0-23). Upozornenia na dokončenie, pripomienky a upozornenia na čistú bielizeň v tomto okne sa zadržia do jeho konca. Môže prechádzať cez polnoc (napr. 22 až 7). Prázdne ho vypne.", "label": "Začiatok tichých hodín" }, "peak_rate_message": { - "doc": "Voliteľný vlastný text tipu o špičkovej sadzbe pripojený k upozorneniu o spustení. Premenné šablóny: {device}, {price}. Prázdne pole použije vstavanú predvolenú hodnotu.", + "doc": "Vlastný text tipu o špičkovej sadzbe. Premenné šablóny: {device}, {price}. Prázdne: predvolený text.", "label": "Správa o špičkovej sadzbe" }, "peak_rate_threshold": { - "doc": "Keď sa cyklus spustí a aktuálna cena za kWh je na tejto hodnote alebo nad ňou, k upozorneniu o spustení sa pripojí tip o špičkovej sadzbe. 0 alebo prázdne pole tip vypne.", + "doc": "Ak je cena za kWh pri štarte cyklu na tejto hodnote alebo vyššia, pridať do upozornenia na štart tip o špičkovej sadzbe. 0 alebo prázdne ho vypne.", "label": "Prah špičkovej sadzby (za kWh)" }, - "dtw_bandwidth": { - "label": "Tolerancia zhody tvaru", - "doc": "Ako veľmi smie nástroj na priraďovanie časovo deformovať cyklus, aby zodpovedal tvaru uloženého programu. Vyššia hodnota toleruje väčšie kolísanie rýchlosti; príliš vysoká riskuje priradenie nesprávneho programu." - }, - "corr_weight": { - "label": "Váha tvaru verzus úrovne", - "doc": "Rovnováha medzi tvarom krivky a absolútnou úrovňou výkonu pri hodnotení zhody. Vyššia uprednostňuje tvar; nižšia uprednostňuje zhodu úrovne výkonu." - }, - "duration_weight": { - "label": "Váha dĺžky trvania", - "doc": "Ako silno je uprednostnený program, ktorého typická dĺžka trvania zodpovedá tomuto cyklu." - }, - "energy_weight": { - "label": "Váha spotreby energie", - "doc": "Ako silno je uprednostnený program, ktorého typická spotreba energie zodpovedá tomuto cyklu." - }, "store_brand": { "label": "Značka spotrebiča", - "doc": "Voliteľné. Značka spotrebiča vybraná z katalógu komunity. Používa sa na vyhľadávanie a zdieľanie zodpovedajúcich referenčných záznamov. Ponechajte prázdne, ak nepoužívate online funkcie." + "doc": "Voliteľné. Značka spotrebiča z katalógu komunity, používaná na hľadanie a zdieľanie referenčných záznamov. Ponechajte prázdne, ak online funkcie nepoužívate." }, "store_model": { "label": "Model spotrebiča", - "doc": "Voliteľné. Model spotrebiča vybraný z katalógu komunity po nastavení značky. Ak váš model nie je uvedený, môžete ho pridať do katalógu." + "doc": "Voliteľné. Model spotrebiča z katalógu komunity (najprv vyberte značku). Ak v zozname nie je, môžete ho pridať." }, "show_contributor": { "doc": "Zobraziť meno prispievateľa pri profiloch stiahnutých z obchodu komunity" }, - "enable_phase_matching": { - "label": "Zostávajúci čas zohľadňujúci fázy", - "doc": "Rozdelí každý bežiaci cyklus do fáz (ohrev, pranie, odstreďovanie) a rozpočíta zostávajúci čas na jednotlivé fázy, zmiešaný s klasickým odhadom - na začiatku cyklu sa opiera o rozpočet fáz a ku koncu o klasický odhad. Prispôsobí odpočet tomu, ako dlho sa váš spotrebič skutočne ohrieva a beží, čo je najviac badateľné v prvej polovici cyklu. Vypnuté = iba klasický odhad. Ovplyvnené je len zobrazenie zostávajúceho času; zhoda programov a detekcia cyklu sa nemenia." - }, - "keep_min_score": { - "label": "Min. skóre zhody", - "doc": "Minimálne skóre podobnosti, ktoré musí mať kandidát, aby zostal v závode o zhodu. Predvolená hodnota 0,1 je zámerne benevolentná - pripúšťa aj slabých kandidátov a spolieha sa na neskoršie fázy pri výbere najlepšej zhody. Zvýšte ju pre skoršie vyradenie nepravdepodobných profilov; znížte ju pre rozšírenie počiatočnej skupiny kandidátov." - }, - "dtw_blend": { - "label": "Miešanie deformácie", - "doc": "Ako veľmi skóre zarovnania DTW nahrádza základné skóre Fázy 2. 0 = použite iba skóre Fázy 2, 1 = použite iba skóre DTW, 0,5 (predvolené) = rovnaké miešanie. Zvýšte, ak chcete viac spoliehať na zarovnanie, keď majú programy podobné úrovne výkonu, ale rôzne časové vzory." - }, - "dtw_ensemble_w": { - "label": "Mix kombinácie DTW", - "doc": "V kombinačnom režime DTW (predvolenom) je to váha škálovaného L1 oproti derivátneho DTW (DDTW). 1,0 = iba škálované L1, 0 = iba DDTW, 0,7 = predvolené. Škálovaná varianta L1 porovnáva úrovne výkonu; derivátna varianta reaguje na zmeny výkonu v čase. Kombinácia spája obe." - }, - "dtw_ddtw_scale": { - "label": "Škála derivátnej deformácie", - "doc": "Vzdialenosť polosaturácie pre hodnotenie derivátnym DTW. Skóre sa zníži na polovicu, keď sa vzdialenosť DDTW rovná tejto hodnote. Menšia = citlivejšia na rozdiely v tvare. Predvolená hodnota 30 je kalibrovaná pre typické priebehy výkonu spotrebičov. Ovplyvňuje iba kombinačný a ddtw režim DTW." - }, - "dtw_refine_top_n": { - "label": "Pocet spresnenych kandidátov", - "doc": "Koľko najlepších kandidátov z Fázy 2 je prehodnotených DTW. DTW je nákladnejší, takže sa používa iba pre N najlepších kandidátov. Predvolených 5. Zvýšte (na 7-9), ak správny profil po Fáze 2 niekedy dosiahne iba 4. alebo 5. miesto - DTW ho môže zachrániť. Zníženie mierne urýchli porovnávanie." - }, - "duration_scale": { - "label": "Ostrost doby trvania", - "doc": "Ostrosť penalizácie za nezhodu dĺžky trvania vo Fáze 4. Je to logaritmický pomer, pri ktorom sa skóre zhody zníži na polovicu. Menší = prísnejší: nezhoda dĺžky trvania viac znižuje skóre. Predvolená hodnota 0,175 zodpovedá približne 18% tolerancii dĺžky trvania pri polovičnej váhe. Kombinujte s Váhou dĺžky trvania." - }, - "energy_scale": { - "label": "Ostrost energie", - "doc": "Ostrosť penalizácie za nezhodu energie vo Fáze 4. Menší = prísnejší: nezhoda energie viac znižuje skóre. Predvolená hodnota 0,25 je zámerne benevolentnejšia ako Ostrosť dĺžky trvania, pretože energia sa líši podľa záťaže. Kombinujte s Váhou energie." - }, "power_profile_interval_min": { "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." + "doc": "Veľkosť úseku atribútu power_profile (priemerné watty na slot), ktorý používajú plánovače ako EMHASS a tibber_prices. Menší zachová krátke špičky; väčší vyhladí krivku. Predvolené 15 min. 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." + "doc": "Iba iOS. Aktualizovať živý priebeh potichu, s prioritou šetriacou batériu, ktorú iOS môže zoskupovať. Prvá aktualizácia a upozornenia na štart a dokončenie stále upozornia. Vypnuté: upozorní každá aktualizácia." }, "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." + "doc": "Iba Android. Ponechať upozornenie živého priebehu aj po klepnutí. Vypnuté: klepnutie ho zavrie." }, "notify_live_click_action": { "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č." + "doc": "Čo otvorí klepnutie na upozornenie tohto spotrebiča. Prázdne: panel WashData pre tento spotrebič. Zadajte cestu k nástenke (napr. /lovelace/laundry) alebo URL, alebo „none“, aby klepnutie nič neotvorilo." }, "door_opens_at_end": { "label": "Dvere sa na konci automaticky otvoria", - "doc": "Pre umývačky riadu, ktoré na konci cyklu otvoria dvere za účelom sušenia (AirDry a podobné). Pri zapnutej funkcii otvorenie dverí počas bežiaceho cyklu už nezastaví cyklus natrvalo; ak dvere zostanú otvorené po dobu čakania nižšie, WashData cyklus ukončí. Krátke otvorenie (pridanie predmetu) sa ignoruje. Vyžaduje entitu senzora dverí." + "doc": "Pre umývačky, ktoré na konci otvoria dvere kvôli sušeniu (napr. AirDry). Dvere otvorené počas času nižšie cyklus ukončia namiesto pozastavenia; krátke otvorenie sa ignoruje. Vyžaduje Entitu senzora dverí." }, "door_end_dwell_seconds": { "label": "Čakanie na otvorenie dverí pre ukončenie", - "doc": "Ako dlho musia dvere zostať otvorené, kým WashData ukončí cyklus, keď je zapnutá funkcia \"Dvere sa na konci automaticky otvoria\". Dostatočne dlho na ignorovanie rýchleho pridania riadu (predvolene 60 s), dostatočne krátko pre rýchle ukončenie po otvorení dverí spotrebičom." + "doc": "Pri zapnutom „Dvere sa na konci automaticky otvoria“: ako dlho musia dvere zostať otvorené, než cyklus skončí. Dosť dlho, aby sa ignorovalo pridanie riadu (predvolené 60 s)." }, "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ý." + "doc": "Ktoré druhy cyklov tvoria krivku a párovanie každého programu. Odškrtnutie druhu nič nevymaže; hodí sa, ak nedôverujete importovaným údajom. Štatistiky vždy počítajú len cykly tohto stroja. Odškrtnutie všetkých sa ignoruje." }, "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." + "doc": "Účtovať každý cyklus cenou platnou v každom okamihu jeho behu, nie cenou pri dokončení. Vyžaduje entitu ceny. Spracovanie histórie (Diagnostika) prepočíta staršie cykly." }, "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.", + "doc": "Pridá na začiatok cyklu hodnoty z neúspešných pokusov o spustenie, pre stroje, ktoré sa pred štartom skúšajú rozbehnúť (zámok dverí, prvé napúšťanie). 300 s stačí väčšine; 0 = vypnuté. Zaznamenaný začiatok sa posunie skôr, takže naučené doby trvania sa nejaký čas posúvajú.", "label": "Nábeh krivky" }, "unload_confirm_entity": { "label": "Entita potvrdenia vyloženia", - "doc": "Voliteľná entita, ktorej aktivácia znamená, že náplň bola vybratá, pre spotrebiče, na ktoré sa nedá umiestniť senzor dverí. Funguje akýkoľvek typ: tlačidlo Zigbee alebo NFC, pomocník input_button, senzor pohybu pred spotrebičom, scéna. Za potvrdenie sa považuje každá zmena stavu okrem vypnutia a prechodu do stavu nedostupný. Nastavenie tejto možnosti zapne pripomienku vyloženia aj na zariadení bez Entity senzora dverí." + "doc": "Voliteľné. Akákoľvek entita, ktorá potvrdí vybratie náplne, pre stroje bez senzora dverí (tlačidlo, input_button, senzor pohybu, scéna). Počíta sa každá zmena stavu okrem vypnutia a nedostupnosti. Zapne aj pripomienku vyloženia." }, "unload_track_without_door": { "label": "Ručné potvrdenie vyloženia", - "doc": "Spustí pripomienku vyloženia na zariadení bez Entity senzora dverí a bez Entity potvrdenia vyloženia, pričom vyloženie potvrdíte z vlastnej automatizácie: stlačte entitu tlačidla „Označiť ako vyložené“ alebo zavolajte službu ha_washdata.mark_unloaded. Ignoruje sa, keď je nastavená Entita senzora dverí, ktorá tento signál už poskytuje." + "doc": "Spúšťať pripomienku vyloženia bez senzora dverí alebo entity potvrdenia. Vyloženie potvrďte z vlastnej automatizácie tlačidlom „Označiť ako vyložené“ alebo službou ha_washdata.mark_unloaded. Ignoruje sa, keď je nastavená Entita senzora dverí." }, "notify_icon_color": { "label": "Farba upozornenia", - "doc": "Farba zvýraznenia upozornení tohto spotrebiča, aby sa práčka, sušička a umývačka riadu dali na prvý pohľad rozlíšiť. iOS zafarbí ikonu a ukazovateľ priebehu Live Activity na zamknutej obrazovke a v Dynamic Island. Android dostane rovnakú farbu, ale čo s ňou urobí, závisí od telefónu: niektoré zafarbia ikonu a mnohé (najmä Samsung One UI) ju úplne ignorujú. Obe platformy rozumejú formátu Hex (napr. #03A9F4). Nechajte prázdne pre predvolenú farbu platformy." + "doc": "Farba zvýraznenia upozornení tohto spotrebiča, aby sa dala ľahko rozlíšiť práčka, sušička a umývačka. V iOS vyplní kruh za ikonou a zafarbí pruh Live Activity. Mnoho telefónov s Androidom (hlavne Samsung) ju ignoruje. Použite hex, napr. #03A9F4. Prázdne: predvolená farba platformy." + }, + "share_name": { + "doc": "Zobraziť názov vášho účtu ako autora zdieľaných cyklov. Vypnuté: zdieľa sa bez mena." } }, "setting_group": { @@ -1678,9 +1626,6 @@ "door_pause": { "label": "Dvere a pauza" }, - "dtw_refinement": { - "label": "DTW doladenie" - }, "duration_gates": { "label": "Brány trvania" }, @@ -1757,17 +1702,20 @@ "rebuilding": "Prebudovávanie…", "analyzing": "Analyzovanie…", "resetting": "Resetovanie…", - "reverting": "Vracanie späť…", "trimming": "Orezávanie…", "splitting": "Rozdeľovanie…", + "stalled": "Zaseknuté", "deleting": "Odstraňovanie…", "imported": "Importované", "preparing": "Príprava…", - "unlabelling": "Odoberanie štítka…" + "unlabelling": "Odoberanie štítka…", + "working": "Spracúva sa…", + "match_uncertain": "Neisté: {top} alebo {runner}", + "match_uncertain_single": "Neisté: možno {top}", + "match_sure": "istota ~{pct} %", + "match_uncertain_tip": "WashData sa zatiaľ nerozhodol pre program. Číslo udáva, ako často mal vedúci odhad, {top}, v minulých cykloch pravdu pri takom náskoku. Len na informáciu." }, "suggestion": { - "both_agree": "WashData odporúča", - "calibrated_label": "Kalibrované", "observed_label": "Namerané", "impact": { "auto_label_confidence": { @@ -1782,10 +1730,6 @@ "higher": "Uzatvára iba cykly so značnou spotrebou energie", "lower": "Uzatvára aj kratšie alebo menej energetické cykly" }, - "end_repeat_count": { - "higher": "Vyžaduje viac opakovaní signálu konca – spoľahlivejšie, mierne pomalšie", - "lower": "Ukončí po menej opakovaniach – rýchlejšia detekcia, väčšie riziko predčasného ukončenia" - }, "learning_confidence": { "higher": "Učí sa iba z veľmi istých zhôd – pomalšie, ale spoľahlivejšie", "lower": "Učí sa z viacerých cyklov; model môže byť menej presný" @@ -1824,40 +1768,25 @@ } }, "reason": { - "auto_label_confidence": "Istota na 15. percentile z {cycles} automatických štítkov, ktoré používateľ nikdy neopravil ({p15}).", "completion_min_seconds": "Polovica dĺžky p05 čistého cyklu ({minutes} min) naprieč {cycles} cyklami; odfiltruje falošné cykly.{excl}", - "dead_zone_batch": "Ponechané krátke (medián okna nestability pri spustení naprieč {cycles} cyklami = {s}s), aby sa skutočný koniec rozpoznal rýchlo.", - "dead_zone_from_dip": "Na základe skorého poklesu výkonu zisteného v {s}s.", - "end_energy_default": "Predvolená odporúčaná hodnota pre prah šumu na konci cyklu.", - "end_energy_false": "Energia p95 falošných koncov ({p95}Wh) naprieč {cycles} cyklami, s dolnou hranicou 0.2% mediánovej energie cyklu ({floor}Wh).", - "end_energy_no_false": "Žiadne falošné konce naprieč {cycles} cyklami; dolná hranica 0.2% mediánovej energie cyklu ({floor}Wh).", - "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.", + "match_interval_floored": "Najkratší program ({shortest}s) by hodnotu obmedzil na {cap}s, pod minimum {minimum}s, takže sa použije minimum: {persistence} po sebe idúcich zhôd trvá {budget}s, viac ako {pct}% tohto programu. Párovanie beží len pri príchode hodnoty (medián={median}s), takže to nič nestojí.", "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.", "min_off_gap": "Na základe {gaps} pozorovaných medzier medzi cyklami (p05={p05}s). Minimum zariadenia: {floor}s.", "min_off_gap_bridge": "Nastavené tak, aby preklenulo najdlhší tichý úsek vnútri cyklu: {span}s + rezerva 60s, z {spans} preklenutých medzier naprieč {cycles} čistými cyklami (zostáva pod {ceiling}s, najkratšou medzerou, ktorú necháte medzi dvoma dávkami).", "min_power": "40% najnižšieho činného výkonu p05 ({p05}W) naprieč {cycles} čistými cyklami, aby prah vypnutia zostal pod skutočným odberom.{excl}", - "ml_auto_label": "Najnižšia istota, pri ktorej model kvality stále hodnotil cyklus ako čistý (p10 z {cycles} čistých cyklov = {p10}).", - "ml_end_repeat": "{false}/{total} cyklov ({pct}%) malo pauzu, ktorú detektor konca ohodnotil nad 50%; vyžadovať {val} potvrdení konca.", - "ml_off_delay": "Pauzy potvrdené detektorom konca: p95 {p95}s + rezerva 60s, z {pauses} páuz overených modelom naprieč {cycles} cyklami (minimum {floor}s).", "no_update_timeout": "Na základe pozorovanej frekvencie aktualizácií (p95={p95}s) * 20 (min. 60s).", "off_delay_cadence": "Na základe pozorovanej frekvencie aktualizácií (p95={p95}s) * 5", "off_delay_device_floor": "Použité bezpečné minimum špecifické pre zariadenie {device} ({floor}s).", "off_delay_generic_floor": "Použité všeobecné bezpečné minimum ({floor}s).", "off_delay_pauses": "Nastavené tak, aby prečkalo skutočné pauzy: pauza vnútri cyklu p95 {p95}s + rezerva 60s, z {pauses} páuz naprieč {cycles} čistými cyklami (minimum {floor}s).", - "profile_match_threshold": "Istota p10 z {cycles} správnych automatických štítkov ({p10}); bezpečná hranica pre potvrdenie za behu.", - "sampling_interval": "Mediánový interval aktualizácie pozorovaný naprieč {cycles} čistými cyklami ({si}s).{excl}", - "smoothing_window": "Nastavené na vyhladenie ~30s odčítaní pri vzorkovaní {si}s ({samples} vzoriek).", - "start_duration": "Ponechané krátke (~jeden interval vzorkovania {si}s), aby detekcia začínala čo najskôr a pritom ignorovala jednotlivé špičky.", - "thr_batch": "Ponechané tesne nad najnižším činným výkonom p05 naprieč {cycles} cyklami ({p05}W), aby sa štart zachytil čo najskôr a prah zastavenia zostal pod najnižším prevádzkovým výkonom stroja.", - "tol_per_profile": "p75 rozptylu dĺžky podľa profilu naprieč {profiles} profilmi ({cycles} cyklov); konzistentné profily nie sú penalizované.", - "tol_pooled": "Na základe zlúčeného rozptylu dĺžky {cycles} nedávnych označených cyklov (odchýlka p95={dev}).", - "watchdog": "Ponechané čo najnižšie pri zachovaní bezpečnosti (tesne nad intervalom aktualizácie p95 vo výške {p95}s a aspoň 2x nad intervalom vzorkovania {median}s, min. 30s), aby sa zaseknutia rýchlo odhalili bez falešných zastavení." + "standby_floor": "Každý nedávny cyklus stále odoberával {idle}W v okamihu ukončenia, takže prah pod touto hodnotou nikdy nezaznamená vypnutie spotrebiča. Bol ponechaný nad ňou; {cycles} čistých cyklov v zázname nikdy nekleslo pod neho na dlhšie ako {pause}s.", + "thr_batch": "Tesne nad najnižším činným výkonom (p05) naprieč {cycles} cyklami ({p05}W), aby sa štarty zachytili skoro a prah zastavenia zostal pod najnižším prevádzkovým výkonom.", + "watchdog": "Ponechané čo najnižšie pri zachovaní bezpečnosti (tesne nad intervalom aktualizácie p95 vo výške {p95}s a aspoň 2x nad intervalom vzorkovania {median}s, min. 30s), aby sa zaseknutia rýchlo odhalili bez falešných zastavení.", + "end_energy_floor": "Zvýšené na minimum, ktoré vyplýva z prahu zastavenia ({stop} W) počas oneskorenia vypnutia ({off_delay} s); pod ním môže cyklus skončiť len záložnou cestou.", + "max_duration_ratio_default": "Vrátené na predvolenú hodnotu: nižšia horná hranica odmietne správny program vždy, keď cyklus trvá dlhšie než zvyčajne.", + "min_duration_ratio_default": "Vrátené na predvolenú hodnotu: vyššia dolná hranica bráni rozpoznaniu bežiaceho cyklu, kým nie je v plnom prúde." }, "exclusions": { "summary": "Vylúčených {total} chybne zistených cyklov: {parts}.", @@ -1875,7 +1804,8 @@ "no_power": "bez výkonu", "no_active_power": "bez aktívneho výkonu" } - } + }, + "now_value": "(aktuálne {v})" }, "tab": { "advanced": "Rozšírené", @@ -1906,13 +1836,17 @@ }, "toast": { "catalog_refreshed": "Katalóg komunity obnovený", + "task_name_required": "Zadajte názov úlohy", + "task_added": "Úloha pridaná", + "task_save_failed": "Úlohu sa nepodarilo uložiť: {error}", + "task_removed": "Úloha odstránená", + "maint_logged": "Zapísané ako hotové: {task}", "access_saved": "Kontrola prístupu uložená", "all_wiped": "Všetky dáta vymazané", "analysis_complete_none": "Analýza dokončená: žiadne nové návrhy", "auto_label_complete": "Automatické označovanie dokončené", "automation_deleted": "Automatizácia odstránená", "correction_submitted": "Oprava odoslaná", - "cycle_deleted": "Cyklus odstránený", "cycle_force_stopped": "Cyklus nútene zastavený", "cycle_labelled": "Cyklus označený", "cycle_paused": "Cyklus pozastavený", @@ -1928,11 +1862,9 @@ "group_deleted": "Skupina odstránená", "group_name_required": "Názov skupiny je povinný", "group_saved": "Skupina uložená", - "import_successful": "Import úspešný; integrácia sa znovu načítava", "json_required": "Dáta JSON sú povinné", "legacy_removed": "Staré akcie odstránené", "logs_exported": "Záznamy exportované", - "matching_reverted": "Váhy porovnávania obnovené na predvolené hodnoty", "min_2_profiles": "Vyberte aspoň 2 profily pre skupinu", "ml_training_no_improvement": "Tréning dokončený: základný model zachovaný (žiadne zlepšenie)", "models_reverted": "Modely zariadenia obnovené na základné", @@ -1942,8 +1874,8 @@ "pg_preset_loaded": "Prednastavenie \"{name}\" načítané", "pg_preset_saved": "Prednastavenie \"{name}\" uložené", "pg_sugg_loaded": "Načítaných {n} navrhovaných hodnôt - spustite testovacie prostredie na porovnanie", - "pg_sugg_ml_loaded": "Načítaných {n} hodnôt kalibrovaných ML - spustite testovacie prostredie na porovnanie", "phase_name_required": "Názov fázy je povinný", + "phase_reset": "Fáza \"{name}\" obnovená na vstavanú verziu", "phase_updated": "Fáza aktualizovaná", "phases_saved": "Fázy uložené", "preferences_saved": "Predvoľby uložené", @@ -1990,15 +1922,11 @@ "store_name_required": "Zadajte názov profilu", "store_import_failed": "Import zlyhal: {error}", "store_imported": "Importované do {profile}", - "store_program_required": "Zadajte názov programu", "store_no_appliance": "Najprv nastavte značku a model spotrebiča v Nastaveniach.", "store_share_failed": "Zdieľanie zlyhalo: {error}", "store_shared": "Zdieľané do komunitného obchodu, čaká na kontrolu.", "contrib_device_changed": "Pridané do komunitného obchodu, ale vybraný spotrebič sa zmenil, kým bol formulár otvorený. Vyberte ho v Nastaveniach.", "appliance_added": "Spotrebič pridaný, čaká na schválenie", - "thanks_confirming": "Ďakujeme za potvrdenie", - "device_approved": "Schválené komunitou", - "rating_saved": "Hodnotenie kvality uložené", "brand_added": "Značka pridaná, čaká na schválenie", "profile_added": "Profil pridaný, čaká na schválenie", "saved_except_conflicts": "Uložené. Opravte zvýraznené konflikty, aby sa uložil zvyšok.", @@ -2021,7 +1949,52 @@ "odometer_saved": "Celkový počet cyklov uložený", "odometer_save_failed": "Celkový počet sa nepodarilo uložiť: {error}", "store_pick_profile_share": "Vyberte profil na zdieľanie", - "remove_failed": "Odstránenie zlyhalo: {error}" + "remove_failed": "Odstránenie zlyhalo: {error}", + "analysis_complete": "Analýza dokončená: návrhy: {count}", + "analysis_failed": "Analýza zlyhala: {error}", + "auto_detect_failed": "Automatické zisťovanie zlyhalo: {error}", + "auto_label_failed": "Automatické označenie zlyhalo: {error}", + "could_not_load_cycle": "Cyklus sa nepodarilo načítať: {error}", + "create_failed": "Vytvorenie zlyhalo: {error}", + "cycles_deleted": "Odstránené cykly: {count}", + "debug_cleared": "Vymazané ladiace záznamy: {count}", + "discard_failed": "Zahodenie zlyhalo: {error}", + "error": "Chyba: {error}", + "export_failed": "Export zlyhal: {error}", + "import_failed": "Import zlyhal: {error}", + "invalid_json": "\"{key}\" nie je platný JSON - opravte ho alebo pred uložením pole vymažte.", + "label_failed": "Označenie zlyhalo: {error}", + "ml_training_promoted": "Trénovanie dokončené: nasadené modely: {count}", + "pause_failed": "Pozastavenie zlyhalo: {error}", + "phase_created": "Fáza \"{name}\" vytvorená", + "phase_deleted": "Fáza \"{name}\" odstránená", + "profile_created": "Profil \"{name}\" vytvorený", + "rename_failed": "Premenovanie zlyhalo: {error}", + "save_failed": "Uloženie zlyhalo: {error}", + "split_complete": "Rozdelené na cykly: {count}", + "start_failed": "Spustenie zlyhalo: {error}", + "stop_failed": "Zastavenie zlyhalo: {error}", + "update_failed": "Aktualizácia zlyhala: {error}", + "store_download_skipped": "Preskočených nahrávok: {n} (príliš krátke, s príliš veľkými medzerami alebo už sú na vašom zariadení).", + "store_import_low_quality": "Táto nahrávka je príliš krátka, má príliš veľa medzier alebo je nevierohodná, preto ju nemožno importovať.", + "store_import_duplicate": "Táto nahrávka sa už nachádza na vašom zariadení.", + "device_approved": "Schválené komunitou", + "thanks_confirming": "Ďakujeme za potvrdenie", + "rating_saved": "Hodnotenie kvality uložené", + "store_download_partial": "Programy, ktoré sa nepodarilo stiahnuť: {n}; skúste to neskôr.", + "import_no_restore_point": "Importované, ale bod obnovenia sa nepodarilo uložiť, takže tento import nemožno vrátiť späť", + "import_undo_failed": "Import sa nepodarilo vrátiť späť: {error}", + "import_undone": "Import vrátený späť: obnovených {profiles} profilov a {cycles} cyklov", + "auto_label_cancelled": "Automatické označovanie zrušené po označení {count} cyklov.", + "auto_label_cancelled_one": "Automatické označovanie zrušené po označení {count} cyklu.", + "auto_label_cancelled_few": "Automatické označovanie zrušené po označení {count} cyklov.", + "auto_label_cancelled_many": "Automatické označovanie zrušené po označení {count} cyklu.", + "auto_label_cancelled_other": "Automatické označovanie zrušené po označení {count} cyklov.", + "auto_label_done": "Automatické označovanie dokončené: označených {count} cyklov, bez označenia zostalo: {skipped}.", + "auto_label_done_one": "Automatické označovanie dokončené: označený {count} cyklus, bez označenia zostalo: {skipped}.", + "auto_label_done_few": "Automatické označovanie dokončené: označené {count} cykly, bez označenia zostalo: {skipped}.", + "auto_label_done_many": "Automatické označovanie dokončené: označené {count} cyklu, bez označenia zostalo: {skipped}.", + "auto_label_done_other": "Automatické označovanie dokončené: označených {count} cyklov, bez označenia zostalo: {skipped}." }, "trend": { "down": "Klesajúci trend", @@ -2029,6 +2002,10 @@ }, "maint": { "bearing_service": "Servis ložísk", + "salt": "Doplniť soľ", + "rinse_aid": "Doplniť leštidlo", + "lint_filter": "Vyčistiť filter na vlákna", + "condenser_clean": "Vyčistiť kondenzátor", "descale": "Odstránenie vodného kameňa", "drum_clean": "Vyčistiť bubon", "filter_clean": "Vyčistiť filter", @@ -2036,34 +2013,22 @@ }, "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", + "smart_termination_duration_ratio": "Zlomok priemernej doby trvania programu, ktorý musí cyklus dosiahnuť, než ho Inteligentné ukončenie smie ukončiť skôr; znížte, ak doby behu kolíšu", + "anti_crease_finalize_ratio": "Zlomok priemernej doby trvania programu, ktorý musí cyklus dosiahnuť, než ho chvost ochrany pred krčením smie dokončiť; pri sušičkách znížte, pri práčkach ponechajte predvolené", "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_exit_power": "Úroveň pokoja medzi impulzmi otáčania; prejaví sa len nad Prahom zastavenia", "anti_wrinkle_idle_timeout": "Povolený pokoj medzi dvoma impulzmi bubna, než sa ochrana pred krčením skončí", "anti_wrinkle_max_duration": "Dlhší impulz ukončí ochranu pred krčením a otvorí nový cyklus", "anti_wrinkle_max_power": "Impulz nad touto hodnotou ukončí ochranu pred krčením a otvorí nový cyklus", "completion_min_seconds": "Najkratší beh, ktorý sa počíta ako skutočný cyklus", - "end_repeat_count": "Nízke čítania za sebou pred ukončením", "interrupted_min_seconds": "Krátke cykly označené ako prerušené", "min_off_gap": "Medzera potrebná na oddelenie dvoch cyklov", "off_delay": "Sekundy nízkeho výkonu pred koncom cyklu", "start_duration_threshold": "Sekundy nad prahom na potvrdenie spustenia", "start_threshold_w": "Minimálny výkon vo wattoch na započítanie spustenia", "stop_threshold_w": "Pod touto hodnotou sa stroj počíta ako vypnutý", - "dtw_bandwidth": "Fáza 3: deformačné pásmo Sakoe-Chiba (0 = DTW vypnuté; predvolené 0,2 = 20 % dĺžky cyklu)", - "corr_weight": "Fáza 2: rovnováha medzi tvarom krivky (korelácia) a úrovňou výkonu (MAE); predvolené 0,45", - "duration_weight": "Fáza 4: ako silno zhoda dĺžky trvania ovplyvňuje výsledné skóre (predvolené 0,22)", - "energy_weight": "Fáza 4: ako silno zhoda energie ovplyvňuje výsledné skóre (predvolené 0,22)", - "profile_match_min_duration_ratio": "Fáza 1: minimálna dĺžka cyklu ako zlomok dĺžky trvania profilu; predvolené 0,1 (10 %)", - "profile_match_max_duration_ratio": "Fáza 1: maximálna dĺžka cyklu ako zlomok dĺžky trvania profilu; predvolené 1,8 (180 %)", - "keep_min_score": "Fáza 2: minimálne skóre na zostanie v závode; predvolené 0,1 pripúšťa slabé zhody, neskoršie fázy vyberú najlepšie", - "dtw_blend": "Fáza 3: 0 = iba základné skóre, 1 = iba skóre DTW, 0,5 = rovnaké miešanie (predvolené)", - "dtw_ensemble_w": "Fáza 3 (režim kombinácie): váha škálovaného L1 oproti derivátneho DTW; predvolené 0,7 uprednostňuje úroveň", - "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)", + "profile_match_min_duration_ratio": "Minimálna dĺžka cyklu ako zlomok doby trvania profilu; predvolené 0,1 (10 %)", + "profile_match_max_duration_ratio": "Maximálna dĺžka cyklu ako zlomok doby trvania profilu; predvolené 1,8 (180 %)", "curve_preroll_seconds": "Sekundy meraní z neúspešných pokusov o spustenie prenesené na začiatok krivky cyklu; 0 = vypnuté" }, "col": { @@ -2071,12 +2036,14 @@ "status_tip": "Výsledok cyklu: Dokončené (prirodzený koniec), Prerušené (náhly pokles výkonu), Vynútené zastavenie (ručne) alebo Vyžaduje kontrolu (čaká sa spätná väzba).", "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 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.", + "energy_tip": "Celková spotrebovaná energia (kWh), vypočítaná z výkonu v čase.", + "cost_tip": "Náklady na energiu za tento cyklus, pevne dané pri dokončení. So zapnutými Časovo váženými nákladmi sa každý okamih účtuje vlastnou cenou; inak cenou na konci. Na vyplnenie nastavte cenu v Nastaveniach.", "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", - "cost_dynamic_tip": "Časovo vážené: účtované cenou platnou počas cyklu (efektívne {price} {cur}/kWh)." + "cost_dynamic_tip": "Časovo vážené: účtované cenou platnou počas cyklu (efektívne {price} {cur}/kWh).", + "dbg_conf": "Spoľ.", + "dbg_mae": "MAE", + "dbg_corr": "Kor." }, "stat": { "avg": "priem.", @@ -2100,7 +2067,10 @@ "notify_finish": "Odoslalo by sa oznámenie o dokončení.", "notify_milestone": "Odoslalo by sa oznámenie o míľniku.", "notify_held": "Oznámenie bolo pozdržané pre tiché hodiny.", - "finished": "Cyklus dosiahol koncový stav a skončil." + "finished": "Cyklus dosiahol koncový stav a skončil.", + "match_reverted": "Program sa vrátil do rozpoznávania: jeho skóre kleslo výrazne pod jeho vlastnú špičku.", + "verified_pause": "Živé zarovnanie potvrdilo (alebo uvoľnilo) pozastavenie v rámci programu. Kým pozastavenie trvá, cyklus sa neukončí.", + "stalled": "Cyklus zotrval na pohotovostnom odbere dlhšie, než pri tomto programe kedy trvá pauza, a práca ešte zostáva. Zobrazené ako Pozastavené (Zaseknuté); cyklus zostáva otvorený." }, "store": { "search_ph2": "Hľadať podľa značky alebo modelu…", @@ -2109,11 +2079,7 @@ "programs_count": "Programy: {n}", "browse": "Prehliadať", "device": "Zariadenie", - "favorites": "Obľúbené", - "search_ph": "Hľadať podľa značky alebo modelu…", "no_results": "Nenašli sa žiadne zodpovedajúce spotrebiče. Skúste iné vyhľadávanie.", - "n_programs": "{n} programov", - "n_cycles": "{n} cyklov", "no_programs": "Pre tento spotrebič zatiaľ nie sú zdieľané žiadne programy.", "no_cycles": "Pre tento program zatiaľ nie sú zdieľané žiadne referenčné cykly.", "anon": "anonymný", @@ -2125,19 +2091,21 @@ "merge_target": "Zlúčiť do profilu", "description": "Popis (voliteľné)", "contributed_by": "od {name}", + "website": "Web obchodu ↗", + "website_tip": "Otvoriť webovú stránku komunitného obchodu", + "favorites": "Obľúbené", "rating_summary": "★ {avg} ({n})", "no_ratings": "Zatiaľ žiadne hodnotenia", - "website": "Web obchodu ↗", - "website_tip": "Otvoriť webovú stránku komunitného obchodu" + "unreachable": "Nepodarilo sa pripojiť ku komunitnému obchodu. Skontrolujte pripojenie a skúste to znova." }, "settings": { "gear": { - "title": "Nastavenia" + "title": "Predvoľby" } }, "link": { - "manual": "Príručka ↗", - "add_appliance": "Pridajte svoj spotrebič" + "maintenance_supporter": "Maintenance Supporter na GitHube ↗", + "manual": "Príručka ↗" }, "tip": { "add_brand": "Značka nie je uvedená? Pridajte ju do katalógu komunity", @@ -2171,7 +2139,13 @@ "rebuild": { "envelopes": "Prepočítavanie obálok" }, - "cancelling": "Ruším..." + "cancelling": "Ruším...", + "auto_label": { + "running": "Automatické označovanie cyklov" + }, + "store_download": { + "running": "Sťahovanie z obchodu" + } }, "setup": { "hdr": { @@ -2180,26 +2154,24 @@ }, "phase0": { "washer": "WashData už deteguje vaše cykly. Nahrajte prvý cyklus, aby ste povolili názvy programov a odhady doby trvania.", - "dishwasher": "WashData sleduje. Umývačky riadu majú zložité cykly – nahranie prvého cyklu je dôrazne odporúčané. Ak detegovaný cyklus trvá príliš dlho, orežte ho v editore cyklov pred uložením ako profil.", + "dishwasher": "WashData sleduje. Cykly umývačky sú zložité, preto prvý cyklus nahrajte. Ak rozpoznaný cyklus trvá príliš dlho, orežte ho v editore cyklu pred uložením ako profil.", "generic": "WashData sleduje. Nahrajte alebo označte detegovaný cyklus a začnite vytvárať profily." }, "phase1a": { - "labelled": "Dobrý začiatok – váš prvý program je uložený. Pre najčistejšie dáta zvážte nahranie ďalšieho cyklu pomocou widgetu nahrávania." + "labelled": "Dobrý začiatok: váš prvý program je uložený. Pre najčistejšie údaje nahrajte ďalší cyklus nahrávačom." }, "phase1b": { - "recorded": "Vaša nahrávka bola uložená ako {profile_name}. Teraz nahrajte alebo označte ďalšie bežné programy, aby ste rozšírili pokrytie." + "recorded": "Vaša nahrávka bola uložená ako {profile_name}. Teraz nahrajte alebo označte ďalšie bežné programy." }, "phase1c": { - "verify": "Máte {count} programov od komunity. Spustite cyklus, aby ste overili, že ich WashData správne rozoznáva – porovnávanie sa bude zlepšovať, ako zariadenie buduje vlastnú históriu." + "verify": "Máte programy od komunity ({count}). Spustite cyklus a overte, že ho WashData rozpozná; párovanie sa zlepšuje, ako si zariadenie buduje vlastnú históriu." }, "phase2": { - "cluster": "WashData zaznamenal {count} cyklov, ktoré nezodpovedajú žiadnemu uloženému programu – navzájom si podobajú. Chcete pre ne vytvoriť nový profil?", + "cluster": "WashData zaznamenal podobné cykly ({count}), ktoré nezodpovedajú žiadnemu uloženému programu. Vytvoriť pre ne nový profil?", "unmatched": "Váš posledný cyklus nezodpovedal žiadnemu uloženému programu. Je to nový program?" }, "phase3": { - "suggestions": "WashData má odporúčania nastavení na základe vašej histórie cyklov – skontrolujte ich pre zlepšenie presnosti detekcie.", - "groups": "Niektoré vaše profily vyzerajú ako rovnaký program pri rôznych teplotách. Usporiadajte ich do skupiny pre lepšie porovnávanie.", - "phases": "Pridajte fázy programu k {profile_name} pre presnejšie odhady zostávajúceho času." + "suggestions": "WashData má návrhy nastavení z vašej histórie cyklov. Prezrite si ich pre lepšiu detekciu." }, "phase4": { "healthy": "Toto zariadenie je plne nastavené ({profile_count} profilov)." @@ -2212,12 +2184,58 @@ "create_from_cluster": "Vytvoriť profil z týchto cyklov", "create_profile": "Vytvoriť preňho profil", "review_suggestions": "Skontrolovať návrhy", - "organise_profiles": "Usporiadať profily", - "configure_phases": "Nakonfigurovať fázy", "skip_step": "Preskočiť tento krok", "skip_forever": "Nezobrazovať znova", "hide_guidance": "Skryť pokyny", "show_guidance": "Zobraziť pokyny" } + }, + "access": { + "edit": "Úpravy", + "full": "Plný", + "inherit": "Zdediť", + "none": "Žiadny (skryté)", + "read": "Čítanie" + }, + "pg_label_reason": { + "no_winner": "Žiadny program nezodpovedá", + "below_floor": "Pod prahom \"Spoľahlivosť učenia\"", + "ambiguous": "Označené ako nejednoznačné", + "margin": "Príliš blízko k druhému najlepšiemu programu", + "unknown_profile": "Program už neexistuje", + "not_evaluated": "Nevyhodnotené" + }, + "diagram": { + "accum": "akum.", + "auto": "auto", + "below_off": "pod = vyp.", + "confirmed": "potvrdené", + "energy_counts": "energia sa počíta", + "feedback": "spätná väzba", + "gap_below_min": "medzera pod min. = jeden cyklus", + "heat_phase": "fáza ohrevu", + "held_at_100": "zostáva na 100 %", + "ignored": "ignorované", + "kept": "ponechané", + "max": "max", + "min": "min", + "minus_tol": "-tol", + "no_match": "bez zhody", + "no_updates": "bez aktualizácií", + "off_delay_wait": "oneskorenie vypnutia", + "off_gap": "medzera vyp.", + "plus_tol": "+tol", + "profile": "profil", + "reading_interval": "typický interval meraní", + "sensor_offline": "senzor offline: nútené zastavenie", + "si": "IV", + "spike_ignored": "špička ignorovaná", + "spike_ignored_short": "špička: ignorovaná", + "start": "spustenie", + "stop": "zastavenie", + "tail_energy_resets": "energia konca nad prahom: časovač sa vynuluje", + "thr": "prah", + "too_short": "príliš krátky", + "tumble_kept": "impulzy bubna ponechané" } } diff --git a/custom_components/ha_washdata/translations/panel/sl.json b/custom_components/ha_washdata/translations/panel/sl.json index 88623539..958520c5 100644 --- a/custom_components/ha_washdata/translations/panel/sl.json +++ b/custom_components/ha_washdata/translations/panel/sl.json @@ -3,6 +3,7 @@ "min": "min", "entity_not_found": "Entiteta ni najdena", "running_long": "Traja predolgo", + "stalled": "V zastoju", "remaining": "preostalo", "progress": "napredek", "show_time_remaining": "Pokaži preostali čas", @@ -48,16 +49,18 @@ } }, "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_tip": "Proti koncu ta program utihne za približno {mins} min, nato {secs} s porablja okoli {watts} W. Zaznano pri {seen} od {measured} izmerjenih ciklov.", "quiet_tail": "tih konec ~{mins} min", - "artifact_tip": "{n} zaznanih nepravilnosti (npr. odprta vrata sredi cikla) – odprite, da jih vidite na grafu", + "artifact_tip": "Zaznanih je {n} nepravilnosti (npr. odprta vrata sredi cikla) – odprite, da jih vidite na grafu", + "artifact_tip_one": "Zaznana je {n} nepravilnost (npr. odprta vrata sredi cikla) – odprite, da jo vidite na grafu", + "artifact_tip_two": "Zaznani sta {n} nepravilnosti (npr. odprta vrata sredi cikla) – odprite, da ju vidite na grafu", + "artifact_tip_few": "Zaznane so {n} nepravilnosti (npr. odprta vrata sredi cikla) – odprite, da jih vidite na grafu", + "artifact_tip_other": "Zaznanih je {n} nepravilnosti (npr. odprta vrata sredi cikla) – odprite, da jih vidite na grafu", "auto": "(samodejno zaznano)", "built_in_tag": "vgrajeno", "declining": "↘ upada", "energy_low": "Nižja poraba energije kot običajno", "energy_spike": "Višja poraba energije kot običajno", - "fair_fit": "sprejemljiva kakovost", - "fair_fit_tip": "Zmerna doslednost ujemanja – nekateri cikli, dodeljeni temu profilu, imajo nižje ocene zaupanja. Označite več ciklov ali znova posnemite profil, da izboljšate natančnost.", "feedback_requested": "Zahtevane povratne informacije", "golden_cycle": "Posneti referenčni cikel", "duration_outlier": "Cikli nenavadne dolžine: {n}", @@ -66,27 +69,40 @@ "needs_review": "Potrebuje pregled", "overrun": "Trajalo dlje kot običajno", "poor_fit": "slaba kakovost", - "poor_fit_tip": "Nedosledna zgodovina ujemanj – razmislite o ponovni izdelavi tega profila", - "restart_gap_tip": "{n} vrzel ponovnega zagona HA: sled moči ima luknjo", + "restart_gap_tip": "{n} vrzeli ponovnega zagona HA: sled moči ima luknje", + "restart_gap_tip_one": "{n} vrzel ponovnega zagona HA: sled moči ima luknjo", + "restart_gap_tip_two": "{n} vrzeli ponovnega zagona HA: sled moči ima luknji", + "restart_gap_tip_few": "{n} vrzeli ponovnega zagona HA: sled moči ima luknje", + "restart_gap_tip_other": "{n} vrzeli ponovnega zagona HA: sled moči ima luknje", "reviewed": "Pregledano", "steady": "→ stabilno", "underrun": "Končal se je hitreje kot običajno", - "using_defaults": "Uporablja privzete nastavitve", - "using_tuned": "Uporablja nastavljene uteži", "overrun_ratio": "({x}x pričakovanega)", "underrun_ratio": "({pct}% pričakovanega)", - "awaiting_tip": "Čaka na odobritev skupnosti", - "awaiting_n": "Čaka na odobritev · potrjeno {n}", - "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.", - "armed": "(velja za naslednji cikel)" + "armed": "(velja za naslednji cikel)", + "awaiting_tip": "Čaka na odobritev skupnosti", + "awaiting_n": "Čaka na odobritev · potrjeno {n}", + "awaiting": "Čaka na odobritev", + "approved": "Odobreno", + "matcher_unused": "Še brez ujemanja v vaših ciklih", + "matcher_used": "Ujemanje najdeno v {n} vaših ciklih", + "matcher_used_one": "Ujemanje najdeno v {n} vašem ciklu", + "matcher_used_two": "Ujemanje najdeno v {n} vaših ciklih", + "matcher_used_few": "Ujemanje najdeno v {n} vaših ciklih", + "matcher_used_other": "Ujemanje najdeno v {n} vaših ciklih", + "matcher_used_tip": "Cikli na tej napravi, ki jih je WashData sam ujel s tem uvoženim programom. Uvoz, ki se nikoli ne ujema, morda ne ustreza vaši napravi; lahko ga odstranite. Šteto lokalno; v trgovino se nič ne pošlje." }, "btn": { + "pg_preset_delete": "Izbriši prednastavitev", + "pg_preset_load": "Naloži prednastavitev", + "pg_preset_load_tip": "Zamenjaj vrednosti v peskovniku s to prednastavitvijo (aktivne nastavitve ostanejo nespremenjene)", + "pg_preset_save": "Shrani kot prednastavitev", + "pg_preset_save_tip": "Shrani vse spodnje vrednosti kot poimenovano prednastavitev za to napravo", "reset_zoom": "Ponastavi povečavo", "set_brand_model": "Nastavi znamko in model", "refresh_catalog": "Osveži katalog", @@ -97,12 +113,11 @@ "add_timer": "+ Dodaj časovnik", "apply_all": "Uporabi vse", "apply_label": "Uporabi oznako", - "apply_set_b": "Uporabi nabor B", "apply_split": "Uporabi razdelitev", "apply_trim": "Uporabi obrezovanje", "auto_detect_split": "Samodejno zaznaj", "auto_label_cycles": "Samodejno označi cikle", - "auto_label_cycles_tip": "Samodejno dodeli imena profilov neoznačenim ciklom, katerih zaupanje ujemanja preseže prag", + "auto_label_cycles_tip": "Poimenujte neoznačene cikle, katerih zaupanje ujemanja preseže prag.", "cancel": "Prekliči", "clear_debug": "Počisti podatke za odpravljanje napak", "clear_splits": "Počisti", @@ -112,8 +127,6 @@ "convert_to_automation": "Pretvori v avtomatizacijo", "correct": "Popravi…", "create": "Ustvari", - "create_from_cluster": "Ustvari profil iz {count} ciklov", - "create_profile": "+ Ustvari profil", "delete": "Izbriši", "delete_group": "Izbriši skupino", "delete_group_tip": "Izbriši samo to skupino – profili članov se ohranijo", @@ -122,39 +135,30 @@ "discard_tip": "Zavrzi posneto sled brez shranjevanja", "dismiss": "Zapri", "mute_suggestion": "Prenehaj predlagati to nastavitev", - "pg_load_calibrated": "Naloži umerjene (ML) ({n})", - "pg_load_calibrated_tip": "Naloži ML-umerjene nastavitve kot nadpisane vrednosti v peskovniku", "pg_load_live": "Naloži aktivne nastavitve", "pg_load_live_tip": "Znova preberi trenutne nastavitve integracije in zavrzi vsa urejanja v peskovniku", "pg_load_suggested": "Naloži predlagane ({n})", "pg_load_suggested_tip": "Naloži predloge samodejnega uglaševanja kot nadpisane vrednosti v peskovniku", - "pg_preset_delete": "Izbriši prednastavitev", - "pg_preset_load": "Naloži prednastavitev", - "pg_preset_load_tip": "Zamenjaj vrednosti v peskovniku s to prednastavitvijo (aktivne nastavitve ostanejo nespremenjene)", - "pg_preset_save": "Shrani kot prednastavitev", - "pg_preset_save_tip": "Shrani vse spodnje vrednosti kot poimenovano prednastavitev za to napravo", "pg_publish_all": "Shrani v integracijo ({n})", "pg_publish_one": "Shrani {label} v integracijo", "reset_muted": "Ponastavi utišane", + "reset_phase_tip": "Razveljavi svoje urejanje in obnovi vgrajeno fazo", "done": "Končano", "edit": "Uredi", "export": "Izvozi", - "export_json": "Izvozi v JSON", "force_stop": "Prisilna ustavitev", "force_stop_tip": "Takoj zaključi trenutni cikel in ga označi kot prisilno ustavljen", "from_template": "Iz predloge ▾", "ignore": "Prezri", "import_json": "Uvozi iz JSON", - "import_overwrite": "Uvozi (prepiše podatke)", "inspect": "Preglej", "label": "Označi", - "load": "Naloži", "load_more": "Naloži več", "manage": "Upravljaj", "merge": "Združi", "new_automation": "+ Nova avtomatizacija", "new_group": "+ Nova skupina", - "new_group_tip": "Združite skoraj enake profile (enaka oblika/trajanje, različna temperatura ali ožemanje), da ujemanje zanesljivo izbira med njimi", + "new_group_tip": "Združite skoraj enake profile (enaka oblika, druga temperatura ali ožemanje), da jih ujemanje loči.", "new_phase": "+ Nova faza", "new_profile": "+ Nov profil", "new_profile_tip": "Ustvari nov profil programa iz obstoječega označenega cikla ali posnetka", @@ -162,23 +166,19 @@ "on_cycle_started": "Ob začetku cikla", "pause_cycle": "Premor", "pause_cycle_tip": "Zaustavi delujoči cikel – naprava se bo nadaljevala tam, kjer je ostala", - "pg_apply_device": "Uporabi na napravi", - "pg_autofill": "Samodejno izpolni", - "play": "Predvajaj", "process": "Obdelaj", "process_history": "Obdelaj zdaj", "process_recording": "Shrani posnetek", "process_tip": "Shrani posneto sled kot nov ali obstoječ profil", "rebuild": "Obnovi ovojnice", "rebuild_envelope": "Obnovi ovojnico", - "rebuild_tip": "Znova izračunaj pričakovano ovojnico moči (min/maks pas) za vse profile iz njihovih označenih ciklov – zaženite po označevanju novih ciklov ali popravljanju starih", + "rebuild_tip": "Znova zgradite pričakovani pas moči vsakega profila iz njegovih označenih ciklov. Zaženite po označevanju ali popravljanju ciklov.", "rec_start_tip": "Začni snemanje sledi moči naprave – zaženite tik preden zaženete cikel", "rec_stop": "Ustavi", "rec_stop_tip": "Ustavi snemanje in obdrži posneto sled za pregled", "record": "Začni snemanje", "refresh": "Osveži", "refresh_diag_tip": "Znova naloži statistike shrambe", - "refresh_logs_tip": "Znova naloži najnovejše zapise dnevnika", "refresh_settings_tip": "Znova naloži nastavitve s strežnika", "relabel": "Spremeni oznako ({count})", "remove": "Odstrani", @@ -186,7 +186,6 @@ "reset": "Ponastavi", "reset_ml_models_tip": "Zavrzite natančno nastavljene modele in se vrnite k vgrajenim. WashData se jih lahko pozneje znova nauči.", "reset_to_builtin": "Ponastavi na vgrajene modele", - "reset_to_defaults": "Ponastavi na privzete vrednosti", "resume_cycle": "Nadaljuj cikel", "resume_cycle_tip": "Nadaljuj zaustavljeni cikel", "revert_settings": "Zavrži spremembe", @@ -199,8 +198,6 @@ "run_analysis": "🔍 Zaženi analizo predlogov", "run_analysis_tip": "Zdaj analizirajte svoje posnete cikle in osvežite predloge za nastavitev", "run_auto_label": "Zaženi samodejno označevanje", - "run_both": "Zaženi A proti B", - "run_simulation": "Zaženi simulacijo", "save": "Shrani", "save_access_control": "Shrani nadzor dostopa", "save_group": "Shrani skupino", @@ -212,10 +209,8 @@ "save_settings": "Shrani nastavitve", "select": "Izberi", "select_all": "Izberi vse", - "select_last20": "Zadnjih 20", "select_none": "Počisti izbor", "show_only": "Prikaži samo", - "skip_setup": "Preskoči nastavitev", "split": "Razdeli", "stop": "Ustavi", "submit_correction": "Pošlji popravek", @@ -224,30 +219,24 @@ "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č.", + "unlabel_selected_tip": "Odstranite oznako z izbranih ciklov. Ostanejo v zgodovini, a ne oblikujejo več tega profila.", "use": "Uporabi", "wipe_all": "Izbriši vse podatke", "delete_selected": "Izbriši izbrano ({n})", - "group_suggest": "Skupina {n}: {members}", "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", "import": "Uvozi", "disconnect": "Prekini povezavo", "connect_github": "Poveži GitHub", - "save_appliance": "Shrani napravo", "share_to_store": "Deli v trgovino", "share": "Deli", - "confirm_appliance": "Potrdi to napravo", - "confirmed": "Potrjeno", "browse_community_setups": "Brskaj po nastavitvah skupnosti", "download_device": "Prenesi nastavitve naprave", "share_device": "Deli nastavitve naprave", - "share_device_tip": "Deli programe in nastavitve te naprave s skupnostjo", + "share_device_tip": "Delite to napravo in njene referenčne cikle v trgovini skupnosti, da jih lahko prevzamejo lastniki enakega stroja.", "share_n": "Deli cikle ({n})", "export_selected": "Izvoz (izberi podatke)", "export_all": "Hitri izvoz vsega", - "import_raw": "Napredno: zamenjaj vse iz datoteke JSON", "download_export": "Prenesi izvoz", "analyze_import": "Analiziraj datoteko", "import_selected": "Uvozi izbrano", @@ -260,7 +249,15 @@ "save_total_cycles": "Shrani skupno število", "clear": "Počisti", "add_brand": "Dodaj znamko", - "add_appliance": "Dodaj napravo" + "log_done_task": "Označi kot opravljeno: {task}", + "add_task": "Dodaj nalogo", + "add_appliance": "Dodaj napravo", + "retry": "Poskusi znova", + "confirm_appliance": "Potrdi to napravo", + "confirmed": "Potrjeno", + "create_profile_from_gap": "Ustvari profil", + "undo_import": "Razveljavi zadnji uvoz", + "pg_rerun": "Posodobi simulacijo" }, "note": { "off_delay": { @@ -268,10 +265,6 @@ } }, "conflict": { - "anti_wrinkle_exit": { - "exit": "Mora biti pod Pragom zaustavitve ({stop} W); sicer je izhodna moč zaščite pred gubami prezrta", - "stop": "Mora biti nad Pragom izhodne moči zaščite pred gubami ({exit} W)" - }, "anti_wrinkle_max": { "max": "Mora biti nad Pragom zagona ({start} W); sicer je časovna omejitev zaščite pred gubami zaobidena", "start": "Mora biti pod Maks. močjo zaščite pred gubami ({max} W)" @@ -309,14 +302,14 @@ "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." + "power_sensor": "To je Senzor moči naprave, ne cena. Cikli bi se obračunali v vatih za kWh; počistite to polje.", + "energy_sensor": "To je Merilnik energije naprave, ne cena. Cikli bi se obračunali po odčitku merilnika za kWh; počistite to polje.", + "device_class": "Ta senzor meri {dc}, ne pa ceno za kWh. Počistite to polje ali izberite senzor tarife.", + "unit": "Ta senzor meri v {unit}, ne pa ceno za kWh. Počistite to polje ali izberite 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" + "duration": "Mora biti manjše od Časovne omejitve brez posodobitve ({to} s), da se alarm zataknjene črpalke sproži prvi", + "timeout": "Mora presegati Trajanje zataknjenosti črpalke ({ps} s), da se alarm zataknjene črpalke sproži prvi" }, "start_dur": { "sampling": "Interval vzorčenja presega Trajanje zagona ({sdt} s); enotni vzorčni impulz lahko odpre cikel", @@ -334,10 +327,15 @@ "sampling": "Interval vzorčenja mora biti največ polovica Intervala nadzornika ({wi} s)" }, "settings_banner": "Konflikti nastavitev: {n}. Preverite označene razdelke in jih popravite pred shranjevanjem.", - "settings_banner_btn": "Pojdi na prvega" + "settings_banner_btn": "Pojdi na prvega", + "end_energy": { + "energy": "Prestrogo za Prag zaustavitve ({w} W) v času Zamude izklopa ({d} s); cikel se lahko konča le po nadomestni poti", + "stop": "Energija konca ({e} Wh v {d} s) dovoljuje le {p} W" + } }, "hdr": { - "ab_comparison": "Primerjava nastavitev A/B", + "custom_tasks": "Lastne naloge", + "maintenance_supporter": "Potrebujete več?", "access_control": "Nadzor dostopa", "add_maintenance": "Dodaj dogodek vzdrževanja", "automation_delete": "Izbriši to avtomatizacijo", @@ -346,28 +344,22 @@ "automation_review_this_cycle": "Preglej ta cikel", "automations": "Avtomatizacije", "clear_debug": "Počisti podatke za odpravljanje napak", - "cycle_simulator": "Simulator ciklov", "diagnostics_pref": "Diagnostika", "display": "Prikaz", - "dtw_inspector": "Pregledovalnik DTW", "export_import": "Izvoz / Uvoz", - "getting_started": "Začetek", "live_power": "Trenutna moč", "logs": "Dnevniki", - "logs_diagnostics": "Diagnostika", "maintenance": "Vzdrževalna dejanja", "maintenance_log": "Dnevnik vzdrževanja", "maintenance_reminders": "Opomniki za servis", "manual_recording": "Ročno snemanje", - "manual_recording_tip": "Namerno zaženite cikel, medtem ko WashData snema sled moči. Začnite tik preden naprava začne, zaustavite ko konča, nato obdelajte za shranjevanje kot poimenovan profil.", + "manual_recording_tip": "Namerno posnemite cikel: pritisnite Začni snemanje tik pred zagonom naprave in Ustavi, ko konča, nato Obdelaj, da ga shranite kot profil.", "ml_learned": "Kar se je WashData naučil", - "ml_matching_tuning": "Natančno prilagajanje prepoznavanja programov", "ml_settings_card": "Nastavitve", "ml_smart_learning": "Pametno učenje", "my_preferences": "Moje nastavitve", "no_automations": "Nobena avtomatizacija še ne sklicuje na to napravo.", "panel_settings": "Nastavitve plošče", - "param_sweep": "Preizkušanje parametra", "pg_settings_source": "Vir nastavitev", "phase_catalog": "Katalog faz", "playground": "Peskovnik", @@ -376,11 +368,8 @@ "status": "Stanje", "status_graph": "Graf stanja", "storage_stats": "Statistike shrambe", - "test_history": "Preizkusi na zgodovini", "toggle_sidebar": "Preklop stranske vrstice domačega pomočnika", - "tools_and_data": "Orodja in podatki", "wipe_history": "Izbriši zgodovino", - "recommendations": "Priporočila ({n})", "pg_detection_params": "Nastavitve zaznavanja", "pg_outcome": "Izid simulacije", "pg_across_cycles": "Po vseh vaših ciklih", @@ -388,7 +377,8 @@ "online_account": "Skupnostna trgovina in spletne funkcije", "import_power_history": "Uvoz zgodovine moči", "service_status": "Stanje servisa", - "online_account_tab": "Splet in skupnost" + "online_account_tab": "Splet in skupnost", + "live_match_debug": "Diagnostika sprotnega ujemanja" }, "health": { "fair": "Sprejemljiva kakovost profila", @@ -396,11 +386,16 @@ "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_tip": "Položaj na posneti krivulji ujemajočega programa: {pct}%. Določen s poravnavo cikla s profilom, zato ostane pravilen, ko je delovanje daljše ali krajše. Osveži se, ko je naprava tiha, zato lahko 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", + "task_name": "Naloga", + "every_cycles": "Interval (cikli)", + "every_days": "Interval (dnevi)", + "new_task_name": "Nova naloga", + "days_since_service": "{since} / {total} dni", "actions": "Dejanja", "activity": "Aktivnost", "administrators": "Skrbniki", @@ -408,10 +403,8 @@ "aria_cycle_chart": "Krivulja moči cikla", "aria_envelope_chart": "Ovojnica moči profila", "aria_phase_chart": "Graf urejevalnika faz", - "aria_playground_chart": "Krivulja moči cikla s premakljivimi pragovi zaznavanja", "aria_power_chart": "Graf moči", "aria_spaghetti_chart": "Prekrivajoče se krivulje moči ciklov", - "aria_sweep_chart": "Graf rezultatov pregleda parametra", "artifact_anomaly": "Nepravilnost", "artifact_high_power": "Visoka moč", "artifact_interruption": "Prekinitev", @@ -425,151 +418,103 @@ "cohesion_good": "kohezija {pct}%", "cohesion_low": "⚠ nizka kohezija {pct}%", "compare_profiles": "Primerjaj s profili", - "concurrency": "Sočasnost", "confidence": "Zaupanje", "confidence_threshold": "Prag zaupanja", "correct_duration": "Popravi trajanje (min, izbirno)", "correct_profile": "Popravi profil", - "correlation": "Korelacija", "cost": "Stroški", "create_new_profile": "+ Ustvari nov profil…", "current_phase": "Trenutna faza", - "curves": "Krivulje", "cycle": "Cikel", "cycle_date_display": "Prikaz datuma cikla", - "cycle_health": "Kakovost cikla", - "cycle_index": "indeks cikla →", - "cycles": "Cikli", "cycles_count": "Cikli", "cycles_lc": "ciklov", - "dashed": "črtkano", "date": "Datum", "debug_traces": "Sledenje za odpravljanje napak", - "default": "Privzeto", "default_access_level": "Privzeta raven za uporabnike, ki niso navedeni spodaj", "default_other": "Privzeto (druge naprave)", "default_tab": "Privzeti zavihek pri odpiranju plošče", "font_size": "Velikost pisave plošče", - "delta": "Δ", "description": "Opis", - "dtw_blend_w": "Utež mešanja", - "dtw_blended": "Zmešani rezultat", - "dtw_ddtw": "Rezultat DDTW", - "dtw_l1": "Rezultat L1", - "dtw_matched_profile": "(ujemajoči profil cikla)", "duration": "Trajanje", - "duration_agreement": "Ujemanje trajanja", - "emphasis": "Poudarek", "enable_access_control": "Omogoči nadzor dostopa za vsakega uporabnika", "energy": "Energija", - "energy_agreement": "Ujemanje energije", - "event_timeline": "Časovnica dogodkov", "event_type": "Vrsta dogodka", "expected": "Pričakovano", + "suspected_profile": "{name} (domnevno)", + "runner_up_profile": "{name} (drugi najboljši)", "expected_duration": "Pričakovano trajanje (min)", "file_kb": "Datoteka (kB)", - "final_score": "Končni rezultat", "from": "Od", "gap_s": "Presledek (s)", "group_name": "Ime skupine", "head_trim": "Obrezovanje začetka (s)", - "health": "Kakovost", "hide_tabs": "Skrij zavihke za ne-skrbnike", - "in_use": "V uporabi", "json_data": "Podatki JSON", "label": "Ujemajoči program", "last": "Zadnji", "last_checked": "Zadnja preveritev:", "load_from_file": "Naloži iz datoteke", - "mae_score": "Rezultat MAE", "manual_duration": "Ročno trajanje (min, izbirno)", "match": "Ujemanje", "members": "Člani", - "metric": "Meritev", "mode_existing_profile": "Dodaj k obstoječemu profilu", "mode_new_profile": "Ustvari nov profil", "models_fine_tuned": "(natančno prilagojeni modeli: {count})", - "n_classic_suggestions": "{n} klasično", - "n_ml_suggestions": "{n} ML", "n_selected": "{n} izbranih", - "n_tuning_suggestions": "{n} predlog za nastavljanje", - "na": "ni na voljo", + "n_tuning_suggestions": "{n} predlogov za nastavljanje", + "n_tuning_suggestions_one": "{n} predlog za nastavljanje", + "n_tuning_suggestions_two": "{n} predloga za nastavljanje", + "n_tuning_suggestions_few": "{n} predlogi za nastavljanje", + "n_tuning_suggestions_other": "{n} predlogov za nastavljanje", "new_profile_name": "Novo ime profila", "notes": "Opombe", - "of": "od", - "outcome": "Izid", "panel_default_tab": "Privzeti zavihek", "panel_language": "Jezik plošče", "per_cycle_short": "cikel", - "pg_autofill_tip": "Samodejno izpolni obseg iz trenutne vrednosti ({lo}–{hi})", "pg_best_value": "Najdena najboljša vrednost", "pg_current_value": "Trenutna vrednost:", "pg_drag_hint": "Povlecite črto na grafu", "pg_anti_wrinkle": "Zaščita pred gubami", - "pg_dtw": "DTW", "pg_ev_ending": "Zaključevanje", "pg_ev_running": "Deluje", "pg_group_advanced": "Robni primeri", "pg_group_detection": "Sprožilci zaznavanja", "pg_group_timing": "Pravila časovanja", "pg_idle": "Nedejavno", - "pg_last_run": "Zadnji zagon: {date}", - "pg_match_score": "Rezultat ujemanja", - "pg_pct_of": "{p}% od {total}", "pg_poor_match": "Slabo ujemanje", "pg_preset": "Prednastavitev peskovnika", "pg_preset_name": "Ime nove prednastavitve", "pg_preset_none": "Izberite prednastavitev…", "pg_strong_match": "Močno ujemanje", - "pg_tile_ambiguous": "Dvoumno", - "pg_tile_ambiguous_tip": "Cikli s skoraj izenačenim rezultatom med dvema programoma", - "pg_tile_detected": "Zaznano", - "pg_tile_detected_tip": "Cikli, pri katerih je moč dovolj dolgo presegla prag zagona, da so bili zabeleženi kot delovanje", - "pg_tile_matched": "Ujemano", - "pg_tile_matched_tip": "Cikli, pravilno pripisani znanemu programu", - "pg_time_left": "Preostali čas", "pg_weak_match": "Šibko ujemanje", "phase_name": "Ime faze", "name": "Ime", "phase_timeline": "Časovnica faz", "power": "Moč", "profile": "Profil", - "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. Izberite program pred zagonom naprave in uporabljen bo takoj, ko se začne naslednji cikel.", + "program_tip": "Izberite, s katerim programom se ujema trenutni cikel. Samodejno zaznavanje izbere najboljše ujemanje. Pripnite program, ko samodejno zaznavanje greši; program, izbran pred zagonom, velja za naslednji cikel.", "progress": "Napredek", "raw_socket": "Surova vtičnica", - "read_only": "Samo za branje", "ref_cycle": "Referenčni cikel (izbirno)", - "refresh_interval": "Interval osveževanja (s)", "remaining": "Preostalo", "reminder_every": "Opomni vsakih (ciklov)", "remove_label": "- Odstrani oznako -", "rename_profile": "Preimenuj profil", - "replay_duration": "Predvajanje", "resulting_profile": "Rezultirajoči profil", "save_mode": "Način shranjevanja", - "score": "Rezultat", - "score_breakdown": "Razčlenitev rezultata", "seconds_from_start": "Sekunde od začetka", "select_profile": "Izberi profil", "setting": "Nastavitev", - "settings_a": "Nastavitve A (trenutne)", - "settings_a_short": "A", "settings_advanced": "Napredno", - "settings_b": "Nastavitve B (test)", - "settings_b_short": "B", "settings_basic": "Osnovno", "settings_detail_level": "Raven podrobnosti nastavitev", - "settings_overrides": "Preglasitve nastavitev", "show_debug": "Prikaži kartico za odpravljanje napak na strani stanja (zaupanje, dvoumnost, najboljši kandidati)", "show_expected": "Prikaži pričakovano krivuljo (ujemajoči profil, oranžna)", "show_raw": "Prikaži stikalo za surove odčitke vtičnice v živem grafu moči", "sparkline": "Nedavni trend trajanja ciklov", - "stage2": "Stopnja 2 – osnovna podobnost", - "stage3": "Stopnja 3 – DTW", - "stage4": "Stopnja 4 – ujemanje", "status": "Stanje", "tail_trim": "Obrezovanje konca (s)", "tap_to_dismiss": "tapnite graf, da zaprete", @@ -580,7 +525,6 @@ "ungrouped": "Brez skupine", "unlabelled": "Brez oznake", "unlabelled_paren": "(brez oznake)", - "warp_path": "Pot deformacije", "input": "Vhod:", "start": "Začetek", "end": "Konec", @@ -605,9 +549,8 @@ "n_shown": ", prikazano {n}", "cycles_title": "Cikli ({n})", "pg_detecting": "Zaznavanje", - "pg_best_marker": "Najboljši: {v}", "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", + "pg_time_left_model_tip": "Ocenjeno po fazah programa, ne fiksno odštevanje", "phase": "Faza", "pg_mode_history": "Preizkusi na zgodovini", "pg_ev_detected": "Zaznano", @@ -647,18 +590,8 @@ "pg_mode_optimize": "Optimiziraj", "pg_group_matching": "Ujemanje programov", "pg_zoom_hint": "dvojni klik ponastavi povečavo", - "aria_playground_chart2": "Interaktivni graf moči cikla: pomaknite kazalec za prikaz časa/moči, drsajte za povečavo, povlecite za premikanje", "pg_alert_timeout_end": "Končano zaradi časovne omejitve, ne na podlagi napovedi", "pg_alert_indefinite": "Deloval bi v nedogled", - "pg_alert_stress_cap": "Dosežena varnostna meja", - "pg_alert_stress_ok": "Zaznavanje mirovanja: cikel ustavljen", - "pg_alert_stress_warn": "Poraba v mirovanju nad pragom ustavitve", - "pg_stress_group": "Preizkus zaznave mirovanja", - "pg_stress_idle_auto": "Auto", - "pg_stress_idle_w": "Raven mirovanja (W)", - "pg_stress_idle_w_desc": "Prepiši samodejno zaznano vrednost stanja pripravljenosti (pusti prazno za samodejno zaznavo).", - "pg_stress_toggle": "Preizkusi zaznavo mirovanja", - "pg_stress_toggle_desc": "Simulira, da naprava ostane pri porabi v stanju pripravljenosti po koncu snemanja -- prikaže, ali in kdaj WashData ustavi cikel.", "task_pg_history": "Preizkus na zgodovini", "task_pg_sweep": "Optimizacija", "task_reprocess": "Ponovna obdelava", @@ -668,16 +601,9 @@ "pg_recent_runs": "Nedavni zagoni", "pg_load_run": "Znova naloži ta zagon", "pg_match_confidence": "Zanesljivost ujemanja", - "pg_envelope_fit": "Prileganje ovojnici", - "pg_envelope_fit_tip": "Kako natančno cikel ostane znotraj shranjene ovojnice moči tega profila, drugačen pogled kot zanesljivost ujemanja.", "n_imported_note": ", {n} uvoženih", "enable_online": "Omogoči spletne funkcije", - "store_brand": "Znamka naprave", - "store_model": "Model naprave", "anonymous": "Anonimno", - "rate_quality": "Oceni kakovost", - "rate_n": "{n} zvezdic", - "approved_only": "Samo odobreno", "adopt_settings": "Sprejmi nastavitve", "flags": "Oznake", "include_phase_map": "Vključi fazno karto", @@ -694,7 +620,6 @@ "cat_custom_phases": "Faze po meri", "cat_profile_groups": "Skupine profilov", "cat_settings": "Nastavitve zaznavanja in prepoznavanja", - "cat_matching_config": "Uravnavanje prepoznavanja", "cat_ml_models": "Modeli ML", "cat_feedback": "Oznake povratnih informacij in pregleda", "cat_suggestions": "Predlogi", @@ -714,7 +639,6 @@ "conflict_overwrite": "Prepiši", "hist_csv_data": "Podatki CSV", "hist_from_recorder": "Ali jo preberite iz Home Assistant", - "days": "dni", "hist_keep": "Ohrani ta cikel", "hist_looks_complete": "zaključen", "peak_power_short": "Vrh", @@ -735,7 +659,44 @@ "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" + "correct_total_cycles": "Popravi skupno število", + "hist_reason_already_recorded": "se prekriva s ciklom, ki ga je WashData že zabeležil", + "share_consent": "Potrjujem, da so se ti cikli normalno izvedli do konca brez prekinitve", + "show_internal": "Prikaži notranje nastavitve", + "show_internal_tip": "Nastavitve, ki jih WashData uglasi namesto vas (vzorčenje, nadzornik, pogostost ujemanja, pragovi). Prikazane so, ko ima ta naprava lastno vrednost, konflikt ali predlog.", + "rate_n": "{n} zvezdic", + "rate_quality": "Oceni kakovost", + "pg_autolabel": "Samodejno označevanje", + "pg_label_review": "Zahteva pregled", + "pg_ev_match_reverted": "Ujemanje preklicano", + "pg_ev_verified_pause": "Potrjen premor", + "n_samples": "{n} vzorcev", + "n_samples_one": "{n} vzorec", + "n_samples_two": "{n} vzorca", + "n_samples_few": "{n} vzorci", + "n_samples_other": "{n} vzorcev", + "n_cycles": "{n} ciklov", + "n_cycles_one": "{n} cikel", + "n_cycles_two": "{n} cikla", + "n_cycles_few": "{n} cikli", + "n_cycles_other": "{n} ciklov", + "energy_per_cycle": "{v}/cikel", + "energy_total": "skupaj {v}", + "automation_off": "(izklopljeno)", + "ref_cycle_none": "Brez", + "share_name": "Prikaži moje ime pri vsebini, ki jo delim", + "cat_backfill_cycles": "Cikli, najdeni v uvoženi zgodovini moči", + "pg_compare_profile": "Primerjaj s profilom", + "pg_obj_early": "Stopnja zgodnjih koncev", + "pg_obj_endlag": "Zamuda konca (mediana)", + "pg_obj_split": "Stopnja razdeljenih ciklov", + "pg_sweep_cycles": "Nedavni cikli", + "cycle_context": "Zgodovina iz recorderja okoli cikla", + "cycle_context_off": "Izklopljeno", + "cycle_context_minutes": "{min} min", + "cycle_context_series": "Zgodovina iz recorderja", + "before_start": "Pred začetkom", + "after_end": "Po koncu" }, "log": { "all_levels": "Vse ravni", @@ -745,11 +706,9 @@ }, "ml": { "builtin_models": "● Uporablja vgrajene modele", - "fit_fair": "Sprejemljivo", "fit_good": "Dobro", "fit_slight": "Rahlo", "fit_strong": "Močno", - "fit_weak": "Šibko", "fit_word": "prileganje", "personalized": "● Prilagojeno tej napravi", "better_than_baseline": "{pct}% bolje od osnovne ocene ({metric})", @@ -757,21 +716,33 @@ "trend_declining_tip": "Prileganje tega modela se je v zadnjih ponovnih preverjanjih poslabšalo, pregled več ciklov mu lahko pomaga, da se ponovno nauči.", "trend_steady_tip": "Prileganje tega modela je v zadnjih ponovnih preverjanjih ostalo približno enako.", "cap_blurb": { - "end": "Prepoznavanje, kdaj se je cikel resnično končal", - "live_match": "Hitrejše prepoznavanje delujočega programa", - "quality": "Odkrivanje napačno zaznanih ali poškodovanih ciklov", - "remaining_time": "Napovedovanje preostalega časa", "total_energy": "Napovedovanje skupne porabe energije in stroškov" }, "cap_label": { - "end": "Zaznavanje konca cikla", - "live_match": "Prepoznavanje programa", - "quality": "Preverjanje kakovosti cikla", - "remaining_time": "Ocena preostalega časa", "total_energy": "Ocena porabe energije" }, - "metric_auc": "AUC {auc} na ločenih podatkih", - "metric_mae": "napaka {model} v primerjavi z izhodiščem {baseline}" + "metric_mae": "napaka {model} v primerjavi z izhodiščem {baseline}", + "fine_tuned_at": "natančno prilagojeno {when}", + "held_out_count": "izmerjeno na {count} ločenih ciklih", + "held_out_count_one": "izmerjeno na {count} ločenem ciklu", + "held_out_count_two": "izmerjeno na {count} ločenih ciklih", + "held_out_count_few": "izmerjeno na {count} ločenih ciklih", + "held_out_count_other": "izmerjeno na {count} ločenih ciklih", + "last_run_not_promoted": "Zadnja preveritev ({when}): {reason}", + "never": "nikoli", + "skip_reason": { + "fit_failed": "Učenje ni uspelo: {error}", + "holdout_too_small": "Premalo ciklov za pošten preizkus novega modela: za preizkus je bilo mogoče ločiti {held_out}, potrebnih je {min}.", + "insufficient_rows": "Še ni dovolj uporabnih ciklov: doslej {count} označenih, čistih ciklov.", + "insufficient_rows_one": "Še ni dovolj uporabnih ciklov: doslej {count} označen, čist cikel.", + "insufficient_rows_two": "Še ni dovolj uporabnih ciklov: doslej {count} označena, čista cikla.", + "insufficient_rows_few": "Še ni dovolj uporabnih ciklov: doslej {count} označeni, čisti cikli.", + "insufficient_rows_other": "Še ni dovolj uporabnih ciklov: doslej {count} označenih, čistih ciklov.", + "not_better_than_incumbent": "Novi model ni bil boljši od trenutno uporabljenega (napaka {model} v primerjavi z {incumbent}), zato je ostal trenutni.", + "not_better_than_naive": "Novi model ni bil izrazito boljši od preproste ocene (napaka {model} v primerjavi z {naive}).", + "unknown": "Naučeno ni bilo nič novega." + }, + "trained_unknown": "ob neznanem času" }, "modal": { "auto_label": "Samodejno označevanje ciklov", @@ -780,7 +751,6 @@ "cycle": "Cikel", "edit_group": "Uredi skupino profilov", "edit_phase": "Uredi fazo", - "import_config": "Uvozi konfiguracijo", "label_cycle": "Označi cikel", "merge_cycles": "Spoji {n} ciklov", "new_group": "Nova skupina profilov", @@ -795,6 +765,8 @@ "delete_automation_title": "Izbriši avtomatizacijo", "delete_automation_msg": "Izbrišem avtomatizacijo \"{name}\" iz Home Assistant? Tega dejanja ni mogoče razveljaviti.", "remove_legacy_title": "Odstrani zastarela dejanja", + "reset_phase_msg": "Razveljavim vaše urejanje faze \"{name}\" in obnovim vgrajeno fazo? Programi ohranijo svoje razpone.", + "reset_phase_title": "Ponastavi fazo", "remove_legacy_msg": "Odstranim zastarela dejanja po meri? Ob dogodkih cikla se ne bodo več sprožala. Tega iz plošče ni mogoče razveljaviti.", "discard_recording_title": "Zavrzi posnetek", "discard_recording_msg": "Zavržem shranjeni posnetek? Tega dejanja ni mogoče razveljaviti.", @@ -815,32 +787,46 @@ "store_share_device": "Deli nastavitve naprave", "export_select": "Izvoz - izberi podatke", "import_wizard": "Uvoz - izberi podatke", - "history_import": "Uvoz zgodovine moči" + "history_import": "Uvoz zgodovine moči", + "apply_suggestions_title": "Uporabi predloge", + "remove_task_title": "Odstrani nalogo", + "remove_task_msg": "Odstranim opomnik \"{task}\"? Njegovi vnosi v dnevniku vzdrževanja ostanejo ohranjeni.", + "apply_suggestions_msg": "Želite spremeniti te nastavitve ({n})? Integracija se po shranjevanju znova naloži.", + "discard_settings_title": "Zavržem neshranjene spremembe?", + "discard_settings_msg": "Neshranjene spremembe nastavitev: {n}. Zapustim Nastavitve in jih zavržem?", + "profile_title": "Profil · {name}", + "undo_import_msg": "Podatki in nastavitve te naprave se vrnejo v stanje pred zadnjim uvozom ({when}). Cikli, posneti od takrat, in spremembe od takrat bodo izgubljeni.", + "undo_import_ok": "Razveljavi uvoz", + "undo_import_title": "Razveljavi zadnji uvoz" }, "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.", + "pg_preset_delete_confirm": "Izbrišem prednastavitev \"{name}\"?", + "pg_preset_limit": "Dosežena omejitev prednastavitev ({n})", + "pg_preset_overwrite": "Prepišem prednastavitev \"{name}\"?", "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.", - "refresh_catalog_hint": "Seznama znamk in naprav skupnosti sta shranjena v predpomnilniku, da skupnostna trgovina ostane v okviru svoje dnevne omejitve. Osvežite, da prevzamete vnose, ki so jih dodali ali odobrili drugi.", + "store_sibling_hint": "Za vaš model ni nič deljeno? Soroden model iste znamke je običajno dober začetek.", + "store_declare_appliance": "Povejte WashData, katero napravo imate, da vidite nastavitve, ki so jih zanjo delili drugi. Ali vpišite znamko zgoraj in se razglejte.", + "refresh_catalog_hint": "Seznami znamk in naprav skupnosti so predpomnjeni zaradi dnevne omejitve trgovine. Osvežite, da vidite vnose, ki so jih drugi dodali ali odobrili.", "head_trim_hint": "Odstranite toliko sekund z začetka", "appliance_monitor": "Monitor naprave", "artifact_dip_detail": "Padel pod normalni pas moči za ~{n} s.", "artifact_footer": "Označeno na zgornjem grafu. To so prehodni artefakti (npr. vrata so se odprla sredi cikla), ne pa nujno težave.", "artifact_header": "{n} nepravilnosti, odkritih v tem ciklu", - "artifact_pause_detail": "Moč je padla skoraj na nič za ~{n} s in se nato obnovila – verjetno so bila odprta vrata sredi cikla ali je bil cikel zaustavljen.", + "artifact_header_one": "{n} nepravilnost, odkrita v tem ciklu", + "artifact_header_two": "{n} nepravilnosti, odkriti v tem ciklu", + "artifact_header_few": "{n} nepravilnosti, odkrite v tem ciklu", + "artifact_header_other": "{n} nepravilnosti, odkritih v tem ciklu", + "artifact_pause_detail": "Moč je za ~{n}s padla skoraj na nič in se nato vrnila. Verjetno so bila odprta vrata ali je bil cikel zaustavljen.", "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 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.", + "cleanup_intro": "Vsi označeni cikli drug čez drugega. Obkljukajte odstopajoče, nato jim odstranite oznako (ostanejo v zgodovini, a ne oblikujejo več tega profila) 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)", - "compare_profiles_tip": "Prekrijte druge ovojnice profila na zgornji tabeli, da vidite, katera najbolj ustreza temu ciklu.", + "compare_profiles_tip": "Prekrijte graf z drugimi profili, da vidite, kateri najbolj ustreza temu ciklu.", "compare_selected_cycles": "Izbrani cikli (polni) – prikaži / skrij", - "consider_new_profile": "Razmislite o ustvarjanju novega profila.", - "coverage_gap": "nedavni cikli nimajo ustreznega profila ({pct} % od zadnjih 30).", - "coverage_gap_similar_cycles": "Najdenih {count} podobnih neoznačenih ciklov – ustvarite profil za začetek ujemanja.", "cycles_deleted": "Izbrisanih ciklov: {count}", "enough_data": "Dovolj podatkov za učenje ({current}/{min} ciklov).", "export_description": "Izberite natančno, katere profile, cikle, nastavitve in drugo želite izvoziti v datoteko JSON, ali analizirajte datoteko in uvozite samo želene dele.", @@ -848,30 +834,28 @@ "feedback_prompt": "Potrdite pravilnost, popravite program ali prezrite.", "feedback_relabel_hint": "Ponovno označevanje tega cikla ga prav tako razreši.", "filter_by_profile": "Filtriraj po profilu…", - "group_modal_help": "Skupinske programe enake oblike, ki se razlikujejo po temperaturi/ožemanju (trajanje se lahko razlikuje). Ujemanje oceni skupino kot enega kandidata, nato pa izbere najbolj primernega člana. Izberite vsaj 2; prekrivanje kaže, kako si sta podobna.", + "group_modal_help": "Združite programe enake oblike, ki se razlikujejo po temperaturi ali ožemanju. Ujemanje oceni skupino kot celoto, nato izbere najbolj ustreznega člana. Izberite vsaj 2; prekrivanje pokaže, kako podobni so.", "group_not_cohesive": "Ti profili si niso dovolj podobni, da bi jih lahko zanesljivo združevali, zato jih ujemanje obravnava posamično, dokler ne odstranite odstopanja ali razdelite skupine.", "group_preview_hint": "Izberite vsaj 2 člana za predogled in primerjavo njihovih krivulj porabe moči.", - "import_intro": "Naložite izvoženo datoteko ali prilepite JSON spodaj.", - "legacy_actions_title": "{count} podedovano dejanje po meri se še vedno izvaja", + "legacy_actions_title": "{count} podedovanih dejanj po meri se še vedno izvaja", + "legacy_actions_title_one": "{count} podedovano dejanje po meri se še vedno izvaja", + "legacy_actions_title_two": "{count} podedovani dejanji po meri se še vedno izvajata", + "legacy_actions_title_few": "{count} podedovana dejanja po meri se še vedno izvajajo", + "legacy_actions_title_other": "{count} podedovanih dejanj po meri se še vedno izvaja", "live_chart_loading": "Graf moči se prikaže ob prihodu odčitkov.", "loading": "Nalaganje…", "loading_curve": "Nalaganje krivulje…", "loading_settings": "Nalaganje nastavitev…", "log_buffer_hint": "Najprej najnovejše · shrani zadnjih 500 zapisov ha_washdata od ponovnega zagona · povlecite spodnji rob, da spremenite velikost.", - "maintenance_advisory": "Podaljšanje trajanja/energije lahko pomeni, da je potrebno vzdrževanje naprave (npr. odstranjevanje vodnega kamna, čiščenje filtra).", "maintenance_due": "Vzdrževanje zapadlo: {items}", - "maintenance_intro": "Beležite servisiranje, ki ga izvajate na tej napravi, in prejmite opomnik, ko je vsaka naloga spet na vrsti.", + "maintenance_intro": "Beležite servisiranje te naprave in prejmite opomnik, ko je vsaka naloga spet na vrsti.", "maintenance_load_error": "Podatkov o vzdrževanju ni bilo mogoče naložiti: {error}", "maintenance_requires_access": "Vzdrževanje in izvoz/uvoz zahtevata polni dostop.", - "manual_duration_hint": "Povprečna/pričakovana dolžina cikla profila, uporabljena za ocene preostalega časa. Uredite, da ga nastavite; če ga ne spremenite, ohranite trenutno vrednost.", - "matching_tuning_intro": "Med učenjem WashData tudi prilagaja, koliko pri ujemanju programov tehta oblika v primerjavi s trajanjem in energijo.", + "manual_duration_hint": "Pričakovana dolžina cikla profila, uporabljena za oceno preostalega časa. Pustite nespremenjeno, da ohranite trenutno vrednost.", "merge_intro": "Izbrani cikli so združeni v enega (kronološki vrstni red; vrzeli zapolnjene z 0 W). Izberite rezultirajoči profil.", "ml_intro": "WashData je priložen s pametnimi modeli, ki delujejo takoj.", "ml_learned_intro": "Modeli natančno prilagojeni tej napravi.", - "ml_loading": "nalaganje ML…", "ml_settings_intro": "Dve neodvisni stikali: ena uporablja modele med delovanjem cikla, druga omogoča WashData, da jih sčasoma prilagodi vaši napravi.", - "name_first_program": "Imate dovolj ciklov – poimenujte svoj prvi program in začnite z ujemanjem.", - "near_duplicate_cluster": "zaznana skoraj podvojena gruča profilov. Združevanje omogoča ujemanje, ki zanesljivo izbira med podobnimi programi (npr. isti program pri različni temperaturi/ožemanju).", "no_cycles_match": "Noben cikel se ne ujema z aktualnim filtrom.", "no_cycles_profile": "Za ta profil ni ciklov.", "no_cycles_selected": "Najprej izberite vsaj en cikel.", @@ -896,79 +880,66 @@ "no_suggestions": "Ni aktivnih predlogov.", "notify_services_hint": "Uporabite ID-je storitev {entity} (ločene z vejicami za več storitev). Spremenljivke predloge: {vars}.", "old_actions_warning": "Konfigurirano s starim urejevalnikom dejanj (zdaj odstranjen). Še vedno se sprožijo ob dogodkih cikla, vendar jih tukaj ni več mogoče urejati. Pretvorite jih v običajno avtomatizacijo ali jih odstranite.", - "onboarding_progress": "Opazovanih ciklov: {n} / 3", - "onboarding_watching": "Napravo uporabljajte običajno – WashData opazuje. Po 3 ciklih se bo začelo ujemanje programov.", "pending_feedback": "Čakajoče povratne informacije o zaznavi", "performance_trend": "Trend uspešnosti ({n} ciklov)", - "pg_analysis_empty": "Naložite cikel za prikaz analize ujemanja.", "pg_apply_confirm": "Uporabim najboljšo vrednost: {label} = {value}?", - "pg_canvas_empty": "Zgoraj izberite cikel in nato pritisnite Naloži, da vidite njegov potek moči.", - "pg_chart_caption": "Stopnja zaznavanja/ujemanja glede na vrednosti {param}", "pg_ctrl_intro": "Vrednosti izhajajo iz aktivnih nastavitev integracije za to napravo. Urejanja ostanejo v peskovniku, dokler jih ne shranite v integracijo.", "pg_live_unavailable": "Aktivne nastavitve niso na voljo, prikazane so privzete vrednosti.", "pg_load_live_confirm": "Zavržem spremembe v peskovniku ({n}) in znova naložim trenutne nastavitve integracije?", - "pg_match_rate": "{pct}% stopnja ujemanja na {total} ciklih", "pg_matches_live": "Ujema se z aktivnimi nastavitvami", "pg_n_changed": "Spremenjeno glede na aktivne nastavitve: {n}", "pg_sugg_none": "Vsi predlogi se že ujemajo s trenutnimi nastavitvami", - "pg_preset_delete_confirm": "Izbrišem prednastavitev \"{name}\"?", - "pg_preset_limit": "Dosežena omejitev prednastavitev ({n})", - "pg_preset_overwrite": "Prepišem prednastavitev \"{name}\"?", "pg_publish_one_confirm": "Shranim {label} = {value} v nastavitve te naprave?", "pg_restart_note": "Znova zaženite Home Assistant, da omogočite orodja simulacije.", - "pg_sim_progress": "{done} / {total} ciklov", - "pg_starting": "Zaganjanje…", - "pg_stress_synthetic": "simulacija mirovanja se začne tukaj", - "pg_sweep_intro": "Kaj če bi bil {param} drugačen? Preizkusite {steps} vrednosti na svojih zadnjih {cycles} ciklih in poiščite nastavitev, pri kateri se pravilno ujema največ ciklov.", - "pg_sweep_step": "Korak {done} / {total}", - "pg_verdict_bad": "Zahteva pozornost: veliko ciklov ostaja nezaznanih.", - "pg_verdict_good": "Dobro nastavljeno: večina ciklov je pravilno prepoznanih in ujemanih.", - "pg_verdict_ok": "Sprejemljivo: nekateri cikli so bili spregledani. Poskusite znižati prag zagona.", "phase_catalog_intro": "Poimenovani segmenti cikla (predhodno pranje, ogrevanje, ožemanje…). Dodelite jih profilu iz nadzorne plošče.", "phase_ranges_intro": "Razponi faz (minute od začetka cikla) naloženi na povprečno krivuljo. Urejajte vrednosti za živahni predogled.", - "playground_intro": "Varen peskovnik za ponovno predvajanje preteklih ciklov z drugačnimi nastavitvami, primerjavo dveh konfiguracij drug ob drugem in pregled, kako se cikel poravna s profilom. Nič tukaj ne spremeni vaših aktivnih podatkov, dokler tega izrecno ne uporabite.", - "playground_needs_restart": "Znova zaženite Home Assistant, da omogočite orodja peskovnika.", - "preferences_admin": "Nastavitve, plošča in nadzor dostopa", - "preferences_adv": "Nastavitve", + "playground_intro": "Preizkusite nastavitve na dejanskih podatkih svojih ciklov. Nič tukaj ne spremeni aktivnih nastavitev, dokler jih ne uporabite.", "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), znova izračunajte stroške ciklov glede na zabeležene cene energije in znova izračunajte zdravje ciklov. Zaženite to po seriji pregledov.", + "process_history_hint": "Znova zaženite ujemanje na vseh shranjenih ciklih, osvežite predloge, ponovno naučite modele ML (če so omogočeni), preračunajte stroške ciklov po zabeleženih cenah in znova izračunajte zdravje ciklov. Zaženite 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.", - "rbac_hint": "Ko je izključeno, ima vsak uporabnik Home Assistant polni dostop (privzeto). Skrbniki imajo vedno polni dostop in lahko upravljajo vse.", - "recent_logs": "Nedavni zapisi ha_washdata", + "rbac_hint": "Izklopljeno: vsak uporabnik Home Assistant ima poln dostop (privzeto). Skrbniki imajo vedno poln dostop in lahko upravljajo vse.", "recording_in_progress": "Snemanje poteka", "reminders_intro": "Prikaži opomnik v plošči po tem številu ciklov od zadnjega servisa. Pustite prazno ali 0, da opomnik izklopite.", "restart_gap_footer": "Označeno na grafu. Podatki o moči manjkajo za te intervale; ujemanje je uporabilo samo dejanske meritve.", - "restart_gap_header": "{n} vrzel ponovnega zagona HA med tem ciklom", + "restart_gap_header": "{n} vrzeli ponovnega zagona HA med tem ciklom", + "restart_gap_header_one": "{n} vrzel ponovnega zagona HA med tem ciklom", + "restart_gap_header_two": "{n} vrzeli ponovnega zagona HA med tem ciklom", + "restart_gap_header_few": "{n} vrzeli ponovnega zagona HA med tem ciklom", + "restart_gap_header_other": "{n} vrzeli ponovnega zagona HA med tem ciklom", "restart_gap_item": "{dur} vrzel", - "review_confirm_help": "Preverite, ali je bil ta cikel pravilno zaznan. Vaši pregledi usposabljajo model na vašem stroju – več ciklov kot potrdite, boljše je ujemanje in točkovanje zdravja. Dovolj je hitro Dobro/Slabo.", + "review_confirm_help": "Je bil ta cikel pravilno zaznan? Potrjevanje ciklov izboljša ujemanje za vaš stroj. Dovolj je hitro Dobro/Slabo.", "review_in_settings": "Pregled v nastavitvah", "review_notes_placeholder": "Opombe (neobvezno)", "review_notes_tip": "Opombe s prostim besedilom za lastno referenco. Ne uporablja se pri ujemanju ali usposabljanju.", - "review_profile_tip": "Program, kot je ta cikel označen. Če je bil samodejno zaznan program napačen, ga popravite tukaj – označevanje uči ujemanja za prihodnje cikle.", - "review_quality_tip": "Kako čist je ta cikel. Dobro = učbeniški primer tega programa; Slabo = zaznano, vendar hrupno ali netipično; Neuporabno = napačno zaznano (združeno, okrnjeno ali lažno). Poganja rezultat zdravja in kateri cikli so dovoljeni za usposabljanje modela.", - "review_recorded_tip": "Označite to kot ročno izbran referenčni cikel za svoj program – enaka vloga kot ročno posnet cikel. Referenčni cikli so vedno shranjeni, zasejejo ujemajočo se predlogo in se nikoli ne izbrišejo s čiščenjem. (To je zlata/posneta zastava; obe sta enaki.)", - "review_tags_tip": "Neobvezne zastavice, ki opisujejo, kaj je šlo narobe s tem ciklom, tako da lahko to pojasnita usposabljanje in čiščenje.", + "review_profile_tip": "Program, s katerim je ta cikel označen. Če je napačen, ga popravite tukaj; oznake učijo ujemanje.", + "review_quality_tip": "Kako čist je ta cikel. Dobro: šolski primer tega programa. Slabo: zaznan, a šumen ali nenavaden. Neuporabno: napačno zaznan (združen, prekinjen ali lažen). Vpliva na oceno zdravja in na to, kateri cikli učijo model.", + "review_recorded_tip": "Označite kot ročno izbran referenčni cikel njegovega programa, kot ročno posnet cikel. Referenčni cikli se vedno ohranijo, tvorijo predlogo ujemanja in jih čiščenje nikoli ne odstrani.", + "review_tags_tip": "Neobvezne oznake o tem, kaj je šlo narobe, da jih učenje in čiščenje lahko upoštevata.", "review_to_cycles": "Odpri čakalno vrsto pregleda ciklov", "samples_decimated": "Prikazanih {shown} od {total} vzorcev (razredčeno za prikaz; konice ohranjene). Široka vrzel tukaj pomeni razredčitev, ne manjkajočih podatkov.", - "saving_triggers_reload": "Shranjevanje sproži ponovni zagon integracije. Entitete HA se lahko za kratek čas prikažejo kot nedostopne.", + "saving_triggers_reload": "Shranjevanje znova naloži integracijo. Entitete so lahko za kratek čas nedosegljive.", "search_placeholder": "Iskanje nastavitev…", "see_recorder": "Glejte pripomoček za snemanje spodaj", "setting_changed": "Spremenjeno iz {old} v {new} dne {date}", "settings_basic_note": "Prikazane so bistvene nastavitve. Za celoten seznam preklopite na Napredno.", - "shape_drift_advisory": "⚠ Odmik oblike", - "shape_drift_detail": "Vzorec moči tega profila se je s časom spremenil – možna obraba naprave ali potrebno vzdrževanje (npr. razkamianje, čiščenje filtra).", "show_all_settings": "Pokaži vse nastavitve", - "showing_suggestions": "Prikaz nastavitve {count} s predlogi.", - "split_intro": "Kliknite na graf, da dodate ali odstranite točko razdelitve, ali samodejno zaznajte po nedejavnih vrzelih. Vsak rezultirajoči segment lahko dobi lasten profil.", - "storage_diagnostics": "Statistike shrambe, vzdrževanje, izvoz/uvoz", + "showing_suggestions": "Prikazanih je {count} nastavitev s predlogi.", + "showing_suggestions_one": "Prikazana je {count} nastavitev s predlogi.", + "showing_suggestions_two": "Prikazani sta {count} nastavitvi s predlogi.", + "showing_suggestions_few": "Prikazane so {count} nastavitve s predlogi.", + "showing_suggestions_other": "Prikazanih je {count} nastavitev s predlogi.", + "split_intro": "Kliknite graf, da dodate ali odstranite točko razdelitve, ali jo samodejno poiščite po presledkih mirovanja. Vsak del lahko dobi svoj profil.", "sug_staged": "Nastavljeno {key} = {val}. Shranite za uveljavitev.", "sug_muted": "Te nastavitve ne bo več predlagalo", "sug_mute_failed": "Utišanje predloga ni uspelo", "sug_unmuted_all": "Utišani predlogi ponastavljeni", - "n_suggestions_muted": "{count} utišanih; samodejni nastavjalnik teh nastavitev ne bo predlagal.", + "n_suggestions_muted": "{count} predlogov je utišanih; samodejni nastavljalnik jih ne bo predlagal.", + "n_suggestions_muted_one": "{count} predlog je utišan; samodejni nastavljalnik ga ne bo predlagal.", + "n_suggestions_muted_two": "{count} predloga sta utišana; samodejni nastavljalnik ju ne bo predlagal.", + "n_suggestions_muted_few": "{count} predlogi so utišani; samodejni nastavljalnik jih ne bo predlagal.", + "n_suggestions_muted_other": "{count} predlogov je utišanih; samodejni nastavljalnik jih ne bo predlagal.", "toast_auto_detect_enabled": "Samodejno zaznavanje omogočeno", "toast_auto_label_complete": "Samodejno označevanje dokončano", "toast_auto_label_failed": "Samodejno označevanje ni uspelo: {error}", @@ -984,7 +955,6 @@ "toast_no_automation": "Tukaj ni mogoče ustvariti avtomatizacije", "toast_profile_name_required": "Ime profila je obvezno", "toast_program_set": "Program nastavljen: {program}", - "toast_rebuild_failed": "Obnova ni uspela: {error}", "toast_resume_failed": "Nadaljevanje ni uspelo: {error}", "toast_revert_failed": "Razveljavitev ni uspela: {error}", "toast_reverted": "{key} povrnjeno", @@ -995,95 +965,82 @@ "trend_energy_up": "Energija narašča ({pct}%/cikel) – zadnje povprečje {avg}", "trim_destructive_confirm": "S tem ostane samo {pct}% cikla in tega dejanja ni mogoče razveljaviti. Nadaljujem?", "trim_intro": "Povlecite rdeče ročke ali vnesite vrednosti. Vse zunaj okna se odstrani.", - "tuning_suggestions_available": "{count} predlog za nastavitev je na voljo iz opazovanih ciklov. Pojavijo se poleg ustreznih polj.", + "tuning_suggestions_available": "Na voljo je {count} predlogov za nastavitev na podlagi opazovanih ciklov. Pojavijo se poleg ustreznih polj.", + "tuning_suggestions_available_one": "Na voljo je {count} predlog za nastavitev na podlagi opazovanih ciklov. Pojavi se poleg ustreznega polja.", + "tuning_suggestions_available_two": "Na voljo sta {count} predloga za nastavitev na podlagi opazovanih ciklov. Pojavita se poleg ustreznih polj.", + "tuning_suggestions_available_few": "Na voljo so {count} predlogi za nastavitev na podlagi opazovanih ciklov. Pojavijo se poleg ustreznih polj.", + "tuning_suggestions_available_other": "Na voljo je {count} predlogov za nastavitev na podlagi opazovanih ciklov. Pojavijo se poleg ustreznih polj.", "unsure_detected_prefix": "WashData ni prepričan, da je zaznal", - "updated": "Posodobljeno", "warmup_badge": "Še se uči ({done}/{needed} ciklov)", - "warmup_detail": "Ta profil potrebuje {needed} označenih ciklov, preden se začne samodejno ujemanje. Vsak potrjen cikel mu pomaga pri učenju.", + "warmup_detail": "Ujemanje že deluje. WashData vas prosi, da potrdite prvih {needed} ciklov tega profila, preden začne sam označevati.", "wipe_history_warning": "Trajno izbrišite vse cikle in profile. Ni mogoče razveljaviti.", "compare_overlay_tip": "Prekrijte naučene ovojnice profilov, da vidite, kateri program je podoben posameznemu ciklu.", - "duration_trend_up_tip": "Trajanje narašča ({pct}%/cikel)", - "duration_trend_down_tip": "Trajanje upada ({pct}%/cikel)", - "energy_trend_up_tip": "Energija narašča ({pct}%/cikel)", - "energy_trend_down_tip": "Energija upada ({pct}%/cikel)", - "performance_trending": "Trend zmogljivosti", - "pg_select_cycle_hint": "Izberite cikel in kliknite ↺ za nalaganje", - "pg_sweep_progress": "Korak {done} / {total}", - "pg_analysis_hint": "Izberite cikel in kliknite ↺ za nalaganje analize.", "diagnostics_load_failed": "Diagnostike ni bilo mogoče naložiti: {error}", - "manual_duration_ref_hint": "Uporablja se samo, kadar ni izbran noben referenčni cikel. Referenčni cikel nastavi trajanje glede na svojo dolžino.", + "manual_duration_ref_hint": "Uporablja se samo brez referenčnega cikla; referenčni cikel nastavi trajanje po svoji dolžini.", + "duration_learned_hint": "Izračunano iz ciklov tega programa (brez odstopanj) in posodobljeno po vsakem novem. Za spremembo obrežite ali odstranite cikle, ki odstopajo.", "toast_convert_rolled_back": "Pretvorba ni uspela in je bila razveljavljena (nobena avtomatizacija ni ostala): {error}", "toast_convert_orphan": "Avtomatizacija je bila ustvarjena, vendar čiščenje starih dejanj ni uspelo. Ne poskušajte znova: stara dejanja odstranite ročno, da se izognete podvojeni avtomatizaciji.", - "advisory_duration_trend_up": "Cikli programa '{name}' postajajo vse daljši (približno +{pct}% na cikel). Če se je vedenje naprave spremenilo, ponovno posnemite ali obnovite ta profil.", - "advisory_energy_trend_up": "Program '{name}' porablja vse več energije (približno +{pct}% na cikel) - če je to nepričakovano, preverite napravo.", - "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.", + "advisory_duration_trend_up": "Cikli programa '{name}' postajajo vse daljši (približno +{pct}% na cikel). Če se je naprava spremenila, ponovno posnemite ali obnovite ta profil.", + "advisory_energy_trend_up": "Program '{name}' z vsakim ciklom porabi več energije (približno +{pct}% na cikel). Če je to nepričakovano, preverite napravo.", + "advisory_poor_health": "Profil '{name}' se slabo prilega: njegovi nedavni cikli se močno razlikujejo ali se šibko ujemajo. Preglejte njegove cikle ali ga ponovno posnemite, da ujemanje in ocene ostanejo natančni.", + "advisory_shape_drift": "Profil '{name}' je odstopil od prvotne oblike moči, morda zaradi vodnega kamna ali obrabe. Razmislite o ponovnem snemanju z nedavnimi cikli.", + "advisory_shape_drift_corr": "Profil '{name}' je odstopil od prvotne oblike moči (korelacija {corr}), morda zaradi vodnega kamna ali obrabe. Razmislite o ponovnem snemanju z nedavnimi cikli.", + "advisory_unmatchable": "Profil '{name}' se ne more nikoli ujemati: za njim ni nobenega cikla s podatki o moči. Označite ali posnemite cikel tega programa, da to popravite.", + "advisory_unmatchable_no_duration": "Profil '{name}' se ne more nikoli ujemati: njegov cikel nima uporabnega trajanja. Ponovno posnemite ta program ali nastavite njegovo pričakovano trajanje.", "pg_no_alerts": "V tem zagonu ni bilo zaznanih težav.", "pg_override_active": "Uporabljajo se vaše urejene nastavitve namesto trenutnih", "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.", "pg_sweep_intro2": "Poiščite nastavitev, ki najbolje izpolnjuje cilj v vaših nedavnih ciklih. Dokler je ne uporabite, se nič ne spremeni.", - "pg_running_history": "Predvajanje vaših ciklov…", - "pg_running_sweep": "Preizkušanje vrednosti po vseh vaših ciklih…", "pg_row_load_hint": "Naloži ta cikel v graf zgoraj", - "pg_history_intro2": "Znova predvajajte svoje nedavne cikle skozi pravi detektor in modul za ujemanje z zgornjimi nastavitvami. Kliknite poljubno vrstico, da naložite ta cikel v graf; spremenite nastavitev za prikaz primerjave pred in po.", + "pg_history_intro2": "Predvajajte nedavne cikle skozi pravi zaznavalnik in ujemanje z zgornjimi nastavitvami. Kliknite vrstico, da jo naložite v graf; spremenite nastavitev, da primerjate prej in potem.", "pg_simulating": "Simuliranje cikla…", - "pg_canvas_empty2": "Izberite cikel zgoraj in pritisnite Zaženi za simulacijo. Nato pomaknite kazalec za prikaz vrednosti, drsajte za povečavo in povlecite za premikanje.", "pg_apply_settings_confirm": "Shrani teh {n} nastavitev v to napravo? {list}", - "pg_stress_terminated_detail": "Ustalil se je na ~{idle}W mirovanja -- cikel se je končal {h} h {m} min pozneje prek {reason}.", - "pg_stress_above_threshold_detail": "Poraba v mirovanju ~{idle}W je enaka ali višja od dejanskega praga ustavitve ({stop}W) -- cikel se nikoli ni zaznal kot mirujoč. Za odpravo zvišajte stop_threshold_w.", - "pg_stress_hit_cap_detail": "Cikel je tekel {h} h {m} min brez ustavitve -- prisilno ustavljen z varnostno mejo. Poraba v mirovanju {idle}W v primerjavi s pragom ustavitve {stop}W.", "no_logs_match": "Noben zapis dnevnika ne ustreza tem filtrom.", "pg_analysis_hint2": "Izberite cikel in pritisnite Zaženi za nalaganje analize ujemanja.", - "pg_analysis_empty2": "Pritisnite Zaženi za prikaz analize ujemanja.", - "imported_readonly": "Uvoženo iz skupnostne trgovine. Prikazano za referenco in ujemanje. Ne šteje se v vašo statistiko in ga ni mogoče urejati.", + "imported_readonly": "Uvoženo iz trgovine skupnosti. Uporablja se kot referenca in za ujemanje, ne šteje se v statistiko in ga ni mogoče urejati.", "merge_no_imports": "Uvoženih posnetkov ni mogoče združiti z resničnim ciklom. Za združitev prekličite njihovo izbiro.", - "store_enable_hint": "Omogočite spletne funkcije v Nastavitvah, da boste lahko brskali in uvažali referenčne cikle skupnosti.", - "advisory_duration_outlier": "Cikli, označeni kot '{name}' ({n}), so preveč oddaljeni od njegove običajne dolžine, da bi se z njim kdaj lahko ujemali (največje odstopanje: {ratio}x). Verjetno so napačno označeni ali pa je en posnetek zajel dva zagona. Znova jih označite ali jih razdelite, da nehajo popačiti oceno časa za ta program.", + "store_enable_hint": "Vklopite spletne funkcije v Prednostne nastavitve > Splet in skupnost, da boste lahko brskali in uvažali referenčne cikle skupnosti.", + "advisory_duration_outlier": "Cikli, označeni kot ‘{name}’ ({n}), so predaleč od njegove običajne dolžine, da bi se kdaj ujemali (najslabši: {ratio}x). Verjetno so napačno označeni ali vsebujejo dva teka. Znova jih označite ali razdelite, da ne popačijo ocene časa.", "standby_above_stop_title": "Ob koncu cikla še vedno porablja {idle} W", "standby_above_stop_sub": "{n} od zadnjih {total} ciklov. Zamuda izklopa se začne šteti šele, ko moč pade pod Prag zaustavitve ({stop} W), zato se cikli končajo z zamudo ali jih je treba ustaviti ročno.", - "online_intro": "Delite in prenašajte referenčne cikle z drugimi uporabniki WashData. Vse spletne funkcije so prostovoljne in privzeto izklopljene.", "store_share_intro": "Naložite ta referenčni cikel, da ga bodo lahko uporabljali drugi z enako napravo. Pred javno objavo je pregledan.", - "online_intro_global": "Brskajte in delite referenčne posnetke z drugimi uporabniki WashData ter potrjujte vnose naprav. Ena povezava velja za celotno integracijo WashData; znamka in model naprave se nastavita za vsako napravo posebej v razdelku Osnovno. Vse spletne funkcije so prostovoljne in privzeto izklopljene.", + "online_intro_global": "Brskajte in delite referenčne posnetke z drugimi uporabniki WashData. Ena povezava velja za vse vaše naprave WashData; znamko in model nastavite za vsako napravo pod Osnovno. Spletne funkcije so izklopljene, dokler jih ne vklopite.", "online_unavailable": "Spletne funkcije na tem strežniku niso na voljo.", "store_picker_offline": "Omogočite spletne funkcije v zobniku nastavitev, da boste lahko izbirali iz kataloga skupnosti.", "pick_brand_first": "Najprej izberite znamko naprave.", - "model_not_found": "Še ni v katalogu.", - "connect_to_confirm": "Povežite se v zobniku nastavitev za potrditev ali oceno.", + "custom_task_limit": "Dosegli ste največje število nalog ({max}).", + "custom_tasks_intro": "Dodajte karkoli drugega, na kar želite biti opomnjeni. Naloga je na vrsti, ko je dosežen kateri koli od intervalov; enega pustite praznega ali 0, da se uporablja samo drugi.", + "maintenance_due_sensor_hint": "Binarni senzor Vzdrževanje na vrsti je vklopljen, dokler je katera koli naloga na vrsti; uporabite ga v lastnih avtomatizacijah obvestil.", + "maintenance_supporter_intro": "Maintenance Supporter je ločena integracija, ki zmore veliko več: katero koli napravo ali nalogo, urnike po času, uporabi ali senzorju, obvestila in zgodovino. Uporablja lahko števec ciklov WashData.", "adopt_settings_hint": "Uporabi nastavitve zaznavanja in ujemanja deljene naprave za to napravo", - "include_settings_hint": "Vključi pragove zaznavanja, uteži ujemanja in druge parametre v deljeni paket", + "include_settings_hint": "Delite pragove zaznavanja in ujemanja te naprave (ne obvestil, entitet ali cene energije). Drugi izberejo, ali jih bodo uporabili.", "onboard_download": "Prenesi vnaprej pripravljene nastavitve iz skupnostne trgovine", - "share_consent": "Delite dejanske podatke svojega aparata. Ne delite, če so vaši vzorci uporabe zasebni.", "share_device_none": "Še ni nastavljenih naprav. Najprej dodajte napravo.", "share_guideline_naming": "Uporabite jasna imena programov (npr. 'Cotton 40', 'Eco 60'), da jih drugi lažje prepoznajo", - "share_guideline_quality": "Delite samo cikle, ki so se normalno zaključili -- brez prekinitev sredi cikla, odpiranja vrat ali nihanj napajanja.", + "share_guideline_quality": "Delite samo cikle, ki so se končali normalno: brez prekinitev, odpiranja vrat in izpadov napajanja.", "share_guideline_review": "Preglejte profile pred deljenjem -- odstranite tiste, ki izgledajo napačni", "share_guidelines_title": "Pred deljenjem", - "store_download_device_intro": "Prenesi nastavitve skupnostne naprave in jih uporabi za novo ali obstoječo napravo", + "store_download_device_intro": "Dodajte svoji napravi vse deljene programe in njihove referenčne cikle. Vaši lastni cikli in statistika ostanejo nespremenjeni.", "store_share_device_intro": "Naložite {brand} {model} z referenčnimi cikli, ki jih izberete. Drugi z enako napravo lahko prevzamejo vaše programe. Vnosi so pregledani pred javno objavo.", "share_profile_no_cycles": "Ni referenčnih ciklov - označite cikel z ⭐ v zavihku Cikli, da vključite ta profil", - "advisory_phase_inconsistent": "Videti je, da '{name}' meša različne programe ali temperature - njegovi cikli se segrevajo zelo različno dolgo. Če ga razdelite na ločene profile (npr. po temperaturi), boste izboljšali ujemanje in ocene časa.", - "advisory_phase_inconsistent_title": "⚠ Morda pomešani programi", - "export_select_intro": "Označite natančno, kaj naj bo vključeno. Izbira profilov brez njihovih ciklov še vedno izvozi prepoznaven program (njegova naučena oblika se prenese z njim).", - "import_analyze_hint": "Naložite izvoženo datoteko (ali prilepite njen JSON). WashData jo analizira in natančno pokaže, kaj je mogoče uvoziti, preden se karkoli spremeni.", + "export_select_intro": "Označite, kaj naj bo vključeno. Profili brez svojih ciklov še vedno izvozijo prepoznaven program.", + "import_analyze_hint": "Naložite izvoženo datoteko (ali prilepite njen JSON). WashData pokaže, kaj je mogoče uvoziti, preden se karkoli spremeni.", "analyzing": "Analiziranje…", - "device_type_mismatch_warn": "Ta izvoz je iz druge vrste naprave ({src} vs {local}). Programe in cikle je še vedno mogoče uvoziti kot referenčne podatke, vendar so nastavitve, specifične za napravo, in uvoz dejanske zgodovine onemogočeni.", + "device_type_mismatch_warn": "Ta izvoz je iz druge vrste naprave ({src} vs {local}). Programe in cikle lahko še vedno uvozite kot referenčne podatke, ne pa nastavitev naprave ali dejanske zgodovine.", "merge_hint": "Uvoženi elementi se dodajo; nič lokalnega se ne izgubi. Spori imen se rešujejo spodaj.", "replace_warn": "Vsaka označena kategorija se izbriše in zamenja s podatki iz datoteke. Neoznačene kategorije ostanejo nespremenjene.", "dest_reference_hint": "Uvoženi cikli le izboljšajo prepoznavanje programov in nikoli ne vplivajo na statistiko porabe/energije.", - "dest_real_history_hint": "Uvoženi cikli se štejejo kot lastna zgodovina te naprave in napajajo statistiko energije/porabe. Uporabite pri prenosu ene naprave na novo namestitev.", - "import_history_description": "Ste pametni vtič uporabljali že pred WashData? Naložite izvoz zgodovine njegovega senzorja moči ali jo preberite neposredno iz Home Assistant in nad njo se izvede običajno zaznavanje, tako da se pretekli cikli pojavijo v vašem seznamu Cikli, pripravljeni za poimenovanje.", - "hist_input_hint": "Naložite CSV, prenesen iz plošče Zgodovina (entiteta, stanje, zadnja sprememba), ali pustite, da WashData prebere zgodovino senzorja neposredno. Zaznavanje nato poteka povsem enako kot v živo, vi pa izberete, katere najdene cikle boste ohranili.", - "hist_recorder_hint": "Bere podatke od izbranega datuma do zdaj. Home Assistant privzeto hrani podrobno zgodovino 10 dni, po tem pa le urna povprečja, ki so za zaznavanje ciklov preveč groba - zgodnejši datum izberite samo, če je vaš recorder nastavljen na daljše hranjenje.", - "hist_scanning": "Vašo zgodovino predvajamo skozi zaznavalnik. To poteka v ozadju - to pogovorno okno lahko zaprete in se vrnete pozneje.", + "dest_real_history_hint": "Uvoženi cikli štejejo kot lastna zgodovina in statistika te naprave. Uporabite pri prenosu naprave na novo namestitev.", + "import_history_description": "Ste pametni vtič imeli že pred WashData? Naložite njegovo zgodovino moči ali jo preberite iz Home Assistant. Zaznavanje gre čez te podatke, pretekli cikli pa se prikažejo na seznamu Cikli, pripravljeni za poimenovanje.", + "hist_input_hint": "Naložite CSV iz plošče Zgodovina (entiteta, stanje, zadnja sprememba) ali pustite, da WashData neposredno prebere zgodovino senzorja. Zaznavanje v živo gre čez te podatke, vi pa izberete, katere cikle obdržati.", + "hist_recorder_hint": "Bere od izbranega datuma do zdaj. Home Assistant privzeto hrani podrobno zgodovino 10 dni; starejša urna povprečja so pregroba za zaznavanje ciklov.", + "hist_scanning": "Vaša zgodovina gre v ozadju skozi zaznavalnik. To pogovorno okno lahko zaprete in se vrnete pozneje.", "hist_imported_count": "Uvoženi cikli: {n}.", "hist_duplicates": "Že prej uvoženo in preskočeno: {n}.", "hist_capped": "Dosežena je bila omejitev uvoženih ciklov na napravo; ostali niso bili shranjeni.", - "hist_next_step": "So v vašem seznamu Cikli, označeni kot uvožena zgodovina. Odprite enega in z gumbom Označi poimenujte program, ki mu pripada.", + "hist_next_step": "So na seznamu Cikli, označeni kot uvožena zgodovina. Odprite enega in z gumbom Označi poimenujte njegov program.", "hist_rows_read": "Prebrani odčitki: {n}", "hist_breaks": "Vrzeli, ko senzor ni bil dosegljiv: {n}", "hist_other_entity": "Prezrti odčitki drugih entitet: {n}", @@ -1095,25 +1052,76 @@ "hist_scan_capped": "Prikazani so samo prvi kandidati (najdenih je bilo {n}).", "hist_recorder_empty": "Home Assistant za ta senzor v tem obdobju nima podrobne zgodovine.", "hist_scan_failed": "Pregledovanje ni uspelo.", - "hist_scan_expired": "Ta pregled ni več na voljo. Zaženite ga znova.", + "hist_scan_expired": "Ta pregled ni več na voljo. Pregled zaženite 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.", - "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.", + "imported_history_readonly": "Najdeno v uvoženi zgodovini moči. Pomaga pri ujemanju, a se ne šteje v statistiko in ga ni mogoče obrezati ali razdeliti. Označite ga, da poimenujete program.", + "odometer_intro": "Ta vsota samo narašča. Brisanje cikla je ne zmanjša, zato opomniki za servis ostanejo pravilni.", + "odometer_correct_hint": "Nastavite to, če je WashData štel cikel, ki se ni zgodil, ali je naprava opravila cikle pred namestitvijo WashData.", "no_reminders_set": "Opomniki za servis še niso nastavljeni.", "toast_program_armed": "Program nastavljen za naslednji cikel: {program}", "toast_range_required": "Nastavite veljaven obseg Od/Do", - "no_profiles_option": "(ni profilov, dodajte novega)" + "no_profiles_option": "(ni profilov, dodajte novega)", + "compare_cycles_title": "Primerjaj cikle ({count})", + "edit_builtin_phase": "To je vgrajena faza. Shranjevanje ustvari lastno kopijo; izbrišite kopijo, da obnovite izvirnik.", + "fetch_error": "Nalaganje podatkov ni uspelo.", + "connection_lost": "Povezava je prekinjena - prikazano je stanje ob {time}.", + "pg_notify_partial": "Oznake prikazujejo samo obvestila o začetku, skorajšnjem koncu, zaključku, mejnikih in tihih urah. Posodobitve v živo, opomniki za raztovarjanje in opozorila o prekoračitvi se ne simulirajo.", + "connect_to_confirm": "Povežite se v zobniku nastavitev za potrditev ali oceno.", + "coverage_gap_cluster": "Nedavni neoznačeni cikli ({count}), dolgi približno {min} min, so videti kot en program, ki ga še niste ustvarili.", + "pg_label_yes": "Ob koncu cikla bi bil označen kot \"{name}\"", + "pg_label_no": "Bil bi poslan v pregled: {reason}", + "live_match_debug_tip": "Zaupanje: kako natančno se trenutna krivulja moči ujema z najboljšim kandidatnim profilom (0-100 %). Dvoumno: rezultata dveh najboljših kandidatov se razlikujeta za manj kot 5 %, zato je oznaka negotova, dokler se cikel ne konča.", + "automation_alias_started": "{device}: cikel se je začel", + "automation_alias_finished": "{device}: cikel je končan", + "automation_desc_started": "Zažene se, ko se začne cikel naprave {device}. Dodajte svoja dejanja (obvestila, luči, ...).", + "automation_desc_finished": "Zažene se, ko se cikel naprave {device} konča. Dodajte svoja dejanja (obvestila, luči, ...).", + "feedback_detected": "WashData je zaznal: {name}", + "import_undo_hint": "Shranjena je obnovitvena točka izpred zadnjega uvoza ({when}): {profiles} profilov in {cycles} ciklov.", + "replace_undo_hint": "Najprej se shrani obnovitvena točka, zato lahko ta uvoz razveljavite v razdelku Izvoz / Uvoz.", + "pg_in_sample_note": "Ti cikli so pomagali zgraditi profile, s katerimi se primerjajo, zato so rezultati optimistični: pri novih ciklih pričakujte nekoliko manj pravilnih ujemanj.", + "hist_tail_trimmed": "Čakanje po koncu cikla ({min} min) je bilo odrezano, tako kot to stori integracija med delovanjem", + "hist_truncated": "ustavljeno pri omejitvi odčitkov ({n}), zato preostanek zgodovine ni bil pregledan", + "hist_warn_kw": "Najvišji odčitek je samo {w}. Videti je, da ta senzor poroča kilovate; WashData potrebuje vate, zato ciklov ni mogoče zaznati.", + "hist_warn_naive_time": "{n} časovnih žigov nima časovnega pasu in je bilo prebranih kot UTC. Če datoteka uporablja lokalni čas, so cikli zamaknjeni za vaš zamik od UTC, odčitki ob spremembi ure pa se lahko izgubijo.", + "hist_warn_naive_time_one": "{n} časovni žig nima časovnega pasu in je bil prebran kot UTC. Če datoteka uporablja lokalni čas, so cikli zamaknjeni za vaš zamik od UTC, odčitki ob spremembi ure pa se lahko izgubijo.", + "hist_warn_naive_time_two": "{n} časovna žiga nimata časovnega pasu in sta bila prebrana kot UTC. Če datoteka uporablja lokalni čas, so cikli zamaknjeni za vaš zamik od UTC, odčitki ob spremembi ure pa se lahko izgubijo.", + "hist_warn_naive_time_few": "{n} časovni žigi nimajo časovnega pasu in so bili prebrani kot UTC. Če datoteka uporablja lokalni čas, so cikli zamaknjeni za vaš zamik od UTC, odčitki ob spremembi ure pa se lahko izgubijo.", + "hist_warn_naive_time_other": "{n} časovnih žigov nima časovnega pasu in je bilo prebranih kot UTC. Če datoteka uporablja lokalni čas, so cikli zamaknjeni za vaš zamik od UTC, odčitki ob spremembi ure pa se lahko izgubijo.", + "pg_compare_profile_tip": "Izbere le profil, narisan čez graf. Simulacija vedno sama poišče ujemajoči se program za cikel, tako kot integracija med delovanjem.", + "pg_outcome_stale": "Nastavitve so se spremenile, odkar je bila ta simulacija zagnana. Še vedno prikazuje star rezultat.", + "pg_sweep_guarded": "Prezgodaj konča več ciklov, jih več razdeli ali zgreši cikle, ki jih trenutna nastavitev zazna, zato ni nikoli priporočena.", + "pg_sweep_keep_current": "Ohrani trenutno vrednost ({value}, {metric}): nobena vrednost ni bila boljša vsaj za en cikel.", + "pg_sweep_n_cycles": "na {count} ciklih", + "pg_sweep_n_cycles_one": "na {count} ciklu", + "pg_sweep_n_cycles_two": "na {count} ciklih", + "pg_sweep_n_cycles_few": "na {count} ciklih", + "pg_sweep_n_cycles_other": "na {count} ciklih", + "pg_task_busy": "Za to napravo že poteka »Preizkusi na zgodovini« ali »Optimiziraj«. Počakajte, da konča, ali ga prekličite.", + "cycle_context_loading": "Nalaganje zgodovine iz recorderja…", + "cycle_context_note": "Sivo: zgodovina senzorja moči iz recorderja okoli tega cikla. Ni del cikla in ne vpliva na trajanje, energijo ali ujemanje.", + "cycle_context_none": "Recorder okoli tega cikla ne hrani nobenih odčitkov (Home Assistant privzeto hrani zgodovino 10 dni).", + "cycle_context_tip": "Samo za prikaz: pokaže zgodovino senzorja moči iz recorderja okoli tega cikla, če je še shranjena. Če želite neuspele poskuse zagona dodati prihodnjim ciklom, uporabite Uvodne sekunde krivulje (Nastavitve > Zaznavanje > Začetek cikla), kar spremeni shranjena trajanja.", + "invalid_threshold_pair": "Prag zaustavitve ({stop} W) mora biti pod Pragom zagona ({start} W). Nič ni bilo shranjeno; spremenite obe vrednosti hkrati." }, "phase_desc": { "anti_crease": "Občasni kratki premiki po zaključku za preprečevanje gub na oblačilih.", "anti_wrinkle": "Periodično vrtenje po ciklu za preprečevanje gub na oblačilih.", + "baking": "Grelec pri visoki temperaturi ustvari skorjo in sredico.", + "cooking": "Glavna faza priprave z vklopljenim grelcem in ventilatorjem.", "cool_down": "Vrtenje brez toplote blizu konca cikla.", + "cool_down_af": "Hlajenje samo z ventilatorjem po segrevanju.", "drain_and_switch": "Prehodno obdobje iz načina pranja v način sušenja.", "dry": "Stopnja sušenja z uporabo grelnika in/ali preostale toplote.", "drying": "Glavno ogrevano obdobje sušenja z vrtenjem.", "heat_up": "Začetno ogrevanje grelnika pred začetkom popolnega sušenja.", + "keep_warm": "Rahlo gretje, da hrana ostane topla.", + "keep_warm_bm": "Rahlo gretje, da kruh po peki ostane topel.", + "kneading": "Motor meša in gnete testo. Velika poraba moči.", + "pause_af": "Kratek premor za stresanje ali pregled.", + "pre_heat": "Začetno segrevanje komore pred pravo pripravo.", "pre_rinse": "Začetno pršenje pred pranjem z detergentom.", + "proving": "Vzhajanje pri nizki toploti: kvas fermentira in testo naraste.", + "resting": "Kratek premor pri nizki moči med gnetenjem, da se gluten sprosti.", "pre_wash": "Začetno namakanje ali predobdelava pred glavnim pranjem.", "rinse": "Stopnja izpiranja s čisto vodo. Ta faza se lahko ponovi večkrat.", "sanitize": "Visokotemperaturna stopnja čiščenja za programe razkuževanja.", @@ -1126,6 +1134,7 @@ "wash_dw": "Glavno pranje z detergentom z ogrevanjem." }, "placeholder": { + "custom_task": "npr. Očisti tesnilo vrat", "add": "dodaj…", "group_name": "npr. Bombaž 2:47", "json_buttons": "[{\"action\":\"ID\",\"title\":\"Label\"}]", @@ -1153,10 +1162,10 @@ "section": { "dishwasher": { "label": "Pomivalni stroj", - "intro": "Obravnava konca cikla za pomivalne stroje, ki se običajno končajo z dolgo, skoraj tiho fazo sušenja pred kratkim končnim izčrpavanjem vode." + "intro": "Obravnava konca za pomivalne stroje, ki pogosto dolgo in tiho sušijo pred kratkim končnim izčrpavanjem." }, "anti_wrinkle": { - "intro": "Način zaščite pred gubami na oblačilih zazna kratke pulze pri nizki moči po glavni fazi in jih obdrži pripisane končanemu ciklu.", + "intro": "Obdrži kratke pulze vrtenja bobna pri nizki moči po glavni fazi sušilnega stroja pri končanem ciklu.", "label": "Zaščita pred gubami" }, "basic": { @@ -1164,7 +1173,7 @@ "label": "Osnovno" }, "delay": { - "intro": "Zaznavanje zakasnjenega zagona prepozna, ko je naprava vklopljena, a cikla še ni začela.", + "intro": "Zazna napravo, ki je vklopljena in čaka na zamik zagona.", "label": "Zakasnjen zagon" }, "detection": { @@ -1188,161 +1197,153 @@ "label": "Časovanje in nadzornik" }, "triggers": { - "intro": "Izbirni zunanji signali: sprožilec za konec, senzor vrat, stikalo za premor in opomnik za raztovarjanje.", + "intro": "Neobvezni dodatki: sprožilec konca, senzor vrat, stikalo za premor in opomnik za raztovarjanje.", "label": "Sprožilci in vrata" - }, - "phase_eta": { - "label": "Preostali čas", - "intro": "Preostali čas z upoštevanjem faz, za naprave, katerih dolžina cikla je odvisna od temperature ali ožemanja." } }, "setting": { "dishwasher_end_spike_quiet_release": { "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." + "doc": "Po pričakovanem trajanju: kako dolgo mora pomivalni stroj ostati pod Pragom zaustavitve, preden WashData neha čakati na končno izčrpavanje. Zvišajte, če se pozno izčrpavanje po dolgem sušenju zgreši. Čakanje nikoli ne preseže 30 minut." }, "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.", + "doc": "Delež povprečnega trajanja programa, ki ga mora cikel doseči, preden se vrtenje bobna pri nizki moči šteje za rep zaščite pred gubami. Znižajte ga (npr. 0,75) pri sušilnih strojih, ki čas prilagajajo količini perila. Pri pralnih strojih pustite privzeto (0,98), sicer se pranje lahko konča na polovici.", "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.", + "doc": "Kratke pulze vrtenja pri nizki moči po glavni fazi segrevanja sušilnega stroja obdržite pri končanem ciklu, namesto da bi jih brali kot nove cikle.", "label": "Omogoči zaznavanje zaščite pred gubami" }, "anti_wrinkle_exit_power": { - "doc": "Moč mora med impulzi pasti pod to vrednost, da način proti gubam ostane aktiven.", + "doc": "V načinu zaščite pred gubami moč pod to vrednostjo šteje kot mirovanje med impulzi vrtenja, mirovanje, daljše od Maks. razmika med impulzi, pa način konča. Če je Prag zaustavitve višji, se uporabi ta, zato nastavitev nekaj spremeni le nad Pragom zaustavitve.", "label": "Prag izhodne moči" }, "anti_wrinkle_idle_timeout": { - "doc": "Kako dolgo lahko stroj med dvema vrtilnima impulzoma ostane tiho, preden se način zaščite pred gubami konča. Nastavite ga nad najdaljši razmik, ki ga vaš sušilni stroj pusti med impulzi, sicer se vsak nadaljnji impulz obravnava kot lažni zagon.", + "doc": "Kako dolgo sme sušilni stroj mirovati med pulzi vrtenja, preden se način zaščite pred gubami konča. Nastavite nad najdaljši premor vašega stroja, sicer se poznejši pulzi berejo kot lažni zagoni.", "label": "Maks. razmik med impulzi" }, "anti_wrinkle_max_duration": { - "doc": "Utripi, ki so daljši od tega, se obravnavajo kot pravi cikel in ne kot sredstvo proti gubam.", + "doc": "Pulz, daljši od tega, šteje za pravi cikel, ne za vrtenje proti gubam.", "label": "Maks. trajanje" }, "anti_wrinkle_max_power": { - "doc": "Impulz nad to močjo se obravnava kot pravi nov cikel, ne pa kot udarec proti gubam. Nastavite tik nad pulzno močjo.", + "doc": "Pulz nad to močjo šteje za pravi nov cikel, ne za vrtenje bobna. Nastavite tik nad moč pulzov vrtenja.", "label": "Maks. moč zaščite pred gubami" }, "auto_label_confidence": { - "doc": "Če je rezultat ujemanja na koncu cikla enak ali višji od tega, je program samodejno označen brez potrditvenega poziva. Povečajte ga tako, da zahteva večjo gotovost pred samodejnim označevanjem; znižajte ga za večjo avtomatizacijo. Deluje v povezavi z Learning Confidence pod njim.", + "doc": "Ocena ujemanja ob koncu cikla, pri kateri ali nad katero se program označi brez vprašanja. Zvišajte za večjo gotovost; znižajte, da se samodejno označi več ciklov. Držite nad Zaupanjem učenja.", "label": "Zaupanje samodejnega označevanja" }, "auto_maintenance": { - "doc": "Zaženite nočno čiščenje: obnovite ovojnice profila, znova izračunajte stanje cikla, odstranite sledi odpravljanja napak in obdržite najnovejše cikle.", + "doc": "Nočno čiščenje: označi starejše cikle, ki zdaj jasno ustrezajo programu, znova zgradi krivulje programov, osveži oznake anomalij in zdravje ciklov ter odstrani sledove za odpravljanje napak, če je Shrani sledove za odpravljanje napak izklopljeno. Cikli in njihovi zapisi moči se vedno ohranijo.", "label": "Samodejno vzdrževanje (nočno čiščenje)" }, "completion_min_seconds": { - "doc": "Cikli, krajši od tega, se zavržejo kot cikli duhov (testne vožnje, odpiranje vrat za dodajanje nogavice).", + "doc": "Cikli, krajši od tega (poskusni teki, odpiranje vrat za dodajanje nogavice), se shranijo kot prekinjeni in ne dobijo obvestila o koncu.", "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.", + "doc": "Doda odčitke iz neuspelih poskusov zagona na začetek cikla, za stroje, ki se pred zagonom preizkušajo (zaklep vrat, prvo polnjenje). 300 s zadošča večini; 0 = izklopljeno. Posneti začetki se premaknejo prej, zato se naučena trajanja nekaj časa spreminjajo.", "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.", + "doc": "Moč mora toliko časa ostati v pasu pripravljenosti, preden naprava šteje, da čaka na zagon.", "label": "Sekunde potrditve" }, "delay_start_detect_enabled": { - "doc": "Zazna, kdaj je naprava vklopljena in čaka (zakasnjen zagon/stanje pripravljenosti), vendar še ni začela svojega cikla, tako da se črpanje v stanju pripravljenosti ne zamenja za cikel v teku.", + "doc": "Zaznajte napravo, ki je vklopljena in čaka na zamik zagona, da se njena poraba v pripravljenosti ne šteje za delujoč cikel.", "label": "Omogoči zaznavanje zakasnjenega zagona" }, "delay_timeout_hours": { - "doc": "Prenehajte čakati v načinu zakasnjenega zagona po toliko urah in se vrnite v stanje mirovanja, tako da stroj, ki ostane pod napajanjem, a se nikoli ne zažene, ne bo čakal večno.", + "doc": "Po toliko urah nehajte čakati na zamik zagona in se vrnite v mirovanje.", "label": "Ure časovne omejitve" }, "device_type": { - "doc": "Razred aparata. Nastavi razumne privzete vrednosti zaznavanja (pragovi, zakasnitev izklopa, končna obdelava), prilagojene za to vrsto naprave; spremenite le, če je bila naprava prvotno nastavljena kot napačna vrsta.", + "doc": "Nastavi privzete vrednosti zaznavanja (pragovi, zamuda izklopa, obravnava konca) za to vrsto naprave. Spremenite le, če je bila naprava nastavljena kot napačna vrsta.", "label": "Vrsta naprave" }, "door_sensor_entity": { - "doc": "Izbirni binarni senzor vrat. Uporablja se za zaznavanje, kdaj je bila naprava odprta/raztovorjena po ciklu.", + "doc": "Neobvezen senzor vrat. Pokaže, kdaj je bila naprava po ciklu odprta za raztovarjanje.", "label": "Entiteta senzorja vrat" }, "duration_tolerance": { - "doc": "Toleranca za ocene preostalega časa (učna povratna informacija, ne ujemanje). Če je dejansko trajanje znotraj +/-X % ocene, se šteje za dobro ujemanje.", + "doc": "Samo za povratne informacije učenja: cikel znotraj +/-X% ocenjenega trajanja šteje za dobro oceno. Ne vpliva na ujemanje.", "label": "Toleranca ocene" }, "enable_ml_models": { - "doc": "Medtem ko cikel teče, pustite modelom, da izboljšajo rezultate v živo: stabilnejša ocena preostalega časa in energije/stroškov ter varovalo proti prezgodnji zaustavitvi ob zaznavi konca (lahko le odloži zaključek, nikoli ga ne konča predčasno in je omejen). Uporablja vaše natančno nastavljene modele, ko so na voljo, drugače pa vgrajene. Izklopljeno = samo klasična logika, ki temelji na moči (še vedno zanesljiva).", + "doc": "Uporabite model, naučen na tem stroju, za napoved energije in stroška cikla, ki poteka, in pustite, da se dobro znan program, ki se nenavadno zgodaj ustavi, konča brez polne zamude izklopa. Izklopljeno: samo logika na podlagi moči.", "label": "Uporabi pametne modele med ciklom" }, "end_energy_threshold": { - "doc": "Med odštevanjem zakasnitve izklopa se akumulirana energija (vati x čas) primerja s tem pragom. Če je preseženo, se odštevanje ponastavi – sušilni stroji proti mečkanju in konci sušenja v pomivalnem stroju ostanejo pritrjeni na cikel, namesto da bi jih skrajšali. Zvišajte ga, če se cikli končajo prezgodaj med ohlajanjem; znižajte, če je zaznavanje počasno.", + "doc": "Energija, dovoljena med zamudo izklopa. Nad to vrednostjo se cikel nadaljuje, zato vrtenja in končno sušenje ostanejo del cikla. Znižajte, če se cikli končajo med repom; zvišajte, če končani cikli še čakajo.", "label": "Energija konca" }, - "end_repeat_count": { - "doc": "Število zaporednih odčitkov pod mejno vrednostjo, potrebnih pred koncem cikla. 1 je primeren za večino vtičev. Povečajte na 2-3, če vaš pametni vtič občasno poroča o vzorcu napačne ničle sredi cikla in se vaši cikli končajo predčasno.", - "label": "Število ponovitev konca" - }, "energy_price_entity": { - "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.", + "doc": "Senzor s trenutno ceno za kWh (npr. dinamična tarifa). Ima prednost pred statično ceno spodaj, zato mora biti cena, ne senzor moči ali energije vtiča.", "label": "Entiteta cene energije" }, "energy_price_static": { - "doc": "Fiksna cena na kWh, ki se uporablja za podatke o stroških, kadar zgoraj ni nastavljen noben subjekt v živo.", + "doc": "Fiksna cena za kWh, uporabljena, ko entiteta cene ni nastavljena.", "label": "Statična cena energije (za kWh)" }, "energy_sensor": { - "doc": "Neobvezni kumulativni števec energije (total_increasing kWh/Wh, npr. lasten števec skupne porabe vtičnice). Ko je nastavljen, se energija, prikazana za vsak cikel, vzame iz razlike odčitkov tega števca med začetkom in koncem, kar prepreči podštevanje, do katerega pride pri integriranju počasi poročajočega senzorja moči. Vrne se na integrirano vrednost, če odčitek manjka, je njegova enota neznana ali razlika ni pozitivna. Pustite prazno, da še naprej integrirate senzor moči.", + "doc": "Neobvezen števec kWh ali Wh (total_increasing, npr. lastni merilnik vtiča). Energija cikla se potem bere iz njega, kar je natančneje od izračuna iz počasnega senzorja moči. Če je njegov odčitek neuporaben, se uporabi izračunana vrednost.", "label": "Entiteta merilnika energije" }, "expose_debug_entities": { - "doc": "Objavite dodatne diagnostične entitete HA (zaupanje ujemanja, dvoumnost, notranje stanje). Izključeno ohranja seznam entitet čist za običajno uporabo.", + "doc": "Dodajte diagnostične entitete (zaupanje ujemanja, dvoumnost, notranje stanje). Izklopljeno: seznam entitet ostane pregleden.", "label": "Izpostavi entitete za odpravljanje napak" }, "external_end_trigger": { - "doc": "Binarni senzor, katerega sprememba stanja označuje konec cikla (npr. \"zaključen\" stik naprave ali integracija spremljevalca).", + "doc": "Binarni senzor, katerega sprememba označuje konec cikla (npr. kontakt »končano« ali druga integracija).", "label": "Zunanja sprožilna entiteta" }, "external_end_trigger_enabled": { - "doc": "Naj zunanji binarni senzor signalizira konec cikla poleg vgrajenega zaznavanja na podlagi moči.", + "doc": "Dovolite binarnemu senzorju, da sporoči konec cikla, poleg zaznavanja na podlagi moči.", "label": "Omogoči zunanji sprožilec za konec" }, "external_end_trigger_inverted": { - "doc": "Izklop sprožilnega senzorja (namesto VKLOP) obravnavajte kot signal za konec cikla.", + "doc": "IZKLOP senzorja (ne vklop) obravnavajte kot signal konca.", "label": "Obrni zunanji sprožilec (sproži ob IZKLOP)" }, "learning_confidence": { - "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.", + "doc": "Najnižja ocena ujemanja za označitev končanega cikla. Jasen zmagovalec se označi; tesen izid gre v čakalno vrsto za pregled v Ciklih. Pod to vrednostjo se nič ne označi in nič ne vpraša. Držite jo pod Zaupanjem samodejnega označevanja.", "label": "Zaupanje učenja" }, "linked_device": { - "doc": "Po želji ugnezdite to napravo WashData pod drugo napravo (npr. pametni vtič) v registru naprav HA, prikazano kot »Povezano prek ...«.", + "doc": "Neobvezno. Prikažite to napravo WashData pod drugo napravo (npr. pametnim vtičem) v Home Assistant kot »Povezano prek ...«.", "label": "Združi pod napravo" }, "min_off_gap": { - "doc": "Če se stroj izklopi za manj kot ta čas, se zaporedje vklop/izklop/vklop obravnava kot en neprekinjen cikel. Preprečuje, da bi se programi namakanja (stroj izklopi za nekaj minut med pranjem) razdelili na dva ločena cikla. Nastavite ga krajše od vrzeli med zaporednimi obremenitvami, če želite, da se te štejejo kot ločeni cikli. Privzete nastavitve vrste naprave ščitijo tipično prekinitev med ciklom za vsako napravo.", + "doc": "Izklop, krajši od tega, ne razdeli cikla, zato premori namakanja ostanejo v enem pranju. Držite krajše od presledka med zaporednimi pranji, sicer se združijo v en cikel.", "label": "Min. presledek izklopa" }, "min_power": { - "doc": "Absolutna najmanjša moč, ki velja za aktivno. Odčitki pod tem se obravnavajo kot 0 W (stanje pripravljenosti), pri čemer se filtrira fantomska obremenitev pametnih vtičev in LED diod v stanju pripravljenosti.", + "doc": "Odčitki pod to vrednostjo štejejo kot 0 W (pripravljenost). Izloči porabo v mirovanju pametnih vtičev in LED-ic pripravljenosti.", "label": "Minimalna moč" }, "ml_training_enabled": { - "doc": "Čez noč občasno preučite svoje pregledane cikle in natančno prilagodite modele temu specifičnemu stroju. Sprememba se ohrani le, če resnično doseže boljše rezultate v zadržanih ciklih, tako da lahko to samo pomaga ali ostane enako – nikoli ne nazaduje.", + "doc": "Občasno ponovno naučite model energije na končanih ciklih tega stroja. Nov model se obdrži le, če na testnih ciklih premaga preprosto oceno.", "label": "Uči se iz te naprave" }, "ml_training_hour": { - "doc": "Lokalna ura dneva (0-23) za natančno nastavitev čez noč. Izberite mirno uro, na primer 2 (02:00).", + "doc": "Ura dneva (0-23) za nočno učenje. Izberite mirno uro, npr. 2.", "label": "Uči se ob uri" }, "ml_training_interval_days": { - "doc": "Ponovno preverite izboljšave največ enkrat na toliko dni.", + "doc": "Ponovno učite največ enkrat na toliko dni.", "label": "Preverjaj največ vsakih" }, "ml_training_min_cycles": { - "doc": "Pred natančno nastavitvijo počakajte, da se zabeleži vsaj toliko ciklov, da se je dovolj naučiti.", + "doc": "Pred učenjem počakajte vsaj na toliko posnetih ciklov.", "label": "Najprej potrebno število ciklov" }, "name": { - "doc": "Prikazno ime, prikazano na seznamu integracij HA in registru naprav.", + "doc": "Ime te naprave, prikazano v Home Assistant.", "label": "Ime naprave" }, "no_update_active_timeout": { - "doc": "Če med delovanjem tako dolgo ne prispe nobena posodobitev napajanja, domnevajte, da je vtič padel brez povezave in ga prisilno ustavite, da se izognete ciklu zombija. Privzetih 600 s omogoča zamik oblaka ali mreže.", + "doc": "Če med ciklom toliko časa ne pride nobena posodobitev moči, se predpostavi, da je vtič brez povezave, in cikel se prisilno ustavi. Privzetih 600 s pokrije zamude oblaka ali mesh omrežja.", "label": "Časovna omejitev brez posodobitve" }, "notify_before_end_minutes": { @@ -1350,35 +1351,35 @@ "label": "Opozorilo pred koncem" }, "notify_channel": { - "doc": "Ime kanala za obveščanje v sistemu Android za sporočila za začetek/v živo (nadzoruje zvok in prednost posameznega kanala v mobilni aplikaciji). Prazno uporablja privzeto spremljevalno aplikacijo.", + "doc": "Kanal Android za sporočila o začetku in v živo; kanal določa zvok in prioriteto. Prazno: privzeti kanal aplikacije.", "label": "Android kanal (zagon/v živo)" }, "notify_cycle_timers": { - "doc": "Obvestila ob določenih minutah v ciklu (npr. za dodajanje mehčalca). Sporočilo podpira {device}, {program}, {minutes}. Omogočite Samodejni premor, da se na tej točki ustavite in prejmete interaktivno obvestilo z gumbom Nadaljuj; nadaljujte prek plošče, storitve za premor/nadaljevanje ali dejanja obvestila.", + "doc": "Obvestila ob nastavljenih minutah cikla (npr. za dodajanje mehčalca). Sporočilo podpira {device}, {program}, {minutes}. S samodejnim premorom se cikel tam tudi ustavi, obvestilo pa dobi gumb Nadaljuj.", "label": "Časovniki cikla" }, "notify_finish_channel": { - "doc": "Ime kanala za obvestila Android za sporočilo o koncu. Prazno ponovno uporabi začetni kanal/kanal v živo.", + "doc": "Kanal Android za sporočilo o koncu. Prazno: uporabi se kanal za začetek in v živo.", "label": "Android kanal (dokončanje)" }, "notify_finish_message": { - "doc": "Telo, poslano ob koncu cikla. Spremenljivke predloge: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}. {time_finished} in {vs_typical} sta tu najbolj uporabna.", + "doc": "Besedilo, poslano ob koncu cikla. Spremenljivke: {device}, {program}, {duration} (minute), {duration_hm} (1 h 05 min), {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}, {status} (completed ali force_stopped; prekinjeni cikli ne pošljejo ničesar). Pri vsaki drugi spremenljivki se uporabi privzeto besedilo.", "label": "Sporočilo ob dokončanju" }, "notify_finish_services": { - "doc": "notify.* storitve, ki se kličejo, ko se cikel konča. Dodajte enega na cilj; pustite prazno, če ni obvestila o koncu.", + "doc": "Storitve notify.*, ki se kličejo ob koncu cikla. Pustite prazno za nobeno.", "label": "Storitve ob dokončanju" }, "notify_fire_events": { - "doc": "Sprožite tudi dogodke ha_washdata_* na začetku/koncu cikla, da lahko ustvarite lastno avtomatizacijo.", + "doc": "Sprožite tudi dogodke ha_washdata_* ob začetku in koncu cikla za lastne avtomatizacije.", "label": "Sproži HA dogodke za obvestila" }, "notify_icon": { - "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.", + "doc": "Majhna ikona na obvestilu (npr. mdi:washing-machine). Pošilja se samo ciljem mobile_app, ne skupinam notify. Na iOS nadomesti ikono aplikacije in zahteva mobilno aplikacijo 2026.8 ali novejšo. Prazno: privzeta ikona platforme.", "label": "Ikona obvestila" }, "notify_live_chronometer": { - "doc": "V obvestilu (na platformah, ki to podpirajo) namesto statične ocene prikažite odštevalnik, ki se posodablja v živo.", + "doc": "V obvestilu prikažite odštevanje v živo (kjer je podprto) namesto fiksne ocene.", "label": "Uporabi sprotni kronometer" }, "notify_live_interval_seconds": { @@ -1386,143 +1387,135 @@ "label": "Interval sprotnih posodobitev" }, "notify_live_overrun_percent": { - "doc": "Če cikel preseže svojo oceno za več kot ta odstotek, pošljite opozorilo o prekoračitvi.", + "doc": "Sprotne posodobitve se ustavijo, ko cikel preseže pričakovano trajanje za ta odstotek (20 = do 1,2-kratnika pričakovanega časa).", "label": "% prekoračitve pred opozorilom" }, "notify_live_services": { - "doc": "storitve notify.* so poklicale posodobitve napredka v živo med izvajanjem cikla. Pustite prazno, da onemogočite obvestila o napredku v živo.", + "doc": "Storitve notify.* za posodobitve napredka v živo med ciklom. Pustite prazno, da jih izklopite.", "label": "Storitve sprotnega napredka" }, "notify_only_when_home": { - "doc": "Pošlji obvestila le, če je vsaj ena od povezanih oseb (zgoraj) doma.", + "doc": "Obveščajte samo, ko je vsaj ena od zgornjih oseb doma.", "label": "Obvesti samo ko doma" }, "notify_people": { - "doc": "person.* entitete, ki jih funkcija »Obvesti samo, ko je doma« uporablja za odločanje, ali je kdo doma.", + "doc": "Entitete person.*, ki jih preverja »Obvesti samo ko doma«.", "label": "Osebe (za Samo ko doma)" }, "notify_pre_complete_message": { - "doc": "Telo opozorila pred koncem / skoraj končano. Spremenljivke predloge: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Besedilo vsake sprotne posodobitve napredka. Spremenljivke predloge: {device}, {program}, {minutes} (preostale minute). Pri vsaki drugi spremenljivki se uporabi privzeto besedilo.", "label": "Sporočilo pred dokončanjem" }, "notify_reminder_message": { - "doc": "Telo opomnika, da perilo še vedno čaka na odstranitev. Prazno uporabi vgrajeno privzeto vrednost. Spremenljivke predloge: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Besedilo enkratnega Opozorila pred koncem. Spremenljivke predloge: {device}, {program}, {minutes} (nastavitev Opozorilo pred koncem). Pri vsaki drugi spremenljivki se uporabi privzeto besedilo.", "label": "Sporočilo opomnika" }, "notify_start_message": { - "doc": "Telo, poslano ob začetku cikla. Spremenljivke predloge: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Besedilo ob začetku cikla. Spremenljivke predloge: {device}, {program}. Pri vsaki drugi spremenljivki se uporabi privzeto besedilo.", "label": "Sporočilo ob zagonu" }, "notify_start_services": { - "doc": "notify.* storitve, ki se kličejo, ko se cikel začne. Dodajte enega na cilj (telefon, nadzorna plošča itd.); pustite prazno, če ni obvestila o začetku.", + "doc": "Storitve notify.*, ki se kličejo ob začetku cikla (telefon, nadzorna plošča, ...). Pustite prazno za nobeno.", "label": "Storitve ob zagonu" }, "notify_timeout_seconds": { - "doc": "Samodejno opusti obvestilo po toliko sekundah (na platformah, ki to podpirajo). 0 ga hrani, dokler ga ročno ne zavržete.", + "doc": "Zaprite obvestilo po toliko sekundah (kjer je podprto). 0 ga pusti, dokler ga ne zaprete.", "label": "Samodejno zavrži po" }, "notify_title": { - "doc": "Naslov obvestila. Spremenljivke predloge: {device}, {program}, {duration}, {energy}, {cost}, {date}, {time}, {minutes}.", + "doc": "Naslov obvestila. Zamenja se samo {device}; pri vsaki drugi spremenljivki se uporabi privzeti naslov.", "label": "Naslov obvestila" }, "notify_unload_delay_minutes": { - "doc": "Minute po koncu cikla pred pošiljanjem še vedno čakajočega opomnika \"raztovarjanje stroja\". Nastavite 0, da onemogočite opomnik.", + "doc": "Minute po koncu cikla pred opomnikom »raztovorite stroj«. 0 ga izklopi.", "label": "Zamuda opomnika za raztovarjanje" }, "notify_unload_repeat": { - "doc": "Znova pošilja opomnik za raztovarjanje vsakih „Zamuda opomnika za raztovarjanje“ minut, dokler raztovarjanje ni potrjeno ali v obvestilu ne tapnete „Nehaj opominjati“. Potrebuje način za potrditev raztovarjanja: Entiteto senzorja vrat, Entiteto potrditve raztovarjanja ali spodnjo možnost „Ročna potrditev raztovarjanja“. Gumb za opustitev deluje pri obvestilih mobilne aplikacije Home Assistant (companion app).", + "doc": "Ponovno pošljite opomnik za raztovarjanje vsakih »Zamuda opomnika za raztovarjanje« minut, dokler raztovarjanje ni potrjeno ali ne tapnete »Nehaj opominjati« (mobilna aplikacija). Potrebuje Entiteto senzorja vrat, Entiteto potrditve raztovarjanja ali »Ročna potrditev raztovarjanja«.", "label": "Ponavljaj do raztovarjanja" }, "off_delay": { - "doc": "Čas za čakanje po padcu moči, preden razglasite, da je cikel končan. Če se napajanje ponovno vzpostavi v tem oknu, se cikel nemoteno nadaljuje - to premosti premore med stopnjami pranja. Pomivalni stroji imajo dolge faze sušenja (izklop za 20–60 min), zato mora biti zakasnitev izklopa daljša od te, da ostane celotno pranje+sušenje kot en cikel.", + "doc": "Kako dolgo mora moč ostati nizka, preden je cikel končan. Če se moč v tem času vrne, se cikel nadaljuje. Pomivalni stroji lahko med sušenjem mirujejo 20-60 min, zato nastavite dlje.", "label": "Zamuda izklopa" }, "pause_cuts_power": { - "doc": "Ko je cikel zaustavljen, izklopite tudi spodnjo entiteto Switch. Samo za naprave, katerih vtič je mogoče varno prerezati sredi cikla.", + "doc": "Ko je cikel zaustavljen, izklopite tudi spodnjo Entiteto stikala. Samo za naprave, ki lahko varno izgubijo napajanje sredi cikla.", "label": "Premor tudi prekine napajanje (prek stikala)" }, "power_off_delay": { - "doc": "Kako dolgo mora moč ostati pod vrednostjo »Prag izklopa napajanja« po zaključku cikla, preden se stanje vrne na »Izključeno«. Uporablja se samo, ko je »Prag izklopa napajanja« večji od 0. Preverja se v ozadju v rednih intervalih, zato se dejanska zamuda zaokroži navzgor na naslednji korak poteka stanja.", + "doc": "Kako dolgo mora moč po ciklu ostati pod Pragom izklopa napajanja, preden se stanje vrne na Izključeno. Uporablja se le, ko je ta prag nad 0. Preverja se občasno, zato je dejansko čakanje lahko nekoliko daljše.", "label": "Zamuda izklopa napajanja" }, "power_off_threshold_w": { - "doc": "Neobvezno zaznavanje stanja »Izključeno« na podlagi moči. Ko je vrednost večja od 0, se po zaključku cikla, če moč ostane pod to ravnjo v času »Zamuda izklopa napajanja«, naprava obravnava kot izklopljena in stanje se vrne na »Izključeno«. Pustite 0 za onemogočenje (privzeto). Nastavite jo nad dejansko raven izklopljene naprave ter pod »Prag zaustavitve« in pod porabo naprave v pripravljenosti po zaključku; če ni nižja od »Prag zaustavitve«, se prezre. Ko je omogočeno, nadomesti »Zamuda ponastavitve napredka« za vrnitev na »Izključeno«, tako da zaključena naprava ostane v stanju »Končano«/»čisto«, dokler ni dejansko izklopljena.", + "doc": "Po ciklu moč pod to vrednostjo med Zamudo izklopa napajanja nastavi stanje na Izključeno, namesto Zamude ponastavitve napredka. Nastavite nad porabo izklopljene naprave, pod pripravljenost in Prag zaustavitve. 0 ga izklopi (privzeto).", "label": "Prag izklopa napajanja" }, "power_sensor": { - "doc": "Entiteta senzorja, ki poroča o trenutni moči v vatih za to napravo (npr. sensor.washer_power). Zaznavanje vseh ciklov temelji na tem signalu.", + "doc": "Senzor, ki poroča trenutno moč te naprave v vatih (npr. sensor.washer_power). Vse zaznavanje temelji na njem.", "label": "Senzor moči" }, - "profile_duration_tolerance": { - "doc": "Pas +/- okoli povprečnega trajanja profila, uporabljenega med ujemanjem. 0,25 pomeni, da se 60-minutni profil ujema s 45-75-minutnimi cikli.", - "label": "Toleranca trajanja profila" - }, "profile_match_interval": { - "doc": "Kako pogosto poskušati ujemati profile med ciklom teka. Privzetih 300 s (5 minut) uravnava hitrost zaznavanja in CPE.", + "doc": "Kako pogosto se cikel, ki poteka, ujema s profili. Privzeto 300 s (5 min).", "label": "Interval ujemanja" }, "profile_match_max_duration_ratio": { - "doc": "Največja dolžina cikla glede na profil. 1.3 pomeni, da mora biti cikel krajši od 130 % trajanja profila, da se ujema.", + "doc": "Program se izloči, ko cikel traja dlje od tega večkratnika njegovega običajnega trajanja. 1,8: cikel, 80% daljši od programa, se z njim ne ujema več.", "label": "Maks. razmerje trajanja" }, "profile_match_min_duration_ratio": { - "doc": "Najmanjša dolžina cikla glede na profil. 0,9 pomeni, da mora biti cikel vsaj 90 % trajanja profila, da se ujema.", + "doc": "Program pride v poštev šele, ko cikel preteče ta delež njegovega običajnega trajanja. 0,10: 2-urni program postane kandidat po 12 minutah. Ne velja v prvih 15 minutah cikla.", "label": "Min. razmerje trajanja" }, "profile_match_threshold": { - "doc": "Najmanjši rezultat podobnosti (0-1), ki je potreben na koncu cikla za sprejem identifikacije programa. Zvišajte ga, da zmanjšate napačne identifikacije; znižajte ga, če se programi vaše naprave ne ujemajo. Privzeta vrednost 0,4 je konzervativno izhodišče.", + "doc": "Ocena ujemanja v živo, ki jo cikel potrebuje, preden ga Pametni zaključek ali zaključek zaščite pred gubami sme predčasno končati. Ne spremeni prikazanega programa. Zvišajte, če šibka ujemanja prezgodaj končajo cikle; znižajte, če jasna ujemanja čakajo zamudo izklopa.", "label": "Prag ujemanja" }, "profile_unmatch_threshold": { - "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.", + "doc": "Če ocena ujemanja v živo pade pod to vrednost, se trenutno ugibanje programa počisti. Držite malo pod Pragom ujemanja, da ga kratek padec ne ponastavi.", "label": "Prag neujemanja" }, "progress_reset_delay": { - "doc": "Ko končate, tako dolgo zadržite napredek pri 100 %, tako da je na nadzornih ploščah vidno Končano, preden se ponastavite na stanje mirovanja.", + "doc": "Po ciklu držite napredek na 100% toliko časa, preden se ponastavi na Nedejavno.", "label": "Zamuda ponastavitve napredka" }, "pump_stuck_duration": { - "doc": "Nekaj sekund lahko črpalka deluje neprekinjeno, preden je označena kot morebitna zastoj (sproži dogodek zastoja črpalke).", + "doc": "Kako dolgo sme črpalka delovati brez premora, preden se sproži dogodek zataknjene črpalke.", "label": "Trajanje zataknjenosti črpalke" }, "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.", + "doc": "Najkrajši čas med obdelanimi odčitki. Odčitki, ki pridejo prej, se preskočijo (nizki odčitki med ciklom se vedno ohranijo). Nastavite na interval poročanja vtiča ali manj; višja vrednost samo zavrže odčitke.", "label": "Interval vzorčenja" }, "save_debug_traces": { - "doc": "Shranite celotno sled moči in ustrezne podatke o odpravljanju napak za vsak cikel. Uporabno za odpravljanje težav, vendar poveča velikost pomnilnika.", + "doc": "Ob vsakem ciklu shranite tudi podatke za odpravljanje napak ujemanja (ocene vseh kandidatov), za reševanje težav. Doda približno 20 KB na cikel.", "label": "Shrani sledove za odpravljanje napak" }, "smart_termination_duration_ratio": { - "doc": "Kako daleč v pričakovanem trajanju ujemajočega programa mora biti cikel, preden ga Pametni zaključek lahko po padcu moči predčasno konča. Pričakovano trajanje je povprečje programa, zato se pri napravah, katerih čas delovanja močno niha - pralni stroji s hladno dovodno vodo pozimi v primerjavi s toplo poleti, sušilni stroji s senzorskim sušenjem, programi, odvisni od količine perila - približno polovica vseh zagonov konča prej kot to povprečje in nikoli ne dobi hitrega zaključka, temveč se konča šele prek nadomestnega časovnega zamika, nekaj minut prepozno. Pri takih strojih to vrednost znižajte (npr. 0,85), da se predčasni zaključek vseeno sproži; za bolj konservativno delovanje jo zvišajte proti 1,0. Pustite prazno za privzeto vrednost (0,98 ali 0,99 za pomivalne stroje). Cikel lahko vedno le predčasno konča, nikoli pozneje, in se nikoli ne sproži pri nejasnem ujemanju ali ujemanju z nizko zanesljivostjo.", + "doc": "Delež povprečnega trajanja programa, ki ga mora cikel doseči, preden ga Pametni zaključek sme predčasno končati. Če se časi delovanja razlikujejo (temperatura vode, sušenje s senzorjem, količina perila), ga znižajte (npr. 0,85). Pustite prazno za 0,98 (0,99 pri pomivalnih strojih).", "label": "Razmerje pametnega zaključka" }, - "smoothing_window": { - "doc": "Koliko je zglajen signal neobdelane moči. Nizka (2) je odzivna, a hrupna; visoko (5) zgladi konice, vendar doda zamik.", - "label": "Okno glajenja" - }, "start_duration_threshold": { - "doc": "Moč mora ostati nad začetnim pragom tako dolgo, da se potrdi dejanski začetek, s čimer se prepreči, da bi stikala za vklop/izklop za delčke sekunde začela cikel.", + "doc": "Moč mora toliko časa ostati nad Pragom zagona, preden se cikel začne, da se kratki vklopi in izklopi prezrejo.", "label": "Trajanje zagona" }, "start_energy_threshold": { - "doc": "Energija (moč x čas), ki jo mora naprava porabiti pred DELOVANJEM. Kratek skok visoke moči ima zelo nizko energijo in se ne upošteva, kar preprečuje napačne zagone.", + "doc": "Energija, ki jo mora naprava porabiti, preden cikel šteje kot delujoč. Kratek skok moči porabi malo energije, zato ne začne cikla.", "label": "Energija zagona" }, "start_threshold_w": { - "doc": "Za potrditev začetka cikla se mora moč dvigniti nad to raven. Nastavitev na prenizko povzroči napačne zagone iz stanja pripravljenosti; izpuščeni so previsoki in počasni zagonski programi (hladno polnjenje). Mehanizem predlogov to nastavi tik nad opazovano najnižjo delovno moč stroja.", + "doc": "Moč mora narasti nad to vrednost, da se cikel začne. Prenizko: poraba v pripravljenosti povzroča lažne zagone. Previsoko: počasi zagnani programi (hladno polnjenje) se zgrešijo.", "label": "Prag zagona" }, "stop_threshold_w": { - "doc": "Moč mora pasti pod to raven, preden se začne odštevanje zakasnitve izklopa. Nastavite ga pod začetnim pragom - vrzel med njima je pas histereze, ki preprečuje utripanje. Če je nastavljeno previsoko, faze z nizko močjo (zadrževanje izpiranja, proti mečkanju) lažno sprožijo končno zaporedje.", + "doc": "Moč mora pasti pod to vrednost, preden začne teči zamuda izklopa. Držite pod Pragom zagona. Če je previsoko, faze nizke moči (premori izpiranja, zaščita pred gubami) pomotoma sprožijo odštevanje konca.", "label": "Prag zaustavitve" }, "switch_entity": { - "doc": "Izbirno stikalo, ki se izklopi ob premoru in se ponovno vklopi ob nadaljevanju, uporablja se skupaj s \"Premor tudi prekine napajanje\".", + "doc": "Neobvezno stikalo, ki se ob premoru izklopi in ob nadaljevanju znova vklopi, skupaj s »Premor tudi prekine napajanje«.", "label": "Entiteta stikala" }, "watchdog_interval": { - "doc": "Kako pogosto nadzorni sistem v ozadju preveri zastoje senzorjev in pretečene časovne omejitve. Privzeto 30 s.", + "doc": "Kako pogosto preverjanje v ozadju išče zataknjene senzorje in pretečene časovne omejitve. Privzeto 30 s.", "label": "Interval nadzornika" }, "notify_milestone_message": { @@ -1530,127 +1523,82 @@ "label": "Sporočilo o mejniku" }, "notify_milestones": { - "doc": "Z vejicami ločena števila ciklov, ki ob dosegu sprožijo enkratno slavnostno obvestilo (npr. 50, 100, 500, 1000). Prazno onemogoči obvestila o mejnikih.", + "doc": "Števila ciklov, ki sprožijo enkratno čestitko, ločena z vejicami (npr. 50, 100, 500, 1000). Prazno jih izklopi.", "label": "Mejniki ciklov" }, "notify_quiet_end_hour": { - "doc": "Konec okna ne moti (0-23). Zadržana obvestila so dostavljena ob tej uri. Pustite prazno za onemogočenje.", + "doc": "Konec okna ne moti (0-23); zadržana obvestila se takrat pošljejo. Prazno ga izklopi.", "label": "Konec tihih ur" }, "notify_quiet_start_hour": { - "doc": "Začetek okna ne moti (0-23). Obvestila o zaključku, opomniki in obvestila o čistem perilu, ki bi se sprožila med tihimi urami, se zadržijo in dostavijo ob koncu okna. Pustite prazno za onemogočenje. Podpira okna, ki prečkajo polnoč (npr. začetek 22, konec 7).", + "doc": "Začetek okna ne moti (0-23). Obvestila o koncu, opomniki in obvestila o čistem perilu v tem oknu se zadržijo do njegovega konca. Lahko prečka polnoč (npr. 22 do 7). Prazno ga izklopi.", "label": "Začetek tihih ur" }, "peak_rate_message": { - "doc": "Neobvezno besedilo po meri za nasvet o konični tarifi, dodano k obvestilu o zagonu. Spremenljivke predloge: {device}, {price}. Prazno uporabi vgrajeno privzeto vrednost.", + "doc": "Lastno besedilo nasveta o konični tarifi. Spremenljivke predloge: {device}, {price}. Prazno: privzeto besedilo.", "label": "Sporočilo o konični tarifi" }, "peak_rate_threshold": { - "doc": "Ko se cikel začne in je trenutna cena na kWh na tej vrednosti ali nad njo, se obvestilu o zagonu doda nasvet o konični tarifi. 0 ali prazno onemogoči nasvet.", + "doc": "Če je cena za kWh ob začetku cikla enaka tej vrednosti ali višja, dodajte nasvet o konični tarifi obvestilu o začetku. 0 ali prazno ga izklopi.", "label": "Prag konične tarife (na kWh)" }, - "dtw_bandwidth": { - "label": "Toleranca ujemanja oblike", - "doc": "Koliko sme modul za ujemanje časovno raztegniti cikel, da se prilega obliki shranjenega programa. Višja vrednost dopušča več nihanja hitrosti; previsoka tvega ujemanje z napačnim programom." - }, - "corr_weight": { - "label": "Utež oblike glede na raven", - "doc": "Ravnovesje med obliko krivulje in absolutno ravnjo moči pri ocenjevanju ujemanja. Višja vrednost daje prednost obliki; nižja daje prednost skladnosti ravni moči." - }, - "duration_weight": { - "label": "Utež trajanja", - "doc": "Kako močno je prednostno izbran program, katerega tipično trajanje se ujema s tem ciklom." - }, - "energy_weight": { - "label": "Utež energije", - "doc": "Kako močno je prednostno izbran program, katerega tipična poraba energije se ujema s tem ciklom." - }, "store_brand": { "label": "Znamka naprave", - "doc": "Neobvezno. Znamka naprave, izbrana iz kataloga skupnosti. Uporablja se za iskanje in deljenje ustreznih referenčnih posnetkov. Pustite prazno, če ne uporabljate spletnih funkcij." + "doc": "Neobvezno. Znamka vaše naprave iz kataloga skupnosti, uporabljena za iskanje in deljenje referenčnih posnetkov. Pustite prazno, če ne uporabljate spletnih funkcij." }, "store_model": { "label": "Model naprave", - "doc": "Neobvezno. Model naprave, izbran iz kataloga skupnosti, ko je znamka nastavljena. Če vašega modela ni na seznamu, ga lahko dodate v katalog." + "doc": "Neobvezno. Model vaše naprave iz kataloga skupnosti (najprej izberite znamko). Če ga ni na seznamu, ga lahko dodate." }, "show_contributor": { "doc": "Prikaži ime sodelavca pri profilih, prenesenih iz skupnostne trgovine" }, - "enable_phase_matching": { - "label": "Preostali čas z upoštevanjem faz", - "doc": "Vsak tekoči cikel razdeli na faze (ogrevanje, pranje, ožemanje) in razporedi preostali čas po fazah, združen s klasično oceno - na začetku cikla se opira na proračun faz, proti koncu pa na klasično oceno. To prilagodi odštevanje temu, kako dolgo se vaša naprava dejansko segreva in deluje, kar je najbolj opazno v prvi polovici cikla. Izklopljeno = samo klasična ocena. Vpliva le na prikaz preostalega časa; ujemanje programov in zaznavanje cikla ostaneta nespremenjena." - }, - "keep_min_score": { - "label": "Min. ocena ujemanja", - "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", - "doc": "Koliko DTW ocena poravnave nadomešča osnovno oceno Stopnje 2. 0 = uporabi samo oceno Stopnje 2, 1 = uporabi samo DTW oceno, 0,5 (privzeto) = enakomerno mešanje. Zvišajte za večje zanašanje na poravnavo, ko imajo programi podobne ravni moči, a različne časovne vzorce." - }, - "dtw_ensemble_w": { - "label": "Mešanica kombinacije DTW", - "doc": "V kombiniranem načinu DTW (privzetem) je to utež skaliranega L1 v primerjavi z izvedenim DTW (DDTW). 1,0 = vse skalirano L1, 0 = vse DDTW, 0,7 = privzeto. Skalirana varianta L1 primerja ravni moči; izvedena varianta se odziva na spremembe moči skozi čas. Kombinirani način združuje oba." - }, - "dtw_ddtw_scale": { - "label": "Merilo izvedene deformacije", - "doc": "Razdalja pol-nasičenja za ocenjevanje z izvedenim DTW. Ocena se prepolovi, ko je razdalja DDTW enaka tej vrednosti. Manjša = bolj občutljiva na razlike v obliki. Privzeta vrednost 30 je umerjena za tipične sledilce moči gospodinjskih aparatov. Vpliva samo na kombinirani in ddtw način DTW." - }, - "dtw_refine_top_n": { - "label": "Stevilo natancnejših kandidatov", - "doc": "Koliko najboljših kandidatov Stopnje 2 DTW znova oceni. DTW je dražji, zato se uporablja samo za N najboljših kandidatov. Privzeto 5. Zvišajte (na 7-9), če pravi profil po Stopnji 2 včasih doseže samo 4. ali 5. mesto - DTW ga lahko reši. Znižanje rahlo pospeši ujemanje." - }, - "duration_scale": { - "label": "Ostrina trajanja", - "doc": "Ostrina kazni za neujemanje trajanja na Stopnji 4. To je logaritmično razmerje, pri katerem se ocena ujemanja prepolovi. Manjše = strožje: neujemanje trajanja bolj znižuje oceno. Privzeta vrednost 0,175 ustreza toleranci trajanja približno 18% pri pol-uteži. Kombinirajte z Utežjo trajanja." - }, - "energy_scale": { - "label": "Ostrina energije", - "doc": "Ostrina kazni za neujemanje energije na Stopnji 4. Manjše = strožje: neujemanje energije bolj znižuje oceno. Privzeta vrednost 0,25 je namerno bolj dovoljna od Ostrine trajanja, ker energija niha z obremenitvijo. Kombinirajte z Utežjo energije." - }, "power_profile_interval_min": { "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." + "doc": "Velikost odseka atributa power_profile (povprečni vati na režo), ki ga uporabljajo načrtovalniki, kot sta EMHASS in tibber_prices. Manjši ohrani kratke skoke; večji zgladi krivuljo. Privzeto 15 min. 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." + "doc": "Samo iOS. Posodabljajte napredek v živo tiho, s prioriteto, ki varčuje z baterijo in jo iOS lahko združuje. Prva posodobitev ter obvestila o začetku in koncu še vedno opozorijo. Izklopljeno: opozori vsaka posodobitev." }, "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." + "doc": "Samo Android. Obvestilo o napredku v živo ostane tudi po dotiku. Izklopljeno: dotik ga zapre." }, "notify_live_click_action": { "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." + "doc": "Kaj odpre dotik obvestila te naprave. Prazno: plošča WashData te naprave. Vpišite pot nadzorne plošče (npr. /lovelace/laundry) ali URL ali »none«, da dotik ne odpre ničesar." }, "door_opens_at_end": { "label": "Vrata se ob koncu samodejno odprejo", - "doc": "Za pomivalne stroje, ki ob koncu cikla odprejo vrata za sušenje (AirDry in podobni). Ko je vklopljeno, odprtje vrat med delujočim ciklom ne zaustavi cikla za vedno; namesto tega, ko vrata ostanejo odprta za čas čakanja spodaj, WashData zaključi cikel. Kratko odprtje (dodajanje predmeta) se prezre. Zahteva entiteto senzorja vrat." + "doc": "Za pomivalne stroje, ki ob koncu odprejo vrata za sušenje (npr. AirDry). Vrata, odprta za spodnji čas zadrževanja, cikel končajo, namesto da ga zaustavijo; kratko odprtje se prezre. Potrebuje Entiteto senzorja vrat." }, "door_end_dwell_seconds": { "label": "Čas odprtosti vrat za zaključek", - "doc": "Kako dolgo morajo vrata ostati odprta, preden WashData zaključi cikel, ko je vklopljeno \"Vrata se ob koncu samodejno odprejo\". Dovolj dolgo, da se prezre hitro dodajanje posode (privzeto 60 s), dovolj kratko za hiter zaključek, ko stroj odpre vrata." + "doc": "Ko je vklopljeno »Vrata se ob koncu samodejno odprejo«: kako dolgo morajo vrata ostati odprta, preden se cikel konča. Dovolj dolgo, da se dodajanje posode prezre (privzeto 60 s)." }, "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." + "doc": "Katere vrste ciklov oblikujejo krivuljo in ujemanje vsakega programa. Odznačitev vrste ne izbriše ničesar; uporabno, če ne zaupate uvoženim podatkom. Statistika vedno šteje samo cikle tega stroja. Odznačitev vseh se prezre." }, "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." + "doc": "Obračunajte vsak cikel po ceni v vsakem trenutku njegovega delovanja, ne po ceni ob koncu. Potrebuje entiteto cene. Obdelava zgodovine (Diagnostika) preračuna starejše cikle." }, "unload_confirm_entity": { "label": "Entiteta potrditve raztovarjanja", - "doc": "Izbirna entiteta, katere sprožitev pomeni, da je bila vsebina vzeta iz naprave, za stroje, na katere ni mogoče namestiti senzorja vrat. Deluje katera koli vrsta: gumb Zigbee ali NFC, pomočnik input_button, senzor gibanja pred strojem, scena. Kot potrditev šteje vsaka sprememba stanja, razen izklopa in prehoda v nedosegljivo stanje. Ta nastavitev omogoči opomnik za raztovarjanje tudi na napravi brez Entitete senzorja vrat." + "doc": "Neobvezno. Katera koli entiteta, ki potrdi, da je vsebina vzeta ven, za stroje brez senzorja vrat (gumb, input_button, senzor gibanja, scena). Šteje vsaka sprememba stanja razen izklopa in nedosegljivosti. Vklopi tudi opomnik za raztovarjanje." }, "unload_track_without_door": { "label": "Ročna potrditev raztovarjanja", - "doc": "Zažene opomnik za raztovarjanje na napravi brez Entitete senzorja vrat in brez Entitete potrditve raztovarjanja, raztovarjanje pa potrdite iz lastne avtomatizacije: pritisnite entiteto gumba „Označi kot raztovorjeno“ ali pokličite storitev ha_washdata.mark_unloaded. Prezrto, kadar je nastavljena Entiteta senzorja vrat, ki ta signal že zagotavlja." + "doc": "Zaženite opomnik za raztovarjanje brez senzorja vrat ali entitete potrditve. Raztovarjanje potrdite iz lastne avtomatizacije z gumbom »Označi kot raztovorjeno« ali storitvijo ha_washdata.mark_unloaded. Prezre se, ko je nastavljena Entiteta senzorja vrat." }, "notify_icon_color": { "label": "Barva obvestila", - "doc": "Poudarna barva obvestil te naprave, da so pralni stroj, sušilni stroj in pomivalni stroj razpoznavni na prvi pogled. iOS obarva ikono in vrstico napredka Live Activity na zaklenjenem zaslonu in v Dynamic Island. Android prejme enako barvo, a kaj bo z njo naredil, je odvisno od telefona: nekateri obarvajo ikono, mnogi (zlasti Samsung One UI) pa jo povsem prezrejo. Obe platformi razumeta Hex (npr. #03A9F4). Pustite prazno za privzeto barvo platforme." + "doc": "Poudarna barva obvestil te naprave, da pralni, sušilni in pomivalni stroj zlahka ločite. Na iOS zapolni krog za ikono in obarva vrstico Live Activity. Številni telefoni Android (zlasti Samsung) jo prezrejo. Uporabite hex, npr. #03A9F4. Prazno: privzeta barva platforme." + }, + "share_name": { + "doc": "Prikažite ime svojega računa kot avtorja ciklov, ki jih delite. Izklopljeno: deljeno brez imena." } }, "setting_group": { @@ -1678,9 +1626,6 @@ "door_pause": { "label": "Vrata in premor" }, - "dtw_refinement": { - "label": "DTW izpopolnjevanje" - }, "duration_gates": { "label": "Vrata trajanja" }, @@ -1757,17 +1702,20 @@ "rebuilding": "Obnavljanje…", "analyzing": "Analiziranje…", "resetting": "Ponastavljanje…", - "reverting": "Povrnitev…", "trimming": "Obrezovanje…", "splitting": "Deljenje…", + "stalled": "V zastoju", "deleting": "Brisanje…", "imported": "Uvoženo", "preparing": "Pripravljanje…", - "unlabelling": "Odstranjevanje oznake…" + "unlabelling": "Odstranjevanje oznake…", + "working": "Obdelava…", + "match_uncertain": "Negotovo: {top} ali {runner}", + "match_uncertain_single": "Negotovo: morda {top}", + "match_sure": "~{pct} % gotovosti", + "match_uncertain_tip": "WashData se še ni odločil za program. Številka pove, kako pogosto je imelo vodilno ugibanje, {top}, prav v preteklih ciklih pri takšni prednosti. Samo informativno." }, "suggestion": { - "both_agree": "WashData priporoča", - "calibrated_label": "Umerjeno", "observed_label": "Izmerjeno", "impact": { "auto_label_confidence": { @@ -1782,10 +1730,6 @@ "higher": "Zaključi samo cikle z znatno porabo energije", "lower": "Zaključi tudi krajše ali manj energijsko zahtevne cikle" }, - "end_repeat_count": { - "higher": "Zahteva več ponovitev signala konca – bolj zanesljivo, nekoliko počasneje", - "lower": "Konča po manj ponovitvah – hitrejše zaznavanje, večje tveganje lažnega konca" - }, "learning_confidence": { "higher": "Uči se le iz zelo zanesljivih ujemanj – počasneje, a zanesljiveje", "lower": "Uči se iz več ciklov; model je lahko manj natančen" @@ -1824,40 +1768,25 @@ } }, "reason": { - "auto_label_confidence": "Zaupanje 15. percentila za {cycles} samodejnih oznak, ki jih uporabnik ni nikoli popravil ({p15}).", "completion_min_seconds": "Polovica trajanja čistega cikla p05 ({minutes} min) čez {cycles} ciklov; odfiltrira lažne cikle.{excl}", - "dead_zone_batch": "Ohranjeno kratko (mediana okna nestabilnosti ob zagonu čez {cycles} ciklov = {s}s), da se resnični konec zazna nemudoma.", - "dead_zone_from_dip": "Na podlagi zgodnjega padca moči, zaznanega pri {s}s.", - "end_energy_default": "Privzeta priporočena izhodiščna vrednost za prag šuma ob koncu cikla.", - "end_energy_false": "Energija lažnih koncev p95 ({p95}Wh) čez {cycles} ciklov, z najnižjo mejo 0,2 % mediane energije cikla ({floor}Wh).", - "end_energy_no_false": "Ni lažnih koncev čez {cycles} ciklov; z najnižjo mejo 0,2 % mediane energije cikla ({floor}Wh).", - "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.", + "match_interval_floored": "Najkrajši program ({shortest}s) bi to omejil na {cap}s, pod minimum {minimum}s, zato se uporabi minimum: {persistence} zaporednih ujemanj traja {budget}s, več kot {pct}% tega programa. Ujemanje teče le ob prihodu odčitka (mediana={median}s), zato to nič ne stane.", "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.", "min_off_gap": "Na podlagi {gaps} opaženih vrzeli med cikli (p05={p05}s). Spodnja meja naprave: {floor}s.", "min_off_gap_bridge": "Nastavljeno tako, da premosti najdaljši tihi odsek znotraj cikla: {span}s + 60s rezerve, iz {spans} premoščenih vrzeli čez {cycles} čistih ciklov (ostane pod vašo najkrajšo vrzeljo med zaporednima zagonoma, ki znaša {ceiling}s).", "min_power": "40 % najnižje delovne moči p05 ({p05}W) čez {cycles} čistih ciklov, da prag izklopa ostane pod dejansko porabo.{excl}", - "ml_auto_label": "Najnižje zaupanje, ki ga je model kakovosti še ocenil kot čisto (p10 od {cycles} čistih ciklov = {p10}).", - "ml_end_repeat": "{false}/{total} ciklov ({pct}%) je imelo premor, ki ga je detektor konca ocenil z >50 %; zahtevaj {val} potrditev konca.", - "ml_off_delay": "Premori, potrjeni z detektorjem konca: p95 {p95}s + 60s rezerve, iz {pauses} z modelom preverjenih premorov čez {cycles} ciklov (spodnja meja {floor}s).", "no_update_timeout": "Na podlagi opažene frekvence posodobitev (p95={p95}s) * 20 (najmanj 60s).", "off_delay_cadence": "Na podlagi opažene frekvence posodobitev (p95={p95}s) * 5", "off_delay_device_floor": "Uporabljen varni minimum, značilen za napravo {device} ({floor}s).", "off_delay_generic_floor": "Uporabljen splošni varni minimum ({floor}s).", "off_delay_pauses": "Nastavljeno tako, da preseže resnične premore: premor znotraj cikla p95 {p95}s + 60s rezerve, iz {pauses} premorov čez {cycles} čistih ciklov (spodnja meja {floor}s).", - "profile_match_threshold": "Zaupanje p10 za {cycles} pravilnih samodejnih oznak ({p10}); varna spodnja meja za sprotno potrditev.", - "sampling_interval": "Mediana intervala posodobitev, opažena čez {cycles} čistih ciklov ({si}s).{excl}", - "smoothing_window": "Nastavljeno za glajenje ~30s meritev pri vzorčenju {si}s ({samples} vzorcev).", - "start_duration": "Ohranjeno kratko (~en interval vzorčenja {si}s), da se zaznavanje začne čim prej, hkrati pa še vedno prezre posamezne konice.", - "thr_batch": "Ohranjeno tik nad najnižjo delovno močjo p05 čez {cycles} ciklov ({p05}W), da se začetek zazna čim prej in da prag ustavitve ostane pod najnižjo delovno močjo naprave.", - "tol_per_profile": "p75 variance trajanja po posameznih profilih čez {profiles} profilov ({cycles} ciklov); dosledni profili niso kaznovani.", - "tol_pooled": "Na podlagi združene variance trajanja {cycles} nedavnih označenih ciklov (p95 odklon={dev}).", - "watchdog": "Ohranjeno tako nizko, kot je varno (tik nad vrzeljo posodobitve p95 {p95}s in vsaj 2× interval vzorčenja {median}s, najmanj 30s), da se zastoji hitro zaznajo brez lažnih ustavitev." + "standby_floor": "Vsak nedavni cikel je ob koncu še vedno porabil {idle}W, zato prag pod to vrednostjo ne more zaznati izklopa naprave. Ohranjen je nad njo; {cycles} čistih ciklov v evidenci ni nikoli padlo pod njega za več kot {pause}s.", + "thr_batch": "Tik nad najnižjo delovno močjo (p05) čez {cycles} ciklov ({p05}W), da se zagoni zaznajo zgodaj, prag zaustavitve pa ostane pod najnižjo delovno močjo.", + "watchdog": "Ohranjeno tako nizko, kot je varno (tik nad vrzeljo posodobitve p95 {p95}s in vsaj 2× interval vzorčenja {median}s, najmanj 30s), da se zastoji hitro zaznajo brez lažnih ustavitev.", + "end_energy_floor": "Zvišano na minimum, ki izhaja iz praga zaustavitve ({stop} W) v času zamude izklopa ({off_delay} s); pod njim se cikel lahko konča le po nadomestni poti.", + "max_duration_ratio_default": "Vrnjeno na privzeto vrednost: nižja zgornja meja zavrne pravi program vsakič, ko cikel traja dlje kot običajno.", + "min_duration_ratio_default": "Vrnjeno na privzeto vrednost: višja spodnja meja prepreči prepoznavanje cikla, ki teče, dokler ne napreduje precej daleč." }, "exclusions": { "summary": "Izključeno {total} napačno zaznanih ciklov: {parts}.", @@ -1875,7 +1804,8 @@ "no_power": "brez moči", "no_active_power": "brez aktivne moči" } - } + }, + "now_value": "(trenutno {v})" }, "tab": { "advanced": "Napredno", @@ -1906,13 +1836,17 @@ }, "toast": { "catalog_refreshed": "Katalog skupnosti je osvežen", + "task_name_required": "Poimenujte nalogo", + "task_added": "Naloga dodana", + "task_save_failed": "Naloge ni bilo mogoče shraniti: {error}", + "task_removed": "Naloga odstranjena", + "maint_logged": "Označeno kot opravljeno: {task}", "access_saved": "Nadzor dostopa shranjen", "all_wiped": "Vsi podatki izbrisani", "analysis_complete_none": "Analiza zaključena: ni novih predlogov", "auto_label_complete": "Samodejno označevanje zaključeno", "automation_deleted": "Avtomatizacija izbrisana", "correction_submitted": "Popravek poslan", - "cycle_deleted": "Cikel izbrisan", "cycle_force_stopped": "Cikel prisilno ustavljen", "cycle_labelled": "Cikel označen", "cycle_paused": "Cikel v premoru", @@ -1928,11 +1862,9 @@ "group_deleted": "Skupina izbrisana", "group_name_required": "Ime skupine je obvezno", "group_saved": "Skupina shranjena", - "import_successful": "Uvoz uspešen; integracija se znova nalaga", "json_required": "Podatki JSON so obvezni", "legacy_removed": "Zastarela dejanja odstranjena", "logs_exported": "Dnevniki izvoženi", - "matching_reverted": "Uteži ujemanja vrnjene na privzete vrednosti", "min_2_profiles": "Izberite vsaj 2 profila za skupino", "ml_training_no_improvement": "Usposabljanje zaključeno: ohranjen osnovni model (brez izboljšav)", "models_reverted": "Modeli naprave vrnjeni na osnovno", @@ -1942,8 +1874,8 @@ "pg_preset_loaded": "Prednastavitev \"{name}\" naložena", "pg_preset_saved": "Prednastavitev \"{name}\" shranjena", "pg_sugg_loaded": "Naloženo {n} predlaganih vrednosti - zaženite peskovnik za primerjavo", - "pg_sugg_ml_loaded": "Naloženo {n} ML-umerjenih vrednosti - zaženite peskovnik za primerjavo", "phase_name_required": "Ime faze je obvezno", + "phase_reset": "Faza \"{name}\" obnovljena na vgrajeno različico", "phase_updated": "Faza posodobljena", "phases_saved": "Faze shranjene", "preferences_saved": "Nastavitve shranjene", @@ -1990,15 +1922,11 @@ "store_name_required": "Vnesite ime profila", "store_import_failed": "Uvoz ni uspel: {error}", "store_imported": "Uvoženo v {profile}", - "store_program_required": "Vnesite ime programa", "store_no_appliance": "Najprej nastavite znamko in model naprave v Nastavitvah.", "store_share_failed": "Deljenje ni uspelo: {error}", "store_shared": "Deljeno v skupnostno trgovino, čaka na pregled.", "contrib_device_changed": "Dodano v skupnostno trgovino, vendar se je izbrana naprava spremenila, medtem ko je bil obrazec odprt. Izberite jo v Nastavitvah.", "appliance_added": "Naprava dodana, čaka na odobritev", - "thanks_confirming": "Hvala za potrditev", - "device_approved": "Odobreno s strani skupnosti", - "rating_saved": "Ocena kakovosti shranjena", "brand_added": "Znamka dodana, čaka na odobritev", "profile_added": "Profil dodan, čaka na odobritev", "saved_except_conflicts": "Shranjeno. Popravite označene konflikte, da shranite še ostalo.", @@ -2021,7 +1949,52 @@ "odometer_saved": "Skupno število ciklov shranjeno", "odometer_save_failed": "Skupnega števila ni bilo mogoče shraniti: {error}", "store_pick_profile_share": "Izberite profil za deljenje", - "remove_failed": "Odstranjevanje ni uspelo: {error}" + "remove_failed": "Odstranjevanje ni uspelo: {error}", + "analysis_complete": "Analiza zaključena: predlogi: {count}", + "analysis_failed": "Analiza ni uspela: {error}", + "auto_detect_failed": "Samodejno zaznavanje ni uspelo: {error}", + "auto_label_failed": "Samodejno označevanje ni uspelo: {error}", + "could_not_load_cycle": "Cikla ni bilo mogoče naložiti: {error}", + "create_failed": "Ustvarjanje ni uspelo: {error}", + "cycles_deleted": "Izbrisani cikli: {count}", + "debug_cleared": "Počiščena sledenja za odpravljanje napak: {count}", + "discard_failed": "Zavrženje ni uspelo: {error}", + "error": "Napaka: {error}", + "export_failed": "Izvoz ni uspel: {error}", + "import_failed": "Uvoz ni uspel: {error}", + "invalid_json": "\"{key}\" ni veljaven JSON - popravite ga ali počistite polje pred shranjevanjem.", + "label_failed": "Označevanje ni uspelo: {error}", + "ml_training_promoted": "Učenje zaključeno: aktivirani modeli: {count}", + "pause_failed": "Začasna ustavitev ni uspela: {error}", + "phase_created": "Faza \"{name}\" ustvarjena", + "phase_deleted": "Faza \"{name}\" izbrisana", + "profile_created": "Profil \"{name}\" ustvarjen", + "rename_failed": "Preimenovanje ni uspelo: {error}", + "save_failed": "Shranjevanje ni uspelo: {error}", + "split_complete": "Razdeljeno na cikle: {count}", + "start_failed": "Zagon ni uspel: {error}", + "stop_failed": "Ustavitev ni uspela: {error}", + "update_failed": "Posodobitev ni uspela: {error}", + "store_download_skipped": "Preskočeni posnetki: {n} (prekratki, s preveč vrzelmi ali že na vaši napravi).", + "store_import_low_quality": "Ta posnetek je prekratek, ima preveč vrzeli ali je neverodostojen, zato ga ni mogoče uvoziti.", + "store_import_duplicate": "Ta posnetek je že na vaši napravi.", + "device_approved": "Odobreno s strani skupnosti", + "thanks_confirming": "Hvala za potrditev", + "rating_saved": "Ocena kakovosti shranjena", + "store_download_partial": "Programov ni bilo mogoče prenesti: {n}; poskusite znova pozneje.", + "import_no_restore_point": "Uvoženo, vendar obnovitvene točke ni bilo mogoče shraniti, zato tega uvoza ni mogoče razveljaviti", + "import_undo_failed": "Uvoza ni bilo mogoče razveljaviti: {error}", + "import_undone": "Uvoz razveljavljen: obnovljenih {profiles} profilov in {cycles} ciklov", + "auto_label_cancelled": "Samodejno označevanje preklicano po označitvi {count} ciklov.", + "auto_label_cancelled_one": "Samodejno označevanje preklicano po označitvi {count} cikla.", + "auto_label_cancelled_two": "Samodejno označevanje preklicano po označitvi {count} ciklov.", + "auto_label_cancelled_few": "Samodejno označevanje preklicano po označitvi {count} ciklov.", + "auto_label_cancelled_other": "Samodejno označevanje preklicano po označitvi {count} ciklov.", + "auto_label_done": "Samodejno označevanje dokončano: označenih je {count} ciklov, brez oznake: {skipped}.", + "auto_label_done_one": "Samodejno označevanje dokončano: označen je {count} cikel, brez oznake: {skipped}.", + "auto_label_done_two": "Samodejno označevanje dokončano: označena sta {count} cikla, brez oznake: {skipped}.", + "auto_label_done_few": "Samodejno označevanje dokončano: označeni so {count} cikli, brez oznake: {skipped}.", + "auto_label_done_other": "Samodejno označevanje dokončano: označenih je {count} ciklov, brez oznake: {skipped}." }, "trend": { "down": "Padajoči trend", @@ -2029,6 +2002,10 @@ }, "maint": { "bearing_service": "Servis ležajev", + "salt": "Dopolni sol", + "rinse_aid": "Dopolni sredstvo za izpiranje", + "lint_filter": "Očisti filter za vlakna", + "condenser_clean": "Očisti kondenzator", "descale": "Odstranjevanje vodnega kamna", "drum_clean": "Očisti boben", "filter_clean": "Očisti filter", @@ -2036,34 +2013,22 @@ }, "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", + "smart_termination_duration_ratio": "Delež povprečnega trajanja programa, ki ga mora cikel doseči, preden ga Pametni zaključek sme predčasno končati; znižajte, če se časi delovanja razlikujejo", + "anti_crease_finalize_ratio": "Delež povprečnega trajanja programa, ki ga mora cikel doseči, preden ga rep zaščite pred gubami sme zaključiti; znižajte pri sušilnih strojih, pri pralnih pustite privzeto", "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_exit_power": "Raven mirovanja med impulzi vrtenja; učinkuje le nad Pragom zaustavitve", "anti_wrinkle_idle_timeout": "Dovoljeni mir med dvema vrtilnima impulzoma, preden se zaščita pred gubami konča", "anti_wrinkle_max_duration": "Daljši impulz konča zaščito pred gubami in odpre nov cikel", "anti_wrinkle_max_power": "Impulz nad to vrednostjo konča zaščito pred gubami in odpre nov cikel", "completion_min_seconds": "Najkrajši zagon, ki šteje kot pravi cikel", - "end_repeat_count": "Zaporedni nizki odčitki pred koncem", "interrupted_min_seconds": "Kratki cikli, označeni kot prekinjeni", "min_off_gap": "Presledek, potreben za ločitev dveh ciklov", "off_delay": "Sekunde nizke moči pred koncem cikla", "start_duration_threshold": "Sekunde nad pragom za potrditev zagona", "start_threshold_w": "Najmanjša moč v vatih, da šteje kot zagnano", "stop_threshold_w": "Pod to vrednostjo naprava šteje kot izklopljena", - "dtw_bandwidth": "Stopnja 3: deformacijski pas Sakoe-Chiba (0 = DTW izklopljen; privzeto 0,2 = 20% dolžine cikla)", - "corr_weight": "Stopnja 2: ravnovesje med obliko krivulje (korelacija) in ravnijo moči (MAE); privzeto 0,45", - "duration_weight": "Stopnja 4: kako močno ujemanje trajanja vpliva na končno oceno (privzeto 0,22)", - "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,8 (180%)", - "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)", + "profile_match_min_duration_ratio": "Najmanjša dolžina cikla kot delež trajanja profila; privzeto 0,1 (10%)", + "profile_match_max_duration_ratio": "Največja dolžina cikla kot delež trajanja profila; privzeto 1,8 (180%)", "curve_preroll_seconds": "Sekunde odčitkov iz prekinjenih poskusov zagona, prenesene na začetek krivulje cikla; 0 = izklopljeno" }, "col": { @@ -2071,12 +2036,14 @@ "status_tip": "Izid cikla: Dokončano (naravni konec), Prekinjeno (nenaden padec moči), Prisilno ustavljeno (ročno) ali Potrebuje pregled (čaka na povratne informacije).", "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. 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.", + "energy_tip": "Skupna porabljena energija (kWh), izračunana iz moči skozi čas.", + "cost_tip": "Strošek energije tega cikla, določen ob koncu. Z vklopljenimi Časovno tehtanimi stroški se vsak trenutek obračuna po svoji ceni; sicer po ceni ob koncu. Nastavite ceno v Nastavitvah, da se to 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", - "cost_dynamic_tip": "Časovno tehtano: obračunano po ceni, ki je veljala med ciklom (efektivno {price} {cur}/kWh)." + "cost_dynamic_tip": "Časovno tehtano: obračunano po ceni, ki je veljala med ciklom (efektivno {price} {cur}/kWh).", + "dbg_conf": "Zaup.", + "dbg_mae": "MAE", + "dbg_corr": "Kor." }, "stat": { "avg": "povpr.", @@ -2100,7 +2067,10 @@ "notify_finish": "Poslano bi bilo obvestilo o zaključku.", "notify_milestone": "Poslano bi bilo obvestilo o mejniku.", "notify_held": "Obvestilo je bilo zadržano zaradi tihih ur.", - "finished": "Cikel je dosegel končno stanje in se je končal." + "finished": "Cikel je dosegel končno stanje in se je končal.", + "match_reverted": "Program se je vrnil v zaznavanje: njegov rezultat je padel precej pod njegov lastni vrh.", + "verified_pause": "Sprotna poravnava je potrdila (ali sprostila) premor znotraj programa. Dokler premor velja, se cikel ne konča.", + "stalled": "Cikel je ostal na porabi v pripravljenosti dlje, kot se ta program tam kdaj ustavi, delo pa še ni končano. Prikazano kot Zaustavljeno (V zastoju); cikel ostane odprt." }, "store": { "search_ph2": "Iskanje po znamki ali modelu…", @@ -2109,11 +2079,7 @@ "programs_count": "Programi: {n}", "browse": "Brskaj", "device": "Naprava", - "favorites": "Priljubljeni", - "search_ph": "Iskanje po znamki ali modelu…", "no_results": "Ni najdenih ustreznih naprav. Poskusite z drugim iskanjem.", - "n_programs": "{n} programov", - "n_cycles": "{n} ciklov", "no_programs": "Za to napravo še ni deljenih programov.", "no_cycles": "Za ta program še ni deljenih referenčnih ciklov.", "anon": "anonimno", @@ -2125,19 +2091,21 @@ "merge_target": "Združi v profil", "description": "Opis (neobvezno)", "contributed_by": "od {name}", + "website": "Spletna stran trgovine ↗", + "website_tip": "Odpri spletno stran skupnostne trgovine", + "favorites": "Priljubljeni", "rating_summary": "★ {avg} ({n})", "no_ratings": "Še ni ocen", - "website": "Spletna stran trgovine ↗", - "website_tip": "Odpri spletno stran skupnostne trgovine" + "unreachable": "Povezava s skupnostno trgovino ni uspela. Preverite povezavo in poskusite znova." }, "settings": { "gear": { - "title": "Nastavitve" + "title": "Prednostne nastavitve" } }, "link": { - "manual": "Priročnik ↗", - "add_appliance": "Dodajte svojo napravo" + "maintenance_supporter": "Maintenance Supporter na GitHubu ↗", + "manual": "Priročnik ↗" }, "tip": { "add_brand": "Znamke ni na seznamu? Dodajte jo v katalog skupnosti", @@ -2171,7 +2139,13 @@ "rebuild": { "envelopes": "Ponovni izračun ovojnic" }, - "cancelling": "Prekinjam..." + "cancelling": "Prekinjam...", + "auto_label": { + "running": "Samodejno označevanje ciklov" + }, + "store_download": { + "running": "Prenašanje iz trgovine" + } }, "setup": { "hdr": { @@ -2180,26 +2154,24 @@ }, "phase0": { "washer": "WashData že zazna vaše cikle. Posnemite prvi cikel, da omogočite imena programov in ocene časa.", - "dishwasher": "WashData opazuje. Pomivalni stroji imajo zapletene cikle – snemanje prvega cikla je izrazito priporočeno. Če zaznani cikel teče predolgo, ga obrežite v urejevalniku ciklov, preden ga shranite kot profil.", + "dishwasher": "WashData opazuje. Cikli pomivalnega stroja so zapleteni, zato posnemite prvi cikel. Če zaznani cikel traja predolgo, ga obrežite v urejevalniku cikla, preden ga shranite kot profil.", "generic": "WashData opazuje. Posnemite ali označite zaznani cikel in začnite graditi profile." }, "phase1a": { - "labelled": "Dober začetek – vaš prvi program je shranjen. Za najčistejše podatke razmislite o snemanju naslednjega cikla s pripomočkom za snemanje." + "labelled": "Dober začetek: vaš prvi program je shranjen. Za najčistejše podatke posnemite naslednji cikel s snemalnikom." }, "phase1b": { - "recorded": "Vaš posnetek je bil shranjen kot {profile_name}. Zdaj posnemite ali označite ostale pogoste programe, da razširite pokritost." + "recorded": "Vaš posnetek je shranjen kot {profile_name}. Zdaj posnemite ali označite druge pogoste programe." }, "phase1c": { - "verify": "Imate {count} programov od skupnosti. Zaženite cikel, da preverite, ali jih WashData pravilno prepozna – ujemanje se bo izboljšalo, ko naprava gradi lastno zgodovino." + "verify": "Imate programe skupnosti ({count}). Zaženite cikel in preverite, ali ga WashData prepozna; ujemanje se izboljšuje, ko naprava gradi lastno zgodovino." }, "phase2": { - "cluster": "WashData je videl {count} ciklov, ki ne ustrezajo nobenemu shranjenemu programu – med seboj so si podobni. Ali želite zanje ustvariti nov profil?", + "cluster": "WashData je opazil podobne cikle ({count}), ki ne ustrezajo nobenemu shranjenemu programu. Želite zanje ustvariti nov profil?", "unmatched": "Vaš zadnji cikel ni ustrezal nobenemu shranjenemu programu. Je to nov program?" }, "phase3": { - "suggestions": "WashData ima priporočila za nastavitve na podlagi vaše zgodovine ciklov – preglejte jih za izboljšanje natančnosti zaznavanja.", - "groups": "Nekateri vaši profili izgledajo kot isti program pri različnih temperaturah. Organizirajte jih v skupino za boljše ujemanje.", - "phases": "Dodajte programske faze k {profile_name} za natančnejše ocene preostalega časa." + "suggestions": "WashData ima predloge nastavitev iz zgodovine vaših ciklov. Preglejte jih za boljše zaznavanje." }, "phase4": { "healthy": "Ta naprava je v celoti nastavljena ({profile_count} profilov)." @@ -2212,12 +2184,58 @@ "create_from_cluster": "Ustvari profil iz teh ciklov", "create_profile": "Ustvari zanj profil", "review_suggestions": "Preglej predloge", - "organise_profiles": "Organiziraj profile", - "configure_phases": "Nastavi faze", "skip_step": "Preskoči ta korak", "skip_forever": "Ne prikaži znova", "hide_guidance": "Skrij navodila", "show_guidance": "Prikaži navodila" } + }, + "access": { + "edit": "Urejanje", + "full": "Polno", + "inherit": "Podeduj", + "none": "Brez (skrito)", + "read": "Branje" + }, + "pg_label_reason": { + "no_winner": "Noben program ne ustreza", + "below_floor": "Pod pragom \"Zaupanje učenja\"", + "ambiguous": "Označeno kot dvoumno", + "margin": "Preblizu drugemu najboljšemu programu", + "unknown_profile": "Program ne obstaja več", + "not_evaluated": "Ni ovrednoteno" + }, + "diagram": { + "accum": "akum.", + "auto": "samodejno", + "below_off": "pod = izkl.", + "confirmed": "potrjeno", + "energy_counts": "energija šteje", + "feedback": "povratna inf.", + "gap_below_min": "presledek pod min. = en cikel", + "heat_phase": "faza gretja", + "held_at_100": "ostane na 100 %", + "ignored": "prezrto", + "kept": "ohranjeno", + "max": "maks.", + "min": "min.", + "minus_tol": "-tol", + "no_match": "ni ujemanja", + "no_updates": "ni posodobitev", + "off_delay_wait": "zamuda izklopa", + "off_gap": "presledek izkl.", + "plus_tol": "+tol", + "profile": "profil", + "reading_interval": "tipični interval odčitkov", + "sensor_offline": "senzor brez povezave: prisilna ustavitev", + "si": "IV", + "spike_ignored": "skok prezrt", + "spike_ignored_short": "skok: prezrt", + "start": "zagon", + "stop": "ustavitev", + "tail_energy_resets": "energija repa nad pragom: odštevanje se ponastavi", + "thr": "prag", + "too_short": "prekratek", + "tumble_kept": "vrtilni impulzi ohranjeni" } } diff --git a/custom_components/ha_washdata/translations/panel/sq.json b/custom_components/ha_washdata/translations/panel/sq.json index 94a69e88..ea05b686 100644 --- a/custom_components/ha_washdata/translations/panel/sq.json +++ b/custom_components/ha_washdata/translations/panel/sq.json @@ -3,6 +3,7 @@ "min": "min", "entity_not_found": "Entiteti nuk u gjet", "running_long": "Po zgjat", + "stalled": "I ngecur", "remaining": "mbetur", "progress": "përparimi", "show_time_remaining": "Shfaq kohën e mbetur", @@ -48,16 +49,16 @@ } }, "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_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. Vërejtur në {seen} nga {measured} cikle të matura.", "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", + "artifact_tip": "U zbuluan {n} anomali (p.sh. dera e hapur në mes të ciklit) - hapeni për t'i parë në grafik", + "artifact_tip_one": "U zbulua {n} anomali (p.sh. dera e hapur në mes të ciklit) - hapeni për ta parë në grafik", + "artifact_tip_other": "U zbuluan {n} anomali (p.sh. dera e hapur në mes të ciklit) - hapeni për t'i parë në grafik", "auto": "(zbuluar automatikisht)", "built_in_tag": "i integruar", "declining": "↘ në rënie", "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 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", @@ -65,28 +66,37 @@ "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 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ë", + "restart_gap_tip": "{n} boshllëqe rinisje të HA: gjurma e energjisë ka vrima", + "restart_gap_tip_one": "{n} boshllëk rinisje të HA: gjurma e energjisë ka një vrimë", + "restart_gap_tip_other": "{n} boshllëqe rinisje të HA: gjurma e energjisë ka vrima", "reviewed": "Shqyrtuar", "steady": "→ i qëndrueshëm", "underrun": "Përfundoi më shpejt se zakonisht", - "using_defaults": "Duke përdorur vlerat e parazgjedhura", - "using_tuned": "Duke përdorur peshat e rregulluara", "overrun_ratio": "({x}x e pritur)", "underrun_ratio": "({pct}% e pritur)", - "awaiting_tip": "Në pritje të miratimit nga komuniteti", - "awaiting_n": "Në pritje të miratimit · {n} të konfirmuara", - "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.", "armed": "(zbatohet për ciklin tjetër)", - "duration_outlier": "{n} cikël(e) me kohëzgjatje të pazakontë" + "duration_outlier": "{n} cikël(e) me kohëzgjatje të pazakontë", + "awaiting_tip": "Në pritje të miratimit nga komuniteti", + "awaiting_n": "Në pritje të miratimit · {n} të konfirmuara", + "awaiting": "Në pritje të miratimit", + "approved": "Miratuar", + "matcher_unused": "Ende nuk është përputhur me asnjë nga ciklet tuaja", + "matcher_used": "U përputh me {n} nga ciklet tuaja", + "matcher_used_one": "U përputh me {n} nga ciklet tuaja", + "matcher_used_other": "U përputh me {n} nga ciklet tuaja", + "matcher_used_tip": "Cikle në këtë pajisje që WashData i përputhi vetë me këtë program të importuar. Një import që nuk përputhet kurrë mund të mos i përshtatet pajisjes suaj; mund ta hiqni. Numërohet në vend; asgjë nuk dërgohet në dyqan." }, "btn": { + "pg_preset_delete": "Fshi konfigurimin", + "pg_preset_load": "Ngarko konfigurimin", + "pg_preset_load_tip": "Zëvendëso vlerat e zonës së provave me këtë konfigurim (cilësimet aktive nuk preken)", + "pg_preset_save": "Ruaj si konfigurim", + "pg_preset_save_tip": "Ruaj të gjitha vlerat më poshtë si një konfigurim me emër për këtë pajisje", "reset_zoom": "Rivendos zmadhimin", "set_brand_model": "Cakto markën dhe modelin", "refresh_catalog": "Rifresko katalogun", @@ -97,12 +107,11 @@ "add_timer": "+ Shto kohëmatës", "apply_all": "Apliko të gjitha", "apply_label": "Apliko etiketën", - "apply_set_b": "Apliko grupin B", "apply_split": "Apliko ndarjen", "apply_trim": "Apliko prerjen", "auto_detect_split": "Zbulo automatikisht", "auto_label_cycles": "Etiketim automatik i cikleve", - "auto_label_cycles_tip": "Cakto automatikisht emra profilesh cikleve të paetiketuara, besueshmëria e përputhjes e të cilave tejkalon pragun", + "auto_label_cycles_tip": "Emërto ciklet e paetiketuara kur besueshmëria e përputhjes tejkalon pragun.", "cancel": "Anulo", "clear_debug": "Pastro të dhënat e korrigjimit", "clear_splits": "Pastro", @@ -112,8 +121,6 @@ "convert_to_automation": "Konverto në automatizim", "correct": "Korrigjo…", "create": "Krijo", - "create_from_cluster": "Krijo profil nga {count} cikle", - "create_profile": "+ Krijo profil", "delete": "Fshi", "delete_group": "Fshi grupin", "delete_group_tip": "Fshije vetëm këtë grup - profilet e anëtarëve mbahen", @@ -124,22 +131,19 @@ "done": "Gatë", "edit": "Modifiko", "export": "Eksporto", - "export_json": "Eksporto në JSON", "force_stop": "Ndalim i detyruar", "force_stop_tip": "Përfundo menjëherë ciklin aktual dhe shënoje si të ndaluar me forcë", "from_template": "Nga shablloni ▾", "ignore": "Injorojë", "import_json": "Importo nga JSON", - "import_overwrite": "Importo (mbishkruan të dhënat)", "inspect": "Inspekto", "label": "Etiketoje", - "load": "Ngarko", "load_more": "Ngarko më shumë", "manage": "Menaxho", "merge": "Bashko", "new_automation": "+ Automatizim i ri", "new_group": "+ Grup i ri", - "new_group_tip": "Grupo profilet pothuajse identike (e njëjta formë/kohëzgjatje, temperaturë ose centrifugim i ndryshëm) kështu që përputhja zgjedh me besueshmëri ndërmjet tyre", + "new_group_tip": "Grupo profilet pothuajse identike (e njëjta formë, temperaturë ose centrifugim i ndryshëm), që përputhja t'i dallojë.", "new_phase": "+ Fazë e re", "new_profile": "+ Profil i ri", "new_profile_tip": "Krijo një profil të ri programi nga një cikël ekzistues i etiketuar ose regjistrim", @@ -147,36 +151,25 @@ "on_cycle_started": "Kur cikli fillon", "pause_cycle": "Ndalo", "pause_cycle_tip": "Ndalo ciklin aktiv – pajisja do të vazhdojë nga aty ku kishte mbetur", - "pg_apply_device": "Apliko te pajisja", - "pg_autofill": "Mbushje automatike", - "pg_load_calibrated": "Ngarko të kalibruar (ML) ({n})", - "pg_load_calibrated_tip": "Ngarkon cilësimet e kalibruara nga ML si anashkalime të zonës së provave", "pg_load_live": "Ngarko cilësimet aktive", "pg_load_live_tip": "Rilexo cilësimet aktuale të integrimit dhe hidh poshtë çdo ndryshim në zonën e provave", "pg_load_suggested": "Ngarko sugjerime ({n})", "pg_load_suggested_tip": "Ngarkon sugjerime aktuale të sintonizuesit automatik si anashkalime të zonës së provave", - "pg_preset_delete": "Fshi konfigurimin", - "pg_preset_load": "Ngarko konfigurimin", - "pg_preset_load_tip": "Zëvendëso vlerat e zonës së provave me këtë konfigurim (cilësimet aktive nuk preken)", - "pg_preset_save": "Ruaj si konfigurim", - "pg_preset_save_tip": "Ruaj të gjitha vlerat më poshtë si një konfigurim me emër për këtë pajisje", "pg_publish_all": "Apliko {n} te integrimi", "pg_publish_one": "Apliko {label} te integrimi", - "play": "Luaj", "process": "Proceso", "process_history": "Proceso tani", "process_recording": "Ruaj regjistrimin", "process_tip": "Ruaj gjurmën e regjistruar si profil të ri ose ekzistues", "rebuild": "Rindërto zarfet", "rebuild_envelope": "Rindërto zarfin", - "rebuild_tip": "Rillogaris zarfin e pritshmë të energjisë (brezin min/maks) për të gjitha profilet nga ciklet e tyre të etiketuara – ekzekuto pas etiketimit të cikleve të reja ose korrigjimit të tjerave", + "rebuild_tip": "Rindërto brezin e pritshëm të fuqisë për çdo profil nga ciklet e tij të etiketuara. Ekzekutoje pas etiketimit ose korrigjimit të cikleve.", "rec_start_tip": "Fillo regjistrimin e gjurmës së energjisë të pajisjes – fillo pak para ekzekutimit të një cikli", "rec_stop": "Ndalo", "rec_stop_tip": "Ndalo regjistrimin dhe mbaj gjurmën e kapur për shqyrtim", "record": "Fillo regjistrimin", "refresh": "Rifresko", "refresh_diag_tip": "Ringarko statistikat e ruajtjes", - "refresh_logs_tip": "Ringarko regjistrimet më të fundit të regjistrit", "refresh_settings_tip": "Ringarko cilësimet nga serveri", "relabel": "Rietiketo ({count})", "remove": "Hiq", @@ -184,7 +177,6 @@ "reset": "Rivendos", "reset_ml_models_tip": "Hidhni modelet e rregulluara mirë dhe kthehuni te ato të integruara. WashData mund t'i rimësojë ato më vonë.", "reset_to_builtin": "Rivendos te modelet e integruara", - "reset_to_defaults": "Rivendos vlerat e parazgjedhura", "resume_cycle": "Vazhdo ciklin", "resume_cycle_tip": "Vazhdo ciklin e ndërprerë", "revert_settings": "Hiq ndryshimet", @@ -197,8 +189,6 @@ "run_analysis": "🔍 Ekzekuto analizën e sugjerimeve", "run_analysis_tip": "Analizoni ciklet tuaja të regjistruara tani dhe rifresko sugjerimet e akordimit", "run_auto_label": "Ekzekuto etiketimin automatik", - "run_both": "Ekzekuto A kundër B", - "run_simulation": "Ekzekuto simulimin", "save": "Ruaj", "save_access_control": "Ruaj kontrollin e aksesit", "save_group": "Ruaj grupin", @@ -210,10 +200,8 @@ "save_settings": "Ruaj cilësimet", "select": "Zgjidh", "select_all": "Zgjidh të gjitha", - "select_last20": "20 e fundit", "select_none": "Pastro", "show_only": "Shfaq vetëm", - "skip_setup": "Anashkalo konfigurimin", "split": "Ndaj", "stop": "Ndalo", "submit_correction": "Dërgo korrigjimin", @@ -222,36 +210,31 @@ "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.", + "unlabel_selected_tip": "Heq etiketën nga ciklet e zgjedhura. Ato mbeten në historik, por 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})", - "group_suggest": "Grupi {n}: {members}", "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", "import": "Importo", "disconnect": "Shkëput", "connect_github": "Lidh GitHub", - "save_appliance": "Ruaj pajisjen", "share_to_store": "Ndaj në dyqan", "share": "Ndaj", - "confirm_appliance": "Konfirmo këtë pajisje", - "confirmed": "Konfirmuar", "browse_community_setups": "Shfletoni konfigurimet e komunitetit", "download_device": "Shkarko këtë konfigurim", "share_device": "⬆ Ndaj këtë pajisje", - "share_device_tip": "Ndani këtë pajisje dhe ciklet e saj të referencës të regjistruara në dyqanin e komunitetit, që të tjerët me të njëjtën makinë t'i adoptojnë", + "share_device_tip": "Ndani këtë pajisje dhe ciklet e saj të referencës në dyqanin e komunitetit, që pronarët e së njëjtës makinë t'i adoptojnë.", "share_n": "Ndaj {n} cikël(-ë)", "export_selected": "Eksporto (zgjidh të dhënat)", "export_all": "Eksportim i shpejtë i gjithçkaje", - "import_raw": "I avancuar: zëvendëso gjithçka nga JSON", "download_export": "Shkarko eksportin", "analyze_import": "Analizo skedarin", "import_selected": "Importo të zgjedhurat", "back": "Prapa", "mute_suggestion": "Mos e sugjeroni këtë cilësim", "reset_muted": "Rivendos të heshturat", + "reset_phase_tip": "Zhbëj ndryshimin tënd dhe rikthe fazën e integruar", "import_power_history": "Importo historikun e fuqisë", "hist_read_recorder": "Lexo nga Home Assistant", "hist_scan": "Kërko cikle", @@ -260,13 +243,17 @@ "save_total_cycles": "Ruaj totalin", "clear": "Pastro", "add_brand": "Shto markë", - "add_appliance": "Shto pajisje" + "log_done_task": "Shëno si të kryer: {task}", + "add_task": "Shto detyrë", + "add_appliance": "Shto pajisje", + "retry": "Riprovo", + "confirm_appliance": "Konfirmo këtë pajisje", + "confirmed": "Konfirmuar", + "create_profile_from_gap": "Krijo profilin", + "undo_import": "Zhbëj importimin e fundit", + "pg_rerun": "Përditëso simulimin" }, "conflict": { - "anti_wrinkle_exit": { - "exit": "Duhet të jetë nën Pragun e Ndalimit ({stop} W); përndryshe fuqia e daljes kundër rrudhave injorohet", - "stop": "Duhet të jetë mbi Fuqinë e Daljes Kundër Rrudhave ({exit} W)" - }, "anti_wrinkle_max": { "max": "Duhet të jetë mbi Pragun e Nisjes ({start} W); përndryshe kufiri i kohëzgjatjes kundër rrudhave anashkalohet", "start": "Duhet të jetë nën Fuqinë Maks. Kundër Rrudhave ({max} W)" @@ -304,14 +291,14 @@ "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." + "power_sensor": "Ky është Sensori i fuqisë i pajisjes, jo një çmim. Kostoja e cikleve do të llogaritej me vatët si çmim për kWh; pastroni këtë fushë.", + "energy_sensor": "Ky është matësi i energjisë i pajisjes, jo një çmim. Kostoja e cikleve do të llogaritej me leximin e matësit si çmim për kWh; pastroni këtë fushë.", + "device_class": "Ky sensor mat {dc}, jo një çmim për kWh. Pastroni këtë fushë ose zgjidhni një sensor tarife.", + "unit": "Ky sensor lexon në {unit}, jo një çmim për kWh. 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ë" + "duration": "Duhet të jetë më e vogël se Koha e skadimit pa përditësim ({to} s), që alarmi i bllokimit të ndizet i pari", + "timeout": "Duhet të jetë më e madhe se Kohëzgjatja e pompës së bllokuar ({ps} s), që alarmi i bllokimit të ndizet i pari" }, "start_dur": { "sampling": "Intervali i Kampionimit tejkalon Kohëzgjatjen e Nisjes ({sdt} s); majat me kampion të vetëm mund të hapin një cikël", @@ -329,10 +316,15 @@ "sampling": "Intervali i Kampionimit duhet të jetë të paktën gjysma e Intervalit të Rojtarit ({wi} s)" }, "settings_banner": "Konflikte cilësimesh: {n}. Kontrolloni seksionet e theksuara dhe korrigjojini para ruajtjes.", - "settings_banner_btn": "Shko te i pari" + "settings_banner_btn": "Shko te i pari", + "end_energy": { + "energy": "Shumë e rreptë për Pragun e ndalimit ({w} W) gjatë Vonesës së fikjes ({d} s); cikli mund të përfundojë vetëm përmes një rruge rezervë", + "stop": "Energjia e fundit ({e} Wh gjatë {d} s) lejon vetëm {p} W" + } }, "hdr": { - "ab_comparison": "Krahasimi i cilësimeve A/B", + "custom_tasks": "Detyrat tuaja", + "maintenance_supporter": "Ju duhet më shumë?", "access_control": "Kontrolli i aksesit", "add_maintenance": "Shto ngjarje mirëmbajtjeje", "automation_delete": "Fshi këtë automatizim", @@ -341,28 +333,22 @@ "automation_review_this_cycle": "Rishiko këtë cikël", "automations": "Automatizime", "clear_debug": "Pastro gjurmët e korrigjimit", - "cycle_simulator": "Simulator ciklesh", "diagnostics_pref": "Diagnostikim", "display": "Shfaqje", - "dtw_inspector": "Inspektor DTW", "export_import": "Eksporto / Importo", - "getting_started": "Fillimi", "live_power": "Fuqia live", "logs": "Regjistrat", - "logs_diagnostics": "Diagnostikim", "maintenance": "Veprime mirëmbajtjeje", "maintenance_log": "Regjistri i mirëmbajtjes", "maintenance_reminders": "Kujtesa për servisin", "manual_recording": "Regjistrim manual", - "manual_recording_tip": "Ekzekuto qëllimisht një cikël ndërkohë që WashData regjistron gjurmën e energjisë. Fillo pak para se pajisja të nisë, Ndalo kur të përfundojë, pastaj Proceso për ta ruajtur si profil me emër.", + "manual_recording_tip": "Regjistroni një cikël qëllimisht: shtypni Fillimi pak para se pajisja të nisë dhe Ndalo kur të përfundojë, pastaj Proceso për ta ruajtur si profil.", "ml_learned": "Çfarë ka mësuar WashData", - "ml_matching_tuning": "Rregullim i hollë i përputhjes së programit", "ml_settings_card": "Cilësimet", "ml_smart_learning": "Të mësuarit inteligjent", "my_preferences": "Preferencat e mia", "no_automations": "Asnjë automatizim nuk i referohet kësaj pajisjeje ende.", "panel_settings": "Cilësimet e panelit", - "param_sweep": "Skanim parametrash", "pg_settings_source": "Burimi i cilësimeve", "phase_catalog": "Katalog fazash", "playground": "Zona e provave", @@ -371,11 +357,8 @@ "status": "Statusi", "status_graph": "Grafiku i statusit", "storage_stats": "Statistikat e ruajtjes", - "test_history": "Testim mbi historikun", "toggle_sidebar": "Ndrysho shiritin anësor të Asistentit në Shtëpi", - "tools_and_data": "Mjete dhe të dhëna", "wipe_history": "Fshi historikun", - "recommendations": "Rekomandime ({n})", "pg_detection_params": "Cilësimet e zbulimit", "pg_outcome": "Rezultati i simulimit", "pg_across_cycles": "Në të gjithë ciklet tuaja", @@ -383,7 +366,8 @@ "online_account": "Dyqani i Komunitetit dhe veçoritë online", "import_power_history": "Importo historikun e fuqisë", "service_status": "Statusi i mirëmbajtjes", - "online_account_tab": "Online dhe komuniteti" + "online_account_tab": "Online dhe komuniteti", + "live_match_debug": "Korrigjimi i përputhjes live" }, "health": { "fair": "Cilësi e pranueshme e profilit", @@ -391,11 +375,16 @@ "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_tip": "Pozicioni në kurbën e regjistruar të programit të përputhur: {pct}%. Gjendet duke përputhur ciklin me profilin, ndaj mbetet i saktë kur cikli zgjat më shumë ose më pak. Përditësohet kur pajisja është e qetë, ndaj mund të vonohet.", "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ë", + "task_name": "Detyra", + "every_cycles": "Çdo (cikle)", + "every_days": "Çdo (ditë)", + "new_task_name": "Detyrë e re", + "days_since_service": "{since} / {total} ditë", "actions": "Veprime", "activity": "Aktivitet", "administrators": "Administratorë", @@ -403,10 +392,8 @@ "aria_cycle_chart": "Kurba e fuqisë së ciklit", "aria_envelope_chart": "Grafiku i zarfit të fuqisë së profilit", "aria_phase_chart": "Grafiku i redaktuesit të fazave", - "aria_playground_chart": "Kurba e fuqisë së ciklit me pragje zbulimi të zvarritshme", "aria_power_chart": "Grafiku i konsumit të fuqisë", "aria_spaghetti_chart": "Kurbat e mbivendosura të fuqisë së cikleve", - "aria_sweep_chart": "Grafiku i rezultateve të skanimit të parametrave", "artifact_anomaly": "Anomali", "artifact_high_power": "Fuqi e lartë", "artifact_interruption": "Ndërprerje", @@ -420,150 +407,100 @@ "cohesion_good": "kohezion {pct}%", "cohesion_low": "⚠ kohezion i ulët {pct}%", "compare_profiles": "Krahaso me profilet", - "concurrency": "Njëkohshmëri", "confidence": "Besueshmëria", "confidence_threshold": "Pragu i besueshmërisë", "correct_duration": "Korrigjo kohëzgjatjen (min, opsionale)", "correct_profile": "Profili i korrigjuar", - "correlation": "Korrelacion", "cost": "Kosto", "create_new_profile": "+ Krijo profil të ri…", "current_phase": "Faza aktuale", - "curves": "Kurbat", "cycle": "Cikël", "cycle_date_display": "Shfaqja e datës së ciklit", - "cycle_health": "Shëndeti i ciklit", - "cycle_index": "indeksi i ciklit →", - "cycles": "Ciklet", "cycles_count": "Cikle", "cycles_lc": "cikle", - "dashed": "me vija", "date": "Data", "debug_traces": "Gjurmë korrigjimi", - "default": "Parazgjedhja", "default_access_level": "Niveli i parazgjedhur për përdoruesit e palistuara më poshtë", "default_other": "Parazgjedhja (pajisje të tjera)", "default_tab": "Skeda e parazgjedhur kur hapet paneli", - "delta": "Δ", "description": "Përshkrimi", - "dtw_blend_w": "Pesha e përzierjes", - "dtw_blended": "Rezultati i përzier", - "dtw_ddtw": "Rezultati DDTW", - "dtw_l1": "Rezultati L1", - "dtw_matched_profile": "(profili i përputhur i ciklit)", "duration": "Kohëzgjatja", - "duration_agreement": "Përputhja e kohëzgjatjes", - "emphasis": "Theksimi", "enable_access_control": "Aktivizo kontrollin e aksesit për çdo përdorues", "energy": "Energjia", - "energy_agreement": "Përputhja e energjisë", - "event_timeline": "Kronologjia e ngjarjeve", "event_type": "Lloji i ngjarjes", "expected": "I pritshëm", + "suspected_profile": "{name} (i supozuar)", + "runner_up_profile": "{name} (vendi i dytë)", "expected_duration": "Kohëzgjatja e pritshme (min)", "file_kb": "Skedari (kB)", - "final_score": "Rezultati përfundimtar", "from": "Nga", "gap_s": "Ndërrimi (s)", "group_name": "Emri i grupit", "head_trim": "Prerje në fillim (s)", - "health": "Shëndeti", "hide_tabs": "Fshih skedat për jo-administratorët", - "in_use": "Në përdorim", "json_data": "Të dhëna JSON", "label": "Program i përputhshëm", "last": "Të fundit", "last_checked": "Kontrolluar herën e fundit:", "load_from_file": "Ngarko nga skedari", - "mae_score": "Rezultati MAE", "manual_duration": "Kohëzgjatja manuale (min, opsionale)", "match": "Përputhje", "members": "Anëtarë", - "metric": "Metrika", "mode_existing_profile": "Shto te profili ekzistues", "mode_new_profile": "Krijo profil të ri", "models_fine_tuned": "(modele të rregulluara mirë: {count})", - "n_classic_suggestions": "{n} klasike", - "n_ml_suggestions": "{n} ML", "n_selected": "{n} të zgjedhura", - "n_tuning_suggestions": "{n} sugjerim rregullimi", - "na": "s'ka", + "n_tuning_suggestions": "{n} sugjerime rregullimi", + "n_tuning_suggestions_one": "{n} sugjerim rregullimi", + "n_tuning_suggestions_other": "{n} sugjerime rregullimi", "new_profile_name": "Emër i ri profili", "notes": "Shënime", - "of": "nga", - "outcome": "Përfundimi", "panel_default_tab": "Skeda e parazgjedhur", "panel_language": "Gjuha e panelit", "per_cycle_short": "cikël", "pg_anti_wrinkle": "Kundër rrudhave", - "pg_autofill_tip": "Mbush automatikisht intervalin nga vlera aktuale ({lo}–{hi})", "pg_best_value": "Vlera më e mirë e gjetur", "pg_current_value": "Vlera aktuale:", "pg_drag_hint": "Tërhiqni vijën në grafik", - "pg_dtw": "DTW", "pg_ev_ending": "Duke përfunduar", "pg_ev_running": "Në punë", "pg_group_advanced": "Rastet e veçanta", "pg_group_detection": "Nxitësit e zbulimit", "pg_group_timing": "Rregullat e kohës", "pg_idle": "Joaktiv", - "pg_last_run": "Ekzekutimi i fundit: {date}", - "pg_match_score": "Rezultati i përputhjes", - "pg_pct_of": "{p}% e {total}", "pg_poor_match": "Përputhje e keqe", "pg_preset": "Konfigurimi i zonës së provave", "pg_preset_name": "Emri i konfigurimit të ri", "pg_preset_none": "Zgjidhni një konfigurim…", "pg_strong_match": "Përputhje e fortë", - "pg_tile_ambiguous": "Të paqarta", - "pg_tile_ambiguous_tip": "Cikle me barazim të afërt midis dy programeve", - "pg_tile_detected": "Të zbuluara", - "pg_tile_detected_tip": "Cikle ku fuqia e kaloi pragun e fillimit mjaftueshëm gjatë sa të regjistrohej si në punë", - "pg_tile_matched": "Të përputhura", - "pg_tile_matched_tip": "Cikle të atribuara saktë një programi të njohur", - "pg_time_left": "Koha e mbetur", "pg_weak_match": "Përputhje e dobët", "phase_name": "Emri i fazës", "name": "Emri", "phase_timeline": "Kronologjia e fazave", "power": "Fuqia", "profile": "Profili", - "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. Zgjidhni një program para se të nisni pajisjen dhe do të zbatohet sapo të fillojë cikli tjetër.", + "program_tip": "Zgjidhni me cilin program përputhet cikli aktual. Zbulim automatik zgjedh përputhjen më të mirë. Fiksoni një program kur Zbulim automatik gabon; nëse e zgjidhni para nisjes, zbatohet në ciklin tjetër.", "progress": "Progresi", "raw_socket": "Fole e papërpunuar", - "read_only": "Vetëm lexim", "ref_cycle": "Cikli referues (opsionale)", - "refresh_interval": "Intervali i rifreskimit (s)", "remaining": "I mbetur", "reminder_every": "Kujto çdo (cikle)", "remove_label": "- Hiq etiketën -", "rename_profile": "Riemërto profilin", - "replay_duration": "Riluajtje", "resulting_profile": "Profili rezultues", "save_mode": "Modaliteti i ruajtjes", - "score": "Rezultati", - "score_breakdown": "Zbërthimi i rezultatit", "seconds_from_start": "Sekonda nga fillimi", "select_profile": "Zgjidh profilin", "setting": "Cilësim", - "settings_a": "Cilësimet A (aktuale)", - "settings_a_short": "A", "settings_advanced": "Të avancuara", - "settings_b": "Cilësimet B (provë)", - "settings_b_short": "B", "settings_basic": "Bazë", "settings_detail_level": "Niveli i detajeve të cilësimeve", - "settings_overrides": "Mbivendosjet e cilësimeve", "show_debug": "Shfaq kartën e korrigjimit live në faqen e Statusit (besueshmëria, paqartësia, kandidatët kryesorë)", "show_expected": "Shfaq mbivendosjen e kurbës së pritshme (profili i përputhshëm, portokalli)", "show_raw": "Shfaq çelësin e të dhënave bruto të prizës në grafikun e fuqisë në kohë reale", "sparkline": "Prirja e fundit e kohëzgjatjes së cikleve", - "stage2": "Etapa 2 – ngjashmëria bazë", - "stage3": "Etapa 3 – DTW", - "stage4": "Etapa 4 – përputhja", "status": "Statusi", "tail_trim": "Prerje në fund (s)", "tap_to_dismiss": "trokit grafikun për ta mbyllur", @@ -574,7 +511,6 @@ "ungrouped": "I pagrupuar", "unlabelled": "I paetiketuar", "unlabelled_paren": "(i paetiketuar)", - "warp_path": "Rruga e deformimit", "input": "Hyrje:", "start": "Fillimi", "end": "Fundi", @@ -599,9 +535,8 @@ "n_shown": ", {n} të shfaqura", "cycles_title": "Ciklet ({n})", "pg_detecting": "Po zbulohet", - "pg_best_marker": "Më i miri: {v}", "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", + "pg_time_left_model_tip": "Vlerësuar nga fazat e programit, jo një numërim mbrapsht fiks", "phase": "Faza", "pg_mode_history": "Testo në histori", "pg_ev_detected": "U zbulua", @@ -633,12 +568,6 @@ "pg_sweep_param": "Parametër", "pg_objective": "Objektivi", "pg_steps": "Hapat", - "pg_stress_group": "Testi i ndërprerjes me gjendje statike", - "pg_stress_idle_auto": "Auto", - "pg_stress_idle_w": "Niveli i gjendjes statike (W)", - "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_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", @@ -647,12 +576,8 @@ "pg_mode_optimize": "Optimizo", "pg_group_matching": "Përputhja e programeve", "pg_zoom_hint": "klikoni dy herë për të rivendosur zmadhimin", - "aria_playground_chart2": "Grafik interaktiv i fuqisë së ciklit: kaloni kursorin për të lexuar kohën/fuqinë, rrëshqitni për të zmadhuar, tërhiqni për të lëvizur", "pg_alert_timeout_end": "Përfundoi nga skadimi i kohës, jo nga parashikimi", "pg_alert_indefinite": "Do të vazhdonte pafundësisht", - "pg_alert_stress_cap": "U arrit kufiri i sigurisë", - "pg_alert_stress_ok": "Ndërprerja e gjendjes statike: cikli u ndal", - "pg_alert_stress_warn": "Konsumi i gjendjes statike mbi pragun e ndalimit", "task_pg_history": "Testo në histori", "task_pg_sweep": "Optimizo", "task_reprocess": "Ripërpunim", @@ -662,16 +587,9 @@ "pg_recent_runs": "Ekzekutimet e fundit", "pg_load_run": "Ringarko këtë ekzekutim", "pg_match_confidence": "Besueshmëria e përputhjes", - "pg_envelope_fit": "Përshtatja me zarfin", - "pg_envelope_fit_tip": "Sa mirë cikli qëndron brenda zarfit të ruajtur të fuqisë së këtij profili, një këndvështrim tjetër nga besueshmëria e përputhjes.", "n_imported_note": ", {n} të importuar", "enable_online": "Aktivizo veçoritë online", - "store_brand": "Marka e pajisjes", - "store_model": "Modeli i pajisjes", "anonymous": "Anonim", - "rate_quality": "Vlerëso cilësinë", - "rate_n": "{n} yje", - "approved_only": "Vetëm të miratuara", "adopt_settings": "Adopto edhe cilësimet", "flags": "Flamuj", "include_phase_map": "Përfshi hartën e fazave", @@ -688,7 +606,6 @@ "cat_custom_phases": "Faza të personalizuara", "cat_profile_groups": "Grupet e profileve", "cat_settings": "Cilësimet e zbulimit & njohjes", - "cat_matching_config": "Rregullimi i njohësit", "cat_ml_models": "Modelet ML", "cat_feedback": "Etiketat e reagimit & rishikimit", "cat_suggestions": "Sugjerime", @@ -710,7 +627,6 @@ "hist_csv_data": "Të dhëna CSV", "hist_from_recorder": "Ose lexoje nga Home Assistant", "hist_since": "Që nga", - "days": "ditë", "hist_keep": "Mbaje këtë cikël", "hist_looks_complete": "i plotë", "peak_power_short": "Kulmi", @@ -730,7 +646,40 @@ "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" + "correct_total_cycles": "Korrigjo totalin", + "hist_reason_already_recorded": "mbivendoset me një cikël që WashData e ka regjistruar tashmë", + "share_consent": "Konfirmoj që këto cikle përfunduan normalisht, pa ndërprerje", + "show_internal": "Shfaq cilësimet e brendshme", + "show_internal_tip": "Cilësime që WashData i rregullon për ju (kampionimi, rojtari, shpeshtësia e përputhjes, pragjet). Shfaqen kur kjo pajisje ka vlerën e vet, një konflikt ose një sugjerim.", + "rate_n": "{n} yje", + "rate_quality": "Vlerëso cilësinë", + "pg_autolabel": "Etiketim automatik", + "pg_label_review": "Kërkon rishikim", + "pg_ev_match_reverted": "Përputhja u anulua", + "pg_ev_verified_pause": "Pauzë e verifikuar", + "n_samples": "{n} kampione", + "n_samples_one": "{n} kampion", + "n_samples_other": "{n} kampione", + "n_cycles": "{n} cikle", + "n_cycles_one": "{n} cikël", + "n_cycles_other": "{n} cikle", + "energy_per_cycle": "{v}/cikël", + "energy_total": "{v} gjithsej", + "automation_off": "(joaktiv)", + "ref_cycle_none": "Asnjë", + "share_name": "Shfaq emrin tim në atë që ndaj", + "cat_backfill_cycles": "Cikle të gjetura në historikun e importuar të fuqisë", + "pg_compare_profile": "Krahaso me profilin", + "pg_obj_early": "Shkalla e përfundimeve të hershme", + "pg_obj_endlag": "Vonesa e përfundimit (mediana)", + "pg_obj_split": "Shkalla e ndarjeve", + "pg_sweep_cycles": "Ciklet e fundit", + "cycle_context": "Historiku i regjistruesit rreth ciklit", + "cycle_context_off": "Joaktiv", + "cycle_context_minutes": "{min} min", + "cycle_context_series": "Historiku i regjistruesit", + "before_start": "Para fillimit", + "after_end": "Pas përfundimit" }, "log": { "all_levels": "Të gjitha nivelet", @@ -740,11 +689,9 @@ }, "ml": { "builtin_models": "● Duke përdorur modelet e integruara", - "fit_fair": "I pranueshëm", "fit_good": "Mirë", "fit_slight": "I lehtë", "fit_strong": "I fortë", - "fit_weak": "I dobët", "fit_word": "përshtatje", "personalized": "● I personalizuar për këtë makinë", "better_than_baseline": "{pct}% më mirë se vlerësimi bazë ({metric})", @@ -752,21 +699,29 @@ "trend_declining_tip": "Përshtatja e këtij modeli ka rënë gjatë rikontrolleve të fundit · shqyrtimi i më shumë cikleve mund ta ndihmojë të rimësojë.", "trend_steady_tip": "Përshtatja e këtij modeli ka mbetur afërsisht e qëndrueshme gjatë rikontrolleve të fundit.", "cap_blurb": { - "end": "Të dijë kur një cikël ka përfunduar vërtet", - "live_match": "Identifikimi më i shpejtë i programit në ekzekutim", - "quality": "Zbulimi i cikleve të gabuara ose të dëmtuara", - "remaining_time": "Parashikimi se sa kohë ka mbetur", "total_energy": "Parashikimi i energjisë totale dhe kostos" }, "cap_label": { - "end": "Zbulimi i fundit të ciklit", - "live_match": "Njohja e programit", - "quality": "Kontrolli i cilësisë së ciklit", - "remaining_time": "Vlerësimi i kohës së mbetur", "total_energy": "Vlerësimi i energjisë" }, - "metric_auc": "AUC {auc} në të dhënat e vlerësimit", - "metric_mae": "gabimi: {model} kundrejt bazës {baseline}" + "metric_mae": "gabimi: {model} kundrejt bazës {baseline}", + "fine_tuned_at": "rregulluar mirë më {when}", + "last_run_not_promoted": "Kontrolli i fundit ({when}): {reason}", + "never": "asnjëherë", + "skip_reason": { + "fit_failed": "Trajnimi dështoi: {error}", + "holdout_too_small": "Shumë pak cikle për të testuar drejt një model të ri: për testim mund të ndaheshin {held_out}, nevojiten {min}.", + "not_better_than_incumbent": "Modeli i ri nuk ishte më i mirë se ai në përdorim (gabimi {model} kundrejt {incumbent}), prandaj u mbajt modeli aktual.", + "not_better_than_naive": "Modeli i ri nuk ishte qartazi më i mirë se vlerësimi i thjeshtë (gabimi {model} kundrejt {naive}).", + "unknown": "Nuk u mësua asgjë e re.", + "insufficient_rows": "Ende nuk ka mjaft cikle të përdorshme: deri tani {count} cikle të etiketuara dhe të pastra.", + "insufficient_rows_one": "Ende nuk ka mjaft cikle të përdorshme: deri tani {count} cikël i etiketuar dhe i pastër.", + "insufficient_rows_other": "Ende nuk ka mjaft cikle të përdorshme: deri tani {count} cikle të etiketuara dhe të pastra." + }, + "trained_unknown": "datë të panjohur", + "held_out_count": "matur në {count} cikle vlerësimi", + "held_out_count_one": "matur në {count} cikël vlerësimi", + "held_out_count_other": "matur në {count} cikle vlerësimi" }, "modal": { "auto_label": "Etiketim automatik i cikleve", @@ -775,7 +730,6 @@ "cycle": "Cikli", "edit_group": "Modifiko grupin e profilit", "edit_phase": "Modifiko fazën", - "import_config": "Importo konfigurimin", "label_cycle": "Etiketoje ciklin", "merge_cycles": "Bashko {n} cikle", "new_group": "Grup i ri profilesh", @@ -790,6 +744,8 @@ "delete_automation_title": "Fshi Automatizimin", "delete_automation_msg": "Të fshihet automatizimi \"{name}\" nga Home Assistant? Kjo nuk mund të zhbëhet.", "remove_legacy_title": "Hiq Veprimet e Vjetra", + "reset_phase_msg": "Të zhbëhet ndryshimi yt i fazës \"{name}\" dhe të rikthehet faza e integruar? Programet i ruajnë intervalet e tyre.", + "reset_phase_title": "Rivendos Fazën", "remove_legacy_msg": "Të hiqen veprimet e personalizuara të vjetra? Ato do të ndalojnë së aktivizuari në ngjarjet e cikleve. Kjo nuk mund të zhbëhet nga paneli.", "discard_recording_title": "Hidh Poshtë Regjistrimin", "discard_recording_msg": "Të hidhet poshtë regjistrimi i ruajtur? Kjo nuk mund të zhbëhet.", @@ -810,32 +766,44 @@ "store_share_device": "Ndaj këtë pajisje", "export_select": "Eksporto - zgjidh të dhënat", "import_wizard": "Importo - zgjidh të dhënat", - "history_import": "Importo historikun e fuqisë" + "history_import": "Importo historikun e fuqisë", + "apply_suggestions_title": "Apliko sugjerimet", + "remove_task_title": "Hiq detyrën", + "remove_task_msg": "Të hiqet kujtesa \"{task}\"? Regjistrimet e saj në regjistrin e mirëmbajtjes ruhen.", + "apply_suggestions_msg": "Të ndryshohen këto {n} cilësime? Integrimi ringarkohet pas ruajtjes.", + "discard_settings_title": "Të hidhen poshtë ndryshimet e paruajtura?", + "discard_settings_msg": "Ndryshime të paruajtura të cilësimeve: {n}. Të dilet nga Cilësimet dhe të hidhen poshtë?", + "profile_title": "Profili · {name}", + "undo_import_msg": "Të dhënat dhe cilësimet e kësaj pajisjeje kthehen si ishin para importimit të fundit ({when}). Ciklet e regjistruara dhe ndryshimet e bëra që atëherë humbasin.", + "undo_import_ok": "Zhbëj importimin", + "undo_import_title": "Zhbëj importimin e fundit" }, "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.", + "pg_preset_delete_confirm": "Të fshihet konfigurimi \"{name}\"?", + "pg_preset_limit": "U arrit kufiri i konfigurimeve ({n})", + "pg_preset_overwrite": "Të mbishkruhet konfigurimi \"{name}\"?", "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.", - "refresh_catalog_hint": "Listat e markave dhe pajisjeve të komunitetit mbahen në kujtesën e përkohshme, që dyqani i komunitetit të mbetet brenda kufirit të tij ditor. Rifreskoni për të marrë hyrjet e shtuara ose të miratuara nga të tjerët.", + "store_sibling_hint": "Asgjë e ndarë për modelin tuaj? Një model i afërt i së njëjtës markë është zakonisht një fillim i mirë.", + "store_declare_appliance": "Tregojini WashData cilën pajisje keni për të parë konfigurimet që të tjerët kanë ndarë për të. Ose shkruani një markë më lart për të shfletuar.", + "refresh_catalog_hint": "Listat e markave dhe pajisjeve të komunitetit ruhen në kujtesën e përkohshme për të kursyer kufirin ditor të dyqanit. Rifreskoni për të parë hyrjet që të tjerët kanë shtuar ose miratuar.", "head_trim_hint": "Hiqni kaq sekonda nga fillimi", "appliance_monitor": "Monitor i pajisjes", "artifact_dip_detail": "Ra nën brezin e zakonshëm të fuqisë për ~{n} s.", "artifact_footer": "E theksuar në grafikun e mësipërm. Këto janë objekte kalimtare (p.sh. dera e hapur në mes të ciklit), jo domosdoshmërisht probleme.", "artifact_header": "{n} anomali të zbuluara gjatë këtij cikli", - "artifact_pause_detail": "Fuqia ra pothuajse në zero për ~{n} s dhe u rikuperua – ndoshta dera u hap në mes të ciklit ose cikli u ndërpre.", + "artifact_header_one": "{n} anomali e zbuluar gjatë këtij cikli", + "artifact_header_other": "{n} anomali të zbuluara gjatë këtij cikli", + "artifact_pause_detail": "Fuqia ra pothuajse në zero për ~{n} s, pastaj u rikthye. Ndoshta dera u hap ose cikli u vu në pauzë.", "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 ë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.", + "cleanup_intro": "Të gjitha ciklet e etiketuara, të mbivendosura. Shëno ciklet jashtë norme, pastaj hiq etiketën e tyre (mbeten në historik, nuk e formësojnë më këtë profil) ose fshiji.", "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)", - "compare_profiles_tip": "Mbivendosni zarfat e tjerë të profileve në grafikun e mësipërm për të parë se cili i përshtatet më mirë këtij cikli.", + "compare_profiles_tip": "Mbivendosni profile të tjera në grafik për të parë cili i përshtatet më mirë këtij cikli.", "compare_selected_cycles": "Ciklet e zgjedhura (solide) – shfaq / fshih", - "consider_new_profile": "Konsideroni krijimin e një profili të ri.", - "coverage_gap": "ciklet e fundit nuk kanë asnjë profil që përputhet ({pct}% e 30 të fundit).", - "coverage_gap_similar_cycles": "U gjetën {count} cikle të ngjashme të paetiketuara – krijo një profil për të filluar përputhjen.", "cycles_deleted": "{count} cikël(e) u fshinë", "enough_data": "Të dhëna të mjaftueshme për të mësuar ({current}/{min} cikle).", "export_description": "Zgjidhni saktësisht cilat profile, cikle, cilësime dhe të tjera do të eksportohen në JSON, ose analizoni një skedar dhe importoni vetëm pjesët që dëshironi.", @@ -843,30 +811,26 @@ "feedback_prompt": "Konfirmo nëse ishte i saktë, korrigjo programin ose injorojë.", "feedback_relabel_hint": "Rietiketimi i këtij cikli e zgjidh atë gjithashtu.", "filter_by_profile": "Filtro sipas profilit…", - "group_modal_help": "Gruponi programe me të njëjtën formë që ndryshojnë në temperaturë/centrifugim (kohëzgjatja mund të ndryshojë). Përputhja shënon grupin si një kandidat dhe më pas zgjedh anëtarin më të përshtatshëm. Zgjidhni të paktën 2; mbivendosja tregon se sa të ngjashëm janë.", + "group_modal_help": "Gruponi programe me të njëjtën formë që ndryshojnë në temperaturë ose centrifugim. Përputhja e vlerëson grupin si një të vetëm, pastaj zgjedh anëtarin më të përshtatshëm. Zgjidhni të paktën 2; mbivendosja tregon sa të ngjashëm janë.", "group_not_cohesive": "Këto profile nuk janë mjaft të ngjashme për t'u grupuar në mënyrë të besueshme, kështu që përputhja i trajton ato individualisht derisa të hiqni pjesën e jashtme ose të ndani grupin.", "group_preview_hint": "Shënoni 2+ anëtarë për të parë dhe krahasuar kurbat e tyre të fuqisë.", - "import_intro": "Ngarko një skedar të eksportuar ose ngjit një ngarkesë JSON më poshtë.", - "legacy_actions_title": "Veprimi i personalizuar i trashëguar i {count} është ende duke u ekzekutuar", + "legacy_actions_title": "{count} veprime të personalizuara të trashëguara janë ende në ekzekutim", + "legacy_actions_title_one": "{count} veprim i personalizuar i trashëguar është ende në ekzekutim", + "legacy_actions_title_other": "{count} veprime të personalizuara të trashëguara janë ende në ekzekutim", "live_chart_loading": "Grafiku i fuqisë live shfaqet ndërsa mbërrijnë leximet.", "loading": "Duke ngarkuar…", "loading_curve": "Duke ngarkuar kurbën…", "loading_settings": "Duke ngarkuar cilësimet…", "log_buffer_hint": "Më i riu i pari · ruan 500 rekordet e fundit ha_washdata që nga rinisja · zvarrit skajin e poshtëm për të ndryshuar madhësinë.", - "maintenance_advisory": "Rritja e kohëzgjatjes/energjisë mund të tregojë nevojën e mirëmbajtjes së pajisjes (p.sh. heqja e gurëve, pastrimi i filtrit).", "maintenance_due": "Mirëmbajtja e duhur: {items}", - "maintenance_intro": "Regjistroni servisimin që kryeni në këtë pajisje dhe merrni kujtesë kur çdo detyrë të jetë përsëri e nevojshme.", + "maintenance_intro": "Regjistroni servisimin e kësaj pajisjeje dhe merrni kujtesë kur çdo detyrë bëhet sërish e nevojshme.", "maintenance_load_error": "Nuk u ngarkuan dot të dhënat e mirëmbajtjes: {error}", "maintenance_requires_access": "Mirëmbajtja dhe eksporti/importi kërkojnë akses të plotë.", - "manual_duration_hint": "Gjatësia mesatare/e pritshme e ciklit të profilit, e përdorur për vlerësimet e kohës së mbetur. Ndrysho për ta vendosur atë; duke e lënë të pandryshuar ruan vlerën aktuale.", - "matching_tuning_intro": "Gjatë të mësuarit, WashData rregullon gjithashtu sa shumë përputhja e programit peshohet sipas formës kundrejt kohëzgjatjes dhe energjisë.", + "manual_duration_hint": "Gjatësia e pritshme e ciklit të profilit, e përdorur për vlerësimin e kohës së mbetur. Lëreni të pandryshuar për të mbajtur vlerën aktuale.", "merge_intro": "Ciklet e zgjedhura kombinohen në një (rend kronologjik; boshllëqet mbushen me 0 W). Zgjidhni profilin që rezulton.", "ml_intro": "WashData vjen me modele inteligjente që funksionojnë menjëherë.", "ml_learned_intro": "Modele të rregulluara mirë për këtë makinë.", - "ml_loading": "duke ngarkuar ML…", "ml_settings_intro": "Dy çelësa të pavarur: njëri aplikon modelet ndërsa një cikël ekzekutohet, tjetri lejon WashData t'i rregullojë ato me kalimin e kohës.", - "name_first_program": "Keni cikle të mjaftueshme – emërtoni programin tuaj të parë për të nisur përputhjen.", - "near_duplicate_cluster": "u zbulua grupi i profilit pothuajse dublikatë. Grupimi lejon që përputhja të zgjedhë në mënyrë të besueshme midis pamjeve të ngjashme (p.sh. i njëjti program në temperaturë/centrifugim të ndryshëm).", "no_cycles_match": "Asnjë cikël nuk përputhet me filtrin aktual.", "no_cycles_profile": "Asnjë cikël për këtë profil.", "no_cycles_selected": "Zgjidhni së pari të paktën një cikël.", @@ -891,73 +855,52 @@ "no_suggestions": "Asnjë sugjerim aktiv.", "notify_services_hint": "Përdorni ID-të e shërbimit {entity} (të ndara me presje për shumë). Variablat e shabllonit: {vars}.", "old_actions_warning": "Konfiguruar me redaktuesin e vjetër të veprimeve (tani i hequr). Ato ende ndezin në ngjarjet e ciklit, por nuk mund të modifikohen më këtu. Shndërrojini ato në një automatizim normal ose hiqni ato.", - "onboarding_progress": "{n} / 3 cikle të vëzhguara", - "onboarding_watching": "Përdorni pajisjen normalisht – WashData po vëzhgon. Pas 3 cikleve, do të nisë përputhja e programeve.", "pending_feedback": "Koment në pritje të zbulimit", "performance_trend": "Tendenca e performancës ({n} cikle)", - "pg_analysis_empty": "Ngarkoni një cikël për të parë analizën e përputhjes.", "pg_apply_confirm": "Të aplikohet vlera më e mirë: {label} = {value}?", - "pg_canvas_empty": "Zgjidhni një cikël më sipër, pastaj shtypni Ngarko për të parë gjurmën e tij të fuqisë.", - "pg_chart_caption": "Shkalla e zbulimit/përputhjes sipas vlerave të {param}", "pg_ctrl_intro": "Vlerat nisen nga cilësimet aktive të integrimit për këtë pajisje. Ndryshimet qëndrojnë në zonën e provave derisa t'i aplikoni.", "pg_live_unavailable": "Cilësimet aktive nuk janë të disponueshme, po shfaqen vlerat e parazgjedhura.", "pg_load_live_confirm": "Të hidhen poshtë ndryshimet në zonën e provave ({n}) dhe të ringarkohen cilësimet aktuale të integrimit?", - "pg_match_rate": "{pct}% shkallë përputhjeje në {total} cikle", "pg_matches_live": "Përputhet me cilësimet aktive", "pg_n_changed": "Ndryshime kundrejt cilësimeve aktive: {n}", "pg_sugg_none": "Të gjitha sugjerime tashmë përputhen me cilësimet aktuale", - "pg_preset_delete_confirm": "Të fshihet konfigurimi \"{name}\"?", - "pg_preset_limit": "U arrit kufiri i konfigurimeve ({n})", - "pg_preset_overwrite": "Të mbishkruhet konfigurimi \"{name}\"?", "pg_publish_one_confirm": "Të ruhet {label} = {value} te cilësimet e kësaj pajisjeje?", "pg_restart_note": "Rinisni Home Assistant për të aktivizuar mjetet e simulimit.", - "pg_sim_progress": "{done} / {total} cikle", - "pg_starting": "Duke filluar…", - "pg_stress_synthetic": "simulimi i gjendjes statike fillon këtu", - "pg_sweep_intro": "Po sikur {param} të ishte ndryshe? Testoni {steps} vlera në {cycles} ciklet tuaja të fundit për të gjetur cilësimin ku përputhen saktë sa më shumë cikle.", - "pg_sweep_step": "Hapi {done} / {total}", - "pg_verdict_bad": "Kërkon vëmendje: shumë cikle mbeten të pazbuluara.", - "pg_verdict_good": "Rregulluar mirë: shumica e cikleve identifikohen dhe përputhen saktë.", - "pg_verdict_ok": "I pranueshëm: disa cikle munguan. Provoni të ulni pragun e fillimit.", "phase_catalog_intro": "Segmente të emërtuara të një cikli (Para-larje, Ngrohje, Centrifugim…). Caktojini ato profilit nga paneli i kontrollit.", "phase_ranges_intro": "Intervalët e fazës (minuta nga fillimi i ciklit) mbivendosur mbi kurbën mesatare. Redakto vlerat për pamje paraprake live.", - "playground_intro": "Një zonë e sigurt provash për të riluajtur ciklet e kaluara me cilësime të ndryshme, për të krahasuar dy konfigurime krah për krah dhe për të inspektuar si përputhet një cikël me një profil. Asgjë këtu nuk i ndryshon të dhënat tuaja aktive derisa ta aplikoni shprehimisht.", - "playground_needs_restart": "Rinisni Home Assistant për të aktivizuar veglat e zonës së provave.", - "preferences_admin": "Preferencat, paneli dhe kontrolli i aksesit", - "preferences_adv": "Preferencat", + "playground_intro": "Provoni cilësimet mbi të dhënat reale të cikleve tuaja. Asgjë këtu nuk ndryshon cilësimet aktive derisa ta zbatoni.", "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, 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.", + "process_history_hint": "Riekzekutoni përputhjen në të gjitha ciklet e ruajtura, rifreskoni sugjerimet, ristërvitni modelet ML (nëse janë aktivizuara), rillogaritni koston e cikleve sipas çmimeve të regjistruara dhe rillogaritni shëndetin e cikleve. Ekzekutojeni 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.", - "rbac_hint": "Kur çaktivizohet, çdo përdorues i Asistentit në Shtëpi ka akses të plotë (parazgjedhja). Administratorët kanë gjithmonë akses të plotë dhe mund të menaxhojnë të gjithë.", - "recent_logs": "Regjistrimet e fundit ha_washdata", + "rbac_hint": "Çaktivizuar: çdo përdorues i Home Assistant ka akses të plotë (parazgjedhja). Administratorët kanë gjithmonë akses të plotë dhe mund të menaxhojnë të gjithë.", "recording_in_progress": "Regjistrimi në progres", "reminders_intro": "Shfaq një kujtesë në panel pas kaq cikleve nga servisimi i fundit. Lëreni bosh ose 0 për ta çaktivizuar një kujtesë.", "restart_gap_footer": "Evidentuar në grafik. Të dhënat e energjisë mungojnë për këto intervale; përputhja përdori vetëm lexime reale.", - "restart_gap_header": "{n} boshllëk rinisje të HA gjatë këtij cikli", + "restart_gap_header": "{n} boshllëqe rinisje të HA gjatë këtij cikli", + "restart_gap_header_one": "{n} boshllëk rinisje të HA gjatë këtij cikli", + "restart_gap_header_other": "{n} boshllëqe rinisje të HA gjatë këtij cikli", "restart_gap_item": "{dur} boshllëk", - "review_confirm_help": "Konfirmoni nëse ky cikël është zbuluar saktë. Komentet tuaja e trajnojnë modelin në kompjuterin tuaj - sa më shumë cikle të konfirmoni, aq më i mirë do të jetë përputhja dhe rezultatet shëndetësore. Mjafton një mirë/e keqe e shpejtë.", + "review_confirm_help": "A u zbulua saktë ky cikël? Konfirmimi i cikleve përmirëson përputhjen për makinën tuaj. Mjafton një Mirë/E keqe e shpejtë.", "review_in_settings": "Rishiko në Cilësime", "review_notes_placeholder": "Shënime (opsionale)", "review_notes_tip": "Shënime me tekst të lirë për referencën tuaj. Nuk përdoret nga përputhja ose trajnimi.", - "review_profile_tip": "Programi i këtij cikli është etiketuar si. Nëse programi i zbuluar automatikisht ishte i gabuar, korrigjoni atë këtu - etiketimi mëson përputhjen për ciklet e ardhshme.", - "review_quality_tip": "Sa i pastër është ky cikël. Mirë = një shembull teksti i këtij programi; E keqe = e zbuluar por e zhurmshme ose atipike; I papërdorshëm = i zbuluar gabimisht (i bashkuar, i cunguar ose i rremë). Drejton rezultatin e shëndetit dhe cilat cikle lejohen për të trajnuar modelin.", - "review_recorded_tip": "Shënojeni këtë si një cikël referimi të zgjedhur vetë për programin e tij - i njëjti rol si një cikël i regjistruar manualisht. Ciklet e referencës mbahen gjithmonë, vendosin shabllonin që përputhet dhe nuk hiqen kurrë nga pastrimi. (Ky është flamuri \"i artë\"/regjistruar; të dy janë e njëjta gjë.)", - "review_tags_tip": "Flamujt opsionalë që përshkruajnë atë që shkoi keq me këtë cikël, kështu që trajnimi dhe pastrimi mund të jenë përgjegjës për këtë.", + "review_profile_tip": "Programi me të cilin është etiketuar ky cikël. Nëse është gabim, korrigjojeni këtu; përputhja mëson nga etiketat.", + "review_quality_tip": "Sa i pastër është ky cikël. Mirë: një cikël model i këtij programi. E keqe: i zbuluar, por me zhurmë ose i pazakontë. E papërdorshme: i zbuluar gabim (i bashkuar, i ndërprerë para kohe ose i rremë). Ndikon në rezultatin e shëndetit dhe në ciklet që trajnojnë modelin.", + "review_recorded_tip": "Shënojeni si cikël referimi të zgjedhur vetë për programin e tij, si një cikël i regjistruar manualisht. Ciklet e referencës mbahen gjithmonë, krijojnë bazën e shabllonit të përputhjes dhe pastrimi nuk i heq kurrë.", + "review_tags_tip": "Flamuj opsionalë për atë që shkoi keq, që trajnimi dhe pastrimi ta marrin parasysh.", "review_to_cycles": "Hap radhën e rishikimit të Cikleve", "samples_decimated": "Duke shfaqur {shown} nga {total} kampione (të holluara për shfaqje; kulmet janë ruajtur). Një hapësirë e gjerë këtu është hollim, jo të dhëna që mungojnë.", - "saving_triggers_reload": "Ruajtja aktivizon ringarkimin e integrimit. Njësitë HA mund të shfaqen shkurtimisht si të padisponueshme.", + "saving_triggers_reload": "Ruajtja ringarkon integrimin. Entitetet mund të shfaqen shkurtimisht si të padisponueshme.", "search_placeholder": "Kërko cilësime…", "see_recorder": "Shiko veglën e regjistruesit më poshtë", "setting_changed": "Ndryshuar nga {old} në {new} më {date}", "settings_basic_note": "Po shfaqen cilësimet thelbësore. Kaloni te Të avancuara për listën e plotë.", - "shape_drift_advisory": "⚠ Zhvendosje forme", - "shape_drift_detail": "Modeli i energjisë i këtij profili ka ndryshuar me kalimin e kohës – mundësi të konsumit të pajisjes ose nevojë mirëmbajtjeje (p.sh. heqja e gurëve, pastrimi i filtrit).", "show_all_settings": "Shfaq të gjitha cilësimet", - "showing_suggestions": "Po shfaq cilësimin e {count} me sugjerime.", - "split_intro": "Kliko grafikun për të shtuar ose hequr një pikë ndarjeje, ose zbulo automatikisht sipas ndërprerjeve. Çdo segment që rezulton mund të ketë profilin e tij.", - "storage_diagnostics": "Statistikat e ruajtjes, mirëmbajtja, eksporti/importi", + "showing_suggestions": "Po shfaqen {count} cilësime me sugjerime.", + "showing_suggestions_one": "Po shfaqet {count} cilësim me sugjerime.", + "showing_suggestions_other": "Po shfaqen {count} cilësime me sugjerime.", + "split_intro": "Kliko grafikun për të shtuar ose hequr një pikë ndarjeje, ose zbulo automatikisht sipas periudhave të qeta. Çdo pjesë mund të ketë profilin e vet.", "sug_staged": "Vendosur {key} = {val}. Ruani për të aplikuar.", "toast_auto_detect_enabled": "Zbulimi automatik u aktivizua", "toast_auto_label_complete": "Etiketimi automatik u përfundua", @@ -974,7 +917,6 @@ "toast_no_automation": "Këtu nuk mund të krijohet automatizim", "toast_profile_name_required": "Emri i profilit kërkohet", "toast_program_set": "Programi u caktua: {program}", - "toast_rebuild_failed": "Rindërtimi dështoi: {error}", "toast_resume_failed": "Rifillimi dështoi: {error}", "toast_revert_failed": "Kthimi prapa dështoi: {error}", "toast_reverted": "{key} u rikthye", @@ -985,97 +927,84 @@ "trend_energy_up": "Tendenca e energjisë në rritje ({pct}%/cikël) - mesatarisht kohët e fundit {avg}", "trim_destructive_confirm": "Kjo ruan vetëm {pct}% të ciklit dhe nuk mund të zhbëhet. Vazhdoni?", "trim_intro": "Zvarrit dorëzat e kuqe ose futo vlerat. Gjithçka jashtë dritares hiqet.", - "tuning_suggestions_available": "Ofrohet {count} sugjerim akordimi nga ciklet e vëzhguara. Ato shfaqen pranë fushave përkatëse.", + "tuning_suggestions_available": "Ofrohen {count} sugjerime rregullimi nga ciklet e vëzhguara. Ato shfaqen pranë fushave përkatëse.", + "tuning_suggestions_available_one": "Ofrohet {count} sugjerim rregullimi nga ciklet e vëzhguara. Ai shfaqet pranë fushës përkatëse.", + "tuning_suggestions_available_other": "Ofrohen {count} sugjerime rregullimi nga ciklet e vëzhguara. Ato shfaqen pranë fushave përkatëse.", "unsure_detected_prefix": "WashData nuk është i sigurt se ka zbuluar", - "updated": "Përditësuar", "warmup_badge": "Akoma duke mësuar ({done}/{needed} cikle)", - "warmup_detail": "Ky profil ka nevojë për {needed} cikle të etiketuara para se të fillojë përputhja automatike. Çdo cikël i konfirmuar e ndihmon të mësojë.", + "warmup_detail": "Përputhja tashmë funksionon. WashData ju kërkon të konfirmoni {needed} ciklet e para të këtij profili përpara se të etiketojë vetë.", "wipe_history_warning": "Fshini përgjithmonë të gjitha ciklet dhe profilet. Nuk mund të zhbëhet.", "compare_overlay_tip": "Mbivendos brezat e profileve të mësuara për të parë se cilin program i ngjason secili cikël.", - "duration_trend_up_tip": "Kohëzgjatja në rritje ({pct}%/cikël)", - "duration_trend_down_tip": "Kohëzgjatja në rënie ({pct}%/cikël)", - "energy_trend_up_tip": "Energjia në rritje ({pct}%/cikël)", - "energy_trend_down_tip": "Energjia në rënie ({pct}%/cikël)", - "performance_trending": "Tendenca e performancës", - "pg_select_cycle_hint": "Zgjidh një cikël dhe kliko ↺ për ta ngarkuar", - "pg_sweep_progress": "Hapi {done} / {total}", - "pg_analysis_hint": "Zgjidh një cikël dhe kliko ↺ për të ngarkuar analizën.", "diagnostics_load_failed": "Diagnostikat nuk u ngarkuan dot: {error}", - "manual_duration_ref_hint": "Përdoret vetëm kur nuk është zgjedhur asnjë cikël referues. Një cikël referues e cakton kohëzgjatjen sipas gjatësisë së vet.", + "manual_duration_ref_hint": "Përdoret vetëm pa një cikël referues; një cikël referues e cakton kohëzgjatjen sipas gjatësisë së vet.", + "duration_learned_hint": "Llogaritet nga ciklet e këtij programi (pa ato jashtë norme) dhe përditësohet pas çdo cikli të ri. Për ta ndryshuar, prisni ose hiqni ciklet që devijojnë.", "toast_convert_rolled_back": "Konvertimi dështoi dhe u kthye mbrapsht (nuk mbeti asnjë automatizim): {error}", "toast_convert_orphan": "Automatizimi u krijua, por pastrimi i veprimeve të vjetra dështoi. Mos provoni përsëri: hiqni veprimet e vjetra manualisht për të shmangur një automatizim të dyfishuar.", - "advisory_duration_trend_up": "Ciklet '{name}' po zgjaten gjithnjë e më shumë (rreth +{pct}% për cikël). Nëse sjellja e pajisjes ka ndryshuar, regjistrojeni sërish ose rindërtojeni këtë profil.", - "advisory_energy_trend_up": "'{name}' po konsumon gjithnjë e më shumë energji (rreth +{pct}% për cikël) - ia vlen ta kontrolloni pajisjen nëse kjo është e papritur.", - "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.", + "advisory_duration_trend_up": "Ciklet '{name}' zgjaten vazhdimisht (rreth +{pct}% për cikël). Nëse pajisja ka ndryshuar, regjistrojeni sërish ose rindërtojeni këtë profil.", + "advisory_energy_trend_up": "'{name}' konsumon më shumë energji në çdo cikël (rreth +{pct}% për cikël). Kontrolloni pajisjen nëse kjo është e papritur.", + "advisory_poor_health": "'{name}' përshtatet dobët: ciklet e tij të fundit ndryshojnë shumë ose përputhen dobët. Rishikoni ciklet ose regjistrojeni sërish, që përputhja dhe vlerësimet të mbeten të sakta.", + "advisory_shape_drift": "'{name}' ka devijuar nga forma e tij fillestare e fuqisë, ndoshta nga gëlqerëzimi ose konsumimi. Merrni në konsideratë ta regjistroni sërish me ciklet e fundit.", + "advisory_shape_drift_corr": "'{name}' ka devijuar nga forma e tij fillestare e fuqisë (korrelacioni {corr}), ndoshta nga gëlqerëzimi ose konsumimi. Merrni në konsideratë ta regjistroni sërish me ciklet e fundit.", + "advisory_unmatchable": "'{name}' nuk mund të përputhet kurrë: pas tij nuk ka asnjë cikël me të dhëna fuqie. Etiketoni ose regjistroni një cikël të këtij programi për ta rregulluar.", + "advisory_unmatchable_no_duration": "'{name}' nuk mund të përputhet kurrë: cikli i tij nuk ka kohëzgjatje të përdorshme. Regjistrojeni sërish këtë program ose caktoni kohëzgjatjen e tij të pritshme.", "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_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.", "pg_sweep_intro2": "Gjeni cilësimin që përmbush më mirë një objektiv në ciklet tuaja të fundit. Asgjë nuk ndryshon derisa ta zbatoni.", - "pg_running_history": "Po riluhen ciklet tuaja…", - "pg_running_sweep": "Po testohen vlerat në të gjithë ciklet tuaja…", "pg_row_load_hint": "Ngarko këtë cikël në grafikun më sipër", - "pg_history_intro2": "Riluajini ciklet tuaja të fundit përmes detektorit dhe përputhësit real me cilësimet e mësipërme. Klikoni çdo rresht për të ngarkuar atë cikël në grafik; ndryshoni një cilësim për të parë një krahasim para/pas.", + "pg_history_intro2": "Riluajini ciklet e fundit përmes zbuluesit dhe përputhësit real me cilësimet e mësipërme. Klikoni një rresht për ta ngarkuar në grafik; ndryshoni një cilësim për të krahasuar para dhe pas.", "pg_simulating": "Po simulohet cikli…", - "pg_canvas_empty2": "Zgjidhni një cikël më sipër dhe shtypni Ekzekuto për ta simuluar. Pastaj kaloni kursorin për të lexuar vlerat, rrëshqitni për të zmadhuar dhe tërhiqni për të lëvizur.", "pg_apply_settings_confirm": "Të ruhen këto {n} cilësime në këtë pajisje? {list}", - "pg_stress_terminated_detail": "U stabilizua në rreth {idle} W gjendje statike -- cikli përfundoi {h} orë {m} min më vonë përmes {reason}.", - "pg_stress_above_threshold_detail": "Konsumi i gjendjes statike (~{idle} W) është në ose mbi pragun efektiv të ndalimit ({stop} W) -- cikli nuk u regjistrua kurrë si i qetë. Për ta rregulluar, rrisni stop_threshold_w.", - "pg_stress_hit_cap_detail": "Cikli funksionoi {h} orë {m} min pa u ndalur -- u ndal me forcë nga kufiri i sigurisë. Konsumi i gjendjes statike {idle} W kundrejt pragut të ndalimit {stop} W.", "no_logs_match": "Asnjë regjistrim nuk përputhet me këta filtra.", "pg_analysis_hint2": "Zgjidhni një cikël dhe shtypni Ekzekuto për të ngarkuar analizën e përputhjes.", - "pg_analysis_empty2": "Shtypni Ekzekuto për të parë analizën e përputhjes.", - "imported_readonly": "Importuar nga dyqani i komunitetit. Shfaqet për referencë dhe përputhje. Nuk llogaritet në statistikat tuaja dhe nuk mund të redaktohet.", + "imported_readonly": "Importuar nga dyqani i komunitetit. Përdoret për referencë dhe përputhje, nuk llogaritet në statistikat tuaja dhe nuk mund të redaktohet.", "merge_no_imports": "Regjistrimet e importuara nuk mund të bashkohen në një cikël real. Çzgjidhni ato për t'i bashkuar.", - "store_enable_hint": "Aktivizoni veçoritë online te Cilësimet për të shfletuar dhe importuar ciklet referuese të komunitetit.", - "online_intro": "Ndani dhe shkarkoni cikle referuese me përdorues të tjerë të WashData. Të gjitha veçoritë online janë me zgjedhje dhe të çaktivizuara si parazgjedhje.", + "store_enable_hint": "Aktivizoni veçoritë online te Preferencat > Online dhe komuniteti për të shfletuar dhe importuar ciklet referuese të komunitetit.", "store_share_intro": "Ngarkoni këtë cikël referues që të tjerët me të njëjtën pajisje ta përdorin. Ai rishikohet para se të shfaqet publikisht.", - "online_intro_global": "Shfletoni dhe ndani regjistrime referuese me përdorues të tjerë të WashData dhe konfirmoni hyrjet e pajisjeve. Një lidhje vlen për të gjithë integrimin tuaj WashData; marka dhe modeli i pajisjes caktohen për çdo pajisje te seksioni Bazë. Të gjitha veçoritë online janë me zgjedhje dhe të çaktivizuara si parazgjedhje.", + "online_intro_global": "Shfletoni dhe ndani regjistrime referuese me përdorues të tjerë të WashData. Një lidhje vlen për të gjitha pajisjet tuaja WashData; marka dhe modeli caktohen për çdo pajisje te seksioni Bazike. Veçoritë online janë të çaktivizuara derisa t'i aktivizoni.", "online_unavailable": "Veçoritë online nuk janë të disponueshme në këtë server.", "store_picker_offline": "Aktivizoni veçoritë online te ingranazhi i cilësimeve për të zgjedhur nga katalogu i komunitetit.", "pick_brand_first": "Zgjidhni së pari një markë pajisjeje.", - "model_not_found": "Ende nuk është në katalog.", - "connect_to_confirm": "Lidhuni te ingranazhi i cilësimeve për të konfirmuar ose vlerësuar.", + "custom_task_limit": "Keni arritur kufirin prej {max} detyrash.", + "custom_tasks_intro": "Shtoni çdo gjë tjetër për të cilën doni të merrni kujtesë. Një detyrë bëhet e nevojshme kur arrihet njëri nga intervalet; lëreni njërin bosh ose 0 për të përdorur vetëm tjetrin.", + "maintenance_due_sensor_hint": "Sensori binar \"Mirëmbajtje e nevojshme\" është i ndezur sa kohë që ndonjë detyrë është e nevojshme, për automatizimet tuaja të njoftimeve.", + "maintenance_supporter_intro": "Maintenance Supporter është një integrim më vete që bën shumë më tepër: çdo pajisje ose detyrë, planifikime sipas kohës, përdorimit ose një sensori, njoftime dhe historik. Mund të përdorë numëruesin e cikleve të WashData.", "adopt_settings_hint": "Mbishkruan pragun e zbulimit dhe përputhjes të kësaj pajisje me të ndarët. Njoftimet, entitetet dhe çmimi i energjisë nuk ndryshohen kurrë.", - "include_settings_hint": "Ndani pragjet e njohjes dhe përputhjes të kësaj pajisje (jo njoftimet, entitetet ose çmimin e energjisë). Adoptuesit zgjedhin nëse do t'i aplikojnë.", + "include_settings_hint": "Ndani pragjet e zbulimit dhe përputhjes të kësaj pajisjeje (jo njoftimet, entitetet ose çmimin e energjisë). Të tjerët zgjedhin nëse do t'i zbatojnë.", "onboard_download": "Pajisje e re? Adoptoni një konfigurim të gatshëm (programe, cikle referimi dhe faza) nga një tjetër përdorues WashData me të njëjtën pajisje.", - "share_consent": "Konfirmoj se këto cikle u ekzekutuan normalisht deri në fund pa ndërprerje", "share_device_none": "Nuk ka ende cikle të ndashme. Shënojeni një cikël të regjistruar ose të zgjedhur me dorë si cikël referimi (⭐) tek skeda Ciklet fillimisht.", "share_guideline_naming": "Emërtoni çdo profil saktësisht siç tregohet në çezmën ose ekranin e pajisjes (p.sh. 'Cotton 40', 'Eco 60').", - "share_guideline_quality": "Ndani vetëm cikle që u kompletuan normalisht -- pa ndërprerje në mes të ciklit, ngjarje hapjeje dere, ose probleme me energji.", + "share_guideline_quality": "Ndani vetëm cikle që përfunduan normalisht: pa ndërprerje, hapje dere ose luhatje të energjisë.", "share_guideline_review": "Ngarkimi juaj fillon si në pritje dhe bëhet publik pasi anëtarë të mjaftueshëm të komunitetit e konfirmojnë.", "share_guidelines_title": "Para se të ndani", - "store_download_device_intro": "Adoptoni çdo program të ndarë dhe ciklet e tij të referencës në pajisjen tuaj. Ciklet dhe statistikat tuaja të regjistruara nuk preken.", + "store_download_device_intro": "Shtoni në pajisjen tuaj çdo program të ndarë dhe ciklet e tij të referencës. Ciklet dhe statistikat tuaja nuk preken.", "store_share_device_intro": "Ngarko {brand} {model} me ciklet e referencës që zgjidhni. Të tjerët me të njëjtën pajisje mund të adoptojnë programet tuaja. Hyrjet shqyrtohen para publikimit.", "share_profile_no_cycles": "Nuk ka cikle referimi -- shënojeni një cikël si ⭐ tek skeda Ciklet për të përfshirë këtë profil", - "advisory_phase_inconsistent": "'{name}' duket se përzien programe ose temperatura të ndryshme - ciklet e tij ngrohin për periudha shumë të ndryshme kohore. Ndarja e tij në profile të veçanta (p.sh. sipas temperaturës) do të përmirësojë përputhjen dhe vlerësimet e kohës.", - "advisory_phase_inconsistent_title": "⚠ Ndoshta programe të përziera", - "export_select_intro": "Shënoni saktësisht çfarë të përfshihet. Zgjedhja e profileve pa ciklet e tyre gjithsesi eksporton një program të njohshëm (forma e mësuar e tij udhëton bashkë me të).", - "import_analyze_hint": "Ngarkoni një skedar të eksportuar (ose ngjisni JSON-in e tij). WashData e analizon dhe tregon saktësisht çfarë mund të importohet përpara se të ndryshojë çdo gjë.", + "export_select_intro": "Shënoni çfarë të përfshihet. Profilet pa ciklet e tyre eksportojnë gjithsesi një program që mund të përputhet.", + "import_analyze_hint": "Ngarkoni një skedar të eksportuar (ose ngjisni JSON-in e tij). WashData tregon çfarë mund të importohet përpara se të ndryshojë gjë.", "analyzing": "Duke analizuar…", - "device_type_mismatch_warn": "Ky eksport është nga një tip tjetër pajisjeje ({src} ndaj {local}). Programet dhe ciklet mund të importohen gjithsesi si të dhëna reference, por cilësimet specifike të pajisjes dhe importimi i historikut real janë çaktivizuar.", + "device_type_mismatch_warn": "Ky eksport është nga një tip tjetër pajisjeje ({src} ndaj {local}). Programet dhe ciklet mund të importohen si të dhëna reference, por jo cilësimet e pajisjes ose historiku real.", "merge_hint": "Artikujt e importuar shtohen; asgjë lokale nuk humbet. Përplasjet e emrave zgjidhen më poshtë.", "replace_warn": "Çdo kategori e shënuar fshihet dhe zëvendësohet nga skedari. Kategoritë e pashënuara mbeten të paprekura.", "dest_reference_hint": "Ciklet e importuara vetëm përmirësojnë njohjen e programit dhe nuk ndikojnë kurrë në statistikat e përdorimit/energjisë.", - "dest_real_history_hint": "Ciklet e importuara llogariten si historik i vetë kësaj pajisjeje dhe ushqejnë statistikat e energjisë/përdorimit. Përdoreni për të zhvendosur një pajisje në një instalim të ri.", + "dest_real_history_hint": "Ciklet e importuara llogariten si historik dhe statistika të vetë kësaj pajisjeje. Përdoreni për të zhvendosur një pajisje në një instalim të ri.", "sug_muted": "Ky cilësim nuk do të sugjerohet më", "sug_mute_failed": "Nuk mund të heshtte sugjerimi", "sug_unmuted_all": "Sugjerime të heshtura u rivendosën", - "n_suggestions_muted": "{count} të heshtura; motori i sugjerimeve nuk do t'i propozojë këto.", + "n_suggestions_muted": "{count} sugjerime të heshtura; motori i sugjerimeve nuk do t'i propozojë këto.", + "n_suggestions_muted_one": "{count} sugjerim i heshtur; motori i sugjerimeve nuk do ta propozojë.", + "n_suggestions_muted_other": "{count} sugjerime të heshtura; motori i sugjerimeve nuk do t'i propozojë këto.", "font_size_hint": "Bëni gjithçka në këtë panel më të madhe ose më të vogël. Aplikohet për llogarinë tuaj në këtë pajisje.", - "import_history_description": "Kishit priz inteligjent edhe përpara WashData? Ngarkoni një eksport të historikut të sensorit të fuqisë, ose lexojeni drejtpërdrejt nga Home Assistant, dhe zbulimi i zakonshëm kalon mbi të, kështu ciklet e kaluara shfaqen në listën e Cikleve, gati për t'u emërtuar.", - "hist_input_hint": "Ngarkoni një CSV të shkarkuar nga paneli i Historikut (entity, state, last changed), ose lini WashData t'i lexojë drejtpërdrejt historikun e sensorit. Më pas zbulimi kalon mbi të saktësisht si në kohë reale, dhe ju zgjidhni cilat nga ciklet e gjetura do të mbani.", - "hist_recorder_hint": "Lexon nga data që zgjidhni deri tani. Home Assistant mban historik të detajuar për 10 ditë si parazgjedhje dhe vetëm mesatare orare më pas, të cilat janë tepër të përafërta për të zbuluar cikle - zgjidhni një datë më të hershme vetëm nëse recorder është konfiguruar të mbajë më shumë.", - "hist_scanning": "Historiku po riluhet përmes zbuluesit. Kjo ekzekutohet në sfond - mund ta mbyllni këtë dritare dhe të ktheheni më vonë.", + "import_history_description": "Kishit priz inteligjent përpara WashData? Ngarkoni historikun e tij të fuqisë ose lexojeni nga Home Assistant. Zbulimi kalon mbi të dhe ciklet e kaluara shfaqen në listën e Cikleve, gati për t'u emërtuar.", + "hist_input_hint": "Ngarkoni një CSV nga paneli i Historikut (entity, state, last changed), ose lërini WashData të lexojë drejtpërdrejt historikun e sensorit. Zbulimi kalon mbi të si në kohë reale dhe ju zgjidhni cilat cikle të mbani.", + "hist_recorder_hint": "Lexon nga data që zgjidhni deri tani. Home Assistant mban historik të detajuar për 10 ditë si parazgjedhje; mesataret orare më të vjetra janë tepër të përafërta për zbulimin e cikleve.", + "hist_scanning": "Historiku po kalon nëpër zbulues në sfond. Mund ta mbyllni këtë dritare dhe të ktheheni më vonë.", "hist_imported_count": "{n} cikle u importuan.", "hist_duplicates": "{n} ishin importuar më parë dhe u anashkaluan.", "hist_capped": "U arrit kufiri i cikleve të importuara për pajisje; pjesa tjetër nuk u ruajt.", - "hist_next_step": "Ndodhen në listën e Cikleve, të shënuara si historik i importuar. Hapni një dhe përdorni Etiketoje për të emërtuar programin të cilit i përket.", + "hist_next_step": "Ndodhen në listën e Cikleve, të shënuara si historik i importuar. Hapni një dhe përdorni Etiketoje për të emërtuar programin e tij.", "hist_rows_read": "U lexuan {n} lexime", "hist_entity_substituted": "u lexua {used} (kjo pajisje është konfiguruar për {wanted})", "hist_breaks": "{n} boshllëqe ku sensori nuk ishte i disponueshëm", @@ -1089,26 +1018,73 @@ "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.", - "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.", + "imported_history_readonly": "Gjetur në historikun e importuar të fuqisë. Ndihmon përputhjen, por nuk llogaritet në statistikat tuaja dhe nuk mund të shkurtohet ose ndahet. Etiketojeni për të emërtuar programin.", + "odometer_intro": "Ky total vetëm rritet. Fshirja e një cikli nuk e ul, ndaj kujtesat e servisit mbeten të sakta.", + "odometer_correct_hint": "Vendoseni nëse WashData numëroi një cikël që nuk ndodhi kurrë, ose nëse pajisja kishte kryer cikle para se të instalohej WashData.", "no_reminders_set": "Nuk është caktuar ende asnjë kujtesë servisi.", "toast_program_armed": "Programi u caktua për ciklin tjetër: {program}", "standby_above_stop_title": "Ende tërheq {idle} W kur cikli përfundoi", "standby_above_stop_sub": "{n} nga {total} ciklet e fundit. Vonesa e fikjes fillon vetëm pasi fuqia bie nën Pragun e ndalimit ({stop} W), prandaj ciklet përfundojnë me vonesë ose duhen ndalur me dorë.", - "advisory_duration_outlier": "{n} cikël(e) të etiketuar si '{name}' janë shumë larg kohëzgjatjes së tij të zakonshme për t'u përputhur ndonjëherë me të (rasti më i keq: {ratio}x). Ka të ngjarë të jenë etiketuar gabimisht, ose një regjistrim i vetëm ka kapur dy ekzekutime. Rietiketojini ose ndajini, që të mos e shtrembërojnë më vlerësimin e kohës së këtij programi.", + "advisory_duration_outlier": "{n} cikël(e) të etiketuar si '{name}' janë shumë larg kohëzgjatjes së tij të zakonshme për t'u përputhur ndonjëherë (më i keqi: {ratio}x). Ka të ngjarë të jenë etiketuar gabim ose të përmbajnë dy cikle. Rietiketojini ose ndajini, që të mos e shtrembërojnë më vlerësimin e kohës.", "toast_range_required": "Vendosni një interval të vlefshëm Nga/Deri", - "no_profiles_option": "(asnjë profil, shtoni një)" + "no_profiles_option": "(asnjë profil, shtoni një)", + "compare_cycles_title": "Krahaso {count} cikle", + "edit_builtin_phase": "Kjo është një fazë e integruar. Ruajtja krijon një kopje të personalizuar; fshini kopjen për të rikthyer origjinalin.", + "fetch_error": "Ngarkimi i të dhënave dështoi.", + "connection_lost": "Lidhja humbi - po shfaqen të dhënat e orës {time}.", + "pg_notify_partial": "Shënuesit tregojnë vetëm njoftimet e fillimit, afër përfundimit, përfundimit, piketave dhe orëve të qeta. Përditësimet e drejtpërdrejta, kujtesat e shkarkimit dhe sinjalizimet e tejkalimit nuk simulohen.", + "connect_to_confirm": "Lidhuni te ingranazhi i cilësimeve për të konfirmuar ose vlerësuar.", + "coverage_gap_cluster": "{count} cikle të fundit të paetiketuara, me kohëzgjatje rreth {min} min, duken si një program që ende nuk e keni krijuar.", + "pg_label_yes": "Do të etiketohej \"{name}\" në fund të ciklit", + "pg_label_no": "Do të kërkonte rishikim: {reason}", + "live_match_debug_tip": "Besueshmëria: sa ngushtë përputhet kurba aktuale e fuqisë me profilin kandidat më të mirë (0-100%). E paqartë: pikët e dy kandidatëve më të mirë janë brenda 5% të njëri-tjetrit - etiketa mbetet e pasigurt derisa të përfundojë cikli.", + "automation_alias_started": "{device}: cikli filloi", + "automation_alias_finished": "{device}: cikli përfundoi", + "automation_desc_started": "Ekzekutohet kur fillon një cikël i {device}. Shtoni veprimet tuaja (njoftim, drita, ...).", + "automation_desc_finished": "Ekzekutohet kur përfundon një cikël i {device}. Shtoni veprimet tuaja (njoftim, drita, ...).", + "feedback_detected": "WashData zbuloi: {name}", + "import_undo_hint": "Ruhet një pikë rikthimi nga para importimit të fundit ({when}): {profiles} profil(e) dhe {cycles} cikël(cikle).", + "replace_undo_hint": "Fillimisht ruhet një pikë rikthimi, kështu që mund ta zhbëni këtë importim nga Eksporto / Importo.", + "pg_in_sample_note": "Këto cikle ndihmuan në ndërtimin e profileve me të cilat përputhen, prandaj rezultatet janë optimiste: në cikle të reja prisni pak më pak përputhje të sakta.", + "hist_tail_trimmed": "U shkurtuan {min} min pritjeje pas përfundimit të ciklit, ashtu siç i shkurton integrimi aktiv", + "hist_truncated": "u ndal te kufiri prej {n} leximesh, prandaj pjesa tjetër e historisë nuk u skanua", + "hist_warn_kw": "Leximi më i lartë është vetëm {w}. Ky sensor duket se raporton në kilovat; WashData ka nevojë për vat, prandaj nuk mund të zbulohet asnjë cikël.", + "pg_compare_profile_tip": "Zgjedh vetëm profilin që vizatohet mbi grafik. Simulimi gjithmonë e përputh ciklin vetë, ashtu si integrimi aktiv.", + "pg_outcome_stale": "Cilësimet kanë ndryshuar që kur u ekzekutua ky simulim. Ai ende tregon rezultatin e vjetër.", + "pg_sweep_guarded": "Përfundon më shumë cikle herët, ndan më shumë prej tyre ose humbet cikle që cilësimi aktual i kap, prandaj nuk rekomandohet kurrë.", + "pg_sweep_keep_current": "Mbani vlerën aktuale ({value}, {metric}): asnjë vlerë nuk doli më mirë me të paktën një cikël.", + "pg_task_busy": "Për këtë pajisje po ekzekutohet tashmë Testo në histori ose Optimizo. Prisni ose anulojeni.", + "hist_warn_naive_time": "{n} vula kohore nuk kanë zonë kohore dhe u lexuan si UTC. Nëse skedari përdor orën lokale, ciklet zhvendosen me diferencën tuaj nga UTC dhe leximet pranë ndryshimit të orës verore mund të humbasin.", + "hist_warn_naive_time_one": "{n} vulë kohore nuk ka zonë kohore dhe u lexua si UTC. Nëse skedari përdor orën lokale, ciklet zhvendosen me diferencën tuaj nga UTC dhe leximet pranë ndryshimit të orës verore mund të humbasin.", + "hist_warn_naive_time_other": "{n} vula kohore nuk kanë zonë kohore dhe u lexuan si UTC. Nëse skedari përdor orën lokale, ciklet zhvendosen me diferencën tuaj nga UTC dhe leximet pranë ndryshimit të orës verore mund të humbasin.", + "pg_sweep_n_cycles": "në {count} cikle", + "pg_sweep_n_cycles_one": "në {count} cikël", + "pg_sweep_n_cycles_other": "në {count} cikle", + "cycle_context_loading": "Duke ngarkuar historikun e regjistruesit…", + "cycle_context_note": "Gri: historiku i regjistruesit për sensorin e fuqisë rreth këtij cikli. Nuk është pjesë e ciklit dhe nuk ndikon në kohëzgjatje, energji ose përputhje.", + "cycle_context_none": "Regjistruesi nuk ka lexime rreth këtij cikli (si parazgjedhje Home Assistant ruan historikun e 10 ditëve).", + "cycle_context_tip": "Vetëm për shfaqje: tregon historikun e regjistruesit për sensorin e fuqisë rreth këtij cikli, nëse ruhet ende. Për të shtuar përpjekjet e dështuara të nisjes në ciklet e ardhshme, përdorni Sekondat hyrëse të kurbës (Cilësimet > Zbulim > Fillimi i ciklit), që ndryshon kohëzgjatjet e ruajtura.", + "invalid_threshold_pair": "Pragu i ndalimit ({stop} W) duhet të jetë më i ulët se Pragu i nisjes ({start} W). Nuk u ruajt asgjë; ndryshojini të dy bashkë." }, "phase_desc": { "anti_crease": "Rrotullime të shkurtra të herëpashershme pas përfundimit për të reduktuar rrudhat.", "anti_wrinkle": "Rrotullimi periodik pas ciklit për të reduktuar rrudhat.", + "baking": "Elementi ngrohës punon në temperaturë të lartë për të formuar koren dhe brendësinë.", + "cooking": "Faza kryesore e gatimit me ngrohësin dhe ventilatorin në punë.", "cool_down": "Rrotullimi pa ngrohje afër fundit të ciklit.", + "cool_down_af": "Ftohje vetëm me ventilator pas ngrohjes.", "drain_and_switch": "Periudha e tranzicionit nga mënyra e larjes në tharje.", "dry": "Faza e tharjes duke përdorur ngrohës dhe/ose nxehtësi të mbetur.", "drying": "Periudha kryesore e tumbullimit me ngrohje.", "heat_up": "Ngrohja fillestare e ngrohësit përpara se të fillojë tharja e plotë.", + "keep_warm": "Mbajtje me nxehtësi të ulët që ushqimi të qëndrojë i ngrohtë.", + "keep_warm_bm": "Mbajtje me nxehtësi të ulët që buka të qëndrojë e ngrohtë pas pjekjes.", + "kneading": "Motori përzien dhe mbrun brumin. Konsum i lartë i fuqisë.", + "pause_af": "Pauzë e shkurtër për tundje ose kontroll.", + "pre_heat": "Ngrohja fillestare e dhomës përpara gatimit të plotë.", "pre_rinse": "Spërkatja fillestare përpara larjes me detergjent.", + "proving": "Ngritje me nxehtësi të ulët: majaja fermenton dhe brumi fryhet.", + "resting": "Pauzë e shkurtër me fuqi të ulët midis fazave të mbrujtjes, që gluteni të çlodhet.", "pre_wash": "Thithja fillestare ose trajtimi paraprak përpara larjes kryesore.", "rinse": "Faza e shpëlarjes me ujë të pastër. Kjo fazë mund të përsëritet disa herë.", "sanitize": "Faza e pastrimit me temperaturë të lartë për programet e higjenizimit.", @@ -1121,6 +1097,7 @@ "wash_dw": "Larja kryesore me detergjent me ngrohje." }, "placeholder": { + "custom_task": "p.sh. Pastro guarnicionin e derës", "add": "shto…", "group_name": "p.sh. Pambuk 2:47", "json_buttons": "[{\"action\":\"ID\",\"title\":\"Label\"}]", @@ -1147,7 +1124,7 @@ }, "section": { "anti_wrinkle": { - "intro": "Mënyra kundër rrudhave të rrobave zbulon pulsimet e tumblimit me fuqi të ulët pas fazës kryesore dhe i mban ato të lidhura me ciklin e përfunduar.", + "intro": "Mban me ciklin e përfunduar pulset e rrotullimit me fuqi të ulët të tharëses pas fazës kryesore.", "label": "Kundër rrudhave" }, "basic": { @@ -1155,7 +1132,7 @@ "label": "Bazike" }, "delay": { - "intro": "Zbulimi i nisjes së vonuar identifikon kur pajisja është ndezur por nuk ka filluar ende ciklin.", + "intro": "Zbulon një pajisje që është e ndezur dhe pret një nisje të vonuar.", "label": "Nisje e vonuar" }, "detection": { @@ -1179,161 +1156,153 @@ "label": "Kohëzgjatja dhe rojtar" }, "triggers": { - "intro": "Sinjalet e jashtme opsionale: aktivizues përfundimi, senzori i derës, çelësi i pauzës dhe kujtues për shkarkim.", + "intro": "Shtesa opsionale: aktivizues përfundimi, sensori i derës, çelësi i pauzës dhe kujtesa për shkarkim.", "label": "Aktivizues dhe dera" }, - "phase_eta": { - "label": "Koha e Mbetur", - "intro": "Kohë e mbetur e ndërgjegjshme për fazat, për makinat gjatësia e ciklit të të cilave varet nga temperatura ose centrifugimi." - }, "dishwasher": { "label": "Pjatalarëse", - "intro": "Trajtimi i fundit të ciklit për pjatalarëset, të cilat zakonisht përfundojnë me një fazë të gjatë tharjeje pothuajse pa konsum energjie përpara një shkarkimi të shkurtër përfundimtar." + "intro": "Trajtimi i fundit për pjatalarëset, që shpesh thahen gjatë në heshtje përpara një zbrazjeje të shkurtër përfundimtare të ujit." } }, "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.", + "doc": "Pjesa e kohëzgjatjes mesatare të programit që një cikël duhet të arrijë përpara se rrotullimi me fuqi të ulët të llogaritet si bishti kundër rrudhave. Uleni (p.sh. 0.75) te tharëset që përshtaten sipas ngarkesës. Makinat larëse lërini në vlerën e parazgjedhur (0.98), përndryshe një larje mund të përfundojë në gjysmë.", "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.", + "doc": "Mban me ciklin e përfunduar pulset e shkurtra me fuqi të ulët të tharëses pas fazës kryesore të nxehjes, në vend që t'i lexojë si cikle të reja.", "label": "Aktivizo zbulimin kundër rrudhave" }, "anti_wrinkle_exit_power": { - "doc": "Fuqia duhet të bjerë nën këtë mes pulseve që modaliteti kundër rrudhave të qëndrojë aktiv.", + "doc": "Në modalitetin kundër rrudhave, fuqia nën këtë vlerë llogaritet si qetësi mes pulseve të rrotullimit, dhe qetësia më e gjatë se Hendeku maks. i pulseve e mbyll modalitetin. Nëse Pragu i ndalimit është më i lartë, përdoret ai, prandaj ky cilësim ndryshon diçka vetëm kur vendoset mbi Pragun e ndalimit.", "label": "Pragu i fuqisë së daljes" }, "anti_wrinkle_idle_timeout": { "label": "Hendeku maks. i pulseve", - "doc": "Sa gjatë mund të qëndrojë e qetë makina mes dy pulseve të rrotullimit para se të mbarojë modaliteti kundër rrudhave. Vendoseni mbi hendekun më të gjatë që lë tharësja juaj mes pulseve, përndryshe çdo puls i mëvonshëm lexohet si nisje e gabuar." + "doc": "Sa gjatë mund të qëndrojë e qetë tharësja mes pulseve të rrotullimit përpara se të mbarojë modaliteti kundër rrudhave. Vendoseni mbi hendekun më të gjatë të tharëses suaj, përndryshe pulset e mëvonshme lexohen si nisje të gabuara." }, "anti_wrinkle_max_duration": { - "doc": "Pulset më të gjata se kjo trajtohen si një cikël i vërtetë dhe jo si një rrëshqitje kundër rrudhave.", + "doc": "Një puls më i gjatë se kjo llogaritet si cikël i vërtetë, jo si rrotullim kundër rrudhave.", "label": "Kohëzgjatja maks." }, "anti_wrinkle_max_power": { - "doc": "Një puls mbi këtë fuqi trajtohet si një cikël i ri i vërtetë, jo një rrëshqitje kundër rrudhave. Vendoseni pak mbi fuqinë e pulsit të rënies.", + "doc": "Një puls mbi këtë fuqi llogaritet si cikël i ri i vërtetë, jo si rrotullim. Vendoseni pak mbi fuqinë e pulseve të rrotullimit.", "label": "Fuqia maks. kundër rrudhave" }, "auto_label_confidence": { - "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.", + "doc": "Rezultati i përputhjes në fund të ciklit, në ose mbi të cilin programi etiketohet pa pyetur. Rriteni për më shumë siguri; uleni për të etiketuar automatikisht më shumë cikle. Mbajeni më të lartë se Besueshmëria e të mësuarit.", "label": "Besueshmëria e etiketimit automatik" }, "auto_maintenance": { - "doc": "Drejtoni mbajtjen e shtëpisë gjatë natës: rindërtoni zarfet e profilit, rillogaritni shëndetin e ciklit, krasitni gjurmët e korrigjimit dhe ruani ciklet më të fundit.", + "doc": "Pastrim i natës: etiketon ciklet e vjetra që tani përputhen qartë me një program, rindërton kurbat e programeve, rifreskon shenjat e anomalive dhe shëndetin e cikleve, dhe heq gjurmët e korrigjimit nëse \"Ruaj gjurmët e korrigjimit\" është joaktive. Ciklet dhe gjurmët e tyre të fuqisë ruhen gjithmonë.", "label": "Mirëmbajtje automatike (pastrimi nate)" }, "completion_min_seconds": { - "doc": "Ciklet më të shkurtra se kjo hidhen si cikle fantazmë (vrapime testimi, hapja e derës për të shtuar një çorape).", + "doc": "Ciklet më të shkurtra se kjo (nisje provë, hapja e derës për të shtuar një çorape) ruhen si I ndërprerë dhe nuk dërgojnë njoftim përfundimi.", "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.", + "doc": "Shton në fillim të ciklit leximet nga përpjekjet e dështuara të nisjes, për makinat që provojnë para nisjes (bllokimi i derës, mbushja e parë). 300 s mjaftojnë për shumicën; 0 = joaktive. Fillimet e regjistruara zhvendosen më herët, ndaj kohëzgjatjet e mësuara ndryshojnë për një kohë.", "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ë.", + "doc": "Fuqia duhet të qëndrojë në brezin e gatishmërisë kaq gjatë përpara se pajisja të llogaritet si në pritje për nisje.", "label": "Sekonda konfirmimi" }, "delay_start_detect_enabled": { - "doc": "Zbuloni kur pajisja është e ndezur dhe është në pritje (fillimi me vonesë / gatishmëria), por nuk e ka filluar ciklin e saj, kështu që tërheqja e gatishmërisë nuk ngatërrohet me një cikël funksionimi.", + "doc": "Zbulon një pajisje që është e ndezur dhe pret një nisje të vonuar, që konsumi i gatishmërisë të mos merret si cikël në punë.", "label": "Aktivizo zbulimin e nisjes së vonuar" }, "delay_timeout_hours": { - "doc": "Ndaloni së prituri në modalitetin e nisjes me vonesë pas kaq shumë orësh dhe kthehuni në gjendje boshe, kështu që një makinë e lënë e ndezur, por nuk u ndez kurrë, nuk pret përgjithmonë.", + "doc": "Pas kaq orësh ndalon pritjen për nisjen e vonuar dhe kthehet në gjendjen joaktive.", "label": "Orë kohe skadimi" }, "device_type": { - "doc": "Klasa e pajisjes. Vendos standardet e paracaktuara të ndjeshme të zbulimit (pragjet, vonesa jashtë vonesës, trajtimi në fund) të akorduara për atë lloj pajisjeje; ndryshoni atë vetëm nëse pajisja fillimisht ishte konfiguruar si lloji i gabuar.", + "doc": "Vendos parazgjedhjet e zbulimit (pragjet, vonesa e fikjes, trajtimi i fundit) për këtë lloj pajisjeje. Ndryshojeni vetëm nëse pajisja u konfigurua me llojin e gabuar.", "label": "Lloji i pajisjes" }, "door_sensor_entity": { - "doc": "Sensori binar opsional i derës. Përdoret për të zbuluar kur pajisja është hapur/shkarkuar pas një cikli.", + "doc": "Sensor opsional i derës. Tregon kur pajisja u hap për t'u shkarkuar pas një cikli.", "label": "Entiteta e sensorit të derës" }, "duration_tolerance": { - "doc": "Toleranca për vlerësimet e mbetura të kohës (reagimi i të mësuarit, jo përputhje). Nëse kohëzgjatja aktuale është brenda +/-X% e vlerësimit, ajo llogaritet si një përputhje e mirë.", + "doc": "Vetëm për reagimin e të mësuarit: një cikël brenda +/-X% të kohëzgjatjes së vlerësuar llogaritet si vlerësim i mirë. Nuk ndikon në përputhje.", "label": "Toleranca e vlerësimit" }, "enable_ml_models": { - "doc": "Ndërsa një cikël funksionon, lërini modelet të përsosin rezultatet e drejtpërdrejta: një vlerësim më i qëndrueshëm i kohës së mbetur dhe energjisë/kostos, dhe një mbrojtës kundër ndalimit të parakohshëm në zbulimin e përfundimit (ai mund të vonojë ndonjëherë përfundimin, të mos përfundojë kurrë më herët dhe të jetë i kufizuar). Përdor modelet e tua të akorduara mirë kur janë të disponueshme, përndryshe ato të integruara. Off = vetëm logjika klasike e bazuar në fuqi (ende e besueshme).", + "doc": "Përdor një model të trajnuar në këtë makinë për të parashikuar energjinë dhe koston e një cikli në punë, dhe lejon një program të njohur mirë që ndalon jashtëzakonisht herët të përfundojë pa pritur gjithë vonesën e fikjes. Joaktiv: vetëm logjika e bazuar në fuqi.", "label": "Apliko modele inteligjente gjatë ciklit" }, "end_energy_threshold": { - "doc": "Gjatë numërimit jashtë vonesës, energjia e akumuluar (vat x kohë) krahasohet me këtë prag. Nëse tejkalohet, numërimi mbrapsht rivendoset - duke i mbajtur bishtat kundër rrudhave dhe tharjen e lavastoviljes të lidhura me ciklin në vend që t'i shkurtoni ato. Rriteni nëse ciklet përfundojnë shumë herët gjatë ftohjes; uleni nëse zbulimi është i ngadaltë.", + "doc": "Energjia e lejuar gjatë vonesës së fikjes. Nëse kalohet, cikli vazhdon, që rrotullimet dhe bishtat e tharjes të mbeten me ciklin. Uleni nëse ciklet përfundojnë gjatë një bishti; rriteni nëse ciklet e përfunduara vazhdojnë të presin.", "label": "Energjia e fundit" }, - "end_repeat_count": { - "doc": "Numri i leximeve të njëpasnjëshme nën pragun e ndalimit të kërkuar para përfundimit të ciklit. 1 është e mirë për shumicën e prizave. Rriteni në 2-3 nëse spina juaj inteligjente raporton herë pas here një mostër të rreme me zero në mes të ciklit dhe ciklet tuaja mbarojnë para kohe.", - "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). 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.", + "doc": "Sensor me çmimin aktual për kWh (p.sh. një tarifë dinamike). Ka përparësi ndaj çmimit statik më poshtë, prandaj duhet të jetë një çmim, jo sensori i fuqisë ose i energjisë së prizës.", "label": "Entiteta e çmimit të energjisë" }, "energy_price_static": { - "doc": "Çmimi fiks për kWh i përdorur për shifrat e kostos kur nuk është vendosur më sipër asnjë subjekt i çmimit të drejtpërdrejtë.", + "doc": "Çmimi fiks për kWh, i përdorur kur nuk është vendosur asnjë entitet çmimi.", "label": "Çmimi statik i energjisë (për kWh)" }, "energy_sensor": { - "doc": "Numërues kumulativ opsional i energjisë (total_increasing kWh/Wh, p.sh. matësi i vetë prizës për konsumin total). Kur vendoset, energjia e raportuar e çdo cikli merret nga diferenca e këtij numëruesi nga fillimi deri në fund, gjë që shmang nënllogaritjen që merrni nga integrimi i një sensori fuqie që raporton ngadalë. Kthehet te vlera e integruar nëse leximi mungon, njësia e tij është e panjohur, ose diferenca nuk është pozitive. Lëreni bosh për të vazhduar integrimin e sensorit të fuqisë.", + "doc": "Numërues opsional kWh ose Wh (total_increasing, p.sh. matësi i vetë prizës). Energjia e ciklit lexohet prej tij, gjë që është më e saktë se llogaritja nga një sensor fuqie i ngadaltë. Nëse leximi i tij nuk përdoret dot, përdoret vlera e llogaritur.", "label": "Entiteti i matësit të energjisë" }, "expose_debug_entities": { - "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.", + "doc": "Shton entitete diagnostikuese (besueshmëria e përputhjes, paqartësia, gjendja e brendshme). Joaktiv e mban listën e entiteteve të pastër.", "label": "Ekspono entitetet e korrigjimit" }, "external_end_trigger": { - "doc": "Sensori binar ndryshimi i gjendjes së të cilit shënon fundin e ciklit (p.sh. një kontakt \"e përfunduar\" i pajisjes ose një integrim shoqërues).", + "doc": "Sensor binar, ndryshimi i të cilit shënon fundin e ciklit (p.sh. një kontakt \"e përfunduar\" ose një integrim tjetër).", "label": "Entiteta e aktivizuesit të jashtëm" }, "external_end_trigger_enabled": { - "doc": "Lëreni një sensor të jashtëm binar të sinjalizojë fundin e një cikli, përveç zbulimit të integruar të bazuar në fuqi.", + "doc": "Lejon një sensor binar të sinjalizojë fundin e një cikli, përveç zbulimit të bazuar në fuqi.", "label": "Aktivizo aktivizuesin e jashtëm të fundit" }, "external_end_trigger_inverted": { - "doc": "Trajtojeni fikjen e sensorit të këmbëzës (në vend të ndezjes) si sinjalin e fundit të ciklit.", + "doc": "Trajtojeni kalimin e sensorit në OFF (jo ON) si sinjal përfundimi.", "label": "Invertoj aktivizuesin e jashtëm (aktivizoj kur FIKUR)" }, "learning_confidence": { - "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.", + "doc": "Rezultati më i ulët i përputhjes për të etiketuar një cikël të përfunduar. Një fitues i qartë etiketohet; një rast i ngushtë shkon në radhën e rishikimit te Cikle. Nën këtë vlerë nuk etiketohet dhe nuk pyetet asgjë. Mbajeni më të ulët se Besueshmëria e etiketimit automatik.", "label": "Besueshmëria e të mësuarit" }, "linked_device": { - "doc": "Futeni opsionalisht këtë pajisje WashData nën një pajisje tjetër (p.sh. spinën inteligjente) në regjistrin e pajisjes HA, e shfaqur si \"Connected via ...\".", + "doc": "Opsionale. Shfaq këtë pajisje WashData nën një pajisje tjetër (p.sh. spinën inteligjente) në Home Assistant, si \"Connected via ...\".", "label": "Grupo nën pajisje" }, "min_off_gap": { - "doc": "Nëse makina fiket për më pak se kjo kohë, sekuenca e ndezjes/fikjes/ndezjes trajtohet si një cikël i vazhdueshëm. Parandalon ndarjen e programeve të njomjes (makina fiket për disa minuta në mes të larjes) në dy cikle të veçanta. Vendoseni atë më të shkurtër se hendeku midis ngarkesave tuaja të pasme nëse dëshironi që ato të numërohen si cikle të veçanta. Parazgjedhjet e tipit të pajisjes mbrojnë pauzën tipike brenda ciklit për çdo pajisje.", + "doc": "Një periudhë fikjeje më e shkurtër se kjo nuk e ndan ciklin, që pauzat e njomjes të mbeten në një larje. Mbajeni më të shkurtër se pauza midis ngarkesave njëra pas tjetrës, përndryshe ato bashkohen në një cikël.", "label": "Hendeku min. i fikjes" }, "min_power": { - "doc": "Fuqia minimale absolute konsiderohet aktive. Leximet poshtë kësaj trajtohen si 0 W (në gatishmëri), duke filtruar ngarkesën fantazmë të prizave inteligjente dhe LED-ve në gatishmëri.", + "doc": "Leximet nën këtë vlerë llogariten si 0 W (gatishmëri). Filtron konsumin në pritje të prizave inteligjente dhe LED-ve të gatishmërisë.", "label": "Fuqia minimale" }, "ml_training_enabled": { - "doc": "Studioni periodikisht ciklet tuaja të rishikuara gjatë natës dhe rregulloni modelet me këtë makinë specifike. Një ndryshim mbahet vetëm kur me të vërtetë ka rezultate më të mira në ciklet e zgjatura, kështu që kjo mund të ndihmojë ose të mbetet e njëjtë - mos u regresoni kurrë.", + "doc": "Ritrajnon herë pas here modelin e energjisë me ciklet e përfunduara të kësaj makine. Një model i ri mbahet vetëm nëse e tejkalon vlerësimin e thjeshtë në ciklet e testimit.", "label": "Mëso nga kjo makinë" }, "ml_training_hour": { - "doc": "Ora lokale e ditës (0-23) për të bërë rregullimin e saktë gjatë natës. Zgjidh një orë qetësie si p.sh. 2 (02:00).", + "doc": "Ora e ditës (0-23) për trajnimin e natës. Zgjidhni një orë të qetë, p.sh. 2.", "label": "Mëso në orën" }, "ml_training_interval_days": { - "doc": "Ri-kontrolloni për përmirësime maksimumi një herë në kaq ditë.", + "doc": "Ritrajnon më së shumti një herë në kaq ditë.", "label": "Kontrolloje më shumë çdo" }, "ml_training_min_cycles": { - "doc": "Prisni derisa të regjistrohen të paktën kaq shumë cikle përpara akordimit, kështu që ka mjaft për të mësuar.", + "doc": "Pret të paktën kaq cikle të regjistruara përpara trajnimit.", "label": "Ciklet e nevojshme fillimisht" }, "name": { - "doc": "Emri i shfaqur shfaqet në listën e integrimeve HA dhe regjistrin e pajisjes.", + "doc": "Emri që shfaqet në Home Assistant për këtë pajisje.", "label": "Emri i pajisjes" }, "no_update_active_timeout": { - "doc": "Nëse nuk vijnë përditësime të energjisë për kaq gjatë funksionimit, supozoni se priza ka rënë jashtë linje dhe ndaloni me forcë për të shmangur një cikël zombie. 600 s i parazgjedhur lejon vonesën në re ose rrjetë.", + "doc": "Nëse gjatë një cikli nuk vjen asnjë përditësim fuqie për kaq kohë, supozohet se priza doli jashtë linje dhe cikli ndalet me forcë. Parazgjedhja 600 s lejon vonesat e resë ose të rrjetit mesh.", "label": "Koha e skadimit pa përditësim" }, "notify_before_end_minutes": { @@ -1341,35 +1310,35 @@ "label": "Alarm para fundit" }, "notify_channel": { - "doc": "Emri i kanalit të njoftimeve në Android për mesazhet e fillimit/live (kontrollon tingullin për kanal dhe përparësinë në aplikacionin celular). Blank përdor parazgjedhjen e aplikacionit shoqërues.", + "doc": "Kanali Android për mesazhet e fillimit dhe live; kanali cakton tingullin dhe përparësinë. Bosh: parazgjedhja e aplikacionit.", "label": "Kanal Android (nisje/live)" }, "notify_cycle_timers": { - "doc": "Njoftimet në minuta specifike në një cikël (p.sh. për të shtuar zbutës). Mesazhi mbështet {device}, {program}, {minutes}. Aktivizo pauzën automatike për të ndalur në atë pikë dhe për të marrë një njoftim interaktiv me një buton Resume; rifilloni nëpërmjet panelit, shërbimit të ndalimit/rifillimit ose veprimit të njoftimit.", + "doc": "Njoftime në minuta të caktuara të ciklit (p.sh. për të shtuar zbutës). Mesazhi mbështet {device}, {program}, {minutes}. Me pauzën automatike, cikli vihet në pauzë aty dhe njoftimi merr një buton Vazhdo.", "label": "Kohëmatës cikli" }, "notify_finish_channel": { - "doc": "Emri i kanalit të njoftimit në Android për mesazhin e përfundimit. Blank ripërdor kanalin start/live.", + "doc": "Kanali Android për mesazhin e përfundimit. Bosh: ripërdoret kanali i fillimit/live.", "label": "Kanal Android (përfundim)" }, "notify_finish_message": { - "doc": "Trupi dërgohet kur përfundon një cikël. Variablat shabllon: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}. {time_finished} dhe {vs_typical} janë më të dobishmet këtu.", + "doc": "Teksti që dërgohet kur përfundon një cikël. Variablat: {device}, {program}, {duration} (minuta), {duration_hm} (1 h 05 min), {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}, {status} (completed ose force_stopped; ciklet e ndërprera nuk dërgojnë asgjë). Me çdo variabël tjetër përdoret teksti i paracaktuar.", "label": "Mesazh përfundimi" }, "notify_finish_services": { - "doc": "njofto.* shërbimet thirren kur përfundon një cikël. Shtoni një për objektiv; lëreni bosh për asnjë njoftim përfundimtar.", + "doc": "Shërbimet notify.* që thirren kur përfundon një cikël. Lëreni bosh për asnjë.", "label": "Shërbime përfundimi" }, "notify_fire_events": { - "doc": "Gjithashtu aktivizoni ngjarjet ha_washdata_* në fillimin/mbarimin e ciklit në mënyrë që të mund të ndërtoni automatizimet tuaja.", + "doc": "Lësho gjithashtu ngjarjet ha_washdata_* në fillimin dhe përfundimin e ciklit, për automatizimet tuaja.", "label": "Aktivizo ngjarje HA për njoftime" }, "notify_icon": { - "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.", + "doc": "Ikona e vogël në njoftim (p.sh. mdi:washing-machine). Dërgohet vetëm te marrësit mobile_app, jo te grupet notify. Në iOS zëvendëson ikonën e aplikacionit dhe kërkon aplikacionin shoqërues 2026.8 ose më të ri. Bosh: parazgjedhja e platformës.", "label": "Ikona e njoftimit" }, "notify_live_chronometer": { - "doc": "Trego një kohëmatës numërimi mbrapsht që përditësohet drejtpërdrejt në njoftim (në platformat që e mbështesin atë) në vend të një vlerësimi statik.", + "doc": "Shfaq në njoftim një kohëmatës live numërimi mbrapsht (ku mbështetet) në vend të një vlerësimi fiks.", "label": "Përdor kronometër live" }, "notify_live_interval_seconds": { @@ -1377,139 +1346,131 @@ "label": "Intervali i përditësimeve live" }, "notify_live_overrun_percent": { - "doc": "Nëse një cikël e kalon vlerësimin e tij me më shumë se kjo përqindje, dërgoni një alarm mbi tejkalimin.", + "doc": "Përditësimet live ndalen kur një cikël zgjat kaq përqind më shumë se kohëzgjatja e pritshme (20 = deri në 1,2 herë kohën e pritshme).", "label": "% tejkalimi para alarmit" }, "notify_live_services": { - "doc": "njoftime.* shërbimet thirrën për përditësime të drejtpërdrejta të progresit gjatë ekzekutimit të një cikli. Lëreni bosh për të çaktivizuar njoftimet e progresit të drejtpërdrejtë.", + "doc": "Shërbimet notify.* për përditësimet live të progresit gjatë ciklit. Lëreni bosh për t'i çaktivizuar.", "label": "Shërbime progres live" }, "notify_only_when_home": { - "doc": "Dërgoni njoftime vetëm kur të paktën një nga personat e lidhur (sipër) është në shtëpi.", + "doc": "Njofto vetëm kur të paktën një nga personat më sipër është në shtëpi.", "label": "Njofto vetëm kur në shtëpi" }, "notify_people": { - "doc": "person.* entitete të përdorura nga \"Njofto vetëm kur në shtëpi\" për të vendosur nëse dikush është në shtëpi.", + "doc": "Entitetet person.* që kontrollon \"Njofto vetëm kur në shtëpi\".", "label": "Njerëzit (për Vetëm kur në shtëpi)" }, "notify_pre_complete_message": { - "doc": "Trupi i alarmit para përfundimit / gati së shpejti. Variablat shabllon: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Teksti i çdo përditësimi live të progresit. Variablat shabllon: {device}, {program}, {minutes} (minutat e mbetura). Me çdo variabël tjetër përdoret teksti i paracaktuar.", "label": "Mesazh para-përfundimi" }, "notify_reminder_message": { - "doc": "Trupi i kujtesës që rrobat ende presin të hiqen. Bosh përdor vlerën e paracaktuar të integruar. Variablat shabllon: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Teksti i Alarmit para fundit, që dërgohet një herë. Variablat shabllon: {device}, {program}, {minutes} (cilësimi Alarm para fundit). Me çdo variabël tjetër përdoret teksti i paracaktuar.", "label": "Mesazh kujtues" }, "notify_start_message": { - "doc": "Trupi dërgohet kur fillon një cikël. Variablat shabllon: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Teksti që dërgohet kur fillon një cikël. Variablat shabllon: {device}, {program}. Me çdo variabël tjetër përdoret teksti i paracaktuar.", "label": "Mesazh nisje" }, "notify_start_services": { - "doc": "njofto.* shërbimet e thirrura kur fillon një cikël. Shto një për objektiv (telefon, panel, etj.); lini bosh pa njoftim për fillim.", + "doc": "Shërbimet notify.* që thirren kur fillon një cikël (telefon, panel kontrolli, ...). Lëreni bosh për asnjë.", "label": "Shërbime nisje" }, "notify_timeout_seconds": { - "doc": "Hiq automatikisht njoftimin pas kaq shumë sekondash (në platformat që e mbështesin atë). 0 e mban atë derisa të hiqet manualisht.", + "doc": "Hiq njoftimin pas kaq sekondash (ku mbështetet). 0 e mban derisa ta hiqni vetë.", "label": "Mbylle automatikisht pas" }, "notify_title": { - "doc": "Titulli i njoftimit. Variablat shabllon: {device}, {program}, {duration}, {energy}, {cost}, {date}, {time}, {minutes}.", + "doc": "Titulli i njoftimit. Zëvendësohet vetëm {device}; me çdo variabël tjetër përdoret titulli i paracaktuar.", "label": "Titulli i njoftimit" }, "notify_unload_delay_minutes": { - "doc": "Minuta pasi përfundon një cikël përpara se të dërgoni alarmin rikujtues \"shkarkoni makinën\" ende në pritje. Vendosni 0 për të çaktivizuar kujtesën.", + "doc": "Minuta pas përfundimit të ciklit përpara kujtesës \"shkarkoni makinën\". 0 e çaktivizon.", "label": "Vonesë njoftimi për shkarkim" }, "notify_unload_repeat": { - "doc": "Ridërgon kujtesën për shkarkim çdo \"Vonesë njoftimi për shkarkim\" minuta, derisa shkarkimi të konfirmohet ose të prekni \"Ndalo kujtesat\" në njoftim. Nevojitet një mënyrë për të konfirmuar shkarkimin: Entiteti i sensorit të derës, Entiteti i konfirmimit të shkarkimit, ose opsioni \"Konfirmo shkarkimin manualisht\" më poshtë. Butoni i largimit funksionon në njoftimet e aplikacionit-shoqërues (celular) të Home Assistant.", + "doc": "Ridërgon kujtesën për shkarkim çdo \"Vonesë njoftimi për shkarkim\" minuta, derisa shkarkimi të konfirmohet ose të prekni \"Ndalo kujtesat\" (aplikacioni celular). Kërkon një nga këto: Entiteta e sensorit të derës, Entiteti i konfirmimit të shkarkimit ose \"Konfirmo shkarkimin manualisht\".", "label": "Përsërit derisa të shkarkohet" }, "off_delay": { - "doc": "Koha për të pritur pasi të bjerë energjia përpara se të deklaroni ciklin të përfunduar. Nëse rryma rifillon brenda kësaj dritareje, cikli vazhdon pa probleme - kjo lidh pauzat midis fazave të larjes. Enëlarëset kanë faza të gjata tharjeje (fikje për 20-60 min), kështu që vonesa e fikjes duhet ta kalojë atë për të mbajtur të gjithë larjen+tharjen si një cikël.", + "doc": "Sa gjatë duhet të qëndrojë fuqia e ulët përpara se cikli të përfundojë. Nëse fuqia rikthehet brenda kësaj kohe, cikli vazhdon. Pjatalarëset mund të qëndrojnë joaktive 20-60 min gjatë tharjes, ndaj vendoseni më gjatë se kaq.", "label": "Vonesë fikjeje" }, "pause_cuts_power": { - "doc": "Kur një cikël është në pauzë, fikni gjithashtu Entitetin e Ndërprerjes më poshtë. Vetëm për pajisjet, priza e të cilave mund të pritet në mënyrë të sigurt në mes të ciklit.", + "doc": "Kur një cikël vihet në pauzë, fiket gjithashtu Entiteta e çelësit më poshtë. Vetëm për pajisjet që mund të mbeten pa rrymë në mes të ciklit pa rrezik.", "label": "Pauzë gjithashtu pret fuqinë (nëpërmjet çelësit)" }, "power_off_delay": { - "doc": "Sa kohë duhet të qëndrojë fuqia nën Pragun e fikjes së energjisë pasi një cikël të përfundojë përpara se gjendja të kthehet në „Fikur“. Përdoret vetëm kur Pragu i fikjes së energjisë është mbi 0. Kontrollohet në ritmin e sfondit, kështu që vonesa efektive rrumbullakoset lart deri te rrahja tjetër e skadimit të gjendjes.", + "doc": "Sa gjatë duhet të qëndrojë fuqia më e ulët se Pragu i fikjes së energjisë pas një cikli, përpara se gjendja të kthehet në „Fikur“. Përdoret vetëm kur ky prag është mbi 0. Kontrollohet periodikisht, ndaj pritja reale mund të jetë pak më e gjatë.", "label": "Vonesa e fikjes së energjisë" }, "power_off_threshold_w": { - "doc": "Zbulim opsional i gjendjes Fikur bazuar në fuqinë. Kur është mbi 0, pasi një cikël të ketë përfunduar dhe fuqia të qëndrojë nën këtë nivel për Vonesën e fikjes së energjisë, makina trajtohet si e fikur dhe gjendja kthehet në „Fikur“. Lëreni në 0 për ta çaktivizuar (parazgjedhja). Vendoseni mbi nivelin real të fikjes dhe nën Pragun e ndalimit dhe nën konsumin në gatishmëri të makinës suaj (e përfunduar, por ende e ndezur); nëse nuk është nën Pragun e ndalimit, ai injorohet. Kur aktivizohet, ai zëvendëson Vonesën e rivendosjes së progresit për kthimin në „Fikur“, kështu që një makinë e përfunduar qëndron në „Përfunduar/I pastër“ derisa të fiket vërtet.", + "doc": "Pas një cikli, nëse fuqia qëndron nën këtë vlerë për kohën e caktuar te Vonesa e fikjes së energjisë, gjendja kalon në „Fikur“, në vend që ta bëjë këtë Vonesë rivendosjeje të progresit. Vendoseni më të lartë se konsumi i pajisjes së fikur dhe më të ulët se konsumi në gatishmëri dhe se Pragu i ndalimit. 0 e çaktivizon (parazgjedhja).", "label": "Pragu i fikjes së energjisë" }, "power_sensor": { - "doc": "Subjekti sensor që raporton fuqinë e drejtpërdrejtë në vat për këtë pajisje (p.sh. sensor.washer_power). I gjithë zbulimi i ciklit bazohet në këtë sinjal.", + "doc": "Sensori që raporton fuqinë live të kësaj pajisjeje në vat (p.sh. sensor.washer_power). I gjithë zbulimi bazohet në të.", "label": "Sensori i fuqisë" }, - "profile_duration_tolerance": { - "doc": "Brezi +/- rreth një kohëzgjatjeje mesatare të profilit të përdorur gjatë përputhjes. 0.25 do të thotë që një profil 60 min përputhet me ciklet 45-75 min.", - "label": "Toleranca e kohëzgjatjes së profilit" - }, "profile_match_interval": { - "doc": "Sa shpesh të provoni përputhjen e profilit gjatë një cikli vrapimi. 300 s (5 minuta) e parazgjedhur balancon shpejtësinë e zbulimit dhe CPU.", + "doc": "Sa shpesh përputhet një cikël në punë me profilet. Parazgjedhja 300 s (5 min).", "label": "Intervali i përputhjes" }, "profile_match_max_duration_ratio": { - "doc": "Gjatësia maksimale e ciklit në lidhje me profilin. 1.3 do të thotë që një cikël duhet të jetë nën 130% të kohëzgjatjes së profilit për t'u përshtatur.", + "doc": "Një program hiqet pasi cikli të ketë zgjatur më shumë se ky shumëfish i kohëzgjatjes së tij tipike. 1.8: një cikël 80% më i gjatë se programi nuk përputhet më me të.", "label": "Raporti maks. i kohëzgjatjes" }, "profile_match_min_duration_ratio": { - "doc": "Gjatësia minimale e ciklit në lidhje me profilin. 0.9 do të thotë që një cikël duhet të jetë të paktën 90% e kohëzgjatjes së profilit për t'u përshtatur.", + "doc": "Një program merret parasysh vetëm pasi cikli të ketë zgjatur këtë pjesë të kohëzgjatjes së tij tipike. 0.10: një program 2-orësh bëhet kandidat pas 12 minutash. Nuk zbatohet në 15 minutat e para të ciklit.", "label": "Raporti min. i kohëzgjatjes" }, "profile_match_threshold": { - "doc": "Rezultati minimal i ngjashmërisë (0-1) kërkohet në fund të ciklit për të pranuar një identifikim programi. Ngrini atë për të zvogëluar identifikimin e gabuar; uleni nëse programet e makinës suaj nuk përputhen. Default 0.4 është një pikë fillestare konservatore.", + "doc": "Rezultati i përputhjes në kohë reale që i duhet një cikli përpara se Përfundimi i Zgjuar ose finalizimi kundër rrudhave ta mbyllin herët. Nuk ndryshon programin e shfaqur. Rriteni nëse përputhjet e dobëta i mbyllin ciklet herët; uleni nëse përputhjet e qarta presin vonesën e fikjes.", "label": "Pragu i përputhjes" }, "profile_unmatch_threshold": { - "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.", + "doc": "Nëse rezultati i përputhjes në kohë reale bie nën këtë vlerë, hamendja aktuale e programit pastrohet. Mbajeni pak më të ulët se Pragu i përputhjes, që një rënie e shkurtër të mos e rivendosë.", "label": "Pragu i mosperputhjeve" }, "progress_reset_delay": { - "doc": "Pas përfundimit, mbajeni progresin në 100% për këtë kohë, në mënyrë që \"Përfunduar\" të jetë i dukshëm në panelet e kontrollit përpara se ta rivendosni në \"Idle\".", + "doc": "Pas një cikli, mbaje progresin në 100% kaq gjatë përpara se të rivendoset në \"Idle\".", "label": "Vonesë rivendosjeje të progresit" }, "pump_stuck_duration": { - "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).", + "doc": "Sa gjatë mund të punojë pa ndalim një pompë përpara se të lëshohet ngjarja e pompës së bllokuar.", "label": "Kohëzgjatja e pompës së bllokuar" }, "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.", + "doc": "Koha minimale ndërmjet leximeve të përpunuara. Leximet që vijnë më herët anashkalohen (leximet e ulëta gjatë ciklit ruhen gjithmonë). Vendoseni sa intervali i raportimit të prizës suaj ose më pak; një vlerë më e lartë vetëm hedh poshtë lexime.", "label": "Intervali i kampionimit" }, "save_debug_traces": { - "doc": "Ruani gjurmën e plotë të fuqisë dhe të dhënat e korrigjimit që përputhen për çdo cikël. I dobishëm për zgjidhjen e problemeve, por rrit madhësinë e ruajtjes.", + "doc": "Ruan gjithashtu me çdo cikël të dhënat e korrigjimit të përputhjes (pikët e çdo kandidati), për zgjidhjen e problemeve. Shton rreth 20 KB për cikël.", "label": "Ruaj gjurmët e korrigjimit" }, - "smoothing_window": { - "doc": "Sa zbutet sinjali i fuqisë së papërpunuar. I ulët (2) është i përgjegjshëm, por i zhurmshëm; i lartë (5) zbut pikat, por shton vonesën.", - "label": "Dritarja e zbutjes" - }, "start_duration_threshold": { - "doc": "Fuqia duhet të qëndrojë mbi pragun e fillimit për një kohë të gjatë për të konfirmuar një fillim të vërtetë, duke parandaluar kyçet e ndezjes/fikjes së sekondës nga fillimi i një cikli.", + "doc": "Fuqia duhet të qëndrojë më e lartë se Pragu i nisjes kaq gjatë përpara se të nisë një cikël, që ndezjet/fikjet e shkurtra të injorohen.", "label": "Kohëzgjatja e nisjes" }, "start_energy_threshold": { - "doc": "Energjia (fuqia x koha) që pajisja duhet të konsumojë përpara se të FUNKSIONOJË. Një majë e shkurtër me fuqi të lartë ka energji shumë të ulët dhe injorohet, duke parandaluar fillimet e rreme.", + "doc": "Energjia që pajisja duhet të konsumojë përpara se cikli të llogaritet si në punë. Një majë e shkurtër fuqie konsumon pak energji, ndaj nuk nis cikël.", "label": "Energjia e nisjes" }, "start_threshold_w": { - "doc": "Fuqia duhet të rritet mbi këtë nivel për të konfirmuar se një cikël ka filluar. Vendosja e tij shumë e ulët shkakton nisje të rreme nga fuqia e gatishmërisë; programet shumë të larta dhe me fillim të ngadalshëm (mbushje e ftohtë) mungojnë. Motori sugjerues e vendos këtë pak mbi fuqinë aktive më të ulët të vërejtur të makinës.", + "doc": "Fuqia duhet të kalojë këtë vlerë që të nisë një cikël. Shumë e ulët: fuqia e gatishmërisë shkakton nisje të rreme. Shumë e lartë: programet që nisin ngadalë (mbushje me ujë të ftohtë) humbasin.", "label": "Pragu i nisjes" }, "stop_threshold_w": { - "doc": "Fuqia duhet të bjerë nën këtë nivel përpara se të fillojë numërimi mbrapsht i vonesës. Vendoseni atë nën pragun e fillimit - hendeku midis tyre është brezi i histerezës që parandalon dridhjen. Nëse vendoset shumë lart, fazat me fuqi të ulët (mbajtjet e shpëlarjes, kundër rrudhave) shkaktojnë gabimisht sekuencën përfundimtare.", + "doc": "Fuqia duhet të bjerë nën këtë vlerë përpara se të nisë numërimi i vonesës së fikjes. Mbajeni më të ulët se Pragu i nisjes. Nëse është shumë e lartë, fazat me fuqi të ulët (pritjet e shpëlarjes, kundër rrudhave) nisin gabimisht numërimin e fundit.", "label": "Pragu i ndalimit" }, "switch_entity": { - "doc": "Çelësi opsional i çaktivizuar në pauzë dhe përsëri ndizet kur rifillon, përdoret së bashku me \"Pauza gjithashtu ndërpret energjinë\".", + "doc": "Çelës opsional që fiket në pauzë dhe ndizet sërish në vazhdim, kur është aktive \"Pauzë gjithashtu pret fuqinë\".", "label": "Entiteta e çelësit" }, "watchdog_interval": { - "doc": "Sa shpesh mbikëqyrësi i sfondit kontrollon për sensorë të bllokuar dhe afate kohore të kaluara. Parazgjedhja 30 s.", + "doc": "Sa shpesh kontrolli në sfond kërkon sensorë të ngecur dhe afate të skaduara. Parazgjedhja 30 s.", "label": "Intervali i rojëtarit" }, "notify_milestone_message": { @@ -1517,135 +1478,90 @@ "label": "Mesazhi i piketës" }, "notify_milestones": { - "doc": "Numra ciklesh të ndarë me presje që, kur arrihen, aktivizojnë një njoftim festiv një herësh (p.sh. 50, 100, 500, 1000). Bosh çaktivizon njoftimet e piketave.", + "doc": "Numra ciklesh që nisin një njoftim festiv një herësh, të ndarë me presje (p.sh. 50, 100, 500, 1000). Bosh i çaktivizon.", "label": "Piketat e cikleve" }, "notify_quiet_end_hour": { - "doc": "Fundi i dritares mos shqetëso (0-23). Njoftimet e mbajtura dërgohen në këtë orë. Lëreni bosh për ta çaktivizuar.", + "doc": "Fundi i dritares mos shqetëso (0-23); njoftimet e mbajtura dërgohen atëherë. Bosh e çaktivizon.", "label": "Fundi i orëve të qeta" }, "notify_quiet_start_hour": { - "doc": "Fillimi i një dritareje mos shqetëso (0-23). Njoftimet e përfundimit, kujtesat dhe njoftimet për rroba të pastra që do të aktivizoheshin gjatë orëve të qeta mbahen dhe dërgohen kur mbaron dritarja. Lëreni bosh për ta çaktivizuar. Mbështet dritare që kalojnë mesnatën (p.sh. fillimi 22, fundi 7).", + "doc": "Fillimi i një dritareje mos shqetëso (0-23). Njoftimet e përfundimit, kujtesat dhe njoftimet për rroba të pastra në këtë dritare mbahen derisa ajo të mbarojë. Mund të kalojë mesnatën (p.sh. 22 deri 7). Bosh e çaktivizon.", "label": "Fillimi i orëve të qeta" }, "peak_rate_message": { - "doc": "Tekst opsional i personalizuar për këshillën e tarifës së pikut, i shtuar te njoftimi i nisjes. Variablat shabllon: {device}, {price}. Bosh përdor vlerën e paracaktuar të integruar.", + "doc": "Tekst i personalizuar për këshillën e tarifës së pikut. Variablat e shabllonit: {device}, {price}. Bosh: përdoret parazgjedhja.", "label": "Mesazhi i tarifës së pikut" }, "peak_rate_threshold": { - "doc": "Kur nis një cikël dhe çmimi aktual për kWh është në këtë vlerë ose mbi të, i shtohet një këshillë për tarifën e pikut njoftimit të nisjes. 0 ose bosh e çaktivizon këshillën.", + "doc": "Nëse çmimi për kWh është në këtë vlerë ose mbi të kur nis një cikël, njoftimit të nisjes i shtohet një këshillë për tarifën e pikut. 0 ose bosh e çaktivizon.", "label": "Pragu i tarifës së pikut (për kWh)" }, - "dtw_bandwidth": { - "label": "Toleranca e përputhjes së formës", - "doc": "Sa mund ta shtrembërojë në kohë përputhësi një cikël që të përshtatet me formën e një programi të ruajtur. Vlera më e lartë toleron më shumë ndryshim shpejtësie; shumë e lartë rrezikon të përputhë programin e gabuar." - }, - "corr_weight": { - "label": "Pesha e formës kundrejt nivelit", - "doc": "Ekuilibri midis formës së kurbës dhe nivelit absolut të fuqisë gjatë vlerësimit të një përputhjeje. Vlera më e lartë favorizon formën; më e ulta favorizon përputhjen e nivelit të fuqisë." - }, - "duration_weight": { - "label": "Pesha e kohëzgjatjes", - "doc": "Sa fort preferohet një program, kohëzgjatja tipike e të cilit përputhet me këtë cikël." - }, - "energy_weight": { - "label": "Pesha e energjisë", - "doc": "Sa fort preferohet një program, konsumi tipik i energjisë i të cilit përputhet me këtë cikël." - }, "store_brand": { "label": "Marka e Pajisjes", - "doc": "Opsionale. Marka e pajisjes, e zgjedhur nga katalogu i komunitetit. Përdoret për të gjetur dhe ndarë regjistrime referuese përkatëse. Lëreni bosh nëse nuk përdorni veçoritë online." + "doc": "Opsionale. Marka e pajisjes nga katalogu i komunitetit, për të gjetur dhe ndarë regjistrime referuese. Lëreni bosh nëse nuk përdorni veçoritë online." }, "store_model": { "label": "Modeli i Pajisjes", - "doc": "Opsionale. Modeli i pajisjes, i zgjedhur nga katalogu i komunitetit sapo të caktohet një markë. Nëse modeli juaj nuk është në listë, mund ta shtoni në katalog." + "doc": "Opsionale. Modeli i pajisjes nga katalogu i komunitetit (zgjidhni fillimisht markën). Nëse nuk është në listë, mund ta shtoni." }, "show_contributor": { "doc": "Shfaq atribuimin \"nga \" në pajisjet dhe ciklet referuese të komunitetit." }, "smart_termination_duration_ratio": { - "doc": "Sa larg brenda kohëzgjatjes së pritur të programit të përputhur duhet të ketë ecur një cikël përpara se Përfundimi i Zgjuar të mund ta mbyllë atë herët sapo bie fuqia. Kohëzgjatja e pritur është mesatarja e programit, prandaj te pajisjet kohëzgjatja e të cilave ndryshon shumë - makinat larëse me ujë hyrës të ftohtë në dimër dhe të ngrohtë në verë, tharëset me sensor lagështie, programet që varen nga ngarkesa - rreth gjysma e të gjitha nisjeve përfundojnë më shkurt se ajo mesatare dhe nuk e marrin kurrë përfundimin e shpejtë, duke mbaruar vetëm përmes afatit rezervë me disa minuta vonesë. Uleni këtë vlerë (p.sh. 0.85) te këto makina që përfundimi i hershëm të aktivizohet gjithsesi; ngrijeni drejt 1.0 për një sjellje më konservatore. Lëreni bosh për vlerën e parazgjedhur (0.98, ose 0.99 për pjatalarëset). Ai mund vetëm ta mbyllë një cikël më herët, kurrë më vonë, dhe nuk aktivizohet kurrë në një përputhje të paqartë ose me besim të ulët.", + "doc": "Pjesa e kohëzgjatjes mesatare të programit që një cikël duhet të arrijë përpara se Përfundimi i Zgjuar ta mbyllë herët. Nëse kohëzgjatjet ndryshojnë (temperatura e ujit, tharja me sensor, madhësia e ngarkesës), uleni (p.sh. 0.85). Lëreni bosh për 0.98 (0.99 te pjatalarëset).", "label": "Raporti i Përfundimit të Zgjuar" }, - "enable_phase_matching": { - "label": "Kohë e mbetur e ndërgjegjshme për fazat", - "doc": "Ndan çdo cikël që po funksionon në faza (ngrohje, larje, centrifugim) dhe shpërndan kohën e mbetur për çdo fazë, e përzier me vlerësimin klasik - duke u mbështetur te shpërndarja sipas fazave në fillim të ciklit dhe te vlerësimi klasik afër fundit. Kjo e personalizon numërimin mbrapsht sipas kohës që makina juaj në të vërtetë ngroh dhe punon, gjë që vihet re më së shumti në gjysmën e parë të një cikli. Off = vetëm vlerësimi klasik. Preket vetëm shfaqja e kohës së mbetur; përputhja e programeve dhe zbulimi i ciklit mbeten të pandryshuara." - }, - "keep_min_score": { - "label": "Rezultat Min. Përputhjeje", - "doc": "Rezultati minimal i ngjashmërisë që i nevojitet një kandidati për të qëndruar në garën e përputhjes. Parazgjedhja 0.1 është me qëllim lejuese - pranon edhe kandidatë të dobët dhe u lë etapave të mëvonshme të gjejnë përputhjen më të mirë. Rriteni për të eliminuar profilet e pamundshme herët; uleni për të zgjeruar grupin fillestar të kandidatëve." - }, - "dtw_blend": { - "label": "Përzierje Deformimi", - "doc": "Sa e zëvendëson rezultati i bashkimit DTW rezultatin bazë të Etapës 2. 0 = përdor vetëm rezultatin e Etapës 2, 1 = përdor vetëm rezultatin DTW, 0.5 (parazgjedhja) = përzierje e barabartë. Rriteni për t'u mbështetur më shumë në bashkimin e deformimit kur programet kanë nivele të ngjashme fuqie por modele të ndryshme kohore." - }, - "dtw_ensemble_w": { - "label": "Mix Ansambël DTW", - "doc": "Në modalitetin ansambël DTW (parazgjedhja), pesë për L1-e shkallëzuar kundrejt DTW derivativ (DDTW). 1.0 = gjithë shkallëzuar-L1, 0 = gjithë DDTW, 0.7 = parazgjedhja. Varianti shkallëzuar-L1 krahason nivelet e fuqisë; varianti derivativ reagon ndaj mënyrës sesi ndryshon fuqia me kalimin e kohës. Ansambëli kombinon të dyja." - }, - "dtw_ddtw_scale": { - "label": "Shkalla e Deformimit Derivativ", - "doc": "Distanca e gjysmë-ngopjes për vlerësimin e DTW derivativ. Rezultati gjysmohet kur distanca DDTW është e barabartë me këtë vlerë. Më i vogël = më i ndjeshëm ndaj ndryshimeve të formës. Parazgjedhja 30 është kalibruar për gjurmët tipike të fuqisë së pajisjeve. Ndikon vetëm mënyrat DTW ansambël dhe ddtw." - }, - "dtw_refine_top_n": { - "label": "Numri i Rafinimit DTW", - "doc": "Sa kandidatë të parë të Etapës 2 ri-vlerësohen nga DTW. DTW është më e shtrenjtë llogaritëse, kështu që aplikohet vetëm tek kandidatët N më të mirë. Parazgjedhja 5. Rriteni (në 7-9) nëse profili i saktë ndonjëherë arrin vetëm në vendin e 4-të ose 5-të pas Etapës 2 - DTW mund ta shpëtojë. Ulja e saj shpejton pak përputhjen." - }, - "duration_scale": { - "label": "Saktësia e Kohëzgjatjes", - "doc": "Ashpërsia e ndëshkimit të marrëveshjes së kohëzgjatjes së Etapës 4. Ky është raporti logaritmik në të cilin rezultati i marrëveshjes gjysmohet. Më i vogël = më i rreptë: një mospërputhje kohëzgjatjeje ndëshkon më shumë. Parazgjedhja 0.175 korrespondon me tolerancën e kohëzgjatjes rreth 18% në gjysmë-peshë. Çiftojeni me Peshën e Kohëzgjatjes." - }, - "energy_scale": { - "label": "Saktësia e Energjisë", - "doc": "Ashpërsia e ndëshkimit të marrëveshjes së energjisë së Etapës 4. Më i vogël = më i rreptë: një mospërputhje energjie ndëshkon më shumë. Parazgjedhja 0.25 është me qëllim më lejuese se Ashpërsia e Kohëzgjatjes sepse energjia ndryshon me ngarkesën. Çiftojeni me Peshën e Energjisë." - }, "power_profile_interval_min": { "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." + "doc": "Madhësia e segmentit kohor për atributin power_profile (vat mesatare për çdo segment), që përdoret nga planifikues si EMHASS dhe tibber_prices. Më i vogël ruan pikat e shkurtra; më i madh e zbut kurbën. Parazgjedhja 15 min. Nuk ndikon në zbulim." }, "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." + "doc": "Vetëm iOS. Përditëson progresin live në heshtje, me një përparësi që kursen baterinë dhe që iOS mund ta grupojë. Përditësimi i parë dhe njoftimet e fillimit/përfundimit sinjalizojnë gjithsesi. Joaktiv: çdo përditësim sinjalizon." }, "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ë." + "doc": "Vetëm Android. Mban njoftimin e progresit live kur trokitni mbi të. Joaktiv: trokitja e largon." }, "notify_live_click_action": { "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." + "doc": "Çfarë hap trokitja mbi njoftimin e kësaj pajisjeje. Bosh: panelin WashData te kjo pajisje. Shkruani shtegun e një paneli kontrolli (p.sh. /lovelace/laundry) ose një URL, ose \"none\" që trokitja të mos bëjë asgjë." }, "door_opens_at_end": { "label": "Dera hapet automatikisht në fund", - "doc": "Për pjatalarëset që hapin derën në fund të ciklit për t'u tharë (AirDry dhe të ngjashme). Kur kjo është aktive, hapja e derës gjatë një cikli nuk e ndal atë përgjithmonë; nëse dera qëndron hapur për kohën e pritjes më poshtë, WashData e trajton ciklin si të përfunduar. Një hapje e shkurtër (shtimi i një gjëje) injorohet. Kërkon një Entitet Sensori Dere." + "doc": "Për pjatalarëset që hapin derën në fund për tharje (p.sh. AirDry). Një derë e lënë hapur për kohën e mëposhtme e përfundon ciklin në vend që ta vërë në pauzë; një hapje e shkurtër injorohet. Kërkon cilësimin Entiteta e sensorit të derës." }, "door_end_dwell_seconds": { "label": "Koha e pritjes me derë të hapur", - "doc": "Sa gjatë duhet të qëndrojë dera hapur përpara se WashData të përfundojë ciklin, kur \"Dera hapet automatikisht në fund\" është aktive. E gjatë sa të injorojë shtimin e shpejtë të një pjate (parazgjedhja 60 s), e shkurtër sa të përfundojë shpejt pasi makina hap derën." + "doc": "Kur \"Dera hapet automatikisht në fund\" është aktive: sa gjatë duhet të qëndrojë dera hapur përpara se cikli të përfundojë. Mjaft gjatë sa të injorohet shtimi i një pjate (parazgjedhja 60 s)." }, "dishwasher_end_spike_quiet_release": { "label": "Lirim në qetësi gjatë tharjes pasive", - "doc": "Sapo cikli të kalojë kohëzgjatjen e tij të pritur, sa kohë duhet të qëndrojë e qetë pjatalarësja (nën Pragun e ndalimit) përpara se WashData të ndalojë pritjen për një shkarkim përfundimtar dhe ta mbyllë ciklin. Rriteni nëse makina juaj ka një fazë të gjatë tharjeje të heshtur përpara një shkarkimi përfundimtar të vonuar që po humbet - një dritare më e gjerë e lejon kohëzgjatjen e mësuar të ndjekë zhvendosjen sezonale (ujë hyrës më i ftohtë = cikle më të gjata) në vend që të bllokohet në mesataren e vjetër. Ai vetëm sa e shkurton pritjen në raport me kufirin e brendshëm 30-minutësh të kulmit të fundit, kurrë nuk e zgjat." + "doc": "Pas kohëzgjatjes së pritur, sa gjatë duhet të qëndrojë fuqia e pjatalarëses më e ulët se Pragu i ndalimit përpara se WashData të ndalojë pritjen për zbrazjen përfundimtare të ujit. Rriteni nëse humbet një zbrazje e vonë pas një tharjeje të gjatë. Pritja nuk i kalon kurrë 30 minuta." }, "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ë." + "doc": "Cilat lloje ciklesh formësojnë kurbën dhe përputhjen e çdo programi. Heqja e shenjës nga një lloj nuk fshin asgjë; e dobishme nëse nuk u besoni të dhënave të importuara. Statistikat numërojnë gjithmonë vetëm ciklet e kësaj makine. Heqja e shenjës nga të gjitha shpërfillet." }, "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." + "doc": "Çdo cikël faturohet me çmimin e çdo çasti kur punoi, në vend të çmimit kur përfundoi. Kërkon një entitet çmimi. Proceso historinë (Diagnostikim) rillogarit koston e cikleve më të vjetra." }, "unload_confirm_entity": { "label": "Entiteti i konfirmimit të shkarkimit", - "doc": "Entitet opsional, aktivizimi i të cilit do të thotë se ngarkesa është hequr, për makinat që nuk mund të kenë sensor dere. Funksionon çdo lloj: një buton Zigbee ose NFC, një ndihmës input_button, një sensor lëvizjeje përpara makinës, një skenë. Çdo ndryshim i gjendjes llogaritet si konfirmim, përveç fikjes dhe kalimit në gjendje të padisponueshme. Vendosja e kësaj aktivizon gjithashtu kujtesën për shkarkim në një pajisje pa Entitetin e sensorit të derës." + "doc": "Opsionale. Çdo entitet që konfirmon se ngarkesa u hoq, për makinat pa sensor dere (një buton, input_button, sensor lëvizjeje, skenë). Çdo ndryshim gjendjeje llogaritet, përveç fikjes ose kalimit në të padisponueshme. Aktivizon gjithashtu kujtesën për shkarkim." }, "unload_track_without_door": { "label": "Konfirmo shkarkimin manualisht", - "doc": "Ekzekuto kujtesën për shkarkim në një pajisje pa Entitetin e sensorit të derës dhe pa Entitetin e konfirmimit të shkarkimit, dhe konfirmo shkarkimin nga automatizimi yt: shtyp entitetin buton \"Shëno si të shkarkuar\", ose thirr shërbimin ha_washdata.mark_unloaded. Shpërfillet kur është vendosur një Entitet i sensorit të derës, i cili tashmë e jep këtë sinjal." + "doc": "Ekzekuto kujtesën për shkarkim pa sensor dere ose entitet konfirmimi. Konfirmoje shkarkimin nga automatizimi yt me butonin \"Shëno si të shkarkuar\" ose shërbimin ha_washdata.mark_unloaded. Shpërfillet kur është vendosur Entiteta e sensorit të derës." }, "notify_icon_color": { "label": "Ngjyra e njoftimit", - "doc": "Ngjyrë theksuese për njoftimet e kësaj pajisjeje, që makina larëse, tharësja dhe pjatalarësja të dallohen me një shikim. iOS ngjyros ikonën dhe shiritin e progresit të Live Activity në ekranin e kyçjes dhe në Dynamic Island. Android-it i dërgohet e njëjta ngjyrë, por çfarë bën me të varet nga telefoni: disa e ngjyrosin ikonën, ndërsa shumë (veçanërisht Samsung One UI) e shpërfillin plotësisht. Të dyja e kuptojnë kodin Hex (p.sh. #03A9F4). Lëreni bosh për parazgjedhjen e platformës." + "doc": "Ngjyrë theksuese për njoftimet e kësaj pajisjeje, që makina larëse, tharësja dhe pjatalarësja të dallohen lehtë. Në iOS mbush rrethin pas ikonës dhe ngjyros shiritin e Live Activity. Shumë telefona Android (sidomos Samsung) e shpërfillin. Përdorni hex, p.sh. #03A9F4. Bosh: parazgjedhja e platformës." + }, + "share_name": { + "doc": "Shfaq emrin e llogarisë suaj si ngarkues të cikleve që ndani. Joaktiv: ndahen pa emër." } }, "setting_group": { @@ -1673,9 +1589,6 @@ "door_pause": { "label": "Dera dhe pauzë" }, - "dtw_refinement": { - "label": "Rafinim DTW" - }, "duration_gates": { "label": "Portat e kohëzgjatjes" }, @@ -1752,13 +1665,18 @@ "rebuilding": "Po rindërtohet…", "analyzing": "Po analizohet…", "resetting": "Po rivendoset…", - "reverting": "Po kthehet…", "trimming": "Duke prerë…", "splitting": "Po ndahet…", + "stalled": "I ngecur", "deleting": "Po fshihet…", "imported": "Importuar", "preparing": "Po përgatitet…", - "unlabelling": "Po hiqet etiketa…" + "unlabelling": "Po hiqet etiketa…", + "working": "Po përpunohet…", + "match_uncertain": "E pasigurt: {top} ose {runner}", + "match_uncertain_single": "E pasigurt: ndoshta {top}", + "match_sure": "~{pct}% e sigurt", + "match_uncertain_tip": "WashData nuk ka vendosur ende për një program. Shifra tregon sa shpesh hamendja kryesore, {top}, doli e saktë në ciklet e kaluara kur printe me kaq. Vetëm për informim." }, "tab": { "advanced": "I avancuar", @@ -1790,13 +1708,17 @@ "toast": { "catalog_refreshed": "Katalogu i komunitetit u rifreskua", "contrib_device_changed": "U shtua në dyqanin e komunitetit, por pajisja e zgjedhur ndryshoi ndërsa formulari ishte i hapur. Zgjidheni te Cilësimet.", + "task_name_required": "Jepini detyrës një emër", + "task_added": "Detyra u shtua", + "task_save_failed": "Nuk u ruajt dot detyra: {error}", + "task_removed": "Detyra u hoq", + "maint_logged": "U shënua si e kryer: {task}", "access_saved": "Kontrolli i aksesit u ruajt", "all_wiped": "Të gjitha të dhënat u fshien", "analysis_complete_none": "Analiza e plotë: asnjë sugjerim i ri", "auto_label_complete": "Etiketimi automatik i plotë", "automation_deleted": "Automatizimi u fshi", "correction_submitted": "Korrigjimi u dërgua", - "cycle_deleted": "Cikli u fshi", "cycle_force_stopped": "Cikli u ndalua me forcë", "cycle_labelled": "Cikli u etiketua", "cycle_paused": "Cikli u ndal", @@ -1812,11 +1734,9 @@ "group_deleted": "Grupi u fshi", "group_name_required": "Kërkohet emri i grupit", "group_saved": "Grupi u ruajt", - "import_successful": "Importimi u krye; integrimi po ringarkohet", "json_required": "Kërkohen të dhëna JSON", "legacy_removed": "Veprimet e trashëgimit u hoqën", "logs_exported": "Regjistrat u eksportuan", - "matching_reverted": "Peshat e përputhjes u kthyen te parazgjedhjet", "min_2_profiles": "Zgjidhni të paktën 2 profile për një grup", "ml_training_no_improvement": "Trajnimi i plotë: baza e mbajtur (pa përmirësim)", "models_reverted": "Modelet e pajisjes u kthyen te baza", @@ -1826,8 +1746,8 @@ "pg_preset_loaded": "Konfigurimi \"{name}\" u ngarkua", "pg_preset_saved": "Konfigurimi \"{name}\" u ruajt", "pg_sugg_loaded": "{n} vlera të sugjeruara u ngarkuan - ekzekuto zonën e provave për krahasim", - "pg_sugg_ml_loaded": "{n} vlera ML të kalibruara u ngarkuan - ekzekuto zonën e provave për krahasim", "phase_name_required": "Kërkohet emri i fazës", + "phase_reset": "\"{name}\" u rikthye në fazën e integruar", "phase_updated": "Faza u përditësua", "phases_saved": "Fazat u ruajtën", "preferences_saved": "Preferencat u ruajtën", @@ -1874,14 +1794,10 @@ "store_name_required": "Shkruani një emër profili", "store_import_failed": "Importimi dështoi: {error}", "store_imported": "Importuar në {profile}", - "store_program_required": "Shkruani një emër programi", "store_no_appliance": "Vendosni së pari markën dhe modelin e pajisjes suaj te Cilësimet.", "store_share_failed": "Ndarja dështoi: {error}", "store_shared": "U nda në dyqanin e komunitetit, në pritje të rishikimit.", "appliance_added": "Pajisja u shtua, në pritje të miratimit", - "thanks_confirming": "Faleminderit që konfirmuat", - "device_approved": "Miratuar nga komuniteti", - "rating_saved": "Vlerësimi i cilësisë u ruajt", "brand_added": "Marka u shtua, në pritje të miratimit", "profile_added": "Profili u shtua, në pritje të miratimit", "saved_except_conflicts": "Ruajtur. Korrigjoni konfliktet e theksuara për të ruajtur pjesën tjetër.", @@ -1904,11 +1820,50 @@ "odometer_saved": "Totali i cikleve u ruajt", "odometer_save_failed": "Nuk u ruajt dot totali: {error}", "store_pick_profile_share": "Zgjidhni një profil për ndarje", - "remove_failed": "Heqja dështoi: {error}" + "remove_failed": "Heqja dështoi: {error}", + "analysis_complete": "Analiza përfundoi: {count} sugjerim(e)", + "analysis_failed": "Analiza dështoi: {error}", + "auto_detect_failed": "Zbulimi automatik dështoi: {error}", + "auto_label_failed": "Etiketimi automatik dështoi: {error}", + "could_not_load_cycle": "Cikli nuk mund të ngarkohej: {error}", + "create_failed": "Krijimi dështoi: {error}", + "cycles_deleted": "Cikle të fshira: {count}", + "debug_cleared": "U pastruan {count} gjurmë korrigjimi", + "discard_failed": "Hedhja poshtë dështoi: {error}", + "error": "Gabim: {error}", + "export_failed": "Eksportimi dështoi: {error}", + "import_failed": "Importimi dështoi: {error}", + "invalid_json": "\"{key}\" nuk është JSON i vlefshëm; korrigjojeni ose pastroni fushën para ruajtjes.", + "label_failed": "Etiketimi dështoi: {error}", + "ml_training_promoted": "Trajnimi përfundoi: {count} model(e) të promovuar", + "pause_failed": "Pauzimi dështoi: {error}", + "phase_created": "Faza \"{name}\" u krijua", + "phase_deleted": "Faza \"{name}\" u fshi", + "profile_created": "Profili \"{name}\" u krijua", + "rename_failed": "Riemërtimi dështoi: {error}", + "save_failed": "Ruajtja dështoi: {error}", + "split_complete": "U nda në {count} cikle", + "start_failed": "Nisja dështoi: {error}", + "stop_failed": "Ndalimi dështoi: {error}", + "update_failed": "Përditësimi dështoi: {error}", + "store_download_skipped": "{n} regjistrim(e) u anashkaluan: tepër të shkurtra, me tepër boshllëqe ose tashmë në pajisjen tuaj.", + "store_import_low_quality": "Ky regjistrim është tepër i shkurtër, ka tepër boshllëqe ose është i pabesueshëm për t'u importuar.", + "store_import_duplicate": "Ky regjistrim është tashmë në pajisjen tuaj.", + "device_approved": "Miratuar nga komuniteti", + "thanks_confirming": "Faleminderit që konfirmuat", + "rating_saved": "Vlerësimi i cilësisë u ruajt", + "store_download_partial": "{n} program(e) nuk mund të shkarkoheshin; provoni përsëri më vonë.", + "import_no_restore_point": "U importua, por pika e rikthimit nuk mund të ruhej, prandaj ky importim nuk mund të zhbëhet", + "import_undo_failed": "Importimi nuk mund të zhbëhej: {error}", + "import_undone": "Importimi u zhbë: u rikthyen {profiles} profil(e) dhe {cycles} cikël(cikle)", + "auto_label_cancelled": "Etiketimi automatik u anulua pasi u etiketuan {count} cikle.", + "auto_label_cancelled_one": "Etiketimi automatik u anulua pasi u etiketua {count} cikël.", + "auto_label_cancelled_other": "Etiketimi automatik u anulua pasi u etiketuan {count} cikle.", + "auto_label_done": "Etiketimi automatik përfundoi: u etiketuan {count} cikle, pa etiketë: {skipped}.", + "auto_label_done_one": "Etiketimi automatik përfundoi: u etiketua {count} cikël, pa etiketë: {skipped}.", + "auto_label_done_other": "Etiketimi automatik përfundoi: u etiketuan {count} cikle, pa etiketë: {skipped}." }, "suggestion": { - "both_agree": "WashData rekomandon", - "calibrated_label": "I kalibruar", "observed_label": "I vëzhguar", "impact": { "auto_label_confidence": { @@ -1923,10 +1878,6 @@ "higher": "Mbyll vetëm ciklet që kanë konsumuar energji të konsiderueshme", "lower": "Mbyll edhe ciklet e shkurtra ose me konsum të ulët energjie" }, - "end_repeat_count": { - "higher": "Kërkon që sinjali i fundit të përsëritet më shumë herë – më i qëndrueshëm, pak më i ngadaltë", - "lower": "Mbyllet pas përsëritjeve më të pakta – detektim më i shpejtë, rrezik më i lartë mbylljeje false" - }, "learning_confidence": { "higher": "Mëson vetëm nga përputhjet me besueshmëri shumë të lartë – më e ngadaltë por më e besueshme", "lower": "Mëson nga më shumë cikle; modeli mund të jetë më i zhurmshëm" @@ -1965,40 +1916,25 @@ } }, "reason": { - "auto_label_confidence": "Besueshmëria e percentilit të 15-të e {cycles} etiketave automatike që përdoruesi nuk i korrigjoi kurrë ({p15}).", "completion_min_seconds": "Gjysma e kohëzgjatjes p05 të cikleve të pastra ({minutes} min) në {cycles} cikle; filtron ciklet e rreme.{excl}", - "dead_zone_batch": "Mbahet i shkurtër (dritarja mesatare e paqëndrueshmërisë në nisje në {cycles} cikle = {s}s) që një fund i vërtetë të zbulohet menjëherë.", - "dead_zone_from_dip": "Bazuar në një rënie të hershme të fuqisë të zbuluar në {s}s.", - "end_energy_default": "Baza e rekomanduar e paracaktuar për pragun e zhurmës në fund të ciklit.", - "end_energy_false": "Energjia p95 e fundit të rremë ({p95}Wh) në {cycles} cikle, e kufizuar në minimum në 0.2% të energjisë mesatare të ciklit ({floor}Wh).", - "end_energy_no_false": "Asnjë fund i rremë në {cycles} cikle; e kufizuar në minimum në 0.2% të energjisë mesatare të ciklit ({floor}Wh).", - "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ë.", + "match_interval_floored": "Programi më i shkurtër ({shortest}s) do ta kufizonte këtë në {cap}s, nën minimumin prej {minimum}s, prandaj përdoret minimumi: {persistence} përputhje të njëpasnjëshme zgjasin {budget}s, mbi {pct}% të atij programi. Përputhja kryhet vetëm kur vjen një lexim (mediana={median}s), ndaj kjo 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.", "min_off_gap": "Bazuar në {gaps} boshllëqe të vëzhguara ndërmjet cikleve (p05={p05}s). Kufiri minimal i pajisjes: {floor}s.", "min_off_gap_bridge": "Përcaktuar që të kapërcejë periudhën më të gjatë të qetësisë brenda ciklit: {span}s + 60s rezervë, nga {spans} boshllëqe të kapërcyera në {cycles} cikle të pastra (qëndron nën {ceiling}s, pauza më e shkurtër që lini midis dy nisjeve të njëpasnjëshme).", "min_power": "40% e fuqisë aktive më të ulët p05 ({p05}W) në {cycles} cikle të pastra, duke e mbajtur pragun e fikjes nën konsumin real.{excl}", - "ml_auto_label": "Besueshmëria më e ulët që modeli i cilësisë e vlerësoi ende si të pastër (p10 e {cycles} cikleve të pastra = {p10}).", - "ml_end_repeat": "{false}/{total} cikle ({pct}%) patën një pauzë që zbuluesi i fundit e vlerësoi >50%; kërkohen {val} konfirmim(e) fundi.", - "ml_off_delay": "Pauza të konfirmuara nga zbuluesi i fundit: p95 {p95}s + 60s rezervë, nga {pauses} pauza të verifikuara nga modeli në {cycles} cikle (kufiri minimal {floor}s).", "no_update_timeout": "Bazuar në ritmin e vëzhguar të përditësimit (p95={p95}s) * 20 (min. 60s).", "off_delay_cadence": "Bazuar në ritmin e vëzhguar të përditësimit (p95={p95}s) * 5", "off_delay_device_floor": "U përdor minimumi i sigurt specifik për pajisjen {device} ({floor}s).", "off_delay_generic_floor": "U përdor minimumi i sigurt i përgjithshëm ({floor}s).", "off_delay_pauses": "Përcaktuar që të zgjasë më shumë se pauzat e vërteta: pauza brenda ciklit p95 {p95}s + 60s rezervë, nga {pauses} pauza në {cycles} cikle të pastra (kufiri minimal {floor}s).", - "profile_match_threshold": "Besueshmëria p10 e {cycles} etiketave automatike të sakta ({p10}); kufi i sigurt i konfirmimit të drejtpërdrejtë.", - "sampling_interval": "Intervali mesatar i përditësimit i vëzhguar në {cycles} cikle të pastra ({si}s).{excl}", - "smoothing_window": "Përcaktuar që të zbusë ~30s leximesh me kampionim {si}s ({samples} kampione).", - "start_duration": "Mbahet i shkurtër (~një interval kampionimi {si}s) që zbulimi të fillojë sa më herët të jetë e mundur, duke injoruar njëkohësisht majat e një kampioni të vetëm.", - "thr_batch": "Mbahet pak mbi fuqinë aktive më të ulët p05 në {cycles} cikle ({p05}W) që një nisje të kapet sa më herët të jetë e mundur dhe pragu i ndalimit të mbetet nën fuqinë më të ulët të punës së makinës.", - "tol_per_profile": "p75 e variancës së kohëzgjatjes për profil në {profiles} profile ({cycles} cikle); profilet e qëndrueshme nuk penalizohen.", - "tol_pooled": "Bazuar në variancën e bashkuar të kohëzgjatjes së {cycles} cikleve të fundit të etiketuara (devijimi p95={dev}).", - "watchdog": "Mbahet sa më i ulët në mënyrë të sigurt (pak mbi boshllëkun e përditësimit p95 prej {p95}s dhe të paktën 2× intervalin e kampionimit prej {median}s, min. 30s) që bllokimet të kapen shpejt pa ndalime të rreme." + "standby_floor": "Çdo cikël i fundit shpenzonte akoma {idle}W kur mbaroi, kështu që një prag nën atë nuk mund të zbulojë kurrë fikjen e pajisjes. Mbajtur mbi të; {cycles} ciklet e pastra të regjistruara nuk ranë asnjëherë nën të për më shumë se {pause}s.", + "thr_batch": "Pak mbi fuqinë aktive më të ulët (p05) në {cycles} cikle ({p05}W), që nisjet të kapen herët dhe pragu i ndalimit të mbetet nën fuqinë më të ulët të punës.", + "watchdog": "Mbahet sa më i ulët në mënyrë të sigurt (pak mbi boshllëkun e përditësimit p95 prej {p95}s dhe të paktën 2× intervalin e kampionimit prej {median}s, min. 30s) që bllokimet të kapen shpejt pa ndalime të rreme.", + "end_energy_floor": "U ngrit në minimumin që nënkuptojnë pragu i ndalimit ({stop} W) gjatë vonesës së fikjes ({off_delay} s); nën të, cikli mund të përfundojë vetëm përmes një rruge rezervë.", + "max_duration_ratio_default": "Kthim te vlera e parazgjedhur: një kufi i sipërm më i ulët refuzon programin e duhur sa herë që një cikël zgjat më shumë se zakonisht.", + "min_duration_ratio_default": "Kthim te vlera e parazgjedhur: një kufi i poshtëm më i lartë pengon njohjen e një cikli në zhvillim derisa të ketë përparuar mjaft." }, "exclusions": { "summary": "U përjashtuan {total} cikle të zbuluar gabimisht: {parts}.", @@ -2016,7 +1952,8 @@ "no_power": "pa fuqi", "no_active_power": "pa fuqi aktive" } - } + }, + "now_value": "(tani {v})" }, "trend": { "down": "Tendencë rënëse", @@ -2024,6 +1961,10 @@ }, "maint": { "bearing_service": "Servisi i kushinetave", + "salt": "Rimbush kripën", + "rinse_aid": "Rimbush lëngun shpëlarës", + "lint_filter": "Pastro filtrin e pushit", + "condenser_clean": "Pastro kondensatorin", "descale": "Heqja e gurëve", "drum_clean": "Pastro tamburin", "filter_clean": "Pastro filtrin", @@ -2031,7 +1972,6 @@ }, "pg_desc": { "completion_min_seconds": "Ekzekutimi më i shkurtër që llogaritet si cikël i vërtetë", - "end_repeat_count": "Lexime të ulëta radhazi para përfundimit", "interrupted_min_seconds": "Ciklet e shkurtra shënohen si të ndërprera", "min_off_gap": "Hendeku i nevojshëm për të ndarë dy cikle", "off_delay": "Sekonda me fuqi të ulët para se të mbarojë cikli", @@ -2039,26 +1979,15 @@ "start_threshold_w": "Watt minimalë për t'u llogaritur si nisur", "stop_threshold_w": "Nën këtë, makina llogaritet e fikur", "anti_wrinkle_enabled": "Thith pulset e rrotullimit pas fazës kryesore në vend që t'i lexojë si cikle të reja", - "anti_wrinkle_exit_power": "Fuqia duhet të bjerë nën këtë mes pulseve që modaliteti kundër rrudhave të qëndrojë aktiv", + "anti_wrinkle_exit_power": "Niveli i qetësisë mes pulseve të rrotullimit; vepron vetëm mbi Pragun e ndalimit", "anti_wrinkle_idle_timeout": "Koha e qetësisë e lejuar mes dy pulseve të rrotullimit para se të mbarojë modaliteti kundër rrudhave", "anti_wrinkle_max_duration": "Një puls më i gjatë se ky e mbaron modalitetin kundër rrudhave dhe hap një cikël të ri", "anti_wrinkle_max_power": "Një puls mbi këtë e mbaron modalitetin kundër rrudhave dhe hap një cikël të ri", - "dtw_bandwidth": "Etapa 3: brez deformimi Sakoe-Chiba (0 = DTW joaktiv; parazgjedhja 0.2 = 20% e gjatësisë së ciklit)", - "corr_weight": "Etapa 2: balancë mes formës së kurbës (korrelacion) dhe nivelit të fuqisë (MAE); parazgjedhja 0.45", - "duration_weight": "Etapa 4: sa fortë ndikon marrëveshja e kohëzgjatjes në rezultatin final (parazgjedhja 0.22)", - "energy_weight": "Etapa 4: sa fortë ndikon marrëveshja e energjisë në rezultatin final (parazgjedhja 0.22)", - "profile_match_min_duration_ratio": "Etapa 1: gjatësia minimale e ciklit si pjesë e kohëzgjatjes së profilit; parazgjedhja 0.1 (10%)", - "profile_match_max_duration_ratio": "Etapa 1: gjatësia maksimale e ciklit si pjesë e kohëzgjatjes së profilit; parazgjedhja 1.8 (180%)", - "keep_min_score": "Etapa 2: rezultati minimal për të qëndruar në garë; parazgjedhja 0.1 pranon përputhje të dobëta, etapat e mëvonshme zgjedhin më të mirën", - "dtw_blend": "Etapa 3: 0 = vetëm rezultati bazë, 1 = vetëm rezultati DTW, 0.5 = përzierje e barabartë (parazgjedhja)", - "dtw_ensemble_w": "Etapa 3 (modaliteti ansambël): pesë për L1-e shkallëzuar kundrejt DTW derivativ; parazgjedhja 0.7 favorizon nivelin e fuqisë", - "dtw_ddtw_scale": "Etapa 3: distanca e gjysmë-ngopjes DDTW; më e vogël = më e ndjeshme ndaj formës (parazgjedhja 30)", - "dtw_refine_top_n": "Etapa 3: kandidatë të rishënuara nga DTW; rriteni në 7-9 nëse profili i saktë renditet i 4-5 (parazgjedhja 5)", - "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)", + "profile_match_min_duration_ratio": "Gjatësia minimale e ciklit si pjesë e kohëzgjatjes së profilit; parazgjedhja 0.1 (10%)", + "profile_match_max_duration_ratio": "Gjatësia maksimale e ciklit si pjesë e kohëzgjatjes së profilit; parazgjedhja 1.8 (180%)", "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", - "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", + "smart_termination_duration_ratio": "Pjesa e kohëzgjatjes mesatare të programit që një cikël duhet të arrijë përpara se Përfundimi i Zgjuar ta mbyllë herët; uleni nëse kohëzgjatjet ndryshojnë", + "anti_crease_finalize_ratio": "Pjesa e kohëzgjatjes mesatare të programit që një cikël duhet të arrijë përpara se bishti kundër rrudhave ta përfundojë; uleni te tharëset, 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": { @@ -2066,12 +1995,14 @@ "status_tip": "Përfundimi i ciklit: I përfunduar (fund natyror), I ndërprerë (rënie e menjëhershme e energjisë), I Ndaluar me Forcë (manual), ose Kërkon Shqyrtim (reagim në pritje).", "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. 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.", + "energy_tip": "Energjia totale e konsumuar (kWh), e llogaritur nga fuqia gjatë kohës.", + "cost_tip": "Kostoja e energjisë së këtij cikli, e fiksuar kur përfundoi. Kur është aktive Kosto e ponderuar sipas kohës, çdo çast faturohet me çmimin e vet; përndryshe me çmimin 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.", - "cost_dynamic_tip": "E ponderuar sipas kohës: e faturuar me çmimin në fuqi gjatë ciklit (efektivisht {price} {cur}/kWh)." + "cost_dynamic_tip": "E ponderuar sipas kohës: e faturuar me çmimin në fuqi gjatë ciklit (efektivisht {price} {cur}/kWh).", + "dbg_conf": "Bes.", + "dbg_mae": "MAE", + "dbg_corr": "Korr." }, "stat": { "avg": "mesatare", @@ -2095,7 +2026,10 @@ "notify_finish": "Do të dërgohej një njoftim përfundimi.", "notify_milestone": "Do të dërgohej një njoftim për një moment kyç.", "notify_held": "Një njoftim u mbajt për shkak të orëve të qeta.", - "finished": "Cikli arriti gjendjen përfundimtare dhe përfundoi." + "finished": "Cikli arriti gjendjen përfundimtare dhe përfundoi.", + "match_reverted": "Programi u kthye te zbulimi: rezultati i tij ra shumë nën kulmin e vet.", + "verified_pause": "Përafrimi në kohë reale konfirmoi (ose liroi) një pauzë brenda programit. Për sa kohë mbetet në fuqi, cikli nuk përfundohet.", + "stalled": "Cikli qëndroi në konsumin e gatishmërisë më gjatë se ç'bën ndonjëherë pauzë ky program, me punë të mbetur. Shfaqet si Në pauzë (I ngecur); cikli mbetet i hapur." }, "store": { "search_ph2": "Kërko sipas markës ose modelit…", @@ -2104,11 +2038,7 @@ "programs_count": "Programe: {n}", "browse": "Shfleto", "device": "Pajisja", - "favorites": "Të preferuarat", - "search_ph": "Kërko sipas markës ose modelit…", "no_results": "Nuk u gjetën pajisje përkatëse. Provoni një kërkim tjetër.", - "n_programs": "{n} programe", - "n_cycles": "{n} cikle", "no_programs": "Ende nuk ka programe të ndara për këtë pajisje.", "no_cycles": "Ende nuk janë ndarë cikle referuese për këtë program.", "anon": "anonim", @@ -2120,19 +2050,21 @@ "merge_target": "Bashko në profil", "description": "Përshkrimi (opsionale)", "contributed_by": "nga {name}", + "website": "Faqja e dyqanit ↗", + "website_tip": "Hapni faqen e dyqanit komunitar", + "favorites": "Të preferuarat", "rating_summary": "★ {avg} ({n})", "no_ratings": "Ende pa vlerësime", - "website": "Faqja e dyqanit ↗", - "website_tip": "Hapni faqen e dyqanit komunitar" + "unreachable": "Dyqani i komunitetit nuk mund të arrihej. Kontrolloni lidhjen dhe provoni përsëri." }, "settings": { "gear": { - "title": "Cilësimet" + "title": "Preferencat" } }, "link": { - "manual": "Manuali ↗", - "add_appliance": "Shtoni pajisjen tuaj" + "maintenance_supporter": "Maintenance Supporter në GitHub ↗", + "manual": "Manuali ↗" }, "tip": { "add_brand": "Marka nuk është në listë? Shtojeni në katalogun e komunitetit", @@ -2166,6 +2098,12 @@ }, "rebuild": { "envelopes": "Rindërtimi i zarfave" + }, + "auto_label": { + "running": "Etiketimi automatik i cikleve" + }, + "store_download": { + "running": "Shkarkimi nga dyqani" } }, "setup": { @@ -2175,26 +2113,24 @@ }, "phase0": { "washer": "WashData tashmë po zbulon ciklet tuaja. Regjistroni ciklin tuaj të parë për të aktivizuar emrat e programeve dhe vlerësimet e kohës.", - "dishwasher": "WashData po vëzhgon. Lavatriçet e enëve kanë cikle komplekse – regjistrimi i ciklit tuaj të parë rekomandohet fuqimisht. Nëse një cikël i zbuluar zgjat shumë, përdorni redaktuesin e ciklit për ta prerë para se ta ruani si profil.", + "dishwasher": "WashData po vëzhgon. Ciklet e pjatalarëses janë komplekse, ndaj regjistroni ciklin e parë. Nëse një cikël i zbuluar zgjat shumë, shkurtojeni në redaktuesin e ciklit përpara se ta ruani si profil.", "generic": "WashData po vëzhgon. Regjistroni ose etiketoni një cikël të zbuluar për të filluar ndërtimin e profileve." }, "phase1a": { - "labelled": "Fillim i mirë – programi juaj i parë është ruajtur. Për të dhëna sa më të pastra, merrni parasysh regjistrimin e ciklit tuaj të ardhshëm me veglën e regjistruesit." + "labelled": "Fillim i mirë: programi juaj i parë u ruajt. Për të dhëna sa më të pastra, regjistroni ciklin e ardhshëm me regjistruesin." }, "phase1b": { - "recorded": "Regjistrimi juaj u ruajt si {profile_name}. Tani regjistroni ose etiketoni programet tuaja të tjera të zakonshme për të ndërtuar mbulimin." + "recorded": "Regjistrimi juaj u ruajt si {profile_name}. Tani regjistroni ose etiketoni programet e tjera të zakonshme." }, "phase1c": { - "verify": "Keni {count} programe nga komuniteti. Ekzekutoni një cikël për të verifikuar nëse WashData e njeh saktë – përputhja do të përmirësohet ndërsa pajisja juaj ndërton historinë e vet." + "verify": "Keni {count} programe nga komuniteti. Ekzekutoni një cikël për të kontrolluar nëse WashData e njeh; përputhja përmirësohet ndërsa pajisja juaj ndërton historikun e vet." }, "phase2": { - "cluster": "WashData ka parë {count} cikle që nuk përputhen me asnjë program të ruajtur – ato duken të ngjashme me njëra-tjetrën. Dëshironi të krijoni një profil të ri për to?", + "cluster": "WashData ka parë {count} cikle të ngjashme që nuk përputhen me asnjë program të ruajtur. Të krijohet një profil i ri për to?", "unmatched": "Cikli juaj i fundit nuk u përputh me asnjë program të ruajtur. A është një program i ri?" }, "phase3": { - "suggestions": "WashData ka rekomandime për cilësimet bazuar në historikun tuaj të cikleve – shqyrtojini ato për të përmirësuar saktësinë e zbulimit.", - "groups": "Disa nga profilet tuaja duken si i njëjti program në temperatura të ndryshme. Organizojini ato në një grup për përputhje më të mirë.", - "phases": "Shtoni faza programi te {profile_name} për vlerësime më të sakta të kohës së mbetur." + "suggestions": "WashData ka sugjerime për cilësimet nga historiku i cikleve tuaja. Shqyrtojini për të përmirësuar zbulimin." }, "phase4": { "healthy": "Kjo pajisje është konfiguruar plotësisht ({profile_count} profile)." @@ -2207,8 +2143,6 @@ "create_from_cluster": "Krijo profil nga këto cikle", "create_profile": "Krijo një profil për të", "review_suggestions": "Shqyrto sugjerimet", - "organise_profiles": "Organizo profilet", - "configure_phases": "Konfiguro fazat", "skip_step": "Anashkalo këtë hap", "skip_forever": "Mos shfaq më", "hide_guidance": "Fshih udhëzuesin", @@ -2219,5 +2153,53 @@ "off_delay": { "effective": "Një cikël i përfunduar pret në të vërtetë {eff} s, jo {delay} s: Hendeku min. i fikjes ({gap} s) është më i gjatë dhe ka përparësi." } + }, + "access": { + "edit": "Redaktim", + "full": "I plotë", + "inherit": "Trashëgo", + "none": "Asnjë (i fshehur)", + "read": "Lexim" + }, + "pg_label_reason": { + "no_winner": "Asnjë program nuk u përshtat", + "below_floor": "Nën Besueshmërinë e të Mësuarit", + "ambiguous": "Shënuar si e paqartë", + "margin": "Shumë afër vendit të dytë", + "unknown_profile": "Programi nuk ekziston më", + "not_evaluated": "Nuk u vlerësua" + }, + "diagram": { + "accum": "akum.", + "auto": "auto", + "below_off": "poshtë = fikur", + "confirmed": "konfirmuar", + "energy_counts": "llogaritet energjia", + "feedback": "reagim", + "gap_below_min": "hendek nën min = një cikël", + "heat_phase": "faza e ngrohjes", + "held_at_100": "mbahet në 100%", + "ignored": "injoruar", + "kept": "mbajtur", + "max": "maks.", + "min": "min.", + "minus_tol": "-tol.", + "no_match": "pa përputhje", + "no_updates": "pa përditësime", + "off_delay_wait": "pritje fikjeje", + "off_gap": "hendek fikjeje", + "plus_tol": "+tol.", + "profile": "profili", + "reading_interval": "intervali tipik i leximeve", + "sensor_offline": "sensori jashtë linje: ndalim i detyruar", + "si": "IK", + "spike_ignored": "maja injorohet", + "spike_ignored_short": "maja: injoruar", + "start": "nisja", + "stop": "ndalimi", + "tail_energy_resets": "energjia e bishtit mbi prag: kohëmatësi rivendoset", + "thr": "prag", + "too_short": "shumë i shkurtër", + "tumble_kept": "pulset e rrotullimit mbahen" } } diff --git a/custom_components/ha_washdata/translations/panel/sr-Latn.json b/custom_components/ha_washdata/translations/panel/sr-Latn.json index 43c13f26..63668198 100644 --- a/custom_components/ha_washdata/translations/panel/sr-Latn.json +++ b/custom_components/ha_washdata/translations/panel/sr-Latn.json @@ -3,6 +3,7 @@ "min": "min", "entity_not_found": "Entitet nije pronađen", "running_long": "Traje predugo", + "stalled": "Zaglavljeno", "remaining": "preostalo", "progress": "napredak", "show_time_remaining": "Prikaži preostalo vreme", @@ -48,16 +49,17 @@ } }, "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_tip": "Pred kraj ovaj program utihne na oko {mins} min, a zatim povuče oko {watts} W tokom {secs} s. Uočeno u {seen} od {measured} izmerenih ciklusa.", "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", + "artifact_tip_one": "Otkrivena je {n} anomalija (npr. vrata otvorena usred ciklusa) – otvorite da je vidite na grafikonu", + "artifact_tip_few": "Otkrivene su {n} anomalije (npr. vrata otvorena usred ciklusa) – otvorite da ih vidite na grafikonu", + "artifact_tip_other": "Otkriveno je {n} anomalija (npr. vrata otvorena usred ciklusa) – otvorite da ih vidite na grafikonu", "auto": "(automatski otkriveno)", "built_in_tag": "ugrađen", "declining": "↘ opadajući", "energy_low": "Manja potrošnja energije nego obično", "energy_spike": "Veća potrošnja energije nego obično", - "fair_fit": "prihvatljiva kvaliteta", - "fair_fit_tip": "Umerena doslednost podudaranja – neki ciklusi dodeljeni ovom profilu imaju niže ocene pouzdanosti. Označite više ciklusa ili ponovo snimite profil da biste poboljšali tačnost.", "feedback_requested": "Zatražena povratna informacija", "golden_cycle": "Snimljeni referentni ciklus", "duration_outlier": "Ciklusi neobične dužine: {n}", @@ -66,27 +68,38 @@ "needs_review": "Potreban pregled", "overrun": "Trajao duže nego obično", "poor_fit": "loša kvaliteta", - "poor_fit_tip": "Nedosledna istorija podudaranja – razmislite o ponovnoj izgradnji ovog profila", - "restart_gap_tip": "{n} HA praznina ponovnog pokretanja: trag snage ima rupu", + "restart_gap_tip": "{n} HA praznina ponovnog pokretanja: trag snage ima rupe", + "restart_gap_tip_one": "{n} HA praznina ponovnog pokretanja: trag snage ima rupu", + "restart_gap_tip_few": "{n} HA praznine ponovnog pokretanja: trag snage ima rupe", + "restart_gap_tip_other": "{n} HA praznina ponovnog pokretanja: trag snage ima rupe", "reviewed": "Pregledano", "steady": "→ stabilan", "underrun": "Završio se brže nego obično", - "using_defaults": "Koriste se zadana podešavanja", - "using_tuned": "Koriste se podešene težine", "overrun_ratio": "({x}x očekivanog)", "underrun_ratio": "({pct}% očekivanog)", - "awaiting_tip": "Čeka odobrenje zajednice", - "awaiting_n": "Čeka odobrenje · potvrđeno {n}", - "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.", - "armed": "(primenjuje se na sledeći ciklus)" + "armed": "(primenjuje se na sledeći ciklus)", + "awaiting_tip": "Čeka odobrenje zajednice", + "awaiting_n": "Čeka odobrenje · potvrđeno {n}", + "awaiting": "Čeka odobrenje", + "approved": "Odobreno", + "matcher_unused": "Još bez podudaranja sa vašim ciklusima", + "matcher_used": "Podudaranje u {n} od vaših ciklusa", + "matcher_used_one": "Podudaranje u {n} od vaših ciklusa", + "matcher_used_few": "Podudaranje u {n} od vaših ciklusa", + "matcher_used_other": "Podudaranje u {n} od vaših ciklusa", + "matcher_used_tip": "Ciklusi na ovom uređaju koje je WashData sam podudario sa ovim uvezenim programom. Uvoz koji se nikada ne podudari možda ne odgovara vašem uređaju; možete ga ukloniti. Broji se lokalno; ništa se ne šalje u prodavnicu." }, "btn": { + "pg_preset_delete": "Izbriši konfiguraciju", + "pg_preset_load": "Učitaj konfiguraciju", + "pg_preset_load_tip": "Zameni vrednosti u testnom okruženju ovom konfiguracijom (aktivna podešavanja ostaju netaknuta)", + "pg_preset_save": "Sačuvaj kao konfiguraciju", + "pg_preset_save_tip": "Sačuvaj sve vrednosti ispod kao imenovanu konfiguraciju za ovaj uređaj", "reset_zoom": "Poništi zumiranje", "set_brand_model": "Postavi marku i model", "refresh_catalog": "Osveži katalog", @@ -97,12 +110,11 @@ "add_timer": "+ Dodaj tajmer", "apply_all": "Primeni sve", "apply_label": "Primeni oznaku", - "apply_set_b": "Primeni skup B", "apply_split": "Primeni podelu", "apply_trim": "Primeni isecanje", "auto_detect_split": "Automatski detektuj", "auto_label_cycles": "Automatski označi cikluse", - "auto_label_cycles_tip": "Automatski dodeli nazive profila neoznačenim ciklusima čija pouzdanost podudaranja prelazi prag", + "auto_label_cycles_tip": "Imenujte neoznačene cikluse čija pouzdanost podudaranja prelazi prag.", "cancel": "Otkaži", "clear_debug": "Obriši podatke za otklanjanje grešaka", "clear_splits": "Obriši", @@ -112,8 +124,6 @@ "convert_to_automation": "Pretvori u automatizaciju", "correct": "Ispravi…", "create": "Napravi", - "create_from_cluster": "Napravi profil od {count} ciklusa", - "create_profile": "+ Napravi profil", "delete": "Izbriši", "delete_group": "Izbriši grupu", "delete_group_tip": "Izbriši samo ovu grupu - profili članova se čuvaju", @@ -122,39 +132,30 @@ "discard_tip": "Odbaci snimljeni trag bez čuvanja", "dismiss": "Odbij", "mute_suggestion": "Prestani predlagati ovo podešavanje", - "pg_load_calibrated": "Učitaj kalibrisano (ML) ({n})", - "pg_load_calibrated_tip": "Učitaj ML-kalibrisana podešavanja kao zamene u testnom okruženju", "pg_load_live": "Učitaj aktivna podešavanja", "pg_load_live_tip": "Ponovo pročitaj trenutna podešavanja integracije i odbaci sve izmene u testnom okruženju", "pg_load_suggested": "Učitaj predloženo ({n})", "pg_load_suggested_tip": "Učitaj trenutne predloge automatskog podešavanja kao zamene u testnom okruženju", - "pg_preset_delete": "Izbriši konfiguraciju", - "pg_preset_load": "Učitaj konfiguraciju", - "pg_preset_load_tip": "Zameni vrednosti u testnom okruženju ovom konfiguracijom (aktivna podešavanja ostaju netaknuta)", - "pg_preset_save": "Sačuvaj kao konfiguraciju", - "pg_preset_save_tip": "Sačuvaj sve vrednosti ispod kao imenovanu konfiguraciju za ovaj uređaj", "pg_publish_all": "Primeni {n} na integraciju", "pg_publish_one": "Primeni {label} na integraciju", "reset_muted": "Resetuj utišane", + "reset_phase_tip": "Poništite svoju izmenu i vratite ugrađenu fazu", "done": "Gotovo", "edit": "Uredi", "export": "Izvezi", - "export_json": "Izvezi u JSON", "force_stop": "Prisilno zaustavi", "force_stop_tip": "Odmah završi trenutni ciklus i označi ga kao prisilno zaustavljen", "from_template": "Iz šablona ▾", "ignore": "Ignoriši", "import_json": "Uvezi iz JSON-a", - "import_overwrite": "Uvezi (prepisuje podatke)", "inspect": "Inspektiraj", "label": "Označi", - "load": "Učitaj", "load_more": "Učitaj još", "manage": "Upravljaj", "merge": "Spoji", "new_automation": "+ Nova automatizacija", "new_group": "+ Nova grupa", - "new_group_tip": "Grupiši skoro identične profile (isti oblik/trajanje, različita temperatura ili centrifugiranje) da podudaranje pouzdano bira između njih", + "new_group_tip": "Grupišite skoro iste profile (isti oblik, druga temperatura ili centrifuga) kako bi ih podudaranje moglo razlikovati.", "new_phase": "+ Nova faza", "new_profile": "+ Novi profil", "new_profile_tip": "Napravi novi profil programa iz postojećeg označenog ciklusa ili snimka", @@ -162,23 +163,19 @@ "on_cycle_started": "Po pokretanju ciklusa", "pause_cycle": "Pauziraj", "pause_cycle_tip": "Pauziraj aktivni ciklus – uređaj će se nastaviti od mesta gde je stao", - "pg_apply_device": "Primeni na uređaj", - "pg_autofill": "Automatsko popunjavanje", - "play": "Reprodukuj", "process": "Obradi", "process_history": "Obradi sada", "process_recording": "Sačuvaj snimak", "process_tip": "Sačuvaj snimljeni trag kao novi ili postojeći profil", "rebuild": "Obnovi omotače", "rebuild_envelope": "Obnovi omotač", - "rebuild_tip": "Ponovo izračunaj očekivani omotač snage (min/maks opseg) za sve profile iz njihovih označenih ciklusa – pokreni nakon označavanja novih ciklusa ili ispravljanja starih", + "rebuild_tip": "Ponovo izgradite očekivani pojas snage svakog profila iz njegovih označenih ciklusa. Pokrenite nakon označavanja ili ispravljanja ciklusa.", "rec_start_tip": "Počni snimanje traga snage uređaja – pokreni neposredno pre pokretanja ciklusa", "rec_stop": "Zaustavi", "rec_stop_tip": "Zaustavi snimanje i zadrži snimljeni trag radi pregleda", "record": "Počni snimanje", "refresh": "Osveži", "refresh_diag_tip": "Ponovo učitaj statistike skladištenja", - "refresh_logs_tip": "Ponovo učitaj najnovije zapise dnevnika", "refresh_settings_tip": "Ponovo učitaj podešavanja sa servera", "relabel": "Promeni oznaku ({count})", "remove": "Ukloni", @@ -186,7 +183,6 @@ "reset": "Resetuj", "reset_ml_models_tip": "Odbaci fino podešene modele i vrati se na ugrađene. WashData ih može ponovo naučiti kasnije.", "reset_to_builtin": "Vrati na ugrađene modele", - "reset_to_defaults": "Vrati na zadana podešavanja", "resume_cycle": "Nastavi ciklus", "resume_cycle_tip": "Nastavi pauzirani ciklus", "revert_settings": "Odbaci izmene", @@ -199,8 +195,6 @@ "run_analysis": "🔍 Pokreni analizu prijedloga", "run_analysis_tip": "Analiziraj svoje snimljene cikluse sada i osveži prijedloge podešavanja", "run_auto_label": "Pokreni automatsko označavanje", - "run_both": "Pokreni A naspram B", - "run_simulation": "Pokreni simulaciju", "save": "Sačuvaj", "save_access_control": "Sačuvaj kontrolu pristupa", "save_group": "Sačuvaj grupu", @@ -212,10 +206,8 @@ "save_settings": "Sačuvaj podešavanja", "select": "Izaberi", "select_all": "Izaberi sve", - "select_last20": "Poslednjih 20", "select_none": "Očisti", "show_only": "Prikaži samo", - "skip_setup": "Preskoči podešavanje", "split": "Podeli", "stop": "Zaustavi", "submit_correction": "Pošalji ispravku", @@ -224,30 +216,24 @@ "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.", + "unlabel_selected_tip": "Uklonite oznaku sa izabranih ciklusa. Ostaju u istoriji, ali prestaju da oblikuju ovaj profil.", "use": "Koristi", "wipe_all": "Obriši sve podatke", "delete_selected": "Izbriši izabrano ({n})", - "group_suggest": "Grupa {n}: {members}", "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", "import": "Uvezi", "disconnect": "Prekini vezu", "connect_github": "Poveži GitHub", - "save_appliance": "Sačuvaj uređaj", "share_to_store": "Podeli u prodavnicu", "share": "Podeli", - "confirm_appliance": "Potvrdi ovaj uređaj", - "confirmed": "Potvrđeno", "browse_community_setups": "Pregledaj podešavanja zajednice", "download_device": "Preuzmi podešavanja uređaja", "share_device": "Podeli podešavanja uređaja", - "share_device_tip": "Podelite programe i podešavanja ovog uređaja sa zajednicom", + "share_device_tip": "Podelite ovaj uređaj i njegove referentne cikluse u prodavnici zajednice kako bi ih vlasnici iste mašine mogli preuzeti.", "share_n": "Podeli cikluse ({n})", "export_selected": "Izvoz (izbor podataka)", "export_all": "Brzi izvoz svega", - "import_raw": "Napredno: zameni sve iz JSON-a", "download_export": "Preuzmi izvoz", "analyze_import": "Analiziraj datoteku", "import_selected": "Uvezi izabrano", @@ -260,7 +246,15 @@ "save_total_cycles": "Sačuvaj ukupan broj", "clear": "Obriši", "add_brand": "Dodaj marku", - "add_appliance": "Dodaj uređaj" + "log_done_task": "Označi kao obavljeno: {task}", + "add_task": "Dodaj zadatak", + "add_appliance": "Dodaj uređaj", + "retry": "Pokušaj ponovo", + "confirm_appliance": "Potvrdi ovaj uređaj", + "confirmed": "Potvrđeno", + "create_profile_from_gap": "Napravi profil", + "undo_import": "Opozovi poslednji uvoz", + "pg_rerun": "Ažuriraj simulaciju" }, "note": { "off_delay": { @@ -268,10 +262,6 @@ } }, "conflict": { - "anti_wrinkle_exit": { - "exit": "Mora biti ispod Praga zaustavljanja ({stop} W); inače se izlazna snaga zaštite od gužvanja zanemaruje", - "stop": "Mora biti iznad Praga izlazne snage zaštite od gužvanja ({exit} W)" - }, "anti_wrinkle_max": { "max": "Mora biti iznad Praga pokretanja ({start} W); inače se limit trajanja zaštite od gužvanja zaobilazi", "start": "Mora biti ispod Maks. snage zaštite od gužvanja ({max} W)" @@ -309,14 +299,14 @@ "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." + "power_sensor": "Ovo je Senzor snage uređaja, a ne cena. Ciklusi bi se obračunavali u vatima po kWh; obrišite ovo polje.", + "energy_sensor": "Ovo je Brojilo energije uređaja, a ne cena. Ciklusi bi se obračunavali po očitavanju brojila po kWh; obrišite ovo polje.", + "device_class": "Ovaj senzor meri {dc}, a ne cenu po kWh. Obrišite ovo polje ili izaberite senzor tarife.", + "unit": "Ovaj senzor očitava u {unit}, a ne cenu po kWh. Obrišite ovo polje ili izaberite 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" + "duration": "Mora biti manje od Vremenskog ograničenja bez ažuriranja ({to} s) kako bi se alarm zaglavljene pumpe aktivirao prvi", + "timeout": "Mora premašiti Trajanje zaglavljene pumpe ({ps} s) kako bi se alarm zaglavljene pumpe aktivirao prvi" }, "start_dur": { "sampling": "Interval uzorkovanja premašuje Trajanje pokretanja ({sdt} s); jednokratni impuls može otvoriti ciklus", @@ -334,10 +324,15 @@ "sampling": "Interval uzorkovanja treba biti najviše pola Intervala nadzornika ({wi} s)" }, "settings_banner": "Konflikti podešavanja: {n}. Proverite istaknute odeljke i ispravite ih pre čuvanja.", - "settings_banner_btn": "Idi na prvi" + "settings_banner_btn": "Idi na prvi", + "end_energy": { + "energy": "Previše strogo za Prag zaustavljanja ({w} W) tokom Kašnjenja isklopa ({d} s); ciklus može da se završi samo rezervnim putem", + "stop": "Energija kraja ({e} Wh tokom {d} s) dozvoljava samo {p} W" + } }, "hdr": { - "ab_comparison": "Poređenje podešavanja A/B", + "custom_tasks": "Sopstveni zadaci", + "maintenance_supporter": "Treba vam više?", "access_control": "Kontrola pristupa", "add_maintenance": "Dodaj događaj održavanja", "automation_delete": "Izbriši ovu automatizaciju", @@ -346,28 +341,22 @@ "automation_review_this_cycle": "Pregledaj ovaj ciklus", "automations": "Automatizacije", "clear_debug": "Obriši tragove otklanjanja grešaka", - "cycle_simulator": "Simulator ciklusa", "diagnostics_pref": "Dijagnostika", "display": "Prikaz", - "dtw_inspector": "DTW inspektor", "export_import": "Izvezi / Uvezi", - "getting_started": "Prvi koraci", "live_power": "Živa snaga", "logs": "Dnevnici", - "logs_diagnostics": "Dijagnostika", "maintenance": "Akcije održavanja", "maintenance_log": "Dnevnik održavanja", "maintenance_reminders": "Podsetnici za servis", "manual_recording": "Ručno snimanje", - "manual_recording_tip": "Namerno pokrenite ciklus dok WashData snima trag snage. Pokrenite neposredno pre nego što uređaj počne, zaustavite kada završi, zatim obradite da sačuvate kao imenovani profil.", + "manual_recording_tip": "Namerno snimite ciklus: pritisnite Počni snimanje neposredno pre pokretanja uređaja i Zaustavi kad završi, zatim Obradi da ga sačuvate kao profil.", "ml_learned": "Šta je WashData naučio", - "ml_matching_tuning": "Fino podešavanje podudaranja programa", "ml_settings_card": "Podešavanja", "ml_smart_learning": "Pametno učenje", "my_preferences": "Moje preference", "no_automations": "Nijedna automatizacija ne referencira ovaj uređaj još.", "panel_settings": "Podešavanja panela", - "param_sweep": "Ispitivanje parametra", "pg_settings_source": "Izvor podešavanja", "phase_catalog": "Katalog faza", "playground": "Testno okruženje", @@ -376,11 +365,8 @@ "status": "Status", "status_graph": "Grafikon statusa", "storage_stats": "Statistike skladištenja", - "test_history": "Provera na istoriji", "toggle_sidebar": "Prebaci Home Assistant bočnu traku", - "tools_and_data": "Alati i podaci", "wipe_history": "Obriši istoriju", - "recommendations": "Preporuke ({n})", "pg_detection_params": "Podešavanja otkrivanja", "pg_outcome": "Ishod simulacije", "pg_across_cycles": "Kroz sve vaše cikluse", @@ -388,7 +374,8 @@ "online_account": "Prodavnica zajednice i online funkcije", "import_power_history": "Uvoz istorije snage", "service_status": "Status servisa", - "online_account_tab": "Online i zajednica" + "online_account_tab": "Online i zajednica", + "live_match_debug": "Dijagnostika poklapanja uživo" }, "health": { "fair": "Prihvatljiva kvaliteta profila", @@ -396,11 +383,16 @@ "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_tip": "Položaj na snimljenoj krivoj podudarenog programa: {pct}%. Određuje se poravnanjem ciklusa sa profilom, pa ostaje tačan kad je rad duži ili kraći. Osvežava se dok je uređaj tih, pa 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", + "task_name": "Zadatak", + "every_cycles": "Interval (ciklusi)", + "every_days": "Interval (dani)", + "new_task_name": "Novi zadatak", + "days_since_service": "{since} / {total} dana", "actions": "Akcije", "activity": "Aktivnost", "administrators": "Administratori", @@ -408,10 +400,8 @@ "aria_cycle_chart": "Kriva snage ciklusa", "aria_envelope_chart": "Grafikon anvelope snage profila", "aria_phase_chart": "Grafikon uređivača faza", - "aria_playground_chart": "Kriva snage ciklusa sa pomerljivim pragovima detekcije", "aria_power_chart": "Grafikon potrošnje snage", "aria_spaghetti_chart": "Preklopljene krive snage ciklusa", - "aria_sweep_chart": "Grafikon rezultata pretrage parametara", "artifact_anomaly": "Anomalija", "artifact_high_power": "Visoka snaga", "artifact_interruption": "Prekid", @@ -425,87 +415,62 @@ "cohesion_good": "kohezija {pct}%", "cohesion_low": "⚠ niska kohezija {pct}%", "compare_profiles": "Uporedi sa profilima", - "concurrency": "Istovremenost", "confidence": "Pouzdanost", "confidence_threshold": "Prag pouzdanosti", "correct_duration": "Ispravi trajanje (min, opciono)", "correct_profile": "Ispravljeni profil", - "correlation": "Korelacija", "cost": "Trošak", "create_new_profile": "+ Napravi novi profil…", "current_phase": "Trenutna faza", - "curves": "Krive", "cycle": "Ciklus", "cycle_date_display": "Prikaz datuma ciklusa", - "cycle_health": "Zdravlje ciklusa", - "cycle_index": "indeks ciklusa →", - "cycles": "Ciklusi", "cycles_count": "Ciklusi", "cycles_lc": "ciklusa", - "dashed": "isprekidano", "date": "Datum", "debug_traces": "Tragovi za otklanjanje grešaka", - "default": "Podrazumevano", "default_access_level": "Zadani nivo za korisnike koji nisu navedeni ispod", "default_other": "Podrazumevano (ostali uređaji)", "default_tab": "Podrazumevana kartica pri otvaranju panela", "font_size": "Veličina fonta panela", - "delta": "Δ", "description": "Opis", - "dtw_blend_w": "Težina mešanja", - "dtw_blended": "Mešani rezultat", - "dtw_ddtw": "DDTW rezultat", - "dtw_l1": "L1 rezultat", - "dtw_matched_profile": "(uparen profil ciklusa)", "duration": "Trajanje", - "duration_agreement": "Slaganje trajanja", - "emphasis": "Naglasak", "enable_access_control": "Omogući kontrolu pristupa po korisniku", "energy": "Energija", - "energy_agreement": "Slaganje energije", - "event_timeline": "Vremenska osa događaja", "event_type": "Tip događaja", "expected": "Očekivano", + "suspected_profile": "{name} (pretpostavka)", + "runner_up_profile": "{name} (drugi najbolji)", "expected_duration": "Očekivano trajanje (min)", "file_kb": "Datoteka (kB)", - "final_score": "Konačni rezultat", "from": "Od", "gap_s": "Razmak (s)", "group_name": "Naziv grupe", "head_trim": "Isecanje početka (s)", - "health": "Zdravlje", "hide_tabs": "Sakrij kartice za ne-administratore", - "in_use": "U upotrebi", "json_data": "JSON podaci", "label": "Odgovarajući program", "last": "Poslednjih", "last_checked": "Poslednja provera:", "load_from_file": "Učitaj iz datoteke", - "mae_score": "MAE rezultat", "manual_duration": "Ručno trajanje (min, opciono)", "match": "Poklapanje", "members": "Članovi", - "metric": "Metrika", "mode_existing_profile": "Dodaj u postojeći profil", "mode_new_profile": "Napravi novi profil", "models_fine_tuned": "(fino podešeni modeli: {count})", - "n_classic_suggestions": "{n} klasično", - "n_ml_suggestions": "{n} ML", "n_selected": "{n} izabrano", - "n_tuning_suggestions": "{n} predlog podešavanja", - "na": "n/d", + "n_tuning_suggestions": "{n} predloga podešavanja", + "n_tuning_suggestions_one": "{n} predlog podešavanja", + "n_tuning_suggestions_few": "{n} predloga podešavanja", + "n_tuning_suggestions_other": "{n} predloga podešavanja", "new_profile_name": "Novi naziv profila", "notes": "Beleške", - "of": "od", - "outcome": "Ishod", "panel_default_tab": "Podrazumevana kartica", "panel_language": "Jezik table", "per_cycle_short": "ciklus", - "pg_autofill_tip": "Automatski popuni opseg iz trenutne vrednosti ({lo}–{hi})", "pg_best_value": "Pronađena najbolja vrednost", "pg_current_value": "Trenutna vrednost:", "pg_drag_hint": "Prevucite liniju na grafikonu", - "pg_dtw": "DTW", "pg_ev_ending": "Završava se", "pg_ev_running": "U radu", "pg_group_advanced": "Granični slučajevi", @@ -513,63 +478,39 @@ "pg_group_timing": "Pravila vremena", "pg_idle": "U mirovanju", "pg_anti_wrinkle": "Zaštita od gužvanja", - "pg_last_run": "Poslednje pokretanje: {date}", - "pg_match_score": "Ocena poklapanja", - "pg_pct_of": "{p}% od {total}", "pg_poor_match": "Loše poklapanje", "pg_preset": "Konfiguracija testnog okruženja", "pg_preset_name": "Naziv nove konfiguracije", "pg_preset_none": "Izaberite konfiguraciju…", "pg_strong_match": "Snažno poklapanje", - "pg_tile_ambiguous": "Dvosmisleni", - "pg_tile_ambiguous_tip": "Ciklusi sa gotovo izjednačenim poklapanjem između dva programa", - "pg_tile_detected": "Otkriveni", - "pg_tile_detected_tip": "Ciklusi kod kojih je snaga prešla prag pokretanja dovoljno dugo da se registruje kao rad", - "pg_tile_matched": "Poklopljeni", - "pg_tile_matched_tip": "Ciklusi ispravno pripisani poznatom programu", - "pg_time_left": "Preostalo vreme", "pg_weak_match": "Slabo poklapanje", "phase_name": "Naziv faze", "name": "Naziv", "phase_timeline": "Vremenska osa faza", "power": "Snaga", "profile": "Profil", - "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. Izaberite program pre pokretanja uređaja i primeniće se čim počne sledeći ciklus.", + "program_tip": "Izaberite sa kojim se programom podudara trenutni ciklus. Automatska detekcija bira najbolje podudaranje. Zakačite program kad automatska detekcija greši; program izabran pre pokretanja važi za sledeći ciklus.", "progress": "Napredak", "raw_socket": "Sirovi priključak", - "read_only": "Samo za čitanje", "ref_cycle": "Referentni ciklus (opciono)", - "refresh_interval": "Interval osvežavanja (s)", "remaining": "Preostalo", "reminder_every": "Podseti svakih (ciklusa)", "remove_label": "- Ukloni oznaku -", "rename_profile": "Preimenuj profil", - "replay_duration": "Reprodukcija", "resulting_profile": "Rezultujući profil", "save_mode": "Režim čuvanja", - "score": "Rezultat", - "score_breakdown": "Raščlanjivanje rezultata", "seconds_from_start": "Sekunde od početka", "select_profile": "Izaberi profil", "setting": "Podešavanje", - "settings_a": "Podešavanja A (trenutna)", - "settings_a_short": "A", "settings_advanced": "Napredno", - "settings_b": "Podešavanja B (test)", - "settings_b_short": "B", "settings_basic": "Osnovno", "settings_detail_level": "Nivo detalja podešavanja", - "settings_overrides": "Zamene podešavanja", "show_debug": "Prikaži karticu za otklanjanje grešaka uživo na stranici Statusa (pouzdanost, višeznačnost, najbolji kandidati)", "show_expected": "Prikaži preklop očekivane krive (usklađeni profil, narandžasta)", "show_raw": "Prikaži prekidač sirovih očitavanja utičnice u živom grafu snage", "sparkline": "Nedavni trend trajanja ciklusa", - "stage2": "Etapa 2 – osnovna sličnost", - "stage3": "Etapa 3 – DTW", - "stage4": "Etapa 4 – slaganje", "status": "Status", "tail_trim": "Isecanje kraja (s)", "tap_to_dismiss": "dodirnite grafikon da zatvorite", @@ -580,7 +521,6 @@ "ungrouped": "Negrupisano", "unlabelled": "Neoznačeno", "unlabelled_paren": "(neoznačeno)", - "warp_path": "Putanja deformacije", "input": "Ulaz:", "start": "Početak", "end": "Kraj", @@ -605,9 +545,8 @@ "n_shown": ", prikazano {n}", "cycles_title": "Ciklusi ({n})", "pg_detecting": "Otkrivanje", - "pg_best_marker": "Najbolji: {v}", "pg_time_left_model": "Preostalo vreme (model)", - "pg_time_left_model_tip": "Procena preostalog vremena modelom (procenjivač faza + ML), a ne statično odbrojavanje", + "pg_time_left_model_tip": "Procena prema fazama programa, a ne fiksno odbrojavanje", "phase": "Faza", "pg_mode_history": "Test na istoriji", "pg_ev_detected": "Otkriveno", @@ -647,18 +586,8 @@ "pg_mode_optimize": "Optimizuj", "pg_group_matching": "Podudaranje programa", "pg_zoom_hint": "dvostruki klik poništava zumiranje", - "aria_playground_chart2": "Interaktivni grafikon snage ciklusa: pređite mišem za prikaz vremena/snage, pomerajte točkić za zumiranje, prevucite za pomeranje", "pg_alert_timeout_end": "Završeno zbog isteka vremena, a ne na osnovu predviđanja", "pg_alert_indefinite": "Radio bi beskonačno", - "pg_alert_stress_cap": "Dostignut sigurnosni limit", - "pg_alert_stress_ok": "Detekcija mirovanja: ciklus zaustavljen", - "pg_alert_stress_warn": "Potrošnja u mirovanju iznad praga zaustavljanja", - "pg_stress_group": "Test detekcije mirovanja", - "pg_stress_idle_auto": "Auto", - "pg_stress_idle_w": "Nivo mirovanja (W)", - "pg_stress_idle_w_desc": "Prepiši automatski otkriven minimalni nivo potrošnje u stanju mirovanja (ostavi prazno za automatsko otkrivanje).", - "pg_stress_toggle": "Testiraj detekciju mirovanja", - "pg_stress_toggle_desc": "Simulira da uređaj ostaje na potrošnji u stanju mirovanja nakon završetka snimanja -- prikazuje da li i kada WashData zaustavlja ciklus.", "task_pg_history": "Test na istoriji", "task_pg_sweep": "Optimizuj", "task_reprocess": "Ponovna obrada", @@ -668,16 +597,9 @@ "pg_recent_runs": "Nedavna pokretanja", "pg_load_run": "Ponovo učitaj ovo pokretanje", "pg_match_confidence": "Pouzdanost poklapanja", - "pg_envelope_fit": "Uklapanje u omotač", - "pg_envelope_fit_tip": "Koliko dobro ciklus ostaje unutar sačuvanog omotača snage ovog profila, drugačiji ugao od pouzdanosti poklapanja.", "n_imported_note": ", {n} uvezeno", "enable_online": "Omogući online funkcije", - "store_brand": "Marka uređaja", - "store_model": "Model uređaja", "anonymous": "Anonimno", - "rate_quality": "Oceni kvalitet", - "rate_n": "{n} zvezdica", - "approved_only": "Samo odobreno", "adopt_settings": "Prihvati podešavanja", "flags": "Oznake", "include_phase_map": "Uključi mapu faza", @@ -694,7 +616,6 @@ "cat_custom_phases": "Prilagođene faze", "cat_profile_groups": "Grupe profila", "cat_settings": "Podešavanja detekcije i prepoznavanja", - "cat_matching_config": "Fino podešavanje prepoznavanja", "cat_ml_models": "ML modeli", "cat_feedback": "Povratne informacije i oznake pregleda", "cat_suggestions": "Predlozi", @@ -714,7 +635,6 @@ "conflict_overwrite": "Prepiši", "hist_csv_data": "CSV podaci", "hist_from_recorder": "Ili je pročitajte iz Home Assistant", - "days": "dana", "hist_keep": "Zadrži ovaj ciklus", "hist_looks_complete": "završen", "peak_power_short": "Vrh", @@ -735,7 +655,42 @@ "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" + "correct_total_cycles": "Ispravi ukupan broj", + "hist_reason_already_recorded": "preklapa se sa ciklusom koji je WashData već zabeležio", + "share_consent": "Potvrđujem da su ovi ciklusi normalno završeni bez prekida", + "show_internal": "Prikaži interna podešavanja", + "show_internal_tip": "Podešavanja koja WashData podešava umesto vas (uzorkovanje, nadzornik, učestalost podudaranja, pragovi). Prikazuju se kad ovaj uređaj ima sopstvenu vrednost, konflikt ili predlog.", + "rate_n": "{n} zvezdica", + "rate_quality": "Oceni kvalitet", + "pg_autolabel": "Automatsko označavanje", + "pg_label_review": "Traži pregled", + "pg_ev_match_reverted": "Poklapanje poništeno", + "pg_ev_verified_pause": "Potvrđena pauza", + "n_samples": "{n} uzoraka", + "n_samples_one": "{n} uzorak", + "n_samples_few": "{n} uzorka", + "n_samples_other": "{n} uzoraka", + "n_cycles": "{n} ciklusa", + "n_cycles_one": "{n} ciklus", + "n_cycles_few": "{n} ciklusa", + "n_cycles_other": "{n} ciklusa", + "energy_per_cycle": "{v}/ciklus", + "energy_total": "ukupno {v}", + "automation_off": "(isključeno)", + "ref_cycle_none": "Nijedan", + "share_name": "Prikaži moje ime uz ono što delim", + "cat_backfill_cycles": "Ciklusi pronađeni u uvezenoj istoriji snage", + "pg_compare_profile": "Uporedi sa profilom", + "pg_obj_early": "Stopa ranih završetaka", + "pg_obj_endlag": "Kašnjenje završetka (medijana)", + "pg_obj_split": "Stopa podeljenih ciklusa", + "pg_sweep_cycles": "Nedavni ciklusi", + "cycle_context": "Istorija recordera oko ciklusa", + "cycle_context_off": "Isključeno", + "cycle_context_minutes": "{min} min", + "cycle_context_series": "Istorija recordera", + "before_start": "Pre početka", + "after_end": "Posle kraja" }, "log": { "all_levels": "Svi nivoi", @@ -745,11 +700,9 @@ }, "ml": { "builtin_models": "● Koriste se ugrađeni modeli", - "fit_fair": "Prihvatljivo", "fit_good": "Dobro", "fit_slight": "Blago", "fit_strong": "Snažno", - "fit_weak": "Slabo", "fit_word": "uklapanje", "personalized": "● Personalizovano za ovu mašinu", "better_than_baseline": "{pct}% bolje od osnovne procene ({metric})", @@ -757,21 +710,31 @@ "trend_declining_tip": "Prilagođenost ovog modela se pogoršala u nedavnim ponovnim provjerama, pregled više ciklusa može mu pomoći da ponovo nauči.", "trend_steady_tip": "Prilagođenost ovog modela je ostala otprilike stabilna u nedavnim ponovnim provjerama.", "cap_blurb": { - "end": "Prepoznavanje kada je ciklus zaista završen", - "live_match": "Brže prepoznavanje programa koji je u toku", - "quality": "Uočavanje pogrešno prepoznatih ili oštećenih ciklusa", - "remaining_time": "Predviđanje preostalog vremena", "total_energy": "Predviđanje ukupne potrošnje energije i troškova" }, "cap_label": { - "end": "Prepoznavanje kraja ciklusa", - "live_match": "Prepoznavanje programa", - "quality": "Provera kvaliteta ciklusa", - "remaining_time": "Procena preostalog vremena", "total_energy": "Procena potrošnje energije" }, - "metric_auc": "AUC {auc} na izdvojenim podacima", - "metric_mae": "greška {model} u odnosu na osnovnu vrednost {baseline}" + "metric_mae": "greška {model} u odnosu na osnovnu vrednost {baseline}", + "fine_tuned_at": "fino podešeno {when}", + "held_out_count": "izmereno na {count} izdvojenih ciklusa", + "held_out_count_one": "izmereno na {count} izdvojenom ciklusu", + "held_out_count_few": "izmereno na {count} izdvojena ciklusa", + "held_out_count_other": "izmereno na {count} izdvojenih ciklusa", + "last_run_not_promoted": "Poslednja provera ({when}): {reason}", + "never": "nikad", + "skip_reason": { + "fit_failed": "Treniranje nije uspelo: {error}", + "holdout_too_small": "Premalo ciklusa za pošteno testiranje novog modela: za testiranje je moglo da se izdvoji {held_out}, a potrebno je {min}.", + "insufficient_rows": "Još nema dovoljno upotrebljivih ciklusa: zasad {count} označenih, čistih ciklusa.", + "insufficient_rows_one": "Još nema dovoljno upotrebljivih ciklusa: zasad {count} označen, čist ciklus.", + "insufficient_rows_few": "Još nema dovoljno upotrebljivih ciklusa: zasad {count} označena, čista ciklusa.", + "insufficient_rows_other": "Još nema dovoljno upotrebljivih ciklusa: zasad {count} označenih, čistih ciklusa.", + "not_better_than_incumbent": "Novi model nije bio bolji od onog koji se koristi (greška {model} u odnosu na {incumbent}), pa je zadržan trenutni.", + "not_better_than_naive": "Novi model nije bio jasno bolji od jednostavne procene (greška {model} u odnosu na {naive}).", + "unknown": "Ništa novo nije naučeno." + }, + "trained_unknown": "u nepoznato vreme" }, "modal": { "auto_label": "Automatski označi cikluse", @@ -780,7 +743,6 @@ "cycle": "Ciklus", "edit_group": "Uredi grupu profila", "edit_phase": "Uredi fazu", - "import_config": "Uvezi konfiguraciju", "label_cycle": "Označi ciklus", "merge_cycles": "Spoji {n} ciklusa", "new_group": "Nova grupa profila", @@ -795,6 +757,8 @@ "delete_automation_title": "Izbriši automatizaciju", "delete_automation_msg": "Izbrisati automatizaciju \"{name}\" iz Home Assistant? Ovo se ne može poništiti.", "remove_legacy_title": "Ukloni zastarele akcije", + "reset_phase_msg": "Poništiti vašu izmenu faze \"{name}\" i vratiti ugrađenu fazu? Programi zadržavaju svoje opsege.", + "reset_phase_title": "Resetuj fazu", "remove_legacy_msg": "Ukloniti zastarele prilagođene akcije? Prestaće da se aktiviraju na događajima ciklusa. Ovo se ne može poništiti iz panela.", "discard_recording_title": "Odbaci snimak", "discard_recording_msg": "Odbaciti sačuvani snimak? Ovo se ne može poništiti.", @@ -815,32 +779,45 @@ "store_share_device": "Podeli podešavanja uređaja", "export_select": "Izvoz - izbor podataka", "import_wizard": "Uvoz - izbor podataka", - "history_import": "Uvoz istorije snage" + "history_import": "Uvoz istorije snage", + "apply_suggestions_title": "Primeni predloge", + "remove_task_title": "Ukloni zadatak", + "remove_task_msg": "Ukloniti podsetnik \"{task}\"? Njegovi unosi u dnevniku održavanja ostaju sačuvani.", + "apply_suggestions_msg": "Promeniti ova podešavanja ({n})? Integracija se ponovo učitava nakon čuvanja.", + "discard_settings_title": "Odbaciti nesačuvane izmene?", + "discard_settings_msg": "Nesačuvane izmene podešavanja: {n}. Napustiti Podešavanja i odbaciti ih?", + "profile_title": "Profil · {name}", + "undo_import_msg": "Podaci i podešavanja ovog uređaja vraćaju se na stanje pre poslednjeg uvoza ({when}). Ciklusi zabeleženi i promene napravljene od tada biće izgubljeni.", + "undo_import_ok": "Opozovi uvoz", + "undo_import_title": "Opozovi poslednji uvoz" }, "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.", + "pg_preset_delete_confirm": "Izbrisati konfiguraciju \"{name}\"?", + "pg_preset_limit": "Dostignut limit konfiguracija ({n})", + "pg_preset_overwrite": "Prepisati konfiguraciju \"{name}\"?", "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.", - "refresh_catalog_hint": "Liste marki i uređaja zajednice privremeno se čuvaju kako bi prodavnica zajednice ostala u okviru svog dnevnog ograničenja. Osvežite da preuzmete unose koje su drugi dodali ili odobrili.", + "store_sibling_hint": "Ništa nije podeljeno za vaš model? Srodan model iste marke obično je dobar početak.", + "store_declare_appliance": "Recite WashData koji uređaj imate da vidite podešavanja koja su drugi podelili za njega. Ili upišite marku iznad i razgledajte.", + "refresh_catalog_hint": "Liste marki i uređaja zajednice privremeno se čuvaju zbog dnevnog ograničenja prodavnice. Osvežite da vidite unose koje su drugi dodali ili odobrili.", "head_trim_hint": "Uklonite ovoliko sekundi sa početka", "appliance_monitor": "Monitor uređaja", "artifact_dip_detail": "Pao ispod uobičajenog opsega snage za ~{n} s.", "artifact_footer": "Istaknuto na grafikonu iznad. Ovo su prolazni artefakti (npr. vrata se otvaraju usred ciklusa), ne nužno problemi.", "artifact_header": "{n} anomalija otkrivenih tokom ovog ciklusa", - "artifact_pause_detail": "Snaga je pala gotovo na nulu za ~{n} s i zatim se obnovila – verovatno su se otvorila vrata usred ciklusa ili je ciklus bio pauziran.", + "artifact_header_one": "{n} anomalija otkrivena tokom ovog ciklusa", + "artifact_header_few": "{n} anomalije otkrivene tokom ovog ciklusa", + "artifact_header_other": "{n} anomalija otkrivenih tokom ovog ciklusa", + "artifact_pause_detail": "Snaga je pala skoro na nulu na ~{n}s, a zatim se vratila. Verovatno su vrata otvorena ili je ciklus pauziran.", "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 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.", + "cleanup_intro": "Svi označeni ciklusi jedan preko drugog. Označite odstupajuće, zatim im uklonite oznaku (ostaju u istoriji, ali više ne oblikuju ovaj profil) ili ih obrišite.", "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)", - "compare_profiles_tip": "Prekrijte druge omotače profila na gornjem grafikonu da vidite koji najbolje odgovara ovom ciklusu.", + "compare_profiles_tip": "Prekrijte grafikon drugim profilima da vidite koji najbolje odgovara ovom ciklusu.", "compare_selected_cycles": "Izabrani ciklusi (neprozirni) – prikaži / sakrij", - "consider_new_profile": "Razmislite o kreiranju novog profila.", - "coverage_gap": "nedavni ciklusi nemaju odgovarajući profil ({pct}% od poslednjih 30).", - "coverage_gap_similar_cycles": "{count} sličnih neoznačenih ciklusa pronađeno – napravite profil da biste počeli njihovo podudaranje.", "cycles_deleted": "Obrisano ciklusa: {count}", "enough_data": "Dovoljno podataka za učenje ({current}/{min} ciklusa).", "export_description": "Izaberite tačno koje profile, cikluse, podešavanja i drugo želite da izvezete u JSON, ili analizirajte datoteku i uvezite samo delove koje želite.", @@ -848,30 +825,27 @@ "feedback_prompt": "Potvrdi da je bio tačan, ispravi program ili ignoriši.", "feedback_relabel_hint": "Ponovno označavanje ovog ciklusa takođe ga rešava.", "filter_by_profile": "Filtriraj po profilu…", - "group_modal_help": "Grupni programi istog oblika koji se razlikuju po temperaturi/centrifugiranju (trajanja mogu varirati). Podudaranje ocenjuje grupu kao jednog kandidata, a zatim bira najfitijeg člana. Izaberite najmanje 2; preklop pokazuje koliko su slični.", + "group_modal_help": "Grupišite programe istog oblika koji se razlikuju po temperaturi ili centrifugi. Podudaranje ocenjuje grupu kao celinu, a zatim bira člana koji najbolje odgovara. Izaberite bar 2; preklapanje pokazuje koliko su slični.", "group_not_cohesive": "Ovi profili nisu dovoljno slični da bi se pouzdano grupisali, tako da ih podudaranje tretira pojedinačno sve dok ne uklonite izuzetak ili ne podelite grupu.", "group_preview_hint": "Označite 2 ili više članova za pregled i poređenje krivulja snage.", - "import_intro": "Učitaj izvezenu datoteku ili nalepi JSON payload ispod.", - "legacy_actions_title": "{count} zastarela prilagođena radnja je još uvek u toku", + "legacy_actions_title": "{count} zastarelih prilagođenih radnji je još uvek u toku", + "legacy_actions_title_one": "{count} zastarela prilagođena radnja je još uvek u toku", + "legacy_actions_title_few": "{count} zastarele prilagođene radnje su još uvek u toku", + "legacy_actions_title_other": "{count} zastarelih prilagođenih radnji je još uvek u toku", "live_chart_loading": "Grafikon žive snage se pojavljuje kako stižu očitavanja.", "loading": "Učitavanje…", "loading_curve": "Učitavanje krive…", "loading_settings": "Učitavanje podešavanja…", "log_buffer_hint": "Najnovije prvo · baferuje poslednjih 500 ha_washdata zapisa od ponovnog pokretanja · prevucite donju ivicu da biste promenili veličinu.", - "maintenance_advisory": "Povećanje trajanja/energije može ukazivati da je potrebno održavanje uređaja (npr. uklanjanje kamenca, čišćenje filtera).", "maintenance_due": "Održavanje dospelo: {items}", - "maintenance_intro": "Beležite servisiranje koje obavljate na ovom uređaju i dobijajte podsetnik kada svaki zadatak ponovo dospe.", + "maintenance_intro": "Beležite servisiranje ovog uređaja i dobijte podsetnik kad svaki zadatak ponovo dođe na red.", "maintenance_load_error": "Nije moguće učitati podatke o održavanju: {error}", "maintenance_requires_access": "Održavanje i izvoz/uvoz zahtevaju pun pristup.", - "manual_duration_hint": "Prosečna/očekivana dužina ciklusa profila, koja se koristi za procene preostalog vremena. Uredite da biste je postavili; ostavljajući je nepromenjenom, zadržava trenutnu vrednost.", - "matching_tuning_intro": "Tokom učenja, WashData takođe podešava koliko podudaranje programa meri oblik nasuprot trajanju i energiji.", + "manual_duration_hint": "Očekivana dužina ciklusa profila, koristi se za procenu preostalog vremena. Ostavite nepromenjeno da zadržite trenutnu vrednost.", "merge_intro": "Izabrani ciklusi su kombinovani u jedan (hronološki redosled; praznine popunjene sa 0 W). Izaberite rezultujući profil.", "ml_intro": "WashData dolazi sa pametnim modelima koji rade odmah.", "ml_learned_intro": "Modeli fino podešeni za ovu mašinu.", - "ml_loading": "učitavanje ML…", "ml_settings_intro": "Dve nezavisne sklopke: jedna primenjuje modele dok se ciklus izvodi, druga omogućava WashData da ih fino podesi za vašu mašinu tokom vremena.", - "name_first_program": "Imate dovoljno ciklusa – imenujte svoj prvi program da biste započeli uparivanje.", - "near_duplicate_cluster": "otkrivena je skoro duplirana grupa profila. Grupisanje omogućava pouzdano podudaranje da bira između sličnih (npr. isti program na različitoj temperaturi/centrifugiranju).", "no_cycles_match": "Nema ciklusa koji odgovaraju trenutnom filteru.", "no_cycles_profile": "Nema ciklusa za ovaj profil.", "no_cycles_selected": "Prvo izaberite bar jedan ciklus.", @@ -896,79 +870,63 @@ "no_suggestions": "Nema aktivnih prijedloga.", "notify_services_hint": "Koristite ID-ove servisa {entity} (odvojene zarezima za više). Promenljive šablona: {vars}.", "old_actions_warning": "Konfigurisano sa starim uređivačem radnji (sada uklonjeno). I dalje se aktiviraju na događaje ciklusa, ali više ne mogu da se uređuju ovde. Pretvorite ih u normalnu automatizaciju ili ih uklonite.", - "onboarding_progress": "Posmatrano ciklusa: {n} / 3", - "onboarding_watching": "Koristite uređaj normalno – WashData posmatra. Nakon 3 ciklusa počeće uparivanje programa.", "pending_feedback": "Povratna informacija detekcije na čekanju", "performance_trend": "Trend učinka ({n} ciklusa)", - "pg_analysis_empty": "Učitajte ciklus da vidite analizu poklapanja.", "pg_apply_confirm": "Primeniti najbolju vrednost: {label} = {value}?", - "pg_canvas_empty": "Izaberite ciklus iznad, zatim pritisnite „Učitaj“ da vidite njegovu krivu snage.", - "pg_chart_caption": "Stopa otkrivanja/poklapanja po vrednostima {param}", "pg_ctrl_intro": "Vrednosti polaze od aktivnih podešavanja integracije za ovaj uređaj. Izmene ostaju u testnom okruženju dok ih ne primenite.", "pg_live_unavailable": "Aktivna podešavanja nisu dostupna, prikazane su zadane vrednosti.", "pg_load_live_confirm": "Odbaciti izmene u testnom okruženju ({n}) i ponovo učitati trenutna podešavanja integracije?", - "pg_match_rate": "{pct}% poklapanja na {total} ciklusa", "pg_matches_live": "Odgovara aktivnim podešavanjima", "pg_n_changed": "Promene u odnosu na aktivna podešavanja: {n}", "pg_sugg_none": "Svi predlozi već odgovaraju trenutnim podešavanjima", - "pg_preset_delete_confirm": "Izbrisati konfiguraciju \"{name}\"?", - "pg_preset_limit": "Dostignut limit konfiguracija ({n})", - "pg_preset_overwrite": "Prepisati konfiguraciju \"{name}\"?", "pg_publish_one_confirm": "Sačuvati {label} = {value} u podešavanja ovog uređaja?", "pg_restart_note": "Ponovo pokrenite Home Assistant da biste omogućili alatke za simulaciju.", - "pg_sim_progress": "{done} / {total} ciklusa", - "pg_starting": "Pokretanje…", - "pg_stress_synthetic": "simulacija mirovanja počinje ovde", - "pg_sweep_intro": "Šta ako bi {param} bio drugačiji? Testirajte {steps} vrednosti na vaših poslednjih {cycles} ciklusa da pronađete podešavanje pri kojem se najviše ciklusa ispravno poklapa.", - "pg_sweep_step": "Korak {done} / {total}", - "pg_verdict_bad": "Zahteva pažnju: mnogi ciklusi ostaju neotkriveni.", - "pg_verdict_good": "Dobro podešeno: većina ciklusa se ispravno prepoznaje i poklapa.", - "pg_verdict_ok": "Prihvatljivo: neki ciklusi su propušteni. Pokušajte da smanjite prag pokretanja.", "phase_catalog_intro": "Nazvani segmenti ciklusa (Pranje, Grejanje, Centrifugiranje…). Dodeli ih profilu iz kontrolnog panela.", "phase_ranges_intro": "Opsezi faza (minuti od početka ciklusa) preklopljeni na prosečnoj krivoj. Uredi vrednosti za pregled uživo.", - "playground_intro": "Bezbedno testno okruženje za ponovno puštanje prošlih ciklusa sa različitim podešavanjima, poređenje dve konfiguracije jedne pored druge i pregled kako se ciklus poravnava sa profilom. Ništa ovde ne menja vaše aktivne podatke dok to izričito ne primenite.", - "playground_needs_restart": "Ponovo pokrenite Home Assistant da biste omogućili alatke testnog okruženja.", - "preferences_admin": "Preference, panel i kontrola pristupa", - "preferences_adv": "Preference", + "playground_intro": "Isprobajte podešavanja na stvarnim podacima svojih ciklusa. Ništa ovde ne menja aktivna podešavanja dok ih ne primenite.", "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 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.", + "process_history_hint": "Ponovo pokrenite podudaranje na svim sačuvanim ciklusima, osvežite predloge, ponovo trenirajte ML modele (ako su omogućeni), preračunajte troškove ciklusa prema zabeleženim cenama i ponovo izračunajte zdravlje ciklusa. Pokrenite 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.", - "rbac_hint": "Kada je isključeno, svaki Home Assistant korisnik ima pun pristup (podrazumevano). Administratori uvek imaju pun pristup i mogu upravljati svima.", - "recent_logs": "Nedavni ha_washdata zapisi", + "rbac_hint": "Isključeno: svaki korisnik Home Assistanta ima pun pristup (podrazumevano). Administratori uvek imaju pun pristup i mogu da upravljaju svima.", "recording_in_progress": "Snimanje u toku", "reminders_intro": "Prikaži podsetnik u panelu nakon ovog broja ciklusa od poslednjeg servisa. Ostavite prazno ili 0 da isključite podsetnik.", "restart_gap_footer": "Istaknuto na grafikonu. Podaci o snazi nedostaju za ove intervale; uparivanje je koristilo samo stvarne očitavanja.", "restart_gap_header": "{n} HA praznina ponovnog pokretanja tokom ovog ciklusa", + "restart_gap_header_one": "{n} HA praznina ponovnog pokretanja tokom ovog ciklusa", + "restart_gap_header_few": "{n} HA praznine ponovnog pokretanja tokom ovog ciklusa", + "restart_gap_header_other": "{n} HA praznina ponovnog pokretanja tokom ovog ciklusa", "restart_gap_item": "{dur} praznina", - "review_confirm_help": "Potvrdite da li je ovaj ciklus ispravno otkriven. Vaše recenzije obučavaju model na vašoj mašini - što više ciklusa potvrdite, bolje se podudaraju i dobijaju bolje ocene zdravlja. Dovoljno je brzo dobro/loše.", + "review_confirm_help": "Da li je ovaj ciklus ispravno otkriven? Potvrđivanje ciklusa poboljšava podudaranje za vašu mašinu. Dovoljno je brzo Dobro/Loše.", "review_in_settings": "Pregled u podešavanjima", "review_notes_placeholder": "Beleške (opciono)", "review_notes_tip": "Beleške u slobodnom tekstu za sopstvenu referencu. Ne koristi se za podudaranje ili obuku.", - "review_profile_tip": "Program ovog ciklusa je označen kao. Ako je program koji je automatski detektovan bio pogrešan, ispravite ga ovde - označavanje uči podudaranje za buduće cikluse.", - "review_quality_tip": "Koliko je čist ovaj ciklus. Dobro = primer iz udžbenika ovog programa; Loše = otkriveno, ali bučno ili netipično; Neupotrebljivo = pogrešno otkriveno (spojeno, skraćeno ili lažno). Upravlja ocenom zdravlja i koji ciklusi su dozvoljeni za obuku modela.", - "review_recorded_tip": "Označite ovo kao ručno odabran referentni ciklus za njegov program - istu ulogu kao i ručno snimljeni ciklus. Referentni ciklusi se uvek čuvaju, postavljaju odgovarajući šablon i nikada se ne ispuštaju čišćenjem. (Ovo je \"zlatna\"/snimljena zastavica; obe su ista stvar.)", - "review_tags_tip": "Opcionalne zastavice koje opisuju šta je pošlo po zlu sa ovim ciklusom, tako da obuka i čišćenje mogu da budu odgovorni za to.", + "review_profile_tip": "Program kojim je ovaj ciklus označen. Ako je pogrešan, ispravite ga ovde; oznake uče podudaranje.", + "review_quality_tip": "Koliko je ovaj ciklus čist. Dobro: školski rad ovog programa. Loše: otkriven, ali bučan ili neobičan. Neupotrebljivo: pogrešno otkriven (spojen, skraćen ili lažan). Utiče na ocenu zdravlja i na to koji ciklusi treniraju model.", + "review_recorded_tip": "Označite kao ručno izabran referentni ciklus njegovog programa, kao ručno snimljen ciklus. Referentni ciklusi se uvek čuvaju, čine šablon podudaranja i čišćenje ih nikada ne uklanja.", + "review_tags_tip": "Opcione oznake o tome šta je pošlo naopako, kako bi ih treniranje i čišćenje uzeli u obzir.", "review_to_cycles": "Otvori red za pregled Ciklusa", "samples_decimated": "Prikazano {shown} od {total} uzoraka (proređeno za prikaz; pikovi zadržani). Široki razmak ovde je proređivanje, a ne nedostajući podaci.", - "saving_triggers_reload": "Čuvanje pokreće ponovni pun integracije. HA entiteti mogu se kratko prikazati kao nedostupni.", + "saving_triggers_reload": "Čuvanje ponovo učitava integraciju. Entiteti mogu nakratko biti nedostupni.", "search_placeholder": "Pretraži podešavanja…", "see_recorder": "Pogledaj widget za snimanje ispod", "setting_changed": "Promenjeno sa {old} na {new} dana {date}", "settings_basic_note": "Prikazuju se osnovna podešavanja. Prebacite na Napredno za potpunu listu.", - "shape_drift_advisory": "⚠ Pomeraj oblika", - "shape_drift_detail": "Obrazac snage ovog profila se promenio s vremenom – moguće habanje uređaja ili potrebno održavanje (npr. uklanjanje kamenca, čišćenje filtera).", "show_all_settings": "Prikaži sva podešavanja", - "showing_suggestions": "Prikazuje se podešavanje {count} sa prijedlozima.", - "split_intro": "Klikni na grafikon za dodavanje ili uklanjanje tačke deljenja, ili automatski otkrij po prazninama. Svaki rezultujući segment može dobiti sopstveni profil.", - "storage_diagnostics": "Statistike skladištenja, održavanje, izvoz/uvoz", + "showing_suggestions": "Prikazuje se {count} podešavanja sa predlozima.", + "showing_suggestions_one": "Prikazuje se {count} podešavanje sa predlozima.", + "showing_suggestions_few": "Prikazuju se {count} podešavanja sa predlozima.", + "showing_suggestions_other": "Prikazuje se {count} podešavanja sa predlozima.", + "split_intro": "Kliknite grafikon da dodate ili uklonite tačku razdvajanja, ili je automatski pronađite po prazninama mirovanja. Svaki deo može da dobije sopstveni profil.", "sug_staged": "Postavljeno {key} = {val}. Sačuvajte za primenu.", "sug_muted": "Ovo podešavanje neće biti predlagano ponovo", "sug_mute_failed": "Predlog nije mogao biti utišan", "sug_unmuted_all": "Utišani predlozi su resetovani", - "n_suggestions_muted": "{count} utišano; auto-podešivač neće predlagati ova podešavanja.", + "n_suggestions_muted": "{count} predloga je utišano; auto-podešivač ih neće predlagati.", + "n_suggestions_muted_one": "{count} predlog je utišan; auto-podešivač ga neće predlagati.", + "n_suggestions_muted_few": "{count} predloga su utišana; auto-podešivač ih neće predlagati.", + "n_suggestions_muted_other": "{count} predloga je utišano; auto-podešivač ih neće predlagati.", "toast_auto_detect_enabled": "Automatsko otkrivanje omogućeno", "toast_auto_label_complete": "Automatsko označavanje završeno", "toast_auto_label_failed": "Automatsko označavanje nije uspelo: {error}", @@ -984,7 +942,6 @@ "toast_no_automation": "Ovde nije moguće kreirati automatizaciju", "toast_profile_name_required": "Naziv profila je obavezan", "toast_program_set": "Program podešen: {program}", - "toast_rebuild_failed": "Obnova nije uspela: {error}", "toast_resume_failed": "Nastavak nije uspeo: {error}", "toast_revert_failed": "Povrat nije uspeo: {error}", "toast_reverted": "{key} vraćeno", @@ -995,95 +952,81 @@ "trend_energy_up": "Energetski trend raste ({pct}%/ciklus) – nedavni prosek {avg}", "trim_destructive_confirm": "Ovo zadržava samo {pct}% ciklusa i ne može se poništiti. Nastaviti?", "trim_intro": "Prevuci crvene ručice ili unesi vrednosti. Sve izvan prozora se uklanja.", - "tuning_suggestions_available": "{count} prijedlog podešavanja dostupan je iz posmatranih ciklusa. Pojavljuju se pored relevantnih polja.", + "tuning_suggestions_available": "{count} predloga podešavanja dostupno je na osnovu posmatranih ciklusa. Pojavljuju se pored relevantnih polja.", + "tuning_suggestions_available_one": "{count} predlog podešavanja dostupan je na osnovu posmatranih ciklusa. Pojavljuje se pored relevantnog polja.", + "tuning_suggestions_available_few": "{count} predloga podešavanja dostupna su na osnovu posmatranih ciklusa. Pojavljuju se pored relevantnih polja.", + "tuning_suggestions_available_other": "{count} predloga podešavanja dostupno je na osnovu posmatranih ciklusa. Pojavljuju se pored relevantnih polja.", "unsure_detected_prefix": "WashData nije siguran da je otkrio", - "updated": "Ažurirano", "warmup_badge": "Još uči ({done}/{needed} ciklusa)", - "warmup_detail": "Ovom profilu treba {needed} označenih ciklusa pre nego što počne automatsko podudaranje. Svaki potvrđeni ciklus mu pomaže da uči.", + "warmup_detail": "Podudaranje već radi. WashData traži da potvrdite prvih {needed} ciklusa ovog profila pre nego što počne sam da označava.", "wipe_history_warning": "Trajno obrišite sve cikluse i profile. Ne može se opozvati.", "compare_overlay_tip": "Preklopite naučene omotače profila da vidite na koji program podseća svaki ciklus.", - "duration_trend_up_tip": "Trajanje raste ({pct}%/ciklus)", - "duration_trend_down_tip": "Trajanje pada ({pct}%/ciklus)", - "energy_trend_up_tip": "Energija raste ({pct}%/ciklus)", - "energy_trend_down_tip": "Energija pada ({pct}%/ciklus)", - "performance_trending": "Trend performansi", - "pg_select_cycle_hint": "Izaberite ciklus i kliknite ↺ za učitavanje", - "pg_sweep_progress": "Korak {done} / {total}", - "pg_analysis_hint": "Izaberite ciklus i kliknite ↺ za učitavanje analize.", "diagnostics_load_failed": "Nije moguće učitati dijagnostiku: {error}", - "manual_duration_ref_hint": "Koristi se samo kada nije izabran referentni ciklus. Referentni ciklus postavlja trajanje prema sopstvenoj dužini.", + "manual_duration_ref_hint": "Koristi se samo bez referentnog ciklusa; referentni ciklus postavlja trajanje prema svojoj dužini.", + "duration_learned_hint": "Izračunato iz ciklusa ovog programa (bez odstupanja) i ažurira se posle svakog novog. Da biste ga promenili, isecite ili uklonite cikluse koji odstupaju.", "toast_convert_rolled_back": "Konverzija nije uspela i vraćena je (nije ostala nijedna automatizacija): {error}", "toast_convert_orphan": "Automatizacija je kreirana, ali brisanje starih akcija nije uspelo. Nemojte pokušavati ponovo: ručno uklonite stare akcije da biste izbegli dupliranu automatizaciju.", - "advisory_duration_trend_up": "Ciklusi programa '{name}' postaju sve duži (otprilike +{pct}% po ciklusu). Ako se ponašanje uređaja promenilo, ponovo snimite ili obnovite ovaj profil.", - "advisory_energy_trend_up": "Program '{name}' troši sve više energije (otprilike +{pct}% po ciklusu) - vredi proveriti uređaj ako je to neočekivano.", - "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.", + "advisory_duration_trend_up": "Ciklusi programa '{name}' postaju sve duži (oko +{pct}% po ciklusu). Ako se uređaj promenio, ponovo snimite ili obnovite ovaj profil.", + "advisory_energy_trend_up": "Program '{name}' troši više energije sa svakim ciklusom (oko +{pct}% po ciklusu). Proverite uređaj ako je to neočekivano.", + "advisory_poor_health": "Profil '{name}' loše odgovara: njegovi noviji ciklusi mnogo variraju ili se slabo podudaraju. Pregledajte njegove cikluse ili ga ponovo snimite kako bi podudaranje i procene ostali tačni.", + "advisory_shape_drift": "Profil '{name}' je odstupio od svog izvornog oblika snage, možda zbog kamenca ili habanja. Razmislite o ponovnom snimanju sa novijim ciklusima.", + "advisory_shape_drift_corr": "Profil '{name}' je odstupio od svog izvornog oblika snage (korelacija {corr}), možda zbog kamenca ili habanja. Razmislite o ponovnom snimanju sa novijim ciklusima.", + "advisory_unmatchable": "Profil '{name}' nikada ne može da se podudari: iza njega ne stoji nijedan ciklus sa podacima o snazi. Označite ili snimite ciklus ovog programa da to popravite.", + "advisory_unmatchable_no_duration": "Profil '{name}' nikada ne može da se podudari: njegov ciklus nema upotrebljivo trajanje. Ponovo snimite ovaj program ili postavite njegovo očekivano trajanje.", "pg_no_alerts": "U ovom pokretanju nisu otkriveni problemi.", "pg_override_active": "Koriste se vaša izmenjena podešavanja umesto trenutnih", "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.", "pg_sweep_intro2": "Pronađite podešavanje koje najbolje ispunjava cilj na vašim nedavnim ciklusima. Ništa se ne menja dok ga ne primenite.", - "pg_running_history": "Ponovno izvođenje vaših ciklusa…", - "pg_running_sweep": "Isprobavanje vrednosti kroz sve vaše cikluse…", "pg_row_load_hint": "Učitaj ovaj ciklus u grafikon iznad", - "pg_history_intro2": "Ponovo izvedite svoje nedavne cikluse kroz stvarni detektor i modul za podudaranje sa postavkama iznad. Kliknite bilo koji red da učitate taj ciklus u grafikon; izmenite postavku da vidite poređenje pre i posle.", + "pg_history_intro2": "Ponovo provedite nedavne cikluse kroz stvarni detektor i podudaranje sa podešavanjima iznad. Kliknite red da ga učitate u grafikon; promenite podešavanje da uporedite pre i posle.", "pg_simulating": "Simuliranje ciklusa…", - "pg_canvas_empty2": "Izaberite ciklus iznad i pritisnite Pokreni da ga simulirate. Zatim pređite mišem za prikaz vrednosti, pomerajte točkić za zumiranje i prevucite za pomeranje.", "pg_apply_settings_confirm": "Sačuvati ovih {n} podešavanja na ovaj uređaj? {list}", - "pg_stress_terminated_detail": "Ustalilo se na ~{idle}W mirovanja -- ciklus se završio {h} h {m} min kasnije preko {reason}.", - "pg_stress_above_threshold_detail": "Potrošnja u mirovanju ~{idle}W jednaka je ili viša od stvarnog praga zaustavljanja ({stop}W) -- ciklus nikada nije zabeležen kao miran. Za ispravku povećajte stop_threshold_w.", - "pg_stress_hit_cap_detail": "Ciklus je radio {h} h {m} min bez zaustavljanja -- prisilno zaustavljen sigurnosnim limitom. Potrošnja u mirovanju {idle}W u odnosu na prag zaustavljanja {stop}W.", "no_logs_match": "Nijedan zapis dnevnika ne odgovara ovim filterima.", "pg_analysis_hint2": "Izaberite ciklus i pritisnite Pokreni da učitate analizu poklapanja.", - "pg_analysis_empty2": "Pritisnite Pokreni da vidite analizu poklapanja.", - "imported_readonly": "Uvezeno iz prodavnice zajednice. Prikazano za referencu i podudaranje. Ne računa se u vašu statistiku i ne može se uređivati.", + "imported_readonly": "Uvezeno iz prodavnice zajednice. Služi kao referenca i za podudaranje, ne broji se u statistiku i ne može da se uređuje.", "merge_no_imports": "Uvezeni snimci ne mogu se spojiti sa stvarnim ciklusom. Poništite njihov izbor da biste ih spojili.", - "store_enable_hint": "Omogućite online funkcije u Podešavanjima da biste mogli da pregledate i uvezete referentne cikluse zajednice.", - "advisory_duration_outlier": "Ciklusi označeni kao '{name}' ({n}) previše odstupaju od njegove uobičajene dužine da bi se ikada mogli podudariti s njim (najveće odstupanje: {ratio}x). Verovatno su pogrešno označeni ili je jedno snimanje obuhvatilo dva pokretanja. Ponovo ih označite ili ih podelite kako bi prestali da iskrivljuju procenu vremena za ovaj program.", + "store_enable_hint": "Uključite online funkcije u Preference > Online i zajednica da biste mogli da pregledate i uvezete referentne cikluse zajednice.", + "advisory_duration_outlier": "Ciklusi označeni kao ‘{name}’ ({n}) previše odstupaju od njegove uobičajene dužine da bi se ikada podudarili (najgori: {ratio}x). Verovatno su pogrešno označeni ili sadrže dva rada. Ponovo ih označite ili podelite kako ne bi iskrivljavali procenu vremena.", "standby_above_stop_title": "Još uvek povlači {idle} W kada je ciklus završio", "standby_above_stop_sub": "{n} od poslednjih {total} ciklusa. Kašnjenje isklopa počinje tek kada snaga padne ispod Praga zaustavljanja ({stop} W), pa se ciklusi završavaju sa zakašnjenjem ili ih treba zaustaviti ručno.", - "online_intro": "Delite i preuzimajte referentne cikluse sa drugim korisnicima WashData. Sve online funkcije su dobrovoljne i podrazumevano isključene.", "store_share_intro": "Otpremite ovaj referentni ciklus kako bi ga koristili drugi sa istim uređajem. Pregleda se pre nego što postane javno dostupan.", - "online_intro_global": "Pregledajte i delite referentne snimke sa drugim korisnicima WashData i potvrđujte unose uređaja. Jedna veza važi za celu vašu WashData integraciju; marka i model uređaja se postavljaju za svaki uređaj u odeljku Osnovno. Sve online funkcije su dobrovoljne i podrazumevano isključene.", + "online_intro_global": "Pregledajte i delite referentne snimke sa drugim korisnicima WashData. Jedna veza važi za sve vaše WashData uređaje; marka i model se podešavaju za svaki uređaj pod Osnovno. Onlajn funkcije su isključene dok ih ne uključite.", "online_unavailable": "Online funkcije nisu dostupne na ovom serveru.", "store_picker_offline": "Omogućite online funkcije u zupčaniku podešavanja da biste birali iz kataloga zajednice.", "pick_brand_first": "Prvo izaberite marku uređaja.", - "model_not_found": "Još nije u katalogu.", - "connect_to_confirm": "Povežite se u zupčaniku podešavanja da biste potvrdili ili ocenili.", + "custom_task_limit": "Dostigli ste najveći broj zadataka ({max}).", + "custom_tasks_intro": "Dodajte sve ostalo na šta želite da budete podsećeni. Zadatak dospeva kada se dostigne bilo koji od intervala; ostavite jedan prazan ili 0 da bi se koristio samo drugi.", + "maintenance_due_sensor_hint": "Binarni senzor Dospelo održavanje uključen je dok je neki zadatak dospeo; koristite ga u sopstvenim automatizacijama obaveštenja.", + "maintenance_supporter_intro": "Maintenance Supporter je zasebna integracija koja može mnogo više: bilo koji uređaj ili zadatak, rasporedi prema vremenu, korišćenju ili senzoru, obaveštenja i istorija. Može da koristi brojač ciklusa WashData.", "adopt_settings_hint": "Primeni podešavanja detekcije i podudaranja podeljenog uređaja na ovaj uređaj", - "include_settings_hint": "Uključi pragove detekcije, težine podudaranja i ostale parametre u podeljeni paket", + "include_settings_hint": "Podelite pragove detekcije i podudaranja ovog uređaja (ne obaveštenja, entitete ni cenu energije). Drugi biraju da li će ih primeniti.", "onboard_download": "Preuzmi gotova podešavanja iz prodavnice zajednice", - "share_consent": "Delite stvarne podatke sa svog uređaja. Nemojte deliti ako su vaši obrasci korišćenja privatni.", "share_device_none": "Još nema postavljenih uređaja. Prvo dodajte uređaj.", "share_guideline_naming": "Koristite jasna imena programa (npr. 'Cotton 40', 'Eco 60') kako bi ih drugi mogli prepoznati", - "share_guideline_quality": "Delite samo cikluse koji su se normalno završili -- bez prekida usred ciklusa, otvaranja vrata ili skokova napajanja.", + "share_guideline_quality": "Delite samo cikluse koji su normalno završeni: bez prekida, otvaranja vrata i nestanaka struje.", "share_guideline_review": "Pregledajte profile pre deljenja -- uklonite one koji izgledaju pogrešno", "share_guidelines_title": "Pre deljenja", - "store_download_device_intro": "Preuzmite podešavanja uređaja iz zajednice i primenite ih na novi ili postojeći uređaj", + "store_download_device_intro": "Dodajte svom uređaju sve podeljene programe i njihove referentne cikluse. Vaši sopstveni ciklusi i statistika ostaju netaknuti.", "store_share_device_intro": "Otpremite {brand} {model} sa referentnim ciklusima koje izaberete. Drugi sa istim uređajem mogu da preuzmu vaše programe. Unosi se pregledaju pre nego što postanu javni.", "share_profile_no_cycles": "Nema referentnih ciklusa - označite ciklus sa ⭐ u kartici Ciklusi da uključite ovaj profil", - "advisory_phase_inconsistent": "Izgleda da '{name}' meša različite programe ili temperature - njegovi ciklusi se greju veoma različito dugo. Podela na zasebne profile (npr. po temperaturi) poboljšaće podudaranje i procene vremena.", - "advisory_phase_inconsistent_title": "⚠ Možda pomešani programi", - "export_select_intro": "Označite tačno šta želite da uključite. Izbor profila bez njihovih ciklusa i dalje izvozi prepoznatljiv program (njegov naučeni oblik ide zajedno sa njim).", - "import_analyze_hint": "Učitajte izvezenu datoteku (ili nalepite njen JSON). WashData je analizira i prikazuje tačno šta može da se uveze pre bilo kakve promene.", + "export_select_intro": "Označite šta da se uključi. Profili bez svojih ciklusa i dalje izvoze prepoznatljiv program.", + "import_analyze_hint": "Učitajte izvezenu datoteku (ili nalepite njen JSON). WashData pokazuje šta može da se uveze pre nego što se išta promeni.", "analyzing": "Analiziranje…", - "device_type_mismatch_warn": "Ovaj izvoz je sa uređaja drugog tipa ({src} umesto {local}). Programi i ciklusi i dalje mogu da se uvezu kao referentni podaci, ali podešavanja specifična za uređaj i uvoz stvarne istorije su onemogućeni.", + "device_type_mismatch_warn": "Ovaj izvoz je sa druge vrste uređaja ({src} vs {local}). Programe i cikluse i dalje možete uvesti kao referentne podatke, ali ne i podešavanja uređaja ni stvarnu istoriju.", "merge_hint": "Uvezene stavke se dodaju; ništa lokalno se ne gubi. Sukobi imena rešavaju se ispod.", "replace_warn": "Svaka označena kategorija se briše i zamenjuje podacima iz datoteke. Neoznačene kategorije ostaju netaknute.", "dest_reference_hint": "Uvezeni ciklusi samo poboljšavaju prepoznavanje programa i nikada ne utiču na statistiku potrošnje/energije.", - "dest_real_history_hint": "Uvezeni ciklusi se računaju kao sopstvena istorija ovog uređaja i pune statistiku energije/potrošnje. Koristite za premeštanje jednog uređaja na novu instalaciju.", - "import_history_description": "Imali ste pametni priključak i pre WashData? Učitajte izvoz istorije njegovog senzora snage ili je pročitajte direktno iz Home Assistant, a uobičajena detekcija proći će kroz te podatke, pa će se prošli ciklusi pojaviti na vašoj listi Ciklusi, spremni za imenovanje.", - "hist_input_hint": "Učitajte CSV preuzet sa panela Istorija (entitet, stanje, poslednja promena) ili pustite WashData da direktno pročita istoriju senzora. Detekcija se zatim izvodi isto kao i uživo, a vi birate koje od pronađenih ciklusa ćete zadržati.", - "hist_recorder_hint": "Čita podatke od izabranog datuma do sada. Home Assistant podrazumevano čuva detaljnu istoriju 10 dana, a posle toga samo satne proseke, koji su previše grubi za detekciju ciklusa - raniji datum birajte samo ako je vaš recorder podešen na duže čuvanje.", - "hist_scanning": "Vašu istoriju ponovo provodimo kroz detektor. Ovo se izvodi u pozadini - možete zatvoriti ovaj dijalog i vratiti se kasnije.", + "dest_real_history_hint": "Uvezeni ciklusi se računaju kao sopstvena istorija i statistika ovog uređaja. Koristite pri premeštanju uređaja na novu instalaciju.", + "import_history_description": "Imali ste pametni utikač pre WashData? Otpremite njegovu istoriju snage ili je pročitajte iz Home Assistanta. Detekcija prolazi kroz te podatke, a stari ciklusi se pojavljuju na listi Ciklusi spremni za imenovanje.", + "hist_input_hint": "Otpremite CSV sa panela Istorija (entitet, stanje, poslednja promena) ili pustite WashData da direktno pročita istoriju senzora. Detekcija uživo prolazi kroz te podatke, a vi birate koje cikluse da zadržite.", + "hist_recorder_hint": "Čita od izabranog datuma do sada. Home Assistant podrazumevano čuva detaljnu istoriju 10 dana; stariji satni proseci su pregrubi za detekciju ciklusa.", + "hist_scanning": "Vaša istorija prolazi kroz detektor u pozadini. Možete zatvoriti ovaj dijalog i vratiti se kasnije.", "hist_imported_count": "Uvezeni ciklusi: {n}.", "hist_duplicates": "Već ranije uvezeno i preskočeno: {n}.", "hist_capped": "Dostignuto je ograničenje uvezenih ciklusa po uređaju; ostatak nije sačuvan.", - "hist_next_step": "Nalaze se na vašoj listi Ciklusi, obeleženi kao uvezena istorija. Otvorite jedan i tasterom Označi mu dodelite naziv programa kojem pripada.", + "hist_next_step": "Nalaze se na listi Ciklusi, obeleženi kao uvezena istorija. Otvorite jedan i tasterom Označi imenujte njegov program.", "hist_rows_read": "Pročitana očitavanja: {n}", "hist_breaks": "Praznine u kojima senzor nije bio dostupan: {n}", "hist_other_entity": "Zanemarena očitavanja drugih entiteta: {n}", @@ -1095,25 +1038,74 @@ "hist_scan_capped": "Prikazani su samo prvi kandidati (pronađeno je {n}).", "hist_recorder_empty": "Home Assistant nema detaljnu istoriju za ovaj senzor u tom periodu.", "hist_scan_failed": "Pretraživanje nije uspelo.", - "hist_scan_expired": "To pretraživanje više nije dostupno. Pokrenite ga ponovo.", + "hist_scan_expired": "To skeniranje više nije dostupno. Skenirajte 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.", - "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.", + "imported_history_readonly": "Pronađeno u uvezenoj istoriji snage. Pomaže podudaranju, ali se ne broji u statistiku i ne može da se skrati ni podeli. Označite ga da imenujete program.", + "odometer_intro": "Ovaj zbir samo raste. Brisanje ciklusa ga ne smanjuje, pa podsetnici za servis ostaju tačni.", + "odometer_correct_hint": "Postavite ovo ako je WashData izbrojao ciklus koji se nije dogodio ili je uređaj radio cikluse pre instalacije WashData.", "no_reminders_set": "Još nema postavljenih podsetnika za servis.", "toast_program_armed": "Program podešen za sledeći ciklus: {program}", "toast_range_required": "Postavite ispravan raspon Od/Do", - "no_profiles_option": "(nema profila, dodajte novi)" + "no_profiles_option": "(nema profila, dodajte novi)", + "compare_cycles_title": "Uporedi cikluse ({count})", + "edit_builtin_phase": "Ovo je ugrađena faza. Čuvanjem se pravi sopstvena kopija; obrišite kopiju da vratite original.", + "fetch_error": "Učitavanje podataka nije uspelo.", + "connection_lost": "Veza je prekinuta - prikazano je stanje u {time}.", + "pg_notify_partial": "Oznake prikazuju samo obaveštenja o početku, skorom kraju, završetku, prekretnicama i tihim satima. Ažuriranja uživo, podsetnici za istovar i upozorenja o prekoračenju se ne simuliraju.", + "connect_to_confirm": "Povežite se u zupčaniku podešavanja da biste potvrdili ili ocenili.", + "coverage_gap_cluster": "Nedavni neoznačeni ciklusi ({count}) od oko {min} min izgledaju kao jedan program koji još niste napravili.", + "pg_label_yes": "Na kraju ciklusa bio bi označen kao \"{name}\"", + "pg_label_no": "Bio bi poslat na pregled: {reason}", + "live_match_debug_tip": "Pouzdanost: koliko tačno trenutna kriva snage odgovara najboljem profilu kandidatu (0-100%). Dvosmisleno: rezultati dva najbolja kandidata razlikuju se za manje od 5%, pa je oznaka nesigurna dok se ciklus ne završi.", + "automation_alias_started": "{device}: ciklus je pokrenut", + "automation_alias_finished": "{device}: ciklus je završen", + "automation_desc_started": "Pokreće se kada počne ciklus uređaja {device}. Dodajte svoje radnje (obaveštenja, svetla, ...).", + "automation_desc_finished": "Pokreće se kada se završi ciklus uređaja {device}. Dodajte svoje radnje (obaveštenja, svetla, ...).", + "feedback_detected": "WashData je prepoznao: {name}", + "import_undo_hint": "Čuva se tačka vraćanja od pre poslednjeg uvoza ({when}): {profiles} profil(a) i {cycles} ciklus(a).", + "replace_undo_hint": "Prvo se čuva tačka vraćanja, pa ovaj uvoz možete opozvati iz odeljka Izvezi / Uvezi.", + "pg_in_sample_note": "Ovi ciklusi su pomogli da se izgrade profili sa kojima se porede, pa su rezultati optimistični: na novim ciklusima očekujte nešto manje tačnih podudaranja.", + "hist_tail_trimmed": "Odsečeno je {min} min čekanja posle završetka ciklusa, kao što to radi integracija tokom rada", + "hist_truncated": "zaustavljeno na ograničenju očitavanja ({n}), pa ostatak istorije nije pregledan", + "hist_warn_kw": "Najveće očitavanje je samo {w}. Izgleda da ovaj senzor javlja kilovate; WashData treba vate, pa ciklusi ne mogu da se otkriju.", + "hist_warn_naive_time": "{n} vremenskih oznaka nema vremensku zonu i pročitano je kao UTC. Ako datoteka koristi lokalno vreme, ciklusi su pomereni za vaš pomak od UTC-a, a očitavanja oko promene vremena mogu da se izgube.", + "hist_warn_naive_time_one": "{n} vremenska oznaka nema vremensku zonu i pročitana je kao UTC. Ako datoteka koristi lokalno vreme, ciklusi su pomereni za vaš pomak od UTC-a, a očitavanja oko promene vremena mogu da se izgube.", + "hist_warn_naive_time_few": "{n} vremenske oznake nemaju vremensku zonu i pročitane su kao UTC. Ako datoteka koristi lokalno vreme, ciklusi su pomereni za vaš pomak od UTC-a, a očitavanja oko promene vremena mogu da se izgube.", + "hist_warn_naive_time_other": "{n} vremenskih oznaka nema vremensku zonu i pročitano je kao UTC. Ako datoteka koristi lokalno vreme, ciklusi su pomereni za vaš pomak od UTC-a, a očitavanja oko promene vremena mogu da se izgube.", + "pg_compare_profile_tip": "Bira samo profil iscrtan preko grafikona. Simulacija uvek sama traži odgovarajući program za ciklus, kao i integracija tokom rada.", + "pg_outcome_stale": "Podešavanja su se promenila nakon pokretanja ove simulacije. I dalje prikazuje stari rezultat.", + "pg_sweep_guarded": "Prerano završava više ciklusa, deli više njih ili propušta cikluse koje trenutno podešavanje hvata, pa se nikada ne preporučuje.", + "pg_sweep_keep_current": "Zadrži trenutnu vrednost ({value}, {metric}): nijedna vrednost nije bila bolja za bar jedan ciklus.", + "pg_sweep_n_cycles": "na {count} ciklusa", + "pg_sweep_n_cycles_one": "na {count} ciklusu", + "pg_sweep_n_cycles_few": "na {count} ciklusa", + "pg_sweep_n_cycles_other": "na {count} ciklusa", + "pg_task_busy": "Za ovaj uređaj već je u toku „Test na istoriji” ili „Optimizuj”. Sačekajte da se završi ili ga otkažite.", + "cycle_context_loading": "Učitavanje istorije recordera…", + "cycle_context_note": "Sivo: istorija senzora snage iz recordera oko ovog ciklusa. Nije deo ciklusa i ne utiče na trajanje, energiju ni podudaranje.", + "cycle_context_none": "Recorder nema očitavanja oko ovog ciklusa (Home Assistant podrazumevano čuva istoriju 10 dana).", + "cycle_context_tip": "Samo za prikaz: pokazuje istoriju senzora snage iz recordera oko ovog ciklusa, ako je još sačuvana. Da biste neuspele pokušaje pokretanja dodali budućim ciklusima, koristite Uvodne sekunde krive (Podešavanja > Detekcija > Start ciklusa), što menja sačuvana trajanja.", + "invalid_threshold_pair": "Prag zaustavljanja ({stop} W) mora biti ispod Praga pokretanja ({start} W). Ništa nije sačuvano; promenite obe vrednosti zajedno." }, "phase_desc": { "anti_crease": "Povremena kratka okretanja nakon završetka radi smanjenja gužvanja.", "anti_wrinkle": "Periodično okretanje posle ciklusa radi smanjenja gužvanja.", + "baking": "Grejač na visokoj temperaturi stvara koru i sredinu hleba.", + "cooking": "Glavna faza pripreme sa uključenim grejačem i ventilatorom.", "cool_down": "Okretanje bez grejanja pri kraju ciklusa.", + "cool_down_af": "Hlađenje samo ventilatorom posle grejanja.", "drain_and_switch": "Period prelaska sa pranja na režim sušenja.", "dry": "Faza sušenja korišćenjem grejača i/ili preostale toplote.", "drying": "Glavni period sušenja uz toplotu.", "heat_up": "Početno zagrevanje pre nego što počne potpuno sušenje.", + "keep_warm": "Blago grejanje da hrana ostane topla.", + "keep_warm_bm": "Blago grejanje da hleb ostane topao posle pečenja.", + "kneading": "Motor meša i mesi testo. Velika potrošnja snage.", + "pause_af": "Kratka pauza za protresanje ili proveru.", + "pre_heat": "Početno zagrevanje komore pre pune pripreme.", "pre_rinse": "Prskanje vodom pre pranja sa deterđentom.", + "proving": "Narastanje na niskoj toploti: kvasac fermentiše i testo raste.", + "resting": "Kratka pauza male snage između mešenja da se gluten opusti.", "pre_wash": "Početno namakanje ili prethodni tretman pre glavnog pranja.", "rinse": "Faza ispiranja čistom vodom. Ova faza se može ponoviti više puta.", "sanitize": "Faza čišćenja na visokoj temperaturi za programe dezinfekcije.", @@ -1126,6 +1118,7 @@ "wash_dw": "Pranje sa deterđentom uz grejanje." }, "placeholder": { + "custom_task": "npr. Očisti zaptivku vrata", "add": "dodaj…", "group_name": "npr. Pamuk 2:47", "json_buttons": "[{\"action\":\"ID\",\"title\":\"Label\"}]", @@ -1153,10 +1146,10 @@ "section": { "dishwasher": { "label": "Mašina za pranje sudova", - "intro": "Obrada kraja ciklusa za mašine za pranje sudova, koje se obično završavaju dugom, gotovo tihom fazom sušenja pre kratkog završnog ispumpavanja vode." + "intro": "Obrada kraja za mašine za pranje sudova, koje često dugo i tiho suše pre kratkog završnog ispumpavanja." }, "anti_wrinkle": { - "intro": "Režim zaštite od gužvanja tkanine detektuje kratke pulseve tumblovanja pri niskoj snazi nakon glavne faze i drži ih pripisanim završenom ciklusu.", + "intro": "Zadržava kratke impulse prevrtanja niske snage nakon glavne faze mašine za sušenje uz završeni ciklus.", "label": "Zaštita od gužvanja" }, "basic": { @@ -1164,7 +1157,7 @@ "label": "Osnovno" }, "delay": { - "intro": "Detekcija odloženog starta prepoznaje kada je uređaj uključen ali još nije počeo ciklus.", + "intro": "Otkriva uređaj koji je uključen i čeka odloženi start.", "label": "Odloženi start" }, "detection": { @@ -1188,161 +1181,153 @@ "label": "Tajming i nadzornik" }, "triggers": { - "intro": "Opcionalni spoljni signali: okidač kraja, senzor vrata, prekidač pauze i podsetnik za istovar.", + "intro": "Opcioni dodaci: okidač završetka, senzor vrata, prekidač za pauzu i podsetnik za istovar.", "label": "Okidači i vrata" - }, - "phase_eta": { - "label": "Preostalo vreme", - "intro": "Procena preostalog vremena zasnovana na fazama, za mašine čija dužina ciklusa zavisi od temperature ili centrifuge." } }, "setting": { "dishwasher_end_spike_quiet_release": { "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." + "doc": "Posle očekivanog trajanja: koliko dugo mašina za pranje sudova mora da ostane ispod Praga zaustavljanja pre nego što WashData prestane da čeka završno ispumpavanje. Povećajte ako se kasno ispumpavanje posle dugog sušenja propusti. Čekanje nikada ne prelazi 30 minuta." }, "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.", + "doc": "Deo prosečnog trajanja programa koji ciklus mora da dostigne pre nego što se prevrtanje niske snage računa kao rep zaštite od gužvanja. Smanjite ga (npr. 0,75) kod mašina za sušenje koje se prilagođavaju količini veša. Kod mašina za veš ostavite podrazumevano (0,98), inače pranje može da se završi na pola.", "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.", + "doc": "Zadržite kratke impulse prevrtanja niske snage nakon glavne faze grejanja mašine za sušenje uz završeni ciklus, umesto da se čitaju kao novi ciklusi.", "label": "Omogući detekciju zaštite od gužvanja" }, "anti_wrinkle_exit_power": { - "doc": "Snaga mora pasti ispod ove između impulsa da bi režim protiv bora ostao aktivan.", + "doc": "U režimu zaštite od gužvanja snaga ispod ove vrednosti smatra se mirovanjem između impulsa okretanja, a mirovanje duže od Maks. razmaka između impulsa završava režim. Ako je Prag zaustavljanja viši, koristi se on, pa ovo podešavanje nešto menja samo iznad Praga zaustavljanja.", "label": "Prag izlazne snage" }, "anti_wrinkle_idle_timeout": { "label": "Maks. razmak između impulsa", - "doc": "Koliko dugo mašina sme da ostane tiha između dva impulsa prevrtanja bubnja pre nego što se režim zaštite od gužvanja završi. Postavite je iznad najdužeg razmaka koji vaša mašina za sušenje ostavlja između impulsa, inače se svaki naredni impuls čita kao lažni start." + "doc": "Koliko dugo mašina za sušenje sme da miruje između impulsa prevrtanja pre nego što se režim zaštite od gužvanja završi. Postavite iznad najdužeg razmaka vaše mašine, inače se kasniji impulsi čitaju kao lažni startovi." }, "anti_wrinkle_max_duration": { - "doc": "Pulsevi duži od ovoga se tretiraju kao pravi ciklus, a ne kao protiv bora.", + "doc": "Impuls duži od ovoga računa se kao pravi ciklus, a ne kao prevrtanje protiv gužvanja.", "label": "Maks. trajanje" }, "anti_wrinkle_max_power": { - "doc": "Puls iznad ove snage se tretira kao pravi novi ciklus, a ne kao protiv bora. Postavite tik iznad snage impulsa prevrtanja.", + "doc": "Impuls iznad ove snage računa se kao pravi novi ciklus, a ne kao prevrtanje. Postavite malo iznad snage impulsa prevrtanja.", "label": "Maks. snaga zaštite od gužvanja" }, "auto_label_confidence": { - "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.", + "doc": "Rezultat podudaranja na kraju ciklusa od kog se program označava bez pitanja. Povećajte za veću sigurnost; smanjite da se automatski označi više ciklusa. Držite iznad Pouzdanosti učenja.", "label": "Pouzdanost automatskog označavanja" }, "auto_maintenance": { - "doc": "Pokrenite noćno održavanje domaćinstva: ponovo napravite koverte profila, ponovo izračunajte zdravlje ciklusa, smanjite tragove otklanjanja grešaka i zadržite najnovije cikluse.", + "doc": "Noćno čišćenje: označava starije cikluse koji sada jasno odgovaraju programu, ponovo gradi krive programa, osvežava oznake anomalija i zdravlje ciklusa i uklanja tragove za otklanjanje grešaka ako je Sačuvaj tragove za otklanjanje grešaka isključeno. Ciklusi i njihovi zapisi snage se uvek čuvaju.", "label": "Automatsko održavanje (noćno čišćenje)" }, "completion_min_seconds": { - "doc": "Ciklusi kraći od ovog se odbacuju kao ciklusi duhova (probni radovi, otvaranje vrata da se doda čarapa).", + "doc": "Ciklusi kraći od ovoga (probni radovi, otvaranje vrata radi dodavanja čarape) čuvaju se kao prekinuti i ne dobijaju obaveštenje o završetku.", "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.", + "doc": "Dodaje očitavanja iz neuspelih pokušaja pokretanja na početak ciklusa, za mašine koje se isprobavaju pre pokretanja (zaključavanje vrata, prvo punjenje). 300 s je dovoljno za većinu; 0 = isključeno. Zabeleženi počeci se pomeraju ranije, pa se naučena trajanja neko vreme menjaju.", "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.", + "doc": "Snaga mora da ostane u pojasu pripravnosti ovoliko dugo pre nego što se uređaj računa kao da čeka start.", "label": "Sekunde potvrde" }, "delay_start_detect_enabled": { - "doc": "Otkrijte kada je uređaj uključen i čeka (odloženo pokretanje / stanje pripravnosti), ali nije započeo svoj ciklus, tako da se izvlačenje u stanju pripravnosti ne smatra greškom u toku.", + "doc": "Otkrijte uređaj koji je uključen i čeka odloženi start, kako se njegova potrošnja u pripravnosti ne bi smatrala radom ciklusa.", "label": "Omogući detekciju odloženog starta" }, "delay_timeout_hours": { - "doc": "Prestanite da čekate u režimu odloženog pokretanja nakon ovoliko sati i vratite se u stanje mirovanja, tako da mašina koja je ostala napajana, ali nikada nije pokrenuta, ne čeka zauvek.", + "doc": "Prestanite da čekate odloženi start posle ovoliko sati i vratite se u mirovanje.", "label": "Sati vremenskog ograničenja" }, "device_type": { - "doc": "Klasa aparata. Postavlja razumne podrazumevane vrednosti detekcije (pragovi, kašnjenje isključenja, rukovanje na kraju) podešene za taj tip uređaja; promenite ga samo ako je uređaj prvobitno podešen kao pogrešan tip.", + "doc": "Postavlja podrazumevane vrednosti detekcije (pragovi, kašnjenje isklopa, obrada kraja) za ovu vrstu uređaja. Menjajte samo ako je uređaj podešen kao pogrešan tip.", "label": "Tip uređaja" }, "door_sensor_entity": { - "doc": "Opcioni binarni senzor vrata. Koristi se za otkrivanje kada je uređaj otvoren/istovaren nakon ciklusa.", + "doc": "Opcioni senzor vrata. Pokazuje kad je uređaj otvoren radi istovara posle ciklusa.", "label": "Entiteta senzora vrata" }, "duration_tolerance": { - "doc": "Tolerancija za procene preostalog vremena (povratne informacije učenja, ne podudaranje). Ako je stvarno trajanje unutar +/-Ks% procene, to se računa kao dobro podudaranje.", + "doc": "Samo za povratne informacije učenja: ciklus unutar +/-X% procenjenog trajanja računa se kao dobra procena. Ne utiče na podudaranje.", "label": "Tolerancija procene" }, "enable_ml_models": { - "doc": "Dok ciklus traje, pustite modele da preciziraju rezultate uživo: stabilnija procena preostalog vremena i energije/troškova, i zaštita od preranog zaustavljanja pri detekciji kraja (može samo da odloži završetak, nikada ne završi ranije i ograničen je). Koristi vaše fino podešene modele kada su dostupni, inače ugrađene. Isključeno = samo klasična logika zasnovana na napajanju (i dalje pouzdana).", + "doc": "Koristite model treniran na ovoj mašini za predviđanje energije i troška ciklusa u toku, i dozvolite dobro poznatom programu koji neobično rano stane da se završi bez punog kašnjenja isklopa. Isključeno: samo logika zasnovana na snazi.", "label": "Primeni pametne modele tokom ciklusa" }, "end_energy_threshold": { - "doc": "Tokom odbrojavanja sa kašnjenjem, akumulirana energija (vati k vreme) se upoređuje sa ovim pragom. Ako je prekoračeno, odbrojavanje se resetuje - držeći klupe protiv gužvanja i repove za sušenje u mašini za pranje sudova pričvršćene za ciklus umesto da ih skrate. Podignite ga ako se ciklusi završe prerano tokom hlađenja; spustite ga ako je detekcija spora.", + "doc": "Energija dozvoljena tokom kašnjenja isklopa. Iznad toga ciklus se nastavlja, pa prevrtanja i završno sušenje ostaju deo ciklusa. Smanjite ako se ciklusi završavaju tokom repa; povećajte ako završeni ciklusi i dalje čekaju.", "label": "Energija kraja" }, - "end_repeat_count": { - "doc": "Broj uzastopnih očitavanja ispod praga zaustavljanja potrebnih pre završetka ciklusa. 1 je u redu za većinu utikača. Povećajte na 2-3 ako vaš pametni priključak povremeno prijavi uzorak lažne nule usred ciklusa i vaši ciklusi se prerano završavaju.", - "label": "Broj ponavljanja kraja" - }, "energy_price_entity": { - "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.", + "doc": "Senzor sa trenutnom cenom po kWh (npr. dinamička tarifa). Ima prednost nad statičnom cenom ispod, pa mora biti cena, a ne senzor snage ili energije utikača.", "label": "Entiteta cene energije" }, "energy_price_static": { - "doc": "Fiksna cena po kWh koja se koristi za brojke troškova kada iznad nije postavljen nijedan entitet cene uživo.", + "doc": "Fiksna cena po kWh, koristi se kad nije postavljen entitet cene.", "label": "Statična cena energije (po kWh)" }, "energy_sensor": { - "doc": "Opciono kumulativno brojilo energije (total_increasing kWh/Wh, npr. sopstveno brojilo ukupne potrošnje utičnice). Kada je postavljeno, energija prijavljena za svaki ciklus uzima se iz razlike očitavanja ovog brojila između početka i kraja, čime se izbegava potcenjivanje koje nastaje integracijom sporo izveštavajućeg senzora snage. Vraća se na integrisanu vrednost ako očitavanje nedostaje, njegova jedinica je nepoznata ili razlika nije pozitivna. Ostavite prazno da biste nastavili da integrišete senzor snage.", + "doc": "Opciono brojilo kWh ili Wh (total_increasing, npr. sopstveno brojilo utikača). Energija ciklusa se tada čita iz njega, što je tačnije od izračunavanja iz sporog senzora snage. Ako je njegovo očitavanje neupotrebljivo, koristi se izračunata vrednost.", "label": "Entitet brojila energije" }, "expose_debug_entities": { - "doc": "Objavite dodatne dijagnostičke HA entitete (pouzdanost podudaranja, dvosmislenost, unutrašnje karakteristike stanja). Isključeno održava listu entiteta čistom za normalnu upotrebu.", + "doc": "Dodajte dijagnostičke entitete (pouzdanost podudaranja, dvosmislenost, unutrašnje stanje). Isključeno: lista entiteta ostaje pregledna.", "label": "Izloži entitete za otklanjanje grešaka" }, "external_end_trigger": { - "doc": "Binarni senzor čija promena stanja označava kraj ciklusa (npr. „završen“ kontakt uređaja ili integracija pratioca).", + "doc": "Binarni senzor čija promena označava kraj ciklusa (npr. kontakt \"završeno\" ili druga integracija).", "label": "Entiteta spoljnog okidača" }, "external_end_trigger_enabled": { - "doc": "Neka spoljni binarni senzor signalizira kraj ciklusa, pored ugrađene detekcije zasnovane na snazi.", + "doc": "Dozvolite binarnom senzoru da signalizira kraj ciklusa, uz detekciju zasnovanu na snazi.", "label": "Omogući spoljni okidač kraja" }, "external_end_trigger_inverted": { - "doc": "Tretirajte isključivanje senzora okidača (umesto UKLjUČENO) kao signal za kraj ciklusa.", + "doc": "Smatrajte ISKLJUČIVANJE senzora (a ne uključivanje) signalom kraja.", "label": "Invertuj spoljni okidač (okidaj na ISKLJUČENO)" }, "learning_confidence": { - "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.", + "doc": "Najniži rezultat podudaranja za označavanje završenog ciklusa. Jasan pobednik se označava; tesan rezultat ide u red za pregled u Ciklusima. Ispod ove vrednosti ništa se ne označava niti pita. Držite je ispod Pouzdanosti automatskog označavanja.", "label": "Pouzdanost učenja" }, "linked_device": { - "doc": "Opciono ugnezdite ovaj WashData uređaj u drugi uređaj (npr. pametni priključak) u registru HA uređaja, prikazan kao „Povezan preko ...“.", + "doc": "Opciono. Prikažite ovaj WashData uređaj ispod drugog uređaja (npr. pametnog utikača) u Home Assistantu, kao \"Povezano preko ...\".", "label": "Grupiši pod uređaj" }, "min_off_gap": { - "doc": "Ako se mašina isključi na kraće od ovog vremena, sekvenca za uključivanje/isključivanje/uključivanje se tretira kao jedan kontinuirani ciklus. Sprečava da se programi namakanja (mašina se isključuje na nekoliko minuta usred pranja) podele u dva odvojena ciklusa. Podesite ga na kraći od razmaka između vaših uzastopnih opterećenja ako želite da se oni računaju kao zasebni ciklusi. Podrazumevane vrednosti tipa uređaja štite tipičnu pauzu unutar ciklusa za svaki uređaj.", + "doc": "Isključenje kraće od ovoga ne deli ciklus, pa pauze potapanja ostaju u jednom pranju. Držite kraće od razmaka između uzastopnih punjenja, inače se ona spajaju u jedan ciklus.", "label": "Min. razmak isklopa" }, "min_power": { - "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.", + "doc": "Očitavanja ispod ove vrednosti računaju se kao 0 W (pripravnost). Filtrira potrošnju u mirovanju pametnih utikača i LED lampica pripravnosti.", "label": "Minimalna snaga" }, "ml_training_enabled": { - "doc": "Periodično proučite svoje recenzirane cikluse preko noći i fino prilagodite modele ovoj specifičnoj mašini. Promena se zadržava samo kada zaista ima bolje rezultate u ciklusima koji se zadržavaju, tako da ovo može samo da pomogne ili ostane ista - nikada ne nazaduje.", + "doc": "Povremeno ponovo trenirajte model energije na završenim ciklusima ove mašine. Novi model se zadržava samo ako na testnim ciklusima nadmaši jednostavnu procenu.", "label": "Uči se od ove mašine" }, "ml_training_hour": { - "doc": "Lokalni sat u danu (0-23) za fino podešavanje preko noći. Izaberite mirni sat kao što je 2 (02:00).", + "doc": "Sat u danu (0-23) za noćno treniranje. Izaberite miran sat, npr. 2.", "label": "Uči se u satu" }, "ml_training_interval_days": { - "doc": "Ponovo proverite da li postoje poboljšanja najviše jednom u ovoliko dana.", + "doc": "Ponovo trenirajte najviše jednom u ovoliko dana.", "label": "Proveravaj najviše svakih" }, "ml_training_min_cycles": { - "doc": "Sačekajte da se snimi barem ovoliko ciklusa pre finog podešavanja, tako da ima dovoljno za učenje.", + "doc": "Pre treniranja sačekajte bar ovoliko zabeleženih ciklusa.", "label": "Potrebni ciklusi najpre" }, "name": { - "doc": "Ime za prikaz prikazano na listi HA integracija i registru uređaja.", + "doc": "Naziv ovog uređaja prikazan u Home Assistantu.", "label": "Naziv uređaja" }, "no_update_active_timeout": { - "doc": "Ako ne stigne ažuriranje napajanja ovoliko dugo dok radi, pretpostavite da je utikač ispao van mreže i prinudno zaustavite da biste izbegli ciklus zombija. Podrazumevano 600 s dozvoljava kašnjenje u oblaku ili mreži.", + "doc": "Ako tokom ciklusa ovoliko dugo ne stigne nijedno ažuriranje snage, pretpostavlja se da je utikač van mreže i ciklus se prinudno zaustavlja. Podrazumevanih 600 s pokriva kašnjenja oblaka ili mesh mreže.", "label": "Vremensko ograničenje bez ažuriranja" }, "notify_before_end_minutes": { @@ -1350,35 +1335,35 @@ "label": "Upozorenje pre kraja" }, "notify_channel": { - "doc": "Naziv kanala za Android obaveštenja za početne/uživo poruke (kontroliše zvuk po kanalu i prioritet na mobilnoj aplikaciji). Prazno koristi podrazumevanu prateću aplikaciju.", + "doc": "Android kanal za poruke o početku i uživo; kanal određuje zvuk i prioritet. Prazno: podrazumevani kanal aplikacije.", "label": "Android kanal (start/živo)" }, "notify_cycle_timers": { - "doc": "Obaveštenja u određenim minutama u ciklusu (npr. za dodavanje omekšivača). Poruka podržava {device}, {program}, {minutes}. Omogućite automatsku pauzu da biste pauzirali u tom trenutku i primili interaktivno obaveštenje pomoću dugmeta Nastavi; nastavi preko panela, usluge pauze/nastavljanja ili radnje obaveštenja.", + "doc": "Obaveštenja u zadatim minutima ciklusa (npr. za dodavanje omekšivača). Poruka podržava {device}, {program}, {minutes}. Uz automatsku pauzu ciklus se tu i pauzira, a obaveštenje dobija dugme Nastavi.", "label": "Tajmeri ciklusa" }, "notify_finish_channel": { - "doc": "Naziv kanala za Android obaveštenja za poruku o završetku. Blank ponovo koristi kanal za početak/živo.", + "doc": "Android kanal za poruku o završetku. Prazno: koristi se kanal za početak i uživo.", "label": "Android kanal (završetak)" }, "notify_finish_message": { - "doc": "Telo koje se šalje kada se ciklus završi. Promenljive šablona: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}. {time_finished} i {vs_typical} su ovde najkorisniji.", + "doc": "Tekst koji se šalje kada se ciklus završi. Promenljive: {device}, {program}, {duration} (minuti), {duration_hm} (1 h 05 min), {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}, {status} (completed ili force_stopped; prekinuti ciklusi ne šalju ništa). Za svaku drugu promenljivu koristi se podrazumevani tekst.", "label": "Poruka pri završetku" }, "notify_finish_services": { - "doc": "notifi.* usluge koje se pozivaju kada se ciklus završi. Dodajte jedan po meti; ostavite prazno da nema obaveštenja o završetku.", + "doc": "notify.* usluge koje se pozivaju kad se ciklus završi. Ostavite prazno za nijednu.", "label": "Servisi pri završetku" }, "notify_fire_events": { - "doc": "Takođe aktivirajte ha_washdata_* događaje na početku/završetku ciklusa kako biste mogli da napravite sopstvene automatizacije.", + "doc": "Pokrenite i događaje ha_washdata_* pri početku i završetku ciklusa za sopstvene automatizacije.", "label": "Pokretanje HA događaja za obaveštenja" }, "notify_icon": { - "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.", + "doc": "Mala ikona na obaveštenju (npr. mdi:washing-machine). Šalje se samo na mobile_app ciljeve, ne na notify grupe. Na iOS-u zamenjuje ikonu aplikacije i zahteva mobilnu aplikaciju 2026.8 ili noviju. Prazno: podrazumevana ikona platforme.", "label": "Ikona obaveštenja" }, "notify_live_chronometer": { - "doc": "Prikažite tajmer za odbrojavanje koji se ažurira uživo u obaveštenju (na platformama koje ga podržavaju) umesto statične procene.", + "doc": "Prikažite odbrojavanje uživo u obaveštenju (gde je podržano) umesto fiksne procene.", "label": "Koristi živi hronometar" }, "notify_live_interval_seconds": { @@ -1386,139 +1371,131 @@ "label": "Interval živih ažuriranja" }, "notify_live_overrun_percent": { - "doc": "Ako ciklus pređe svoju procenu za više od ovog procenta, pošaljite upozorenje o prekoračenju.", + "doc": "Ažuriranja uživo prestaju kada ciklus premaši očekivano trajanje za ovaj procenat (20 = do 1,2x očekivanog vremena).", "label": "% prekoračenja pre upozorenja" }, "notify_live_services": { - "doc": "notifi.* usluge koje se pozivaju za ažuriranje napretka uživo dok ciklus traje. Ostavite prazno da biste onemogućili obaveštenja o napretku uživo.", + "doc": "notify.* usluge za ažuriranja napretka uživo dok ciklus traje. Ostavite prazno da ih isključite.", "label": "Servisi živog napretka" }, "notify_only_when_home": { - "doc": "Šaljite obaveštenja samo kada je bar jedna od povezanih osoba (gore) kod kuće.", + "doc": "Obaveštavajte samo kad je bar jedna od osoba iznad kod kuće.", "label": "Obavesti samo kad su kod kuće" }, "notify_people": { - "doc": "osoba.* entiteti koje koristi „Notifi Onli Vhen Home“ da odluči da li je neko kod kuće.", + "doc": "person.* entiteti koje proverava „Obavesti samo kad su kod kuće“.", "label": "Osobe (za Samo kad su kod kuće)" }, "notify_pre_complete_message": { - "doc": "Telo obaveštenja pre kraja / skoro gotovo. Promenljive šablona: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Tekst svakog ažuriranja napretka uživo. Promenljive šablona: {device}, {program}, {minutes} (preostali minuti). Za svaku drugu promenljivu koristi se podrazumevani tekst.", "label": "Poruka pre završetka" }, "notify_reminder_message": { - "doc": "Telo podsetnika da veš još čeka na vađenje. Prazno koristi ugrađenu podrazumevanu vrednost. Promenljive šablona: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Tekst jednokratnog Upozorenja pre kraja. Promenljive šablona: {device}, {program}, {minutes} (podešavanje Upozorenje pre kraja). Za svaku drugu promenljivu koristi se podrazumevani tekst.", "label": "Poruka podsetnika" }, "notify_start_message": { - "doc": "Telo koje se šalje kada ciklus počne. Promenljive šablona: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Tekst koji se šalje kada ciklus počne. Promenljive šablona: {device}, {program}. Za svaku drugu promenljivu koristi se podrazumevani tekst.", "label": "Poruka pri startu" }, "notify_start_services": { - "doc": "notifi.* usluge koje se pozivaju kada ciklus započne. Dodajte jedan po meti (telefon, kontrolna tabla, itd.); ostavite prazno da nema obaveštenja o početku.", + "doc": "notify.* usluge koje se pozivaju kad ciklus počne (telefon, kontrolna tabla, ...). Ostavite prazno za nijednu.", "label": "Servisi pri startu" }, "notify_timeout_seconds": { - "doc": "Automatski odbacite obaveštenje nakon ovoliko sekundi (na platformama koje ga podržavaju). 0 ga čuva dok se ne odbaci ručno.", + "doc": "Zatvorite obaveštenje posle ovoliko sekundi (gde je podržano). 0 ga zadržava dok ga ne zatvorite.", "label": "Automatski odbaci posle" }, "notify_title": { - "doc": "Naslov obaveštenja. Promenljive šablona: {device}, {program}, {duration}, {energy}, {cost}, {date}, {time}, {minutes}.", + "doc": "Naslov obaveštenja. Zamenjuje se samo {device}; za svaku drugu promenljivu koristi se podrazumevani naslov.", "label": "Naslov obaveštenja" }, "notify_unload_delay_minutes": { - "doc": "Nekoliko minuta nakon završetka ciklusa pre slanja podsetnika „istovarite mašinu“ koji još uvek čeka. Postavite 0 da biste onemogućili podsetnik.", + "doc": "Minuti posle završetka ciklusa pre podsetnika \"istovarite mašinu\". 0 ga isključuje.", "label": "Kašnjenje podsetnika za istovar" }, "notify_unload_repeat": { - "doc": "Ponovo šalje podsetnik za istovar svakih \"Kašnjenje podsetnika za istovar\" minuta dok se istovar ne potvrdi ili dok ne dodirnete \"Prestani da podsećaš\" na obaveštenju. Potreban je način za potvrdu istovara: Entitet senzora vrata, Entitet potvrde istovara ili opcija \"Ručno potvrdi istovar\" ispod. Dugme za odbacivanje radi na obaveštenjima mobilne aplikacije Home Assistant (companion app).", + "doc": "Ponovo šaljite podsetnik za istovar svakih \"Kašnjenje podsetnika za istovar\" minuta dok se istovar ne potvrdi ili ne dodirnete \"Prestani da podsećaš\" (mobilna aplikacija). Zahteva Entitet senzora vrata, Entitet potvrde istovara ili \"Ručno potvrdi istovar\".", "label": "Ponavljaj do istovara" }, "off_delay": { - "doc": "Vreme za čekanje nakon pada struje pre nego što proglasimo ciklus završenim. Ako se napajanje nastavi unutar ovog prozora, ciklus se nastavlja bez problema - ovo premošćuje pauze između faza pranja. Mašine za pranje sudova imaju duge faze sušenja (isključenje na 20-60 min) tako da kašnjenje isključenja mora biti veće od toga da bi se celo pranje+suvo održalo kao jedan ciklus.", + "doc": "Koliko dugo snaga mora da ostane niska pre nego što se ciklus završi. Povratak snage u tom vremenu nastavlja ciklus. Mašine za pranje sudova mogu da miruju 20-60 min tokom sušenja, pa postavite duže od toga.", "label": "Kašnjenje isklopa" }, "pause_cuts_power": { - "doc": "Kada je ciklus pauziran, takođe isključite Svitch Entiti ispod. Samo za uređaje čiji se utikač može bezbedno iseći usred ciklusa.", + "doc": "Kad je ciklus pauziran, isključite i Entitet prekidača ispod. Samo za uređaje koji bezbedno mogu da izgube napajanje usred ciklusa.", "label": "Pauza takođe prekida napajanje (putem prekidača)" }, "power_off_delay": { - "doc": "Koliko dugo snaga mora da ostane ispod Praga isključivanja napajanja nakon što se ciklus završi pre nego što se stanje vrati na „Isključeno“. Koristi se samo kada je Prag isključivanja napajanja iznad 0. Proverava se na pozadinskom ritmu, tako da se efektivno kašnjenje zaokružuje naviše na sledeći otkucaj isteka stanja.", + "doc": "Koliko dugo snaga mora da ostane ispod Praga isključivanja napajanja posle ciklusa pre nego što se stanje vrati na Isključeno. Koristi se samo kad je taj prag iznad 0. Proverava se povremeno, pa stvarno čekanje može biti malo duže.", "label": "Kašnjenje isključivanja napajanja" }, "power_off_threshold_w": { - "doc": "Opciono otkrivanje isključenog stanja na osnovu snage. Kada je iznad 0, nakon što se ciklus završi i snaga ostane ispod ovog nivoa tokom Kašnjenja isključivanja napajanja, mašina se tretira kao isključena i stanje se vraća na „Isključeno“. Ostavite na 0 da biste onemogućili (podrazumevano). Postavite ga iznad stvarnog nivoa isključenog napajanja, a ispod Praga zaustavljanja i ispod potrošnje vaše mašine u stanju pripravnosti (završeno, ali još uključeno); ako nije ispod Praga zaustavljanja, ignoriše se. Kada je omogućeno, zamenjuje Kašnjenje resetovanja napretka za povratak na „Isključeno“, tako da završena mašina ostaje na „Završeno/Čisto“ dok se stvarno ne isključi.", + "doc": "Posle ciklusa snaga ispod ove vrednosti tokom Kašnjenja isključivanja napajanja postavlja stanje na Isključeno, umesto Kašnjenja resetovanja napretka. Postavite iznad potrošnje isključenog uređaja, a ispod pripravnosti i Praga zaustavljanja. 0 ga isključuje (podrazumevano).", "label": "Prag isključivanja napajanja" }, "power_sensor": { - "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.", + "doc": "Senzor koji javlja trenutnu snagu ovog uređaja u vatima (npr. sensor.washer_power). Cela detekcija se zasniva na njemu.", "label": "Senzor snage" }, - "profile_duration_tolerance": { - "doc": "Opseg +/- oko prosečnog trajanja profila koji se koristi tokom uparivanja. 0,25 znači da profil od 60 minuta odgovara ciklusima od 45-75 minuta.", - "label": "Tolerancija trajanja profila" - }, "profile_match_interval": { - "doc": "Koliko često pokušavati uparivanje profila tokom ciklusa trčanja. Podrazumevanih 300 s (5 minuta) balansira brzinu detekcije i CPU.", + "doc": "Koliko često se ciklus u toku podudara sa profilima. Podrazumevano 300 s (5 min).", "label": "Interval podudaranja" }, "profile_match_max_duration_ratio": { - "doc": "Maksimalna dužina ciklusa u odnosu na profil. 1.3 znači da ciklus mora biti manji od 130% trajanja profila da bi se podudarao.", + "doc": "Program se odbacuje kad ciklus traje duže od ovog umnoška njegovog uobičajenog trajanja. 1,8: ciklus 80% duži od programa mu više ne odgovara.", "label": "Maks. odnos trajanja" }, "profile_match_min_duration_ratio": { - "doc": "Minimalna dužina ciklusa u odnosu na profil. 0,9 znači da ciklus mora biti najmanje 90% trajanja profila da bi se podudarao.", + "doc": "Program se razmatra tek kad ciklus prođe ovaj deo njegovog uobičajenog trajanja. 0,10: program od 2 sata postaje kandidat posle 12 minuta. Ne primenjuje se u prvih 15 minuta ciklusa.", "label": "Min. odnos trajanja" }, "profile_match_threshold": { - "doc": "Minimalni rezultat sličnosti (0-1) potreban na kraju ciklusa da bi se prihvatila identifikacija programa. Podignite ga da biste smanjili pogrešne identifikacije; smanjite ako se programi vaše mašine ne poklapaju. Podrazumevano 0,4 je konzervativna polazna tačka.", + "doc": "Rezultat podudaranja uživo koji ciklus treba pre nego što Pametno završavanje ili završetak zaštite od gužvanja sme da ga okonča ranije. Ne menja prikazani program. Povećajte ako slaba podudaranja prerano završavaju cikluse; smanjite ako jasna podudaranja čekaju kašnjenje isklopa.", "label": "Prag podudaranja" }, "profile_unmatch_threshold": { - "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.", + "doc": "Ako rezultat podudaranja uživo padne ispod ove vrednosti, trenutna pretpostavka programa se briše. Držite malo ispod Praga podudaranja kako je kratak pad ne bi poništio.", "label": "Prag nepodudaranja" }, "progress_reset_delay": { - "doc": "Nakon što završite, držite napredak na 100% toliko dugo kako bi Završeno bilo vidljivo na kontrolnoj tabli pre nego što se vratite na stanje mirovanja.", + "doc": "Posle ciklusa držite napredak na 100% ovoliko dugo pre vraćanja na U mirovanju.", "label": "Kašnjenje resetovanja napretka" }, "pump_stuck_duration": { - "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).", + "doc": "Koliko dugo pumpa sme da radi bez prekida pre nego što se pokrene događaj zaglavljene pumpe.", "label": "Trajanje zaglavljene pumpe" }, "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.", + "doc": "Najkraće vreme između obrađenih očitavanja. Očitavanja koja stignu ranije se preskaču (niska očitavanja tokom ciklusa se uvek zadržavaju). Postavite na interval javljanja utikača ili manje; veća vrednost samo odbacuje očitavanja.", "label": "Interval uzorkovanja" }, "save_debug_traces": { - "doc": "Sačuvajte kompletan trag napajanja i odgovarajuće podatke za otklanjanje grešaka za svaki ciklus. Korisno za rešavanje problema, ali povećava veličinu skladišta.", + "doc": "Uz svaki ciklus čuvajte i podatke za otklanjanje grešaka podudaranja (rezultate svih kandidata), za rešavanje problema. Dodaje oko 20 KB po ciklusu.", "label": "Sačuvaj tragove za otklanjanje grešaka" }, - "smoothing_window": { - "doc": "Koliko je signal sirove snage izglađen. Nizak (2) je osetljiv, ali bučan; high (5) izglađuje šiljke, ali dodaje zaostajanje.", - "label": "Prozor za glačanje" - }, "start_duration_threshold": { - "doc": "Snaga mora da ostane iznad početne granice ovoliko dugo da bi se potvrdio pravi početak, sprečavajući prekidače za uključivanje/isključivanje u deliću sekunde da započnu ciklus.", + "doc": "Snaga mora da ostane iznad Praga pokretanja ovoliko dugo pre nego što ciklus počne, kako bi se kratka uključivanja i isključivanja zanemarila.", "label": "Trajanje pokretanja" }, "start_energy_threshold": { - "doc": "Energija (snaga k vreme) koju uređaj mora da potroši pre POKRETANjA. Kratak skok velike snage ima veoma nisku energiju i ignoriše se, sprečavajući lažne startove.", + "doc": "Energija koju uređaj mora da potroši pre nego što se ciklus računa kao pokrenut. Kratak skok snage troši malo energije, pa ne pokreće ciklus.", "label": "Energija pokretanja" }, "start_threshold_w": { - "doc": "Snaga mora porasti iznad ovog nivoa da bi se potvrdilo da je ciklus započeo. Podešavanje na prenisko dovodi do lažnih pokretanja iz stanja pripravnosti; propušteni su previsoki i spori programi (hladno punjenje). Mehanizam za predloge postavlja ovo tik iznad uočene najniže aktivne snage mašine.", + "doc": "Snaga mora da poraste iznad ove vrednosti da bi ciklus počeo. Prenisko: potrošnja u pripravnosti izaziva lažne startove. Previsoko: programi koji sporo kreću (hladno punjenje) se propuštaju.", "label": "Prag pokretanja" }, "stop_threshold_w": { - "doc": "Snaga mora pasti ispod ovog nivoa pre nego što otpočne odbrojavanje sa kašnjenjem. Podesite ga ispod početnog praga - jaz između njih je histerezis pojas koji sprečava treperenje. Ako su postavljene previsoko, faze male snage (zadržavanje ispiranja, sprečavanje gužvanja) lažno pokreću krajnju sekvencu.", + "doc": "Snaga mora da padne ispod ove vrednosti pre nego što kašnjenje isklopa počne da odbrojava. Držite ispod Praga pokretanja. Ako je previsoko, faze niske snage (pauze ispiranja, zaštita od gužvanja) greškom pokreću odbrojavanje kraja.", "label": "Prag zaustavljanja" }, "switch_entity": { - "doc": "Opcioni prekidač se isključuje u pauzi i ponovo uključuje kada se nastavlja, koristi se zajedno sa „Pauza takođe isključuje napajanje“.", + "doc": "Opcioni prekidač koji se isključuje pri pauzi i ponovo uključuje pri nastavku, uz „Pauza takođe prekida napajanje“.", "label": "Entiteta prekidača" }, "watchdog_interval": { - "doc": "Koliko često pozadinski nadzornik proverava zastoje senzora i proteklo vremensko ograničenje. Standardno 30 s.", + "doc": "Koliko često pozadinska provera traži zaglavljene senzore i istekla vremenska ograničenja. Podrazumevano 30 s.", "label": "Interval nadzornika" }, "notify_milestone_message": { @@ -1526,131 +1503,86 @@ "label": "Poruka o prekretnici" }, "notify_milestones": { - "doc": "Brojevi ciklusa razdvojeni zarezima koji pri dostizanju pokreću jednokratno slavljeničko obaveštenje (npr. 50, 100, 500, 1000). Prazno onemogućava obaveštenja o prekretnicama.", + "doc": "Brojevi ciklusa koji pokreću jednokratnu čestitku, odvojeni zarezima (npr. 50, 100, 500, 1000). Prazno ih isključuje.", "label": "Prekretnice ciklusa" }, "notify_quiet_end_hour": { - "doc": "Kraj perioda ne uznemiravaj (0-23). Zadržana obaveštenja se isporučuju u ovom satu. Ostavite prazno da onemogućite.", + "doc": "Kraj perioda ne uznemiravaj (0-23); zadržana obaveštenja se tada šalju. Prazno ga isključuje.", "label": "Kraj tihih sati" }, "notify_quiet_start_hour": { - "doc": "Početak perioda ne uznemiravaj (0-23). Obaveštenja o završetku, podsetnici i obaveštenja o čistom vešu koja bi se aktivirala tokom tihih sati zadržavaju se i isporučuju kada se period završi. Ostavite prazno da onemogućite. Podržava periode koji prelaze ponoć (npr. početak 22, kraj 7).", + "doc": "Početak perioda ne uznemiravaj (0-23). Obaveštenja o završetku, podsetnici i obaveštenja o čistom vešu u tom periodu zadržavaju se do njegovog kraja. Može da pređe ponoć (npr. 22 do 7). Prazno ga isključuje.", "label": "Početak tihih sati" }, "peak_rate_message": { - "doc": "Opcionalni prilagođeni tekst za savet o vršnoj tarifi dodat obaveštenju o početku. Promenljive šablona: {device}, {price}. Prazno koristi ugrađenu podrazumevanu vrednost.", + "doc": "Sopstveni tekst saveta o vršnoj tarifi. Promenljive šablona: {device}, {price}. Prazno: podrazumevani tekst.", "label": "Poruka o vršnoj tarifi" }, "peak_rate_threshold": { - "doc": "Kada ciklus započne, a trenutna cena po kWh je na ovoj vrednosti ili iznad nje, obaveštenju o početku se dodaje savet o vršnoj tarifi. 0 ili prazno onemogućava savet.", + "doc": "Ako je cena po kWh na početku ciklusa jednaka ili veća od ove, dodajte savet o vršnoj tarifi obaveštenju o početku. 0 ili prazno ga isključuje.", "label": "Prag vršne tarife (po kWh)" }, - "dtw_bandwidth": { - "label": "Tolerancija podudaranja oblika", - "doc": "Koliko modul za podudaranje sme vremenski da izobliči ciklus da odgovara obliku sačuvanog programa. Viša vrednost podnosi veće varijacije brzine; previsoka rizikuje podudaranje sa pogrešnim programom." - }, - "corr_weight": { - "label": "Težina oblika u odnosu na nivo", - "doc": "Ravnoteža između oblika krive i apsolutnog nivoa snage pri ocenjivanju podudaranja. Viša vrednost daje prednost obliku; niža daje prednost slaganju nivoa snage." - }, - "duration_weight": { - "label": "Težina trajanja", - "doc": "Koliko snažno se daje prednost programu čije se tipično trajanje podudara sa ovim ciklusom." - }, - "energy_weight": { - "label": "Težina energije", - "doc": "Koliko snažno se daje prednost programu čija se tipična potrošnja energije podudara sa ovim ciklusom." - }, "store_brand": { "label": "Marka uređaja", - "doc": "Opciono. Marka uređaja izabrana iz kataloga zajednice. Koristi se za pronalaženje i deljenje odgovarajućih referentnih snimaka. Ostavite prazno ako ne koristite online funkcije." + "doc": "Opciono. Marka vašeg uređaja iz kataloga zajednice, koristi se za pronalaženje i deljenje referentnih snimaka. Ostavite prazno ako ne koristite onlajn funkcije." }, "store_model": { "label": "Model uređaja", - "doc": "Opciono. Model uređaja izabran iz kataloga zajednice nakon što je marka postavljena. Ako vaš model nije na listi, možete ga dodati u katalog." + "doc": "Opciono. Model vašeg uređaja iz kataloga zajednice (prvo izaberite marku). Ako nije na listi, možete ga dodati." }, "show_contributor": { "doc": "Prikaži ime saradnika na profilima preuzetim iz prodavnice zajednice" }, "smart_termination_duration_ratio": { - "doc": "Koliko duboko u očekivano trajanje podudarenog programa ciklus mora da odmakne pre nego što Pametno završavanje sme da ga okonča ranije kad snaga opadne. Očekivano trajanje je prosek programa, pa kod uređaja čije vreme rada dosta varira - mašine za pranje veša pri hladnoj zimskoj i toploj letnjoj dovodnoj vodi, mašine za sušenje sa senzorom vlage, programi zavisni od količine veša - oko polovine svih ciklusa završi kraće od tog proseka i nikada ne dobije brzi završetak, već se okonča tek preko rezervnog tajmauta sa nekoliko minuta zakašnjenja. Smanjite ovu vrednost (npr. 0.85) na takvim mašinama da bi se rano završavanje ipak aktiviralo; povećajte je ka 1.0 za oprezniji rad. Ostavite prazno za podrazumevanu vrednost (0.98, ili 0.99 za mašine za pranje sudova). Može samo da okonča ciklus ranije, nikada kasnije, i nikada se ne aktivira pri dvosmislenom ili nepouzdanom podudaranju.", + "doc": "Deo prosečnog trajanja programa koji ciklus mora da dostigne pre nego što Pametno završavanje sme da ga okonča ranije. Ako vremena rada variraju (temperatura vode, sušenje po senzoru, količina veša), smanjite ga (npr. 0,85). Ostavite prazno za 0,98 (0,99 kod mašina za pranje sudova).", "label": "Koeficijent pametnog završavanja" }, - "enable_phase_matching": { - "label": "Preostalo vreme prilagođeno fazama", - "doc": "Deli svaki aktivni ciklus na faze (zagrevanje, pranje, centrifuga) i raspoređuje preostalo vreme po fazama, u kombinaciji sa klasičnom procenom - oslanjajući se na raspored po fazama na početku ciklusa, a na klasičnu procenu pri kraju. Ovo prilagođava odbrojavanje tome koliko se vaša mašina zaista greje i radi, što je najuočljivije u prvoj polovini ciklusa. Isključeno = samo klasična procena. Utiče samo na prikaz preostalog vremena; podudaranje programa i detekcija ciklusa ostaju nepromenjeni." - }, - "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 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", - "doc": "Koliko DTW rezultat poravnanja zamenjuje osnovni rezultat Faze 2. 0 = koristi samo rezultat Faze 2, 1 = koristi samo DTW rezultat, 0,5 (podrazumevano) = jednako mešanje. Povećajte za veće oslanjanje na warp poravnanje kada programi imaju slične nivoe snage ali različite obrasce tajminga." - }, - "dtw_ensemble_w": { - "label": "Miks kombinacije DTW", - "doc": "U kombinovanom DTW režimu (podrazumevanom), težina skaliranog L1 naspram derivatnog DTW (DDTW). 1,0 = sve skalirano L1, 0 = sve DDTW, 0,7 = podrazumevano. Skalirana L1 varijanta upoređuje nivoe snage; derivatna varijanta reaguje na promene snage tokom vremena. Kombinovani režim spaja oba." - }, - "dtw_ddtw_scale": { - "label": "Razmera derivatne deformacije", - "doc": "Rastojanje polu-zasićenja za ocenjivanje derivatnim DTW. Rezultat se prepolovi kada rastojanje DDTW bude jednako ovoj vrednosti. Manje = osetljivije na razlike u obliku. Podrazumevana vrednost 30 je kalibrisana za tipične tragove snage kućanskih uređaja. Utiče samo na kombinovani i ddtw DTW režim." - }, - "dtw_refine_top_n": { - "label": "Broj preciziranih kandidata", - "doc": "Koliko prvih kandidata Faze 2 DTW ponovo ocenjuje. DTW je skuplji pa se primenjuje samo na N najboljih kandidata. Podrazumevano 5. Povećajte (na 7-9) ako ispravni profil ponekad dostigne samo 4. ili 5. mesto nakon Faze 2 - DTW ga može spasiti. Smanjenje malo ubrzava podudaranje." - }, - "duration_scale": { - "label": "Ostrina trajanja", - "doc": "Oštrina kazne za nepodudaranje trajanja u Fazi 4. Ovo je odnos logaritma pri kome se rezultat slaganja prepolovi. Manje = strože: nepodudaranje trajanja više snižava rezultat. Podrazumevana vrednost 0,175 odgovara toleranciji trajanja od oko 18% pri polu-težini. Koristite zajedno s Težinom trajanja." - }, - "energy_scale": { - "label": "Ostrina energije", - "doc": "Oštrina kazne za nepodudaranje energije u Fazi 4. Manje = strože: nepodudaranje energije više snižava rezultat. Podrazumevana vrednost 0,25 je namerno blaža od Oštrine trajanja jer energija varira s opterećenjem. Koristite zajedno s Težinom energije." - }, "power_profile_interval_min": { "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." + "doc": "Veličina segmenta atributa power_profile (prosečni vati po slotu) koji koriste planeri kao što su EMHASS i tibber_prices. Manji čuva kratke skokove; veći izglađuje krivu. Podrazumevano 15 min. 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." + "doc": "Samo iOS. Ažurirajte napredak uživo tiho, uz prioritet koji štedi bateriju i koji iOS može da grupiše. Prvo ažuriranje i obaveštenja o početku i završetku i dalje upozoravaju. Isključeno: svako ažuriranje upozorava." }, "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." + "doc": "Samo Android. Zadržite obaveštenje o napretku uživo posle dodira. Isključeno: dodir ga zatvara." }, "notify_live_click_action": { "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." + "doc": "Šta otvara dodir obaveštenja ovog uređaja. Prazno: WashData panel ovog uređaja. Upišite putanju kontrolne table (npr. /lovelace/laundry) ili URL, ili \"none\" da dodir ništa ne otvara." }, "door_opens_at_end": { "label": "Vrata se automatski otvaraju na kraju", - "doc": "Za mašine za suđe čija se vrata otvaraju na kraju ciklusa radi sušenja (AirDry i slično). Kada je uključeno, otvaranje vrata tokom aktivnog ciklusa više ga ne pauzira zauvek; umesto toga, ako vrata ostanu otvorena tokom vremena zadržavanja ispod, WashData smatra ciklus završenim. Kratko otvaranje (dodavanje posuđa) se zanemaruje. Zahteva entitet senzora vrata." + "doc": "Za mašine za pranje sudova koje na kraju otvaraju vrata radi sušenja (npr. AirDry). Vrata otvorena tokom vremena zadržavanja ispod završavaju ciklus umesto da ga pauziraju; kratko otvaranje se zanemaruje. Zahteva Entitet senzora vrata." }, "door_end_dwell_seconds": { "label": "Zadržavanje pri otvorenim vratima", - "doc": "Koliko dugo vrata moraju ostati otvorena pre nego WashData završi ciklus, kada je uključeno \"Vrata se automatski otvaraju na kraju\". Dovoljno dugo da se ignoriše brzo dodavanje posuđa (podrazumevano 60 s), dovoljno kratko za brzo završavanje kada mašina otvori vrata." + "doc": "Uz uključeno \"Vrata se automatski otvaraju na kraju\": koliko dugo vrata moraju da ostanu otvorena pre nego što se ciklus završi. Dovoljno dugo da se zanemari dodavanje posuđa (podrazumevano 60 s)." }, "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." + "doc": "Koje vrste ciklusa oblikuju krivu i podudaranje svakog programa. Poništavanje izbora vrste ništa ne briše; korisno ako ne verujete uvezenim podacima. Statistika uvek broji samo cikluse ove mašine. Poništavanje svih se zanemaruje." }, "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." + "doc": "Naplatite svaki ciklus po ceni u svakom trenutku njegovog rada, umesto po ceni kad je završen. Zahteva entitet cene. Obrada istorije (Dijagnostika) preračunava starije cikluse." }, "unload_confirm_entity": { "label": "Entitet potvrde istovara", - "doc": "Opcioni entitet čije aktiviranje znači da je sadržaj izvađen, za mašine koje ne mogu imati senzor vrata. Radi bilo koja vrsta: Zigbee ili NFC dugme, input_button pomoćnik, senzor pokreta ispred mašine, scena. Svaka promena stanja računa se kao potvrda, osim isključivanja i prelaska u nedostupno stanje. Postavljanje ovoga ujedno omogućava podsetnik za istovar na uređaju bez Entiteta senzora vrata." + "doc": "Opciono. Bilo koji entitet koji potvrđuje da je sadržaj izvađen, za mašine bez senzora vrata (dugme, input_button, senzor pokreta, scena). Broji se svaka promena stanja osim isključivanja i nedostupnosti. Uključuje i podsetnik za istovar." }, "unload_track_without_door": { "label": "Ručno potvrdi istovar", - "doc": "Pokreni podsetnik za istovar na uređaju bez Entiteta senzora vrata i bez Entiteta potvrde istovara, a istovar potvrdi iz sopstvene automatizacije: pritisni entitet dugmeta \"Označi kao istovareno\" ili pozovi uslugu ha_washdata.mark_unloaded. Zanemaruje se kada je postavljen Entitet senzora vrata, koji već daje taj signal." + "doc": "Pokrenite podsetnik za istovar bez senzora vrata ili entiteta potvrde. Potvrdite istovar iz sopstvene automatizacije dugmetom \"Označi kao istovareno\" ili uslugom ha_washdata.mark_unloaded. Zanemaruje se kad je postavljen Entitet senzora vrata." }, "notify_icon_color": { "label": "Boja obaveštenja", - "doc": "Akcentna boja obaveštenja ovog uređaja, kako bi se mašina za pranje veša, mašina za sušenje i mašina za pranje sudova razlikovale na prvi pogled. iOS boji ikonu i traku napretka Live Activity na zaključanom ekranu i u Dynamic Island. Android dobija istu boju, ali šta će s njom uraditi zavisi od telefona: neki oboje ikonu, a mnogi (naročito Samsung One UI) je potpuno ignorišu. Obe platforme razumeju Hex (npr. #03A9F4). Ostavite prazno za podrazumevanu boju platforme." + "doc": "Akcentna boja obaveštenja ovog uređaja, kako bi se mašina za veš, mašina za sušenje i mašina za sudove lako razlikovale. Na iOS-u ispunjava krug iza ikone i boji traku Live Activity. Mnogi Android telefoni (posebno Samsung) je zanemaruju. Koristite hex, npr. #03A9F4. Prazno: podrazumevana boja platforme." + }, + "share_name": { + "doc": "Prikažite naziv svog naloga kao autora ciklusa koje delite. Isključeno: deli se bez imena." } }, "setting_group": { @@ -1678,9 +1610,6 @@ "door_pause": { "label": "Vrata i pauza" }, - "dtw_refinement": { - "label": "DTW preciziranje" - }, "duration_gates": { "label": "Kapije trajanja" }, @@ -1757,17 +1686,20 @@ "rebuilding": "Ponovna izgradnja…", "analyzing": "Analiziranje…", "resetting": "Resetovanje…", - "reverting": "Vraćanje…", "trimming": "Isecanje…", "splitting": "Deljenje…", + "stalled": "Zaglavljeno", "deleting": "Brisanje…", "imported": "Uvezeno", "preparing": "Priprema…", - "unlabelling": "Uklanjanje oznake…" + "unlabelling": "Uklanjanje oznake…", + "working": "Obrada…", + "match_uncertain": "Nesigurno: {top} ili {runner}", + "match_uncertain_single": "Nesigurno: možda {top}", + "match_sure": "~{pct}% sigurnosti", + "match_uncertain_tip": "WashData se još nije odlučio za program. Broj pokazuje koliko je često vodeća pretpostavka, {top}, bila tačna u prošlim ciklusima uz ovakvu prednost. Samo informativno." }, "suggestion": { - "both_agree": "WashData preporučuje", - "calibrated_label": "Kalibrisano", "observed_label": "Izmereno", "impact": { "auto_label_confidence": { @@ -1782,10 +1714,6 @@ "higher": "Zatvara samo cikluse sa značajnom potrošnjom energije", "lower": "Zatvara i kraće ili manje energetske cikluse" }, - "end_repeat_count": { - "higher": "Zahteva više ponavljanja signala kraja – pouzdanije, malo sporije", - "lower": "Završava nakon manje ponavljanja – brže otkrivanje, veći rizik lažnog završetka" - }, "learning_confidence": { "higher": "Uči se samo iz visoko pouzdanih podudaranja – sporije, ali pouzdanije", "lower": "Uči se iz više ciklusa; model može biti manje precizan" @@ -1824,40 +1752,25 @@ } }, "reason": { - "auto_label_confidence": "Pouzdanost 15. percentila za {cycles} automatskih oznaka koje korisnik nikada nije ispravio ({p15}).", "completion_min_seconds": "Polovina trajanja čistog ciklusa p05 ({minutes} min) kroz {cycles} ciklusa; filtrira lažne cikluse.{excl}", - "dead_zone_batch": "Zadržano kratkim (medijana prozora nestabilnosti pri pokretanju kroz {cycles} ciklusa = {s}s) kako bi se stvarni kraj brzo prepoznao.", - "dead_zone_from_dip": "Na osnovu ranog pada snage prepoznatog na {s}s.", - "end_energy_default": "Podrazumevana preporučena osnovna vrednost za prag šuma na kraju ciklusa.", - "end_energy_false": "Energija lažnih završetaka p95 ({p95}Wh) kroz {cycles} ciklusa, sa donjom granicom od 0,2 % medijane energije ciklusa ({floor}Wh).", - "end_energy_no_false": "Nema lažnih završetaka kroz {cycles} ciklusa; sa donjom granicom od 0,2 % medijane energije ciklusa ({floor}Wh).", - "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.", + "match_interval_floored": "Najkraći program ({shortest}s) ograničio bi ovo na {cap}s, ispod minimuma od {minimum}s, pa se koristi minimum: {persistence} uzastopnih podudaranja traje {budget}s, više od {pct}% tog programa. Podudaranje se izvodi samo kad stigne očitavanje (medijana={median}s), pa to ništa ne košta.", "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.", "min_off_gap": "Na osnovu {gaps} uočenih razmaka između ciklusa (p05={p05}s). Donja granica uređaja: {floor}s.", "min_off_gap_bridge": "Postavljeno tako da premosti najduži tihi odsečak unutar ciklusa: {span}s + 60s rezerve, iz {spans} premošćenih razmaka kroz {cycles} čistih ciklusa (ostaje ispod vašeg najkraćeg razmaka između dva uzastopna pokretanja, koji iznosi {ceiling}s).", "min_power": "40 % najniže radne snage p05 ({p05}W) kroz {cycles} čistih ciklusa, čime prag isključenja ostaje ispod stvarne potrošnje.{excl}", - "ml_auto_label": "Najniža pouzdanost koju je model kvaliteta još ocenio čistom (p10 od {cycles} čistih ciklusa = {p10}).", - "ml_end_repeat": "{false}/{total} ciklusa ({pct}%) imalo je pauzu koju je detektor kraja ocenio sa >50 %; zahtevaj {val} potvrdu(e) kraja.", - "ml_off_delay": "Pauze potvrđene detektorom kraja: p95 {p95}s + 60s rezerve, iz {pauses} modelom proverenih pauza kroz {cycles} ciklusa (donja granica {floor}s).", "no_update_timeout": "Na osnovu uočene učestalosti ažuriranja (p95={p95}s) * 20 (najmanje 60s).", "off_delay_cadence": "Na osnovu uočene učestalosti ažuriranja (p95={p95}s) * 5", "off_delay_device_floor": "Korišćen bezbedni minimum specifičan za uređaj {device} ({floor}s).", "off_delay_generic_floor": "Korišćen opšti bezbedni minimum ({floor}s).", "off_delay_pauses": "Postavljeno tako da nadmaši stvarne pauze: pauza unutar ciklusa p95 {p95}s + 60s rezerve, iz {pauses} pauza kroz {cycles} čistih ciklusa (donja granica {floor}s).", - "profile_match_threshold": "Pouzdanost p10 za {cycles} tačnih automatskih oznaka ({p10}); bezbedna donja granica za potvrdu tokom rada.", - "sampling_interval": "Medijana intervala ažuriranja uočena kroz {cycles} čistih ciklusa ({si}s).{excl}", - "smoothing_window": "Postavljeno za izglađivanje ~30s očitavanja pri uzorkovanju od {si}s ({samples} uzoraka).", - "start_duration": "Zadržano kratkim (~jedan interval uzorkovanja od {si}s) kako bi prepoznavanje počelo što ranije, uz zanemarivanje pojedinačnih skokova.", - "thr_batch": "Zadržano tik iznad najniže radne snage p05 kroz {cycles} ciklusa ({p05}W) kako bi se početak uhvatio što ranije, a prag zaustavljanja ostao ispod najniže radne snage mašine.", - "tol_per_profile": "p75 varijanse trajanja po profilu kroz {profiles} profila ({cycles} ciklusa); dosledni profili nisu kažnjeni.", - "tol_pooled": "Na osnovu objedinjene varijanse trajanja {cycles} novijih označenih ciklusa (p95 odstupanje={dev}).", - "watchdog": "Zadržano što nižim uz bezbednost (tik iznad razmaka ažuriranja p95 od {p95}s i najmanje 2× intervala uzorkovanja od {median}s, najmanje 30s) kako bi se zastoji brzo uhvatili bez lažnih zaustavljanja." + "standby_floor": "Svaki nedavni ciklus je još uvek trošio {idle}W kada je završio, pa prag ispod toga nikad ne može registrovati isključenje uređaja. Zadržan je iznad te vrednosti; {cycles} čistih ciklusa u evidenciji nikada nije palo ispod njega na duže od {pause}s.", + "thr_batch": "Malo iznad najniže aktivne snage (p05) u {cycles} ciklusa ({p05}W), kako bi se startovi rano uhvatili, a prag zaustavljanja ostao ispod najniže radne snage.", + "watchdog": "Zadržano što nižim uz bezbednost (tik iznad razmaka ažuriranja p95 od {p95}s i najmanje 2× intervala uzorkovanja od {median}s, najmanje 30s) kako bi se zastoji brzo uhvatili bez lažnih zaustavljanja.", + "end_energy_floor": "Povećano na minimum koji proizlazi iz praga zaustavljanja ({stop} W) tokom kašnjenja isklopa ({off_delay} s); ispod toga ciklus može da se završi samo rezervnim putem.", + "max_duration_ratio_default": "Vraćeno na podrazumevanu vrednost: niža gornja granica odbacuje pravi program kad god ciklus traje duže nego obično.", + "min_duration_ratio_default": "Vraćeno na podrazumevanu vrednost: viša donja granica sprečava prepoznavanje ciklusa u toku sve dok dobro ne odmakne." }, "exclusions": { "summary": "Isključeno {total} pogrešno detektovanih ciklusa: {parts}.", @@ -1875,7 +1788,8 @@ "no_power": "bez snage", "no_active_power": "bez aktivne snage" } - } + }, + "now_value": "(trenutno {v})" }, "tab": { "advanced": "Napredno", @@ -1906,13 +1820,17 @@ }, "toast": { "catalog_refreshed": "Katalog zajednice je osvežen", + "task_name_required": "Dajte zadatku naziv", + "task_added": "Zadatak dodat", + "task_save_failed": "Nije moguće sačuvati zadatak: {error}", + "task_removed": "Zadatak uklonjen", + "maint_logged": "Označeno kao obavljeno: {task}", "access_saved": "Kontrola pristupa sačuvana", "all_wiped": "Svi podaci obrisani", "analysis_complete_none": "Analiza završena: nema novih prijedloga", "auto_label_complete": "Automatsko označavanje završeno", "automation_deleted": "Automatizacija izbrisana", "correction_submitted": "Ispravka poslata", - "cycle_deleted": "Ciklus izbrisan", "cycle_force_stopped": "Ciklus prisilno zaustavljen", "cycle_labelled": "Ciklus označen", "cycle_paused": "Ciklus pauziran", @@ -1928,11 +1846,9 @@ "group_deleted": "Grupa izbrisana", "group_name_required": "Naziv grupe je obavezan", "group_saved": "Grupa sačuvana", - "import_successful": "Uvoz uspešan; integracija se ponovo učitava", "json_required": "JSON podaci su obavezni", "legacy_removed": "Zastarele akcije uklonjene", "logs_exported": "Dnevnici izvezeni", - "matching_reverted": "Težine podudaranja vraćene na zadane vrednosti", "min_2_profiles": "Izaberi najmanje 2 profila za grupu", "ml_training_no_improvement": "Obuka završena: baza zadržana (bez poboljšanja)", "models_reverted": "Modeli na uređaju vraćeni na bazu", @@ -1942,8 +1858,8 @@ "pg_preset_loaded": "Konfiguracija \"{name}\" učitana", "pg_preset_saved": "Konfiguracija \"{name}\" sačuvana", "pg_sugg_loaded": "Učitano {n} predloženih vrednosti - pokrenite testno okruženje za poređenje", - "pg_sugg_ml_loaded": "Učitano {n} ML-kalibriranih vrednosti - pokrenite testno okruženje za poređenje", "phase_name_required": "Naziv faze je obavezan", + "phase_reset": "Faza \"{name}\" je vraćena na ugrađenu verziju", "phase_updated": "Faza ažurirana", "phases_saved": "Faze sačuvane", "preferences_saved": "Preference sačuvane", @@ -1990,15 +1906,11 @@ "store_name_required": "Unesite naziv profila", "store_import_failed": "Uvoz nije uspeo: {error}", "store_imported": "Uvezeno u {profile}", - "store_program_required": "Unesite naziv programa", "store_no_appliance": "Prvo postavite marku i model uređaja u Podešavanjima.", "store_share_failed": "Deljenje nije uspelo: {error}", "store_shared": "Podeljeno u prodavnicu zajednice, čeka pregled.", "contrib_device_changed": "Dodato u prodavnicu zajednice, ali se izabrani uređaj promenio dok je obrazac bio otvoren. Izaberite ga u Podešavanjima.", "appliance_added": "Uređaj dodat, čeka odobrenje", - "thanks_confirming": "Hvala na potvrdi", - "device_approved": "Odobreno od strane zajednice", - "rating_saved": "Ocena kvaliteta sačuvana", "brand_added": "Marka dodata, čeka odobrenje", "profile_added": "Profil dodat, čeka odobrenje", "saved_except_conflicts": "Sačuvano. Ispravite istaknute konflikte da sačuvate ostalo.", @@ -2021,7 +1933,50 @@ "odometer_saved": "Ukupan broj ciklusa sačuvan", "odometer_save_failed": "Nije moguće sačuvati ukupan broj: {error}", "store_pick_profile_share": "Izaberite profil za deljenje", - "remove_failed": "Uklanjanje nije uspelo: {error}" + "remove_failed": "Uklanjanje nije uspelo: {error}", + "analysis_complete": "Analiza završena: predloga: {count}", + "analysis_failed": "Analiza nije uspela: {error}", + "auto_detect_failed": "Automatska detekcija nije uspela: {error}", + "auto_label_failed": "Automatsko označavanje nije uspelo: {error}", + "could_not_load_cycle": "Nije moguće učitati ciklus: {error}", + "create_failed": "Kreiranje nije uspelo: {error}", + "cycles_deleted": "Obrisano ciklusa: {count}", + "debug_cleared": "Obrisano tragova za otklanjanje grešaka: {count}", + "discard_failed": "Odbacivanje nije uspelo: {error}", + "error": "Greška: {error}", + "export_failed": "Izvoz nije uspeo: {error}", + "import_failed": "Uvoz nije uspeo: {error}", + "invalid_json": "\"{key}\" nije ispravan JSON - ispravite ga ili ispraznite polje pre čuvanja.", + "label_failed": "Označavanje nije uspelo: {error}", + "ml_training_promoted": "Treniranje završeno: aktivirano modela: {count}", + "pause_failed": "Pauziranje nije uspelo: {error}", + "phase_created": "Faza \"{name}\" je kreirana", + "phase_deleted": "Faza \"{name}\" je obrisana", + "profile_created": "Profil \"{name}\" je kreiran", + "rename_failed": "Preimenovanje nije uspelo: {error}", + "save_failed": "Čuvanje nije uspelo: {error}", + "split_complete": "Podeljeno na cikluse: {count}", + "start_failed": "Pokretanje nije uspelo: {error}", + "stop_failed": "Zaustavljanje nije uspelo: {error}", + "update_failed": "Ažuriranje nije uspelo: {error}", + "store_download_skipped": "Preskočeni snimci: {n} (prekratki, sa previše praznina ili već na vašem uređaju).", + "store_import_low_quality": "Ovaj snimak je prekratak, sa previše praznina ili neverodostojan da bi se uvezao.", + "store_import_duplicate": "Ovaj snimak je već na vašem uređaju.", + "device_approved": "Odobreno od strane zajednice", + "thanks_confirming": "Hvala na potvrdi", + "rating_saved": "Ocena kvaliteta sačuvana", + "store_download_partial": "Programi koji nisu mogli da se preuzmu: {n}; pokušajte ponovo kasnije.", + "import_no_restore_point": "Uvezeno, ali tačka vraćanja nije mogla da se sačuva, pa ovaj uvoz ne može da se opozove", + "import_undo_failed": "Uvoz nije moguće opozvati: {error}", + "import_undone": "Uvoz opozvan: vraćeno {profiles} profil(a) i {cycles} ciklus(a)", + "auto_label_cancelled": "Automatsko označavanje otkazano posle označavanja {count} ciklusa.", + "auto_label_cancelled_one": "Automatsko označavanje otkazano posle označavanja {count} ciklusa.", + "auto_label_cancelled_few": "Automatsko označavanje otkazano posle označavanja {count} ciklusa.", + "auto_label_cancelled_other": "Automatsko označavanje otkazano posle označavanja {count} ciklusa.", + "auto_label_done": "Automatsko označavanje završeno: označeno je {count} ciklusa, bez oznake: {skipped}.", + "auto_label_done_one": "Automatsko označavanje završeno: označen je {count} ciklus, bez oznake: {skipped}.", + "auto_label_done_few": "Automatsko označavanje završeno: označena su {count} ciklusa, bez oznake: {skipped}.", + "auto_label_done_other": "Automatsko označavanje završeno: označeno je {count} ciklusa, bez oznake: {skipped}." }, "trend": { "down": "Opadajući trend", @@ -2029,6 +1984,10 @@ }, "maint": { "bearing_service": "Servis ležajeva", + "salt": "Dopuni so", + "rinse_aid": "Dopuni sredstvo za ispiranje", + "lint_filter": "Očisti filter za vlakna", + "condenser_clean": "Očisti kondenzator", "descale": "Uklanjanje kamenca", "drum_clean": "Očisti bubanj", "filter_clean": "Očisti filter", @@ -2036,10 +1995,9 @@ }, "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", + "smart_termination_duration_ratio": "Deo prosečnog trajanja programa koji ciklus mora da dostigne pre nego što Pametno završavanje sme da ga okonča ranije; smanjite ako vremena rada variraju", + "anti_crease_finalize_ratio": "Deo prosečnog trajanja programa koji ciklus mora da dostigne pre nego što rep zaštite od gužvanja sme da ga završi; smanjite kod mašina za sušenje, kod mašina za veš ostavite podrazumevano", "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", "min_off_gap": "Razmak potreban za razdvajanje dva ciklusa", "off_delay": "Sekundi niske snage pre završetka ciklusa", @@ -2047,23 +2005,12 @@ "start_threshold_w": "Minimum vati da bi se računalo kao pokrenuto", "stop_threshold_w": "Ispod ovoga mašina se računa kao isključena", "anti_wrinkle_enabled": "Upijte impulse prevrtanja bubnja nakon glavne faze umesto da ih čitate kao nove cikluse", - "anti_wrinkle_exit_power": "Snaga između impulsa mora pasti ispod ove da bi zaštita od gužvanja ostala aktivna", + "anti_wrinkle_exit_power": "Nivo mirovanja između impulsa okretanja; deluje samo iznad Praga zaustavljanja", "anti_wrinkle_idle_timeout": "Dozvoljena tišina između dva impulsa prevrtanja bubnja pre nego što se zaštita od gužvanja završi", "anti_wrinkle_max_duration": "Impuls duži od ovoga završava zaštitu od gužvanja i otvara novi ciklus", "anti_wrinkle_max_power": "Impuls iznad ovoga završava zaštitu od gužvanja i otvara novi ciklus", - "dtw_bandwidth": "Faza 3: warp opseg Sakoe-Chiba (0 = DTW isključen; podrazumevano 0,2 = 20% dužine ciklusa)", - "corr_weight": "Faza 2: ravnoteža između oblika krive (korelacija) i nivoa snage (MAE); podrazumevano 0,45", - "duration_weight": "Faza 4: koliko jako slaganje trajanja utiče na konačni rezultat (podrazumevano 0,22)", - "energy_weight": "Faza 4: koliko jako slaganje energije utiče na konačni rezultat (podrazumevano 0,22)", - "profile_match_min_duration_ratio": "Faza 1: minimalna dužina ciklusa kao udeo trajanja profila; podrazumevano 0,1 (10%)", - "profile_match_max_duration_ratio": "Faza 1: maksimalna dužina ciklusa kao udeo trajanja profila; podrazumevano 1,8 (180%)", - "keep_min_score": "Faza 2: minimalni rezultat za ostanak u utrci; podrazumevano 0,1 prihvata slaba podudaranja, kasniji koraci biraju najbolje", - "dtw_blend": "Faza 3: 0 = samo osnovni rezultat, 1 = samo DTW rezultat, 0,5 = jednako mešanje (podrazumevano)", - "dtw_ensemble_w": "Faza 3 (kombinovani režim): težina skaliranog L1 naspram derivatnog DTW; podrazumevano 0,7 favorizuje nivo", - "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)", + "profile_match_min_duration_ratio": "Minimalna dužina ciklusa kao deo trajanja profila; podrazumevano 0,1 (10%)", + "profile_match_max_duration_ratio": "Maksimalna dužina ciklusa kao deo trajanja profila; podrazumevano 1,8 (180%)", "curve_preroll_seconds": "Sekunde očitavanja iz prekinutih pokušaja pokretanja prenete na početak krive ciklusa; 0 = isključeno" }, "col": { @@ -2071,12 +2018,14 @@ "status_tip": "Ishod ciklusa: Završeno (prirodni kraj), Prekinuto (nagli pad snage), Prisilno zaustavljeno (ručno) ili Potrebna provera (čeka se povratna informacija).", "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. 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.", + "energy_tip": "Ukupna potrošena energija (kWh), izračunata iz snage kroz vreme.", + "cost_tip": "Trošak energije ovog ciklusa, utvrđen kad je završen. Uz uključen Vremenski ponderisan trošak svaki trenutak se naplaćuje po svojoj ceni; inače po ceni na kraju. Postavite cenu u Podešavanjima da se ovo 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", - "cost_dynamic_tip": "Vremenski ponderisano: naplaćeno po ceni koja je važila tokom ciklusa (efektivno {price} {cur}/kWh)." + "cost_dynamic_tip": "Vremenski ponderisano: naplaćeno po ceni koja je važila tokom ciklusa (efektivno {price} {cur}/kWh).", + "dbg_conf": "Pouzd.", + "dbg_mae": "MAE", + "dbg_corr": "Kor." }, "stat": { "avg": "pros.", @@ -2100,7 +2049,10 @@ "notify_finish": "Bilo bi poslato obaveštenje o završetku.", "notify_milestone": "Bilo bi poslato obaveštenje o prekretnici.", "notify_held": "Obaveštenje je zadržano zbog tihih sati.", - "finished": "Ciklus je dostigao završno stanje i završio." + "finished": "Ciklus je dostigao završno stanje i završio.", + "match_reverted": "Program se vratio na otkrivanje: njegov rezultat je pao znatno ispod sopstvenog vrha.", + "verified_pause": "Poravnanje uživo je potvrdilo (ili ukinulo) pauzu unutar programa. Dok pauza traje, ciklus se ne završava.", + "stalled": "Ciklus je ostao na potrošnji u pripravnosti duže nego što ovaj program ikada tu pauzira, a posao nije gotov. Prikazano kao Pauzirano (Zaglavljeno); ciklus ostaje otvoren." }, "store": { "search_ph2": "Pretraži po marki ili modelu…", @@ -2109,11 +2061,7 @@ "programs_count": "Programi: {n}", "browse": "Pregledaj", "device": "Uređaj", - "favorites": "Omiljeni", - "search_ph": "Pretraži po marki ili modelu…", "no_results": "Nisu pronađeni odgovarajući uređaji. Pokušajte sa drugom pretragom.", - "n_programs": "{n} programa", - "n_cycles": "{n} ciklusa", "no_programs": "Za ovaj uređaj još nema podeljenih programa.", "no_cycles": "Za ovaj program još nema podeljenih referentnih ciklusa.", "anon": "anonimno", @@ -2125,19 +2073,21 @@ "merge_target": "Spoji u profil", "description": "Opis (opciono)", "contributed_by": "od {name}", + "website": "Veb stranica prodavnice ↗", + "website_tip": "Otvorite veb stranicu prodavnice zajednice", + "favorites": "Omiljeni", "rating_summary": "★ {avg} ({n})", "no_ratings": "Još nema ocena", - "website": "Veb stranica prodavnice ↗", - "website_tip": "Otvorite veb stranicu prodavnice zajednice" + "unreachable": "Nije moguće povezati se sa prodavnicom zajednice. Proverite vezu i pokušajte ponovo." }, "settings": { "gear": { - "title": "Podešavanja" + "title": "Preference" } }, "link": { - "manual": "Uputstvo ↗", - "add_appliance": "Dodajte svoj uređaj" + "maintenance_supporter": "Maintenance Supporter na GitHubu ↗", + "manual": "Uputstvo ↗" }, "tip": { "add_brand": "Marke nema na listi? Dodajte je u katalog zajednice", @@ -2171,6 +2121,12 @@ }, "rebuild": { "envelopes": "Ponovna izgradnja omotača" + }, + "auto_label": { + "running": "Automatsko označavanje ciklusa" + }, + "store_download": { + "running": "Preuzimanje iz prodavnice" } }, "setup": { @@ -2180,26 +2136,24 @@ }, "phase0": { "washer": "WashData već otkriva vaše cikluse. Snimite prvi ciklus kako biste omogućili nazive programa i procene vremena.", - "dishwasher": "WashData posmatra. Mašine za pranje sudova imaju složene cikluse – snimanje prvog ciklusa je izrazito preporučeno. Ako otkriveni ciklus traje predugo, koristite uređivač ciklusa za isecanje pre čuvanja kao profila.", + "dishwasher": "WashData prati. Ciklusi mašine za pranje sudova su složeni, pa snimite prvi ciklus. Ako otkriveni ciklus traje predugo, skratite ga u uređivaču ciklusa pre čuvanja kao profila.", "generic": "WashData posmatra. Snimite ili označite otkriveni ciklus kako biste počeli da gradite profile." }, "phase1a": { - "labelled": "Dobar početak – vaš prvi program je sačuvan. Za najčistije podatke razmislite o snimanju sledećeg ciklusa pomoću vidžeta za snimanje." + "labelled": "Dobar početak: vaš prvi program je sačuvan. Za najčistije podatke snimite sledeći ciklus snimačem." }, "phase1b": { - "recorded": "Vaša snimka sačuvana je kao {profile_name}. Sada snimite ili označite ostale uobičajene programe za izgradnju pokrivenosti." + "recorded": "Vaš snimak je sačuvan kao {profile_name}. Sada snimite ili označite ostale česte programe." }, "phase1c": { - "verify": "Imate {count} programa od zajednice. Pokrenite ciklus kako biste proverili da li ih WashData ispravno prepoznaje – poklapanje će se poboljšavati kako vaš uređaj gradi sopstvenu istoriju." + "verify": "Imate programe iz zajednice ({count}). Pokrenite ciklus da proverite da li ga WashData prepoznaje; podudaranje se poboljšava kako uređaj gradi sopstvenu istoriju." }, "phase2": { - "cluster": "WashData je video {count} ciklusa koji se ne poklapaju ni sa jednim sačuvanim programom – međusobno su slični. Da li želite da napravite novi profil za njih?", + "cluster": "WashData je uočio slične cikluse ({count}) koji ne odgovaraju nijednom sačuvanom programu. Napraviti novi profil za njih?", "unmatched": "Vaš poslednji ciklus se nije poklopio ni sa jednim sačuvanim programom. Da li je to novi program?" }, "phase3": { - "suggestions": "WashData ima preporuke podešavanja zasnovane na istoriji vaših ciklusa – pregledajte ih da biste poboljšali tačnost otkrivanja.", - "groups": "Neki vaši profili izgledaju kao isti program na različitim temperaturama. Organizujte ih u grupu za bolje poklapanje.", - "phases": "Dodajte faze programa u {profile_name} za tačnije procene preostalog vremena." + "suggestions": "WashData ima predloge podešavanja iz istorije vaših ciklusa. Pregledajte ih za bolju detekciju." }, "phase4": { "healthy": "Ovaj uređaj je u potpunosti podešen ({profile_count} profila)." @@ -2212,12 +2166,58 @@ "create_from_cluster": "Napravi profil od ovih ciklusa", "create_profile": "Napravi za njega profil", "review_suggestions": "Preglej predloge", - "organise_profiles": "Organizuj profile", - "configure_phases": "Konfiguriši faze", "skip_step": "Preskoči ovaj korak", "skip_forever": "Ne prikazuj ponovo", "hide_guidance": "Sakrij uputstvo", "show_guidance": "Prikaži uputstvo" } + }, + "access": { + "edit": "Uređivanje", + "full": "Potpuno", + "inherit": "Nasledi", + "none": "Nema (skriveno)", + "read": "Čitanje" + }, + "pg_label_reason": { + "no_winner": "Nijedan program ne odgovara", + "below_floor": "Ispod praga \"Pouzdanost učenja\"", + "ambiguous": "Označeno kao dvosmisleno", + "margin": "Previše blizu drugom najboljem programu", + "unknown_profile": "Program više ne postoji", + "not_evaluated": "Nije procenjeno" + }, + "diagram": { + "accum": "akum.", + "auto": "auto", + "below_off": "ispod = isklj.", + "confirmed": "potvrđeno", + "energy_counts": "energija se broji", + "feedback": "povratna inf.", + "gap_below_min": "razmak ispod min. = jedan ciklus", + "heat_phase": "faza grejanja", + "held_at_100": "ostaje na 100%", + "ignored": "zanemareno", + "kept": "zadržano", + "max": "maks.", + "min": "min.", + "minus_tol": "-tol", + "no_match": "nema poklapanja", + "no_updates": "nema ažuriranja", + "off_delay_wait": "kašnjenje isklopa", + "off_gap": "razmak isklopa", + "plus_tol": "+tol", + "profile": "profil", + "reading_interval": "tipični interval očitavanja", + "sensor_offline": "senzor van mreže: prisilno zaustavljanje", + "si": "IU", + "spike_ignored": "skok zanemaren", + "spike_ignored_short": "skok: zanemaren", + "start": "pokretanje", + "stop": "zaustavljanje", + "tail_energy_resets": "energija kraja iznad praga: odbrojavanje se resetuje", + "thr": "prag", + "too_short": "prekratak", + "tumble_kept": "impulsi bubnja zadržani" } } diff --git a/custom_components/ha_washdata/translations/panel/sv.json b/custom_components/ha_washdata/translations/panel/sv.json index 0d733745..df0662f7 100644 --- a/custom_components/ha_washdata/translations/panel/sv.json +++ b/custom_components/ha_washdata/translations/panel/sv.json @@ -3,6 +3,7 @@ "min": "min", "entity_not_found": "Entitet hittades inte", "running_long": "Kör längre", + "stalled": "Avstannad", "remaining": "återstår", "progress": "förlopp", "show_time_remaining": "Visa återstående tid", @@ -48,16 +49,16 @@ } }, "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_tip": "Mot slutet blir det här programmet tyst i cirka {mins} min och drar sedan cirka {watts} W i {secs} s. Sett i {seen} av {measured} uppmätta cykler.", "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", + "artifact_tip": "{n} anomalier upptäckta (t.ex. dörren öppnades mitt i cykeln) - öppna för att se dem i diagrammet", + "artifact_tip_one": "{n} anomali upptäckt (t.ex. dörren öppnades mitt i cykeln) - öppna för att se den i diagrammet", + "artifact_tip_other": "{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", "declining": "↘ sjunkande", "energy_low": "Lägre energi än vanligt", "energy_spike": "Högre energi än vanligt", - "fair_fit": "acceptabel kvalitet", - "fair_fit_tip": "Måttlig matchkonsistens – vissa cykler tilldelade den här profilen har lägre konfidenspoäng. Märk fler cykler eller spela in profilen igen för att förbättra noggrannheten.", "feedback_requested": "Feedback begärd", "golden_cycle": "Inspelad referenscykel", "duration_outlier": "{n} cykel/cykler med avvikande längd", @@ -66,27 +67,38 @@ "needs_review": "Behöver granskning", "overrun": "Pågick längre än vanligt", "poor_fit": "dålig kvalitet", - "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", + "restart_gap_tip": "{n} HA-omstartsluckor: effektspåret har hål", + "restart_gap_tip_one": "{n} HA-omstartslucka: effektspåret har ett hål", + "restart_gap_tip_other": "{n} HA-omstartsluckor: effektspåret har hål", "reviewed": "Granskad", "steady": "→ stabil", "underrun": "Klart snabbare än vanligt", - "using_defaults": "Använder standardinställningar", - "using_tuned": "Använder finjusterade vikter", "overrun_ratio": "({x}x förväntat)", "underrun_ratio": "({pct}% av förväntat)", - "awaiting_tip": "Väntar på godkännande från gemenskapen", - "awaiting_n": "Väntar på godkännande · {n} bekräftade", - "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.", - "armed": "(gäller nästa cykel)" + "armed": "(gäller nästa cykel)", + "awaiting_tip": "Väntar på godkännande från gemenskapen", + "awaiting_n": "Väntar på godkännande · {n} bekräftade", + "awaiting": "Väntar på godkännande", + "approved": "Godkänd", + "matcher_unused": "Ännu inte igenkänt i dina cykler", + "matcher_used": "Igenkänt i {n} av dina cykler", + "matcher_used_one": "Igenkänt i {n} av dina cykler", + "matcher_used_other": "Igenkänt i {n} av dina cykler", + "matcher_used_tip": "Cykler på den här apparaten som WashData själv har matchat mot det här importerade programmet. En import som aldrig matchar kanske inte passar din apparat; du kan ta bort den. Räknas lokalt; inget skickas till gemenskapsbutiken." }, "btn": { + "log_done_task": "Logga som klar: {task}", + "add_task": "Lägg till uppgift", + "pg_preset_delete": "Ta bort förinställning", + "pg_preset_load": "Ladda förinställning", + "pg_preset_load_tip": "Ersätt värdena i testytan med den här förinställningen (de aktiva inställningarna påverkas inte)", + "pg_preset_save": "Spara som förinställning", + "pg_preset_save_tip": "Spara alla värden nedan som en namngiven förinställning för den här enheten", "reset_zoom": "Återställ zoom", "set_brand_model": "Ange märke och modell", "refresh_catalog": "Uppdatera katalog", @@ -97,12 +109,11 @@ "add_timer": "+ Lägg till timer", "apply_all": "Tillämpa alla", "apply_label": "Tillämpa etikett", - "apply_set_b": "Använd uppsättning B", "apply_split": "Tillämpa delning", "apply_trim": "Tillämpa beskärning", "auto_detect_split": "Autoidentifiera", "auto_label_cycles": "Automatisk märkning av cykler", - "auto_label_cycles_tip": "Tilldela automatiskt profilnamn till omärkta cykler vars matchningskonfidens klarar tröskeln", + "auto_label_cycles_tip": "Namnge omärkta cykler vars matchningskonfidens klarar tröskeln.", "cancel": "Avbryt", "clear_debug": "Rensa felsökningsdata", "clear_splits": "Rensa", @@ -112,8 +123,6 @@ "convert_to_automation": "Konvertera till automatisering", "correct": "Korrigera…", "create": "Skapa", - "create_from_cluster": "Skapa profil från {count} cykler", - "create_profile": "+ Skapa profil", "delete": "Ta bort", "delete_group": "Ta bort grupp", "delete_group_tip": "Ta bara bort denna grupp - medlemsprofilerna behålls", @@ -124,22 +133,19 @@ "done": "Klar", "edit": "Redigera", "export": "Exportera", - "export_json": "Exportera till JSON", "force_stop": "Tvångsstopp", "force_stop_tip": "Avsluta den pågående cykeln omedelbart och markera den som tvångsstopad", "from_template": "Från mall ▾", "ignore": "Ignorera", "import_json": "Importera från JSON", - "import_overwrite": "Importera (skriver över data)", "inspect": "Inspektera", "label": "Märk", - "load": "Ladda", "load_more": "Ladda fler", "manage": "Hantera", "merge": "Slå samman", "new_automation": "+ Ny automatisering", "new_group": "+ Ny grupp", - "new_group_tip": "Gruppera nästan identiska profiler (samma form/varaktighet, olika temperatur eller centrifugering) så att matcharen tillförlitligt väljer mellan dem", + "new_group_tip": "Gruppera nästan identiska profiler (samma form, annan temperatur eller centrifugering) så att matchningen kan skilja dem åt.", "new_phase": "+ Ny fas", "new_profile": "+ Ny profil", "new_profile_tip": "Skapa en ny programprofil från en befintlig märkt cykel eller inspelning", @@ -147,36 +153,25 @@ "on_cycle_started": "När cykeln startar", "pause_cycle": "Pausa", "pause_cycle_tip": "Pausa den pågående cykeln – apparaten återupptar där den slutade", - "pg_apply_device": "Tillämpa på enhet", - "pg_autofill": "Autofyll", - "pg_load_calibrated": "Ladda kalibrerade (ML) ({n})", - "pg_load_calibrated_tip": "Laddar ML-kalibrerade inställningar som testyteöverskridanden", "pg_load_live": "Ladda aktiva inställningar", "pg_load_live_tip": "Läs in integrationens aktuella inställningar på nytt och kasta alla ändringar i testytan", "pg_load_suggested": "Ladda förslag ({n})", "pg_load_suggested_tip": "Laddar auto-tunarens aktuella förslag som testyteöverskridanden", - "pg_preset_delete": "Ta bort förinställning", - "pg_preset_load": "Ladda förinställning", - "pg_preset_load_tip": "Ersätt värdena i testytan med den här förinställningen (de aktiva inställningarna påverkas inte)", - "pg_preset_save": "Spara som förinställning", - "pg_preset_save_tip": "Spara alla värden nedan som en namngiven förinställning för den här enheten", "pg_publish_all": "Spara {n} till integrationen", "pg_publish_one": "Spara {label} till integrationen", - "play": "Spela", "process": "Bearbeta", "process_history": "Bearbeta nu", "process_recording": "Spara inspelning", "process_tip": "Spara det inspelade spåret som en ny eller befintlig profil", "rebuild": "Bygg om kuvert", "rebuild_envelope": "Bygg om kuvert", - "rebuild_tip": "Beräkna om det förväntade effektkuvertet (min/max-band) för alla profiler från deras märkta cykler – kör efter märkning av nya cykler eller korrigering av gamla", + "rebuild_tip": "Bygg om varje profils förväntade effektband från dess märkta cykler. Kör efter märkning eller korrigering av cykler.", "rec_start_tip": "Börja spela in apparatens effektspår – starta strax innan du kör en cykel", "rec_stop": "Stoppa", "rec_stop_tip": "Stoppa inspelningen och behåll det registrerade spåret för granskning", "record": "Starta inspelning", "refresh": "Uppdatera", "refresh_diag_tip": "Uppdatera lagringsstatistik", - "refresh_logs_tip": "Uppdatera de senaste loggposterna", "refresh_settings_tip": "Ladda om inställningar från servern", "relabel": "Märk om ({count})", "remove": "Ta bort", @@ -184,7 +179,6 @@ "reset": "Återställ", "reset_ml_models_tip": "Släng de finjusterade modellerna och gå tillbaka till de inbyggda. WashData kan lära sig om dem senare.", "reset_to_builtin": "Återställ till inbyggda modeller", - "reset_to_defaults": "Återställ till standardvärden", "resume_cycle": "Återuppta cykel", "resume_cycle_tip": "Återuppta den pausade cykeln", "revert_settings": "Ångra ändringar", @@ -197,8 +191,6 @@ "run_analysis": "🔍 Kör förslagsanalys", "run_analysis_tip": "Analysera dina inspelade cykler nu och uppdatera inställningsförslag", "run_auto_label": "Kör automatisk märkning", - "run_both": "Kör A mot B", - "run_simulation": "Kör simulering", "save": "Spara", "save_access_control": "Spara åtkomstkontroll", "save_group": "Spara grupp", @@ -210,10 +202,8 @@ "save_settings": "Spara inställningar", "select": "Välj", "select_all": "Markera alla", - "select_last20": "Senaste 20", "select_none": "Rensa", "show_only": "Visa bara", - "skip_setup": "Hoppa över konfiguration", "split": "Dela", "stop": "Stoppa", "submit_correction": "Skicka korrigering", @@ -222,36 +212,32 @@ "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.", + "unlabel_selected_tip": "Ta bort märkningen från de valda cyklerna. De ligger kvar i din historik men formar inte längre den här profilen.", "use": "Använd", "wipe_all": "Rensa all data", + "pg_rerun": "Uppdatera simulering", "delete_selected": "Radera valda ({n})", - "group_suggest": "Grupp {n}: {members}", "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", "import": "Importera", "disconnect": "Koppla från", "connect_github": "Anslut GitHub", - "save_appliance": "Spara apparat", "share_to_store": "Dela till butiken", "share": "Dela", - "confirm_appliance": "Bekräfta den här apparaten", - "confirmed": "Bekräftad", "browse_community_setups": "Bläddra bland gemenskapsinställningar", "download_device": "Ladda ner denna inställning", "share_device": "⬆ Dela den här apparaten", - "share_device_tip": "Dela denna apparat och dess inspelade referenscykler till gemenskapsbutiken så att andra med samma maskin kan ta över dem", + "share_device_tip": "Dela den här apparaten och dess referenscykler till gemenskapsbutiken så att ägare av samma maskin kan ta över dem.", "share_n": "Dela {n} cykel/cyklar", "export_selected": "Exportera (välj data)", "export_all": "Snabbexportera allt", - "import_raw": "Avancerat: ersätt allt från JSON", "download_export": "Ladda ner export", "analyze_import": "Analysera fil", "import_selected": "Importera valda", "back": "Tillbaka", "mute_suggestion": "Sluta föreslå den här inställningen", "reset_muted": "Återställ tysta förslag", + "reset_phase_tip": "Ångra din ändring och återställ den inbyggda fasen", "import_power_history": "Importera effekthistorik", "hist_read_recorder": "Läs från Home Assistant", "hist_scan": "Sök efter cykler", @@ -260,7 +246,12 @@ "save_total_cycles": "Spara total", "clear": "Rensa", "add_brand": "Lägg till märke", - "add_appliance": "Lägg till apparat" + "add_appliance": "Lägg till apparat", + "retry": "Försök igen", + "confirm_appliance": "Bekräfta den här apparaten", + "confirmed": "Bekräftad", + "create_profile_from_gap": "Skapa profil", + "undo_import": "Ångra senaste import" }, "note": { "off_delay": { @@ -268,10 +259,6 @@ } }, "conflict": { - "anti_wrinkle_exit": { - "exit": "Måste vara under stopptröskel ({stop} W); annars ignoreras skrynkelskyddets uteffekt", - "stop": "Måste vara över skrynkelskyddets uteffekt ({exit} W)" - }, "anti_wrinkle_max": { "max": "Måste vara över starttröskel ({start} W); annars kringgås skrynkelskyddets varaktighetsgräns", "start": "Måste vara under skrynkelskyddets maxeffekt ({max} W)" @@ -309,14 +296,14 @@ "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." + "power_sensor": "Det här är apparatens Effektsensor, inte ett pris. Cykler skulle debiteras i watt per kWh; töm det här fältet.", + "energy_sensor": "Det här är apparatens Energimätare, inte ett pris. 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. Töm fältet eller välj en tariffsensor.", + "unit": "Den här sensorn mäter i {unit}, inte ett pris per kWh. Töm fältet eller välj 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" + "duration": "Måste vara under Tidsgräns utan uppdatering ({to} s) så att larmet för fastnad pump kan utlösas först", + "timeout": "Måste vara över Pumpens fastnadsvaraktighet ({ps} s) så att larmet för fastnad pump kan utlösas först" }, "start_dur": { "sampling": "Samplingsintervall överstiger startvaraktighet ({sdt} s); enstaka mätvärdestoppar kan öppna en cykel", @@ -334,10 +321,15 @@ "sampling": "Samplingsintervall bör vara högst hälften av vakthundsintervallet ({wi} s)" }, "settings_banner": "Inställningskonflikter: {n}. Kontrollera de markerade avsnitten och åtgärda dem innan du sparar.", - "settings_banner_btn": "Gå till första" + "settings_banner_btn": "Gå till första", + "end_energy": { + "energy": "För strikt för Stopptröskel ({w} W) under Avfördröjning ({d} s); cykeln kan bara avslutas via en reservväg", + "stop": "Slutenergi ({e} Wh på {d} s) tillåter bara {p} W" + } }, "hdr": { - "ab_comparison": "A/B-inställningsjämförelse", + "custom_tasks": "Dina egna uppgifter", + "maintenance_supporter": "Behöver du mer?", "access_control": "Åtkomstkontroll", "add_maintenance": "Lägg till underhållshändelse", "automation_delete": "Ta bort denna automatisering", @@ -346,28 +338,22 @@ "automation_review_this_cycle": "Granska denna cykel", "automations": "Automatiseringar", "clear_debug": "Rensa felsökningsspår", - "cycle_simulator": "Cykelsimulator", "diagnostics_pref": "Diagnostik", "display": "Visning", - "dtw_inspector": "DTW-inspektör", "export_import": "Exportera / Importera", - "getting_started": "Kom igång", "live_power": "Direkteffekt", "logs": "Loggar", - "logs_diagnostics": "Diagnostik", "maintenance": "Underhållsåtgärder", "maintenance_log": "Underhållslogg", "maintenance_reminders": "Servicepåminnelser", "manual_recording": "Manuell inspelning", - "manual_recording_tip": "Kör en cykel avsiktligt medan WashData spelar in effektspåret. Klicka på Starta inspelning strax innan apparaten startar, Stoppa när den är klar, och sedan Bearbeta för att spara det som en namngiven profil.", + "manual_recording_tip": "Spela in en cykel avsiktligt: tryck på Start strax innan apparaten startar och Stoppa när den är klar, och sedan Bearbeta för att spara den som en profil.", "ml_learned": "Vad WashData har lärt sig", - "ml_matching_tuning": "Finjustering av programmatchning", "ml_settings_card": "Inställningar", "ml_smart_learning": "Smart inlärning", "my_preferences": "Mina inställningar", "no_automations": "Inga automatiseringar refererar till denna enhet ännu.", "panel_settings": "Panelinställningar", - "param_sweep": "Parametersvep", "pg_settings_source": "Inställningarnas källa", "phase_catalog": "Faskatalog", "playground": "Testyta", @@ -376,11 +362,8 @@ "status": "Status", "status_graph": "Statusdiagram", "storage_stats": "Lagringsstatistik", - "test_history": "Testa på historik", "toggle_sidebar": "Växla Home Assistant-sidofältet", - "tools_and_data": "Verktyg och data", "wipe_history": "Rensa historik", - "recommendations": "Rekommendationer ({n})", "pg_detection_params": "Detekteringsinställningar", "pg_outcome": "Simuleringsresultat", "pg_across_cycles": "Över dina cykler", @@ -388,7 +371,8 @@ "online_account": "Gemenskapsbutik och onlinefunktioner", "import_power_history": "Importera effekthistorik", "service_status": "Servicestatus", - "online_account_tab": "Online & gemenskap" + "online_account_tab": "Online & gemenskap", + "live_match_debug": "Felsökning av livematchning" }, "health": { "fair": "Acceptabel profilkvalitet", @@ -396,7 +380,12 @@ "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.", + "task_name": "Uppgift", + "every_cycles": "Intervall (cykler)", + "every_days": "Intervall (dagar)", + "new_task_name": "Ny uppgift", + "days_since_service": "{since} / {total} dagar", + "envelope_position_tip": "Position på det matchade programmets inspelade kurva: {pct} %. Hittas genom att passa in cykeln mot profilen, så den stämmer även när en körning är längre eller kortare. Uppdateras medan apparaten är tyst, så den kan släpa efter.", "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})", @@ -408,10 +397,8 @@ "aria_cycle_chart": "Cykelns effektkurva", "aria_envelope_chart": "Diagram över profilens effektenvelopp", "aria_phase_chart": "Diagram för fasredigeraren", - "aria_playground_chart": "Cykelns effektkurva med dragbara detektionströsklar", "aria_power_chart": "Diagram över effektförbrukning", "aria_spaghetti_chart": "Överlagrade effektkurvor för cykler", - "aria_sweep_chart": "Diagram över resultat från parametersvep", "artifact_anomaly": "Anomali", "artifact_high_power": "Hög effekt", "artifact_interruption": "Avbrott", @@ -425,155 +412,105 @@ "cohesion_good": "samhörighet {pct} %", "cohesion_low": "⚠ låg samhörighet {pct} %", "compare_profiles": "Jämför med profiler", - "concurrency": "Samtidighet", "confidence": "Konfidens", "confidence_threshold": "Konfidenströskel", "correct_duration": "Korrigera varaktighet (min, valfritt)", "correct_profile": "Korrigerad profil", - "correlation": "Korrelation", "cost": "Kostnad", "create_new_profile": "+ Skapa ny profil…", "current_phase": "Aktuell fas", - "curves": "Kurvor", "cycle": "Cykel", "cycle_date_display": "Visning av cykelns datum", - "cycle_health": "Cykelns hälsa", - "cycle_index": "cykelindex →", - "cycles": "Cykler", + "cycle_context": "Recorder-historik runt cykeln", + "cycle_context_off": "Av", + "cycle_context_minutes": "{min} min", + "cycle_context_series": "Recorder-historik", + "before_start": "Före start", + "after_end": "Efter slut", "cycles_count": "Cykler", "cycles_lc": "cykler", - "dashed": "streckad", "date": "Datum", "debug_traces": "Felsökningsspår", - "default": "Standard", "default_access_level": "Standardnivå för användare som inte listas nedan", "default_other": "Standard (andra enheter)", "default_tab": "Standardflik när panelen öppnas", - "delta": "Δ", "description": "Beskrivning", - "dtw_blend_w": "Blandningsvikt", - "dtw_blended": "Blandad poäng", - "dtw_ddtw": "DDTW-poäng", - "dtw_l1": "L1-poäng", - "dtw_matched_profile": "(cykelns matchade profil)", "duration": "Varaktighet", - "duration_agreement": "Varaktighetsöverensstämmelse", - "emphasis": "Betoning", "enable_access_control": "Aktivera åtkomstkontroll per användare", "energy": "Energi", - "energy_agreement": "Energiöverensstämmelse", - "event_timeline": "Händelsetidslinje", "event_type": "Händelsetyp", "expected": "Förväntad", + "suspected_profile": "{name} (förmodat)", + "runner_up_profile": "{name} (tvåan)", "expected_duration": "Förväntad varaktighet (min)", "file_kb": "Fil (kB)", - "final_score": "Slutpoäng", "from": "Från", "gap_s": "Lucka (s)", "group_name": "Gruppnamn", "head_trim": "Beskär start (s)", - "health": "Hälsa", "hide_tabs": "Dölj flikar för icke-administratörer", - "in_use": "Används", "json_data": "JSON-data", "label": "Matchat program", "last": "Senaste", "last_checked": "Senast kontrollerad:", "load_from_file": "Läs in från fil", - "mae_score": "MAE-poäng", "manual_duration": "Manuell varaktighet (min, valfritt)", "match": "Matchning", "members": "Medlemmar", - "metric": "Mått", "mode_existing_profile": "Lägg till i befintlig profil", "mode_new_profile": "Skapa ny profil", "models_fine_tuned": "(finjusterade modeller: {count})", - "n_classic_suggestions": "{n} klassisk", - "n_ml_suggestions": "{n} ML", "n_selected": "{n} valda", "n_tuning_suggestions": "{n} justeringsförslag", - "na": "ej tillg.", + "n_tuning_suggestions_one": "{n} justeringsförslag", + "n_tuning_suggestions_other": "{n} justeringsförslag", "new_profile_name": "Nytt profilnamn", "notes": "Anteckningar", - "of": "av", - "outcome": "Resultat", "panel_default_tab": "Standardflik", "panel_language": "Panelspråk", "per_cycle_short": "cykel", - "pg_autofill_tip": "Fyll i intervall automatiskt från aktuellt värde ({lo}–{hi})", "pg_best_value": "Bästa hittade värdet", "pg_current_value": "Aktuellt värde:", "pg_drag_hint": "Dra linjen i diagrammet", - "pg_dtw": "DTW", "pg_ev_ending": "Avslutas", "pg_ev_running": "Körs", "pg_group_advanced": "Specialfall", "pg_group_detection": "Detekteringsutlösare", "pg_group_timing": "Tidsregler", "pg_idle": "Inaktiv", + "pg_poor_match": "Dålig matchning", "pg_preset": "Förinställning för testytan", "pg_preset_name": "Namn på ny förinställning", "pg_preset_none": "Välj en förinställning…", - "pg_stress_group": "Test av inaktiv avslutning", - "pg_stress_idle_auto": "Auto", - "pg_stress_idle_w": "Inaktiv nivå (W)", - "pg_stress_idle_w_desc": "Åsidosätt den automatiskt identifierade standby-golvnivån (lämna tomt för automatisk).", - "pg_stress_toggle": "Testa inaktiv avslutning", - "pg_stress_toggle_desc": "Simulerar att apparaten stannar kvar vid sin standby-förbrukning efter att inspelningen slutar - visar om och när WashData stoppar cykeln.", - "pg_last_run": "Senaste körning: {date}", - "pg_match_score": "Matchningspoäng", - "pg_pct_of": "{p}% av {total}", - "pg_poor_match": "Dålig matchning", "pg_strong_match": "Stark matchning", - "pg_tile_ambiguous": "Tvetydig", - "pg_tile_ambiguous_tip": "Cykler med nästan oavgjort mellan två program", - "pg_tile_detected": "Upptäckt", - "pg_tile_detected_tip": "Cykler där effekten korsade startröskeln tillräckligt länge för att registreras som igång", - "pg_tile_matched": "Matchad", - "pg_tile_matched_tip": "Cykler som korrekt tillskrivits ett känt program", - "pg_time_left": "Tid kvar", "pg_weak_match": "Svag matchning", "phase_name": "Fasnamn", "name": "Namn", "phase_timeline": "Fastidslinje", "power": "Effekt", "profile": "Profil", - "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. Välj ett innan du startar apparaten, så tillämpas det så snart nästa cykel börjar.", + "program_tip": "Välj vilket program den aktuella cykeln matchas mot. Autoidentifiera väljer bästa matchning. Fäst ett program när autoidentifieringen har fel; väljs det före start gäller det nästa cykel.", "progress": "Förlopp", "raw_socket": "Råsocket", - "read_only": "Skrivskyddad", "ref_cycle": "Referenscykel (valfritt)", - "refresh_interval": "Uppdateringsintervall (s)", "remaining": "Återstående", "reminder_every": "Påminn var (cykler)", "remove_label": "- Ta bort etikett -", "rename_profile": "Byt namn på profil", - "replay_duration": "Uppspelning", "resulting_profile": "Resulterande profil", "save_mode": "Sparläge", - "score": "Poäng", - "score_breakdown": "Poänguppdelning", "seconds_from_start": "Sekunder från start", "select_profile": "Välj profil", "setting": "Inställning", - "settings_a": "Inställningar A (aktuella)", - "settings_a_short": "A", "settings_advanced": "Avancerat", - "settings_b": "Inställningar B (test)", - "settings_b_short": "B", "settings_basic": "Grundläggande", "settings_detail_level": "Detaljnivå för inställningar", - "settings_overrides": "Åsidosatta inställningar", "show_debug": "Visa livefelsökningskort på statussidan (konfidens, tvetydighet, toppkandidater)", "show_expected": "Visa förväntat kurvöverlägg (matchad profil, orange)", "show_raw": "Visa växlare för råa uttag-värden i live-effektdiagram", "sparkline": "Trend för senaste cykellängder", - "stage2": "Steg 2 – kärnlikhet", - "stage3": "Steg 3 – DTW", - "stage4": "Steg 4 – överensstämmelse", "status": "Status", "tail_trim": "Beskär slut (s)", "tap_to_dismiss": "tryck på diagrammet för att stänga", @@ -584,7 +521,6 @@ "ungrouped": "Ej grupperad", "unlabelled": "Omärkt", "unlabelled_paren": "(omärkt)", - "warp_path": "Warp-väg", "input": "Indata:", "start": "Start", "end": "Slut", @@ -609,9 +545,8 @@ "n_shown": ", {n} visas", "cycles_title": "Cykler ({n})", "pg_detecting": "Upptäcker", - "pg_best_marker": "Bästa: {v}", "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", + "pg_time_left_model_tip": "Beräknad utifrån programmets faser, inte en fast nedräkning", "phase": "Fas", "pg_mode_history": "Testa på historik", "pg_ev_detected": "Upptäckt", @@ -651,12 +586,8 @@ "pg_mode_optimize": "Optimera", "pg_group_matching": "Programmatchning", "pg_zoom_hint": "dubbelklicka för att återställa zoomen", - "aria_playground_chart2": "Interaktiv effektgraf för cykeln: hovra för att avläsa tid/effekt, skrolla för att zooma, dra för att panorera", "pg_alert_timeout_end": "Avslutad genom timeout, inte genom förutsägelse", "pg_alert_indefinite": "Skulle köra i all oändlighet", - "pg_alert_stress_cap": "Säkerhetsgräns nådd", - "pg_alert_stress_ok": "Inaktiv avslutning: cykel stoppad", - "pg_alert_stress_warn": "Standby-förbrukning över stopptröskel", "task_pg_history": "Testa på historik", "task_pg_sweep": "Optimera", "task_reprocess": "Bearbetar igen", @@ -666,16 +597,9 @@ "pg_recent_runs": "Senaste körningar", "pg_load_run": "Ladda om den här körningen", "pg_match_confidence": "Matchningssäkerhet", - "pg_envelope_fit": "Anpassning till envelopp", - "pg_envelope_fit_tip": "Hur väl cykeln håller sig inom den här profilens sparade effektenvelopp, ett annat perspektiv än matchningssäkerhet.", "n_imported_note": ", {n} importerade", "enable_online": "Aktivera onlinefunktioner", - "store_brand": "Apparatens märke", - "store_model": "Apparatens modell", "anonymous": "Anonym", - "rate_quality": "Betygsätt kvalitet", - "rate_n": "{n} stjärnor", - "approved_only": "Endast godkända", "adopt_settings": "Ta även över inställningar", "flags": "Flaggor", "include_phase_map": "Inkludera fasekarta", @@ -692,7 +616,6 @@ "cat_custom_phases": "Anpassade faser", "cat_profile_groups": "Profilgrupper", "cat_settings": "Detekterings- & matchningsinställningar", - "cat_matching_config": "Finjustering av matchning", "cat_ml_models": "ML-modeller", "cat_feedback": "Feedback & granskningsetiketter", "cat_suggestions": "Förslag", @@ -715,7 +638,6 @@ "hist_csv_data": "CSV-data", "hist_from_recorder": "Eller läs den från Home Assistant", "hist_since": "Sedan", - "days": "dagar", "hist_keep": "Behåll den här cykeln", "hist_looks_complete": "komplett", "peak_power_short": "Topp", @@ -735,7 +657,34 @@ "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" + "correct_total_cycles": "Korrigera totalen", + "hist_reason_already_recorded": "överlappar en cykel som WashData redan har registrerat", + "share_consent": "Jag bekräftar att dessa cykler slutfördes normalt utan avbrott", + "show_internal": "Visa interna inställningar", + "show_internal_tip": "Inställningar som WashData justerar åt dig (sampling, vakthund, matchningsfrekvens, trösklar). Visas när den här enheten har ett eget värde, en konflikt eller ett förslag.", + "rate_n": "{n} stjärnor", + "rate_quality": "Betygsätt kvalitet", + "pg_autolabel": "Automärkning", + "pg_label_review": "Ber om granskning", + "pg_ev_match_reverted": "Matchning återkallad", + "pg_ev_verified_pause": "Bekräftad paus", + "n_samples": "{n} mätvärden", + "n_samples_one": "{n} mätvärde", + "n_samples_other": "{n} mätvärden", + "n_cycles": "{n} cykler", + "n_cycles_one": "{n} cykel", + "n_cycles_other": "{n} cykler", + "energy_per_cycle": "{v}/cykel", + "energy_total": "{v} totalt", + "automation_off": "(av)", + "ref_cycle_none": "Ingen", + "share_name": "Visa mitt namn på det jag delar", + "pg_compare_profile": "Jämför med profil", + "pg_obj_endlag": "Slutfördröjning (median)", + "pg_obj_early": "Andel för tidiga slut", + "pg_obj_split": "Uppdelningsandel", + "pg_sweep_cycles": "Senaste cykler", + "cat_backfill_cycles": "Cykler hittade i importerad effekthistorik" }, "log": { "all_levels": "Alla nivåer", @@ -745,11 +694,9 @@ }, "ml": { "builtin_models": "● Använder inbyggda modeller", - "fit_fair": "Acceptabel", "fit_good": "Bra", "fit_slight": "Liten", "fit_strong": "Stark", - "fit_weak": "Svag", "fit_word": "passform", "personalized": "● Anpassad till denna maskin", "better_than_baseline": "{pct}% bättre än basuppskattningen ({metric})", @@ -757,30 +704,39 @@ "trend_declining_tip": "Den här modellens passning har försämrats vid de senaste omkontrollerna. Att granska fler cykler kan hjälpa den att lära om.", "trend_steady_tip": "Den här modellens passning har hållit sig ungefär stabil vid de senaste omkontrollerna.", "cap_blurb": { - "end": "Veta när en cykel verkligen är klar", - "live_match": "Identifiera det pågående programmet snabbare", - "quality": "Upptäcka feldetekterade eller skadade cykler", - "remaining_time": "Förutsäga hur lång tid som är kvar", "total_energy": "Förutsäga total energi och kostnad" }, "cap_label": { - "end": "Detektering av cykelslut", - "live_match": "Programmatchning", - "quality": "Kvalitetskontroll av cykler", - "remaining_time": "Uppskattning av återstående tid", "total_energy": "Energiuppskattning" }, - "metric_auc": "AUC {auc} på valideringsdata", - "metric_mae": "fel {model} mot baslinje {baseline}" + "metric_mae": "fel {model} mot baslinje {baseline}", + "fine_tuned_at": "finjusterad {when}", + "never": "aldrig", + "trained_unknown": "vid en okänd tidpunkt", + "held_out_count": "mätt på {count} undanhållna valideringscykler", + "held_out_count_one": "mätt på {count} undanhållen valideringscykel", + "held_out_count_other": "mätt på {count} undanhållna valideringscykler", + "last_run_not_promoted": "Senaste kontroll ({when}): {reason}", + "skip_reason": { + "insufficient_rows": "Inte tillräckligt med användbara cykler än: {count} märkta, rena cykler hittills.", + "insufficient_rows_one": "Inte tillräckligt med användbara cykler än: {count} märkt, ren cykel hittills.", + "insufficient_rows_other": "Inte tillräckligt med användbara cykler än: {count} märkta, rena cykler hittills.", + "fit_failed": "Träningen misslyckades: {error}", + "unknown": "Inget nytt lärdes in.", + "holdout_too_small": "För få cykler för att testa en ny modell rättvist: {held_out} kunde hållas undan för test, {min} behövs.", + "not_better_than_naive": "Den nya modellen var inte tydligt bättre än den enkla uppskattningen (fel {model} mot {naive}).", + "not_better_than_incumbent": "Den nya modellen var inte bättre än den som används (fel {model} mot {incumbent}), så den nuvarande behölls." + } }, "modal": { + "remove_task_title": "Ta bort uppgift", + "remove_task_msg": "Ta bort påminnelsen \"{task}\"? Dess poster i underhållsloggen behålls.", "auto_label": "Automatisk märkning av cykler", "correct_feedback": "Korrigera feedback", "create_profile": "Skapa profil", "cycle": "Cykel", "edit_group": "Redigera profilgrupp", "edit_phase": "Redigera fas", - "import_config": "Importera konfiguration", "label_cycle": "Märk cykel", "merge_cycles": "Slå samman {n} cykler", "new_group": "Ny profilgrupp", @@ -795,6 +751,8 @@ "delete_automation_title": "Radera automation", "delete_automation_msg": "Radera automationen \"{name}\" från Home Assistant? Detta kan inte ångras.", "remove_legacy_title": "Ta bort äldre åtgärder", + "reset_phase_msg": "Ångra din ändring av \"{name}\" och återställ den inbyggda fasen? Program behåller sina intervall.", + "reset_phase_title": "Återställ fas", "remove_legacy_msg": "Ta bort de äldre anpassade åtgärderna? De slutar att utlösas vid cykelhändelser. Detta kan inte ångras från panelen.", "discard_recording_title": "Kasta inspelning", "discard_recording_msg": "Kasta den sparade inspelningen? Detta kan inte ångras.", @@ -815,63 +773,77 @@ "store_share_device": "Dela den här apparaten", "export_select": "Export - välj data", "import_wizard": "Import - välj data", - "history_import": "Importera effekthistorik" + "history_import": "Importera effekthistorik", + "apply_suggestions_title": "Tillämpa förslag", + "apply_suggestions_msg": "Ändra dessa {n} inställningar? Integrationen laddas om efter att du har sparat.", + "discard_settings_title": "Kasta osparade ändringar?", + "discard_settings_msg": "Osparade inställningsändringar: {n}. Lämna Inställningar och kasta dem?", + "profile_title": "Profil · {name}", + "undo_import_msg": "Enhetens data och inställningar återställs till hur de var före den senaste importen ({when}). Cykler och ändringar sedan dess går förlorade.", + "undo_import_ok": "Ångra import", + "undo_import_title": "Ångra senaste import" }, "msg": { + "custom_task_limit": "Du har nått gränsen på {max} uppgifter.", + "custom_tasks_intro": "Lägg till allt annat du vill bli påmind om. En uppgift behöver göras när något av intervallen nås; lämna det ena tomt eller 0 för att bara använda det andra.", + "maintenance_due_sensor_hint": "Den binära sensorn Dags för underhåll är på så länge någon uppgift behöver göras, för dina egna aviseringsautomationer.", + "maintenance_supporter_intro": "Maintenance Supporter är en separat integration som kan mycket mer: valfri apparat eller uppgift, intervall efter tid, användning eller en sensor, meddelanden och en historik. Den kan använda WashDatas cykelräknare.", "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.", + "pg_preset_delete_confirm": "Ta bort förinställningen \"{name}\"?", + "pg_preset_limit": "Gränsen för förinställningar är nådd ({n})", + "pg_preset_overwrite": "Skriva över förinställningen \"{name}\"?", "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.", - "refresh_catalog_hint": "Gemenskapens listor över märken och apparater cachelagras för att gemenskapsbutiken ska hålla sig inom sin dagliga gräns. Uppdatera för att hämta poster som andra har lagt till eller godkänt.", + "store_sibling_hint": "Inget delat för din modell? En närbesläktad modell från samma märke är oftast en bra start.", + "store_declare_appliance": "Berätta för WashData vilken apparat du har för att se uppsättningar som andra har delat för den. Eller skriv ett märke ovan för att se dig omkring.", + "refresh_catalog_hint": "Gemenskapens listor över märken och apparater cachelagras för att spara butikens dagliga budget. Uppdatera för att se poster som andra har lagt till eller godkänt.", "head_trim_hint": "Ta bort så här många sekunder från början", "appliance_monitor": "Apparatövervakning", "artifact_dip_detail": "Föll under det normala effektbandet i ~{n} s.", "artifact_footer": "Markerad i grafen ovan. Dessa är övergående artefakter (t.ex. dörren öppnas mitt i cykeln), inte nödvändigtvis problem.", "artifact_header": "{n} anomalier upptäcktes under denna cykel", - "artifact_pause_detail": "Effekten föll till nära noll i ~{n} s och återupptogs sedan – troligtvis öppnades dörren mitt i cykeln eller cykeln pausades.", + "artifact_header_one": "{n} anomali upptäcktes under denna cykel", + "artifact_header_other": "{n} anomalier upptäcktes under denna cykel", + "artifact_pause_detail": "Effekten föll till nära noll i ~{n}s och kom sedan tillbaka. Dörren öppnades troligen eller så pausades cykeln.", "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 ö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.", + "cleanup_intro": "Alla märkta cykler överlagrade. Kryssa i avvikare och ta sedan bort deras märkning (de ligger kvar i historiken men formar inte längre den här profilen) eller radera dem.", "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)", - "compare_profiles_tip": "Lägg över andra profilkuvert på diagrammet ovan för att se vilket som passar bäst för denna cykel.", + "compare_profiles_tip": "Lägg andra profiler över diagrammet för att se vilken som passar bäst till den här cykeln.", "compare_selected_cycles": "Valda cykler (solid) – visa / dölj", - "consider_new_profile": "Överväg att skapa en ny profil.", - "coverage_gap": "de senaste cyklerna har ingen matchande profil ({pct}% av de senaste 30).", - "coverage_gap_similar_cycles": "{count} liknande omärkta cykler hittades – skapa en profil för att börja matcha dem.", "cycles_deleted": "{count} cykler borttagna", + "cycle_context_loading": "Läser in recorder-historik…", + "cycle_context_note": "Grått: effektsensorns recorder-historik runt den här cykeln. Den är inte en del av cykeln och påverkar inte längd, energi eller matchning.", + "cycle_context_none": "Recordern har inga mätvärden runt den här cykeln (Home Assistant sparar 10 dagars historik som standard).", + "cycle_context_tip": "Endast visning: visar effektsensorns recorder-historik runt den här cykeln, om den fortfarande finns kvar. För att lägga till misslyckade startförsök i framtida cykler, använd Kurvupptakt (Inställningar > Detektering > Cykelstart), som ändrar sparade längder.", "enough_data": "Tillräckligt med data för att lära sig av ({current}/{min} cykler).", "export_description": "Välj exakt vilka profiler, cykler, inställningar och mer som ska exporteras till JSON, eller analysera en fil och importera bara de delar du vill ha.", "feedback_cycles_pending": "Att granska: {n}", "feedback_prompt": "Bekräfta att det var rätt, korrigera programmet eller ignorera.", "feedback_relabel_hint": "Att märka om den här cykeln löser också granskningen.", "filter_by_profile": "Filtrera efter profil…", - "group_modal_help": "Gruppera program med samma form som skiljer sig i temperatur/centrifugering (längderna kan variera). Matchning ger gruppen poäng som en kandidat och väljer sedan den medlem som passar bäst. Välj minst 2; överlägget visar hur lika de är.", + "group_modal_help": "Gruppera program med samma form som skiljer sig i temperatur eller centrifugering. Matchningen poängsätter gruppen som en och väljer sedan den medlem som passar bäst. Välj minst 2; överlagringen visar hur lika de är.", "group_not_cohesive": "Dessa profiler är inte tillräckligt lika för att gruppera på ett tillförlitligt sätt, så matchning behandlar dem individuellt tills du tar bort avvikelsen eller delar upp gruppen.", "group_preview_hint": "Markera 2 eller fler medlemmar för att förhandsgranska och jämföra deras effektkurvor.", - "import_intro": "Läs in en exporterad fil eller klistra in en JSON-nyttolast nedan.", - "legacy_actions_title": "{count} äldre anpassad åtgärd körs fortfarande", + "legacy_actions_title": "{count} äldre anpassade åtgärder körs fortfarande", + "legacy_actions_title_one": "{count} äldre anpassad åtgärd körs fortfarande", + "legacy_actions_title_other": "{count} äldre anpassade åtgärder körs fortfarande", "live_chart_loading": "Direkteffektdiagrammet visas när avläsningarna kommer in.", "loading": "Laddar…", "loading_curve": "Laddar kurva…", "loading_settings": "Laddar inställningar…", "log_buffer_hint": "Nyaste först · buffrar de senaste 500 ha_washdataposterna sedan omstart · dra den nedre kanten för att ändra storlek.", - "maintenance_advisory": "Ökande varaktighet/energi kan indikera att apparaten behöver underhållas (t.ex. avkalkning, filterrengöring).", "maintenance_due": "Underhåll att göra: {items}", - "maintenance_intro": "Logga underhåll du utför på den här apparaten och få en påminnelse när varje uppgift behöver göras igen.", + "maintenance_intro": "Logga service på den här apparaten och få en påminnelse när varje uppgift ska göras igen.", "maintenance_load_error": "Kunde inte ladda underhållsdata: {error}", "maintenance_requires_access": "Underhåll och export/import kräver fullständig åtkomst.", - "manual_duration_hint": "Profilens genomsnittliga/förväntade cykellängd, som används för återstående uppskattningar. Redigera för att ställa in den; om den lämnas oförändrad behålls det aktuella värdet.", - "matching_tuning_intro": "Under inlärning justerar WashData också hur mycket programmatchning väger form mot varaktighet och energi.", + "manual_duration_hint": "Profilens förväntade cykellängd, som används för uppskattning av återstående tid. Lämna oförändrad för att behålla det aktuella värdet.", "merge_intro": "De valda cyklerna kombineras till en (kronologisk ordning; luckor fylls med 0 W). Välj den resulterande profilen.", "ml_intro": "WashData levereras med smarta modeller som fungerar direkt.", "ml_learned_intro": "Modeller finjusterade till denna maskin.", - "ml_loading": "laddar ML…", "ml_settings_intro": "Två oberoende switchar: en tillämpar modellerna medan en cykel körs, den andra låter WashData finjustera dem till din maskin över tid.", - "name_first_program": "Du har tillräckligt med cykler – namnge ditt första program för att börja matcha.", - "near_duplicate_cluster": "nästan duplicerat profilkluster upptäcktes. Gruppering gör att matchning på ett tillförlitligt sätt kan välja mellan look-alikes (t.ex. samma program vid olika temperaturer/centrifugering).", "no_cycles_match": "Inga cykler matchar det aktuella filtret.", "no_cycles_profile": "Inga cykler för denna profil.", "no_cycles_selected": "Välj minst en cykel först.", @@ -896,73 +868,52 @@ "no_suggestions": "Inga aktiva förslag.", "notify_services_hint": "Använd {entity}-tjänst-ID:n (kommaavgränsade för flera). Mallvariabler: {vars}.", "old_actions_warning": "Konfigurerad med den gamla åtgärdsredigeraren (nu borttagen). De utlöses fortfarande vid cykelhändelser men kan inte längre redigeras här. Konvertera dem till en normal automatisering, eller ta bort dem.", - "onboarding_progress": "{n} / 3 cykler observerade", - "onboarding_watching": "Använd apparaten som vanligt – WashData observerar. Efter 3 cykler börjar programmatchningen.", "pending_feedback": "Väntande detektionsfeedback", "performance_trend": "Prestandatrend ({n} cykler)", - "pg_analysis_empty": "Ladda en cykel för att se matchningsanalys.", "pg_apply_confirm": "Tillämpa bästa värdet: {label} = {value}?", - "pg_canvas_empty": "Välj en cykel ovan och tryck sedan på Ladda för att se dess effektkurva.", - "pg_chart_caption": "Detekterings-/matchningsgrad över värden på {param}", "pg_ctrl_intro": "Värdena utgår från den här enhetens aktiva inställningar i integrationen. Ändringar stannar i testytan tills du sparar dem till integrationen.", "pg_live_unavailable": "Aktiva inställningar är inte tillgängliga, visar standardvärden.", "pg_load_live_confirm": "Kasta {n} ändring(ar) i testytan och läsa in integrationens aktuella inställningar på nytt?", - "pg_match_rate": "{pct}% matchningsgrad på {total} cykler", "pg_matches_live": "Stämmer med de aktiva inställningarna", "pg_n_changed": "{n} ändrade jämfört med de aktiva inställningarna", "pg_sugg_none": "Alla förslag matchar redan de aktiva inställningarna", - "pg_preset_delete_confirm": "Ta bort förinställningen \"{name}\"?", - "pg_preset_limit": "Gränsen för förinställningar är nådd ({n})", - "pg_preset_overwrite": "Skriva över förinställningen \"{name}\"?", "pg_publish_one_confirm": "Spara {label} = {value} till den här enhetens inställningar?", "pg_restart_note": "Starta om Home Assistant för att aktivera simuleringsverktygen.", - "pg_sim_progress": "{done} / {total} cykler", - "pg_starting": "Startar…", - "pg_stress_synthetic": "inaktiv simulering börjar här", - "pg_sweep_intro": "Tänk om {param} vore annorlunda? Testa {steps} värden över dina senaste {cycles} cykler för att hitta inställningen där flest cykler matchas korrekt.", - "pg_sweep_step": "Steg {done} / {total}", - "pg_verdict_bad": "Behöver åtgärdas: många cykler förblir oupptäckta.", - "pg_verdict_good": "Väl inställt: de flesta cykler identifieras och matchas korrekt.", - "pg_verdict_ok": "Godtagbart: några cykler missades. Prova att sänka startröskeln.", "phase_catalog_intro": "Namngivna segment av en cykel (Förhandstvätt, Uppvärmning, Centrifugering…). Tilldela dem till en profil från dess kontrollpanel.", "phase_ranges_intro": "Fasintervall (minuter från cykelstart) överlagda på genomsnittskurvan. Redigera värden för förhandsvisning live.", - "playground_intro": "En säker testyta för att spela upp tidigare cykler med olika inställningar, jämföra två konfigurationer sida vid sida och granska hur en cykel matchar en profil. Inget här ändrar dina aktiva data förrän du uttryckligen tillämpar det.", - "playground_needs_restart": "Starta om Home Assistant för att aktivera Testyta-verktygen.", - "preferences_admin": "Inställningar, panel och åtkomstkontroll", - "preferences_adv": "Inställningar", + "playground_intro": "Prova inställningar på dina riktiga cykeldata. Inget här ändrar dina aktiva inställningar förrän du tillämpar det.", "prefs_personal": "Dessa gäller endast ditt Home Assistant-konto.", - "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.", + "process_history_hint": "Kör matchningen igen på alla sparade cykler, uppdatera förslag, träna om ML-modeller (om aktiverat), räkna om kostnader utifrån registrerade priser och beräkna om cykelhälsan. Kör 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.", - "rbac_hint": "När det är av har varje Home Assistant-användare fullständig åtkomst (standard). Administratörer har alltid fullständig åtkomst och kan hantera alla.", - "recent_logs": "Senaste ha_washdata-poster", + "rbac_hint": "Av: alla Home Assistant-användare har full åtkomst (standard). Administratörer har alltid full åtkomst och kan hantera alla.", "recording_in_progress": "Inspelning pågår", "reminders_intro": "Visa en påminnelse i panelen så här många cykler efter senaste servicen. Lämna tomt eller 0 för att stänga av en påminnelse.", "restart_gap_footer": "Markerat i diagrammet. Effektdata saknas för dessa intervall; matchning använde bara faktiska avläsningar.", - "restart_gap_header": "{n} HA-omstartslucka under denna cykel", + "restart_gap_header": "{n} HA-omstartsluckor under denna cykel", + "restart_gap_header_one": "{n} HA-omstartslucka under denna cykel", + "restart_gap_header_other": "{n} HA-omstartsluckor under denna cykel", "restart_gap_item": "{dur} lucka", - "review_confirm_help": "Bekräfta om denna cykel identifierades korrekt. Dina granskningar tränar modellen på din maskin - ju fler cykler du bekräftar, desto bättre matchning och hälsopoäng får du. En snabb bra/dålig räcker.", + "review_confirm_help": "Identifierades den här cykeln korrekt? Bekräftade cykler förbättrar matchningen för din maskin. Ett snabbt Bra/Dålig räcker.", "review_in_settings": "Granska i Inställningar", "review_notes_placeholder": "Anteckningar (valfritt)", "review_notes_tip": "Fritextanteckningar för din egen referens. Används inte av matchning eller träning.", - "review_profile_tip": "Programmet som denna cykel är märkt som. Om det automatiskt identifierade programmet var fel, korrigera det här - märkning lär ut matchning för framtida cykler.", - "review_quality_tip": "Hur ren den här cykeln är. Bra = ett läroboksexempel på detta program; Dåligt = identifierat men bullrigt eller atypiskt; Oanvändbart = felidentifierat (sammanslagna, trunkerade eller falska). Driver hälsopoängen och vilka cykler som tillåts träna modellen.", - "review_recorded_tip": "Markera detta som en handplockad referenscykel för sitt program - samma roll som en manuellt inspelad cykel. Referenscykler hålls alltid, såddar den matchande mallen och släpps aldrig vid rengöring. (Detta är den \"gyllene\"/inspelade flaggan; båda är samma sak.)", - "review_tags_tip": "Valfria flaggor som beskriver vad som gick fel med den här cykeln, så träning och städning kan förklara det.", + "review_profile_tip": "Programmet som den här cykeln är märkt som. Om det är fel, korrigera det här; märkningar lär matchningen.", + "review_quality_tip": "Hur ren den här cykeln är. Bra: ett läroboksexempel på det här programmet. Dålig: identifierad, men brusig eller ovanlig. Oanvändbar: felidentifierad (sammanslagen, avkortad eller falsk). Påverkar hälsopoängen och vilka cykler som tränar modellen.", + "review_recorded_tip": "Markera detta som en handplockad referenscykel för sitt program, som en manuellt inspelad cykel. Referenscykler behålls alltid, ligger till grund för matchningsmallen och tas aldrig bort vid städning.", + "review_tags_tip": "Valfria flaggor för vad som gick fel, så att träning och städning kan ta hänsyn till det.", "review_to_cycles": "Öppna granskningskön för Cykler", "samples_decimated": "Visar {shown} av {total} sampel (gallrat för visning; toppar behålls). En bred lucka här är gallring, inte saknade data.", - "saving_triggers_reload": "Sparande utlöser en integration-omladdning. HA-enheter kan kortvarigt visas som otillgängliga.", + "saving_triggers_reload": "När du sparar laddas integrationen om. Entiteter kan kortvarigt visas som otillgängliga.", "search_placeholder": "Sök inställningar…", "see_recorder": "Se inspelningswidgeten nedan", "setting_changed": "Ändrad från {old} till {new} den {date}", "settings_basic_note": "Visar väsentliga inställningar. Byt till Avancerat för hela listan.", - "shape_drift_advisory": "⚠ Form avviker", - "shape_drift_detail": "Effektmönstret för den här profilen har förändrats över tid – möjligt apparatslitage eller underhåll behövs (t.ex. avkalkning, filterrengöring).", "show_all_settings": "Visa alla inställningar", - "showing_suggestions": "Visar {count} inställning med förslag.", - "split_intro": "Klicka på diagrammet för att lägga till eller ta bort en delningspunkt, eller autoidentifiera via tomma luckor. Varje resulterande segment kan få sin egen profil.", - "storage_diagnostics": "Lagringsstatistik, underhåll, export/import", + "showing_suggestions": "Visar {count} inställningar med förslag.", + "showing_suggestions_one": "Visar {count} inställning med förslag.", + "showing_suggestions_other": "Visar {count} inställningar med förslag.", + "split_intro": "Klicka på diagrammet för att lägga till eller ta bort en delningspunkt, eller autoidentifiera via tomgångsluckor. Varje del kan få sin egen profil.", "sug_staged": "Satt {key} = {val}. Spara för att tillämpa.", "toast_auto_detect_enabled": "Automatisk igenkänning aktiverad", "toast_auto_label_complete": "Automatisk märkning klar", @@ -979,7 +930,6 @@ "toast_no_automation": "Kan inte skapa automatisering här", "toast_profile_name_required": "Profilnamn krävs", "toast_program_set": "Program valt: {program}", - "toast_rebuild_failed": "Ombyggnad misslyckades: {error}", "toast_resume_failed": "Återupptagning misslyckades: {error}", "toast_revert_failed": "Återställning misslyckades: {error}", "toast_reverted": "{key} återställd", @@ -990,100 +940,83 @@ "trend_energy_up": "Energitrend uppåt ({pct}%/cykel) – senaste snitt {avg}", "trim_destructive_confirm": "Endast {pct}% av cykeln behålls. Detta kan inte ångras. Fortsätta?", "trim_intro": "Dra de röda handtagen eller ange värden. Allt utanför fönstret tas bort.", - "tuning_suggestions_available": "{count} inställningsförslag tillgängligt från observerade cykler. De visas bredvid de relevanta fälten.", + "tuning_suggestions_available": "{count} inställningsförslag tillgängliga från observerade cykler. De visas bredvid de relevanta fälten.", + "tuning_suggestions_available_one": "{count} inställningsförslag tillgängligt från observerade cykler. Det visas bredvid det relevanta fältet.", + "tuning_suggestions_available_other": "{count} inställningsförslag tillgängliga från observerade cykler. De visas bredvid de relevanta fälten.", "unsure_detected_prefix": "WashData är osäker på att den identifierade", - "updated": "Uppdaterad", "warmup_badge": "Lär sig fortfarande ({done}/{needed} cykler)", - "warmup_detail": "Den här profilen behöver {needed} märkta cykler innan automatisk matchning börjar. Varje bekräftad cykel hjälper den att lära sig.", + "warmup_detail": "Matchningen fungerar redan. WashData ber dig bekräfta de första {needed} cyklerna för den här profilen innan den märker på egen hand.", "wipe_history_warning": "Ta bort alla cykler och profiler permanent. Kan inte ångras.", "compare_overlay_tip": "Lägg inlärda profilenvelopper ovanpå varandra för att se vilket program varje cykel liknar.", - "duration_trend_up_tip": "Varaktighet ökar ({pct}%/cykel)", - "duration_trend_down_tip": "Varaktighet minskar ({pct}%/cykel)", - "energy_trend_up_tip": "Energi ökar ({pct}%/cykel)", - "energy_trend_down_tip": "Energi minskar ({pct}%/cykel)", - "performance_trending": "Prestandatrend", - "pg_select_cycle_hint": "Välj en cykel och klicka på ↺ för att läsa in", - "pg_sweep_progress": "Steg {done} / {total}", - "pg_analysis_hint": "Välj en cykel och klicka på ↺ för att läsa in analysen.", "diagnostics_load_failed": "Kunde inte läsa in diagnostik: {error}", - "manual_duration_ref_hint": "Används endast när ingen referenscykel är vald. En referenscykel anger varaktigheten utifrån sin egen längd.", + "manual_duration_ref_hint": "Används bara utan referenscykel; en referenscykel anger längden utifrån sin egen längd.", + "duration_learned_hint": "Beräknad från programmets cykler (avvikare borträknade) och uppdaterad efter varje ny cykel. Vill du ändra den kan du beskära eller ta bort cyklerna som avviker.", "toast_convert_rolled_back": "Konverteringen misslyckades och återställdes (ingen automatisering lämnades kvar): {error}", "toast_convert_orphan": "Automatiseringen skapades, men rensningen av de gamla åtgärderna misslyckades. Försök inte igen: ta bort de gamla åtgärderna manuellt för att undvika en dubblerad automatisering.", - "advisory_duration_trend_up": "Cyklerna för '{name}' blir gradvis längre (cirka +{pct}% per cykel). Om apparatens beteende har förändrats, spela in eller bygg om profilen på nytt.", - "advisory_energy_trend_up": "'{name}' drar gradvis mer energi (cirka +{pct}% per cykel) - värt att kontrollera apparaten om det är oväntat.", - "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.", + "advisory_duration_trend_up": "Cyklerna för '{name}' blir allt längre (cirka +{pct}% per cykel). Om apparaten har förändrats, spela in eller bygg om profilen.", + "advisory_energy_trend_up": "'{name}' använder mer energi för varje cykel (cirka +{pct}% per cykel). Kontrollera apparaten om det är oväntat.", + "advisory_poor_health": "'{name}' passar dåligt: dess senaste cykler varierar mycket eller matchar svagt. Granska dess cykler eller spela in den på nytt så att matchning och uppskattningar förblir korrekta.", + "advisory_shape_drift": "'{name}' har glidit bort från sin ursprungliga effektkurva, kanske på grund av kalk eller slitage. Överväg att spela in den på nytt med nya cykler.", + "advisory_shape_drift_corr": "'{name}' har glidit bort från sin ursprungliga effektkurva (korrelation {corr}), kanske på grund av kalk eller slitage. Överväg att spela in den på nytt med nya cykler.", + "advisory_unmatchable": "'{name}' kan aldrig matchas: det finns ingen cykel med effektdata bakom den. Märk eller spela in en cykel av det här programmet för att åtgärda det.", + "advisory_unmatchable_no_duration": "'{name}' kan aldrig matchas: dess cykel har ingen användbar längd. Spela in programmet på nytt eller ange dess förväntade längd.", "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_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.", "pg_sweep_intro2": "Hitta inställningen som bäst uppfyller ett mål över dina senaste cykler. Ingenting ändras förrän du tillämpar det.", - "pg_running_history": "Spelar upp dina cykler igen…", - "pg_running_sweep": "Testar värden över dina cykler…", "pg_row_load_hint": "Ladda den här cykeln i grafen ovanför", - "pg_history_intro2": "Spela upp dina senaste cykler genom den riktiga detektorn och matchningen med inställningarna ovan. Klicka på en rad för att ladda den cykeln i grafen; ändra en inställning för att se en före/efter-jämförelse.", + "pg_history_intro2": "Spela upp senaste cykler genom den riktiga detektorn och matcharen med inställningarna ovan. Klicka på en rad för att läsa in den i grafen; ändra en inställning för att jämföra före och efter.", "pg_simulating": "Simulerar cykel…", - "pg_canvas_empty2": "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 för att zooma och dra för att panorera.", "pg_apply_settings_confirm": "Spara dessa {n} inställning(ar) till den här enheten? {list}", - "pg_stress_terminated_detail": "Stabiliserades på ~{idle}W inaktiv -- cykeln avslutades {h} h {m} min senare via {reason}.", - "pg_stress_above_threshold_detail": "Standby-förbrukningen på ~{idle}W ligger på eller över den effektiva stopptröskeln ({stop}W) -- cykeln registrerades aldrig som inaktiv. Höj stop_threshold_w för att åtgärda detta.", - "pg_stress_hit_cap_detail": "Cykeln kördes {h} h {m} min utan att stoppa -- tvångsstoppad av säkerhetsgränsen. Standby-förbrukning {idle}W mot stopptröskel {stop}W.", "no_logs_match": "Inga loggposter matchar dessa filter.", "pg_analysis_hint2": "Välj en cykel och tryck på Kör för att läsa in matchningsanalysen.", - "pg_analysis_empty2": "Tryck på Kör för att se matchningsanalysen.", - "imported_readonly": "Importerad från gemenskapsbutiken. Visas för referens och matchning. Räknas inte in i din statistik och kan inte redigeras.", + "imported_readonly": "Importerad från gemenskapsbutiken. Används som referens och för matchning, räknas inte i din statistik och kan inte redigeras.", "merge_no_imports": "Importerade inspelningar kan inte slås samman med en riktig cykel. Avmarkera dem för att slå samman.", - "store_enable_hint": "Aktivera onlinefunktioner i Inställningar för att bläddra bland och importera gemenskapens referenscykler.", - "advisory_duration_outlier": "{n} cykel/cykler märkta '{name}' ligger för långt från programmets vanliga längd för att någonsin kunna matcha det (värst: {ratio}x). De är troligen felmärkta, eller så fångade en inspelning två körningar. Märk om dem, eller dela upp dem, så att de slutar snedvrida tidsuppskattningen för det här programmet.", + "store_enable_hint": "Slå på onlinefunktioner under Preferenser > Online & gemenskap för att bläddra bland och importera gemenskapens referenscykler.", + "advisory_duration_outlier": "{n} cykel/cykler märkta '{name}' ligger för långt från dess vanliga längd för att någonsin matcha (värst: {ratio}x). De är troligen felmärkta eller innehåller två körningar. Märk om eller dela upp dem så att de inte längre snedvrider tidsuppskattningen.", "standby_above_stop_title": "Drog fortfarande {idle} W när cykeln avslutades", "standby_above_stop_sub": "{n} av de senaste {total} cyklerna. Avfördröjningen startar först när effekten är under Stopptröskel ({stop} W), så cykler avslutas för sent eller måste stoppas för hand.", - "online_intro": "Dela referenscykler med andra WashData-användare och ladda ner dem. Alla onlinefunktioner är frivilliga och avstängda som standard.", "store_share_intro": "Ladda upp den här referenscykeln så att andra med samma apparat kan använda den. Den granskas innan den visas offentligt.", - "online_intro_global": "Bläddra bland och dela referensinspelningar med andra WashData-användare, och bekräfta apparatposter. En anslutning gäller för hela din WashData-integration; apparatens märke och modell anges per enhet under Grundläggande. Alla onlinefunktioner är frivilliga och avstängda som standard.", + "online_intro_global": "Bläddra bland och dela referensinspelningar med andra WashData-användare. En anslutning gäller alla dina WashData-enheter; märke och modell anges per enhet under Grundläggande. Onlinefunktioner är av tills du slår på dem.", "online_unavailable": "Onlinefunktioner är inte tillgängliga på den här servern.", "store_picker_offline": "Aktivera onlinefunktioner i inställningskugghjulet för att välja från gemenskapskatalogen.", "pick_brand_first": "Välj ett apparatmärke först.", - "model_not_found": "Inte i katalogen ännu.", - "connect_to_confirm": "Anslut i inställningskugghjulet för att bekräfta eller betygsätta.", "adopt_settings_hint": "Skriver över den här apparatens identifierings- och matchningsgränser med de delade. Dina aviseringar, entiteter och energipris ändras aldrig.", - "include_settings_hint": "Delar den här apparatens identifierings- och matchningsgränser (inte dina aviseringar, entiteter eller energipris). Användare väljer själva om de vill tillämpa dem.", + "include_settings_hint": "Dela enhetens detekterings- och matchningströsklar (inte aviseringar, entiteter eller energipris). Andra väljer själva om de vill tillämpa dem.", "onboard_download": "Ny apparat? Ta över en färdig inställning (program, referenscykler och faser) från en annan WashData-användare med samma apparat.", - "share_consent": "Jag bekräftar att dessa cykler slutfördes normalt utan avbrott", "share_device_none": "Inga delbara cykler ännu. Markera en inspelad eller handplockat vald cykel som referenscykel (⭐) på fliken Cykler först.", "share_guideline_naming": "Namnge varje profil exakt som det visas på apparatens ratt eller display (t.ex. 'Cotton 40', 'Eco 60').", - "share_guideline_quality": "Dela bara cykler som slutfördes normalt -- inga avbrott mitt i cykeln, öppnade dörrar eller strömavbrott.", + "share_guideline_quality": "Dela bara cykler som slutfördes normalt: inga avbrott, dörröppningar eller strömavbrott.", "share_guideline_review": "Din uppladdning börjar som väntande och visas offentligt när tillräckligt många gemenskapsmedlemmar bekräftar den.", "share_guidelines_title": "Innan du delar", - "store_download_device_intro": "Ta över alla delade program och deras referenscykler till din apparat. Dina egna inspelade cykler och statistik påverkas inte.", + "store_download_device_intro": "Lägg till alla delade program och deras referenscykler på din enhet. Dina egna cykler och din statistik påverkas inte.", "store_share_device_intro": "Ladda upp {brand} {model} med de referenscykler du väljer. Andra med samma apparat kan ta över dina program. Poster granskas innan de visas offentligt.", "share_profile_no_cycles": "Inga referenscykler -- markera en cykel som ⭐ på fliken Cykler för att inkludera den här profilen", - "advisory_phase_inconsistent": "'{name}' verkar blanda olika program eller temperaturer - dess cykler värms upp under mycket olika lång tid. Om du delar upp den i separata profiler (t.ex. per temperatur) förbättras matchningen och tidsuppskattningarna.", - "advisory_phase_inconsistent_title": "⚠ Möjligen blandade program", - "export_select_intro": "Kryssa i exakt vad som ska inkluderas. Att välja profiler utan deras cykler exporterar ändå ett matchningsbart program (den inlärda formen följer med).", - "import_analyze_hint": "Läs in en exporterad fil (eller klistra in dess JSON). WashData analyserar den och visar exakt vad som kan importeras innan något ändras.", + "export_select_intro": "Kryssa i det som ska med. Profiler utan sina cykler exporterar ändå ett program som kan matchas.", + "import_analyze_hint": "Läs in en exporterad fil (eller klistra in dess JSON). WashData visar vad som kan importeras innan något ändras.", "analyzing": "Analyserar…", - "device_type_mismatch_warn": "Den här exporten kommer från en annan apparattyp ({src} vs {local}). Program och cykler kan fortfarande importeras som referensdata, men apparatspecifika inställningar och import av verklig historik är inaktiverade.", + "device_type_mismatch_warn": "Den här exporten kommer från en annan apparattyp ({src} vs {local}). Program och cykler kan fortfarande importeras som referensdata, men inte enhetsinställningar eller verklig historik.", "merge_hint": "Importerade objekt läggs till; inget lokalt går förlorat. Namnkonflikter löses nedan.", "replace_warn": "Varje ikryssad kategori raderas och ersätts från filen. Icke ikryssade kategorier lämnas orörda.", "dest_reference_hint": "Importerade cykler förbättrar bara programmatchning och påverkar aldrig användnings-/energistatistik.", - "dest_real_history_hint": "Importerade cykler räknas som den här apparatens egen historik och matar energi-/användningsstatistik. Använd detta för att flytta en apparat till en ny installation.", + "dest_real_history_hint": "Importerade cykler räknas som den här apparatens egen historik och statistik. Använd detta för att flytta en apparat till en ny installation.", "sug_muted": "Kommer inte att föreslå den här inställningen igen", "sug_mute_failed": "Kunde inte tysta förslaget", "sug_unmuted_all": "Tysta förslag återställda", - "n_suggestions_muted": "{count} tystade; auto-tunern föreslår inte dessa.", + "n_suggestions_muted": "{count} förslag tystade; auto-tunern föreslår inte dessa.", + "n_suggestions_muted_one": "{count} förslag tystat; auto-tunern föreslår det inte.", + "n_suggestions_muted_other": "{count} förslag tystade; auto-tunern föreslår inte dessa.", "font_size_hint": "Gör allt i den här panelen större eller mindre. Gäller för ditt konto på den här enheten.", - "import_history_description": "Hade du redan en smart kontakt innan du började med WashData? Ladda upp en historikexport från dess effektsensor, eller låt den läsas direkt från Home Assistant, och den vanliga detekteringen körs över den så att tidigare cykler dyker upp i din Cykler-lista, klara att namnges.", - "hist_input_hint": "Ladda upp en CSV-fil som hämtats från Historik-panelen (entitet, tillstånd, senast ändrad), eller låt WashData läsa sensorns historik direkt. Detekteringen körs sedan över den precis som live, och du väljer vilka av cyklerna den hittar du vill behålla.", - "hist_recorder_hint": "Läser från det datum du väljer fram till nu. Home Assistant sparar detaljerad historik i 10 dagar som standard och därefter bara timmedelvärden, som är för grova för att detektera cykler ur - välj ett datum längre tillbaka bara om din recorder är inställd på att spara mer.", - "hist_scanning": "Din historik spelas upp genom detekteringen. Detta körs i bakgrunden - du kan stänga den här dialogen och komma tillbaka till den senare.", + "import_history_description": "Hade du en smart kontakt före WashData? Ladda upp dess effekthistorik eller läs den från Home Assistant. Detekteringen körs över den och tidigare cykler dyker upp i din Cykler-lista, redo att namnges.", + "hist_input_hint": "Ladda upp en CSV från Historik-panelen (entitet, tillstånd, senast ändrad), eller låt WashData läsa sensorns historik direkt. Livedetekteringen körs över den och du väljer vilka cykler som ska behållas.", + "hist_recorder_hint": "Läser från valt datum fram till nu. Home Assistant sparar detaljerad historik i 10 dagar som standard; äldre timmedelvärden är för grova för cykeldetektering.", + "hist_scanning": "Din historik körs genom detektorn i bakgrunden. Du kan stänga den här dialogen och komma tillbaka senare.", "hist_imported_count": "{n} cykler importerade.", "hist_duplicates": "{n} var redan importerade och hoppades över.", "hist_capped": "Gränsen per enhet för importerade cykler nåddes; resten sparades inte.", - "hist_next_step": "De finns i din Cykler-lista, märkta som importerad historik. Öppna en och använd Märk för att namnge programmet den hör till.", + "hist_next_step": "De finns i din Cykler-lista, märkta som importerad historik. Öppna en och använd Märk för att namnge dess program.", "hist_rows_read": "{n} avläsningar lästa", "hist_entity_substituted": "läste {used} (den här enheten är konfigurerad för {wanted})", "hist_breaks": "{n} luckor där sensorn var otillgänglig", @@ -1097,55 +1030,86 @@ "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.", - "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.", + "imported_history_readonly": "Hittad i importerad effekthistorik. Den hjälper matchningen men räknas inte i din statistik och kan inte beskäras eller delas upp. Märk den för att namnge programmet.", + "odometer_intro": "Den här totalen ökar bara. Att ta bort en cykel sänker den inte, så servicepåminnelser förblir korrekta.", + "odometer_correct_hint": "Ange detta om WashData räknade en körning som aldrig ägde rum, eller om apparaten körde 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}", "toast_range_required": "Ange ett giltigt Från/Till-intervall.", - "no_profiles_option": "(inga profiler ännu, lägg till en)" + "no_profiles_option": "(inga profiler ännu, lägg till en)", + "compare_cycles_title": "Jämför {count} cykler", + "edit_builtin_phase": "Det här är en inbyggd fas. När du sparar skapas en anpassad kopia; ta bort kopian för att återställa originalet.", + "fetch_error": "Det gick inte att läsa in data.", + "connection_lost": "Anslutningen bröts - visar data från {time}.", + "pg_notify_partial": "Markörer visar bara aviseringar för start, nästan klar, klar, milstolpar och tystnadstimmar. Liveuppdateringar, lossningspåminnelser och varningar för överskriden tid simuleras inte.", + "connect_to_confirm": "Anslut i inställningskugghjulet för att bekräfta eller betygsätta.", + "coverage_gap_cluster": "{count} nyliga omärkta cykler på cirka {min} min ser ut att vara ett program som du inte har skapat än.", + "pg_label_yes": "Skulle märkas \"{name}\" vid cykelns slut", + "pg_label_no": "Skulle be om granskning: {reason}", + "live_match_debug_tip": "Konfidens: hur väl den aktuella effektkurvan matchar den bästa kandidatprofilen (0-100%). Tvetydig: de två bästa kandidaterna ligger inom 5% från varandra - etiketten är osäker tills cykeln är klar.", + "automation_alias_started": "{device}: cykel startad", + "automation_alias_finished": "{device}: cykel klar", + "automation_desc_started": "Körs när en {device}-cykel startar. Lägg till dina åtgärder (avisering, lampor, ...).", + "automation_desc_finished": "Körs när en {device}-cykel är klar. Lägg till dina åtgärder (avisering, lampor, ...).", + "feedback_detected": "WashData upptäckte: {name}", + "pg_outcome_stale": "Inställningarna har ändrats sedan den här simuleringen kördes. Den visar fortfarande det gamla resultatet.", + "pg_compare_profile_tip": "Väljer bara vilken profil som ritas över grafen. Simuleringen matchar alltid cykeln på egen hand, precis som integrationen gör under drift.", + "pg_task_busy": "En körning av Testa på historik eller Optimera pågår redan för den här enheten. Vänta på den eller avbryt den.", + "pg_sweep_guarded": "Avslutar fler cykler för tidigt, delar upp fler av dem eller missar cykler som den nuvarande inställningen fångar, så den rekommenderas aldrig.", + "pg_sweep_n_cycles": "över {count} cykler", + "pg_sweep_n_cycles_one": "över {count} cykel", + "pg_sweep_n_cycles_other": "över {count} cykler", + "pg_sweep_keep_current": "Behåll det nuvarande värdet ({value}, {metric}): inget värde var bättre med minst en cykel.", + "hist_truncated": "stoppade vid gränsen på {n} avläsningar, så resten av historiken genomsöktes inte", + "hist_warn_kw": "Det högsta mätvärdet är bara {w}. Den här sensorn verkar rapportera kilowatt; WashData behöver watt, så inga cykler kan detekteras.", + "hist_warn_naive_time": "{n} tidsstämplar saknar tidszon och lästes som UTC. Om filen använder lokal tid är cyklerna förskjutna med din UTC-förskjutning, och mätvärden kring ett sommartidsbyte kan försvinna.", + "hist_warn_naive_time_one": "{n} tidsstämpel saknar tidszon och lästes som UTC. Om filen använder lokal tid är cyklerna förskjutna med din UTC-förskjutning, och mätvärden kring ett sommartidsbyte kan försvinna.", + "hist_warn_naive_time_other": "{n} tidsstämplar saknar tidszon och lästes som UTC. Om filen använder lokal tid är cyklerna förskjutna med din UTC-förskjutning, och mätvärden kring ett sommartidsbyte kan försvinna.", + "hist_tail_trimmed": "{min} min väntan efter att cykeln avslutats klipptes bort, precis som integrationen gör under drift", + "invalid_threshold_pair": "Stopptröskel ({stop} W) måste vara under Starttröskel ({start} W). Inget sparades; ändra båda tillsammans.", + "import_undo_hint": "En återställningspunkt från före den senaste importen ({when}) finns kvar: {profiles} profil(er) och {cycles} cykel/cykler.", + "replace_undo_hint": "En återställningspunkt sparas först, så att du kan ångra den här importen under Exportera / Importera.", + "pg_in_sample_note": "De här cyklerna var med och byggde de profiler de matchas mot, så resultaten är optimistiska: räkna med något färre korrekta matchningar på nya cykler." }, "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", - "smart_termination_duration_ratio": "Andel av det matchade programmets förväntade varaktighet som en cykel måste nå innan smart avslutning får avsluta den i förtid; sänk den för last- eller temperaturberoende maskiner", + "smart_termination_duration_ratio": "Andel av programmets genomsnittliga längd som en cykel måste nå innan smart avslutning får avsluta den i förtid; sänk den om körtiderna varierar", "completion_min_seconds": "Kortaste körning som räknas som en riktig cykel", - "end_repeat_count": "Antal låga avläsningar i rad innan avslut", "interrupted_min_seconds": "Korta cykler markeras som avbrutna", "min_off_gap": "Uppehåll som krävs för att skilja två cykler åt", "off_delay": "Sekunder med låg effekt innan cykeln avslutas", "start_duration_threshold": "Sekunder över tröskeln för att bekräfta start", "start_threshold_w": "Minsta antal watt för att räknas som startad", "stop_threshold_w": "Under detta räknas maskinen som avstängd", - "dtw_bandwidth": "Steg 3: Sakoe-Chiba-förvrängningsband (0 = DTW av; standard 0,2 = 20% av cykellängd)", - "corr_weight": "Steg 2: balans mellan kurvform (korrelation) och effektnivå (MAE); standard 0,45", - "duration_weight": "Steg 4: hur starkt körtidsöverensstämmelse påverkar slutpoängen (standard 0,22)", - "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,8 (180%)", - "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)", - "dtw_refine_top_n": "Steg 3: kandidater som DTW ompoängsätter; öka till 7-9 om rätt profil placerar sig 4-5:a (standard 5)", - "duration_scale": "Steg 4: log-kvot där varaktighetsöverensstämmelse halveras; lägre = striktare straff (standard 0,175)", - "energy_scale": "Steg 4: log-kvot där energiöverensstämmelse halveras; lägre = striktare straff (standard 0,25)", + "profile_match_min_duration_ratio": "Lägsta cykellängd som andel av profilens längd; standard 0,1 (10%)", + "profile_match_max_duration_ratio": "Högsta cykellängd som andel av profilens längd; standard 1,8 (180%)", "anti_wrinkle_idle_timeout": "Tillåten tyst tid mellan två tumlingspulser innan skrynkelskyddet avslutas", "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_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", + "anti_wrinkle_exit_power": "Tyst nivå mellan tumlingspulser; gäller bara över Stopptröskeln", + "anti_crease_finalize_ratio": "Andel av programmets genomsnittliga längd som en cykel måste nå innan skrynkelskyddssvansen får avsluta den; sänk den på torktumlare, låt tvättmaskiner stå kvar på standard", "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.", "anti_wrinkle": "Periodisk tumlande efter cykeln för att förhindra skrynklor i tvätten.", + "baking": "Värmeelement på hög temperatur aktivt för skorpa och inkråm.", + "cooking": "Huvudsaklig tillagning med värmeelement och fläkt aktiva.", "cool_down": "Tumling utan värme nära cykelns slut.", + "cool_down_af": "Nedkylning med enbart fläkt efter uppvärmningen.", "drain_and_switch": "Övergångsperiod från tvätt- till torkläge.", "dry": "Torkningssteg med värmare och/eller restvärme.", "drying": "Huvudsaklig uppvärmd tumlingsperiod.", "heat_up": "Första uppvärmningen av värmaren innan full torkning börjar.", + "keep_warm": "Låg värme för att hålla maten varm.", + "keep_warm_bm": "Låg värme för att hålla brödet varmt efter gräddningen.", + "kneading": "Motorn blandar och knådar degen. Hög effektförbrukning.", + "pause_af": "Kort paus för att skaka eller kontrollera maten.", + "pre_heat": "Första uppvärmningen av kammaren före själva tillagningen.", "pre_rinse": "Initial sprayning innan tvättmedelstvätt.", + "proving": "Jäsning på låg värme så att jästen arbetar och degen växer.", + "resting": "Kort paus med låg effekt mellan knådningarna så att glutenet slappnar av.", "pre_wash": "Inledande blötläggning eller förbehandling före huvudtvätt.", "rinse": "Sköljningssteg med rent vatten. Denna fas kan upprepas flera gånger.", "sanitize": "Högtemperaturrengöringssteg för saneringsprogram.", @@ -1158,6 +1122,7 @@ "wash_dw": "Huvudtvättmedelstvätt med värme." }, "placeholder": { + "custom_task": "t.ex. rengör dörrtätning", "add": "lägg till…", "group_name": "t.ex. Bomull 2:47", "json_buttons": "[{\"action\":\"ID\",\"title\":\"Label\"}]", @@ -1185,10 +1150,10 @@ "section": { "dishwasher": { "label": "Diskmaskin", - "intro": "Hantering av cykelslut för diskmaskiner, som vanligtvis avslutas med en lång, nästan ljudlös torkfas före en kort sista tömning." + "intro": "Sluthantering för diskmaskiner, som ofta torkar tyst länge före en kort sista tömning." }, "anti_wrinkle": { - "intro": "Skrynkelskyddsläget identifierar korta lågeffektstumlingspulser efter huvudfasen och håller dem kopplade till den avslutade cykeln för att förhindra skrynklor i tvätten.", + "intro": "Håller en torktumlares tumlingspulser med låg effekt efter huvudfasen kvar i den avslutade cykeln.", "label": "Skrynkelskydd" }, "basic": { @@ -1196,7 +1161,7 @@ "label": "Grundläggande" }, "delay": { - "intro": "Detektering av försenad start identifierar när apparaten är påslagen men ännu inte har startat sin cykel.", + "intro": "Upptäcker en apparat som är på och väntar på en fördröjd start.", "label": "Försenad start" }, "detection": { @@ -1220,161 +1185,153 @@ "label": "Timing och vakthund" }, "triggers": { - "intro": "Valfria externa signaler: en slututlösare, en dörrsensor, en pausknapp och en lossningspåminnelse.", + "intro": "Valfria tillägg: slututlösare, dörrsensor, pausbrytare och lossningspåminnelse.", "label": "Utlösare och dörrar" - }, - "phase_eta": { - "label": "Återstående tid", - "intro": "Fasmedveten återstående tid, för maskiner vars cykellängd beror på temperatur eller centrifugering." } }, "setting": { "dishwasher_end_spike_quiet_release": { "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." + "doc": "Efter den förväntade längden: hur länge diskmaskinen måste ligga under Stopptröskel innan WashData slutar vänta på den sista tömningen. Höj den om en sen tömning efter lång torkning missas. Väntan överstiger aldrig 30 minuter." }, "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." + "doc": "Andel av programmets genomsnittliga längd som en cykel måste nå innan dess tumling med låg effekt räknas som skrynkelskyddssvansen. Sänk den (t.ex. 0,75) på torktumlare med fuktsensor. Låt tvättmaskiner stå kvar på standard (0,98), annars kan en tvätt avslutas halvvägs." }, "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.", + "doc": "Håll en torktumlares korta tumlingspulser med låg effekt efter huvudvärmefasen kvar i den avslutade cykeln, i stället för att läsa dem som nya cykler.", "label": "Aktivera skrynkelskyddsdetektering" }, "anti_wrinkle_exit_power": { - "doc": "Effekten måste sjunka under detta mellan pulserna för att anti-rynkläget ska förbli aktivt.", + "doc": "I skrynkelskyddsläget räknas effekt under detta som tyst mellan tumlingspulser, och tystnad längre än Maks. pulspaus avslutar läget. Stopptröskeln används när den är högre, så detta påverkar bara något när det är satt över Stopptröskeln.", "label": "Avslutningseffekttröskel" }, "anti_wrinkle_idle_timeout": { - "doc": "Hur länge maskinen får förbli tyst mellan två tumlingspulser innan skrynkelskyddsläget avslutas. Ställ in det högre än det längsta uppehåll din torktumlare lämnar mellan två pulser, annars läses varje senare puls som en falsk start.", + "doc": "Hur länge torktumlaren får vara tyst mellan tumlingspulser innan skrynkelskyddsläget avslutas. Ställ in den över torktumlarens längsta paus, annars läses senare pulser som falska starter.", "label": "Maks. pulspaus" }, "anti_wrinkle_max_duration": { - "doc": "Pulser längre än detta behandlas som en riktig cykel snarare än en anti-rynktumling.", + "doc": "En längre puls räknas som en riktig cykel, inte som skrynkelskyddstumling.", "label": "Maks. varaktighet" }, "anti_wrinkle_max_power": { - "doc": "En puls över denna effekt behandlas som en riktig ny cykel, inte en anti-rynktumling. Ställ in precis ovanför tumlingspulseffekten.", + "doc": "En puls över den här effekten räknas som en riktig ny cykel, inte som tumling. Ställ in den strax över effekten på dina tumlingspulser.", "label": "Maks. skrynkelskyddseffekt" }, "auto_label_confidence": { - "doc": "Om matchresultatet vid cykelslutet är på eller över detta, märks programmet automatiskt utan någon bekräftelse. Höj den för att kräva högre säkerhet innan automatisk märkning; sänk den för att automatisera mer. Fungerar tillsammans med Learning Confidence under den.", + "doc": "Matchningspoäng vid cykelslut vid eller över vilken programmet märks utan att fråga. Höj den för mer säkerhet; sänk den för att märka fler cykler automatiskt. Håll den över Inlärningskonfidens.", "label": "Automärkningskonfidens" }, "auto_maintenance": { - "doc": "Kör nattlig städning: bygg om profilkuvert, beräkna om cykelns hälsa, beskära felsökningsspår och behåll de senaste cyklerna.", + "doc": "Nattlig städning: märk gamla cykler som nu tydligt matchar ett program, bygg om programkurvor, uppdatera avvikelsemarkeringar och cykelhälsa, och ta bort felsökningsspår om Spara felsökningsspår är av. Cykler och deras effektspår behålls alltid.", "label": "Automatiskt underhåll (nattlig städning)" }, "completion_min_seconds": { - "doc": "Cykler som är kortare än detta kasseras som spökcykler (testkörningar, öppna dörren för att lägga till en strumpa).", + "doc": "Cykler kortare än detta (testkörningar, öppna dörren för att lägga i en strumpa) sparas som Avbruten och får ingen slutavisering.", "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." + "doc": "Lägger till mätvärden från misslyckade startförsök först i en cykel, för maskiner som provar innan de startar (dörrlås, första vattenpåfyllning). 300 s räcker för de flesta; 0 = av. Inspelade starter flyttas tidigare, så inlärda längder glider ett tag." }, "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.", + "doc": "Effekten måste ligga i standby-bandet så länge innan apparaten räknas som att den väntar på att starta.", "label": "Bekräftelsesekunder" }, "delay_start_detect_enabled": { - "doc": "Upptäck när apparaten är påslagen och väntar (fördröjd start/standby) men inte har börjat sin cykel, så standby-dragning förväxlas inte med en pågående cykel.", + "doc": "Upptäck en apparat som är på och väntar på en fördröjd start, så att dess standbyförbrukning inte tas för en pågående cykel.", "label": "Aktivera detektering av försenad start" }, "delay_timeout_hours": { - "doc": "Sluta vänta i fördröjd start efter så här många timmar och återgå till tomgång, så en maskin som lämnas strömförsörjd men aldrig startad väntar inte för evigt.", + "doc": "Sluta vänta på en fördröjd start efter så här många timmar och återgå till inaktiv.", "label": "Tidsgränstimmar" }, "device_type": { - "doc": "Apparatklass. Ställer in förnuftiga detekteringsstandarder (tröskelvärden, avstängningsfördröjning, sluthantering) inställda för den apparattypen; ändra den endast om enheten ursprungligen var inställd som fel typ.", + "doc": "Ställer in standardvärden för detektering (trösklar, avfördröjning, sluthantering) för den här typen av apparat. Ändra bara om enheten konfigurerades som fel typ.", "label": "Enhetstyp" }, "door_sensor_entity": { - "doc": "Tillval dörr binär sensor. Används för att upptäcka när apparaten har öppnats/avlastats efter en cykel.", + "doc": "Valfri dörrsensor. Visar när apparaten öppnades för att tömmas efter en cykel.", "label": "Dörrsensorentitet" }, "duration_tolerance": { - "doc": "Tolerans för återstående uppskattningar (inlärningsfeedback, inte matchning). Om den faktiska varaktigheten är inom +/-X % av uppskattningen räknas det som en bra matchning.", + "doc": "Endast inlärningsfeedback: en cykel inom +/-X % av sin uppskattade längd räknas som en bra uppskattning. Påverkar inte matchningen.", "label": "Uppskattingstolerans" }, "enable_ml_models": { - "doc": "Medan en cykel körs, låt modellerna förfina liveresultaten: en stabilare tid som återstår och energi/kostnadsuppskattning, och ett skydd mot för tidigt stopp vid slutdetektering (det kan bara någonsin fördröja en målgång, aldrig avsluta en för tidigt och är begränsad). Använder dina finjusterade modeller när sådana finns, annars de inbyggda. Av = endast den klassiska kraftbaserade logiken (fortfarande tillförlitlig).", + "doc": "Använd en modell tränad på den här maskinen för att förutsäga en pågående cykels energi och kostnad, och låt ett välkänt program som stannar ovanligt tidigt avslutas utan hela avfördröjningen. Av: endast effektbaserad logik.", "label": "Tillämpa smarta modeller under en cykel" }, "end_energy_threshold": { - "doc": "Under nedräkningen av frånslagsfördröjningen jämförs ackumulerad energi (watt x tid) med denna tröskel. Om den överskrids återställs nedräkningen - bibehåller skrynkelskydd och diskmaskinstorkande svansar på cykeln istället för att korta dem. Höj den om cyklerna slutar för tidigt under nedkylning; sänk den om detekteringen är trög.", + "doc": "Energi som tillåts under avfördröjningen. Mer än så och cykeln fortsätter, så att tumlingar och torksvansar förblir en del av den. Sänk den om cykler avslutas under en svans; höj den om färdiga cykler fortsätter att vänta.", "label": "Slutenergi" }, - "end_repeat_count": { - "doc": "Antalet på varandra följande avläsningar under stopptröskelvärdet som krävs innan cykeln avslutas. 1 är bra för de flesta pluggar. Höj till 2-3 om din smarta kontakt då och då rapporterar ett prov med falskt noll i mitten av cykeln och dina cykler slutar i förtid.", - "label": "Antal slutrepetitioner" - }, "energy_price_entity": { - "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.", + "doc": "Sensor med aktuellt pris per kWh (t.ex. en dynamisk tariff). Den har företräde framför det statiska priset nedan, så den måste vara ett pris, inte kontaktens effekt- eller energisensor.", "label": "Energiprisentitet" }, "energy_price_static": { - "doc": "Fast pris per kWh som används för kostnadssiffror när ingen levande prisenhet anges ovan.", + "doc": "Fast pris per kWh, används när ingen prisentitet är angiven.", "label": "Statiskt energipris (per kWh)" }, "energy_sensor": { - "doc": "Valfri kumulativ energiräknare (total_increasing i kWh/Wh, t.ex. uttagets egen livstidsmätare). När den anges hämtas varje cykels rapporterade energi från skillnaden på denna räknare mellan start och slut, vilket undviker den underräkning du får när en långsamt rapporterande effektsensor integreras. Faller tillbaka på det integrerade värdet om avläsningen saknas, dess enhet är okänd eller skillnaden inte är positiv. Lämna tomt för att fortsätta integrera effektsensorn.", + "doc": "Valfri kWh- eller Wh-räknare (total_increasing, t.ex. kontaktens egen mätare). Cykelns energi läses då från den, vilket är mer exakt än att beräkna den från en långsam effektsensor. Om dess mätvärde är oanvändbart används det beräknade värdet.", "label": "Energimätarentitet" }, "expose_debug_entities": { - "doc": "Publicera extra diagnostiska HA-enheter (matchningsförtroende, tvetydighet, tillståndsinterna). Av håller enhetslistan ren för normal användning.", + "doc": "Lägg till diagnostiska entiteter (matchningskonfidens, tvetydighet, internt tillstånd). Av håller entitetslistan ren.", "label": "Exponera felsökningsentiteter" }, "external_end_trigger": { - "doc": "Binär sensor vars tillståndsändring markerar cykelns slut (t.ex. en \"färdig\" kontakt med apparaten eller en kompletterande integration).", + "doc": "Binär sensor vars ändring markerar cykelns slut (t.ex. en \"klar\"-kontakt eller en annan integration).", "label": "Extern utlösarentitet" }, "external_end_trigger_enabled": { - "doc": "Låt en extern binär sensor signalera slutet på en cykel, förutom den inbyggda effektbaserade detekteringen.", + "doc": "Låt en binär sensor signalera slutet på en cykel, utöver den effektbaserade detekteringen.", "label": "Aktivera extern slututlösare" }, "external_end_trigger_inverted": { - "doc": "Behandla avtryckarsensorn som stängs AV (istället för PÅ) som slutsignalen.", + "doc": "Behandla att sensorn slås AV (inte PÅ) som slutsignalen.", "label": "Invertera extern utlösare (utlös vid AV)" }, "learning_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.", + "doc": "Lägsta matchningspoäng för att märka en avslutad cykel. En klar vinnare märks; ett jämnt fall hamnar i granskningskön under Cykler. Under detta märks eller frågas ingenting. Håll den under Automärkningskonfidens.", "label": "Inlärningskonfidens" }, "linked_device": { - "doc": "Eventuellt kapslar denna WashData-enhet under en annan enhet (t.ex. den smarta kontakten) i HA-enhetsregistret, visad som \"Ansluten via ...\".", + "doc": "Valfritt. Visa den här WashData-enheten under en annan enhet (t.ex. den smarta kontakten) i Home Assistant, som \"Ansluten via ...\".", "label": "Gruppera under enhet" }, "min_off_gap": { - "doc": "Om maskinen stängs av under kortare tid än denna tid, behandlas på/av/på-sekvensen som en kontinuerlig cykel. Förhindrar att blötläggningsprogram (maskinen stängs av i flera minuter mitt i tvätten) delas upp i två separata cykler. Ställ in det kortare än avståndet mellan dina rygg-mot-rygg-laster om du vill att de ska räknas som separata cykler. Standardinställningarna för enhetstyp skyddar den typiska pausen inom cykeln för varje apparat.", + "doc": "En avstängd period kortare än detta delar inte en cykel, så blötläggningspauser stannar i en tvätt. Håll den kortare än pausen mellan tvättar direkt efter varandra, annars slås de ihop till en cykel.", "label": "Min. avlucka" }, "min_power": { - "doc": "Absolut minimieffekt anses vara aktiv. Avläsningar under detta behandlas som 0 W (standby), vilket filtrerar bort fantombelastningen av smarta pluggar och standby-lysdioder.", + "doc": "Mätvärden under detta räknas som 0 W (standby). Filtrerar bort tomgångsförbrukningen från smarta kontakter och standby-lysdioder.", "label": "Minimieffekt" }, "ml_training_enabled": { - "doc": "Studera regelbundet dina granskade cykler över natten och finjustera modellerna till denna specifika maskin. En förändring behålls bara när den verkligen ger bättre resultat på uthållna cykler, så detta kan bara hjälpa eller förbli oförändrat - aldrig gå tillbaka.", + "doc": "Träna om energimodellen på den här maskinens avslutade cykler då och då. En ny modell behålls bara om den slår den enkla uppskattningen på testcykler.", "label": "Lär dig av denna maskin" }, "ml_training_hour": { - "doc": "Lokal timme på dygnet (0-23) för att göra finjusteringen över natten. Välj en lugn timme som 2 (02:00).", + "doc": "Timme på dygnet (0-23) för nattlig träning. Välj en lugn timme, t.ex. 2.", "label": "Lär dig vid timmen" }, "ml_training_interval_days": { - "doc": "Kontrollera igen efter förbättringar högst en gång per så många dagar.", + "doc": "Träna om högst en gång per så här många dagar.", "label": "Kontrollera som mest varje" }, "ml_training_min_cycles": { - "doc": "Vänta tills åtminstone så många cykler har spelats in innan du finjusterar, så det finns tillräckligt att lära av.", + "doc": "Vänta tills minst så här många cykler har spelats in innan träning.", "label": "Cykler som behövs först" }, "name": { - "doc": "Visningsnamn som visas i HA-integreringslistan och enhetsregistret.", + "doc": "Namnet som visas i Home Assistant för den här enheten.", "label": "Enhetsnamn" }, "no_update_active_timeout": { - "doc": "Om det inte kommer några strömuppdateringar under så här lång tid medan du kör, anta att kontakten tappade offline och tvångsstopp för att undvika en zombiecykel. Standard 600 s tillåter moln eller mesh lag.", + "doc": "Om ingen effektuppdatering kommer på så här lång tid under en cykel antas kontakten vara offline och cykeln tvångsstoppas. Standardvärdet 600 s ger utrymme för fördröjningar i moln eller mesh.", "label": "Tidsgräns utan uppdatering" }, "notify_before_end_minutes": { @@ -1382,35 +1339,35 @@ "label": "Förvarning före slut" }, "notify_channel": { - "doc": "Android-aviseringskanalnamn för start-/livemeddelanden (kontrollerar ljud per kanal och prioritet i mobilappen). Blank använder standardappen för kompletterande app.", + "doc": "Android-kanal för start- och livemeddelanden; kanalen styr ljud och prioritet. Tomt: appens standard.", "label": "Android-kanal (start/direkt)" }, "notify_cycle_timers": { - "doc": "Aviseringar vid specifika minuter in i en cykel (t.ex. för att lägga till mjukgörare). Meddelande stöder {device}, {program}, {minutes}. Aktivera automatisk paus för att pausa vid den punkten och få ett interaktivt meddelande med en Fortsätt-knapp; återuppta via panelen, paus-/återuppta-tjänsten eller aviseringsåtgärden.", + "doc": "Aviseringar vid bestämda minuter in i en cykel (t.ex. för att tillsätta sköljmedel). Meddelandet stöder {device}, {program}, {minutes}. Med Automatisk paus pausas cykeln också där och aviseringen får en Återuppta-knapp.", "label": "Cykeltidurar" }, "notify_finish_channel": { - "doc": "Android-aviseringskanalnamn för slutmeddelandet. Blank återanvänder start/live-kanalen.", + "doc": "Android-kanal för slutmeddelandet. Tomt: start-/livekanalen återanvänds.", "label": "Android-kanal (slut)" }, "notify_finish_message": { - "doc": "Brödtext som skickas när en cykel är klar. Mallvariabler: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}. {time_finished} och {vs_typical} är mest användbara här.", + "doc": "Text som skickas när en cykel är klar. Variabler: {device}, {program}, {duration} (minuter), {duration_hm} (1 h 05 min), {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}, {status} (completed eller force_stopped; avbrutna cykler skickar inget). Med någon annan variabel används standardtexten.", "label": "Slutmeddelande" }, "notify_finish_services": { - "doc": "notify.* tjänster anropas när en cykel avslutas. Lägg till en per mål; lämna tomt för ingen avisering om avslutad.", + "doc": "notify.*-tjänster som anropas när en cykel är klar. Lämna tomt för inga.", "label": "Sluttjänster" }, "notify_fire_events": { - "doc": "Avfyra även ha_washdata_*-händelser på cykelstart/slut så att du kan bygga dina egna automatiseringar.", + "doc": "Avfyra även ha_washdata_*-händelser vid cykelstart och -slut, för dina egna automationer.", "label": "Avfyra HA-händelser för meddelanden" }, "notify_icon": { - "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.", + "doc": "Liten ikon på aviseringen (t.ex. mdi:washing-machine). Skickas bara till mobile_app-mål, inte till notify-grupper. På iOS ersätter den appikonen och kräver companion-appen 2026.8 eller senare. Tomt: plattformens standard.", "label": "Meddelandeikon" }, "notify_live_chronometer": { - "doc": "Visa en nedräkningstimer som uppdateras live i meddelandet (på plattformar som stöder det) istället för en statisk uppskattning.", + "doc": "Visa en löpande nedräkning i aviseringen (där det stöds) i stället för en fast uppskattning.", "label": "Använd direktkronometer" }, "notify_live_interval_seconds": { @@ -1418,143 +1375,135 @@ "label": "Direktuppdateringsintervall" }, "notify_live_overrun_percent": { - "doc": "Om en cykel går förbi sin uppskattning med mer än denna procentsats, skicka en överskridningsvarning.", + "doc": "Liveuppdateringarna slutar när en cykel drar över sin förväntade längd med den här procentsatsen (20 = upp till 1,2x den förväntade tiden).", "label": "% överköring innan varning" }, "notify_live_services": { - "doc": "notify.*-tjänster kallas för uppdateringar av förlopp i realtid medan en cykel körs. Lämna tomt för att inaktivera aviseringar om liveförlopp.", + "doc": "notify.*-tjänster för liveuppdateringar av förloppet medan en cykel körs. Lämna tomt för att stänga av dem.", "label": "Direktframstegstjänster" }, "notify_only_when_home": { - "doc": "Skicka bara aviseringar när minst en av de länkade personerna (ovan) är hemma.", + "doc": "Avisera bara när minst en av personerna ovan är hemma.", "label": "Meddela bara när hemma" }, "notify_people": { - "doc": "person.* enheter som används av \"Meddela endast när hemma\" för att avgöra om någon är hemma.", + "doc": "person.*-entiteter som \"Meddela bara när hemma\" kontrollerar.", "label": "Personer (för Bara när hemma)" }, "notify_pre_complete_message": { - "doc": "Brödtext för varningen före slut / nästan klar. Mallvariabler: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Text i varje liveuppdatering av förloppet. Mallvariabler: {device}, {program}, {minutes} (minuter kvar). Med någon annan variabel används standardtexten.", "label": "Nästan klart-meddelande" }, "notify_reminder_message": { - "doc": "Brödtext för påminnelsen om att tömma maskinen som fortfarande väntar. Tomt använder det inbyggda standardvärdet. Mallvariabler: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Text i Förvarning före slut, som skickas en gång. Mallvariabler: {device}, {program}, {minutes} (inställningen Förvarning före slut). Med någon annan variabel används standardtexten.", "label": "Påminnelsemeddelande" }, "notify_start_message": { - "doc": "Brödtext som skickas när en cykel startar. Mallvariabler: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Text som skickas när en cykel startar. Mallvariabler: {device}, {program}. Med någon annan variabel används standardtexten.", "label": "Startmeddelande" }, "notify_start_services": { - "doc": "notify.* tjänster anropas när en cykel startar. Lägg till en per mål (telefon, instrumentpanel, etc.); lämna tomt för ingen startavisering.", + "doc": "notify.*-tjänster som anropas när en cykel startar (telefon, instrumentpanel, ...). Lämna tomt för inga.", "label": "Starttjänster" }, "notify_timeout_seconds": { - "doc": "Avvisa meddelandet automatiskt efter så här många sekunder (på plattformar som stöder det). 0 behåller den tills den avvisas manuellt.", + "doc": "Stäng aviseringen efter så här många sekunder (där det stöds). 0 behåller den tills du stänger den.", "label": "Avfärda automatiskt efter" }, "notify_title": { - "doc": "Aviseringsrubrik. Mallvariabler: {device}, {program}, {duration}, {energy}, {cost}, {date}, {time}, {minutes}.", + "doc": "Aviseringsrubrik. Bara {device} ersätts; med någon annan variabel används standardrubriken.", "label": "Meddelanderubrik" }, "notify_unload_delay_minutes": { - "doc": "Minuter efter att en cykel har avslutats innan den fortfarande väntande påminnelsen om \"lasta av maskinen\" skickas. Ställ in 0 för att inaktivera påminnelsen.", + "doc": "Minuter efter att en cykel har avslutats innan påminnelsen \"töm maskinen\". 0 stänger av den.", "label": "Lossningspåminnelsefördröjning" }, "notify_unload_repeat": { - "doc": "Fortsätter att skicka lossningspåminnelsen igen var \"Lossningspåminnelsefördröjning\" minut tills lossningen har bekräftats eller du trycker på \"Sluta påminna\" i aviseringen. Kräver ett sätt att bekräfta lossningen: en Dörrsensorentitet, en Lossningsbekräftelse-entitet eller alternativet \"Bekräfta lossning manuellt\" nedan. Avfärda-knappen fungerar på aviseringar från Home Assistant companion-appen (mobil).", + "doc": "Skicka lossningspåminnelsen igen var \"Lossningspåminnelsefördröjning\" minut tills lossningen är bekräftad eller du trycker på \"Sluta påminna\" (mobilappen). Kräver en Dörrsensorentitet, en Lossningsbekräftelse-entitet eller \"Bekräfta lossning manuellt\".", "label": "Upprepa tills lossad" }, "off_delay": { - "doc": "Dags att vänta efter strömavbrott innan man förklarar att cykeln är avslutad. Om strömmen återupptas inom detta fönster fortsätter cykeln sömlöst - detta överbryggar pauser mellan tvättstegen. Diskmaskiner har långa torkningsfaser (ström av i 20-60 min) så avstängningsfördröjningen måste överstiga den för att hålla hela tvätten+torr som en cykel.", + "doc": "Hur länge effekten måste vara låg innan cykeln är klar. Kommer effekten tillbaka inom den tiden fortsätter cykeln. Diskmaskiner kan vara tysta i 20-60 min under torkning, så ställ in den längre än så.", "label": "Avfördröjning" }, "pause_cuts_power": { - "doc": "När en cykel är pausad, stäng även av Switch Entity nedan. Endast för apparater vars kontakt säkert kan skäras av mitt i cykeln.", + "doc": "När en cykel pausas, stäng även av Switchentitet nedan. Endast för apparater som säkert kan bli strömlösa mitt i en cykel.", "label": "Paus bryter även ström (via switch)" }, "power_off_delay": { - "doc": "Hur länge effekten måste hålla sig under Avstängningströskel efter att en cykel har avslutats innan tillståndet återgår till Av. Används endast när Avstängningströskel är över 0. Kontrolleras enligt bakgrundskadensen, så den effektiva fördröjningen avrundas uppåt till nästa tillståndsutgångstick.", + "doc": "Hur länge effekten måste ligga under Avstängningströskel efter en cykel innan tillståndet går tillbaka till Av. Används bara när den tröskeln är över 0. Kontrolleras periodiskt, så den verkliga väntan kan bli något längre.", "label": "Avstängningsfördröjning" }, "power_off_threshold_w": { - "doc": "Valfri effektbaserad Av-detektering. När värdet är över 0 betraktas maskinen som avstängd och tillståndet återgår till Av så snart en cykel har avslutats och effekten håller sig under denna nivå under hela Avstängningsfördröjningen. Lämna på 0 för att inaktivera (standard). Ställ in värdet över maskinens verkliga avstängda nollnivå och under Stopptröskel samt under maskinens standbyförbrukning i tillståndet färdig-men-på; om det inte är under Stopptröskel ignoreras det. När det är aktiverat ersätter det Förloppåterställningsfördröjning för återgång till Av, så att en färdig maskin stannar i Färdig/Rena tills den faktiskt stängs av.", + "doc": "Efter en cykel sätter effekt under detta under Avstängningsfördröjning tillståndet till Av, i stället för att Förloppåterställningsfördröjning gör det. Ställ in den över förbrukningen i avstängt läge, under standby och Stopptröskel. 0 stänger av den (standard).", "label": "Avstängningströskel" }, "power_sensor": { - "doc": "Sensorenheten rapporterar levande effekt i watt för denna apparat (t.ex. sensor.washer_power). All cykeldetektering baseras på denna signal.", + "doc": "Sensorn som rapporterar apparatens aktuella effekt i watt (t.ex. sensor.washer_power). All detektering bygger på den.", "label": "Effektsensor" }, - "profile_duration_tolerance": { - "doc": "+/- bandet runt en profils genomsnittliga varaktighet som används under matchning. 0,25 betyder att en 60 min profil matchar 45-75 min cykler.", - "label": "Profilvaraktighetstolerans" - }, "profile_match_interval": { - "doc": "Hur ofta man ska försöka profilmatcha under en löpcykel. Standard 300 s (5 minuter) balanserar detekteringshastighet och CPU.", + "doc": "Hur ofta en pågående cykel matchas mot profilerna. Standard 300 s (5 min).", "label": "Matchningsintervall" }, "profile_match_max_duration_ratio": { - "doc": "Maximal cykellängd i förhållande till profilen. 1.3 betyder att en cykel måste vara under 130 % av profilens varaktighet för att matcha.", + "doc": "Ett program utesluts när cykeln har pågått längre än den här multipeln av dess typiska längd. 1,8: en cykel 80 % längre än programmet matchar det inte längre.", "label": "Maks. varaktighetsförhållande" }, "profile_match_min_duration_ratio": { - "doc": "Minsta cykellängd i förhållande till profilen. 0,9 betyder att en cykel måste vara minst 90 % av profilens varaktighet för att matcha.", + "doc": "Ett program övervägs först när cykeln har pågått den här andelen av dess typiska längd. 0,10: ett program på 2 timmar blir kandidat efter 12 minuter. Används inte under en cykels första 15 minuter.", "label": "Min. varaktighetsförhållande" }, "profile_match_threshold": { - "doc": "Minsta likhetspoäng (0-1) krävs i slutet av cykeln för att acceptera en programidentifiering. Höj den för att minska felaktiga identifieringar; sänk den om din maskins program inte matchas. Standard 0,4 är en konservativ utgångspunkt.", + "doc": "Live-matchningspoäng som en cykel behöver innan smart avslutning eller skrynkelskyddsavslutet får avsluta den i förtid. Ändrar inte det visade programmet. Höj den om svaga matchningar avslutar cykler för tidigt; sänk den om tydliga matchningar väntar ut avfördröjningen.", "label": "Matchningströskel" }, "profile_unmatch_threshold": { - "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.", + "doc": "Om live-matchningspoängen sjunker under detta rensas den aktuella programgissningen. Håll den lite under Matchningströskel så att en kort dipp inte nollställer den.", "label": "Omatchningströskel" }, "progress_reset_delay": { - "doc": "När du är klar håller du framsteg på 100 % så länge så att Slutfört är synligt på instrumentpanelerna innan du återställer till viloläge.", + "doc": "Efter en cykel, håll förloppet på 100 % så här länge innan återställning till Inaktiv.", "label": "Förloppåterställningsfördröjning" }, "pump_stuck_duration": { - "doc": "Sekunder kan en pump köras kontinuerligt innan den flaggas som eventuellt fast (utlöser händelsen med fast pump).", + "doc": "Hur länge en pump får köras oavbrutet innan händelsen för fastnad pump utlöses.", "label": "Pumpens fastnadsvaraktighet" }, "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.", + "doc": "Minsta tid mellan bearbetade mätvärden. Mätvärden som kommer tidigare hoppas över (låga mätvärden under en cykel behålls alltid). Ställ in den på kontaktens rapporteringsintervall eller lägre; ett högre värde slänger bara mätvärden.", "label": "Samplingsintervall" }, "save_debug_traces": { - "doc": "Lagra spårningen av full effekt och matchande felsökningsdata för varje cykel. Användbar för felsökning men ökar lagringsstorleken.", + "doc": "Spara även matcharens felsökningsdata (alla kandidaters poäng) med varje cykel, för felsökning. Tar cirka 20 KB per cykel.", "label": "Spara felsökningsspår" }, "smart_termination_duration_ratio": { - "doc": "Hur långt in i det matchade programmets förväntade varaktighet en cykel måste vara innan smart avslutning får avsluta den i förtid när effekten sjunker. Den förväntade varaktigheten är programmets genomsnitt, så på apparater vars körtid varierar mycket - tvättmaskiner med kallt vinter- kontra varmt sommarinloppsvatten, torktumlare med fuktsensor, lastberoende program - avslutas ungefär hälften av alla körningar kortare än det genomsnittet och får aldrig den snabba avslutningen, utan avslutas först minuter senare via reserv-timeouten. Sänk detta värde (t.ex. 0,85) på sådana maskiner så att den tidiga avslutningen ändå utlöses; höj det mot 1,0 för att vara mer försiktig. Lämna tomt för standardvärdet (0,98, eller 0,99 för diskmaskiner). Den kan bara avsluta en cykel tidigare, aldrig senare, och utlöses aldrig vid en tvetydig eller osäker matchning.", + "doc": "Andel av programmets genomsnittliga längd som en cykel måste nå innan smart avslutning får avsluta den i förtid. Om körtiderna varierar (vattentemperatur, fuktsensor, mängd), sänk den (t.ex. 0,85). Lämna tomt för 0,98 (0,99 på diskmaskiner).", "label": "Kvot för smart avslutning" }, - "smoothing_window": { - "doc": "Hur mycket råeffektsignalen utjämnas. Låg (2) är lyhörd men bullrig; hög (5) jämnar ut spikar men lägger till eftersläpning.", - "label": "Utjämningsfönster" - }, "start_duration_threshold": { - "doc": "Strömmen måste ligga över starttröskeln så här länge för att bekräfta en riktig start, vilket förhindrar att på/av-växlar på en sekund startar en cykel.", + "doc": "Effekten måste ligga över Starttröskel så här länge innan en cykel startar, så att korta på/av-blippar ignoreras.", "label": "Startvaraktighet" }, "start_energy_threshold": { - "doc": "Energi (effekt x tid) som apparaten måste förbruka innan den KÖR. En kort spik med hög effekt har mycket låg energi och ignoreras, vilket förhindrar felstarter.", + "doc": "Energi som apparaten måste förbruka innan cykeln räknas som igång. En kort effekttopp förbrukar lite energi, så den startar ingen cykel.", "label": "Startenergi" }, "start_threshold_w": { - "doc": "Effekten måste stiga över denna nivå för att bekräfta att en cykel har startat. Om den ställs in för lågt orsakar felstarter från standby-ström; för högt och långsamt startande program (kallfyllning) missas. Förslagsmotorn ställer in detta strax över maskinens observerade lägsta aktiva effekt.", + "doc": "Effekten måste stiga över detta för att starta en cykel. För lågt: standbyeffekt ger falska starter. För högt: långsamt startande program (kallvattenfyllning) missas.", "label": "Starttröskel" }, "stop_threshold_w": { - "doc": "Effekten måste sjunka under denna nivå innan nedräkningen av avstängningsfördröjningen börjar. Ställ in den under starttröskeln - gapet mellan dem är hysteresbandet som förhindrar flimmer. Om inställningen är för hög, utlöser lågeffektfaser (sköljstopp, skrynkelskydd) felaktigt slutsekvensen.", + "doc": "Effekten måste sjunka under detta innan avfördröjningen börjar räknas. Håll den under Starttröskel. För högt, och faser med låg effekt (sköljpauser, skrynkelskydd) startar slutnedräkningen av misstag.", "label": "Stopptröskel" }, "switch_entity": { - "doc": "Valfri strömbrytare slås av vid paus och på igen vid återupptagning, används tillsammans med \"Pausen bryter också strömmen\".", + "doc": "Valfri brytare som slås av vid paus och på igen vid återupptagning, med \"Paus bryter även ström\".", "label": "Switchentitet" }, "watchdog_interval": { - "doc": "Hur ofta kontrollerar bakgrundsvakten efter avstängda sensorer och förflutna timeouts. Standard 30 s.", + "doc": "Hur ofta bakgrundskontrollen letar efter avstannade sensorer och tidsgränser. Standard 30 s.", "label": "Vakthundsintervall" }, "notify_milestone_message": { @@ -1562,127 +1511,82 @@ "label": "Milstolpsmeddelande" }, "notify_milestones": { - "doc": "Kommaseparerade cykelantal som utlöser en engångsnotis med gratulation när de uppnås (t.ex. 50, 100, 500, 1000). Tomt inaktiverar milstolpsnotiser.", + "doc": "Cykelantal som utlöser en engångsavisering med gratulation, kommaseparerade (t.ex. 50, 100, 500, 1000). Tomt stänger av dem.", "label": "Cykelmilstolpar" }, "notify_quiet_end_hour": { - "doc": "Slut på stör ej-fönstret (0-23). Kvarhållna notiser levereras vid denna timme. Lämna tomt för att inaktivera.", + "doc": "Slut på stör ej-fönstret (0-23); kvarhållna aviseringar skickas då. Tomt stänger av det.", "label": "Tystnadstimmar slut" }, "notify_quiet_start_hour": { - "doc": "Start på ett stör ej-fönster (0-23). Notiser om slutförande, påminnelser och ren tvätt som skulle skickas under tystnadstimmar hålls kvar och levereras när fönstret slutar. Lämna tomt för att inaktivera. Stöder fönster som passerar midnatt (t.ex. start 22, slut 7).", + "doc": "Start på ett stör ej-fönster (0-23). Slut-, påminnelse- och ren tvätt-aviseringar i det här fönstret hålls kvar tills det slutar. Kan gå över midnatt (t.ex. 22 till 7). Tomt stänger av det.", "label": "Tystnadstimmar start" }, "peak_rate_message": { - "doc": "Valfri anpassad text för högtaxetipset som läggs till i startnotisen. Mallvariabler: {device}, {price}. Tomt använder det inbyggda standardvärdet.", + "doc": "Egen text för högtaxetipset. Mallvariabler: {device}, {price}. Tomt använder standardtexten.", "label": "Högtaxemeddelande" }, "peak_rate_threshold": { - "doc": "När en cykel startar och det aktuella priset per kWh är lika med eller över detta värde läggs ett högtaxetips till i startnotisen. 0 eller tomt inaktiverar tipset.", + "doc": "Om priset per kWh är lika med eller över detta när en cykel startar läggs ett högtaxetips till i startaviseringen. 0 eller tomt stänger av det.", "label": "Högtaxetröskel (per kWh)" }, - "dtw_bandwidth": { - "label": "Tolerans för formmatchning", - "doc": "Hur mycket matchningen får tidsförvränga en cykel för att passa formen på ett sparat program. Högre tillåter mer hastighetsvariation; för högt riskerar att matcha fel program." - }, - "corr_weight": { - "label": "Vikt: form kontra nivå", - "doc": "Balans mellan kurvform och absolut effektnivå när en matchning bedöms. Högre gynnar formen; lägre gynnar att träffa effektnivån." - }, - "duration_weight": { - "label": "Vikt på varaktighet", - "doc": "Hur starkt ett program vars typiska körtid stämmer med den här cykeln föredras." - }, - "energy_weight": { - "label": "Vikt på energi", - "doc": "Hur starkt ett program vars typiska energiförbrukning stämmer med den här cykeln föredras." - }, "store_brand": { "label": "Apparatens märke", - "doc": "Valfritt. Apparatens märke, valt från gemenskapskatalogen. Används för att hitta och dela matchande referensinspelningar. Lämna tomt om du inte använder onlinefunktioner." + "doc": "Valfritt. Din apparats märke från gemenskapskatalogen, används för att hitta och dela referensinspelningar. Lämna tomt om du inte använder onlinefunktioner." }, "store_model": { "label": "Apparatens modell", - "doc": "Valfritt. Apparatens modell, vald från gemenskapskatalogen när ett märke har angetts. Om din modell inte finns med kan du lägga till den i katalogen." + "doc": "Valfritt. Din apparats modell från gemenskapskatalogen (välj ett märke först). Om den inte finns med kan du lägga till den." }, "show_contributor": { "doc": "Visar tillskrivningen 'av ' på gemenskapsapparater och referenscykler." }, - "enable_phase_matching": { - "label": "Fasmedveten återstående tid", - "doc": "Dela upp varje pågående cykel i faser (uppvärmning, tvätt, centrifugering) och budgetera den återstående tiden per fas, blandat med den klassiska uppskattningen - tidigt i cykeln lutar den sig mot fasbudgeten och mot slutet mot den klassiska uppskattningen. Detta anpassar nedräkningen efter hur länge din maskin faktiskt värms upp och körs, vilket märks tydligast under den första halvan av en cykel. Av = endast den klassiska uppskattningen. Endast visningen av återstående tid påverkas; programmatchning och cykeldetektering är oförändrade." - }, - "keep_min_score": { - "label": "Min matchningspoäng", - "doc": "Lägsta likhetspoäng som en kandidat behöver för att vara kvar i matchningen. Standard 0,1 är avsiktligt tillåtande – det tillåter även svaga kandidater och förlitar sig på senare steg för att hitta bästa match. Höj för att gallra bort osannolika profiler tidigare; sänk för att utöka den initiala kandidatpoolen." - }, - "dtw_blend": { - "label": "DTW-blandning", - "doc": "Hur mycket DTW-justeringspoängen ersätter kärnpoängen från Steg 2. 0 = använd bara Steg 2-poängen, 1 = använd bara DTW-poängen, 0,5 (standard) = lika blandning. Höj om program har liknande effektnivåer men olika timingmönster." - }, - "dtw_ensemble_w": { - "label": "DTW-ensemblemix", - "doc": "I ensemble-DTW-läge (standard) anger detta vikten på skalad L1 kontra derivativ DTW (DDTW). 1,0 = enbart skalad L1, 0 = enbart DDTW, 0,7 = standard. Den skalade varianten jämför effektnivåer; den derivativa reagerar på hur effekten förändras över tid. Ensemble blandar båda." - }, - "dtw_ddtw_scale": { - "label": "Derivativ DTW-skala", - "doc": "Halvmättnadsavstånd för derivativ DTW-poängsättning. Poängen halveras när DDTW-avståndet är lika med detta värde. Lägre = mer känslig för formskillnader. Standard 30 är kalibrerad för typiska apparaters effektprofiler. Påverkar bara ensemble- och ddtw-DTW-lägen." - }, - "dtw_refine_top_n": { - "label": "DTW-förfinandetal", - "doc": "Hur många toppkandidater från Steg 2 som ompoängsätts av DTW. DTW är mer beräkningskrävande så det tillämpas bara på de N bästa. Standard 5. Höj (till 7-9) om rätt profil ibland bara når 4:e eller 5:e plats efter Steg 2 – DTW kan rädda den. Lägre värde snabbar upp matchningen något." - }, - "duration_scale": { - "label": "Varaktighetsskärpa", - "doc": "Skärpan i Steg 4:s straff för varaktighetsöverensstämmelse. Det är log-kvoten vid vilken överensstämmelsepoängen halveras. Lägre = striktare: en varaktighetsavvikelse straffas mer. Standard 0,175 motsvarar ungefär 18% varaktighetstolerans vid halvvikt. Para ihop med Varaktighetssvikt." - }, - "energy_scale": { - "label": "Energiskärpa", - "doc": "Skärpan i Steg 4:s straff för energiöverensstämmelse. Lägre = striktare: en energiavvikelse straffas mer. Standard 0,25 är avsiktligt mer tillåtande än varaktighetsskärpan eftersom energiförbrukning varierar med last. Para ihop med Energivikt." - }, "power_profile_interval_min": { "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." + "doc": "Bucketstorlek för attributet power_profile (genomsnittlig watt per slot) som planerare som EMHASS och tibber_prices använder. Mindre behåller korta toppar; större jämnar ut kurvan. Standard 15 min. Påverkar inte detekteringen." }, "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." + "doc": "Endast iOS. Uppdatera liveförloppet ljudlöst, med en batterisnål prioritet som iOS kan gruppera. Den första uppdateringen och start-/slutaviseringarna hörs fortfarande. Av: varje uppdatering hörs." }, "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." + "doc": "Endast Android. Behåll live-förloppsaviseringen vid tryck. Av: ett tryck stänger den." }, "notify_live_click_action": { "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." + "doc": "Vad ett tryck på den här apparatens avisering öppnar. Tomt: WashData-panelen för den här apparaten. Ange en sökväg till en instrumentpanel (t.ex. /lovelace/laundry) eller en URL, eller \"none\" för ingen tryckåtgärd." }, "door_opens_at_end": { "label": "Dörren öppnas automatiskt vid avslut", - "doc": "För diskmaskiner som poppar upp dörren vid cykelslutet för torkning (AirDry och liknande). Med detta aktiverat sätter en döröppning under en aktiv cykel inte den på paus för evigt; om dörren förblir öppen under den konfigurerade uppehållstiden behandlar WashData cykeln som avslutad. En kort öppning (lägga till ett föremål) ignoreras. Kräver en dörrsensorenhet." + "doc": "För diskmaskiner som öppnar dörren i slutet för att torka (t.ex. AirDry). En dörr som står öppen under väntetiden nedan avslutar cykeln i stället för att pausa den; en kort öppning ignoreras. Kräver en Dörrsensorentitet." }, "door_end_dwell_seconds": { "label": "Dörröppningens uppehållstid vid avslut", - "doc": "Hur länge dörren måste förbli öppen innan WashData avslutar cykeln, när \"Dörren öppnas automatiskt vid avslut\" är aktiverat. Tillräckligt länge för att ignorera snabb tilläggning av ett fat (standard 60 s), tillräckligt kort för att avsluta snabbt när maskinen poppar upp dörren." + "doc": "Med \"Dörren öppnas automatiskt vid avslut\" på: hur länge dörren måste vara öppen innan cykeln avslutas. Tillräckligt länge för att ignorera att man ställer in en tallrik (standard 60 s)." }, "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." + "doc": "Vilka typer av cykler som formar varje programs kurva och matchning. Att avmarkera en typ raderar inget; användbart om du inte litar på importerade data. Statistiken räknar alltid bara den här maskinens cykler. Att avmarkera alla ignoreras." }, "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." + "doc": "Debitera varje cykel till priset i varje ögonblick den kördes, i stället för priset när den slutade. Kräver en prisentitet. Bearbeta historik (Diagnostik) räknar om äldre cykler." }, "unload_confirm_entity": { "label": "Lossningsbekräftelse-entitet", - "doc": "Valfri entitet vars aktivering betyder att tvätten har tagits ut, för maskiner som inte kan ha en dörrsensor. Alla typer fungerar: en Zigbee- eller NFC-knapp, en input_button-hjälpare, en rörelsesensor framför maskinen, en scen. Varje tillståndsändring räknas som en bekräftelse, utom att stängas av och att bli otillgänglig. Att ställa in den här aktiverar även lossningspåminnelsen på en enhet utan Dörrsensorentitet." + "doc": "Valfritt. Valfri entitet som bekräftar att tvätten har tagits ut, för maskiner utan dörrsensor (en knapp, input_button, rörelsesensor, scen). Varje tillståndsändring räknas utom avstängning eller otillgänglig. Aktiverar även lossningspåminnelsen." }, "unload_track_without_door": { "label": "Bekräfta lossning manuellt", - "doc": "Kör lossningspåminnelsen på en enhet utan Dörrsensorentitet och utan Lossningsbekräftelse-entitet, och bekräfta lossningen från din egen automation i stället: tryck på knappentiteten \"Markera som lossad\", eller anropa åtgärden ha_washdata.mark_unloaded. Ignoreras när en Dörrsensorentitet är angiven, eftersom den redan ger signalen." + "doc": "Kör lossningspåminnelsen utan dörrsensor eller bekräftelseentitet. Bekräfta lossningen från din egen automation med knappen \"Markera som lossad\" eller tjänsten ha_washdata.mark_unloaded. Ignoreras när en Dörrsensorentitet är angiven." }, "notify_icon_color": { "label": "Meddelandefärg", - "doc": "Accentfärg för den här apparatens aviseringar, så att en tvättmaskin, en torktumlare och en diskmaskin går att skilja åt med en blick. iOS färgar ikonen och Liveaktivitetens förloppsindikator på den låsta skärmen och i Dynamic Island. Android får samma färg skickad till sig, men vad telefonen gör med den varierar: vissa färgar ikonen, och många (särskilt Samsung One UI) struntar helt i den. Båda förstår hex-värden (t.ex. #03A9F4). Lämna tomt för plattformens standard." + "doc": "Accentfärg för den här apparatens aviseringar, så att tvättmaskin, torktumlare och diskmaskin är lätta att skilja åt. På iOS fyller den cirkeln bakom ikonen och färgar Liveaktivitetens stapel. Många Android-telefoner (särskilt Samsung) ignorerar den. Använd hex, t.ex. #03A9F4. Tomt: plattformens standard." + }, + "share_name": { + "doc": "Visa ditt kontonamn som uppladdare av cykler du delar. Av: delas utan namn." } }, "setting_group": { @@ -1710,9 +1614,6 @@ "door_pause": { "label": "Dörr och paus" }, - "dtw_refinement": { - "label": "DTW-förfining" - }, "duration_gates": { "label": "Varaktighetsportar" }, @@ -1789,17 +1690,20 @@ "rebuilding": "Bygger om…", "analyzing": "Analyserar…", "resetting": "Återställer…", - "reverting": "Återgår…", "trimming": "Beskär…", "splitting": "Delar upp…", + "stalled": "Avstannad", "deleting": "Raderar…", "imported": "Importerad", "preparing": "Förbereder…", - "unlabelling": "Tar bort märkning…" + "unlabelling": "Tar bort märkning…", + "working": "Arbetar…", + "match_uncertain": "Osäker: {top} eller {runner}", + "match_uncertain_single": "Osäker: kanske {top}", + "match_sure": "~{pct}% säker", + "match_uncertain_tip": "WashData har ännu inte bestämt sig för ett program. Siffran visar hur ofta den ledande gissningen, {top}, var rätt i tidigare cykler när den ledde med så här mycket. Endast information." }, "suggestion": { - "both_agree": "WashData rekommenderar", - "calibrated_label": "Kalibrerat", "observed_label": "Observerat", "impact": { "auto_label_confidence": { @@ -1814,10 +1718,6 @@ "higher": "Avslutar bara cykler med betydande energiförbrukning", "lower": "Avslutar även kortare eller energisnålare cykler" }, - "end_repeat_count": { - "higher": "Slutsignalen måste upprepas fler gånger – robustare, något långsammare", - "lower": "Avslutar efter färre upprepningar – snabbare detektering, mer risk för falskt slut" - }, "learning_confidence": { "higher": "Lär sig bara från mycket säkra matchningar – långsammare men mer tillförlitligt", "lower": "Lär sig från fler cykler – modellen kan bli mer oprecis" @@ -1856,40 +1756,25 @@ } }, "reason": { - "auto_label_confidence": "Konfidens vid 15:e percentilen för {cycles} automatiska etiketter som användaren aldrig korrigerade ({p15}).", "completion_min_seconds": "Hälften av p05-varaktigheten för rena cykler ({minutes} min) över {cycles} cykler; filtrerar bort spökcykler.{excl}", - "dead_zone_batch": "Hålls kort (median för uppstartsinstabilitetsfönstret över {cycles} cykler = {s}s) så att ett verkligt slut upptäcks snabbt.", - "dead_zone_from_dip": "Baserat på ett tidigt effektfall som upptäcktes vid {s}s.", - "end_energy_default": "Standardrekommenderad baslinje för brusgränsen vid cykelslut.", - "end_energy_false": "p95 för energi vid falska slut ({p95}Wh) över {cycles} cykler, med golv på 0.2% av medianens cykelenergi ({floor}Wh).", - "end_energy_no_false": "Inga falska slut över {cycles} cykler; med golv på 0.2% av medianens cykelenergi ({floor}Wh).", - "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.", + "match_interval_floored": "Det kortaste programmet ({shortest}s) skulle begränsa detta till {cap}s, under minimivärdet {minimum}s, så minimivärdet används: {persistence} matchningar i följd tar {budget}s, över {pct}% av det programmet. Matchningen körs bara när ett mätvärde kommer (median={median}s), så det kostar inget.", "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.", "min_off_gap": "Baserat på {gaps} observerade mellanrum mellan cykler (p05={p05}s). Enhetsgolv: {floor}s.", "min_off_gap_bridge": "Dimensionerad för att överbrygga den längsta tysta perioden inom en cykel: {span}s + 60s buffert, från {spans} överbryggade mellanrum över {cycles} rena cykler (håller sig under ditt kortaste mellanrum på {ceiling}s mellan två körningar).", "min_power": "40% av den lägsta aktiva effekten vid p05 ({p05}W) över {cycles} rena cykler, vilket håller av-tröskeln under verklig förbrukning.{excl}", - "ml_auto_label": "Lägsta konfidens som kvalitetsmodellen fortfarande bedömde som ren (p10 av {cycles} rena cykler = {p10}).", - "ml_end_repeat": "{false}/{total} cykler ({pct}%) hade en paus som slutdetektorn bedömde till >50%; kräver {val} slutbekräftelse(r).", - "ml_off_delay": "Pauser bekräftade av slutdetektorn: p95 {p95}s + 60s buffert, från {pauses} modellverifierade pauser över {cycles} cykler (golv {floor}s).", "no_update_timeout": "Baserat på observerad uppdateringstakt (p95={p95}s) * 20 (min 60s).", "off_delay_cadence": "Baserat på observerad uppdateringstakt (p95={p95}s) * 5", "off_delay_device_floor": "Använde ett enhetsspecifikt säkert minimum för {device} ({floor}s).", "off_delay_generic_floor": "Använde ett generiskt säkert minimum ({floor}s).", "off_delay_pauses": "Dimensionerad för att vara längre än verkliga pauser: p95 för pauser inom cykeln {p95}s + 60s buffert, från {pauses} pauser över {cycles} rena cykler (golv {floor}s).", - "profile_match_threshold": "p10-konfidens för {cycles} korrekta automatiska etiketter ({p10}); säkert golv för direktmatchning.", - "sampling_interval": "Median för uppdateringsintervallet observerat över {cycles} rena cykler ({si}s).{excl}", - "smoothing_window": "Dimensionerad för att jämna ut ~30s avläsningar vid {si}s samplingsintervall ({samples} sampel).", - "start_duration": "Hålls kort (~ett samplingsintervall på {si}s) så att detekteringen börjar så tidigt som möjligt men fortfarande ignorerar enstaka toppar.", - "thr_batch": "Hålls precis över den lägsta aktiva effekten vid p05 över {cycles} cykler ({p05}W) så att en start fångas så tidigt som möjligt och stopptröskeln stannar under maskinens lägsta driftseffekt.", - "tol_per_profile": "p75 för varaktighetsvariansen per profil över {profiles} profiler ({cycles} cykler); snäva profiler bestraffas inte.", - "tol_pooled": "Baserat på sammanslagen varaktighetsvarians för {cycles} senaste märkta cykler (p95 avvikelse={dev}).", - "watchdog": "Hålls så lågt som är säkert (strax över p95-uppdateringsmellanrummet på {p95}s och minst 2x samplingsintervallet på {median}s, min. 30s) så att stillestånd upptäcks snabbt utan falska stopp." + "standby_floor": "Varje nylig cykel drog fortfarande {idle}W när den avslutades, så ett tröskelvärde under det kan aldrig registrera att apparaten slår av sig. Hålls ovan det; de {cycles} rena cyklerna i registret pausade aldrig under det i mer än {pause}s.", + "thr_batch": "Strax över den lägsta aktiva effekten (p05) över {cycles} cykler ({p05}W), så att starter fångas tidigt och stopptröskeln stannar under den lägsta drifteffekten.", + "watchdog": "Hålls så lågt som är säkert (strax över p95-uppdateringsmellanrummet på {p95}s och minst 2x samplingsintervallet på {median}s, min. 30s) så att stillestånd upptäcks snabbt utan falska stopp.", + "end_energy_floor": "Höjd till den nedre gräns som stopptröskeln ({stop} W) under avfördröjningen ({off_delay} s) ger; lägre än så kan cykeln bara avslutas via en reservväg.", + "max_duration_ratio_default": "Tillbaka till standardvärdet: ett lägre tak avvisar rätt program så fort en körning tar längre tid än vanligt.", + "min_duration_ratio_default": "Tillbaka till standardvärdet: ett högre golv gör att en pågående cykel inte känns igen förrän den har kommit en bra bit." }, "exclusions": { "summary": "Uteslöt {total} feldetekterade cykler: {parts}.", @@ -1907,7 +1792,8 @@ "no_power": "ingen effekt", "no_active_power": "ingen aktiv effekt" } - } + }, + "now_value": "(nu {v})" }, "tab": { "advanced": "Avancerat", @@ -1937,6 +1823,11 @@ "wrong_profile": "Fel profil" }, "toast": { + "task_name_required": "Ge uppgiften ett namn", + "task_added": "Uppgift tillagd", + "task_save_failed": "Kunde inte spara uppgiften: {error}", + "task_removed": "Uppgift borttagen", + "maint_logged": "Loggad som klar: {task}", "catalog_refreshed": "Gemenskapskatalogen har uppdaterats", "access_saved": "Åtkomstkontroll sparad", "all_wiped": "All data rensad", @@ -1944,7 +1835,6 @@ "auto_label_complete": "Automatisk märkning klar", "automation_deleted": "Automatisering borttagen", "correction_submitted": "Korrigering skickad", - "cycle_deleted": "Cykel borttagen", "cycle_force_stopped": "Cykel tvångsstoppad", "cycle_labelled": "Cykel märkt", "cycle_paused": "Cykel pausad", @@ -1960,11 +1850,9 @@ "group_deleted": "Grupp borttagen", "group_name_required": "Gruppnamn krävs", "group_saved": "Grupp sparad", - "import_successful": "Import lyckades; integration laddas om", "json_required": "JSON-data krävs", "legacy_removed": "Äldre åtgärder borttagna", "logs_exported": "Loggar exporterade", - "matching_reverted": "Matchningsvikter återställda till standardvärden", "min_2_profiles": "Välj minst 2 profiler för en grupp", "ml_training_no_improvement": "Träning klar: baslinjen behålls (ingen förbättring)", "models_reverted": "On-device-modeller återställda till baslinjen", @@ -1974,8 +1862,8 @@ "pg_preset_loaded": "Förinställningen \"{name}\" har laddats", "pg_preset_saved": "Förinställningen \"{name}\" har sparats", "pg_sugg_loaded": "{n} föreslagna värde(n) inlästa - kör testytan för att jämföra", - "pg_sugg_ml_loaded": "{n} ML-kalibrerade värde(n) inlästa - kör testytan för att jämföra", "phase_name_required": "Fasnamn krävs", + "phase_reset": "\"{name}\" återställd till den inbyggda fasen", "phase_updated": "Fas uppdaterad", "phases_saved": "Faser sparade", "preferences_saved": "Inställningar sparade", @@ -2022,14 +1910,10 @@ "store_name_required": "Ange ett profilnamn", "store_import_failed": "Importen misslyckades: {error}", "store_imported": "Importerad till {profile}", - "store_program_required": "Ange ett programnamn", "store_no_appliance": "Ange apparatens märke och modell i Inställningar först.", "store_share_failed": "Delningen misslyckades: {error}", "store_shared": "Delad till gemenskapsbutiken, väntar på granskning.", "appliance_added": "Apparat tillagd, väntar på godkännande", - "thanks_confirming": "Tack för bekräftelsen", - "device_approved": "Godkänd av gemenskapen", - "rating_saved": "Kvalitetsbetyg sparat", "brand_added": "Märke tillagt, väntar på godkännande", "profile_added": "Profil tillagd, väntar på godkännande", "saved_except_conflicts": "Sparat. Åtgärda de markerade konflikterna för att spara resten.", @@ -2053,13 +1937,58 @@ "odometer_save_failed": "Kunde inte spara totalen: {error}", "contrib_device_changed": "Tillagd i gemenskapsbutiken, men apparaten byttes medan formuläret var öppet. Välj den i Inställningar.", "store_pick_profile_share": "Välj en profil att dela till", - "remove_failed": "Borttagning misslyckades: {error}" + "remove_failed": "Borttagning misslyckades: {error}", + "analysis_complete": "Analys klar: {count} förslag", + "analysis_failed": "Analys misslyckades: {error}", + "auto_detect_failed": "Autoidentifiering misslyckades: {error}", + "auto_label_failed": "Automatisk märkning misslyckades: {error}", + "auto_label_done": "Automärkning klar: {count} cykler märkta, {skipped} utan märkning.", + "auto_label_done_one": "Automärkning klar: {count} cykel märkt, {skipped} utan märkning.", + "auto_label_done_other": "Automärkning klar: {count} cykler märkta, {skipped} utan märkning.", + "auto_label_cancelled": "Automärkning avbruten efter att {count} cykler märkts.", + "auto_label_cancelled_one": "Automärkning avbruten efter att {count} cykel märkts.", + "auto_label_cancelled_other": "Automärkning avbruten efter att {count} cykler märkts.", + "could_not_load_cycle": "Kunde inte läsa in cykel: {error}", + "create_failed": "Skapande misslyckades: {error}", + "cycles_deleted": "Borttagna cykler: {count}", + "debug_cleared": "Rensade felsökningsspår: {count}", + "discard_failed": "Kassering misslyckades: {error}", + "error": "Fel: {error}", + "export_failed": "Export misslyckades: {error}", + "import_failed": "Importen misslyckades: {error}", + "invalid_json": "\"{key}\" är inte giltig JSON - rätta det eller töm fältet innan du sparar.", + "label_failed": "Märkning misslyckades: {error}", + "ml_training_promoted": "Träning klar: {count} modell(er) aktiverade", + "pause_failed": "Paus misslyckades: {error}", + "phase_created": "Fasen \"{name}\" skapad", + "phase_deleted": "Fasen \"{name}\" borttagen", + "profile_created": "Profilen \"{name}\" skapad", + "rename_failed": "Namnbyte misslyckades: {error}", + "save_failed": "Sparning misslyckades: {error}", + "split_complete": "Delad i {count} cykler", + "start_failed": "Start misslyckades: {error}", + "stop_failed": "Stopp misslyckades: {error}", + "update_failed": "Uppdatering misslyckades: {error}", + "store_download_skipped": "{n} inspelning(ar) överhoppade: för korta, med för många luckor eller redan på din apparat.", + "store_import_low_quality": "Den här inspelningen är för kort, har för många luckor eller är orimlig och kan inte importeras.", + "store_import_duplicate": "Den här inspelningen finns redan på din apparat.", + "device_approved": "Godkänd av gemenskapen", + "thanks_confirming": "Tack för bekräftelsen", + "rating_saved": "Kvalitetsbetyg sparat", + "store_download_partial": "{n} program kunde inte laddas ned; försök igen senare.", + "import_no_restore_point": "Importerat, men ingen återställningspunkt kunde sparas, så den här importen kan inte ångras", + "import_undo_failed": "Det gick inte att ångra importen: {error}", + "import_undone": "Import ångrad: {profiles} profil(er) och {cycles} cykel/cykler återställda" }, "trend": { "down": "Nedåtgående trend", "up": "Uppåtgående trend" }, "maint": { + "salt": "Fyll på salt", + "rinse_aid": "Fyll på spolglans", + "lint_filter": "Rengör luddfilter", + "condenser_clean": "Rengör kondensor", "bearing_service": "Lagerservice", "descale": "Avkalka", "drum_clean": "Rengör trumma", @@ -2071,12 +2000,14 @@ "status_tip": "Cykelresultat: Slutförd (naturligt slut), Avbruten (plötsligt effektfall), Tvångsstoppad (manuellt) eller Behöver granskning (feedback väntar).", "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, 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.", + "energy_tip": "Total förbrukad energi (kWh), beräknad från effekt över tid.", + "cost_tip": "Energikostnad för den här cykeln, låst när den avslutades. Med Tidsviktad kostnad på debiteras varje ögonblick till sitt eget pris; annars till priset vid slutet. Ange ett pris i Inställningar för att fylla i detta.", "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.", - "cost_dynamic_tip": "Tidsviktad: debiterad till det pris som gällde under cykeln (effektivt {price} {cur}/kWh)." + "cost_dynamic_tip": "Tidsviktad: debiterad till det pris som gällde under cykeln (effektivt {price} {cur}/kWh).", + "dbg_conf": "Konf.", + "dbg_mae": "MAE", + "dbg_corr": "Korr." }, "stat": { "avg": "snitt", @@ -2100,7 +2031,10 @@ "notify_finish": "En klaravisering skulle skickas.", "notify_milestone": "En milstolpsavisering skulle skickas.", "notify_held": "En avisering hölls tillbaka på grund av tysta timmar.", - "finished": "Cykeln nådde ett sluttillstånd och avslutades." + "finished": "Cykeln nådde ett sluttillstånd och avslutades.", + "match_reverted": "Programmet föll tillbaka till detektering: dess poäng sjönk långt under sin egen topp.", + "verified_pause": "Den pågående justeringen bekräftade (eller släppte) en paus inne i programmet. Så länge den gäller avslutas inte cykeln.", + "stalled": "Cykeln låg på standbyförbrukning längre än det här programmet någonsin pausar där, med arbete kvar. Visas som Pausad (Avstannad); cykeln förblir öppen." }, "store": { "search_ph2": "Sök efter märke eller modell…", @@ -2109,11 +2043,7 @@ "programs_count": "Program: {n}", "browse": "Bläddra", "device": "Enhet", - "favorites": "Favoriter", - "search_ph": "Sök efter märke eller modell…", "no_results": "Inga matchande apparater hittades. Prova en annan sökning.", - "n_programs": "{n} program", - "n_cycles": "{n} cykler", "no_programs": "Inga delade program för den här apparaten ännu.", "no_cycles": "Inga referenscykler delade för det här programmet ännu.", "anon": "anonym", @@ -2125,19 +2055,21 @@ "merge_target": "Slå samman med profil", "description": "Beskrivning (valfritt)", "contributed_by": "av {name}", + "website": "Gemenskapsbutikens webbplats ↗", + "website_tip": "Öppna gemenskapsbutikens webbplats", + "favorites": "Favoriter", "rating_summary": "★ {avg} ({n})", "no_ratings": "Inga betyg ännu", - "website": "Gemenskapsbutikens webbplats ↗", - "website_tip": "Öppna gemenskapsbutikens webbplats" + "unreachable": "Kunde inte nå gemenskapsbutiken. Kontrollera din anslutning och försök igen." }, "settings": { "gear": { - "title": "Inställningar" + "title": "Preferenser" } }, "link": { - "manual": "Manual ↗", - "add_appliance": "Lägg till din apparat" + "maintenance_supporter": "Maintenance Supporter på GitHub ↗", + "manual": "Manual ↗" }, "tip": { "add_brand": "Märke inte med i listan? Lägg till det i gemenskapskatalogen", @@ -2171,7 +2103,13 @@ "rebuild": { "envelopes": "Bygger om enveloppar" }, - "cancelling": "Avbryter..." + "cancelling": "Avbryter...", + "auto_label": { + "running": "Märker cykler automatiskt" + }, + "store_download": { + "running": "Laddar ned från butiken" + } }, "setup": { "hdr": { @@ -2180,26 +2118,24 @@ }, "phase0": { "washer": "WashData identifierar redan dina cykler. Spela in din första cykel för att aktivera programnamn och tidsuppskattningar.", - "dishwasher": "WashData observerar. Diskmaskiner har komplexa cykler – att spela in din första cykel rekommenderas starkt. Om en identifierad cykel körs för länge, använd cykeleditorn för att trimma den innan du sparar den som en profil.", + "dishwasher": "WashData bevakar. Diskcykler är komplexa, så spela in din första cykel. Om en identifierad cykel pågår för länge, trimma den i cykeleditorn innan du sparar den som en profil.", "generic": "WashData observerar. Spela in eller märk en identifierad cykel för att börja bygga profiler." }, "phase1a": { - "labelled": "Bra start – ditt första program är sparat. För renast möjliga data kan du spela in din nästa cykel med inspelningswidgeten." + "labelled": "Bra start: ditt första program är sparat. För renast data, spela in din nästa cykel med inspelaren." }, "phase1b": { - "recorded": "Din inspelning sparades som {profile_name}. Spela nu in eller märk dina andra vanliga program för att bygga upp täckning." + "recorded": "Din inspelning sparades som {profile_name}. Spela nu in eller märk dina andra vanliga program." }, "phase1c": { - "verify": "Du har {count} program från gemenskapen. Kör en cykel för att verifiera att WashData identifierar den rätt – matchningen förbättras ju mer historik din enhet bygger upp." + "verify": "Du har {count} program från gemenskapen. Kör en cykel för att kontrollera att WashData känner igen den; matchningen blir bättre i takt med att din enhet bygger upp egen historik." }, "phase2": { - "cluster": "WashData har sett {count} cykler som inte matchar något sparat program – de liknar varandra. Vill du skapa en ny profil för dem?", + "cluster": "WashData har sett {count} liknande cykler som inte matchar något sparat program. Skapa en ny profil för dem?", "unmatched": "Din senaste cykel matchade inget sparat program. Är det ett nytt program?" }, "phase3": { - "suggestions": "WashData har inställningsrekommendationer baserade på din cykelhistorik – granska dem för att förbättra detekteringens noggrannhet.", - "groups": "Några av dina profiler ser ut som samma program vid olika temperaturer. Organisera dem i en grupp för bättre matchning.", - "phases": "Lägg till programfaser i {profile_name} för mer exakta uppskattningar av återstående tid." + "suggestions": "WashData har inställningsförslag utifrån din cykelhistorik. Granska dem för att förbättra detekteringen." }, "phase4": { "healthy": "Den här enheten är helt konfigurerad ({profile_count} profiler)." @@ -2212,12 +2148,58 @@ "create_from_cluster": "Skapa profil från dessa cykler", "create_profile": "Skapa en profil för det", "review_suggestions": "Granska förslag", - "organise_profiles": "Organisera profiler", - "configure_phases": "Konfigurera faser", "skip_step": "Hoppa över det här steget", "skip_forever": "Visa inte igen", "hide_guidance": "Dölj vägledning", "show_guidance": "Visa vägledning" } + }, + "access": { + "edit": "Redigera", + "full": "Fullständig", + "inherit": "Ärv", + "none": "Ingen (dold)", + "read": "Läs" + }, + "pg_label_reason": { + "no_winner": "Inget program passade", + "below_floor": "Under inlärningskonfidensen", + "ambiguous": "Flaggad som tvetydig", + "margin": "För nära tvåan", + "unknown_profile": "Programmet finns inte längre", + "not_evaluated": "Inte utvärderad" + }, + "diagram": { + "accum": "ackumulerad", + "auto": "auto", + "below_off": "under = av", + "confirmed": "bekräftad", + "energy_counts": "energi räknas", + "feedback": "feedback", + "gap_below_min": "lucka under min = en cykel", + "heat_phase": "värmefas", + "held_at_100": "hålls på 100%", + "ignored": "ignorerad", + "kept": "behållen", + "max": "max", + "min": "min", + "minus_tol": "-tol", + "no_match": "ingen matchning", + "no_updates": "inga uppdateringar", + "off_delay_wait": "avfördröjning", + "off_gap": "avlucka", + "plus_tol": "+tol", + "profile": "profil", + "reading_interval": "typiskt mätintervall", + "sensor_offline": "sensor offline: tvångsstopp", + "si": "SI", + "spike_ignored": "topp ignorerad", + "spike_ignored_short": "topp: ignorerad", + "start": "start", + "stop": "stopp", + "tail_energy_resets": "restenergi > tröskel: timern nollställs", + "thr": "trösk.", + "too_short": "för kort", + "tumble_kept": "tumlingspulser behålls" } } diff --git a/custom_components/ha_washdata/translations/panel/tr.json b/custom_components/ha_washdata/translations/panel/tr.json index 123b41dd..ea707ea0 100644 --- a/custom_components/ha_washdata/translations/panel/tr.json +++ b/custom_components/ha_washdata/translations/panel/tr.json @@ -3,6 +3,7 @@ "min": "dk", "entity_not_found": "Varlık bulunamadı", "running_long": "Uzun sürüyor", + "stalled": "Takıldı", "remaining": "kalan", "progress": "ilerleme", "show_time_remaining": "Kalan süreyi göster", @@ -48,16 +49,16 @@ } }, "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_tip": "Sona doğru bu program yaklaşık {mins} dk sessizleşir, ardından {secs} sn boyunca yaklaşık {watts} W çeker. Ölçülen {measured} döngünün {seen} tanesinde görüldü.", "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", + "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", + "artifact_tip_one": "{n} anormallik algılandı (ör. döngünün ortasında kapının açılması) - grafikte görmek için açın", + "artifact_tip_other": "{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", "declining": "↘ düşüyor", "energy_low": "Normalden düşük enerji", "energy_spike": "Normalden yüksek enerji", - "fair_fit": "kabul edilebilir kalite", - "fair_fit_tip": "Orta düzeyde eşleşme tutarlılığı – bu profile atanan bazı döngülerin güven puanları daha düşüktür. Doğruluğu artırmak için daha fazla döngü etiketleyin veya profili yeniden kaydedin.", "feedback_requested": "Geri bildirim istendi", "golden_cycle": "Kaydedilen referans döngüsü", "duration_outlier": "{n} olağandışı uzunlukta döngü", @@ -66,27 +67,36 @@ "needs_review": "İncelenmesi gerekiyor", "overrun": "Normalden daha uzun sürdü", "poor_fit": "düşük kalite", - "poor_fit_tip": "Tutarsız eşleşme geçmişi – bu profili yeniden oluşturmayı düşünün", - "restart_gap_tip": "{n} HA yeniden başlatma boşluğu: güç izinde bir delik var", + "restart_gap_tip": "{n} HA yeniden başlatma boşluğu: güç izinde delikler var", + "restart_gap_tip_one": "{n} HA yeniden başlatma boşluğu: güç izinde bir delik var", + "restart_gap_tip_other": "{n} HA yeniden başlatma boşluğu: güç izinde delikler var", "reviewed": "İncelendi", "steady": "→ sabit", "underrun": "Normalden daha hızlı bitti", - "using_defaults": "Varsayılan değerler kullanılıyor", - "using_tuned": "Ayarlanmış ağırlıklar kullanılıyor", "overrun_ratio": "({x}x beklenen)", "underrun_ratio": "(beklenenin %{pct}'i)", - "awaiting_tip": "Topluluk onayı bekleniyor", - "awaiting_n": "Onay bekleniyor · {n} onaylandı", - "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.", - "armed": "(sonraki döngüye uygulanır)" + "armed": "(sonraki döngüye uygulanır)", + "awaiting_tip": "Topluluk onayı bekleniyor", + "awaiting_n": "Onay bekleniyor · {n} onaylandı", + "awaiting": "Onay bekleniyor", + "approved": "Onaylandı", + "matcher_unused": "Henüz döngülerinizle eşleşmedi", + "matcher_used": "Döngülerinizden {n} tanesiyle eşleşti", + "matcher_used_one": "Döngülerinizden {n} tanesiyle eşleşti", + "matcher_used_other": "Döngülerinizden {n} tanesiyle eşleşti", + "matcher_used_tip": "WashData'nın bu cihazda kendi başına bu içe aktarılan programla eşleştirdiği döngüler. Hiç eşleşmeyen bir içe aktarma cihazınıza uymuyor olabilir; kaldırabilirsiniz. Yerel olarak sayılır; mağazaya hiçbir şey gönderilmez." }, "btn": { + "pg_preset_delete": "Ön ayarı sil", + "pg_preset_load": "Ön ayarı yükle", + "pg_preset_load_tip": "Test Alanı değerlerini bu ön ayarla değiştirir (etkin ayarlara dokunulmaz)", + "pg_preset_save": "Ön ayar olarak kaydet", + "pg_preset_save_tip": "Aşağıdaki tüm değerleri bu cihaz için adlandırılmış bir ön ayar olarak kaydeder", "reset_zoom": "Yakınlaştırmayı sıfırla", "set_brand_model": "Marka ve modeli ayarla", "refresh_catalog": "Kataloğu yenile", @@ -97,12 +107,11 @@ "add_timer": "+ Zamanlayıcı ekle", "apply_all": "Tümünü uygula", "apply_label": "Etiket Uygula", - "apply_set_b": "B setini uygula", "apply_split": "Bölmeyi Uygula", "apply_trim": "Kırpmayı Uygula", "auto_detect_split": "Otomatik algıla", "auto_label_cycles": "Döngüleri otomatik etiketle", - "auto_label_cycles_tip": "Eşleşme güveni eşiği geçen etiketsiz döngülere otomatik olarak profil adları atayın", + "auto_label_cycles_tip": "Eşleşme güveni eşiği geçen etiketsiz döngüleri adlandırın.", "cancel": "İptal", "clear_debug": "Hata Ayıklama Verilerini Temizle", "clear_splits": "Temizle", @@ -112,8 +121,6 @@ "convert_to_automation": "Otomasyona dönüştür", "correct": "Düzelt…", "create": "Oluştur", - "create_from_cluster": "{count} döngüden profil oluştur", - "create_profile": "+ Profil oluştur", "delete": "Sil", "delete_group": "Grubu Sil", "delete_group_tip": "Yalnızca bu grubu sil - üye profilleri korunur", @@ -124,22 +131,19 @@ "done": "Tamam", "edit": "Düzenle", "export": "Dışa Aktar", - "export_json": "JSON olarak dışa aktar", "force_stop": "Zorla Durdur", "force_stop_tip": "Geçerli döngüyü hemen sonlandırın ve zorla durduruldu olarak işaretleyin", "from_template": "Şablondan ▾", "ignore": "Yoksay", "import_json": "JSON'dan içe aktar", - "import_overwrite": "İçe aktar (veriyi üzerine yazar)", "inspect": "İncele", "label": "Etiketle", - "load": "Yükle", "load_more": "Daha fazla yükle", "manage": "Yönet", "merge": "Birleştir", "new_automation": "+ Yeni Otomasyon", "new_group": "+ Yeni Grup", - "new_group_tip": "Neredeyse aynı profilleri (aynı şekil/süre, farklı sıcaklık veya sıkma) gruplayın; böylece eşleştirici aralarında güvenilir biçimde seçim yapar", + "new_group_tip": "Neredeyse aynı profilleri (aynı şekil, farklı sıcaklık veya sıkma) gruplayın; böylece eşleştirme onları ayırt edebilir.", "new_phase": "+ Yeni Aşama", "new_profile": "+ Yeni Profil", "new_profile_tip": "Mevcut etiketlenmiş bir döngüden veya kayıttan yeni bir program profili oluşturun", @@ -147,36 +151,25 @@ "on_cycle_started": "Döngü başladığında", "pause_cycle": "Duraklat", "pause_cycle_tip": "Çalışan döngüyü duraklatın – cihaz kaldığı yerden devam edecek", - "pg_apply_device": "Cihaza uygula", - "pg_autofill": "Otomatik doldur", - "pg_load_calibrated": "Kalibre edilmiş (ML) yükle ({n})", - "pg_load_calibrated_tip": "ML ile kalibre edilmiş ayarları Test Alanı geçersiz kılmaları olarak yükler", "pg_load_live": "Etkin ayarları yükle", "pg_load_live_tip": "Entegrasyonun geçerli ayarlarını yeniden okur ve Test Alanı'ndaki tüm düzenlemeleri atar", "pg_load_suggested": "Önerileri yükle ({n})", "pg_load_suggested_tip": "Otomatik ayarlayıcının geçerli önerilerini Test Alanı geçersiz kılmaları olarak yükler", - "pg_preset_delete": "Ön ayarı sil", - "pg_preset_load": "Ön ayarı yükle", - "pg_preset_load_tip": "Test Alanı değerlerini bu ön ayarla değiştirir (etkin ayarlara dokunulmaz)", - "pg_preset_save": "Ön ayar olarak kaydet", - "pg_preset_save_tip": "Aşağıdaki tüm değerleri bu cihaz için adlandırılmış bir ön ayar olarak kaydeder", "pg_publish_all": "{n} ayarı entegrasyona uygula", "pg_publish_one": "{label} ayarını entegrasyona uygula", - "play": "Oynat", "process": "İşle", "process_history": "Şimdi İşle", "process_recording": "Kaydı Kaydet", "process_tip": "Kaydedilen izi yeni veya mevcut bir profil olarak kaydedin", "rebuild": "Zarfları Yeniden Oluştur", "rebuild_envelope": "Zarfı Yeniden Oluştur", - "rebuild_tip": "Tüm profiller için etiketlenmiş döngülerinden beklenen güç zarfını (min/maks bandı) yeniden hesaplayın – yeni döngüler etiketledikten veya eskilerini düzelttikten sonra çalıştırın", + "rebuild_tip": "Her profilin beklenen güç bandını etiketli döngülerinden yeniden oluşturun. Döngüleri etiketledikten veya düzelttikten sonra çalıştırın.", "rec_start_tip": "Cihazın güç izini kaydetmeye başlayın – döngü çalıştırmadan hemen önce başlatın", "rec_stop": "Durdur", "rec_stop_tip": "Kaydı durdurun ve yakalanan izi inceleme için saklayın", "record": "Kayda Başla", "refresh": "Yenile", "refresh_diag_tip": "Depolama istatistiklerini yeniden yükleyin", - "refresh_logs_tip": "En son günlük kayıtlarını yeniden yükleyin", "refresh_settings_tip": "Ayarları sunucudan yeniden yükleyin", "relabel": "Yeniden etiketle ({count})", "remove": "Kaldır", @@ -184,7 +177,6 @@ "reset": "Sıfırla", "reset_ml_models_tip": "İnce ayarlı modelleri atın ve yerleşik modellere geri dönün. WashData bunları daha sonra yeniden öğrenebilir.", "reset_to_builtin": "Yerleşik modellere sıfırla", - "reset_to_defaults": "Varsayılanlara sıfırla", "resume_cycle": "Döngüyü sürdür", "resume_cycle_tip": "Duraklatılmış döngüyü sürdürün", "revert_settings": "Değişiklikleri geri al", @@ -197,8 +189,6 @@ "run_analysis": "🔍 Öneri analizi çalıştır", "run_analysis_tip": "Kaydedilen döngülerinizi şimdi analiz edin ve ayar önerilerini yenileyin", "run_auto_label": "Otomatik Etiketlemeyi Çalıştır", - "run_both": "A ile B'yi çalıştır", - "run_simulation": "Simülasyonu çalıştır", "save": "Kaydet", "save_access_control": "Erişim Denetimini Kaydet", "save_group": "Grubu Kaydet", @@ -210,10 +200,8 @@ "save_settings": "Ayarları Kaydet", "select": "Seç", "select_all": "Tümünü seç", - "select_last20": "Son 20", "select_none": "Temizle", "show_only": "Yalnızca göster", - "skip_setup": "Kurulumu atla", "split": "Böl", "stop": "Durdur", "submit_correction": "Düzeltme Gönder", @@ -222,36 +210,31 @@ "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.", + "unlabel_selected_tip": "Seçili döngülerden etiketi kaldırın. Geçmişinizde kalırlar ama artık bu profili şekillendirmezler.", "use": "Kullan", "wipe_all": "Tüm Verileri Sil", "delete_selected": "Seçilenleri sil ({n})", - "group_suggest": "Grup {n}: {members}", "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", "import": "İçe aktar", "disconnect": "Bağlantıyı kes", "connect_github": "GitHub'a bağlan", - "save_appliance": "Cihazı kaydet", "share_to_store": "Mağazada paylaş", "share": "Paylaş", - "confirm_appliance": "Bu cihazı onayla", - "confirmed": "Onaylandı", "browse_community_setups": "Topluluk kurulumlarına göz at", "download_device": "Bu kurulumu indir", "share_device": "⬆ Bu cihazı paylaş", - "share_device_tip": "Bu cihazı ve kayıtlı referans döngülerini topluluk mağazasına paylaşın; aynı makineye sahip diğer kullanıcılar bunları benimseyebilsin", + "share_device_tip": "Bu cihazı ve referans döngülerini topluluk mağazasında paylaşın; aynı makinenin sahipleri bunları benimseyebilsin.", "share_n": "{n} döngü paylaş", "export_selected": "Dışa aktar (veri seç)", "export_all": "Her şeyi hızlı dışa aktar", - "import_raw": "Gelişmiş: JSON'dan tümünü değiştir", "download_export": "Dışa aktarmayı indir", "analyze_import": "Dosyayı analiz et", "import_selected": "Seçileni içe aktar", "back": "Geri", "mute_suggestion": "Bu ayarı önerme", "reset_muted": "Susturulanları sıfırla", + "reset_phase_tip": "Düzenlemenizi geri alın ve yerleşik aşamayı geri yükleyin", "import_power_history": "Güç geçmişini içe aktar", "hist_read_recorder": "Home Assistant'tan oku", "hist_scan": "Döngüleri tara", @@ -260,7 +243,15 @@ "save_total_cycles": "Toplamı Kaydet", "clear": "Temizle", "add_brand": "Marka ekle", - "add_appliance": "Cihaz ekle" + "log_done_task": "Yapıldı olarak kaydet: {task}", + "add_task": "Görev ekle", + "add_appliance": "Cihaz ekle", + "retry": "Yeniden dene", + "confirm_appliance": "Bu cihazı onayla", + "confirmed": "Onaylandı", + "create_profile_from_gap": "Profil oluştur", + "undo_import": "Son içe aktarmayı geri al", + "pg_rerun": "Simülasyonu güncelle" }, "note": { "off_delay": { @@ -268,10 +259,6 @@ } }, "conflict": { - "anti_wrinkle_exit": { - "exit": "Durdurma Eşiğinin altında olmalıdır ({stop} W); aksi takdirde kırışıklık önleme çıkış gücü göz ardı edilir", - "stop": "Kırışıklık Önleme Çıkış Gücü'nün üzerinde olmalıdır ({exit} W)" - }, "anti_wrinkle_max": { "max": "Başlangıç Eşiğinin üzerinde olmalıdır ({start} W); aksi takdirde kırışıklık önleme süre sınırı atlanır", "start": "Kırışıklık Önleme Maks. Gücü'nün altında olmalıdır ({max} W)" @@ -279,10 +266,10 @@ "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." + "power_sensor": "Bu, cihazın Güç Sensörü; fiyat değil. Döngüler kWh başına watt ile ücretlendirilirdi; bu alanı temizleyin.", + "energy_sensor": "Bu, cihazın Enerji Sayacı; fiyat değil. Döngüler kWh başına sayaç okumasıyla ücretlendirilirdi; bu alanı temizleyin.", + "device_class": "Bu sensör kWh başına fiyat değil, {dc} ölçer. Bu alanı temizleyin veya bir tarife sensörü seçin.", + "unit": "Bu sensör kWh başına fiyat değil, {unit} biriminde okur. Bu alanı temizleyin veya bir tarife sensörü seçin." }, "settings_banner": "Ayar çakışmaları: {n}. Vurgulanan bölümleri kontrol edin ve kaydetmeden önce düzeltin.", "settings_banner_btn": "İlkine git", @@ -317,8 +304,8 @@ "threshold": "Etkili olabilmesi için Durdurma Eşiğinin altında olmalıdır ({stop} W)" }, "pump_stuck": { - "duration": "Pompa takılma alarmı bekçi döngüyü öldürmeden önce tetiklenecek şekilde Güncelleme Yok Zaman Aşımından ({to} s) küçük olmalıdır", - "timeout": "Pompa takılma alarmı döngü zorla durdurulmadan önce tetiklenecek şekilde Pompa Takılma Süresini ({ps} s) aşmalıdır" + "duration": "Takılma alarmı önce tetiklenebilsin diye Güncelleme Yok Zaman Aşımı ({to} s) değerinden küçük olmalı", + "timeout": "Takılma alarmı önce tetiklenebilsin diye Pompa Takılma Süresi ({ps} s) değerinden büyük olmalı" }, "start_dur": { "sampling": "Örnekleme Aralığı, Başlangıç Süresini aşıyor ({sdt} s); tek örnekli ani yükselişler bir döngü açabilir", @@ -334,10 +321,15 @@ "watchdog": { "interval": "En az 2 kat Örnekleme Aralığı olmalıdır ({si} s)", "sampling": "Örnekleme Aralığı, Bekçi Aralığının en fazla yarısı olmalıdır ({wi} s)" + }, + "end_energy": { + "energy": "Kapatma Gecikmesi ({d} s) boyunca Durdurma Eşiği ({w} W) için fazla katı; döngü yalnızca bir yedek yoldan bitebilir", + "stop": "Bitiş Enerjisi ({d} s içinde {e} Wh) yalnızca {p} W değerine izin verir" } }, "hdr": { - "ab_comparison": "A/B Ayar Karşılaştırması", + "custom_tasks": "Kendi görevleriniz", + "maintenance_supporter": "Daha fazlası mı gerekiyor?", "access_control": "Erişim Denetimi", "add_maintenance": "Bakım Kaydı Ekle", "automation_delete": "Bu otomasyonu sil", @@ -346,28 +338,22 @@ "automation_review_this_cycle": "Bu döngüyü incele", "automations": "Otomasyonlar", "clear_debug": "Hata Ayıklama İzlerini Temizle", - "cycle_simulator": "Döngü Simülatörü", "diagnostics_pref": "Tanılamalar", "display": "Görüntü", - "dtw_inspector": "DTW İnceleyici", "export_import": "Dışa / İçe Aktar", - "getting_started": "Başlarken", "live_power": "Canlı Güç", "logs": "Günlükler", - "logs_diagnostics": "Tanılamalar", "maintenance": "Bakım Eylemleri", "maintenance_log": "Bakım Günlüğü", "maintenance_reminders": "Servis Hatırlatıcıları", "manual_recording": "Manuel Kayıt", - "manual_recording_tip": "WashData güç izini kaydederken kasıtlı olarak bir döngü çalıştırın. Cihaz başlamadan hemen önce Başlat'a basın, bittiğinde Durdur'a basın, ardından adlandırılmış profil olarak kaydetmek için İşle'ye tıklayın.", + "manual_recording_tip": "Bir döngüyü bilerek kaydedin: cihaz başlamadan hemen önce Başlat'a, bittiğinde Durdur'a basın, ardından profil olarak kaydetmek için İşle'ye tıklayın.", "ml_learned": "WashData'nın öğrendikleri", - "ml_matching_tuning": "Program eşleştirme ince ayarı", "ml_settings_card": "Ayarlar", "ml_smart_learning": "Akıllı Öğrenme", "my_preferences": "Tercihlerim", "no_automations": "Henüz bu cihaza referans veren otomasyon yok.", "panel_settings": "Panel Ayarları", - "param_sweep": "Parametre taraması", "pg_settings_source": "Ayarların kaynağı", "phase_catalog": "Aşama Kataloğu", "playground": "Deneme Alanı", @@ -376,11 +362,8 @@ "status": "Durum", "status_graph": "Durum Grafiği", "storage_stats": "Depolama İstatistikleri", - "test_history": "Geçmiş üzerinde test et", "toggle_sidebar": "Ev Asistanı kenar çubuğunu aç/kapat", - "tools_and_data": "Araçlar ve Veriler", "wipe_history": "Geçmişi Sil", - "recommendations": "Öneriler ({n})", "pg_detection_params": "Algılama ayarları", "pg_outcome": "Simülasyon sonucu", "pg_across_cycles": "Tüm döngülerinizde", @@ -388,7 +371,8 @@ "online_account": "Topluluk Mağazası ve çevrimiçi özellikler", "import_power_history": "Güç geçmişini içe aktar", "service_status": "Bakım Durumu", - "online_account_tab": "Çevrimiçi ve Topluluk" + "online_account_tab": "Çevrimiçi ve Topluluk", + "live_match_debug": "Canlı Eşleşme Hata Ayıklama" }, "health": { "fair": "Kabul edilebilir profil kalitesi", @@ -396,11 +380,16 @@ "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_tip": "Eşleşen programın kayıtlı eğrisi üzerindeki konum: %{pct}. Döngü profile hizalanarak bulunur, bu yüzden çalışma uzun ya da kısa sürse de doğru kalır. Cihaz sessizken güncellenir, bu yüzden 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", + "task_name": "Görev", + "every_cycles": "Sıklık (döngü)", + "every_days": "Sıklık (gün)", + "new_task_name": "Yeni görev", + "days_since_service": "{since} / {total} gün", "actions": "Eylemler", "activity": "Aktivite", "administrators": "Yöneticiler", @@ -408,10 +397,8 @@ "aria_cycle_chart": "Döngü güç eğrisi", "aria_envelope_chart": "Profil güç zarfı grafiği", "aria_phase_chart": "Aşama düzenleyici grafiği", - "aria_playground_chart": "Sürüklenebilir algılama eşiklerine sahip döngü güç eğrisi", "aria_power_chart": "Güç tüketimi grafiği", "aria_spaghetti_chart": "Üst üste bindirilmiş döngü güç eğrileri", - "aria_sweep_chart": "Parametre tarama sonuçları grafiği", "artifact_anomaly": "Anomali", "artifact_high_power": "Yüksek güç", "artifact_interruption": "Kesinti", @@ -425,150 +412,100 @@ "cohesion_good": "uyum {pct}%", "cohesion_low": "⚠ düşük uyum {pct}%", "compare_profiles": "Profillerle karşılaştır", - "concurrency": "Eşzamanlılık", "confidence": "Güven", "confidence_threshold": "Güven eşiği", "correct_duration": "Doğru Süre (dk, isteğe bağlı)", "correct_profile": "Doğru Profil", - "correlation": "Korelasyon", "cost": "Maliyet", "create_new_profile": "+ Yeni profil oluştur…", "current_phase": "Geçerli aşama", - "curves": "Eğriler", "cycle": "Döngü", "cycle_date_display": "Döngü tarihi görünümü", - "cycle_health": "Döngü sağlığı", - "cycle_index": "döngü dizini →", - "cycles": "Döngüler", "cycles_count": "Döngüler", "cycles_lc": "döngü", - "dashed": "kesikli", "date": "Tarih", "debug_traces": "Hata Ayıklama İzleri", - "default": "Varsayılan", "default_access_level": "Aşağıda listelenmeyen kullanıcılar için varsayılan düzey", "default_other": "Varsayılan (diğer cihazlar)", "default_tab": "Panel açıldığında varsayılan sekme", - "delta": "Δ", "description": "Açıklama", - "dtw_blend_w": "Karışım ağırlığı", - "dtw_blended": "Karışık puan", - "dtw_ddtw": "DDTW puanı", - "dtw_l1": "L1 puanı", - "dtw_matched_profile": "(döngünün eşleşen profili)", "duration": "Süre", - "duration_agreement": "Süre uyumu", - "emphasis": "Vurgu", "enable_access_control": "Kullanıcı bazlı erişim denetimini etkinleştir", "energy": "Enerji", - "energy_agreement": "Enerji uyumu", - "event_timeline": "Olay zaman çizelgesi", "event_type": "Olay türü", "expected": "Beklenen", + "suspected_profile": "{name} (tahmini)", + "runner_up_profile": "{name} (ikinci aday)", "expected_duration": "Beklenen Süre (dk)", "file_kb": "Dosya (kB)", - "final_score": "Nihai puan", "from": "Başlangıç", "gap_s": "Boşluk (s)", "group_name": "Grup adı", "head_trim": "Baştan Kırpma (s)", - "health": "Sağlık", "hide_tabs": "Yönetici olmayanlar için sekmeleri gizle", - "in_use": "Kullanımda", "json_data": "JSON Verisi", "label": "Eşleşen program", "last": "Son", "last_checked": "Son kontrol:", "load_from_file": "Dosyadan yükle", - "mae_score": "MAE puanı", "manual_duration": "Manuel Süre (dk, isteğe bağlı)", "match": "Eşleşme", "members": "Üyeler", - "metric": "Metrik", "mode_existing_profile": "Mevcut Profile Ekle", "mode_new_profile": "Yeni Profil Oluştur", "models_fine_tuned": "(ince ayarlı modeller: {count})", - "n_classic_suggestions": "{n} klasik", - "n_ml_suggestions": "{n} ML", "n_selected": "{n} seçili", "n_tuning_suggestions": "{n} ayar önerisi", - "na": "yok", + "n_tuning_suggestions_one": "{n} ayar önerisi", + "n_tuning_suggestions_other": "{n} ayar önerisi", "new_profile_name": "Yeni profil adı", "notes": "Notlar", - "of": "/", - "outcome": "Sonuç", "panel_default_tab": "Varsayılan sekme", "panel_language": "Panel dili", "per_cycle_short": "döngü", "pg_anti_wrinkle": "Kırışıklık önleme", - "pg_autofill_tip": "Geçerli değerden aralığı otomatik doldur ({lo}–{hi})", "pg_best_value": "Bulunan en iyi değer", "pg_current_value": "Geçerli değer:", "pg_drag_hint": "Grafikteki çizgiyi sürükleyin", - "pg_dtw": "DTW", "pg_ev_ending": "Bitiyor", "pg_ev_running": "Çalışıyor", "pg_group_advanced": "Uç durumlar", "pg_group_detection": "Algılama tetikleyicileri", "pg_group_timing": "Zamanlama kuralları", "pg_idle": "Boşta", - "pg_last_run": "Son çalıştırma: {date}", - "pg_match_score": "Eşleşme puanı", - "pg_pct_of": "{total} içinde %{p}", "pg_poor_match": "Kötü eşleşme", "pg_preset": "Test Alanı ön ayarı", "pg_preset_name": "Yeni ön ayar adı", "pg_preset_none": "Bir ön ayar seçin…", "pg_strong_match": "Güçlü eşleşme", - "pg_tile_ambiguous": "Belirsiz", - "pg_tile_ambiguous_tip": "İki program arasında neredeyse berabere kalan döngüler", - "pg_tile_detected": "Algılandı", - "pg_tile_detected_tip": "Gücün, çalışıyor olarak kaydedilecek kadar uzun süre başlangıç eşiğini aştığı döngüler", - "pg_tile_matched": "Eşleşti", - "pg_tile_matched_tip": "Bilinen bir programa doğru şekilde atfedilen döngüler", - "pg_time_left": "Kalan süre", "pg_weak_match": "Zayıf eşleşme", "phase_name": "Aşama Adı", "name": "Ad", "phase_timeline": "Aşama zaman çizelgesi", "power": "Güç", "profile": "Profil", - "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. Cihazı çalıştırmadan önce bir program seçin; sonraki döngü başladığı anda uygulanır.", + "program_tip": "Geçerli döngünün hangi programla eşleştirileceğini seçin. Otomatik algılama en iyi eşleşmeyi seçer. Otomatik algılama yanılıyorsa bir programı sabitleyin; başlatmadan önce seçerseniz sonraki döngüye uygulanır.", "progress": "İlerleme", "raw_socket": "Ham soket", - "read_only": "Salt okunur", "ref_cycle": "Referans Döngü (isteğe bağlı)", - "refresh_interval": "Yenileme aralığı (s)", "remaining": "Kalan", "reminder_every": "Hatırlatma sıklığı (döngü)", "remove_label": "- Etiketi kaldır -", "rename_profile": "Profili Yeniden Adlandır", - "replay_duration": "Yeniden oynatma", "resulting_profile": "Sonuçlanan profil", "save_mode": "Kaydetme Modu", - "score": "Puan", - "score_breakdown": "Puan dökümü", "seconds_from_start": "Başlangıçtan saniyeler", "select_profile": "Profil Seç", "setting": "Ayar", - "settings_a": "Ayarlar A (geçerli)", - "settings_a_short": "A", "settings_advanced": "Gelişmiş", - "settings_b": "Ayarlar B (test)", - "settings_b_short": "B", "settings_basic": "Temel", "settings_detail_level": "Ayar ayrıntı düzeyi", - "settings_overrides": "Ayar geçersiz kılmaları", "show_debug": "Durum sayfasında canlı eşleşme hata ayıklama kartını göster (güven, belirsizlik, en iyi adaylar)", "show_expected": "Beklenen eğri bindirmesini göster (eşleşen profil, turuncu)", "show_raw": "Canlı güç grafiğinde ham soket değiştiriciyi göster", "sparkline": "Son döngü süresi eğilimi", - "stage2": "Aşama 2 – çekirdek benzerlik", - "stage3": "Aşama 3 – DTW", - "stage4": "Aşama 4 – uyum", "status": "Durum", "tail_trim": "Sondan Kırpma (s)", "tap_to_dismiss": "kapatmak için grafiğe dokunun", @@ -579,7 +516,6 @@ "ungrouped": "Grupsuz", "unlabelled": "Etiketsiz", "unlabelled_paren": "(etiketsiz)", - "warp_path": "Büküm yolu", "input": "Girdi:", "start": "Başlangıç", "end": "Bitiş", @@ -604,9 +540,8 @@ "n_shown": ", {n} gösteriliyor", "cycles_title": "Döngüler ({n})", "pg_detecting": "Algılanıyor", - "pg_best_marker": "En iyi: {v}", "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", + "pg_time_left_model_tip": "Programın aşamalarına göre tahmin edilir, sabit bir geri sayım değildir", "phase": "Aşama", "pg_mode_history": "Geçmişte test et", "pg_ev_detected": "Algılandı", @@ -638,12 +573,6 @@ "pg_sweep_param": "Parametre", "pg_objective": "Hedef", "pg_steps": "Adımlar", - "pg_stress_group": "Bekleme sonlandırma testi", - "pg_stress_idle_auto": "Auto", - "pg_stress_idle_w": "Bekleme seviyesi (W)", - "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_obj_match": "Eşleşme doğruluğu", "pg_obj_endtiming": "Bitiş zamanı doğruluğu", "pg_obj_falseend": "Yanlış bitiş oranı", @@ -652,12 +581,8 @@ "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", - "aria_playground_chart2": "Etkileşimli döngü güç grafiği: zamanı/gücü okumak için üzerine gelin, yakınlaştırmak için kaydırın, kaydırmak için sürükleyin", "pg_alert_timeout_end": "Tahminle değil, zaman aşımıyla sona erdi", "pg_alert_indefinite": "Süresiz olarak çalışırdı", - "pg_alert_stress_cap": "Güvenlik sınırına ulaşıldı", - "pg_alert_stress_ok": "Bekleme sonlandırması: döngü durduruldu", - "pg_alert_stress_warn": "Bekleme tüketimi durdurma eşiğinin üzerinde", "task_pg_history": "Geçmişte test et", "task_pg_sweep": "Optimize et", "task_reprocess": "Yeniden işleme", @@ -667,16 +592,9 @@ "pg_recent_runs": "Son çalıştırmalar", "pg_load_run": "Bu çalıştırmayı yeniden yükle", "pg_match_confidence": "Eşleşme güveni", - "pg_envelope_fit": "Zarfa uyum", - "pg_envelope_fit_tip": "Döngünün bu profilin kaydedilmiş güç zarfının içinde ne kadar iyi kaldığı, eşleşme güveninden farklı bir bakış açısı.", "n_imported_note": ", {n} içe aktarıldı", "enable_online": "Çevrimiçi özellikleri etkinleştir", - "store_brand": "Cihaz markası", - "store_model": "Cihaz modeli", "anonymous": "Anonim", - "rate_quality": "Kaliteyi değerlendir", - "rate_n": "{n} yıldız", - "approved_only": "Yalnızca onaylılar", "adopt_settings": "Ayarları da benimse", "flags": "İşaretler", "include_phase_map": "Faz haritasını dahil et", @@ -693,7 +611,6 @@ "cat_custom_phases": "Özel aşamalar", "cat_profile_groups": "Profil grupları", "cat_settings": "Algılama ve tanıma ayarları", - "cat_matching_config": "Tanıyıcı ayarı", "cat_ml_models": "ML modelleri", "cat_feedback": "Geri bildirim ve inceleme etiketleri", "cat_suggestions": "Öneriler", @@ -715,7 +632,6 @@ "hist_csv_data": "CSV verisi", "hist_from_recorder": "Veya Home Assistant'tan okuyun", "hist_since": "Başlangıç tarihi", - "days": "gün", "hist_keep": "Bu döngüyü sakla", "hist_looks_complete": "tamamlanmış", "peak_power_short": "Tepe", @@ -735,7 +651,40 @@ "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" + "correct_total_cycles": "Toplamı düzelt", + "hist_reason_already_recorded": "WashData'nın zaten kaydettiği bir döngüyle çakışıyor", + "share_consent": "Bu döngülerin kesintisiz ve normal şekilde tamamlandığını onaylıyorum", + "show_internal": "Dahili ayarları göster", + "show_internal_tip": "WashData'nın sizin için ayarladığı ayarlar (örnekleme, bekçi, eşleşme sıklığı, eşikler). Bu cihazın kendi değeri, bir çakışması veya bir önerisi olduğunda gösterilir.", + "rate_n": "{n} yıldız", + "rate_quality": "Kaliteyi değerlendir", + "pg_autolabel": "Otomatik etiket", + "pg_label_review": "İnceleme ister", + "pg_ev_match_reverted": "Eşleşme geri alındı", + "pg_ev_verified_pause": "Doğrulanmış duraklama", + "n_samples": "{n} örnek", + "n_samples_one": "{n} örnek", + "n_samples_other": "{n} örnek", + "n_cycles": "{n} döngü", + "n_cycles_one": "{n} döngü", + "n_cycles_other": "{n} döngü", + "energy_per_cycle": "{v}/döngü", + "energy_total": "toplam {v}", + "automation_off": "(kapalı)", + "ref_cycle_none": "Yok", + "share_name": "Paylaştıklarımda adımı göster", + "cat_backfill_cycles": "İçe aktarılan güç geçmişinde bulunan döngüler", + "pg_compare_profile": "Karşılaştırılacak profil", + "pg_obj_early": "Erken bitiş oranı", + "pg_obj_endlag": "Bitiş gecikmesi (medyan)", + "pg_obj_split": "Bölünme oranı", + "pg_sweep_cycles": "Son döngüler", + "cycle_context": "Döngü çevresindeki kaydedici geçmişi", + "cycle_context_off": "Kapalı", + "cycle_context_minutes": "{min} dk", + "cycle_context_series": "Kaydedici geçmişi", + "before_start": "Başlangıçtan önce", + "after_end": "Bitişten sonra" }, "log": { "all_levels": "Tüm seviyeler", @@ -745,11 +694,9 @@ }, "ml": { "builtin_models": "● Yerleşik modeller kullanılıyor", - "fit_fair": "Kabul edilebilir", "fit_good": "İyi", "fit_slight": "Hafif", "fit_strong": "Güçlü", - "fit_weak": "Zayıf", "fit_word": "uyum", "personalized": "● Bu makineye kişiselleştirilmiş", "better_than_baseline": "temel tahminden %{pct} daha iyi ({metric})", @@ -757,21 +704,29 @@ "trend_declining_tip": "Bu modelin uyumu son yeniden kontrollerde geriledi, daha fazla döngü incelemek yeniden öğrenmesine yardımcı olabilir.", "trend_steady_tip": "Bu modelin uyumu son yeniden kontrollerde yaklaşık olarak sabit kaldı.", "cap_blurb": { - "end": "Bir çevrimin gerçekten bittiğini anlama", - "live_match": "Çalışan programı daha erken tanıma", - "quality": "Yanlış algılanan veya bozuk çevrimleri fark etme", - "remaining_time": "Ne kadar süre kaldığını tahmin etme", "total_energy": "Toplam enerji ve maliyeti tahmin etme" }, "cap_label": { - "end": "Çevrim sonu algılama", - "live_match": "Program eşleştirme", - "quality": "Çevrim kalite kontrolü", - "remaining_time": "Kalan süre tahmini", "total_energy": "Enerji tahmini" }, - "metric_auc": "Ayrılmış doğrulama verisinde AUC {auc}", - "metric_mae": "hata {model} - temel model {baseline}" + "metric_mae": "hata {model} - temel model {baseline}", + "fine_tuned_at": "ince ayar: {when}", + "last_run_not_promoted": "Son kontrol ({when}): {reason}", + "never": "hiçbir zaman", + "skip_reason": { + "fit_failed": "Eğitim başarısız oldu: {error}", + "holdout_too_small": "Yeni bir modeli adil şekilde test etmek için çok az döngü var: test için {held_out} döngü ayrılabildi, {min} gerekiyor.", + "not_better_than_incumbent": "Yeni model kullanımdaki modelden daha iyi değildi (hata: {model}, kullanımdaki: {incumbent}), bu yüzden mevcut model korundu.", + "not_better_than_naive": "Yeni model basit tahminden açıkça daha iyi değildi (hata: {model}, basit tahmin: {naive}).", + "unknown": "Yeni bir şey öğrenilmedi.", + "insufficient_rows": "Henüz yeterli kullanılabilir döngü yok: şu ana kadar {count} etiketli, temiz döngü var.", + "insufficient_rows_one": "Henüz yeterli kullanılabilir döngü yok: şu ana kadar {count} etiketli, temiz döngü var.", + "insufficient_rows_other": "Henüz yeterli kullanılabilir döngü yok: şu ana kadar {count} etiketli, temiz döngü var." + }, + "trained_unknown": "bilinmeyen zaman", + "held_out_count": "{count} ayrılmış doğrulama döngüsünde ölçüldü", + "held_out_count_one": "{count} ayrılmış doğrulama döngüsünde ölçüldü", + "held_out_count_other": "{count} ayrılmış doğrulama döngüsünde ölçüldü" }, "modal": { "auto_label": "Döngüleri Otomatik Etiketle", @@ -780,7 +735,6 @@ "cycle": "Döngü", "edit_group": "Profil grubunu düzenle", "edit_phase": "Aşamayı Düzenle", - "import_config": "Yapılandırmayı İçe Aktar", "label_cycle": "Döngüyü Etiketle", "merge_cycles": "{n} Döngüyü Birleştir", "new_group": "Yeni profil grubu", @@ -795,6 +749,8 @@ "delete_automation_title": "Otomasyonu Sil", "delete_automation_msg": "\"{name}\" otomasyonu Home Assistant'tan silinsin mi? Bu işlem geri alınamaz.", "remove_legacy_title": "Eski Eylemleri Kaldır", + "reset_phase_msg": "\"{name}\" üzerindeki düzenlemeniz geri alınsın ve yerleşik aşama geri yüklensin mi? Programlar aralıklarını korur.", + "reset_phase_title": "Aşamayı Sıfırla", "remove_legacy_msg": "Eski özel eylemler kaldırılsın mı? Döngü olaylarında tetiklenmeyi durduracaklar. Bu işlem panelden geri alınamaz.", "discard_recording_title": "Kaydı At", "discard_recording_msg": "Saklanan kayıt atılsın mı? Bu işlem geri alınamaz.", @@ -815,32 +771,44 @@ "store_share_device": "Bu cihazı paylaş", "export_select": "Dışa aktarma - veri seçin", "import_wizard": "İçe aktarma - veri seçin", - "history_import": "Güç geçmişini içe aktar" + "history_import": "Güç geçmişini içe aktar", + "apply_suggestions_title": "Önerileri uygula", + "remove_task_title": "Görevi Kaldır", + "remove_task_msg": "\"{task}\" hatırlatıcısı kaldırılsın mı? Bakım günlüğündeki kayıtları korunur.", + "apply_suggestions_msg": "Bu {n} ayar değiştirilsin mi? Entegrasyon kaydettikten sonra yeniden yüklenir.", + "discard_settings_title": "Kaydedilmemiş değişiklikler atılsın mı?", + "discard_settings_msg": "Kaydedilmemiş {n} ayar değişikliği var. Ayarlar'dan çıkılıp bu değişiklikler atılsın mı?", + "profile_title": "Profil · {name}", + "undo_import_msg": "Bu cihazın verileri ve ayarları son içe aktarmadan ({when}) önceki haline döner. O zamandan beri kaydedilen döngüler ve yapılan değişiklikler kaybolur.", + "undo_import_ok": "İçe aktarmayı geri al", + "undo_import_title": "Son içe aktarmayı geri al" }, "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.", + "pg_preset_delete_confirm": "\"{name}\" ön ayarı silinsin mi?", + "pg_preset_limit": "Ön ayar sınırına ulaşıldı ({n})", + "pg_preset_overwrite": "\"{name}\" ön ayarının üzerine yazılsın mı?", "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.", - "refresh_catalog_hint": "Topluluk marka ve cihaz listeleri, paylaşılan mağazanın günlük okuma kotası içinde kalması için önbelleğe alınır. Başkalarının eklediği veya onayladığı kayıtları almak için yenileyin.", + "store_sibling_hint": "Modeliniz için paylaşılan bir şey yok mu? Aynı markanın yakın bir modeli genellikle iyi bir başlangıçtır.", + "store_declare_appliance": "Başkalarının cihazınız için paylaştığı kurulumları görmek için WashData'ya hangi cihaza sahip olduğunuzu söyleyin. Ya da göz atmak için yukarıya bir marka yazın.", + "refresh_catalog_hint": "Topluluk marka ve cihaz listeleri, mağazanın günlük kotasını korumak için önbelleğe alınır. Başkalarının eklediği veya onayladığı kayıtları görmek için yenileyin.", "head_trim_hint": "Baştan kaldırılacak saniye sayısı", "appliance_monitor": "Cihaz monitörü", "artifact_dip_detail": "Normal güç bandının altında yaklaşık ~{n} saniye sürdü.", "artifact_footer": "Yukarıdaki grafikte vurgulanmıştır. Bunlar geçici eserlerdir (örn. döngünün ortasında kapının açılması), mutlaka sorun teşkil etmez.", "artifact_header": "Bu döngü sırasında {n} anormallik algılandı", - "artifact_pause_detail": "Güç yaklaşık ~{n} saniye boyunca sıfıra yakın düştü, sonra devam etti – döngünün ortasında kapı açılmış veya döngü duraklatılmış olabilir.", + "artifact_header_one": "Bu döngü sırasında {n} anormallik algılandı", + "artifact_header_other": "Bu döngü sırasında {n} anormallik algılandı", + "artifact_pause_detail": "Güç ~{n} sn boyunca sıfıra yakın düştü, sonra devam etti. Muhtemelen kapı açıldı veya döngü duraklatıldı.", "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şaretleyin, ardından etiketlerini kaldırın (geçmişte kalırlar ama artık bu profili şekillendirmezler) veya tamamen silin.", + "cleanup_intro": "Tüm etiketli döngüler üst üste. Aykırı olanları işaretleyin, sonra etiketlerini kaldırın (geçmişte kalırlar, artık bu profili şekillendirmezler) veya 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)", - "compare_profiles_tip": "Hangisinin bu döngüye en uygun olduğunu görmek için diğer profil zarflarını yukarıdaki tabloya yerleştirin.", + "compare_profiles_tip": "Bu döngüye en iyi hangisinin uyduğunu görmek için diğer profilleri grafiğe bindirin.", "compare_selected_cycles": "Seçili döngüler (katı) – göster / gizle", - "consider_new_profile": "Yeni bir profil oluşturmayı düşünün.", - "coverage_gap": "son döngülerin eşleşen profili yok (son 30 döngünün %{pct}'i).", - "coverage_gap_similar_cycles": "{count} benzer etiketsiz döngü bulundu – bunları eşleştirmeye başlamak için bir profil oluşturun.", "cycles_deleted": "{count} döngü silindi", "enough_data": "Öğrenmek için yeterli veri ({current}/{min} döngü).", "export_description": "JSON'a dışa aktarılacak profilleri, döngüleri, ayarları ve daha fazlasını tam olarak seçin ya da bir dosyayı analiz edip yalnızca istediğiniz kısımları içe aktarın.", @@ -848,30 +816,26 @@ "feedback_prompt": "Doğru olduğunu onaylayın, programı düzeltin veya yoksayın.", "feedback_relabel_hint": "Bu döngüyü yeniden etiketlemek de bunu çözer.", "filter_by_profile": "Profile göre filtrele…", - "group_modal_help": "Sıcaklık/dönme açısından farklı olan aynı şekle sahip programları gruplandırın (süreler değişebilir). Eşleştirme, grubu bir aday olarak puanlar ve ardından en uygun üyeyi seçer. En az 2 tanesini seçin; kaplama ne kadar benzer olduklarını gösteriyor.", + "group_modal_help": "Aynı şekle sahip, sıcaklık veya sıkmada farklılaşan programları gruplayın. Eşleştirme grubu tek aday olarak puanlar, sonra en uygun üyeyi seçer. En az 2 tane seçin; bindirme ne kadar benzer olduklarını gösterir.", "group_not_cohesive": "Bu profiller güvenilir bir şekilde gruplandırılacak kadar benzer değildir; bu nedenle eşleştirme, siz aykırı değeri kaldırana veya grubu bölene kadar bunları ayrı ele alır.", "group_preview_hint": "Güç eğrilerini önizlemek ve karşılaştırmak için 2 veya daha fazla üyeyi işaretleyin.", - "import_intro": "Dışa aktarılan bir dosyayı yükleyin veya aşağıya bir JSON yükü yapıştırın.", "legacy_actions_title": "{count} eski özel işlem hâlâ çalışıyor", + "legacy_actions_title_one": "{count} eski özel işlem hâlâ çalışıyor", + "legacy_actions_title_other": "{count} eski özel işlem hâlâ çalışıyor", "live_chart_loading": "Canlı güç grafiği okumalar gelince görünür.", "loading": "Yükleniyor…", "loading_curve": "Eğri yükleniyor…", "loading_settings": "Ayarlar yükleniyor…", "log_buffer_hint": "Önce en yeni · yeniden başlatmadan bu yana son 500 ha_washdata kaydını ara belleğe alır · yeniden boyutlandırmak için alt kenarı sürükleyin.", - "maintenance_advisory": "Sürenin/enerjinin artması, cihazın bakımının gerekli olduğunu gösterebilir (örn. kireç çözme, filtre temizliği).", "maintenance_due": "Zamanı gelen bakım: {items}", - "maintenance_intro": "Bu cihazda yaptığınız bakımları kaydedin ve her görevin zamanı yeniden geldiğinde hatırlatma alın.", + "maintenance_intro": "Bu cihazdaki bakımları kaydedin ve her görevin zamanı yeniden geldiğinde hatırlatma alın.", "maintenance_load_error": "Bakım verileri yüklenemedi: {error}", "maintenance_requires_access": "Bakım ve dışa/içe aktarma tam erişim gerektirir.", - "manual_duration_hint": "Kalan süre tahminleri için kullanılan profilin ortalama/beklenen döngü uzunluğu. Ayarlamak için düzenleyin; değişmeden bırakmak mevcut değeri korur.", - "matching_tuning_intro": "Öğrenirken WashData, program eşleştirmenin şekle, süreye ve enerjiye ne kadar ağırlık verdiğini de ayarlar.", + "manual_duration_hint": "Profilin beklenen döngü uzunluğu; kalan süre tahminlerinde kullanılır. Mevcut değeri korumak için değiştirmeyin.", "merge_intro": "Seçili döngüler tek birinde birleştirilir (kronolojik sıra; boşluklar 0 W ile doldurulur). Sonuçlanan profili seçin.", "ml_intro": "WashData, kullanıma hazır akıllı modellerle birlikte gelir.", "ml_learned_intro": "Bu makineye ince ayarlı modeller.", - "ml_loading": "ML yükleniyor…", "ml_settings_intro": "İki bağımsız anahtar: biri döngü çalışırken modelleri uygular, diğeri WashData'nın zamanla makinenize ince ayar yapmasına izin verir.", - "name_first_program": "Yeterli döngünüz var – eşleştirmeye başlamak için ilk programınıza ad verin.", - "near_duplicate_cluster": "neredeyse yinelenen profil kümesi algılandı. Gruplandırma, eşleştirmenin benzerler arasında güvenilir bir şekilde seçim yapmasına olanak tanır (örneğin, farklı sıcaklıkta/döndürmede aynı program).", "no_cycles_match": "Mevcut filtreyle eşleşen döngü yok.", "no_cycles_profile": "Bu profil için döngü yok.", "no_cycles_selected": "Önce en az bir döngü seçin.", @@ -896,73 +860,52 @@ "no_suggestions": "Aktif öneri yok.", "notify_services_hint": "{entity} hizmet kimliklerini kullanın (birden fazlası için virgülle ayrın). Şablon değişkenleri: {vars}.", "old_actions_warning": "Eski eylem düzenleyiciyle yapılandırılmıştır (artık kaldırılmıştır). Döngü etkinliklerinde hala tetikleniyorlar ancak artık burada düzenlenemezler. Bunları normal bir otomasyona dönüştürün veya kaldırın.", - "onboarding_progress": "{n} / 3 döngü gözlemlendi", - "onboarding_watching": "Cihazınızı normal şekilde kullanın – WashData izliyor. 3 döngüden sonra program eşleştirmesi başlar.", "pending_feedback": "Bekleyen algılama geri bildirimi", "performance_trend": "Performans trendi ({n} döngü)", - "pg_analysis_empty": "Eşleşme analizini görmek için bir döngü yükleyin.", "pg_apply_confirm": "En iyi değeri uygula: {label} = {value}?", - "pg_canvas_empty": "Yukarıdan bir döngü seçin, ardından güç izini görmek için Yükle'ye basın.", - "pg_chart_caption": "{param} değerlerine göre algılama/eşleşme oranı", "pg_ctrl_intro": "Değerler bu cihazın etkin entegrasyon ayarlarından başlar. Düzenlemeler, siz uygulayana kadar Test Alanı'nda kalır.", "pg_live_unavailable": "Etkin ayarlar kullanılamıyor, varsayılanlar gösteriliyor.", "pg_load_live_confirm": "{n} Test Alanı düzenlemesi atılıp entegrasyonun geçerli ayarları yeniden yüklensin mi?", - "pg_match_rate": "{total} döngüde %{pct} eşleşme oranı", "pg_matches_live": "Etkin ayarlarla aynı", "pg_n_changed": "Etkin ayarlara göre {n} değişiklik", "pg_sugg_none": "Tüm öneriler zaten geçerli ayarlarla eşleşiyor", - "pg_preset_delete_confirm": "\"{name}\" ön ayarı silinsin mi?", - "pg_preset_limit": "Ön ayar sınırına ulaşıldı ({n})", - "pg_preset_overwrite": "\"{name}\" ön ayarının üzerine yazılsın mı?", "pg_publish_one_confirm": "{label} = {value} bu cihazın ayarlarına kaydedilsin mi?", "pg_restart_note": "Simülasyon araçlarını etkinleştirmek için Home Assistant'ı yeniden başlatın.", - "pg_sim_progress": "{done} / {total} döngü", - "pg_starting": "Başlatılıyor…", - "pg_stress_synthetic": "bekleme simülasyonu burada başlar", - "pg_sweep_intro": "{param} farklı olsaydı ne olurdu? En çok döngünün doğru şekilde eşleştirildiği ayarı bulmak için son {cycles} döngünüzde {steps} değeri test edin.", - "pg_sweep_step": "Adım {done} / {total}", - "pg_verdict_bad": "Dikkat gerekiyor: birçok döngü algılanmadan geçiyor.", - "pg_verdict_good": "İyi ayarlanmış: çoğu döngü doğru şekilde tanımlanıp eşleştiriliyor.", - "pg_verdict_ok": "Kabul edilebilir: bazı döngüler kaçırıldı. Başlangıç eşiğini düşürmeyi deneyin.", "phase_catalog_intro": "Bir döngünün adlandırılmış bölümleri (Ön yıkama, Isınma, Sıkma…). Denetim panelinden bir profile atayın.", "phase_ranges_intro": "Ortalama eğri üzerinde bindirilen faz aralıkları (döngü başlangıcından dakikalar). Canlı önizleme için değerleri düzenleyin.", - "playground_intro": "Geçmiş döngüleri farklı ayarlarla yeniden oynatmak, iki kurulumu yan yana karşılaştırmak ve bir döngünün bir profile nasıl hizalandığını incelemek için güvenli bir test alanı. Siz açıkça uygulamadıkça buradaki hiçbir şey canlı verilerinizi değiştirmez.", - "playground_needs_restart": "Test Alanı araçlarını etkinleştirmek için Home Assistant'ı yeniden başlatın.", - "preferences_admin": "Tercihler, panel ve erişim denetimi", - "preferences_adv": "Tercihler", + "playground_intro": "Ayarları gerçek döngü verileriniz üzerinde deneyin. Siz uygulamadıkça buradaki hiçbir şey canlı ayarlarınızı değiştirmez.", "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), 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.", + "process_history_hint": "Saklanan tüm döngülerde eşleştirmeyi yeniden çalıştırın, önerileri yenileyin, ML modellerini yeniden eğitin (etkinse), döngü maliyetlerini kayıtlı fiyatlardan ve döngü sağlığını yeniden hesaplayın. Bir grup incelemeden sonra ç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.", - "rbac_hint": "Kapalı olduğunda her Home Assistant kullanıcısı tam erişime sahip olur (varsayılan). Yöneticiler her zaman tam erişime sahip olup herkesi yönetebilir.", - "recent_logs": "Son ha_washdata kayıtları", + "rbac_hint": "Kapalı: her Home Assistant kullanıcısı tam erişime sahiptir (varsayılan). Yöneticiler her zaman tam erişime sahiptir ve herkesi yönetebilir.", "recording_in_progress": "Kayıt devam ediyor", "reminders_intro": "Son servisten bu kadar döngü sonra panelde bir hatırlatma göster. Bir hatırlatmayı kapatmak için boş bırakın veya 0 girin.", "restart_gap_footer": "Grafikte vurgulandı. Bu aralıklar için güç verisi eksik; eşleştirme yalnızca gerçek okumaları kullandı.", - "restart_gap_header": "{n} HA yeniden başlatma boşluğu bu döngü sırasında", + "restart_gap_header": "Bu döngü sırasında {n} HA yeniden başlatma boşluğu", + "restart_gap_header_one": "Bu döngü sırasında {n} HA yeniden başlatma boşluğu", + "restart_gap_header_other": "Bu döngü sırasında {n} HA yeniden başlatma boşluğu", "restart_gap_item": "{dur} boşluk", - "review_confirm_help": "Bu döngünün doğru şekilde algılanıp algılanmadığını doğrulayın. İncelemeleriniz makinenizdeki modeli eğitir; ne kadar çok döngü onaylarsanız o kadar iyi eşleştirme ve sağlık puanlaması elde edilir. Hızlı bir İyi/Kötü yeterlidir.", + "review_confirm_help": "Bu döngü doğru algılandı mı? Döngüleri onaylamak makineniz için eşleştirmeyi iyileştirir. Hızlı bir İyi/Kötü yeterlidir.", "review_in_settings": "Ayarlarda İncele", "review_notes_placeholder": "Notlar (isteğe bağlı)", "review_notes_tip": "Kendi referansınız için serbest metin notları. Eşleştirme veya antrenman yoluyla kullanılmaz.", - "review_profile_tip": "Bu döngünün etiketlendiği program. Otomatik algılanan program yanlışsa burada düzeltin; etiketleme gelecekteki döngüler için eşleştirmeyi öğretir.", - "review_quality_tip": "Bu döngü ne kadar temiz. İyi = bu programın ders kitabı örneği; Kötü = algılandı ancak gürültülü veya atipik; Kullanılamaz = yanlış algılandı (birleştirilmiş, kısaltılmış veya sahte). Sağlık puanını ve modeli eğitmek için hangi döngülere izin verildiğini yönlendirir.", - "review_recorded_tip": "Bunu, programı için özenle seçilmiş bir referans döngüsü olarak işaretleyin; manuel olarak kaydedilen bir döngüyle aynı rol. Referans döngüleri her zaman tutulur, eşleşen şablonun çekirdeği oluşturulur ve hiçbir zaman temizleme işlemi nedeniyle bırakılmaz. (Bu \"altın\"/kaydedilmiş bayraktır; ikisi de aynı şeydir.)", - "review_tags_tip": "Bu döngüde neyin yanlış gittiğini açıklayan isteğe bağlı bayraklar, böylece eğitim ve temizlik bunu açıklayabilir.", + "review_profile_tip": "Bu döngünün etiketlendiği program. Yanlışsa burada düzeltin; etiketler eşleştirmeyi öğretir.", + "review_quality_tip": "Bu döngü ne kadar temiz. İyi: bu programın ders kitabı gibi bir çalışması. Kötü: algılandı ama gürültülü veya olağandışı. Kullanılamaz: yanlış algılandı (birleşmiş, erken kesilmiş veya sahte). Sağlık puanını ve modeli hangi döngülerin eğiteceğini etkiler.", + "review_recorded_tip": "Bunu, elle kaydedilmiş bir döngü gibi, programı için özenle seçilmiş bir referans döngü olarak işaretleyin. Referans döngüler her zaman saklanır, eşleştirme şablonunun temelini oluşturur ve temizlikte asla kaldırılmaz.", + "review_tags_tip": "Neyin yanlış gittiğine dair isteğe bağlı işaretler; böylece eğitim ve temizlik bunu hesaba katabilir.", "review_to_cycles": "Döngü inceleme kuyruğunu aç", "samples_decimated": "{total} örnekten {shown} tanesi gösteriliyor (görüntü için seyreltildi; tepe noktaları korundu). Buradaki geniş bir boşluk seyreltmedir, eksik veri değildir.", - "saving_triggers_reload": "Kaydetme bir entegrasyon yeniden yüklemesini tetikler. HA varlıkları kısa süreliğine kullanılamaz olarak görünebilir.", + "saving_triggers_reload": "Kaydetmek entegrasyonu yeniden yükler. Varlıklar kısa süre kullanılamaz görünebilir.", "search_placeholder": "Ayar ara…", "see_recorder": "Aşağıdaki kaydedici widget'ına bakın", "setting_changed": "{date} tarihinde {old} değerinden {new} değerine değiştirildi", "settings_basic_note": "Temel ayarlar gösteriliyor. Tam liste için Gelişmiş'e geçin.", - "shape_drift_advisory": "⚠ Şekil kayıyor", - "shape_drift_detail": "Bu profil için güç paterni zamanla kaymıştır – olası cihaz aşınması veya bakım gerekebilir (örn. kireç çözme, filtre temizliği).", "show_all_settings": "Tüm ayarları göster", - "showing_suggestions": "Önerilerle birlikte {count} ayarı gösteriliyor.", - "split_intro": "Bölme noktası eklemek veya kaldırmak için grafiğe tıklayın veya boşta kalma aralıklarına göre otomatik algılayın. Her elde edilen bölüm kendi profilini alabilir.", - "storage_diagnostics": "Depolama istatistikleri, bakım, dışa/içe aktarma", + "showing_suggestions": "Öneri içeren {count} ayar gösteriliyor.", + "showing_suggestions_one": "Öneri içeren {count} ayar gösteriliyor.", + "showing_suggestions_other": "Öneri içeren {count} ayar gösteriliyor.", + "split_intro": "Bölme noktası eklemek veya kaldırmak için grafiğe tıklayın ya da boşta kalma aralıklarına göre otomatik algılayın. Her parça kendi profilini alabilir.", "sug_staged": "{key} = {val} olarak ayarlandı. Uygulamak için kaydedin.", "toast_auto_detect_enabled": "Otomatik algılama etkinleştirildi", "toast_auto_label_complete": "Otomatik etiketleme tamamlandı", @@ -979,7 +922,6 @@ "toast_no_automation": "Burada otomasyon oluşturulamaz", "toast_profile_name_required": "Profil adı gereklidir", "toast_program_set": "Program ayarlandı: {program}", - "toast_rebuild_failed": "Yeniden oluşturma başarısız: {error}", "toast_resume_failed": "Devam ettirme başarısız: {error}", "toast_revert_failed": "Geri alma başarısız: {error}", "toast_reverted": "{key} geri alındı", @@ -991,99 +933,86 @@ "trim_destructive_confirm": "Bu, döngünün yalnızca %{pct}'ini korur ve bu işlem geri alınamaz. Devam edilsin mi?", "trim_intro": "Kırmızı tutamaçları sürükleyin veya değer girin. Pencerenin dışındaki her şey kaldırılır.", "tuning_suggestions_available": "Gözlemlenen döngülerden {count} ayarlama önerisi mevcut. İlgili alanların yanında görünürler.", + "tuning_suggestions_available_one": "Gözlemlenen döngülerden {count} ayarlama önerisi mevcut. İlgili alanın yanında görünür.", + "tuning_suggestions_available_other": "Gözlemlenen döngülerden {count} ayarlama önerisi mevcut. İlgili alanların yanında görünürler.", "unsure_detected_prefix": "WashData algıladığından emin değil", - "updated": "Güncellendi", "warmup_badge": "Hâlâ öğreniyor ({done}/{needed} döngü)", - "warmup_detail": "Bu profil, otomatik eşleştirme başlamadan önce {needed} etiketli döngüye ihtiyaç duyar. Her onaylanan döngü öğrenmesine yardımcı olur.", + "warmup_detail": "Eşleştirme zaten çalışıyor. WashData kendi başına etiketlemeden önce bu profilin ilk {needed} döngüsünü onaylamanızı ister.", "wipe_history_warning": "Tüm döngüleri ve profilleri kalıcı olarak silin. Geri alınamaz.", "compare_overlay_tip": "Her döngünün hangi programa benzediğini görmek için öğrenilen profil zarflarını üst üste bindirin.", - "duration_trend_up_tip": "Süre artıyor (%{pct}/döngü)", - "duration_trend_down_tip": "Süre azalıyor (%{pct}/döngü)", - "energy_trend_up_tip": "Enerji artıyor (%{pct}/döngü)", - "energy_trend_down_tip": "Enerji azalıyor (%{pct}/döngü)", - "performance_trending": "Performans eğilimi", - "pg_select_cycle_hint": "Yüklemek için bir döngü seçin ve ↺ düğmesine tıklayın", - "pg_sweep_progress": "Adım {done} / {total}", - "pg_analysis_hint": "Analizi yüklemek için bir döngü seçin ve ↺ düğmesine tıklayın.", "diagnostics_load_failed": "Tanılamalar yüklenemedi: {error}", - "manual_duration_ref_hint": "Yalnızca hiçbir referans döngü seçilmediğinde kullanılır. Bir referans döngü, süreyi kendi uzunluğuna göre belirler.", + "manual_duration_ref_hint": "Yalnızca referans döngü yokken kullanılır; referans döngü süreyi kendi uzunluğundan belirler.", + "duration_learned_hint": "Bu programın döngülerinden (aykırı olanlar hariç) hesaplanır ve her yeni döngüden sonra güncellenir. Değiştirmek için sapan döngüleri kırpın veya kaldırın.", "toast_convert_rolled_back": "Dönüştürme başarısız oldu ve geri alındı (geride otomasyon bırakılmadı): {error}", "toast_convert_orphan": "Otomasyon oluşturuldu ancak eski eylemler temizlenemedi. Tekrar denemeyin: yinelenen bir otomasyondan kaçınmak için eski eylemleri elle kaldırın.", - "advisory_duration_trend_up": "'{name}' çevrimleri giderek uzuyor (çevrim başına yaklaşık +{pct}%). Cihazın davranışı değiştiyse bu profili yeniden kaydedin veya yeniden oluşturun.", - "advisory_energy_trend_up": "'{name}' giderek daha fazla enerji çekiyor (çevrim başına yaklaşık +{pct}%) - beklenmedikse cihazı kontrol etmekte fayda var.", - "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.", + "advisory_duration_trend_up": "'{name}' döngüleri giderek uzuyor (döngü başına yaklaşık +{pct}%). Cihaz değiştiyse bu profili yeniden kaydedin veya yeniden oluşturun.", + "advisory_energy_trend_up": "'{name}' her döngüde daha fazla enerji kullanıyor (döngü başına yaklaşık +{pct}%). Beklenmedikse cihazı kontrol edin.", + "advisory_poor_health": "'{name}' zayıf uyuyor: son döngüleri çok değişken veya zayıf eşleşiyor. Eşleştirme ve tahminler doğru kalsın diye döngülerini gözden geçirin ya da profili yeniden kaydedin.", + "advisory_shape_drift": "'{name}' orijinal güç eğrisinden uzaklaştı; kireç veya aşınma nedeniyle olabilir. Son döngülerle yeniden kaydetmeyi düşünün.", + "advisory_shape_drift_corr": "'{name}' orijinal güç eğrisinden uzaklaştı (korelasyon {corr}); kireç veya aşınma nedeniyle olabilir. Son döngülerle yeniden kaydetmeyi düşünün.", + "advisory_unmatchable": "'{name}' asla eşleşemez: arkasında güç verisi olan bir döngü yok. Düzeltmek için bu programın bir döngüsünü etiketleyin veya kaydedin.", + "advisory_unmatchable_no_duration": "'{name}' asla eşleşemez: döngüsünün kullanılabilir bir süresi yok. Bu programı yeniden kaydedin veya beklenen süresini girin.", "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_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.", "pg_sweep_intro2": "Son döngülerinizde bir hedefi en iyi karşılayan ayarı bulun. Siz uygulayana kadar hiçbir şey değişmez.", - "pg_running_history": "Döngüleriniz yeniden oynatılıyor…", - "pg_running_sweep": "Değerler tüm döngülerinizde test ediliyor…", "pg_row_load_hint": "Bu döngüyü yukarıdaki grafiğe yükle", - "pg_history_intro2": "Son döngülerinizi yukarıdaki ayarlarla gerçek algılayıcı ve eşleştiriciden geçirerek yeniden oynatın. Bir döngüyü grafiğe yüklemek için herhangi bir satıra tıklayın; öncesi/sonrası karşılaştırması görmek için bir ayarı değiştirin.", + "pg_history_intro2": "Son döngüleri yukarıdaki ayarlarla gerçek algılayıcı ve eşleştiriciden geçirerek yeniden oynatın. Grafiğe yüklemek için bir satıra tıklayın; öncesi ve sonrasını karşılaştırmak için bir ayarı değiştirin.", "pg_simulating": "Döngü simüle ediliyor…", - "pg_canvas_empty2": "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 ve kaydırmak için sürükleyin.", "pg_apply_settings_confirm": "Bu {n} ayar bu cihaza kaydedilsin mi? {list}", - "pg_stress_terminated_detail": "Yaklaşık {idle} W bekleme seviyesine oturdu -- döngü {h} sa {m} dk sonra {reason} ile sona erdi.", - "pg_stress_above_threshold_detail": "Bekleme tüketimi (~{idle} W) etkin durdurma eşiğine ({stop} W) eşit veya bu eşiğin üzerinde -- döngü hiçbir zaman sessiz olarak kaydedilmedi. Düzeltmek için stop_threshold_w değerini artırın.", - "pg_stress_hit_cap_detail": "Döngü {h} sa {m} dk boyunca durmadan çalıştı -- güvenlik sınırı tarafından zorla durduruldu. Bekleme tüketimi {idle} W, durdurma eşiği {stop} W.", "no_logs_match": "Bu filtrelerle eşleşen günlük kaydı yok.", "pg_analysis_hint2": "Bir döngü seçin ve eşleşme analizini yüklemek için Çalıştır'a basın.", - "pg_analysis_empty2": "Eşleşme analizini görmek için Çalıştır'a basın.", - "imported_readonly": "Topluluk mağazasından içe aktarıldı. Referans ve eşleştirme için gösterilir. İstatistiklerinize sayılmaz ve düzenlenemez.", + "imported_readonly": "Topluluk mağazasından içe aktarıldı. Referans ve eşleştirme için kullanılır, istatistiklerinize sayılmaz ve düzenlenemez.", "merge_no_imports": "İçe aktarılan kayıtlar gerçek bir döngüye birleştirilemez. Birleştirmek için seçimlerini kaldırın.", - "store_enable_hint": "Topluluk referans döngülerine göz atmak ve içe aktarmak için Ayarlar'da çevrimiçi özellikleri etkinleştirin.", - "advisory_duration_outlier": "'{name}' olarak etiketlenen {n} çevrimin süresi, bu programın olağan uzunluğuna hiç eşleşemeyecek kadar uzak (en kötüsü: {ratio}x). Muhtemelen yanlış etiketlenmişlerdir ya da tek bir kayıt iki çalışmayı birden içeriyordur. Bu programın süre tahminini bozmamaları için onları yeniden etiketleyin veya bölün.", + "store_enable_hint": "Topluluk referans döngülerine göz atmak ve içe aktarmak için Tercihler > Çevrimiçi ve Topluluk bölümünden çevrimiçi özellikleri açın.", + "advisory_duration_outlier": "'{name}' olarak etiketlenen {n} döngü, olağan uzunluğundan eşleşemeyecek kadar uzak (en kötüsü: {ratio}x). Muhtemelen yanlış etiketlenmişler ya da iki çalışma içeriyorlar. Süre tahminini bozmasınlar diye yeniden etiketleyin veya bölün.", "standby_above_stop_title": "Döngü bittiğinde hâlâ {idle} W çekiyordu", "standby_above_stop_sub": "Son {total} döngünün {n} tanesi. Kapatma Gecikmesi ancak güç Durdurma Eşiğinin ({stop} W) altına indiğinde başlar, bu yüzden döngüler geç biter veya elle durdurulmaları gerekir.", - "online_intro": "Diğer WashData kullanıcılarıyla referans döngüleri paylaşın ve indirin. Tüm çevrimiçi özellikler isteğe bağlıdır ve varsayılan olarak kapalıdır.", "store_share_intro": "Aynı cihaza sahip diğerlerinin kullanabilmesi için bu referans döngüyü yükleyin. Herkese açık olmadan önce incelenir.", - "online_intro_global": "Diğer WashData kullanıcılarıyla referans kayıtlarına göz atın ve paylaşın, cihaz girişlerini onaylayın. Tek bir bağlantı tüm WashData entegrasyonunuz için geçerlidir; cihaz markası ve modeli her cihaz için Temel bölümünde ayarlanır. Tüm çevrimiçi özellikler isteğe bağlıdır ve varsayılan olarak kapalıdır.", + "online_intro_global": "Diğer WashData kullanıcılarıyla referans kayıtlarına göz atın ve paylaşın. Tek bağlantı tüm WashData cihazlarınızı kapsar; marka ve model her cihaz için Temel altında ayarlanır. Çevrimiçi özellikler siz açana kadar kapalıdır.", "online_unavailable": "Çevrimiçi özellikler bu sunucuda kullanılamıyor.", "store_picker_offline": "Topluluk kataloğundan seçim yapmak için ayarlar dişlisinde çevrimiçi özellikleri etkinleştirin.", "pick_brand_first": "Önce bir cihaz markası seçin.", - "model_not_found": "Henüz katalogda yok.", - "connect_to_confirm": "Onaylamak veya değerlendirmek için ayarlar dişlisinde bağlanın.", + "custom_task_limit": "{max} görev sınırına ulaştınız.", + "custom_tasks_intro": "Hatırlatılmasını istediğiniz başka her şeyi ekleyin. Aralıklardan herhangi birine ulaşıldığında görevin zamanı gelir; yalnızca diğerini kullanmak için birini boş bırakın veya 0 girin.", + "maintenance_due_sensor_hint": "\"Bakım zamanı\" ikili sensörü, herhangi bir görevin zamanı geldiğinde açıktır; kendi bildirim otomasyonlarınız için.", + "maintenance_supporter_intro": "Maintenance Supporter çok daha fazlasını yapan ayrı bir entegrasyondur: her türlü cihaz veya görev, zamana, kullanıma ya da bir sensöre göre planlama, bildirimler ve geçmiş. WashData döngü sayacını kullanabilir.", "adopt_settings_hint": "Bu cihazın algılama ve eşleştirme eşiklerini paylaşılanlarla üzerine yaz. Bildirimleriniz, varlıklarınız ve enerji fiyatınız hiçbir zaman değiştirilmez.", - "include_settings_hint": "Bu cihazın tanıma ve eşleştirme eşiklerini paylaşın (bildirimlerinizi, varlıklarınızı veya enerji fiyatınızı değil). Benimseyenler bunların uygulanıp uygulanmayacağını seçer.", + "include_settings_hint": "Bu cihazın algılama ve eşleştirme eşiklerini paylaşın (bildirimler, varlıklar ve enerji fiyatı hariç). Diğerleri bunları uygulayıp uygulamayacağını seçer.", "onboard_download": "Yeni cihaz mı? Aynı cihaza sahip başka bir WashData kullanıcısından hazır bir kurulum (programlar, referans döngüleri ve fazlar) benimseyin.", - "share_consent": "Bu döngülerin kesintisiz normal şekilde tamamlandığını onaylıyorum", "share_device_none": "Henüz paylaşılabilir döngü yok. Önce Döngüler sekmesinde kayıtlı veya el ile seçilen bir döngüyü referans döngüsü (⭐) olarak işaretleyin.", "share_guideline_naming": "Her profili cihazın kadranında veya ekranında tam olarak gösterildiği gibi adlandırın (örn. 'Cotton 40', 'Eco 60').", - "share_guideline_quality": "Yalnızca normal şekilde tamamlanan döngüleri paylaşın -- döngü ortasında kesinti, kapı açma olayları veya güç kesintisi olmadan.", + "share_guideline_quality": "Yalnızca normal biten döngüleri paylaşın: kesinti, kapı açılması veya güç dalgalanması olmadan.", "share_guideline_review": "Yüklemeniz beklemede olarak başlar ve yeterince topluluk üyesi onayladığında kamuya açılır.", "share_guidelines_title": "Paylaşmadan önce", - "store_download_device_intro": "Her paylaşılan programı ve referans döngülerini cihazınıza benimseyin. Kendi kayıtlı döngüleriniz ve istatistikleriniz etkilenmez.", + "store_download_device_intro": "Paylaşılan her programı ve referans döngülerini cihazınıza ekleyin. Kendi döngüleriniz ve istatistikleriniz etkilenmez.", "store_share_device_intro": "Seçtiğiniz referans döngüleriyle {brand} {model} cihazını yükleyin. Aynı cihaza sahip diğer kullanıcılar programlarınızı benimseyebilir. Girişler kamuya açılmadan önce incelenir.", "share_profile_no_cycles": "Referans döngüsü yok -- bu profili dahil etmek için Döngüler sekmesinde bir döngüyü ⭐ olarak işaretleyin", - "advisory_phase_inconsistent": "'{name}' farklı programları veya sıcaklıkları karıştırıyor gibi görünüyor - çevrimleri çok farklı sürelerde ısıtma yapıyor. Bunu ayrı profillere bölmek (örneğin her sıcaklık için) eşleştirmeyi ve süre tahminlerini iyileştirir.", - "advisory_phase_inconsistent_title": "⚠ Karışık olabilecek programlar", - "export_select_intro": "Nelerin dahil edileceğini tam olarak işaretleyin. Profilleri döngüleri olmadan seçmek yine de tanınabilir bir program dışa aktarır (öğrenilen şekli de birlikte taşınır).", - "import_analyze_hint": "Dışa aktarılmış bir dosya yükleyin (veya JSON'unu yapıştırın). WashData bunu analiz eder ve herhangi bir değişiklik olmadan önce tam olarak neyin içe aktarılabileceğini gösterir.", + "export_select_intro": "Nelerin dahil edileceğini işaretleyin. Döngüleri olmayan profiller de eşleşebilir bir program olarak dışa aktarılır.", + "import_analyze_hint": "Dışa aktarılmış bir dosya yükleyin (veya JSON'unu yapıştırın). WashData, hiçbir şey değişmeden önce neyin içe aktarılabileceğini gösterir.", "analyzing": "Analiz ediliyor…", - "device_type_mismatch_warn": "Bu dışa aktarma farklı bir cihaz türünden ({src} ile {local}). Programlar ve döngüler yine de referans verisi olarak içe aktarılabilir, ancak cihaza özgü ayarlar ve gerçek geçmiş içe aktarımı devre dışıdır.", + "device_type_mismatch_warn": "Bu dışa aktarma farklı bir cihaz türünden ({src} ile {local}). Programlar ve döngüler yine de referans verisi olarak içe aktarılabilir, ancak cihaz ayarları ve gerçek geçmiş aktarılamaz.", "merge_hint": "İçe aktarılan öğeler eklenir; yerel hiçbir şey kaybolmaz. Ad çakışmaları aşağıda çözülür.", "replace_warn": "İşaretlenen her kategori silinir ve dosyadan gelenle değiştirilir. İşaretlenmeyen kategorilere dokunulmaz.", "dest_reference_hint": "İçe aktarılan döngüler yalnızca program tanımayı iyileştirir ve kullanım/enerji istatistiklerini asla etkilemez.", - "dest_real_history_hint": "İçe aktarılan döngüler bu cihazın kendi geçmişi olarak sayılır ve enerji/kullanım istatistiklerini besler. Bir cihazı yeni bir kuruluma taşırken kullanın.", + "dest_real_history_hint": "İçe aktarılan döngüler bu cihazın kendi geçmişi ve istatistikleri sayılır. Bir cihazı yeni bir kuruluma taşımak için kullanın.", "sug_muted": "Bu ayar bir daha önerilmeyecek", "sug_mute_failed": "Öneri susturulamadı", "sug_unmuted_all": "Susturulan öneriler sıfırlandı", - "n_suggestions_muted": "{count} susturuldu; öneri motoru bunları önermeyecek.", + "n_suggestions_muted": "{count} öneri susturuldu; öneri motoru bunları önermeyecek.", + "n_suggestions_muted_one": "{count} öneri susturuldu; öneri motoru bunu önermeyecek.", + "n_suggestions_muted_other": "{count} öneri susturuldu; öneri motoru bunları önermeyecek.", "font_size_hint": "Bu paneldeki her şeyi büyütün veya küçültün. Bu cihazdaki hesabınıza uygulanır.", - "import_history_description": "WashData'dan önce de akıllı bir priziniz var mıydı? Güç sensörünün geçmiş dışa aktarımını yükleyin veya doğrudan Home Assistant'tan okutun; normal algılama bunun üzerinde çalışır ve geçmiş döngüler adlandırılmaya hazır şekilde Döngüler listenizde görünür.", - "hist_input_hint": "Geçmiş panelinden indirdiğiniz bir CSV dosyasını yükleyin (entity, state, last changed) veya WashData'nın sensörün geçmişini doğrudan okumasına izin verin. Ardından algılama, tıpkı canlı çalışırken olduğu gibi bunun üzerinde çalışır ve bulunan döngülerden hangilerini saklayacağınızı siz seçersiniz.", - "hist_recorder_hint": "Seçtiğiniz tarihten şimdiye kadar okur. Home Assistant varsayılan olarak ayrıntılı geçmişi 10 gün tutar, sonrasında yalnızca saatlik ortalamaları saklar; bunlar döngü algılamak için fazla kabadır - yalnızca recorder daha uzun süre saklayacak şekilde ayarlıysa daha eski bir tarih seçin.", - "hist_scanning": "Geçmişiniz algılayıcı üzerinden yeniden oynatılıyor. Bu işlem arka planda çalışır - bu pencereyi kapatıp daha sonra geri dönebilirsiniz.", + "import_history_description": "WashData'dan önce akıllı bir priziniz var mıydı? Güç geçmişini yükleyin veya Home Assistant'tan okutun. Algılama bunun üzerinde çalışır ve geçmiş döngüler adlandırılmaya hazır olarak Döngüler listenizde görünür.", + "hist_input_hint": "Geçmiş panelinden bir CSV yükleyin (entity, state, last changed) veya WashData'nın sensör geçmişini doğrudan okumasına izin verin. Canlı algılama bunun üzerinde çalışır ve hangi döngülerin saklanacağını siz seçersiniz.", + "hist_recorder_hint": "Seçtiğiniz tarihten bugüne kadar okur. Home Assistant ayrıntılı geçmişi varsayılan olarak 10 gün tutar; daha eski saatlik ortalamalar döngü algılama için fazla kabadır.", + "hist_scanning": "Geçmişiniz arka planda algılayıcıdan geçiriliyor. Bu pencereyi kapatıp daha sonra dönebilirsiniz.", "hist_imported_count": "{n} döngü içe aktarıldı.", "hist_duplicates": "{n} tanesi daha önce içe aktarılmıştı ve atlandı.", "hist_capped": "İçe aktarılan döngüler için cihaz başına sınıra ulaşıldı; kalanlar kaydedilmedi.", - "hist_next_step": "Döngüler listenizde, içe aktarılmış geçmiş olarak işaretli halde duruyorlar. Birini açın ve ait olduğu programı adlandırmak için Etiketle'yi kullanın.", + "hist_next_step": "Döngüler listenizde, içe aktarılmış geçmiş olarak işaretli duruyorlar. Birini açın ve programını adlandırmak için Etiketle'yi kullanın.", "hist_rows_read": "{n} okuma alındı", "hist_entity_substituted": "{used} okundu (bu cihaz {wanted} için yapılandırılmış)", "hist_breaks": "sensörün kullanılamadığı {n} boşluk", @@ -1095,19 +1024,55 @@ "hist_scan_capped": "Yalnızca ilk adaylar gösteriliyor (toplam {n} bulundu).", "hist_recorder_empty": "Home Assistant'ta bu sensör için o aralıkta ayrıntılı geçmiş yok.", "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_scan_expired": "Bu tarama artık kullanılamıyor. 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.", - "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.", + "imported_history_readonly": "İçe aktarılan güç geçmişinde bulundu. Eşleştirmeye yardımcı olur, ancak istatistiklerinize sayılmaz ve kırpılamaz ya da bölünemez. Programını adlandırmak için etiketleyin.", + "odometer_intro": "Bu toplam yalnızca artar. Bir döngüyü silmek onu düşürmez, böylece servis hatırlatıcıları doğru kalır.", + "odometer_correct_hint": "WashData hiç gerçekleşmemiş bir çalışmayı saydıysa veya cihaz WashData kurulmadan önce döngü çalıştırdıysa bunu ayarlayın.", "no_reminders_set": "Henüz servis hatırlatıcısı ayarlanmadı.", "toast_program_armed": "Program sonraki döngü için ayarlandı: {program}", "toast_range_required": "Geçerli bir Başlangıç/Bitiş aralığı belirleyin", - "no_profiles_option": "(henüz profil yok, bir tane ekleyin)" + "no_profiles_option": "(henüz profil yok, bir tane ekleyin)", + "compare_cycles_title": "{count} döngüyü karşılaştır", + "edit_builtin_phase": "Bu yerleşik bir aşama. Kaydetmek özel bir kopya oluşturur; orijinali geri yüklemek için kopyayı silin.", + "fetch_error": "Veriler yüklenemedi.", + "connection_lost": "Bağlantı kesildi - {time} itibarıyla veriler gösteriliyor.", + "pg_notify_partial": "İşaretler yalnızca başlangıç, neredeyse bitti, bitiş, kilometre taşı ve sessiz saat bildirimlerini gösterir. Canlı güncellemeler, boşaltma hatırlatıcıları ve aşım uyarıları simüle edilmez.", + "connect_to_confirm": "Onaylamak veya değerlendirmek için ayarlar dişlisinde bağlanın.", + "coverage_gap_cluster": "Yaklaşık {min} dk süren {count} yakın tarihli etiketsiz döngü, henüz oluşturmadığınız tek bir programa benziyor.", + "pg_label_yes": "Döngü sonunda \"{name}\" olarak etiketlenirdi", + "pg_label_no": "İnceleme isterdi: {reason}", + "live_match_debug_tip": "Güven: mevcut güç eğrisinin en iyi aday profille ne kadar yakından eşleştiği (%0-100). Belirsiz: en iyi iki adayın puanları arasındaki fark %5'ten az - döngü bitene kadar etiket belirsizdir.", + "automation_alias_started": "{device}: döngü başladı", + "automation_alias_finished": "{device}: döngü bitti", + "automation_desc_started": "Bir {device} döngüsü başladığında çalışır. Eylemlerinizi ekleyin (bildirim, ışıklar, ...).", + "automation_desc_finished": "Bir {device} döngüsü bittiğinde çalışır. Eylemlerinizi ekleyin (bildirim, ışıklar, ...).", + "feedback_detected": "WashData algıladı: {name}", + "import_undo_hint": "Son içe aktarmadan ({when}) önceki bir geri yükleme noktası saklanıyor: {profiles} profil ve {cycles} döngü.", + "replace_undo_hint": "Önce bir geri yükleme noktası kaydedilir, böylece bu içe aktarmayı Dışa / İçe Aktar bölümünden geri alabilirsiniz.", + "pg_in_sample_note": "Bu döngüler, eşleştirildikleri profillerin oluşturulmasına katkıda bulundu, bu yüzden sonuçlar iyimserdir: yeni döngülerde biraz daha az doğru eşleşme bekleyin.", + "hist_tail_trimmed": "Döngü bittikten sonraki {min} dk bekleme, canlı entegrasyonun yaptığı gibi kırpıldı", + "hist_truncated": "{n} okuma sınırında durdu, bu yüzden geçmişin geri kalanı taranmadı", + "hist_warn_kw": "En yüksek okuma yalnızca {w}. Bu sensör kilovat bildiriyor gibi görünüyor; WashData watt gerektirir, bu yüzden hiçbir döngü algılanamaz.", + "pg_compare_profile_tip": "Yalnızca grafiğin üzerine çizilecek profili seçer. Simülasyon, canlı entegrasyonda olduğu gibi döngüyü her zaman kendisi eşleştirir.", + "pg_outcome_stale": "Bu simülasyon çalıştırıldıktan sonra ayarlar değişti. Hâlâ eski sonucu gösteriyor.", + "pg_sweep_guarded": "Daha fazla döngüyü erken bitirir, daha fazlasını böler veya mevcut ayarın yakaladığı döngüleri kaçırır, bu yüzden asla önerilmez.", + "pg_sweep_keep_current": "Mevcut değeri koruyun ({value}, {metric}): hiçbir değer en az bir döngü farkla daha iyi sonuç vermedi.", + "pg_task_busy": "Bu cihaz için zaten bir Geçmişte test et veya Optimize et çalışması sürüyor. Bitmesini bekleyin veya iptal edin.", + "hist_warn_naive_time": "{n} zaman damgasının saat dilimi yok ve UTC olarak okundu. Dosya yerel saat kullanıyorsa döngüler UTC farkınız kadar kayar ve yaz saati geçişine yakın okumalar atılabilir.", + "hist_warn_naive_time_one": "{n} zaman damgasının saat dilimi yok ve UTC olarak okundu. Dosya yerel saat kullanıyorsa döngüler UTC farkınız kadar kayar ve yaz saati geçişine yakın okumalar atılabilir.", + "hist_warn_naive_time_other": "{n} zaman damgasının saat dilimi yok ve UTC olarak okundu. Dosya yerel saat kullanıyorsa döngüler UTC farkınız kadar kayar ve yaz saati geçişine yakın okumalar atılabilir.", + "pg_sweep_n_cycles": "{count} döngü üzerinden", + "pg_sweep_n_cycles_one": "{count} döngü üzerinden", + "pg_sweep_n_cycles_other": "{count} döngü üzerinden", + "cycle_context_loading": "Kaydedici geçmişi yükleniyor…", + "cycle_context_note": "Gri: güç sensörünün bu döngü çevresindeki kaydedici geçmişi. Döngünün parçası değildir; süreyi, enerjiyi veya eşleştirmeyi etkilemez.", + "cycle_context_none": "Kaydedicide bu döngünün çevresine ait okuma yok (Home Assistant varsayılan olarak 10 günlük geçmiş tutar).", + "cycle_context_tip": "Yalnızca görüntüleme: hâlâ saklanıyorsa güç sensörünün bu döngü çevresindeki kaydedici geçmişini gösterir. Başarısız başlatma denemelerini gelecekteki döngülere eklemek için Eğri Ön Kaydı'nı kullanın (Ayarlar > Algılama > Döngü Başlangıcı); bu, kaydedilen süreleri değiştirir.", + "invalid_threshold_pair": "Durdurma Eşiği ({stop} W), Başlangıç Eşiği'nden ({start} W) düşük olmalı. Hiçbir şey kaydedilmedi; ikisini birlikte değiştirin." }, "pg_desc": { "completion_min_seconds": "Gerçek döngü sayılan en kısa çalışma", - "end_repeat_count": "Bitmeden önce arka arkaya düşük okuma", "interrupted_min_seconds": "Kısa döngüler kesintiye uğramış olarak işaretlenir", "min_off_gap": "İki döngüyü ayırmak için gereken boşluk", "off_delay": "Döngü bitmeden önceki düşük güç saniyesi", @@ -1115,37 +1080,36 @@ "start_threshold_w": "Başladı sayılması için minimum watt", "stop_threshold_w": "Bunun altında makine kapalı sayılır", "anti_wrinkle_enabled": "Ana aşamadan sonraki takla darbelerini yeni döngü olarak okumak yerine emer", - "anti_wrinkle_exit_power": "Kırışıklık önlemenin etkin kalması için gücün darbeler arasında bunun altına düşmesi gerekir", + "anti_wrinkle_exit_power": "Tambur darbeleri arasındaki sessiz seviye; yalnızca Durdurma Eşiği'nin üzerinde etkili olur", "anti_wrinkle_idle_timeout": "Kırışıklık önleme sona ermeden önce iki takla darbesi arasında izin verilen sessiz süre", "anti_wrinkle_max_duration": "Bundan uzun bir darbe kırışıklık önlemeyi bitirir ve yeni bir döngü açar", "anti_wrinkle_max_power": "Bunun üzerindeki bir darbe kırışıklık önlemeyi bitirir ve yeni bir döngü açar", - "dtw_bandwidth": "Aşama 3: Sakoe-Chiba büküm bandı (0 = DTW kapalı; varsayılan 0.2 = döngü uzunluğunun %20'si)", - "corr_weight": "Aşama 2: eğri şekli (korelasyon) ile güç düzeyi (MAE) arasındaki denge; varsayılan 0.45", - "duration_weight": "Aşama 4: çalışma süresi uyumunun nihai puanı ne kadar etkilediği (varsayılan 0.22)", - "energy_weight": "Aşama 4: enerji uyumunun nihai puanı ne kadar etkilediği (varsayılan 0.22)", - "profile_match_min_duration_ratio": "Aşama 1: profil süresinin bir kesri olarak minimum döngü uzunluğu; varsayılan 0.1 (%10)", - "profile_match_max_duration_ratio": "Aşama 1: profil süresinin bir kesri olarak maksimum döngü uzunluğu; varsayılan 1.8 (%180)", - "keep_min_score": "Aşama 2: yarışta kalmak için taban puan; varsayılan 0.1 zayıf eşleşmelere izin verir, sonraki aşamalar en iyisini seçer", - "dtw_blend": "Aşama 3: 0 = yalnızca çekirdek puan, 1 = yalnızca DTW puanı, 0.5 = eşit karışım (varsayılan)", - "dtw_ensemble_w": "Aşama 3 (topluluk modu): ölçeklendirilmiş-L1 ile türev DTW arasındaki ağırlık; varsayılan 0.7 düzey farkındalığını tercih eder", - "dtw_ddtw_scale": "Aşama 3: DDTW yarı doygunluk mesafesi; küçük = şekle daha duyarlı (varsayılan 30)", - "dtw_refine_top_n": "Aşama 3: DTW'nin yeniden puanladığı aday sayısı; doğru profil 4-5. sıraya düşüyorsa 7-9'a yükseltin (varsayılan 5)", - "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)", + "profile_match_min_duration_ratio": "Profil süresinin kesri olarak en kısa döngü uzunluğu; varsayılan 0.1 (%10)", + "profile_match_max_duration_ratio": "Profil süresinin kesri olarak en uzun döngü uzunluğu; varsayılan 1.8 (%180)", "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", - "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", + "smart_termination_duration_ratio": "Akıllı Sonlandırma'nın döngüyü erken bitirebilmesi için döngünün ulaşması gereken, programın ortalama süresine oranı; çalışma süreleri değişkense düşürün", + "anti_crease_finalize_ratio": "Kırışıklık önleme kuyruğunun döngüyü bitirebilmesi için döngünün ulaşması gereken, programın ortalama süresine oranı; 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.", "anti_wrinkle": "Kırışıklıkları azaltmak için döngü sonrası periyodik yuvarlanma.", + "baking": "Kabuk ve iç dokunun oluşması için yüksek sıcaklıkta çalışan ısıtıcı.", + "cooking": "Isıtıcı ve fanın çalıştığı ana pişirme aşaması.", "cool_down": "Döngü sonuna yaklaşıldığında ısınma olmadan yuvarlanma.", + "cool_down_af": "Isıtmadan sonra yalnızca fanla soğutma aşaması.", "drain_and_switch": "Yıkama modundan kurutma moduna geçiş süresi.", "dry": "Isıtıcı ve/veya artık ısı kullanılarak kurutma aşaması.", "drying": "Ana ısıtmalı tamburlama dönemi.", "heat_up": "Tam kurutma başlamadan önce ısıtıcının ilk ısınması.", + "keep_warm": "Yemeği sıcak tutmak için düşük ısıda bekletme aşaması.", + "keep_warm_bm": "Pişirmeden sonra ekmeği sıcak tutmak için düşük ısıda bekletme aşaması.", + "kneading": "Motorla hamuru karıştırma ve yoğurma. Yüksek güç çekişi.", + "pause_af": "Sallamak veya kontrol etmek için kısa duraklama.", + "pre_heat": "Tam pişirmeden önce haznenin ilk ısıtılması.", "pre_rinse": "Deterjanla yıkamadan önce ilk püskürtme.", + "proving": "Düşük ısıda mayalanma: maya çalışır ve hamur kabarır.", + "resting": "Glutenin dinlenmesi için yoğurma aşamaları arasında kısa, düşük güçlü duraklama.", "pre_wash": "Ana yıkamadan önce ilk ıslatma veya ön işlemden geçirin.", "rinse": "Temiz su durulama aşaması. Bu aşama birden çok kez tekrarlanabilir.", "sanitize": "Sterilizasyon programları için yüksek sıcaklıkta temizleme aşaması.", @@ -1158,6 +1122,7 @@ "wash_dw": "Ana deterjanla ısıtma ile yıkama." }, "placeholder": { + "custom_task": "örn. Kapı contasını temizle", "add": "ekle…", "group_name": "örn. Pamuk 2:47", "json_buttons": "[{\"action\":\"ID\",\"title\":\"Label\"}]", @@ -1184,7 +1149,7 @@ }, "section": { "anti_wrinkle": { - "intro": "Kırışıklık önleme modu, ana aşamadan sonra düşük güçlü tumble darbelerini algılar ve bunları tamamlanan döngüye bağlı tutar.", + "intro": "Kurutucunun ana aşamadan sonraki düşük güçlü tambur darbelerini biten döngüye bağlı tutar.", "label": "Kırışıklık Önleme" }, "basic": { @@ -1192,7 +1157,7 @@ "label": "Temel" }, "delay": { - "intro": "Gecikmeli başlatma algılaması, cihazın açık olduğu ancak henüz döngüsüne başlamadığı durumu tespit eder.", + "intro": "Açık olup gecikmeli başlatmayı bekleyen bir cihazı algılar.", "label": "Gecikmeli Başlatma" }, "detection": { @@ -1216,153 +1181,145 @@ "label": "Zamanlama & Bekçi" }, "triggers": { - "intro": "İsteğe bağlı harici sinyaller: bitiş tetikleyicisi, kapı sensörü, duraklatma anahtarı ve boşaltma hatırlatıcısı.", + "intro": "İsteğe bağlı ekler: bitiş tetikleyicisi, kapı sensörü, duraklatma anahtarı ve boşaltma hatırlatıcısı.", "label": "Tetikleyiciler & Kapılar" }, - "phase_eta": { - "label": "Kalan Süre", - "intro": "Çevrim uzunluğu sıcaklığa veya sıkmaya bağlı olan makineler için aşama farkındalıklı kalan süre." - }, "dishwasher": { "label": "Bulaşık Makinesi", - "intro": "Bulaşık makineleri için döngü sonu işleme; bu makineler genellikle kısa bir son tahliyeden önce, neredeyse hiç güç çekmeyen uzun bir kurutma aşamasıyla biter." + "intro": "Bulaşık makineleri için bitiş işleme; bu makineler kısa bir son tahliyeden önce çoğu zaman uzun süre sessizce kurutur." } }, "setting": { "anti_wrinkle_enabled": { - "doc": "Kurutucunun ana ısıtma aşamasından sonra yaydığı kısa, düşük güçlü tambur darbelerini tanıyın ve bunları yeni döngüler olarak okumak yerine bitmiş döngüye bağlı tutun.", + "doc": "Kurutucunun ana ısıtma aşamasından sonraki kısa, düşük güçlü tambur darbelerini yeni döngü saymak yerine biten döngüyle birlikte tutun.", "label": "Kırışıklık Önleme Algılamayı Etkinleştir" }, "anti_wrinkle_exit_power": { - "doc": "Kırışıklık önleme modunun aktif kalması için darbeler arasında gücün bu değerin altına düşmesi gerekir.", + "doc": "Kırışıklık önleme modunda bunun altındaki güç tambur darbeleri arasında sessizlik sayılır ve Maks. Darbe Aralığı'ndan uzun süren sessizlik modu bitirir. Durdurma Eşiği daha yüksekse o kullanılır; bu yüzden bu ayar yalnızca Durdurma Eşiği'nin üzerine ayarlandığında bir şey değiştirir.", "label": "Çıkış Gücü Eşiği" }, "anti_wrinkle_idle_timeout": { "label": "Maks. Darbe Aralığı", - "doc": "Kırışıklık önleme modu sona ermeden önce makinenin iki takla darbesi arasında ne kadar süre sessiz kalabileceği. Bunu kurutucunuzun darbeler arasında bıraktığı en uzun boşluğun üzerine ayarlayın, aksi halde sonraki her darbe hatalı başlangıç olarak okunur." + "doc": "Kırışıklık önleme modu bitmeden önce kurutucunun tambur darbeleri arasında ne kadar sessiz kalabileceği. Kurutucunuzun en uzun boşluğunun üzerine ayarlayın, yoksa sonraki darbeler hatalı başlangıç olarak okunur." }, "anti_wrinkle_max_duration": { - "doc": "Bundan daha uzun darbeler, kırışıklık önleyici bir takla yerine gerçek bir döngü olarak değerlendirilir.", + "doc": "Bundan uzun bir darbe, kırışıklık önleme tambur dönüşü değil, gerçek bir döngü sayılır.", "label": "Maks. Süre" }, "anti_wrinkle_max_power": { - "doc": "Bu gücün üzerindeki bir nabız, kırışıklık karşıtı bir düşüş değil, gerçek bir yeni döngü olarak değerlendirilir. Takla darbe gücünün hemen üstüne ayarlayın.", + "doc": "Bu gücün üzerindeki bir darbe, tambur dönüşü değil, gerçek bir yeni döngü sayılır. Tambur darbesi gücünüzün hemen üzerine ayarlayın.", "label": "Maks. Kırışıklık Önleme Gücü" }, "auto_label_confidence": { - "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.", + "doc": "Döngü sonunda eşleşme puanı bu değerde veya üzerindeyse program sormadan etiketlenir. Daha fazla kesinlik için yükseltin; daha çok döngüyü otomatik etiketlemek için düşürün. Öğrenme Güveni'nin üzerinde tutun.", "label": "Otomatik Etiket Güveni" }, "auto_maintenance": { - "doc": "Gecelik temizlik yapın: profil zarflarını yeniden oluşturun, döngü sağlığını yeniden hesaplayın, hata ayıklama izlerini budayın ve en son döngüleri koruyun.", + "doc": "Gece temizliği: artık bir programla açıkça eşleşen eski döngüleri etiketler, program eğrilerini yeniden oluşturur, anomali işaretlerini ve döngü sağlığını yeniler, Hata Ayıklama İzlerini Kaydet kapalıysa hata ayıklama izlerini kaldırır. Döngüler ve güç izleri her zaman saklanır.", "label": "Otomatik Bakım (gece temizliği)" }, "completion_min_seconds": { - "doc": "Bundan daha kısa döngüler hayalet döngüler olarak atılır (test çalıştırmaları, çorap eklemek için kapıyı açma).", + "doc": "Bundan kısa döngüler (test çalıştırmaları, çorap eklemek için kapıyı açmak) Kesintiye uğradı olarak kaydedilir ve bitiş bildirimi gönderilmez.", "label": "Min. Döngü Süresi" }, "delay_confirm_seconds": { - "doc": "Cihazın çalışmak yerine başlamayı bekliyor olarak değerlendirilmesi için gücün bu süre boyunca bekleme bandında kalması gerekir.", + "doc": "Cihazın başlamayı bekliyor sayılması için gücün bu süre boyunca bekleme bandında kalması gerekir.", "label": "Onay Saniyeleri" }, "delay_start_detect_enabled": { - "doc": "Cihazın ne zaman açık olduğunu ve beklediğini (gecikmeli başlatma / bekleme) ancak kendi döngüsüne başlamadığını tespit edin, böylece bekleme çekmesi bir çalışma döngüsüyle karıştırılmaz.", + "doc": "Açık olup gecikmeli başlatmayı bekleyen bir cihazı algılayın; böylece bekleme tüketimi çalışan bir döngü sanılmaz.", "label": "Gecikmeli Başlatma Algılamayı Etkinleştir" }, "delay_timeout_hours": { - "doc": "Bu kadar saatten sonra gecikmeli başlatma modunda beklemeyi bırakın ve boşta çalışmaya dönün; böylece, açık bırakılan ancak hiç başlatılmayan bir makine sonsuza kadar beklemez.", + "doc": "Bu kadar saat sonra gecikmeli başlatmayı beklemeyi bırakın ve boşta durumuna dönün.", "label": "Zaman Aşımı Saatleri" }, "device_type": { - "doc": "Cihaz sınıfı. Söz konusu cihaz türü için ayarlanmış mantıklı algılama varsayılanlarını (eşikler, düşmede gecikme, son işleme) ayarlar; bunu yalnızca aygıt başlangıçta yanlış türde ayarlanmışsa değiştirin.", + "doc": "Bu tür cihaz için algılama varsayılanlarını (eşikler, kapatma gecikmesi, bitiş işleme) ayarlar. Yalnızca cihaz yanlış türle kurulduysa değiştirin.", "label": "Cihaz Türü" }, "door_sensor_entity": { - "doc": "Opsiyonel kapı ikili sensörü. Bir döngüden sonra cihazın ne zaman açıldığını/boşaltıldığını tespit etmek için kullanılır.", + "doc": "İsteğe bağlı kapı sensörü. Bir döngüden sonra cihazın boşaltmak için ne zaman açıldığını gösterir.", "label": "Kapı Sensörü Varlığı" }, "duration_tolerance": { - "doc": "Kalan zaman tahminlerine yönelik tolerans (öğrenme geri bildirimi, eşleşme değil). Gerçek süre tahminin +/-%X'i dahilindeyse, iyi bir eşleşme olarak sayılır.", + "doc": "Yalnızca öğrenme geri bildirimi: tahmini süresinin +/-%X'i içinde kalan bir döngü iyi tahmin sayılır. Eşleştirmeyi etkilemez.", "label": "Tahmin Toleransı" }, "enable_ml_models": { - "doc": "Bir döngü çalışırken, modellerin canlı sonuçları iyileştirmesine izin verin: daha istikrarlı bir kalan süre ve enerji/maliyet tahmini ve son tespitte erken durdurma önleme koruması (bir bitişi yalnızca geciktirebilir, hiçbir zaman erken bitiremez ve sınırlıdır). Mümkün olduğunda ince ayarlı modellerinizi, aksi takdirde yerleşik olanları kullanır. Kapalı = yalnızca klasik güç tabanlı mantık (hala güvenilir).", + "doc": "Çalışan bir döngünün enerjisini ve maliyetini tahmin etmek için bu makinede eğitilmiş bir model kullanın; iyi bilinen bir program alışılmadık erken durursa kapatma gecikmesinin tamamını beklemeden bitsin. Kapalı: yalnızca güce dayalı mantık.", "label": "Döngü sırasında akıllı modelleri uygula" }, "end_energy_threshold": { - "doc": "Düşmede gecikme geri sayımı sırasında biriken enerji (watt x zaman) bu eşikle karşılaştırılır. Aşılması durumunda geri sayım sıfırlanır; kırışık önleyici tamburlar ve bulaşık makinesi kurutma kuyrukları kısa kesilmek yerine döngüye bağlı kalır. Soğutma sırasında döngüler çok erken biterse yükseltin; algılama yavaşsa düşürün.", + "doc": "Kapatma gecikmesi sırasında izin verilen enerji. Bundan fazlası olursa döngü devam eder; böylece tambur dönüşleri ve kurutma kuyrukları döngüye bağlı kalır. Döngüler bir kuyruk sırasında bitiyorsa düşürün; biten döngüler beklemeye devam ediyorsa yükseltin.", "label": "Bitiş Enerjisi" }, - "end_repeat_count": { - "doc": "Döngü sona ermeden önce gereken ardışık durma eşiği altı okumalarının sayısı. Çoğu fiş için 1 uygundur. Akıllı priziniz ara sıra döngünün ortasında yanlış sıfır örneği bildiriyorsa ve döngüleriniz zamanından önce sona eriyorsa 2-3'e yükseltin.", - "label": "Bitiş Tekrar Sayısı" - }, "energy_price_entity": { - "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.", + "doc": "kWh başına güncel fiyatı veren sensör (örn. dinamik tarife). Aşağıdaki sabit fiyatı geçersiz kılar, bu yüzden prizin güç veya enerji sensörü değil, bir fiyat olmalıdır.", "label": "Enerji Fiyatı Varlığı" }, "energy_price_static": { - "doc": "Yukarıda hiçbir canlı fiyat varlığı belirlenmediğinde maliyet rakamları için kullanılan kWh başına sabit fiyat.", + "doc": "Fiyat varlığı ayarlanmadığında kullanılan kWh başına sabit fiyat.", "label": "Sabit Enerji Fiyatı (kWh başına)" }, "energy_sensor": { - "doc": "İsteğe bağlı kümülatif enerji sayacı (total_increasing kWh/Wh, örn. prizin kendi ömür boyu sayacı). Ayarlandığında, her döngünün raporlanan enerjisi bu sayacın baştan sona olan farkından alınır; bu da yavaş raporlayan bir güç sensörünün integralini almaktan kaynaklanan eksik sayımı önler. Okuma eksikse, birimi bilinmiyorsa veya fark pozitif değilse integral alınmış değere geri döner. Güç sensörünün integralini almaya devam etmek için boş bırakın.", + "doc": "İsteğe bağlı kWh veya Wh sayacı (total_increasing, örn. prizin kendi sayacı). Döngü enerjisi o zaman buradan okunur; bu, yavaş bir güç sensöründen hesaplamaktan daha doğrudur. Okuması kullanılamazsa hesaplanan değer kullanılır.", "label": "Enerji Sayacı Varlığı" }, "expose_debug_entities": { - "doc": "Ekstra teşhis HA varlıkları yayınlayın (eşleşme güveni, belirsizlik, dahili durum bilgileri). Kapalı, varlık listesini normal kullanım için temiz tutar.", + "doc": "Tanılama varlıkları ekleyin (eşleşme güveni, belirsizlik, dahili durum). Kapalı, varlık listesini temiz tutar.", "label": "Hata Ayıklama Varlıklarını Göster" }, "external_end_trigger": { - "doc": "Durum değişikliği döngünün sonunu işaret eden ikili sensör (örneğin, cihazın \"tamamlanmış\" kontağı veya tamamlayıcı entegrasyon).", + "doc": "Değişimi döngünün sonunu işaretleyen ikili sensör (örn. \"tamamlandı\" kontağı veya başka bir entegrasyon).", "label": "Harici Tetikleyici Varlığı" }, "external_end_trigger_enabled": { - "doc": "Yerleşik güç tabanlı algılamaya ek olarak harici bir ikili sensörün döngünün sonunu bildirmesine izin verin.", + "doc": "Güce dayalı algılamaya ek olarak bir ikili sensörün döngü sonunu bildirmesine izin verin.", "label": "Harici Bitiş Tetikleyicisini Etkinleştir" }, "external_end_trigger_inverted": { - "doc": "Tetik sensörünün KAPALI duruma geçmesini (AÇIK yerine) döngü sonu sinyali olarak değerlendirin.", + "doc": "Sensörün AÇIK yerine KAPALI duruma geçmesini bitiş sinyali sayın.", "label": "Harici Tetikleyiciyi Tersine Çevir (KAPALI'da tetikle)" }, "learning_confidence": { - "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.", + "doc": "Biten bir döngüyü etiketlemek için en düşük eşleşme puanı. Açık bir kazanan etiketlenir; kıl payı durumlar Döngüler'deki inceleme kuyruğuna gider. Bunun altında hiçbir şey etiketlenmez veya sorulmaz. Otomatik Etiket Güveni'nin altında tutun.", "label": "Öğrenme Güveni" }, "linked_device": { - "doc": "İsteğe bağlı olarak bu WashData cihazını, HA cihaz kaydında \"Bağlandı ...\" olarak gösterilen başka bir cihazın (örn. akıllı priz) altına yerleştirin.", + "doc": "İsteğe bağlı. Bu WashData cihazını Home Assistant'ta başka bir cihazın (örn. akıllı priz) altında \"Bağlandı ...\" olarak gösterin.", "label": "Cihaz Altında Grupla" }, "min_off_gap": { - "doc": "Makine bu süreden daha kısa bir süre kapanırsa açma/kapama/açma sırası tek bir sürekli döngü olarak değerlendirilir. Islatma programlarının (makine yıkamanın ortasında birkaç dakika süreyle kapanır) iki ayrı programa bölünmesini önler. Bunların ayrı döngüler olarak sayılmasını istiyorsanız, arka arkaya yükleriniz arasındaki boşluktan daha kısa ayarlayın. Cihaz tipi varsayılanları, her cihaz için tipik döngü içi duraklamayı korur.", + "doc": "Bundan kısa bir kapalı süre döngüyü bölmez; böylece ıslatma duraklamaları tek yıkamada kalır. Arka arkaya yükler arasındaki boşluktan kısa tutun, yoksa bunlar tek döngüde birleşir.", "label": "Min. Kapalı Aralık" }, "min_power": { - "doc": "Mutlak minimum gücün aktif olduğu kabul edilir. Bunun altındaki okumalar 0 W (bekleme) olarak kabul edilir ve akıllı prizlerin ve bekleme LED'lerinin hayalet yükü filtrelenir.", + "doc": "Bunun altındaki okumalar 0 W (bekleme) sayılır. Akıllı prizlerin ve bekleme LED'lerinin boşta tüketimini filtreler.", "label": "Minimum Güç" }, "ml_training_enabled": { - "doc": "İncelenen döngülerinizi gece boyunca periyodik olarak inceleyin ve modellerde bu özel makineye göre ince ayar yapın. Bir değişiklik yalnızca uzayan döngülerde gerçekten daha iyi puan aldığında korunur, bu nedenle bu yalnızca yardımcı olabilir veya aynı kalabilir; asla gerilemez.", + "doc": "Enerji modelini zaman zaman bu makinenin biten döngüleriyle yeniden eğitin. Yeni model yalnızca test döngülerinde basit tahminden iyi sonuç verirse tutulur.", "label": "Bu makineden öğren" }, "ml_training_hour": { - "doc": "Gece ince ayarını yapmak için günün yerel saati (0-23). 2 (02:00) gibi sessiz bir saat seçin.", + "doc": "Gece eğitimi için günün saati (0-23). Sessiz bir saat seçin, örn. 2.", "label": "Saatte öğren" }, "ml_training_interval_days": { - "doc": "Gelişmeleri en fazla bu kadar günde bir kez yeniden kontrol edin.", + "doc": "En fazla bu kadar günde bir yeniden eğitin.", "label": "En fazla her seferinde kontrol et" }, "ml_training_min_cycles": { - "doc": "İnce ayar yapmadan önce en azından bu sayıda döngü kaydedilene kadar bekleyin, böylece öğrenilecek yeterli şey olur.", + "doc": "Eğitimden önce en az bu kadar kayıtlı döngü bekleyin.", "label": "Önce gereken döngüler" }, "name": { - "doc": "HA entegrasyonları listesinde ve cihaz kayıt defterinde gösterilen görünen ad.", + "doc": "Bu cihaz için Home Assistant'ta gösterilen ad.", "label": "Cihaz Adı" }, "no_update_active_timeout": { - "doc": "Çalışırken bu kadar uzun süre güç güncellemesi gelmezse, fişin çevrimdışı olduğunu varsayalım ve bir zombi döngüsünden kaçınmak için zorla durdurun. Varsayılan 600 sn, bulut veya ağ gecikmesine izin verir.", + "doc": "Bir döngü sırasında bu süre boyunca güç güncellemesi gelmezse prizin çevrimdışı olduğu varsayılır ve döngü zorla durdurulur. Varsayılan 600 sn, bulut veya ağ gecikmelerine pay bırakır.", "label": "Güncelleme Yok Zaman Aşımı" }, "notify_before_end_minutes": { @@ -1370,35 +1327,35 @@ "label": "Bitiş Öncesi Uyarı" }, "notify_channel": { - "doc": "Başlat/canlı mesajlar için Android bildirim kanalı adı (kanal başına sesi ve mobil uygulamada önceliği kontrol eder). Blank, tamamlayıcı uygulama varsayılanını kullanır.", + "doc": "Başlangıç ve canlı mesajlar için Android kanalı; kanal sesi ve önceliği belirler. Boş: uygulama varsayılanı.", "label": "Android Kanalı (başlangıç/canlı)" }, "notify_cycle_timers": { - "doc": "Bir döngünün belirli dakikalarında bildirimler (örneğin yumuşatıcı eklemek için). Mesaj {device}, {program}, {minutes}'yi destekliyor. Bu noktada duraklatmak ve Devam Et düğmesiyle etkileşimli bir bildirim almak için Otomatik duraklatmayı etkinleştirin; panel, duraklatma/devam ettirme hizmeti veya bildirim eylemi aracılığıyla devam ettirebilirsiniz.", + "doc": "Döngünün belirli dakikalarında bildirimler (örn. yumuşatıcı eklemek için). Mesaj {device}, {program}, {minutes} destekler. Otomatik duraklatma ile döngü orada da duraklar ve bildirime Devam et düğmesi eklenir.", "label": "Döngü Zamanlayıcıları" }, "notify_finish_channel": { - "doc": "Bitiş mesajı için Android bildirim kanalı adı. Boş, başlangıç/canlı kanalını yeniden kullanır.", + "doc": "Bitiş mesajı için Android kanalı. Boş: başlangıç/canlı kanalı kullanılır.", "label": "Android Kanalı (bitiş)" }, "notify_finish_message": { - "doc": "Bir döngü bittiğinde gönderilen gövde metni. Şablon değişkenleri: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}. Burada en kullanışlı olanlar {time_finished} ve {vs_typical}.", + "doc": "Döngü bittiğinde gönderilen metin. Değişkenler: {device}, {program}, {duration} (dakika), {duration_hm} (1 h 05 min), {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}, {status} (completed veya force_stopped; kesintiye uğrayan döngüler hiçbir şey göndermez). Başka bir değişken kullanılırsa varsayılan metin gönderilir.", "label": "Bitiş Mesajı" }, "notify_finish_services": { - "doc": "notify.* Bir döngü bittiğinde çağrılan hizmetler. Hedef başına bir tane ekleyin; bitiş bildirimi yoksa boş bırakın.", + "doc": "Döngü bittiğinde çağrılacak notify.* hizmetleri. Hiçbiri için boş bırakın.", "label": "Bitiş Servisleri" }, "notify_fire_events": { - "doc": "Ayrıca döngü başlangıcında/bitişinde ha_washdata_* olaylarını tetikleyin, böylece kendi otomasyonlarınızı oluşturabilirsiniz.", + "doc": "Kendi otomasyonlarınız için döngü başlangıcında ve bitişinde ha_washdata_* olaylarını da tetikleyin.", "label": "Bildirimler için HA Olayları Tetikle" }, "notify_icon": { - "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.", + "doc": "Bildirimdeki küçük simge (örn. mdi:washing-machine). Yalnızca mobile_app hedeflerine gönderilir, notify gruplarına gönderilmez. iOS'ta uygulama simgesinin yerini alır ve 2026.8 veya daha yeni tamamlayıcı uygulama gerektirir. Boş: platform varsayılanı.", "label": "Bildirim Simgesi" }, "notify_live_chronometer": { - "doc": "Bildirimde statik bir tahmin yerine (bunu destekleyen platformlarda) canlı güncellenen bir geri sayım sayacı gösterin.", + "doc": "Bildirimde sabit bir tahmin yerine canlı bir geri sayım sayacı gösterin (destekleniyorsa).", "label": "Canlı Kronometre Kullan" }, "notify_live_interval_seconds": { @@ -1406,143 +1363,135 @@ "label": "Canlı Güncelleme Aralığı" }, "notify_live_overrun_percent": { - "doc": "Bir döngü, tahminini bu yüzdeden daha fazla aşarsa, bir taşma uyarısı gönderin.", + "doc": "Bir döngü beklenen süresini bu yüzde kadar aştığında canlı güncellemeler durur (20 = beklenen sürenin 1,2 katına kadar).", "label": "Uyarı Öncesi Canlı Aşım %" }, "notify_live_services": { - "doc": "notify.* hizmetleri, bir döngü çalışırken canlı ilerleme güncellemeleri için çağrıda bulundu. Canlı ilerleme bildirimlerini devre dışı bırakmak için boş bırakın.", + "doc": "Döngü çalışırken canlı ilerleme güncellemeleri için notify.* hizmetleri. Kapatmak için boş bırakın.", "label": "Canlı İlerleme Servisleri" }, "notify_only_when_home": { - "doc": "Yalnızca bağlantılı kişilerden (yukarıdaki) en az biri evde olduğunda bildirim gönderin.", + "doc": "Yalnızca yukarıdaki kişilerden en az biri evdeyken bildirim gönderin.", "label": "Yalnızca Evdeyken Bildir" }, "notify_people": { - "doc": "kişi.* \"Yalnızca Evdeyken Bildir\" tarafından evde kimsenin olup olmadığına karar vermek için kullanılan varlıklar.", + "doc": "\"Yalnızca Evdeyken Bildir\" ayarının kontrol ettiği person.* varlıkları.", "label": "Kişiler (Evde İken için)" }, "notify_pre_complete_message": { - "doc": "Bitiş öncesi / neredeyse tamam uyarısının gövde metni. Şablon değişkenleri: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Her canlı ilerleme güncellemesinin metni. Şablon değişkenleri: {device}, {program}, {minutes} (kalan dakika). Başka bir değişken kullanılırsa varsayılan metin gönderilir.", "label": "Bitiş Öncesi Mesaj" }, "notify_reminder_message": { - "doc": "Hâlâ bekleyen boşaltma hatırlatıcısının gövde metni. Boş bırakmak yerleşik varsayılanı kullanır. Şablon değişkenleri: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Tek seferlik Bitiş Öncesi Uyarı metni. Şablon değişkenleri: {device}, {program}, {minutes} (Bitiş Öncesi Uyarı ayarı). Başka bir değişken kullanılırsa varsayılan metin gönderilir.", "label": "Hatırlatıcı Mesaj" }, "notify_start_message": { - "doc": "Bir döngü başladığında gönderilen gövde metni. Şablon değişkenleri: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Döngü başladığında gönderilen metin. Şablon değişkenleri: {device}, {program}. Başka bir değişken kullanılırsa varsayılan metin gönderilir.", "label": "Başlangıç Mesajı" }, "notify_start_services": { - "doc": "notify.* Bir döngü başladığında çağrılan hizmetler. Hedef başına bir tane ekleyin (telefon, kontrol paneli vb.); Başlatma bildirimi için boş bırakın.", + "doc": "Döngü başladığında çağrılacak notify.* hizmetleri (telefon, pano, ...). Hiçbiri için boş bırakın.", "label": "Başlangıç Servisleri" }, "notify_timeout_seconds": { - "doc": "Bu kadar saniye sonra bildirimi otomatik olarak kapat (bunu destekleyen platformlarda). 0 manuel olarak kapatılana kadar bunu korur.", + "doc": "Bildirimi bu kadar saniye sonra kapatın (destekleniyorsa). 0, siz kapatana kadar tutar.", "label": "Sonra Otomatik Kapat" }, "notify_title": { - "doc": "Bildirim başlığı. Şablon değişkenleri: {device}, {program}, {duration}, {energy}, {cost}, {date}, {time}, {minutes}.", + "doc": "Bildirim başlığı. Yalnızca {device} yerine konur; başka bir değişken kullanılırsa varsayılan başlık gönderilir.", "label": "Bildirim Başlığı" }, "notify_unload_delay_minutes": { - "doc": "Hala bekleyen \"makineyi boşaltın\" hatırlatıcısı gönderilmeden önce bir döngü bittikten dakikalar sonra. Hatırlatıcıyı devre dışı bırakmak için 0'a ayarlayın.", + "doc": "Döngü bittikten sonra \"makineyi boşaltın\" hatırlatıcısına kadar geçecek dakika. 0 kapatır.", "label": "Boşaltma Hatırlatıcısı Gecikmesi" }, "notify_unload_repeat": { - "doc": "Boşaltma onaylanana veya bildirimdeki \"Hatırlatmayı durdur\" düğmesine dokunana kadar boşaltma hatırlatıcısını \"Boşaltma Hatırlatıcısı Gecikmesi\" dakikada bir yeniden gönderir. Boşaltmayı onaylamak için bir yola ihtiyaç duyar: bir Kapı Sensörü Varlığı, bir Boşaltma Onay Varlığı veya aşağıdaki \"Boşaltmayı Elle Onayla\" seçeneği. Kapatma düğmesi Home Assistant refakatçi uygulaması (mobil) bildirimlerinde çalışır.", + "doc": "Boşaltma onaylanana veya siz \"Hatırlatmayı durdur\"a dokunana kadar (mobil uygulama) boşaltma hatırlatıcısını her \"Boşaltma Hatırlatıcısı Gecikmesi\" dakikada bir yeniden gönderin. Kapı Sensörü Varlığı, Boşaltma Onay Varlığı veya \"Boşaltmayı Elle Onayla\" gerektirir.", "label": "Boşaltılana Kadar Tekrarla" }, "off_delay": { - "doc": "Güç kesildikten sonra döngünün bittiğini bildirmeden önce beklenecek süre. Bu pencerede güç yeniden gelirse döngü sorunsuz bir şekilde devam eder; bu, yıkama aşamaları arasındaki duraklamaları köprüler. Bulaşık makinelerinin kurutma aşamaları uzundur (20-60 dakika boyunca güç kapalıdır), bu nedenle, tüm yıkama+kuruluğun tek bir döngüde tutulması için kapatma gecikmesinin bunu aşması gerekir.", + "doc": "Döngünün bitmiş sayılması için gücün ne kadar düşük kalması gerektiği. Bu süre içinde geri gelen güç döngüyü sürdürür. Bulaşık makineleri kurutma sırasında 20-60 dk boşta kalabilir, bu yüzden bundan uzun ayarlayın.", "label": "Kapatma Gecikmesi" }, "pause_cuts_power": { - "doc": "Bir döngü duraklatıldığında aşağıdaki Anahtar Varlığını da kapatın. Yalnızca kullanım süresinin ortasında fişi güvenli bir şekilde kesilebilen cihazlar için.", + "doc": "Döngü duraklatıldığında aşağıdaki Anahtar Varlığı'nı da kapatın. Yalnızca döngü ortasında gücü güvenle kesilebilen cihazlar için.", "label": "Duraklatma Gücü de Keser (anahtar üzerinden)" }, "power_off_delay": { - "doc": "Bir döngü bittikten sonra durumun Kapalı'ya dönmesinden önce gücün Güç Kapatma Eşiği'nin altında ne kadar süre kalması gerektiğidir. Yalnızca Güç Kapatma Eşiği 0'ın üzerindeyken kullanılır. Arka plan döngüsünde denetlendiğinden, geçerli gecikme bir sonraki durum sona erme adımına yukarı yuvarlanır.", + "doc": "Bir döngüden sonra durumun Kapalı'ya dönmesi için gücün Güç Kapatma Eşiği'nin altında ne kadar kalması gerektiği. Yalnızca bu eşik 0'ın üzerindeyken kullanılır. Düzenli aralıklarla kontrol edilir, bu yüzden gerçek bekleme biraz daha uzun olabilir.", "label": "Güç Kapatma Gecikmesi" }, "power_off_threshold_w": { - "doc": "Güce dayalı isteğe bağlı Kapalı algılama. 0'ın üzerinde olduğunda, bir döngü bittikten sonra güç, Güç Kapatma Gecikmesi süresince bu seviyenin altında kalırsa makine kapatılmış sayılır ve durum Kapalı'ya döner. Devre dışı bırakmak için 0'da bırakın (varsayılan). Bu değeri, gerçek kapalı taban değerinin üzerinde ve Durdurma Eşiği ile makinenizin bittiği hâlde açık kalan bekleme tüketiminin altında ayarlayın; Durdurma Eşiği'nin altında değilse yok sayılır. Etkinleştirildiğinde, Kapalı'ya dönmek için İlerleme Sıfırlama Gecikmesi'nin yerini alır; böylece biten bir makine, gerçekten kapatılana kadar Bitti/Temiz durumunda kalır.", + "doc": "Bir döngüden sonra güç, Güç Kapatma Gecikmesi boyunca bunun altında kalırsa durum, İlerleme Sıfırlama Gecikmesi'ni beklemeden Kapalı olur. Kapatılmış cihazın çektiği gücün üzerine, bekleme gücünün ve Durdurma Eşiği'nin altına ayarlayın. 0 kapatır (varsayılan).", "label": "Güç Kapatma Eşiği" }, "power_sensor": { - "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.", + "doc": "Bu cihazın canlı gücünü watt cinsinden bildiren sensör (örn. sensor.washer_power). Tüm algılama buna dayanır.", "label": "Güç Sensörü" }, - "profile_duration_tolerance": { - "doc": "Eşleştirme sırasında kullanılan profil ortalama süresi etrafındaki +/- bandı. 0,25, 60 dakikalık bir profilin 45-75 dakikalık döngülerle eşleştiği anlamına gelir.", - "label": "Profil Süre Toleransı" - }, "profile_match_interval": { - "doc": "Çalışan bir döngü sırasında profil eşleştirmenin ne sıklıkla deneneceği. Varsayılan 300 sn (5 dakika), algılama hızını ve CPU'yu dengeler.", + "doc": "Çalışan bir döngünün profillerle ne sıklıkla eşleştirileceği. Varsayılan 300 sn (5 dk).", "label": "Eşleşme Aralığı" }, "profile_match_max_duration_ratio": { - "doc": "Profile göre maksimum döngü uzunluğu. 1.3, bir döngünün eşleşmesi için profil süresinin %130'unun altında olması gerektiği anlamına gelir.", + "doc": "Döngü, programın tipik süresinin bu katından uzun çalıştığında program elenir. 1,8: programdan %80 uzun süren bir döngü artık onunla eşleşmez.", "label": "Maks. Süre Oranı" }, "profile_match_min_duration_ratio": { - "doc": "Profile göre minimum döngü uzunluğu. 0,9, bir döngünün eşleşmesi için profil süresinin en az %90'ı olması gerektiği anlamına gelir.", + "doc": "Bir program ancak döngü, programın tipik süresinin bu oranı kadar çalıştıktan sonra değerlendirilir. 0,10: 2 saatlik bir program 12 dakika sonra aday olur. Döngünün ilk 15 dakikasında uygulanmaz.", "label": "Min. Süre Oranı" }, "profile_match_threshold": { - "doc": "Bir program tanımlamasını kabul etmek için döngü sonunda gereken minimum benzerlik puanı (0-1). Yanlış tanımlamaları azaltmak için kaldırın; makinenizin programları eşleşmiyorsa düşürün. Varsayılan 0,4 ihtiyatlı bir başlangıç noktasıdır.", + "doc": "Akıllı Sonlandırma veya kırışıklık önleme bitişinin döngüyü erken bitirebilmesi için gereken canlı eşleşme puanı. Gösterilen programı değiştirmez. Zayıf eşleşmeler döngüleri erken bitiriyorsa yükseltin; net eşleşmeler kapatma gecikmesini bekliyorsa düşürün.", "label": "Eşleşme Eşiği" }, "profile_unmatch_threshold": { - "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.", + "doc": "Canlı eşleşme puanı bunun altına düşerse mevcut program tahmini silinir. Kısa bir düşüş sıfırlamasın diye Eşleşme Eşiği'nin biraz altında tutun.", "label": "Eşleşmeme Eşiği" }, "progress_reset_delay": { - "doc": "Bitirdikten sonra, bu süre boyunca ilerlemeyi %100'de tutun; böylece Tamamlandı, Boşta durumuna sıfırlamadan önce kontrol panellerinde görünür.", + "doc": "Bir döngüden sonra, Boşta durumuna sıfırlamadan önce ilerlemeyi bu süre %100'de tutun.", "label": "İlerleme Sıfırlama Gecikmesi" }, "pump_stuck_duration": { - "doc": "Bir pompa, muhtemelen sıkışmış olarak işaretlenmeden önce sürekli olarak çalışabilir (sıkışmış pompa olayını tetikler).", + "doc": "Takılı pompa olayı tetiklenmeden önce bir pompanın kesintisiz ne kadar çalışabileceği.", "label": "Pompa Takılma Süresi" }, "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.", + "doc": "İşlenen okumalar arasındaki en kısa süre. Daha erken gelen okumalar atlanır (döngü sırasındaki düşük okumalar her zaman korunur). Prizinizin raporlama aralığına veya daha düşüğe ayarlayın; daha yüksek değer yalnızca okumaları çöpe atar.", "label": "Örnekleme Aralığı" }, "save_debug_traces": { - "doc": "Her döngü için tam güç takibini ve eşleşen hata ayıklama verilerini saklayın. Sorun giderme için kullanışlıdır ancak depolama boyutunu artırır.", + "doc": "Sorun giderme için eşleştiricinin hata ayıklama verilerini (her adayın puanları) her döngüyle birlikte saklayın. Döngü başına yaklaşık 20 KB ekler.", "label": "Hata Ayıklama İzlerini Kaydet" }, "smart_termination_duration_ratio": { - "doc": "Güç düştüğünde Akıllı Sonlandırma'nın bir döngüyü erken bitirebilmesi için, döngünün eşleşen programın beklenen süresinin ne kadarına ilerlemiş olması gerektiğini belirler. Beklenen süre, programın ortalamasıdır; bu nedenle çalışma süresi çok değişen cihazlarda (kışın soğuk, yazın sıcak şebeke suyuyla çalışan çamaşır makineleri, sensörle kurutan kurutucular, yüke bağlı programlar) tüm çalışmaların yaklaşık yarısı bu ortalamadan daha kısa biter ve hızlı bitişi hiç alamaz, yalnızca yedek zaman aşımıyla dakikalar sonra sona erer. Bu makinelerde erken bitişin yine de devreye girmesi için bu değeri düşürün (örn. 0,85); daha ihtiyatlı olmak için 1,0'a doğru yükseltin. Varsayılan (0,98, bulaşık makineleri için 0,99) için boş bırakın. Bir döngüyü yalnızca daha erken bitirebilir, asla daha geç bitiremez ve belirsiz veya düşük güvenilirlikli bir eşleşmede asla devreye girmez.", + "doc": "Akıllı Sonlandırma'nın döngüyü erken bitirebilmesi için döngünün ulaşması gereken, programın ortalama süresine oranı. Çalışma süreleri değişkense (su sıcaklığı, sensörlü kurutma, yük miktarı) düşürün (örn. 0,85). 0,98 için boş bırakın (bulaşık makinelerinde 0,99).", "label": "Akıllı Sonlandırma Oranı" }, - "smoothing_window": { - "doc": "Ham güç sinyalinin ne kadar yumuşatıldığı. Düşük (2) duyarlıdır ancak gürültülüdür; yüksek (5) ani artışları yumuşatır ancak gecikme ekler.", - "label": "Yumuşatma Penceresi" - }, "start_duration_threshold": { - "doc": "Gerçek bir başlatmayı onaylamak için gücün, başlatma eşiğinin bu kadar uzun süre üzerinde kalması gerekir; böylece anlık açma/kapama geçişlerinin bir döngü başlatması önlenir.", + "doc": "Döngünün başlaması için gücün Başlangıç Eşiği'nin üzerinde bu kadar kalması gerekir; böylece kısa açma/kapama sıçramaları yok sayılır.", "label": "Başlangıç Süresi" }, "start_energy_threshold": { - "doc": "Cihazın ÇALIŞMADAN önce tüketmesi gereken enerji (güç x süre). Kısa bir yüksek güçlü ani yükselişin enerjisi çok düşüktür ve dikkate alınmaz, böylece hatalı başlatmalar önlenir.", + "doc": "Döngünün çalışıyor sayılması için cihazın kullanması gereken enerji. Kısa bir güç sıçraması az enerji kullanır, bu yüzden döngü başlatmaz.", "label": "Başlangıç Enerjisi" }, "start_threshold_w": { - "doc": "Bir döngünün başladığını doğrulamak için gücün bu seviyenin üzerine çıkması gerekir. Çok düşük bir değere ayarlanması, bekleme gücünden hatalı başlatmalara neden olur; çok yüksek ve yavaş başlayan programlar (soğuk dolum) kaçırılır. Öneri motoru bunu makinenin gözlemlenen en düşük aktif gücünün hemen üzerine ayarlar.", + "doc": "Döngünün başlaması için gücün bunun üzerine çıkması gerekir. Çok düşük: bekleme gücü hatalı başlatmalara yol açar. Çok yüksek: yavaş başlayan programlar (soğuk su alma) kaçırılır.", "label": "Başlangıç Eşiği" }, "stop_threshold_w": { - "doc": "Düşmede gecikme geri sayımı başlamadan önce güç bu seviyenin altına düşmelidir. Bunu Başlangıç Eşiğinin altına ayarlayın; aralarındaki boşluk, titremeyi önleyen histerezis bandıdır. Çok yükseğe ayarlanırsa, düşük güçlü fazlar (durulamada bekletme, kırışık önleme) hatalı şekilde bitiş sırasını tetikler.", + "doc": "Kapatma gecikmesinin saymaya başlaması için gücün bunun altına düşmesi gerekir. Başlangıç Eşiği'nin altında tutun. Çok yüksekse düşük güçlü aşamalar (durulama bekletmesi, kırışıklık önleme) bitiş geri sayımını yanlışlıkla başlatır.", "label": "Durdurma Eşiği" }, "switch_entity": { - "doc": "İsteğe bağlı anahtar, duraklatma durumunda kapatılır ve devam ederken tekrar açılır; \"Duraklatma aynı zamanda gücü de keser\" ile birlikte kullanılır.", + "doc": "\"Duraklatma Gücü de Keser\" açıkken duraklatmada kapatılan ve devam edince yeniden açılan isteğe bağlı anahtar.", "label": "Anahtar Varlığı" }, "watchdog_interval": { - "doc": "Arka plan gözlemcisinin durmuş sensörleri ve geçen zaman aşımlarını ne sıklıkla kontrol ettiği. Varsayılan 30 sn.", + "doc": "Arka plan denetiminin durmuş sensörleri ve zaman aşımlarını ne sıklıkla kontrol ettiği. Varsayılan 30 sn.", "label": "Bekçi Aralığı" }, "notify_milestone_message": { @@ -1550,139 +1499,94 @@ "label": "Kilometre Taşı Mesajı" }, "notify_milestones": { - "doc": "Ulaşıldığında bir kerelik kutlama bildirimi tetikleyen, virgülle ayrılmış döngü sayıları (örn. 50, 100, 500, 1000). Boş bırakmak kilometre taşı bildirimlerini devre dışı bırakır.", + "doc": "Bir kerelik kutlama bildirimi tetikleyen döngü sayıları, virgülle ayrılmış (örn. 50, 100, 500, 1000). Boş bırakmak bunları kapatır.", "label": "Döngü Kilometre Taşları" }, "notify_quiet_end_hour": { - "doc": "Rahatsız etmeyin aralığının sonu (0-23). Bekletilen bildirimler bu saatte iletilir. Devre dışı bırakmak için boş bırakın.", + "doc": "Rahatsız etmeyin aralığının sonu (0-23); bekletilen bildirimler o zaman gönderilir. Boş bırakmak kapatır.", "label": "Sessiz Saatler Sonu" }, "notify_quiet_start_hour": { - "doc": "Rahatsız etmeyin aralığının başlangıcı (0-23). Sessiz saatlerde gönderilecek bitiş, hatırlatma ve temiz çamaşır bildirimleri bekletilir ve aralık bittiğinde iletilir. Devre dışı bırakmak için boş bırakın. Gece yarısını geçen aralıkları destekler (örn. başlangıç 22, bitiş 7).", + "doc": "Rahatsız etmeyin aralığının başlangıcı (0-23). Bu aralıktaki bitiş, hatırlatma ve temiz çamaşır bildirimleri aralık bitene kadar bekletilir. Gece yarısını geçebilir (örn. 22'den 7'ye). Boş bırakmak kapatır.", "label": "Sessiz Saatler Başlangıcı" }, "peak_rate_message": { - "doc": "Başlangıç bildirimine eklenen yüksek tarife ipucu için isteğe bağlı özel metin. Şablon değişkenleri: {device}, {price}. Boş bırakmak yerleşik varsayılanı kullanır.", + "doc": "Yüksek tarife ipucu için özel metin. Şablon değişkenleri: {device}, {price}. Boş: varsayılan kullanılır.", "label": "Yüksek Tarife Mesajı" }, "peak_rate_threshold": { - "doc": "Bir döngü başladığında ve kWh başına geçerli fiyat bu değere eşit veya bu değerin üzerindeyse, başlangıç bildirimine bir yüksek tarife ipucu eklenir. 0 veya boş bırakmak ipucunu devre dışı bırakır.", + "doc": "Döngü başladığında kWh başına fiyat bu değerde veya üzerindeyse başlangıç bildirimine bir yüksek tarife ipucu ekleyin. 0 veya boş bırakmak kapatır.", "label": "Yüksek Tarife Eşiği (kWh başına)" }, - "dtw_bandwidth": { - "label": "Şekil eşleştirme toleransı", - "doc": "Eşleştiricinin, kaydedilmiş bir programın şekline uyması için bir döngüyü zaman içinde ne kadar bükebileceği. Daha yüksek değer daha fazla hız değişimini tolere eder; çok yüksek olması yanlış programı eşleştirme riski taşır." - }, - "corr_weight": { - "label": "Şekil ve düzey ağırlığı", - "doc": "Bir eşleşme puanlanırken eğri şekli ile mutlak güç düzeyi arasındaki denge. Daha yüksek değer şekli tercih eder; daha düşük değer güç düzeyinin eşleştirilmesini tercih eder." - }, - "duration_weight": { - "label": "Süre ağırlığı", - "doc": "Tipik çalışma süresi bu döngüyle eşleşen bir programın ne kadar güçlü tercih edileceği." - }, - "energy_weight": { - "label": "Enerji ağırlığı", - "doc": "Tipik enerji tüketimi bu döngüyle eşleşen bir programın ne kadar güçlü tercih edileceği." - }, "store_brand": { "label": "Cihaz Markası", - "doc": "İsteğe bağlı. Topluluk kataloğundan seçilen cihaz markası. Eşleşen referans kayıtlarını bulmak ve paylaşmak için kullanılır. Çevrimiçi özellikleri kullanmıyorsanız boş bırakın." + "doc": "İsteğe bağlı. Topluluk kataloğundan cihaz markanız; referans kayıtlarını bulmak ve paylaşmak için kullanılır. Çevrimiçi özellikleri kullanmıyorsanız boş bırakın." }, "store_model": { "label": "Cihaz Modeli", - "doc": "İsteğe bağlı. Bir marka ayarlandığında topluluk kataloğundan seçilen cihaz modeli. Modeliniz listede yoksa kataloğa ekleyebilirsiniz." + "doc": "İsteğe bağlı. Topluluk kataloğundan cihaz modeliniz (önce bir marka seçin). Listede yoksa ekleyebilirsiniz." }, "show_contributor": { "doc": "Topluluk cihazlarında ve referans döngülerinde \"- \" özelliğini göster." }, - "enable_phase_matching": { - "label": "Aşama farkındalıklı kalan süre", - "doc": "Çalışan her çevrimi aşamalara (ısıtma, yıkama, sıkma) böler ve kalan süreyi aşama başına ayırarak klasik tahminle harmanlar - çevrimin başında aşama bütçesine, sona yaklaşırken klasik tahmine ağırlık verir. Bu, geri sayımı makinenizin gerçekte ne kadar ısıttığına ve çalıştığına göre kişiselleştirir; en çok çevrimin ilk yarısında fark edilir. Kapalı = yalnızca klasik tahmin. Yalnızca kalan süre göstergesi etkilenir; program eşleştirme ve çevrim algılama değişmez." - }, - "keep_min_score": { - "label": "Min. Eşleşme Puanı", - "doc": "Bir adayın eşleşme yarışında kalabilmesi için gereken minimum benzerlik puanı. Varsayılan 0.1 kasıtlı olarak gevşektir - zayıf adayları bile kabul eder ve en iyi eşleşmeyi sonraki aşamalara bırakır. Olası olmayan profilleri daha erken elemek için artırın; başlangıç aday havuzunu genişletmek için azaltın." - }, - "dtw_blend": { - "label": "Büküm Karışımı", - "doc": "Zaman bükme (DTW) hizalama puanının Aşama 2 çekirdek puanını ne ölçüde değiştirdiği. 0 = yalnızca Aşama 2 puanı, 1 = yalnızca DTW puanı, 0.5 (varsayılan) = eşit karışım. Programların güç düzeyleri benzer ancak zamanlama kalıpları farklıysa büküm hizalamasına daha fazla güvenmek için artırın." - }, - "dtw_ensemble_w": { - "label": "Büküm Topluluk Karışımı", - "doc": "Topluluk DTW modunda (varsayılan), ölçeklendirilmiş L1 ile türev DTW (DDTW) arasındaki ağırlık. 1.0 = tamamen ölçeklendirilmiş-L1, 0 = tamamen DDTW, 0.7 = varsayılan. Ölçeklendirilmiş-L1 değişkeni güç düzeylerini karşılaştırır; türev değişkeni gücün zaman içinde nasıl değiştiğine tepki verir. Topluluk her ikisini de birleştirir." - }, - "dtw_ddtw_scale": { - "label": "Türev Büküm Ölçeği", - "doc": "Türev DTW puanlaması için yarı doygunluk mesafesi. DDTW mesafesi bu değere eşit olduğunda puan yarıya düşer. Küçük = şekil farklılıklarına daha duyarlı. Varsayılan 30, tipik ev aletleri güç izleri için kalibre edilmiştir. Yalnızca topluluk ve ddtw DTW modlarını etkiler." - }, - "dtw_refine_top_n": { - "label": "Büküm Yeniden Skor Sayısı", - "doc": "Kaç üst Aşama 2 adayı DTW tarafından yeniden puanlandırılır. DTW daha pahalıdır, bu nedenle yalnızca en iyi N adaya uygulanır. Varsayılan 5. Doğru profil bazen Aşama 2'den sonra yalnızca 4. veya 5. sıraya geliyorsa artırın (7-9'a) - DTW onu kurtarabilir. Azaltmak eşleşmeyi biraz hızlandırır." - }, - "duration_scale": { - "label": "Süre Keskinliği", - "doc": "Aşama 4 süre uyumu cezasının keskinliği. Bu, uyum puanının yarıya düştüğü logaritmik orandır. Küçük = daha sert: bir süre uyumsuzluğu daha fazla zarara yol açar. Varsayılan 0.175, yarı ağırlıkta yaklaşık %18 süre toleransına karşılık gelir. Süre Ağırlığı ile birlikte kullanın." - }, - "energy_scale": { - "label": "Enerji Keskinliği", - "doc": "Aşama 4 enerji uyumu cezasının keskinliği. Küçük = daha sert: bir enerji uyumsuzluğu daha fazla zarara yol açar. Varsayılan 0.25, enerji yükle değiştiğinden Süre Keskinliğinden kasıtlı olarak daha gevşektir. Enerji Ağırlığı ile birlikte kullanın." - }, "power_profile_interval_min": { "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." + "doc": "EMHASS ve tibber_prices gibi planlayıcıların kullandığı power_profile özelliğinin kova boyutu (dilim başına ortalama watt). Küçük değer kısa sıçramaları korur; büyük değer eğriyi yumuşatır. Varsayılan 15 dk. 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." + "doc": "Yalnızca iOS. Canlı ilerlemeyi sessizce, iOS'un gruplayabileceği pil tasarruflu bir öncelikle güncelleyin. İlk güncelleme ve başlangıç/bitiş bildirimleri yine uyarı verir. Kapalı: her güncelleme uyarı verir." }, "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." + "doc": "Yalnızca Android. Canlı ilerleme bildirimi dokunulduğunda kalsın. Kapalı: dokunmak bildirimi kapatır." }, "notify_live_click_action": { "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." + "doc": "Bu cihazın bildirimine dokunulduğunda ne açılacağı. Boş: bu cihazda WashData paneli. Bir pano yolu (örn. /lovelace/laundry) veya URL girin; dokunma eylemi olmaması için \"none\" yazın." }, "door_opens_at_end": { "label": "Döngü Sonunda Kapı Otomatik Açılır", - "doc": "Döngü sonunda kurutmak için kapıyı açan (AirDry ve benzeri) bulaşık makineleri için. Bu açıkken, çalışan bir döngüde kapının açılması onu sonsuza kadar duraklatmaz; bunun yerine kapı aşağıdaki bekleme süresi kadar açık kalırsa WashData döngüyü tamamlanmış sayar. Kısa bir açılma (nesne ekleme) dikkate alınmaz. Kapı Sensörü Varlığı gerektirir." + "doc": "Kurutmak için sonda kapıyı açan bulaşık makineleri için (örn. AirDry). Aşağıdaki bekleme süresi kadar açık kalan kapı döngüyü duraklatmak yerine bitirir; kısa bir açılma yok sayılır. Kapı Sensörü Varlığı gerektirir." }, "door_end_dwell_seconds": { "label": "Kapı Açık Bekleme Süresi", - "doc": "\"Döngü Sonunda Kapı Otomatik Açılır\" açık olduğunda WashData'nın döngüyü sonlandırması için kapının ne kadar süre açık kalması gerektiği. Bir tabağı hızla eklemeyi göz ardı edecek kadar uzun (varsayılan 60 sn), makine kapıyı açtıktan sonra zamanında sona erecek kadar kısa." + "doc": "\"Döngü Sonunda Kapı Otomatik Açılır\" açıkken: döngünün bitmesi için kapının ne kadar açık kalması gerektiği. Tabak eklemeyi yok sayacak kadar uzun olmalı (varsayılan 60 sn)." }, "dishwasher_end_spike_quiet_release": { "label": "Pasif Kurutma Sessiz Serbest Bırakma", - "doc": "Döngü beklenen süresini aştıktan sonra, WashData bir son tahliye beklemeyi bırakıp döngüyü sonlandırmadan önce bulaşık makinesinin ne kadar süre sessiz (Durdurma Eşiği'nin altında) kalması gerektiğidir. Makineniz, gecikmeli bir son tahliyeden önce uzun ve sessiz bir kurutma aşamasına sahipse ve bu tahliye kaçırılıyorsa değeri artırın - daha geniş bir pencere, öğrenilen sürenin eski ortalamaya kilitlenmek yerine mevsimsel kaymayı (daha soğuk şebeke suyu = daha uzun döngüler) izlemesini sağlar. Bu ayar, dahili 30 dakikalık son tahliye tepesi sınırına göre beklemeyi yalnızca kısaltabilir, asla uzatamaz." + "doc": "Beklenen süreden sonra, WashData son tahliyeyi beklemeyi bırakmadan önce bulaşık makinesinin Durdurma Eşiği'nin altında ne kadar kalması gerektiği. Uzun bir kurutmadan sonraki geç tahliye kaçırılıyorsa yükseltin. Bekleme asla 30 dakikayı aşmaz." }, "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." + "doc": "Her programın eğrisini ve eşleştirmesini hangi tür döngülerin şekillendireceği. Bir türün işaretini kaldırmak hiçbir şey silmez; içe aktarılan verilere güvenmiyorsanız kullanışlıdır. İstatistikler her zaman yalnızca bu makinenin döngülerini sayar. Hepsinin işaretini kaldırmak yok sayılır." }, "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." + "doc": "Her döngüyü, bittiğindeki fiyat yerine çalıştığı her andaki fiyattan ücretlendirin. Bir fiyat varlığı gerektirir. Geçmişi İşle (Tanılamalar) eski döngülerin maliyetini yeniden hesaplar." }, "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." + "doc": "Düşük güçlü tambur dönüşünün kırışıklık önleme kuyruğu sayılması için döngünün ulaşması gereken, programın ortalama süresine oranı. Yük algılayan kurutucularda düşürün (örn. 0,75). Çamaşır makinelerini varsayılanda (0,98) bırakın, yoksa bir yıkama yarıda bitebilir." }, "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." + "doc": "Başlamadan önce yoklayan makineler için (kapı kilidi, ilk su alma) başarısız başlatma denemelerinin okumalarını döngünün başına ekler. 300 sn çoğu için yeterlidir; 0 = kapalı. Kaydedilen başlangıçlar öne kayar, bu yüzden öğrenilen süreler bir süre kayar." }, "unload_confirm_entity": { "label": "Boşaltma Onay Varlığı", - "doc": "Kapı sensörü takılamayan makineler için, etkinleşmesi yükün çıkarıldığı anlamına gelen isteğe bağlı varlık. Her türü çalışır: bir Zigbee veya NFC düğmesi, bir input_button yardımcısı, makinenin önündeki bir hareket sensörü, bir sahne. Kapanma ve kullanılamaz duruma geçme dışındaki her durum değişikliği onay sayılır. Bunu ayarlamak, Kapı Sensörü Varlığı olmayan bir cihazda da boşaltma hatırlatıcısını etkinleştirir." + "doc": "İsteğe bağlı. Kapı sensörü olmayan makineler için yükün çıkarıldığını onaylayan herhangi bir varlık (düğme, input_button, hareket sensörü, sahne). Kapanma veya kullanılamaz olma dışındaki her durum değişikliği sayılır. Boşaltma hatırlatıcısını da etkinleştirir." }, "unload_track_without_door": { "label": "Boşaltmayı Elle Onayla", - "doc": "Kapı Sensörü Varlığı ve Boşaltma Onay Varlığı bulunmayan bir cihazda boşaltma hatırlatıcısını çalıştırır ve boşaltmayı bunun yerine kendi otomasyonunuzdan onaylarsınız: \"Boşaltıldı Olarak İşaretle\" düğme varlığına basın veya ha_washdata.mark_unloaded servisini çağırın. Bir Kapı Sensörü Varlığı ayarlandığında yok sayılır, çünkü sinyali zaten o sağlar." + "doc": "Boşaltma hatırlatıcısını kapı sensörü veya onay varlığı olmadan çalıştırın. Boşaltmayı kendi otomasyonunuzdan \"Boşaltıldı Olarak İşaretle\" düğmesiyle veya ha_washdata.mark_unloaded servisiyle onaylayın. Kapı Sensörü Varlığı ayarlıysa yok sayılır." }, "notify_icon_color": { "label": "Bildirim Rengi", - "doc": "Bu cihazın bildirimleri için vurgu rengi; böylece çamaşır makinesi, kurutma makinesi ve bulaşık makinesi bir bakışta ayırt edilebilir. iOS simgeyi ve Canlı Etkinlik ilerleme çubuğunu Kilitli ekranda ve Dynamic Island'da renklendirir. Android'e de aynı renk gönderilir, ancak bununla ne yapacağı telefona bağlıdır: bazıları simgeyi renklendirir, birçoğu ise (özellikle Samsung One UI) rengi tamamen yok sayar. Her ikisi de Hex kodunu (ör. #03A9F4) anlar. Platform varsayılanı için boş bırakın." + "doc": "Bu cihazın bildirimleri için vurgu rengi; böylece çamaşır makinesi, kurutma makinesi ve bulaşık makinesi kolayca ayırt edilir. iOS'ta simgenin arkasındaki daireyi doldurur ve Canlı Etkinlik çubuğunu renklendirir. Birçok Android telefon (özellikle Samsung) bunu yok sayar. Hex kullanın, örn. #03A9F4. Boş: platform varsayılanı." + }, + "share_name": { + "doc": "Paylaştığınız döngülerin yükleyeni olarak hesap adınızı gösterin. Kapalı: adsız paylaşılır." } }, "setting_group": { @@ -1710,9 +1614,6 @@ "door_pause": { "label": "Kapı & Duraklatma" }, - "dtw_refinement": { - "label": "DTW İyileştirme" - }, "duration_gates": { "label": "Süre Kapıları" }, @@ -1789,13 +1690,18 @@ "rebuilding": "Yeniden oluşturuluyor…", "analyzing": "Analiz ediliyor…", "resetting": "Sıfırlanıyor…", - "reverting": "Geri alınıyor…", "trimming": "Kırpılıyor…", "splitting": "Bölünüyor…", + "stalled": "Takıldı", "deleting": "Siliniyor…", "imported": "İçe aktarıldı", "preparing": "Hazırlanıyor…", - "unlabelling": "Etiket kaldırılıyor…" + "unlabelling": "Etiket kaldırılıyor…", + "working": "İşleniyor…", + "match_uncertain": "Belirsiz: {top} veya {runner}", + "match_uncertain_single": "Belirsiz: belki {top}", + "match_sure": "~%{pct} emin", + "match_uncertain_tip": "WashData henüz bir programa karar vermedi. Bu oran, öndeki tahmin {top} bu kadar öndeyken geçmiş döngülerde ne sıklıkla doğru çıktığını gösterir. Yalnızca bilgi amaçlıdır." }, "tab": { "advanced": "Gelişmiş", @@ -1827,13 +1733,17 @@ "toast": { "catalog_refreshed": "Topluluk kataloğu yenilendi", "contrib_device_changed": "Topluluk mağazasına eklendi, ancak form açıkken seçili cihaz değişti. Bunu Ayarlar'dan seçin.", + "task_name_required": "Göreve bir ad verin", + "task_added": "Görev eklendi", + "task_save_failed": "Görev kaydedilemedi: {error}", + "task_removed": "Görev kaldırıldı", + "maint_logged": "Yapıldı olarak kaydedildi: {task}", "access_saved": "Erişim denetimi kaydedildi", "all_wiped": "Tüm veriler silindi", "analysis_complete_none": "Analiz tamamlandı: yeni öneri yok", "auto_label_complete": "Otomatik etiketleme tamamlandı", "automation_deleted": "Otomasyon silindi", "correction_submitted": "Düzeltme gönderildi", - "cycle_deleted": "Döngü silindi", "cycle_force_stopped": "Döngü zorla durduruldu", "cycle_labelled": "Döngü etiketlendi", "cycle_paused": "Döngü duraklatıldı", @@ -1849,11 +1759,9 @@ "group_deleted": "Grup silindi", "group_name_required": "Grup adı gerekli", "group_saved": "Grup kaydedildi", - "import_successful": "İçe aktarma başarılı; entegrasyon yeniden yükleniyor", "json_required": "JSON verisi gerekli", "legacy_removed": "Eski eylemler kaldırıldı", "logs_exported": "Günlükler dışa aktarıldı", - "matching_reverted": "Eşleşme ağırlıkları varsayılanlara döndürüldü", "min_2_profiles": "Grup için en az 2 profil seçin", "ml_training_no_improvement": "Eğitim tamamlandı: taban çizgisi korundu (iyileşme yok)", "models_reverted": "Cihazdaki modeller taban çizgisine döndürüldü", @@ -1863,8 +1771,8 @@ "pg_preset_loaded": "\"{name}\" ön ayarı yüklendi", "pg_preset_saved": "\"{name}\" ön ayarı kaydedildi", "pg_sugg_loaded": "{n} önerilen değer yüklendi - karşılaştırmak için Test Alanı'nı çalıştırın", - "pg_sugg_ml_loaded": "{n} ML kalibre değer yüklendi - karşılaştırmak için Test Alanı'nı çalıştırın", "phase_name_required": "Aşama adı gerekli", + "phase_reset": "\"{name}\" yerleşik aşamaya geri döndürüldü", "phase_updated": "Aşama güncellendi", "phases_saved": "Aşamalar kaydedildi", "preferences_saved": "Tercihler kaydedildi", @@ -1911,14 +1819,10 @@ "store_name_required": "Bir profil adı girin", "store_import_failed": "İçe aktarma başarısız: {error}", "store_imported": "{profile} içine aktarıldı", - "store_program_required": "Bir program adı girin", "store_no_appliance": "Önce cihaz markanızı ve modelinizi Ayarlar'da belirleyin.", "store_share_failed": "Paylaşım başarısız: {error}", "store_shared": "Topluluk mağazasında paylaşıldı, inceleme bekliyor.", "appliance_added": "Cihaz eklendi, onay bekliyor", - "thanks_confirming": "Onayladığınız için teşekkürler", - "device_approved": "Topluluk tarafından onaylandı", - "rating_saved": "Kalite değerlendirmesi kaydedildi", "brand_added": "Marka eklendi, onay bekliyor", "profile_added": "Profil eklendi, onay bekliyor", "saved_except_conflicts": "Kaydedildi. Geri kalanı kaydetmek için vurgulanan çakışmaları düzeltin.", @@ -1941,11 +1845,50 @@ "odometer_saved": "Toplam döngü sayısı kaydedildi", "odometer_save_failed": "Toplam kaydedilemedi: {error}", "store_pick_profile_share": "Paylaşılacak bir profil seçin", - "remove_failed": "Kaldırma başarısız: {error}" + "remove_failed": "Kaldırma başarısız: {error}", + "analysis_complete": "Analiz tamamlandı: {count} öneri", + "analysis_failed": "Analiz başarısız: {error}", + "auto_detect_failed": "Otomatik algılama başarısız: {error}", + "auto_label_failed": "Otomatik etiketleme başarısız: {error}", + "could_not_load_cycle": "Döngü yüklenemedi: {error}", + "create_failed": "Oluşturma başarısız: {error}", + "cycles_deleted": "{count} döngü silindi", + "debug_cleared": "{count} hata ayıklama izi temizlendi", + "discard_failed": "İptal başarısız: {error}", + "error": "Hata: {error}", + "export_failed": "Dışa aktarma başarısız: {error}", + "import_failed": "İçe aktarma başarısız: {error}", + "invalid_json": "\"{key}\" geçerli bir JSON değil; kaydetmeden önce düzeltin veya alanı temizleyin.", + "label_failed": "Etiketleme başarısız: {error}", + "ml_training_promoted": "Eğitim tamamlandı: {count} model yükseltildi", + "pause_failed": "Duraklatma başarısız: {error}", + "phase_created": "\"{name}\" aşaması oluşturuldu", + "phase_deleted": "\"{name}\" aşaması silindi", + "profile_created": "\"{name}\" profili oluşturuldu", + "rename_failed": "Yeniden adlandırma başarısız: {error}", + "save_failed": "Kaydetme başarısız: {error}", + "split_complete": "{count} döngüye bölündü", + "start_failed": "Başlatma başarısız: {error}", + "stop_failed": "Durdurma başarısız: {error}", + "update_failed": "Güncelleme başarısız: {error}", + "store_download_skipped": "{n} kayıt atlandı: çok kısa, çok fazla boşluk içeriyor veya zaten cihazınızda.", + "store_import_low_quality": "Bu kayıt çok kısa, çok fazla boşluk içeriyor veya gerçekçi değil; içe aktarılamaz.", + "store_import_duplicate": "Bu kayıt zaten cihazınızda.", + "device_approved": "Topluluk tarafından onaylandı", + "thanks_confirming": "Onayladığınız için teşekkürler", + "rating_saved": "Kalite değerlendirmesi kaydedildi", + "store_download_partial": "{n} program indirilemedi; daha sonra tekrar deneyin.", + "import_no_restore_point": "İçe aktarıldı, ancak geri yükleme noktası kaydedilemedi, bu nedenle bu içe aktarma geri alınamaz", + "import_undo_failed": "İçe aktarma geri alınamadı: {error}", + "import_undone": "İçe aktarma geri alındı: {profiles} profil ve {cycles} döngü geri yüklendi", + "auto_label_cancelled": "Otomatik etiketleme {count} döngü etiketlendikten sonra iptal edildi.", + "auto_label_cancelled_one": "Otomatik etiketleme {count} döngü etiketlendikten sonra iptal edildi.", + "auto_label_cancelled_other": "Otomatik etiketleme {count} döngü etiketlendikten sonra iptal edildi.", + "auto_label_done": "Otomatik etiketleme tamamlandı: {count} döngü etiketlendi, {skipped} döngü etiketsiz kaldı.", + "auto_label_done_one": "Otomatik etiketleme tamamlandı: {count} döngü etiketlendi, {skipped} döngü etiketsiz kaldı.", + "auto_label_done_other": "Otomatik etiketleme tamamlandı: {count} döngü etiketlendi, {skipped} döngü etiketsiz kaldı." }, "suggestion": { - "both_agree": "WashData öneriyor", - "calibrated_label": "Kalibre edilmiş", "observed_label": "Gözlemlenen", "impact": { "auto_label_confidence": { @@ -1960,10 +1903,6 @@ "higher": "Yalnızca önemli enerji kullanan döngüleri kapatır", "lower": "Daha kısa veya düşük enerjili döngüleri de kapatır" }, - "end_repeat_count": { - "higher": "Bitiş sinyalinin daha fazla tekrarlanmasını gerektirir – daha güvenilir, biraz daha yavaş", - "lower": "Daha az tekrardan sonra biter – daha hızlı algılama, daha yüksek yanlış bitiş riski" - }, "learning_confidence": { "higher": "Yalnızca çok güvenli eşleşmelerden öğrenir – daha yavaş ama daha güvenilir", "lower": "Daha fazla döngüden öğrenir; model daha gürültülü olabilir" @@ -2002,40 +1941,25 @@ } }, "reason": { - "auto_label_confidence": "Kullanıcının hiç düzeltmediği {cycles} otomatik etiketin 15. yüzdelik güven değeri ({p15}).", "completion_min_seconds": "{cycles} çevrim boyunca p05 temiz çevrim süresinin yarısı ({minutes} min); sahte çevrimleri filtreler.{excl}", - "dead_zone_batch": "Kısa tutuldu ({cycles} çevrim boyunca medyan başlangıç kararsızlık penceresi = {s}s), böylece gerçek bir son hemen algılanır.", - "dead_zone_from_dip": "{s}s'de algılanan erken güç düşüşüne dayalı.", - "end_energy_default": "Çevrim sonu gürültü eşiği için önerilen varsayılan değer.", - "end_energy_false": "{cycles} çevrim boyunca p95 sahte-son enerjisi ({p95}Wh); medyan çevrim enerjisinin %0.2'sinde ({floor}Wh) alt sınırlandı.", - "end_energy_no_false": "{cycles} çevrim boyunca sahte son yok; medyan çevrim enerjisinin %0.2'sinde ({floor}Wh) alt sınırlandı.", - "end_repeat_count": "{false}/{total} temiz çevrimde (%{pct}) devam eden 60s'den uzun bir iç duraklama vardı; {val} son doğrulaması gerektir.", - "learning_confidence": "{cycles} kullanıcı etiketli çevrimin p05 güven değeri ({p05}); bunun altında doğrulama iste.", "match_interval": "Gözlemlenen güncelleme sıklığına dayalı (medyan={median}s) * 10.", "max_duration_ratio": "Etiketli çevrim sürelerine dayalı (p95={p95}).", - "min_active": "Son çevrimde gözlemlenen minimum aktif güce ({min}W) dayalı.", - "min_duration_ratio": "Bir programın çevrimin başında tanınabilmesi için olabildiğince düşük tutuldu; güven ve belirsizlik eşikleri erken kesinleşmeyi önler.", "min_off_gap": "Gözlemlenen {gaps} çevrimler arası boşluğa dayalı (p05={p05}s). Cihaz alt sınırı: {floor}s.", "min_off_gap_bridge": "Çevrim içindeki en uzun sessiz aralığı köprülemek üzere boyutlandırıldı: {span}s + 60s tampon, {cycles} temiz çevrim boyunca köprülenen {spans} boşluktan (iki ardışık çalıştırma arasında bıraktığınız en kısa süre olan {ceiling}s'nin altında kalır).", "min_power": "{cycles} temiz çevrim boyunca p05 en düşük aktif gücün ({p05}W) %40'ı; kapalı eşiğini gerçek çekişin altında tutar.{excl}", - "ml_auto_label": "Kalite modelinin hâlâ temiz olarak değerlendirdiği en düşük güven değeri ({cycles} temiz çevrimin p10'u = {p10}).", - "ml_end_repeat": "{false}/{total} çevrimde (%{pct}) çevrim sonu algılayıcısının 50%'nin üzerinde puanladığı bir duraklama vardı; {val} son doğrulaması gerektir.", - "ml_off_delay": "Çevrim sonu algılayıcısının onayladığı duraklamalar: p95 {p95}s + 60s tampon, {cycles} çevrim boyunca modelce doğrulanmış {pauses} duraklamadan (alt sınır {floor}s).", "no_update_timeout": "Gözlemlenen güncelleme sıklığına dayalı (p95={p95}s) * 20 (en az 60s).", "off_delay_cadence": "Gözlemlenen güncelleme sıklığına dayalı (p95={p95}s) * 5", "off_delay_device_floor": "{device} için cihaza özgü güvenli minimum kullanıldı ({floor}s).", "off_delay_generic_floor": "Genel güvenli minimum kullanıldı ({floor}s).", "off_delay_pauses": "Gerçek duraklamalardan uzun sürecek şekilde boyutlandırıldı: p95 çevrim içi duraklama {p95}s + 60s tampon, {cycles} temiz çevrim boyunca {pauses} duraklamadan (alt sınır {floor}s).", - "profile_match_threshold": "{cycles} doğru otomatik etiketin p10 güven değeri ({p10}); güvenli canlı kesinleşme alt sınırı.", - "sampling_interval": "{cycles} temiz çevrim boyunca gözlemlenen medyan güncelleme aralığı ({si}s).{excl}", - "smoothing_window": "{si}s örnekleme ile ~30s'lik okumaları yumuşatacak şekilde boyutlandırıldı ({samples} örnek).", - "start_duration": "Kısa tutuldu (~bir {si}s örnekleme aralığı), böylece tek örneklik ani sıçramalar yok sayılırken algılama olabildiğince erken başlar.", - "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}).", + "standby_floor": "Son döngülerin her biri bittiğinde hâlâ {idle} W çekiyordu; dolayısıyla bunun altındaki bir eşik, cihazın kapandığını hiçbir zaman göremez. Eşik bunun üzerinde tutuldu; kayıttaki {cycles} temiz döngü hiçbir zaman bunun altında {pause} saniyeden uzun süre kalmadı.", + "thr_batch": "{cycles} döngüdeki en düşük aktif gücün (p05, {p05}W) hemen üzerinde; böylece başlangıçlar erken yakalanır ve durdurma eşiği en düşük çalışma gücünün altında kalı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." + "match_interval_floored": "En kısa program ({shortest}s) bunu {cap}s ile sınırlardı; bu {minimum}s alt sınırının altında olduğundan alt sınır kullanılır: {persistence} ardışık eşleşme {budget}s sürer, bu da o programın %{pct} kadarından fazladır. Eşleştirme yalnızca bir okuma geldiğinde çalışır (medyan={median}s), bu yüzden bunun bir maliyeti yok.", + "end_energy_floor": "Kapatma gecikmesi ({off_delay} s) boyunca durdurma eşiğinin ({stop} W) gerektirdiği alt sınıra yükseltildi; bunun altında döngü yalnızca bir yedek yoldan bitebilir.", + "max_duration_ratio_default": "Varsayılana döndürüldü: daha düşük bir üst sınır, bir döngü her zamankinden uzun sürdüğünde doğru programı reddeder.", + "min_duration_ratio_default": "Varsayılana döndürüldü: daha yüksek bir alt sınır, çalışan bir döngünün iyice ilerleyene kadar tanınmasını engeller." }, "exclusions": { "summary": "{total} yanlış algılanan döngü hariç tutuldu: {parts}.", @@ -2053,7 +1977,8 @@ "no_power": "güç yok", "no_active_power": "aktif güç yok" } - } + }, + "now_value": "(şu an {v})" }, "trend": { "down": "Düşen eğilim", @@ -2061,6 +1986,10 @@ }, "maint": { "bearing_service": "Rulman servisi", + "salt": "Tuz ekle", + "rinse_aid": "Parlatıcı ekle", + "lint_filter": "Tüy filtresini temizle", + "condenser_clean": "Kondenseri temizle", "descale": "Kireç çözme", "drum_clean": "Tamburu temizle", "filter_clean": "Filtreyi temizle", @@ -2071,12 +2000,14 @@ "status_tip": "Döngü sonucu: Tamamlandı (doğal bitiş), Kesintili (ani güç düşüşü), Zorla Durduruldu (manuel) veya İnceleme Gerekiyor (geri bildirim bekliyor).", "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 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.", + "energy_tip": "Kullanılan toplam enerji (kWh); zaman içindeki güçten hesaplanır.", + "cost_tip": "Bu döngünün enerji maliyeti; bittiğinde sabitlenir. Zaman Ağırlıklı Maliyet açıkken her an kendi fiyatından ücretlendirilir, aksi halde bitişteki fiyattan. Doldurmak için Ayarlar'da 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.", - "cost_dynamic_tip": "Zaman ağırlıklı: döngü boyunca geçerli olan fiyattan ücretlendirildi (efektif {price} {cur}/kWh)." + "cost_dynamic_tip": "Zaman ağırlıklı: döngü boyunca geçerli olan fiyattan ücretlendirildi (efektif {price} {cur}/kWh).", + "dbg_conf": "Güven", + "dbg_mae": "MAE", + "dbg_corr": "Kor." }, "stat": { "avg": "ort.", @@ -2100,7 +2031,10 @@ "notify_finish": "Bir bitiş bildirimi gönderilirdi.", "notify_milestone": "Bir kilometre taşı bildirimi gönderilirdi.", "notify_held": "Bir bildirim, sessiz saatler nedeniyle bekletildi.", - "finished": "Döngü, son duruma ulaştı ve sona erdi." + "finished": "Döngü, son duruma ulaştı ve sona erdi.", + "match_reverted": "Program algılama durumuna geri döndü: puanı kendi zirvesinin oldukça altına düştü.", + "verified_pause": "Canlı hizalama, program içindeki bir duraklamayı doğruladı (veya kaldırdı). Bu durum sürdükçe döngü sonlandırılmaz.", + "stalled": "Döngü, yapılacak iş varken bu programın orada hiç duraklamadığı kadar uzun süre bekleme gücünde kaldı. Duraklatıldı (Takıldı) olarak gösterilir; döngü açık kalır." }, "store": { "search_ph2": "Marka veya modele göre ara…", @@ -2109,11 +2043,7 @@ "programs_count": "Programlar: {n}", "browse": "Göz at", "device": "Cihaz", - "favorites": "Favoriler", - "search_ph": "Marka veya modele göre ara…", "no_results": "Eşleşen cihaz bulunamadı. Farklı bir arama deneyin.", - "n_programs": "{n} program", - "n_cycles": "{n} döngü", "no_programs": "Bu cihaz için henüz paylaşılan program yok.", "no_cycles": "Bu program için henüz paylaşılan referans döngü yok.", "anon": "anonim", @@ -2125,19 +2055,21 @@ "merge_target": "Profile birleştir", "description": "Açıklama (isteğe bağlı)", "contributed_by": "{name} tarafından", + "website": "Mağaza web sitesi ↗", + "website_tip": "Topluluk mağazasının web sitesini aç", + "favorites": "Favoriler", "rating_summary": "★ {avg} ({n})", "no_ratings": "Henüz değerlendirme yok", - "website": "Mağaza web sitesi ↗", - "website_tip": "Topluluk mağazasının web sitesini aç" + "unreachable": "Topluluk mağazasına ulaşılamadı. Bağlantınızı kontrol edip tekrar deneyin." }, "settings": { "gear": { - "title": "Ayarlar" + "title": "Tercihler" } }, "link": { - "manual": "Kılavuz ↗", - "add_appliance": "Cihazınızı ekleyin" + "maintenance_supporter": "GitHub'da Maintenance Supporter ↗", + "manual": "Kılavuz ↗" }, "tip": { "add_brand": "Marka listede yok mu? Topluluk kataloğuna ekleyin", @@ -2171,7 +2103,13 @@ "rebuild": { "envelopes": "Zarflar yeniden oluşturuluyor" }, - "cancelling": "İptal ediliyor..." + "cancelling": "İptal ediliyor...", + "auto_label": { + "running": "Döngüler otomatik etiketleniyor" + }, + "store_download": { + "running": "Mağazadan indiriliyor" + } }, "setup": { "hdr": { @@ -2180,26 +2118,24 @@ }, "phase0": { "washer": "WashData döngülerinizi zaten algılıyor. Program adlarını ve süre tahminlerini etkinleştirmek için ilk döngünüzü kaydedin.", - "dishwasher": "WashData izliyor. Bulaşık makinelerinin döngüleri karmaşıktır – ilk döngünüzü kaydetmeniz kesinlikle önerilir. Algılanan bir döngü çok uzun sürerse, profil olarak kaydetmeden önce döngü düzenleyicisini kullanarak kırpın.", + "dishwasher": "WashData izliyor. Bulaşık makinesi döngüleri karmaşıktır, bu yüzden ilk döngünüzü kaydedin. Algılanan bir döngü çok uzun sürerse, profil olarak kaydetmeden önce döngü düzenleyicide kırpın.", "generic": "WashData izliyor. Profil oluşturmaya başlamak için algılanan bir döngüyü kaydedin veya etiketleyin." }, "phase1a": { - "labelled": "İyi bir başlangıç – ilk programınız kaydedildi. En temiz veri için bir sonraki döngünüzü kaydedici widget'ı ile kaydetmeyi düşünün." + "labelled": "İyi başlangıç: ilk programınız kaydedildi. En temiz veri için sonraki döngünüzü kaydediciyle kaydedin." }, "phase1b": { - "recorded": "Kaydınız {profile_name} olarak kaydedildi. Kapsama alanı oluşturmak için diğer yaygın programlarınızı kaydedin veya etiketleyin." + "recorded": "Kaydınız {profile_name} olarak kaydedildi. Şimdi sık kullandığınız diğer programları kaydedin veya etiketleyin." }, "phase1c": { - "verify": "Topluluktan {count} programınız var. WashData'nın doğru tanıyıp tanımadığını doğrulamak için bir döngü çalıştırın – cihazınız kendi geçmişini oluşturdukça eşleştirme iyileşecektir." + "verify": "Topluluktan {count} programınız var. WashData'nın onu tanıdığını kontrol etmek için bir döngü çalıştırın; cihazınız kendi geçmişini oluşturdukça eşleştirme iyileşir." }, "phase2": { - "cluster": "WashData, kayıtlı hiçbir programla eşleşmeyen {count} döngü gördü – bunlar birbirlerine benziyor. Bunlar için yeni bir profil oluşturmak ister misiniz?", + "cluster": "WashData, kayıtlı hiçbir programla eşleşmeyen {count} benzer döngü gördü. Bunlar için yeni bir profil oluşturulsun mu?", "unmatched": "Son döngünüz kayıtlı hiçbir programla eşleşmedi. Yeni bir program mı?" }, "phase3": { - "suggestions": "WashData, döngü geçmişinize göre ayar önerileri sunuyor – algılama doğruluğunu artırmak için bunları inceleyin.", - "groups": "Bazı profilleriniz farklı sıcaklıklardaki aynı programa benziyor. Daha iyi eşleştirme için bunları bir grupta düzenleyin.", - "phases": "Daha doğru kalan süre tahminleri için {profile_name} profiline program aşamaları ekleyin." + "suggestions": "WashData'nın döngü geçmişinize dayalı ayar önerileri var. Algılamayı iyileştirmek için inceleyin." }, "phase4": { "healthy": "Bu cihaz tamamen kuruldu ({profile_count} profil)." @@ -2212,12 +2148,58 @@ "create_from_cluster": "Bu döngülerden profil oluştur", "create_profile": "Bunun için bir profil oluştur", "review_suggestions": "Önerileri incele", - "organise_profiles": "Profilleri düzenle", - "configure_phases": "Aşamaları yapılandır", "skip_step": "Bu adımı atla", "skip_forever": "Bir daha gösterme", "hide_guidance": "Rehberi gizle", "show_guidance": "Rehberi göster" } + }, + "access": { + "edit": "Düzenleme", + "full": "Tam", + "inherit": "Devral", + "none": "Yok (gizli)", + "read": "Okuma" + }, + "pg_label_reason": { + "no_winner": "Uyan program yok", + "below_floor": "Öğrenme Güveni'nin altında", + "ambiguous": "Belirsiz olarak işaretlendi", + "margin": "İkinciye çok yakın", + "unknown_profile": "Program artık mevcut değil", + "not_evaluated": "Değerlendirilmedi" + }, + "diagram": { + "accum": "birikim", + "auto": "otomatik", + "below_off": "altı = kapalı", + "confirmed": "onaylandı", + "energy_counts": "enerji sayılır", + "feedback": "geri bildirim", + "gap_below_min": "min altı boşluk = tek döngü", + "heat_phase": "ısıtma aşaması", + "held_at_100": "%100'de tutulur", + "ignored": "yok sayıldı", + "kept": "korundu", + "max": "maks.", + "min": "min.", + "minus_tol": "-tol.", + "no_match": "eşleşme yok", + "no_updates": "güncelleme yok", + "off_delay_wait": "kapatma gecikmesi", + "off_gap": "kapalı aralık", + "plus_tol": "+tol.", + "profile": "profil", + "reading_interval": "tipik okuma aralığı", + "sensor_offline": "sensör çevrimdışı: zorla durdurma", + "si": "ÖA", + "spike_ignored": "sıçrama yok sayıldı", + "spike_ignored_short": "sıçrama: yok sayıldı", + "start": "başlangıç", + "stop": "durma", + "tail_energy_resets": "kuyruk enerjisi eşiğin üstünde: zamanlayıcı sıfırlanır", + "thr": "eşik", + "too_short": "çok kısa", + "tumble_kept": "tambur darbeleri korundu" } } diff --git a/custom_components/ha_washdata/translations/panel/uk.json b/custom_components/ha_washdata/translations/panel/uk.json index 44085783..0c56cd5d 100644 --- a/custom_components/ha_washdata/translations/panel/uk.json +++ b/custom_components/ha_washdata/translations/panel/uk.json @@ -3,6 +3,7 @@ "min": "хв", "entity_not_found": "Сутність не знайдено", "running_long": "Працює довго", + "stalled": "Застряг", "remaining": "залишилось", "progress": "прогрес", "show_time_remaining": "Показувати час, що залишився", @@ -48,16 +49,18 @@ } }, "badge": { - "quiet_tail_tip": "Ближче до кінця ця програма затихає приблизно на {mins} хв, потім споживає близько {watts} Вт протягом {secs} с і завершується. Помічено у {seen}/{measured} виміряних циклів: прилад робить так не за кожного запуску.", + "quiet_tail_tip": "Ближче до кінця ця програма затихає приблизно на {mins} хв, потім споживає близько {watts} Вт протягом {secs} с. Помічено в {seen} з {measured} виміряних циклів.", "quiet_tail": "~{mins} хв тиші в кінці", - "artifact_tip": "Виявлено {n} аномалій (наприклад, двері відкриті посеред циклу) – відкрийте, щоб побачити їх на графіку", + "artifact_tip": "Виявлено {n} аномалії (наприклад, двері відкриті посеред циклу) – відкрийте, щоб побачити їх на графіку", + "artifact_tip_one": "Виявлено {n} аномалію (наприклад, двері відкриті посеред циклу) – відкрийте, щоб побачити її на графіку", + "artifact_tip_few": "Виявлено {n} аномалії (наприклад, двері відкриті посеред циклу) – відкрийте, щоб побачити їх на графіку", + "artifact_tip_many": "Виявлено {n} аномалій (наприклад, двері відкриті посеред циклу) – відкрийте, щоб побачити їх на графіку", + "artifact_tip_other": "Виявлено {n} аномалії (наприклад, двері відкриті посеред циклу) – відкрийте, щоб побачити їх на графіку", "auto": "(виявлено автоматично)", "built_in_tag": "вбудований", "declining": "↘ знижується", "energy_low": "Енергії менше, ніж зазвичай", "energy_spike": "Енергії більше, ніж зазвичай", - "fair_fit": "прийнятна якість", - "fair_fit_tip": "Помірна постійність збігів – деякі цикли, призначені цьому профілю, мають нижчі оцінки надійності. Позначте більше циклів або перезапишіть профіль, щоб підвищити точність.", "feedback_requested": "Запит на відгук", "golden_cycle": "Записаний контрольний цикл", "duration_outlier": "Цикли незвичної тривалості: {n}", @@ -66,27 +69,40 @@ "needs_review": "Потребує перегляду", "overrun": "Тривав довше, ніж зазвичай", "poor_fit": "погана якість", - "poor_fit_tip": "Непослідовна історія збігів – розгляньте можливість відновлення цього профілю", - "restart_gap_tip": "{n} пропуск при перезапуску HA: у графіку потужності є розрив", + "restart_gap_tip": "{n} пропуску при перезапуску HA: у графіку потужності є розриви", + "restart_gap_tip_one": "{n} пропуск при перезапуску HA: у графіку потужності є розрив", + "restart_gap_tip_few": "{n} пропуски при перезапуску HA: у графіку потужності є розриви", + "restart_gap_tip_many": "{n} пропусків при перезапуску HA: у графіку потужності є розриви", + "restart_gap_tip_other": "{n} пропуску при перезапуску HA: у графіку потужності є розриви", "reviewed": "Переглянуто", "steady": "→ стабільно", "underrun": "Завершився швидше, ніж зазвичай", - "using_defaults": "Використовуються стандартні налаштування", - "using_tuned": "Використовуються налаштовані ваги", "overrun_ratio": "({x}× від очікуваного)", "underrun_ratio": "({pct}% від очікуваного)", - "awaiting_tip": "Очікує схвалення спільнотою", - "awaiting_n": "Очікує схвалення · {n} підтверджено", - "approved": "Схвалено", - "awaiting": "Очікує схвалення", "imported_tip": "Імпортовано з магазину спільноти. Використовується лише для зіставлення, не враховується у статистиці.", "unmatchable": "не зіставляється", "not_importable": "недоступно", "exists": "існує", "backfilled_tip": "Виявлено в імпортованій історії потужності. Впливає лише на зіставлення програм, не враховується у статистиці.", - "armed": "(застосується до наступного циклу)" + "armed": "(застосується до наступного циклу)", + "awaiting_tip": "Очікує схвалення спільнотою", + "awaiting_n": "Очікує схвалення · {n} підтверджено", + "awaiting": "Очікує схвалення", + "approved": "Схвалено", + "matcher_unused": "Ще не зіставлено з жодним вашим циклом", + "matcher_used": "Зіставлено з {n} вашими циклами", + "matcher_used_one": "Зіставлено з {n} вашим циклом", + "matcher_used_few": "Зіставлено з {n} вашими циклами", + "matcher_used_many": "Зіставлено з {n} вашими циклами", + "matcher_used_other": "Зіставлено з {n} вашими циклами", + "matcher_used_tip": "Цикли на цьому приладі, які WashData сам зіставив із цією імпортованою програмою. Імпорт, який жодного разу не зіставився, може не пасувати вашому приладу; його можна видалити. Рахується локально; до магазину нічого не надсилається." }, "btn": { + "pg_preset_delete": "Видалити пресет", + "pg_preset_load": "Завантажити пресет", + "pg_preset_load_tip": "Замінити значення в пісочниці цим пресетом (активні налаштування не змінюються)", + "pg_preset_save": "Зберегти як пресет", + "pg_preset_save_tip": "Зберегти всі значення нижче як іменований пресет для цього пристрою", "reset_zoom": "Скинути масштаб", "set_brand_model": "Вказати бренд і модель", "refresh_catalog": "Оновити каталог", @@ -97,12 +113,11 @@ "add_timer": "+ Додати таймер", "apply_all": "Застосувати всі", "apply_label": "Застосувати мітку", - "apply_set_b": "Застосувати набір B", "apply_split": "Застосувати поділ", "apply_trim": "Застосувати обрізання", "auto_detect_split": "Автовизначення", "auto_label_cycles": "Автоматично позначити цикли", - "auto_label_cycles_tip": "Автоматично присвоїти назви профілів непозначеним циклам, чия впевненість збігу перевищує поріг", + "auto_label_cycles_tip": "Назвати непозначені цикли, впевненість зіставлення яких перевищує поріг.", "cancel": "Скасувати", "clear_debug": "Очистити дані налагодження", "clear_splits": "Очистити", @@ -112,8 +127,6 @@ "convert_to_automation": "Перетворити на автоматизацію", "correct": "Виправити…", "create": "Створити", - "create_from_cluster": "Створити профіль із {count} циклів", - "create_profile": "+ Створити профіль", "delete": "Видалити", "delete_group": "Видалити групу", "delete_group_tip": "Видалити лише цю групу – профілі учасників збережуться", @@ -122,39 +135,30 @@ "discard_tip": "Відхилити запис без збереження", "dismiss": "Закрити", "mute_suggestion": "Не пропонувати це налаштування", - "pg_load_calibrated": "Завантажити відкалібровані (ML) ({n})", - "pg_load_calibrated_tip": "Завантажити налаштування, відкалібровані ML, як перевизначення в пісочниці", "pg_load_live": "Завантажити активні налаштування", "pg_load_live_tip": "Перечитати поточні налаштування інтеграції та скасувати всі зміни в пісочниці", "pg_load_suggested": "Завантажити рекомендовані ({n})", "pg_load_suggested_tip": "Завантажити поточні рекомендації автоматичного налаштування як перевизначення в пісочниці", - "pg_preset_delete": "Видалити пресет", - "pg_preset_load": "Завантажити пресет", - "pg_preset_load_tip": "Замінити значення в пісочниці цим пресетом (активні налаштування не змінюються)", - "pg_preset_save": "Зберегти як пресет", - "pg_preset_save_tip": "Зберегти всі значення нижче як іменований пресет для цього пристрою", "pg_publish_all": "Зберегти в налаштування ({n})", "pg_publish_one": "Зберегти «{label}» в налаштування інтеграції", "reset_muted": "Скинути заглушені", + "reset_phase_tip": "Скасувати ваші зміни та відновити вбудовану фазу", "done": "Готово", "edit": "Редагувати", "export": "Експортувати", - "export_json": "Експортувати до JSON", "force_stop": "Примусово зупинити", "force_stop_tip": "Негайно завершити поточний цикл і позначити його як примусово зупинений", "from_template": "З шаблону ▾", "ignore": "Ігнорувати", "import_json": "Імпортувати з JSON", - "import_overwrite": "Імпортувати (перезаписує дані)", "inspect": "Перевірити", "label": "Позначити", - "load": "Завантажити", "load_more": "Завантажити ще", "manage": "Керувати", "merge": "Об'єднати", "new_automation": "+ Нова автоматизація", "new_group": "+ Нова група", - "new_group_tip": "Згрупувати майже ідентичні профілі (однаковий тип/тривалість, різна температура або віджимання), щоб система зіставлення надійно вибирала між ними", + "new_group_tip": "Згрупувати майже однакові профілі (та сама форма, інша температура або віджим), щоб зіставлення могло їх розрізняти.", "new_phase": "+ Нова фаза", "new_profile": "+ Новий профіль", "new_profile_tip": "Створити новий профіль програми з наявного позначеного циклу або запису", @@ -162,23 +166,19 @@ "on_cycle_started": "Коли цикл розпочато", "pause_cycle": "Пауза", "pause_cycle_tip": "Призупинити поточний цикл – прилад продовжить роботу з того місця, де зупинився", - "pg_apply_device": "Застосувати до пристрою", - "pg_autofill": "Автозаповнення", - "play": "Відтворити", "process": "Обробити", "process_history": "Обробити зараз", "process_recording": "Зберегти запис", "process_tip": "Зберегти запис як новий або наявний профіль", "rebuild": "Відновити конверти", "rebuild_envelope": "Відновити конверт", - "rebuild_tip": "Перерахувати очікуваний конверт потужності (смуга мін./макс.) для всіх профілів з позначених циклів – запускати після позначення нових або виправлення старих циклів", + "rebuild_tip": "Перебудувати очікувану смугу потужності кожного профілю з його позначених циклів. Запускайте після позначення або виправлення циклів.", "rec_start_tip": "Розпочати запис потужності приладу – запустіть безпосередньо перед початком циклу", "rec_stop": "Зупинити", "rec_stop_tip": "Зупинити запис і зберегти захоплені дані для перегляду", "record": "Почати запис", "refresh": "Оновити", "refresh_diag_tip": "Перезавантажити статистику сховища", - "refresh_logs_tip": "Перезавантажити останні записи журналу", "refresh_settings_tip": "Перезавантажити налаштування із сервера", "relabel": "Перепозначити ({count})", "remove": "Видалити", @@ -186,7 +186,6 @@ "reset": "Скинути", "reset_ml_models_tip": "Відмовтеся від налаштованих моделей і поверніться до вбудованих. WashData може повторно навчити їх пізніше.", "reset_to_builtin": "Скинути до вбудованих моделей", - "reset_to_defaults": "Скинути до стандартних", "resume_cycle": "Продовжити цикл", "resume_cycle_tip": "Відновити призупинений цикл", "revert_settings": "Скасувати зміни", @@ -199,8 +198,6 @@ "run_analysis": "🔍 Запустити аналіз пропозицій", "run_analysis_tip": "Проаналізуйте записані цикли зараз і оновіть пропозиції щодо налаштування", "run_auto_label": "Запустити автопозначення", - "run_both": "Запустити A проти B", - "run_simulation": "Запустити симуляцію", "save": "Зберегти", "save_access_control": "Зберегти контроль доступу", "save_group": "Зберегти групу", @@ -212,10 +209,8 @@ "save_settings": "Зберегти налаштування", "select": "Вибрати", "select_all": "Вибрати всі", - "select_last20": "Останні 20", "select_none": "Очистити", "show_only": "Показати лише", - "skip_setup": "Пропустити налаштування", "split": "Розділити", "stop": "Зупинити", "submit_correction": "Надіслати виправлення", @@ -224,30 +219,24 @@ "trim_split": "Обрізати / Розділити", "undo": "Скасувати", "unlabel_selected": "Зняти мітку з вибраних ({n})", - "unlabel_selected_tip": "Зняти мітку профілю з вибраних циклів. Вони залишаться у вашій історії, і їх можна буде позначити знову пізніше - вони просто перестануть формувати цей профіль.", + "unlabel_selected_tip": "Зняти мітку з вибраних циклів. Вони залишаться в історії, але перестануть формувати цей профіль.", "use": "Використати", "wipe_all": "Видалити всі дані", "delete_selected": "Видалити вибрані ({n})", - "group_suggest": "Група {n}: {members}", "pg_apply_best": "Застосувати найкраще", - "pg_apply_to_settings": "Зберегти в налаштування", "pg_apply_to_settings_tip": "Скопіювати значення, змінені тут, до активних налаштувань цього пристрою", "import": "Імпортувати", "disconnect": "Від'єднати", "connect_github": "Підключити GitHub", - "save_appliance": "Зберегти пристрій", "share_to_store": "Поділитися в магазині", "share": "Поділитися", - "confirm_appliance": "Підтвердити цей пристрій", - "confirmed": "Підтверджено", "browse_community_setups": "Огляд конфігурацій спільноти", "download_device": "Завантажити конфігурацію пристрою", "share_device": "Поділитися конфігурацією пристрою", - "share_device_tip": "Поділитися програмами та налаштуваннями цього пристрою зі спільнотою", + "share_device_tip": "Поділитися цим приладом і його еталонними циклами в магазині спільноти, щоб власники такої ж машини могли їх використати.", "share_n": "Поділитися циклами: {n}", "export_selected": "Експорт (вибрати дані)", "export_all": "Швидкий експорт усього", - "import_raw": "Розширено: замінити все з JSON", "download_export": "Завантажити експорт", "analyze_import": "Проаналізувати файл", "import_selected": "Імпортувати вибране", @@ -260,7 +249,15 @@ "save_total_cycles": "Зберегти загальну кількість", "clear": "Очистити", "add_brand": "Додати бренд", - "add_appliance": "Додати пристрій" + "log_done_task": "Позначити виконаним: {task}", + "add_task": "Додати завдання", + "add_appliance": "Додати пристрій", + "retry": "Повторити", + "confirm_appliance": "Підтвердити цей пристрій", + "confirmed": "Підтверджено", + "create_profile_from_gap": "Створити профіль", + "undo_import": "Скасувати останній імпорт", + "pg_rerun": "Оновити симуляцію" }, "note": { "off_delay": { @@ -268,10 +265,6 @@ } }, "conflict": { - "anti_wrinkle_exit": { - "exit": "Має бути нижчим за Поріг Зупинки ({stop} Вт); інакше потужність виходу з режиму захисту від зминання ігнорується", - "stop": "Має бути вищим за Поріг Потужності Виходу із Захисту від Зминання ({exit} Вт)" - }, "anti_wrinkle_max": { "max": "Має бути вищим за Поріг Початку ({start} Вт); інакше обмеження тривалості захисту від зминання обходиться", "start": "Має бути нижчим за Макс. Потужність Захисту від Зминання ({max} Вт)" @@ -279,10 +272,10 @@ "attn_sub": "Виправте конфлікти перед збереженням", "attn_title": "Конфлікти налаштувань: {n}", "price_entity": { - "power_sensor": "Це Датчик потужності пристрою, а не ціна. Сутність ціни має пріоритет над фіксованою ціною нижче, тож цикли рахувалися б за ватами за кВт·год - очистіть це поле.", - "energy_sensor": "Це Лічильник енергії пристрою, а не ціна. Сутність ціни має пріоритет над фіксованою ціною нижче, тож цикли рахувалися б за показанням лічильника за кВт·год - очистіть це поле.", - "device_class": "Клас цього сенсора - {dc}, а не ціна за кВт·год. Сутність ціни має пріоритет над фіксованою ціною нижче - очистіть це поле або вкажіть сенсор тарифу.", - "unit": "Цей сенсор видає значення у {unit}, а це не ціна за кВт·год. Сутність ціни має пріоритет над фіксованою ціною нижче - очистіть це поле або вкажіть сенсор тарифу." + "power_sensor": "Це Датчик потужності пристрою, а не ціна. Цикли рахувалися б у ватах за кВт·год; очистіть це поле.", + "energy_sensor": "Це Лічильник енергії пристрою, а не ціна. Цикли рахувалися б за показником лічильника за кВт·год; очистіть це поле.", + "device_class": "Цей датчик вимірює {dc}, а не ціну за кВт·год. Очистіть це поле або виберіть датчик тарифу.", + "unit": "Цей датчик видає значення в {unit}, а не ціну за кВт·год. Очистіть це поле або виберіть датчик тарифу." }, "settings_banner": "Конфлікти налаштувань: {n}. Перевірте виділені розділи та виправте їх перед збереженням.", "settings_banner_btn": "До першого", @@ -317,8 +310,8 @@ "threshold": "Має бути нижчим за Поріг Зупинки ({stop} Вт), щоб мати ефект" }, "pump_stuck": { - "duration": "Має бути меншим за Тайм-аут Без Оновлення ({to} с), щоб сигнал застрявшого насоса спрацьовував раніше за сторожовий пес", - "timeout": "Має перевищувати Тривалість Застрявання Насоса ({ps} с), щоб сигнал застрявшого насоса спрацьовував до примусової зупинки циклу" + "duration": "Має бути меншим за Тайм-аут без оновлення ({to} с), щоб сигнал застряглого насоса спрацював першим", + "timeout": "Має перевищувати Тривалість застрявання насоса ({ps} с), щоб сигнал застряглого насоса спрацював першим" }, "start_dur": { "sampling": "Інтервал Вибірки перевищує Тривалість Початку ({sdt} с); одиночні сплески можуть відкрити цикл", @@ -334,10 +327,15 @@ "watchdog": { "interval": "Має бути щонайменше 2x Інтервалу Вибірки ({si} с)", "sampling": "Інтервал Вибірки має бути не більше половини Інтервалу Сторожового Пса ({wi} с)" + }, + "end_energy": { + "energy": "Надто суворо для Порогу зупинки ({w} Вт) протягом Затримки вимкнення ({d} с); цикл може завершитися лише резервним шляхом", + "stop": "Енергія завершення ({e} Вт·год за {d} с) допускає лише {p} Вт" } }, "hdr": { - "ab_comparison": "Порівняння налаштувань A/B", + "custom_tasks": "Власні завдання", + "maintenance_supporter": "Потрібно більше?", "access_control": "Контроль доступу", "add_maintenance": "Додати запис обслуговування", "automation_delete": "Видалити цю автоматизацію", @@ -346,28 +344,22 @@ "automation_review_this_cycle": "Переглянути цей цикл", "automations": "Автоматизації", "clear_debug": "Очистити трасування налагодження", - "cycle_simulator": "Симулятор циклів", "diagnostics_pref": "Діагностика", "display": "Відображення", - "dtw_inspector": "Інспектор DTW", "export_import": "Експорт / Імпорт", - "getting_started": "Початок роботи", "live_power": "Потужність в реальному часі", "logs": "Журнали", - "logs_diagnostics": "Діагностика", "maintenance": "Дії з обслуговування", "maintenance_log": "Журнал обслуговування", "maintenance_reminders": "Нагадування про обслуговування", "manual_recording": "Ручний запис", - "manual_recording_tip": "Запустіть цикл навмисно, поки WashData записує потужність. Натисніть «Запис» перед запуском приладу, «Стоп» після завершення, потім «Обробити», щоб зберегти як іменований профіль.", + "manual_recording_tip": "Запишіть цикл навмисно: натисніть «Почати запис» перед запуском приладу і «Зупинити» після завершення, потім «Обробити», щоб зберегти його як профіль.", "ml_learned": "Що WashData дізнався", - "ml_matching_tuning": "Точне налаштування відповідності програм", "ml_settings_card": "Налаштування", "ml_smart_learning": "Розумне навчання", "my_preferences": "Мої налаштування", "no_automations": "Жодна автоматизація ще не посилається на цей пристрій.", "panel_settings": "Налаштування панелі", - "param_sweep": "Перебір параметра", "pg_settings_source": "Джерело налаштувань", "phase_catalog": "Каталог фаз", "playground": "Пісочниця", @@ -376,11 +368,8 @@ "status": "Стан", "status_graph": "Графік стану", "storage_stats": "Статистика сховища", - "test_history": "Перевірка на історії", "toggle_sidebar": "Перемкнути бічну панель Домашнього помічника", - "tools_and_data": "Інструменти та дані", "wipe_history": "Очистити історію", - "recommendations": "Рекомендації ({n})", "pg_detection_params": "Налаштування виявлення", "pg_outcome": "Результат симуляції", "pg_across_cycles": "За всіма вашими циклами", @@ -388,7 +377,8 @@ "online_account": "Магазин спільноти та онлайн-функції", "import_power_history": "Імпорт історії потужності", "service_status": "Стан обслуговування", - "online_account_tab": "Онлайн та спільнота" + "online_account_tab": "Онлайн та спільнота", + "live_match_debug": "Налагодження збігу в реальному часі" }, "health": { "fair": "Прийнятна якість профілю", @@ -396,11 +386,16 @@ "poor": "⚠ Погана якість профілю" }, "lbl": { - "envelope_position_tip": "Позиція на власній записаній кривій зіставленої програми, {pct}%. Визначається суміщенням цього циклу з профілем, а не підрахунком часу, тому залишається правильною, коли запуск довший або коротший за звичайний. Оновлюється, доки прилад мовчить, тому під час активної фази може відставати.", + "envelope_position_tip": "Позиція на записаній кривій зіставленої програми: {pct}%. Визначається суміщенням циклу з профілем, тому лишається точною при довшому чи коротшому прогоні. Оновлюється, коли прилад затихає, тому може відставати.", "envelope_position": "крива {pct}%", "aria_playground_chart3": "Інтерактивний графік потужності циклу: наведіть, щоб побачити час/потужність, прокрутіть або використайте два пальці для масштабування, перетягніть для панорамування", "device_unresolved": "Недоступний пристрій ({id})", "drag_to_resize": "Перетягніть, щоб змінити розмір", + "task_name": "Завдання", + "every_cycles": "Інтервал (цикли)", + "every_days": "Інтервал (дні)", + "new_task_name": "Нове завдання", + "days_since_service": "{since} / {total} днів", "actions": "Дії", "activity": "Активність", "administrators": "Адміністратори", @@ -408,10 +403,8 @@ "aria_cycle_chart": "Крива потужності циклу", "aria_envelope_chart": "Графік огинаючої потужності профілю", "aria_phase_chart": "Графік редактора фаз", - "aria_playground_chart": "Крива потужності циклу з перетягуваними порогами виявлення", "aria_power_chart": "Графік споживання потужності", "aria_spaghetti_chart": "Накладені криві потужності циклів", - "aria_sweep_chart": "Графік результатів перебору параметрів", "artifact_anomaly": "Аномалія", "artifact_high_power": "Висока потужність", "artifact_interruption": "Переривання", @@ -425,87 +418,63 @@ "cohesion_good": "згуртованість {pct}%", "cohesion_low": "⚠ низька згуртованість {pct}%", "compare_profiles": "Порівняти з профілями", - "concurrency": "Паралельність", "confidence": "Впевненість", "confidence_threshold": "Поріг впевненості", "correct_duration": "Правильна тривалість (хв, необов'язково)", "correct_profile": "Правильний профіль", - "correlation": "Кореляція", "cost": "Вартість", "create_new_profile": "+ Створити новий профіль…", "current_phase": "Поточна фаза", - "curves": "Криві", "cycle": "Цикл", "cycle_date_display": "Відображення дати циклу", - "cycle_health": "Стан циклу", - "cycle_index": "індекс циклу →", - "cycles": "Цикли", "cycles_count": "Цикли", "cycles_lc": "циклів", - "dashed": "пунктир", "date": "Дата", "debug_traces": "Трасування налагодження", - "default": "Стандартний", "default_access_level": "Стандартний рівень для користувачів, яких немає в списку нижче", "default_other": "Стандартний (інші пристрої)", "default_tab": "Стандартна вкладка при відкритті панелі", "font_size": "Розмір шрифту панелі", - "delta": "Δ", "description": "Опис", - "dtw_blend_w": "Вага змішування", - "dtw_blended": "Змішаний бал", - "dtw_ddtw": "Бал DDTW", - "dtw_l1": "Бал L1", - "dtw_matched_profile": "(зіставлений профіль циклу)", "duration": "Тривалість", - "duration_agreement": "Відповідність тривалості", - "emphasis": "Акцент", "enable_access_control": "Увімкнути контроль доступу для кожного користувача", "energy": "Енергія", - "energy_agreement": "Відповідність енергії", - "event_timeline": "Хронологія подій", "event_type": "Тип події", "expected": "Очікуваний", + "suspected_profile": "{name} (імовірно)", + "runner_up_profile": "{name} (другий варіант)", "expected_duration": "Очікувана тривалість (хв)", "file_kb": "Файл (кБ)", - "final_score": "Підсумковий бал", "from": "Від", "gap_s": "Розрив (с)", "group_name": "Назва групи", "head_trim": "Обрізання початку (с)", - "health": "Стан", "hide_tabs": "Приховати вкладки для не-адміністраторів", - "in_use": "Використовується", "json_data": "Дані JSON", "label": "Відповідна програма", "last": "Останні", "last_checked": "Остання перевірка:", "load_from_file": "Завантажити з файлу", - "mae_score": "Бал MAE", "manual_duration": "Ручна тривалість (хв, необов'язково)", "match": "Збіг", "members": "Учасники", - "metric": "Метрика", "mode_existing_profile": "Додати до наявного профілю", "mode_new_profile": "Створити новий профіль", "models_fine_tuned": "(дотреновані моделі: {count})", - "n_classic_suggestions": "{n} класичне", - "n_ml_suggestions": "{n} ML", "n_selected": "Вибрано {n}", - "n_tuning_suggestions": "{n} пропозиція налаштування", - "na": "н/д", + "n_tuning_suggestions": "{n} пропозиції налаштування", + "n_tuning_suggestions_one": "{n} пропозиція налаштування", + "n_tuning_suggestions_few": "{n} пропозиції налаштування", + "n_tuning_suggestions_many": "{n} пропозицій налаштування", + "n_tuning_suggestions_other": "{n} пропозиції налаштування", "new_profile_name": "Нова назва профілю", "notes": "Нотатки", - "of": "з", - "outcome": "Результат", "panel_default_tab": "Стандартна вкладка", "panel_language": "Мова панелі", "per_cycle_short": "цикл", - "pg_autofill_tip": "Автозаповнити діапазон із поточного значення ({lo}–{hi})", "pg_best_value": "Знайдено найкраще значення", "pg_current_value": "Поточне значення:", "pg_drag_hint": "Перетягніть лінію на графіку", - "pg_dtw": "DTW", "pg_ev_ending": "Завершення", "pg_ev_running": "Виконується", "pg_group_advanced": "Особливі випадки", @@ -513,63 +482,39 @@ "pg_group_timing": "Правила часу", "pg_idle": "Бездіяльність", "pg_anti_wrinkle": "Захист від зминання", - "pg_last_run": "Останній запуск: {date}", - "pg_match_score": "Оцінка збігу", - "pg_pct_of": "{p}% з {total}", "pg_poor_match": "Поганий збіг", "pg_preset": "Пресет пісочниці", "pg_preset_name": "Назва нового пресета", "pg_preset_none": "Виберіть пресет…", "pg_strong_match": "Сильний збіг", - "pg_tile_ambiguous": "Неоднозначно", - "pg_tile_ambiguous_tip": "Цикли з майже однаковим збігом двох програм", - "pg_tile_detected": "Виявлено", - "pg_tile_detected_tip": "Цикли, у яких потужність перевищувала поріг запуску достатньо довго, щоб зарахуватися як робота", - "pg_tile_matched": "Зіставлено", - "pg_tile_matched_tip": "Цикли, правильно віднесені до відомої програми", - "pg_time_left": "Залишилось часу", "pg_weak_match": "Слабкий збіг", "phase_name": "Назва фази", "name": "Назва", "phase_timeline": "Хронологія фаз", "power": "Потужність", "profile": "Профіль", - "profile_index": "індекс профілю →", "profile_name": "Назва профілю", "program": "Програма", - "program_tip": "Замініть профіль, що зіставляється з поточним циклом. Автоматичне виявлення дозволяє інтеграції автоматично вибрати найкраще співпадіння. Закріпіть конкретну програму, щоб примусово зіставити її, коли автоматичне виявлення неправильне або ви знаєте, що запущено. Виберіть програму до запуску приладу, і вона буде застосована, щойно почнеться наступний цикл.", + "program_tip": "Виберіть, із якою програмою зіставляється поточний цикл. Автовизначення вибирає найкращий збіг. Закріпіть програму, коли автовизначення помиляється; програма, вибрана до запуску, застосується до наступного циклу.", "progress": "Прогрес", "raw_socket": "Необроблений сокет", - "read_only": "Лише читання", "ref_cycle": "Контрольний цикл (необов'язково)", - "refresh_interval": "Інтервал оновлення (с)", "remaining": "Залишилось", "reminder_every": "Нагадувати кожні (циклів)", "remove_label": "- Видалити мітку -", "rename_profile": "Перейменувати профіль", - "replay_duration": "Відтворення", "resulting_profile": "Отриманий профіль", "save_mode": "Режим збереження", - "score": "Бал", - "score_breakdown": "Розбивка балів", "seconds_from_start": "Секунди від початку", "select_profile": "Вибрати профіль", "setting": "Налаштування", - "settings_a": "Налаштування A (поточні)", - "settings_a_short": "A", "settings_advanced": "Розширені", - "settings_b": "Налаштування B (тест)", - "settings_b_short": "B", "settings_basic": "Основні", "settings_detail_level": "Рівень деталізації налаштувань", - "settings_overrides": "Перевизначення налаштувань", "show_debug": "Показати картку налагодження збігів на сторінці стану (впевненість, неоднозначність, найкращі кандидати)", "show_expected": "Показати накладення очікуваної кривої (відповідний профіль, помаранчевий)", "show_raw": "Показати перемикач необроблених даних розетки на графіку потужності в реальному часі", "sparkline": "Тренд тривалості останніх циклів", - "stage2": "Етап 2 – основна подібність", - "stage3": "Етап 3 – DTW", - "stage4": "Етап 4 – відповідність", "status": "Стан", "tail_trim": "Обрізання кінця (с)", "tap_to_dismiss": "торкніться графіка, щоб закрити", @@ -580,7 +525,6 @@ "ungrouped": "Без групи", "unlabelled": "Без мітки", "unlabelled_paren": "(без мітки)", - "warp_path": "Шлях викривлення", "input": "Вхід:", "start": "Початок", "end": "Кінець", @@ -605,9 +549,8 @@ "n_shown": ", показано {n}", "cycles_title": "Цикли ({n})", "pg_detecting": "Виявлення", - "pg_best_marker": "Найкраще: {v}", "pg_time_left_model": "Залишилось часу (модель)", - "pg_time_left_model_tip": "Оцінка залишку часу моделлю (оцінювач фаз + ML), а не статичний відлік", + "pg_time_left_model_tip": "Оцінка за фазами програми, а не фіксований зворотний відлік", "phase": "Фаза", "pg_mode_history": "Перевірка на історії", "pg_ev_detected": "Виявлено", @@ -647,18 +590,8 @@ "pg_mode_optimize": "Оптимізувати", "pg_group_matching": "Зіставлення програм", "pg_zoom_hint": "подвійний клік скидає масштаб", - "aria_playground_chart2": "Інтерактивний графік потужності циклу: наведіть, щоб побачити час/потужність, прокрутіть для масштабування, перетягніть для панорамування", "pg_alert_timeout_end": "Завершено за тайм-аутом, а не за прогнозом", "pg_alert_indefinite": "Працював би нескінченно", - "pg_alert_stress_cap": "Досягнуто захисний ліміт", - "pg_alert_stress_ok": "Виявлено режим очікування: цикл зупинено", - "pg_alert_stress_warn": "Споживання в режимі очікування перевищує поріг зупинки", - "pg_stress_group": "Тест виявлення режиму очікування", - "pg_stress_idle_auto": "Auto", - "pg_stress_idle_w": "Рівень у режимі очікування (Вт)", - "pg_stress_idle_w_desc": "Замінити автоматично визначений нижній поріг споживання в режимі очікування (залиште порожнім для автоматичного визначення).", - "pg_stress_toggle": "Тестувати виявлення режиму очікування", - "pg_stress_toggle_desc": "Імітує роботу пристрою в режимі очікування після завершення запису -- показує, чи і коли WashData зупинить цикл.", "task_pg_history": "Тест на історії", "task_pg_sweep": "Оптимізація", "task_reprocess": "Повторна обробка", @@ -668,16 +601,9 @@ "pg_recent_runs": "Останні запуски", "pg_load_run": "Перезавантажити цей запуск", "pg_match_confidence": "Достовірність збігу", - "pg_envelope_fit": "Відповідність обвідній", - "pg_envelope_fit_tip": "Наскільки точно цикл вписується в збережену обвідну потужності цього профілю, інший погляд, ніж достовірність збігу.", "n_imported_note": ", {n} імпортовано", "enable_online": "Увімкнути онлайн-функції", - "store_brand": "Бренд пристрою", - "store_model": "Модель пристрою", "anonymous": "Анонімно", - "rate_quality": "Оцінити якість", - "rate_n": "{n} зірок", - "approved_only": "Лише схвалені", "adopt_settings": "Прийняти налаштування", "flags": "Мітки", "include_phase_map": "Включити карту фаз", @@ -694,7 +620,6 @@ "cat_custom_phases": "Користувацькі фази", "cat_profile_groups": "Групи профілів", "cat_settings": "Налаштування виявлення та розпізнавання", - "cat_matching_config": "Тонке налаштування розпізнавання", "cat_ml_models": "ML-моделі", "cat_feedback": "Відгуки та мітки перевірки", "cat_suggestions": "Пропозиції", @@ -714,7 +639,6 @@ "conflict_overwrite": "Перезаписати", "hist_csv_data": "Дані CSV", "hist_from_recorder": "Або зчитати з Home Assistant", - "days": "дн.", "hist_keep": "Зберегти цей цикл", "hist_looks_complete": "завершений", "peak_power_short": "Пік", @@ -735,7 +659,44 @@ "zoom_hint_touch_zoomed": "масштаб двома пальцями · подвійний дотик для скидання", "cycles_since_service": "{since} / {total} циклів", "total_cycles_run": "циклів усього", - "correct_total_cycles": "Виправити загальну кількість" + "correct_total_cycles": "Виправити загальну кількість", + "hist_reason_already_recorded": "перекривається з циклом, який WashData вже записав", + "share_consent": "Підтверджую, що ці цикли пройшли до нормального завершення без переривань", + "show_internal": "Показати внутрішні налаштування", + "show_internal_tip": "Налаштування, які WashData підбирає за вас (вибірка, сторожовий пес, частота зіставлення, пороги). Показуються, коли цей пристрій має власне значення, конфлікт або пропозицію.", + "rate_n": "{n} зірок", + "rate_quality": "Оцінити якість", + "pg_autolabel": "Автоматичне маркування", + "pg_label_review": "Потребує перегляду", + "pg_ev_match_reverted": "Збіг скасовано", + "pg_ev_verified_pause": "Підтверджена пауза", + "n_samples": "{n} показань", + "n_samples_one": "{n} показання", + "n_samples_few": "{n} показання", + "n_samples_many": "{n} показань", + "n_samples_other": "{n} показання", + "n_cycles": "{n} циклів", + "n_cycles_one": "{n} цикл", + "n_cycles_few": "{n} цикли", + "n_cycles_many": "{n} циклів", + "n_cycles_other": "{n} циклу", + "energy_per_cycle": "{v}/цикл", + "energy_total": "усього {v}", + "automation_off": "(вимк.)", + "ref_cycle_none": "Немає", + "share_name": "Показувати моє ім’я в публікаціях", + "cat_backfill_cycles": "Цикли, знайдені в імпортованій історії потужності", + "pg_compare_profile": "Порівняти з профілем", + "pg_obj_early": "Частота ранніх завершень", + "pg_obj_endlag": "Затримка завершення (медіана)", + "pg_obj_split": "Частота розбиття циклів", + "pg_sweep_cycles": "Останні цикли", + "cycle_context": "Історія з recorder навколо циклу", + "cycle_context_off": "Вимкнено", + "cycle_context_minutes": "{min} хв", + "cycle_context_series": "Історія з recorder", + "before_start": "До початку", + "after_end": "Після завершення" }, "log": { "all_levels": "Усі рівні", @@ -745,11 +706,9 @@ }, "ml": { "builtin_models": "● Використовуються вбудовані моделі", - "fit_fair": "Прийнятно", "fit_good": "Добре", "fit_slight": "Незначно", "fit_strong": "Сильно", - "fit_weak": "Слабко", "fit_word": "відповідність", "personalized": "● Персоналізовано для цього пристрою", "better_than_baseline": "{pct}% краще за базову оцінку ({metric})", @@ -757,21 +716,33 @@ "trend_declining_tip": "Відповідність цієї моделі погіршилася за останні повторні перевірки; перегляд більшої кількості циклів може допомогти їй перенавчитися.", "trend_steady_tip": "Відповідність цієї моделі залишалася приблизно стабільною за останні повторні перевірки.", "cap_blurb": { - "end": "Визначення моменту, коли цикл справді завершився", - "live_match": "Раніше розпізнавання запущеної програми", - "quality": "Виявлення помилково визначених або пошкоджених циклів", - "remaining_time": "Прогнозування залишку часу", "total_energy": "Прогнозування загальної енергії та вартості" }, "cap_label": { - "end": "Визначення кінця циклу", - "live_match": "Розпізнавання програми", - "quality": "Перевірка якості циклу", - "remaining_time": "Оцінка залишку часу", "total_energy": "Оцінка енергоспоживання" }, - "metric_auc": "AUC {auc} на відкладених даних", - "metric_mae": "похибка {model} проти базової моделі {baseline}" + "metric_mae": "похибка {model} проти базової моделі {baseline}", + "fine_tuned_at": "дотреновано {when}", + "held_out_count": "виміряно на {count} відкладених циклах", + "held_out_count_one": "виміряно на {count} відкладеному циклі", + "held_out_count_few": "виміряно на {count} відкладених циклах", + "held_out_count_many": "виміряно на {count} відкладених циклах", + "held_out_count_other": "виміряно на {count} відкладеного циклу", + "last_run_not_promoted": "Остання перевірка ({when}): {reason}", + "never": "ніколи", + "skip_reason": { + "fit_failed": "Помилка навчання: {error}", + "holdout_too_small": "Замало циклів, щоб чесно перевірити нову модель: для перевірки вдалося відкласти {held_out}, а потрібно {min}.", + "insufficient_rows": "Поки що недостатньо придатних циклів: наразі {count} позначених чистих циклів.", + "insufficient_rows_one": "Поки що недостатньо придатних циклів: наразі {count} позначений чистий цикл.", + "insufficient_rows_few": "Поки що недостатньо придатних циклів: наразі {count} позначені чисті цикли.", + "insufficient_rows_many": "Поки що недостатньо придатних циклів: наразі {count} позначених чистих циклів.", + "insufficient_rows_other": "Поки що недостатньо придатних циклів: наразі {count} позначеного чистого циклу.", + "not_better_than_incumbent": "Нова модель виявилася не кращою за поточну (похибка {model} проти {incumbent}), тому поточну залишено.", + "not_better_than_naive": "Нова модель не була помітно кращою за просту оцінку (похибка {model} проти {naive}).", + "unknown": "Нічого нового не вивчено." + }, + "trained_unknown": "у невідомий час" }, "modal": { "auto_label": "Автопозначення циклів", @@ -780,7 +751,6 @@ "cycle": "Цикл", "edit_group": "Редагувати групу профілів", "edit_phase": "Редагувати фазу", - "import_config": "Імпортувати конфігурацію", "label_cycle": "Позначити цикл", "merge_cycles": "Об'єднати {n} циклів", "new_group": "Нова група профілів", @@ -795,6 +765,8 @@ "delete_automation_title": "Видалити автоматизацію", "delete_automation_msg": "Видалити автоматизацію «{name}» з Home Assistant? Цю дію не можна скасувати.", "remove_legacy_title": "Видалити застарілі дії", + "reset_phase_msg": "Скасувати ваші зміни фази «{name}» та відновити вбудовану фазу? Програми збережуть свої діапазони.", + "reset_phase_title": "Скинути фазу", "remove_legacy_msg": "Видалити застарілі користувацькі дії? Вони перестануть спрацьовувати на подіях циклу. Це не можна скасувати з панелі.", "discard_recording_title": "Відхилити запис", "discard_recording_msg": "Відхилити збережений запис? Цю дію не можна скасувати.", @@ -815,32 +787,46 @@ "store_share_device": "Поділитися конфігурацією пристрою", "export_select": "Експорт - вибір даних", "import_wizard": "Імпорт - вибір даних", - "history_import": "Імпорт історії потужності" + "history_import": "Імпорт історії потужності", + "apply_suggestions_title": "Застосувати пропозиції", + "remove_task_title": "Видалити завдання", + "remove_task_msg": "Видалити нагадування «{task}»? Його записи в журналі обслуговування збережуться.", + "apply_suggestions_msg": "Змінити ці налаштування ({n})? Після збереження інтеграція перезавантажиться.", + "discard_settings_title": "Відхилити незбережені зміни?", + "discard_settings_msg": "Незбережені зміни налаштувань: {n}. Вийти з Налаштувань і відхилити їх?", + "profile_title": "Профіль · {name}", + "undo_import_msg": "Дані та налаштування цього пристрою повернуться до стану перед останнім імпортом ({when}). Цикли, записані відтоді, і зроблені зміни буде втрачено.", + "undo_import_ok": "Скасувати імпорт", + "undo_import_title": "Скасувати останній імпорт" }, "msg": { "pg_canvas_empty3": "Виберіть цикл вище й натисніть «Запустити», щоб змоделювати його. Потім наведіть, щоб побачити значення, прокрутіть або використайте два пальці для масштабування та перетягніть для панорамування.", + "pg_preset_delete_confirm": "Видалити пресет «{name}»?", + "pg_preset_limit": "Досягнуто ліміт пресетів ({n})", + "pg_preset_overwrite": "Перезаписати пресет «{name}»?", "tail_trim_hint": "Скільки секунд обрізати з кінця", - "store_sibling_hint": "Для вашої точної моделі нічого не опубліковано? Дуже схожа модель того самого бренду зазвичай є доброю відправною точкою.", - "store_declare_appliance": "Вкажіть WashData, який у вас пристрій, і на цій вкладці з'являться конфігурації, опубліковані для нього іншими користувачами. Можна також ввести бренд вище і просто подивитися, що є.", - "refresh_catalog_hint": "Списки брендів і пристроїв спільноти кешуються, щоб спільний магазин не перевищував свій денний ліміт запитів. Оновіть, щоб отримати записи, додані або схвалені іншими.", + "store_sibling_hint": "Для вашої моделі нічого немає? Споріднена модель тієї ж марки зазвичай добрий початок.", + "store_declare_appliance": "Укажіть WashData, який у вас прилад, щоб побачити налаштування, якими для нього поділилися інші. Або введіть марку вище й роздивіться.", + "refresh_catalog_hint": "Списки марок і приладів спільноти кешуються, щоб заощаджувати денний ліміт магазину. Оновіть, щоб побачити записи, які інші додали або схвалили.", "head_trim_hint": "Скільки секунд обрізати з початку", "appliance_monitor": "Монітор приладу", "artifact_dip_detail": "Опустився нижче звичайної смуги потужності приблизно на ~{n} с.", "artifact_footer": "Виділено на графіку вище. Це тимчасові артефакти (наприклад, двері відчинені посередині циклу), не обов'язково проблеми.", - "artifact_header": "Протягом цього циклу виявлено {n} аномалій", - "artifact_pause_detail": "Потужність впала майже до нуля приблизно на ~{n} с, а потім відновилася – імовірно, двері відчинили під час циклу або цикл призупинили.", + "artifact_header": "Протягом цього циклу виявлено {n} аномалії", + "artifact_header_one": "Протягом цього циклу виявлено {n} аномалію", + "artifact_header_few": "Протягом цього циклу виявлено {n} аномалії", + "artifact_header_many": "Протягом цього циклу виявлено {n} аномалій", + "artifact_header_other": "Протягом цього циклу виявлено {n} аномалії", + "artifact_pause_detail": "Потужність упала майже до нуля на ~{n}с, потім повернулася. Імовірно, відчинили дверцята або поставили цикл на паузу.", "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": "Накласти профілі (блідо)", - "compare_profiles_tip": "Накладіть інші конверти профілів на таблицю вище, щоб побачити, який з них найкраще підходить для цього циклу.", + "compare_profiles_tip": "Накладіть на графік інші профілі, щоб побачити, який найкраще пасує цьому циклу.", "compare_selected_cycles": "Вибрані цикли (суцільно) – показати / приховати", - "consider_new_profile": "Розгляньте можливість створення нового профілю.", - "coverage_gap": "останні цикли не мають відповідного профілю ({pct}% з останніх 30).", - "coverage_gap_similar_cycles": "Знайдено {count} схожих непозначених циклів – створіть профіль, щоб почати їх зіставлення.", "cycles_deleted": "Видалено циклів: {count}", "enough_data": "Достатньо даних для навчання ({current}/{min} циклів).", "export_description": "Виберіть, які саме профілі, цикли, налаштування та інше експортувати в JSON, або проаналізуйте файл та імпортуйте лише потрібні частини.", @@ -848,30 +834,28 @@ "feedback_prompt": "Підтвердьте правильність, виправте програму або проігноруйте.", "feedback_relabel_hint": "Повторне маркування цього циклу також вирішує його.", "filter_by_profile": "Фільтр за профілем…", - "group_modal_help": "Групуйте програми однакової форми, які відрізняються температурою/віджиманням (тривалість може відрізнятися). Підбір оцінює групу як одного кандидата, а потім вибирає найкраще підходящого учасника. Виберіть принаймні 2; накладка показує, наскільки вони схожі.", + "group_modal_help": "Згрупуйте програми однакової форми, що відрізняються температурою або віджимом. Зіставлення оцінює групу цілком, потім вибирає найкращого учасника. Виберіть щонайменше 2; накладання показує, наскільки вони схожі.", "group_not_cohesive": "Ці профілі недостатньо схожі для надійного групування, тому при зіставленні вони обробляються окремо, доки ви не видалите викид або не розділите групу.", "group_preview_hint": "Відмітьте 2 або більше учасників, щоб переглянути та порівняти їхні криві потужності.", - "import_intro": "Завантажте експортований файл або вставте JSON нижче.", - "legacy_actions_title": "{count} застаріла спеціальна дія ще виконується", + "legacy_actions_title": "{count} застарілої спеціальної дії ще виконується", + "legacy_actions_title_one": "{count} застаріла спеціальна дія ще виконується", + "legacy_actions_title_few": "{count} застарілі спеціальні дії ще виконуються", + "legacy_actions_title_many": "{count} застарілих спеціальних дій ще виконуються", + "legacy_actions_title_other": "{count} застарілої спеціальної дії ще виконується", "live_chart_loading": "Графік потужності в реальному часі з'являється по мірі надходження показань.", "loading": "Завантаження…", "loading_curve": "Завантаження кривої…", "loading_settings": "Завантаження налаштувань…", "log_buffer_hint": "Спочатку найновіші · буферизує останні 500 записів ha_washdata з моменту перезапуску · перетягніть нижній край, щоб змінити розмір.", - "maintenance_advisory": "Збільшення тривалості/енергії може свідчити про необхідність обслуговування приладу (наприклад, видалення накипу, очищення фільтра).", "maintenance_due": "Час для обслуговування: {items}", - "maintenance_intro": "Записуйте обслуговування цього приладу й отримуйте нагадування, коли настане час знову виконати кожне завдання.", + "maintenance_intro": "Записуйте обслуговування цього приладу й отримуйте нагадування, коли кожне завдання знову настане.", "maintenance_load_error": "Не вдалося завантажити дані обслуговування: {error}", "maintenance_requires_access": "Обслуговування та експорт/імпорт потребують повного доступу.", - "manual_duration_hint": "Середня/очікувана тривалість циклу профілю, що використовується для оцінки часу, що залишився. Відредагуйте, щоб встановити; якщо залишити без змін, збереже поточне значення.", - "matching_tuning_intro": "Під час навчання WashData також налаштовує, скільки ваги відповідність програм надає формі, тривалості та енергії.", + "manual_duration_hint": "Очікувана довжина циклу профілю, використовується для оцінки залишку часу. Не змінюйте, щоб зберегти поточне значення.", "merge_intro": "Вибрані цикли об'єднуються в один (хронологічний порядок; розриви заповнені 0 Вт). Оберіть отриманий профіль.", "ml_intro": "WashData постачається з розумними моделями, які працюють одразу.", "ml_learned_intro": "Моделі, налаштовані під цей пристрій.", - "ml_loading": "завантаження ML…", "ml_settings_intro": "Два незалежних перемикачі: один застосовує моделі під час циклу, інший дозволяє WashData з часом налаштовуватися під ваш пристрій.", - "name_first_program": "У вас достатньо циклів – назвіть першу програму, щоб почати зіставлення.", - "near_duplicate_cluster": "виявлено майже повторюваний кластер профілів. Групування дає змогу надійно вибирати між подібними програмами (наприклад, однакова програма з різною температурою/віджиманням).", "no_cycles_match": "Жоден цикл не відповідає поточному фільтру.", "no_cycles_profile": "Немає циклів для цього профілю.", "no_cycles_selected": "Спочатку виберіть принаймні один цикл.", @@ -896,79 +880,66 @@ "no_suggestions": "Немає активних пропозицій.", "notify_services_hint": "Використовуйте ID сервісів {entity} (через кому для кількох). Змінні шаблону: {vars}.", "old_actions_warning": "Налаштовано за допомогою старого редактора дій (тепер вилучено). Вони все ще запускаються на події циклу, але їх більше не можна редагувати тут. Перетворіть їх на звичайну автоматизацію або видаліть.", - "onboarding_progress": "{n} / 3 циклів спостережено", - "onboarding_watching": "Користуйтеся приладом як завжди – WashData спостерігає. Після 3 циклів почнеться зіставлення програм.", "pending_feedback": "Очікує відгук про виявлення", "performance_trend": "Тенденція продуктивності ({n} циклів)", - "pg_analysis_empty": "Завантажте цикл, щоб побачити аналіз збігу.", "pg_apply_confirm": "Застосувати найкраще значення: {label} = {value}?", - "pg_canvas_empty": "Виберіть цикл вище, потім натисніть «Завантажити», щоб побачити його графік потужності.", - "pg_chart_caption": "Частка виявлення/збігу за значеннями {param}", "pg_ctrl_intro": "Значення беруться з активних налаштувань інтеграції для цього пристрою. Зміни залишаються в пісочниці, доки ви не збережете їх у налаштування.", "pg_live_unavailable": "Активні налаштування недоступні, показано стандартні значення.", "pg_load_live_confirm": "Скасувати зміни в пісочниці ({n}) і завантажити поточні налаштування інтеграції?", - "pg_match_rate": "{pct}% збігів на {total} циклах", "pg_matches_live": "Збігається з активними налаштуваннями", "pg_n_changed": "Відмінностей від активних налаштувань: {n}", "pg_sugg_none": "Усі рекомендації вже відповідають поточним налаштуванням", - "pg_preset_delete_confirm": "Видалити пресет «{name}»?", - "pg_preset_limit": "Досягнуто ліміт пресетів ({n})", - "pg_preset_overwrite": "Перезаписати пресет «{name}»?", "pg_publish_one_confirm": "Зберегти {label} = {value} у налаштування цього пристрою?", "pg_restart_note": "Перезапустіть Home Assistant, щоб увімкнути інструменти симуляції.", - "pg_sim_progress": "{done} / {total} циклів", - "pg_starting": "Запуск…", - "pg_stress_synthetic": "тут починається симуляція режиму очікування", - "pg_sweep_intro": "Що якби {param} було іншим? Перевірте {steps} значень на ваших останніх {cycles} циклах, щоб знайти налаштування, за якого правильно зіставляється найбільше циклів.", - "pg_sweep_step": "Крок {done} / {total}", - "pg_verdict_bad": "Потребує уваги: багато циклів залишаються невиявленими.", - "pg_verdict_good": "Добре налаштовано: більшість циклів правильно визначаються та зіставляються.", - "pg_verdict_ok": "Прийнятно: деякі цикли пропущено. Спробуйте знизити поріг запуску.", "phase_catalog_intro": "Іменовані сегменти циклу (Попереднє прання, Нагрів, Віджимання…). Призначте їх профілю з його панелі керування.", "phase_ranges_intro": "Діапазони фаз (хвилини від початку циклу), накладені на середню криву. Редагуйте значення для попереднього перегляду в реальному часі.", - "playground_intro": "Безпечна пісочниця для відтворення минулих циклів з різними налаштуваннями, порівняння двох конфігурацій поруч і перегляду того, як цикл вирівнюється з профілем. Ніщо тут не змінює ваші активні дані, доки ви явно не застосуєте це.", - "playground_needs_restart": "Перезапустіть Home Assistant, щоб увімкнути інструменти Пісочниці.", - "preferences_admin": "Налаштування, панель і контроль доступу", - "preferences_adv": "Налаштування", + "playground_intro": "Пробуйте налаштування на реальних даних своїх циклів. Ніщо тут не змінює чинні налаштування, доки ви їх не застосуєте.", "prefs_personal": "Застосовується лише до вашого облікового запису Home Assistant.", "font_size_hint": "Зробіть усе на цій панелі більшим або меншим. Застосовується до вашого облікового запису на цьому пристрої.", - "process_history_hint": "Повторно запустіть зіставлення для всіх збережених циклів, оновіть пропозиції з налаштування, перенавчіть моделі ML (якщо увімкнено), перерахуйте вартість циклів за записаними цінами на енергію та перерахуйте якість циклів. Запускайте це після серії переглядів.", + "process_history_hint": "Заново запускає зіставлення всіх збережених циклів, оновлює пропозиції, перенавчає моделі ML (якщо ввімкнено), перераховує вартість циклів за записаними цінами та перераховує стан циклів. Запускайте після серії перевірок.", "profile_deleted": "Профіль видалено", - "profile_poor_health_detail": "Цикли, призначені цьому профілю, мають суперечливі форми або низьку надійність. Розгляньте можливість перебудови конверта або перегляду позначених циклів.", "profiles_intro": "Натисніть на профіль для статистики, фаз та очищення. Групуйте майже ідентичні програми (однакова форма/тривалість, різна температура або віджимання) для надійного вибору.", - "rbac_hint": "Коли вимкнено, кожен користувач Home Assistant має повний доступ (за замовчуванням). Адміністратори завжди мають повний доступ і можуть керувати всіма.", - "recent_logs": "Останні записи ha_washdata", + "rbac_hint": "Вимкнено: кожен користувач Home Assistant має повний доступ (типово). Адміністратори завжди мають повний доступ і можуть керувати всіма.", "recording_in_progress": "Запис триває", "reminders_intro": "Показувати нагадування на панелі через стільки циклів після останнього обслуговування. Залиште порожнім або 0, щоб вимкнути нагадування.", "restart_gap_footer": "Виділено на графіку. Дані потужності відсутні для цих інтервалів; зіставлення використовувало лише реальні показання.", - "restart_gap_header": "{n} пропуск при перезапуску HA під час цього циклу", + "restart_gap_header": "{n} пропуску при перезапуску HA під час цього циклу", + "restart_gap_header_one": "{n} пропуск при перезапуску HA під час цього циклу", + "restart_gap_header_few": "{n} пропуски при перезапуску HA під час цього циклу", + "restart_gap_header_many": "{n} пропусків при перезапуску HA під час цього циклу", + "restart_gap_header_other": "{n} пропуску при перезапуску HA під час цього циклу", "restart_gap_item": "{dur} пропуск", - "review_confirm_help": "Перевірте, чи правильно було виявлено цей цикл. Ваші відгуки навчають модель на вашому пристрої – що більше циклів ви підтвердите, то кращим буде відповідність та оцінка стану. Достатньо швидкого Добре/Погано.", + "review_confirm_help": "Цикл визначено правильно? Підтвердження циклів покращує зіставлення для вашої машини. Досить швидкого Добре/Погано.", "review_in_settings": "Переглянути в налаштуваннях", "review_notes_placeholder": "Примітки (необов'язково)", "review_notes_tip": "Довільні текстові нотатки для власної довідки. Не використовується під час підбору чи навчання.", - "review_profile_tip": "Програма, якою позначено цей цикл. Якщо автоматично визначена програма була неправильною, виправте це тут – маркування навчає відповідності для майбутніх циклів.", - "review_quality_tip": "Наскільки чистий цей цикл. Добре = хрестоматійний приклад цієї програми; Погано = виявлено, але шумно або нетипово; Непридатний = неправильно виявлений (об'єднаний, усічений або фальшивий). Керує оцінкою стану та циклами, дозволеними для навчання моделі.", - "review_recorded_tip": "Позначте це як підібраний вручну еталонний цикл для своєї програми – така ж роль, як цикл, записаний вручну. Довідкові цикли завжди зберігаються, заповнюють відповідний шаблон і ніколи не відкидаються під час очищення. (Це «золотий»/записаний прапор; обидва – одне й те саме.)", - "review_tags_tip": "Необов'язкові прапорці, що описують, що пішло не так у цьому циклі, щоб навчання та очищення могли це врахувати.", + "review_profile_tip": "Програма, якою позначено цей цикл. Якщо вона хибна, виправте її тут; мітки навчають зіставлення.", + "review_quality_tip": "Наскільки чистий цей цикл. Добре: зразковий прогін цієї програми. Погано: визначено, але з шумом або незвично. Непридатний: визначено хибно (злитий, обрізаний або хибний). Впливає на оцінку стану та на те, які цикли навчають модель.", + "review_recorded_tip": "Позначити як вручну вибраний еталонний цикл його програми, як цикл, записаний вручну. Еталонні цикли завжди зберігаються, задають шаблон зіставлення й ніколи не видаляються під час очищення.", + "review_tags_tip": "Необов'язкові позначки про те, що пішло не так, щоб навчання й очищення могли це врахувати.", "review_to_cycles": "Відкрити чергу огляду циклів", "samples_decimated": "Показано {shown} із {total} відліків (проріджено для відображення; піки збережено). Широкий розрив тут - це проріджування, а не пропуск даних.", - "saving_triggers_reload": "Збереження запускає перезавантаження інтеграції. Об'єкти HA можуть короткочасно відображатися як недоступні.", + "saving_triggers_reload": "Збереження перезавантажує інтеграцію. Сутності можуть ненадовго стати недоступними.", "search_placeholder": "Пошук налаштувань…", "see_recorder": "Дивіться віджет записувача нижче", "setting_changed": "Змінено з {old} на {new} {date}", "settings_basic_note": "Показано основні налаштування. Перейдіть до Розширених, щоб побачити весь список.", - "shape_drift_advisory": "⚠ Форма змінюється", - "shape_drift_detail": "Профіль потужності для цього профілю змінився з часом – можливе зношення приладу або потрібне обслуговування (наприклад, видалення накипу, очищення фільтра).", "show_all_settings": "Показати всі налаштування", - "showing_suggestions": "Показано {count} налаштувань із пропозиціями.", - "split_intro": "Натисніть на графік, щоб додати або видалити точку поділу, або автовизначення за паузами. Кожен отриманий сегмент може отримати власний профіль.", - "storage_diagnostics": "Статистика сховища, обслуговування, експорт/імпорт", + "showing_suggestions": "Показано {count} налаштування з пропозиціями.", + "showing_suggestions_one": "Показано {count} налаштування з пропозиціями.", + "showing_suggestions_few": "Показано {count} налаштування з пропозиціями.", + "showing_suggestions_many": "Показано {count} налаштувань із пропозиціями.", + "showing_suggestions_other": "Показано {count} налаштування з пропозиціями.", + "split_intro": "Натисніть на графік, щоб додати або прибрати точку поділу, або знайдіть їх автоматично за паузами простою. Кожна частина може отримати власний профіль.", "sug_staged": "Встановлено {key} = {val}. Збережіть для застосування.", "sug_muted": "Це налаштування більше не пропонуватиметься", "sug_mute_failed": "Не вдалося заглушити пропозицію", "sug_unmuted_all": "Заглушені пропозиції скинуто", - "n_suggestions_muted": "{count} заглушено; авто-налаштувач не пропонуватиме їх.", + "n_suggestions_muted": "{count} пропозиції заглушено; авто-налаштувач не пропонуватиме їх.", + "n_suggestions_muted_one": "{count} пропозицію заглушено; авто-налаштувач не пропонуватиме її.", + "n_suggestions_muted_few": "{count} пропозиції заглушено; авто-налаштувач не пропонуватиме їх.", + "n_suggestions_muted_many": "{count} пропозицій заглушено; авто-налаштувач не пропонуватиме їх.", + "n_suggestions_muted_other": "{count} пропозиції заглушено; авто-налаштувач не пропонуватиме їх.", "toast_auto_detect_enabled": "Автовизначення увімкнено", "toast_auto_label_complete": "Автоматичне маркування завершено", "toast_auto_label_failed": "Автоматичне маркування не виконано: {error}", @@ -984,7 +955,6 @@ "toast_no_automation": "Тут не можна створити автоматизацію", "toast_profile_name_required": "Потрібна назва профілю", "toast_program_set": "Програму вибрано: {program}", - "toast_rebuild_failed": "Перебудову не виконано: {error}", "toast_resume_failed": "Відновлення не виконано: {error}", "toast_revert_failed": "Відкат не виконано: {error}", "toast_reverted": "Повернено: {key}", @@ -995,95 +965,82 @@ "trend_energy_up": "Енергія зростає ({pct}%/цикл) – останній середній показник {avg}", "trim_destructive_confirm": "Залишиться лише {pct}% циклу, і цю дію не можна скасувати. Продовжити?", "trim_intro": "Перетягніть червоні маркери або введіть значення. Все за межами вікна видаляється.", - "tuning_suggestions_available": "Доступна {count} пропозиція налаштування на основі спостережених циклів. Вони з'являються поряд із відповідними полями.", + "tuning_suggestions_available": "Доступно {count} пропозиції налаштування на основі спостережених циклів. Вони з'являються поряд із відповідними полями.", + "tuning_suggestions_available_one": "Доступна {count} пропозиція налаштування на основі спостережених циклів. Вона з'являється поряд із відповідним полем.", + "tuning_suggestions_available_few": "Доступні {count} пропозиції налаштування на основі спостережених циклів. Вони з'являються поряд із відповідними полями.", + "tuning_suggestions_available_many": "Доступно {count} пропозицій налаштування на основі спостережених циклів. Вони з'являються поряд із відповідними полями.", + "tuning_suggestions_available_other": "Доступно {count} пропозиції налаштування на основі спостережених циклів. Вони з'являються поряд із відповідними полями.", "unsure_detected_prefix": "WashData не впевнений, що виявив", - "updated": "Оновлено", "warmup_badge": "Ще навчається ({done}/{needed} циклів)", - "warmup_detail": "Для початку автоматичного зіставлення цьому профілю потрібно {needed} позначених циклів. Кожен підтверджений цикл допомагає навчанню.", + "warmup_detail": "Зіставлення вже працює. WashData просить підтвердити перші {needed} циклів цього профілю, перш ніж позначати самостійно.", "wipe_history_warning": "Назавжди видалити всі цикли та профілі. Не можна скасувати.", "compare_overlay_tip": "Накладіть конверти вивчених профілів, щоб побачити, на яку програму схожий кожен цикл.", - "duration_trend_up_tip": "Тривалість зростає ({pct}%/цикл)", - "duration_trend_down_tip": "Тривалість зменшується ({pct}%/цикл)", - "energy_trend_up_tip": "Енергія зростає ({pct}%/цикл)", - "energy_trend_down_tip": "Енергія зменшується ({pct}%/цикл)", - "performance_trending": "Тенденція продуктивності", - "pg_select_cycle_hint": "Виберіть цикл і натисніть ↺ для завантаження", - "pg_sweep_progress": "Крок {done} / {total}", - "pg_analysis_hint": "Виберіть цикл і натисніть ↺ для завантаження аналізу.", "diagnostics_load_failed": "Не вдалося завантажити діагностику: {error}", - "manual_duration_ref_hint": "Використовується лише тоді, коли не вибрано контрольний цикл. Контрольний цикл задає тривалість за власною довжиною.", + "manual_duration_ref_hint": "Використовується лише без еталонного циклу; еталонний цикл задає тривалість за своєю довжиною.", + "duration_learned_hint": "Обчислено з циклів цієї програми (без викидів) і оновлюється після кожного нового. Щоб змінити, обріжте або видаліть цикли, що відхиляються.", "toast_convert_rolled_back": "Конвертацію не виконано, її було скасовано (жодної автоматизації не залишено): {error}", "toast_convert_orphan": "Автоматизацію створено, але очистити старі дії не вдалося. Не повторюйте спробу: вилучіть старі дії вручну, щоб уникнути дублювання автоматизації.", - "advisory_duration_trend_up": "Цикли '{name}' стають дедалі довшими (приблизно +{pct}% за цикл). Якщо поведінка приладу змінилася, перезапишіть або перебудуйте цей профіль.", - "advisory_energy_trend_up": "'{name}' споживає дедалі більше енергії (приблизно +{pct}% за цикл) - варто перевірити прилад, якщо це несподівано.", - "advisory_poor_health": "У '{name}' низька оцінка відповідності - його останні цикли сильно різняться або погано розпізнаються. Перегляньте його цикли або перезапишіть профіль, щоб розпізнавання та оцінки часу залишалися точними.", - "advisory_shape_drift": "'{name}' суттєво відхилився від початкової форми кривої потужності. Можливо, поведінка приладу з часом змінилася (наприклад, накип, знос). Радимо перезаписати цей профіль за останніми циклами.", - "advisory_shape_drift_corr": "'{name}' суттєво відхилився від початкової форми кривої потужності (кореляція {corr}). Можливо, поведінка приладу з часом змінилася (наприклад, накип, знос). Радимо перезаписати цей профіль за останніми циклами.", - "advisory_unmatchable": "'{name}' ніколи не буде зіставлений: за ним немає жодного циклу з даними потужності, тому WashData не може порівняти з ним поточний цикл. Позначте цикл цієї програми (або запишіть новий), щоб повернути профіль у зіставлення.", - "advisory_unmatchable_no_duration": "'{name}' ніколи не буде зіставлений: у нього є цикл із даними потужності, але без придатної тривалості, тому WashData не має еталонної довжини, за якою вимірює зіставлення. Запишіть цю програму ще раз або задайте її очікувану тривалість, щоб повернути профіль у зіставлення.", + "advisory_duration_trend_up": "Цикли '{name}' стають дедалі довшими (приблизно +{pct}% за цикл). Якщо прилад змінився, перезапишіть або перебудуйте цей профіль.", + "advisory_energy_trend_up": "'{name}' з кожним циклом споживає більше енергії (приблизно +{pct}% за цикл). Перевірте прилад, якщо це неочікувано.", + "advisory_poor_health": "'{name}' погано пасує: його останні цикли сильно відрізняються або слабко збігаються. Перегляньте його цикли або перезапишіть його, щоб зіставлення й оцінки лишалися точними.", + "advisory_shape_drift": "'{name}' відхилився від початкової форми кривої потужності, можливо через накип або знос. Спробуйте перезаписати його за останніми циклами.", + "advisory_shape_drift_corr": "'{name}' відхилився від початкової форми кривої потужності (кореляція {corr}), можливо через накип або знос. Спробуйте перезаписати його за останніми циклами.", + "advisory_unmatchable": "'{name}' ніколи не зіставиться: за ним немає жодного циклу з даними про потужність. Позначте або запишіть цикл цієї програми, щоб це виправити.", + "advisory_unmatchable_no_duration": "'{name}' ніколи не зіставиться: його цикл не має придатної тривалості. Перезапишіть цю програму або задайте її очікувану тривалість.", "pg_no_alerts": "У цьому запуску проблем не виявлено.", "pg_override_active": "Використовуються ваші змінені налаштування замість поточних", "pg_history_empty": "Натисніть «Запустити», щоб відтворити цикли.", "pg_history_summary": "{detected}/{total} виявлено · {correct} зіставлено правильно", "pg_sweep_no_metric": "Недостатньо даних для оцінки цієї цілі.", "pg_sweep_intro2": "Знайдіть налаштування, яке найкраще досягає цілі на ваших останніх циклах. Нічого не зміниться, доки ви не застосуєте.", - "pg_running_history": "Відтворення ваших циклів…", - "pg_running_sweep": "Перевірка значень за всіма вашими циклами…", "pg_row_load_hint": "Завантажити цей цикл на графік вище", - "pg_history_intro2": "Відтворіть недавні цикли через справжній детектор і модуль зіставлення з наведеними вище налаштуваннями. Натисніть будь-який рядок, щоб завантажити цей цикл на графік; змініть налаштування, щоб побачити порівняння до і після.", + "pg_history_intro2": "Проженіть останні цикли через справжній детектор і зіставлення з налаштуваннями вище. Натисніть на рядок, щоб завантажити його в графік; змініть налаштування, щоб порівняти до й після.", "pg_simulating": "Моделювання циклу…", - "pg_canvas_empty2": "Виберіть цикл вище й натисніть «Запустити», щоб змоделювати його. Потім наведіть, щоб побачити значення, прокрутіть для масштабування та перетягніть для панорамування.", "pg_apply_settings_confirm": "Зберегти ці налаштування ({n}) на цей пристрій? {list}", - "pg_stress_terminated_detail": "Стабілізувалося на ~{idle}W у режимі очікування -- цикл завершився {h} год {m} хв пізніше через {reason}.", - "pg_stress_above_threshold_detail": "Споживання в режимі очікування ~{idle}W дорівнює або перевищує фактичний поріг зупинки ({stop}W) -- цикл так і не був зафіксований як неактивний. Щоб виправити, збільште stop_threshold_w.", - "pg_stress_hit_cap_detail": "Цикл працював {h} год {m} хв без зупинки -- примусово зупинений захисним лімітом. Споживання в режимі очікування {idle}W порівняно з порогом зупинки {stop}W.", "no_logs_match": "Немає записів журналу, що відповідають цим фільтрам.", "pg_analysis_hint2": "Виберіть цикл і натисніть Запустити, щоб завантажити аналіз збігу.", - "pg_analysis_empty2": "Натисніть Запустити, щоб побачити аналіз збігу.", - "imported_readonly": "Імпортовано з магазину спільноти. Показується для довідки та зіставлення. Не враховується у вашій статистиці й не підлягає редагуванню.", + "imported_readonly": "Імпортовано з магазину спільноти. Використовується як еталон і для зіставлення, не враховується в статистиці й не редагується.", "merge_no_imports": "Імпортовані записи не можна об'єднати з реальним циклом. Зніміть їх позначку, щоб об'єднати.", - "store_enable_hint": "Увімкніть онлайн-функції в налаштуваннях, щоб переглядати та імпортувати еталонні цикли спільноти.", - "advisory_duration_outlier": "Цикли з міткою '{name}' ({n}) надто далекі від її звичайної тривалості, щоб коли-небудь із нею збігтися (найбільше відхилення: {ratio}x). Імовірно, їх позначено помилково або один запис охопив два запуски. Змініть мітку або розділіть їх, щоб вони перестали спотворювати оцінку часу для цієї програми.", + "store_enable_hint": "Увімкніть онлайн-функції в розділі Параметри > Онлайн та спільнота, щоб переглядати та імпортувати еталонні цикли спільноти.", + "advisory_duration_outlier": "Цикли з міткою ‘{name}’ ({n}) надто далекі від його звичайної довжини, щоб колись зіставитися (найгірший: {ratio}x). Імовірно, їх позначено хибно або вони містять два прогони. Перепозначте або розділіть їх, щоб вони не спотворювали оцінку часу.", "standby_above_stop_title": "У момент завершення циклу все ще споживається {idle} Вт", "standby_above_stop_sub": "{n} з останніх {total} циклів. Затримка вимкнення починає відлік лише після того, як потужність опуститься нижче Порога зупинки ({stop} Вт), тому цикли завершуються із запізненням або їх доводиться зупиняти вручну.", - "online_intro": "Діліться еталонними циклами з іншими користувачами WashData і завантажуйте їх. Усі онлайн-функції вмикаються за бажанням і типово вимкнені.", "store_share_intro": "Завантажте цей еталонний цикл, щоб інші користувачі з таким самим пристроєм могли його використати. Перед публікацією він проходить перевірку.", - "online_intro_global": "Переглядайте еталонні записи та діліться ними з іншими користувачами WashData, а також підтверджуйте записи про пристрої. Одне підключення діє для всієї вашої інтеграції WashData; бренд і модель пристрою задаються для кожного пристрою в розділі «Основне». Усі онлайн-функції вмикаються за бажанням і типово вимкнені.", + "online_intro_global": "Переглядайте еталонні записи та діліться ними з іншими користувачами WashData. Одне підключення діє для всіх ваших пристроїв WashData; марку й модель задають для кожного пристрою в розділі «Основні». Онлайн-функції вимкнені, доки ви їх не ввімкнете.", "online_unavailable": "Онлайн-функції недоступні на цьому сервері.", "store_picker_offline": "Увімкніть онлайн-функції в меню налаштувань (шестерня), щоб вибирати з каталогу спільноти.", "pick_brand_first": "Спочатку виберіть бренд пристрою.", - "model_not_found": "Ще немає в каталозі.", - "connect_to_confirm": "Підключіться в меню налаштувань (шестерня), щоб підтверджувати або оцінювати.", + "custom_task_limit": "Досягнуто ліміту завдань ({max}).", + "custom_tasks_intro": "Додайте все інше, про що хочете отримувати нагадування. Час виконати завдання настає, щойно досягнуто будь-якого з інтервалів; залиште один порожнім або 0, щоб використовувати лише інший.", + "maintenance_due_sensor_hint": "Бінарний датчик «Потрібне обслуговування» увімкнений, поки є завдання до виконання; використовуйте його у власних автоматизаціях сповіщень.", + "maintenance_supporter_intro": "Maintenance Supporter є окремою інтеграцією, яка вміє значно більше: будь-який прилад чи завдання, розклад за часом, використанням або датчиком, сповіщення та історія. Вона може використовувати лічильник циклів WashData.", "adopt_settings_hint": "Застосувати налаштування виявлення та зіставлення спільного пристрою до цього пристрою", - "include_settings_hint": "Включити пороги виявлення, ваги зіставлення та інші параметри до спільного пакету", + "include_settings_hint": "Поділитися порогами виявлення та зіставлення цього пристрою (без сповіщень, сутностей і ціни енергії). Інші самі вирішать, чи застосовувати їх.", "onboard_download": "Завантажити готову конфігурацію з магазину спільноти", - "share_consent": "Ви ділитеся реальними даними вашого приладу. Не діліться, якщо ваші шаблони використання є приватними.", "share_device_none": "Пристрої ще не налаштовані. Спочатку додайте пристрій.", "share_guideline_naming": "Використовуйте зрозумілі назви програм (наприклад, 'Cotton 40', 'Eco 60'), щоб інші могли їх ідентифікувати", - "share_guideline_quality": "Діліться лише циклами, які завершилися нормально: без перерв посеред циклу, відкривання дверцят або короткочасних збоїв живлення.", + "share_guideline_quality": "Діліться лише циклами, що завершилися нормально: без переривань, відчинення дверцят і перебоїв живлення.", "share_guideline_review": "Перегляньте профілі перед публікацією -- видаліть ті, що виглядають неправильно", "share_guidelines_title": "Перед публікацією", - "store_download_device_intro": "Завантажити конфігурацію пристрою зі спільноти та застосувати її до нового або наявного пристрою", + "store_download_device_intro": "Додати на ваш пристрій усі спільні програми та їхні еталонні цикли. Ваші власні цикли й статистика не змінюються.", "store_share_device_intro": "Завантажте {brand} {model} з вибраними вами еталонними циклами. Інші користувачі з таким самим пристроєм зможуть прийняти ваші програми. Записи перевіряються перед публікацією.", "share_profile_no_cycles": "Немає еталонних циклів. Щоб включити цей профіль, позначте цикл як ⭐ на вкладці Цикли", - "advisory_phase_inconsistent": "Схоже, що '{name}' поєднує різні програми або температури - його цикли нагріваються протягом дуже різного часу. Розділення на окремі профілі (напр. за температурою) покращить зіставлення та оцінки часу.", - "advisory_phase_inconsistent_title": "⚠ Можливо, змішані програми", - "export_select_intro": "Позначте, що саме включити. Вибір профілів без їхніх циклів усе одно експортує розпізнавану програму (її вивчена форма передається разом із нею).", - "import_analyze_hint": "Завантажте експортований файл (або вставте його JSON). WashData проаналізує його та покаже, що саме можна імпортувати, перш ніж щось зміниться.", + "export_select_intro": "Позначте, що включити. Профілі без своїх циклів усе одно експортують розпізнавану програму.", + "import_analyze_hint": "Завантажте експортований файл (або вставте його JSON). WashData покаже, що можна імпортувати, перш ніж щось зміниться.", "analyzing": "Аналіз…", - "device_type_mismatch_warn": "Цей експорт отримано від приладу іншого типу ({src} замість {local}). Програми та цикли все одно можна імпортувати як довідкові дані, але налаштування, специфічні для пристрою, та імпорт реальної історії вимкнено.", + "device_type_mismatch_warn": "Цей експорт від іншого типу приладу ({src} vs {local}). Програми й цикли все одно можна імпортувати як еталонні дані, але без налаштувань пристрою та реальної історії.", "merge_hint": "Імпортовані елементи додаються; нічого локального не втрачається. Конфлікти імен вирішуються нижче.", "replace_warn": "Кожна позначена категорія очищується та замінюється даними з файлу. Непозначені категорії залишаються без змін.", "dest_reference_hint": "Імпортовані цикли лише покращують розпізнавання програм і ніколи не впливають на статистику використання/енергії.", - "dest_real_history_hint": "Імпортовані цикли зараховуються як власна історія цього пристрою та враховуються в статистиці енергії/використання. Використовуйте для перенесення одного приладу на нове встановлення.", - "import_history_description": "Розумна розетка була у вас ще до WashData? Завантажте експорт історії її датчика потужності або зчитайте її прямо з Home Assistant, і звичайне виявлення пройде цими даними, тож минулі цикли з'являться у списку «Цикли», готові до позначення.", - "hist_input_hint": "Завантажте CSV, отриманий на панелі «Історія» (сутність, стан, час зміни), або дозвольте WashData зчитати історію датчика напряму. Далі виявлення пройде цими даними так само, як у реальному часі, а ви виберете, які зі знайдених циклів зберегти.", - "hist_recorder_hint": "Читає дані від вибраної дати до цього моменту. Типово Home Assistant зберігає докладну історію 10 днів, а після цього лише погодинні середні значення, які надто грубі для виявлення циклів - вибирайте давнішу дату лише тоді, коли в налаштуваннях recorder задано довший термін зберігання.", - "hist_scanning": "Ваша історія відтворюється через детектор. Це виконується у фоні - можна закрити це вікно та повернутися пізніше.", + "dest_real_history_hint": "Імпортовані цикли вважаються власною історією та статистикою цього пристрою. Використовуйте під час перенесення приладу на нову інсталяцію.", + "import_history_description": "Розумна розетка була у вас ще до WashData? Завантажте її історію потужності або прочитайте її з Home Assistant. По ній пройде виявлення, і минулі цикли з'являться в списку «Цикли», готові до назви.", + "hist_input_hint": "Завантажте CSV з панелі «Історія» (сутність, стан, час зміни) або дозвольте WashData прочитати історію датчика напряму. По ній пройде виявлення як у реальному часі, а ви виберете, які цикли залишити.", + "hist_recorder_hint": "Читає дані з вибраної дати до поточного моменту. Home Assistant типово зберігає детальну історію 10 днів; старіші погодинні середні надто грубі для виявлення циклів.", + "hist_scanning": "Ваша історія проходить через детектор у фоні. Можна закрити це вікно й повернутися пізніше.", "hist_imported_count": "Імпортовано циклів: {n}.", "hist_duplicates": "Уже імпортовано раніше та пропущено: {n}.", "hist_capped": "Досягнуто ліміту імпортованих циклів для пристрою; решту не збережено.", - "hist_next_step": "Вони у списку «Цикли» з позначкою імпортованої історії. Відкрийте цикл і натисніть «Позначити», щоб вказати його програму.", + "hist_next_step": "Вони в списку «Цикли» з позначкою імпортованої історії. Відкрийте будь-який і кнопкою «Позначити» назвіть його програму.", "hist_rows_read": "Прочитано показань: {n}", "hist_breaks": "Пропусків, де датчик був недоступний: {n}", "hist_other_entity": "Показань інших сутностей пропущено: {n}", @@ -1095,25 +1052,76 @@ "hist_scan_capped": "Показано лише перших кандидатів (знайдено: {n}).", "hist_recorder_empty": "У Home Assistant немає докладної історії для цього датчика за цей період.", "hist_scan_failed": "Сканування не вдалося.", - "hist_scan_expired": "Це сканування вже недоступне. Виконайте сканування ще раз.", + "hist_scan_expired": "Це сканування більше недоступне. Запустіть сканування знову.", "hist_import_failed": "Імпорт не вдався.", - "imported_history_readonly": "Виявлено в імпортованій історії потужності. Впливає на зіставлення програм, але не враховується у вашій статистиці; такий цикл не можна обрізати чи розділити. Натисніть «Позначити», щоб вказати програму.", - "odometer_intro": "Ця загальна кількість лише зростає. Видалення запису про цикл її не змінює, а підрахунок продовжується й поза межами ліміту збереженої історії, тому нагадування про обслуговування залишаються правильними.", - "odometer_correct_hint": "Задайте значення, якщо WashData записав цикл, якого не було, або якщо прилад уже виконував цикли до встановлення WashData.", + "imported_history_readonly": "Знайдено в імпортованій історії потужності. Допомагає зіставленню, але не враховується в статистиці, і його не можна обрізати чи розділити. Позначте його, щоб назвати програму.", + "odometer_intro": "Це число лише зростає. Видалення циклу його не зменшує, тому нагадування про обслуговування лишаються правильними.", + "odometer_correct_hint": "Задайте це, якщо WashData порахував цикл, якого не було, або прилад працював до встановлення WashData.", "no_reminders_set": "Нагадування про обслуговування ще не задано.", "toast_program_armed": "Програму вибрано для наступного циклу: {program}", "toast_range_required": "Задайте коректний діапазон Від/До", - "no_profiles_option": "(профілів ще немає, додайте новий)" + "no_profiles_option": "(профілів ще немає, додайте новий)", + "compare_cycles_title": "Порівняти цикли ({count})", + "edit_builtin_phase": "Це вбудована фаза. Збереження створює власну копію; видаліть копію, щоб повернути оригінал.", + "fetch_error": "Не вдалося завантажити дані.", + "connection_lost": "З'єднання втрачено - показано дані станом на {time}.", + "pg_notify_partial": "Маркери показують лише сповіщення про старт, майже готово, завершення, досягнення й тихі години. Оновлення в реальному часі, нагадування про вивантаження та попередження про перевищення не моделюються.", + "connect_to_confirm": "Підключіться в меню налаштувань (шестерня), щоб підтверджувати або оцінювати.", + "coverage_gap_cluster": "Останні цикли без мітки ({count}) тривалістю близько {min} хв схожі на одну програму, яку ви ще не створили.", + "pg_label_yes": "Наприкінці циклу отримав би мітку «{name}»", + "pg_label_no": "Було б надіслано на перегляд: {reason}", + "live_match_debug_tip": "Впевненість: наскільки точно поточна крива потужності збігається з найкращим профілем-кандидатом (0-100%). Неоднозначно: оцінки двох найкращих кандидатів відрізняються менш ніж на 5%, тому мітка залишається невизначеною до завершення циклу.", + "automation_alias_started": "{device}: цикл розпочато", + "automation_alias_finished": "{device}: цикл завершено", + "automation_desc_started": "Спрацьовує, коли розпочинається цикл пристрою {device}. Додайте власні дії (сповіщення, освітлення, ...).", + "automation_desc_finished": "Спрацьовує, коли завершується цикл пристрою {device}. Додайте власні дії (сповіщення, освітлення, ...).", + "feedback_detected": "WashData виявив: {name}", + "import_undo_hint": "Збережено точку відновлення до останнього імпорту ({when}): профілів: {profiles}, циклів: {cycles}.", + "replace_undo_hint": "Спочатку зберігається точка відновлення, тож цей імпорт можна скасувати в розділі «Експорт / Імпорт».", + "pg_in_sample_note": "Ці цикли допомогли побудувати профілі, з якими вони порівнюються, тому результати оптимістичні: на нових циклах очікуйте трохи менше правильних збігів.", + "hist_tail_trimmed": "Обрізано {min} хв очікування після завершення циклу, так само як це робить інтеграція під час роботи", + "hist_truncated": "зупинено на ліміті показань ({n}), тому решту історії не переглянуто", + "hist_warn_kw": "Найбільший показник лише {w}. Схоже, датчик передає кіловати; WashData потрібні вати, тому цикли виявити неможливо.", + "hist_warn_naive_time": "{n} позначок часу не мають часового поясу й прочитані як UTC. Якщо у файлі місцевий час, цикли зсунуто на ваш зсув від UTC, а показники біля переведення годинника можуть загубитися.", + "hist_warn_naive_time_one": "{n} позначка часу не має часового поясу й прочитана як UTC. Якщо у файлі місцевий час, цикли зсунуто на ваш зсув від UTC, а показники біля переведення годинника можуть загубитися.", + "hist_warn_naive_time_few": "{n} позначки часу не мають часового поясу й прочитані як UTC. Якщо у файлі місцевий час, цикли зсунуто на ваш зсув від UTC, а показники біля переведення годинника можуть загубитися.", + "hist_warn_naive_time_many": "{n} позначок часу не мають часового поясу й прочитані як UTC. Якщо у файлі місцевий час, цикли зсунуто на ваш зсув від UTC, а показники біля переведення годинника можуть загубитися.", + "hist_warn_naive_time_other": "{n} позначки часу не мають часового поясу й прочитані як UTC. Якщо у файлі місцевий час, цикли зсунуто на ваш зсув від UTC, а показники біля переведення годинника можуть загубитися.", + "pg_compare_profile_tip": "Вибирає лише профіль, що накладається на графік. Симуляція завжди сама зіставляє програму з циклом, як і інтеграція під час роботи.", + "pg_outcome_stale": "Налаштування змінилися після запуску цієї симуляції. Вона досі показує старий результат.", + "pg_sweep_guarded": "Завершує більше циклів передчасно, розбиває більше з них або пропускає цикли, які виявляє поточне налаштування, тому ніколи не рекомендується.", + "pg_sweep_keep_current": "Залишити поточне значення ({value}, {metric}): жодне значення не було кращим хоча б на один цикл.", + "pg_sweep_n_cycles": "на {count} циклах", + "pg_sweep_n_cycles_one": "на {count} циклі", + "pg_sweep_n_cycles_few": "на {count} циклах", + "pg_sweep_n_cycles_many": "на {count} циклах", + "pg_sweep_n_cycles_other": "на {count} циклу", + "pg_task_busy": "Для цього пристрою вже триває «Перевірка на історії» або «Оптимізувати». Дочекайтеся завершення або скасуйте.", + "cycle_context_loading": "Завантаження історії з recorder…", + "cycle_context_note": "Сірим: історія датчика потужності з recorder навколо цього циклу. Вона не є частиною циклу й не впливає на тривалість, енергію чи зіставлення.", + "cycle_context_none": "У recorder немає показань навколо цього циклу (типово Home Assistant зберігає історію 10 днів).", + "cycle_context_tip": "Лише для перегляду: показує історію датчика потужності з recorder навколо цього циклу, якщо вона ще зберігається. Щоб додавати невдалі спроби запуску до майбутніх циклів, використайте Передзапис кривої (Налаштування > Виявлення > Початок циклу); це змінює збережені тривалості.", + "invalid_threshold_pair": "Поріг зупинки ({stop} Вт) має бути нижчим за Поріг початку ({start} Вт). Нічого не збережено; змініть обидва значення разом." }, "phase_desc": { "anti_crease": "Час від часу короткі оберти після завершення для зменшення зморшок.", "anti_wrinkle": "Періодичне обертання після циклу для зменшення зморшок.", + "baking": "Нагрів на високій температурі для скоринки та м'якушки.", + "cooking": "Основна фаза приготування з нагрівачем і вентилятором.", "cool_down": "Обертання без нагріву ближче до кінця циклу.", + "cool_down_af": "Охолодження лише вентилятором після нагріву.", "drain_and_switch": "Перехідний період з режиму прання на режим сушіння.", "dry": "Стадія сушіння з використанням нагрівача та/або залишкового тепла.", "drying": "Основний період нагрітого барабанного сушіння.", "heat_up": "Початковий прогрів нагрівача перед початком повного висихання.", + "keep_warm": "Слабкий нагрів, щоб їжа залишалася теплою.", + "keep_warm_bm": "Слабкий нагрів, щоб хліб залишався теплим після випікання.", + "kneading": "Мотор перемішує та вимішує тісто. Високе споживання.", + "pause_af": "Коротка пауза, щоб струсити або перевірити.", + "pre_heat": "Початковий прогрів камери перед основним приготуванням.", "pre_rinse": "Початкове зрошення перед пранням мийним засобом.", + "proving": "Розстоювання при слабкому нагріві: дріжджі бродять, тісто піднімається.", + "resting": "Коротка пауза на малій потужності між замісами, щоб клейковина розслабилася.", "pre_wash": "Початкове замочування або попередня обробка перед основним пранням.", "rinse": "Стадія промивання чистою водою. Ця фаза може повторюватися кілька разів.", "sanitize": "Етап високотемпературного очищення для програм дезінфекції.", @@ -1126,6 +1134,7 @@ "wash_dw": "Основне мийне прання з підігрівом." }, "placeholder": { + "custom_task": "напр. Очистити ущільнювач дверцят", "add": "додати…", "group_name": "напр. Бавовна 2:47", "json_buttons": "[{\"action\":\"ID\",\"title\":\"Label\"}]", @@ -1152,11 +1161,11 @@ }, "section": { "dishwasher": { - "intro": "Обробка завершення циклу для посудомийних машин, які зазвичай закінчуються тривалою фазою сушіння майже без споживання енергії перед коротким фінальним зливом.", + "intro": "Обробка завершення для посудомийних машин, які часто довго й тихо сушать перед коротким фінальним зливом.", "label": "Посудомийна машина" }, "anti_wrinkle": { - "intro": "Режим захисту від зминання виявляє короткі імпульси обертання барабана з малою потужністю після основного циклу та зберігає їх як частину завершеного циклу.", + "intro": "Залишає короткі імпульси обертання барабана з малою потужністю після основної фази сушильної машини у складі завершеного циклу.", "label": "Захист від зминання" }, "basic": { @@ -1164,7 +1173,7 @@ "label": "Основні" }, "delay": { - "intro": "Виявлення відкладеного старту визначає, коли прилад увімкнено, але цикл ще не розпочато.", + "intro": "Виявляє прилад, який увімкнено і який чекає відкладеного старту.", "label": "Відкладений старт" }, "detection": { @@ -1188,157 +1197,149 @@ "label": "Час & Сторожовий пес" }, "triggers": { - "intro": "Додаткові зовнішні сигнали: тригер завершення, датчик дверей, перемикач паузи та нагадування про вивантаження.", + "intro": "Необов'язкові доповнення: тригер завершення, датчик дверей, перемикач паузи та нагадування про вивантаження.", "label": "Тригери & Двері" - }, - "phase_eta": { - "label": "Залишок часу", - "intro": "Оцінка залишку часу з урахуванням фаз, для машин, тривалість циклу яких залежить від температури або віджиму." } }, "setting": { "dishwasher_end_spike_quiet_release": { - "doc": "Після того як цикл перевищує очікувану тривалість, скільки часу посудомийна машина має залишатися неактивною (нижче «Поріг зупинки»), перш ніж WashData припинить очікувати фінальний злив і завершить цикл. Збільште значення, якщо ваша машина має тривалу тиху фазу сушіння перед пізнім фінальним зливом, який пропускається - ширше вікно дозволяє вивченій тривалості слідувати за сезонними змінами (холодніша вхідна вода = довші цикли), а не фіксуватися на старому середньому значенні. Воно лише скорочує очікування відносно внутрішнього 30-хвилинного ліміту очікування завершального сплеску (відкачування води), але ніколи не збільшує його.", + "doc": "Після очікуваної тривалості: скільки посудомийна машина має лишатися нижче Порога зупинки, перш ніж WashData перестане чекати фінального зливу. Збільште, якщо пізній злив після довгого сушіння пропускається. Очікування ніколи не перевищує 30 хвилин.", "label": "Тихе завершення пасивного сушіння" }, "anti_crease_finalize_ratio": { - "doc": "Наскільки глибоко в очікувану тривалість зіставленої програми має зайти цикл, перш ніж WashData зможе визнати спостережуване обертання барабана на малій потужності хвостом захисту від зминання і завершити цикл на цьому місці. Очікувана тривалість - це середнє значення програми, тому на сушильній машині, час сушіння за датчиком вологості в якої залежить від завантаження, більшість запусків ніколи не досягає 98% цього значення, і хвіст тягнеться аж до резервного тайм-ауту - часто через 15-40 хвилин після того, як машина фактично зупинилася. На такій сушильній машині зменшіть значення (напр., 0.75). Для пральних машин залиште значення за замовчуванням: пральна машина проводить більшу частину циклу нижче налаштування Макс. потужність захисту від зминання, тож ця частка - єдине, що відокремлює тиху фазу середини прання від хвоста, і її зниження може скоротити прання удвічі. Залиште порожнім для значення за замовчуванням (0.98). Це не те саме, що Коефіцієнт розумного завершення, який керує окремою перевіркою дострокового завершення.", + "doc": "Частка середньої тривалості програми, яку цикл має пройти, перш ніж обертання барабана з малою потужністю вважатиметься хвостом захисту від зминання. Зменште (наприклад, 0,75) для сушильних машин, що підлаштовуються під завантаження. Для пральних машин залиште типове значення (0,98), інакше прання може закінчитися на половині.", "label": "Коефіцієнт завершення захисту від зминання" }, "anti_wrinkle_enabled": { - "doc": "Розпізнавайте короткі малопотужні імпульси обертання, які видає сушильна машина після основної фази нагрівання, і зберігайте їх прив’язаними до завершеного циклу замість того, щоб зчитувати їх як нові цикли.", + "doc": "Залишати короткі імпульси обертання з малою потужністю після основної фази нагрівання сушильної машини у складі завершеного циклу, а не вважати їх новими циклами.", "label": "Увімкнути виявлення захисту від зминання" }, "anti_wrinkle_exit_power": { - "doc": "Потужність має бути нижче цього між імпульсами, щоб режим проти зморшок залишався активним.", + "doc": "У режимі захисту від зминання потужність нижче цього значення вважається паузою між імпульсами обертання, а пауза, довша за Макс. паузу між імпульсами, завершує режим. Якщо Поріг зупинки вищий, використовується він, тож налаштування діє лише вище Порогу зупинки.", "label": "Поріг потужності виходу" }, "anti_wrinkle_idle_timeout": { "label": "Макс. пауза між імпульсами", - "doc": "Як довго машина може залишатися тихою між двома імпульсами перекидання барабана, перш ніж режим захисту від зминання завершиться. Установіть вище за найдовшу паузу, яку ваша сушильна машина залишає між імпульсами, інакше кожен наступний імпульс буде зчитано як хибний запуск." + "doc": "Скільки сушильна машина може мовчати між імпульсами обертання, перш ніж режим захисту від зминання завершиться. Задайте більше за найдовшу паузу вашої машини, інакше наступні імпульси вважатимуться хибними стартами." }, "anti_wrinkle_max_duration": { - "doc": "Імпульси, довші за цей, розглядаються як справжній цикл, а не як рух проти зморшок.", + "doc": "Імпульс, довший за цей, вважається справжнім циклом, а не обертанням захисту від зминання.", "label": "Максимальна тривалість" }, "anti_wrinkle_max_power": { - "doc": "Пульс вище цієї потужності розглядається як справжній новий цикл, а не як рух проти зморшок. Встановіть трохи вище потужності перекидання.", + "doc": "Імпульс вище цієї потужності вважається справжнім новим циклом, а не обертанням барабана. Задайте трохи вище потужності імпульсів обертання.", "label": "Макс. потужність захисту від зминання" }, "auto_label_confidence": { - "doc": "Якщо результат збігу наприкінці циклу дорівнює або перевищує цей показник, програма автоматично позначається без підтвердження. Підніміть його, щоб вимагати більшої впевненості перед автоматичним маркуванням; знизити його, щоб автоматизувати більше. Працює в поєднанні з Learning Confidence під ним.", + "doc": "Оцінка збігу в кінці циклу, за якої або вище якої програма позначається без запитання. Підвищте для більшої певності; знизьте, щоб автоматично позначати більше циклів. Тримайте вище Впевненості навчання.", "label": "Впевненість автоматичного маркування" }, "auto_maintenance": { - "doc": "Виконуйте нічне прибирання: перебудовуйте конверти профілів, повторно обчислюйте працездатність циклу, видаляйте сліди налагодження та зберігайте найновіші цикли.", + "doc": "Нічне прибирання: позначає старі цикли, які тепер явно відповідають програмі, перебудовує криві програм, оновлює позначки аномалій і стан циклів та видаляє відлагоджувальні трасування, якщо «Зберегти відлагоджувальні трасування» вимкнено. Цикли та їхні записи потужності завжди зберігаються.", "label": "Автоматичне обслуговування (нічне очищення)" }, "completion_min_seconds": { - "doc": "Цикли, коротші за цей, відкидаються як цикли-привиди (тестові запуски, відкриття дверей, щоб додати носок).", + "doc": "Цикли, коротші за це (пробні запуски, відчинили дверцята, щоб докинути шкарпетку), зберігаються як перервані й не отримують сповіщення про завершення.", "label": "Мін. тривалість циклу" }, "delay_confirm_seconds": { - "doc": "Потужність має залишатися в режимі очікування протягом цього часу, перш ніж пристрій розглядатиметься як очікування запуску, а не як запущений.", + "doc": "Потужність має стільки часу лишатися в смузі очікування, перш ніж прилад вважатиметься таким, що чекає старту.", "label": "Секунди підтвердження" }, "delay_start_detect_enabled": { - "doc": "Визначайте, коли прилад увімкнено та очікує (відкладений запуск/режим очікування), але ще не розпочав свій цикл, тому режим очікування не сприймається як робочий цикл.", + "doc": "Виявляти прилад, який увімкнено і який чекає відкладеного старту, щоб його споживання в очікуванні не сприймалося як робота циклу.", "label": "Увімкнути виявлення відкладеного старту" }, "delay_timeout_hours": { - "doc": "Припиніть очікування в режимі відкладеного запуску через стільки годин і поверніться до режиму очікування, щоб машина, залишена під напругою, але ніколи не запускалася, не чекала вічно.", + "doc": "Через стільки годин перестати чекати відкладеного старту й повернутися до бездіяльності.", "label": "Години тайм-ауту" }, "device_type": { - "doc": "Клас приладу. Встановлює розумні параметри виявлення за замовчуванням (порогові значення, затримка вимкнення, обробка завершення), налаштовані для цього типу пристрою; змінюйте його, лише якщо пристрій спочатку було налаштовано як неправильний тип.", + "doc": "Задає типові параметри виявлення (пороги, затримка вимкнення, обробка завершення) для цього типу приладу. Змінюйте, лише якщо пристрій налаштовано з хибним типом.", "label": "Тип пристрою" }, "door_sensor_entity": { - "doc": "Додатковий бінарний датчик дверей. Використовується для визначення того, коли прилад було відкрито/вивантажено після циклу.", + "doc": "Необов'язковий датчик дверей. Показує, коли прилад відчинили для вивантаження після циклу.", "label": "Сутність датчика дверей" }, "duration_tolerance": { - "doc": "Толерантність до оцінок часу, що залишився (зворотний зв’язок навчання, не збіг). Якщо фактична тривалість знаходиться в межах +/-X% від оцінки, це вважається хорошим збігом.", + "doc": "Лише для зворотного зв'язку навчання: цикл у межах +/-X% від оцінки тривалості вважається доброю оцінкою. Не впливає на зіставлення.", "label": "Допуск оцінки" }, "enable_ml_models": { - "doc": "Під час виконання циклу дозвольте моделям уточнювати результати в реальному часі: більш стабільну оцінку залишкового часу та енергії/вартості, а також захист від передчасної зупинки при виявленні кінця (він може лише відкладати фініш, ніколи не завершувати його раніше та є обмеженим). Використовує ваші точно налаштовані моделі, якщо вони доступні, інакше – вбудовані. Вимкнено = лише класична логіка на основі живлення (все ще надійна).", + "doc": "Використовувати модель, навчену на цій машині, для прогнозу енергії та вартості поточного циклу і дозволяти добре знайомій програмі, що зупинилася незвично рано, завершуватися без повної затримки вимкнення. Вимкнено: лише логіка за потужністю.", "label": "Застосовувати розумні моделі під час циклу" }, "end_energy_threshold": { - "doc": "Під час зворотного відліку затримки вимкнення накопичена енергія (ват х час) порівнюється з цим порогом. У разі перевищення, зворотний відлік скидається, зберігаючи валики проти зминання та хвости сушіння в посудомийній машині, приєднані до циклу, замість того, щоб скорочувати їх. Підніміть його, якщо цикли закінчуються занадто рано під час охолодження; зменште його, якщо виявлення відбувається повільно.", + "doc": "Енергія, допустима під час затримки вимкнення. Якщо більше, цикл триває, і обертання та фінальне сушіння лишаються його частиною. Зменште, якщо цикли закінчуються на хвості; збільште, якщо завершені цикли й далі чекають.", "label": "Енергія завершення" }, - "end_repeat_count": { - "doc": "Кількість послідовних вимірювань нижче порогового значення, необхідних до завершення циклу. 1 підходить для більшості розеток. Збільште до 2-3, якщо ваша розумна розетка час від часу повідомляє про хибний нуль у середині циклу, і ваші цикли закінчуються передчасно.", - "label": "Кількість повторів завершення" - }, "energy_price_entity": { - "doc": "Сенсор із поточною ціною електроенергії за кВт·год (наприклад, динамічний тариф). Це має бути ціна, а не власна сутність потужності чи енергії розетки: вона має пріоритет над фіксованою ціною нижче, тож вказаний тут лічильник кВт·год рахує кожен цикл за показанням лічильника замість вашого тарифу. Коли увімкнено «Вартість, зважена за часом», кожен цикл обчислюється за ціною, що діяла в кожну мить його роботи; інакше за ним закріплюється ціна, що діяла на момент завершення.", + "doc": "Датчик поточної ціни за кВт·год (наприклад, динамічний тариф). Має пріоритет над фіксованою ціною нижче, тому це має бути ціна, а не датчик потужності чи енергії розетки.", "label": "Сутність ціни на енергію" }, "energy_price_static": { - "doc": "Фіксована ціна за кВт-год, яка використовується для показників витрат, якщо вище не встановлено реальну ціну.", + "doc": "Фіксована ціна за кВт·год, використовується, якщо сутність ціни не задано.", "label": "Фіксована ціна на енергію (за kWh)" }, "energy_sensor": { - "doc": "Необов’язковий накопичувальний лічильник енергії (total_increasing kWh/Wh, наприклад, власний лічильник загального споживання розетки). Якщо задано, енергія, що відображається для кожного циклу, береться з різниці показань цього лічильника між початком і кінцем, що дозволяє уникнути заниження, яке виникає при інтегруванні датчика потужності, що повільно оновлюється. Якщо показання відсутнє, його одиниця невідома або різниця не додатна, використовується інтегроване значення. Залиште порожнім, щоб продовжувати інтегрувати датчик потужності.", + "doc": "Необов'язковий лічильник кВт·год або Вт·год (total_increasing, наприклад власний лічильник розетки). Енергія циклу тоді береться з нього, що точніше за розрахунок за повільним датчиком потужності. Якщо його показник непридатний, використовується розраховане значення.", "label": "Сутність лічильника енергії" }, "expose_debug_entities": { - "doc": "Опублікуйте додаткові діагностичні об’єкти високої доступності (достовірність відповідності, неоднозначність, внутрішні стани). Якщо вимкнено, список об’єктів залишається чистим для звичайного використання.", + "doc": "Додати діагностичні сутності (впевненість зіставлення, неоднозначність, внутрішній стан). Вимкнено: список сутностей лишається чистим.", "label": "Показати відлагоджувальні сутності" }, "external_end_trigger": { - "doc": "Бінарний датчик, зміна стану якого позначає кінець циклу (наприклад, «завершений» контакт приладу або супутня інтеграція).", + "doc": "Бінарний датчик, зміна якого позначає кінець циклу (наприклад, контакт «готово» або інша інтеграція).", "label": "Сутність зовнішнього тригера" }, "external_end_trigger_enabled": { - "doc": "Нехай зовнішній бінарний датчик сигналізує про кінець циклу на додаток до вбудованого детектування на основі потужності.", + "doc": "Дозволити бінарному датчику повідомляти про кінець циклу на додачу до виявлення за потужністю.", "label": "Увімкнути зовнішній тригер завершення" }, "external_end_trigger_inverted": { - "doc": "Розглядайте вимкнення тригерного датчика (а не увімкнення) як сигнал кінця циклу.", + "doc": "Вважати сигналом кінця ВИМКНЕННЯ датчика (а не ввімкнення).", "label": "Інвертувати зовнішній тригер (спрацювання при ВИМКНЕННІ)" }, "learning_confidence": { - "doc": "Якщо показник відповідності перебуває між цим значенням і Впевненістю автоматичного маркування, сповіщення зворотного зв’язку попросить вас підтвердити розпізнану програму. Нижче цього показника відповідність надто непевна, щоб її показувати. Має залишатися нижчою за Впевненість автоматичного маркування.", + "doc": "Найнижча оцінка збігу для маркування завершеного циклу. Явного переможця маркують; спірний випадок потрапляє до черги перевірки у вкладці «Цикли». Нижче цього значення нічого не маркується і не запитується. Тримайте нижче Впевненості автоматичного маркування.", "label": "Впевненість навчання" }, "linked_device": { - "doc": "Необов’язково вставте цей пристрій WashData в інший пристрій (наприклад, розумний штекер) у реєстрі пристроїв HA, який відображається як «Підключено через ...».", + "doc": "Необов'язково. Показувати цей пристрій WashData під іншим пристроєм (наприклад, розумною розеткою) у Home Assistant як «Під'єднано через ...».", "label": "Згрупувати під пристроєм" }, "min_off_gap": { - "doc": "Якщо машина вимикається менше, ніж на цей час, послідовність увімкнення/вимкнення/увімкнення розглядається як один безперервний цикл. Запобігає розділенню програм замочування (машина вимикається на кілька хвилин під час прання) на два окремі цикли. Встановіть його меншим, ніж проміжок між вашими послідовними навантаженнями, якщо ви хочете, щоб вони рахувалися як окремі цикли. Стандартні параметри типу пристрою захищають типову паузу всередині циклу для кожного пристрою.", + "doc": "Вимкнення, коротше за це, не ділить цикл, тому паузи замочування лишаються в одному пранні. Тримайте меншим за паузу між завантаженнями поспіль, інакше вони зіллються в один цикл.", "label": "Мін. пауза вимкнення" }, "min_power": { - "doc": "Абсолютна мінімальна потужність, яка вважається активною. Показання, нижчі за це, розглядаються як 0 Вт (режим очікування), фільтруючи фантомне навантаження розумних розеток і світлодіодів режиму очікування.", + "doc": "Показники нижче цього значення вважаються 0 Вт (очікування). Відсікає холосте споживання розумних розеток та індикаторів очікування.", "label": "Мінімальна потужність" }, "ml_training_enabled": { - "doc": "Періодично протягом ночі вивчайте переглянуті цикли та налаштовуйте моделі на цю конкретну машину. Зміна зберігається лише тоді, коли вона справді має кращі результати під час затриманих циклів, тому це може лише допомогти або залишитися незмінним – ніколи не регресувати.", + "doc": "Час від часу перенавчати модель енергії на завершених циклах цієї машини. Нова модель лишається, лише якщо на тестових циклах вона краща за просту оцінку.", "label": "Навчатися від цієї машини" }, "ml_training_hour": { - "doc": "Місцева година доби (0-23) для точного налаштування протягом ночі. Виберіть тиху годину, наприклад 2 (02:00).", + "doc": "Година доби (0-23) для нічного навчання. Виберіть тиху годину, наприклад 2.", "label": "Навчатися о годині" }, "ml_training_interval_days": { - "doc": "Повторно перевіряйте наявність покращень щонайбільше один раз за цю кількість днів.", + "doc": "Перенавчати не частіше одного разу за цю кількість днів.", "label": "Перевіряти не частіше ніж кожні" }, "ml_training_min_cycles": { - "doc": "Зачекайте, доки буде записано принаймні стільки циклів, перш ніж виконувати точне налаштування, щоб було достатньо чого вчитися.", + "doc": "Перед навчанням дочекатися щонайменше такої кількості записаних циклів.", "label": "Спочатку потрібні цикли" }, "name": { - "doc": "Відображуване ім’я, яке відображається в списку інтеграцій HA та реєстрі пристроїв.", + "doc": "Назва цього пристрою в Home Assistant.", "label": "Назва пристрою" }, "no_update_active_timeout": { - "doc": "Якщо протягом такого тривалого часу під час роботи не надходить оновлення живлення, припустіть, що штекер вимкнено, і примусово зупиніть його, щоб уникнути циклу зомбі. За замовчуванням 600 с враховують затримку хмари або сітки.", + "doc": "Якщо за цей час під час циклу не надійшло жодного оновлення потужності, вважається, що розетка відключилася, і цикл примусово зупиняється. Типові 600 с враховують затримки хмари або mesh-мережі.", "label": "Тайм-аут без оновлення" }, "notify_before_end_minutes": { @@ -1346,35 +1347,35 @@ "label": "Попередження перед завершенням" }, "notify_channel": { - "doc": "Назва каналу сповіщень Android для початкових/живих повідомлень (керує звуком для кожного каналу та пріоритетом у мобільному додатку). Blank використовує супутню програму за замовчуванням.", + "doc": "Канал Android для повідомлень про старт і в реальному часі; канал задає звук і пріоритет. Порожньо: типовий канал застосунку.", "label": "Канал Android (початок/пряма трансляція)" }, "notify_cycle_timers": { - "doc": "Сповіщення в певні хвилини циклу (наприклад, щоб додати пом’якшувач). Повідомлення підтримує {device}, {program}, {minutes}. Увімкніть автоматичну паузу, щоб зробити паузу на цьому місці та отримати інтерактивне сповіщення з кнопкою «Відновити»; відновити за допомогою панелі, служби призупинення/відновлення або дії сповіщення.", + "doc": "Сповіщення на заданих хвилинах циклу (наприклад, щоб додати кондиціонер). Повідомлення підтримує {device}, {program}, {minutes}. З автопаузою цикл там само стає на паузу, а сповіщення отримує кнопку «Продовжити».", "label": "Таймери циклу" }, "notify_finish_channel": { - "doc": "Назва каналу сповіщень Android для повідомлення про завершення. Бланк повторно використовує початковий/прямий канал.", + "doc": "Канал Android для повідомлення про завершення. Порожньо: використовується канал старту й реального часу.", "label": "Канал Android (завершення)" }, "notify_finish_message": { - "doc": "Тіло повідомлення, що надсилається після завершення циклу. Змінні шаблону: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}. Тут найкорисніші {time_finished} та {vs_typical}.", + "doc": "Текст, що надсилається після завершення циклу. Змінні: {device}, {program}, {duration} (хвилини), {duration_hm} (1 h 05 min), {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}, {status} (completed або force_stopped; перервані цикли нічого не надсилають). Для будь-якої іншої змінної використовується текст за замовчуванням.", "label": "Повідомлення про завершення" }, "notify_finish_services": { - "doc": "notify.* служби викликаються після завершення циклу. Додайте по одному на ціль; залиште порожнім, щоб не сповіщати про закінчення.", + "doc": "Служби notify.*, що викликаються після завершення циклу. Залиште порожнім, щоб не надсилати.", "label": "Служби завершення" }, "notify_fire_events": { - "doc": "Також запускайте події ha_washdata_* на початку/закінченні циклу, щоб ви могли створити власну автоматизацію.", + "doc": "Також генерувати події ha_washdata_* на старті й завершенні циклу для власних автоматизацій.", "label": "Запускати події HA для сповіщень" }, "notify_icon": { - "doc": "Невелика піктограма, що показується у сповіщенні (наприклад, mdi:washing-machine). Android малює її в рядку стану; iOS показує її замість піктограми застосунку, і для цього потрібен супутній застосунок 2026.8 або новіший. Залиште порожнім, щоб використовувати піктограму за замовчуванням для платформи.", + "doc": "Маленька іконка в сповіщенні (наприклад, mdi:washing-machine). Надсилається лише на цілі mobile_app, не до груп notify. В iOS замінює іконку застосунку й потребує мобільного застосунку 2026.8 або новішого. Порожньо: типова іконка платформи.", "label": "Іконка сповіщення" }, "notify_live_chronometer": { - "doc": "Показувати в сповіщенні оновлюваний таймер зворотного відліку (на платформах, які його підтримують) замість статичного прогнозу.", + "doc": "Показувати у сповіщенні живий зворотний відлік (де підтримується) замість фіксованої оцінки.", "label": "Використовувати живий хронометр" }, "notify_live_interval_seconds": { @@ -1382,139 +1383,131 @@ "label": "Інтервал прямих оновлень" }, "notify_live_overrun_percent": { - "doc": "Якщо цикл перевищить оцінку більш ніж на цей відсоток, надішліть сповіщення про перевищення.", + "doc": "Живі оновлення припиняються, коли цикл перевищує очікувану тривалість на цей відсоток (20 = до 1,2x очікуваного часу).", "label": "% перевищення часу перед сповіщенням" }, "notify_live_services": { - "doc": "notify.* служби, що викликали поточні оновлення прогресу під час виконання циклу. Залиште поле порожнім, щоб вимкнути сповіщення про хід виконання.", + "doc": "Служби notify.* для оновлень прогресу в реальному часі під час циклу. Залиште порожнім, щоб вимкнути їх.", "label": "Служби прямого прогресу" }, "notify_only_when_home": { - "doc": "Надсилайте сповіщення лише тоді, коли принаймні один із пов’язаних людей (вище) перебуває вдома.", + "doc": "Сповіщати лише тоді, коли хоча б одна з людей вище вдома.", "label": "Сповіщати лише вдома" }, "notify_people": { - "doc": "person.*, які використовуються функцією «Повідомляти лише коли вдома», щоб визначити, чи є хтось удома.", + "doc": "Сутності person.*, які перевіряє «Сповіщати лише вдома».", "label": "Люди (для «лише вдома»)" }, "notify_pre_complete_message": { - "doc": "Тіло сповіщення перед завершенням / майже готово. Змінні шаблону: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Текст кожного живого оновлення прогресу. Змінні шаблону: {device}, {program}, {minutes} (залишилось хвилин). Для будь-якої іншої змінної використовується текст за замовчуванням.", "label": "Повідомлення перед завершенням" }, "notify_reminder_message": { - "doc": "Тіло нагадування про вивантаження, що досі очікує. Порожнє поле використовує вбудоване значення за замовчуванням. Змінні шаблону: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Текст одноразового Попередження перед завершенням. Змінні шаблону: {device}, {program}, {minutes} (налаштування Попередження перед завершенням). Для будь-якої іншої змінної використовується текст за замовчуванням.", "label": "Повідомлення-нагадування" }, "notify_start_message": { - "doc": "Тіло повідомлення, що надсилається на початку циклу. Змінні шаблону: {device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}.", + "doc": "Текст, що надсилається на початку циклу. Змінні шаблону: {device}, {program}. Для будь-якої іншої змінної використовується текст за замовчуванням.", "label": "Повідомлення про початок" }, "notify_start_services": { - "doc": "notify.* служби викликаються, коли починається цикл. Додайте по одному на ціль (телефон, інформаційну панель тощо); залиште порожнім, щоб не сповіщати про початок.", + "doc": "Служби notify.*, що викликаються на старті циклу (телефон, дашборд, ...). Залиште порожнім, щоб не надсилати.", "label": "Служби початку" }, "notify_timeout_seconds": { - "doc": "Автоматично відхилити сповіщення через стільки секунд (на платформах, які це підтримують). 0 зберігає його, доки не буде звільнено вручну.", + "doc": "Закрити сповіщення через стільки секунд (де підтримується). 0 лишає його, доки ви його не закриєте.", "label": "Автоматично закрити через" }, "notify_title": { - "doc": "Назва сповіщення. Змінні шаблону: {device}, {program}, {duration}, {energy}, {cost}, {date}, {time}, {minutes}.", + "doc": "Заголовок сповіщення. Підставляється лише {device}; для будь-якої іншої змінної використовується заголовок за замовчуванням.", "label": "Заголовок сповіщення" }, "notify_unload_delay_minutes": { - "doc": "Через кілька хвилин після завершення циклу перед надсиланням нагадування «розвантажити машину», що все ще очікує. Встановіть 0, щоб вимкнути нагадування.", + "doc": "Хвилини після завершення циклу до нагадування «вивантажте машину». 0 вимикає його.", "label": "Затримка нагадування про вивантаження" }, "notify_unload_repeat": { - "doc": "Продовжувати надсилати нагадування про вивантаження кожні «Затримка нагадування про вивантаження» хвилин, доки вивантаження не буде підтверджено або доки ви не натиснете «Не нагадувати» у сповіщенні. Потрібен спосіб підтвердити вивантаження: Сутність датчика дверей, Сутність підтвердження вивантаження або параметр «Підтверджувати вивантаження вручну» нижче. Кнопка відхилення працює у сповіщеннях мобільного застосунку Home Assistant (companion).", + "doc": "Повторювати нагадування про вивантаження кожні «Затримка нагадування про вивантаження» хвилин, доки вивантаження не підтверджено або ви не натиснете «Не нагадувати» (мобільний застосунок). Потрібна Сутність датчика дверей, Сутність підтвердження вивантаження або «Підтверджувати вивантаження вручну».", "label": "Повторювати, доки не вивантажено" }, "off_delay": { - "doc": "Час очікування після падіння живлення, перш ніж оголосити цикл завершеним. Якщо живлення відновлюється протягом цього вікна, цикл продовжується безперебійно - це мости призупиняються між етапами прання. Посудомийні машини мають довгі фази сушіння (вимкнення на 20-60 хв), тому затримка вимкнення повинна перевищувати цю, щоб весь цикл миття+сушіння відбувався як один цикл.", + "doc": "Скільки потужність має лишатися низькою, перш ніж цикл завершиться. Якщо потужність повернеться за цей час, цикл триває. Посудомийні машини можуть простоювати 20-60 хв під час сушіння, тому задайте більше.", "label": "Затримка вимкнення" }, "pause_cuts_power": { - "doc": "Коли цикл призупинено, також вимкніть елемент Switch Entity нижче. Тільки для приладів, вилку яких можна безпечно відрізати в середині циклу.", + "doc": "Під час паузи циклу також вимикати Сутність перемикача нижче. Лише для приладів, які можуть безпечно втратити живлення посеред циклу.", "label": "Пауза також вимикає живлення (через перемикач)" }, "power_off_delay": { - "doc": "Як довго потужність має залишатися нижче «Поріг вимкнення живлення» після завершення циклу, перш ніж стан повернеться до «Вимкнено». Використовується лише коли «Поріг вимкнення живлення» більше 0. Перевіряється з фоновою періодичністю, тому фактична затримка округлюється вгору до наступного моменту завершення стану.", + "doc": "Скільки потужність після циклу має лишатися нижче Порога вимкнення живлення, перш ніж стан повернеться до «Вимкнено». Використовується, лише якщо цей поріг більший за 0. Перевіряється періодично, тому реальне очікування може бути трохи довшим.", "label": "Затримка вимкнення живлення" }, "power_off_threshold_w": { - "doc": "Необов'язкове визначення стану «Вимкнено» за потужністю. Якщо значення більше 0, то після завершення циклу, коли потужність залишається нижче цього рівня протягом часу «Затримка вимкнення живлення», машина вважається вимкненою, і стан повертається до «Вимкнено». Залиште 0, щоб вимкнути (типово). Задайте значення вище реального рівня вимкненого приладу, але нижче «Поріг зупинки» та нижче споживання машини в режимі очікування після завершення; якщо значення не нижче «Поріг зупинки», воно ігнорується. Коли параметр увімкнено, він замінює «Затримка скидання прогресу» для повернення до «Вимкнено», тож машина, яка завершила роботу, залишається у стані «Готово»/«Чистий», доки її справді не буде вимкнено.", + "doc": "Після циклу потужність нижче цього значення протягом Затримки вимкнення живлення переводить стан у «Вимкнено» замість Затримки скидання прогресу. Задайте вище споживання вимкненого приладу, але нижче очікування та Порога зупинки. 0 вимикає (типово).", "label": "Поріг вимкнення живлення" }, "power_sensor": { - "doc": "Об’єкт датчика, який повідомляє поточну потужність у ватах для цього приладу (наприклад, sensor.washer_power). Виявлення всіх циклів базується на цьому сигналі.", + "doc": "Датчик поточної потужності цього приладу у ватах (наприклад, sensor.washer_power). На ньому ґрунтується все виявлення.", "label": "Датчик потужності" }, - "profile_duration_tolerance": { - "doc": "Смуга +/- навколо середньої тривалості профілю, яка використовується під час зіставлення. 0,25 означає, що 60-хвилинний профіль відповідає 45-75-хвилинним циклам.", - "label": "Допуск тривалості профілю" - }, "profile_match_interval": { - "doc": "Як часто намагатися зіставити профіль протягом робочого циклу. За замовчуванням 300 с (5 хвилин) балансує швидкість виявлення та ЦП.", + "doc": "Як часто зіставляти поточний цикл із профілями. Типово 300 с (5 хв).", "label": "Інтервал зіставлення" }, "profile_match_max_duration_ratio": { - "doc": "Максимальна довжина циклу відносно профілю. 1.3 означає, що для відповідності цикл має бути менше 130% тривалості профілю.", + "doc": "Програма відкидається, коли цикл триває довше за це кратне її звичайної тривалості. 1,8: цикл на 80% довший за програму більше з нею не зіставляється.", "label": "Макс. коефіцієнт тривалості" }, "profile_match_min_duration_ratio": { - "doc": "Мінімальна довжина циклу відносно профілю. 0,9 означає, що для відповідності циклу має бути щонайменше 90% тривалості профілю.", + "doc": "Програма розглядається лише після того, як цикл пройшов цю частку її звичайної тривалості. 0,10: 2-годинна програма стає кандидатом через 12 хвилин. Не діє в перші 15 хвилин циклу.", "label": "Мін. коефіцієнт тривалості" }, "profile_match_threshold": { - "doc": "Мінімальна оцінка подібності (0-1), необхідна в кінці циклу, щоб прийняти ідентифікацію програми. Підніміть його, щоб зменшити кількість помилкових ідентифікацій; зменште його, якщо програми вашої машини не відповідають. За умовчанням 0,4 є консервативною початковою точкою.", + "doc": "Оцінка збігу в реальному часі, потрібна циклу, перш ніж Розумне завершення або завершення захисту від зминання зможуть закінчити його раніше. Не змінює показану програму. Підвищте, якщо слабкі збіги закінчують цикли рано; знизьте, якщо явні збіги чекають затримку вимкнення.", "label": "Поріг зіставлення" }, "profile_unmatch_threshold": { - "doc": "Якщо відповідність у реальному часі в середині циклу опускається нижче цього показника, попередня ідентифікація скасовується. Тримайте його трохи нижче Порога зіставлення, щоб короткочасне зниження подібності не повернуло відображення до стану без відповідності.", + "doc": "Якщо оцінка збігу в реальному часі падає нижче цього значення, поточне припущення про програму скидається. Тримайте трохи нижче Порога зіставлення, щоб короткий провал його не скидав.", "label": "Поріг незіставлення" }, "progress_reset_delay": { - "doc": "Після завершення утримуйте прогрес на рівні 100% протягом цього часу, щоб завершено було видно на інформаційних панелях, перш ніж перейти до режиму очікування.", + "doc": "Після циклу тримати прогрес на 100% цей час, перш ніж скинути до «Бездіяльність».", "label": "Затримка скидання прогресу" }, "pump_stuck_duration": { - "doc": "Через кілька секунд насос може працювати безперервно, перш ніж його буде позначено як можливе зависання (спрацьовує подія stuck-pump).", + "doc": "Скільки насос може працювати без перерви, перш ніж спрацює подія застряглого насоса.", "label": "Тривалість застрявання насоса" }, "sampling_interval": { - "doc": "Очікуваний час між показаннями датчика – використовується для визначення розміру вікна згладжування та правильного початку усунення дребезгу. Кожне оновлення датчика реєструється незалежно від цього значення; він лише калібрує наступні обчислення. Механізм підказок вимірює фактичну частоту педалей вашого датчика з минулих циклів і встановлює її автоматично.", + "doc": "Мінімальний час між оброблюваними показниками. Показники, що надійшли раніше, пропускаються (низькі показники під час циклу зберігаються завжди). Задайте інтервал звітів вашої розетки або менше; більше значення лише відкидає показники.", "label": "Інтервал вибірки" }, "save_debug_traces": { - "doc": "Зберігайте повне трасування потужності та відповідні дані налагодження для кожного циклу. Корисно для усунення несправностей, але збільшує розмір пам’яті.", + "doc": "Також зберігати з кожним циклом відлагоджувальні дані зіставлення (оцінки всіх кандидатів) для пошуку проблем. Додає близько 20 КБ на цикл.", "label": "Зберегти відлагоджувальні трасування" }, - "smoothing_window": { - "doc": "Наскільки згладжується необроблений сигнал потужності. Низький (2) чуйний, але шумний; високий (5) згладжує стрибки, але додає затримку.", - "label": "Вікно згладжування" - }, "start_duration_threshold": { - "doc": "Потужність має залишатися вище початкового порогу стільки часу, щоб підтвердити справжній старт, запобігаючи запуску циклу перемикачами увімкнення/вимкнення частки секунди.", + "doc": "Потужність має стільки часу лишатися вище Порога початку, перш ніж почнеться цикл, щоб короткі ввімкнення й вимкнення ігнорувалися.", "label": "Тривалість початку" }, "start_energy_threshold": { - "doc": "Енергія (потужність х час), яку прилад має спожити перед ЗАПУСКОМ. Короткий сплеск високої потужності має дуже низьку енергію та ігнорується, запобігаючи хибним стартам.", + "doc": "Енергія, яку прилад має спожити, перш ніж цикл вважатиметься запущеним. Короткий стрибок потужності споживає мало енергії й цикл не запускає.", "label": "Енергія початку" }, "start_threshold_w": { - "doc": "Потужність має піднятися вище цього рівня, щоб підтвердити початок циклу. Встановлення занадто низького значення спричиняє помилкові старти від живлення в режимі очікування; занадто високі та повільні програми (холодне заповнення) пропускаються. Механізм підказок встановлює це трохи вище спостережуваної найнижчої активної потужності машини.", + "doc": "Потужність має піднятися вище цього значення, щоб почався цикл. Надто низько: споживання в очікуванні спричиняє хибні старти. Надто високо: програми з повільним стартом (холодний набір води) пропускаються.", "label": "Поріг початку" }, "stop_threshold_w": { - "doc": "Потужність має впасти нижче цього рівня, перш ніж розпочнеться відлік затримки вимкнення. Встановіть його нижче початкового порогу - проміжок між ними є смугою гістерезису, яка запобігає мерехтіння. Якщо встановлено занадто високе значення, фази низької потужності (затримки полоскання, захист від зминання) помилково запускають завершення послідовності.", + "doc": "Потужність має впасти нижче цього значення, перш ніж почнеться відлік затримки вимкнення. Тримайте нижче Порога початку. Якщо надто високо, фази малої потужності (паузи полоскання, захист від зминання) помилково запускають відлік завершення.", "label": "Поріг зупинки" }, "switch_entity": { - "doc": "Додатковий перемикач вимикається під час паузи та знову вмикається під час відновлення, використовується разом із «Пауза також вимикає живлення».", + "doc": "Необов'язковий перемикач, який вимикається під час паузи й знову вмикається при продовженні, разом із «Пауза також вимикає живлення».", "label": "Сутність перемикача" }, "watchdog_interval": { - "doc": "Як часто фоновий сторожовий таймер перевіряє наявність зупинених датчиків і час очікування, що минув. За замовчуванням 30 с.", + "doc": "Як часто фонова перевірка шукає завислі датчики та вичерпані тайм-аути. Типово 30 с.", "label": "Інтервал сторожового пса" }, "notify_milestone_message": { @@ -1522,135 +1515,90 @@ "label": "Повідомлення про віху" }, "notify_milestones": { - "doc": "Розділені комами кількості циклів, які під час досягнення запускають одноразове святкове сповіщення (напр. 50, 100, 500, 1000). Порожнє поле вимикає сповіщення про віхи.", + "doc": "Кількості циклів, на яких приходить разове святкове сповіщення, через кому (наприклад, 50, 100, 500, 1000). Порожньо: вимкнено.", "label": "Віхи циклів" }, "notify_quiet_end_hour": { - "doc": "Кінець вікна «не турбувати» (0-23). Відкладені сповіщення надсилаються о цій годині. Залиште порожнім, щоб вимкнути.", + "doc": "Кінець вікна «не турбувати» (0-23); відкладені сповіщення надсилаються в цей час. Порожньо: вимкнено.", "label": "Кінець тихих годин" }, "notify_quiet_start_hour": { - "doc": "Початок вікна «не турбувати» (0-23). Сповіщення про завершення, нагадування та про чисту білизну, які мали б спрацювати під час тихих годин, відкладаються й надсилаються після закінчення вікна. Залиште порожнім, щоб вимкнути. Підтримує вікна, що переходять опівніч (напр. початок 22, кінець 7).", + "doc": "Початок вікна «не турбувати» (0-23). Сповіщення про завершення, нагадування та сповіщення про чисту білизну в цьому вікні відкладаються до його кінця. Може переходити через північ (наприклад, з 22 до 7). Порожньо: вимкнено.", "label": "Початок тихих годин" }, "peak_rate_message": { - "doc": "Необов'язковий власний текст для підказки про пікову ціну, що додається до сповіщення про початок. Змінні шаблону: {device}, {price}. Порожнє поле використовує вбудоване значення за замовчуванням.", + "doc": "Власний текст підказки про піковий тариф. Змінні шаблону: {device}, {price}. Порожньо: типовий текст.", "label": "Повідомлення про пікову ціну" }, "peak_rate_threshold": { - "doc": "Коли цикл починається і поточна ціна за кВт·год дорівнює цьому значенню або перевищує його, до сповіщення про початок додається підказка про пікову ціну. 0 або порожнє поле вимикає підказку.", + "doc": "Якщо ціна за кВт·год на старті циклу дорівнює цьому значенню або вища, додати підказку про піковий тариф до сповіщення про старт. 0 або порожньо: вимкнено.", "label": "Поріг пікової ціни (за кВт·год)" }, - "dtw_bandwidth": { - "label": "Допуск зіставлення за формою", - "doc": "Наскільки сильно модуль зіставлення може розтягнути цикл у часі, щоб підігнати його під форму збереженої програми. Чим вище значення, тим більше допускається коливань швидкості; надто високе ризикує зіставити не ту програму." - }, - "corr_weight": { - "label": "Вага форми відносно рівня", - "doc": "Баланс між формою кривої та абсолютним рівнем потужності під час оцінювання зіставлення. Вище значення надає перевагу формі; нижче надає перевагу збігу рівня потужності." - }, - "duration_weight": { - "label": "Вага тривалості", - "doc": "Наскільки сильно перевага надається програмі, типова тривалість якої збігається з цим циклом." - }, - "energy_weight": { - "label": "Вага енергії", - "doc": "Наскільки сильно перевага надається програмі, типове споживання енергії якої збігається з цим циклом." - }, "store_brand": { "label": "Бренд пристрою", - "doc": "Необов'язково. Бренд пристрою, вибраний із каталогу спільноти. Використовується для пошуку та публікації відповідних еталонних записів. Залиште порожнім, якщо не користуєтеся онлайн-функціями." + "doc": "Необов'язково. Марка вашого приладу з каталогу спільноти, використовується для пошуку еталонних записів та обміну ними. Залиште порожнім, якщо не користуєтеся онлайн-функціями." }, "store_model": { "label": "Модель пристрою", - "doc": "Необов'язково. Модель пристрою, вибрана з каталогу спільноти після вказання бренду. Якщо вашої моделі немає у списку, ви можете додати її до каталогу." + "doc": "Необов'язково. Модель вашого приладу з каталогу спільноти (спершу виберіть марку). Якщо її немає в списку, можна додати." }, "show_contributor": { "doc": "Показати ім'я автора на профілях, завантажених з магазину спільноти" }, "smart_termination_duration_ratio": { - "doc": "Наскільки глибоко в очікувану тривалість зіставленої програми має зайти цикл, перш ніж Розумне завершення зможе завершити його достроково після падіння потужності. Очікувана тривалість - це середнє значення програми, тому на приладах із дуже мінливим часом роботи - пральні машини за холодної зимової та теплої літньої води на вході, сушильні машини з датчиком вологості, програми, що залежать від завантаження - близько половини всіх запусків завершуються коротше за це середнє й ніколи не отримують швидкого завершення, закінчуючись лише за резервним тайм-аутом із запізненням на кілька хвилин. Зменшіть це значення (напр., 0.85) на таких машинах, щоб дострокове завершення все ж спрацьовувало; підвищуйте ближче до 1.0 для більшої обережності. Залиште порожнім для значення за замовчуванням (0.98 або 0.99 для посудомийних машин). Воно може лише завершити цикл раніше, але ніколи пізніше, і ніколи не спрацьовує за неоднозначного або недостатньо впевненого зіставлення.", + "doc": "Частка середньої тривалості програми, яку цикл має пройти, перш ніж Розумне завершення зможе закінчити його раніше. Якщо час роботи коливається (температура води, сушіння за датчиком, розмір завантаження), зменште її (наприклад, 0,85). Порожньо: 0,98 (0,99 для посудомийних машин).", "label": "Коефіцієнт розумного завершення" }, - "enable_phase_matching": { - "label": "Залишок часу з урахуванням фаз", - "doc": "Розбиває кожен активний цикл на фази (нагрівання, прання, віджим) і розподіляє залишок часу за фазами, поєднуючи це з класичною оцінкою - спираючись на розподіл за фазами на початку циклу та на класичну оцінку ближче до кінця. Це персоналізує відлік під те, скільки насправді нагрівається та працює ваша машина, що найпомітніше в першій половині циклу. Вимкнено = лише класична оцінка. Впливає лише на відображення залишку часу; зіставлення програм і виявлення циклу залишаються незмінними." - }, - "keep_min_score": { - "label": "Мін. оцінка збігу", - "doc": "Мінімальна оцінка подібності, необхідна кандидату для участі у змаганні зі збігів. Значення за замовчуванням 0,1 є навмисно м'яким - допускає навіть слабких кандидатів і покладається на пізніші етапи для знаходження найкращого збігу. Збільшіть для ранішого відхилення малоймовірних профілів; зменшіть для розширення початкового пулу кандидатів." - }, - "dtw_blend": { - "label": "Змішування деформації", - "doc": "Наскільки оцінка вирівнювання DTW замінює базову оцінку Етапу 2. 0 = використовуй лише оцінку Етапу 2, 1 = використовуй лише оцінку DTW, 0,5 (за замовчуванням) = рівне змішування. Збільшіть для більшого покладання на вирівнювання деформацією, коли програми мають схожі рівні потужності, але різні часові шаблони." - }, - "dtw_ensemble_w": { - "label": "Мікс комбінації DTW", - "doc": "В режимі ансамблю DTW (за замовчуванням), вага масштабованого L1 проти похідного DTW (DDTW). 1,0 = лише масштабований L1, 0 = лише DDTW, 0,7 = за замовчуванням. Масштабований варіант L1 порівнює рівні потужності; похідний варіант реагує на зміни потужності в часі. Ансамбль поєднує обидва." - }, - "dtw_ddtw_scale": { - "label": "Масштаб похідної деформації", - "doc": "Відстань напівнасичення для оцінювання похідним DTW. Оцінка зменшується вдвічі, коли відстань DDTW рівна цьому значенню. Менша = більш чутлива до відмінностей форми. Значення за замовчуванням 30 відкалібровано для типових слідів потужності побутових приладів. Впливає лише на режими ensemble та ddtw DTW." - }, - "dtw_refine_top_n": { - "label": "Кількість уточнених кандидатів", - "doc": "Скільки провідних кандидатів Етапу 2 DTW переоцінює. DTW є дорожчим, тому застосовується лише до N найкращих кандидатів. За замовчуванням 5. Збільшіть (до 7-9), якщо правильний профіль іноді досягає лише 4-го або 5-го місця після Етапу 2 - DTW може його врятувати. Зменшення трохи прискорює зіставлення." - }, - "duration_scale": { - "label": "Гострота тривалості", - "doc": "Гострота штрафу за невідповідність тривалості на Етапі 4. Це лог-відношення, при якому оцінка збігу зменшується вдвічі. Менше = суворіше: невідповідність тривалості більше знижує оцінку. Значення за замовчуванням 0,175 відповідає допуску тривалості близько 18% при половинній вазі. Використовуйте разом із Вагою тривалості." - }, - "energy_scale": { - "label": "Гострота енергії", - "doc": "Гострота штрафу за невідповідність енергії на Етапі 4. Менше = суворіше: невідповідність енергії більше знижує оцінку. Значення за замовчуванням 0,25 є навмисно більш м'яким, ніж Гострота тривалості, оскільки енергія варіюється з навантаженням. Використовуйте разом із Вагою енергії." - }, "power_profile_interval_min": { "label": "Інтервал профілю потужності", - "doc": "Розмір сегмента для атрибута датчика power_profile по профілю (плоский масив середніх ват по слотах), що використовується зовнішніми планувальниками, такими як EMHASS та tibber_prices. Менші сегменти зберігають короткі сплески потужності гострими; більші сегменти згладжують форму. За замовчуванням 15 хв. Лише для читання; не впливає на виявлення." + "doc": "Розмір інтервалу атрибута power_profile (середні вати на слот) для планувальників на кшталт EMHASS і tibber_prices. Менший зберігає короткі піки; більший згладжує криву. Типово 15 хв. Не впливає на виявлення." }, "notify_live_silent": { "label": "Тихі живі оновлення", - "doc": "Лише для iOS. Оновлювати хід виконання тихо: кожне оновлення надходить без звуку та вібрації, зі зниженим пріоритетом для економії заряду, тож iOS може ненадовго групувати їх. Перше оновлення циклу (те, що запускає Live Activity), а також сповіщення про початок і завершення сповіщають як зазвичай. Вимкніть, щоб отримувати сигнал на кожне оновлення." + "doc": "Лише iOS. Оновлювати прогрес у реальному часі беззвучно, з пріоритетом, що заощаджує батарею і який iOS може групувати. Перше оновлення та сповіщення про старт і завершення й далі зі звуком. Вимкнено: звук при кожному оновленні." }, "notify_live_sticky": { "label": "Зберігати живе сповіщення при натисканні", - "doc": "Лише для Android. Зробіть сповіщення про хід виконання постійним (закріпленим), щоб натискання не закривало його. Вимкнено зберігає поведінку за замовчуванням, де натискання закриває сповіщення." + "doc": "Лише Android. Не прибирати сповіщення про прогрес після натискання. Вимкнено: натискання його закриває." }, "notify_live_click_action": { "label": "Ціль натискання сповіщення", - "doc": "Куди відкривається натискання на сповіщення від цього приладу, на Android та iOS. Порожнє поле відкриває панель WashData на цьому приладі. Укажіть шлях до інформаційної панелі (наприклад, /lovelace/laundry) або повний URL, щоб перейти в інше місце, або «none», щоб натискання нічого не відкривало." + "doc": "Що відкриває натискання на сповіщення цього приладу. Порожньо: панель WashData цього приладу. Введіть шлях дашборду (наприклад, /lovelace/laundry) або URL, або «none», щоб натискання нічого не відкривало." }, "door_opens_at_end": { "label": "Дверцята відкриваються автоматично наприкінці", - "doc": "Для посудомийних машин, які відкривають дверцята наприкінці циклу для сушіння (AirDry та подібні). При увімкненні відкриття дверцят під час активного циклу більше не призупиняє його назавжди; натомість, якщо дверцята залишаються відкритими протягом часу витримки нижче, WashData вважає цикл завершеним. Коротке відкриття (додавання посуду) ігнорується. Потрібен об'єкт датчика дверей." + "doc": "Для посудомийних машин, які наприкінці відчиняють дверцята для сушіння (наприклад, AirDry). Дверцята, відчинені протягом часу нижче, завершують цикл, а не ставлять його на паузу; коротке відчинення ігнорується. Потрібна Сутність датчика дверей." }, "door_end_dwell_seconds": { "label": "Витримка при відкритих дверцятах", - "doc": "Як довго дверцята повинні залишатися відкритими до завершення циклу, коли увімкнено \"Дверцята відкриваються автоматично наприкінці\". Достатньо довго, щоб ігнорувати швидке додавання посуду (за замовчуванням 60 с), достатньо коротко для швидкого завершення після відкриття дверцят машиною." + "doc": "Коли ввімкнено «Дверцята відкриваються автоматично наприкінці»: скільки дверцята мають лишатися відчиненими, перш ніж цикл завершиться. Досить довго, щоб не реагувати на додавання тарілки (типово 60 с)." }, "profile_evidence_sources": { "label": "Цикли, що формують програму", - "doc": "Які цикли беруть участь у побудові кривої потужності кожної програми та в зіставленні з нею завершеного циклу. Якщо зняти позначку з певного виду, він більше не формує ваші програми, але нічого не видаляється: цикли залишаються у списку Цикли, їх так само можна позначити або вилучити. Корисно, якщо ви не довіряєте імпортованим даним. На статистику це не впливає: у ній завжди враховуються лише цикли, які цей прилад справді виконав. Якщо зняти всі позначки, це буде проігноровано, адже програма без жодного циклу ніколи не могла б зіставитися." + "doc": "Які види циклів формують криву та зіставлення кожної програми. Зняття позначки нічого не видаляє; корисно, якщо ви не довіряєте імпортованим даним. Статистика завжди рахує лише цикли цієї машини. Зняття всіх позначок ігнорується." }, "energy_price_dynamic": { "label": "Вартість, зважена за часом", - "doc": "Обчислює вартість кожного циклу за ціною, що діяла в кожну мить його роботи, а не за єдиною ціною на момент завершення. Працює лише із сутністю ціни (фіксована ціна змінюватися не може). Додаткового налаштування не потрібно: поки цикл триває, ціна відстежується наживо, а Обробка історії в Діагностиці перераховує вартість давніших циклів за даними recorder. Вимкнено = класична поведінка, одна ціна, зафіксована наприкінці." + "doc": "Рахувати вартість кожного циклу за ціною в кожен момент його роботи, а не за ціною на момент завершення. Потрібна сутність ціни. «Обробка історії» (Діагностика) перераховує старі цикли." }, "curve_preroll_seconds": { - "doc": "Скільки секунд показань від перерваних спроб запуску WashData може перенести на початок кривої циклу. Крива циклу зазвичай починається зі спроби запуску, яка врешті вдалася, тому на машині, яка робить кілька проб, перш ніж вийти на режим - вибір програми, блокування дверцят, перший набір води, - у кожній кривій бракує перших хвилин справжньої роботи. Задайте значення трохи більше за найдовший такий розгін вашої машини (300 с із запасом вистачає більшості). 0 вимикає цю функцію. Зважте, що її увімкнення зсуває записаний початок раніше, тому цикли, записані до зміни та після неї, мають різну тривалість для однієї й тієї самої програми; вивчені середні будуть зміщуватися, доки старіші цикли не вийдуть з історії.", + "doc": "Додає на початок циклу показники невдалих спроб запуску, для машин, які пробують перед стартом (блокування дверцят, перший набір води). 300 с вистачає більшості; 0 = вимкнено. Записаний початок зсувається раніше, тому вивчені тривалості якийсь час змінюються.", "label": "Передзапис кривої" }, "unload_confirm_entity": { "label": "Сутність підтвердження вивантаження", - "doc": "Необов'язкова сутність, спрацювання якої означає, що вміст вийнято; для машин, на які не можна встановити датчик дверей. Підійде будь-яка: кнопка Zigbee або NFC, помічник input_button, датчик руху перед машиною, сцена. Підтвердженням вважається будь-яка зміна стану, окрім вимкнення та переходу в стан «Недоступно». Встановлення цієї сутності також вмикає нагадування про вивантаження на пристрої без Сутності датчика дверей." + "doc": "Необов'язково. Будь-яка сутність, що підтверджує, що завантаження вийняли, для машин без датчика дверей (кнопка, input_button, датчик руху, сцена). Рахується будь-яка зміна стану, крім вимкнення й недоступності. Також вмикає нагадування про вивантаження." }, "unload_track_without_door": { "label": "Підтверджувати вивантаження вручну", - "doc": "Використовувати нагадування про вивантаження на пристрої без Сутності датчика дверей і без Сутності підтвердження вивантаження, підтверджуючи вивантаження власною автоматизацією: натисніть сутність-кнопку «Позначити як вивантажене» або викличте службу ha_washdata.mark_unloaded. Ігнорується, якщо задано Сутність датчика дверей, яка вже дає такий сигнал." + "doc": "Вмикати нагадування про вивантаження без датчика дверей і сутності підтвердження. Підтверджуйте вивантаження з власної автоматизації кнопкою «Позначити як вивантажене» або службою ha_washdata.mark_unloaded. Ігнорується, якщо задано Сутність датчика дверей." }, "notify_icon_color": { "label": "Колір сповіщення", - "doc": "Акцентний колір сповіщень цього приладу, щоб пральну, сушильну та посудомийну машину можна було розрізнити з першого погляду. iOS забарвлює піктограму та смужку перебігу Live Activity на екрані блокування і в Dynamic Island. Android отримує той самий колір, але що з ним робити, вирішує сам телефон: деякі забарвлюють піктограму, а багато яких (особливо Samsung One UI) повністю його ігнорують. Обидві платформи розуміють Hex (наприклад, #03A9F4). Залиште порожнім, щоб використовувати колір за замовчуванням для платформи." + "doc": "Колір акценту сповіщень цього приладу, щоб легко відрізняти пральну, сушильну й посудомийну машини. В iOS заповнює коло за іконкою та фарбує смугу Live Activity. Багато телефонів Android (особливо Samsung) його ігнорують. Використовуйте hex, наприклад #03A9F4. Порожньо: типовий колір платформи." + }, + "share_name": { + "doc": "Показувати назву вашого облікового запису як автора циклів, якими ви ділитеся. Вимкнено: без імені." } }, "setting_group": { @@ -1678,9 +1626,6 @@ "door_pause": { "label": "Двері & Пауза" }, - "dtw_refinement": { - "label": "Уточнення DTW" - }, "duration_gates": { "label": "Ворота тривалості" }, @@ -1757,13 +1702,18 @@ "rebuilding": "Відновлення…", "analyzing": "Аналіз…", "resetting": "Скидання…", - "reverting": "Відкат…", "trimming": "Обрізання…", "splitting": "Розділення…", + "stalled": "Застряг", "deleting": "Видалення…", "imported": "Імпортовано", "preparing": "Підготовка…", - "unlabelling": "Зняття мітки…" + "unlabelling": "Зняття мітки…", + "working": "Виконується…", + "match_uncertain": "Невпевнено: {top} або {runner}", + "match_uncertain_single": "Невпевнено: можливо, {top}", + "match_sure": "впевненість ~{pct}%", + "match_uncertain_tip": "WashData ще не вибрав програму. Число показує, як часто провідне припущення, {top}, виявлялося правильним у минулих циклах за такого відриву. Лише для відома." }, "tab": { "advanced": "Розширені", @@ -1794,13 +1744,17 @@ }, "toast": { "catalog_refreshed": "Каталог спільноти оновлено", + "task_name_required": "Вкажіть назву завдання", + "task_added": "Завдання додано", + "task_save_failed": "Не вдалося зберегти завдання: {error}", + "task_removed": "Завдання видалено", + "maint_logged": "Позначено виконаним: {task}", "access_saved": "Контроль доступу збережено", "all_wiped": "Усі дані видалено", "analysis_complete_none": "Аналіз завершено: нових пропозицій немає", "auto_label_complete": "Автопозначення завершено", "automation_deleted": "Автоматизацію видалено", "correction_submitted": "Виправлення надіслано", - "cycle_deleted": "Цикл видалено", "cycle_force_stopped": "Цикл примусово зупинено", "cycle_labelled": "Цикл позначено", "cycle_paused": "Цикл призупинено", @@ -1816,11 +1770,9 @@ "group_deleted": "Групу видалено", "group_name_required": "Назва групи обов'язкова", "group_saved": "Групу збережено", - "import_successful": "Імпорт успішний; інтеграція перезавантажується", "json_required": "Дані JSON обов'язкові", "legacy_removed": "Застарілі дії видалено", "logs_exported": "Журнали експортовано", - "matching_reverted": "Ваги збігів скинуто до стандартних", "min_2_profiles": "Виберіть принаймні 2 профілі для групи", "ml_training_no_improvement": "Навчання завершено: базовий рівень збережено (без покращень)", "models_reverted": "Моделі пристрою скинуто до базового рівня", @@ -1830,8 +1782,8 @@ "pg_preset_loaded": "Пресет «{name}» завантажено", "pg_preset_saved": "Пресет «{name}» збережено", "pg_sugg_loaded": "Завантажено {n} рекомендованих значень - запустіть пісочницю для порівняння", - "pg_sugg_ml_loaded": "Завантажено {n} значень, відкаліброваних ML - запустіть пісочницю для порівняння", "phase_name_required": "Назва фази обов'язкова", + "phase_reset": "Фазу «{name}» повернуто до вбудованої версії", "phase_updated": "Фазу оновлено", "phases_saved": "Фази збережено", "preferences_saved": "Налаштування збережено", @@ -1878,15 +1830,11 @@ "store_name_required": "Введіть назву профілю", "store_import_failed": "Не вдалося імпортувати: {error}", "store_imported": "Імпортовано в {profile}", - "store_program_required": "Введіть назву програми", "store_no_appliance": "Спочатку вкажіть бренд і модель пристрою в налаштуваннях.", "store_share_failed": "Не вдалося поділитися: {error}", "store_shared": "Опубліковано в магазині спільноти, очікує перевірки.", "contrib_device_changed": "Додано до магазину спільноти, але вибраний пристрій змінився, поки форма була відкрита. Виберіть його в налаштуваннях.", "appliance_added": "Пристрій додано, очікує схвалення", - "thanks_confirming": "Дякуємо за підтвердження", - "device_approved": "Схвалено спільнотою", - "rating_saved": "Оцінку якості збережено", "brand_added": "Бренд додано, очікує схвалення", "profile_added": "Профіль додано, очікує схвалення", "saved_except_conflicts": "Збережено. Виправте виділені конфлікти, щоб зберегти решту.", @@ -1909,11 +1857,54 @@ "odometer_saved": "Загальну кількість циклів збережено", "odometer_save_failed": "Не вдалося зберегти загальну кількість: {error}", "store_pick_profile_share": "Виберіть профіль для публікації", - "remove_failed": "Не вдалося видалити: {error}" + "remove_failed": "Не вдалося видалити: {error}", + "analysis_complete": "Аналіз завершено: пропозицій: {count}", + "analysis_failed": "Не вдалося виконати аналіз: {error}", + "auto_detect_failed": "Не вдалося виконати автовизначення: {error}", + "auto_label_failed": "Не вдалося автоматично позначити: {error}", + "could_not_load_cycle": "Не вдалося завантажити цикл: {error}", + "create_failed": "Не вдалося створити: {error}", + "cycles_deleted": "Видалено циклів: {count}", + "debug_cleared": "Очищено трасувань налагодження: {count}", + "discard_failed": "Не вдалося відхилити: {error}", + "error": "Помилка: {error}", + "export_failed": "Не вдалося експортувати: {error}", + "import_failed": "Не вдалося імпортувати: {error}", + "invalid_json": "«{key}» не є коректним JSON - виправте його або очистьте поле перед збереженням.", + "label_failed": "Не вдалося позначити: {error}", + "ml_training_promoted": "Навчання завершено: активовано моделей: {count}", + "pause_failed": "Не вдалося призупинити: {error}", + "phase_created": "Фазу «{name}» створено", + "phase_deleted": "Фазу «{name}» видалено", + "profile_created": "Профіль «{name}» створено", + "rename_failed": "Не вдалося перейменувати: {error}", + "save_failed": "Не вдалося зберегти: {error}", + "split_complete": "Розділено на цикли: {count}", + "start_failed": "Не вдалося запустити: {error}", + "stop_failed": "Не вдалося зупинити: {error}", + "update_failed": "Не вдалося оновити: {error}", + "store_download_skipped": "Пропущено записів: {n} (надто короткі, із надто великими пропусками або вже є на вашому пристрої).", + "store_import_low_quality": "Цей запис надто короткий, має надто великі пропуски або неправдоподібний, тому його не можна імпортувати.", + "store_import_duplicate": "Цей запис уже є на вашому пристрої.", + "device_approved": "Схвалено спільнотою", + "thanks_confirming": "Дякуємо за підтвердження", + "rating_saved": "Оцінку якості збережено", + "store_download_partial": "Не завантажено програм: {n}; спробуйте пізніше.", + "import_no_restore_point": "Імпорт виконано, але точку відновлення зберегти не вдалося, тому цей імпорт не можна скасувати", + "import_undo_failed": "Не вдалося скасувати імпорт: {error}", + "import_undone": "Імпорт скасовано: відновлено профілів: {profiles}, циклів: {cycles}", + "auto_label_cancelled": "Автоматичне маркування скасовано після позначення {count} циклів.", + "auto_label_cancelled_one": "Автоматичне маркування скасовано після позначення {count} циклу.", + "auto_label_cancelled_few": "Автоматичне маркування скасовано після позначення {count} циклів.", + "auto_label_cancelled_many": "Автоматичне маркування скасовано після позначення {count} циклів.", + "auto_label_cancelled_other": "Автоматичне маркування скасовано після позначення {count} циклу.", + "auto_label_done": "Автоматичне маркування завершено: позначено {count} циклів, без позначки залишилося: {skipped}.", + "auto_label_done_one": "Автоматичне маркування завершено: позначено {count} цикл, без позначки залишилося: {skipped}.", + "auto_label_done_few": "Автоматичне маркування завершено: позначено {count} цикли, без позначки залишилося: {skipped}.", + "auto_label_done_many": "Автоматичне маркування завершено: позначено {count} циклів, без позначки залишилося: {skipped}.", + "auto_label_done_other": "Автоматичне маркування завершено: позначено {count} циклу, без позначки залишилося: {skipped}." }, "suggestion": { - "both_agree": "WashData рекомендує", - "calibrated_label": "Відкалібровано", "observed_label": "Спостережуване", "impact": { "auto_label_confidence": { @@ -1928,10 +1919,6 @@ "higher": "Закриває лише цикли зі значним споживанням енергії", "lower": "Закриває також коротші або маломощні цикли" }, - "end_repeat_count": { - "higher": "Потребує більшої кількості повторень сигналу завершення – надійніше, дещо повільніше", - "lower": "Завершує після меншої кількості повторень – швидше, більший ризик хибного завершення" - }, "learning_confidence": { "higher": "Навчається лише на дуже впевнених збігах – повільніше, але надійніше", "lower": "Навчається на більшій кількості циклів; модель може бути більш зашумленою" @@ -1970,40 +1957,25 @@ } }, "reason": { - "auto_label_confidence": "Впевненість на 15-му процентилі для {cycles} автоматичних міток, які користувач жодного разу не виправляв ({p15}).", "completion_min_seconds": "Половина тривалості p05 коректного циклу ({minutes} min) за {cycles} циклами; відфільтровує фантомні цикли.{excl}", - "dead_zone_batch": "Залишено невеликим (медіанне вікно нестабільності на старті за {cycles} циклами = {s}s), щоб реальне завершення визначалося швидко.", - "dead_zone_from_dip": "На основі ранньої просадки потужності, виявленої на {s}s.", - "end_energy_default": "Рекомендоване значення за замовчуванням для порога шуму наприкінці циклу.", - "end_energy_false": "Енергія p95 хибних завершень ({p95}Wh) за {cycles} циклами, з нижньою межею 0.2% від медіанної енергії циклу ({floor}Wh).", - "end_energy_no_false": "Хибних завершень за {cycles} циклів немає; нижня межа - 0.2% від медіанної енергії циклу ({floor}Wh).", - "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), тому мінімум не додає навантаження.", + "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": "Залишено якомога нижчим, щоб програма розпізнавалася на початку циклу; пороги впевненості та неоднозначності запобігають передчасним підтвердженням.", "min_off_gap": "На основі {gaps} спостережуваних проміжків між циклами (p05={p05}s). Мінімум пристрою: {floor}s.", "min_off_gap_bridge": "Розраховано так, щоб перекрити найдовшу тиху ділянку всередині циклу: {span}s + запас 60s, за {spans} перекритими проміжками за {cycles} коректних циклів (залишається нижче вашого найкоротшого проміжку між двома запусками поспіль, який становить {ceiling}s).", "min_power": "40% від найменшої активної потужності p05 ({p05}W) за {cycles} коректними циклами, щоб поріг вимкнення залишався нижчим за реальне споживання.{excl}", - "ml_auto_label": "Найменша впевненість, за якої модель якості все ще вважала цикл коректним (p10 за {cycles} коректними циклами = {p10}).", - "ml_end_repeat": "{false}/{total} циклів ({pct}%) мали паузу, яку детектор завершення оцінив вище 50%; потрібно {val} підтвердження завершення.", - "ml_off_delay": "Паузи, підтверджені детектором завершення: p95 {p95}s + запас 60s, за {pauses} паузами, перевіреними моделлю, за {cycles} циклів (мінімум {floor}s).", "no_update_timeout": "На основі спостережуваної частоти оновлень (p95={p95}s) * 20 (мін. 60s).", "off_delay_cadence": "На основі спостережуваної частоти оновлень (p95={p95}s) * 5", "off_delay_device_floor": "Використано безпечний мінімум для пристрою {device} ({floor}s).", "off_delay_generic_floor": "Використано універсальний безпечний мінімум ({floor}s).", "off_delay_pauses": "Розраховано так, щоб перекрити реальні паузи: пауза всередині циклу p95 {p95}s + запас 60s, за {pauses} паузами за {cycles} коректних циклів (мінімум {floor}s).", - "profile_match_threshold": "Впевненість p10 для {cycles} правильних автоматичних міток ({p10}); безпечний поріг підтвердження в реальному часі.", - "sampling_interval": "Медіанний інтервал оновлення за {cycles} коректними циклами ({si}s).{excl}", - "smoothing_window": "Розраховано на згладжування ~30s показань за частоти опитування {si}s ({samples} відліків).", - "start_duration": "Залишено невеликим (~один інтервал опитування {si}s), щоб виявлення починалося якомога раніше, але при цьому ігнорувалися поодинокі сплески.", - "thr_batch": "Залишено трохи вище найменшої активної потужності p05 за {cycles} циклами ({p05}W), щоб запуск фіксувався якомога раніше, а поріг зупинки залишався нижчим за мінімальну робочу потужність машини.", - "tol_per_profile": "p75 розкиду тривалості за профілями серед {profiles} профілів ({cycles} циклів); стабільні профілі не штрафуються.", - "tol_pooled": "На основі об'єднаного розкиду тривалості {cycles} нещодавніх розмічених циклів (відхилення p95={dev}).", - "watchdog": "Залишено настільки низьким, наскільки це безпечно (трохи вище інтервалу оновлення p95, що дорівнює {p95}s, і щонайменше 2x інтервалу вибірки {median}s, мін. 30s), щоб зависання виявлялися швидко без хибних зупинок." + "standby_floor": "Кожен останній цикл продовжував споживати {idle}W в момент завершення, тому поріг нижче цього значення ніколи не зможе зафіксувати вимкнення приладу. Залишено вище цієї позначки; {cycles} чистих циклів в архіві жодного разу не опускалися нижче нього більш ніж на {pause}с.", + "thr_batch": "Трохи вище найнижчої робочої потужності (p05) за {cycles} циклами ({p05}W), щоб старти ловилися рано, а поріг зупинки лишався нижче найнижчої робочої потужності.", + "watchdog": "Залишено настільки низьким, наскільки це безпечно (трохи вище інтервалу оновлення p95, що дорівнює {p95}s, і щонайменше 2x інтервалу вибірки {median}s, мін. 30s), щоб зависання виявлялися швидко без хибних зупинок.", + "end_energy_floor": "Підвищено до мінімуму, що випливає з порогу зупинки ({stop} Вт) протягом затримки вимкнення ({off_delay} с); нижче нього цикл може завершитися лише резервним шляхом.", + "max_duration_ratio_default": "Повернуто значення за замовчуванням: нижча верхня межа відхиляє правильну програму щоразу, коли цикл триває довше, ніж зазвичай.", + "min_duration_ratio_default": "Повернуто значення за замовчуванням: вища нижня межа не дає розпізнати поточний цикл, доки він не просунеться достатньо далеко." }, "exclusions": { "summary": "Виключено {total} помилково виявлених циклів: {parts}.", @@ -2021,7 +1993,8 @@ "no_power": "без потужності", "no_active_power": "без активної потужності" } - } + }, + "now_value": "(зараз {v})" }, "trend": { "down": "Низхідна тенденція", @@ -2029,6 +2002,10 @@ }, "maint": { "bearing_service": "Обслуговування підшипників", + "salt": "Додати сіль", + "rinse_aid": "Додати ополіскувач", + "lint_filter": "Очистити фільтр ворсу", + "condenser_clean": "Очистити конденсатор", "descale": "Видалення накипу", "drum_clean": "Очистити барабан", "filter_clean": "Очистити фільтр", @@ -2036,34 +2013,22 @@ }, "pg_desc": { "dishwasher_end_spike_quiet_release": "Посудомийна машина: секунди неактивності після очікуваної тривалості перед зняттям очікування фінального зливу в кінці циклу", - "smart_termination_duration_ratio": "Частка очікуваної тривалості зіставленої програми, якої має досягти цикл, перш ніж Розумне завершення зможе завершити його достроково; зменшіть її для машин, що залежать від завантаження або температури", + "smart_termination_duration_ratio": "Частка середньої тривалості програми, яку цикл має пройти, перш ніж Розумне завершення зможе закінчити його раніше; зменште, якщо час роботи коливається", "completion_min_seconds": "Найкоротший запуск, що вважається справжнім циклом", - "end_repeat_count": "Скільки низьких показань поспіль до завершення", "interrupted_min_seconds": "Короткі цикли позначаються як перервані", "min_off_gap": "Проміжок для розділення двох циклів", "off_delay": "Секунди низької потужності до завершення циклу", "start_duration_threshold": "Секунди вище порогу для підтвердження старту", "start_threshold_w": "Мінімум ват, щоб вважати запущеним", "stop_threshold_w": "Нижче цього машина вважається вимкненою", - "anti_crease_finalize_ratio": "Частка очікуваної тривалості зіставленої програми, якої має досягти цикл, перш ніж можна завершити хвіст обертання захисту від зминання; зменшіть її на сушильних машинах із довгим хвостом обертання, для пральних машин залиште значення за замовчуванням", + "anti_crease_finalize_ratio": "Частка середньої тривалості програми, яку цикл має пройти, перш ніж хвіст захисту від зминання зможе його завершити; зменште для сушильних машин, для пральних залиште типове", "anti_wrinkle_enabled": "Поглинати імпульси перекидання барабана після основної фази, а не зчитувати їх як нові цикли", - "anti_wrinkle_exit_power": "Потужність між імпульсами має падати нижче цього, щоб захист від зминання залишався активним", + "anti_wrinkle_exit_power": "Рівень потужності в паузах між імпульсами обертання; діє лише вище Порогу зупинки", "anti_wrinkle_idle_timeout": "Дозволена тиша між двома імпульсами перекидання барабана, перш ніж захист від зминання завершиться", "anti_wrinkle_max_duration": "Імпульс, довший за цей, завершує захист від зминання та відкриває новий цикл", "anti_wrinkle_max_power": "Імпульс, вищий за цей, завершує захист від зминання та відкриває новий цикл", - "dtw_bandwidth": "Етап 3: смуга деформації Sakoe-Chiba (0 = DTW вимкнено; за замовчуванням 0,2 = 20% довжини циклу)", - "corr_weight": "Етап 2: баланс між формою кривої (кореляція) та рівнем потужності (MAE); за замовчуванням 0,45", - "duration_weight": "Етап 4: наскільки сильно збіг за тривалістю впливає на кінцеву оцінку (за замовчуванням 0,22)", - "energy_weight": "Етап 4: наскільки сильно збіг за енергією впливає на кінцеву оцінку (за замовчуванням 0,22)", - "profile_match_min_duration_ratio": "Етап 1: мінімальна довжина циклу як частка тривалості профілю; за замовчуванням 0,1 (10%)", - "profile_match_max_duration_ratio": "Етап 1: максимальна довжина циклу як частка тривалості профілю; за замовчуванням 1,8 (180%)", - "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)", - "dtw_refine_top_n": "Етап 3: кандидати, які DTW переоцінює; збільшіть до 7-9, якщо правильний профіль займає 4-5 місце (за замовчуванням 5)", - "duration_scale": "Етап 4: лог-відношення, при якому збіг за тривалістю зменшується вдвічі; менше = суворіший штраф (за замовчуванням 0,175)", - "energy_scale": "Етап 4: лог-відношення, при якому збіг за енергією зменшується вдвічі; менше = суворіший штраф (за замовчуванням 0,25)", + "profile_match_min_duration_ratio": "Мінімальна довжина циклу як частка тривалості профілю; типово 0,1 (10%)", + "profile_match_max_duration_ratio": "Максимальна довжина циклу як частка тривалості профілю; типово 1,8 (180%)", "curve_preroll_seconds": "Секунди показань від перерваних спроб запуску, що переносяться на початок кривої циклу; 0 = вимкнено" }, "col": { @@ -2071,12 +2036,14 @@ "status_tip": "Результат циклу: Завершено (природне завершення), Перервано (різке падіння потужності), Примусово зупинено (вручну) або Потребує перегляду (очікується відгук).", "date_tip": "Дата та час початку циклу.", "duration_tip": "Загальний час роботи циклу від початку до кінця.", - "energy_tip": "Загальна спожита енергія (кВт·год). Обчислюється інтегруванням потужності за часом.", - "cost_tip": "Вартість енергії для цього циклу, зафіксована при завершенні. Коли увімкнено «Вартість, зважена за часом», крива потужності обчислюється за ціною, що діє в кожну мить; інакше енергія x єдина ціна, що діє наприкінці. Задайте ціну в Налаштуваннях, щоб значення заповнилося.", + "energy_tip": "Усього спожито енергії (кВт·год), розраховано за потужністю в часі.", + "cost_tip": "Вартість енергії цього циклу, зафіксована при завершенні. Коли ввімкнено «Вартість, зважена за часом», кожен момент оплачується за своєю ціною; інакше за ціною наприкінці. Задайте ціну в налаштуваннях, щоб це заповнилося.", "confidence_tip": "Впевненість збігу профілю (0–100%). Наскільки точно крива потужності циклу збіглася з визначеною програмою.", - "health_tip": "ML-стан циклу (вище = краще). Натисніть на цикл, щоб перевірити та переглянути його.", "flags_tip": "Мітки спільноти та індикатори якості даних", - "cost_dynamic_tip": "Зважено за часом: обчислено за ціною, що діяла протягом циклу (ефективно {price} {cur}/кВт·год)." + "cost_dynamic_tip": "Зважено за часом: обчислено за ціною, що діяла протягом циклу (ефективно {price} {cur}/кВт·год).", + "dbg_conf": "Впевн.", + "dbg_mae": "MAE", + "dbg_corr": "Кор." }, "stat": { "avg": "сер.", @@ -2100,7 +2067,10 @@ "notify_finish": "Було б надіслано сповіщення про завершення.", "notify_milestone": "Було б надіслано сповіщення про проміжний етап.", "notify_held": "Сповіщення було відкладено через тихі години.", - "finished": "Цикл досяг кінцевого стану й завершився." + "finished": "Цикл досяг кінцевого стану й завершився.", + "match_reverted": "Програма повернулася до виявлення: її оцінка впала значно нижче власного піку.", + "verified_pause": "Вирівнювання в реальному часі підтвердило (або зняло) паузу всередині програми. Поки пауза діє, цикл не завершується.", + "stalled": "Цикл тримався на споживанні очікування довше, ніж ця програма будь-коли робить там паузу, хоча робота не завершена. Показано як Призупинено (Застряг); цикл лишається відкритим." }, "store": { "search_ph2": "Пошук за брендом або моделлю…", @@ -2109,11 +2079,7 @@ "programs_count": "Програми: {n}", "browse": "Огляд", "device": "Пристрій", - "favorites": "Обране", - "search_ph": "Пошук за брендом або моделлю…", "no_results": "Відповідних пристроїв не знайдено. Спробуйте інший запит.", - "n_programs": "{n} програм", - "n_cycles": "{n} циклів", "no_programs": "Для цього пристрою поки немає опублікованих програм.", "no_cycles": "Для цієї програми поки немає опублікованих еталонних циклів.", "anon": "анонім", @@ -2125,19 +2091,21 @@ "merge_target": "Об'єднати з профілем", "description": "Опис (необов'язково)", "contributed_by": "від {name}", + "website": "Сайт магазину ↗", + "website_tip": "Відкрити сайт магазину спільноти", + "favorites": "Обране", "rating_summary": "★ {avg} ({n})", "no_ratings": "Оцінок ще немає", - "website": "Сайт магазину ↗", - "website_tip": "Відкрити сайт магазину спільноти" + "unreachable": "Не вдалося підключитися до магазину спільноти. Перевірте з’єднання та спробуйте ще раз." }, "settings": { "gear": { - "title": "Налаштування" + "title": "Параметри" } }, "link": { - "manual": "Посібник ↗", - "add_appliance": "Додати ваш пристрій" + "maintenance_supporter": "Maintenance Supporter на GitHub ↗", + "manual": "Посібник ↗" }, "tip": { "add_brand": "Бренду немає у списку? Додайте його до каталогу спільноти", @@ -2171,7 +2139,13 @@ "rebuild": { "envelopes": "Перерахунок обвідних" }, - "cancelling": "Скасування..." + "cancelling": "Скасування...", + "auto_label": { + "running": "Автоматичне позначення циклів" + }, + "store_download": { + "running": "Завантаження з магазину" + } }, "setup": { "hdr": { @@ -2180,26 +2154,24 @@ }, "phase0": { "washer": "WashData вже виявляє ваші цикли. Запишіть перший цикл, щоб увімкнути назви програм і оцінки часу.", - "dishwasher": "WashData спостерігає. Посудомийні машини мають складні цикли – запис першого циклу наполегливо рекомендується. Якщо виявлений цикл триває занадто довго, обріжте його в редакторі циклів перед збереженням як профіль.", + "dishwasher": "WashData стежить. Цикли посудомийної машини складні, тому запишіть перший цикл. Якщо виявлений цикл задовгий, обріжте його в редакторі циклу перед збереженням як профілю.", "generic": "WashData спостерігає. Запишіть або позначте виявлений цикл, щоб почати створювати профілі." }, "phase1a": { - "labelled": "Гарний початок – ваша перша програма збережена. Для найчистіших даних розгляньте запис наступного циклу за допомогою віджета запису." + "labelled": "Гарний початок: ваша перша програма збережена. Для найчистіших даних запишіть наступний цикл через ручний запис." }, "phase1b": { - "recorded": "Ваш запис збережено як {profile_name}. Тепер запишіть або позначте інші поширені програми для розширення охоплення." + "recorded": "Ваш запис збережено як {profile_name}. Тепер запишіть або позначте інші часті програми." }, "phase1c": { - "verify": "У вас є {count} програм від спільноти. Запустіть цикл, щоб переконатися, що WashData правильно їх розпізнає – зіставлення покращуватиметься в міру того, як пристрій накопичує власну історію." + "verify": "У вас є програми від спільноти ({count}). Запустіть цикл, щоб перевірити, чи розпізнає його WashData; зіставлення покращується в міру того, як пристрій набирає власну історію." }, "phase2": { - "cluster": "WashData виявив {count} циклів, які не відповідають жодній збереженій програмі – вони схожі один на одного. Хочете створити для них новий профіль?", + "cluster": "WashData помітив схожі цикли ({count}), які не пасують жодній збереженій програмі. Створити для них новий профіль?", "unmatched": "Ваш останній цикл не збігся з жодною збереженою програмою. Це нова програма?" }, "phase3": { - "suggestions": "WashData має рекомендації щодо налаштувань на основі вашої історії циклів – перегляньте їх для підвищення точності виявлення.", - "groups": "Деякі ваші профілі схожі на одну й ту саму програму при різних температурах. Об'єднайте їх у групу для покращення зіставлення.", - "phases": "Додайте фази програми до {profile_name} для точніших оцінок часу, що залишився." + "suggestions": "У WashData є пропозиції налаштувань на основі історії ваших циклів. Перегляньте їх, щоб покращити виявлення." }, "phase4": { "healthy": "Цей пристрій повністю налаштовано ({profile_count} профілів)." @@ -2212,12 +2184,58 @@ "create_from_cluster": "Створити профіль з цих циклів", "create_profile": "Створити для нього профіль", "review_suggestions": "Переглянути пропозиції", - "organise_profiles": "Упорядкувати профілі", - "configure_phases": "Налаштувати фази", "skip_step": "Пропустити цей крок", "skip_forever": "Більше не показувати", "hide_guidance": "Приховати підказки", "show_guidance": "Показати підказки" } + }, + "access": { + "edit": "Редагування", + "full": "Повний", + "inherit": "Успадкувати", + "none": "Немає (приховано)", + "read": "Читання" + }, + "pg_label_reason": { + "no_winner": "Жодна програма не підійшла", + "below_floor": "Нижче порогу «Впевненість навчання»", + "ambiguous": "Позначено як неоднозначне", + "margin": "Замалий відрив від другої програми", + "unknown_profile": "Програма більше не існує", + "not_evaluated": "Не оцінено" + }, + "diagram": { + "accum": "накоп.", + "auto": "авто", + "below_off": "нижче = вимк.", + "confirmed": "підтверджено", + "energy_counts": "енергія враховується", + "feedback": "відгук", + "gap_below_min": "пауза менше мін. = один цикл", + "heat_phase": "фаза нагріву", + "held_at_100": "залишається на 100%", + "ignored": "ігнорується", + "kept": "збережено", + "max": "макс.", + "min": "мін.", + "minus_tol": "-доп", + "no_match": "немає збігу", + "no_updates": "немає оновлень", + "off_delay_wait": "затримка вимкнення", + "off_gap": "пауза вимк.", + "plus_tol": "+доп", + "profile": "профіль", + "reading_interval": "типовий інтервал показань", + "sensor_offline": "датчик офлайн: примус. зупинка", + "si": "ІВ", + "spike_ignored": "сплеск ігнорується", + "spike_ignored_short": "сплеск: ігнорується", + "start": "початок", + "stop": "зупинка", + "tail_energy_resets": "енергія хвоста вище порогу: таймер скидається", + "thr": "поріг", + "too_short": "закороткий", + "tumble_kept": "імпульси барабана збережено" } } diff --git a/custom_components/ha_washdata/translations/panel/zh-Hans.json b/custom_components/ha_washdata/translations/panel/zh-Hans.json index 2868d74c..e37bf165 100644 --- a/custom_components/ha_washdata/translations/panel/zh-Hans.json +++ b/custom_components/ha_washdata/translations/panel/zh-Hans.json @@ -3,6 +3,7 @@ "min": "分钟", "entity_not_found": "未找到实体", "running_long": "运行超时", + "stalled": "停滞", "remaining": "剩余", "progress": "进度", "show_time_remaining": "显示剩余时间", @@ -48,16 +49,15 @@ } }, "badge": { - "quiet_tail_tip": "接近结束时,该程序会静默约 {mins} 分钟,然后在结束前以约 {watts} W 运行约 {secs} 秒。在已测量的 {measured} 个周期中有 {seen} 个出现过:设备并非每次运行都会如此。", + "quiet_tail_tip": "接近结束时,该程序会静默约 {mins} 分钟,然后以约 {watts} W 运行约 {secs} 秒。在已测量的 {measured} 个周期中有 {seen} 个出现过。", "quiet_tail": "结尾静默约 {mins} 分钟", - "artifact_tip": "检测到 {n} 个异常(例如,门在周期中打开)– 打开以在图表上查看", + "artifact_tip": "检测到 {n} 个异常(例如周期中途开门)- 打开即可在图表上查看", + "artifact_tip_other": "检测到 {n} 个异常(例如周期中途开门)- 打开即可在图表上查看", "auto": "(自动检测)", "built_in_tag": "内置", "declining": "↘ 下降中", "energy_low": "能耗低于正常水平", "energy_spike": "能耗高于正常水平", - "fair_fit": "质量尚可", - "fair_fit_tip": "匹配一致性适中 – 分配给此配置文件的部分周期置信度较低。标记更多周期或重新录制配置文件以提高准确性。", "feedback_requested": "已请求反馈", "golden_cycle": "记录参考周期", "duration_outlier": "{n} 个时长异常的周期", @@ -66,27 +66,34 @@ "needs_review": "需要审查", "overrun": "运行时间超出正常", "poor_fit": "质量差", - "poor_fit_tip": "匹配历史不一致 – 考虑重建此配置文件", - "restart_gap_tip": "{n} HA 重启间隙 - 电源跟踪有漏洞", + "restart_gap_tip": "{n} 个 HA 重启间隙:功率曲线存在缺口", + "restart_gap_tip_other": "{n} 个 HA 重启间隙:功率曲线存在缺口", "reviewed": "已审核", "steady": "→ 稳定", "underrun": "完成比通常更快", - "using_defaults": "使用出厂默认值", - "using_tuned": "使用已调整的权重", "overrun_ratio": "(预期的 {x} 倍)", "underrun_ratio": "(预期的 {pct}%)", - "awaiting_tip": "等待社区批准", - "awaiting_n": "等待批准 · 已确认 {n} 个", - "approved": "已批准", - "awaiting": "等待批准", "imported_tip": "从社区商店导入。仅用于匹配,不计入统计。", "unmatchable": "无法匹配", "not_importable": "此处不适用", "exists": "已存在", "backfilled_tip": "在导入的功率历史中检测到。仅影响程序匹配,不计入统计。", - "armed": "(应用于下一个周期)" + "armed": "(应用于下一个周期)", + "awaiting_tip": "等待社区批准", + "awaiting_n": "等待批准 · 已确认 {n} 个", + "awaiting": "等待批准", + "approved": "已批准", + "matcher_unused": "尚未匹配到您的任何周期", + "matcher_used": "已匹配您的 {n} 个周期", + "matcher_used_other": "已匹配您的 {n} 个周期", + "matcher_used_tip": "此设备上由 WashData 自动匹配到此导入程序的周期。从未匹配过的导入程序可能不适合您的设备,可以删除。仅在本地统计,不会向商店发送任何内容。" }, "btn": { + "pg_preset_delete": "删除预设", + "pg_preset_load": "加载预设", + "pg_preset_load_tip": "用该预设替换试验场中的值(当前生效的设置不受影响)", + "pg_preset_save": "另存为预设", + "pg_preset_save_tip": "将下面的所有值保存为该设备的一个命名预设", "reset_zoom": "重置缩放", "set_brand_model": "设置品牌和型号", "refresh_catalog": "刷新目录", @@ -97,12 +104,11 @@ "add_timer": "+ 添加计时器", "apply_all": "全部应用", "apply_label": "应用标签", - "apply_set_b": "应用设置 B", "apply_split": "应用拆分", "apply_trim": "应用裁剪", "auto_detect_split": "自动检测", "auto_label_cycles": "自动标记周期", - "auto_label_cycles_tip": "自动为置信度达到阈值的未标记周期分配配置文件名称", + "auto_label_cycles_tip": "为匹配置信度达到阈值的未标记周期命名。", "cancel": "取消", "clear_debug": "清除调试数据", "clear_splits": "清除", @@ -112,8 +118,6 @@ "convert_to_automation": "转换为自动化", "correct": "修正…", "create": "创建", - "create_from_cluster": "从 {count} 个周期创建配置文件", - "create_profile": "+ 创建配置文件", "delete": "删除", "delete_group": "删除组", "delete_group_tip": "仅删除此组 – 保留成员配置文件", @@ -124,22 +128,19 @@ "done": "完成", "edit": "编辑", "export": "导出", - "export_json": "导出为 JSON", "force_stop": "强制停止", "force_stop_tip": "立即结束当前周期并将其标记为强制停止", "from_template": "从模板 ▾", "ignore": "忽略", "import_json": "从 JSON 导入", - "import_overwrite": "导入(覆盖数据)", "inspect": "检查", "label": "标记", - "load": "加载", "load_more": "加载更多", "manage": "管理", "merge": "合并", "new_automation": "+ 新建自动化", "new_group": "+ 新建组", - "new_group_tip": "将形状/时长相近(不同温度或转速)的配置文件分组,使匹配器能可靠地区分它们", + "new_group_tip": "将几乎相同的配置文件(形状相同,温度或转速不同)分组,使匹配能够区分它们。", "new_phase": "+ 新建阶段", "new_profile": "+ 新建配置文件", "new_profile_tip": "从现有已标记的周期或录制中创建新程序配置文件", @@ -147,36 +148,25 @@ "on_cycle_started": "周期开始时", "pause_cycle": "暂停", "pause_cycle_tip": "暂停正在运行的周期 – 设备将从中断处恢复", - "pg_apply_device": "应用到设备", - "pg_autofill": "自动填充", - "pg_load_calibrated": "加载已校准 (ML) ({n})", - "pg_load_calibrated_tip": "将 ML 校准设置作为试验场覆盖值载入", "pg_load_live": "加载生效设置", "pg_load_live_tip": "重新读取集成的当前设置,并放弃试验场中的所有修改", "pg_load_suggested": "加载建议项 ({n})", "pg_load_suggested_tip": "将自动调谐器的当前建议作为试验场覆盖值载入", - "pg_preset_delete": "删除预设", - "pg_preset_load": "加载预设", - "pg_preset_load_tip": "用该预设替换试验场中的值(当前生效的设置不受影响)", - "pg_preset_save": "另存为预设", - "pg_preset_save_tip": "将下面的所有值保存为该设备的一个命名预设", "pg_publish_all": "将 {n} 项应用到集成", "pg_publish_one": "将 {label} 应用到集成", - "play": "播放", "process": "处理", "process_history": "立即处理", "process_recording": "保存录制", "process_tip": "将录制的电力记录保存为新的或现有的配置文件", "rebuild": "重建包络", "rebuild_envelope": "重建包络", - "rebuild_tip": "从已标记周期重新计算所有配置文件的预期功率包络(最小/最大范围)– 在标记新周期或修正旧周期后运行", + "rebuild_tip": "根据已标记的周期重建每个配置文件的预期功率范围。在标记或修正周期后运行。", "rec_start_tip": "开始录制设备的电力轨迹 – 在运行周期之前立即开始", "rec_stop": "停止", "rec_stop_tip": "停止录制并保留已捕获的电力记录以供审核", "record": "开始录制", "refresh": "刷新", "refresh_diag_tip": "重新加载存储统计信息", - "refresh_logs_tip": "重新加载最新日志记录", "refresh_settings_tip": "从服务器重新加载设置", "relabel": "重新标记({count})", "remove": "移除", @@ -184,7 +174,6 @@ "reset": "重置", "reset_ml_models_tip": "放弃微调模型并恢复内置模型。WashData 可以稍后重新学习。", "reset_to_builtin": "重置为内置模型", - "reset_to_defaults": "重置为默认值", "resume_cycle": "继续运行周期", "resume_cycle_tip": "恢复已暂停的周期", "revert_settings": "放弃更改", @@ -197,8 +186,6 @@ "run_analysis": "🔍 运行建议分析", "run_analysis_tip": "立即分析已记录的周期并刷新调整建议", "run_auto_label": "运行自动标记", - "run_both": "运行 A 对比 B", - "run_simulation": "运行模拟", "save": "保存", "save_access_control": "保存访问控制", "save_group": "保存组", @@ -210,10 +197,8 @@ "save_settings": "保存设置", "select": "选择", "select_all": "全选", - "select_last20": "最近 20", "select_none": "清除", "show_only": "只显示", - "skip_setup": "跳过设置", "split": "拆分", "stop": "停止", "submit_correction": "提交修正", @@ -222,36 +207,31 @@ "trim_split": "裁剪 / 拆分", "undo": "撤销", "unlabel_selected": "取消标记所选({n})", - "unlabel_selected_tip": "从所选周期中移除配置文件标签。这些周期仍留在您的历史中,之后可以重新标记,只是不再用于构建此配置文件。", + "unlabel_selected_tip": "从所选周期中移除标签。这些周期仍保留在历史中,但不再用于构建此配置文件。", "use": "使用", "wipe_all": "清除所有数据", "delete_selected": "删除所选({n})", - "group_suggest": "组 {n}:{members}", "pg_apply_best": "应用最佳值", - "pg_apply_to_settings": "保存到设置", "pg_apply_to_settings_tip": "将您在此处编辑的值复制到该设备当前生效的设置中", "import": "导入", "disconnect": "断开连接", "connect_github": "连接 GitHub", - "save_appliance": "保存家电", "share_to_store": "分享到商店", "share": "分享", - "confirm_appliance": "确认此家电", - "confirmed": "已确认", "browse_community_setups": "浏览社区配置", "download_device": "下载此配置", "share_device": "⬆ 分享此设备", - "share_device_tip": "将此设备及其录制的参考周期分享到社区商店,让拥有相同机器的其他用户可以采用", + "share_device_tip": "将此设备及其参考周期分享到社区商店,让拥有相同机器的用户可以采用。", "share_n": "分享 {n} 个周期", "export_selected": "导出(选择数据)", "export_all": "快速导出全部", - "import_raw": "高级:用 JSON 替换全部", "download_export": "下载导出文件", "analyze_import": "分析文件", "import_selected": "导入所选", "back": "返回", "mute_suggestion": "停止建议此设置", "reset_muted": "重置已屏蔽", + "reset_phase_tip": "撤销您的编辑并恢复内置阶段", "import_power_history": "导入功率历史", "hist_read_recorder": "从 Home Assistant 读取", "hist_scan": "扫描周期", @@ -260,7 +240,15 @@ "save_total_cycles": "保存累计值", "clear": "清除", "add_brand": "添加品牌", - "add_appliance": "添加家电" + "log_done_task": "记录完成:{task}", + "add_task": "添加任务", + "add_appliance": "添加家电", + "retry": "重试", + "confirm_appliance": "确认此家电", + "confirmed": "已确认", + "create_profile_from_gap": "创建配置文件", + "undo_import": "撤销上次导入", + "pg_rerun": "更新模拟" }, "note": { "off_delay": { @@ -268,10 +256,6 @@ } }, "conflict": { - "anti_wrinkle_exit": { - "exit": "必须低于停止阈值 ({stop} W);否则防皱退出功率将被忽略", - "stop": "必须高于防皱退出功率 ({exit} W)" - }, "anti_wrinkle_max": { "max": "必须高于启动阈值 ({start} W);否则防皱时长限制将被绕过", "start": "必须低于防皱最大功率 ({max} W)" @@ -309,14 +293,14 @@ "threshold": "必须低于停止阈值 ({stop} W) 才能生效" }, "price_entity": { - "power_sensor": "这是该设备的功率传感器,不是电价。价格实体会覆盖下面的固定电价,这样每个周期都会把瓦数当作每 kWh 单价来计费,请清空此字段。", - "energy_sensor": "这是该设备的电能表,不是电价。价格实体会覆盖下面的固定电价,这样每个周期都会把电表读数当作每 kWh 单价来计费,请清空此字段。", - "device_class": "该传感器测量的是 {dc},不是每 kWh 电价。价格实体会覆盖下面的固定电价,请清空此字段,或改为指向电价(费率)传感器。", - "unit": "该传感器的读数单位是 {unit},不是每 kWh 电价。价格实体会覆盖下面的固定电价,请清空此字段,或改为指向电价(费率)传感器。" + "power_sensor": "这是该设备的功率传感器,不是电价。周期会按瓦数作为每 kWh 单价计费,请清空此字段。", + "energy_sensor": "这是该设备的电能表,不是电价。周期会按电表读数作为每 kWh 单价计费,请清空此字段。", + "device_class": "该传感器测量的是 {dc},不是每 kWh 电价。请清空此字段,或选择电价传感器。", + "unit": "该传感器的读数单位是 {unit},不是每 kWh 电价。请清空此字段,或选择电价传感器。" }, "pump_stuck": { - "duration": "必须小于无更新超时 ({to} 秒),以确保卡泵警报在看门狗终止周期前触发", - "timeout": "必须大于泵卡住时长 ({ps} 秒),以确保卡泵警报在周期被强制停止前触发" + "duration": "必须小于无更新超时({to} 秒),以便卡泵警报先触发", + "timeout": "必须大于泵卡住时长({ps} 秒),以便卡泵警报先触发" }, "start_dur": { "sampling": "采样间隔超过启动时长 ({sdt} 秒);单样本尖峰可能触发周期开始", @@ -334,10 +318,15 @@ "sampling": "采样间隔应最多为看门狗间隔 ({wi} 秒) 的一半" }, "settings_banner": "设置冲突:{n}。请检查高亮显示的部分,并在保存前修复。", - "settings_banner_btn": "前往第一处" + "settings_banner_btn": "前往第一处", + "end_energy": { + "energy": "对于停止阈值 ({w} W) 和关闭延迟 ({d} s) 而言过于严格;周期只能通过备用路径结束", + "stop": "结束能耗 ({d} s 内 {e} Wh) 只允许 {p} W" + } }, "hdr": { - "ab_comparison": "A/B 设置对比", + "custom_tasks": "自定义任务", + "maintenance_supporter": "需要更多功能?", "access_control": "访问控制", "add_maintenance": "添加维护事件", "automation_delete": "删除此自动化", @@ -346,28 +335,22 @@ "automation_review_this_cycle": "审查此周期", "automations": "自动化", "clear_debug": "清除调试追踪", - "cycle_simulator": "周期模拟器", "diagnostics_pref": "诊断", "display": "显示", - "dtw_inspector": "DTW 检查器", "export_import": "导出 / 导入", - "getting_started": "快速上手", "live_power": "实时功率", "logs": "日志", - "logs_diagnostics": "诊断", "maintenance": "维护操作", "maintenance_log": "维护日志", "maintenance_reminders": "保养提醒", "manual_recording": "手动录制", - "manual_recording_tip": "在 WashData 记录电力轨迹时有意运行一个周期。在设备启动前立即按开始,完成后按停止,然后按处理将其保存为命名配置文件。", + "manual_recording_tip": "有意录制一个周期:在设备启动前按“开始”,完成后按“停止”,然后按“处理”将其保存为配置文件。", "ml_learned": "WashData 已学到的内容", - "ml_matching_tuning": "程序匹配微调", "ml_settings_card": "设置", "ml_smart_learning": "智能学习", "my_preferences": "我的偏好", "no_automations": "尚未有自动化引用此设备。", "panel_settings": "面板设置", - "param_sweep": "参数扫描", "pg_settings_source": "设置来源", "phase_catalog": "阶段目录", "playground": "试验场", @@ -376,11 +359,8 @@ "status": "状态", "status_graph": "状态图表", "storage_stats": "存储统计", - "test_history": "在历史数据上测试", "toggle_sidebar": "切换家庭助理侧边栏", - "tools_and_data": "工具与数据", "wipe_history": "清除历史", - "recommendations": "建议({n})", "pg_detection_params": "检测设置", "pg_outcome": "模拟结果", "pg_across_cycles": "所有周期综合", @@ -388,7 +368,8 @@ "online_account": "社区商店与在线功能", "import_power_history": "导入功率历史", "service_status": "保养状态", - "online_account_tab": "在线与社区" + "online_account_tab": "在线与社区", + "live_match_debug": "实时匹配调试" }, "health": { "fair": "配置文件质量尚可", @@ -396,11 +377,16 @@ "poor": "⚠ 配置文件质量差" }, "lbl": { - "envelope_position_tip": "在匹配程序自身记录曲线上的位置,{pct}%。它通过将本次周期与配置文件对齐来测量,而不是计算时间,因此运行比平时更长或更短时仍然准确。它在设备静默时刷新,因此在活跃阶段可能会滞后。", + "envelope_position_tip": "在匹配程序记录曲线上的位置:{pct}%。通过将周期与配置文件对齐得出,因此运行偏长或偏短时仍然准确。在设备静默时更新,因此可能滞后。", "envelope_position": "曲线 {pct}%", "aria_playground_chart3": "交互式周期功率图:悬停查看时间/功率,滚动或双指缩放,拖动平移", "device_unresolved": "不可用的设备 ({id})", "drag_to_resize": "拖动以调整大小", + "task_name": "任务", + "every_cycles": "间隔(周期)", + "every_days": "间隔(天)", + "new_task_name": "新任务", + "days_since_service": "{since} / {total} 天", "actions": "操作", "activity": "活动", "administrators": "管理员", @@ -408,10 +394,8 @@ "aria_cycle_chart": "周期功率曲线", "aria_envelope_chart": "配置文件功率包络图表", "aria_phase_chart": "阶段编辑器图表", - "aria_playground_chart": "带可拖动检测阈值的周期功率曲线", "aria_power_chart": "功耗图表", "aria_spaghetti_chart": "叠加的周期功率曲线", - "aria_sweep_chart": "参数扫描结果图表", "artifact_anomaly": "异常", "artifact_high_power": "高功率", "artifact_interruption": "中断", @@ -425,150 +409,99 @@ "cohesion_good": "凝聚力 {pct}%", "cohesion_low": "⚠ 低凝聚力 {pct}%", "compare_profiles": "与配置文件比较", - "concurrency": "并发", "confidence": "置信度", "confidence_threshold": "置信度阈值", "correct_duration": "修正时长(分钟,可选)", "correct_profile": "修正配置文件", - "correlation": "相关性", "cost": "费用", "create_new_profile": "+ 创建新配置文件…", "current_phase": "当前阶段", - "curves": "曲线", "cycle": "周期", "cycle_date_display": "周期日期显示", - "cycle_health": "周期健康", - "cycle_index": "周期序号 →", - "cycles": "周期", "cycles_count": "周期数", "cycles_lc": "个周期", - "dashed": "虚线", "date": "日期", "debug_traces": "调试追踪", - "default": "默认", "default_access_level": "未列出用户的默认级别", "default_other": "默认(其他设备)", "default_tab": "打开面板时的默认标签", - "delta": "Δ", "description": "描述", - "dtw_blend_w": "混合权重", - "dtw_blended": "混合得分", - "dtw_ddtw": "DDTW 得分", - "dtw_l1": "L1 得分", - "dtw_matched_profile": "(该周期匹配的配置文件)", "duration": "时长", - "duration_agreement": "时长一致度", - "emphasis": "强调", "enable_access_control": "启用每用户访问控制", "energy": "能耗", - "energy_agreement": "能耗一致度", - "event_timeline": "事件时间线", "event_type": "事件类型", "expected": "预期", + "suspected_profile": "{name}(推测)", + "runner_up_profile": "{name}(第二名)", "expected_duration": "预期时长(分钟)", "file_kb": "文件(kB)", - "final_score": "最终得分", "from": "起始", "gap_s": "间隔(秒)", "group_name": "组名称", "head_trim": "开头裁剪(秒)", - "health": "健康", "hide_tabs": "对非管理员隐藏标签", - "in_use": "使用中", "json_data": "JSON 数据", "label": "匹配的程序", "last": "最近", "last_checked": "上次检查:", "load_from_file": "从文件加载", - "mae_score": "MAE 得分", "manual_duration": "手动时长(分钟,可选)", "match": "匹配", "members": "成员", - "metric": "指标", "mode_existing_profile": "添加到现有配置文件", "mode_new_profile": "创建新配置文件", "models_fine_tuned": "(已微调的模型:{count})", - "n_classic_suggestions": "{n} 经典", - "n_ml_suggestions": "{n} ML", "n_selected": "已选 {n} 个", - "n_tuning_suggestions": "{n} 调整建议", - "na": "不适用", + "n_tuning_suggestions": "{n} 条调整建议", + "n_tuning_suggestions_other": "{n} 条调整建议", "new_profile_name": "新配置文件名称", "notes": "备注", - "of": "/", - "outcome": "结果", "panel_default_tab": "默认标签", "panel_language": "面板语言", "per_cycle_short": "周期", "pg_anti_wrinkle": "防皱", - "pg_autofill_tip": "根据当前值自动填充范围({lo}–{hi})", "pg_best_value": "找到的最佳值", "pg_current_value": "当前值:", "pg_drag_hint": "拖动图表上的线条", - "pg_dtw": "DTW", "pg_ev_ending": "结束中", "pg_ev_running": "运行中", "pg_group_advanced": "边缘情况", "pg_group_detection": "检测触发条件", "pg_group_timing": "计时规则", "pg_idle": "空闲", - "pg_last_run": "上次运行:{date}", - "pg_match_score": "匹配得分", - "pg_pct_of": "{total} 中的 {p}%", "pg_poor_match": "匹配较差", "pg_preset": "试验场预设", "pg_preset_name": "新预设名称", "pg_preset_none": "选择预设…", "pg_strong_match": "强匹配", - "pg_tile_ambiguous": "不明确", - "pg_tile_ambiguous_tip": "两个程序几乎打平的周期", - "pg_tile_detected": "已检测", - "pg_tile_detected_tip": "功率越过启动阈值足够久、被记录为运行中的周期", - "pg_tile_matched": "已匹配", - "pg_tile_matched_tip": "正确归属到已知程序的周期", - "pg_time_left": "剩余时间", "pg_weak_match": "弱匹配", "phase_name": "阶段名称", "name": "名称", "phase_timeline": "阶段时间线", "power": "功率", "profile": "配置文件", - "profile_index": "配置文件序号 →", "profile_name": "配置文件名称", "program": "程序", - "program_tip": "覆盖与当前周期匹配的配置文件。自动检测让集成自动选择最佳匹配。当自动检测出错或您知道正在运行什么时,固定特定程序以强制匹配。在启动家电之前选择一个程序,下一个周期开始时即会应用。", + "program_tip": "选择当前周期匹配到哪个程序。自动检测会选出最佳匹配。自动检测出错时可固定一个程序;在启动前选择,则应用于下一个周期。", "progress": "进度", "raw_socket": "原始套接字", - "read_only": "只读", "ref_cycle": "参考周期(可选)", - "refresh_interval": "刷新间隔(秒)", "remaining": "剩余", "reminder_every": "提醒间隔(周期)", "remove_label": "- 删除标签 -", "rename_profile": "重命名配置文件", - "replay_duration": "回放", "resulting_profile": "结果配置文件", "save_mode": "保存模式", - "score": "得分", - "score_breakdown": "得分明细", "seconds_from_start": "从开始计的秒数", "select_profile": "选择配置文件", "setting": "设置", - "settings_a": "设置 A(当前)", - "settings_a_short": "A", "settings_advanced": "高级", - "settings_b": "设置 B(测试)", - "settings_b_short": "B", "settings_basic": "基础", "settings_detail_level": "设置详细程度", - "settings_overrides": "设置覆盖项", "show_debug": "在状态页面显示实时匹配调试卡(置信度、歧义、最佳候选)", "show_expected": "显示预期曲线叠加(匹配配置文件,橙色)", "show_raw": "在实时功率图中显示原始插座切换开关", "sparkline": "近期周期时长趋势", - "stage2": "阶段 2 – 核心相似度", - "stage3": "阶段 3 – DTW", - "stage4": "阶段 4 – 一致度", "status": "状态", "tail_trim": "末尾裁剪(秒)", "tap_to_dismiss": "点按图表关闭", @@ -579,7 +512,6 @@ "ungrouped": "未分组", "unlabelled": "未标记", "unlabelled_paren": "(未标记)", - "warp_path": "规整路径", "input": "输入:", "start": "开始", "end": "结束", @@ -604,9 +536,8 @@ "n_shown": ",显示 {n} 个", "cycles_title": "周期({n})", "pg_detecting": "检测中", - "pg_best_marker": "最佳:{v}", "pg_time_left_model": "剩余时间(模型)", - "pg_time_left_model_tip": "由模型估算的剩余时间(阶段估算 + ML 混合),并非固定倒计时。", + "pg_time_left_model_tip": "根据程序的阶段估算,并非固定倒计时。", "phase": "阶段", "pg_mode_history": "历史测试", "pg_ev_detected": "已检测", @@ -638,12 +569,6 @@ "pg_sweep_param": "参数", "pg_objective": "目标", "pg_steps": "步数", - "pg_stress_group": "闲置终止测试", - "pg_stress_idle_auto": "Auto", - "pg_stress_idle_w": "待机功率 (W)", - "pg_stress_idle_w_desc": "覆盖自动检测的待机功率下限(留空则使用自动值)。", - "pg_stress_toggle": "测试闲置终止", - "pg_stress_toggle_desc": "模拟设备在录制结束后维持待机功耗水平,查看 WashData 是否以及何时停止运行周期。", "pg_obj_match": "匹配准确度", "pg_obj_endtiming": "结束时机准确度", "pg_obj_falseend": "误结束率", @@ -652,12 +577,8 @@ "pg_mode_optimize": "优化", "pg_group_matching": "程序匹配", "pg_zoom_hint": "双击可重置缩放", - "aria_playground_chart2": "交互式周期功率图:悬停查看时间/功率,滚动缩放,拖动平移", "pg_alert_timeout_end": "因超时而结束,并非预测结束", "pg_alert_indefinite": "将一直运行下去", - "pg_alert_stress_cap": "已达到安全上限", - "pg_alert_stress_ok": "闲置终止: 运行周期已停止", - "pg_alert_stress_warn": "待机功耗超过停止阈值", "task_pg_history": "用历史测试", "task_pg_sweep": "优化", "task_reprocess": "重新处理", @@ -667,16 +588,9 @@ "pg_recent_runs": "最近的运行", "pg_load_run": "重新加载此运行", "pg_match_confidence": "匹配置信度", - "pg_envelope_fit": "包络拟合度", - "pg_envelope_fit_tip": "周期与此配置文件已保存的功率包络的贴合程度,与匹配置信度不同的视角。", "n_imported_note": ",已导入 {n} 个", "enable_online": "启用在线功能", - "store_brand": "家电品牌", - "store_model": "家电型号", "anonymous": "匿名", - "rate_quality": "评价质量", - "rate_n": "{n} 星", - "approved_only": "仅已批准", "adopt_settings": "同时采用设置", "flags": "标志", "include_phase_map": "包含阶段图", @@ -693,7 +607,6 @@ "cat_custom_phases": "自定义阶段", "cat_profile_groups": "配置文件组", "cat_settings": "检测与识别设置", - "cat_matching_config": "识别器调优", "cat_ml_models": "ML 模型", "cat_feedback": "反馈与审阅标签", "cat_suggestions": "建议", @@ -715,7 +628,6 @@ "hist_csv_data": "CSV 数据", "hist_from_recorder": "或从 Home Assistant 读取", "hist_since": "起始日期", - "days": "天", "hist_keep": "保留此周期", "hist_looks_complete": "完整", "peak_power_short": "峰值", @@ -735,7 +647,38 @@ "zoom_hint_touch_zoomed": "双指缩放 · 双击重置", "cycles_since_service": "{since} / {total} 个周期", "total_cycles_run": "累计运行周期", - "correct_total_cycles": "更正累计值" + "correct_total_cycles": "更正累计值", + "hist_reason_already_recorded": "与 WashData 已记录的周期重叠", + "share_consent": "我确认这些周期均正常完成,无中断", + "show_internal": "显示内部设置", + "show_internal_tip": "由 WashData 自动调校的设置(采样、看门狗、匹配频率、阈值)。当此设备有自定义值、冲突或建议时显示。", + "rate_n": "{n} 星", + "rate_quality": "评价质量", + "pg_autolabel": "自动标记", + "pg_label_review": "请求审查", + "pg_ev_match_reverted": "匹配已撤回", + "pg_ev_verified_pause": "已验证暂停", + "n_samples": "{n} 个样本", + "n_samples_other": "{n} 个样本", + "n_cycles": "{n} 个周期", + "n_cycles_other": "{n} 个周期", + "energy_per_cycle": "{v}/周期", + "energy_total": "共 {v}", + "automation_off": "(已关闭)", + "ref_cycle_none": "无", + "share_name": "在我分享的内容上显示我的名字", + "cat_backfill_cycles": "在导入的功率历史中发现的周期", + "pg_compare_profile": "与配置文件比较", + "pg_obj_early": "提前结束率", + "pg_obj_endlag": "结束延迟(中位数)", + "pg_obj_split": "拆分率", + "pg_sweep_cycles": "最近周期数", + "cycle_context": "周期前后的记录器历史", + "cycle_context_off": "关闭", + "cycle_context_minutes": "{min} 分钟", + "cycle_context_series": "记录器历史", + "before_start": "开始前", + "after_end": "结束后" }, "log": { "all_levels": "所有级别", @@ -745,11 +688,9 @@ }, "ml": { "builtin_models": "● 使用内置模型", - "fit_fair": "尚可", "fit_good": "良好", "fit_slight": "轻微", "fit_strong": "强", - "fit_weak": "弱", "fit_word": "拟合", "personalized": "● 已针对此机器个性化", "better_than_baseline": "比基线估计好 {pct}%({metric})", @@ -757,21 +698,27 @@ "trend_declining_tip": "该模型的拟合度在最近几次重新检查中有所下降,审查更多周期可能有助于它重新学习。", "trend_steady_tip": "该模型的拟合度在最近几次重新检查中大致保持稳定。", "cap_blurb": { - "end": "判断洗涤周期何时真正结束", - "live_match": "更早识别正在运行的程序", - "quality": "发现误检或损坏的周期", - "remaining_time": "预测剩余时间", "total_energy": "预测总能耗和费用" }, "cap_label": { - "end": "周期结束检测", - "live_match": "程序匹配", - "quality": "周期质量检查", - "remaining_time": "剩余时间估算", "total_energy": "能耗估算" }, - "metric_auc": "留出验证数据上的 AUC {auc}", - "metric_mae": "误差 {model} vs 基线 {baseline}" + "metric_mae": "误差 {model} vs 基线 {baseline}", + "fine_tuned_at": "微调于 {when}", + "last_run_not_promoted": "上次检查({when}):{reason}", + "never": "从未", + "skip_reason": { + "fit_failed": "训练失败:{error}", + "holdout_too_small": "周期太少,无法公平地测试新模型:可留出用于测试的周期为 {held_out} 个,需要 {min} 个。", + "not_better_than_incumbent": "新模型并不优于当前使用的模型(误差 {model} vs {incumbent}),因此保留了当前模型。", + "not_better_than_naive": "新模型没有明显优于简单估算(误差 {model} vs {naive})。", + "unknown": "没有学到新内容。", + "insufficient_rows": "可用周期尚不足:目前已标记且干净的周期为 {count} 个。", + "insufficient_rows_other": "可用周期尚不足:目前已标记且干净的周期为 {count} 个。" + }, + "trained_unknown": "未知时间", + "held_out_count": "基于 {count} 个留出验证周期测得", + "held_out_count_other": "基于 {count} 个留出验证周期测得" }, "modal": { "auto_label": "自动标记周期", @@ -780,7 +727,6 @@ "cycle": "周期", "edit_group": "编辑配置文件组", "edit_phase": "编辑阶段", - "import_config": "导入配置", "label_cycle": "标记周期", "merge_cycles": "合并 {n} 个周期", "new_group": "新建配置文件组", @@ -795,6 +741,8 @@ "delete_automation_title": "删除自动化", "delete_automation_msg": "从 Home Assistant 中删除自动化“{name}”吗?此操作无法撤销。", "remove_legacy_title": "移除旧版操作", + "reset_phase_msg": "撤销对“{name}”的编辑并恢复内置阶段吗?程序会保留其范围。", + "reset_phase_title": "重置阶段", "remove_legacy_msg": "移除旧版自定义操作吗?它们将不再在周期事件上触发。此操作无法从面板撤销。", "discard_recording_title": "丢弃录制", "discard_recording_msg": "丢弃已保存的录制吗?此操作无法撤销。", @@ -815,32 +763,43 @@ "store_share_device": "分享此设备", "export_select": "导出 - 选择数据", "import_wizard": "导入 - 选择数据", - "history_import": "导入功率历史" + "history_import": "导入功率历史", + "apply_suggestions_title": "应用建议", + "remove_task_title": "移除任务", + "remove_task_msg": "移除提醒“{task}”吗?它在维护日志中的记录会保留。", + "apply_suggestions_msg": "要更改这 {n} 项设置吗?保存后集成将重新加载。", + "discard_settings_title": "丢弃未保存的更改?", + "discard_settings_msg": "有 {n} 项设置更改尚未保存。要离开设置并丢弃这些更改吗?", + "profile_title": "配置文件 · {name}", + "undo_import_msg": "此设备的数据和设置将恢复到上次导入({when})之前的状态。此后记录的周期和所做的更改都将丢失。", + "undo_import_ok": "撤销导入", + "undo_import_title": "撤销上次导入" }, "msg": { "pg_canvas_empty3": "在上方选择一个周期并按“运行”进行模拟。然后悬停查看数值,滚动或双指缩放,拖动平移。", + "pg_preset_delete_confirm": "删除预设“{name}”吗?", + "pg_preset_limit": "已达预设数量上限({n})", + "pg_preset_overwrite": "覆盖预设“{name}”吗?", "tail_trim_hint": "从末尾移除的秒数", - "store_sibling_hint": "没有与您型号完全一致的分享内容?同品牌中相近的型号通常是不错的起点。", - "store_declare_appliance": "告知 WashData 您拥有哪台家电,此标签页便会显示其他用户为它分享的程序和录制。您也可以在上方输入品牌名称随意浏览。", - "refresh_catalog_hint": "社区品牌和家电列表已缓存,以便共享商店保持在每日读取额度内。刷新可获取他人新增或已批准的条目。", + "store_sibling_hint": "没有您型号的分享内容?同品牌的相近型号通常是不错的起点。", + "store_declare_appliance": "告诉 WashData 您拥有哪台家电,即可查看其他用户为它分享的设置。也可以在上方输入品牌随意浏览。", + "refresh_catalog_hint": "社区品牌和家电列表已缓存,以节省商店的每日额度。刷新可查看他人新增或批准的条目。", "head_trim_hint": "从开头移除的秒数", "appliance_monitor": "设备监控", "artifact_dip_detail": "跌破通常功率范围约 {n} 秒。", "artifact_footer": "在上图中高亮显示。这些是瞬态异常(例如门在周期中打开),不一定是问题。", "artifact_header": "此周期中检测到 {n} 个异常", - "artifact_pause_detail": "功率降至接近零约 {n} 秒后恢复 – 可能是周期中途开门或周期被暂停。", + "artifact_header_other": "此周期中检测到 {n} 个异常", + "artifact_pause_detail": "功率降至接近零约 {n} 秒后恢复。可能是开了门或周期被暂停。", "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": "叠加配置文件(浅色)", - "compare_profiles_tip": "将其他配置文件包络叠加在上方图表上,看哪个最适合此周期。", + "compare_profiles_tip": "在图表上叠加其他配置文件,查看哪个最适合此周期。", "compare_selected_cycles": "选中的周期(实线)– 显示/隐藏", - "consider_new_profile": "考虑创建新配置文件。", - "coverage_gap": "近期周期没有匹配的配置文件(最近 30 次的 {pct}%)。", - "coverage_gap_similar_cycles": "发现 {count} 个相似未标记周期 – 创建配置文件以开始匹配它们。", "cycles_deleted": "已删除 {count} 个周期", "enough_data": "有足够数据可供学习({current}/{min} 个循环)。", "export_description": "精确选择要导出到 JSON 的配置文件、周期、设置等,或分析文件并仅导入所需的部分。", @@ -848,30 +807,25 @@ "feedback_prompt": "确认正确、修正程序或忽略。", "feedback_relabel_hint": "重新标记此周期也会解决它。", "filter_by_profile": "按配置文件筛选…", - "group_modal_help": "将形状相同但温度/转速不同的程序分组(时长可能不同)。匹配将组作为一个候选项评分,然后选择最合适的成员。至少选 2 个;叠加显示相似程度。", + "group_modal_help": "将形状相同但温度或转速不同的程序分组。匹配会把组当作一个整体评分,然后选出最合适的成员。至少选 2 个;叠加图显示它们的相似程度。", "group_not_cohesive": "这些配置文件不够相似,无法可靠分组,因此匹配会单独处理它们,直到您移除离群值或拆分组。", "group_preview_hint": "勾选 2+ 个成员以预览并比较其功率曲线。", - "import_intro": "加载导出文件或在下方粘贴 JSON 内容。", "legacy_actions_title": "{count} 个旧版自定义操作仍在运行", + "legacy_actions_title_other": "{count} 个旧版自定义操作仍在运行", "live_chart_loading": "实时功率图表将随读数到达而显示。", "loading": "加载中…", "loading_curve": "加载曲线…", "loading_settings": "加载设置…", "log_buffer_hint": "最新在前 · 缓冲自重启以来最后 500 条 ha_washdata 记录 · 拖动底边调整大小。", - "maintenance_advisory": "时长/能耗增加可能表明设备需要维护(例如除垢、清洗滤网)。", "maintenance_due": "待办维护:{items}", - "maintenance_intro": "记录您对此设备执行的保养,并在每项任务再次到期时收到提醒。", + "maintenance_intro": "记录此设备的保养,并在每项任务再次到期时收到提醒。", "maintenance_load_error": "无法加载维护数据:{error}", "maintenance_requires_access": "维护和导出/导入需要完全访问权限。", - "manual_duration_hint": "配置文件的平均/预期周期时长,用于剩余时间估算。编辑以设置;保持不变则保留当前值。", - "matching_tuning_intro": "在学习时,WashData 还会调整程序匹配对形状与时长和能耗的权重比例。", + "manual_duration_hint": "配置文件的预期周期时长,用于剩余时间估算。保持不变则保留当前值。", "merge_intro": "选中的周期将按时间顺序合并(间隙填充为 0 W)。选择结果配置文件。", "ml_intro": "WashData 配备了开箱即用的智能模型。", "ml_learned_intro": "针对此机器微调的模型。", - "ml_loading": "加载 ML 中…", "ml_settings_intro": "两个独立开关:一个在周期运行时应用模型,另一个让 WashData 随时间针对您的机器进行微调。", - "name_first_program": "您已有足够的周期 – 为您的第一个程序命名即可开始匹配。", - "near_duplicate_cluster": "检测到近似重复的配置文件群。分组可让匹配在相似项目间可靠地选择(例如同一程序在不同温度/转速下)。", "no_cycles_match": "没有与当前筛选条件匹配的周期。", "no_cycles_profile": "此配置文件没有周期。", "no_cycles_selected": "请先至少选择一个周期。", @@ -896,73 +850,50 @@ "no_suggestions": "没有活跃的建议。", "notify_services_hint": "使用 {entity} 服务 ID(多个请用英文逗号分隔)。模板变量:{vars}。", "old_actions_warning": "使用旧版操作编辑器配置(现已移除)。它们仍会在周期事件上触发,但无法在此处编辑。将它们转换为普通自动化,或移除它们。", - "onboarding_progress": "已观察 {n} / 3 个周期", - "onboarding_watching": "照常使用您的设备 – WashData 正在观察。3 个周期后将开始程序匹配。", "pending_feedback": "待处理的检测反馈", "performance_trend": "性能趋势({n} 个周期)", - "pg_analysis_empty": "加载一个周期以查看匹配分析。", "pg_apply_confirm": "应用最佳值:{label} = {value}?", - "pg_canvas_empty": "在上方选择一个周期,然后按加载查看其功率轨迹。", - "pg_chart_caption": "不同 {param} 取值下的检测/匹配率", "pg_ctrl_intro": "数值以该设备当前生效的集成设置为起点。修改会保留在试验场中,直到您将其应用为止。", "pg_live_unavailable": "无法获取生效设置,显示默认值。", "pg_load_live_confirm": "放弃试验场中的 {n} 项修改并重新加载集成的当前设置吗?", - "pg_match_rate": "在 {total} 个周期上的匹配率为 {pct}%", "pg_matches_live": "与生效设置一致", "pg_n_changed": "与生效设置有 {n} 项不同", "pg_sugg_none": "所有建议项已与当前设置一致", - "pg_preset_delete_confirm": "删除预设“{name}”吗?", - "pg_preset_limit": "已达预设数量上限({n})", - "pg_preset_overwrite": "覆盖预设“{name}”吗?", "pg_publish_one_confirm": "将 {label} = {value} 保存到该设备的设置吗?", "pg_restart_note": "重启 Home Assistant 以启用模拟工具。", - "pg_sim_progress": "{done} / {total} 个周期", - "pg_starting": "正在启动…", - "pg_stress_synthetic": "待机模拟从此处开始", - "pg_sweep_intro": "如果 {param} 不同会怎样?在最近 {cycles} 个周期上测试 {steps} 个值,找到能正确匹配最多周期的设置。", - "pg_sweep_step": "步骤 {done} / {total}", - "pg_verdict_bad": "需要注意: 许多周期未被检测到。", - "pg_verdict_good": "调校良好: 大多数周期都被正确识别和匹配。", - "pg_verdict_ok": "尚可: 漏掉了一些周期。可尝试降低启动阈值。", "phase_catalog_intro": "周期的命名片段(预洗、加热、甩干…)。从其控制面板将它们分配给配置文件。", "phase_ranges_intro": "叠加在平均曲线上的阶段范围(从周期开始的分钟数)。编辑值以实时预览。", - "playground_intro": "一个安全的沙盒,可用不同设置回放过去的周期、并排比较两套配置,并查看周期如何与配置文件对齐。在您明确应用之前,此处的任何操作都不会更改您的实时数据。", - "playground_needs_restart": "重启 Home Assistant 以启用试验场工具。", - "preferences_admin": "偏好、面板和访问控制", - "preferences_adv": "偏好", + "playground_intro": "用您的真实周期数据试用设置。在您应用之前,这里的任何操作都不会更改当前生效的设置。", "prefs_personal": "这些仅适用于您的 Home Assistant 账户。", - "process_history_hint": "对所有存储的周期重新运行匹配、刷新调优建议、重新训练 ML 模型(如果启用)、按记录的电价重新计算各周期的费用,并重新计算周期健康。在批量审查后运行此项。", + "process_history_hint": "对所有存储的周期重新运行匹配、刷新建议、重新训练 ML 模型(如已启用)、按记录的电价重新计算费用,并重新计算周期健康。在批量审查后运行。", "profile_deleted": "配置文件已删除", - "profile_poor_health_detail": "分配给此配置文件的周期形状不一致或置信度较低。考虑重建包络或审查已标记的周期。", "profiles_intro": "点击配置文件查看统计、阶段和清理。将形状/时长相近(不同温度或转速)的程序分组,以便匹配可靠地区分它们。", - "rbac_hint": "关闭时,每个 Home Assistant 用户都有完全访问权限(默认)。管理员始终有完全访问权限并可管理所有人。", - "recent_logs": "最近的 ha_washdata 记录", + "rbac_hint": "关闭:每个 Home Assistant 用户都有完全访问权限(默认)。管理员始终有完全访问权限,并可管理所有人。", "recording_in_progress": "录制进行中", "reminders_intro": "在上次保养后经过这么多个周期时在面板中显示提醒。留空或填 0 可关闭提醒。", "restart_gap_footer": "在图表上突出显示。这些间隔的功率数据缺失––仅使用真实读数进行匹配。", - "restart_gap_header": "{n} 此周期内的 HA 重新启动间隔", + "restart_gap_header": "此周期内有 {n} 个 HA 重启间隙", + "restart_gap_header_other": "此周期内有 {n} 个 HA 重启间隙", "restart_gap_item": "{dur}间隙", - "review_confirm_help": "确认此周期是否被正确检测。您的审查可在您的机器上训练模型 – 确认的周期越多,匹配和健康评分越好。快速的好/差就足够了。", + "review_confirm_help": "此周期检测得对吗?确认周期可改进您机器的匹配。简单选择“好”或“差”即可。", "review_in_settings": "在设置中审查", "review_notes_placeholder": "注释(可选)", "review_notes_tip": "供个人参考的自由文本注释。不用于匹配或训练。", - "review_profile_tip": "此周期标记的程序。如果自动检测的程序有误,请在此处修正 – 标记可帮助未来周期的匹配学习。", - "review_quality_tip": "此周期的质量。好 = 该程序的标准示例;差 = 已检测但有噪声或不典型;不可用 = 误检测(合并、截断或虚假)。影响健康评分及允许训练模型的周期。", - "review_recorded_tip": "将此标记为其程序的精选参考周期 – 与手动录制周期的作用相同。参考周期始终保留,为匹配模板提供种子,且不会被清理删除。(这是“黄金”/录制标志;两者是同一件事。)", - "review_tags_tip": "描述此周期出现问题的可选标志,以便训练和清理可以考虑这些问题。", + "review_profile_tip": "此周期被标记的程序。如有错误,请在此修正;标记会用于匹配学习。", + "review_quality_tip": "此周期的质量。好:该程序的标准运行。差:已检测到,但有噪声或不典型。不可用:误检测(合并、截断或虚假)。影响健康评分以及哪些周期用于训练模型。", + "review_recorded_tip": "将此周期标记为其程序的精选参考周期,作用与手动录制的周期相同。参考周期始终保留,作为匹配模板的基础,且不会被清理删除。", + "review_tags_tip": "用于说明出了什么问题的可选标志,以便训练和清理将其考虑在内。", "review_to_cycles": "打开周期审查队列", "samples_decimated": "正在显示 {total} 个样本中的 {shown} 个(为显示而抽稀;保留峰值)。此处的较大间隔是抽稀,而非缺失数据。", - "saving_triggers_reload": "保存会触发集成重载。HA 实体可能短暂显示为不可用。", + "saving_triggers_reload": "保存会重新加载集成。实体可能会短暂显示为不可用。", "search_placeholder": "搜索设置…", "see_recorder": "查看下方的录制小部件", "setting_changed": "于 {date} 从 {old} 更改为 {new}", "settings_basic_note": "正在显示基本设置。切换到高级可查看完整列表。", - "shape_drift_advisory": "⚠ 形状偏移", - "shape_drift_detail": "此配置文件的功率模式随时间发生了变化 – 可能是设备磨损或需要维护(例如除垢、清洁滤网)。", "show_all_settings": "显示所有设置", - "showing_suggestions": "显示 {count} 个有建议的设置。", - "split_intro": "点击图表添加或删除分割点,或通过空闲间隔自动检测。每个结果片段可获得自己的配置文件。", - "storage_diagnostics": "存储统计、维护、导出/导入", + "showing_suggestions": "正在显示 {count} 个有建议的设置。", + "showing_suggestions_other": "正在显示 {count} 个有建议的设置。", + "split_intro": "点击图表添加或删除分割点,或按空闲间隔自动检测。每一部分都可以有自己的配置文件。", "sug_staged": "已设置 {key} = {val}。保存以应用。", "toast_auto_detect_enabled": "自动检测已启用", "toast_auto_label_complete": "自动标注完成", @@ -979,7 +910,6 @@ "toast_no_automation": "此处无法创建自动化", "toast_profile_name_required": "配置文件名称为必填项", "toast_program_set": "已设置程序:{program}", - "toast_rebuild_failed": "重建失败:{error}", "toast_resume_failed": "恢复失败:{error}", "toast_revert_failed": "还原失败:{error}", "toast_reverted": "已还原 {key}", @@ -990,100 +920,85 @@ "trend_energy_up": "能耗趋势上升({pct}%/周期)– 近期平均 {avg}", "trim_destructive_confirm": "这将仅保留该周期的 {pct}%,且此操作无法撤销。是否继续?", "trim_intro": "拖动红色控制柄,或输入值。窗口外的所有内容将被移除。", - "tuning_suggestions_available": "来自观察周期的 {count} 个调整建议可用。它们显示在相关字段旁边。", + "tuning_suggestions_available": "根据观察到的周期,共有 {count} 条调整建议可用。它们显示在相关字段旁边。", + "tuning_suggestions_available_other": "根据观察到的周期,共有 {count} 条调整建议可用。它们显示在相关字段旁边。", "unsure_detected_prefix": "WashData 不确定是否检测到", - "updated": "已更新", "warmup_badge": "仍在学习中({done}/{needed} 个周期)", - "warmup_detail": "该配置文件需要 {needed} 个已标记的周期后才能开始自动匹配。每个已确认的周期都有助于学习。", + "warmup_detail": "匹配已在工作。在 WashData 自行标记之前,请先确认此配置文件的前 {needed} 个周期。", "wipe_history_warning": "永久删除所有周期和配置文件。无法撤销。", "compare_overlay_tip": "叠加已学习的配置文件包络,查看每个周期与哪个程序相似。", - "duration_trend_up_tip": "时长上升({pct}%/周期)", - "duration_trend_down_tip": "时长下降({pct}%/周期)", - "energy_trend_up_tip": "能耗上升({pct}%/周期)", - "energy_trend_down_tip": "能耗下降({pct}%/周期)", - "performance_trending": "性能趋势变化", - "pg_select_cycle_hint": "选择一个周期并点击 ↺ 加载", - "pg_sweep_progress": "步骤 {done} / {total}", - "pg_analysis_hint": "选择一个周期并点击 ↺ 加载分析。", "diagnostics_load_failed": "无法加载诊断:{error}", - "manual_duration_ref_hint": "仅在未选择参考周期时使用。参考周期会根据自身长度设置时长。", + "manual_duration_ref_hint": "仅在没有参考周期时使用;参考周期会根据自身长度设置时长。", + "duration_learned_hint": "根据此程序的周期计算(排除离群值),每个新周期后更新。如需修改,请裁剪或删除偏差较大的周期。", "toast_convert_rolled_back": "转换失败并已回滚(未遗留任何自动化):{error}", "toast_convert_orphan": "自动化已创建,但清除旧操作失败。请勿重试:请手动移除旧操作,以避免自动化重复。", - "advisory_duration_trend_up": "'{name}' 的周期正逐渐变长(每个周期约 +{pct}%)。如果设备行为发生了变化,请重新录制或重建此配置文件。", - "advisory_energy_trend_up": "'{name}' 的能耗正逐渐增加(每个周期约 +{pct}%) - 如果这不符合预期,建议检查一下设备。", - "advisory_poor_health": "'{name}' 的拟合度评分较低 - 其近期周期波动较大或匹配较弱。请检查其周期或重新录制该配置文件,以保持匹配和时间估算的准确性。", - "advisory_shape_drift": "'{name}' 已明显偏离其原始功率曲线。设备行为可能随时间发生了变化(例如水垢、磨损)。建议用近期周期重新录制此配置文件。", - "advisory_shape_drift_corr": "'{name}' 已明显偏离其原始功率曲线(相关系数 {corr})。设备行为可能随时间发生了变化(例如水垢、磨损)。建议用近期周期重新录制此配置文件。", - "advisory_unmatchable": "'{name}' 永远无法被匹配:其背后没有任何带功率数据的周期,因此 WashData 没有可与运行中的周期进行比较的对象。请为此程序标记一个周期(或录制一个周期),使其重新参与匹配。", - "advisory_unmatchable_no_duration": "'{name}' 永远无法被匹配:它有带功率数据的周期,但没有可用的时长,因此 WashData 没有可用来衡量匹配的长度。请重新录制此程序,或设置其预期时长,使其重新参与匹配。", + "advisory_duration_trend_up": "'{name}' 的周期越来越长(每个周期约 +{pct}%)。如果设备有变化,请重新录制或重建此配置文件。", + "advisory_energy_trend_up": "'{name}' 每个周期的能耗都在增加(每个周期约 +{pct}%)。如果这不符合预期,请检查设备。", + "advisory_poor_health": "'{name}' 拟合度较低:其近期周期波动较大或匹配较弱。请检查其周期或重新录制,以保持匹配和估算的准确性。", + "advisory_shape_drift": "'{name}' 已偏离其原始功率曲线,可能是水垢或磨损所致。建议用近期周期重新录制。", + "advisory_shape_drift_corr": "'{name}' 已偏离其原始功率曲线(相关系数 {corr}),可能是水垢或磨损所致。建议用近期周期重新录制。", + "advisory_unmatchable": "'{name}' 永远无法匹配:没有任何带功率数据的周期作为依据。请为此程序标记或录制一个周期来解决。", + "advisory_unmatchable_no_duration": "'{name}' 永远无法匹配:其周期没有可用的时长。请重新录制此程序,或设置其预期时长。", "pg_no_alerts": "本次运行未检测到问题。", "pg_override_active": "正在使用您编辑的设置对比当前设置", "pg_history_empty": "按运行以重放您的周期。", "pg_history_summary": "{detected}/{total} 已检测 · {correct} 正确匹配", "pg_sweep_no_metric": "数据不足,无法为此目标评分。", "pg_sweep_intro2": "在您最近的周期中找到最能满足某个目标的设置。在您应用之前不会有任何更改。", - "pg_running_history": "正在重放您的周期…", - "pg_running_sweep": "正在所有周期上测试各个值…", "pg_row_load_hint": "在上方图表中加载此周期", - "pg_history_intro2": "使用上方设置,将您最近的周期通过真实的检测器和匹配器重新运行。点击任意行可在图表中加载该周期;修改设置可查看修改前后的对比。", + "pg_history_intro2": "使用上方设置,用真实的检测器和匹配器重放最近的周期。点击一行可在图表中加载;修改设置可对比前后差异。", "pg_simulating": "正在模拟周期…", - "pg_canvas_empty2": "在上方选择一个周期并按“运行”进行模拟。然后悬停查看数值,滚动缩放,拖动平移。", "pg_apply_settings_confirm": "将这 {n} 项设置保存到该设备吗? {list}", - "pg_stress_terminated_detail": "稳定在约 {idle}W 的待机功率 -- {h} 小时 {m} 分钟后通过 {reason} 结束了周期。", - "pg_stress_above_threshold_detail": "待机功耗约 {idle}W 达到或超过有效停止阈值 ({stop}W) -- 周期从未被判定为静止状态。请提高 stop_threshold_w 以修复。", - "pg_stress_hit_cap_detail": "周期运行了 {h} 小时 {m} 分钟仍未停止 -- 被安全上限强制停止。待机功耗 {idle}W 对比停止阈值 {stop}W。", "no_logs_match": "没有日志记录符合这些筛选条件。", "pg_analysis_hint2": "选择一个周期并点击运行以加载匹配分析。", - "pg_analysis_empty2": "点击运行以查看匹配分析。", - "imported_readonly": "从社区商店导入。用于参考和匹配。不计入您的统计,且无法编辑。", + "imported_readonly": "从社区商店导入。用于参考和匹配,不计入您的统计,且无法编辑。", "merge_no_imports": "导入的录制无法合并到真实周期中。取消选择它们以进行合并。", - "store_enable_hint": "在设置中启用在线功能,以浏览和导入社区参考周期。", - "advisory_duration_outlier": "标记为 '{name}' 的 {n} 个周期与该程序的常见时长相差太远,永远无法与之匹配(最差:{ratio}x)。它们可能被标记错了,或者一次录制包含了两次运行。请重新标记或拆分它们,以免继续扭曲该程序的时间估算。", + "store_enable_hint": "在 偏好设置 > 在线与社区 中开启在线功能,即可浏览和导入社区参考周期。", + "advisory_duration_outlier": "标记为 '{name}' 的 {n} 个周期与其常见时长相差太远,永远无法匹配(最差:{ratio}x)。它们可能标错了,或包含了两次运行。请重新标记或拆分,以免扭曲其时间估算。", "standby_above_stop_title": "周期结束时仍在消耗 {idle} W", "standby_above_stop_sub": "最近 {total} 个周期中有 {n} 个。关闭延迟只有在功率低于停止阈值({stop} W)之后才开始计时,因此周期会结束过晚或需要手动停止。", - "online_intro": "与其他 WashData 用户分享和下载参考周期。所有在线功能均为自愿加入,默认关闭。", "store_share_intro": "上传此参考周期,以便拥有相同家电的其他人使用。它会经过审核后才公开显示。", - "online_intro_global": "与其他 WashData 用户浏览和分享参考录制,并确认家电条目。一次连接适用于您的整个 WashData 集成;家电品牌和型号在“基本配置”下按设备设置。所有在线功能均为自愿加入,默认关闭。", + "online_intro_global": "与其他 WashData 用户浏览和分享参考录制。一次连接适用于您所有的 WashData 设备;品牌和型号在“基本”下按设备设置。在线功能在您开启前保持关闭。", "online_unavailable": "此服务器不支持在线功能。", "store_picker_offline": "在设置齿轮中启用在线功能,即可从社区目录中选择。", "pick_brand_first": "请先选择家电品牌。", - "model_not_found": "尚未收录在目录中。", - "connect_to_confirm": "在设置齿轮中连接以确认或评分。", + "custom_task_limit": "已达到 {max} 个任务的上限。", + "custom_tasks_intro": "添加任何其他想要提醒的事项。任一间隔达到时任务即到期;将其中一个留空或填 0,即只使用另一个。", + "maintenance_due_sensor_hint": "只要有任务到期,“需要维护”二元传感器就会开启,可用于您自己的通知自动化。", + "maintenance_supporter_intro": "Maintenance Supporter 是一个独立的集成,功能更多:支持任何设备或任务,可按时间、使用量或传感器安排计划,提供通知和历史记录。它可以使用 WashData 的周期计数器。", "adopt_settings_hint": "用共享的设置覆盖此设备的检测和匹配阈值。您的通知、实体和电价永远不会更改。", - "include_settings_hint": "分享此设备的识别和匹配阈值(不含通知、实体或电价)。采用者自行选择是否应用。", + "include_settings_hint": "分享此设备的检测和匹配阈值(不含通知、实体或电价)。是否应用由其他用户决定。", "onboard_download": "新设备?从拥有相同设备的其他 WashData 用户处采用现成配置(程序、参考周期和阶段)。", - "share_consent": "我确认这些周期均正常完成,无中断", "share_device_none": "暂无可分享的周期。请先在\"周期\"选项卡中将录制或手动选择的周期标记为参考周期(⭐)。", "share_guideline_naming": "请按设备表盘或显示屏上显示的名称命名每个配置文件(例如:'Cotton 40'、'Eco 60')。", - "share_guideline_quality": "仅分享正常完成的周期 -- 无周期中断、开门事件或电源波动。", + "share_guideline_quality": "仅分享正常完成的周期:无中断、无开门、无电源波动。", "share_guideline_review": "您的上传从待审核状态开始,待足够多的社区成员确认后将公开显示。", "share_guidelines_title": "分享前须知", - "store_download_device_intro": "将所有共享程序及其参考周期采用到您的设备。您自己录制的周期和统计数据不受影响。", + "store_download_device_intro": "将所有共享程序及其参考周期添加到您的设备。您自己的周期和统计数据不受影响。", "store_share_device_intro": "上传 {brand} {model} 及您选择的参考周期。拥有相同设备的其他用户可以采用您的程序。条目在公开前经过审核。", "share_profile_no_cycles": "无参考周期 -- 请在\"周期\"选项卡中将周期标记为 ⭐ 以包含此配置文件", - "advisory_phase_inconsistent": "'{name}' 似乎混合了不同的程序或温度,各周期的加热时长差异很大。将其拆分为独立的配置文件(例如按温度区分)可以改善匹配和时间估算。", - "advisory_phase_inconsistent_title": "⚠ 可能混合了不同程序", - "export_select_intro": "精确勾选要包含的内容。即使选择配置文件而不包含其周期,仍会导出可识别的程序(其学习到的形状会一同迁移)。", - "import_analyze_hint": "加载导出的文件(或粘贴其 JSON)。WashData 会对其进行分析,并在任何更改之前准确显示可导入的内容。", + "export_select_intro": "勾选要包含的内容。不含周期的配置文件仍会导出为可匹配的程序。", + "import_analyze_hint": "加载导出的文件(或粘贴其 JSON)。在做任何更改之前,WashData 会显示可导入的内容。", "analyzing": "正在分析…", - "device_type_mismatch_warn": "此导出文件来自不同的设备类型({src} 对 {local})。程序和周期仍可作为参考数据导入,但设备专属设置和真实历史导入将被禁用。", + "device_type_mismatch_warn": "此导出文件来自不同的设备类型({src} 对 {local})。程序和周期仍可作为参考数据导入,但设备设置和真实历史不能导入。", "merge_hint": "导入的项目会被添加,本地数据不会丢失。名称冲突将在下方解决。", "replace_warn": "每个勾选的类别都会被清除并用文件中的内容替换。未勾选的类别保持不变。", "dest_reference_hint": "导入的周期仅用于改进程序识别,绝不影响用量/能耗统计。", - "dest_real_history_hint": "导入的周期计为本设备自身的历史记录,并纳入能耗/用量统计。适用于将一台设备迁移到新的安装环境。", + "dest_real_history_hint": "导入的周期计为本设备自身的历史和统计。用于将设备迁移到新的安装环境。", "sug_muted": "将不再建议此设置", "sug_mute_failed": "无法屏蔽建议", "sug_unmuted_all": "已重置屏蔽的建议", - "n_suggestions_muted": "已屏蔽 {count} 项,自动调整器将不再提议这些设置。", + "n_suggestions_muted": "已屏蔽 {count} 条建议,自动调整器将不再提议这些建议。", + "n_suggestions_muted_other": "已屏蔽 {count} 条建议,自动调整器将不再提议这些建议。", "font_size_hint": "调整此面板所有内容的字体大小,仅对您在此设备上的账户生效。", - "import_history_description": "在使用 WashData 之前就已经有智能插座了?上传其功率传感器的历史导出文件,或直接从 Home Assistant 读取,常规检测会在这些数据上运行,过去的周期便会出现在您的周期列表中,等待命名。", - "hist_input_hint": "上传从历史面板下载的 CSV(entity、state、last changed),或让 WashData 直接读取该传感器的历史。随后检测会像实时运行时一样在这些数据上运行,并由您选择保留其中哪些周期。", - "hist_recorder_hint": "从您选择的日期读取到现在。Home Assistant 默认保留 10 天的详细历史,之后仅保留每小时平均值,过于粗略,无法用于检测周期;只有当 recorder 配置为保留更长时间时,才选择更早的日期。", - "hist_scanning": "正在通过检测器重放您的历史数据。该任务在后台运行 - 您可以关闭此对话框,稍后再回来查看。", + "import_history_description": "在使用 WashData 之前就有智能插座?上传其功率历史,或从 Home Assistant 读取。系统会运行检测,过去的周期将出现在您的周期列表中,等待命名。", + "hist_input_hint": "上传历史面板中的 CSV(entity、state、last changed),或让 WashData 直接读取传感器历史。系统会像实时一样运行检测,由您选择保留哪些周期。", + "hist_recorder_hint": "从您选择的日期读取到现在。Home Assistant 默认保留 10 天的详细历史;更早的每小时平均值过于粗略,无法检测周期。", + "hist_scanning": "正在后台用检测器处理您的历史数据。您可以关闭此对话框,稍后再回来。", "hist_imported_count": "已导入 {n} 个周期。", "hist_duplicates": "其中 {n} 个此前已导入,已跳过。", "hist_capped": "已达到每台设备可导入周期的上限,其余未保存。", - "hist_next_step": "它们已在您的周期列表中,标记为导入的历史。打开其中一个,用“标记”指定它所属的程序。", + "hist_next_step": "它们已在您的周期列表中,标记为导入的历史。打开其中一个,用“标记”为其程序命名。", "hist_rows_read": "已读取 {n} 条读数", "hist_entity_substituted": "已读取 {used}(此设备配置的是 {wanted})", "hist_breaks": "{n} 处传感器不可用的空缺", @@ -1097,17 +1012,51 @@ "hist_scan_failed": "扫描失败。", "hist_scan_expired": "该次扫描结果已不可用,请重新扫描。", "hist_import_failed": "导入失败。", - "imported_history_readonly": "在导入的功率历史中检测到。它会影响程序匹配,但不计入您的统计,且无法裁剪或拆分。请标记以指定其程序。", - "odometer_intro": "此累计值只会增加。删除某条周期记录不会改变它,并且它会在保存的历史记录上限之外继续累加,因此保养提醒始终准确。", - "odometer_correct_hint": "如果 WashData 记录了从未发生的运行,或者设备在安装 WashData 之前已经运行过周期,请设置此值。", + "imported_history_readonly": "在导入的功率历史中发现。它有助于匹配,但不计入您的统计,且无法裁剪或拆分。请标记以指定其程序。", + "odometer_intro": "此累计值只增不减。删除周期不会使它减少,因此保养提醒始终准确。", + "odometer_correct_hint": "如果 WashData 计入了从未发生的运行,或设备在安装 WashData 之前已运行过周期,请设置此值。", "no_reminders_set": "尚未设置保养提醒。", "toast_program_armed": "已为下一个周期设置程序:{program}", "toast_range_required": "请设置有效的起始/结束范围。", - "no_profiles_option": "(暂无配置文件,请添加)" + "no_profiles_option": "(暂无配置文件,请添加)", + "compare_cycles_title": "比较 {count} 个周期", + "edit_builtin_phase": "这是内置阶段。保存会创建自定义副本;删除副本即可恢复原始阶段。", + "fetch_error": "加载数据失败。", + "connection_lost": "连接已断开,正在显示 {time} 的数据。", + "pg_notify_partial": "标记仅显示开始、即将完成、完成、里程碑和免打扰时段通知。实时更新、卸载提醒和超时警报不会被模拟。", + "connect_to_confirm": "在设置齿轮中连接以确认或评分。", + "coverage_gap_cluster": "最近有 {count} 个未标记周期(约 {min} 分钟)看起来是同一个您尚未创建的程序。", + "pg_label_yes": "周期结束时将被标记为“{name}”", + "pg_label_no": "将请求审查:{reason}", + "live_match_debug_tip": "置信度:当前功率曲线与排名第一的候选配置文件的吻合程度(0-100%)。不明确:两个最佳候选的得分相差不到 5%,在周期结束前标签尚不确定。", + "automation_alias_started": "{device}:周期已开始", + "automation_alias_finished": "{device}:周期已完成", + "automation_desc_started": "{device} 周期开始时运行。请添加您的动作(通知、灯光等)。", + "automation_desc_finished": "{device} 周期完成时运行。请添加您的动作(通知、灯光等)。", + "feedback_detected": "WashData 检测到:{name}", + "import_undo_hint": "已保留上次导入({when})之前的还原点:{profiles} 个配置文件和 {cycles} 个周期。", + "replace_undo_hint": "系统会先保存一个还原点,因此您可以在“导出 / 导入”中撤销此次导入。", + "pg_in_sample_note": "这些周期参与了构建与之匹配的配置文件,因此结果偏乐观:在新周期上,正确匹配会略少一些。", + "hist_tail_trimmed": "已裁掉周期结束后 {min} 分钟的等待时间,与实际运行的集成一致", + "hist_truncated": "在 {n} 条读数的上限处停止,因此其余历史未被扫描", + "hist_warn_kw": "最高读数仅为 {w}。该传感器似乎以千瓦为单位上报;WashData 需要以瓦为单位,因此无法检测周期。", + "pg_compare_profile_tip": "仅选择叠加在图表上的配置文件。模拟始终会像实际运行的集成一样,自行匹配该周期。", + "pg_outcome_stale": "自本次模拟运行以来设置已更改。当前显示的仍是旧结果。", + "pg_sweep_guarded": "会让更多周期提前结束、拆分更多周期,或漏掉当前设置能捕捉到的周期,因此绝不会被推荐。", + "pg_sweep_keep_current": "保持当前值({value},{metric}):没有任何值能至少多改善一个周期。", + "pg_task_busy": "此设备已有历史测试或优化任务正在运行。请等待或将其取消。", + "hist_warn_naive_time": "{n} 个时间戳没有时区,已按 UTC 读取。如果文件使用本地时间,周期会偏移您的 UTC 时差,夏令时切换前后的读数可能会被丢弃。", + "hist_warn_naive_time_other": "{n} 个时间戳没有时区,已按 UTC 读取。如果文件使用本地时间,周期会偏移您的 UTC 时差,夏令时切换前后的读数可能会被丢弃。", + "pg_sweep_n_cycles": "基于 {count} 个周期", + "pg_sweep_n_cycles_other": "基于 {count} 个周期", + "cycle_context_loading": "正在加载记录器历史…", + "cycle_context_note": "灰色:功率传感器在此周期前后的记录器历史。它不属于该周期,不影响时长、能耗或匹配。", + "cycle_context_none": "记录器中没有此周期前后的读数(Home Assistant 默认保留 10 天的历史)。", + "cycle_context_tip": "仅用于显示:显示功率传感器在此周期前后的记录器历史(如仍保留)。若要将失败的启动尝试并入以后的周期,请使用“曲线前导采样”(设置 > 检测 > 周期开始),这会改变存储的时长。", + "invalid_threshold_pair": "停止阈值({stop} W)必须低于启动阈值({start} W)。未保存任何内容;请同时修改这两个值。" }, "pg_desc": { "completion_min_seconds": "计为真实周期的最短运行时间", - "end_repeat_count": "结束前连续的低读数次数", "interrupted_min_seconds": "短周期标记为中断", "min_off_gap": "区分两个周期所需的间隔", "off_delay": "周期结束前低功率持续的秒数", @@ -1115,37 +1064,36 @@ "start_threshold_w": "计为启动的最小瓦数", "stop_threshold_w": "低于此值机器计为关闭", "anti_wrinkle_enabled": "吸收主阶段之后的翻滚脉冲,而不是将其识别为新周期", - "anti_wrinkle_exit_power": "脉冲之间的功率必须低于此值,防皱才能保持活动", + "anti_wrinkle_exit_power": "翻滚脉冲之间的静止水平;仅在高于停止阈值时生效", "anti_wrinkle_idle_timeout": "防皱结束前两次翻滚脉冲之间允许的静止时间", "anti_wrinkle_max_duration": "长于此值的脉冲将结束防皱并开启新周期", "anti_wrinkle_max_power": "高于此值的脉冲将结束防皱并开启新周期", - "dtw_bandwidth": "Stage 3: Sakoe-Chiba 规整带宽 (0 = DTW 关闭; 默认 0.2 = 周期长度的 20%)", - "corr_weight": "Stage 2: 曲线形状(相关性)与功率水平(MAE)之间的平衡; 默认 0.45", - "duration_weight": "Stage 4: 运行时长一致性对最终得分的影响程度(默认 0.22)", - "energy_weight": "Stage 4: 能耗一致性对最终得分的影响程度(默认 0.22)", - "profile_match_min_duration_ratio": "Stage 1: 最短允许运行时长占档案时长的比例; 默认 0.1 (10%)", - "profile_match_max_duration_ratio": "Stage 1: 最长允许运行时长占档案时长的比例; 默认 1.8 (180%)", - "keep_min_score": "Stage 2: 保留在竞争中的最低得分; 默认 0.1 接受弱候选,后续阶段选出最佳", - "dtw_blend": "Stage 3: 0 = 仅核心得分,1 = 仅 DTW 得分,0.5 = 等权混合(默认)", - "dtw_ensemble_w": "Stage 3 (集成模式): 缩放 L1 对导数 DTW 的权重; 默认 0.7 偏重水平感知", - "dtw_ddtw_scale": "Stage 3: DDTW 半饱和距离; 越小对形状越敏感(默认 30)", - "dtw_refine_top_n": "Stage 3: DTW 重新评分的候选数量; 若正确档案排在第 4-5 位则调高至 7-9(默认 5)", - "duration_scale": "Stage 4: 时长一致性得分减半时的对数比率; 越小惩罚越严格(默认 0.175)", - "energy_scale": "Stage 4: 能耗一致性得分减半时的对数比率; 越小惩罚越严格(默认 0.25)", + "profile_match_min_duration_ratio": "最短周期时长占配置文件时长的比例;默认 0.1 (10%)", + "profile_match_max_duration_ratio": "最长周期时长占配置文件时长的比例;默认 1.8 (180%)", "dishwasher_end_spike_quiet_release": "洗碗机:预期时长过后、解除周期结束排水等待之前保持静止的秒数", - "smart_termination_duration_ratio": "匹配程序预期时长中,周期在智能终止可以提前结束之前必须达到的比例;对于取决于负载或温度的机器请调低", - "anti_crease_finalize_ratio": "在可以结束防皱翻滚尾段之前,周期必须达到的匹配程序预期时长的比例;翻滚尾段较长的烘干机请调低,洗衣机保持默认值", + "smart_termination_duration_ratio": "智能终止可以提前结束周期之前,周期必须达到的程序平均时长比例;运行时间波动大时请调低", + "anti_crease_finalize_ratio": "防皱尾段可以结束周期之前,周期必须达到的程序平均时长比例;烘干机请调低,洗衣机保持默认值", "curve_preroll_seconds": "并入周期曲线开头的中止启动尝试读数秒数;0 = 关闭" }, "phase_desc": { "anti_crease": "完成后偶尔进行短暂的翻滚以减少皱纹。", "anti_wrinkle": "周期后定期翻滚以减少皱纹。", + "baking": "加热元件高温工作,形成面包外皮和内瓤。", + "cooking": "加热器和风扇工作的主烹饪阶段。", "cool_down": "接近循环结束时在没有热量的情况下翻滚。", + "cool_down_af": "加热后仅用风扇降温的阶段。", "drain_and_switch": "从洗涤模式到干燥模式的过渡期。", "dry": "使用加热器和/或余热的干燥阶段。", "drying": "主要加热翻滚期。", "heat_up": "完全干燥开始之前加热器初始预热。", + "keep_warm": "以低温保持食物温热的阶段。", + "keep_warm_bm": "烘烤后以低温保持面包温热的阶段。", + "kneading": "电机搅拌并揉制面团。功率较高。", + "pause_af": "用于摇晃或查看的短暂暂停。", + "pre_heat": "正式烹饪前对腔体进行初始加热。", "pre_rinse": "洗涤剂清洗前初步喷洒。", + "proving": "低温发酵阶段,让酵母发酵、面团膨胀。", + "resting": "揉面阶段之间的短暂低功率暂停,让面筋松弛。", "pre_wash": "主洗前的初步浸泡或预处理。", "rinse": "清水冲洗阶段。该阶段可能会重复多次。", "sanitize": "用于消毒程序的高温清洁阶段。", @@ -1158,6 +1106,7 @@ "wash_dw": "主要洗涤剂加热洗涤。" }, "placeholder": { + "custom_task": "例如:清洁门封", "add": "添加…", "group_name": "例如 棉布 2:47", "json_buttons": "[{\"action\":\"ID\",\"title\":\"Label\"}]", @@ -1184,7 +1133,7 @@ }, "section": { "anti_wrinkle": { - "intro": "防皱模式检测主阶段后的低功率翻滚脉冲,并将其附加到完成的周期。", + "intro": "将烘干机在主阶段后的低功率翻滚脉冲保留在已完成的周期中。", "label": "防皱" }, "basic": { @@ -1192,7 +1141,7 @@ "label": "基本" }, "delay": { - "intro": "延迟启动检测识别设备已通电但尚未开始运行的情况。", + "intro": "检测已通电并在等待延迟启动的设备。", "label": "延迟启动" }, "detection": { @@ -1216,153 +1165,145 @@ "label": "计时与看门狗" }, "triggers": { - "intro": "可选的外部信号:结束触发器、门传感器、暂停开关和卸载提醒。", + "intro": "可选附加功能:结束触发器、门传感器、暂停开关和卸载提醒。", "label": "触发器与门" }, - "phase_eta": { - "label": "剩余时间", - "intro": "阶段感知的剩余时间,适用于周期长度取决于温度或脱水的机器。" - }, "dishwasher": { "label": "洗碗机", - "intro": "洗碗机的周期结束处理。洗碗机通常在短暂的最终排水之前,会经历一个近乎无功耗的长时间干燥阶段。" + "intro": "洗碗机的结束处理。洗碗机常在短暂的最终排水前长时间安静干燥。" } }, "setting": { "anti_wrinkle_enabled": { - "doc": "识别烘干机在主要加热阶段后发出的短的低功率翻滚脉冲,并将它们附加到完成的循环,而不是将它们视为新循环。", + "doc": "将烘干机在主加热阶段后的短暂低功率翻滚脉冲保留在已完成的周期中,而不是视为新周期。", "label": "启用防皱检测" }, "anti_wrinkle_exit_power": { - "doc": "脉冲之间的功率必须低于此值,抗皱模式才能保持活动状态。", + "doc": "在防皱模式下,低于此值的功率视为翻滚脉冲之间的静止,静止时间超过最大脉冲间隔即结束该模式。如果停止阈值更高则使用停止阈值,因此只有将此值设置得高于停止阈值时才会有影响。", "label": "退出功率阈值" }, "anti_wrinkle_idle_timeout": { "label": "最大脉冲间隔", - "doc": "防皱模式结束前,机器在两次翻滚脉冲之间可以保持静止的时长。请将其设置为高于烘干机在脉冲之间留出的最长间隔,否则之后的每个脉冲都会被识别为误启动。" + "doc": "防皱模式结束前,烘干机在两次翻滚脉冲之间可以保持静止的时长。请设置为高于烘干机的最长间隔,否则之后的脉冲会被视为误启动。" }, "anti_wrinkle_max_duration": { - "doc": "比这更长的脉冲被视为真正的周期,而不是抗皱滚动。", + "doc": "比此值更长的脉冲视为真正的周期,而不是防皱翻滚。", "label": "最长时长" }, "anti_wrinkle_max_power": { - "doc": "高于此功率的脉冲被视为真正的新周期,而不是抗皱翻滚。设置在翻滚脉冲功率的正上方。", + "doc": "高于此功率的脉冲视为真正的新周期,而不是翻滚。请设置为略高于翻滚脉冲的功率。", "label": "最大防皱功率" }, "auto_label_confidence": { - "doc": "如果周期结束时的匹配分数达到或高于此值,程序将被自动标记,无需任何确认提示。提高它可在自动标记前要求更高的确定性;降低它可实现更多自动化。与下方的学习置信度配合使用。", + "doc": "周期结束时匹配分数达到或高于此值,将不经询问直接标记程序。提高可要求更高的确定性;降低可自动标记更多周期。请保持高于学习置信度。", "label": "自动标记置信度" }, "auto_maintenance": { - "doc": "每晚运行内务管理:重建配置文件包络、重新计算周期运行状况、修剪调试跟踪并保留最新的周期。", + "doc": "夜间清理:为现已明确匹配某个程序的旧周期添加标记,重建程序曲线,刷新异常标注和周期健康,并在「保存调试追踪」关闭时删除调试追踪。周期及其功率记录始终保留。", "label": "自动维护(每夜清理)" }, "completion_min_seconds": { - "doc": "比这个短的周期将被丢弃为幽灵周期(测试运行,打开门添加袜子)。", + "doc": "短于此值的周期(例如测试运行、开门添加袜子)将保存为“已中断”,且不发送完成通知。", "label": "最短周期时长" }, "delay_confirm_seconds": { - "doc": "在设备被视为等待启动而不是运行之前,电源必须保持在待机状态这么长时间。", + "doc": "功率必须在待机范围内保持这么长时间,设备才会被视为等待启动。", "label": "确认秒数" }, "delay_start_detect_enabled": { - "doc": "检测设备何时开机并等待(延迟启动/待机)但尚未开始其周期,因此待机绘制不会被误认为是运行周期。", + "doc": "检测已通电并在等待延迟启动的设备,避免将其待机功耗误认为正在运行的周期。", "label": "启用延迟启动检测" }, "delay_timeout_hours": { - "doc": "经过这么多小时后,停止在延迟启动模式下等待并返回空闲状态,因此保持通电但从未启动的机器不会永远等待。", + "doc": "超过此小时数后放弃等待延迟启动,并返回空闲状态。", "label": "超时小时数" }, "device_type": { - "doc": "家电类。设置针对该设备类型调整的合理检测默认值(阈值、关闭延迟、结束处理);仅当设备最初设置为错误类型时才更改它。", + "doc": "为此类设备设置检测默认值(阈值、关闭延迟、结束处理)。仅当设备被设置为错误类型时才更改。", "label": "设备类型" }, "door_sensor_entity": { - "doc": "可选门二进制传感器。用于检测设备在一个周期后何时打开/卸载。", + "doc": "可选门传感器。用于显示周期结束后设备何时被打开取出物品。", "label": "门传感器实体" }, "duration_tolerance": { - "doc": "对剩余时间估计的容忍度(学习反馈,而不是匹配)。如果实际持续时间在估计值的 +/-X% 范围内,则视为良好匹配。", + "doc": "仅用于学习反馈:在估计时长 +/-X% 以内的周期视为估计准确。不影响匹配。", "label": "估算容差" }, "enable_ml_models": { - "doc": "当一个循环运行时,让模型完善实时结果:更稳定的剩余时间和能量/成本估计,以及结束检测的防过早停止防护(它只能延迟完成,永远不会提前结束,并且是有限的)。如果可用,则使用您的微调模型,否则使用内置模型。关闭 = 仅经典的基于电源的逻辑(仍然可靠)。", + "doc": "使用在此机器上训练的模型预测运行中周期的能耗和费用,并让熟悉的程序在异常提前停止时无需等待完整的关闭延迟即可结束。关闭:仅使用基于功率的逻辑。", "label": "在周期中应用智能模型" }, "end_energy_threshold": { - "doc": "在关闭延迟倒计时期间,累积能量(瓦特 x 时间)与该阈值进行比较。如果超过,倒计时会重置 - 将防皱翻滚和洗碗机干燥尾部连接到循环上,而不是缩短它们。如果冷却过程中循环结束得太早,则提高它;如果检测缓慢,则降低它。", + "doc": "关闭延迟期间允许的能量。超过此值时周期会继续,从而将翻滚和烘干尾段保留在周期内。如果周期在尾段期间结束,请调低;如果已完成的周期一直在等待,请调高。", "label": "结束能耗" }, - "end_repeat_count": { - "doc": "周期结束前所需的连续低于停止阈值的读数数。 1 适用于大多数插头。如果您的智能插头偶尔在周期中报告假零样本并且您的周期提前结束,则提高到 2-3。", - "label": "结束重复次数" - }, "energy_price_entity": { - "doc": "提供当前每 kWh 电价的传感器(例如动态电价)。必须是电价,不能选插座自身的功率或电能实体:它优先于下面的固定价格,因此这里若填 kWh 累计电能计数器,每个周期都会按电表读数而不是你的电价来计费。启用“时间加权成本”后,每个周期按其运行期间每一时刻生效的电价计费;否则按结束时生效的电价冻结。", + "doc": "提供当前每 kWh 电价的传感器(例如动态电价)。它优先于下面的固定价格,因此必须是电价,而不是插座的功率或电能传感器。", "label": "电价实体" }, "energy_price_static": { - "doc": "当上面未设置实时价格实体时,每千瓦时的固定价格用于成本数据。", + "doc": "未设置价格实体时使用的每千瓦时固定价格。", "label": "固定电价(每 kWh)" }, "energy_sensor": { - "doc": "可选的累积电能计数器(total_increasing 的 kWh/Wh,例如插座自带的累计电能表)。设置后,每个周期报告的能耗将取自该计数器从开始到结束的差值(delta),从而避免因积分上报缓慢的功率传感器而导致的少计。若读数缺失、单位未知或差值不为正,则回退到积分值。留空则继续对功率传感器进行积分。", + "doc": "可选的 kWh 或 Wh 计数器(total_increasing,例如插座自带的电能表)。周期能耗将从中读取,比用上报缓慢的功率传感器计算更准确。若读数不可用,则使用计算值。", "label": "电能表实体" }, "expose_debug_entities": { - "doc": "发布额外的诊断 HA 实体(匹配置信度、模糊性、状态内部)。关闭可保持实体列表干净以供正常使用。", + "doc": "添加诊断实体(匹配置信度、模糊度、内部状态)。关闭可保持实体列表简洁。", "label": "暴露调试实体" }, "external_end_trigger": { - "doc": "状态变化标志着周期结束的二进制传感器(例如,设备“完成”接触或配套集成)。", + "doc": "状态变化即表示周期结束的二元传感器(例如“完成”触点或其他集成)。", "label": "外部触发器实体" }, "external_end_trigger_enabled": { - "doc": "除了内置的基于电源的检测之外,还可以让外部二进制传感器发出周期结束信号。", + "doc": "除基于功率的检测外,还允许二元传感器发出周期结束信号。", "label": "启用外部结束触发器" }, "external_end_trigger_inverted": { - "doc": "将触发传感器关闭(而不是打开)视为周期结束信号。", + "doc": "将传感器变为关闭(而非打开)视为结束信号。", "label": "反转外部触发器(关闭时触发)" }, "learning_confidence": { - "doc": "如果匹配分数介于此值和自动标记置信度之间,则会发出反馈通知,请您验证已识别的程序。低于此分数时,匹配过于不确定,不会显示。必须保持在自动标记置信度以下。", + "doc": "为已完成周期打标签所需的最低匹配分数。明确的胜出者会被标记;难分高下时进入“周期”中的审查队列。低于此值时既不标记也不询问。请保持低于自动标记置信度。", "label": "学习置信度" }, "linked_device": { - "doc": "可以选择将此 WashData 设备嵌套在 HA 设备注册表中的另一个设备(例如智能插头)下,显示为“通过...连接”。", + "doc": "可选。在 Home Assistant 中将此 WashData 设备显示在另一个设备(例如智能插座)下,显示为“通过...连接”。", "label": "归入设备" }, "min_off_gap": { - "doc": "如果机器关闭时间少于该时间,则开/关/开序列被视为一个连续周期。防止浸泡程序(机器在洗涤过程中关闭几分钟)被分成两个单独的周期。如果您希望将这些负载算作单独的循环,请将其设置为短于连续负载之间的间隙。设备类型默认值可保护每个设备的典型周期内暂停。", + "doc": "短于此值的关机时段不会拆分周期,因此浸泡暂停仍属于同一次洗涤。请设置为短于连续洗涤之间的间隔,否则它们会合并成一个周期。", "label": "最小关闭间隔" }, "min_power": { - "doc": "视为活动的绝对最小功率。低于此值的读数被视为 0 W(待机),滤除智能插头和待机 LED 的幻象负载。", + "doc": "低于此值的读数视为 0 W(待机)。用于滤除智能插座和待机 LED 的空载功耗。", "label": "最小功率" }, "ml_training_enabled": { - "doc": "定期隔夜研究您审查的循环,并针对该特定机器微调模型。只有当改变在持续的周期中真正取得更好的成绩时,才会保留改变,因此这只能有所帮助或保持不变––永远不会倒退。", + "doc": "定期用本机已完成的周期重新训练能耗模型。只有新模型在测试周期上优于简单估算时才会保留。", "label": "从此机器学习" }, "ml_training_hour": { - "doc": "一天中的当地时间 (0-23) 进行夜间微调。选择一个安静的时间,例如 2 (02:00)。", + "doc": "夜间训练的时刻(0-23)。选择一个空闲时段,例如 2。", "label": "学习时间(小时)" }, "ml_training_interval_days": { - "doc": "每这么多天最多重新检查一次改进。", + "doc": "每隔这么多天最多重新训练一次。", "label": "最多每隔几天检查一次" }, "ml_training_min_cycles": { - "doc": "等到至少记录了这么多周期后再进行微调,这样就有足够的东西可以学习了。", + "doc": "至少记录这么多周期后才开始训练。", "label": "需要的最少周期数" }, "name": { - "doc": "HA 集成列表和设备注册表中显示的显示名称。", + "doc": "此设备在 Home Assistant 中显示的名称。", "label": "设备名称" }, "no_update_active_timeout": { - "doc": "如果运行时这么长时间没有电源更新到达,则假设插头脱机并强制停止以避免僵尸循环。默认 600 秒允许云或网格延迟。", + "doc": "如果周期中这么长时间没有收到功率更新,则认为插座已离线并强制停止周期。默认 600 秒可容许云端或网状网络的延迟。", "label": "无更新超时" }, "notify_before_end_minutes": { @@ -1370,35 +1311,35 @@ "label": "结束前提醒" }, "notify_channel": { - "doc": "开始/实时消息的 Android 通知通道名称(控制移动应用程序上的每个通道声音和优先级)。 Blank 使用配套应用程序默认值。", + "doc": "用于开始和实时消息的 Android 通道;声音和优先级由通道决定。留空:应用默认值。", "label": "Android 频道(启动/实时)" }, "notify_cycle_timers": { - "doc": "在一个周期的特定分钟发出通知(例如添加柔软剂)。消息支持{device}、{program}、{minutes}。启用“自动暂停”以在该点暂停并通过“恢复”按钮接收交互式通知;通过面板、暂停/恢复服务或通知操作恢复。", + "doc": "在周期进行到设定分钟时发送通知(例如添加柔顺剂)。消息支持 {device}、{program}、{minutes}。启用“自动暂停”时,周期也会在该点暂停,通知中会带有“继续”按钮。", "label": "周期计时器" }, "notify_finish_channel": { - "doc": "完成消息的 Android 通知通道名称。空白重用开始/直播频道。", + "doc": "用于完成消息的 Android 通道。留空则沿用开始/实时通道。", "label": "Android 频道(结束)" }, "notify_finish_message": { - "doc": "循环结束时发送的正文。模板变量:{device}、{duration}、{minutes}、{program}、{energy_kwh}、{cost}、{time_finished}、{vs_typical}、{cycle_count}。此处 {time_finished} 和 {vs_typical} 最为有用。", + "doc": "周期结束时发送的正文。变量:{device}、{program}、{duration}(分钟)、{duration_hm}(1 h 05 min)、{energy_kwh}、{cost}、{time_finished}、{vs_typical}、{cycle_count}、{status}(completed 或 force_stopped;被中断的周期不发送任何内容)。使用其他变量时将改用默认文本。", "label": "结束消息" }, "notify_finish_services": { - "doc": "当一个周期结束时调用notify.*服务。每个目标添加一个;留空表示没有完成通知。", + "doc": "周期结束时调用的 notify.* 服务。留空表示不调用。", "label": "结束服务" }, "notify_fire_events": { - "doc": "还在循环开始/结束时触发 ha_washdata_* 事件,以便您可以构建自己的自动化。", + "doc": "在周期开始和结束时同时触发 ha_washdata_* 事件,供您自己的自动化使用。", "label": "为通知触发 HA 事件" }, "notify_icon": { - "doc": "通知上显示的小图标(例如 mdi:washing-machine)。Android 会将其绘制在状态栏中;iOS 则用它代替应用图标,需要配套应用 2026.8 或更高版本。留空则使用平台默认值。", + "doc": "通知上的小图标(例如 mdi:washing-machine)。仅发送到 mobile_app 目标,不发送到 notify 组。在 iOS 上它会替代应用图标,需要配套应用 2026.8 或更高版本。留空:平台默认值。", "label": "通知图标" }, "notify_live_chronometer": { - "doc": "在通知中显示实时更新的倒计时器(在支持它的平台上),而不是静态估计。", + "doc": "在通知中显示实时倒计时(如支持),而不是固定的估计值。", "label": "使用实时计时器" }, "notify_live_interval_seconds": { @@ -1406,143 +1347,135 @@ "label": "实时更新间隔" }, "notify_live_overrun_percent": { - "doc": "如果周期超出其估计值超过此百分比,则发送超限警报。", + "doc": "周期超出预期时长达到此百分比后,实时更新停止(20 = 最多为预期时间的 1.2 倍)。", "label": "实时超时 % 提醒前" }, "notify_live_services": { - "doc": "在循环运行时,notify.* 服务需要实时更新进度。留空以禁用实时进度通知。", + "doc": "周期运行时用于实时进度更新的 notify.* 服务。留空则关闭。", "label": "实时进度服务" }, "notify_only_when_home": { - "doc": "仅当至少一名关联人员(上图)在家时才发送通知。", + "doc": "仅当上方至少一人在家时才发送通知。", "label": "仅在家时通知" }, "notify_people": { - "doc": "person.* “Notify Only When Home”使用的实体来决定是否有人在家。", + "doc": "“仅在家时通知”所检查的 person.* 实体。", "label": "人员(仅在家时通知用)" }, "notify_pre_complete_message": { - "doc": "临近结束 / 即将完成提醒的正文。模板变量:{device}、{duration}、{minutes}、{program}、{energy_kwh}、{cost}、{time_finished}、{vs_typical}、{cycle_count}。", + "doc": "每次实时进度更新的正文。模板变量:{device}、{program}、{minutes}(剩余分钟数)。使用其他变量时将改用默认文本。", "label": "完成前消息" }, "notify_reminder_message": { - "doc": "仍在等待的取出衣物提醒的正文。留空则使用内置默认值。模板变量:{device}、{duration}、{minutes}、{program}、{energy_kwh}、{cost}、{time_finished}、{vs_typical}、{cycle_count}。", + "doc": "仅发送一次的结束前提醒的正文。模板变量:{device}、{program}、{minutes}(结束前提醒的设置值)。使用其他变量时将改用默认文本。", "label": "提醒消息" }, "notify_start_message": { - "doc": "循环开始时发送的正文。模板变量:{device}、{duration}、{minutes}、{program}、{energy_kwh}、{cost}、{time_finished}、{vs_typical}、{cycle_count}。", + "doc": "周期开始时发送的正文。模板变量:{device}、{program}。使用其他变量时将改用默认文本。", "label": "启动消息" }, "notify_start_services": { - "doc": "循环开始时调用的notify.*服务。每个目标(电话、仪表板等)添加一个;留空表示没有开始通知。", + "doc": "周期开始时调用的 notify.* 服务(手机、仪表板等)。留空表示不调用。", "label": "启动服务" }, "notify_timeout_seconds": { - "doc": "在这么多秒后自动关闭通知(在支持它的平台上)。 0 保留它直到手动关闭。", + "doc": "在这么多秒后关闭通知(如支持)。0 表示保留到您手动关闭。", "label": "自动关闭延迟" }, "notify_title": { - "doc": "通知标题。模板变量:{device}、{program}、{duration}、{energy}、{cost}、{date}、{time}、{minutes}。", + "doc": "通知标题。仅替换 {device};使用其他变量时将改用默认标题。", "label": "通知标题" }, "notify_unload_delay_minutes": { - "doc": "一个周期结束后几分钟,发送仍在等待的“卸载机器”提醒。设置 0 禁用提醒。", + "doc": "周期结束后多少分钟发送“请取出物品”提醒。0 表示禁用。", "label": "卸载提醒延迟" }, "notify_unload_repeat": { - "doc": "每隔 \"卸载提醒延迟\" 分钟持续重新发送卸载提醒,直到卸载被确认或您点按通知上的 \"停止提醒\"。需要一种确认卸载的方式:门传感器实体、卸载确认实体,或下方的 \"手动确认卸载\" 选项。关闭按钮适用于 Home Assistant 配套应用(移动端)通知。", + "doc": "每隔“卸载提醒延迟”分钟重新发送卸载提醒,直到卸载被确认或您点按“停止提醒”(移动应用)。需要门传感器实体、卸载确认实体或“手动确认卸载”。", "label": "重复直到卸载" }, "off_delay": { - "doc": "断电后宣告周期结束之前等待的时间。如果在此窗口内恢复供电,则循环将无缝继续 - 这会在清洗阶段之间建立暂停的桥梁。洗碗机具有较长的干燥阶段(关闭电源 20-60 分钟),因此关闭延迟必须超过该时间才能保持整个洗涤+干燥为一个循环。", + "doc": "功率必须保持低位多长时间,周期才算结束。在此时间内功率恢复则周期继续。洗碗机干燥时可能静止 20-60 分钟,因此请设置得比这更长。", "label": "关闭延迟" }, "pause_cuts_power": { - "doc": "当循环暂停时,还要关闭下面的开关实体。仅适用于插头可以在周期中安全切断的电器。", + "doc": "周期暂停时,同时关闭下方的开关实体。仅适用于可在周期中途安全断电的设备。", "label": "暂停同时断电(通过开关)" }, "power_off_delay": { - "doc": "周期完成后,在状态恢复为关闭之前,功率必须持续低于电源关闭阈值的时长。仅在电源关闭阈值大于 0 时使用。此项按后台周期检查,因此实际延迟会向上取整到下一个状态过期时刻。", + "doc": "周期结束后,功率必须低于电源关闭阈值多长时间,状态才会恢复为关闭。仅在该阈值大于 0 时使用。系统定期检查,因此实际等待可能稍长。", "label": "电源关闭延迟" }, "power_off_threshold_w": { - "doc": "基于功率的关闭检测(可选)。当设置为大于 0 时,周期完成后,如果功率在电源关闭延迟期间持续低于此值,则机器被视为已关机,状态会恢复为关闭。保持为 0 可禁用此功能(默认值)。请将其设置为高于真正关机时的最低功率,且低于停止阈值以及机器完成后仍开机时的待机功耗;如果它不低于停止阈值,则会被忽略。启用后,它将取代进度重置延迟来恢复为关闭状态,因此已完成的机器会保持在已完成/干净的状态,直到实际断电为止。", + "doc": "周期结束后,功率在电源关闭延迟期间低于此值时,将状态设为关闭,取代进度重置延迟。请设置为高于关机时的功耗,低于待机功耗和停止阈值。0 表示禁用(默认)。", "label": "电源关闭阈值" }, "power_sensor": { - "doc": "报告该设备的实时功率(以瓦为单位)的传感器实体(例如sensor.washer_power)。所有周期检测均基于该信号。", + "doc": "以瓦为单位报告此设备实时功率的传感器(例如 sensor.washer_power)。所有检测都基于它。", "label": "功率传感器" }, - "profile_duration_tolerance": { - "doc": "匹配过程中使用的配置文件平均持续时间周围的 +/- 范围。 0.25 表示 60 分钟的配置文件与 45-75 分钟的循环匹配。", - "label": "运行档案时长容差" - }, "profile_match_interval": { - "doc": "在运行周期中尝试配置文件匹配的频率。默认 300 秒(5 分钟)平衡检测速度和 CPU。", + "doc": "将运行中的周期与配置文件匹配的频率。默认 300 秒(5 分钟)。", "label": "匹配间隔" }, "profile_match_max_duration_ratio": { - "doc": "相对于轮廓的最大循环长度。 1.3 表示周期必须低于配置文件持续时间的 130% 才能匹配。", + "doc": "当周期运行时间超过程序典型时长的此倍数时,该程序将被排除。1.8:比程序长 80% 的周期不再与其匹配。", "label": "最大时长比例" }, "profile_match_min_duration_ratio": { - "doc": "相对于轮廓的最小周期长度。 0.9 表示一个周期必须至少为配置文件持续时间的 90% 才能匹配。", + "doc": "只有当周期运行达到程序典型时长的这一比例后,才会考虑该程序。0.10:2 小时的程序在 12 分钟后成为候选。周期的前 15 分钟内不适用。", "label": "最小时长比例" }, "profile_match_threshold": { - "doc": "周期结束时接受程序识别所需的最低相似度分数 (0-1)。提高它可以减少错误识别;如果您的机器的程序不匹配,请降低它。默认值 0.4 是一个保守的起点。", + "doc": "智能终止或防皱收尾提前结束周期所需的实时匹配分数。不会改变显示的程序。如果弱匹配导致周期过早结束,请调高;如果明确的匹配仍要等待关闭延迟,请调低。", "label": "匹配阈值" }, "profile_unmatch_threshold": { - "doc": "如果周期运行期间的实时匹配分数低于此分数,则临时识别结果将被清除。请将其保持在匹配阈值稍下方,这样相似度的短暂下降不会使显示退回到未匹配状态。", + "doc": "实时匹配分数低于此值时,当前的程序猜测将被清除。请设置得比匹配阈值稍低,以免短暂下降导致重置。", "label": "不匹配阈值" }, "progress_reset_delay": { - "doc": "完成后,将进度保持在 100% 一段时间,以便在重置为“空闲”之前在仪表板上可以看到“已完成”。", + "doc": "周期结束后,进度保持 100% 的时长,之后重置为“空闲”。", "label": "进度重置延迟" }, "pump_stuck_duration": { - "doc": "泵在被标记为可能卡住之前可以连续运行的秒数(触发泵卡住事件)。", + "doc": "泵连续运行多长时间后触发泵卡住事件。", "label": "泵卡住时长" }, "sampling_interval": { - "doc": "传感器读数之间的预期时间 - 用于调整平滑窗口的大小并正确启动去抖。无论该值如何,都会捕获每个传感器更新;它仅校准下游计算。建议引擎会测量传感器过去周期的实际踏频并自动设置。", + "doc": "两次处理读数之间的最短时间。更早到达的读数会被跳过(周期中的低读数始终保留)。请设为插座的上报间隔或更低;更高的值只会丢弃读数。", "label": "采样间隔" }, "save_debug_traces": { - "doc": "存储每个周期的完整功率跟踪和匹配调试数据。对于故障排除很有用,但会增加存储大小。", + "doc": "同时为每个周期存储匹配器的调试数据(每个候选的评分),便于故障排除。每个周期约增加 20 KB。", "label": "保存调试追踪" }, "smart_termination_duration_ratio": { - "doc": "指定在功率下降后、智能终止可以提前结束一个周期之前,该周期必须进行到匹配程序预期时长的多大比例。预期时长是该程序的平均值,因此对于运行时间波动很大的家电(冬季冷进水与夏季暖进水的洗衣机、传感器烘干的烘干机、取决于负载的程序),大约一半的运行会比该平均值更早结束,从而无法获得快速结束,只能通过后备超时晚几分钟才结束。对于这些机器,请调低此值(例如 0.85),使提前结束仍然生效;若要更保守,则将其调高至接近 1.0。留空则使用默认值(0.98,洗碗机为 0.99)。它只能让周期更早结束,绝不会更晚,并且在匹配结果不明确或置信度较低时绝不会触发。", + "doc": "智能终止可以提前结束周期之前,周期必须达到的程序平均时长比例。如果运行时间波动(水温、传感烘干、负载量),请调低(例如 0.85)。留空则为 0.98(洗碗机为 0.99)。", "label": "智能终止比率" }, - "smoothing_window": { - "doc": "原始功率信号被平滑了多少。低 (2) 响应灵敏,但噪音较大;高 (5) 可平滑尖峰,但会增加滞后。", - "label": "平滑窗口" - }, "start_duration_threshold": { - "doc": "电源必须保持在启动阈值以上这么长的时间才能确认真正的启动,从而防止瞬间开/关切换启动一个周期。", + "doc": "功率必须在启动阈值以上保持这么长时间,周期才会开始,因此短暂的开/关波动会被忽略。", "label": "启动时长" }, "start_energy_threshold": { - "doc": "设备在运行前必须消耗的能量(功率 x 时间)。短暂的高功率尖峰能量非常低,会被忽略,从而防止误启动。", + "doc": "设备必须消耗这么多能量,周期才算正在运行。短暂的功率尖峰能量很少,因此不会启动周期。", "label": "启动能耗" }, "start_threshold_w": { - "doc": "功率必须高于此水平才能确认周期已开始。设置太低会导致待机功率误启动;过高且启动缓慢的程序(冷填充)会被错过。建议引擎将其设置为略高于机器观察到的最低有功功率。", + "doc": "功率必须高于此值才会开始周期。过低:待机功率会导致误启动。过高:会漏掉启动缓慢的程序(冷水进水)。", "label": "启动阈值" }, "stop_threshold_w": { - "doc": "在关闭延迟倒计时开始之前,功率必须降至此水平以下。将其设置为低于启动阈值 - 它们之间的间隙是防止闪烁的滞后带。如果设置得太高,低功率阶段(漂洗保持、防皱)会错误地触发结束序列。", + "doc": "功率必须降到此值以下,关闭延迟才开始计时。请保持低于启动阈值。过高时,低功率阶段(漂洗保持、防皱)会误启动结束倒计时。", "label": "停止阈值" }, "switch_entity": { - "doc": "可选开关在暂停时关闭,在恢复时重新打开,与“暂停也会切断电源”一起使用。", + "doc": "与“暂停同时断电”配合使用的可选开关,暂停时关闭,恢复时重新打开。", "label": "开关实体" }, "watchdog_interval": { - "doc": "后台看门狗检查停滞的传感器和超时的频率。默认 30 秒。", + "doc": "后台检查停滞传感器和超时的频率。默认 30 秒。", "label": "看门狗间隔" }, "notify_milestone_message": { @@ -1550,139 +1483,94 @@ "label": "里程碑消息" }, "notify_milestones": { - "doc": "以逗号分隔的周期计数,达到时触发一次性庆祝通知(例如 50、100、500、1000)。留空则禁用里程碑通知。", + "doc": "触发一次性庆祝通知的周期数,以逗号分隔(例如 50、100、500、1000)。留空则禁用。", "label": "周期里程碑" }, "notify_quiet_end_hour": { - "doc": "免打扰时段的结束时间(0-23)。暂存的通知将在此时段发送。留空则禁用。", + "doc": "免打扰时段的结束时间(0-23);暂存的通知将在此时发送。留空则禁用。", "label": "免打扰结束" }, "notify_quiet_start_hour": { - "doc": "免打扰时段的开始时间(0-23)。在免打扰时段内本应触发的完成、提醒和衣物已洗好通知将被暂存,并在时段结束时发送。留空则禁用。支持跨越午夜的时段(例如开始 22,结束 7)。", + "doc": "免打扰时段的开始时间(0-23)。此时段内的完成、提醒和衣物已洗好通知会暂存到时段结束。可跨越午夜(例如 22 到 7)。留空则禁用。", "label": "免打扰开始" }, "peak_rate_message": { - "doc": "附加到启动通知的高峰电价提示的可选自定义文本。模板变量:{device}、{price}。留空则使用内置默认值。", + "doc": "高峰电价提示的自定义文本。模板变量:{device}、{price}。留空则使用默认值。", "label": "高峰电价消息" }, "peak_rate_threshold": { - "doc": "当周期开始且当前每千瓦时电价达到或超过此值时,在启动通知中附加高峰电价提示。0 或留空则禁用该提示。", + "doc": "周期开始时如果每 kWh 电价达到或高于此值,则在启动通知中添加高峰电价提示。0 或留空则禁用。", "label": "高峰电价阈值(每千瓦时)" }, - "dtw_bandwidth": { - "label": "形状匹配容差", - "doc": "匹配器为了贴合已保存程序的形状,可以对周期进行多大程度的时间扭曲。数值越高越能容忍速度变化;过高则可能匹配到错误的程序。" - }, - "corr_weight": { - "label": "形状与水平权重", - "doc": "在为匹配评分时,曲线形状与绝对功率水平之间的平衡。数值越高越偏重形状;越低越偏重功率水平的吻合。" - }, - "duration_weight": { - "label": "时长权重", - "doc": "典型运行时长与此周期吻合的程序会被优先选择的程度。" - }, - "energy_weight": { - "label": "能耗权重", - "doc": "典型能耗与此周期吻合的程序会被优先选择的程度。" - }, "store_brand": { "label": "家电品牌", - "doc": "可选。从社区目录中选择的家电品牌。用于查找和分享匹配的参考录制。如果您不使用在线功能,请留空。" + "doc": "可选。从社区目录中选择的家电品牌,用于查找和分享参考录制。如果不使用在线功能,请留空。" }, "store_model": { "label": "家电型号", - "doc": "可选。设置品牌后,从社区目录中选择的家电型号。如果目录中没有您的型号,您可以将其添加到目录。" + "doc": "可选。从社区目录中选择的家电型号(请先选择品牌)。如果未列出,可以自行添加。" }, "show_contributor": { "doc": "在社区家电和参考周期上显示\"by \"的贡献者署名。" }, - "enable_phase_matching": { - "label": "阶段感知的剩余时间", - "doc": "将每个运行中的周期分解为多个阶段(加热、洗涤、脱水),按阶段分配剩余时间,并与经典估算相融合:周期早期更依赖阶段分配,接近结束时更依赖经典估算。这会根据您的机器实际加热和运行的时长来个性化倒计时,在周期的前半段效果最明显。关闭 = 仅使用经典估算。仅影响剩余时间显示;程序匹配和周期检测保持不变。" - }, - "keep_min_score": { - "label": "最低匹配得分", - "doc": "候选档案留在匹配竞争中所需的最低相似度得分。默认 0.1 有意设置得较宽松,允许弱候选也进入,由后续阶段找出最佳匹配。提高此值可更早淘汰不合适的档案;降低此值可扩大初始候选池。" - }, - "dtw_blend": { - "label": "规整混合", - "doc": "时间规整(DTW)对齐得分替换 Stage 2 核心得分的程度。0 = 仅使用 Stage 2 得分,1 = 仅使用 DTW 得分,0.5(默认)= 等权混合。当程序功率水平相近但时序模式不同时,可提高此值以更多依赖规整对齐。" - }, - "dtw_ensemble_w": { - "label": "规整集成混合", - "doc": "在集成 DTW 模式(默认)下,缩放 L1 与导数 DTW(DDTW)的权重分配。1.0 = 全部缩放-L1,0 = 全部 DDTW,0.7 = 默认。缩放-L1 变体比较功率水平;导数变体对功率如何随时间变化做出响应。集成模式将二者结合。" - }, - "dtw_ddtw_scale": { - "label": "导数规整尺度", - "doc": "导数 DTW 评分的半饱和距离。当 DDTW 距离等于此值时,得分减半。越小对形状差异越敏感。默认 30 针对典型家电功率曲线进行了校准。仅影响集成和 ddtw DTW 模式。" - }, - "dtw_refine_top_n": { - "label": "规整精化数量", - "doc": "DTW 重新评分的 Stage 2 顶部候选数量。由于 DTW 计算代价较高,仅对前 N 个候选应用。默认 5。若正确档案有时在 Stage 2 后只排到第 4 或第 5 位,可调高此值(至 7-9),DTW 能将其拉回。降低此值可略微加速匹配。" - }, - "duration_scale": { - "label": "时长锐度", - "doc": "Stage 4 时长一致性惩罚的锐度。这是一致性得分减半时的对数比率。越小越严格:时长不匹配的惩罚越大。默认 0.175 对应半权重下约 18% 的时长容差。请与时长权重配合使用。" - }, - "energy_scale": { - "label": "能耗锐度", - "doc": "Stage 4 能耗一致性惩罚的锐度。越小越严格:能耗不匹配的惩罚越大。默认 0.25 有意比时长锐度更宽松,因为能耗会随负荷变化。请与能耗权重配合使用。" - }, "power_profile_interval_min": { "label": "电力分布间隔", - "doc": "外部规划工具(如 EMHASS 和 tibber_prices)使用的 per-profile power_profile 传感器属性(每槽平均瓦数的数组)的桶大小。较小的桶保留短暂功率尖峰的细节,较大的桶平滑形状。默认 15 分钟。仅在读取时使用,不影响检测。" + "doc": "EMHASS、tibber_prices 等规划工具使用的 power_profile 属性(每槽平均瓦数)的桶大小。较小值保留短暂尖峰;较大值使曲线更平滑。默认 15 分钟。不影响检测。" }, "notify_live_silent": { "label": "静默实时更新", - "doc": "仅限 iOS。安静地刷新实时进度:每次更新都不会发出声音或震动,并以更省电的较低优先级送达,iOS 可能会将其短暂合并后再显示。每个周期的第一次更新(即启动实时活动的那次)以及开始/完成通知仍会照常提醒。关闭此选项可在每次更新时都收到提醒。" + "doc": "仅限 iOS。以省电优先级静默更新实时进度,iOS 可能会合并显示。第一次更新以及开始/完成通知仍会提醒。关闭:每次更新都提醒。" }, "notify_live_sticky": { "label": "点击后保留实时通知", - "doc": "仅限 Android。将实时进度通知设置为固定(常驻),点击时不会关闭正在进行的通知线程。关闭则恢复默认行为,点击后通知消失。" + "doc": "仅限 Android。点击实时进度通知后仍保留。关闭:点击即关闭通知。" }, "notify_live_click_action": { "label": "通知点击目标", - "doc": "点击此家电的通知时打开的位置,在 Android 和 iOS 上均适用。留空则打开显示此家电的 WashData 面板。若要跳转到其他位置,请输入仪表板路径(例如 /lovelace/laundry)或完整 URL;若不需要任何点击目标,请输入“none”。" + "doc": "点击此家电通知时打开的内容。留空:此家电的 WashData 面板。可输入仪表板路径(例如 /lovelace/laundry)或 URL,输入“none”则点击无动作。" }, "door_opens_at_end": { "label": "结束时自动开门", - "doc": "适用于周期结束后自动弹开舱门进行干燥的洗碗机(AirDry 等功能)。开启后,正在运行的周期中门被打开不会导致周期永久暂停;若门持续敞开达到下方设定的停留时间,WashData 将视为周期已完成。短暂开门(例如放入餐具)将被忽略。需要配置门传感器实体。" + "doc": "适用于结束时开门干燥的洗碗机(例如 AirDry)。门保持打开达到下方的停留时间后,将结束周期而不是暂停;短暂开门会被忽略。需要门传感器实体。" }, "door_end_dwell_seconds": { "label": "门开放终止停留时间", - "doc": "在「结束时自动开门」开启时,WashData 终止周期前门需保持打开的时长。设置得足够长以忽略快速放入餐具的短暂开门(默认 60 秒),同时足够短以在机器弹开舱门后迅速结束。" + "doc": "「结束时自动开门」开启时,周期结束前门需保持打开的时长。应足够长,以忽略放入餐具时的开门(默认 60 秒)。" }, "dishwasher_end_spike_quiet_release": { "label": "自然干燥静止释放", - "doc": "当周期超过其预期时长后,洗碗机必须保持静止(低于停止阈值)多长时间,WashData 才会停止等待最终排水并结束该周期。如果您的机器在较晚的最终排水之前有一个较长的近乎无声的干燥阶段,导致该次排水被漏检,请调高此值 - 更宽的窗口可让学习到的时长跟随季节性漂移(进水更冷 = 周期更长),而不是锁定在旧的平均值上。它只能相对于内部 30 分钟的结束尖峰上限缩短等待时间,绝不会延长。" + "doc": "超过预期时长后,洗碗机必须保持低于停止阈值多长时间,WashData 才会停止等待最终排水。如果长时间干燥后的较晚排水被漏检,请调高。等待时间绝不会超过 30 分钟。" }, "profile_evidence_sources": { "label": "用于构建程序的周期", - "doc": "用于构建每个程序的功率曲线,并将已完成的周期与其匹配的周期种类。取消勾选某一种类会使其不再影响您的程序,但不会删除任何内容 - 这些周期仍留在周期列表中,仍可标记或删除。如果您不信任导入的数据,此选项很有用。统计不受影响:统计始终只计入本机实际运行过的周期。取消勾选全部会被忽略,因为没有任何周期作为依据的程序永远无法匹配。" + "doc": "哪些种类的周期用于构建每个程序的曲线和匹配。取消勾选不会删除任何内容;适用于不信任导入数据的情况。统计始终只计入本机的周期。全部取消勾选会被忽略。" }, "energy_price_dynamic": { "label": "时间加权成本", - "doc": "按周期运行期间每一时刻生效的电价计费,而不是只用结束时的单一价格。仅对价格实体生效(固定价格不会变动)。无需额外设置:周期运行时会实时跟踪电价,诊断下的“处理历史”会根据 recorder 的记录重新计算旧周期的费用。关闭 = 传统方式,按结束时的单一价格冻结。" + "doc": "按周期运行期间每一时刻的电价计费,而不是结束时的价格。需要价格实体。诊断中的“处理历史”会重新计算旧周期的费用。" }, "anti_crease_finalize_ratio": { "label": "防皱收尾比率", - "doc": "周期必须走到匹配程序预期时长的多大比例,WashData 才可以判定当前看到的低功率翻滚是防皱尾段并就此结束周期。预期时长是该程序的平均值,因此对于按衣物量决定传感烘干时间的烘干机,大多数运行都达不到它的 98%,尾段会一直挂到备用超时才结束,常常比机器实际停止晚 15 到 40 分钟。这类烘干机请调低此值(例如 0.75)。洗衣机请保持默认值:洗衣机的大部分周期都低于“最大防皱功率”,因此这个比例是区分安静的洗涤中段与尾段的唯一依据,调低它可能会把一次洗涤从中间截断。留空表示使用默认值(0.98)。本设置与“智能终止比率”不同,后者控制的是另一项提前结束检查。" + "doc": "周期必须达到程序平均时长的多大比例,其低功率翻滚才算作防皱尾段。对于按负载感应的烘干机请调低(例如 0.75)。洗衣机请保持默认值(0.98),否则洗涤可能中途结束。" }, "curve_preroll_seconds": { "label": "曲线前导采样", - "doc": "WashData 最多可以把中止的启动尝试中多少秒的读数并入周期曲线的开头。周期曲线通常从最终成功的那次启动尝试开始,因此对于需要试探几次才稳定下来的机器(选择程序、门锁、首次进水),每条曲线都会缺少真实运行的最初几分钟。请把此值设得略大于您的机器最长的一次起步过程(多数情况下 300 秒绰绰有余)。0 表示关闭。请注意,开启后记录的开始时间会提前,因此在更改前后记录的周期,即使是同一个程序,时长也会不同;在较早的周期被淘汰之前,学习到的平均值会一直存在偏差。" + "doc": "对于启动前会多次试探的机器(门锁、首次进水),将失败启动尝试的读数加到周期开头。多数情况下 300 秒足够;0 = 关闭。记录的开始时间会提前,因此学习到的时长会在一段时间内出现偏差。" }, "unload_confirm_entity": { "label": "卸载确认实体", - "doc": "可选实体,其触发即表示物品已被取出,适用于无法安装门传感器的机器。任何类型都可以:Zigbee 或 NFC 按钮、input_button 辅助元素、机器前方的运动传感器、场景。除了关闭和变为不可用之外,每次状态变化都算作一次确认。设置此项后,即使设备没有门传感器实体也会启用卸载提醒。" + "doc": "可选。用于确认物品已取出的任意实体,适用于没有门传感器的机器(按钮、input_button、运动传感器、场景)。除关闭或变为不可用外,每次状态变化都算作确认。同时会启用卸载提醒。" }, "unload_track_without_door": { "label": "手动确认卸载", - "doc": "在既没有门传感器实体也没有卸载确认实体的设备上运行卸载提醒,改为通过您自己的自动化来确认卸载:按下 \"标记为已卸载\" 按钮实体,或调用 ha_washdata.mark_unloaded 服务。已设置门传感器实体时将忽略此项,因为它已经提供了该信号。" + "doc": "在没有门传感器或确认实体的情况下运行卸载提醒。通过您自己的自动化,用“标记为已卸载”按钮或 ha_washdata.mark_unloaded 服务确认卸载。已设置门传感器实体时将忽略此项。" }, "notify_icon_color": { "label": "通知颜色", - "doc": "此家电通知使用的强调色,让洗衣机、烘干机和洗碗机一眼就能区分开。iOS 会为图标以及锁定屏幕和灵动岛上的实时活动进度条着色。Android 也会收到同样的颜色,但如何处理取决于手机:有些会为图标着色,也有不少(尤其是 Samsung One UI)完全忽略。十六进制(例如 #03A9F4)两者都支持。留空则使用平台默认值。" + "doc": "此家电通知的强调色,便于区分洗衣机、烘干机和洗碗机。在 iOS 上它会填充图标背后的圆形,并为实时活动进度条着色。许多 Android 手机(尤其是 Samsung)会忽略它。使用十六进制,例如 #03A9F4。留空:平台默认值。" + }, + "share_name": { + "doc": "将您的账户名称显示为所分享周期的上传者。关闭:分享时不显示名称。" } }, "setting_group": { @@ -1710,9 +1598,6 @@ "door_pause": { "label": "门与暂停" }, - "dtw_refinement": { - "label": "DTW 细化" - }, "duration_gates": { "label": "时长门控" }, @@ -1789,13 +1674,18 @@ "rebuilding": "重建中…", "analyzing": "分析中…", "resetting": "重置中…", - "reverting": "还原中…", "trimming": "裁剪中…", "splitting": "拆分中…", + "stalled": "停滞", "deleting": "删除中…", "imported": "已导入", "preparing": "正在准备…", - "unlabelling": "取消标记中…" + "unlabelling": "取消标记中…", + "working": "处理中…", + "match_uncertain": "不确定:{top} 或 {runner}", + "match_uncertain_single": "不确定:可能是 {top}", + "match_sure": "约 {pct}% 把握", + "match_uncertain_tip": "WashData 尚未确定程序。该数值表示在过去的周期中,领先猜测 {top} 领先这么多时猜对的频率。仅供参考。" }, "tab": { "advanced": "高级", @@ -1826,13 +1716,17 @@ }, "toast": { "catalog_refreshed": "社区目录已刷新", + "task_name_required": "请为任务命名", + "task_added": "已添加任务", + "task_save_failed": "无法保存任务:{error}", + "task_removed": "已移除任务", + "maint_logged": "已记录为完成:{task}", "access_saved": "已保存访问控制", "all_wiped": "所有数据已清除", "analysis_complete_none": "分析完成:没有新建议", "auto_label_complete": "自动标记完成", "automation_deleted": "自动化已删除", "correction_submitted": "修正已提交", - "cycle_deleted": "周期已删除", "cycle_force_stopped": "周期已强制停止", "cycle_labelled": "周期已标记", "cycle_paused": "周期已暂停", @@ -1848,11 +1742,9 @@ "group_deleted": "组已删除", "group_name_required": "需要组名", "group_saved": "组已保存", - "import_successful": "导入成功;集成重载中", "json_required": "需要 JSON 数据", "legacy_removed": "旧版操作已移除", "logs_exported": "日志已导出", - "matching_reverted": "匹配权重已恢复为默认值", "min_2_profiles": "为组至少选择 2 个配置文件", "ml_training_no_improvement": "训练完成:保留基准(无改进)", "models_reverted": "设备上的模型已恢复为基准", @@ -1862,8 +1754,8 @@ "pg_preset_loaded": "已加载预设“{name}”", "pg_preset_saved": "已保存预设“{name}”", "pg_sugg_loaded": "已载入 {n} 个建议值 - 运行试验场以比较效果", - "pg_sugg_ml_loaded": "已载入 {n} 个 ML 校准值 - 运行试验场以比较效果", "phase_name_required": "需要阶段名称", + "phase_reset": "“{name}”已恢复为内置阶段", "phase_updated": "阶段已更新", "phases_saved": "阶段已保存", "preferences_saved": "偏好已保存", @@ -1910,14 +1802,10 @@ "store_name_required": "请输入配置文件名称", "store_import_failed": "导入失败:{error}", "store_imported": "已导入到 {profile}", - "store_program_required": "请输入程序名称", "store_no_appliance": "请先在设置中设置您的家电品牌和型号。", "store_share_failed": "分享失败:{error}", "store_shared": "已分享到社区商店(等待审核)", "appliance_added": "已添加家电(等待批准)", - "thanks_confirming": "感谢您的确认", - "device_approved": "已获社区批准", - "rating_saved": "质量评分已保存", "brand_added": "已添加品牌(等待批准)", "profile_added": "已添加配置文件(等待批准)", "saved_except_conflicts": "已保存。请修复突出显示的冲突以保存其余内容。", @@ -1941,11 +1829,48 @@ "odometer_save_failed": "无法保存累计值:{error}", "contrib_device_changed": "已添加到社区商店,但表单打开期间所选设备已更改。请在设置中选择。", "store_pick_profile_share": "请选择要分享到的配置文件", - "remove_failed": "移除失败:{error}" + "remove_failed": "移除失败:{error}", + "analysis_complete": "分析完成:{count} 条建议", + "analysis_failed": "分析失败:{error}", + "auto_detect_failed": "自动检测失败:{error}", + "auto_label_failed": "自动标记失败:{error}", + "could_not_load_cycle": "无法加载周期:{error}", + "create_failed": "创建失败:{error}", + "cycles_deleted": "已删除 {count} 个周期", + "debug_cleared": "已清除 {count} 条调试追踪", + "discard_failed": "丢弃失败:{error}", + "error": "错误:{error}", + "export_failed": "导出失败:{error}", + "import_failed": "导入失败:{error}", + "invalid_json": "“{key}”不是有效的 JSON - 请在保存前修正或清空该字段。", + "label_failed": "标记失败:{error}", + "ml_training_promoted": "训练完成:已升级 {count} 个模型", + "pause_failed": "暂停失败:{error}", + "phase_created": "已创建阶段“{name}”", + "phase_deleted": "已删除阶段“{name}”", + "profile_created": "已创建配置文件“{name}”", + "rename_failed": "重命名失败:{error}", + "save_failed": "保存失败:{error}", + "split_complete": "已拆分为 {count} 个周期", + "start_failed": "启动失败:{error}", + "stop_failed": "停止失败:{error}", + "update_failed": "更新失败:{error}", + "store_download_skipped": "已跳过 {n} 个录制:过短、空缺过多或已在您的设备上。", + "store_import_low_quality": "此录制过短、空缺过多或不合理,无法导入。", + "store_import_duplicate": "此录制已在您的设备上。", + "device_approved": "已获社区批准", + "thanks_confirming": "感谢您的确认", + "rating_saved": "质量评分已保存", + "store_download_partial": "{n} 个程序无法下载,请稍后重试。", + "import_no_restore_point": "已导入,但无法保存还原点,因此此次导入无法撤销", + "import_undo_failed": "无法撤销导入:{error}", + "import_undone": "已撤销导入:已恢复 {profiles} 个配置文件和 {cycles} 个周期", + "auto_label_cancelled": "自动标记已取消,此前已标记 {count} 个周期。", + "auto_label_cancelled_other": "自动标记已取消,此前已标记 {count} 个周期。", + "auto_label_done": "自动标记完成:已标记 {count} 个周期,{skipped} 个保持未标记。", + "auto_label_done_other": "自动标记完成:已标记 {count} 个周期,{skipped} 个保持未标记。" }, "suggestion": { - "both_agree": "WashData建议", - "calibrated_label": "已校准", "observed_label": "观测值", "impact": { "auto_label_confidence": { @@ -1960,10 +1885,6 @@ "higher": "仅关闭消耗大量能量的循环", "lower": "也关闭较短或低能耗的循环" }, - "end_repeat_count": { - "higher": "结束信号需重复更多次––更稳健,但略慢", - "lower": "更少重复后结束––检测更快,但误结束风险更高" - }, "learning_confidence": { "higher": "仅从高置信度匹配中学习––较慢但更可靠", "lower": "从更多循环中学习,模型可能更嘈杂" @@ -2002,40 +1923,25 @@ } }, "reason": { - "auto_label_confidence": "用户从未纠正的 {cycles} 个自动标注的第 15 百分位置信度({p15})。", "completion_min_seconds": "{cycles} 个周期中 p05 正常周期时长的一半({minutes} min);用于过滤误检周期。{excl}", - "dead_zone_batch": "保持较短({cycles} 个周期中启动不稳定窗口的中位数 = {s}s),以便及时检测到真正的结束。", - "dead_zone_from_dip": "基于在 {s}s 时检测到的早期功率下降。", - "end_energy_default": "周期结束噪声门限的推荐默认值。", - "end_energy_false": "{cycles} 个周期中 p95 的误结束能量({p95}Wh),下限为周期能量中位数的 0.2%({floor}Wh)。", - "end_energy_no_false": "{cycles} 个周期中无误结束;下限为周期能量中位数的 0.2%({floor}Wh)。", - "end_repeat_count": "{false}/{total} 个正常周期({pct}%)出现了超过 60s 且随后恢复的内部暂停;要求进行 {val} 次结束确认。", - "learning_confidence": "{cycles} 个用户标注周期的 p05 置信度({p05});低于此值时请求验证。", "match_interval": "基于观测到的更新频率(中位数={median}s) * 10。", "max_duration_ratio": "基于已标注的周期时长(p95={p95})。", - "min_active": "基于上一个周期中观测到的最小工作功率({min}W)。", - "min_duration_ratio": "尽可能保持较低,以便在周期早期识别出程序;置信度和歧义门限可防止过早确定。", "min_off_gap": "基于观测到的 {gaps} 个周期间间隔(p05={p05}s)。设备下限:{floor}s。", "min_off_gap_bridge": "设定为足以跨过周期内最长的静默段:{span}s + 60s 缓冲,来自 {cycles} 个正常周期中的 {spans} 段已跨过的间隔(保持在您连续两次运行之间最短间隔 {ceiling}s 以内)。", "min_power": "{cycles} 个正常周期中 p05 最低工作功率({p05}W)的 40%,使关闭门限保持在实际功耗之下。{excl}", - "ml_auto_label": "质量模型仍判定为正常的最低置信度({cycles} 个正常周期的 p10 = {p10})。", - "ml_end_repeat": "{false}/{total} 个周期({pct}%)出现了结束检测器评分超过 50% 的暂停;要求进行 {val} 次结束确认。", - "ml_off_delay": "结束检测器已确认的暂停:p95 {p95}s + 60s 缓冲,来自 {cycles} 个周期中经模型验证的 {pauses} 次暂停(下限 {floor}s)。", "no_update_timeout": "基于观测到的更新频率(p95={p95}s) * 20(最小 60s)。", "off_delay_cadence": "基于观测到的更新频率(p95={p95}s) * 5", "off_delay_device_floor": "对 {device} 使用了特定于设备的安全最小值({floor}s)。", "off_delay_generic_floor": "使用了通用安全最小值({floor}s)。", "off_delay_pauses": "设定为足以超过真实暂停:p95 周期内暂停 {p95}s + 60s 缓冲,来自 {cycles} 个正常周期中的 {pauses} 次暂停(下限 {floor}s)。", - "profile_match_threshold": "{cycles} 个正确自动标注的 p10 置信度({p10});安全的实时确定下限。", - "sampling_interval": "{cycles} 个正常周期中观测到的更新间隔中位数({si}s)。{excl}", - "smoothing_window": "设定为在 {si}s 采样下平滑约 30s 的读数({samples} 个样本)。", - "start_duration": "保持较短(约一个 {si}s 采样间隔),以便尽早开始检测,同时忽略单个采样的尖峰。", - "thr_batch": "保持在 {cycles} 个周期中 p05 最低工作功率({p05}W)略上方,以便尽早捕捉到启动,同时使停止阈值保持在设备最低运行功率之下。", - "tol_per_profile": "{profiles} 个配置文件({cycles} 个周期)中每个配置文件时长方差的 p75;波动小的配置文件不受惩罚。", - "tol_pooled": "基于 {cycles} 个近期已标注周期的合并时长方差(p95 偏差={dev})。", + "standby_floor": "每个近期周期结束时仍在消耗 {idle} W,因此低于此值的阈值永远无法检测到设备关闭。阈值保持在其上方;记录中的 {cycles} 个正常周期从未在其下方暂停超过 {pause} 秒。", + "thr_batch": "略高于 {cycles} 个周期中的最低工作功率(p05,{p05}W),以便尽早捕捉启动,并使停止阈值保持在最低运行功率之下。", "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} 秒),因此使用最小值不会带来额外开销。" + "match_interval_floored": "按最短程序({shortest} 秒)计算,上限为 {cap} 秒,低于 {minimum} 秒的最小值,因此使用最小值:{persistence} 次连续匹配需要 {budget} 秒,超过该程序的 {pct}%。匹配只在有读数到达时运行(中位数={median} 秒),因此不会带来额外开销。", + "end_energy_floor": "已提高到停止阈值({stop} W)和关闭延迟({off_delay} s)所决定的下限;低于该值,周期只能通过备用路径结束。", + "max_duration_ratio_default": "已恢复默认值:上限较低时,只要某次运行比平时长,就会排除正确的程序。", + "min_duration_ratio_default": "已恢复默认值:下限较高时,运行中的周期要进行到相当程度后才会被识别。" }, "exclusions": { "summary": "已排除 {total} 个误检测周期:{parts}。", @@ -2053,7 +1959,8 @@ "no_power": "无功率", "no_active_power": "无工作功率" } - } + }, + "now_value": "(当前 {v})" }, "trend": { "down": "下降趋势", @@ -2061,6 +1968,10 @@ }, "maint": { "bearing_service": "轴承保养", + "salt": "补充软水盐", + "rinse_aid": "补充光亮剂", + "lint_filter": "清洁绒毛过滤器", + "condenser_clean": "清洁冷凝器", "descale": "除垢", "drum_clean": "清洁滚筒", "filter_clean": "清洁过滤器", @@ -2071,12 +1982,14 @@ "status_tip": "周期结果:已完成(自然结束)、已中断(功率骤降)、已强制停止(手动)或需要审查(反馈待处理)。", "date_tip": "周期开始的日期和时间。", "duration_tip": "从开始到结束的总周期运行时间。", - "energy_tip": "消耗的总能量(kWh)。通过对功率随时间积分计算得出。", - "cost_tip": "此周期的能耗成本,在完成时冻结。启用“时间加权成本”后,功率曲线按各时刻生效的电价计费;否则为能量 x 结束时的单一价格。在设置中设定价格以填充此项。", + "energy_tip": "消耗的总能量(kWh),根据功率随时间计算。", + "cost_tip": "此周期的能耗成本,在结束时确定。启用“时间加权成本”时按各时刻的电价计费;否则按结束时的电价。在设置中设定价格即可显示。", "confidence_tip": "配置文件匹配置信度(0-100%)。周期功率曲线与识别出的程序的匹配程度。", - "health_tip": "ML 周期健康(越高越好)。点击周期以检查并审查。", "flags_tip": "周期的审核、异常和来源标志。将鼠标悬停在图标上可查看详细信息。", - "cost_dynamic_tip": "时间加权:按周期运行期间生效的电价计费(实际 {price} {cur}/kWh)。" + "cost_dynamic_tip": "时间加权:按周期运行期间生效的电价计费(实际 {price} {cur}/kWh)。", + "dbg_conf": "置信度", + "dbg_mae": "MAE", + "dbg_corr": "相关" }, "stat": { "avg": "平均", @@ -2100,7 +2013,10 @@ "notify_finish": "将发送完成通知。", "notify_milestone": "将发送里程碑通知。", "notify_held": "由于处于免打扰时段,通知已被暂缓。", - "finished": "循环到达终止状态并已结束。" + "finished": "循环到达终止状态并已结束。", + "match_reverted": "程序退回到检测中:其得分已远低于自身峰值。", + "verified_pause": "实时对齐确认(或解除)了程序内的一次暂停。暂停保持期间,循环不会结束。", + "stalled": "仍有工作未完成,但周期停留在待机功耗的时间超过了该程序在此处的任何暂停。显示为“已暂停(停滞)”;周期保持未结束。" }, "store": { "search_ph2": "按品牌或型号搜索…", @@ -2109,11 +2025,7 @@ "programs_count": "程序:{n}", "browse": "浏览", "device": "设备", - "favorites": "收藏", - "search_ph": "按品牌或型号搜索…", "no_results": "未找到匹配的家电。请尝试其他搜索。", - "n_programs": "{n} 个程序", - "n_cycles": "{n} 个周期", "no_programs": "此家电尚无共享的程序。", "no_cycles": "此程序尚无共享的参考周期。", "anon": "匿名", @@ -2125,19 +2037,21 @@ "merge_target": "合并到配置文件", "description": "描述(可选)", "contributed_by": "由 {name} 提供", + "website": "商店网站 ↗", + "website_tip": "打开社区商店网站", + "favorites": "收藏", "rating_summary": "★ {avg} ({n})", "no_ratings": "暂无评分", - "website": "商店网站 ↗", - "website_tip": "打开社区商店网站" + "unreachable": "无法连接社区商店。请检查网络连接后重试。" }, "settings": { "gear": { - "title": "设置" + "title": "偏好设置" } }, "link": { - "manual": "手册 ↗", - "add_appliance": "添加您的家电" + "maintenance_supporter": "GitHub 上的 Maintenance Supporter ↗", + "manual": "手册 ↗" }, "tip": { "add_brand": "品牌未收录?将其添加到社区目录", @@ -2171,7 +2085,13 @@ "rebuild": { "envelopes": "正在重建包络" }, - "cancelling": "正在取消..." + "cancelling": "正在取消...", + "auto_label": { + "running": "正在自动标记周期" + }, + "store_download": { + "running": "正在从商店下载" + } }, "setup": { "hdr": { @@ -2180,26 +2100,24 @@ }, "phase0": { "washer": "WashData 已在检测您的周期。录制第一个周期以启用程序名称和时间估算。", - "dishwasher": "WashData 正在监视。洗碗机的周期较为复杂 – 强烈建议录制第一个周期。如果检测到的周期运行过长,请使用周期编辑器在保存为配置文件前进行裁剪。", + "dishwasher": "WashData 正在监视。洗碗机的周期较复杂,请录制第一个周期。如果检测到的周期过长,请在保存为配置文件前用周期编辑器裁剪。", "generic": "WashData 正在监视。录制或标记检测到的周期,开始构建配置文件。" }, "phase1a": { - "labelled": "良好的开始 – 您的第一个程序已保存。为获得最准确的数据,建议使用录制小部件录制下一个周期。" + "labelled": "良好的开始:您的第一个程序已保存。为获得最干净的数据,请用“手动录制”录制下一个周期。" }, "phase1b": { - "recorded": "您的录制已保存为 {profile_name}。现在录制或标记您的其他常用程序以建立覆盖范围。" + "recorded": "您的录制已保存为 {profile_name}。现在请录制或标记其他常用程序。" }, "phase1c": { - "verify": "您有来自社区的 {count} 个程序。运行一个周期以验证 WashData 是否正确识别 – 随着设备积累自己的历史记录,匹配将不断改善。" + "verify": "您有 {count} 个来自社区的程序。运行一个周期,检查 WashData 能否识别;随着设备积累自己的历史,匹配会不断改善。" }, "phase2": { - "cluster": "WashData 发现了 {count} 个与任何已保存程序不匹配的周期 – 它们彼此相似。想为这些周期创建新配置文件吗?", + "cluster": "WashData 发现了 {count} 个彼此相似、但不匹配任何已保存程序的周期。要为它们创建新的配置文件吗?", "unmatched": "您的上一个周期与任何已保存的程序不匹配。是新程序吗?" }, "phase3": { - "suggestions": "WashData 根据您的周期历史记录提供了设置建议 – 请查看以提高检测精度。", - "groups": "您的部分配置文件看起来像在不同温度下运行的同一程序。将它们整理成组以获得更好的匹配效果。", - "phases": "为 {profile_name} 添加程序阶段,以获得更准确的剩余时间估算。" + "suggestions": "WashData 根据您的周期历史提供了设置建议。请查看以改进检测。" }, "phase4": { "healthy": "此设备已完全设置好({profile_count} 个配置文件)。" @@ -2212,12 +2130,58 @@ "create_from_cluster": "从这些周期创建配置文件", "create_profile": "为此创建配置文件", "review_suggestions": "查看建议", - "organise_profiles": "整理配置文件", - "configure_phases": "配置阶段", "skip_step": "跳过此步骤", "skip_forever": "不再显示", "hide_guidance": "隐藏指南", "show_guidance": "显示指南" } + }, + "access": { + "edit": "编辑", + "full": "完全", + "inherit": "继承", + "none": "无(隐藏)", + "read": "读取" + }, + "pg_label_reason": { + "no_winner": "没有合适的程序", + "below_floor": "低于学习置信度", + "ambiguous": "被判定为不明确", + "margin": "与第二名过于接近", + "unknown_profile": "程序已不存在", + "not_evaluated": "未评估" + }, + "diagram": { + "accum": "累计", + "auto": "自动", + "below_off": "低于 = 关闭", + "confirmed": "已确认", + "energy_counts": "按能量计", + "feedback": "反馈", + "gap_below_min": "间隔小于最小值 = 同一周期", + "heat_phase": "加热阶段", + "held_at_100": "保持在 100%", + "ignored": "已忽略", + "kept": "保留", + "max": "最大", + "min": "最小", + "minus_tol": "-容差", + "no_match": "无匹配", + "no_updates": "无更新", + "off_delay_wait": "关闭延迟等待", + "off_gap": "关闭间隔", + "plus_tol": "+容差", + "profile": "配置文件", + "reading_interval": "典型读数间隔", + "sensor_offline": "传感器离线:强制停止", + "si": "间隔", + "spike_ignored": "尖峰已忽略", + "spike_ignored_short": "尖峰:忽略", + "start": "启动", + "stop": "停止", + "tail_energy_resets": "尾段能量高于阈值:计时器重置", + "thr": "阈值", + "too_short": "太短", + "tumble_kept": "保留翻滚脉冲" } } diff --git a/custom_components/ha_washdata/translations/pl.json b/custom_components/ha_washdata/translations/pl.json index d82252ed..f01f9376 100644 --- a/custom_components/ha_washdata/translations/pl.json +++ b/custom_components/ha_washdata/translations/pl.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "Konfiguracja WashData", - "description": "Skonfiguruj swoją pralkę lub inne urządzenie.\n\nCzujnik mocy jest wymagany.\n\nPo konfiguracji otwórz panel WashData z paska bocznego (lub przejdź do [/ha-washdata](/ha-washdata)), aby przeglądać cykle, dostosowywać wykrywanie i zarządzać profilami.", + "description": "Skonfiguruj pralkę lub inne urządzenie. Wymagany jest czujnik mocy.\n\nPo konfiguracji otwórz panel WashData z paska bocznego ([/ha-washdata](/ha-washdata)), aby zobaczyć cykle, profile i wszystkie ustawienia.", "data": { "name": "Nazwa urządzenia", "device_type": "Typ urządzenia", @@ -12,14 +12,14 @@ }, "data_description": { "name": "Przyjazna nazwa tego urządzenia (np. „Pralka”, „Zmywarka”).", - "device_type": "Jakiego typu jest to urządzenie? Pomaga dostosować wykrywanie i etykietowanie.", - "power_sensor": "Encja czujnika, która raportuje zużycie energii w czasie rzeczywistym (w watach) przez inteligentną wtyczkę.", - "min_power": "Odczyty mocy powyżej tego progu (w watach) wskazują, że urządzenie pracuje. Zacznij od 2 W dla większości urządzeń." + "device_type": "Ustawia domyślne parametry wykrywania dla tego rodzaju urządzenia.", + "power_sensor": "Czujnik inteligentnej wtyczki, który podaje bieżącą moc w watach.", + "min_power": "Odczyty powyżej tej wartości (w watach) oznaczają, że urządzenie pracuje. 2 W pasuje do większości urządzeń." } }, "reconfigure": { "title": "Rekonfiguracja WashData", - "description": "Zaktualizuj nazwę urządzenia, typ urządzenia lub czujnik mocy.\n\nPanel WashData (pasek boczny lub [/ha-washdata](/ha-washdata)) służy do zarządzania wykrywaniem, cyklami, profilami i powiadomieniami.", + "description": "Zmień nazwę urządzenia, jego typ lub czujnik mocy.\n\nWszystko inne znajdziesz w panelu WashData (pasek boczny lub [/ha-washdata](/ha-washdata)).", "data": { "name": "Nazwa urządzenia", "device_type": "Typ urządzenia", @@ -29,8 +29,6 @@ } }, "error": { - "cannot_connect": "Nie udało się połączyć", - "invalid_auth": "Nieprawidłowe uwierzytelnienie", "unknown": "Nieoczekiwany błąd", "invalid_power": "Próg mocy musi być większy niż 0" }, @@ -43,7 +41,7 @@ "step": { "init": { "title": "Ustawienia WashData", - "description": "Zmień nazwę urządzenia lub zmień jego podstawowe ustawienia sprzętowe. Wszystkie ustawienia wykrywania, dopasowania i powiadomień znajdują się w [panelu WashData](/ha-washdata) (dostępny również z paska bocznego).", + "description": "Zmień nazwę urządzenia lub jego podstawowe ustawienia. Wszystko inne znajdziesz w [panelu WashData](/ha-washdata) na pasku bocznym.", "data": { "name": "Nazwa urządzenia", "device_type": "Typ urządzenia", @@ -93,7 +91,7 @@ }, "profile_name": { "name": "Nazwa profilu", - "description": "Nazwa istniejącego profilu (utwórz profile w menu Zarządzaj profilami). Pozostaw puste, aby usunąć etykietę." + "description": "Nazwa istniejącego profilu (profile tworzy się w panelu WashData). Pozostaw puste, aby usunąć etykietę." } } }, @@ -143,13 +141,13 @@ }, "confidence_threshold": { "name": "Próg pewności", - "description": "Minimalna pewność dopasowania (0,50–0,95), aby zastosować etykiety." + "description": "Minimalna pewność dopasowania (0,50–0,95), aby zastosować etykiety. Pozostaw puste, aby użyć ustawienia urządzenia „Pewność Automatycznego Etykietowania”." } } }, "export_config": { "name": "Eksportuj konfigurację", - "description": "Eksportuj profile, cykle i ustawienia tego urządzenia do pliku JSON (na urządzenie).", + "description": "Eksportuj profile, cykle i ustawienia tego urządzenia do pliku JSON.", "fields": { "device_id": { "name": "Urządzenie", @@ -177,7 +175,7 @@ }, "submit_cycle_feedback": { "name": "Prześlij opinię dotyczącą cyklu", - "description": "Potwierdź lub popraw automatycznie wykryty program po zakończeniu cyklu. Podaj `entry_id` (zaawansowane) lub `device_id` (zalecane).", + "description": "Potwierdź lub popraw wykryty program zakończonego cyklu. Podaj `device_id` (zalecane) lub `entry_id`.", "fields": { "device_id": { "name": "Urządzenie", @@ -283,7 +281,7 @@ }, "mark_unloaded": { "name": "Oznacz jako rozładowane", - "description": "Potwierdź, że gotowy wsad został wyjęty z urządzenia WashData, co kasuje stan „Czysty” i ewentualne przypomnienie o rozładunku.", + "description": "Potwierdź, że gotowy wsad został wyjęty. Kasuje stan „Czysty” i przypomnienie o rozładunku.", "fields": { "device_id": { "name": "Urządzenie", @@ -299,6 +297,32 @@ }, "match_ambiguity": { "name": "Niejednoznaczność dopasowania" + }, + "maintenance_due": { + "name": "Wymagana konserwacja", + "state": { + "on": "Wymagana", + "off": "Niewymagana" + }, + "state_attributes": { + "due_task_ids": { + "name": "ID wymaganych zadań" + }, + "due_tasks": { + "name": "Wymagane zadania", + "state": { + "descale": "Odkamienianie", + "filter_clean": "Wyczyść filtr", + "drum_clean": "Wyczyść bęben", + "bearing_service": "Serwis łożysk", + "other": "Inne", + "salt": "Uzupełnij sól", + "rinse_aid": "Uzupełnij nabłyszczacz", + "lint_filter": "Wyczyść filtr kłaczków", + "condenser_clean": "Wyczyść skraplacz" + } + } + } } }, "sensor": { @@ -316,10 +340,18 @@ "anti_wrinkle": "Antygniotowy", "interrupted": "Przerwany", "force_stopped": "Wymuszone zatrzymanie", - "rinse": "Płukanie", "unknown": "Nieznany", "clean": "Czysty", "delay_wait": "Oczekiwanie na start" + }, + "state_attributes": { + "cycle_anomaly": { + "name": "Anomalia cyklu", + "state": { + "overrun": "Działa długo", + "stalled": "Utknął" + } + } } }, "washer_program": { diff --git a/custom_components/ha_washdata/translations/pt-BR.json b/custom_components/ha_washdata/translations/pt-BR.json index d5b7915a..b4733388 100644 --- a/custom_components/ha_washdata/translations/pt-BR.json +++ b/custom_components/ha_washdata/translations/pt-BR.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "Configuração WashData", - "description": "Configure sua máquina de lavar ou outro eletrodoméstico.\n\nSensor de potência é necessário.\n\nApós a configuração, abra o painel WashData pela barra lateral (ou acesse [/ha-washdata](/ha-washdata)) para visualizar ciclos, ajustar a detecção e gerenciar perfis.", + "description": "Configure uma máquina de lavar ou outro eletrodoméstico. É necessário um sensor de potência.\n\nApós a configuração, abra o painel WashData pela barra lateral ([/ha-washdata](/ha-washdata)) para ver ciclos, perfis e todas as configurações.", "data": { "name": "Nome do dispositivo", "device_type": "Tipo de dispositivo", @@ -12,43 +12,41 @@ }, "data_description": { "name": "Um nome amigável para este dispositivo (por exemplo, 'Máquina de lavar', 'Lava-louças').", - "device_type": "Que tipo de aparelho é esse? Ajuda a personalizar a detecção e a rotulagem.", - "power_sensor": "A entidade do sensor que informa o consumo de energia em tempo real (em watts) do seu plugue inteligente.", - "min_power": "As leituras de potência acima deste limite (em watts) indicam que o aparelho está funcionando. Comece com 2W para a maioria dos dispositivos." + "device_type": "Define os padrões de detecção para este tipo de eletrodoméstico.", + "power_sensor": "O sensor do plugue inteligente que informa a potência atual em watts.", + "min_power": "Leituras acima deste valor (em watts) indicam que o aparelho está funcionando. 2 W serve para a maioria dos dispositivos." } }, "reconfigure": { - "title": "Reconfigure WashData", - "description": "Atualize o nome do dispositivo, o tipo de eletrodoméstico ou o sensor de potência.\n\nO painel WashData (barra lateral ou [/ha-washdata](/ha-washdata)) é onde detecção, ciclos, perfis e notificações são gerenciados.", + "title": "Reconfigurar WashData", + "description": "Altere o nome do dispositivo, o tipo de eletrodoméstico ou o sensor de potência.\n\nTodo o resto fica no painel WashData (barra lateral, ou [/ha-washdata](/ha-washdata)).", "data": { - "name": "Device Name", - "device_type": "Device Type", - "power_sensor": "Power Sensor", - "min_power": "Minimum Power Threshold (W)" + "name": "Nome do dispositivo", + "device_type": "Tipo de dispositivo", + "power_sensor": "Sensor de potência", + "min_power": "Limite Mínimo de Potência (W)" } } }, "error": { - "cannot_connect": "Falha ao conectar", - "invalid_auth": "Autenticação inválida", "unknown": "Erro inesperado", - "invalid_power": "Power threshold must be greater than 0" + "invalid_power": "O limite de potência deve ser maior que 0" }, "abort": { "already_configured": "O dispositivo já está configurado", - "reconfigure_successful": "Reconfiguration was successful" + "reconfigure_successful": "Reconfiguração bem-sucedida" } }, "options": { "step": { "init": { - "title": "WashData Settings", - "description": "Renomeie o dispositivo ou altere suas configurações principais de hardware. Todas as configurações de detecção, correspondência e notificação estão no [painel WashData](/ha-washdata) (também acessível pela barra lateral).", + "title": "Configurações do WashData", + "description": "Renomeie o dispositivo ou altere suas configurações principais. Todo o resto fica no [painel WashData](/ha-washdata) na barra lateral.", "data": { "name": "Nome do dispositivo", - "device_type": "Device Type", - "power_sensor": "Power Sensor", - "min_power": "Minimum Power Threshold (W)" + "device_type": "Tipo de dispositivo", + "power_sensor": "Sensor de potência", + "min_power": "Limite Mínimo de Potência (W)" } } }, @@ -60,7 +58,7 @@ "notify_live_waiting_message": "{device}: Nenhum perfil identificado ainda.", "vs_typical_longer": "{pct}% mais longo que o normal", "vs_typical_shorter": "{pct}% mais curto que o normal", - "invalid_power": "Power threshold must be greater than 0" + "invalid_power": "O limite de potência deve ser maior que 0" } }, "selector": { @@ -93,7 +91,7 @@ }, "profile_name": { "name": "Nome do perfil", - "description": "O nome de um perfil existente (crie perfis no menu Gerenciar Perfis). Deixe em branco para remover o rótulo." + "description": "Nome de um perfil existente (crie perfis no painel WashData). Deixe em branco para remover o rótulo." } } }, @@ -143,13 +141,13 @@ }, "confidence_threshold": { "name": "Limite de confiança", - "description": "Confiança mínima de correspondência (0,50–0,95) para aplicar rótulos." + "description": "Confiança mínima de correspondência (0,50–0,95) para aplicar rótulos. Deixe vazio para usar a configuração Confiança de Rotulagem Automática do dispositivo." } } }, "export_config": { "name": "Exportar configuração", - "description": "Exporte os perfis, ciclos e configurações deste dispositivo para um arquivo JSON (por dispositivo).", + "description": "Exporte os perfis, ciclos e configurações deste dispositivo para um arquivo JSON.", "fields": { "device_id": { "name": "Dispositivo", @@ -177,7 +175,7 @@ }, "submit_cycle_feedback": { "name": "Enviar feedback do ciclo", - "description": "Confirme ou corrija um programa detectado automaticamente após um ciclo concluído. Forneça `entry_id` (avançado) ou `device_id` (recomendado).", + "description": "Confirme ou corrija o programa detectado de um ciclo concluído. Informe `device_id` (recomendado) ou `entry_id`.", "fields": { "device_id": { "name": "Dispositivo", @@ -272,18 +270,18 @@ } }, "trigger_ml_training": { - "name": "Trigger ML Training", - "description": "Manually retrain the on-device ML models from this device's own labelled cycles (experimental).", + "name": "Iniciar treinamento de ML", + "description": "Retreine manualmente os modelos de ML no dispositivo a partir dos ciclos rotulados deste dispositivo (experimental).", "fields": { "device_id": { - "name": "Device", - "description": "The WashData device to retrain models for." + "name": "Dispositivo", + "description": "O dispositivo WashData cujos modelos serão retreinados." } } }, "mark_unloaded": { "name": "Marcar como descarregado", - "description": "Confirme que a carga finalizada foi retirada de um dispositivo WashData, limpando o estado Limpo e qualquer lembrete de descarga.", + "description": "Confirme que a carga finalizada foi retirada. Limpa o estado Limpo e qualquer lembrete de descarga.", "fields": { "device_id": { "name": "Dispositivo", @@ -299,6 +297,32 @@ }, "match_ambiguity": { "name": "Ambiguidade de correspondência" + }, + "maintenance_due": { + "name": "Manutenção pendente", + "state": { + "on": "Pendente", + "off": "Não pendente" + }, + "state_attributes": { + "due_task_ids": { + "name": "IDs das tarefas pendentes" + }, + "due_tasks": { + "name": "Tarefas pendentes", + "state": { + "descale": "Descalcificar", + "filter_clean": "Limpar o filtro", + "drum_clean": "Limpar o tambor", + "bearing_service": "Manutenção de rolamentos", + "other": "Outro", + "salt": "Reabastecer o sal", + "rinse_aid": "Reabastecer o secante", + "lint_filter": "Limpar o filtro de fiapos", + "condenser_clean": "Limpar o condensador" + } + } + } } }, "sensor": { @@ -316,10 +340,18 @@ "anti_wrinkle": "Anti-amasso", "interrupted": "Interrompido", "force_stopped": "Parada forçada", - "rinse": "Enxaguar", "unknown": "Desconhecido", - "clean": "Limpar", + "clean": "Limpo", "delay_wait": "Esperando para começar" + }, + "state_attributes": { + "cycle_anomaly": { + "name": "Anomalia do ciclo", + "state": { + "overrun": "Em atraso", + "stalled": "Travado" + } + } } }, "washer_program": { diff --git a/custom_components/ha_washdata/translations/pt.json b/custom_components/ha_washdata/translations/pt.json index ca003818..512d9c08 100644 --- a/custom_components/ha_washdata/translations/pt.json +++ b/custom_components/ha_washdata/translations/pt.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "Configuração WashData", - "description": "Configure a sua máquina de lavar ou outro electrodoméstico.\n\nÉ necessário um sensor de potência.\n\nApós a configuração, abra o painel WashData a partir da barra lateral (ou aceda a [/ha-washdata](/ha-washdata)) para ver os ciclos, ajustar a deteção e gerir perfis.", + "description": "Configure uma máquina de lavar ou outro eletrodoméstico. É necessário um sensor de potência.\n\nApós a configuração, abra o painel WashData a partir da barra lateral ([/ha-washdata](/ha-washdata)) para ver ciclos, perfis e todas as definições.", "data": { "name": "Nome do dispositivo", "device_type": "Tipo de dispositivo", @@ -12,14 +12,14 @@ }, "data_description": { "name": "Um nome descritivo para este dispositivo (por exemplo, 'Máquina de lavar', 'Máquina de lavar loiça').", - "device_type": "Que tipo de electrodoméstico é este? Ajuda a personalizar a detecção e a rotulagem.", - "power_sensor": "A entidade do sensor que reporta o consumo de energia em tempo real (em watts) da sua tomada inteligente.", - "min_power": "Leituras de potência acima deste limiar (em watts) indicam que o electrodoméstico está a funcionar. Comece com 2W para a maioria dos dispositivos." + "device_type": "Define os valores de deteção predefinidos para este tipo de eletrodoméstico.", + "power_sensor": "O sensor da tomada inteligente que reporta a potência atual em watts.", + "min_power": "Leituras acima deste valor (em watts) indicam que o eletrodoméstico está a funcionar. 2 W serve para a maioria dos dispositivos." } }, "reconfigure": { "title": "Reconfigurar WashData", - "description": "Actualize o nome do dispositivo, o tipo de electrodoméstico ou o sensor de potência.\n\nO painel WashData (barra lateral, ou [/ha-washdata](/ha-washdata)) é onde a deteção, os ciclos, os perfis e as notificações são geridos.", + "description": "Altere o nome do dispositivo, o tipo de eletrodoméstico ou o sensor de potência.\n\nTudo o resto está no painel WashData (barra lateral, ou [/ha-washdata](/ha-washdata)).", "data": { "name": "Nome do dispositivo", "device_type": "Tipo de dispositivo", @@ -29,8 +29,6 @@ } }, "error": { - "cannot_connect": "Falha ao ligar", - "invalid_auth": "Autenticação inválida", "unknown": "Erro inesperado", "invalid_power": "O limiar de potência tem de ser maior do que 0" }, @@ -43,7 +41,7 @@ "step": { "init": { "title": "Definições WashData", - "description": "Mude o nome do dispositivo ou altere as suas definições de hardware principais. Todas as definições de deteção, correspondência e notificações estão no [painel WashData](/ha-washdata) (também acessível a partir da barra lateral).", + "description": "Mude o nome do dispositivo ou altere as suas definições principais. Tudo o resto está no [painel WashData](/ha-washdata) na barra lateral.", "data": { "name": "Nome do dispositivo", "device_type": "Tipo de dispositivo", @@ -93,7 +91,7 @@ }, "profile_name": { "name": "Nome do perfil", - "description": "O nome de um perfil existente (crie perfis no menu Gerir perfis). Deixe em branco para remover o rótulo." + "description": "Nome de um perfil existente (crie perfis no painel WashData). Deixe em branco para remover o rótulo." } } }, @@ -143,13 +141,13 @@ }, "confidence_threshold": { "name": "Limiar de confiança", - "description": "Confiança mínima de correspondência (0,50-0,95) para aplicar rótulos." + "description": "Confiança mínima de correspondência (0,50-0,95) para aplicar rótulos. Deixe vazio para usar a definição Confiança de Rotulagem Automática do dispositivo." } } }, "export_config": { "name": "Exportar configuração", - "description": "Exporte os perfis, ciclos e definições deste dispositivo para um ficheiro JSON (por dispositivo).", + "description": "Exporte os perfis, ciclos e definições deste dispositivo para um ficheiro JSON.", "fields": { "device_id": { "name": "Dispositivo", @@ -177,7 +175,7 @@ }, "submit_cycle_feedback": { "name": "Enviar feedback do ciclo", - "description": "Confirme ou corrija um programa detectado automaticamente após um ciclo concluído. Forneça `entry_id` (avançado) ou `device_id` (recomendado).", + "description": "Confirme ou corrija o programa detetado de um ciclo concluído. Indique `device_id` (recomendado) ou `entry_id`.", "fields": { "device_id": { "name": "Dispositivo", @@ -283,7 +281,7 @@ }, "mark_unloaded": { "name": "Marcar como descarregado", - "description": "Confirme que a carga terminada foi retirada de um dispositivo WashData, limpando o estado Limpo e qualquer lembrete de descarga.", + "description": "Confirme que a carga terminada foi retirada. Limpa o estado Limpo e qualquer lembrete de descarga.", "fields": { "device_id": { "name": "Dispositivo", @@ -299,6 +297,32 @@ }, "match_ambiguity": { "name": "Ambiguidade de correspondência" + }, + "maintenance_due": { + "name": "Manutenção pendente", + "state": { + "on": "Pendente", + "off": "Não pendente" + }, + "state_attributes": { + "due_task_ids": { + "name": "IDs das tarefas pendentes" + }, + "due_tasks": { + "name": "Tarefas pendentes", + "state": { + "descale": "Descalcificar", + "filter_clean": "Limpar o filtro", + "drum_clean": "Limpar o tambor", + "bearing_service": "Manutenção de rolamentos", + "other": "Outro", + "salt": "Repor o sal", + "rinse_aid": "Repor o abrilhantador", + "lint_filter": "Limpar o filtro de cotão", + "condenser_clean": "Limpar o condensador" + } + } + } } }, "sensor": { @@ -316,10 +340,18 @@ "anti_wrinkle": "Anti-vincos", "interrupted": "Interrompido", "force_stopped": "Parado forçosamente", - "rinse": "Enxaguar", "unknown": "Desconhecido", - "clean": "Limpar", + "clean": "Limpo", "delay_wait": "Esperando para começar" + }, + "state_attributes": { + "cycle_anomaly": { + "name": "Anomalia do ciclo", + "state": { + "overrun": "Em atraso", + "stalled": "Parado" + } + } } }, "washer_program": { diff --git a/custom_components/ha_washdata/translations/ro.json b/custom_components/ha_washdata/translations/ro.json index 492a114f..13a71d5d 100644 --- a/custom_components/ha_washdata/translations/ro.json +++ b/custom_components/ha_washdata/translations/ro.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "Configurare WashData", - "description": "Configurați mașina de spălat sau alt aparat.\n\nEste necesar un senzor de putere.\n\nDupă configurare, deschideți panoul WashData din bara laterală (sau accesați [/ha-washdata](/ha-washdata)) pentru a vizualiza ciclurile, a ajusta detectarea și a gestiona profilurile.", + "description": "Configurați o mașină de spălat sau alt aparat. Este necesar un senzor de putere.\n\nDupă configurare, deschideți panoul WashData din bara laterală ([/ha-washdata](/ha-washdata)) pentru a vedea ciclurile, profilurile și toate setările.", "data": { "name": "Numele dispozitivului", "device_type": "Tip de dispozitiv", @@ -12,43 +12,41 @@ }, "data_description": { "name": "Un nume prietenos pentru acest dispozitiv (de exemplu, „Mașină de spălat”, „Mașină de spălat vase”).", - "device_type": "Ce tip de aparat este acesta? Ajută la personalizarea detectării și etichetării.", - "power_sensor": "Entitatea senzor care raportează consumul de energie în timp real (în wați) de la priza inteligentă.", - "min_power": "Citirile de putere peste acest prag (în wați) indică faptul că aparatul funcționează. Începeți cu 2W pentru majoritatea dispozitivelor." + "device_type": "Stabilește valorile implicite de detectare pentru acest tip de aparat.", + "power_sensor": "Senzorul prizei inteligente care raportează puterea curentă în wați.", + "min_power": "Citirile peste această valoare (în wați) înseamnă că aparatul funcționează. 2 W se potrivește pentru majoritatea dispozitivelor." } }, "reconfigure": { - "title": "Reconfigure WashData", - "description": "Actualizați numele dispozitivului, tipul aparatului sau senzorul de putere.\n\nPanoul WashData (bara laterală sau [/ha-washdata](/ha-washdata)) este locul unde se gestionează detectarea, ciclurile, profilurile și notificările.", + "title": "Reconfigurare WashData", + "description": "Schimbați numele dispozitivului, tipul aparatului sau senzorul de putere.\n\nTot restul se află în panoul WashData (bara laterală sau [/ha-washdata](/ha-washdata)).", "data": { - "name": "Device Name", - "device_type": "Device Type", - "power_sensor": "Power Sensor", - "min_power": "Minimum Power Threshold (W)" + "name": "Numele dispozitivului", + "device_type": "Tip de dispozitiv", + "power_sensor": "Senzor de putere", + "min_power": "Pragul minim de putere (W)" } } }, "error": { - "cannot_connect": "Nu s-a putut conecta", - "invalid_auth": "Autentificare nevalidă", "unknown": "Eroare neașteptată", - "invalid_power": "Power threshold must be greater than 0" + "invalid_power": "Pragul de putere trebuie să fie mai mare decât 0" }, "abort": { "already_configured": "Dispozitivul este deja configurat", - "reconfigure_successful": "Reconfiguration was successful" + "reconfigure_successful": "Reconfigurarea s-a realizat cu succes" } }, "options": { "step": { "init": { - "title": "WashData Settings", - "description": "Redenumiți dispozitivul sau modificați setările hardware de bază. Toate setările de detectare, potrivire și notificare se află în [panoul WashData](/ha-washdata) (accesibil și din bara laterală).", + "title": "Setări WashData", + "description": "Redenumiți dispozitivul sau modificați setările sale de bază. Tot restul se află în [panoul WashData](/ha-washdata) din bara laterală.", "data": { "name": "Numele dispozitivului", - "device_type": "Device Type", - "power_sensor": "Power Sensor", - "min_power": "Minimum Power Threshold (W)" + "device_type": "Tip de dispozitiv", + "power_sensor": "Senzor de putere", + "min_power": "Pragul minim de putere (W)" } } }, @@ -60,7 +58,7 @@ "notify_live_waiting_message": "{device}: Niciun profil nu a fost recunoscut încă.", "vs_typical_longer": "cu {pct}% mai mult decât de obicei", "vs_typical_shorter": "cu {pct}% mai puțin decât de obicei", - "invalid_power": "Power threshold must be greater than 0" + "invalid_power": "Pragul de putere trebuie să fie mai mare decât 0" } }, "selector": { @@ -93,7 +91,7 @@ }, "profile_name": { "name": "Nume profil", - "description": "Numele unui profil existent (creați profiluri în meniul Gestionare profiluri). Lăsați necompletat pentru a elimina eticheta." + "description": "Numele unui profil existent (creați profiluri în panoul WashData). Lăsați gol pentru a elimina eticheta." } } }, @@ -143,13 +141,13 @@ }, "confidence_threshold": { "name": "Pragul de încredere", - "description": "Încrederea minimă de potrivire (0,50-0,95) pentru aplicarea etichetelor." + "description": "Încrederea minimă de potrivire (0,50-0,95) pentru aplicarea etichetelor. Lăsați gol pentru a folosi setarea Încredere Etichetare Automată a dispozitivului." } } }, "export_config": { "name": "Exportați configurația", - "description": "Exportați profilurile, ciclurile și setările acestui dispozitiv într-un fișier JSON (per dispozitiv).", + "description": "Exportați profilurile, ciclurile și setările acestui dispozitiv într-un fișier JSON.", "fields": { "device_id": { "name": "Dispozitiv", @@ -177,7 +175,7 @@ }, "submit_cycle_feedback": { "name": "Trimiteți feedback pentru ciclu", - "description": "Confirmați sau corectați un program detectat automat după un ciclu finalizat. Furnizați fie „entry_id” (avansat), fie „device_id” (recomandat).", + "description": "Confirmați sau corectați programul detectat al unui ciclu încheiat. Indicați `device_id` (recomandat) sau `entry_id`.", "fields": { "device_id": { "name": "Dispozitiv", @@ -272,18 +270,18 @@ } }, "trigger_ml_training": { - "name": "Trigger ML Training", - "description": "Manually retrain the on-device ML models from this device's own labelled cycles (experimental).", + "name": "Porniți antrenarea ML", + "description": "Reantrenați manual modelele ML de pe dispozitiv pe baza ciclurilor etichetate ale acestui dispozitiv (experimental).", "fields": { "device_id": { - "name": "Device", - "description": "The WashData device to retrain models for." + "name": "Dispozitiv", + "description": "Dispozitivul WashData pentru care se reantrenează modelele." } } }, "mark_unloaded": { "name": "Marcați ca descărcat", - "description": "Confirmați că încărcătura terminată a fost scoasă dintr-un dispozitiv WashData, ștergând starea Curat și orice memento de descărcare.", + "description": "Confirmați că încărcătura terminată a fost scoasă. Șterge starea Curat și orice memento de descărcare.", "fields": { "device_id": { "name": "Dispozitiv", @@ -299,6 +297,32 @@ }, "match_ambiguity": { "name": "Ambiguitate la potrivire" + }, + "maintenance_due": { + "name": "Întreținere scadentă", + "state": { + "on": "Scadentă", + "off": "Nu este scadentă" + }, + "state_attributes": { + "due_task_ids": { + "name": "ID-uri sarcini scadente" + }, + "due_tasks": { + "name": "Sarcini scadente", + "state": { + "descale": "Detartrare", + "filter_clean": "Curățare filtru", + "drum_clean": "Curățare tambur", + "bearing_service": "Service rulmenți", + "other": "Altele", + "salt": "Reumplere sare", + "rinse_aid": "Reumplere agent de clătire", + "lint_filter": "Curățare filtru de scame", + "condenser_clean": "Curățare condensator" + } + } + } } }, "sensor": { @@ -316,10 +340,18 @@ "anti_wrinkle": "Antirid", "interrupted": "Întrerupt", "force_stopped": "Oprit forțat", - "rinse": "Clătire", "unknown": "Necunoscut", "clean": "Curat", "delay_wait": "Așteaptă să Înceapă" + }, + "state_attributes": { + "cycle_anomaly": { + "name": "Anomalie ciclu", + "state": { + "overrun": "Întârziere", + "stalled": "Blocat" + } + } } }, "washer_program": { diff --git a/custom_components/ha_washdata/translations/ru.json b/custom_components/ha_washdata/translations/ru.json index 471e7a7d..7a00d3fa 100644 --- a/custom_components/ha_washdata/translations/ru.json +++ b/custom_components/ha_washdata/translations/ru.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "Настройка WashData", - "description": "Настройте стиральную машину или другой прибор.\n\nТребуется датчик мощности.\n\nПосле настройки откройте панель WashData на боковой панели (или перейдите на [/ha-washdata](/ha-washdata)), чтобы просматривать циклы, настраивать обнаружение и управлять профилями.", + "description": "Настройте стиральную машину или другой прибор. Нужен датчик мощности.\n\nПосле настройки откройте панель WashData на боковой панели ([/ha-washdata](/ha-washdata)): там циклы, профили и все настройки.", "data": { "name": "Имя устройства", "device_type": "Тип устройства", @@ -12,43 +12,41 @@ }, "data_description": { "name": "Понятное имя для этого устройства (например, «Стиральная машина», «Посудомоечная машина»).", - "device_type": "Что это за прибор? Помогает адаптировать обнаружение и маркировку.", - "power_sensor": "Датчик, который в реальном времени сообщает о потребляемой мощности (в ваттах) от вашей умной розетки.", - "min_power": "Показания мощности выше этого порога (в ваттах) указывают на то, что устройство работает. Начните с 2 Вт для большинства устройств." + "device_type": "Задаёт параметры обнаружения по умолчанию для этого типа прибора.", + "power_sensor": "Датчик умной розетки, который передаёт текущую мощность в ваттах.", + "min_power": "Показания выше этого значения (в ваттах) означают, что прибор работает. Для большинства устройств подходит 2 Вт." } }, "reconfigure": { - "title": "Reconfigure WashData", - "description": "Обновите имя устройства, тип прибора или датчик мощности.\n\nПанель WashData (боковая панель или [/ha-washdata](/ha-washdata)) — это место для управления обнаружением, циклами, профилями и уведомлениями.", + "title": "Повторная настройка WashData", + "description": "Измените имя устройства, тип прибора или датчик мощности.\n\nВсё остальное находится в панели WashData (боковая панель или [/ha-washdata](/ha-washdata)).", "data": { - "name": "Device Name", - "device_type": "Device Type", - "power_sensor": "Power Sensor", - "min_power": "Minimum Power Threshold (W)" + "name": "Имя устройства", + "device_type": "Тип устройства", + "power_sensor": "Датчик мощности", + "min_power": "Минимальный порог мощности (Вт)" } } }, "error": { - "cannot_connect": "Не удалось подключиться", - "invalid_auth": "Неверная аутентификация", "unknown": "Неожиданная ошибка", - "invalid_power": "Power threshold must be greater than 0" + "invalid_power": "Порог мощности должен быть больше 0" }, "abort": { "already_configured": "Устройство уже настроено", - "reconfigure_successful": "Reconfiguration was successful" + "reconfigure_successful": "Повторная настройка выполнена успешно" } }, "options": { "step": { "init": { - "title": "WashData Settings", - "description": "Переименуйте устройство или измените его основные аппаратные настройки. Все настройки обнаружения, сопоставления и уведомлений находятся в [панели WashData](/ha-washdata) (также доступна из боковой панели).", + "title": "Настройки WashData", + "description": "Переименуйте устройство или измените его основные настройки. Всё остальное находится в [панели WashData](/ha-washdata) на боковой панели.", "data": { "name": "Имя устройства", - "device_type": "Device Type", - "power_sensor": "Power Sensor", - "min_power": "Minimum Power Threshold (W)" + "device_type": "Тип устройства", + "power_sensor": "Датчик мощности", + "min_power": "Минимальный порог мощности (Вт)" } } }, @@ -60,7 +58,7 @@ "notify_live_waiting_message": "{device}: Профиль пока не распознан.", "vs_typical_longer": "на {pct}% дольше обычного", "vs_typical_shorter": "на {pct}% короче обычного", - "invalid_power": "Power threshold must be greater than 0" + "invalid_power": "Порог мощности должен быть больше 0" } }, "selector": { @@ -93,7 +91,7 @@ }, "profile_name": { "name": "Имя профиля", - "description": "Имя существующего профиля (создайте профили в меню «Управление профилями»). Оставьте пустым, чтобы удалить метку." + "description": "Имя существующего профиля (профили создаются в панели WashData). Оставьте пустым, чтобы снять метку." } } }, @@ -143,13 +141,13 @@ }, "confidence_threshold": { "name": "Порог уверенности", - "description": "Минимальная уверенность в совпадении (0,50–0,95) для применения меток." + "description": "Минимальная уверенность в совпадении (0,50–0,95) для применения меток. Оставьте пустым, чтобы использовать настройку устройства «Уверенность Авто-Маркировки»." } } }, "export_config": { "name": "Экспортировать конфигурацию", - "description": "Экспортируйте профили, циклы и настройки этого устройства в файл JSON (для каждого устройства).", + "description": "Экспортировать профили, циклы и настройки этого устройства в файл JSON.", "fields": { "device_id": { "name": "Устройство", @@ -177,7 +175,7 @@ }, "submit_cycle_feedback": { "name": "Отправить отзыв о цикле", - "description": "Подтвердите или исправьте автоматически определённую программу после завершения цикла. Укажите либо `entry_id` (дополнительно), либо `device_id` (рекомендуется).", + "description": "Подтвердите или исправьте определённую программу завершённого цикла. Укажите `device_id` (рекомендуется) или `entry_id`.", "fields": { "device_id": { "name": "Устройство", @@ -272,18 +270,18 @@ } }, "trigger_ml_training": { - "name": "Trigger ML Training", + "name": "Запустить обучение ML", "description": "Вручную переобучите модели машинного обучения на устройстве из собственных помеченных циклов этого устройства (экспериментально).", "fields": { "device_id": { - "name": "Device", - "description": "The WashData device to retrain models for." + "name": "Устройство", + "description": "Устройство WashData, модели которого нужно переобучить." } } }, "mark_unloaded": { "name": "Отметить как выгруженное", - "description": "Подтвердите, что содержимое завершённого цикла извлечено из устройства WashData: состояние «Чистый» и напоминание о выгрузке будут сброшены.", + "description": "Подтвердите, что готовая загрузка вынута. Сбрасывает состояние «Чистый» и напоминание о выгрузке.", "fields": { "device_id": { "name": "Устройство", @@ -299,6 +297,32 @@ }, "match_ambiguity": { "name": "Неоднозначность сопоставления" + }, + "maintenance_due": { + "name": "Требуется обслуживание", + "state": { + "on": "Требуется", + "off": "Не требуется" + }, + "state_attributes": { + "due_task_ids": { + "name": "ID задач к выполнению" + }, + "due_tasks": { + "name": "Задачи к выполнению", + "state": { + "descale": "Удаление накипи", + "filter_clean": "Очистка фильтра", + "drum_clean": "Очистка барабана", + "bearing_service": "Обслуживание подшипников", + "other": "Другое", + "salt": "Пополнение соли", + "rinse_aid": "Пополнение ополаскивателя", + "lint_filter": "Очистка фильтра ворса", + "condenser_clean": "Очистка конденсатора" + } + } + } } }, "sensor": { @@ -316,10 +340,18 @@ "anti_wrinkle": "Защита от морщин", "interrupted": "Прервано", "force_stopped": "Принудительная остановка", - "rinse": "Полоскание", "unknown": "Неизвестно", "clean": "Чистый", "delay_wait": "Ожидание начала" + }, + "state_attributes": { + "cycle_anomaly": { + "name": "Аномалия цикла", + "state": { + "overrun": "Работает долго", + "stalled": "Завис" + } + } } }, "washer_program": { diff --git a/custom_components/ha_washdata/translations/sk.json b/custom_components/ha_washdata/translations/sk.json index 091f147c..f3406606 100644 --- a/custom_components/ha_washdata/translations/sk.json +++ b/custom_components/ha_washdata/translations/sk.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "Nastavenie WashData", - "description": "Nakonfigurujte si práčku alebo iný spotrebič.\n\nSnímač výkonu je povinný.\n\nPo nastavení otvorte panel WashData z bočného panelu (alebo prejdite na [/ha-washdata](/ha-washdata)) na zobrazenie cyklov, ladenie detekcie a správu profilov.", + "description": "Nastavte práčku alebo iný spotrebič. Je potrebný snímač výkonu.\n\nPo nastavení otvorte panel WashData z bočného panela ([/ha-washdata](/ha-washdata)), kde nájdete cykly, profily a všetky nastavenia.", "data": { "name": "Názov zariadenia", "device_type": "Typ zariadenia", @@ -12,43 +12,41 @@ }, "data_description": { "name": "Priateľský názov pre toto zariadenie (napr. „Práčka“, „Umývačka riadu“).", - "device_type": "O aký typ spotrebiča ide? Pomáha prispôsobiť detekciu a označovanie.", - "power_sensor": "Entita senzora, ktorá hlási spotrebu energie v reálnom čase (vo wattoch) z vašej inteligentnej zásuvky.", - "min_power": "Údaje o príkone nad touto hranicou (vo wattoch) indikujú, že spotrebič beží. Začnite s 2 W pre väčšinu zariadení." + "device_type": "Nastaví predvolené hodnoty detekcie pre tento druh spotrebiča.", + "power_sensor": "Senzor inteligentnej zásuvky, ktorý hlási aktuálny výkon vo wattoch.", + "min_power": "Hodnoty nad touto hranicou (vo wattoch) znamenajú, že spotrebič beží. Pre väčšinu zariadení stačia 2 W." } }, "reconfigure": { - "title": "Reconfigure WashData", - "description": "Aktualizujte názov zariadenia, typ spotrebiča alebo snímač výkonu.\n\nPanel WashData (bočný panel alebo [/ha-washdata](/ha-washdata)) slúži na správu detekcie, cyklov, profilov a oznámení.", + "title": "Opätovná konfigurácia WashData", + "description": "Zmeňte názov zariadenia, typ spotrebiča alebo snímač výkonu.\n\nVšetko ostatné je v paneli WashData (bočný panel alebo [/ha-washdata](/ha-washdata)).", "data": { - "name": "Device Name", - "device_type": "Device Type", - "power_sensor": "Power Sensor", - "min_power": "Minimum Power Threshold (W)" + "name": "Názov zariadenia", + "device_type": "Typ zariadenia", + "power_sensor": "Snímač výkonu", + "min_power": "Minimálny prah výkonu (W)" } } }, "error": { - "cannot_connect": "Nepodarilo sa pripojiť", - "invalid_auth": "Neplatné overenie", "unknown": "Neočakávaná chyba", - "invalid_power": "Power threshold must be greater than 0" + "invalid_power": "Prah výkonu musí byť väčší ako 0" }, "abort": { "already_configured": "Zariadenie je už nakonfigurované", - "reconfigure_successful": "Reconfiguration was successful" + "reconfigure_successful": "Opätovná konfigurácia bola úspešná" } }, "options": { "step": { "init": { - "title": "WashData Settings", - "description": "Premenujte zariadenie alebo zmeňte jeho základné hardvérové nastavenia. Všetky nastavenia detekcie, párovania a oznámení sú na [paneli WashData](/ha-washdata) (dostupný aj z bočného panelu).", + "title": "Nastavenia WashData", + "description": "Premenujte zariadenie alebo zmeňte jeho základné nastavenia. Všetko ostatné je v [paneli WashData](/ha-washdata) v bočnom paneli.", "data": { "name": "Názov zariadenia", - "device_type": "Device Type", - "power_sensor": "Power Sensor", - "min_power": "Minimum Power Threshold (W)" + "device_type": "Typ zariadenia", + "power_sensor": "Snímač výkonu", + "min_power": "Minimálny prah výkonu (W)" } } }, @@ -60,7 +58,7 @@ "notify_live_waiting_message": "{device}: Zatiaľ nebol rozpoznaný žiadny profil.", "vs_typical_longer": "o {pct}% dlhšie než zvyčajne", "vs_typical_shorter": "o {pct}% kratšie než zvyčajne", - "invalid_power": "Power threshold must be greater than 0" + "invalid_power": "Prah výkonu musí byť väčší ako 0" } }, "selector": { @@ -93,7 +91,7 @@ }, "profile_name": { "name": "Názov profilu", - "description": "Názov existujúceho profilu (vytvorte profily v ponuke Spravovať profily). Ak chcete štítok odstrániť, nechajte pole prázdne." + "description": "Názov existujúceho profilu (profily vytvoríte v paneli WashData). Ponechajte prázdne na odstránenie štítka." } } }, @@ -143,13 +141,13 @@ }, "confidence_threshold": { "name": "Prah spoľahlivosti", - "description": "Minimálna spoľahlivosť zhody (0,50–0,95) na použitie štítkov." + "description": "Minimálna spoľahlivosť zhody (0,50–0,95) na použitie štítkov. Ak ju necháte prázdnu, použije sa nastavenie zariadenia „Spoľahlivosť autooznačovania“." } } }, "export_config": { "name": "Exportovať konfiguráciu", - "description": "Exportujte profily, cykly a nastavenia tohto zariadenia do súboru JSON (na server).", + "description": "Exportuje profily, cykly a nastavenia tohto zariadenia do súboru JSON.", "fields": { "device_id": { "name": "Zariadenie", @@ -177,7 +175,7 @@ }, "submit_cycle_feedback": { "name": "Odoslať spätnú väzbu cyklu", - "description": "Po dokončení cyklu potvrďte alebo opravte automaticky rozpoznaný program. Zadajte buď `entry_id` (rozšírené) alebo `device_id` (odporúčané).", + "description": "Potvrďte alebo opravte rozpoznaný program dokončeného cyklu. Zadajte `device_id` (odporúčané) alebo `entry_id`.", "fields": { "device_id": { "name": "Zariadenie", @@ -272,18 +270,18 @@ } }, "trigger_ml_training": { - "name": "Trigger ML Training", - "description": "Manually retrain the on-device ML models from this device's own labelled cycles (experimental).", + "name": "Spustiť ML tréning", + "description": "Ručne pretrénujte ML modely v zariadení z vlastných označených cyklov tohto zariadenia (experimentálne).", "fields": { "device_id": { - "name": "Device", - "description": "The WashData device to retrain models for." + "name": "Zariadenie", + "description": "Zariadenie WashData, ktorého modely sa majú pretrénovať." } } }, "mark_unloaded": { "name": "Označiť ako vyložené", - "description": "Potvrďte, že dokončená náplň bola vybratá zo zariadenia WashData, čím sa zruší stav „Čistý“ aj prípadná pripomienka vyloženia.", + "description": "Potvrďte, že dokončená náplň bola vybratá. Zruší stav „Čistý“ aj pripomienku vyloženia.", "fields": { "device_id": { "name": "Zariadenie", @@ -299,6 +297,32 @@ }, "match_ambiguity": { "name": "Nejednoznačnosť porovnania" + }, + "maintenance_due": { + "name": "Údržba na rade", + "state": { + "on": "Na rade", + "off": "Nie je na rade" + }, + "state_attributes": { + "due_task_ids": { + "name": "ID úloh na rade" + }, + "due_tasks": { + "name": "Úlohy na rade", + "state": { + "descale": "Odstránenie vodného kameňa", + "filter_clean": "Vyčistiť filter", + "drum_clean": "Vyčistiť bubon", + "bearing_service": "Servis ložísk", + "other": "Iné", + "salt": "Doplniť soľ", + "rinse_aid": "Doplniť leštidlo", + "lint_filter": "Vyčistiť filter na vlákna", + "condenser_clean": "Vyčistiť kondenzátor" + } + } + } } }, "sensor": { @@ -316,10 +340,18 @@ "anti_wrinkle": "Ochrana proti vráskam", "interrupted": "Prerušený", "force_stopped": "Nútene zastavené", - "rinse": "Oplachovanie", "unknown": "Neznámy", "clean": "Čistý", "delay_wait": "Čaká sa na spustenie" + }, + "state_attributes": { + "cycle_anomaly": { + "name": "Anomália cyklu", + "state": { + "overrun": "Beží dlho", + "stalled": "Zaseknuté" + } + } } }, "washer_program": { diff --git a/custom_components/ha_washdata/translations/sl.json b/custom_components/ha_washdata/translations/sl.json index bdeea77e..e886201c 100644 --- a/custom_components/ha_washdata/translations/sl.json +++ b/custom_components/ha_washdata/translations/sl.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "Nastavitev WashData", - "description": "Konfigurirajte svoj pralni stroj ali drugo napravo.\n\nSenzor moči je obvezen.\n\nPo nastavitvi odprite ploščo WashData s stranske vrstice (ali pojdite na [/ha-washdata](/ha-washdata)) za ogled ciklov, prilagoditev zaznavanja in upravljanje profilov.", + "description": "Nastavite pralni stroj ali drugo napravo. Potreben je senzor moči.\n\nPo nastavitvi odprite ploščo WashData v stranski vrstici ([/ha-washdata](/ha-washdata)) za cikle, profile in vse nastavitve.", "data": { "name": "Ime naprave", "device_type": "Vrsta naprave", @@ -12,14 +12,14 @@ }, "data_description": { "name": "Prijazno ime za to napravo (npr. \"Pralni stroj\", \"Pomivalni stroj\").", - "device_type": "Kakšna vrsta naprave je to? Pomaga pri prilagajanju odkrivanja in označevanja.", - "power_sensor": "Entiteta senzorja, ki poroča o porabi energije v realnem času (v vatih) iz vašega pametnega vtiča.", - "min_power": "Odčitki moči nad tem pragom (v vatih) pomenijo, da naprava deluje. Začnite z 2 W za večino naprav." + "device_type": "Nastavi privzete vrednosti zaznavanja za to vrsto naprave.", + "power_sensor": "Senzor pametnega vtiča, ki poroča trenutno moč v vatih.", + "min_power": "Odčitki nad to vrednostjo (v vatih) pomenijo, da naprava deluje. 2 W ustreza večini naprav." } }, "reconfigure": { "title": "Ponovno konfigurirajte WashData", - "description": "Posodobite ime naprave, vrsto naprave ali senzor moči.\n\nPlosca WashData (stranska vrstica ali [/ha-washdata](/ha-washdata)) je namenjena upravljanju zaznavanja, ciklov, profilov in obvestil.", + "description": "Spremenite ime naprave, vrsto naprave ali senzor moči.\n\nVse drugo je v plošči WashData (stranska vrstica ali [/ha-washdata](/ha-washdata)).", "data": { "name": "Ime naprave", "device_type": "Vrsta naprave", @@ -29,8 +29,6 @@ } }, "error": { - "cannot_connect": "Povezava ni uspela", - "invalid_auth": "Neveljavna avtentikacija", "unknown": "Nepričakovana napaka", "invalid_power": "Prag moči mora biti večji od 0" }, @@ -49,7 +47,7 @@ "min_power": "Najmanjši prag moči (W)" }, "title": "Nastavitve WashData", - "description": "Preimenujte napravo ali spremenite njene osnovne nastavitve strojne opreme. Vse nastavitve zaznavanja, ujemanja in obveščanja so na [plošči WashData](/ha-washdata) (dostopna tudi s stranske vrstice)." + "description": "Preimenujte napravo ali spremenite njene osnovne nastavitve. Vse drugo je v [plošči WashData](/ha-washdata) v stranski vrstici." } }, "error": { @@ -93,7 +91,7 @@ }, "profile_name": { "name": "Ime profila", - "description": "Ime obstoječega profila (profile ustvarite v meniju Upravljanje profilov). Pustite prazno, da odstranite oznako." + "description": "Ime obstoječega profila (profile ustvarite v plošči WashData). Pustite prazno, da odstranite oznako." } } }, @@ -143,13 +141,13 @@ }, "confidence_threshold": { "name": "Prag zaupanja", - "description": "Najmanjše zaupanje ujemanja (0,50–0,95) za uporabo oznak." + "description": "Najmanjše zaupanje ujemanja (0,50–0,95) za uporabo oznak. Pustite prazno, da se uporabi nastavitev naprave „Zaupanje samodejnega označevanja“." } } }, "export_config": { "name": "Izvozi konfiguracijo", - "description": "Izvozite profile, cikle in nastavitve te naprave v datoteko JSON (na strežnik).", + "description": "Izvozite profile, cikle in nastavitve te naprave v datoteko JSON.", "fields": { "device_id": { "name": "Naprava", @@ -177,7 +175,7 @@ }, "submit_cycle_feedback": { "name": "Pošlji povratne informacije o ciklu", - "description": "Po zaključenem ciklu potrdite ali popravite samodejno zaznan program. Navedite `entry_id` (napredno) ali `device_id` (priporočeno).", + "description": "Potrdite ali popravite zaznani program končanega cikla. Navedite `device_id` (priporočeno) ali `entry_id`.", "fields": { "device_id": { "name": "Naprava", @@ -283,7 +281,7 @@ }, "mark_unloaded": { "name": "Označi kot raztovorjeno", - "description": "Potrdite, da je bila končana vsebina vzeta iz naprave WashData, s čimer se počisti stanje „čisto“ in morebitni opomnik za raztovarjanje.", + "description": "Potrdite, da je bila končana vsebina vzeta ven. Počisti stanje „čisto“ in opomnik za raztovarjanje.", "fields": { "device_id": { "name": "Naprava", @@ -299,6 +297,32 @@ }, "match_ambiguity": { "name": "Dvoumnost ujemanja" + }, + "maintenance_due": { + "name": "Vzdrževanje na vrsti", + "state": { + "on": "Na vrsti", + "off": "Ni na vrsti" + }, + "state_attributes": { + "due_task_ids": { + "name": "ID-ji nalog na vrsti" + }, + "due_tasks": { + "name": "Naloge na vrsti", + "state": { + "descale": "Odstranjevanje vodnega kamna", + "filter_clean": "Očisti filter", + "drum_clean": "Očisti boben", + "bearing_service": "Servis ležajev", + "other": "Drugo", + "salt": "Dopolni sol", + "rinse_aid": "Dopolni sredstvo za izpiranje", + "lint_filter": "Očisti filter za vlakna", + "condenser_clean": "Očisti kondenzator" + } + } + } } }, "sensor": { @@ -317,9 +341,17 @@ "delay_wait": "Čakam na začetek", "interrupted": "Prekinjeno", "force_stopped": "Prisilno ustavljeno", - "rinse": "Izpiranje", "unknown": "Neznano", "clean": "čisto" + }, + "state_attributes": { + "cycle_anomaly": { + "name": "Anomalija cikla", + "state": { + "overrun": "Traja predolgo", + "stalled": "V zastoju" + } + } } }, "washer_program": { diff --git a/custom_components/ha_washdata/translations/sq.json b/custom_components/ha_washdata/translations/sq.json index 4ea58a6b..054fc041 100644 --- a/custom_components/ha_washdata/translations/sq.json +++ b/custom_components/ha_washdata/translations/sq.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "Konfigurimi WashData", - "description": "Konfiguro makinën larëse ose pajisje tjetër.\n\nKërkohet sensori i fuqisë.\n\nPas konfigurimit, hap panelin WashData nga shiriti anësor (ose shko te [/ha-washdata](/ha-washdata)) për të parë ciklet, rregulluar zbulimin dhe menaxhuar profilet.", + "description": "Konfiguro një makinë larëse ose pajisje tjetër. Kërkohet një sensor fuqie.\n\nPas konfigurimit, hap panelin WashData nga shiriti anësor ([/ha-washdata](/ha-washdata)) për të parë ciklet, profilet dhe të gjitha cilësimet.", "data": { "name": "Emri i pajisjes", "device_type": "Lloji i pajisjes", @@ -12,14 +12,14 @@ }, "data_description": { "name": "Një emër miqësor për këtë pajisje (p.sh., \"Lavatriçe\", \"Pjatalarëse\").", - "device_type": "Çfarë lloj pajisjeje është kjo? Ndihmon në përshtatjen e zbulimit dhe etiketimit.", - "power_sensor": "Njësia e sensorit që raporton konsumin e energjisë në kohë reale (në vat) nga spina juaj inteligjente.", - "min_power": "Leximet e fuqisë mbi këtë prag (në vat) tregojnë se pajisja është në punë. Filloni me 2 W për shumicën e pajisjeve." + "device_type": "Cakton parazgjedhjet e zbulimit për këtë lloj pajisjeje.", + "power_sensor": "Sensori i spinës inteligjente që raporton fuqinë në kohë reale, në vat.", + "min_power": "Leximet mbi këtë vlerë (në vat) tregojnë se pajisja është në punë. 2 W u përshtatet shumicës së pajisjeve." } }, "reconfigure": { "title": "Rikonfiguro WashData", - "description": "Përditëso emrin e pajisjes, llojin e pajisjes ose sensorin e fuqisë.\n\nPaneli WashData (shiriti anësor, ose [/ha-washdata](/ha-washdata)) është ku menaxhohen zbulimi, ciklet, profilet dhe njoftimet.", + "description": "Ndrysho emrin e pajisjes, llojin e saj ose sensorin e fuqisë.\n\nGjithçka tjetër është në panelin WashData (shiriti anësor, ose [/ha-washdata](/ha-washdata)).", "data": { "name": "Emri i pajisjes", "device_type": "Lloji i pajisjes", @@ -29,8 +29,6 @@ } }, "error": { - "cannot_connect": "Lidhja dështoi", - "invalid_auth": "Vërtetim i pavlefshëm", "unknown": "Gabim i papritur", "invalid_power": "Pragu i fuqisë duhet të jetë më i madh se 0" }, @@ -43,7 +41,7 @@ "step": { "init": { "title": "Cilësimet WashData", - "description": "Riemërtoni pajisjen ose ndryshoni cilësimet e saj bazë të harduerit. Të gjitha cilësimet e zbulimit, përputhjes dhe njoftimeve janë në [panelin WashData](/ha-washdata) (i arritshëm edhe nga shiriti anësor).", + "description": "Riemërtoni pajisjen ose ndryshoni cilësimet e saj bazë. Gjithçka tjetër është në [panelin WashData](/ha-washdata) në shiritin anësor.", "data": { "name": "Emri i pajisjes", "device_type": "Lloji i pajisjes", @@ -93,7 +91,7 @@ }, "profile_name": { "name": "Emri i profilit", - "description": "Emri i një profili ekzistues (krijoni profile në menunë Menaxho profilet). Lëreni bosh për të hequr etiketën." + "description": "Emri i një profili ekzistues (krijoni profile në panelin WashData). Lëreni bosh për të hequr etiketën." } } }, @@ -143,13 +141,13 @@ }, "confidence_threshold": { "name": "Pragu i besueshmërisë", - "description": "Besueshmëria minimale e përputhjes (0,50–0,95) për të aplikuar etiketat." + "description": "Besueshmëria minimale e përputhjes (0,50-0,95) për të aplikuar etiketat. Lëreni bosh për të përdorur cilësimin Besueshmëria e etiketimit automatik të pajisjes." } } }, "export_config": { "name": "Eksporto konfigurimin", - "description": "Eksporto profilet, ciklet dhe cilësimet e kësaj pajisjeje në një skedar JSON (për pajisje).", + "description": "Eksporto profilet, ciklet dhe cilësimet e kësaj pajisjeje në një skedar JSON.", "fields": { "device_id": { "name": "Pajisja", @@ -177,7 +175,7 @@ }, "submit_cycle_feedback": { "name": "Dërgo komente për ciklin", - "description": "Konfirmo ose korrigjo një program të zbuluar automatikisht pas një cikli të përfunduar. Jep `entry_id` (i avancuar) ose `device_id` (rekomandohet).", + "description": "Konfirmo ose korrigjo programin e zbuluar të një cikli të përfunduar. Jep `device_id` (rekomandohet) ose `entry_id`.", "fields": { "device_id": { "name": "Pajisja", @@ -283,7 +281,7 @@ }, "mark_unloaded": { "name": "Shëno si të shkarkuar", - "description": "Konfirmo se ngarkesa e përfunduar është hequr nga një pajisje WashData, duke pastruar gjendjen \"I pastër\" dhe çdo kujtesë për shkarkim.", + "description": "Konfirmo se ngarkesa e përfunduar u hoq. Pastron gjendjen \"I pastër\" dhe çdo kujtesë për shkarkim.", "fields": { "device_id": { "name": "Pajisja", @@ -299,6 +297,32 @@ }, "match_ambiguity": { "name": "Paqartësia e përputhjes" + }, + "maintenance_due": { + "name": "Mirëmbajtje e nevojshme", + "state": { + "on": "E nevojshme", + "off": "Jo e nevojshme" + }, + "state_attributes": { + "due_task_ids": { + "name": "ID-të e detyrave të nevojshme" + }, + "due_tasks": { + "name": "Detyrat e nevojshme", + "state": { + "descale": "Heqja e gurëve", + "filter_clean": "Pastro filtrin", + "drum_clean": "Pastro tamburin", + "bearing_service": "Servisi i kushinetave", + "other": "Tjetër", + "salt": "Rimbush kripën", + "rinse_aid": "Rimbush lëngun shpëlarës", + "lint_filter": "Pastro filtrin e pushit", + "condenser_clean": "Pastro kondensatorin" + } + } + } } }, "sensor": { @@ -316,10 +340,18 @@ "anti_wrinkle": "Kundër rrudhave", "interrupted": "E ndërprerë", "force_stopped": "Ndaluar me forcë", - "rinse": "Shpëlarje", "unknown": "E panjohur", "clean": "I pastër", "delay_wait": "Në pritje për të filluar" + }, + "state_attributes": { + "cycle_anomaly": { + "name": "Anomali e ciklit", + "state": { + "overrun": "Po zgjat", + "stalled": "I ngecur" + } + } } }, "washer_program": { diff --git a/custom_components/ha_washdata/translations/sr-Latn.json b/custom_components/ha_washdata/translations/sr-Latn.json index 307cbe00..be1298b2 100644 --- a/custom_components/ha_washdata/translations/sr-Latn.json +++ b/custom_components/ha_washdata/translations/sr-Latn.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "WashData podešavanje", - "description": "Konfiguriši svoju mašinu za pranje veša ili drugi uređaj.\n\nSenzor snage je obavezan.\n\nNakon podešavanja, otvori WashData panel iz bočne trake (ili idi na [/ha-washdata](/ha-washdata)) za pregled ciklusa, podešavanje detekcije i upravljanje profilima.", + "description": "Podesi mašinu za pranje veša ili drugi uređaj. Potreban je senzor snage.\n\nNakon podešavanja otvori WashData panel iz bočne trake ([/ha-washdata](/ha-washdata)) za cikluse, profile i sva podešavanja.", "data": { "name": "Naziv uređaja", "device_type": "Tip uređaja", @@ -12,14 +12,14 @@ }, "data_description": { "name": "Prijateljski naziv za ovaj uređaj (npr. 'Veš mašina', 'Mašina za suđe').", - "device_type": "Koja je ovo vrsta uređaja? Pomaže u prilagođenom otkrivanju i označavanju.", - "power_sensor": "Senzorski entitet koji prijavljuje potrošnju energije u realnom vremenu (u vatima) iz tvog pametnog priključka.", - "min_power": "Očitavanja snage iznad ovog praga (u vatima) ukazuju da uređaj radi. Počni sa 2W za većinu uređaja." + "device_type": "Postavlja podrazumevane vrednosti detekcije za ovu vrstu uređaja.", + "power_sensor": "Senzor pametnog utikača koji javlja trenutnu snagu u vatima.", + "min_power": "Očitavanja iznad ove vrednosti (u vatima) znače da uređaj radi. 2 W odgovara većini uređaja." } }, "reconfigure": { "title": "Ponovo konfiguriši WashData", - "description": "Ažuriraj naziv uređaja, tip uređaja ili senzor snage.\n\nWashData panel (bočna traka ili [/ha-washdata](/ha-washdata)) je mesto gde se upravljaju detekcija, ciklusi, profili i obaveštenja.", + "description": "Promeni naziv uređaja, tip uređaja ili senzor snage.\n\nSve ostalo je u WashData panelu (bočna traka ili [/ha-washdata](/ha-washdata)).", "data": { "name": "Naziv uređaja", "device_type": "Tip uređaja", @@ -29,8 +29,6 @@ } }, "error": { - "cannot_connect": "Povezivanje nije uspelo", - "invalid_auth": "Nevažeća autentifikacija", "unknown": "Neočekivana greška", "invalid_power": "Prag snage mora biti veći od 0" }, @@ -43,7 +41,7 @@ "step": { "init": { "title": "WashData podešavanja", - "description": "Preimenuj uređaj ili promeni njegova osnovna hardverska podešavanja. Sva podešavanja detekcije, podudaranja i obaveštenja nalaze se u [WashData panelu](/ha-washdata) (dostupan i iz bočne trake).", + "description": "Preimenuj uređaj ili promeni njegova osnovna podešavanja. Sve ostalo je u [WashData panelu](/ha-washdata) u bočnoj traci.", "data": { "name": "Naziv uređaja", "device_type": "Tip uređaja", @@ -93,7 +91,7 @@ }, "profile_name": { "name": "Naziv profila", - "description": "Naziv postojećeg profila (napravi profile u meniju Upravljaj profilima). Ostavi praznim da ukloniš oznaku." + "description": "Naziv postojećeg profila (profile pravi u WashData panelu). Ostavi prazno da ukloniš oznaku." } } }, @@ -143,13 +141,13 @@ }, "confidence_threshold": { "name": "Prag pouzdanosti", - "description": "Minimalna pouzdanost podudaranja (0,50–0,95) za primenu oznaka." + "description": "Minimalna pouzdanost podudaranja (0,50–0,95) za primenu oznaka. Ostavite prazno da bi se koristilo podešavanje uređaja „Pouzdanost automatskog označavanja“." } } }, "export_config": { "name": "Izvezi konfiguraciju", - "description": "Izvezi profile, cikluse i podešavanja ovog uređaja u JSON datoteku (po uređaju).", + "description": "Izvezi profile, cikluse i podešavanja ovog uređaja u JSON datoteku.", "fields": { "device_id": { "name": "Uređaj", @@ -177,7 +175,7 @@ }, "submit_cycle_feedback": { "name": "Pošalji povratne informacije o ciklusu", - "description": "Potvrdi ili ispravi automatski otkriveni program nakon završenog ciklusa. Navedi ili `entry_id` (napredno) ili `device_id` (preporučeno).", + "description": "Potvrdi ili ispravi otkriveni program završenog ciklusa. Navedi `device_id` (preporučeno) ili `entry_id`.", "fields": { "device_id": { "name": "Uređaj", @@ -283,7 +281,7 @@ }, "mark_unloaded": { "name": "Označi kao istovareno", - "description": "Potvrdi da je gotov sadržaj izvađen iz WashData uređaja, čime se briše stanje \"Čisto\" i svaki podsetnik za istovar.", + "description": "Potvrdi da je gotov sadržaj izvađen. Briše stanje \"Čisto\" i podsetnik za istovar.", "fields": { "device_id": { "name": "Uređaj", @@ -299,6 +297,32 @@ }, "match_ambiguity": { "name": "Višeznačnost podudaranja" + }, + "maintenance_due": { + "name": "Dospelo održavanje", + "state": { + "on": "Dospelo", + "off": "Nije dospelo" + }, + "state_attributes": { + "due_task_ids": { + "name": "ID-jevi dospelih zadataka" + }, + "due_tasks": { + "name": "Dospeli zadaci", + "state": { + "descale": "Uklanjanje kamenca", + "filter_clean": "Očisti filter", + "drum_clean": "Očisti bubanj", + "bearing_service": "Servis ležajeva", + "other": "Ostalo", + "salt": "Dopuni so", + "rinse_aid": "Dopuni sredstvo za ispiranje", + "lint_filter": "Očisti filter za vlakna", + "condenser_clean": "Očisti kondenzator" + } + } + } } }, "sensor": { @@ -316,10 +340,18 @@ "anti_wrinkle": "Zaštita od gužvanja", "interrupted": "Prekinuto", "force_stopped": "Prisilno zaustavljeno", - "rinse": "Ispiranje", "unknown": "Nepoznato", "clean": "Čisto", "delay_wait": "Čeka se početak" + }, + "state_attributes": { + "cycle_anomaly": { + "name": "Anomalija ciklusa", + "state": { + "overrun": "Traje predugo", + "stalled": "Zaglavljeno" + } + } } }, "washer_program": { diff --git a/custom_components/ha_washdata/translations/sv.json b/custom_components/ha_washdata/translations/sv.json index faead552..7d7c0808 100644 --- a/custom_components/ha_washdata/translations/sv.json +++ b/custom_components/ha_washdata/translations/sv.json @@ -2,8 +2,8 @@ "config": { "step": { "user": { - "title": "WashData Setup", - "description": "Konfigurera din tvättmaskin eller annan apparat.\n\nEffektsensor krävs.\n\nEfter installationen öppnar du WashData-panelen från sidofältet (eller gå till [/ha-washdata](/ha-washdata)) för att visa cykler, finjustera identifiering och hantera profiler.", + "title": "Konfigurera WashData", + "description": "Konfigurera en tvättmaskin eller annan apparat. En effektsensor krävs.\n\nÖppna efter konfigurationen WashData-panelen från sidofältet ([/ha-washdata](/ha-washdata)) för att se cykler, profiler och alla inställningar.", "data": { "name": "Enhetens namn", "device_type": "Enhetstyp", @@ -12,14 +12,14 @@ }, "data_description": { "name": "Ett vänligt namn för den här enheten (t.ex. \"Tvättmaskin\", \"Diskmaskin\").", - "device_type": "Vilken typ av apparat är detta? Hjälper till att skräddarsy detektering och märkning.", - "power_sensor": "Sensorenheten som rapporterar strömförbrukning i realtid (i watt) från din smarta kontakt.", - "min_power": "Effektavläsningar över detta tröskelvärde (i watt) indikerar att apparaten är igång. Börja med 2W för de flesta enheter." + "device_type": "Ställer in standardvärden för detektering för den här typen av apparat.", + "power_sensor": "Den smarta kontaktens sensor som rapporterar aktuell effekt i watt.", + "min_power": "Mätvärden över detta (i watt) betyder att apparaten är igång. 2 W passar de flesta enheter." } }, "reconfigure": { "title": "Konfigurera om WashData", - "description": "Uppdatera enhetsnamnet, apparattypen eller effektsensorn.\n\nWashData-panelen (sidofältet eller [/ha-washdata](/ha-washdata)) är där identifiering, cykler, profiler och notiser hanteras.", + "description": "Ändra enhetsnamn, apparattyp eller effektsensor.\n\nAllt annat finns i WashData-panelen (sidofältet eller [/ha-washdata](/ha-washdata)).", "data": { "name": "Enhetens namn", "device_type": "Enhetstyp", @@ -29,8 +29,6 @@ } }, "error": { - "cannot_connect": "Det gick inte att ansluta", - "invalid_auth": "Ogiltig autentisering", "unknown": "Oväntat fel", "invalid_power": "Effekttröskeln måste vara större än 0" }, @@ -49,7 +47,7 @@ "min_power": "Minsta effekttröskel (W)" }, "title": "WashData-inställningar", - "description": "Byt namn på enheten eller ändra dess grundläggande hårdvaruinställningar. Alla inställningar för identifiering, matchning och notiser finns i [WashData-panelen](/ha-washdata) (även nåbar från sidofältet)." + "description": "Byt namn på enheten eller ändra dess grundinställningar. Allt annat finns i [WashData-panelen](/ha-washdata) i sidofältet." } }, "error": { @@ -70,9 +68,9 @@ "dryer": "Torktumlare", "washer_dryer": "Kombination av tvättmaskin och torktumlare", "dishwasher": "Diskmaskin", - "air_fryer": "Air Fryer", + "air_fryer": "Airfryer", "bread_maker": "Brödmaskin", - "pump": "Pump / Sump Pump", + "pump": "Pump / Dräneringspump", "generic": "Annat (avancerat)", "other": "Tröskelenhet" } @@ -93,7 +91,7 @@ }, "profile_name": { "name": "Profilnamn", - "description": "Namnet på en befintlig profil (skapa profiler i menyn Hantera profiler). Lämna tomt för att ta bort etiketten." + "description": "Namnet på en befintlig profil (skapa profiler i WashData-panelen). Lämna tomt för att ta bort märkningen." } } }, @@ -143,13 +141,13 @@ }, "confidence_threshold": { "name": "Konfidenströskel", - "description": "Minsta matchningskonfidens (0,50–0,95) för att applicera etiketter." + "description": "Minsta matchningskonfidens (0,50–0,95) för att applicera etiketter. Lämna tomt för att använda enhetens inställning \"Automärkningskonfidens\"." } } }, "export_config": { "name": "Exportera konfiguration", - "description": "Exportera enhetens profiler, cykler och inställningar till en JSON-fil (per enhet).", + "description": "Exportera enhetens profiler, cykler och inställningar till en JSON-fil.", "fields": { "device_id": { "name": "Enhet", @@ -177,7 +175,7 @@ }, "submit_cycle_feedback": { "name": "Skicka cykelfeedback", - "description": "Bekräfta eller korrigera ett automatiskt identifierat program efter en avslutad cykel. Ange antingen `entry_id` (avancerat) eller `device_id` (rekommenderas).", + "description": "Bekräfta eller korrigera det identifierade programmet för en avslutad cykel. Ange `device_id` (rekommenderas) eller `entry_id`.", "fields": { "device_id": { "name": "Enhet", @@ -272,7 +270,7 @@ } }, "trigger_ml_training": { - "name": "Trigger ML Training", + "name": "Starta ML-träning", "description": "Träna om ML-modellerna på enheten manuellt från enhetens egna märkta cykler (experimentell).", "fields": { "device_id": { @@ -283,7 +281,7 @@ }, "mark_unloaded": { "name": "Markera som lossad", - "description": "Bekräfta att den färdiga tvätten har tagits ut ur en WashData-enhet, vilket rensar tillståndet \"Rena\" och en eventuell lossningspåminnelse.", + "description": "Bekräfta att den färdiga tvätten har tagits ut. Rensar tillståndet \"Ren\" och en eventuell lossningspåminnelse.", "fields": { "device_id": { "name": "Enhet", @@ -299,6 +297,32 @@ }, "match_ambiguity": { "name": "Matchningstvetydighet" + }, + "maintenance_due": { + "name": "Dags för underhåll", + "state": { + "on": "Dags", + "off": "Inte dags" + }, + "state_attributes": { + "due_task_ids": { + "name": "ID för uppgifter att göra" + }, + "due_tasks": { + "name": "Uppgifter att göra", + "state": { + "descale": "Avkalka", + "filter_clean": "Rengör filter", + "drum_clean": "Rengör trumma", + "bearing_service": "Lagerservice", + "other": "Övrigt", + "salt": "Fyll på salt", + "rinse_aid": "Fyll på spolglans", + "lint_filter": "Rengör luddfilter", + "condenser_clean": "Rengör kondensor" + } + } + } } }, "sensor": { @@ -317,9 +341,17 @@ "delay_wait": "Väntar på att starta", "interrupted": "Avbruten", "force_stopped": "Tvångsstoppad", - "rinse": "Sköljning", "unknown": "Okänd", - "clean": "Rena" + "clean": "Ren" + }, + "state_attributes": { + "cycle_anomaly": { + "name": "Cykelavvikelse", + "state": { + "overrun": "Kör längre", + "stalled": "Avstannad" + } + } } }, "washer_program": { diff --git a/custom_components/ha_washdata/translations/tr.json b/custom_components/ha_washdata/translations/tr.json index 9345a772..692e8f71 100644 --- a/custom_components/ha_washdata/translations/tr.json +++ b/custom_components/ha_washdata/translations/tr.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "WashData Kurulumu", - "description": "Çamaşır makinenizi veya diğer cihazınızı yapılandırın.\n\nGüç sensörü gereklidir.\n\nKurulumdan sonra döngüleri görüntülemek, algılamayı ayarlamak ve profilleri yönetmek için kenar çubuğundan WashData panelini açın (veya [/ha-washdata](/ha-washdata) adresine gidin).", + "description": "Bir çamaşır makinesi veya başka bir cihaz kurun. Güç sensörü gereklidir.\n\nKurulumdan sonra döngüleri, profilleri ve tüm ayarları görmek için kenar çubuğundan WashData panelini açın ([/ha-washdata](/ha-washdata)).", "data": { "name": "Cihaz Adı", "device_type": "Cihaz Türü", @@ -12,43 +12,41 @@ }, "data_description": { "name": "Bu cihaz için kolay bir ad (ör. 'Çamaşır Makinesi', 'Bulaşık Makinesi').", - "device_type": "Bu ne tür bir cihaz? Tespit ve etiketlemenin özel olarak yapılmasına yardımcı olur.", - "power_sensor": "Akıllı prizinizin gerçek zamanlı güç tüketimini (watt cinsinden) bildiren sensör varlığı.", - "min_power": "Bu eşiğin üzerindeki güç değerleri (watt cinsinden) cihazın çalıştığını gösterir. Çoğu cihaz için 2W ile başlayın." + "device_type": "Bu tür cihaz için algılama varsayılanlarını ayarlar.", + "power_sensor": "Canlı gücü watt cinsinden bildiren akıllı priz sensörü.", + "min_power": "Bunun üzerindeki okumalar (watt) cihazın çalıştığı anlamına gelir. Çoğu cihaz için 2 W uygundur." } }, "reconfigure": { - "title": "Reconfigure WashData", - "description": "Cihaz adını, cihaz türünü veya güç sensörünü güncelleyin.\n\nAlgılama, döngüler, profiller ve bildirimler WashData panelinde (kenar çubuğu veya [/ha-washdata](/ha-washdata)) yönetilir.", + "title": "WashData'yı Yeniden Yapılandır", + "description": "Cihaz adını, cihaz türünü veya güç sensörünü değiştirin.\n\nDiğer her şey WashData panelindedir (kenar çubuğu veya [/ha-washdata](/ha-washdata)).", "data": { - "name": "Device Name", - "device_type": "Device Type", - "power_sensor": "Power Sensor", - "min_power": "Minimum Power Threshold (W)" + "name": "Cihaz Adı", + "device_type": "Cihaz Türü", + "power_sensor": "Güç Sensörü", + "min_power": "Minimum Güç Eşiği (W)" } } }, "error": { - "cannot_connect": "Bağlantı başarısız oldu", - "invalid_auth": "Geçersiz kimlik doğrulama", "unknown": "Beklenmeyen hata", - "invalid_power": "Power threshold must be greater than 0" + "invalid_power": "Güç eşiği 0'dan büyük olmalıdır" }, "abort": { "already_configured": "Cihaz zaten yapılandırılmış", - "reconfigure_successful": "Reconfiguration was successful" + "reconfigure_successful": "Yeniden yapılandırma başarılı oldu" } }, "options": { "step": { "init": { - "title": "WashData Settings", - "description": "Cihazı yeniden adlandırın veya temel donanım ayarlarını değiştirin. Tüm algılama, eşleştirme ve bildirim ayarları [WashData panelinde](/ha-washdata) bulunur (kenar çubuğundan da erişilebilir).", + "title": "WashData Ayarları", + "description": "Cihazı yeniden adlandırın veya temel ayarlarını değiştirin. Diğer her şey kenar çubuğundaki [WashData panelinde](/ha-washdata) bulunur.", "data": { "name": "Cihaz Adı", - "device_type": "Device Type", - "power_sensor": "Power Sensor", - "min_power": "Minimum Power Threshold (W)" + "device_type": "Cihaz Türü", + "power_sensor": "Güç Sensörü", + "min_power": "Minimum Güç Eşiği (W)" } } }, @@ -60,7 +58,7 @@ "notify_live_waiting_message": "{device}: Henüz eşleşen profil yok.", "vs_typical_longer": "olağandan %{pct} daha uzun", "vs_typical_shorter": "olağandan %{pct} daha kısa", - "invalid_power": "Power threshold must be greater than 0" + "invalid_power": "Güç eşiği 0'dan büyük olmalıdır" } }, "selector": { @@ -93,7 +91,7 @@ }, "profile_name": { "name": "Profil Adı", - "description": "Mevcut bir profilin adı (Profilleri Yönet menüsünde profiller oluşturun). Etiketi kaldırmak için boş bırakın." + "description": "Mevcut bir profil adı (profilleri WashData panelinde oluşturun). Etiketi kaldırmak için boş bırakın." } } }, @@ -143,13 +141,13 @@ }, "confidence_threshold": { "name": "Güven Eşiği", - "description": "Etiketleri uygulamak için minimum eşleşme güvenirliği (0,50-0,95)." + "description": "Etiketleri uygulamak için minimum eşleşme güvenirliği (0,50-0,95). Cihazın Otomatik Etiket Güveni ayarını kullanmak için boş bırakın." } } }, "export_config": { "name": "Yapılandırmayı Dışa Aktar", - "description": "Bu cihazın profillerini, döngülerini ve ayarlarını bir JSON dosyasına (cihaz başına) aktarın.", + "description": "Bu cihazın profillerini, döngülerini ve ayarlarını bir JSON dosyasına aktarın.", "fields": { "device_id": { "name": "Cihaz", @@ -177,7 +175,7 @@ }, "submit_cycle_feedback": { "name": "Döngü Geri Bildirimi Gönder", - "description": "Tamamlanan bir döngüden sonra otomatik olarak algılanan bir programı onaylayın veya düzeltin. \"Entry_id\" (gelişmiş) veya \"device_id\" (önerilen) bilgilerini sağlayın.", + "description": "Biten bir döngünün algılanan programını onaylayın veya düzeltin. `device_id` (önerilen) veya `entry_id` verin.", "fields": { "device_id": { "name": "Cihaz", @@ -272,18 +270,18 @@ } }, "trigger_ml_training": { - "name": "Trigger ML Training", - "description": "Manually retrain the on-device ML models from this device's own labelled cycles (experimental).", + "name": "ML Eğitimini Başlat", + "description": "Cihazdaki ML modellerini bu cihazın kendi etiketlenmiş döngülerinden manuel olarak yeniden eğitin (deneysel).", "fields": { "device_id": { - "name": "Device", - "description": "The WashData device to retrain models for." + "name": "Cihaz", + "description": "Modelleri yeniden eğitilecek WashData cihazı." } } }, "mark_unloaded": { "name": "Boşaltıldı Olarak İşaretle", - "description": "Bir WashData cihazındaki tamamlanmış yükün çıkarıldığını onaylar, Temiz durumunu ve varsa boşaltma hatırlatıcısını temizler.", + "description": "Biten yükün çıkarıldığını onaylayın. Temiz durumunu ve varsa boşaltma hatırlatıcısını temizler.", "fields": { "device_id": { "name": "Cihaz", @@ -299,6 +297,32 @@ }, "match_ambiguity": { "name": "Eşleşme Belirsizliği" + }, + "maintenance_due": { + "name": "Bakım zamanı", + "state": { + "on": "Zamanı geldi", + "off": "Zamanı gelmedi" + }, + "state_attributes": { + "due_task_ids": { + "name": "Zamanı gelen görev kimlikleri" + }, + "due_tasks": { + "name": "Zamanı gelen görevler", + "state": { + "descale": "Kireç çözme", + "filter_clean": "Filtreyi temizle", + "drum_clean": "Tamburu temizle", + "bearing_service": "Rulman servisi", + "other": "Diğer", + "salt": "Tuz ekle", + "rinse_aid": "Parlatıcı ekle", + "lint_filter": "Tüy filtresini temizle", + "condenser_clean": "Kondenseri temizle" + } + } + } } }, "sensor": { @@ -316,10 +340,18 @@ "anti_wrinkle": "Kırışıklık Karşıtı", "interrupted": "Kesintiye uğradı", "force_stopped": "Zorla Durduruldu", - "rinse": "Durulmak", "unknown": "Bilinmiyor", "clean": "Temiz", "delay_wait": "Başlamak için Bekleniyor" + }, + "state_attributes": { + "cycle_anomaly": { + "name": "Döngü anomalisi", + "state": { + "overrun": "Uzun sürüyor", + "stalled": "Takıldı" + } + } } }, "washer_program": { diff --git a/custom_components/ha_washdata/translations/uk.json b/custom_components/ha_washdata/translations/uk.json index 274866d7..60ba8b5f 100644 --- a/custom_components/ha_washdata/translations/uk.json +++ b/custom_components/ha_washdata/translations/uk.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "Налаштування WashData", - "description": "Налаштуйте пральну машину чи інший прилад.\n\nПотрібен датчик потужності.\n\nПісля налаштування відкрийте панель WashData з бічної панелі (або перейдіть на [/ha-washdata](/ha-washdata)), щоб переглядати цикли, налаштувати виявлення та керувати профілями.", + "description": "Налаштуйте пральну машину або інший прилад. Потрібен датчик потужності.\n\nПісля налаштування відкрийте панель WashData на бічній панелі ([/ha-washdata](/ha-washdata)): там цикли, профілі та всі налаштування.", "data": { "name": "Назва пристрою", "device_type": "Тип пристрою", @@ -12,43 +12,41 @@ }, "data_description": { "name": "Зрозуміла назва цього пристрою (наприклад, «Пральна машина», «Посудомийна машина»).", - "device_type": "Що це за тип приладу? Допомагає адаптувати виявлення та маркування.", - "power_sensor": "Датчик, який повідомляє споживання електроенергії в реальному часі (у ватах) від вашої розумної розетки.", - "min_power": "Показники потужності вище цього порогу (у ватах) вказують на те, що прилад працює. Почніть з 2 Вт для більшості пристроїв." + "device_type": "Задає типові параметри виявлення для цього типу приладу.", + "power_sensor": "Датчик розумної розетки, що передає поточну потужність у ватах.", + "min_power": "Показники вище цього значення (у ватах) означають, що прилад працює. Для більшості пристроїв підходить 2 Вт." } }, "reconfigure": { - "title": "Reconfigure WashData", - "description": "Оновіть назву пристрою, тип приладу або датчик потужності.\n\nПанель WashData (бічна панель або [/ha-washdata](/ha-washdata)) — це місце для керування виявленням, циклами, профілями та сповіщеннями.", + "title": "Повторне налаштування WashData", + "description": "Змініть назву пристрою, тип приладу або датчик потужності.\n\nУсе інше в панелі WashData (бічна панель або [/ha-washdata](/ha-washdata)).", "data": { - "name": "Device Name", - "device_type": "Device Type", - "power_sensor": "Power Sensor", - "min_power": "Minimum Power Threshold (W)" + "name": "Назва пристрою", + "device_type": "Тип пристрою", + "power_sensor": "Датчик потужності", + "min_power": "Мінімальний поріг потужності (Вт)" } } }, "error": { - "cannot_connect": "Не вдалося підключитися", - "invalid_auth": "Недійсна автентифікація", "unknown": "Несподівана помилка", - "invalid_power": "Power threshold must be greater than 0" + "invalid_power": "Поріг потужності має бути більшим за 0" }, "abort": { "already_configured": "Пристрій уже налаштовано", - "reconfigure_successful": "Reconfiguration was successful" + "reconfigure_successful": "Повторне налаштування виконано успішно" } }, "options": { "step": { "init": { - "title": "WashData Settings", - "description": "Перейменуйте пристрій або змініть його основні апаратні налаштування. Усі налаштування виявлення, зіставлення та сповіщень знаходяться в [панелі WashData](/ha-washdata) (також доступна з бічної панелі).", + "title": "Налаштування WashData", + "description": "Перейменуйте пристрій або змініть його основні налаштування. Усе інше в [панелі WashData](/ha-washdata) на бічній панелі.", "data": { "name": "Назва пристрою", - "device_type": "Device Type", - "power_sensor": "Power Sensor", - "min_power": "Minimum Power Threshold (W)" + "device_type": "Тип пристрою", + "power_sensor": "Датчик потужності", + "min_power": "Мінімальний поріг потужності (Вт)" } } }, @@ -60,7 +58,7 @@ "notify_live_waiting_message": "{device}: Профіль ще не розпізнано.", "vs_typical_longer": "на {pct}% довше, ніж зазвичай", "vs_typical_shorter": "на {pct}% коротше, ніж зазвичай", - "invalid_power": "Power threshold must be greater than 0" + "invalid_power": "Поріг потужності має бути більшим за 0" } }, "selector": { @@ -93,7 +91,7 @@ }, "profile_name": { "name": "Ім'я профілю", - "description": "Ім’я наявного профілю (створити профілі в меню Керування профілями). Залиште поле порожнім, щоб видалити мітку." + "description": "Назва наявного профілю (профілі створюються в панелі WashData). Залиште порожнім, щоб зняти мітку." } } }, @@ -143,13 +141,13 @@ }, "confidence_threshold": { "name": "Поріг впевненості", - "description": "Мінімальна впевненість збігу (0,50-0,95) для застосування міток." + "description": "Мінімальна впевненість збігу (0,50-0,95) для застосування міток. Залиште порожнім, щоб використати налаштування пристрою «Впевненість автоматичного маркування»." } } }, "export_config": { "name": "Експорт конфігурації", - "description": "Експортуйте профілі, цикли та налаштування цього пристрою у файл JSON (для кожного пристрою).", + "description": "Експортувати профілі, цикли та налаштування цього пристрою у файл JSON.", "fields": { "device_id": { "name": "Пристрій", @@ -177,7 +175,7 @@ }, "submit_cycle_feedback": { "name": "Надіслати відгук про цикл", - "description": "Підтвердьте або виправте автоматично визначену програму після завершення циклу. Укажіть `entry_id` (розширений) або `device_id` (рекомендовано).", + "description": "Підтвердьте або виправте визначену програму завершеного циклу. Укажіть `device_id` (рекомендовано) або `entry_id`.", "fields": { "device_id": { "name": "Пристрій", @@ -272,18 +270,18 @@ } }, "trigger_ml_training": { - "name": "Trigger ML Training", + "name": "Запустити навчання ML", "description": "Вручну перенавчіть моделі ML на пристрої з власних позначених циклів цього пристрою (експериментальний).", "fields": { "device_id": { - "name": "Device", - "description": "The WashData device to retrain models for." + "name": "Пристрій", + "description": "Пристрій WashData, моделі якого потрібно перенавчити." } } }, "mark_unloaded": { "name": "Позначити як вивантажене", - "description": "Підтвердьте, що вміст завершеного циклу вийнято з пристрою WashData: стан «Чистий» і нагадування про вивантаження буде скинуто.", + "description": "Підтвердьте, що готове завантаження вийнято. Скидає стан «Чистий» і нагадування про вивантаження.", "fields": { "device_id": { "name": "Пристрій", @@ -299,6 +297,32 @@ }, "match_ambiguity": { "name": "Неоднозначність відповідності" + }, + "maintenance_due": { + "name": "Потрібне обслуговування", + "state": { + "on": "Потрібно", + "off": "Не потрібно" + }, + "state_attributes": { + "due_task_ids": { + "name": "ID потрібних завдань" + }, + "due_tasks": { + "name": "Потрібні завдання", + "state": { + "descale": "Видалення накипу", + "filter_clean": "Очистити фільтр", + "drum_clean": "Очистити барабан", + "bearing_service": "Обслуговування підшипників", + "other": "Інше", + "salt": "Додати сіль", + "rinse_aid": "Додати ополіскувач", + "lint_filter": "Очистити фільтр ворсу", + "condenser_clean": "Очистити конденсатор" + } + } + } } }, "sensor": { @@ -316,10 +340,18 @@ "anti_wrinkle": "Проти зморшок", "interrupted": "Перерваний", "force_stopped": "Примусово зупинено", - "rinse": "Полоскання", "unknown": "Невідомо", "clean": "Чистий", "delay_wait": "Очікування початку" + }, + "state_attributes": { + "cycle_anomaly": { + "name": "Аномалія циклу", + "state": { + "overrun": "Працює довго", + "stalled": "Застряг" + } + } } }, "washer_program": { diff --git a/custom_components/ha_washdata/translations/zh-Hans.json b/custom_components/ha_washdata/translations/zh-Hans.json index 1dd46c85..04322a3d 100644 --- a/custom_components/ha_washdata/translations/zh-Hans.json +++ b/custom_components/ha_washdata/translations/zh-Hans.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "WashData 设置", - "description": "配置您的洗衣机或其他家电设备。\n\n需要功率传感器。\n\n设置完成后,请从侧边栏打开 WashData 面板(或访问 [/ha-washdata](/ha-washdata))来查看周期、调整检测参数和管理配置文件。", + "description": "设置洗衣机或其他家电。需要功率传感器。\n\n设置完成后,从侧边栏打开 WashData 面板([/ha-washdata](/ha-washdata)),即可查看周期、配置文件和所有设置。", "data": { "name": "设备名称", "device_type": "设备类型", @@ -12,14 +12,14 @@ }, "data_description": { "name": "该设备的友好名称(例如“洗衣机”、“洗碗机”)。", - "device_type": "这是什么类型的设备?帮助定制检测和标记。", - "power_sensor": "报告智能插头实时功耗(以瓦为单位)的传感器实体。", - "min_power": "功率读数高于此阈值(以瓦为单位)表明设备正在运行。大多数设备从 2W 开始。" + "device_type": "为此类家电设置检测默认值。", + "power_sensor": "以瓦为单位报告实时功率的智能插头传感器。", + "min_power": "高于此值(瓦)的读数表示设备正在运行。大多数设备适合 2 W。" } }, "reconfigure": { "title": "重新配置 WashData", - "description": "更新设备名称、家电类型或功率传感器。\n\nWashData 面板(侧边栏或 [/ha-washdata](/ha-washdata))用于管理检测、周期、配置文件和通知。", + "description": "更改设备名称、家电类型或功率传感器。\n\n其他所有设置都在 WashData 面板中(侧边栏或 [/ha-washdata](/ha-washdata))。", "data": { "name": "设备名称", "device_type": "设备类型", @@ -29,8 +29,6 @@ } }, "error": { - "cannot_connect": "连接失败", - "invalid_auth": "验证无效", "unknown": "意外错误", "invalid_power": "功率阈值必须大于 0" }, @@ -43,7 +41,7 @@ "step": { "init": { "title": "WashData 设置", - "description": "重命名设备或更改其核心硬件设置。所有检测、匹配和通知设置均在 [WashData 面板](/ha-washdata) 中(也可从侧边栏访问)。", + "description": "重命名设备或更改其核心设置。其他所有设置都在侧边栏的 [WashData 面板](/ha-washdata) 中。", "data": { "name": "设备名称", "device_type": "设备类型", @@ -93,7 +91,7 @@ }, "profile_name": { "name": "个人资料名称", - "description": "现有配置文件的名称(在“管理配置文件”菜单中创建配置文件)。留空以删除标签。" + "description": "现有配置文件的名称(在 WashData 面板中创建配置文件)。留空则删除标签。" } } }, @@ -143,13 +141,13 @@ }, "confidence_threshold": { "name": "置信阈值", - "description": "应用标签的最小匹配置信度 (0.50-0.95)。" + "description": "应用标签的最小匹配置信度 (0.50-0.95)。留空则使用设备的“自动标记置信度”设置。" } } }, "export_config": { "name": "导出配置", - "description": "将此设备的配置文件、周期和设置导出到 JSON 文件(每个设备)。", + "description": "将此设备的配置文件、周期和设置导出到 JSON 文件。", "fields": { "device_id": { "name": "设备", @@ -177,7 +175,7 @@ }, "submit_cycle_feedback": { "name": "提交周期反馈", - "description": "完成循环后确认或更正自动检测的程序。提供“entry_id”(高级)或“device_id”(推荐)。", + "description": "确认或更正已完成周期检测到的程序。请提供 `device_id`(推荐)或 `entry_id`。", "fields": { "device_id": { "name": "设备", @@ -283,7 +281,7 @@ }, "mark_unloaded": { "name": "标记为已卸载", - "description": "确认已从 WashData 设备中取出完成的物品,清除干净状态和所有卸载提醒。", + "description": "确认已取出完成的物品。清除干净状态和所有卸载提醒。", "fields": { "device_id": { "name": "设备", @@ -299,6 +297,32 @@ }, "match_ambiguity": { "name": "匹配歧义" + }, + "maintenance_due": { + "name": "需要维护", + "state": { + "on": "到期", + "off": "未到期" + }, + "state_attributes": { + "due_task_ids": { + "name": "到期任务 ID" + }, + "due_tasks": { + "name": "到期任务", + "state": { + "descale": "除垢", + "filter_clean": "清洁过滤器", + "drum_clean": "清洁滚筒", + "bearing_service": "轴承保养", + "other": "其他", + "salt": "补充软水盐", + "rinse_aid": "补充光亮剂", + "lint_filter": "清洁绒毛过滤器", + "condenser_clean": "清洁冷凝器" + } + } + } } }, "sensor": { @@ -316,10 +340,18 @@ "anti_wrinkle": "抗皱", "interrupted": "被打断", "force_stopped": "强制停止", - "rinse": "冲洗", "unknown": "未知", "clean": "干净的", "delay_wait": "等待开始" + }, + "state_attributes": { + "cycle_anomaly": { + "name": "周期异常", + "state": { + "overrun": "运行超时", + "stalled": "停滞" + } + } } }, "washer_program": { diff --git a/custom_components/ha_washdata/ws_api.py b/custom_components/ha_washdata/ws_api.py index 36caf52d..e439a928 100644 --- a/custom_components/ha_washdata/ws_api.py +++ b/custom_components/ha_washdata/ws_api.py @@ -19,6 +19,7 @@ from __future__ import annotations import asyncio import collections +import copy import functools import json import logging @@ -32,12 +33,15 @@ from typing import Any import voluptuous as vol from homeassistant.components import websocket_api +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.storage import Store from homeassistant.util import dt as dt_util from .const import ( - CONF_AUTO_LABEL_CONFIDENCE, + coerce_numeric_option, + drop_invalid_numeric_options, + numeric_option_keys, CONF_NAME, CONF_COMPLETION_MIN_SECONDS, CONF_DEVICE_TYPE, @@ -51,32 +55,25 @@ from .const import ( CONF_UNLOAD_CONFIRM_ENTITY, CONF_ANTI_WRINKLE_EXIT_POWER, CONF_ANTI_WRINKLE_MAX_POWER, - CONF_DURATION_TOLERANCE, CONF_END_ENERGY_THRESHOLD, - CONF_END_REPEAT_COUNT, CONF_ENERGY_SENSOR, CONF_EXTERNAL_END_TRIGGER, - CONF_LEARNING_CONFIDENCE, CONF_LINKED_DEVICE, CONF_MAINTENANCE_REMINDER_CYCLES, CONF_MIN_OFF_GAP, CONF_MIN_POWER, CONF_NO_UPDATE_ACTIVE_TIMEOUT, CONF_OFF_DELAY, - CONF_PROFILE_DURATION_TOLERANCE, CONF_PROFILE_MATCH_INTERVAL, CONF_PROFILE_MATCH_MAX_DURATION_RATIO, CONF_PROFILE_MATCH_MIN_DURATION_RATIO, - CONF_PROFILE_MATCH_THRESHOLD, CONF_PROFILE_MIN_WARMUP_CYCLES, - CONF_PROFILE_UNMATCH_THRESHOLD, CONF_PUMP_STUCK_DURATION, CONF_POWER_OFF_THRESHOLD_W, CONF_POWER_SENSOR, CONF_ENERGY_PRICE_ENTITY, CONF_NOTIFY_PEOPLE, CONF_SAMPLING_INTERVAL, - CONF_SMOOTHING_WINDOW, CONF_START_DURATION_THRESHOLD, CONF_START_THRESHOLD_W, CONF_STOP_THRESHOLD_W, @@ -84,28 +81,25 @@ from .const import ( CONF_WATCHDOG_INTERVAL, DEFAULT_DEVICE_TYPE, DISHWASHER_END_SPIKE_QUIET_RELEASE_SECONDS, - DEFAULT_SMART_TERMINATION_DURATION_RATIO, - DEFAULT_SMART_TERMINATION_DURATION_RATIO_BY_DEVICE, DEFAULT_ANTI_CREASE_FINALIZE_RATIO, DEFAULT_PROFILE_MATCH_MAX_DURATION_RATIO, - DEFAULT_CURVE_PREROLL_SECONDS, CURVE_PREROLL_MAX_SECONDS, resolve_sampling_interval_default, resolve_watchdog_interval_default, resolve_start_duration_default, resolve_smart_termination_duration_ratio_default, - DEFAULT_MAINTENANCE_REMINDER_CYCLES, - DEFAULT_MIN_POWER, - DEFAULT_OFF_DELAY, - DEFAULT_OFF_DELAY_BY_DEVICE, + MAINTENANCE_CUSTOM_TASK_MAX, + MAINTENANCE_INTERVAL_CYCLES_MAX, + MAINTENANCE_INTERVAL_DAYS_MAX, + MAINTENANCE_TASK_NAME_MAX, resolve_min_off_gap_default, resolve_off_delay_default, - DEFAULT_PROFILE_MATCH_THRESHOLD, DEVICE_TYPE_PUMP, MAINTENANCE_EVENT_TYPES, + PLAYGROUND_PRESET_MAX, + ML_HEALTH_MIN_TRACE_POINTS, DEVICE_TYPES, DOMAIN, - ENABLE_ML_SUGGESTIONS, ENABLE_ML_TRAINING, HISTORY_IMPORT_CHUNK_BYTES, HISTORY_IMPORT_CHUNK_SAMPLES, @@ -115,7 +109,6 @@ from .const import ( HISTORY_IMPORT_MAX_TOTAL_CYCLES, HISTORY_IMPORT_RECORDER_EMPTY_DAY_STOP, HISTORY_IMPORT_RECORDER_MAX_DAYS, - PLAYGROUND_PRESET_MAX, SHOW_ML_LAB, STATE_COLORS, ) @@ -124,9 +117,13 @@ from . import playground from . import task_registry from .cycle_detector import ( CycleDetectorConfig, - effective_anticrease_finalize_ratio, - effective_curve_preroll_seconds, ) +from .detector_config import ( + build_detector_config, + effective_option_values, + inverted_threshold_pair, +) +from .maintenance import editor_types, effective_reminders from .options_utils import strip_null_options from .setup_advisor import compute_setup_phase from .ws_schema import WS_OPEN_RESPONSES, WS_RESPONSE_TYPES @@ -204,31 +201,27 @@ _CYCLE_STRIP_KEYS = frozenset({"power_data", "power_trace", "debug_data", "sampl # Settings keys that can be staged from suggestions. Mirrors the OptionsFlow's # _suggestion_keys_to_apply so the panel and the flow agree on what is tunable. +# The keys the suggestion engine can still produce. A stored suggestion for any +# other key - one written by an older version for a setting no longer suggested +# (sampling_interval, smoothing_window, start debounce, the confidence ladder, the +# tolerances, end_repeat_count; audit SUGGEST-01/04/12) - is never shown or applied. _SUGGESTION_KEYS: tuple[str, ...] = ( CONF_MIN_POWER, CONF_OFF_DELAY, CONF_WATCHDOG_INTERVAL, CONF_NO_UPDATE_ACTIVE_TIMEOUT, - CONF_SAMPLING_INTERVAL, + # Held back from 2026-10-04 (audit SUGGEST-21) until register item 469(b): a + # shorter interval let an ambiguous tick land in the end wait and delay the end + # (6.7 -> 23.2 min on one cycle). `match_rules.hold_in_ending` closes that. CONF_PROFILE_MATCH_INTERVAL, - CONF_AUTO_LABEL_CONFIDENCE, - CONF_DURATION_TOLERANCE, - CONF_PROFILE_DURATION_TOLERANCE, CONF_PROFILE_MATCH_MIN_DURATION_RATIO, CONF_PROFILE_MATCH_MAX_DURATION_RATIO, CONF_MIN_OFF_GAP, CONF_START_THRESHOLD_W, CONF_STOP_THRESHOLD_W, CONF_END_ENERGY_THRESHOLD, - # Stage 1 detection suggestions - CONF_SMOOTHING_WINDOW, - CONF_START_DURATION_THRESHOLD, CONF_COMPLETION_MIN_SECONDS, - CONF_LEARNING_CONFIDENCE, - CONF_PROFILE_MATCH_THRESHOLD, - CONF_END_REPEAT_COUNT, # Device-class-specific suggestions from reconcile_suggestions - CONF_PROFILE_UNMATCH_THRESHOLD, CONF_POWER_OFF_THRESHOLD_W, CONF_ANTI_WRINKLE_EXIT_POWER, CONF_ANTI_WRINKLE_MAX_POWER, @@ -242,9 +235,7 @@ _SUGGESTION_INT_KEYS: frozenset[str] = frozenset({ CONF_NO_UPDATE_ACTIVE_TIMEOUT, CONF_PROFILE_MATCH_INTERVAL, CONF_MIN_OFF_GAP, - CONF_SMOOTHING_WINDOW, CONF_COMPLETION_MIN_SECONDS, - CONF_END_REPEAT_COUNT, CONF_PUMP_STUCK_DURATION, }) @@ -261,7 +252,7 @@ def _coerce_suggested(key: str, val: Any) -> Any: """ try: return int(float(val)) if key in _SUGGESTION_INT_KEYS else round(float(val), 4) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): return val @@ -276,22 +267,77 @@ def _suggestion_equivalent(suggested: Any, current: Any) -> bool: return False try: return abs(float(suggested) - float(current)) < 1e-6 - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): return str(suggested) == str(current) -# Settings surfaced in the ML Lab side-by-side comparison: key -> (label, unit). -# Order defines display order. Only keys either engine produces are shown. -_ML_COMPARE_SETTINGS: tuple[tuple[str, str, str], ...] = ( - (CONF_OFF_DELAY, "Off Delay", "s"), - (CONF_END_REPEAT_COUNT, "End Repeat Count", "x"), - (CONF_AUTO_LABEL_CONFIDENCE, "Auto-label Confidence", ""), - (CONF_STOP_THRESHOLD_W, "Stop Threshold", "W"), - (CONF_START_THRESHOLD_W, "Start Threshold", "W"), - (CONF_END_ENERGY_THRESHOLD, "End Energy Threshold", "Wh"), - (CONF_MIN_POWER, "Min Power", "W"), - (CONF_MIN_OFF_GAP, "Min Off Gap", "s"), - (CONF_DURATION_TOLERANCE, "Duration Tolerance", ""), -) + +def _visible_suggestions( + store: Any, merged: dict[str, Any], device_type: str +) -> list[tuple[str, dict[str, Any], Any, Any]]: + """``(key, stored item, suggested, current)`` for every suggestion worth showing. + + The ONE filter behind the Settings list, the Overview card, Apply-all and + the setup advisor, which had drifted (audit SUGGEST-11/17): a muted key could + be re-created by the reconcile cascade and applied by Apply-all, and the setup + card counted stale keys nothing would show. Drops keys the engine no longer + produces, muted keys, and values equal to what the key already RUNS with - the + effective default when it is unset, not ``None`` (SUGGEST-10). + """ + raw = store.get_suggestions() or {} + try: + muted = set(store.get_locked_suggestions() or []) + except Exception: # pylint: disable=broad-exception-caught + muted = set() + effective = effective_option_values(merged, device_type) + out: list[tuple[str, dict[str, Any], Any, Any]] = [] + for key in _SUGGESTION_KEYS: + item = raw.get(key) + if key in muted or not isinstance(item, dict) or item.get("value") is None: + continue + suggested = _coerce_suggested(key, item["value"]) + current = merged.get(key) + if current is None: + current = effective.get(key) + if _suggestion_equivalent(suggested, current): + continue + out.append((key, item, suggested, current)) + return out + + +def _threshold_pair_message(start: float, stop: float) -> str: + """The error a write that would invert the threshold pair is refused with (item 515).""" + return ( + f"Stop Threshold ({stop:g} W) must be below Start Threshold ({start:g} W). " + "Nothing was saved; change both together." + ) + + +def _without_inverted_threshold_pair( + entry: ConfigEntry, updates: dict[str, Any], source: str +) -> dict[str, Any]: + """An import's settings, less its threshold pair when that would invert this + device's (item 515): the device keeps its own pair, the rest still applies. + + An import is someone else's tuning (an export, a store bundle), so a refused + pair is logged rather than failing the import that carries profiles and cycles. + """ + merged = {**entry.data, **entry.options} + device_type = merged.get(CONF_DEVICE_TYPE, DEFAULT_DEVICE_TYPE) + pair = inverted_threshold_pair(merged, {**merged, **updates}, device_type) + if pair is None: + return updates + kept = { + k: v for k, v in updates.items() + if k not in (CONF_START_THRESHOLD_W, CONF_STOP_THRESHOLD_W) + } + if inverted_threshold_pair(merged, {**merged, **kept}, device_type) is not None: + kept.pop(CONF_MIN_POWER, None) # it sets an unset threshold's default + _LOGGER.warning( + "%s for %s: not applying its thresholds (start %g W, stop %g W): the stop " + "threshold must be below the start threshold; the device keeps its own", + source, entry.title, pair[0], pair[1], + ) + return kept def _downsample(samples: Any, max_points: int = 240) -> list[list[float]]: @@ -347,12 +393,18 @@ async def _recorder_power( start_dt: Any, *, end_dt: Any = None, -) -> list[tuple[float, float]]: + keep_unavailable: bool = False, +) -> list[tuple[float, float | None]]: """Raw (unix_ts, watts) readings for entity_id over a window, via the recorder. ``end_dt`` defaults to now (the live chart overlay's use). Passing it lets a caller read the history in bounded windows instead of one unbounded query - a month of 5-second data is millions of rows in a single recorder-executor job. + + A row that is not a number (``unavailable``/``unknown``) is skipped, unless + ``keep_unavailable`` asks for it as ``(ts, None)``: the cycle-context view + (register item 513) breaks its line there instead of holding the last reading + across an outage. Watts are never None without it. """ try: from homeassistant.components.recorder import ( # pylint: disable=import-outside-toplevel @@ -364,11 +416,11 @@ async def _recorder_power( # tz-aware; use dt_util.now() per the datetime convention window_end = end_dt if end_dt is not None else dt_util.now() - def _query() -> list[tuple[float, float]]: + def _query() -> list[tuple[float, float | None]]: res = history.state_changes_during_period( hass, start_dt, window_end, entity_id, include_start_time_state=True ) - rows: list[tuple[float, float]] = [] + rows: list[tuple[float, float | None]] = [] start_ts = start_dt.timestamp() if hasattr(start_dt, "timestamp") else None for s in res.get(entity_id, []) or []: try: @@ -380,7 +432,15 @@ async def _recorder_power( if start_ts is not None and ts < start_ts: ts = start_ts rows.append((ts, round(float(s.state), 1))) - except (ValueError, TypeError): + except (ValueError, TypeError, OverflowError): + if keep_unavailable: + try: + gap_ts = s.last_changed.timestamp() + except (AttributeError, TypeError, ValueError, OverflowError): + continue + if start_ts is not None and gap_ts < start_ts: + gap_ts = start_ts + rows.append((gap_ts, None)) continue return rows @@ -403,7 +463,7 @@ def _cycle_kwh(c: dict[str, Any]) -> float | None: if wh is not None: try: return round(float(wh) / 1000.0, 4) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): pass return c.get("energy_kwh") @@ -551,7 +611,7 @@ def _json_safe(value: Any) -> Any: try: json.dumps(value) return value - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): return str(value) @@ -637,6 +697,9 @@ _PREF_DATE_FORMATS = ("relative", "absolute") # than coupling the WS handler to the translations/panel/ language file list — # the panel already falls back to system language for any tag it can't load. _PREF_LANG_TAG_RE = re.compile(r"^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$") +# cycle_context_min: the cycle chart's recorder-history length per device, in +# minutes (register item 513). The panel offers exactly these; 0 = off. +_PREF_CYCLE_CONTEXT_MINUTES = (0, 5, 10, 30, 60) # Commands that require 'full' (destructive or full-data export/import). _FULL_COMMANDS = frozenset({ @@ -644,8 +707,10 @@ _FULL_COMMANDS = frozenset({ "trigger_ml_training", # Selective export/import wizard (same full-data reach as the wholesale variants). "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", + # Undo of a replace import: a whole-store replace like the import itself. + "undo_import", + # Reverting on-device models discards learned state -> full access. + "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. @@ -671,6 +736,7 @@ _ADMIN_COMMANDS = frozenset({ "analyze_import", "export_config_selective", "import_config_selective", + "undo_import", "reprocess_history", "clear_debug_data", # Global community-store mutations: these change the ONE integration-wide GitHub @@ -680,6 +746,12 @@ _ADMIN_COMMANDS = frozenset({ "store_disconnect", "store_set_online", "store_set_prefs", + # They publish under that same install-wide account (audit PLATFORM-11): with + # RBAC off, any non-admin could otherwise post under the admin's identity. + "store_upload_cycle", + "store_upload_device", + "store_rate_device", + "store_confirm_device", # Historical power-data import: reads a whole recorder history (or an uploaded # export of one) and writes cycles into the store, so it is admin-only even with # RBAC disabled, exactly like the selective import wizard. @@ -695,13 +767,12 @@ _ADMIN_COMMANDS = frozenset({ # read-only what-if replay (it never persists anything) whose name does not start # with get_, so it is whitelisted here to gate at the 'read' level. _READ_WRITE_COMMANDS = frozenset({ + # Picking the live program stays a read-level exception by design, although + # the pick is stored as the cycle's label at cycle end (audit PLATFORM-11 + # proposed edit; the maintainer kept it open to every user of the device). "set_program", - "run_playground_cycle_detail", - "run_playground_history", - "run_playground_sweep", # Background-task registry: read-level runtime actions (watch progress, fetch # a what-if/maintenance result, or stop a task). None mutate stored data. - "list_tasks", "subscribe_tasks", "cancel_task", "get_task_result", @@ -714,7 +785,6 @@ _READ_WRITE_COMMANDS = frozenset({ "store_list_brands", "store_get_profiles", "store_get_cycles", - "store_get_device_quality", "store_get_device_profiles", "store_get_catalog_entry", # NB: store_refresh_catalog is deliberately NOT here. It drops the install-wide @@ -820,7 +890,12 @@ def _effective_level(hass: HomeAssistant, user: Any, entry_id: str | None) -> st # Background-task commands that may reference a task by id with no device context. -_TASK_COMMANDS = frozenset({"list_tasks", "subscribe_tasks", "cancel_task", "get_task_result"}) +_TASK_COMMANDS = frozenset({"subscribe_tasks", "cancel_task", "get_task_result"}) +# Cancelling a task needs the level its START needed (audit PLATFORM-11): a read +# user could stop an admin's history import or reprocess. Read-level kinds are the +# what-if Playground runs; admin kinds are started by admin-only commands. +_READ_TASK_KINDS = frozenset({"pg_history", "pg_sweep", "pg_detail"}) +_ADMIN_TASK_KINDS = frozenset({"reprocess", "history_import", "history_import_apply"}) def _rbac_ok(hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]) -> bool: @@ -853,10 +928,27 @@ def _rbac_ok(hass: HomeAssistant, connection: websocket_api.ActiveConnection, ms connection.send_error(msg["id"], "forbidden", "Task not found or not authorized") return False else: - # list_tasks / subscribe_tasks with no device context: require a device so + # subscribe_tasks with no device context: require a device so # the result set can be authorized (the panel passes entry_id per device). connection.send_error(msg["id"], "forbidden", "You need to specify a device") return False + if cmd == "cancel_task" and msg.get("task_id"): + from . import task_registry # pylint: disable=import-outside-toplevel + + task = task_registry.get_registry(hass).get(msg["task_id"]) + kind = getattr(task, "kind", None) + if kind in _ADMIN_TASK_KINDS: + connection.send_error(msg["id"], "forbidden", "Administrator access required") + return False + if kind not in _READ_TASK_KINDS: + entry_id = entry_id or getattr(task, "entry_id", None) + if entry_id and _LEVEL_RANK.get( + _effective_level(hass, user, entry_id), 0 + ) < _LEVEL_RANK["edit"]: + connection.send_error( + msg["id"], "forbidden", "You need edit access to this device" + ) + return False if not entry_id: return True # no device context and not admin/open: harmless read-style command if cmd in _READ_WRITE_COMMANDS: @@ -894,7 +986,7 @@ def _sanitize_panel(p: dict[str, Any], current: dict[str, Any]) -> dict[str, Any if "poll_interval_s" in p: try: out["poll_interval_s"] = max(2, min(60, int(p["poll_interval_s"]))) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): pass if p.get("default_tab") in _PANEL_TABS: out["default_tab"] = p["default_tab"] @@ -922,7 +1014,6 @@ def _sanitize_rbac(r: dict[str, Any]) -> dict[str, Any]: # ─── Registration ───────────────────────────────────────────────────────────── -@callback # ─── Community store (online features) ───────────────────────────────────────── def _store_ctx(hass: HomeAssistant, entry_id: str) -> tuple[Any, dict[str, Any]] | None: @@ -1011,20 +1102,6 @@ async def ws_store_list_brands(hass, connection, msg): _send_result(connection, msg["id"], "store_list_brands", {"items": items}) -@websocket_api.websocket_command({ - vol.Required("type"): "ha_washdata/store_get_device_quality", vol.Required("entry_id"): str, - vol.Required("device_id"): str, -}) -@websocket_api.async_response -async def ws_store_get_device_quality(hass, connection, msg): - ctx = _store_ctx(hass, msg["entry_id"]) - if ctx is None: - _send_result(connection, msg["id"], "store_get_device_quality", {"disabled": True}) - return - manager, _ = ctx - _send_result(connection, msg["id"], "store_get_device_quality", await manager.store_bridge.get_device_quality(msg["device_id"])) - - @websocket_api.websocket_command({ vol.Required("type"): "ha_washdata/store_get_catalog_entry", vol.Required("entry_id"): str, vol.Required("brand"): str, vol.Required("model"): str, vol.Required("appliance_type"): str, @@ -1151,7 +1228,9 @@ async def ws_store_get_profiles(hass, connection, msg): return manager, _ = ctx items = await manager.store_bridge.get_profiles(msg["device_id"]) - _send_result(connection, msg["id"], "store_get_profiles", {"items": items}) + # None = the store could not be reached, not "no shared programs" (audit STORE-09). + _send_result(connection, msg["id"], "store_get_profiles", + {"items": items} if items is not None else {"items": [], "error": "store_unreachable"}) @websocket_api.websocket_command({ @@ -1166,7 +1245,8 @@ async def ws_store_get_cycles(hass, connection, msg): return manager, _ = ctx items = await manager.store_bridge.get_cycles(msg["profile_id"]) - _send_result(connection, msg["id"], "store_get_cycles", {"items": items}) + _send_result(connection, msg["id"], "store_get_cycles", + {"items": items} if items is not None else {"items": [], "error": "store_unreachable"}) @websocket_api.websocket_command({ @@ -1181,9 +1261,12 @@ async def ws_store_import_cycle(hass, connection, msg): _send_result(connection, msg["id"], "store_import_cycle", {"disabled": True}) return manager, _ = ctx - res = await manager.store_bridge.import_cycle( - msg["cycle_id"], msg.get("target_profile"), msg.get("new_profile_name") - ) + # Under the write lock imports, reprocess and merges hold (audit STORE-11): a + # replace-mode selective import resets reference_cycles mid-download otherwise. + async with _entry_write_lock(hass, msg["entry_id"]): + res = await manager.store_bridge.import_cycle( + msg["cycle_id"], msg.get("target_profile"), msg.get("new_profile_name") + ) manager.notify_update() _send_result(connection, msg["id"], "store_import_cycle", res) @@ -1227,7 +1310,7 @@ async def ws_store_upload_device(hass, connection, msg): ``include_settings`` bundles the allow-listed recognition/matching settings.""" from .const import ( CONF_STORE_BRAND, CONF_STORE_MODEL, CONF_DEVICE_TYPE, DEFAULT_DEVICE_TYPE, - SHAREABLE_SETTING_KEYS, + sanitize_shared_settings, ) ctx = _store_ctx(hass, msg["entry_id"]) if ctx is None: @@ -1243,7 +1326,7 @@ async def ws_store_upload_device(hass, connection, msg): settings = None if msg.get("include_settings"): # Only the allow-listed numeric thresholds; the WS layer owns entry.options. - settings = {k: opts[k] for k in SHAREABLE_SETTING_KEYS if k in opts} + settings = sanitize_shared_settings(dict(opts)) res = await manager.store_bridge.share_device( brand, model, appliance, msg["items"], include_phases=msg.get("include_phases"), settings=settings, @@ -1260,40 +1343,87 @@ async def ws_store_download_device(hass, connection, msg): """Adopt a whole-device bundle into this device's reference cycles (merge/upsert). When ``include_settings`` is set, also apply the bundle's allow-listed recognition/matching settings onto this device's options (overwrites live tuning).""" - from .const import CONF_DEVICE_TYPE, DEFAULT_DEVICE_TYPE, SHAREABLE_SETTING_KEYS + from .const import CONF_DEVICE_TYPE, DEFAULT_DEVICE_TYPE ctx = _store_ctx(hass, msg["entry_id"]) if ctx is None: _send_result(connection, msg["id"], "store_download_device", {"disabled": True}) return manager, opts = ctx device_type = opts.get(CONF_DEVICE_TYPE, manager.config_entry.data.get(CONF_DEVICE_TYPE, DEFAULT_DEVICE_TYPE)) - res = await manager.store_bridge.download_device(msg["device_id"], device_type) + # A registry task under the write lock (audit STORE-10/11): a bundle import ran + # 7-61 s inside this request, unlocked against reprocess and imports. + reg = task_registry.get_registry(hass) + task = reg.create( + msg["entry_id"], "store_download", "Downloading from the store", + label_key="task.store_download.running", + ) + raw = hass.async_create_task(_store_download_task( + hass, task, msg["entry_id"], manager, msg["device_id"], device_type, + bool(msg.get("include_settings")), + )) + if raw is not None: + reg.link_asyncio_task(task.id, raw) + _send_result(connection, msg["id"], "store_download_device", {"task_id": task.id}) + + +async def _store_download_task( + hass: HomeAssistant, task: Any, entry_id: str, manager: Any, device_id: str, + device_type: str, include_settings: bool, +) -> None: + reg = task_registry.get_registry(hass) + try: + async with _entry_write_lock(hass, entry_id): + if _get_manager(hass, entry_id) is not manager: + reg.finish(task, state=task_registry.STATE_ERROR, error="device reloaded") + return + res = await manager.store_bridge.download_device( + device_id, device_type, + progress=lambda done, total: reg.update(task, done=done, total=total), + should_cancel=lambda: task.cancel_requested, + ) + res = {**res, "settings_applied": await _apply_store_settings( + hass, entry_id, res, include_settings, + )} + manager.notify_update() + reg.finish( + task, + state=task_registry.STATE_CANCELLED if res.get("cancelled") + else task_registry.STATE_DONE, + result=res, + ) + except Exception as exc: # pylint: disable=broad-exception-caught + reg.finish(task, state=task_registry.STATE_ERROR, error=str(exc)) + + +async def _apply_store_settings( + hass: HomeAssistant, entry_id: str, res: dict[str, Any], include_settings: bool, +) -> int: + """Write a bundle's opted-in settings; returns how many were applied.""" + from .const import sanitize_shared_settings # noqa: PLC0415 + settings_applied = 0 - if msg.get("include_settings"): + if include_settings: bundle_settings = res.get("settings") if isinstance(res.get("settings"), dict) else {} # Accept only allow-listed, numeric (non-bool) values - matching what the # upload side ever writes - so a malformed/hostile bundle can't inject a # string/list/bool into this device's live options. - filtered = { - k: v for k, v in bundle_settings.items() - if k in SHAREABLE_SETTING_KEYS - and isinstance(v, (int, float)) and not isinstance(v, bool) - } - entry = _get_entry(hass, msg["entry_id"]) + filtered = sanitize_shared_settings(bundle_settings) + entry = _get_entry(hass, entry_id) if filtered and entry is not None: # Same critical section as ws_set_options: the changelog snapshot and # the options write have to be atomic per entry, or a concurrent # writer records the same "old" value and one of the two updates is # silently lost (#442 follow-up). - async with _entry_options_lock(hass, msg["entry_id"]): + async with _entry_options_lock(hass, entry_id): + filtered = _without_inverted_threshold_pair( + entry, filtered, "store_download" + ) # item 515 await _record_option_changes(hass, entry, filtered, "store_download") hass.config_entries.async_update_entry( entry, options={**entry.options, **filtered} ) settings_applied = len(filtered) - res = {**res, "settings_applied": settings_applied} - manager.notify_update() - _send_result(connection, msg["id"], "store_download_device", res) + return settings_applied @websocket_api.websocket_command({ @@ -1339,6 +1469,7 @@ def async_register_commands(hass: HomeAssistant) -> None: # Maintenance log (Group E) ws_get_maintenance_log, ws_add_maintenance_event, ws_delete_maintenance_event, ws_set_lifetime_cycle_count, + ws_add_maintenance_task, ws_update_maintenance_task, ws_delete_maintenance_task, # Cycles ws_label_cycle, ws_delete_cycle, ws_auto_label_cycles, # Phase catalog @@ -1354,6 +1485,8 @@ def async_register_commands(hass: HomeAssistant) -> None: # Selective export/import wizard (inventory + analyze + selective export/import) ws_get_export_inventory, ws_analyze_import, ws_export_config_selective, ws_import_config_selective, + # "Undo last import" (register item 195) + ws_undo_import, # Shared constants ws_get_constants, # Suggestions @@ -1361,6 +1494,8 @@ def async_register_commands(hass: HomeAssistant) -> None: ws_set_suggestion_lock, # Cycle curve / interactive editing ws_get_cycle_power_data, ws_trim_cycle, ws_analyze_split, ws_apply_split, ws_apply_merge, + # Recorder history around a stored cycle, display only (item 513) + ws_get_cycle_context, # Profile envelope / member cycles ws_get_profile_envelope, ws_get_profile_cycles, # Panel config + RBAC @@ -1377,20 +1512,14 @@ def async_register_commands(hass: HomeAssistant) -> None: ws_get_ml_comparison, # ML Lab review write-back (Stage 4b) ws_set_ml_review, - # On-device ML training (status + manual trigger + matcher-tuning revert + models revert) - ws_get_ml_training_status, ws_trigger_ml_training, ws_revert_matching_config, - ws_revert_ml_models, + # On-device ML training (status + manual trigger + models revert) + ws_get_ml_training_status, ws_trigger_ml_training, ws_revert_ml_models, # Cycle controls (pause / resume / force-stop) ws_pause_cycle, ws_resume_cycle, ws_terminate_cycle, - # Playground (F3): DTW visualizer - ws_get_dtw_debug, # Playground settings control panel: live values + named presets ws_get_playground_settings, ws_save_playground_preset, ws_delete_playground_preset, - # Playground redesign: faithful single-cycle sim + history table + sweep - ws_run_playground_cycle_detail, ws_run_playground_history, - ws_run_playground_sweep, # Background-task registry (progress / cancel / reconnect-safe results) - ws_list_tasks, ws_subscribe_tasks, ws_cancel_task, ws_get_task_result, + ws_subscribe_tasks, ws_cancel_task, ws_get_task_result, # Playground batch/sweep as detached registry-tracked tasks ws_start_playground_history, ws_start_playground_sweep, ws_start_playground_cycle_detail, @@ -1403,7 +1532,7 @@ def async_register_commands(hass: HomeAssistant) -> None: # Local reference cycles eligible to share (share-device tree source) ws_get_shareable_cycles, # Community catalog: brand list, device quality, confirm/rate, global online toggle - ws_store_list_brands, ws_store_get_device_quality, ws_store_get_device_profiles, + ws_store_list_brands, ws_store_get_device_profiles, ws_store_confirm_device, ws_store_rate_device, ws_store_set_online, ws_store_set_prefs, # Catalog identity badges (point reads) + manual cache refresh @@ -1450,6 +1579,7 @@ def ws_get_devices( "current_program": None, "time_remaining_s": None, "total_duration_s": None, + "expected_duration_s": None, "current_power_w": None, "cycle_progress_pct": None, "suggestions_count": 0, @@ -1467,6 +1597,8 @@ def ws_get_devices( # 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, + # Top two of an undecided live match (MATCH-DECIDE-15): display only. + "match_uncertainty": None, # Merged data+options, matching ``ws_get_options`` and the # options-first resolution in WashDataManager (#450). An entry added # after its last schema migration carries the structural keys in @@ -1485,8 +1617,11 @@ def ws_get_devices( try: detector = getattr(manager, "detector", None) if detector is not None: - info["detector_state"] = detector.state - info["sub_state"] = detector.sub_state + # What the entities show (item 501): a hidden standby re-probe + # stays "off" here too instead of flickering the Status card. + info["detector_state"] = getattr(detector, "exposed_state", detector.state) + # Matching it (#452: "Stalled" while a stalled cycle shows paused). + info["sub_state"] = getattr(detector, "exposed_sub_state", detector.sub_state) program: str | None = getattr(manager, "_current_program", None) if program in (None, "off", "unknown", "detecting...", "restored..."): @@ -1499,6 +1634,12 @@ def ws_get_devices( info["time_remaining_s"] = getattr(manager, "_time_remaining", None) info["total_duration_s"] = getattr(manager, "_total_duration", None) + # The matched program's expected length: the span the phase sensor + # maps progress onto, so the Status timeline names the same phase + # (audit PROGRESS-10). + info["expected_duration_s"] = getattr( + manager, "_matched_profile_duration", 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 @@ -1516,6 +1657,12 @@ def ws_get_devices( # the panel's tooltip says. info["envelope_position"] = getattr(manager, "envelope_position", None) + # "Uncertain: X or Y, ~N% sure" on the Status card while the live + # match is undecided (MATCH-DECIDE-15). The sensor state keeps its + # raw `detecting...` value, which automations compare against. + _unc = getattr(manager, "match_uncertainty", None) + info["match_uncertainty"] = _unc if isinstance(_unc, dict) else None + store = getattr(manager, "profile_store", None) if store is not None: # Assigned per entry and OUTSIDE both probes. It used to be @@ -1526,23 +1673,12 @@ def ws_get_devices( # entry, raised NameError into a debug-level log. merged = {**entry.data, **entry.options} try: - # Same filters as ws_get_suggestions (muted keys and - # no-op values dropped) so the device-pill badge can - # never disagree with the Settings tab banner. - raw = store.get_suggestions() or {} - try: - muted = set(store.get_locked_suggestions() or []) - except Exception: # pylint: disable=broad-exception-caught - muted = set() + # The same filter as the Settings list, so the Overview + # card can never disagree with the Settings tab banner. keys = [ - k for k in _SUGGESTION_KEYS - if k not in muted - and isinstance(raw.get(k), dict) - and raw[k].get("value") is not None - # Coerce first, exactly like ws_get_suggestions, so the pill - # and the Settings list agree on int-key rounding. - and not _suggestion_equivalent( - _coerce_suggested(k, raw[k]["value"]), merged.get(k) + k for k, _i, _s, _c in _visible_suggestions( + store, merged, + getattr(manager, "device_type", None) or DEFAULT_DEVICE_TYPE, ) ] info["suggestion_keys"] = keys @@ -1611,6 +1747,7 @@ def ws_get_devices( vol.Required("entry_id"): str, vol.Optional("limit", default=50): vol.All(int, vol.Range(min=1, max=200)), vol.Optional("offset", default=0): vol.All(int, vol.Range(min=0)), + vol.Optional("imported_offset"): vol.All(int, vol.Range(min=0)), } ) @callback @@ -1621,13 +1758,22 @@ def ws_get_device_cycles( ) -> None: """Return a page of recent cycles for a device, stripping large binary fields. - Cycles are returned most-recent-first and sliced ``[offset : offset+limit]`` - so the panel can page. ``total`` is the device's full cycle count and - ``has_more`` is True when cycles remain beyond the returned window. + Real cycles are returned most-recent-first and sliced ``[offset : offset+limit]`` + so the panel can page. ``total`` is the device's real cycle count and + ``has_more`` is True when real cycles remain beyond the returned window. + + Imported cycles (``reference_cycles`` + ``backfill_cycles``) page on their own + cursor, ``imported_offset``, over one newest-start-first list of both, with the + same ``limit``; ``imported_total`` / ``imported_has_more`` describe it. They keep + their own cursor because they stay out of ``total`` (they never enter usage + stats), so folding them into ``offset`` would change what ``total`` counts. + A client that omits ``imported_offset`` gets the pre-paging behaviour: every + imported cycle on ``offset == 0`` and none after (register item 129a). """ entry_id: str = msg["entry_id"] limit: int = msg.get("limit", 50) offset: int = msg.get("offset", 0) + imported_offset: int | None = msg.get("imported_offset") manager = _get_manager(hass, entry_id) if manager is None: @@ -1638,6 +1784,8 @@ def ws_get_device_cycles( reference_cycles: list[dict[str, Any]] = [] backfill_cycles: list[dict[str, Any]] = [] total = 0 + imported_total = 0 + imported_end = 0 try: store = getattr(manager, "profile_store", None) if store is not None: @@ -1650,22 +1798,27 @@ def ws_get_device_cycles( window = ordered[offset:offset + limit] for c in window: cycles.append(_strip_cycle(c)) - # Imported store recordings and cycles recovered from raw history are - # bounded sets kept out of the paginated `cycles`/`total` (they never enter - # usage stats). Return them once, on the first page, tagged so the panel can - # badge them and route edits/deletes correctly. They travel in separate - # arrays because they are separate categories: a curated community template - # and an auto-detected segment from the user's own past are not the same - # claim about a cycle. - if offset == 0: - for c in reversed(store.get_reference_cycles()): - ref = _strip_cycle(c) - ref.update(_cycle_capabilities(c, "reference")) - reference_cycles.append(ref) - for c in reversed(store.get_backfill_cycles()): - item = _strip_cycle(c) - item.update(_cycle_capabilities(c, "backfill")) - backfill_cycles.append(item) + # Imported store recordings and cycles recovered from raw history are kept + # out of `cycles`/`total` (they never enter usage stats), tagged so the + # panel can badge them and route edits/deletes correctly. They travel in + # separate arrays because they are separate categories: a curated community + # template and an auto-detected segment from the user's own past are not + # the same claim about a cycle. + imported = _imported_cycles_newest_first(store) + imported_total = len(imported) + if imported_offset is not None: + imported_window = imported[imported_offset:imported_offset + limit] + imported_end = imported_offset + len(imported_window) + elif offset == 0: + imported_window = imported + imported_end = imported_total + else: + imported_window = [] + imported_end = imported_total + for c, origin in imported_window: + item = _strip_cycle(c) + item.update(_cycle_capabilities(c, origin)) + (reference_cycles if origin == "reference" else backfill_cycles).append(item) except Exception as exc: # pylint: disable=broad-exception-caught _LOGGER.debug("Error fetching cycles for entry %s: %s", entry_id, exc) @@ -1677,10 +1830,47 @@ def ws_get_device_cycles( "backfill_cycles": backfill_cycles, "total": total, "has_more": has_more, + "imported_total": imported_total, + "imported_has_more": imported_end < imported_total, }, ) +def _imported_cycles_newest_first(store: Any) -> list[tuple[dict[str, Any], str]]: + """Reference + backfill cycles as ``(cycle, origin)``, newest start first. + + One ordering for both lists so a page holds the most recent imports whatever their + category. Storage order (reversed, newest-added first) breaks ties and places + cycles whose ``start_time`` does not parse last, so the order is total and stable + across requests, which paging needs. + """ + rows: list[tuple[dict[str, Any], str]] = [] + for origin, getter in ( + ("reference", "get_reference_cycles"), + ("backfill", "get_backfill_cycles"), + ): + seq = getattr(store, getter)() + if not isinstance(seq, list): + continue + rows.extend((c, origin) for c in reversed(seq) if isinstance(c, dict)) + + def _start_ts(row: tuple[dict[str, Any], str]) -> float: + value = row[0].get("start_time") + try: + if isinstance(value, (int, float)) and not isinstance(value, bool): + return float(value) # legacy numeric unix timestamp + parsed = dt_util.parse_datetime(str(value or "")) + # Aware stamps only: a naive one would compare in local time. + if parsed is not None and parsed.tzinfo is not None: + return parsed.timestamp() + except (TypeError, ValueError, OverflowError, OSError): + pass + return float("-inf") + + # sorted() is stable, so equal starts keep the newest-added-first order above. + return sorted(rows, key=_start_ts, reverse=True) + + # ─── Settings ───────────────────────────────────────────────────────────────── @@ -1802,6 +1992,20 @@ async def ws_set_options( # from entry.options and overridden by the submission. new_options = {**entry.options, **msg["options"]} + # A numeric setting must be a finite number (audit PLATFORM-13): stored, + # "abc" raised in the manager's constructor and the entry never set up + # again. A bad value drops the key so its default applies - the contract + # the per-key blocks below already follow; None / "" reach their reset path. + _numeric = numeric_option_keys() + for _key, _val in msg["options"].items(): + if _key not in _numeric or _val is None or _val == "": + continue + _num = coerce_numeric_option(_val, _numeric[_key]) + if _num is None: + new_options.pop(_key, None) + else: + new_options[_key] = _num + # Capture the submitted display name for the entry title before it is # stripped out of options below. submitted_name = new_options.get(CONF_NAME) @@ -1927,6 +2131,22 @@ async def ws_set_options( for key in _OPTIONS_IDENTITY_KEYS: new_options.pop(key, None) + # Register item 515: a save must not leave the stop threshold at or above + # the start threshold. The panel's own conflict check holds such an edit + # back, but the Playground publish/sweep, the per-setting Revert and any + # other client wrote it straight through. An entry already running an + # inverted pair keeps saving every other setting (see the helper). + _inverted = inverted_threshold_pair( + {**entry.data, **entry.options}, + {**entry.data, **new_options}, + effective_device_type, + ) + if _inverted is not None: + connection.send_error( + msg["id"], "invalid_threshold_pair", _threshold_pair_message(*_inverted) + ) + return + update_kwargs: dict[str, Any] = {"options": new_options} if isinstance(submitted_name, str) and submitted_name.strip(): update_kwargs["title"] = submitted_name.strip() @@ -2042,18 +2262,35 @@ async def ws_get_setup_status( # Gather store data (executor-safe reads) store = manager.profile_store profile_names = list(store._data.get("profiles", {}).keys()) - past_cycles = store._data.get("past_cycles", []) - ref_names: set[str] = set() - for rc in store._data.get("reference_cycles", []): - if rc.get("profile_name"): - ref_names.add(rc["profile_name"]) + # The evidence view, split back into its three lists: every advisor check asks + # "can this profile be matched", which is what has_real_profiles answers with the + # same view, so the card's phase0 coincides with the manager skipping matching. + # Backfilled history is this machine's own (item 129d); before, only past_cycles + # were read and an import-only device was told to start recording. + evidence = {id(c) for c in store.iter_evidence_cycles()} + past_cycles = [c for c in store.get_past_cycles() if id(c) in evidence] + backfill_cycles = [c for c in store.get_backfill_cycles() if id(c) in evidence] + known_profiles = set(profile_names) + ref_names: set[str] = { + rc["profile_name"] + for rc in store.get_reference_cycles() + if id(rc) in evidence and rc.get("profile_name") in known_profiles + } - coverage_gap = await hass.async_add_executor_job(store.suggest_coverage_gaps) + # Read from a loop-side snapshot: the executor must not iterate live store + # dicts (audit PERF-10). + coverage_gap = await hass.async_add_executor_job( + _store_read_snapshot(store).suggest_coverage_gaps + ) # suggestions: read from the store (cheap dict lookup; heavy computation happens # in the SuggestionEngine background task, not here). - suggestions = list((store.get_suggestions() or {}).values()) - pg_data = store._data.get("profile_groups", {}) - pending_groups = (pg_data.get("suggestions") or []) if isinstance(pg_data, dict) else [] + _entry = _get_entry(hass, msg["entry_id"]) + _merged = {**_entry.data, **_entry.options} if _entry is not None else {} + suggestions = [ + item for _k, item, _s, _c in _visible_suggestions( + store, _merged, getattr(manager, "device_type", None) or DEFAULT_DEVICE_TYPE + ) + ] device_type = manager.device_type @@ -2064,9 +2301,9 @@ async def ws_get_setup_status( ref_profile_names=ref_names, coverage_gap=coverage_gap, suggestions=suggestions, - profile_groups=pending_groups, skipped_steps=skipped_steps, now=dt_util.now(), + backfill_cycles=backfill_cycles, ) _send_result(connection, msg["id"], "get_setup_status", { @@ -2083,6 +2320,35 @@ async def ws_get_setup_status( }) +def _store_read_snapshot(store: Any) -> Any: + """A copy of ``store`` an executor job can read while the loop keeps writing. + + Loop-only. Shallow-copies the store object and gives it a ``_data`` whose + containers are copies two levels deep: every top-level list and dict, and every + dict inside them (each cycle, profile, envelope). The executor can then iterate + what it reads while the loop appends a cycle, adds a profile or sets a key on a + cycle (audit PERF-10, same class as register items 82 and 264). Large values + (power traces, envelope arrays) are shared, not copied: the loop replaces them + rather than mutating them in place. Cache attributes the store's methods set on + ``self`` land on the copy and are dropped with it. Anything without a real + ``_data`` dict (a test double) is returned as is. + """ + data = getattr(store, "_data", None) + if not isinstance(data, dict): + return store + + def _copy(value: Any) -> Any: + if isinstance(value, list): + return [dict(v) if isinstance(v, dict) else v for v in value] + if isinstance(value, dict): + return {k: (dict(v) if isinstance(v, dict) else v) for k, v in value.items()} + return value + + snapshot = copy.copy(store) + snapshot._data = {key: _copy(value) for key, value in data.items()} # pylint: disable=protected-access + return snapshot + + # ─── Profiles ───────────────────────────────────────────────────────────────── @websocket_api.websocket_command( @@ -2101,34 +2367,50 @@ async def ws_get_profiles( _err_not_found(connection, msg["id"], entry_id) return - def _compute_stats() -> dict[str, Any]: - profiles: list[dict[str, Any]] = [] - try: - profiles = manager.profile_store.list_profiles() - except Exception as exc: # pylint: disable=broad-exception-caught - _LOGGER.debug("Error listing profiles for %s: %s", entry_id, exc) + # Everything below reads the store from an executor thread while the loop keeps + # appending cycles and rebuilding envelopes, and each block swallows its own + # error - so "dictionary changed size during iteration" used to show up as a + # silently empty health/trends/advisories panel (audit PERF-10). The executor + # reads a snapshot taken here, on the loop; list_profiles is cached and stays + # on the loop. + profiles: list[dict[str, Any]] = [] + try: + profiles = manager.profile_store.list_profiles() + # #158: the panel shows a computed duration read-only (rows are copies). + learned = manager.profile_store.learned_duration_profiles() + for row in profiles: + row["duration_learned"] = row.get("name") in learned + except Exception as exc: # pylint: disable=broad-exception-caught + _LOGGER.debug("Error listing profiles for %s: %s", entry_id, exc) + store = _store_read_snapshot(manager.profile_store) + def _compute_stats() -> dict[str, Any]: health: dict[str, dict] = {} try: - health = manager.profile_store.compute_profile_health() + health = store.compute_profile_health() except Exception: # pylint: disable=broad-exception-caught pass trends: dict[str, dict] = {} try: - trends = manager.profile_store.compute_profile_trends() + trends = store.compute_profile_trends() except Exception: # pylint: disable=broad-exception-caught pass + # Coverage gaps: unlabelled recent cycles that look like one program the user + # has not created. Measured by devtools/advisory_usefulness_eval.py: 92-95% + # of its clusters are the missing program, and it stays silent on intact + # stores, so the Profiles tab shows them (audit UI-13, register item 432). coverage_gaps: dict[str, Any] = {} try: - coverage_gaps = manager.profile_store.suggest_coverage_gaps() + coverage_gaps = store.suggest_coverage_gaps() except Exception: # pylint: disable=broad-exception-caught pass advisories: list[dict] = [] try: - advisories = manager.profile_store.compute_profile_advisories() + # Reuse the health/trends computed above instead of recomputing both. + advisories = store.compute_profile_advisories(health, trends) except Exception: # pylint: disable=broad-exception-caught pass @@ -2146,12 +2428,21 @@ async def ws_get_profiles( _name = _profile.get("name") if isinstance(_profile, dict) else None if not _name: continue - _sig = manager.profile_store.compute_profile_terminal_signature(_name) + _sig = store.compute_profile_terminal_signature(_name) if _sig is not None: terminal[_name] = _sig except Exception: # pylint: disable=broad-exception-caught terminal = {} + # How often the matcher labelled one of this appliance's own cycles with + # each program (STORE-21): the panel shows it on imported program cards so + # an import that never fits can be pruned. Local only, no store writes. + matcher_counts: dict[str, int] = {} + try: + matcher_counts = store.matcher_label_counts() + except Exception: # pylint: disable=broad-exception-caught + matcher_counts = {} + return { "profiles": profiles, "profile_health": health, @@ -2159,6 +2450,7 @@ async def ws_get_profiles( "coverage_gaps": coverage_gaps, "profile_advisories": advisories, "profile_terminal": terminal, + "profile_matcher_counts": matcher_counts, } stats = await hass.async_add_executor_job(_compute_stats) @@ -2294,8 +2586,8 @@ async def ws_get_profile_groups( connection: websocket_api.ActiveConnection, msg: dict[str, Any], ) -> None: - """Return profile groups with member lists, cohesion, and near-duplicate - suggestions the user can act on.""" + """Return profile groups with member lists and cohesion. (Near-duplicate group + suggestions were deleted in 0.5.8, register item 432.)""" from .const import GROUP_MIN_COHESION # pylint: disable=import-outside-toplevel entry_id: str = msg["entry_id"] manager = _get_manager(hass, entry_id) @@ -2316,11 +2608,11 @@ async def ws_get_profile_groups( "cohesion": round(coh, 3), "cohesive": coh >= GROUP_MIN_COHESION, # False => not aggregated by matcher; UI warns }) - suggestions = await hass.async_add_executor_job(store.suggest_profile_groups) + # Group suggestions were computed on every Profiles visit and rendered nowhere + # (audit UI-13), so they are no longer sent. _send_result(connection, msg["id"], "get_profile_groups", { "groups": groups, "min_cohesion": GROUP_MIN_COHESION, - "suggestions": suggestions, }) @@ -2559,15 +2851,21 @@ async def ws_get_maintenance_log( if manager is None: _err_not_found(connection, msg["id"], entry_id) return - reminders = dict(DEFAULT_MAINTENANCE_REMINDER_CYCLES) - cfg = manager.config_entry.options.get(CONF_MAINTENANCE_REMINDER_CYCLES) - if isinstance(cfg, dict): - reminders.update(cfg) + device_type = manager.device_type + # The reminders that actually apply (#461): the device type's preset until the + # config is saved, the saved config after. Until #461 this merged the washer + # defaults over the saved config, so a partial config showed rows the manager + # never evaluated. + reminders = effective_reminders( + device_type, manager.config_entry.options.get(CONF_MAINTENANCE_REMINDER_CYCLES) + ) store = manager.profile_store + status = store.get_maintenance_status(reminders) _send_result(connection, msg["id"], "get_maintenance_log", { "log": store.get_maintenance_log(), - "due": manager.maintenance_due, - "event_types": list(MAINTENANCE_EVENT_TYPES), + "due": [row["id"] for row in status if row["due"]], + # The built-in types this device's editor offers (device-type aware). + "event_types": editor_types(device_type, reminders), "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 @@ -2575,7 +2873,16 @@ async def ws_get_maintenance_log( "cycles_since": { evt: store.cycles_since_maintenance(evt) for evt in MAINTENANCE_EVENT_TYPES }, + "custom_tasks": store.get_maintenance_tasks(), + # Every active reminder (built-in and custom) with its progress. + "status": status, "lifetime_cycle_count": manager.lifetime_cycle_count, + "limits": { + "tasks_max": MAINTENANCE_CUSTOM_TASK_MAX, + "name_max": MAINTENANCE_TASK_NAME_MAX, + "cycles_max": MAINTENANCE_INTERVAL_CYCLES_MAX, + "days_max": MAINTENANCE_INTERVAL_DAYS_MAX, + }, }) @@ -2744,6 +3051,121 @@ async def ws_delete_maintenance_event( connection.send_error(msg["id"], "unknown_error", str(exc)) +# Custom maintenance tasks (#461). Edit level, like the maintenance log: a task is +# a reminder the user defines, never a change to detection or stored cycles. +# Intervals are validated by the store (whole numbers, bounded) so the WS layer, +# the panel and any future service share one rule; a refusal is invalid_format. +_TASK_INTERVAL = vol.Any(None, int, float, str) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "ha_washdata/add_maintenance_task", + vol.Required("entry_id"): str, + vol.Required("name"): str, + vol.Optional("cycles"): _TASK_INTERVAL, + vol.Optional("days"): _TASK_INTERVAL, + } +) +@websocket_api.async_response +async def ws_add_maintenance_task( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Create a custom maintenance task (free-text name, cycles and/or days).""" + 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: + task = await manager.profile_store.async_add_maintenance_task( + msg["name"], msg.get("cycles"), msg.get("days") + ) + manager.notify_update() + _send_result(connection, msg["id"], "add_maintenance_task", {"success": True, "task": task}) + except ValueError as exc: + connection.send_error(msg["id"], "invalid_format", str(exc)) + 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/update_maintenance_task", + vol.Required("entry_id"): str, + vol.Required("task_id"): str, + vol.Optional("name"): str, + vol.Optional("cycles"): _TASK_INTERVAL, + vol.Optional("days"): _TASK_INTERVAL, + } +) +@websocket_api.async_response +async def ws_update_maintenance_task( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Rename a custom maintenance task or change its intervals. + + An omitted field is left as it is; an explicit ``null`` interval switches + that axis off, the same as 0. + """ + 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 + def _interval(key: str) -> Any: + # The store reads None as "leave it"; on the wire null means "off". + if key not in msg: + return None + return 0 if msg[key] is None else msg[key] + + try: + task = await manager.profile_store.async_update_maintenance_task( + msg["task_id"], + name=msg.get("name"), + cycles=_interval("cycles"), + days=_interval("days"), + ) + manager.notify_update() + _send_result(connection, msg["id"], "update_maintenance_task", {"success": True, "task": task}) + except ValueError as exc: + connection.send_error(msg["id"], "invalid_format", str(exc)) + 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/delete_maintenance_task", + vol.Required("entry_id"): str, + vol.Required("task_id"): str, + } +) +@websocket_api.async_response +async def ws_delete_maintenance_task( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Remove a custom maintenance task. Its log entries stay, with their name.""" + 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: + removed = await manager.profile_store.async_delete_maintenance_task(msg["task_id"]) + if removed: + manager.notify_update() + _send_result(connection, msg["id"], "delete_maintenance_task", {"success": removed}) + except Exception as exc: # pylint: disable=broad-exception-caught + connection.send_error(msg["id"], "unknown_error", str(exc)) + + # ─── Cycles ─────────────────────────────────────────────────────────────────── @websocket_api.websocket_command( @@ -2828,12 +3250,39 @@ async def ws_delete_cycle( connection.send_error(msg["id"], "unknown_error", str(exc)) +# The panel's bulk auto-label threshold range (the modal input's min/max). +AUTO_LABEL_MIN_THRESHOLD = 0.5 +AUTO_LABEL_MAX_THRESHOLD = 0.95 + + +def configured_auto_label_threshold(entry: ConfigEntry | None) -> float: + """The device's Auto-Label Confidence, clamped to the bulk pass's range. + + The bulk pass and the cycle-end label both put a guess on a cycle with no + confirmation, so they share the user's one bar for that. Never raises. + """ + from .const import ( # pylint: disable=import-outside-toplevel + CONF_AUTO_LABEL_CONFIDENCE, + DEFAULT_AUTO_LABEL_CONFIDENCE, + ) + + merged = {**(entry.data if entry else {}), **(entry.options if entry else {})} + try: + value = float(merged.get(CONF_AUTO_LABEL_CONFIDENCE, DEFAULT_AUTO_LABEL_CONFIDENCE)) + except (TypeError, ValueError, OverflowError): + value = DEFAULT_AUTO_LABEL_CONFIDENCE + if not math.isfinite(value): + value = DEFAULT_AUTO_LABEL_CONFIDENCE + return min(max(value, AUTO_LABEL_MIN_THRESHOLD), AUTO_LABEL_MAX_THRESHOLD) + + @websocket_api.websocket_command( { vol.Required("type"): "ha_washdata/auto_label_cycles", vol.Required("entry_id"): str, - vol.Optional("confidence_threshold", default=0.75): vol.All( - vol.Coerce(float), vol.Range(min=0.5, max=0.95) + vol.Optional("confidence_threshold"): vol.All( + vol.Coerce(float), + vol.Range(min=AUTO_LABEL_MIN_THRESHOLD, max=AUTO_LABEL_MAX_THRESHOLD), ), } ) @@ -2843,20 +3292,72 @@ async def ws_auto_label_cycles( connection: websocket_api.ActiveConnection, msg: dict[str, Any], ) -> None: - """Auto-label all cycles with matched profiles above the confidence threshold.""" + """Auto-label all cycles with matched profiles above the confidence threshold. + + Without a threshold the device's own Auto-Label Confidence applies (audit + UI-10: the panel used to send a fixed 0.75, below the 0.9 default). + """ 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 - threshold: float = msg.get("confidence_threshold", 0.75) + threshold = msg.get("confidence_threshold") + if threshold is None: + threshold = configured_auto_label_threshold(_get_entry(hass, entry_id)) + task, _raw = start_auto_label_task(hass, entry_id, float(threshold)) + _send_result(connection, msg["id"], "auto_label_cycles", {"task_id": task.id}) + + +def start_auto_label_task( + hass: HomeAssistant, entry_id: str, threshold: float +) -> tuple[Any, asyncio.Task[Any] | None]: + """Start the one auto-label runner (panel and service) as a registry task. + + It re-matches every unlabelled stored cycle - 56 s for 201 cycles - so it runs + detached with progress and cancel, under the per-entry write lock that + reprocess, merge and imports hold (audit PLATFORM-05). Never overwrites (audit + MANAGER-01 / UI-10). + """ + reg = task_registry.get_registry(hass) + task = reg.create( + entry_id, "auto_label", "Auto-labelling cycles", label_key="task.auto_label.running", + ) + raw = hass.async_create_task(_auto_label_task(hass, task, entry_id, threshold)) + if raw is not None: + reg.link_asyncio_task(task.id, raw) + return task, raw + + +async def _auto_label_task( + hass: HomeAssistant, task: Any, entry_id: str, threshold: float +) -> None: + reg = task_registry.get_registry(hass) + manager = _get_manager(hass, entry_id) + if manager is None: + reg.finish(task, state=task_registry.STATE_ERROR, error="device unavailable") + return try: - await manager.profile_store.auto_label_cycles(threshold, overwrite=True) + async with _entry_write_lock(hass, entry_id): + if _get_manager(hass, entry_id) is not manager: + reg.finish(task, state=task_registry.STATE_ERROR, error="device reloaded") + return + stats = await manager.profile_store.auto_label_cycles( + threshold, + overwrite=False, + progress=lambda done, total: reg.update(task, done=done, total=total), + should_cancel=lambda: task.cancel_requested, + ) manager.notify_update() - _send_result(connection, msg["id"], "auto_label_cycles", {"success": True}) + reg.finish( + task, + state=task_registry.STATE_CANCELLED if stats.get("cancelled") + else task_registry.STATE_DONE, + result=stats, + ) except Exception as exc: # pylint: disable=broad-exception-caught - connection.send_error(msg["id"], "unknown_error", str(exc)) + reg.finish(task, state=task_registry.STATE_ERROR, error=str(exc)) # ─── Phase catalog ──────────────────────────────────────────────────────────── @@ -3434,7 +3935,12 @@ async def ws_get_diagnostics( try: stats = await manager.profile_store.get_storage_stats() - _send_result(connection, msg["id"], "get_diagnostics", {"stats": stats}) + # The restore point of the last replace import rides along on the fetch that + # already fills the Export / Import card, so the panel needs no second call. + undo = await manager.profile_store.async_get_pre_import_snapshot() + _send_result( + connection, msg["id"], "get_diagnostics", {"stats": stats, "import_undo": undo} + ) except Exception as exc: # pylint: disable=broad-exception-caught connection.send_error(msg["id"], "unknown_error", str(exc)) @@ -3628,6 +4134,23 @@ async def ws_wipe_history( connection.send_error(msg["id"], "unknown_error", str(exc)) +def _export_json(payload: dict[str, Any]) -> str: + """Serialise an export payload compactly, the way the store is written to disk. + + ``json.dumps(indent=2)`` put every number of every power trace on its own + indented line, and now that no cycle is dropped (item 463) that is one + multi-megabyte WebSocket frame per click (audit PERF-11). Home Assistant's + orjson encoder is the one ``Store`` saves with, so anything persisted + serialises the same way, and ``import_config``'s ``json.loads`` reads it back + unchanged. It also runs as one C call, so the event loop cannot mutate the + shallow-copied store under it mid-encode the way it could between the + pure-Python encoder's steps. + """ + from homeassistant.helpers.json import json_dumps # pylint: disable=import-outside-toplevel + + return json_dumps(payload) + + @websocket_api.websocket_command( {vol.Required("type"): "ha_washdata/export_config", vol.Required("entry_id"): str} ) @@ -3650,15 +4173,53 @@ async def ws_export_config( entry_data=dict(entry.data) if entry else {}, entry_options=dict(entry.options) if entry else {}, ) - # Offload serialization to executor — power traces can be megabytes - json_str = await hass.async_add_executor_job( - lambda: json.dumps(payload, indent=2) - ) + # Offload serialization to executor - power traces can be megabytes + json_str = await hass.async_add_executor_job(_export_json, payload) _send_result(connection, msg["id"], "export_config", {"json_data": json_str}) except Exception as exc: # pylint: disable=broad-exception-caught connection.send_error(msg["id"], "unknown_error", str(exc)) +async def async_apply_imported_entry_options( + hass: HomeAssistant, + entry: ConfigEntry, + config_updates: dict[str, Any], + source: str, +) -> None: + """Apply an import payload's tunables to ``entry.options``, the one safe way. + + Shared by the ``import_config`` WS command and the ``import_config`` service, + which had drifted: the service still rebound the device to the EXPORTER's + power and door sensors (the item-317 "integration silently goes dead" + failure), skipped the options lock and wrote no settings changelog + (audit PLATFORM-02). + + Identity never lands in options and local entity/device bindings are dropped: + an import carries the exporter's ids. ``config_updates["entry_data"]`` is + deliberately NOT written to ``entry.data`` - it is the source device's raw, + un-redacted identity; identity changes go through the reconfigure flow. + The caller holds the per-entry write lock; order is always write -> options. + """ + entry_options_updates = dict(config_updates.get("entry_options", {}) or {}) + for key in _IMPORT_LOCAL_BINDING_KEYS: + entry_options_updates.pop(key, None) + if not entry_options_updates: + return + async with _entry_options_lock(hass, entry.entry_id): + # Read INSIDE the lock: a snapshot taken before the `async with` suspended + # would silently revert a ws_set_options that committed while we waited. + # A persisted null survives options.get(key, DEFAULT) and breaks setup + # (#389), so the same write-boundary strip as ws_set_options applies. + entry_options_updates = _without_inverted_threshold_pair( + entry, entry_options_updates, source + ) + new_options = strip_null_options({**entry.options, **entry_options_updates}) + # ...and a non-numeric numeric setting is dropped, same reason (PLATFORM-13). + new_options, _ = drop_invalid_numeric_options(new_options) + await _record_option_changes(hass, entry, entry_options_updates, source) + hass.config_entries.async_update_entry(entry, options=new_options) + + @websocket_api.websocket_command( { vol.Required("type"): "ha_washdata/import_config", @@ -3685,7 +4246,16 @@ async def ws_import_config( async with _entry_write_lock(hass, entry_id): try: payload = await hass.async_add_executor_job(json.loads, msg["json_data"]) - config_updates = await manager.profile_store.async_import_data(payload) + pre_entry = _get_entry(hass, entry_id) + config_updates = await manager.profile_store.async_import_data( + payload, + # Kept with the restore point so an undo puts them back (item 195). + entry_options=dict(pre_entry.options) if pre_entry else None, + ) + result = { + "success": True, + "restore_point_saved": bool(config_updates.get("restore_point_saved")), + } # Re-validate after the awaits — the entry may have been reloaded # (a new manager + store) during the import. Persisting through the @@ -3696,52 +4266,14 @@ async def ws_import_config( _LOGGER.warning( "Manager replaced during import for %s; aborting notify", entry_id ) - _send_result(connection, msg["id"], "import_config", {"success": True}) + _send_result(connection, msg["id"], "import_config", result) return entry = _get_entry(hass, entry_id) if entry and config_updates: - entry_options_updates = dict(config_updates.get("entry_options", {})) - # Identity must never be persisted into options; the display name - # rides the entry title. Local entity/device bindings are dropped - # too: they are tunables when the panel writes them, but an - # import carries the exporter's ids and would repoint this device - # at entities that do not exist here (item 317). device_type and - # min_power are genuinely portable and stay. - for key in _IMPORT_LOCAL_BINDING_KEYS: - entry_options_updates.pop(key, None) - if entry_options_updates: - # Apply the imported tunables on top of the current options; - # never spread entry.data into options. An import payload can - # carry a null (an export taken from an entry that still held - # one), and a persisted null survives options.get(key, DEFAULT) - # and breaks setup (#389), so the same write-boundary strip as - # ws_set_options applies here. - # Nested inside the write lock this handler already holds; - # order is always write -> options, so no deadlock. - async with _entry_options_lock(hass, entry_id): - # Read INSIDE the lock. Built before it, `new_options` - # is a snapshot of `entry.options` from before the - # `async with` suspended - so a `ws_set_options` that - # committed while we waited would be silently reverted - # by this write, and the changelog would record the - # post-save value as `old`. The other four option - # writers already read inside their lock; this was the - # one that did not. - new_options = strip_null_options( - {**entry.options, **entry_options_updates} - ) - await _record_option_changes( - hass, entry, entry_options_updates, "import_config" - ) - hass.config_entries.async_update_entry( - entry, options=new_options - ) - # NB: config_updates["entry_data"] is intentionally NOT written to - # entry.data. export_data ships the raw, un-redacted entry.data of - # the *source* device (its power_sensor and other identity), so - # blindly applying it would hijack this device's sensor binding. - # Identity changes must go through the reconfigure flow. + await async_apply_imported_entry_options( + hass, entry, config_updates, "import_config" + ) # An old payload re-arms the one-time banked-tail repair, and the # options write above is the only thing here that could reload the @@ -3749,7 +4281,7 @@ async def ws_import_config( # the imported tails inflating avg_duration until the next restart. manager.async_schedule_banked_tail_repair() manager.notify_update() - _send_result(connection, msg["id"], "import_config", {"success": True}) + _send_result(connection, msg["id"], "import_config", result) except json.JSONDecodeError as exc: connection.send_error(msg["id"], "invalid_json", str(exc)) except Exception as exc: # pylint: disable=broad-exception-caught @@ -3873,9 +4405,7 @@ async def ws_export_config_selective( entry_options=dict(entry.options) if entry else {}, selection=selection, ) - json_str = await hass.async_add_executor_job( - lambda: json.dumps(payload, indent=2) - ) + json_str = await hass.async_add_executor_job(_export_json, payload) _send_result( connection, msg["id"], "export_config_selective", {"json_data": json_str} ) @@ -3904,7 +4434,7 @@ async def ws_import_config_selective( msg: dict[str, Any], ) -> None: """Selectively import chosen categories/items, merging into existing data.""" - from .const import CONF_DEVICE_TYPE, DEFAULT_DEVICE_TYPE, SHAREABLE_SETTING_KEYS + from .const import CONF_DEVICE_TYPE, DEFAULT_DEVICE_TYPE, sanitize_shared_settings entry_id: str = msg["entry_id"] manager = _get_manager(hass, entry_id) @@ -3935,6 +4465,9 @@ async def ws_import_config_selective( cycle_destination=msg["cycle_destination"], apply_settings=msg["apply_settings"], local_device_type=device_type, + # Kept with the restore point of a destructive import so an undo puts + # them back (item 195); never applied to the entry from here. + entry_options=dict(entry.options) if entry is not None else None, ) # Re-validate after the awaits — the entry may have been reloaded (a new @@ -3960,17 +4493,16 @@ async def ws_import_config_selective( settings = summary.get("settings") if isinstance(summary.get("settings"), dict) else {} settings_applied = 0 if entry is not None and settings: - filtered = { - k: v for k, v in settings.items() - if k in SHAREABLE_SETTING_KEYS - and isinstance(v, (int, float)) and not isinstance(v, bool) - } + filtered = sanitize_shared_settings(settings) for key in _OPTIONS_IDENTITY_KEYS: filtered.pop(key, None) if filtered: # Nested inside the write lock this handler already holds; # order is always write -> options, so no deadlock. async with _entry_options_lock(hass, entry_id): + filtered = _without_inverted_threshold_pair( + entry, filtered, "store_device_package" + ) # item 515 await _record_option_changes( hass, entry, filtered, "store_device_package" ) @@ -3998,6 +4530,87 @@ async def ws_import_config_selective( connection.send_error(msg["id"], "unknown_error", str(exc)) +async def _async_restore_entry_options( + hass: HomeAssistant, entry: ConfigEntry, snapshot_options: dict[str, Any] +) -> None: + """Put ``entry.options`` back to what they were before the undone import. + + Exactly the snapshot's options, so a key the import added goes away again, with + one exception: this device's own entity/device bindings keep their CURRENT + values. The import never wrote them (``_IMPORT_LOCAL_BINDING_KEYS``), so any + change to them since was the user's own, and reverting it could re-point the + device at a sensor it no longer uses. Same write-boundary hygiene as the import. + """ + async with _entry_options_lock(hass, entry.entry_id): + current = dict(entry.options) + restored = { + k: v for k, v in snapshot_options.items() if k not in _IMPORT_LOCAL_BINDING_KEYS + } + restored.update({k: current[k] for k in _IMPORT_LOCAL_BINDING_KEYS if k in current}) + restored = strip_null_options(restored) + restored, _ = drop_invalid_numeric_options(restored) + if restored == current: + return + await _record_option_changes( + hass, entry, {k: v for k, v in restored.items() if current.get(k) != v}, + "undo_import", + ) + hass.config_entries.async_update_entry(entry, options=restored) + + +@websocket_api.websocket_command( + {vol.Required("type"): "ha_washdata/undo_import", vol.Required("entry_id"): str} +) +@websocket_api.async_response +async def ws_undo_import( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Undo the last replace import: restore the store and options saved before it. + + The restore point is consumed, so the panel's undo affordance disappears once + this succeeds. No restore point is a clean ``not_found`` error (register item 195). + """ + 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 + + # A restore rewrites the whole store, so it takes the same per-entry write lock + # as the imports and cannot interleave with a reprocess / recording persist. + async with _entry_write_lock(hass, entry_id): + try: + restored = await manager.profile_store.async_restore_pre_import_snapshot() + result = { + "success": True, + "summary": { + "restored_from": restored.get("restored_from"), + "counts": restored.get("counts") or {}, + }, + } + # Re-validate after the awaits, as the imports do: the entry may have + # been reloaded and the options belong to the live one. + if _get_manager(hass, entry_id) is not manager: + _LOGGER.warning( + "Manager replaced during import undo for %s; aborting notify", entry_id + ) + _send_result(connection, msg["id"], "undo_import", result) + return + entry = _get_entry(hass, entry_id) + snapshot_options = restored.get("entry_options") + if entry is not None and isinstance(snapshot_options, dict): + await _async_restore_entry_options(hass, entry, snapshot_options) + manager.async_schedule_banked_tail_repair() + manager.notify_update() + _send_result(connection, msg["id"], "undo_import", result) + except ValueError as exc: + connection.send_error(msg["id"], "not_found", str(exc)) + except Exception as exc: # pylint: disable=broad-exception-caught + connection.send_error(msg["id"], "unknown_error", str(exc)) + + # ─── Shared constants ───────────────────────────────────────────────────────── @websocket_api.websocket_command({vol.Required("type"): "ha_washdata/get_constants"}) @@ -4026,7 +4639,6 @@ def ws_get_constants( "device_types": device_types, "state_colors": dict(STATE_COLORS), "ml_lab_enabled": SHOW_ML_LAB, - "ml_suggestions_enabled": ENABLE_ML_SUGGESTIONS, "ml_training_available": ENABLE_ML_TRAINING, "PROFILE_MIN_WARMUP_CYCLES": CONF_PROFILE_MIN_WARMUP_CYCLES, # Canonical matcher defaults for the Playground's matcher-param fields, so @@ -4070,17 +4682,10 @@ def ws_get_suggestions( out: list[dict[str, Any]] = [] try: - raw: dict[str, Any] = manager.profile_store.get_suggestions() or {} - for key in _SUGGESTION_KEYS: - item = raw.get(key) - if not isinstance(item, dict) or item.get("value") is None: - continue - val = item["value"] - suggested = _coerce_suggested(key, val) - current = merged.get(key) - # Hide suggestions that would not change the current value. - if _suggestion_equivalent(suggested, current): - continue + for key, item, suggested, current in _visible_suggestions( + manager.profile_store, merged, + getattr(manager, "device_type", None) or DEFAULT_DEVICE_TYPE, + ): out.append( { "key": key, @@ -4136,12 +4741,19 @@ async def ws_apply_suggestions( return try: - raw: dict[str, Any] = manager.profile_store.get_suggestions() or {} + merged = {**entry.data, **entry.options} + # Only what the Settings list would show: a muted key re-created by the + # reconcile cascade used to be applied here (audit SUGGEST-11). + visible = { + key: item + for key, item, _s, _c in _visible_suggestions( + manager.profile_store, merged, + getattr(manager, "device_type", None) or DEFAULT_DEVICE_TYPE, + ) + } updates: dict[str, Any] = {} for key in msg["keys"]: - if key not in _SUGGESTION_KEYS: - continue - item = raw.get(key) + item = visible.get(key) if not isinstance(item, dict) or item.get("value") is None: continue val = item["value"] @@ -4149,10 +4761,26 @@ async def ws_apply_suggestions( int(float(val)) if key in _SUGGESTION_INT_KEYS else float(val) ) + # Item 515: the reconciler keeps a stored start/stop pair ordered, but + # applying one of the two alone (a subset, a muted cascade, or a pair + # reconciled against options that changed since) could still invert it. + _inverted = inverted_threshold_pair( + merged, + {**merged, **updates}, + getattr(manager, "device_type", None) or DEFAULT_DEVICE_TYPE, + ) + if _inverted is not None: + connection.send_error( + msg["id"], "invalid_threshold_pair", _threshold_pair_message(*_inverted) + ) + return + if updates: # Clear before updating the entry: async_update_entry schedules a # reload that rebuilds the store, so persist the cleared state first. - cycle_count = len(manager.profile_store.get_past_cycles()) + # The odometer, like the cooldown check (audit SUGGEST-05): at the + # retention cap len(past_cycles) never grows past the stamp again. + cycle_count = manager.profile_store.get_lifetime_cycle_count() manager.profile_store.set_suggestion_apply_cycle_count(cycle_count) # Same critical section as ws_set_options (#442 follow-up): the # changelog snapshot and the options write must be atomic per entry. @@ -4215,7 +4843,11 @@ async def ws_set_suggestion_lock( proposing it (#343).""" entry_id: str = msg["entry_id"] key: str = msg["key"] - if key not in _SUGGESTION_KEYS: + # Only LOCKING needs a key the engine can suggest. Unlocking must accept any + # stored key: a mute on a setting that 0.5.8 stopped suggesting (the confidence + # thresholds, sampling interval, ...) could otherwise never be cleared, and + # "Reset muted" failed on it forever. + if msg["locked"] and key not in _SUGGESTION_KEYS: connection.send_error(msg["id"], "invalid_key", f"Unknown suggestion key: {key!r}") return manager = _get_manager(hass, entry_id) @@ -4308,7 +4940,8 @@ async def ws_get_cycle_power_data( artifacts = await hass.async_add_executor_job( store.detect_cycle_artifacts, cycle["profile_name"], samples ) - meta["artifacts"] = artifacts or [] + # Only with a trace to draw them on (#459). + meta["artifacts"] = (artifacts or []) if samples else [] # HA restart gaps recorded during this cycle (for panel shading). meta["restart_gaps"] = cycle.get("restart_gaps") or [] except Exception as exc: # pylint: disable=broad-exception-caught @@ -4343,6 +4976,166 @@ async def ws_get_cycle_power_data( ) +# Cycle context (register item 513, discussion #463): the power sensor's recorder +# history just before and just after a stored cycle, for the cycle chart to draw +# greyed either side of the stored trace. The cap bounds the two recorder queries. +_CYCLE_CONTEXT_MAX_S = 3600.0 +_CYCLE_CONTEXT_MAX_POINTS = 300 + + +def _cycle_context_points( + rows: list[tuple[float, float | None]], + origin_ts: float, + lo_ts: float, + hi_ts: float, + *, + hi_inclusive: bool, + max_points: int = _CYCLE_CONTEXT_MAX_POINTS, +) -> list[list[float | None]]: + """Recorder rows inside a window as ``[offset_s, watts]`` from ``origin_ts``. + + ``watts`` is None at an unavailable (or non-finite) row, so the panel breaks the + line there rather than holding the last reading across an outage. Each run + between two such rows is thinned on its own (``_downsample`` keeps a run's + extrema), so thinning never bridges a gap. + """ + inside = [ + (ts, w) for ts, w in rows + if lo_ts <= ts and (ts <= hi_ts if hi_inclusive else ts < hi_ts) + ] + total = sum(1 for _ts, w in inside if w is not None) or 1 + out: list[list[float | None]] = [] + run: list[tuple[float, float]] = [] + + def _flush() -> None: + if run: + budget = max(4, round(max_points * len(run) / total)) + out.extend(_downsample([(ts - origin_ts, w) for ts, w in run], budget)) + run.clear() + + for ts, w in inside: + if w is None or not math.isfinite(float(w)): + _flush() + if not out or out[-1][1] is not None: + out.append([round(ts - origin_ts, 2), None]) + continue + run.append((ts, float(w))) + _flush() + return out + + +@websocket_api.websocket_command( + { + vol.Required("type"): "ha_washdata/get_cycle_context", + vol.Required("entry_id"): str, + vol.Required("cycle_id"): str, + vol.Optional("before_s", default=600.0): vol.Coerce(float), + vol.Optional("after_s", default=600.0): vol.Coerce(float), + } +) +@websocket_api.async_response +async def ws_get_cycle_context( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """The power sensor's recorder history around one stored cycle (item 513). + + ``before`` covers ``[start - before_s, start)`` and ``after`` covers + ``[trace end, trace end + after_s]`` (clamped to now), both as + ``[offset_s, watts]`` from the cycle's stored start: the axis the cycle chart + already plots, so the panel draws them either side of the stored trace. + Display only: it reads the recorder and the cycle's start and trace end, writes + nothing, and nothing it returns reaches duration, energy, matching or envelopes. + + Only for a cycle this device observed live (``past``): a community-store + reference was recorded on someone else's machine and a backfill came from an + imported history, so this plug's recorder at that time says nothing about + either. ``available`` is False while the recorder holds no reading for either + window (it keeps 10 days by default; an excluded entity has none), and + ``reason`` says why. + """ + 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 + + def _window(key: str) -> float: + value = float(msg.get(key) or 0.0) + if not math.isfinite(value): + return 0.0 + return max(0.0, min(_CYCLE_CONTEXT_MAX_S, value)) + + cycle_id: str = msg["cycle_id"] + before_s, after_s = _window("before_s"), _window("after_s") + out: dict[str, Any] = { + "cycle_id": cycle_id, + "available": False, + "reason": None, + "entity_id": None, + "before_s": before_s, + "after_s": after_s, + "trace_end_s": 0.0, + "after_end_s": 0.0, + "before": [], + "after": [], + } + try: + store = manager.profile_store + cycle, origin = store.find_stored_cycle(cycle_id) + entity_id = getattr(manager, "power_sensor_entity_id", None) + start_raw = cycle.get("start_time") if cycle else None + start_dt = dt_util.parse_datetime(str(start_raw)) if start_raw else None + if cycle is None: + out["reason"] = "not_found" + elif origin != "past": + out["reason"] = "not_live" + elif not entity_id: + out["reason"] = "no_sensor" + elif start_dt is None: + out["reason"] = "no_start" + else: + out["entity_id"] = entity_id + start_dt = dt_util.as_utc(start_dt) + start_ts = start_dt.timestamp() + samples = store.get_cycle_power_data(cycle_id) + try: + trace_end = float(samples[-1][0]) if samples else float(cycle.get("duration") or 0.0) + except (TypeError, ValueError, OverflowError): + trace_end = 0.0 + trace_end = max(0.0, trace_end) if math.isfinite(trace_end) else 0.0 + out["trace_end_s"] = round(trace_end, 2) + if before_s > 0: + lo = start_ts - before_s + rows = await _recorder_power( + hass, entity_id, start_dt - timedelta(seconds=before_s), + end_dt=start_dt, keep_unavailable=True, + ) + out["before"] = _cycle_context_points( + rows, start_ts, lo, start_ts, hi_inclusive=False + ) + end_dt = start_dt + timedelta(seconds=trace_end) + after_end_dt = min(end_dt + timedelta(seconds=after_s), dt_util.utcnow()) + if after_s > 0 and after_end_dt > end_dt: + rows = await _recorder_power( + hass, entity_id, end_dt, end_dt=after_end_dt, keep_unavailable=True, + ) + out["after"] = _cycle_context_points( + rows, start_ts, end_dt.timestamp(), after_end_dt.timestamp(), + hi_inclusive=True, + ) + out["after_end_s"] = round(after_end_dt.timestamp() - start_ts, 2) + out["available"] = any(p[1] is not None for p in out["before"] + out["after"]) + if not out["available"]: + out["reason"] = "no_history" + except Exception as exc: # pylint: disable=broad-exception-caught + _LOGGER.debug("Error building cycle context for %s: %s", cycle_id, exc) + out.update(available=False, reason="no_history", before=[], after=[]) + + _send_result(connection, msg["id"], "get_cycle_context", out) + + @websocket_api.websocket_command( { vol.Required("type"): "ha_washdata/trim_cycle", @@ -4893,7 +5686,9 @@ async def ws_set_user_prefs( p = msg["prefs"] if p.get("default_tab") in _PANEL_TABS: cur["default_tab"] = p["default_tab"] - for k in ("show_expected", "show_raw", "show_raw_active", "show_debug", "onboarding_dismissed"): + for k in ("show_expected", "show_raw", "show_raw_active", "show_debug", "onboarding_dismissed", + # Settings: reveal the internal tuning fields (audit UI-02). + "show_internal"): if k in p: cur[k] = bool(p[k]) # F2: per-user Basic/Advanced settings disclosure level. @@ -4905,7 +5700,7 @@ async def ws_set_user_prefs( if "font_scale" in p: try: cur["font_scale"] = max(0.7, min(2.0, float(p["font_scale"]))) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): cur.pop("font_scale", None) # Display prefs: cycle date format + panel language override (paired with the # panel's save-prefs payload; without these they would be silently dropped). @@ -4917,6 +5712,19 @@ async def ws_set_user_prefs( cur.pop("lang_override", None) # empty clears -> system default elif isinstance(lang, str) and _PREF_LANG_TAG_RE.match(lang): cur["lang_override"] = lang + # Cycle chart context length per device (item 513): {entry_id: minutes}. Merged + # into the stored map, so a panel only has to send the device it changed. + ctx = p.get("cycle_context_min") + if isinstance(ctx, dict): + ctx_map = dict(cur.get("cycle_context_min") or {}) + for eid, minutes in ctx.items(): + if not isinstance(eid, str) or not eid or len(eid) > 64: + continue + if isinstance(minutes, bool) or minutes not in _PREF_CYCLE_CONTEXT_MINUTES: + continue + ctx_map[eid] = int(minutes) + # Bounded: one key per config entry, never more than any real install has. + cur["cycle_context_min"] = dict(list(ctx_map.items())[-64:]) # Allow setup guidance skip keys: setup_skip_ -> "never" | ISO timestamp for k, v in p.items(): if isinstance(k, str) and k.startswith("setup_skip_"): @@ -4954,11 +5762,15 @@ def ws_get_match_debug( out: dict[str, Any] = {"confidence": None, "ambiguous": False, "candidates": []} try: + # All three from the SAME result (audit MATCH-DECIDE-14). The confidence + # used to be `_last_match_confidence`, the committed program's (moved only + # when the switching rules commit or confirm it), shown beside the newest + # result's ambiguity flag and candidates. mr = getattr(manager, "_last_match_result", None) - conf = getattr(manager, "_last_match_confidence", None) - out["confidence"] = round(float(conf), 4) if conf is not None else None - out["ambiguous"] = bool(getattr(manager, "_last_match_ambiguous", False)) if mr is not None: + conf = getattr(mr, "confidence", None) + out["confidence"] = round(float(conf), 4) if conf is not None else None + out["ambiguous"] = bool(getattr(mr, "is_ambiguous", False)) out["candidates"] = manager.profile_store.get_match_candidates_summary(mr, 5) except Exception as exc: # pylint: disable=broad-exception-caught _LOGGER.debug("Error building match debug for %s: %s", entry_id, exc) @@ -5182,21 +5994,15 @@ def _compute_cycle_events( return events[:max_events] -def _health_model_sig(store: Any) -> str: - """Signature of the active health/end models, so persisted per-cycle health - can be invalidated (recomputed) whenever the model changes or is retrained. - """ - versions = store.get_ml_model_versions() or {} - parts = [] - for cap in ("quality", "end"): - v = versions.get(cap) - ts = v.get("trained_at") if isinstance(v, dict) else None - parts.append(f"{cap}:{ts or 'base'}") - return "|".join(parts) +# Signature persisted with each cycle's ``ml_health``: the shipped quality/end +# baselines. It used to carry an on-device model's ``trained_at`` so a retrain +# invalidated the cache; no on-device classifier exists since 0.5.8, and the +# value is unchanged so health already persisted stays valid. +_HEALTH_MODEL_SIG = "quality:base|end:base" def _compute_ml_comparison( - store: Any, current_off_delay: int, *, force_recompute: bool = False + store: Any, *, force_recompute: bool = False ) -> dict[str, Any]: """Build the ML shadow-mode comparison report (CPU-intensive; runs in executor). @@ -5218,15 +6024,15 @@ def _compute_ml_comparison( from .ml.feature_extraction import latest_end_event_features, quality_features from .profile_store import decompress_power_data except Exception: # pylint: disable=broad-exception-caught - return {"enabled": False, "error": "ML models not available", "cycles": [], "settings_comparison": {}} + return {"enabled": False, "error": "ML models not available", "cycles": []} - # Prefer on-device trained models when present, else the embedded baseline. - quality_score_fn, quality_source = resolve_scorer("quality", store) - end_score_fn, end_source = resolve_scorer("end", store) + # The shipped embedded baselines (no on-device classifier exists since 0.5.8). + quality_score_fn, quality_source = resolve_scorer("quality") + end_score_fn, end_source = resolve_scorer("end") if quality_score_fn is None and end_score_fn is None: - return {"enabled": False, "error": "ML models not available", "cycles": [], "settings_comparison": {}} + return {"enabled": False, "error": "ML models not available", "cycles": []} - model_sig = _health_model_sig(store) + model_sig = _HEALTH_MODEL_SIG health_dirty = False health_updates: dict[str, Any] = {} cycles: list[Any] = store.get_past_cycles() @@ -5257,11 +6063,9 @@ def _compute_ml_comparison( "count": len(s["durations"]), } - # --- Evaluate cycles (newest 200 for scoring/display; all for pause analysis) --- + # --- Evaluate the newest 200 cycles --- # `cycles` is oldest-first (new cycles are appended at the end), so the newest - # 200 are the tail. Score / persist health for only that window; the pause - # scan below still runs over every cycle. - intra_pauses: list[float] = [] + # 200 are the tail; older ones are skipped before their trace is decompressed. evaluated: list[dict[str, Any]] = [] recent_start_idx = max(0, len(cycles) - 200) @@ -5289,35 +6093,56 @@ def _compute_ml_comparison( else: proxy_dist, proxy_margin, proxy_fit = 0.25, 0.30, 0.75 - points = decompress_power_data(cycle) - - # Collect intra-cycle pauses for off_delay recommendation (all cycles). - if points and len(points) >= 4 and status == "completed": - powers = [p for _, p in points] - peak = max(powers) if powers else 0.0 - low_thresh = max(1.0, 0.05 * peak) - in_pause = False - pause_start_s = 0.0 - for offset_s, pwr in points: - if not in_pause and pwr < low_thresh: - in_pause = True - pause_start_s = offset_s - elif in_pause and pwr >= low_thresh: - pause_dur = offset_s - pause_start_s - if 5.0 <= pause_dur <= 1800.0: - intra_pauses.append(pause_dur) - in_pause = False - - # Only score and display the newest 200 cycles (the tail of the - # oldest-first list); older cycles still contributed pauses above. if idx < recent_start_idx: continue + points = decompress_power_data(cycle) + # Reuse persisted per-cycle health when it was computed against the # current model (unless a recompute is forced). This is what keeps the # panel from re-scoring every cycle on every load. cached = cycle.get("ml_health") - if ( + has_trace = len(points) >= ML_HEALTH_MIN_TRACE_POINTS + if not has_trace: + # #459: a cycle whose trace was pruned by retention cannot be scored. + # `quality_features` falls back to a `has_trace = 0` row that no model + # was trained on (the baseline sits ~38 sigma away from it), so every + # pruned cycle came back as ~0.99 / "review" at the next forced + # recompute and joined the review queue. Keep the last assessment that + # was made WITH a trace; anything else (never scored, or scored after + # the trace was gone, which is what the nightly run did until now) is + # "no data". Checked before the cache so polluted entries heal on the + # next panel load rather than at the next maintenance run. + if isinstance(cached, dict) and cached.get("has_trace"): + ml_quality = cached.get("score") + quality_label = cached.get("label", "no_data") + ml_end_conf = cached.get("end_score") + end_label = cached.get("end_label", "no_event") + events = cached.get("events") or [] + else: + ml_quality = None + quality_label = "no_data" + ml_end_conf = None + end_label = "no_event" + events = [] + cycle_id_key = cycle.get("id", "") + if cycle_id_key and not ( + isinstance(cached, dict) + and cached.get("label") == "no_data" + and cached.get("score") is None + ): + health_updates[cycle_id_key] = { + "score": None, + "label": "no_data", + "end_score": None, + "end_label": "no_event", + "events": [], + "model_sig": model_sig, + "has_trace": False, + "at": dt_util.now().isoformat(), + } + health_dirty = True + elif ( not force_recompute and isinstance(cached, dict) and cached.get("model_sig") == model_sig @@ -5395,6 +6220,9 @@ def _compute_ml_comparison( "end_label": end_label, "events": events, "model_sig": model_sig, + # #459: marks a score as trace-backed, so it survives the + # trace being pruned later instead of being re-scored blind. + "has_trace": True, "at": dt_util.now().isoformat(), } health_dirty = True @@ -5413,7 +6241,7 @@ def _compute_ml_comparison( "ml_quality_label": quality_label, "ml_end_confidence": round(ml_end_conf, 3) if ml_end_conf is not None else None, "ml_end_label": end_label, - "has_power_data": len(points) > 0, + "has_power_data": has_trace, "events": events, "ml_review": cycle.get("ml_review") or {}, }) @@ -5421,40 +6249,11 @@ def _compute_ml_comparison( # Panel expects most-recent-first ordering; the loop appended oldest-first. evaluated.reverse() - # --- Settings comparison: off_delay --- - settings_comparison: dict[str, Any] = {} - ml_off_delay: int | None = None - pause_p95: float | None = None - if intra_pauses: - intra_pauses.sort() - p95_idx = min(int(len(intra_pauses) * 0.95), len(intra_pauses) - 1) - pause_p95 = intra_pauses[p95_idx] - ml_off_delay = max(60, int(pause_p95) + 60) - - original_suggestions = store.get_suggestions() or {} - orig_off = original_suggestions.get(CONF_OFF_DELAY) - settings_comparison["off_delay"] = { - "key": CONF_OFF_DELAY, - "label": "Off Delay", - "unit": "s", - "current_value": current_off_delay, - "original_suggestion": orig_off.get("value") if isinstance(orig_off, dict) else None, - "original_reason": orig_off.get("reason") if isinstance(orig_off, dict) else None, - "ml_recommendation": ml_off_delay, - "ml_reasoning": ( - f"p95 intra-cycle pause: {int(pause_p95)}s + 60s buffer = {ml_off_delay}s " - f"(from {len(intra_pauses)} observed pauses)" - if ml_off_delay is not None else "Not enough pause data to recommend" - ), - "pause_count": len(intra_pauses), - } - return { "enabled": True, "cycle_count": len(cycles), "evaluated_count": sum(1 for e in evaluated if e["ml_quality_score"] is not None), "cycles": evaluated, - "settings_comparison": settings_comparison, "model_source": {"quality": quality_source, "end": end_source}, "_health_dirty": health_dirty, "_health_updates": health_updates, @@ -5465,122 +6264,6 @@ def _compute_ml_comparison( } -def _build_settings_comparison( - manager: Any, merged: dict[str, Any], engine: Any = None -) -> dict[str, Any]: - """Build the enriched Classic-vs-ML settings comparison (executor-safe). - - Runs both the classic :class:`SuggestionEngine` and the - :class:`MLSuggestionEngine` against clean cycle history and merges their - recommendations with the current option values. Gated by the caller behind - ``ENABLE_ML_SUGGESTIONS``. - """ - try: - from .suggestion_engine import ( # pylint: disable=import-outside-toplevel - MLSuggestionEngine, - select_clean_cycles, - ) - except Exception: # pylint: disable=broad-exception-caught - return {} - - # `engine` is a for_job() copy prepared on the event loop with the config - # snapshot already bound; fall back to the shared engine only for callers that - # did not supply one (its _entry_options() then does a live read). - classic = engine - if classic is None: - learning = getattr(manager, "learning_manager", None) - classic = getattr(learning, "suggestion_engine", None) - if classic is None: - return {} - - stop_thr = classic._current_stop_threshold(merged) # pylint: disable=protected-access - raw_cycles = classic.profile_store.get_past_cycles() - - # Classic recommendations pooled from every classic pass. - classic_vals: dict[str, Any] = {} - for producer in ( - classic.generate_detection_suggestions, - classic.generate_model_suggestions, - ): - try: - classic_vals.update(producer() or {}) - except Exception: # pylint: disable=broad-exception-caught - pass - try: - classic_vals.update(classic.run_batch_simulation(raw_cycles) or {}) - except Exception: # pylint: disable=broad-exception-caught - pass - # Classic off_delay via the (Stage 2) pause analysis. - try: - clean, _excl = select_clean_cycles(raw_cycles[-200:], stop_threshold_w=stop_thr) - device_floor = DEFAULT_OFF_DELAY_BY_DEVICE.get( - classic.device_type, DEFAULT_OFF_DELAY - ) if classic.device_type else DEFAULT_OFF_DELAY - od = classic._suggest_off_delay_from_pauses(clean, stop_thr, device_floor) # pylint: disable=protected-access - if od is not None: - classic_vals[CONF_OFF_DELAY] = { - "value": od[0], - "reason": od[1], - "reason_key": od[2], - "reason_params": od[3], - } - except Exception: # pylint: disable=broad-exception-caught - pass - - # ML recommendations. - ml_vals: dict[str, Any] = {} - try: - ml_vals = MLSuggestionEngine(classic).generate_ml_suggestions() or {} - except Exception: # pylint: disable=broad-exception-caught - pass - - def _val(entry: Any) -> Any: - return entry.get("value") if isinstance(entry, dict) else None - - def _reason(entry: Any) -> str: - return entry.get("reason", "") if isinstance(entry, dict) else "" - - def _reason_key(entry: Any) -> str | None: - return entry.get("reason_key") if isinstance(entry, dict) else None - - def _reason_params(entry: Any) -> dict[str, Any] | None: - return entry.get("reason_params") if isinstance(entry, dict) else None - - comparison: dict[str, Any] = {} - for key, label, unit in _ML_COMPARE_SETTINGS: - cv, mv = classic_vals.get(key), ml_vals.get(key) - if cv is None and mv is None: - continue - current = merged.get(key) - classic_value = _val(cv) - ml_value = _val(mv) - # Hide a recommendation that would not change the current value. - if classic_value is not None and _suggestion_equivalent(classic_value, current): - classic_value = None - if ml_value is not None and _suggestion_equivalent(ml_value, current): - ml_value = None - if classic_value is None and ml_value is None: - continue - comparison[key] = { - "key": key, - # ``label`` is the English fallback; the panel renders the field's own - # localized setting label (setting..label) so this is not shown - # directly. Reasons carry _key/_params sidecars for _t() rendering. - "label": label, - "unit": unit, - "current_value": current, - "classic_value": classic_value, - "classic_reason": _reason(cv) if classic_value is not None else "", - "classic_reason_key": _reason_key(cv) if classic_value is not None else None, - "classic_reason_params": _reason_params(cv) if classic_value is not None else None, - "ml_value": ml_value, - "ml_reason": _reason(mv) if ml_value is not None else "", - "ml_reason_key": _reason_key(mv) if ml_value is not None else None, - "ml_reason_params": _reason_params(mv) if ml_value is not None else None, - } - return comparison - - @websocket_api.websocket_command( { vol.Required("type"): "ha_washdata/get_ml_comparison", @@ -5607,13 +6290,7 @@ async def ws_get_ml_comparison( try: store = manager.profile_store - entry = _get_entry(hass, entry_id) - merged: dict[str, Any] = {**(entry.data if entry else {}), **(entry.options if entry else {})} - current_off_delay = int(merged.get(CONF_OFF_DELAY, 120)) - - result = await hass.async_add_executor_job( - _compute_ml_comparison, store, current_off_delay - ) + result = await hass.async_add_executor_job(_compute_ml_comparison, store) # Apply any freshly-computed per-cycle health updates on the event loop # (the executor must not mutate live store dicts directly), then persist. health_updates = result.pop("_health_updates", {}) @@ -5624,25 +6301,6 @@ async def ws_get_ml_comparison( if cid and cid in health_updates: cycle["ml_health"] = health_updates[cid] await store.async_save() - result["ml_suggestions_enabled"] = ENABLE_ML_SUGGESTIONS - - # Stage 3: replace the basic off_delay-only comparison with the full - # Classic-vs-ML settings table when ML suggestions are unlocked. - if ENABLE_ML_SUGGESTIONS: - # Bind this handler's already-merged view to a throwaway engine copy - # before the executor hop: reading the config entry is loop-affine, so - # the generators must not re-read it from the worker thread, and a - # copy keeps concurrent jobs from clobbering each other's snapshot. - shared = getattr( - getattr(manager, "learning_manager", None), "suggestion_engine", None - ) - job_engine = shared.for_job(merged) if shared is not None else None - enriched = await hass.async_add_executor_job( - _build_settings_comparison, manager, merged, job_engine - ) - if enriched: - result["settings_comparison"] = enriched - _send_result(connection, msg["id"], "get_ml_comparison", result) except Exception as exc: # pylint: disable=broad-exception-caught _LOGGER.warning("ML comparison failed for %s: %s", entry_id, exc) @@ -5671,10 +6329,6 @@ async def ws_get_ml_training_status( DEFAULT_ML_TRAINING_HOUR, DEFAULT_ML_TRAINING_INTERVAL_DAYS, DEFAULT_ML_TRAINING_MIN_CYCLES, - MATCH_CORR_WEIGHT, - MATCH_DTW_ENSEMBLE_W, - MATCH_DURATION_WEIGHT, - MATCH_ENERGY_WEIGHT, ) entry_id: str = msg["entry_id"] @@ -5690,15 +6344,17 @@ async def ws_get_ml_training_status( # Plain-language names + a one-line "what it does" for each capability, so the # panel never has to show raw internal keys to non-ML users. _cap_labels = { - "end": ("Cycle-end detection", "Knowing when a cycle has truly finished"), - "quality": ("Cycle quality check", "Spotting mis-detected or corrupted cycles"), - "live_match": ("Program matching", "Identifying the running program sooner"), - "remaining_time": ("Time-remaining estimate", "Predicting how long is left"), "total_energy": ("Energy estimate", "Predicting total energy and cost"), } + # Only what is trained on-device AND consumed is listed, which since 0.5.8 is + # `total_energy` alone. `end` is no longer trained (its consumer is frozen + # off, audit ML-05/11) and `remaining_time` / `live_match` / `quality` lost + # consumer and training; storage v17 drops all four records, so an older + # import is the only way one can still be here. + live_caps = set(_cap_labels) models: dict[str, Any] = {} for cap, v in versions.items(): - if not isinstance(v, dict): + if not isinstance(v, dict) or cap not in live_caps: continue spec = v.get("spec") if isinstance(v.get("spec"), dict) else {} label, blurb = _cap_labels.get(cap, (cap, "")) @@ -5715,14 +6371,9 @@ async def ws_get_ml_training_status( "blurb_key": f"ml.cap_blurb.{cap}" if cap in _cap_labels else None, } # Raw metric numbers so the panel can render a humanized quality indicator - # (a bar + word) with the exact figure on hover: held-out AUC for - # classifiers, MAE-vs-naive for regressors. - if v.get("new_auc") is not None: - info["auc"] = round(float(v["new_auc"]), 4) - info["metric"] = f"AUC {float(v['new_auc']):.2f} on held-out data" - info["metric_key"] = "ml.metric_auc" - info["metric_params"] = {"auc": f"{float(v['new_auc']):.2f}"} - elif v.get("model_mae") is not None and v.get("naive_mae") is not None: + # (a bar + word) with the exact figure on hover: MAE vs the naive estimate + # (every listed capability is a regressor since 0.5.8). + if v.get("model_mae") is not None and v.get("naive_mae") is not None: info["model_mae"] = round(float(v["model_mae"]), 5) info["naive_mae"] = round(float(v["naive_mae"]), 5) info["metric"] = f"error {float(v['model_mae']):.3f} vs {float(v['naive_mae']):.3f} baseline" @@ -5731,20 +6382,31 @@ async def ws_get_ml_training_status( "model": f"{float(v['model_mae']):.3f}", "baseline": f"{float(v['naive_mae']):.3f}", } + # How many cycles the metric was measured on (audit ML-20); absent on a + # record promoted before the count was stored. + if isinstance(v.get("held_out_cycles"), int): + info["held_out_cycles"] = int(v["held_out_cycles"]) models[cap] = info - # Fit trend across recent training runs (drift): compare the mean held-out - # score of the most-recent third of runs to the oldest third, respecting each - # metric's direction. Only meaningful with a few runs of history. + # Fit trend across the PROMOTED runs (drift): compare the mean held-out score + # of the most-recent third of the models that were put in use to the oldest + # third, respecting each metric's direction. A candidate that was rejected + # never served, so its score says nothing about the model in use (audit + # ML-20); entries from before that rule carry no `promoted` and are skipped. + # Only meaningful with a few promotions of history. history = store.get_ml_training_history() for cap, info in models.items(): series = history.get(cap) if isinstance(history, dict) else None - if not isinstance(series, list) or len(series) < 4: + if not isinstance(series, list): continue - scores = [float(e["score"]) for e in series if isinstance(e, dict) and "score" in e] + promoted_runs = [ + e for e in series + if isinstance(e, dict) and e.get("promoted") is True and "score" in e + ] + scores = [float(e["score"]) for e in promoted_runs] if len(scores) < 4: continue - higher_better = bool(series[-1].get("higher_better", True)) + higher_better = bool(promoted_runs[-1].get("higher_better", True)) third = max(1, len(scores) // 3) old_mean = sum(scores[:third]) / third new_mean = sum(scores[-third:]) / third @@ -5755,20 +6417,22 @@ async def ws_get_ml_training_status( rel = -rel # for error metrics, a decrease is an improvement info["trend"] = "improving" if rel > 0.03 else "declining" if rel < -0.03 else "steady" - # Matcher scoring-weight tuning (Stage 4/5): current shipped defaults, the - # on-device tuned record (if promoted), and which set is actually in use. - tuned_rec = store.get_matching_config() - tuned_cfg = tuned_rec.get("config") if isinstance(tuned_rec, dict) else None - matching = { - "defaults": { - "corr_weight": MATCH_CORR_WEIGHT, - "duration_weight": MATCH_DURATION_WEIGHT, - "energy_weight": MATCH_ENERGY_WEIGHT, - "dtw_ensemble_w": MATCH_DTW_ENSEMBLE_W, - }, - "tuned": tuned_rec or None, - "active": "tuned" if tuned_cfg else "default", - } + # The last run per capability, so the panel can say why nothing was (re)learnt + # (audit ML-20). Only runs recorded with `promoted` qualify. + last_run: dict[str, Any] = {} + for cap in live_caps: + series = history.get(cap) if isinstance(history, dict) else None + entry = series[-1] if isinstance(series, list) and series else None + if not isinstance(entry, dict) or "promoted" not in entry: + continue + run: dict[str, Any] = {"ts": entry.get("ts"), "promoted": bool(entry["promoted"])} + if not run["promoted"]: + params = entry.get("reason_params") + run["reason_code"] = str(entry.get("reason_code") or "unknown") + run["reason_params"] = dict(params) if isinstance(params, dict) else {} + run["reason"] = str(entry.get("reason") or "") + last_run[cap] = run + _send_result(connection, msg["id"], "get_ml_training_status", { "available": ENABLE_ML_TRAINING, "enabled": bool(merged.get(CONF_ML_TRAINING_ENABLED, DEFAULT_ML_TRAINING_ENABLED)), @@ -5779,7 +6443,7 @@ async def ws_get_ml_training_status( "interval_days": int(merged.get(CONF_ML_TRAINING_INTERVAL_DAYS, DEFAULT_ML_TRAINING_INTERVAL_DAYS)), "hour": int(merged.get(CONF_ML_TRAINING_HOUR, DEFAULT_ML_TRAINING_HOUR)), "on_device_models": models, - "matching": matching, + "last_run": last_run, }, ) @@ -5848,33 +6512,6 @@ def ws_trigger_ml_training( _send_result(connection, msg["id"], "trigger_ml_training", {"task_id": task.id}) -@websocket_api.websocket_command( - { - vol.Required("type"): "ha_washdata/revert_matching_config", - vol.Required("entry_id"): str, - } -) -@websocket_api.async_response -async def ws_revert_matching_config( - hass: HomeAssistant, - connection: websocket_api.ActiveConnection, - msg: dict[str, Any], -) -> None: - """Revert the matcher's scoring weights to the shipped defaults. - - Drops the on-device tuned override so matching falls back to the const - defaults. The next training pass may re-promote a new one if it wins. - """ - 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 - await manager.profile_store.clear_matching_config() - manager.notify_update() - _send_result(connection, msg["id"], "revert_matching_config", {"success": True}) - - @websocket_api.websocket_command( { vol.Required("type"): "ha_washdata/revert_ml_models", @@ -5889,11 +6526,9 @@ async def ws_revert_ml_models( ) -> None: """Revert all on-device trained models to the shipped embedded baselines. - Drops every promoted spec in ``ml_model_versions`` so ``resolve_scorer`` / - ``resolve_regressor`` fall back to the baseline (or, for the baseline-less - remaining-time regressor, become inert). The next training pass may - re-promote models if they beat the baseline again. Mirrors - ``revert_matching_config`` for the matcher weights. + Drops every promoted spec in ``ml_model_versions`` so ``resolve_regressor`` + becomes inert for the baseline-less ``total_energy`` regressor. The next training pass may + re-promote models if they beat the baseline again. """ entry_id: str = msg["entry_id"] manager = _get_manager(hass, entry_id) @@ -5927,8 +6562,8 @@ async def ws_set_ml_review( ) -> None: """Attach an ML-Lab review (quality / golden / tags / notes) to a cycle. - This is the write-back that turns the read-only shadow view into a feedback - loop: reviews become strong training labels for the on-device quality model. + The write-back from the read-only shadow view. (Until 0.5.8 a review also + labelled the on-device quality model, whose training was removed.) """ entry_id: str = msg["entry_id"] manager = _get_manager(hass, entry_id) @@ -6013,15 +6648,6 @@ async def ws_terminate_cycle( # ─── Playground (F3): headless what-if replay + DTW visualizer ────────────────── -def _safe_float_finite(value: Any, default: float) -> float: - """Convert ``value`` to a finite float, falling back to ``default`` on failure.""" - try: - v = float(value) - return v if math.isfinite(v) else default - except (TypeError, ValueError): - return default - - def _playground_base_config(manager: Any, entry: Any) -> CycleDetectorConfig: """Resolve the device's live CycleDetectorConfig as the simulation base. @@ -6033,47 +6659,15 @@ def _playground_base_config(manager: Any, entry: Any) -> CycleDetectorConfig: cfg = getattr(detector, "config", None) if isinstance(cfg, CycleDetectorConfig): return cfg - opts: dict[str, Any] = {} - if entry is not None: - opts = {**getattr(entry, "data", {}), **getattr(entry, "options", {})} - min_power = float(opts.get(CONF_MIN_POWER, DEFAULT_MIN_POWER) or DEFAULT_MIN_POWER) - return CycleDetectorConfig( - min_power=min_power, - off_delay=int(opts.get(CONF_OFF_DELAY, DEFAULT_OFF_DELAY)), - device_type=str(opts.get(CONF_DEVICE_TYPE, DEFAULT_DEVICE_TYPE)), - completion_min_seconds=int(opts.get(CONF_COMPLETION_MIN_SECONDS, 600)), - end_repeat_count=int(opts.get(CONF_END_REPEAT_COUNT, 1)), - min_off_gap=int(opts.get(CONF_MIN_OFF_GAP, 60)), - start_threshold_w=float(opts.get(CONF_START_THRESHOLD_W, min_power)), - stop_threshold_w=float( - opts.get(CONF_STOP_THRESHOLD_W, min_power * 0.6 if min_power else 2.0) - ), - dishwasher_end_spike_quiet_release=_safe_float_finite( - opts.get(CONF_DISHWASHER_END_SPIKE_QUIET_RELEASE), - DISHWASHER_END_SPIKE_QUIET_RELEASE_SECONDS, - ), - smart_termination_duration_ratio=_safe_float_finite( - opts.get(CONF_SMART_TERMINATION_DURATION_RATIO), - resolve_smart_termination_duration_ratio_default( - 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( - opts.get(CONF_PROFILE_MATCH_THRESHOLD), DEFAULT_PROFILE_MATCH_THRESHOLD - ), + # The same builder the manager uses (audit F2): this fallback had its own + # defaults (min_off_gap 60 s for every device, scalar completion floor) and + # gave a not-yet-initialised device a configuration no detector runs. + options = dict(getattr(entry, "options", {}) or {}) if entry is not None else {} + data = dict(getattr(entry, "data", {}) or {}) if entry is not None else {} + device_type = str( + options.get(CONF_DEVICE_TYPE, data.get(CONF_DEVICE_TYPE, DEFAULT_DEVICE_TYPE)) ) + return build_detector_config(options, data, device_type) def _playground_context(hass: HomeAssistant, entry_id: str): @@ -6097,199 +6691,17 @@ def _playground_context(hass: HomeAssistant, entry_id: str): return manager, store, base_config, options, price -@websocket_api.websocket_command( - { - vol.Required("type"): "ha_washdata/run_playground_cycle_detail", - vol.Required("entry_id"): str, - vol.Required("cycle_id"): str, - vol.Optional("settings_override", default=dict): dict, - } -) -@websocket_api.async_response -async def ws_run_playground_cycle_detail( - hass: HomeAssistant, - connection: websocket_api.ActiveConnection, - msg: dict[str, Any], -) -> None: - """Faithful single-cycle simulation timeline (series + events + alerts + - outcome) for the Playground "Simulate" view. Read-only what-if.""" - entry_id: str = msg["entry_id"] - ctx = _playground_context(hass, entry_id) - if ctx is None: - _err_not_found(connection, msg["id"], entry_id) - return - _manager, store, base_config, options, price = ctx - try: - cycle_id = msg["cycle_id"] - override = dict(msg.get("settings_override") or {}) - # The store lookup + replay run together in the executor so no store - # access happens on the event loop. - payload = await hass.async_add_executor_job( - playground.simulate_cycle_detail_by_id, - store, cycle_id, base_config, override, options, price, - ) - if isinstance(payload, dict) and payload.get("error") == "not_found": - connection.send_error(msg["id"], "not_found", "Cycle not found") - return - _send_result(connection, msg["id"], "run_playground_cycle_detail", payload) - except Exception as exc: # pylint: disable=broad-exception-caught - _LOGGER.debug("Playground cycle detail failed for %s: %s", entry_id, exc) - connection.send_error(msg["id"], "unknown_error", str(exc)) - - -@websocket_api.websocket_command( - { - vol.Required("type"): "ha_washdata/run_playground_history", - vol.Required("entry_id"): str, - vol.Optional("cycle_ids", default=list): [str], - vol.Optional("settings_override", default=dict): dict, - vol.Optional("concurrency", default=25): vol.Coerce(int), - } -) -@websocket_api.async_response -async def ws_run_playground_history( - hass: HomeAssistant, - connection: websocket_api.ActiveConnection, - msg: dict[str, Any], -) -> None: - """Per-cycle results table (+ before/after diff when settings_override is set) - for the Playground "Test on history" view. Read-only what-if.""" - entry_id: str = msg["entry_id"] - ctx = _playground_context(hass, entry_id) - if ctx is None: - _err_not_found(connection, msg["id"], entry_id) - return - _manager, store, base_config, options, price = ctx - try: - cycle_ids = list(msg.get("cycle_ids") or []) - override = dict(msg.get("settings_override") or {}) - # Bound the batch size to the same safe cap as the backend (and sweep), - # so an oversized caller value can't request unbounded replay work. - concurrency = max(1, min(playground.MAX_BATCH_CYCLES, int(msg.get("concurrency", 25)))) - payload = await hass.async_add_executor_job( - playground.run_playground_history, - store, cycle_ids, base_config, override, options, price, concurrency, - ) - _send_result(connection, msg["id"], "run_playground_history", payload) - except Exception as exc: # pylint: disable=broad-exception-caught - _LOGGER.debug("Playground history failed for %s: %s", entry_id, exc) - connection.send_error(msg["id"], "unknown_error", str(exc)) - - -#: Max sweep points per axis - caps the grid so a caller can't request an -#: unbounded number of full-history replays (values x values_y x cycles). -_MAX_SWEEP_VALUES = 20 - - -@websocket_api.websocket_command( - { - vol.Required("type"): "ha_washdata/run_playground_sweep", - vol.Required("entry_id"): str, - vol.Required("param"): str, - vol.Required("values"): vol.All([vol.Coerce(float)], vol.Length(min=1, max=_MAX_SWEEP_VALUES)), - vol.Required("objective"): str, - vol.Optional("cycle_ids", default=list): [str], - vol.Optional("concurrency", default=15): vol.Coerce(int), - vol.Optional("param_y"): str, - vol.Optional("values_y"): vol.All([vol.Coerce(float)], vol.Length(max=_MAX_SWEEP_VALUES)), - } -) -@websocket_api.async_response -async def ws_run_playground_sweep( - hass: HomeAssistant, - connection: websocket_api.ActiveConnection, - msg: dict[str, Any], -) -> None: - """Objective-driven parameter sweep (1D curve or 2D heatmap) for the - Playground "Sweep" view. Read-only what-if.""" - entry_id: str = msg["entry_id"] - ctx = _playground_context(hass, entry_id) - if ctx is None: - _err_not_found(connection, msg["id"], entry_id) - return - _manager, store, base_config, options, price = ctx - param_y = msg.get("param_y") - values_y = list(msg["values_y"]) if msg.get("values_y") else None - # A 2D sweep needs BOTH the second parameter and its values, or neither. - if bool(param_y) != bool(values_y): - connection.send_error( - msg["id"], "invalid_format", - "param_y and values_y must both be provided for a 2D sweep, or both omitted", - ) - return - try: - cycle_ids = list(msg.get("cycle_ids") or []) - concurrency = max(1, min(playground.MAX_BATCH_CYCLES, int(msg.get("concurrency", 15)))) - payload = await hass.async_add_executor_job( - playground.run_playground_sweep, - store, - cycle_ids, - base_config, - msg["param"], - list(msg.get("values") or []), - msg["objective"], - options, - price, - concurrency, - param_y, - values_y, - ) - _send_result(connection, msg["id"], "run_playground_sweep", payload) - except Exception as exc: # pylint: disable=broad-exception-caught - _LOGGER.debug("Playground sweep failed for %s: %s", entry_id, exc) - connection.send_error(msg["id"], "unknown_error", str(exc)) - - -@websocket_api.websocket_command( - { - vol.Required("type"): "ha_washdata/get_dtw_debug", - vol.Required("entry_id"): str, - vol.Required("cycle_id"): str, - vol.Optional("profile_name"): vol.Any(str, None), - } -) -@websocket_api.async_response -async def ws_get_dtw_debug( - hass: HomeAssistant, - connection: websocket_api.ActiveConnection, - msg: dict[str, Any], -) -> None: - """Return the Stage 2 / DTW / Stage 4 score breakdown + resampled traces + - DTW warp path for one cycle vs one profile (defaults to the cycle's label).""" - 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 - - store = getattr(manager, "profile_store", None) - if store is None: - connection.send_error(msg["id"], "unavailable", "Profile store unavailable") - return - - try: - payload = await hass.async_add_executor_job( - playground.dtw_debug_payload, - store, - msg["cycle_id"], - msg.get("profile_name"), - ) - if isinstance(payload, dict) and payload.get("error"): - connection.send_error( - msg["id"], payload["error"], payload.get("detail", payload["error"]) - ) - return - _send_result(connection, msg["id"], "get_dtw_debug", payload) - except Exception as exc: # pylint: disable=broad-exception-caught - _LOGGER.debug("DTW debug failed for %s: %s", entry_id, exc) - connection.send_error(msg["id"], "unknown_error", str(exc)) - - # ─── Playground settings control panel (live values + presets) ───────────────── def _playground_preset_list(store: Any) -> list[dict[str, Any]]: - """Presets as a name-sorted list for the panel dropdown.""" + """Presets as a name-sorted list for the panel dropdown. + + Each stored record's values are passed through the same allow-list a save + uses, so a preset written before 0.5.8 still loads: keys the Playground no + longer offers (the Stage 2-4 matcher weights) are dropped from the view while + the stored record stays untouched. + """ presets = store.get_playground_presets() out: list[dict[str, Any]] = [] for name, record in presets.items(): @@ -6297,7 +6709,7 @@ def _playground_preset_list(store: Any) -> list[dict[str, Any]]: continue out.append({ "name": name, - "values": dict(record.get("values") or {}), + "values": playground.sanitize_setting_values(record.get("values")), "created_at": record.get("created_at"), "updated_at": record.get("updated_at"), }) @@ -6325,12 +6737,9 @@ async def ws_get_playground_settings( really running - never on a stale schema default. ``publishable`` lists the keys the panel may write back to the config entry. - ``include_suggestions=False`` skips the auto-tuner/ML suggestion blocks. They exist - only to label the two "Load suggested" buttons, and the ML one runs statistics across - every clean cycle - real work, on the critical path of opening the tab. The panel - therefore opens without them and fetches them in the background; the buttons already - render only once their count is non-zero, so they simply appear when the data lands. - Defaults to True so every other caller is unaffected. + ``include_suggestions=False`` skips the auto-tuner suggestions. They exist only to + label the "Load suggested" button, so the panel opens without them and fetches them + in the background. Defaults to True so every other caller is unaffected. """ entry_id: str = msg["entry_id"] ctx = _playground_context(hass, entry_id) @@ -6362,50 +6771,15 @@ async def ws_get_playground_settings( val = item["value"] try: classic_sugg[key] = int(float(val)) if key in _SUGGESTION_INT_KEYS else round(float(val), 4) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): pass - # ML suggestions — computed on-demand; executor-offloaded because it runs - # statistics across all clean cycles. None when the feature is disabled. - ml_sugg: dict[str, Any] | None = None - if ENABLE_ML_SUGGESTIONS and include_suggestions: - ml_sugg = {} - try: - learning = getattr(_manager, "learning_manager", None) - classic_engine = getattr(learning, "suggestion_engine", None) - if classic_engine is not None: - _pg_setting_keys = playground.SETTING_KEYS - _pg_int_keys = _SUGGESTION_INT_KEYS - job_engine = classic_engine.for_job(options) - - def _run_ml_sugg() -> dict[str, Any]: - from .suggestion_engine import MLSuggestionEngine as _ML # pylint: disable=import-outside-toplevel - raw = _ML(job_engine).generate_ml_suggestions() or {} - out: dict[str, Any] = {} - for k in _pg_setting_keys: - entry = raw.get(k) - if isinstance(entry, dict) and entry.get("value") is not None: - v = entry["value"] - try: - out[k] = int(float(v)) if k in _pg_int_keys else round(float(v), 4) - except (TypeError, ValueError): - pass - return out - - ml_sugg = await hass.async_add_executor_job(_run_ml_sugg) - except Exception as exc: # pylint: disable=broad-exception-caught - _LOGGER.debug( - "Could not generate Playground ML suggestions for %s: %s", entry_id, exc - ) - _send_result(connection, msg["id"], "get_playground_settings", { "effective": playground.effective_settings(base_config, match_config), "presets": _playground_preset_list(store), "publishable": sorted(playground.PUBLISHABLE_SETTING_KEYS), "preset_limit": PLAYGROUND_PRESET_MAX, "classic_suggestions": classic_sugg, - "ml_suggestions": ml_sugg, - "ml_suggestions_enabled": ENABLE_ML_SUGGESTIONS, }) @@ -6484,24 +6858,6 @@ async def ws_delete_playground_preset( # ─── Background-task registry (progress / cancel / reconnect-safe results) ────── -@websocket_api.websocket_command( - { - vol.Required("type"): "ha_washdata/list_tasks", - vol.Optional("entry_id"): vol.Any(str, None), - } -) -@callback -def ws_list_tasks( - hass: HomeAssistant, - connection: websocket_api.ActiveConnection, - msg: dict[str, Any], -) -> None: - """Snapshot of active + recently-finished background tasks (for reconnect - rehydration / a one-shot refresh). Optionally filtered to one device.""" - reg = task_registry.get_registry(hass) - _send_result(connection, msg["id"], "list_tasks", {"tasks": reg.snapshot(msg.get("entry_id"))}) - - @websocket_api.websocket_command( { vol.Required("type"): "ha_washdata/subscribe_tasks", @@ -6585,10 +6941,36 @@ def ws_get_task_result( _PG_HISTORY_CHUNK = 2 +# One batch replay (Test on history / Optimize) per device at a time (audit +# PLATFORM-04): each is minutes of executor CPU on a Pi, and nothing stopped a +# reconnecting panel or a script from stacking them. A second is refused with +# `task_busy`; a new Simulate supersedes the previous one instead (PLAYGROUND-12). +_PG_BATCH_KINDS = frozenset({"pg_history", "pg_sweep"}) + + +def _pg_running(reg: Any, entry_id: str, kinds: frozenset[str]) -> list[str]: + """Ids of this entry's running tasks of ``kinds``.""" + return [ + t["id"] for t in reg.snapshot(entry_id) + if t.get("state") == task_registry.STATE_RUNNING and t.get("kind") in kinds + ] + + +def _pg_refuse_busy(hass: HomeAssistant, connection: Any, msg_id: int, entry_id: str) -> bool: + """Send ``task_busy`` and return True while a batch replay runs for the entry.""" + if not _pg_running(task_registry.get_registry(hass), entry_id, _PG_BATCH_KINDS): + return False + connection.send_error( + msg_id, "task_busy", + "A Test-on-history or Optimize run is already in progress for this device", + ) + return True + async def _pg_history_task( hass: HomeAssistant, task: Any, entry_id: str, cycle_ids: list[str], override: dict[str, Any] | None, + count: int | None = None, ) -> None: reg = task_registry.get_registry(hass) ctx = _playground_context(hass, entry_id) @@ -6597,13 +6979,10 @@ async def _pg_history_task( return _manager, store, base_config, options, price = ctx try: - past = await hass.async_add_executor_job(lambda: list(store.get_past_cycles() or [])) - by_id = {c.get("id"): c for c in past if isinstance(c, dict)} - if cycle_ids: - ids = [c for c in cycle_ids if c in by_id] - else: - ids = [c.get("id") for c in past[-playground.DEFAULT_RECENT_CYCLES:]] - ids = [i for i in ids[:playground.MAX_BATCH_CYCLES] if i] + selected = await hass.async_add_executor_job( + playground._select_cycles, store, cycle_ids or None, count # noqa: SLF001 + ) + ids = [c.get("id") for c in selected if c.get("id")] # Build match snapshots once — they are store-derived and identical for # every chunk; rebuilding per chunk is O(n_profiles) wasted work. prebuilt = await hass.async_add_executor_job(playground._build_match_snapshots, store) @@ -6639,7 +7018,8 @@ async def _pg_history_task( async def _pg_sweep_task( hass: HomeAssistant, task: Any, entry_id: str, param: str, values: list[float], objective: str, - param_y: str | None, values_y: list[float] | None, + cycle_ids: list[str] | None = None, + count: int | None = None, ) -> None: reg = task_registry.get_registry(hass) ctx = _playground_context(hass, entry_id) @@ -6648,54 +7028,40 @@ async def _pg_sweep_task( return _manager, store, base_config, options, price = ctx try: - past = await hass.async_add_executor_job(lambda: list(store.get_past_cycles() or [])) - ids = [c.get("id") for c in past[-playground.DEFAULT_RECENT_CYCLES:] if isinstance(c, dict)] - ids = [i for i in ids[:playground.MAX_BATCH_CYCLES] if i] + # The cycles the panel's "Last N" picked (audit PLAYGROUND-18), else the + # most recent ones; the selection rule is the playground's own. + selected = await hass.async_add_executor_job( + playground._select_cycles, store, cycle_ids or None, count # noqa: SLF001 + ) + ids = [c.get("id") for c in selected if c.get("id")] n = max(1, len(ids)) # Build match snapshots once — identical for every sweep value/cell. prebuilt = await hass.async_add_executor_job(playground._build_match_snapshots, store) - if param_y and values_y: - reg.update(task, total=len(values) * len(values_y)) - grid: list[list[float | None]] = [[None] * len(values) for _ in values_y] - current: dict[str, Any] = {} - done = 0 - cancelled = False - for j, vy in enumerate(values_y): - for i, vx in enumerate(values): - if task.cancel_requested: - cancelled = True - break - r = await hass.async_add_executor_job( - playground.run_playground_sweep, - store, ids, base_config, param, [vx], objective, - options, price, n, param_y, [vy], prebuilt, - ) - cell = (r.get("grid") or [[None]])[0] - grid[j][i] = cell[0] if cell else None - if r.get("current"): - current = r["current"] - done += 1 - reg.update(task, done=done) - if cancelled: - break - payload = playground.finalize_sweep_2d(param, param_y, objective, values, values_y, grid, current) - else: - reg.update(task, total=len(values)) - points: list[dict[str, Any]] = [] - current_value: Any = None - for i, vx in enumerate(values): - if task.cancel_requested: - break - r = await hass.async_add_executor_job( - playground.run_playground_sweep, - store, ids, base_config, param, [vx], objective, options, price, n, - None, None, prebuilt, - ) - points.extend(r.get("points") or []) - if r.get("current_value") is not None: - current_value = r["current_value"] - reg.update(task, done=i + 1) - payload = playground.finalize_sweep_1d(param, objective, points, current_value) + reg.update(task, total=len(values) + 1) + # The current settings on the same cycles: what a value has to beat, and + # the early-end / split / detection floor none may lose (PLAYGROUND-08). + baseline = await hass.async_add_executor_job( + playground.sweep_baseline, + store, ids, base_config, objective, options, price, prebuilt, + ) + reg.update(task, done=1) + points: list[dict[str, Any]] = [] + current_value: Any = None + for i, vx in enumerate(values): + if task.cancel_requested: + break + r = await hass.async_add_executor_job( + playground.run_playground_sweep, + store, ids, base_config, param, [vx], objective, options, price, n, + prebuilt, + ) + points.extend(r.get("points") or []) + if r.get("current_value") is not None: + current_value = r["current_value"] + reg.update(task, done=i + 2) + payload = playground.finalize_sweep_1d( + param, objective, points, current_value, baseline + ) payload["partial"] = task.cancel_requested reg.finish( task, @@ -6716,6 +7082,10 @@ async def _pg_sweep_task( vol.Required("entry_id"): str, vol.Optional("cycle_ids", default=list): [str], vol.Optional("settings_override", default=dict): dict, + # "Last N" (audit PLAYGROUND-18): the most recent N, newest first. + vol.Optional("count"): vol.All( + vol.Coerce(int), vol.Range(min=1, max=playground.MAX_BATCH_CYCLES) + ), } ) @callback @@ -6730,11 +7100,15 @@ def ws_start_playground_history( if _playground_context(hass, entry_id) is None: _err_not_found(connection, msg["id"], entry_id) return + if _pg_refuse_busy(hass, connection, msg["id"], entry_id): + return reg = task_registry.get_registry(hass) task = reg.create(entry_id, "pg_history", "Test on history") override = dict(msg.get("settings_override") or {}) or None cycle_ids = list(msg.get("cycle_ids") or []) - _raw = hass.async_create_task(_pg_history_task(hass, task, entry_id, cycle_ids, override)) + _raw = hass.async_create_task( + _pg_history_task(hass, task, entry_id, cycle_ids, override, msg.get("count")) + ) if _raw is not None: reg.link_asyncio_task(task.id, _raw) _send_result(connection, msg["id"], "start_playground_history", {"task_id": task.id}) @@ -6745,10 +7119,15 @@ def ws_start_playground_history( vol.Required("type"): "ha_washdata/start_playground_sweep", vol.Required("entry_id"): str, vol.Required("param"): str, - vol.Required("values"): [vol.Coerce(float)], + # Capped like the old one-shot command's 20x20 (audit PLAYGROUND-16). + vol.Required("values"): vol.All([vol.Coerce(float)], vol.Length(max=20)), vol.Required("objective"): str, - vol.Optional("param_y"): vol.Any(str, None), - vol.Optional("values_y"): vol.Any([vol.Coerce(float)], None), + # Which cycles (audit PLAYGROUND-18): the panel's "Last N" as `count`, the + # most recent N; explicit ids win; neither = the most recent 20. + vol.Optional("cycle_ids", default=list): [str], + vol.Optional("count"): vol.All( + vol.Coerce(int), vol.Range(min=1, max=playground.MAX_BATCH_CYCLES) + ), } ) @callback @@ -6762,10 +7141,7 @@ def ws_start_playground_sweep( if _playground_context(hass, entry_id) is None: _err_not_found(connection, msg["id"], entry_id) return - param_y = msg.get("param_y") - values_y = msg.get("values_y") - if bool(param_y) != bool(values_y): - connection.send_error(msg["id"], "invalid_format", "param_y and values_y must be set together") + if _pg_refuse_busy(hass, connection, msg["id"], entry_id): return reg = task_registry.get_registry(hass) task = reg.create( @@ -6774,7 +7150,7 @@ def ws_start_playground_sweep( ) _raw = hass.async_create_task(_pg_sweep_task( hass, task, entry_id, msg["param"], list(msg.get("values") or []), - msg["objective"], param_y, list(values_y) if values_y else None, + msg["objective"], list(msg.get("cycle_ids") or []), msg.get("count"), )) if _raw is not None: reg.link_asyncio_task(task.id, _raw) @@ -6790,8 +7166,6 @@ _PG_DETAIL_CHUNK = 250 async def _pg_detail_task( hass: HomeAssistant, task: Any, entry_id: str, cycle_id: str, override: dict[str, Any] | None, - stress_tail: bool = False, - stress_idle_w: float | None = None, ) -> None: reg = task_registry.get_registry(hass) ctx = _playground_context(hass, entry_id) @@ -6803,7 +7177,6 @@ async def _pg_detail_task( sim = await hass.async_add_executor_job( playground.build_cycle_detail_sim_by_id, store, cycle_id, base_config, override, options, price, - stress_tail, stress_idle_w, ) if isinstance(sim, dict): # {"error": ...} marker (not_found / build failure) if sim.get("error") == "not_found": @@ -6822,10 +7195,7 @@ async def _pg_detail_task( await hass.async_add_executor_job(sim.step, i, i + _PG_DETAIL_CHUNK) reg.update(task, done=min(total, i + _PG_DETAIL_CHUNK)) if not task.cancel_requested: - if sim.stress_tail: - await hass.async_add_executor_job(sim.run_stress_tail) - else: - await hass.async_add_executor_job(sim.run_tail) + await hass.async_add_executor_job(sim.run_tail) payload = await hass.async_add_executor_job(sim.finalize) payload["partial"] = task.cancel_requested reg.finish( @@ -6847,8 +7217,6 @@ async def _pg_detail_task( vol.Required("entry_id"): str, vol.Required("cycle_id"): str, vol.Optional("settings_override", default=dict): dict, - vol.Optional("stress_tail", default=False): bool, - vol.Optional("stress_idle_w", default=None): vol.Any(vol.Coerce(float), None), } ) @callback @@ -6866,15 +7234,17 @@ def ws_start_playground_cycle_detail( _err_not_found(connection, msg["id"], entry_id) return reg = task_registry.get_registry(hass) + # A new Simulate replaces the previous one: its result would be dropped by the + # panel anyway, and it kept replaying to the end (PLAYGROUND-12 / PLATFORM-04). + for stale in _pg_running(reg, entry_id, frozenset({"pg_detail"})): + reg.cancel(stale) task = reg.create( entry_id, "pg_detail", "Simulate cycle", label_key="task.pg_detail.simulate", label_params={}, ) override = dict(msg.get("settings_override") or {}) - stress_tail = bool(msg.get("stress_tail", False)) - stress_idle_w = msg.get("stress_idle_w") _raw = hass.async_create_task( - _pg_detail_task(hass, task, entry_id, msg["cycle_id"], override, stress_tail, stress_idle_w) + _pg_detail_task(hass, task, entry_id, msg["cycle_id"], override) ) if _raw is not None: reg.link_asyncio_task(task.id, _raw) @@ -7098,6 +7468,8 @@ async def ws_history_import_recorder( "source": "recorder", "rows": rows[:HISTORY_IMPORT_MAX_ROWS], "entity_id": entity_id, + # Carried into the parse report, so the review step says the read was cut. + "truncated": len(rows) > HISTORY_IMPORT_MAX_ROWS, } _send_result(connection, msg["id"], "history_import_recorder", { "token": token, @@ -7111,24 +7483,41 @@ async def ws_history_import_recorder( }) -def _history_samples(slot: dict[str, Any], entity_id: str | None) -> Any: +def _history_samples( + slot: dict[str, Any], entity_id: str | None, parser: Any = None +) -> Any: """Turn a staging slot into samples, whichever way it was filled. - Executor-side: CSV parsing is pure Python over megabytes of text. Returns either a - ``(samples, report)`` pair or an ``{"error": ...}`` marker. + Executor-side: CSV parsing is pure Python over megabytes of text, so the scan task + steps a ``HistoryCsvParser`` across jobs first and hands it in (``parser``). + Returns either a ``(samples, report)`` pair or an ``{"error": ...}`` marker. """ if slot.get("source") == "recorder": - samples = history_import.samples_from_readings(slot.get("rows") or []) + # `parser` is then a stepped RecorderReadings (audit PLAYGROUND-11). + samples = ( + parser.result() + if parser is not None + else history_import.samples_from_readings(slot.get("rows") or []) + ) if len(samples) < 2: return {"error": "no_readings"} - return samples, { - "rows_total": len(samples), - "rows_parsed": len(samples), - "entity_id": slot.get("entity_id"), - "source": "recorder", - } - parsed = history_import.parse_history_csv( - "".join(slot.get("chunks") or []), entity_id=entity_id + # The same report a CSV gets, so the review step shows the span, the peak, + # the kW hint and the row-limit cut for a recorder read too (PLAYGROUND-11). + report = history_import.ParsedHistory( + samples=samples, + entity_id=slot.get("entity_id"), + rows_total=len(samples), + rows_parsed=len(samples), + truncated=bool(slot.get("truncated")), + ).report() + report["source"] = "recorder" + return samples, report + parsed = ( + parser.result() + if parser is not None + else history_import.parse_history_csv( + "".join(slot.get("chunks") or []), entity_id=entity_id + ) ) if isinstance(parsed, dict): return parsed @@ -7167,21 +7556,49 @@ async def _history_import_scan_task( return try: entity_id = getattr(manager, "power_sensor_entity_id", None) - parsed = await hass.async_add_executor_job(_history_samples, slot, entity_id) + parser = None + if slot.get("source") != "recorder": + # Parsed a slice per executor job, not in one (audit PLAYGROUND-11). + parser = await hass.async_add_executor_job(functools.partial( + history_import.HistoryCsvParser, + "".join(slot.get("chunks") or []), entity_id=entity_id, + )) + reg.update(task, total=parser.rows_estimate) + while not parser.finished: + if task.cancel_requested: + reg.finish(task, state=task_registry.STATE_CANCELLED) + return + await hass.async_add_executor_job(parser.step, history_import.PARSE_STEP_ROWS) + reg.update(task, done=min(parser.rows_estimate, parser.out.rows_total)) + else: + # The recorder rows are converted a slice per job too. + parser = history_import.RecorderReadings(slot.get("rows") or []) + reg.update(task, total=parser.rows_estimate) + while not parser.finished: + if task.cancel_requested: + reg.finish(task, state=task_registry.STATE_CANCELLED) + return + await hass.async_add_executor_job(parser.step, history_import.PARSE_STEP_ROWS) + reg.update(task, done=parser.done) + parsed = await hass.async_add_executor_job(_history_samples, slot, entity_id, parser) if isinstance(parsed, dict): reg.finish(task, state=task_registry.STATE_ERROR, error=str(parsed.get("error"))) return samples, report = parsed sampling_interval = options.get(CONF_SAMPLING_INTERVAL) - runner = await hass.async_add_executor_job( - functools.partial( - history_import.build_scan, - samples, - base_config, - sampling_interval_s=sampling_interval, - parse_report=report, - ) + # Blocks, gates and densification, a slice per job (audit PLAYGROUND-11). + builder = history_import.ScanBuilder( + samples, base_config, + sampling_interval_s=sampling_interval, parse_report=report, ) + while not builder.finished: + if task.cancel_requested: + reg.finish(task, state=task_registry.STATE_CANCELLED) + return + await hass.async_add_executor_job( + builder.step, history_import.SCAN_BUILD_STEP_SAMPLES + ) + runner = builder.result() if isinstance(runner, dict): # A stream with nothing usable in it is a *result*, not a failure: the panel # explains which spans were skipped and why (six months of hourly averages @@ -7203,6 +7620,21 @@ async def _history_import_scan_task( payload = await hass.async_add_executor_job( functools.partial(runner.finalize, partial=task.cancel_requested) ) + # The replay is unmatched, so a dishwasher's timeout finish banked its whole + # end wait (audit PLAYGROUND-07): cut it by the programme it matches. + if not task.cancel_requested: + await history_import.async_import_tail_cuts( + manager.profile_store, base_config, payload.get("cycles") or [], + options, payload.get("segments") or [], + ) + # A candidate that overlaps a cycle WashData already holds is that cycle + # replayed from raw history: show it, but never pre-tick it (audit + # PLAYGROUND-05 - 81% of them slipped past the exact start/duration key, and + # a labelled copy double-weights the real cycle in its envelope). + history_import.mark_already_recorded( + payload.get("segments") or [], + history_import.stored_intervals(manager.profile_store.iter_stored_cycles()), + ) # Split the payload: traces stay server-side, keyed by this task so a reconnect # can still apply them; only the preview rows travel. cycles = payload.pop("cycles", []) @@ -7314,6 +7746,9 @@ async def _history_import_apply_task( target = store.get_backfill_cycles() room = max(0, HISTORY_IMPORT_MAX_TOTAL_CYCLES - len(target)) existing = history_import.existing_dedup_keys(store.iter_stored_cycles()) + # Time overlap, not just the exact key: the same run recorded live and + # replayed from history rarely agrees to the second (PLAYGROUND-05). + intervals = history_import.stored_intervals(store.iter_stored_cycles()) id_pool = {c.get("id") for c in target if isinstance(c, dict)} imported = 0 duplicates = 0 @@ -7322,20 +7757,30 @@ async def _history_import_apply_task( if task.cancel_requested: break raw = cycles[index] - key = history_import.dedup_key(raw.get("start_time"), raw.get("duration")) - if key is not None and key in existing: + duration = history_import.effective_duration(raw) + key = history_import.dedup_key(raw.get("start_time"), duration) + if (key is not None and key in existing) or history_import.overlaps_stored( + raw.get("start_time"), duration, intervals + ): duplicates += 1 reg.update(task, done=done) continue if imported >= room: break + stored = history_import.build_backfill_cycle(raw) store._add_cycle_data( # noqa: SLF001 - the bulk insert primitive - history_import.build_backfill_cycle(raw), + stored, target=target, id_pool=id_pool, ) + if raw.get("tail_cut_s"): + # The banked-tail repair's own trim (PLAYGROUND-07). + store._apply_repaired_duration(stored, float(raw["tail_cut_s"])) # noqa: SLF001 if key is not None: existing.add(key) + interval = history_import.stored_intervals([{**raw, "duration": duration}]) + if interval: + intervals = sorted([*intervals, *interval]) imported += 1 reg.update(task, done=done) if imported: @@ -7395,7 +7840,7 @@ def ws_apply_history_import( for item in msg["accept"][:HISTORY_IMPORT_MAX_SEGMENTS]: try: accept.append(int(item)) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): continue reg = task_registry.get_registry(hass) task = reg.create( diff --git a/custom_components/ha_washdata/ws_schema.py b/custom_components/ha_washdata/ws_schema.py index 68d5fbd7..be0d213c 100644 --- a/custom_components/ha_washdata/ws_schema.py +++ b/custom_components/ha_washdata/ws_schema.py @@ -19,8 +19,8 @@ This module is the **single source of truth** for the shape of every ``ha_washdata/*`` WebSocket command: its request parameters (:data:`WS_COMMANDS`) and its response payload (:data:`WS_RESPONSE_TYPES`, one ``TypedDict`` per -command). It is deliberately dependency-free — it imports nothing from Home -Assistant and nothing from :mod:`ws_api` — so it is safe to import from tooling +command). It is deliberately dependency-free: it imports nothing from Home +Assistant and nothing from :mod:`ws_api`, so it is safe to import from tooling (``devtools/generate_ws_types.py``), from tests, and from ``ws_api`` itself without creating an import cycle. @@ -66,8 +66,22 @@ class OkResponse(TypedDict): # ─── Devices ──────────────────────────────────────────────────────────────────── +class MatchUncertainty(TypedDict): + """The top two of an undecided live match (MATCH-DECIDE-15). Display only. + + ``runner_up`` and ``margin`` are None when only one programme scored. + ``sure_pct`` is ``match_rules.display_sure_pct``: how often the leading guess + was right at that margin on the corpus, rounded to 5. Never a gate. + """ + + top: str + runner_up: str | None + margin: float | None + sure_pct: int + + class DeviceInfo(TypedDict): - """One entry in :class:`GetDevicesResponse` — live state for a device.""" + """One entry in :class:`GetDevicesResponse`: live state for a device.""" entry_id: str perm: str @@ -77,9 +91,15 @@ class DeviceInfo(TypedDict): current_program: str | None time_remaining_s: float | None total_duration_s: float | None + # The matched program's expected length (s): what the phase sensor maps + # progress onto, so the Status phase timeline can use the same span. + expected_duration_s: float | None current_power_w: float | None cycle_progress_pct: float | None envelope_position: float | None + # Set while a cycle runs with no committed program, or with a runner-up within + # the ambiguity margin; None otherwise. The program sensor keeps `detecting...`. + match_uncertainty: MatchUncertainty | None suggestions_count: int suggestion_keys: list[str] feedback_count: int @@ -107,8 +127,12 @@ class GetDeviceCyclesResponse(TypedDict): cycles: list[dict[str, Any]] reference_cycles: list[dict[str, Any]] backfill_cycles: list[dict[str, Any]] + # `total` / `has_more` describe the real (`cycles`) list only. total: int has_more: bool + # Reference + backfill, paged on their own `imported_offset` cursor (item 129a). + imported_total: int + imported_has_more: bool # ─── Settings ────────────────────────────────────────────────────────────────── @@ -134,6 +158,9 @@ class GetProfilesResponse(TypedDict): coverage_gaps: dict[str, Any] profile_advisories: list[dict[str, Any]] profile_terminal: dict[str, Any] + # Profile name -> observed cycles the matcher labelled with it on its own + # (STORE-21); shown on imported program cards. Absent names mean 0. + profile_matcher_counts: dict[str, int] class CreateProfileResponse(TypedDict): @@ -151,7 +178,6 @@ class ProfileGroupInfo(TypedDict): class GetProfileGroupsResponse(TypedDict): groups: list[ProfileGroupInfo] min_cohesion: float - suggestions: list[dict[str, Any]] class GetProfilePhasesResponse(TypedDict): @@ -160,13 +186,61 @@ class GetProfilePhasesResponse(TypedDict): # ─── Maintenance log ─────────────────────────────────────────────────────────── +class MaintenanceTask(TypedDict): + """A user-defined maintenance task (#461). Intervals of 0 are off. + + ``since`` / ``since_cycle_count``: the date and odometer it counts from until + it is first logged (creation, or when it was last switched back on). + """ + + id: str + name: str + cycles: int + days: int + since: str + since_cycle_count: int + + +class MaintenanceStatusRow(TypedDict): + """One active reminder and how far along it is (#461). + + ``name`` is the user's text for a custom task and None for a built-in type + (the panel translates those). ``days_since`` is None without a date to count + from. Due when either interval (0 = off) is reached. + """ + + id: str + custom: bool + name: str | None + cycles_interval: int + days_interval: int + cycles_since: int + days_since: int | None + due: bool + + +class MaintenanceLimits(TypedDict): + tasks_max: int + name_max: int + cycles_max: int + days_max: int + + class GetMaintenanceLogResponse(TypedDict): log: list[dict[str, Any]] - due: Any + due: list[str] event_types: list[str] - reminders: dict[str, Any] + reminders: dict[str, int] cycles_since: dict[str, int] + custom_tasks: list[MaintenanceTask] + status: list[MaintenanceStatusRow] lifetime_cycle_count: int + limits: MaintenanceLimits + + +class MaintenanceTaskResponse(TypedDict): + success: bool + task: MaintenanceTask class SetLifetimeCycleCountResponse(TypedDict): @@ -188,10 +262,13 @@ class GetPhaseCatalogResponse(TypedDict): # ─── Recording ───────────────────────────────────────────────────────────────── -class GetRecordingStateResponse(TypedDict, total=False): +class _GetRecordingStateBase(TypedDict): + state: str + + +class GetRecordingStateResponse(_GetRecordingStateBase, total=False): """``state`` is always present; the remaining keys depend on the state.""" - state: str duration_s: int sample_count: int start_time: str | None @@ -213,6 +290,9 @@ class DismissAllFeedbacksResponse(TypedDict): class GetDiagnosticsResponse(TypedDict): stats: dict[str, Any] + # The last replace import's restore point, ``{created_at, source, counts}``, or + # None when there is nothing to undo (register item 195). + import_undo: dict[str, Any] | None class ClearDebugDataResponse(TypedDict): @@ -237,6 +317,18 @@ class ImportConfigSelectiveResponse(TypedDict): summary: dict[str, Any] +class ImportConfigResponse(TypedDict): + success: bool + # False when the pre-import restore point could not be written (item 195). + restore_point_saved: bool + + +class UndoImportResponse(TypedDict): + success: bool + # ``{restored_from, counts}``: when the restore point was taken, what it held. + summary: dict[str, Any] + + # ─── Shared constants ────────────────────────────────────────────────────────── class GetConstantsResponse(TypedDict): @@ -245,7 +337,6 @@ class GetConstantsResponse(TypedDict): device_types: list[dict[str, Any]] state_colors: dict[str, Any] ml_lab_enabled: bool - ml_suggestions_enabled: bool ml_training_available: bool PROFILE_MIN_WARMUP_CYCLES: Any store_online_available: bool @@ -281,17 +372,20 @@ class RunSuggestionAnalysisResponse(TypedDict, total=False): # ─── Cycle curve / interactive editing ───────────────────────────────────────── -class GetCyclePowerDataResponse(TypedDict, total=False): - """``cycle_id`` / ``samples`` / ``sample_count`` / ``decimated`` / - ``full_duration_s`` are always present; the metadata keys are present only - when the cycle is found. ``sample_count`` is the stored point count and - ``decimated`` is True when ``samples`` was thinned below it (#395).""" - +class _GetCyclePowerDataBase(TypedDict): cycle_id: str samples: list[list[float]] sample_count: int decimated: bool full_duration_s: float + + +class GetCyclePowerDataResponse(_GetCyclePowerDataBase, total=False): + """``cycle_id`` / ``samples`` / ``sample_count`` / ``decimated`` / + ``full_duration_s`` are always present (the required base); the metadata keys + are present only when the cycle is found. ``sample_count`` is the stored point + count and ``decimated`` is True when ``samples`` was thinned below it (#395).""" + start_time: str | None end_time: str | None duration: float | None @@ -314,6 +408,27 @@ class GetCyclePowerDataResponse(TypedDict, total=False): expected: list[list[float]] | None +class GetCycleContextResponse(TypedDict): + """The power sensor's recorder history around a stored cycle (item 513). + + ``before`` / ``after`` are ``[offset_s, watts | null]`` from the cycle's stored + start (``before`` offsets are negative; ``after`` starts at ``trace_end_s``); + a null watt marks an unavailable row. ``reason`` is null when ``available``, + else one of ``not_found`` / ``not_live`` / ``no_sensor`` / ``no_start`` / + ``no_history``. Display only: nothing here is part of the cycle.""" + + cycle_id: str + available: bool + reason: str | None + entity_id: str | None + before_s: float + after_s: float + trace_end_s: float + after_end_s: float + before: list[list[float | None]] + after: list[list[float | None]] + + class AnalyzeSplitResponse(TypedDict): segments: list[list[float]] split_offsets: list[float] @@ -345,14 +460,17 @@ class GetProfileCyclesResponse(TypedDict): # ─── Panel config + RBAC ─────────────────────────────────────────────────────── -class GetPanelConfigResponse(TypedDict, total=False): - """``panel`` / ``is_admin`` / ``user`` / ``prefs`` are always present; - ``rbac`` and ``users`` are added for admins only.""" - +class _GetPanelConfigBase(TypedDict): panel: dict[str, Any] is_admin: bool user: dict[str, Any] prefs: dict[str, Any] + + +class GetPanelConfigResponse(_GetPanelConfigBase, total=False): + """``panel`` / ``is_admin`` / ``user`` / ``prefs`` are always present (the + required base); ``rbac`` and ``users`` are added for admins only.""" + rbac: dict[str, Any] users: list[dict[str, Any]] @@ -367,14 +485,17 @@ class GetMatchDebugResponse(TypedDict): # ─── Live power history ──────────────────────────────────────────────────────── -class GetPowerHistoryResponse(TypedDict, total=False): - """Base keys are always present; ``cycle_start_iso`` only while a cycle runs.""" - +class _GetPowerHistoryBase(TypedDict): cycle_active: bool cycle_elapsed_s: float live: list[list[float]] raw: list[list[float]] restart_gaps: list[Any] + + +class GetPowerHistoryResponse(_GetPowerHistoryBase, total=False): + """Base keys are always present; ``cycle_start_iso`` only while a cycle runs.""" + cycle_start_iso: str @@ -388,17 +509,15 @@ class GetLogsResponse(TypedDict): class GetMlComparisonResponse(TypedDict, total=False): """Two shapes: a disabled report (``enabled=False`` + ``error``) and an - enabled report; both carry ``cycles`` + ``settings_comparison``.""" + enabled report; both carry ``cycles``.""" enabled: bool error: str cycles: list[dict[str, Any]] - settings_comparison: dict[str, Any] cycle_count: int evaluated_count: int model_source: dict[str, Any] profile_stats: dict[str, Any] - ml_suggestions_enabled: bool # ─── On-device ML training ───────────────────────────────────────────────────── @@ -413,84 +532,19 @@ class GetMlTrainingStatusResponse(TypedDict): interval_days: int hour: int on_device_models: dict[str, Any] - matching: dict[str, Any] + # capability -> {ts, promoted, reason_code, reason_params, reason} of the last + # training run; the reason fields only when it promoted nothing (audit ML-20). + last_run: dict[str, Any] # ─── Playground (F3) ─────────────────────────────────────────────────────────── -class RunPlaygroundCycleDetailResponse(TypedDict, total=False): - cycle_id: Any - label: str | None - duration_s: float | None - config_summary: dict[str, Any] - series: list[dict[str, Any]] - events: list[dict[str, Any]] - alerts: list[dict[str, Any]] - outcome: dict[str, Any] - error: str - - -class RunPlaygroundHistoryResponse(TypedDict, total=False): - rows: list[dict[str, Any]] - summary: dict[str, Any] - baseline_rows: list[dict[str, Any]] - baseline_summary: dict[str, Any] - diff: dict[str, list[str]] - - -class RunPlaygroundSweepResponse(TypedDict, total=False): - param: str - objective: str - points: list[dict[str, Any]] - current_value: Any - best_value: Any - best_metric: float | None - param_x: str - param_y: str - x_values: list[float] - y_values: list[float] - grid: list[list[Any]] - best: dict[str, Any] - current: dict[str, Any] - error: str - - -class DtwStage2Scores(TypedDict): - correlation: float - mae_score: float - score: float - - -class DtwScores(TypedDict): - l1_score: float - ddtw_score: float - ensemble_score: float - blend_weight: float - blended_score: float - - -class DtwStage4Scores(TypedDict): - duration_agreement: float - energy_agreement: float - final_score: float - - -class GetDtwDebugResponse(TypedDict): - cycle_id: Any - profile_name: str - grid_n: int - cycle_duration_s: float - profile_duration_s: float - cycle_trace: list[list[float]] - profile_trace: list[list[float]] - stage2: DtwStage2Scores - dtw: DtwScores - stage4: DtwStage4Scores - warp_path: list[list[int]] - - class PlaygroundPreset(TypedDict): - """One saved Playground settings snapshot.""" + """One saved Playground settings snapshot. + + ``values`` holds only keys the Playground offers today; a stored key it no + longer offers is filtered out on read. + """ name: str values: dict[str, Any] @@ -501,10 +555,8 @@ class PlaygroundPreset(TypedDict): class GetPlaygroundSettingsResponse(TypedDict): """Live effective Playground settings + the device's saved presets. - ``classic_suggestions`` and ``ml_suggestions`` are filtered to keys the - Playground exposes so the panel can stage them directly. ``ml_suggestions`` - is ``None`` when ``ENABLE_ML_SUGGESTIONS`` is off - the key is still always - present, so every field here is required (total=True) and + ``classic_suggestions`` is filtered to keys the Playground exposes so the + panel can stage them directly. Every field here is required (total=True) so ``_validate_ws_contract()`` can catch a handler that drops one. """ @@ -513,8 +565,6 @@ class GetPlaygroundSettingsResponse(TypedDict): publishable: list[str] preset_limit: int classic_suggestions: dict[str, Any] - ml_suggestions: dict[str, Any] | None - ml_suggestions_enabled: bool class PlaygroundPresetsResponse(TypedDict): @@ -542,10 +592,6 @@ class TaskSnapshot(TypedDict, total=False): result: Any -class ListTasksResponse(TypedDict): - tasks: list[TaskSnapshot] - - class CancelTaskResponse(TypedDict): cancelled: bool @@ -602,8 +648,13 @@ class StoreStatusResponse(TypedDict, total=False): class StoreItemsResponse(TypedDict, total=False): - """A list of store docs (devices / profiles / cycles).""" + """A list of store docs (devices / profiles / cycles). + + ``error`` (``"store_unreachable"``) means the store could not be read, as opposed + to an empty ``items`` that really has nothing (audit STORE-09). + """ items: list + error: str disabled: bool @@ -634,13 +685,6 @@ class StoreSimpleResponse(TypedDict, total=False): disabled: bool -class StoreQualityResponse(TypedDict, total=False): - """Device 5-star quality summary (count + average).""" - avg: float | None - count: int - disabled: bool - - class StoreConfirmResponse(TypedDict, total=False): """Result of confirming a device (drives community auto-approval).""" confirmed: bool @@ -669,6 +713,7 @@ class StoreDeviceProfilesResponse(TypedDict, total=False): """Resolved store deviceId + its profiles (for the Share dialog picker).""" device_id: str items: list + error: str disabled: bool @@ -702,13 +747,22 @@ class StoreUploadDeviceResponse(TypedDict, total=False): class StoreDownloadDeviceResponse(TypedDict, total=False): - """Result of adopting a whole-device bundle into local reference cycles.""" + """Result of adopting a whole-device bundle into local reference cycles (the + ``store_download`` task's result). ``partial`` + ``failed_profiles``: some + programs' cycles could not be fetched; ``error`` ``"store_unreachable"``: the + store could not be read, never reported as an empty adopt (audit STORE-09). + ``settings`` is the bundle's shared settings, ``settings_applied`` how many + of them were written (only with ``include_settings``).""" profiles_adopted: int cycles_imported: int + cycles_skipped: int phases_applied: int + settings: dict settings_applied: int + cancelled: bool + partial: bool + failed_profiles: int error: str - disabled: bool class GetShareableCyclesResponse(TypedDict, total=False): @@ -754,10 +808,13 @@ WS_RESPONSE_TYPES: dict[str, type] = { "get_maintenance_log": GetMaintenanceLogResponse, "add_maintenance_event": AddMaintenanceEventResponse, "delete_maintenance_event": SuccessResponse, + "add_maintenance_task": MaintenanceTaskResponse, + "update_maintenance_task": MaintenanceTaskResponse, + "delete_maintenance_task": SuccessResponse, "set_lifetime_cycle_count": SetLifetimeCycleCountResponse, "label_cycle": SuccessResponse, "delete_cycle": SuccessResponse, - "auto_label_cycles": SuccessResponse, + "auto_label_cycles": StartTaskResponse, "get_phase_catalog": GetPhaseCatalogResponse, "create_phase": SuccessResponse, "update_phase": SuccessResponse, @@ -775,11 +832,12 @@ WS_RESPONSE_TYPES: dict[str, type] = { "clear_debug_data": ClearDebugDataResponse, "wipe_history": SuccessResponse, "export_config": ExportConfigResponse, - "import_config": SuccessResponse, + "import_config": ImportConfigResponse, "get_export_inventory": GetExportInventoryResponse, "analyze_import": AnalyzeImportResponse, "export_config_selective": ExportConfigResponse, "import_config_selective": ImportConfigSelectiveResponse, + "undo_import": UndoImportResponse, "get_constants": GetConstantsResponse, "get_suggestions": GetSuggestionsResponse, "apply_suggestions": ApplySuggestionsResponse, @@ -787,6 +845,7 @@ WS_RESPONSE_TYPES: dict[str, type] = { "set_suggestion_lock": SetSuggestionLockResponse, "run_suggestion_analysis": RunSuggestionAnalysisResponse, "get_cycle_power_data": GetCyclePowerDataResponse, + "get_cycle_context": GetCycleContextResponse, "trim_cycle": StartTaskResponse, "analyze_split": AnalyzeSplitResponse, "apply_split": StartTaskResponse, @@ -803,20 +862,14 @@ WS_RESPONSE_TYPES: dict[str, type] = { "get_ml_comparison": GetMlComparisonResponse, "get_ml_training_status": GetMlTrainingStatusResponse, "trigger_ml_training": StartTaskResponse, - "revert_matching_config": SuccessResponse, "revert_ml_models": SuccessResponse, "set_ml_review": SuccessResponse, "pause_cycle": OkResponse, "resume_cycle": OkResponse, "terminate_cycle": OkResponse, - "run_playground_cycle_detail": RunPlaygroundCycleDetailResponse, - "run_playground_history": RunPlaygroundHistoryResponse, - "run_playground_sweep": RunPlaygroundSweepResponse, - "get_dtw_debug": GetDtwDebugResponse, "get_playground_settings": GetPlaygroundSettingsResponse, "save_playground_preset": PlaygroundPresetsResponse, "delete_playground_preset": PlaygroundPresetsResponse, - "list_tasks": ListTasksResponse, "subscribe_tasks": SubscribeTasksResponse, "cancel_task": CancelTaskResponse, "get_task_result": TaskSnapshot, @@ -834,32 +887,33 @@ WS_RESPONSE_TYPES: dict[str, type] = { "store_search_devices": StoreItemsResponse, "store_get_profiles": StoreItemsResponse, "store_get_cycles": StoreItemsResponse, + "store_confirm_device": StoreConfirmResponse, + "store_rate_device": StoreOnlineResponse, "store_import_cycle": StoreImportResponse, "store_upload_cycle": StoreUploadResponse, "store_list_brands": StoreItemsResponse, - "store_get_device_quality": StoreQualityResponse, - "store_confirm_device": StoreConfirmResponse, - "store_rate_device": StoreOnlineResponse, "store_set_online": StoreOnlineResponse, "store_set_prefs": StorePrefsResponse, "store_get_device_profiles": StoreDeviceProfilesResponse, "store_get_catalog_entry": StoreCatalogEntryResponse, "store_refresh_catalog": StoreRefreshCatalogResponse, "store_upload_device": StoreUploadDeviceResponse, - "store_download_device": StoreDownloadDeviceResponse, + "store_download_device": StartTaskResponse, "get_shareable_cycles": GetShareableCyclesResponse, } +#: Map a task-registry ``kind`` -> the TypedDict of its finished ``result`` +#: (``TaskSnapshot.result`` from ``get_task_result`` / ``subscribe_tasks``). Only +#: kinds whose result the panel reads by key are listed; the rest stay ``Any``. +WS_TASK_RESULT_TYPES: dict[str, type] = { + "store_download": StoreDownloadDeviceResponse, +} + #: Commands whose response splats an upstream summary dict and therefore has an #: open-ended top-level key set. The debug validator still checks required keys #: for these but does not flag extra keys. WS_OPEN_RESPONSES: frozenset[str] = frozenset({ "run_suggestion_analysis", - # Playground what-if responses carry nested/variant shapes (incl. an error - # variant); skip strict extra-key validation. - "run_playground_cycle_detail", - "run_playground_history", - "run_playground_sweep", # Task snapshot splats a variant key set (result present only when finished). "get_task_result", }) @@ -895,6 +949,7 @@ WS_COMMANDS: dict[str, dict] = { _entry(), _p("limit", "int", False), _p("offset", "int", False), + _p("imported_offset", "int", False), ]}, "get_options": {"params": [_entry()]}, "set_options": {"params": [_entry(), _p("options", "dict")]}, @@ -945,6 +1000,20 @@ WS_COMMANDS: dict[str, dict] = { _p("notes", "str", False), ]}, "delete_maintenance_event": {"params": [_entry(), _p("event_id", "str")]}, + "add_maintenance_task": {"params": [ + _entry(), + _p("name", "str"), + _p("cycles", "int", False), + _p("days", "int", False), + ]}, + "update_maintenance_task": {"params": [ + _entry(), + _p("task_id", "str"), + _p("name", "str", False), + _p("cycles", "int", False), + _p("days", "int", False), + ]}, + "delete_maintenance_task": {"params": [_entry(), _p("task_id", "str")]}, "set_lifetime_cycle_count": {"params": [_entry(), _p("count", "int")]}, "label_cycle": {"params": [ _entry(), @@ -1009,6 +1078,7 @@ WS_COMMANDS: dict[str, dict] = { _p("cycle_destination", "str", False, enum=["reference", "real_history"]), _p("apply_settings", "bool", False), ]}, + "undo_import": {"params": [_entry()]}, "get_constants": {"params": []}, "get_suggestions": {"params": [_entry()]}, "apply_suggestions": {"params": [_entry(), _p("keys", "list[str]")]}, @@ -1016,6 +1086,12 @@ WS_COMMANDS: dict[str, dict] = { "set_suggestion_lock": {"params": [_entry(), _p("key", "str"), _p("locked", "bool")]}, "run_suggestion_analysis": {"params": [_entry()]}, "get_cycle_power_data": {"params": [_entry(), _p("cycle_id", "str")]}, + "get_cycle_context": {"params": [ + _entry(), + _p("cycle_id", "str"), + _p("before_s", "float", False), + _p("after_s", "float", False), + ]}, "trim_cycle": {"params": [ _entry(), _p("cycle_id", "str"), @@ -1061,7 +1137,6 @@ WS_COMMANDS: dict[str, dict] = { "get_ml_comparison": {"params": [_entry()]}, "get_ml_training_status": {"params": [_entry()]}, "trigger_ml_training": {"params": [_entry()]}, - "revert_matching_config": {"params": [_entry()]}, "revert_ml_models": {"params": [_entry()]}, "set_ml_review": {"params": [ _entry(), @@ -1074,32 +1149,6 @@ WS_COMMANDS: dict[str, dict] = { "pause_cycle": {"params": [_entry()]}, "resume_cycle": {"params": [_entry()]}, "terminate_cycle": {"params": [_entry()]}, - "run_playground_cycle_detail": {"params": [ - _entry(), - _p("cycle_id", "str"), - _p("settings_override", "dict", False), - ]}, - "run_playground_history": {"params": [ - _entry(), - _p("cycle_ids", "list[str]", False), - _p("settings_override", "dict", False), - _p("concurrency", "int", False), - ]}, - "run_playground_sweep": {"params": [ - _entry(), - _p("param", "str"), - _p("values", "list[float]"), - _p("objective", "str"), - _p("cycle_ids", "list[str]", False), - _p("concurrency", "int", False), - _p("param_y", "str", False), - _p("values_y", "list[float]", False), - ]}, - "get_dtw_debug": {"params": [ - _entry(), - _p("cycle_id", "str"), - _p("profile_name", "str|null", False), - ]}, "get_playground_settings": {"params": [ _entry(), _p("include_suggestions", "bool", False), @@ -1110,7 +1159,6 @@ WS_COMMANDS: dict[str, dict] = { _p("values", "dict"), ]}, "delete_playground_preset": {"params": [_entry(), _p("name", "str")]}, - "list_tasks": {"params": [_p("entry_id", "str|null", False)]}, "subscribe_tasks": {"params": [_p("entry_id", "str|null", False)]}, "cancel_task": {"params": [_p("task_id", "str")]}, "get_task_result": {"params": [_p("task_id", "str")]}, @@ -1118,21 +1166,20 @@ WS_COMMANDS: dict[str, dict] = { _entry(), _p("cycle_ids", "list[str]", False), _p("settings_override", "dict", False), + _p("count", "int", False), ]}, "start_playground_sweep": {"params": [ _entry(), _p("param", "str"), _p("values", "list[float]"), _p("objective", "str"), - _p("param_y", "str|null", False), - _p("values_y", "list[float]", False), + _p("cycle_ids", "list[str]", False), + _p("count", "int", False), ]}, "start_playground_cycle_detail": {"params": [ _entry(), _p("cycle_id", "str"), _p("settings_override", "dict", False), - _p("stress_tail", "bool", False), - _p("stress_idle_w", "float|null", False), ]}, "history_import_begin": {"params": [_entry()]}, "history_import_chunk": {"params": [ @@ -1169,12 +1216,11 @@ WS_COMMANDS: dict[str, dict] = { ]}, "store_get_profiles": {"params": [_entry(), _p("device_id", "str")]}, "store_get_cycles": {"params": [_entry(), _p("profile_id", "str")]}, - "store_get_device_quality": {"params": [_entry(), _p("device_id", "str")]}, + "store_confirm_device": {"params": [_entry(), _p("device_id", "str")]}, + "store_rate_device": {"params": [_entry(), _p("device_id", "str"), _p("rating", "int")]}, "store_get_device_profiles": {"params": [_entry(), _p("brand", "str"), _p("model", "str"), _p("appliance_type", "str")]}, "store_get_catalog_entry": {"params": [_entry(), _p("brand", "str"), _p("model", "str"), _p("appliance_type", "str")]}, "store_refresh_catalog": {"params": [_entry()]}, - "store_confirm_device": {"params": [_entry(), _p("device_id", "str")]}, - "store_rate_device": {"params": [_entry(), _p("device_id", "str"), _p("rating", "int")]}, "store_set_online": {"params": [_entry(), _p("enabled", "bool")]}, "store_set_prefs": {"params": [_entry(), _p("prefs", "dict")]}, "store_import_cycle": {"params": [ @@ -1202,6 +1248,7 @@ def full_command_name(command: str) -> str: __all__ = [ "WS_COMMANDS", "WS_RESPONSE_TYPES", + "WS_TASK_RESULT_TYPES", "WS_OPEN_RESPONSES", "WS_PREFIX", "full_command_name", diff --git a/custom_components/ha_washdata/www/build-manifest.json b/custom_components/ha_washdata/www/build-manifest.json index acd5e477..e8c6ccf8 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": "4d89abc002319d37b42a2644375516a515ffe0607e663326d4ad22f72efd8660", - "artifact_sha256": "6034a4a223c7192064736c11d184317c9885dad819bf74ca0306358673844e30", - "source_bytes": 948888, - "artifact_bytes": 607586 + "source_sha256": "566fd32ab6d109ce5558496840b9cf9987d62130a06df2dc84fe601d2e6be009", + "artifact_sha256": "4ccb426ed8a7371e5488bbcf8cdc1dcc4465f8a8cfa99dc2c3e227270a5ac842", + "source_bytes": 990144, + "artifact_bytes": 620530 }, "ha-washdata-card.js": { "artifact": "ha-washdata-card.min.js", - "source_sha256": "8782b8429957cfdd344efa36ed5fe4334818dbf199b48245f14eba02f1243b0b", - "artifact_sha256": "d6a42777e7102610983ff27638e9b4640cfaa7c797c668532e31c1afaf1a5d70", - "source_bytes": 56140, - "artifact_bytes": 30343 + "source_sha256": "688e7370be6ccb260046c25884eb33ef7653dd97e0972fe7fd4856ffbbcc45ce", + "artifact_sha256": "5ca5a6ae8a8540b9aac5797eb9804f3b8f54e7106cad3adf92f71cd8c771697f", + "source_bytes": 59434, + "artifact_bytes": 31553 } } } diff --git a/custom_components/ha_washdata/www/ha-washdata-card.js b/custom_components/ha_washdata/www/ha-washdata-card.js index c03ce604..6e36a7ab 100644 --- a/custom_components/ha_washdata/www/ha-washdata-card.js +++ b/custom_components/ha_washdata/www/ha-washdata-card.js @@ -53,7 +53,7 @@ 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"]; +const ACTIVE_STATES = ["running", "paused", "user_paused", "ending", "starting", "anti_wrinkle"]; // 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 @@ -82,8 +82,16 @@ function loadConstants(hass) { const _panelTrans = {}; // lang -> dict const _panelTransPending = {}; // lang -> Promise +// The card is loaded as ha-washdata-card.js?v=; reuse it for the +// translation files, as the panel does. Without it HA serves them with a 31-day +// cache, so an upgraded card kept last release's strings (audit UI-17). +const _CARD_VERSION = (() => { + try { return new URL(import.meta.url).searchParams.get("v") || ""; } + catch (_) { return ""; } +})(); function _panelTransUrl(lang) { - return "/" + DOMAIN + "/panel-translations/" + encodeURIComponent(lang) + ".json"; + const base = "/" + DOMAIN + "/panel-translations/" + encodeURIComponent(lang) + ".json"; + return _CARD_VERSION ? base + "?v=" + encodeURIComponent(_CARD_VERSION) : base; } async function _fetchPanelLang(lang) { if (!lang) return null; @@ -191,6 +199,8 @@ class WashDataCard extends HTMLElement { f.showSparkline ? "spark" : "", f.buttons.join("+"), layout === "glance" ? String(this._glanceEntities().length) : "", + // _attachGestures makes the card focusable only when it has a tap action. + c.tap_action && c.tap_action.action === "none" ? "notap" : "", ].join("|"); } @@ -292,7 +302,26 @@ class WashDataCard extends HTMLElement { } // ── Entity discovery ────────────────────────────────────────────────────── + // Memoised per registry object: HA replaces `hass.entities` only when the entity + // registry changes, but calls `set hass` on every state change in the house, and + // each call walked the whole registry once per card (audit UI-16). Results from + // the suffix fallback read live states, so only registry-resolved roles are kept. _resolveRoles(primaryEntity) { + const reg = (this._hass && this._hass.entities) || null; + const key = primaryEntity || (this._cfg && this._cfg.entity) || ""; + let c = this._rolesCache; + if (!c || c.reg !== reg || c.cfg !== this._cfg) { + c = this._rolesCache = { reg, cfg: this._cfg, map: new Map() }; + } + if (c.map.has(key)) return c.map.get(key); + const roles = this._computeRoles(primaryEntity); + // A role the suffix fallback had to look for in live states may appear later + // without a registry change, so such a result is recomputed, never cached. + if (reg && roles._deviceId && !roles._fromStates) c.map.set(key, roles); + return roles; + } + + _computeRoles(primaryEntity) { const hass = this._hass; const cfg = this._cfg || {}; const reg = (hass && hass.entities) || {}; @@ -333,9 +362,11 @@ class WashDataCard extends HTMLElement { // Suffix fallback for the common case where the primary is the state sensor // but the registry lookup came up empty (e.g. template entity, no device). + let fromStates = false; if (!roles.time || !roles.progress || !roles.program) { const m = /^sensor\.(.+)_state$/.exec(primary); if (m) { + fromStates = true; const base = "sensor." + m[1] + "_"; const st = hass && hass.states; const guess = (suffix) => (st && st[base + suffix] ? base + suffix : null); @@ -348,6 +379,7 @@ class WashDataCard extends HTMLElement { roles._buttons = buttons; roles._select = selectEntity; roles._deviceId = deviceId; + roles._fromStates = fromStates; return roles; } @@ -580,6 +612,7 @@ class WashDataCard extends HTMLElement { _baseStyle() { return ( ":host{display:block;height:100%}" + + "[role=button]:focus-visible{outline:2px solid var(--primary-color);outline-offset:2px}" + "ha-card{padding:0;background:var(--ha-card-background,var(--card-background-color,white));" + "border-radius:var(--ha-card-border-radius,12px);box-shadow:var(--ha-card-box-shadow,none);" + "overflow:hidden;cursor:pointer;height:100%;box-sizing:border-box;" + @@ -611,7 +644,21 @@ class WashDataCard extends HTMLElement { return this._buildTile(); } - _attachGestures(cardEl) { + _attachGestures(cardEl, keyEl = cardEl) { + // Keyboard: focusable with Enter / Space running the tap action, as HA's own + // tile card does (audit UI-17: pointer-only until 0.5.8). `keyEl` carries the + // button role: a card holding its own buttons or program selector passes a + // part without them, since a role="button" may not contain interactive controls. + const tapCfg = (this._cfg && this._cfg.tap_action) || { action: "more-info" }; + if (tapCfg.action !== "none") { + keyEl.tabIndex = 0; + keyEl.setAttribute("role", "button"); + keyEl.addEventListener("keydown", (ev) => { + if (ev.target !== keyEl || (ev.key !== "Enter" && ev.key !== " ")) return; + ev.preventDefault(); + this._executeAction((this._cfg && this._cfg.tap_action) || { action: "more-info" }); + }); + } cardEl.addEventListener("pointerdown", this._onPointerDown); cardEl.addEventListener("pointermove", this._onPointerMove); cardEl.addEventListener("pointerup", this._onPointerUp); @@ -668,7 +715,8 @@ class WashDataCard extends HTMLElement { (flags.showSparkline ? '' : "") + (flags.buttons.length ? '
' : "") + "
"; - this._attachGestures(this.shadowRoot.getElementById("card")); + const card = this.shadowRoot.getElementById("card"); + this._attachGestures(card, flags.buttons.length ? card.querySelector(".top") : card); if (flags.buttons.length) this._buildButtons(this.shadowRoot.getElementById("acts"), flags.buttons); } @@ -682,6 +730,9 @@ class WashDataCard extends HTMLElement { sel.id = "prog-select"; sel.addEventListener("click", (ev) => ev.stopPropagation()); sel.addEventListener("pointerdown", (ev) => ev.stopPropagation()); + // As the buttons below: a pointerup reaching the card ran tap_action, whose + // more-info dialog closed the dropdown before a program could be picked (#468). + sel.addEventListener("pointerup", (ev) => ev.stopPropagation()); sel.addEventListener("change", (ev) => { ev.stopPropagation(); this._onProgramChange(ev.target.value); @@ -812,7 +863,12 @@ class WashDataCard extends HTMLElement { else if (vm.timeText) parts.push(vm.timeText); else if (vm.pct !== null) parts.push(vm.pct + "%"); } - if (flags.showAnomaly && vm.anomaly) parts.push(this._t("card.running_long", null, "running long")); + if (flags.showAnomaly && vm.anomaly) { + // #452: a stalled cycle (halted on its standby draw) is not one running long. + parts.push(vm.anomaly.kind === "stalled" + ? this._t("card.stalled", null, "Stalled") + : this._t("card.running_long", null, "running long")); + } stateEl.textContent = parts.join(" • "); } @@ -872,7 +928,9 @@ class WashDataCard extends HTMLElement { // and a zero; above it the decimal is noise. if (vm.isRunning && vm.powerW !== null) addChip((vm.powerW >= 100 ? Math.round(vm.powerW) : vm.powerW.toFixed(1)) + " W"); - if (flags.showAnomaly && vm.anomaly) { + if (flags.showAnomaly && vm.anomaly && vm.anomaly.kind === "stalled") { + addChip(this._t("card.stalled", null, "Stalled"), true); // #452 + } else if (flags.showAnomaly && vm.anomaly) { const ratio = vm.anomaly.ratio ? " (" + Math.round((vm.anomaly.ratio - 1) * 100) + "%)" : ""; addChip(this._t("card.running_long", null, "Running long") + ratio, true); } diff --git a/custom_components/ha_washdata/www/ha-washdata-card.min.js b/custom_components/ha_washdata/www/ha-washdata-card.min.js index f0e5daa6..8e452bc0 100644 --- a/custom_components/ha_washdata/www/ha-washdata-card.min.js +++ b/custom_components/ha_washdata/www/ha-washdata-card.min.js @@ -8,4 +8,4 @@ * This program comes with ABSOLUTELY NO WARRANTY. See the GNU Affero General * Public License for details: . */ -const b="ha-washdata-card",v="ha-washdata-card-editor",E="ha_washdata",$=500,K=250,X=10,j={state:"washer_state",program:"washer_program",time:"time_remaining",progress:"cycle_progress",power:"current_power",energy:"energy_total",phase:"current_phase"},N={pause:"pause_cycle",resume:"resume_cycle",terminate:"force_end_cycle",record_start:"record_start",record_stop:"record_stop"},W={pause:"mdi:pause",resume:"mdi:play",terminate:"mdi:stop",record_start:"mdi:record-circle-outline",record_stop:"mdi:stop-circle-outline",open_panel:"mdi:open-in-new"},D=["pause","resume","terminate","record_start","record_stop","program","open_panel"],H=["running","paused","user_paused","ending","starting","anti_wrinkle","rinse"],z=["running","starting","paused","ending"],T=["off","unknown","unavailable","idle"],U=60,R=new WeakMap;function F(d){const t=d&&d.connection;if(!t)return Promise.resolve({stateColors:{}});if(R.has(t))return R.get(t);const e=d.callWS({type:E+"/get_constants"}).then(i=>({stateColors:i&&i.state_colors||{}})).catch(()=>({stateColors:{}}));return R.set(t,e),e}const y={},A={};function G(d){return"/"+E+"/panel-translations/"+encodeURIComponent(d)+".json"}async function q(d){if(!d)return null;const t=[d],e=d.indexOf("-");e>0&&t.push(d.slice(0,e));for(const i of t)try{const s=await fetch(G(i));if(s.ok){const n=await s.json();if(n&&typeof n=="object")return n}}catch{}return null}async function S(d){if(!d||y[d])return;if(A[d]){await A[d];return}const t=q(d).then(e=>{e&&(y[d]=e)});A[d]=t,await t}class V extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}),this._cfg=null,this._hass=null,this._builtSig=null,this._constantsLoaded=!1,this._langEnsured="",this._constants=null,this._spark=[],this._sparkState=null,this._holdTimer=null,this._holdTriggered=!1,this._tapTimer=null,this._lastTapTime=0,this._pointerStart=null,this._pointerCanceled=!1,this._onPointerDown=this._onPointerDown.bind(this),this._onPointerMove=this._onPointerMove.bind(this),this._onPointerUp=this._onPointerUp.bind(this),this._onPointerCancel=this._onPointerCancel.bind(this)}static getStubConfig(){return{entity:"sensor.washing_machine_state",layout:"tile",tap_action:{action:"more-info"},hold_action:{action:"none"},double_tap_action:{action:"none"}}}static getConfigElement(){return document.createElement(v)}setConfig(t){if(!t)throw new Error("Invalid configuration");const e=t.layout||"tile";if(e!=="glance"&&!t.entity)throw new Error("Please define an entity");if(e==="glance"&&!(Array.isArray(t.entities)&&t.entities.length)&&!t.entity)throw new Error("Please define entities for the glance layout");this._cfg={...t,layout:e},this._builtSig!==this._structureSig()&&(this._builtSig=null,this.shadowRoot&&(this.shadowRoot.innerHTML="")),this._render()}_structureSig(){const t=this._cfg||{},e=this._flags(),i=t.layout||"tile";return[i,e.showBar?"bar":"",e.showSparkline?"spark":"",e.buttons.join("+"),i==="glance"?String(this._glanceEntities().length):""].join("|")}set hass(t){this._hass=t,this._ensureResources(),this._render()}_detailRows(){const t=this._flags();let e=2;return t.buttons.length&&(e+=1),t.showSparkline&&(e+=1),e}getCardSize(){const t=this._cfg&&this._cfg.layout;return t==="detail"?this._detailRows():t==="glance"&&this._glanceEntities().length||1}getGridOptions(){const t=this._cfg&&this._cfg.layout;if(t==="detail"){const e=this._detailRows();return{rows:e,min_rows:e,columns:12,min_columns:6}}return t==="glance"?{rows:Math.max(1,this._glanceEntities().length),min_rows:1,columns:12,min_columns:6}:{rows:1,min_rows:1,columns:6,min_columns:3}}disconnectedCallback(){this._clearHoldTimer(),this._tapTimer&&(window.clearTimeout(this._tapTimer),this._tapTimer=null)}_lang(){const t=this._hass&&this._hass.locale&&this._hass.locale.language||this._hass&&this._hass.language||"en";return typeof t=="string"&&t?t:"en"}_tLookup(t,e){const i=y[e];if(!i)return null;const s=t.split(".").reduce((n,o)=>n&&n[o]!==void 0?n[o]:null,i);return s&&typeof s=="string"?s:null}_t(t,e,i){const s=this._lang();let n=s&&this._tLookup(t,s)||this._tLookup(t,"en")||i||t;if(e)for(const[o,a]of Object.entries(e))n=n.replace(new RegExp("\\{"+o+"\\}","g"),String(a));return n}_ensureResources(){if(!this._hass)return;const t=this._lang(),e=!this._constantsLoaded,i=t!==this._langEnsured;if(!e&&!i)return;const s=[];e&&(this._constantsLoaded=!0,s.push(F(this._hass).then(n=>{this._constants=n}),S("en"))),i&&(this._langEnsured=t,t&&t!=="en"&&s.push(S(t))),Promise.all(s).then(()=>this._update()).catch(()=>{})}_resolveRoles(t){const e=this._hass,i=this._cfg||{},s=e&&e.entities||{},n=t||i.entity,o=s[n],a=i.device_id||o&&o.device_id||null,r={state:n},l={};let h=null;if(a)for(const[c,u]of Object.entries(s)){if(!u||u.device_id!==a||u.platform&&u.platform!==E)continue;const p=u.translation_key;if(c.startsWith("sensor."))for(const[f,g]of Object.entries(j))p===g&&!r[f]&&(r[f]=c);for(const[f,g]of Object.entries(N))p===g&&!l[f]&&(l[f]=c);!h&&c.startsWith("select.")&&(h=c)}if(i.program_entity&&(r.program=i.program_entity),i.time_entity&&(r.time=i.time_entity),i.pct_entity&&(r.progress=i.pct_entity),i.power_entity&&(r.power=i.power_entity),i.energy_entity&&(r.energy=i.energy_entity),i.phase_entity&&(r.phase=i.phase_entity),!r.time||!r.progress||!r.program){const c=/^sensor\.(.+)_state$/.exec(n);if(c){const u="sensor."+c[1]+"_",p=e&&e.states,f=g=>p&&p[u+g]?u+g:null;r.time=r.time||f("time_remaining"),r.progress=r.progress||f("cycle_progress"),r.program=r.program||f("program")}}return r._buttons=l,r._select=h,r._deviceId=a,r}_fmtState(t){if(!t)return"";const e=this._hass;if(e&&typeof e.formatEntityState=="function")try{return e.formatEntityState(t)}catch{}const i=t.state||"";return i&&i.charAt(0).toUpperCase()+i.slice(1)}_vm(t){const e=this._hass,i=this._cfg,s=e.states,n=s[t.state];if(!n)return{missing:!0,missingEntity:t.state};const o=String(n.state||"").toLowerCase(),a=T.includes(o),r=H.includes(o),l=n.attributes||{};let h="";const c=t.program?s[t.program]:null;if(c){const _=String(c.state||"").toLowerCase();["unknown","none","off","unavailable",""].includes(_)||(h=this._fmtState(c))}if(!h&&l.current_program_guess){const _=String(l.current_program_guess).toLowerCase();["unknown","none","off","unavailable"].includes(_)||(h=l.current_program_guess)}let u="";const p=t.phase?s[t.phase]:null;if(p){const _=String(p.state||"").toLowerCase();["unknown","none","off","unavailable",""].includes(_)||(u=this._fmtState(p))}!u&&c&&c.attributes&&c.attributes.active_phase&&(u=c.attributes.active_phase);let f="";if(o==="running"&&l.sub_state){const _=String(l.sub_state).match(/Running \((.*)\)/);f=_&&_[1]?_[1]:l.sub_state}let g=null;const m=t.progress?s[t.progress]:null;m&&!isNaN(parseFloat(m.state))&&(g=Math.max(0,Math.min(100,Math.round(parseFloat(m.state)))));let x="";const w=t.time?s[t.time]:null;w&&!T.includes(String(w.state).toLowerCase())&&(e&&typeof e.formatEntityState=="function"?x=this._fmtState(w):isNaN(parseFloat(w.state))?x=w.state:x=w.state+" "+this._t("card.min",null,"min"));let L="",P="";if(m&&m.attributes){const _=m.attributes.projected_energy_kwh;_!=null&&(L=_+" kWh");const k=m.attributes.projected_cost;k!=null&&(P=this._fmtCost(k))}let I=null;const C=t.power?s[t.power]:null;C&&!isNaN(parseFloat(C.state))&&(I=parseFloat(C.state));let B=null;l.cycle_anomaly&&l.cycle_anomaly!=="none"&&(B={kind:l.cycle_anomaly,ratio:l.overrun_ratio});const M=this._stateColor(o,r,i.active_color);return{missing:!1,sk:o,stateObj:n,isInactive:a,isActive:r,isRunning:o==="running",stateLabel:this._fmtState(n),color:M,program:h,phase:u,subState:f,pct:g,timeText:x,energyText:L,costText:P,powerW:I,anomaly:B,buttons:t._buttons||{},select:t._select||null}}_fmtCost(t){const e=this._hass,i=e&&e.config&&e.config.currency;if(i)try{return new Intl.NumberFormat(this._lang(),{style:"currency",currency:i}).format(Number(t))}catch{}return String(t)}_stateColor(t,e,i){if(e&&i){if(Array.isArray(i)){const[o,a,r]=i;return{fg:"rgb("+o+","+a+","+r+")",bg:"rgba("+o+","+a+","+r+",0.2)"}}return{fg:i,bg:"rgba(128,128,128,0.15)"}}const n=(this._constants&&this._constants.stateColors||{})[t];return n?{fg:n,bg:this._alpha(n,t)}:T.includes(t)?{fg:"var(--disabled-text-color, grey)",bg:"rgba(128,128,128,0.1)"}:{fg:"var(--primary-color)",bg:"rgba(var(--rgb-primary-color, 33,150,243),0.2)"}}_alpha(t,e){const i=/#([0-9a-fA-F]{6})/.exec(t);if(i){const s=parseInt(i[1],16),n=s>>16&255,o=s>>8&255,a=s&255;return"rgba("+n+","+o+","+a+",0.18)"}return T.includes(e)?"rgba(128,128,128,0.1)":"rgba(128,128,128,0.15)"}_deviceTypeIcon(){const t=this._hass,e=this._cfg;if(e.icon)return e.icon;const i=t&&t.states[e.entity];return i&&i.attributes&&i.attributes.icon?i.attributes.icon:"mdi:washing-machine"}_titleText(){const t=this._cfg;if(t.title)return t.title;const e=this._hass,i=e&&e.entities||{},s=e&&e.devices||{},n=i[t.entity];if(n&&n.device_id&&s[n.device_id]){const a=s[n.device_id],r=a.name_by_user||a.name;if(r)return r}const o=e&&e.states[t.entity];return o&&o.attributes&&o.attributes.friendly_name?String(o.attributes.friendly_name).replace(/ (State|Status)$/i,""):"WashData"}_flags(){const t=this._cfg||{};return{showState:t.show_state!==!1,showProgram:t.show_program!==!1,showDetails:t.show_details!==!1,showBar:t.show_progress_bar!==!1,showPhase:t.show_phase!==!1,showEnergy:t.show_energy!==!1,showAnomaly:t.show_anomaly!==!1,showSparkline:!!t.show_sparkline,displayMode:t.display_mode||"time",buttons:Array.isArray(t.buttons)?t.buttons.filter(e=>D.includes(e)):[]}}_render(){if(!this.shadowRoot||!this._cfg)return;const t=this._structureSig();this._builtSig!==t&&(this._build(this._cfg.layout||"tile"),this._builtSig=t),this._update()}_baseStyle(){return":host{display:block;height:100%}ha-card{padding:0;background:var(--ha-card-background,var(--card-background-color,white));border-radius:var(--ha-card-border-radius,12px);box-shadow:var(--ha-card-box-shadow,none);overflow:hidden;cursor:pointer;height:100%;box-sizing:border-box;border:var(--ha-card-border-width,1px) solid var(--ha-card-border-color,var(--divider-color))}.icon-container{border-radius:12px;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background-color .3s,color .3s;position:relative}ha-icon{--mdc-icon-size:24px}.primary{font-weight:500;color:var(--primary-text-color);white-space:nowrap;text-overflow:ellipsis;overflow:hidden;line-height:1.2}.secondary{color:var(--secondary-text-color);white-space:nowrap;text-overflow:ellipsis;overflow:hidden;line-height:1.2}.bar{height:4px;border-radius:2px;background:var(--divider-color,rgba(128,128,128,.25));overflow:hidden}.bar>i{display:block;height:100%;width:0;border-radius:2px;transition:width .5s ease,background-color .3s}.chip{display:inline-flex;align-items:center;gap:4px;font-size:11px;padding:1px 8px;border-radius:10px;background:rgba(128,128,128,.14);color:var(--secondary-text-color);white-space:nowrap}.chip.warn{background:rgba(255,152,0,.16);color:var(--warning-color,#ff9800)}.acts{display:flex;gap:4px;align-items:center}.wd-act{border:none;background:rgba(128,128,128,.12);color:var(--primary-text-color);border-radius:10px;width:34px;height:34px;display:flex;align-items:center;justify-content:center;cursor:pointer;padding:0}.wd-act:hover{background:rgba(128,128,128,.22)}.wd-act[disabled]{opacity:.35;pointer-events:none}.wd-act ha-icon{--mdc-icon-size:20px}select.wd-prog{max-width:150px;font:inherit;color:var(--primary-text-color);background:var(--secondary-background-color);border:1px solid var(--divider-color);border-radius:8px;padding:4px 6px}"}_build(t){return t==="detail"?this._buildDetail():t==="glance"?this._buildGlance():this._buildTile()}_attachGestures(t){t.addEventListener("pointerdown",this._onPointerDown),t.addEventListener("pointermove",this._onPointerMove),t.addEventListener("pointerup",this._onPointerUp),t.addEventListener("pointercancel",this._onPointerCancel),t.addEventListener("pointerleave",this._onPointerCancel)}_buildTile(){const t=this._flags(),e=this._baseStyle()+".tile{display:flex;flex-direction:row;align-items:center;padding:8px 12px;gap:12px;width:100%;box-sizing:border-box}.tile .icon-container{width:40px;height:40px}.tile .info{display:flex;flex-direction:column;justify-content:center;overflow:hidden;flex:1;min-width:0}.tile .primary{font-size:14px}.tile .secondary{font-size:12px;margin-top:2px}.tile .bar{margin-top:6px}";this.shadowRoot.innerHTML="
'+(t.showBar?'
':"")+"
",this._attachGestures(this.shadowRoot.getElementById("card"))}_buildDetail(){const t=this._flags(),e=this._baseStyle()+"ha-card.wd-detail{height:auto}.detail{display:flex;flex-direction:column;gap:8px;padding:12px 14px;width:100%;box-sizing:border-box}.detail .top{display:flex;align-items:center;gap:12px}.detail .icon-container{width:48px;height:48px}.detail .icon-container ha-icon{--mdc-icon-size:30px}.detail .info{flex:1;min-width:0}.detail .primary{font-size:16px}.detail .secondary{font-size:13px;margin-top:2px}.detail .eta{text-align:right;flex-shrink:0}.detail .eta .big{font-size:22px;font-weight:600;color:var(--primary-text-color);line-height:1.1}.detail .eta .lbl{font-size:11px;color:var(--secondary-text-color);text-transform:uppercase;letter-spacing:.04em}.detail .bar{height:6px}.detail .meta{display:flex;flex-wrap:wrap;gap:6px;align-items:center}.detail .meta:empty{display:none}.detail canvas{width:100%;height:34px;display:block}";this.shadowRoot.innerHTML="
'+(t.showBar?'
':"")+'
'+(t.showSparkline?'':"")+(t.buttons.length?'
':"")+"
",this._attachGestures(this.shadowRoot.getElementById("card")),t.buttons.length&&this._buildButtons(this.shadowRoot.getElementById("acts"),t.buttons)}_buildButtons(t,e){if(t){t.innerHTML="";for(const i of e){if(i==="program"){const o=document.createElement("select");o.className="wd-prog",o.id="prog-select",o.addEventListener("click",a=>a.stopPropagation()),o.addEventListener("pointerdown",a=>a.stopPropagation()),o.addEventListener("change",a=>{a.stopPropagation(),this._onProgramChange(a.target.value)}),t.appendChild(o);continue}const s=document.createElement("button");s.className="wd-act",s.dataset.btn=i,s.title=this._t("card.btn."+i,null,this._defaultBtnLabel(i)),s.addEventListener("pointerdown",o=>o.stopPropagation()),s.addEventListener("pointerup",o=>o.stopPropagation()),s.addEventListener("click",o=>{o.stopPropagation(),this._onActionButton(i)});const n=document.createElement("ha-icon");n.setAttribute("icon",W[i]||"mdi:gesture-tap-button"),s.appendChild(n),t.appendChild(s)}}}_defaultBtnLabel(t){return{pause:"Pause",resume:"Resume",terminate:"End cycle",record_start:"Start recording",record_stop:"Stop recording",open_panel:"Open WashData"}[t]||t}_buildGlance(){const t=this._baseStyle()+".glance{display:flex;flex-direction:column;padding:6px 0}.row{display:flex;align-items:center;gap:12px;padding:8px 14px;cursor:pointer}.row:hover{background:rgba(128,128,128,.06)}.dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}.row .info{flex:1;min-width:0}.row .primary{font-size:14px}.row .secondary{font-size:12px;margin-top:1px}.row .rt{font-size:13px;color:var(--secondary-text-color);flex-shrink:0;text-align:right}",e=this._glanceEntities().map((i,s)=>'
').join("");this.shadowRoot.innerHTML="
'+e+"
",this.shadowRoot.querySelectorAll(".row").forEach(i=>{i.addEventListener("click",()=>{const s=parseInt(i.dataset.idx,10),n=this._glanceEntities()[s];n&&this._moreInfo(n)})})}_glanceEntities(){const t=this._cfg||{};return Array.isArray(t.entities)&&t.entities.length?t.entities.map(e=>typeof e=="string"?e:e&&e.entity).filter(Boolean):t.entity?[t.entity]:[]}_update(){if(!this.shadowRoot||!this._hass||!this._cfg)return;const t=this._cfg.layout||"tile";if(t==="glance")return this._updateGlance();const e=this._resolveRoles(),i=this._vm(e);return t==="detail"?this._updateDetail(i):this._updateTile(i)}_applyIcon(t){const e=this.shadowRoot.getElementById("icon"),i=this.shadowRoot.getElementById("iconc");!e||!i||(e.setAttribute("icon",this._deviceTypeIcon()),i.style.color=t.color.fg,i.style.background=t.color.bg)}_applyBar(t){const e=this.shadowRoot.getElementById("barfill"),i=this.shadowRoot.getElementById("bar");if(!(!e||!i)){if(!t.isActive||t.pct===null){i.style.visibility="hidden",e.style.width="0%";return}i.style.visibility="visible",e.style.width=t.pct+"%",e.style.background=t.color.fg}}_updateTile(t){const e=this.shadowRoot.getElementById("title"),i=this.shadowRoot.getElementById("state");if(t.missing){e&&(e.textContent=this._t("card.entity_not_found",null,"Entity not found")),i&&(i.textContent=t.missingEntity||"");return}e.textContent=this._titleText(),this._applyIcon(t),this._applyBar(t);const s=this._flags(),n=[];s.showState&&(t.isRunning?t.subState&&n.push(t.subState):n.push(t.stateLabel)),s.showProgram&&t.program&&n.push(t.program),!t.isInactive&&s.showDetails&&(s.displayMode==="percentage"&&t.pct!==null?n.push(t.pct+"%"):t.timeText?n.push(t.timeText):t.pct!==null&&n.push(t.pct+"%")),s.showAnomaly&&t.anomaly&&n.push(this._t("card.running_long",null,"running long")),i.textContent=n.join(" \u2022 ")}_updateDetail(t){const e=this.shadowRoot.getElementById("title"),i=this.shadowRoot.getElementById("state"),s=this.shadowRoot.getElementById("eta"),n=this.shadowRoot.getElementById("etalbl"),o=this.shadowRoot.getElementById("etawrap"),a=this.shadowRoot.getElementById("meta");if(t.missing){e&&(e.textContent=this._t("card.entity_not_found",null,"Entity not found")),i&&(i.textContent=t.missingEntity||"");return}e.textContent=this._titleText(),this._applyIcon(t),this._applyBar(t);const r=this._flags(),l=[];if(r.showState&&l.push(t.isRunning&&t.subState?t.subState:t.stateLabel),r.showProgram&&t.program&&l.push(t.program),i.textContent=l.join(" \u2022 "),o&&s&&n&&(t.isActive&&t.timeText?(s.textContent=t.timeText,n.textContent=this._t("card.remaining",null,"remaining"),o.style.visibility="visible"):t.isActive&&t.pct!==null?(s.textContent=t.pct+"%",n.textContent=this._t("card.progress",null,"progress"),o.style.visibility="visible"):o.style.visibility="hidden"),a){a.innerHTML="";const h=(c,u)=>{if(!c)return;const p=document.createElement("span");p.className="chip"+(u?" warn":""),p.textContent=c,a.appendChild(p)};if(r.showPhase&&t.phase&&t.isActive&&h(t.phase),r.showEnergy&&t.energyText&&h(t.energyText),r.showEnergy&&t.costText&&h(t.costText),t.isRunning&&t.powerW!==null&&h((t.powerW>=100?Math.round(t.powerW):t.powerW.toFixed(1))+" W"),r.showAnomaly&&t.anomaly){const c=t.anomaly.ratio?" ("+Math.round((t.anomaly.ratio-1)*100)+"%)":"";h(this._t("card.running_long",null,"Running long")+c,!0)}}this._updateButtons(t),r.showSparkline&&this._updateSparkline(t)}_updateButtons(t){const e=this.shadowRoot.getElementById("acts");if(!e)return;const i=t.sk,s={pause:z.includes(i),resume:i==="user_paused",terminate:t.isActive,record_start:!t.isActive,record_stop:!0,open_panel:!0};e.querySelectorAll(".wd-act").forEach(o=>{const a=o.dataset.btn,r=a==="open_panel"||a==="program"?!0:t.buttons[a];s[a]!==!1&&(a==="open_panel"||r)?o.removeAttribute("disabled"):o.setAttribute("disabled","")});const n=this.shadowRoot.getElementById("prog-select");if(n&&t.select){const o=this._hass.states[t.select],a=o&&o.attributes&&o.attributes.options||[],r=o&&o.state,l=a.join("|");if(n._sig!==l){n._sig=l,n.innerHTML="";for(const h of a){const c=document.createElement("option");c.value=h,c.textContent=h,n.appendChild(c)}}r!==void 0&&(n.value=r)}else n&&n.setAttribute("disabled","")}_updateSparkline(t){const e=this.shadowRoot.getElementById("spark");if(!e)return;if(t.sk!==this._sparkState&&(t.isActive||(this._spark=[]),this._sparkState=t.sk),t.isActive&&t.powerW!==null){const h=this._spark[this._spark.length-1],c=Date.now();(!h||c-h.t>2e3)&&(this._spark.push({t:c,w:t.powerW}),this._spark.length>U&&this._spark.shift())}const i=this._spark;if(i.length<2){e.style.display="none";return}e.style.display="block";const s=e.clientWidth||300,n=34;e.width!==s&&(e.width=s),e.height!==n&&(e.height=n);const o=e.getContext("2d");o.clearRect(0,0,s,n);const a=Math.max.apply(null,i.map(h=>h.w))||1,r=3,l=(s-r*2)/(i.length-1);o.beginPath(),i.forEach((h,c)=>{const u=r+c*l,p=n-r-h.w/a*(n-r*2);c===0?o.moveTo(u,p):o.lineTo(u,p)}),o.strokeStyle=t.color.fg,o.lineWidth=2,o.lineJoin="round",o.stroke()}_updateGlance(){const t=this._glanceEntities(),e=this.shadowRoot.querySelectorAll(".row");t.forEach((i,s)=>{const n=e[s];if(!n)return;const o=this._resolveRoles(i),a=this._vm(o),r=n.querySelector("[data-dot]"),l=n.querySelector("[data-title]"),h=n.querySelector("[data-sub]"),c=n.querySelector("[data-rt]");if(a.missing){l&&(l.textContent=i),h&&(h.textContent=this._t("card.entity_not_found",null,"Entity not found")),r&&(r.style.background="var(--disabled-text-color, grey)"),c&&(c.textContent="");return}r&&(r.style.background=a.color.fg),l&&(l.textContent=this._deviceNameFor(i));const u=[];a.isRunning&&a.subState?u.push(a.subState):u.push(a.stateLabel),a.program&&u.push(a.program),h&&(h.textContent=u.join(" \u2022 ")),c&&(a.isActive&&a.timeText?c.textContent=a.timeText:a.isActive&&a.pct!==null?c.textContent=a.pct+"%":c.textContent="")})}_deviceNameFor(t){const e=this._hass,i=e&&e.entities||{},s=e&&e.devices||{},n=i[t];if(n&&n.device_id&&s[n.device_id]){const a=s[n.device_id],r=a.name_by_user||a.name;if(r)return r}const o=e&&e.states[t];return o&&o.attributes&&o.attributes.friendly_name?String(o.attributes.friendly_name).replace(/ (State|Status)$/i,""):t}_onActionButton(t){const e=this._hass;if(!e)return;if(t==="open_panel"){this._navigate("/ha-washdata"+this._deepLinkQuery());return}const i=this._resolveRoles(),s=i._buttons&&i._buttons[t];s&&(this._fireHaptic("light"),e.callService("button","press",{entity_id:s}))}_onProgramChange(t){const e=this._hass;if(!e||!t)return;const s=this._resolveRoles()._select;s&&e.callService("select","select_option",{entity_id:s,option:t})}_moreInfo(t){t&&this.dispatchEvent(new CustomEvent("hass-more-info",{detail:{entityId:t},bubbles:!0,composed:!0}))}_deepLinkQuery(){const t=this._hass,e=this._cfg||{},i=t&&t.entities||{},s=e.entity,n=i[s],o=e.device_id||n&&n.device_id||null,a=o&&t&&t.devices?t.devices[o]:null;let r="";if(a&&n&&n.platform===E){const l=Array.isArray(a.config_entries)?a.config_entries:[];r=a.primary_config_entry||(l.length===1?l[0]:"")}return!r&&a&&(r=a.name_by_user||a.name||""),!r&&s&&(r=this._deviceNameFor(s)),r?"?device="+encodeURIComponent(r):""}_navigate(t){window.history.pushState(null,"",t),window.dispatchEvent(new CustomEvent("location-changed",{detail:{replace:!1}}))}_clearHoldTimer(){this._holdTimer&&(window.clearTimeout(this._holdTimer),this._holdTimer=null)}_onPointerDown(t){if(t.button!==void 0&&t.button!==0)return;this._holdTriggered=!1,this._pointerCanceled=!1,this._pointerStart={x:t.clientX,y:t.clientY};const e=this._cfg&&this._cfg.hold_action;e&&e.action&&e.action!=="none"&&(this._clearHoldTimer(),this._holdTimer=window.setTimeout(()=>{this._holdTimer=null,this._holdTriggered=!0,this._fireHaptic("success"),this._executeAction(e)},500))}_onPointerMove(t){if(!this._pointerStart)return;const e=t.clientX-this._pointerStart.x,i=t.clientY-this._pointerStart.y;e*e+i*i>100&&(this._clearHoldTimer(),this._pointerStart=null,this._pointerCanceled=!0)}_onPointerCancel(){this._clearHoldTimer()}_onPointerUp(){if(this._clearHoldTimer(),this._pointerCanceled){this._pointerCanceled=!1;return}if(this._holdTriggered){this._holdTriggered=!1;return}const t=this._cfg&&this._cfg.tap_action||{action:"more-info"},e=this._cfg&&this._cfg.double_tap_action;if(!(e&&e.action&&e.action!=="none")){this._executeAction(t);return}const s=Date.now();if(this._tapTimer&&s-this._lastTapTime<250){window.clearTimeout(this._tapTimer),this._tapTimer=null,this._lastTapTime=0,this._executeAction(e);return}this._lastTapTime=s,this._tapTimer=window.setTimeout(()=>{this._tapTimer=null,this._executeAction(t)},250)}_fireHaptic(t){this.dispatchEvent(new CustomEvent("haptic",{detail:t,bubbles:!0,composed:!0}))}_executeAction(t){if(!t)return;const e=t.action||"more-info",i=t.entity||this._cfg&&this._cfg.entity;switch(e){case"none":return;case"more-info":this._moreInfo(i);return;case"toggle":if(!this._hass||!i)return;this._hass.callService("homeassistant","toggle",{entity_id:i});return;case"call-service":case"perform-action":{const s=t.perform_action||t.service;if(!s||!this._hass)return;const[n,o]=s.split(".");if(!n||!o)return;const a={...t.data||t.service_data||{}};this._hass.callService(n,o,a,t.target);return}case"navigate":{const s=t.navigation_path;if(!s)return;t.navigation_replace?(window.history.replaceState(window.history.state,"",s),window.dispatchEvent(new CustomEvent("location-changed",{detail:{replace:!0}}))):this._navigate(s);return}case"url":{const s=t.url_path;if(!s)return;window.open(s,"_blank","noopener,noreferrer");return}default:return}}}class Y extends HTMLElement{_lang(){const t=this._hass&&this._hass.locale&&this._hass.locale.language||this._hass&&this._hass.language||"en";return typeof t=="string"&&t?t:"en"}_t(t,e){const i=this._lang(),s=y[i]||y.en;if(s){const n=t.split(".").reduce((o,a)=>o&&o[a]!==void 0?o[a]:null,s);if(n&&typeof n=="string")return n}return e||t}setConfig(t){this._cfg={layout:"tile",...t},this._render()}set hass(t){this._hass=t;const e=this._lang(),i=[];y.en||i.push(S("en")),e&&e!=="en"&&!y[e]&&i.push(S(e)),i.length&&Promise.all(i).then(()=>this._render()),this._form&&(this._form.hass=t)}_render(){this.shadowRoot||this.attachShadow({mode:"open"}),this._form||(this.shadowRoot.innerHTML='
',this._form=document.createElement("ha-form"),this.shadowRoot.getElementById("editor-container").appendChild(this._form),this._form.addEventListener("value-changed",t=>this._valueChanged(t)),this._form.computeLabel=t=>this._t("card.editor."+t.name,this._humanize(t.name))),this._form.schema=this._schema(),this._form.data=this._cfg,this._hass&&(this._form.hass=this._hass)}_humanize(t){return t.replace(/_/g," ").replace(/\b\w/g,e=>e.toUpperCase())}_schema(){const t=this._cfg&&this._cfg.layout||"tile",e={name:"layout",selector:{select:{mode:"dropdown",options:[{value:"tile",label:this._t("card.layout.tile","Tile (compact)")},{value:"detail",label:this._t("card.layout.detail","Detail (rich)")},{value:"glance",label:this._t("card.layout.glance","Glance (multiple devices)")}]}}};if(t==="glance")return[e,{name:"entities",selector:{entity:{domain:"sensor",multiple:!0}}},{name:"title",selector:{text:{}}}];const i=[e,{name:"entity",selector:{entity:{domain:"sensor"}}},{name:"title",selector:{text:{}}},{name:"icon",selector:{icon:{}}},{name:"show_state",selector:{boolean:{}}},{name:"show_program",selector:{boolean:{}}},{name:"show_progress_bar",selector:{boolean:{}}},{name:"display_mode",selector:{select:{mode:"dropdown",options:[{value:"time",label:this._t("card.show_time_remaining","Show Time Remaining")},{value:"percentage",label:this._t("card.show_percentage","Show Percentage")}]}}},{name:"active_color",selector:{color_rgb:{}}}];return t==="detail"&&i.push({name:"show_phase",selector:{boolean:{}}},{name:"show_energy",selector:{boolean:{}}},{name:"show_anomaly",selector:{boolean:{}}},{name:"show_sparkline",selector:{boolean:{}}},{name:"buttons",selector:{select:{multiple:!0,mode:"list",options:D.map(s=>({value:s,label:this._t("card.btn."+s,this._humanize(s))}))}}}),i.push({name:"program_entity",selector:{entity:{domain:["sensor","select","input_select","input_text"]}}},{name:"time_entity",selector:{entity:{domain:"sensor"}}},{name:"pct_entity",selector:{entity:{domain:"sensor"}}},{name:"power_entity",selector:{entity:{domain:"sensor"}}},{name:"tap_action",selector:{ui_action:{}}},{name:"hold_action",selector:{ui_action:{}}},{name:"double_tap_action",selector:{ui_action:{}}}),i}_valueChanged(t){if(!this._cfg)return;const e=t.detail.value,i=e.layout&&e.layout!==this._cfg.layout;this._cfg={...this._cfg,...e},this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._cfg},bubbles:!0,composed:!0})),i&&this._render()}}const O=()=>{for(const[d,t]of[[b,V],[v,Y]])try{customElements.get(d)||customElements.define(d,t)}catch(e){console.error(`WashData: could not define <${d}>`,e)}};O(),(async()=>{const d=Date.now()+3e4;for(;Date.now()setTimeout(t,200))}})(),window.customCards=window.customCards||[],window.customCards.some(d=>d&&d.type===b)||window.customCards.push({type:b,name:"WashData Card",preview:!0,description:"Adaptive card for WashData appliances: compact tile, rich detail, or multi-device glance.",documentationURL:"https://github.com/3dg1luk43/ha_washdata"}); +const b="ha-washdata-card",v="ha-washdata-card-editor",E="ha_washdata",K=500,X=250,Q=10,N={state:"washer_state",program:"washer_program",time:"time_remaining",progress:"cycle_progress",power:"current_power",energy:"energy_total",phase:"current_phase"},W={pause:"pause_cycle",resume:"resume_cycle",terminate:"force_end_cycle",record_start:"record_start",record_stop:"record_stop"},H={pause:"mdi:pause",resume:"mdi:play",terminate:"mdi:stop",record_start:"mdi:record-circle-outline",record_stop:"mdi:stop-circle-outline",open_panel:"mdi:open-in-new"},D=["pause","resume","terminate","record_start","record_stop","program","open_panel"],z=["running","paused","user_paused","ending","starting","anti_wrinkle"],U=["running","starting","paused","ending"],S=["off","unknown","unavailable","idle"],F=60,R=new WeakMap;function G(d){const t=d&&d.connection;if(!t)return Promise.resolve({stateColors:{}});if(R.has(t))return R.get(t);const e=d.callWS({type:E+"/get_constants"}).then(s=>({stateColors:s&&s.state_colors||{}})).catch(()=>({stateColors:{}}));return R.set(t,e),e}const y={},A={},O=(()=>{try{return new URL(import.meta.url).searchParams.get("v")||""}catch{return""}})();function q(d){const t="/"+E+"/panel-translations/"+encodeURIComponent(d)+".json";return O?t+"?v="+encodeURIComponent(O):t}async function V(d){if(!d)return null;const t=[d],e=d.indexOf("-");e>0&&t.push(d.slice(0,e));for(const s of t)try{const i=await fetch(q(s));if(i.ok){const n=await i.json();if(n&&typeof n=="object")return n}}catch{}return null}async function T(d){if(!d||y[d])return;if(A[d]){await A[d];return}const t=V(d).then(e=>{e&&(y[d]=e)});A[d]=t,await t}class Y extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}),this._cfg=null,this._hass=null,this._builtSig=null,this._constantsLoaded=!1,this._langEnsured="",this._constants=null,this._spark=[],this._sparkState=null,this._holdTimer=null,this._holdTriggered=!1,this._tapTimer=null,this._lastTapTime=0,this._pointerStart=null,this._pointerCanceled=!1,this._onPointerDown=this._onPointerDown.bind(this),this._onPointerMove=this._onPointerMove.bind(this),this._onPointerUp=this._onPointerUp.bind(this),this._onPointerCancel=this._onPointerCancel.bind(this)}static getStubConfig(){return{entity:"sensor.washing_machine_state",layout:"tile",tap_action:{action:"more-info"},hold_action:{action:"none"},double_tap_action:{action:"none"}}}static getConfigElement(){return document.createElement(v)}setConfig(t){if(!t)throw new Error("Invalid configuration");const e=t.layout||"tile";if(e!=="glance"&&!t.entity)throw new Error("Please define an entity");if(e==="glance"&&!(Array.isArray(t.entities)&&t.entities.length)&&!t.entity)throw new Error("Please define entities for the glance layout");this._cfg={...t,layout:e},this._builtSig!==this._structureSig()&&(this._builtSig=null,this.shadowRoot&&(this.shadowRoot.innerHTML="")),this._render()}_structureSig(){const t=this._cfg||{},e=this._flags(),s=t.layout||"tile";return[s,e.showBar?"bar":"",e.showSparkline?"spark":"",e.buttons.join("+"),s==="glance"?String(this._glanceEntities().length):"",t.tap_action&&t.tap_action.action==="none"?"notap":""].join("|")}set hass(t){this._hass=t,this._ensureResources(),this._render()}_detailRows(){const t=this._flags();let e=2;return t.buttons.length&&(e+=1),t.showSparkline&&(e+=1),e}getCardSize(){const t=this._cfg&&this._cfg.layout;return t==="detail"?this._detailRows():t==="glance"&&this._glanceEntities().length||1}getGridOptions(){const t=this._cfg&&this._cfg.layout;if(t==="detail"){const e=this._detailRows();return{rows:e,min_rows:e,columns:12,min_columns:6}}return t==="glance"?{rows:Math.max(1,this._glanceEntities().length),min_rows:1,columns:12,min_columns:6}:{rows:1,min_rows:1,columns:6,min_columns:3}}disconnectedCallback(){this._clearHoldTimer(),this._tapTimer&&(window.clearTimeout(this._tapTimer),this._tapTimer=null)}_lang(){const t=this._hass&&this._hass.locale&&this._hass.locale.language||this._hass&&this._hass.language||"en";return typeof t=="string"&&t?t:"en"}_tLookup(t,e){const s=y[e];if(!s)return null;const i=t.split(".").reduce((n,o)=>n&&n[o]!==void 0?n[o]:null,s);return i&&typeof i=="string"?i:null}_t(t,e,s){const i=this._lang();let n=i&&this._tLookup(t,i)||this._tLookup(t,"en")||s||t;if(e)for(const[o,a]of Object.entries(e))n=n.replace(new RegExp("\\{"+o+"\\}","g"),String(a));return n}_ensureResources(){if(!this._hass)return;const t=this._lang(),e=!this._constantsLoaded,s=t!==this._langEnsured;if(!e&&!s)return;const i=[];e&&(this._constantsLoaded=!0,i.push(G(this._hass).then(n=>{this._constants=n}),T("en"))),s&&(this._langEnsured=t,t&&t!=="en"&&i.push(T(t))),Promise.all(i).then(()=>this._update()).catch(()=>{})}_resolveRoles(t){const e=this._hass&&this._hass.entities||null,s=t||this._cfg&&this._cfg.entity||"";let i=this._rolesCache;if((!i||i.reg!==e||i.cfg!==this._cfg)&&(i=this._rolesCache={reg:e,cfg:this._cfg,map:new Map}),i.map.has(s))return i.map.get(s);const n=this._computeRoles(t);return e&&n._deviceId&&!n._fromStates&&i.map.set(s,n),n}_computeRoles(t){const e=this._hass,s=this._cfg||{},i=e&&e.entities||{},n=t||s.entity,o=i[n],a=s.device_id||o&&o.device_id||null,r={state:n},c={};let h=null;if(a)for(const[u,p]of Object.entries(i)){if(!p||p.device_id!==a||p.platform&&p.platform!==E)continue;const m=p.translation_key;if(u.startsWith("sensor."))for(const[g,_]of Object.entries(N))m===_&&!r[g]&&(r[g]=u);for(const[g,_]of Object.entries(W))m===_&&!c[g]&&(c[g]=u);!h&&u.startsWith("select.")&&(h=u)}s.program_entity&&(r.program=s.program_entity),s.time_entity&&(r.time=s.time_entity),s.pct_entity&&(r.progress=s.pct_entity),s.power_entity&&(r.power=s.power_entity),s.energy_entity&&(r.energy=s.energy_entity),s.phase_entity&&(r.phase=s.phase_entity);let l=!1;if(!r.time||!r.progress||!r.program){const u=/^sensor\.(.+)_state$/.exec(n);if(u){l=!0;const p="sensor."+u[1]+"_",m=e&&e.states,g=_=>m&&m[p+_]?p+_:null;r.time=r.time||g("time_remaining"),r.progress=r.progress||g("cycle_progress"),r.program=r.program||g("program")}}return r._buttons=c,r._select=h,r._deviceId=a,r._fromStates=l,r}_fmtState(t){if(!t)return"";const e=this._hass;if(e&&typeof e.formatEntityState=="function")try{return e.formatEntityState(t)}catch{}const s=t.state||"";return s&&s.charAt(0).toUpperCase()+s.slice(1)}_vm(t){const e=this._hass,s=this._cfg,i=e.states,n=i[t.state];if(!n)return{missing:!0,missingEntity:t.state};const o=String(n.state||"").toLowerCase(),a=S.includes(o),r=z.includes(o),c=n.attributes||{};let h="";const l=t.program?i[t.program]:null;if(l){const f=String(l.state||"").toLowerCase();["unknown","none","off","unavailable",""].includes(f)||(h=this._fmtState(l))}if(!h&&c.current_program_guess){const f=String(c.current_program_guess).toLowerCase();["unknown","none","off","unavailable"].includes(f)||(h=c.current_program_guess)}let u="";const p=t.phase?i[t.phase]:null;if(p){const f=String(p.state||"").toLowerCase();["unknown","none","off","unavailable",""].includes(f)||(u=this._fmtState(p))}!u&&l&&l.attributes&&l.attributes.active_phase&&(u=l.attributes.active_phase);let m="";if(o==="running"&&c.sub_state){const f=String(c.sub_state).match(/Running \((.*)\)/);m=f&&f[1]?f[1]:c.sub_state}let g=null;const _=t.progress?i[t.progress]:null;_&&!isNaN(parseFloat(_.state))&&(g=Math.max(0,Math.min(100,Math.round(parseFloat(_.state)))));let x="";const w=t.time?i[t.time]:null;w&&!S.includes(String(w.state).toLowerCase())&&(e&&typeof e.formatEntityState=="function"?x=this._fmtState(w):isNaN(parseFloat(w.state))?x=w.state:x=w.state+" "+this._t("card.min",null,"min"));let L="",P="";if(_&&_.attributes){const f=_.attributes.projected_energy_kwh;f!=null&&(L=f+" kWh");const k=_.attributes.projected_cost;k!=null&&(P=this._fmtCost(k))}let I=null;const C=t.power?i[t.power]:null;C&&!isNaN(parseFloat(C.state))&&(I=parseFloat(C.state));let B=null;c.cycle_anomaly&&c.cycle_anomaly!=="none"&&(B={kind:c.cycle_anomaly,ratio:c.overrun_ratio});const j=this._stateColor(o,r,s.active_color);return{missing:!1,sk:o,stateObj:n,isInactive:a,isActive:r,isRunning:o==="running",stateLabel:this._fmtState(n),color:j,program:h,phase:u,subState:m,pct:g,timeText:x,energyText:L,costText:P,powerW:I,anomaly:B,buttons:t._buttons||{},select:t._select||null}}_fmtCost(t){const e=this._hass,s=e&&e.config&&e.config.currency;if(s)try{return new Intl.NumberFormat(this._lang(),{style:"currency",currency:s}).format(Number(t))}catch{}return String(t)}_stateColor(t,e,s){if(e&&s){if(Array.isArray(s)){const[o,a,r]=s;return{fg:"rgb("+o+","+a+","+r+")",bg:"rgba("+o+","+a+","+r+",0.2)"}}return{fg:s,bg:"rgba(128,128,128,0.15)"}}const n=(this._constants&&this._constants.stateColors||{})[t];return n?{fg:n,bg:this._alpha(n,t)}:S.includes(t)?{fg:"var(--disabled-text-color, grey)",bg:"rgba(128,128,128,0.1)"}:{fg:"var(--primary-color)",bg:"rgba(var(--rgb-primary-color, 33,150,243),0.2)"}}_alpha(t,e){const s=/#([0-9a-fA-F]{6})/.exec(t);if(s){const i=parseInt(s[1],16),n=i>>16&255,o=i>>8&255,a=i&255;return"rgba("+n+","+o+","+a+",0.18)"}return S.includes(e)?"rgba(128,128,128,0.1)":"rgba(128,128,128,0.15)"}_deviceTypeIcon(){const t=this._hass,e=this._cfg;if(e.icon)return e.icon;const s=t&&t.states[e.entity];return s&&s.attributes&&s.attributes.icon?s.attributes.icon:"mdi:washing-machine"}_titleText(){const t=this._cfg;if(t.title)return t.title;const e=this._hass,s=e&&e.entities||{},i=e&&e.devices||{},n=s[t.entity];if(n&&n.device_id&&i[n.device_id]){const a=i[n.device_id],r=a.name_by_user||a.name;if(r)return r}const o=e&&e.states[t.entity];return o&&o.attributes&&o.attributes.friendly_name?String(o.attributes.friendly_name).replace(/ (State|Status)$/i,""):"WashData"}_flags(){const t=this._cfg||{};return{showState:t.show_state!==!1,showProgram:t.show_program!==!1,showDetails:t.show_details!==!1,showBar:t.show_progress_bar!==!1,showPhase:t.show_phase!==!1,showEnergy:t.show_energy!==!1,showAnomaly:t.show_anomaly!==!1,showSparkline:!!t.show_sparkline,displayMode:t.display_mode||"time",buttons:Array.isArray(t.buttons)?t.buttons.filter(e=>D.includes(e)):[]}}_render(){if(!this.shadowRoot||!this._cfg)return;const t=this._structureSig();this._builtSig!==t&&(this._build(this._cfg.layout||"tile"),this._builtSig=t),this._update()}_baseStyle(){return":host{display:block;height:100%}[role=button]:focus-visible{outline:2px solid var(--primary-color);outline-offset:2px}ha-card{padding:0;background:var(--ha-card-background,var(--card-background-color,white));border-radius:var(--ha-card-border-radius,12px);box-shadow:var(--ha-card-box-shadow,none);overflow:hidden;cursor:pointer;height:100%;box-sizing:border-box;border:var(--ha-card-border-width,1px) solid var(--ha-card-border-color,var(--divider-color))}.icon-container{border-radius:12px;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background-color .3s,color .3s;position:relative}ha-icon{--mdc-icon-size:24px}.primary{font-weight:500;color:var(--primary-text-color);white-space:nowrap;text-overflow:ellipsis;overflow:hidden;line-height:1.2}.secondary{color:var(--secondary-text-color);white-space:nowrap;text-overflow:ellipsis;overflow:hidden;line-height:1.2}.bar{height:4px;border-radius:2px;background:var(--divider-color,rgba(128,128,128,.25));overflow:hidden}.bar>i{display:block;height:100%;width:0;border-radius:2px;transition:width .5s ease,background-color .3s}.chip{display:inline-flex;align-items:center;gap:4px;font-size:11px;padding:1px 8px;border-radius:10px;background:rgba(128,128,128,.14);color:var(--secondary-text-color);white-space:nowrap}.chip.warn{background:rgba(255,152,0,.16);color:var(--warning-color,#ff9800)}.acts{display:flex;gap:4px;align-items:center}.wd-act{border:none;background:rgba(128,128,128,.12);color:var(--primary-text-color);border-radius:10px;width:34px;height:34px;display:flex;align-items:center;justify-content:center;cursor:pointer;padding:0}.wd-act:hover{background:rgba(128,128,128,.22)}.wd-act[disabled]{opacity:.35;pointer-events:none}.wd-act ha-icon{--mdc-icon-size:20px}select.wd-prog{max-width:150px;font:inherit;color:var(--primary-text-color);background:var(--secondary-background-color);border:1px solid var(--divider-color);border-radius:8px;padding:4px 6px}"}_build(t){return t==="detail"?this._buildDetail():t==="glance"?this._buildGlance():this._buildTile()}_attachGestures(t,e=t){(this._cfg&&this._cfg.tap_action||{action:"more-info"}).action!=="none"&&(e.tabIndex=0,e.setAttribute("role","button"),e.addEventListener("keydown",i=>{i.target!==e||i.key!=="Enter"&&i.key!==" "||(i.preventDefault(),this._executeAction(this._cfg&&this._cfg.tap_action||{action:"more-info"}))})),t.addEventListener("pointerdown",this._onPointerDown),t.addEventListener("pointermove",this._onPointerMove),t.addEventListener("pointerup",this._onPointerUp),t.addEventListener("pointercancel",this._onPointerCancel),t.addEventListener("pointerleave",this._onPointerCancel)}_buildTile(){const t=this._flags(),e=this._baseStyle()+".tile{display:flex;flex-direction:row;align-items:center;padding:8px 12px;gap:12px;width:100%;box-sizing:border-box}.tile .icon-container{width:40px;height:40px}.tile .info{display:flex;flex-direction:column;justify-content:center;overflow:hidden;flex:1;min-width:0}.tile .primary{font-size:14px}.tile .secondary{font-size:12px;margin-top:2px}.tile .bar{margin-top:6px}";this.shadowRoot.innerHTML="
'+(t.showBar?'
':"")+"
",this._attachGestures(this.shadowRoot.getElementById("card"))}_buildDetail(){const t=this._flags(),e=this._baseStyle()+"ha-card.wd-detail{height:auto}.detail{display:flex;flex-direction:column;gap:8px;padding:12px 14px;width:100%;box-sizing:border-box}.detail .top{display:flex;align-items:center;gap:12px}.detail .icon-container{width:48px;height:48px}.detail .icon-container ha-icon{--mdc-icon-size:30px}.detail .info{flex:1;min-width:0}.detail .primary{font-size:16px}.detail .secondary{font-size:13px;margin-top:2px}.detail .eta{text-align:right;flex-shrink:0}.detail .eta .big{font-size:22px;font-weight:600;color:var(--primary-text-color);line-height:1.1}.detail .eta .lbl{font-size:11px;color:var(--secondary-text-color);text-transform:uppercase;letter-spacing:.04em}.detail .bar{height:6px}.detail .meta{display:flex;flex-wrap:wrap;gap:6px;align-items:center}.detail .meta:empty{display:none}.detail canvas{width:100%;height:34px;display:block}";this.shadowRoot.innerHTML="
'+(t.showBar?'
':"")+'
'+(t.showSparkline?'':"")+(t.buttons.length?'
':"")+"
";const s=this.shadowRoot.getElementById("card");this._attachGestures(s,t.buttons.length?s.querySelector(".top"):s),t.buttons.length&&this._buildButtons(this.shadowRoot.getElementById("acts"),t.buttons)}_buildButtons(t,e){if(t){t.innerHTML="";for(const s of e){if(s==="program"){const o=document.createElement("select");o.className="wd-prog",o.id="prog-select",o.addEventListener("click",a=>a.stopPropagation()),o.addEventListener("pointerdown",a=>a.stopPropagation()),o.addEventListener("pointerup",a=>a.stopPropagation()),o.addEventListener("change",a=>{a.stopPropagation(),this._onProgramChange(a.target.value)}),t.appendChild(o);continue}const i=document.createElement("button");i.className="wd-act",i.dataset.btn=s,i.title=this._t("card.btn."+s,null,this._defaultBtnLabel(s)),i.addEventListener("pointerdown",o=>o.stopPropagation()),i.addEventListener("pointerup",o=>o.stopPropagation()),i.addEventListener("click",o=>{o.stopPropagation(),this._onActionButton(s)});const n=document.createElement("ha-icon");n.setAttribute("icon",H[s]||"mdi:gesture-tap-button"),i.appendChild(n),t.appendChild(i)}}}_defaultBtnLabel(t){return{pause:"Pause",resume:"Resume",terminate:"End cycle",record_start:"Start recording",record_stop:"Stop recording",open_panel:"Open WashData"}[t]||t}_buildGlance(){const t=this._baseStyle()+".glance{display:flex;flex-direction:column;padding:6px 0}.row{display:flex;align-items:center;gap:12px;padding:8px 14px;cursor:pointer}.row:hover{background:rgba(128,128,128,.06)}.dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}.row .info{flex:1;min-width:0}.row .primary{font-size:14px}.row .secondary{font-size:12px;margin-top:1px}.row .rt{font-size:13px;color:var(--secondary-text-color);flex-shrink:0;text-align:right}",e=this._glanceEntities().map((s,i)=>'
').join("");this.shadowRoot.innerHTML="
'+e+"
",this.shadowRoot.querySelectorAll(".row").forEach(s=>{s.addEventListener("click",()=>{const i=parseInt(s.dataset.idx,10),n=this._glanceEntities()[i];n&&this._moreInfo(n)})})}_glanceEntities(){const t=this._cfg||{};return Array.isArray(t.entities)&&t.entities.length?t.entities.map(e=>typeof e=="string"?e:e&&e.entity).filter(Boolean):t.entity?[t.entity]:[]}_update(){if(!this.shadowRoot||!this._hass||!this._cfg)return;const t=this._cfg.layout||"tile";if(t==="glance")return this._updateGlance();const e=this._resolveRoles(),s=this._vm(e);return t==="detail"?this._updateDetail(s):this._updateTile(s)}_applyIcon(t){const e=this.shadowRoot.getElementById("icon"),s=this.shadowRoot.getElementById("iconc");!e||!s||(e.setAttribute("icon",this._deviceTypeIcon()),s.style.color=t.color.fg,s.style.background=t.color.bg)}_applyBar(t){const e=this.shadowRoot.getElementById("barfill"),s=this.shadowRoot.getElementById("bar");if(!(!e||!s)){if(!t.isActive||t.pct===null){s.style.visibility="hidden",e.style.width="0%";return}s.style.visibility="visible",e.style.width=t.pct+"%",e.style.background=t.color.fg}}_updateTile(t){const e=this.shadowRoot.getElementById("title"),s=this.shadowRoot.getElementById("state");if(t.missing){e&&(e.textContent=this._t("card.entity_not_found",null,"Entity not found")),s&&(s.textContent=t.missingEntity||"");return}e.textContent=this._titleText(),this._applyIcon(t),this._applyBar(t);const i=this._flags(),n=[];i.showState&&(t.isRunning?t.subState&&n.push(t.subState):n.push(t.stateLabel)),i.showProgram&&t.program&&n.push(t.program),!t.isInactive&&i.showDetails&&(i.displayMode==="percentage"&&t.pct!==null?n.push(t.pct+"%"):t.timeText?n.push(t.timeText):t.pct!==null&&n.push(t.pct+"%")),i.showAnomaly&&t.anomaly&&n.push(t.anomaly.kind==="stalled"?this._t("card.stalled",null,"Stalled"):this._t("card.running_long",null,"running long")),s.textContent=n.join(" \u2022 ")}_updateDetail(t){const e=this.shadowRoot.getElementById("title"),s=this.shadowRoot.getElementById("state"),i=this.shadowRoot.getElementById("eta"),n=this.shadowRoot.getElementById("etalbl"),o=this.shadowRoot.getElementById("etawrap"),a=this.shadowRoot.getElementById("meta");if(t.missing){e&&(e.textContent=this._t("card.entity_not_found",null,"Entity not found")),s&&(s.textContent=t.missingEntity||"");return}e.textContent=this._titleText(),this._applyIcon(t),this._applyBar(t);const r=this._flags(),c=[];if(r.showState&&c.push(t.isRunning&&t.subState?t.subState:t.stateLabel),r.showProgram&&t.program&&c.push(t.program),s.textContent=c.join(" \u2022 "),o&&i&&n&&(t.isActive&&t.timeText?(i.textContent=t.timeText,n.textContent=this._t("card.remaining",null,"remaining"),o.style.visibility="visible"):t.isActive&&t.pct!==null?(i.textContent=t.pct+"%",n.textContent=this._t("card.progress",null,"progress"),o.style.visibility="visible"):o.style.visibility="hidden"),a){a.innerHTML="";const h=(l,u)=>{if(!l)return;const p=document.createElement("span");p.className="chip"+(u?" warn":""),p.textContent=l,a.appendChild(p)};if(r.showPhase&&t.phase&&t.isActive&&h(t.phase),r.showEnergy&&t.energyText&&h(t.energyText),r.showEnergy&&t.costText&&h(t.costText),t.isRunning&&t.powerW!==null&&h((t.powerW>=100?Math.round(t.powerW):t.powerW.toFixed(1))+" W"),r.showAnomaly&&t.anomaly&&t.anomaly.kind==="stalled")h(this._t("card.stalled",null,"Stalled"),!0);else if(r.showAnomaly&&t.anomaly){const l=t.anomaly.ratio?" ("+Math.round((t.anomaly.ratio-1)*100)+"%)":"";h(this._t("card.running_long",null,"Running long")+l,!0)}}this._updateButtons(t),r.showSparkline&&this._updateSparkline(t)}_updateButtons(t){const e=this.shadowRoot.getElementById("acts");if(!e)return;const s=t.sk,i={pause:U.includes(s),resume:s==="user_paused",terminate:t.isActive,record_start:!t.isActive,record_stop:!0,open_panel:!0};e.querySelectorAll(".wd-act").forEach(o=>{const a=o.dataset.btn,r=a==="open_panel"||a==="program"?!0:t.buttons[a];i[a]!==!1&&(a==="open_panel"||r)?o.removeAttribute("disabled"):o.setAttribute("disabled","")});const n=this.shadowRoot.getElementById("prog-select");if(n&&t.select){const o=this._hass.states[t.select],a=o&&o.attributes&&o.attributes.options||[],r=o&&o.state,c=a.join("|");if(n._sig!==c){n._sig=c,n.innerHTML="";for(const h of a){const l=document.createElement("option");l.value=h,l.textContent=h,n.appendChild(l)}}r!==void 0&&(n.value=r)}else n&&n.setAttribute("disabled","")}_updateSparkline(t){const e=this.shadowRoot.getElementById("spark");if(!e)return;if(t.sk!==this._sparkState&&(t.isActive||(this._spark=[]),this._sparkState=t.sk),t.isActive&&t.powerW!==null){const h=this._spark[this._spark.length-1],l=Date.now();(!h||l-h.t>2e3)&&(this._spark.push({t:l,w:t.powerW}),this._spark.length>F&&this._spark.shift())}const s=this._spark;if(s.length<2){e.style.display="none";return}e.style.display="block";const i=e.clientWidth||300,n=34;e.width!==i&&(e.width=i),e.height!==n&&(e.height=n);const o=e.getContext("2d");o.clearRect(0,0,i,n);const a=Math.max.apply(null,s.map(h=>h.w))||1,r=3,c=(i-r*2)/(s.length-1);o.beginPath(),s.forEach((h,l)=>{const u=r+l*c,p=n-r-h.w/a*(n-r*2);l===0?o.moveTo(u,p):o.lineTo(u,p)}),o.strokeStyle=t.color.fg,o.lineWidth=2,o.lineJoin="round",o.stroke()}_updateGlance(){const t=this._glanceEntities(),e=this.shadowRoot.querySelectorAll(".row");t.forEach((s,i)=>{const n=e[i];if(!n)return;const o=this._resolveRoles(s),a=this._vm(o),r=n.querySelector("[data-dot]"),c=n.querySelector("[data-title]"),h=n.querySelector("[data-sub]"),l=n.querySelector("[data-rt]");if(a.missing){c&&(c.textContent=s),h&&(h.textContent=this._t("card.entity_not_found",null,"Entity not found")),r&&(r.style.background="var(--disabled-text-color, grey)"),l&&(l.textContent="");return}r&&(r.style.background=a.color.fg),c&&(c.textContent=this._deviceNameFor(s));const u=[];a.isRunning&&a.subState?u.push(a.subState):u.push(a.stateLabel),a.program&&u.push(a.program),h&&(h.textContent=u.join(" \u2022 ")),l&&(a.isActive&&a.timeText?l.textContent=a.timeText:a.isActive&&a.pct!==null?l.textContent=a.pct+"%":l.textContent="")})}_deviceNameFor(t){const e=this._hass,s=e&&e.entities||{},i=e&&e.devices||{},n=s[t];if(n&&n.device_id&&i[n.device_id]){const a=i[n.device_id],r=a.name_by_user||a.name;if(r)return r}const o=e&&e.states[t];return o&&o.attributes&&o.attributes.friendly_name?String(o.attributes.friendly_name).replace(/ (State|Status)$/i,""):t}_onActionButton(t){const e=this._hass;if(!e)return;if(t==="open_panel"){this._navigate("/ha-washdata"+this._deepLinkQuery());return}const s=this._resolveRoles(),i=s._buttons&&s._buttons[t];i&&(this._fireHaptic("light"),e.callService("button","press",{entity_id:i}))}_onProgramChange(t){const e=this._hass;if(!e||!t)return;const i=this._resolveRoles()._select;i&&e.callService("select","select_option",{entity_id:i,option:t})}_moreInfo(t){t&&this.dispatchEvent(new CustomEvent("hass-more-info",{detail:{entityId:t},bubbles:!0,composed:!0}))}_deepLinkQuery(){const t=this._hass,e=this._cfg||{},s=t&&t.entities||{},i=e.entity,n=s[i],o=e.device_id||n&&n.device_id||null,a=o&&t&&t.devices?t.devices[o]:null;let r="";if(a&&n&&n.platform===E){const c=Array.isArray(a.config_entries)?a.config_entries:[];r=a.primary_config_entry||(c.length===1?c[0]:"")}return!r&&a&&(r=a.name_by_user||a.name||""),!r&&i&&(r=this._deviceNameFor(i)),r?"?device="+encodeURIComponent(r):""}_navigate(t){window.history.pushState(null,"",t),window.dispatchEvent(new CustomEvent("location-changed",{detail:{replace:!1}}))}_clearHoldTimer(){this._holdTimer&&(window.clearTimeout(this._holdTimer),this._holdTimer=null)}_onPointerDown(t){if(t.button!==void 0&&t.button!==0)return;this._holdTriggered=!1,this._pointerCanceled=!1,this._pointerStart={x:t.clientX,y:t.clientY};const e=this._cfg&&this._cfg.hold_action;e&&e.action&&e.action!=="none"&&(this._clearHoldTimer(),this._holdTimer=window.setTimeout(()=>{this._holdTimer=null,this._holdTriggered=!0,this._fireHaptic("success"),this._executeAction(e)},500))}_onPointerMove(t){if(!this._pointerStart)return;const e=t.clientX-this._pointerStart.x,s=t.clientY-this._pointerStart.y;e*e+s*s>100&&(this._clearHoldTimer(),this._pointerStart=null,this._pointerCanceled=!0)}_onPointerCancel(){this._clearHoldTimer()}_onPointerUp(){if(this._clearHoldTimer(),this._pointerCanceled){this._pointerCanceled=!1;return}if(this._holdTriggered){this._holdTriggered=!1;return}const t=this._cfg&&this._cfg.tap_action||{action:"more-info"},e=this._cfg&&this._cfg.double_tap_action;if(!(e&&e.action&&e.action!=="none")){this._executeAction(t);return}const i=Date.now();if(this._tapTimer&&i-this._lastTapTime<250){window.clearTimeout(this._tapTimer),this._tapTimer=null,this._lastTapTime=0,this._executeAction(e);return}this._lastTapTime=i,this._tapTimer=window.setTimeout(()=>{this._tapTimer=null,this._executeAction(t)},250)}_fireHaptic(t){this.dispatchEvent(new CustomEvent("haptic",{detail:t,bubbles:!0,composed:!0}))}_executeAction(t){if(!t)return;const e=t.action||"more-info",s=t.entity||this._cfg&&this._cfg.entity;switch(e){case"none":return;case"more-info":this._moreInfo(s);return;case"toggle":if(!this._hass||!s)return;this._hass.callService("homeassistant","toggle",{entity_id:s});return;case"call-service":case"perform-action":{const i=t.perform_action||t.service;if(!i||!this._hass)return;const[n,o]=i.split(".");if(!n||!o)return;const a={...t.data||t.service_data||{}};this._hass.callService(n,o,a,t.target);return}case"navigate":{const i=t.navigation_path;if(!i)return;t.navigation_replace?(window.history.replaceState(window.history.state,"",i),window.dispatchEvent(new CustomEvent("location-changed",{detail:{replace:!0}}))):this._navigate(i);return}case"url":{const i=t.url_path;if(!i)return;window.open(i,"_blank","noopener,noreferrer");return}default:return}}}class $ extends HTMLElement{_lang(){const t=this._hass&&this._hass.locale&&this._hass.locale.language||this._hass&&this._hass.language||"en";return typeof t=="string"&&t?t:"en"}_t(t,e){const s=this._lang(),i=y[s]||y.en;if(i){const n=t.split(".").reduce((o,a)=>o&&o[a]!==void 0?o[a]:null,i);if(n&&typeof n=="string")return n}return e||t}setConfig(t){this._cfg={layout:"tile",...t},this._render()}set hass(t){this._hass=t;const e=this._lang(),s=[];y.en||s.push(T("en")),e&&e!=="en"&&!y[e]&&s.push(T(e)),s.length&&Promise.all(s).then(()=>this._render()),this._form&&(this._form.hass=t)}_render(){this.shadowRoot||this.attachShadow({mode:"open"}),this._form||(this.shadowRoot.innerHTML='
',this._form=document.createElement("ha-form"),this.shadowRoot.getElementById("editor-container").appendChild(this._form),this._form.addEventListener("value-changed",t=>this._valueChanged(t)),this._form.computeLabel=t=>this._t("card.editor."+t.name,this._humanize(t.name))),this._form.schema=this._schema(),this._form.data=this._cfg,this._hass&&(this._form.hass=this._hass)}_humanize(t){return t.replace(/_/g," ").replace(/\b\w/g,e=>e.toUpperCase())}_schema(){const t=this._cfg&&this._cfg.layout||"tile",e={name:"layout",selector:{select:{mode:"dropdown",options:[{value:"tile",label:this._t("card.layout.tile","Tile (compact)")},{value:"detail",label:this._t("card.layout.detail","Detail (rich)")},{value:"glance",label:this._t("card.layout.glance","Glance (multiple devices)")}]}}};if(t==="glance")return[e,{name:"entities",selector:{entity:{domain:"sensor",multiple:!0}}},{name:"title",selector:{text:{}}}];const s=[e,{name:"entity",selector:{entity:{domain:"sensor"}}},{name:"title",selector:{text:{}}},{name:"icon",selector:{icon:{}}},{name:"show_state",selector:{boolean:{}}},{name:"show_program",selector:{boolean:{}}},{name:"show_progress_bar",selector:{boolean:{}}},{name:"display_mode",selector:{select:{mode:"dropdown",options:[{value:"time",label:this._t("card.show_time_remaining","Show Time Remaining")},{value:"percentage",label:this._t("card.show_percentage","Show Percentage")}]}}},{name:"active_color",selector:{color_rgb:{}}}];return t==="detail"&&s.push({name:"show_phase",selector:{boolean:{}}},{name:"show_energy",selector:{boolean:{}}},{name:"show_anomaly",selector:{boolean:{}}},{name:"show_sparkline",selector:{boolean:{}}},{name:"buttons",selector:{select:{multiple:!0,mode:"list",options:D.map(i=>({value:i,label:this._t("card.btn."+i,this._humanize(i))}))}}}),s.push({name:"program_entity",selector:{entity:{domain:["sensor","select","input_select","input_text"]}}},{name:"time_entity",selector:{entity:{domain:"sensor"}}},{name:"pct_entity",selector:{entity:{domain:"sensor"}}},{name:"power_entity",selector:{entity:{domain:"sensor"}}},{name:"tap_action",selector:{ui_action:{}}},{name:"hold_action",selector:{ui_action:{}}},{name:"double_tap_action",selector:{ui_action:{}}}),s}_valueChanged(t){if(!this._cfg)return;const e=t.detail.value,s=e.layout&&e.layout!==this._cfg.layout;this._cfg={...this._cfg,...e},this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._cfg},bubbles:!0,composed:!0})),s&&this._render()}}const M=()=>{for(const[d,t]of[[b,Y],[v,$]])try{customElements.get(d)||customElements.define(d,t)}catch(e){console.error(`WashData: could not define <${d}>`,e)}};M(),(async()=>{const d=Date.now()+3e4;for(;Date.now()setTimeout(t,200))}})(),window.customCards=window.customCards||[],window.customCards.some(d=>d&&d.type===b)||window.customCards.push({type:b,name:"WashData Card",preview:!0,description:"Adaptive card for WashData appliances: compact tile, rich detail, or multi-device glance.",documentationURL:"https://github.com/3dg1luk43/ha_washdata"}); diff --git a/custom_components/ha_washdata/www/ha-washdata-panel.js b/custom_components/ha_washdata/www/ha-washdata-panel.js index f2708934..a63c771b 100644 --- a/custom_components/ha_washdata/www/ha-washdata-panel.js +++ b/custom_components/ha_washdata/www/ha-washdata-panel.js @@ -43,20 +43,28 @@ const _HASS_REFRESH_MS = 6000; // Cycles-tab page size. Kept modest so the "Load more" control actually engages // for typical histories instead of loading everything in one page. const _CYCLE_PAGE_SIZE = 25; +// playground.MAX_BATCH_CYCLES: the most cycles one Test-on-history / Optimize run replays. +const _PG_MAX_BATCH_CYCLES = 50; +// The external integration the Maintenance section points to (#461). It reads the +// cycle-count sensor (translation_key "cycle_count", unit "cycles"), a public contract. +const _MAINTENANCE_SUPPORTER_URL = 'https://github.com/iluebbe/maintenance_supporter'; // Detector states that mean "a cycle is in flight". Single source for the device // bar dot, the status header and the pause/resume/force-stop controls -- these // used to be three hand-copied lists and the controls one had drifted (it was // missing 'paused', so an auto-paused cycle showed no buttons at all). -const _ACTIVE_STATES = ['running', 'starting', 'paused', 'user_paused', 'ending', 'anti_wrinkle', 'rinse']; +const _ACTIVE_STATES = ['running', 'starting', 'paused', 'user_paused', 'ending', 'anti_wrinkle']; // Declarative community-store preference toggles, rendered in the gear's Online & // Community pane. To ship a new online setting: add one row here AND one default in // store_account._DEFAULT_PREFS -- the generic get_prefs / store_set_prefs plumbing // carries it end-to-end (no per-setting wiring). All are booleans. const _STORE_PREFS = [ - { key: 'show_contributor', labelKey: 'lbl.show_contributor', labelFb: 'Show contributor names', + { key: 'show_contributor', def: true, labelKey: 'lbl.show_contributor', labelFb: 'Show contributor names', docKey: 'setting.show_contributor.doc', docFb: 'Show the "by " attribution on community appliances and reference cycles.' }, + // Opt-in (audit STORE-14): cycles you share carry no uploader name unless this is on. + { key: 'share_name', def: false, labelKey: 'lbl.share_name', labelFb: 'Show my name on what I share', + docKey: 'setting.share_name.doc', docFb: 'Show your account name as the uploader of cycles you share. Off: shared without a name.' }, ]; // Height (CSS px) of the band above the Playground plot where event pin heads // sit, out of the busy curve area. Shared by _pgDrawCanvas and the pointer @@ -78,6 +86,10 @@ const _CANVAS_MIN_VIEW_S = 5; // Grab radius (CSS px) of the axis-pointer handle. 22 is a touch target, not a // pixel-hunt: HA draws its handle at size 20. const _AXIS_HANDLE_GRAB = 22; +// Minutes of recorder history the cycle dialog can draw either side of a cycle +// (item 513, discussion #463). Same list ws_set_user_prefs accepts; 0 = off. +const _CYCLE_CONTEXT_CHOICES = [0, 5, 10, 30, 60]; +const _CYCLE_CONTEXT_DEFAULT_MIN = 10; // Floor for the measured panel height (_syncPanelHeight), so a bad measurement in a // hidden/zero-height container cannot collapse the UI to nothing. @@ -99,198 +111,200 @@ const _NOTIFY_VARS = '{device}, {duration}, {minutes}, {program}, {energy_kwh}, const _SETTINGS_SECTIONS = [ { id: 'basic', label: 'Basic', intro: 'Core identity and the essentials most setups need.', groups: [ { sub: 'Device info', fields: [ - { key: 'name', label: 'Device Name', type: 'text', - doc: 'Display name shown in the HA integrations list and device registry.' }, - { key: 'device_type', label: 'Device Type', type: 'devicetype', - doc: 'Appliance class. Sets sensible detection defaults (thresholds, off-delay, end handling) tuned for that appliance type; change it only if the device was originally set up as the wrong type.' }, - { key: 'store_brand', label: 'Appliance Brand', type: 'storebrand', optional: true, - doc: 'Optional. The appliance brand, picked from the community catalog. Used to find and share matching reference recordings. Leave blank if you are not using online features.' }, - { key: 'store_model', label: 'Appliance Model', type: 'storemodel', optional: true, - doc: 'Optional. The appliance model, picked from the community catalog once a brand is set. If your model is not listed you can add it to the catalog.' }, + { key: 'name', label: 'Device Name', basic: true, type: 'text', + doc: 'Name shown in Home Assistant for this device.' }, + { key: 'device_type', label: 'Device Type', basic: true, type: 'devicetype', + doc: 'Sets detection defaults (thresholds, off delay, end handling) for this kind of appliance. Change it only if the device was set up as the wrong type.' }, + { key: 'store_brand', label: 'Appliance Brand', type: 'storebrand', optional: true, basic: true, + doc: 'Optional. Your appliance brand from the community catalog, used to find and share reference recordings. Leave blank if you do not use online features.' }, + { key: 'store_model', label: 'Appliance Model', type: 'storemodel', optional: true, basic: true, + doc: 'Optional. Your appliance model from the community catalog (pick a brand first). If it is not listed, you can add it.' }, ] }, { sub: 'Basic configuration', fields: [ - { key: 'power_sensor', label: 'Power Sensor', type: 'entity', domain: 'sensor', - doc: 'The sensor entity reporting live power in watts for this appliance (e.g. sensor.washer_power). All cycle detection is based on this signal.' }, + { key: 'power_sensor', label: 'Power Sensor', basic: true, type: 'entity', domain: 'sensor', + doc: 'The sensor that reports this appliance\'s live power in watts (e.g. sensor.washer_power). All detection is based on it.' }, { key: 'min_power', label: 'Minimum Power', unit: 'W', type: 'number', step: 0.1, min: 0, def: 2.0, basic: true, - doc: 'Absolute minimum power considered active. Readings below this are treated as 0 W (standby), filtering out the phantom load of smart plugs and standby LEDs.' }, + doc: 'Readings below this count as 0 W (standby). Filters out the idle draw of smart plugs and standby LEDs.' }, { key: 'off_delay', label: 'Off Delay', unit: 's', type: 'number', min: 0, def: 180, basic: true, - doc: 'Time to wait after power drops before declaring the cycle finished. If power resumes within this window the cycle continues seamlessly - this bridges pauses between wash stages. Dishwashers have long drying phases (power off for 20-60 min) so the off-delay must exceed that to keep the whole wash+dry as one cycle.' }, + doc: 'How long power must stay low before the cycle is finished. Power coming back within this time continues the cycle. Dishwashers can be idle 20-60 min while drying, so set it longer than that.' }, { key: 'linked_device', label: 'Group Under Device', type: 'device', - doc: 'Optionally nest this WashData device under another device (e.g. the smart plug) in the HA device registry, shown as "Connected via ...".' }, + doc: 'Optional. Show this WashData device under another device (e.g. the smart plug) in Home Assistant, as "Connected via ...".' }, + ] }, + // Energy and cost moved here from Notifications (audit UI-25): the price drives + // every cost figure, not just the peak-rate tip that stays with the messages. + { sub: 'Energy', fields: [ + { key: 'energy_sensor', label: 'Energy Meter Entity', type: 'entity', domain: 'sensor', optional: true, + doc: 'Optional kWh or Wh counter (total_increasing, e.g. the plug\'s own meter). Cycle energy is then read from it, which is more accurate than computing it from a slow power sensor. If its reading is unusable, the computed value is used.' }, + { key: 'energy_price_entity', label: 'Energy Price Entity', type: 'entity', domain: 'sensor', basic: true, notPrice: true, + doc: 'Sensor with the current price per kWh (e.g. a dynamic tariff). It overrides the static price below, so it must be a price, not the plug\'s power or energy sensor.' }, + { key: 'energy_price_static', label: 'Static Energy Price (per kWh)', type: 'number', step: 0.001, min: 0, basic: true, + doc: 'Fixed price per kWh, used when no price entity is set.' }, + { key: 'energy_price_dynamic', label: 'Time-Weighted Cost', type: 'checkbox', def: true, basic: true, + doc: 'Charge each cycle at the price at each moment it ran, instead of the price when it finished. Needs a price entity. Process History (Diagnostics) recosts older cycles.' }, ] }, ] }, { id: 'detection', label: 'Detection', intro: 'How a cycle is detected as starting, running and finishing.', groups: [ { sub: 'Thresholds & Gap', fields: [ - { key: 'start_threshold_w', label: 'Start Threshold', unit: 'W', type: 'number', step: 1, min: 0, basic: true, - doc: 'Power must rise above this level to confirm a cycle has started. Setting it too low causes false starts from standby power; too high and slow-starting programs (cold fill) are missed. The suggestion engine sets this just above the machine\'s observed lowest active power.' }, - { key: 'stop_threshold_w', label: 'Stop Threshold', unit: 'W', type: 'number', step: 0.1, min: 0, basic: true, - doc: 'Power must fall below this level before the off-delay countdown begins. Set it below the Start Threshold - the gap between them is the hysteresis band that prevents flicker. If set too high, low-power phases (rinse holds, anti-crease) falsely trigger the end sequence.' }, - { key: 'min_off_gap', label: 'Min Off Gap', unit: 's', type: 'number', min: 0, basic: true, - doc: 'If the machine powers off for less than this time, the on/off/on sequence is treated as one continuous cycle. Prevents soak programs (machine powers off for several minutes mid-wash) from being split into two separate cycles. Set it shorter than the gap between your back-to-back loads if you want those counted as separate cycles. Device-type defaults protect the typical intra-cycle pause for each appliance.' }, + { key: 'start_threshold_w', label: 'Start Threshold', unit: 'W', type: 'number', step: 1, min: 0, + doc: 'Power must rise above this to start a cycle. Too low: standby power causes false starts. Too high: slow-starting programs (cold fill) are missed.' }, + { key: 'stop_threshold_w', label: 'Stop Threshold', unit: 'W', type: 'number', step: 0.1, min: 0, + doc: 'Power must fall below this before the off delay starts counting. Keep it below the Start Threshold. Too high, and low-power phases (rinse holds, anti-crease) start the end countdown by mistake.' }, + { key: 'min_off_gap', label: 'Min Off Gap', unit: 's', type: 'number', min: 0, + doc: 'An off period shorter than this does not split a cycle, so soak pauses stay in one wash. Keep it shorter than the gap between back-to-back loads, or those merge into one cycle.' }, ] }, { sub: 'Cycle Start', fields: [ - { key: 'start_duration_threshold', label: 'Start Duration', unit: 's', type: 'number', min: 0, def: 5, - doc: 'Power must stay above the start threshold this long to confirm a real start, preventing split-second on/off toggles from starting a cycle.' }, - { key: 'start_energy_threshold', label: 'Start Energy', unit: 'Wh', type: 'number', step: 0.01, min: 0, def: 0.2, - doc: 'Energy (power x time) the appliance must consume before RUNNING. A brief high-power spike has very low energy and is ignored, preventing false starts.' }, - { key: 'completion_min_seconds', label: 'Min Cycle Duration', unit: 's', type: 'number', min: 0, def: 600, basic: true, - doc: 'Cycles shorter than this are discarded as ghost cycles (test runs, opening the door to add a sock).' }, + { key: 'start_duration_threshold', label: 'Start Duration', internal: true, unit: 's', type: 'number', min: 0, def: 5, + doc: 'Power must stay above the Start Threshold this long before a cycle starts, so brief on/off blips are ignored.' }, + { key: 'start_energy_threshold', label: 'Start Energy', internal: true, unit: 'Wh', type: 'number', step: 0.01, min: 0, def: 0.2, + doc: 'Energy the appliance must use before the cycle counts as running. A short power spike uses little energy, so it does not start a cycle.' }, + { key: 'completion_min_seconds', label: 'Min Cycle Duration', unit: 's', type: 'number', min: 0, def: 600, + doc: 'Cycles shorter than this (test runs, opening the door to add a sock) are saved as Interrupted and get no finish notification.' }, { key: 'curve_preroll_seconds', label: 'Curve Pre-roll', unit: 's', type: 'number', step: 10, min: 0, max: 600, def: 0, - doc: 'Seconds of readings from aborted start attempts that may be carried into the front of a cycle\'s curve. Machines that probe before settling (programme selection, door lock, first fill) can drop the first minutes of real activity from every curve. 0 turns this off. Note that enabling it moves the recorded start earlier, so cycles recorded before and after the change carry different durations for the same program until the older ones age out - expect the learned averages to drift for a while.' }, + doc: 'Adds readings from failed start attempts to the front of a cycle, for machines that probe before starting (door lock, first fill). 300 s covers most; 0 = off. Recorded starts move earlier, so learned durations drift for a while.' }, ] }, { sub: 'Cycle End', fields: [ { key: 'end_energy_threshold', label: 'End Energy', unit: 'Wh', type: 'number', step: 0.001, min: 0, def: 0.05, - doc: 'During the off-delay countdown, accumulated energy (watts x time) is compared to this threshold. If exceeded, the countdown resets - keeping anti-crease tumbles and dishwasher drying tails attached to the cycle instead of cutting them short. Raise it if cycles end too early during cool-down; lower it if detection is sluggish.' }, - { key: 'end_repeat_count', label: 'End Repeat Count', type: 'number', min: 1, def: 1, - 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.' }, + doc: 'Energy allowed during the off delay. More than this and the cycle keeps going, so tumbles and drying tails stay attached. Lower it if cycles end during a tail; raise it if finished cycles keep waiting.' }, { key: 'smart_termination_duration_ratio', label: 'Smart Termination Ratio', type: 'number', step: 0.01, min: 0.5, max: 1.0, - doc: 'How far into the matched program\'s expected duration a cycle must be before Smart Termination may end it early once power drops. The expected duration is the program\'s average, so on appliances whose runtime varies a lot - washers on cold winter vs warm summer inlet water, sensor-dry dryers, load-dependent programs - about half of all runs finish shorter than that average and never get the fast finish, ending only via the fallback timeout minutes late. Lower this (e.g. 0.85) on those machines so the early finish still fires; raise it toward 1.0 to be more conservative. Leave empty for the default (0.98, or 0.99 for dishwashers). It can only ever end a cycle earlier, never later, and never fires on an ambiguous or low-confidence match.' }, + doc: 'Fraction of the program\'s average duration a cycle must reach before Smart Termination may end it early. If run times vary (water temperature, sensor-dry, load size), lower it (e.g. 0.85). Leave empty for 0.98 (0.99 on dishwashers).' }, ] }, { sub: 'Power Off', fields: [ { key: 'power_off_threshold_w', label: 'Power Off Threshold', unit: 'W', type: 'number', step: 0.1, min: 0, def: 0, - doc: 'Optional power-based Off detection. When above 0, once a cycle has finished and power stays below this level for the Power Off Delay, the machine is treated as switched off and the state returns to Off. Leave at 0 to disable (the default). Set it above the true switched-off floor and below the Stop Threshold and your machine\'s finished-but-on standby draw; if it is not below the Stop Threshold it is ignored. When enabled it replaces the Progress Reset Delay for returning to Off, so a finished machine stays in Finished/Clean until it is actually powered off.' }, + doc: 'After a cycle, power below this for the Power Off Delay sets the state to Off, instead of the Progress Reset Delay doing it. Set it above the switched-off draw, below standby and the Stop Threshold. 0 disables it (default).' }, { key: 'power_off_delay', label: 'Power Off Delay', unit: 's', type: 'number', min: 0, def: 30, - doc: 'How long power must stay below the Power Off Threshold after a cycle finishes before the state returns to Off. Only used when the Power Off Threshold is above 0. Checked on the background cadence, so the effective delay rounds up to the next state-expiry tick.' }, + doc: 'How long power must stay below the Power Off Threshold after a cycle before the state returns to Off. Only used when that threshold is above 0. Checked periodically, so the real wait can be slightly longer.' }, ] }, { sub: 'Signal Processing', fields: [ - { key: 'sampling_interval', label: 'Sampling Interval', unit: 's', type: 'number', min: 1, def: 30, - doc: 'Expected time between sensor readings - used to size the smoothing window and start debounce correctly. Every sensor update is captured regardless of this value; it only calibrates the downstream calculations. The suggestion engine measures your sensor\'s actual cadence from past cycles and sets this automatically.' }, - { key: 'smoothing_window', label: 'Smoothing Window', type: 'number', min: 1, def: 2, - doc: 'How much the raw power signal is smoothed. Low (2) is responsive but noisy; high (5) smooths spikes but adds lag.' }, + { key: 'sampling_interval', label: 'Sampling Interval', internal: true, unit: 's', type: 'number', min: 1, def: 30, + doc: 'Minimum time between processed readings. Readings that arrive sooner are skipped (low readings during a cycle are always kept). Set it to your plug\'s reporting interval or lower; a higher value only throws readings away.' }, ] }, ] }, { id: 'matching', label: 'Matching', intro: 'How finished cycles are matched to learned profiles and labelled.', notDeviceTypes: ['other'], groups: [ { sub: 'Match Scoring', fields: [ { key: 'profile_match_threshold', label: 'Match Threshold', type: 'number', step: 0.01, min: 0, max: 1, def: 0.4, - doc: 'Minimum similarity score (0-1) required at cycle end to accept a program identification. Raise it to reduce wrong identifications; lower it if your machine\'s programs are not being matched. Default 0.4 is a conservative starting point.' }, - { key: 'profile_unmatch_threshold', label: 'Unmatch Threshold', type: 'number', step: 0.01, min: 0, max: 1, def: 0.35, - doc: 'If a live mid-cycle match drops below this score, the tentative identification is cleared. Keep it a little below the Match Threshold so a brief dip in similarity does not flip the display back to unmatched.' }, - { key: 'profile_match_interval', label: 'Match Interval', unit: 's', type: 'number', min: 0, - doc: 'How often to attempt profile matching during a running cycle. Default 300 s (5 minutes) balances detection speed and CPU.' }, + doc: 'Live match score a cycle needs before Smart Termination or the anti-crease finish may end it early. It does not change the shown program. Raise it if weak matches end cycles early; lower it if clear matches wait the off delay.' }, + { key: 'profile_unmatch_threshold', label: 'Unmatch Threshold', internal: true, type: 'number', step: 0.01, min: 0, max: 1, def: 0.35, + doc: 'If the live match score drops below this, the current program guess is cleared. Keep it a little below the Match Threshold so a brief dip does not reset it.' }, + { key: 'profile_match_interval', label: 'Match Interval', internal: true, unit: 's', type: 'number', min: 0, + doc: 'How often to match a running cycle against profiles. Default 300 s (5 min).' }, ] }, { sub: 'Duration Gates', fields: [ - { key: 'profile_match_min_duration_ratio', label: 'Min Duration Ratio', type: 'number', step: 0.01, min: 0, max: 1, def: 0.1, - doc: 'Minimum cycle length relative to the profile. 0.9 means a cycle must be at least 90% of the profile duration to match.' }, - { key: 'profile_match_max_duration_ratio', label: 'Max Duration Ratio', type: 'number', step: 0.01, min: 0, def: 1.8, - doc: 'Maximum cycle length relative to the profile. 1.3 means a cycle must be under 130% of the profile duration to match.' }, - { key: 'profile_duration_tolerance', label: 'Profile Duration Tolerance', type: 'number', step: 0.01, min: 0, max: 1, def: 0.25, - doc: 'The +/- band around a profile average duration used during matching. 0.25 means a 60 min profile matches 45-75 min cycles.' }, - { key: 'duration_tolerance', label: 'Estimate Tolerance', type: 'number', step: 0.01, min: 0, max: 1, def: 0.1, - doc: 'Tolerance for time-remaining estimates (learning feedback, not matching). If the actual duration is within +/-X% of the estimate it counts as a good match.' }, + { key: 'profile_match_min_duration_ratio', label: 'Min Duration Ratio', internal: true, type: 'number', step: 0.01, min: 0, max: 1, def: 0.1, + doc: 'A program is only considered after the cycle has run this fraction of its typical duration. 0.10: a 2-hour program becomes a candidate after 12 minutes. Not applied in a cycle\'s first 15 minutes.' }, + { key: 'profile_match_max_duration_ratio', label: 'Max Duration Ratio', internal: true, type: 'number', step: 0.01, min: 0, def: 1.8, + doc: 'A program is dropped once the cycle has run longer than this multiple of its typical duration. 1.8: a cycle 80% longer than the program no longer matches it.' }, + { key: 'duration_tolerance', label: 'Estimate Tolerance', internal: true, type: 'number', step: 0.01, min: 0, max: 1, def: 0.1, + doc: 'Learning feedback only: a cycle within +/-X% of its estimated duration counts as a good estimate. Does not affect matching.' }, ] }, { sub: 'Profile Evidence', fields: [ { key: 'profile_evidence_sources', label: 'Cycles that shape a program', type: 'checkboxlist', def: ['real_cycles', 'reference_cycles', 'backfill_cycles'], choices: [['real_cycles', 'Cycles this machine ran'], ['reference_cycles', 'Downloaded from the community store'], ['backfill_cycles', 'Found in imported power history']], - doc: 'Which cycles are used to build each program\'s power curve, and to match a finished cycle against it. Unticking a kind stops it shaping your programs without deleting anything - the cycles stay in your Cycles list and can still be labelled or removed. Useful if you do not trust imported data. Statistics are unaffected: they always count only the cycles this machine actually ran. Unticking everything is ignored, since a program with no cycles behind it could never match.' }, + doc: 'Which kinds of cycles shape each program\'s curve and matching. Unticking a kind deletes nothing; useful if you do not trust imported data. Statistics always count only this machine\'s cycles. Unticking all is ignored.' }, ] }, { sub: 'Auto-Labeling', fields: [ { key: 'auto_label_confidence', label: 'Auto-Label Confidence', type: 'number', step: 0.01, min: 0, max: 1, def: 0.9, - doc: 'If the match score at cycle end is at or above this, the program is labeled automatically without any confirmation prompt. Raise it to require higher certainty before auto-labeling; lower it to automate more. Works in conjunction with Learning Confidence below it.' }, - { key: 'learning_confidence', label: 'Learning Confidence', type: 'number', step: 0.01, min: 0, max: 1, def: 0.6, - doc: 'If the match score falls between this and Auto-Label Confidence, WashData flags the finished cycle for review in the Cycles queue so you can verify the identified program. Below this score the match is too uncertain to surface. Must be kept below Auto-Label Confidence.' }, + doc: 'Match score at cycle end at or above which the program is labelled without asking. Raise it for more certainty; lower it to label more cycles automatically. Keep it above Learning Confidence.' }, + { key: 'learning_confidence', label: 'Learning Confidence', internal: true, type: 'number', step: 0.01, min: 0, max: 1, def: 0.6, + doc: 'Lowest match score for labelling a finished cycle. A clear winner is labelled; a close call goes to the review queue in Cycles. Below this, nothing is labelled or asked. Keep it below Auto-Label Confidence.' }, ] }, ] }, - { id: 'phase_eta', label: 'Time Remaining', intro: 'Phase-aware time-remaining, for machines whose cycle length depends on temperature or spin.', onlyDeviceTypes: ['washing_machine', 'washer_dryer'], fields: [ - { key: 'enable_phase_matching', label: 'Phase-aware time remaining', type: 'checkbox', def: false, - doc: 'Break each running cycle into phases (heating, wash, spin) and budget the time remaining per phase, blended with the classic estimate - leaning on the phase budget early in the cycle and the classic estimate near the end. This personalises the countdown to how long your machine actually heats and runs, which is most noticeable in the first half of a cycle. Off = the classic estimate only. Only the time-remaining display is affected; program matching and cycle detection are unchanged.' }, - ] }, { id: 'timing', label: 'Timing & Watchdog', intro: 'Background cadence, the offline watchdog and housekeeping.', groups: [ { sub: 'Watchdog', fields: [ - { key: 'watchdog_interval', label: 'Watchdog Interval', unit: 's', type: 'number', min: 1, def: 30, - doc: 'How often the background watchdog checks for stalled sensors and elapsed timeouts. Default 30 s.' }, - { key: 'no_update_active_timeout', label: 'No-Update Timeout', unit: 's', type: 'number', min: 0, def: 600, - doc: 'If no power updates arrive for this long while running, assume the plug dropped offline and force-stop to avoid a zombie cycle. Default 600 s allows for cloud or mesh lag.' }, + { key: 'watchdog_interval', label: 'Watchdog Interval', internal: true, unit: 's', type: 'number', min: 1, def: 30, + doc: 'How often the background check looks for stalled sensors and timeouts. Default 30 s.' }, + { key: 'no_update_active_timeout', label: 'No-Update Timeout', internal: true, unit: 's', type: 'number', min: 0, def: 600, + doc: 'If no power update arrives for this long during a cycle, assume the plug went offline and force-stop the cycle. The default 600 s allows for cloud or mesh delays.' }, ] }, { sub: 'Housekeeping', fields: [ { key: 'progress_reset_delay', label: 'Progress Reset Delay', unit: 's', type: 'number', min: 0, def: 1800, - doc: 'After finishing, hold progress at 100% for this long so Completed is visible on dashboards before resetting to Idle.' }, + doc: 'After a cycle, keep progress at 100% this long before resetting to Idle.' }, { key: 'auto_maintenance', label: 'Auto Maintenance (nightly cleanup)', type: 'checkbox', def: true, - doc: 'Run nightly housekeeping: rebuild profile envelopes, recompute cycle health, prune debug traces and retain the most recent cycles.' }, + doc: 'Nightly cleanup: label old cycles that now clearly match a program, rebuild program curves, refresh anomaly marks and cycle health, and remove debug traces if Save Debug Traces is off. Cycles and their power traces are always kept.' }, { key: 'power_profile_interval_min', label: 'Power Profile Interval', unit: 'min', type: 'number', min: 1, def: 15, - doc: 'Bucket size for the per-profile power_profile sensor attribute (the flat per-slot average-watts array consumed by external planners such as EMHASS and tibber_prices). Smaller buckets keep short power spikes sharp; larger buckets smooth the shape. Default 15 min. Read-time only; does not affect detection.' }, + doc: 'Bucket size of the power_profile attribute (average watts per slot) used by planners such as EMHASS and tibber_prices. Smaller keeps short spikes; larger smooths the curve. Default 15 min. Does not affect detection.' }, ] }, { sub: 'Debug', fields: [ - { key: 'expose_debug_entities', label: 'Expose Debug Entities', type: 'checkbox', - doc: 'Publish extra diagnostic HA entities (match confidence, ambiguity, state internals). Off keeps the entity list clean for normal use.' }, + { key: 'expose_debug_entities', label: 'Expose Debug Entities', internal: true, type: 'checkbox', + doc: 'Add diagnostic entities (match confidence, ambiguity, internal state). Off keeps the entity list clean.' }, { key: 'save_debug_traces', label: 'Save Debug Traces', type: 'checkbox', - doc: 'Store the full power trace and matching debug data for each cycle. Useful for troubleshooting but increases storage size.' }, + doc: 'Also store the matcher\'s debug data (every candidate\'s scores) with each cycle, for troubleshooting. Adds about 20 KB per cycle.' }, ] }, ] }, - { id: 'anti_wrinkle', label: 'Anti-Wrinkle', intro: 'Anti-wrinkle / anti-crease mode detects low-power tumble pulses after the main phase and keeps them attached to the finished cycle instead of reading them as new cycles.', onlyDeviceTypes: ['washing_machine', 'dryer', 'washer_dryer'], fields: [ - { key: 'anti_wrinkle_enabled', label: 'Enable Anti-Wrinkle Detection', type: 'checkbox', - 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.' }, + { id: 'anti_wrinkle', label: 'Anti-Wrinkle', intro: 'Keeps a dryer\'s low-power tumble pulses after the main phase attached to the finished cycle.', onlyDeviceTypes: ['washing_machine', 'dryer', 'washer_dryer'], fields: [ + { key: 'anti_wrinkle_enabled', label: 'Enable Anti-Wrinkle Detection', basic: true, type: 'checkbox', + doc: 'Keep a dryer\'s short low-power tumble pulses after the main heat phase with the finished cycle, instead of reading them as new cycles.' }, { key: 'anti_wrinkle_max_power', label: 'Max Anti-Wrinkle Power', unit: 'W', type: 'number', step: 10, min: 0, def: 400, - doc: 'A pulse above this power is treated as a real new cycle, not an anti-wrinkle tumble. Set just above the tumble-pulse power.' }, + doc: 'A pulse above this power counts as a real new cycle, not a tumble. Set it just above your tumble-pulse power.' }, { key: 'anti_wrinkle_max_duration', label: 'Max Duration', unit: 's', type: 'number', min: 0, def: 60, - doc: 'Pulses longer than this are treated as a real cycle rather than an anti-wrinkle tumble.' }, - { key: 'anti_wrinkle_exit_power', label: 'Exit Power Threshold', unit: 'W', type: 'number', step: 0.1, min: 0, def: 0.8, - doc: 'Power must fall below this between pulses for anti-wrinkle mode to stay active.' }, + doc: 'A pulse longer than this counts as a real cycle, not an anti-wrinkle tumble.' }, + { key: 'anti_wrinkle_exit_power', label: 'Exit Power Threshold', internal: true, unit: 'W', type: 'number', step: 0.1, min: 0, def: 0.8, + doc: 'In anti-wrinkle mode, power below this counts as quiet between tumble pulses, and quiet for longer than the Max Pulse Gap ends the mode. The Stop Threshold is used when it is higher, so this only changes anything when set above the Stop Threshold.' }, { key: 'anti_wrinkle_idle_timeout', label: 'Max Pulse Gap', unit: 's', type: 'number', step: 30, min: 0, def: 120, - doc: 'How long the machine may stay quiet between two tumble pulses before anti-wrinkle mode ends. Set it above the longest gap your dryer leaves between pulses, otherwise every later pulse is read as a false start.' }, + doc: 'How long the dryer may stay quiet between tumble pulses before anti-wrinkle mode ends. Set it above your dryer\'s longest gap, or later pulses are read as false starts.' }, { key: 'anti_crease_finalize_ratio', label: 'Anti-Crease Finalize Ratio', type: 'number', step: 0.01, min: 0.5, max: 1.0, def: 0.98, - doc: 'Fraction of the matched program\'s expected duration a cycle must reach before the anti-crease tumble tail may be finalised. Lower it on a dryer whose sensor-dry runtime follows the load, so its tail is recognised instead of sitting until the fallback timeout. Leave washing machines at the default: there this fraction is what keeps a quiet mid-wash phase from being mistaken for the tail. Separate from the Smart Termination Ratio, which gates a different check.' }, + doc: 'Fraction of the program\'s average duration a cycle must reach before its low-power tumbling counts as the anti-crease tail. Lower it (e.g. 0.75) on load-sensing dryers. Keep washers at the default (0.98), or a wash can end halfway.' }, ] }, - { id: 'dishwasher', label: 'Dishwasher', intro: 'End-of-cycle handling for dishwashers, which typically finish with a long near-silent drying phase before a short final drain.', onlyDeviceTypes: ['dishwasher'], fields: [ + { id: 'dishwasher', label: 'Dishwasher', intro: 'End handling for dishwashers, which often dry quietly for a long time before a short final drain.', onlyDeviceTypes: ['dishwasher'], fields: [ { key: 'dishwasher_end_spike_quiet_release', label: 'Passive-Dry Quiet Release', unit: 's', type: 'number', step: 60, min: 0, def: 600, - doc: 'Once the cycle passes its expected duration, how long the dishwasher must stay quiet (below the Stop Threshold) before WashData stops waiting for a final drain and ends the cycle. Raise it if your machine has a long silent drying phase before a late final drain that is being missed - a wider window lets the learned duration follow seasonal drift (colder inlet water = longer cycles) instead of locking to the old average. It only ever shortens the wait relative to the internal 30-minute end-spike cap, never extends it.' }, + doc: 'After the expected duration, how long the dishwasher must stay below the Stop Threshold before WashData stops waiting for the final drain. Raise it if a late drain after a long dry is missed. The wait never exceeds 30 minutes.' }, ] }, - { id: 'delay', label: 'Delay Start', intro: 'Delayed-start detection identifies when an appliance is powered but has not yet begun its cycle.', fields: [ + { id: 'delay', label: 'Delay Start', intro: 'Detects an appliance that is on and waiting for a delayed start.', fields: [ { key: 'delay_start_detect_enabled', label: 'Enable Delay-Start Detection', type: 'checkbox', - doc: 'Detect when the appliance is powered on and waiting (delayed start / standby) but has not begun its cycle, so standby draw is not mistaken for a running cycle.' }, + doc: 'Detect an appliance that is on and waiting for a delayed start, so its standby draw is not taken for a running cycle.' }, { key: 'delay_confirm_seconds', label: 'Confirm Seconds', unit: 's', type: 'number', min: 0, def: 60, - doc: 'Power must stay in the standby band for this long before the appliance is treated as waiting-to-start rather than running.' }, + doc: 'Power must stay in the standby band this long before the appliance counts as waiting to start.' }, { key: 'delay_timeout_hours', label: 'Timeout Hours', unit: 'h', type: 'number', step: 0.5, min: 0, def: 8.0, - doc: 'Stop waiting in delayed-start mode after this many hours and return to idle, so a machine left powered but never started does not wait forever.' }, + doc: 'Give up waiting for a delayed start after this many hours and return to idle.' }, ] }, - { id: 'triggers', label: 'Triggers & Door', intro: 'Optional external signals: an end trigger, a door sensor, a pause switch, and the unload reminder.', groups: [ + { id: 'triggers', label: 'Triggers & Door', intro: 'Optional extras: end trigger, door sensor, pause switch and unload reminder.', groups: [ { sub: 'External End Trigger', fields: [ { key: 'external_end_trigger_enabled', label: 'Enable External End Trigger', type: 'checkbox', - doc: 'Let an external binary sensor signal the end of a cycle, in addition to the built-in power-based detection.' }, + doc: 'Let a binary sensor signal the end of a cycle, in addition to power-based detection.' }, { key: 'external_end_trigger', label: 'External Trigger Entity', type: 'entity', domain: 'binary_sensor', - doc: 'Binary sensor whose state change marks the cycle end (e.g. an appliance "finished" contact or a companion integration).' }, + doc: 'Binary sensor whose change marks the cycle end (e.g. a "finished" contact or another integration).' }, { key: 'external_end_trigger_inverted', label: 'Invert External Trigger (trigger on OFF)', type: 'checkbox', - doc: 'Treat the trigger sensor turning OFF (rather than ON) as the end-of-cycle signal.' }, + doc: 'Treat the sensor turning OFF (not ON) as the end signal.' }, ] }, { sub: 'Door & Pause', fields: [ - { key: 'door_sensor_entity', label: 'Door Sensor Entity', type: 'entity', domain: 'binary_sensor', - doc: 'Optional door binary sensor. Used to detect when the appliance has been opened/unloaded after a cycle.' }, + { key: 'door_sensor_entity', label: 'Door Sensor Entity', basic: true, type: 'entity', domain: 'binary_sensor', + doc: 'Optional door sensor. Shows when the appliance was opened to unload after a cycle.' }, { key: 'door_opens_at_end', label: 'Door Opens Automatically At End', type: 'checkbox', - doc: 'For dishwashers that pop the door open at the end of the cycle to dry (AirDry and similar). With this on, a door-open on a running cycle no longer pauses it forever; instead, if the door stays open for the dwell below, WashData treats the cycle as finished. A brief open (adding an item) is ignored. Requires a Door Sensor Entity.' }, + doc: 'For dishwashers that open the door at the end to dry (e.g. AirDry). A door left open for the dwell time below ends the cycle instead of pausing it; a brief open is ignored. Needs a Door Sensor Entity.' }, { key: 'door_end_dwell_seconds', label: 'Door-Open End Dwell', unit: 's', type: 'number', min: 1, def: 60, - doc: 'How long the door must stay open before WashData ends the cycle, when "Door Opens Automatically At End" is on. Long enough to ignore quickly adding a dish (default 60 s), short enough to end promptly once the machine pops the door.' }, + doc: 'With "Door Opens Automatically At End" on: how long the door must stay open before the cycle ends. Long enough to ignore adding a dish (default 60 s).' }, { key: 'pause_cuts_power', label: 'Pause Also Cuts Power (via switch)', type: 'checkbox', - doc: 'When a cycle is paused, also switch off the Switch Entity below. Only for appliances whose plug can safely be cut mid-cycle.' }, + doc: 'When a cycle is paused, also switch off the Switch Entity below. Only for appliances that can safely lose power mid-cycle.' }, { key: 'switch_entity', label: 'Switch Entity', type: 'entity', domain: 'switch', - doc: 'Optional switch toggled off on pause and back on when resuming, used together with "Pause also cuts power".' }, + doc: 'Optional switch turned off on pause and back on when resuming, with "Pause Also Cuts Power".' }, ] }, { sub: 'Unload Reminder', fields: [ { key: 'notify_unload_delay_minutes', label: 'Unload Nag Delay', unit: 'min', type: 'number', min: 0, def: 60, basic: true, - doc: 'Minutes after a cycle ends before sending the still-waiting "unload the machine" reminder. Set 0 to disable the reminder.' }, + doc: 'Minutes after a cycle ends before the "unload the machine" reminder. 0 disables it.' }, { key: 'notify_unload_repeat', label: 'Repeat Until Unloaded', type: 'checkbox', - doc: 'Keep re-sending the unload reminder every "Unload Nag Delay" minutes until the unload is confirmed or you tap "Stop reminding" on the notification. Needs a way to confirm the unload: a Door Sensor Entity, an Unload Confirmation Entity, or the "Confirm Unload Manually" option below. The dismiss button works on Home Assistant companion-app (mobile) notifications.' }, + doc: 'Resend the unload reminder every "Unload Nag Delay" minutes until the unload is confirmed or you tap "Stop reminding" (mobile app). Needs a Door Sensor Entity, an Unload Confirmation Entity, or "Confirm Unload Manually".' }, { key: 'unload_confirm_entity', label: 'Unload Confirmation Entity', type: 'entity', domains: ['event', 'button', 'input_button', 'binary_sensor', 'sensor', 'input_boolean', 'switch', 'scene', 'tag'], - doc: 'Optional entity whose activation means the load has been taken out, for machines that cannot have a door sensor. Any kind works: a Zigbee or NFC button, an input_button helper, a motion sensor in front of the machine, a scene. Every state change counts as a confirmation except switching off and going unavailable. Setting this also enables the unload reminder on a device with no Door Sensor Entity.' }, + doc: 'Optional. Any entity that confirms the load was taken out, for machines without a door sensor (a button, input_button, motion sensor, scene). Every state change counts except turning off or unavailable. Also enables the unload reminder.' }, { key: 'unload_track_without_door', label: 'Confirm Unload Manually', type: 'checkbox', - doc: 'Run the unload reminder on a device with no Door Sensor Entity and no Unload Confirmation Entity, and confirm the unload from your own automation instead: press the "Mark Unloaded" button entity, or call the ha_washdata.mark_unloaded service. Ignored when a Door Sensor Entity is set, which already provides the signal.' }, + doc: 'Run the unload reminder without a door sensor or confirmation entity. Confirm the unload from your own automation with the "Mark Unloaded" button or the ha_washdata.mark_unloaded service. Ignored when a Door Sensor Entity is set.' }, { key: 'pump_stuck_duration', label: 'Pump Stuck Duration', unit: 's', type: 'number', min: 0, def: 1800, - onlyDeviceType: 'pump', doc: 'Seconds a pump may run continuously before it is flagged as possibly stuck (fires the stuck-pump event).' }, + onlyDeviceType: 'pump', doc: 'How long a pump may run nonstop before the stuck-pump event fires.' }, ] }, ] }, { id: 'notifications', label: 'Notifications', groups: [ { sub: 'Services', fields: [ { key: 'notify_start_services', label: 'Start Services', type: 'entitylist', domain: 'notify', placeholder: 'add a notify service…', basic: true, - doc: 'notify.* services called when a cycle starts. Add one per target (phone, dashboard, etc.); leave empty for no start notification.' }, + doc: 'notify.* services to call when a cycle starts (phone, dashboard, ...). Leave empty for none.' }, { key: 'notify_finish_services', label: 'Finish Services', type: 'entitylist', domain: 'notify', placeholder: 'add a notify service…', basic: true, - doc: 'notify.* services called when a cycle finishes. Add one per target; leave empty for no finish notification.' }, - { key: 'notify_live_services', label: 'Live Progress Services', type: 'entitylist', domain: 'notify', placeholder: 'add a notify service…', - doc: 'notify.* services called for live progress updates while a cycle runs. Leave empty to disable live-progress notifications.' }, + doc: 'notify.* services to call when a cycle finishes. Leave empty for none.' }, + { key: 'notify_live_services', label: 'Live Progress Services', basic: true, type: 'entitylist', domain: 'notify', placeholder: 'add a notify service…', + doc: 'notify.* services for live progress updates while a cycle runs. Leave empty to turn them off.' }, { key: 'notify_people', label: 'People (for Only When Home)', type: 'entitylist', domain: 'person', placeholder: 'add a person…', - doc: 'person.* entities used by "Notify Only When Home" to decide whether anyone is home.' }, + doc: 'person.* entities that "Notify Only When Home" checks.' }, { key: 'notify_only_when_home', label: 'Notify Only When Home', type: 'checkbox', - doc: 'Only send notifications when at least one of the linked people (above) is home.' }, + doc: 'Only notify when at least one of the people above is home.' }, { key: 'notify_fire_events', label: 'Fire HA Events for Notifications', type: 'checkbox', def: true, - doc: 'Also fire ha_washdata_* events on cycle start/finish so you can build your own automations.' }, + doc: 'Also fire ha_washdata_* events on cycle start and finish for your own automations.' }, ] }, { sub: 'Timing', fields: [ { key: 'notify_before_end_minutes', label: 'Pre-End Alert', unit: 'min', type: 'number', min: 0, def: 0, @@ -298,80 +312,70 @@ const _SETTINGS_SECTIONS = [ { key: 'notify_live_interval_seconds', label: 'Live Update Interval', unit: 's', type: 'number', min: 30, def: 300, doc: 'How often live-progress notifications are refreshed while a cycle runs.' }, { key: 'notify_live_overrun_percent', label: 'Live Overrun % Before Alert', unit: '%', type: 'number', min: 0, def: 20, - doc: 'If a cycle runs past its estimate by more than this percentage, send an overrun alert.' }, + doc: 'Live updates stop once a cycle runs this percent past its expected duration (20 = up to 1.2x the expected time).' }, { key: 'notify_live_chronometer', label: 'Use Live Chronometer', type: 'checkbox', - doc: 'Show a live-updating countdown timer in the notification (on platforms that support it) instead of a static estimate.' }, + doc: 'Show a live countdown timer in the notification (where supported) instead of a fixed estimate.' }, { key: 'notify_live_silent', label: 'Silent Live Updates', type: 'checkbox', def: true, - 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.' }, + doc: 'iOS only. Update live progress silently, at a battery-saving priority that iOS may batch. The first update and the start/finish notifications still alert. Off: every update alerts.' }, { key: 'notify_live_sticky', label: 'Keep Live Notification On Tap', type: 'checkbox', - 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.' }, + doc: 'Android only. Keep the live progress notification when tapped. Off: a tap dismisses it.' }, { key: 'notify_live_click_action', label: 'Notification Tap Target', type: 'text', optional: true, - 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.' }, + doc: 'What a tap on this appliance\'s notification opens. Blank: the WashData panel on this appliance. Enter a dashboard path (e.g. /lovelace/laundry) or URL, or "none" for no tap action.' }, { key: 'notify_timeout_seconds', label: 'Auto-Dismiss After', unit: 's', type: 'number', min: 0, def: 0, - doc: 'Automatically dismiss the notification after this many seconds (on platforms that support it). 0 keeps it until dismissed manually.' }, + doc: 'Dismiss the notification after this many seconds (where supported). 0 keeps it until you dismiss it.' }, ] }, { sub: 'Messages', fields: [ { key: 'notify_title', label: 'Notification Title', type: 'text', def: 'WashData: {device}', - doc: `Notification title. Template variables: ${_NOTIFY_VARS}.` }, + doc: 'Notification title. Only {device} is substituted; any other variable falls back to the default title.' }, { key: 'notify_icon', label: 'Notification Icon', type: 'text', def: '', - 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.' }, + doc: 'Small icon on the notification (e.g. mdi:washing-machine). Sent only to mobile_app targets, not notify groups. On iOS it replaces the app icon and needs companion app 2026.8 or newer. Blank: platform default.' }, { key: 'notify_icon_color', label: 'Notification Colour', type: 'color', def: '', placeholder: '#03A9F4', - doc: 'Accent colour for this appliance\'s notifications, so a washer, a dryer and a dishwasher are tellable apart at a glance. iOS tints the icon and the Live Activity progress bar on the Lock Screen and in the Dynamic Island. Android is sent the same colour, but what it does with it is up to the phone: some tint the icon, and many (Samsung One UI especially) ignore it entirely. Hex (e.g. #03A9F4) is understood by both. Leave blank for the platform default.' }, + doc: 'Accent colour for this appliance\'s notifications, so washer, dryer and dishwasher are easy to tell apart. On iOS it fills the circle behind the icon and colours the Live Activity bar. Many Android phones (Samsung especially) ignore it. Use hex, e.g. #03A9F4. Blank: platform default.' }, { key: 'notify_start_message', label: 'Start Message', type: 'textarea', def: '{device} started.', - doc: `Body sent when a cycle starts. Template variables: ${_NOTIFY_VARS}.` }, - { key: 'notify_finish_message', label: 'Finish Message', type: 'textarea', def: '{device} finished. Duration: {duration}m.', basic: true, - doc: `Body sent when a cycle finishes. Template variables: ${_NOTIFY_VARS}. {time_finished} and {vs_typical} are most useful here.` }, + doc: 'Body sent when a cycle starts. Template variables: {device}, {program}. Any other variable falls back to the default text.' }, + { key: 'notify_finish_message', label: 'Finish Message', type: 'textarea', def: '{device} finished. Duration: {duration} min.', basic: true, + doc: 'Body sent when a cycle finishes. Variables: {device}, {program}, {duration} (minutes), {duration_hm} (1 h 05 min), {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}, {status} (completed or force_stopped; interrupted cycles send nothing). Any other variable falls back to the default text.' }, { key: 'notify_pre_complete_message', label: 'Pre-Complete Message', type: 'textarea', def: '{device}: Less than {minutes} minutes remaining.', - doc: `Body of the pre-end / almost-done alert. Template variables: ${_NOTIFY_VARS}.` }, + doc: 'Text of each live progress update. Template variables: {device}, {program}, {minutes} (minutes left). Any other variable falls back to the default text.' }, { key: 'notify_reminder_message', label: 'Reminder Message', type: 'textarea', def: '', - doc: `Body of the still-waiting unload reminder. Blank uses the built-in default. Template variables: ${_NOTIFY_VARS}.` }, + doc: 'Text of the one-time Pre-End Alert. Template variables: {device}, {program}, {minutes} (the Pre-End Alert setting). Any other variable falls back to the default text.' }, { key: 'notify_channel', label: 'Android Channel (start/live)', type: 'text', def: '', placeholder: 'e.g. WashData', suggestions: ['WashData', 'WashData Status', 'Appliance Status'], - doc: 'Android notification channel name for start/live messages (controls per-channel sound and priority on the mobile app). Blank uses the companion app default.' }, + doc: 'Android channel for start and live messages; the channel sets sound and priority. Blank: app default.' }, { key: 'notify_finish_channel', label: 'Android Channel (finish)', type: 'text', def: '', placeholder: 'e.g. WashData Finished', suggestions: ['WashData Finished', 'WashData Alerts', 'Appliance Finished'], - doc: 'Android notification channel name for the finish message. Blank reuses the start/live channel.' }, - ] }, - { sub: 'Energy', fields: [ - { key: 'energy_sensor', label: 'Energy Meter Entity', type: 'entity', domain: 'sensor', optional: true, - doc: 'Optional cumulative energy counter (total_increasing kWh/Wh, e.g. the plug\'s own lifetime meter). When set, each cycle\'s reported energy is taken from this counter\'s start-to-end delta, which avoids the under-counting you get from integrating a slow-reporting power sensor. Falls back to the integrated value if the reading is missing, its unit is unknown, or the delta is not positive. Leave blank to keep integrating the power sensor.' }, - { key: 'energy_price_entity', label: 'Energy Price Entity', type: 'entity', domain: 'sensor', basic: true, notPrice: true, - 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.' }, - { key: 'energy_price_static', label: 'Static Energy Price (per kWh)', type: 'number', step: 0.001, min: 0, basic: true, - doc: 'Fixed price per kWh used for cost figures when no live price entity is set above.' }, - { key: 'energy_price_dynamic', label: 'Time-Weighted Cost', type: 'checkbox', def: true, basic: true, - 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.' }, + doc: 'Android channel for the finish message. Blank reuses the start/live channel.' }, { key: 'peak_rate_threshold', label: 'Peak-Rate Threshold (per kWh)', type: 'number', step: 0.001, min: 0, def: 0, clearable: true, - doc: 'When a cycle starts and the current price per kWh is at or above this value, append a peak-rate tip to the start notification. 0 or blank disables the tip.' }, + doc: 'If the price per kWh is at or above this when a cycle starts, add a peak-rate tip to the start notification. 0 or blank disables it.' }, { key: 'peak_rate_message', label: 'Peak-Rate Message', type: 'text', def: '', placeholder: 'Running at peak rate ({price}/kWh).', - doc: 'Optional custom text for the peak-rate tip appended to the start notification. Template variables: {device}, {price}. Blank uses the built-in default.' }, + doc: 'Custom text for the peak-rate tip. Template variables: {device}, {price}. Blank uses the default.' }, ] }, { sub: 'Cycle Timers', fields: [ { key: 'notify_cycle_timers', label: 'Cycle Timers', type: 'timerlist', - doc: 'Notifications at specific minutes into a cycle (e.g. to add softener). Message supports {device}, {program}, {minutes}. Enable Auto-pause to pause at that point and receive an interactive notification with a Resume button; resume via the panel, the pause/resume service, or the notification action.' }, + doc: 'Notifications at set minutes into a cycle (e.g. to add softener). Message supports {device}, {program}, {minutes}. With Auto-pause, the cycle also pauses there and the notification gets a Resume button.' }, ] }, { sub: 'Quiet Hours & Milestones', fields: [ - { key: 'notify_quiet_start_hour', label: 'Quiet Hours Start', unit: 'h', type: 'number', min: 0, max: 23, clearable: true, - doc: 'Start of a do-not-disturb window (0-23). Finish, reminder and clean-laundry notifications that would fire during quiet hours are held and delivered when the window ends. Leave blank to disable. Supports windows that cross midnight (e.g. start 22, end 7).' }, - { key: 'notify_quiet_end_hour', label: 'Quiet Hours End', unit: 'h', type: 'number', min: 0, max: 23, clearable: true, - doc: 'End of the do-not-disturb window (0-23). Held notifications are delivered at this hour. Leave blank to disable.' }, + { key: 'notify_quiet_start_hour', label: 'Quiet Hours Start', basic: true, unit: 'h', type: 'number', min: 0, max: 23, clearable: true, + doc: 'Start of a do-not-disturb window (0-23). Finish, reminder and clean-laundry notifications in this window are held until it ends. Can cross midnight (e.g. 22 to 7). Blank disables it.' }, + { key: 'notify_quiet_end_hour', label: 'Quiet Hours End', basic: true, unit: 'h', type: 'number', min: 0, max: 23, clearable: true, + doc: 'End of the do-not-disturb window (0-23); held notifications are sent then. Blank disables it.' }, { key: 'notify_milestones', label: 'Cycle Milestones', type: 'intlist', def: '50, 100, 500, 1000', placeholder: '50, 100, 500, 1000', - doc: 'Comma-separated cycle counts that trigger a one-off celebration notification when reached (e.g. 50, 100, 500, 1000). Blank disables milestone notifications.' }, + doc: 'Cycle counts that trigger a one-off celebration notification, comma-separated (e.g. 50, 100, 500, 1000). Blank disables them.' }, { key: 'notify_milestone_message', label: 'Milestone Message', type: 'textarea', def: '{device} has completed {cycle_count} cycles!', doc: 'Message for the milestone notification. Template variables: {device}, {cycle_count}.' }, ] }, ] }, { id: 'ml_training', label: 'ML Training', fields: [ { key: 'enable_ml_models', label: 'Apply smart models during a cycle', type: 'checkbox', def: false, - doc: 'While a cycle runs, let the models refine the live results: a steadier time-remaining and energy/cost estimate, and an anti-premature-stop guard on end detection (it can only ever delay a finish, never end one early, and is bounded). Uses your fine-tuned models when available, otherwise the built-in ones. Off = the classic power-based logic only (still reliable).' }, + doc: 'Use a model trained on this machine to predict a running cycle\'s energy and cost, and let a well-known program that stops unusually early finish without the full off delay. Off: power-based logic only.' }, { key: 'ml_training_enabled', label: 'Learn from this machine', type: 'checkbox', def: false, - doc: 'Periodically study your reviewed cycles overnight and fine-tune the models to this specific machine. A change is only kept when it genuinely scores better on held-out cycles, so this can only help or stay the same — never regress.' }, + doc: 'Retrain the energy model on this machine\'s finished cycles from time to time. A new model is kept only if it beats the simple estimate on test cycles.' }, { key: 'ml_training_hour', label: 'Learn at hour', unit: 'h', type: 'number', min: 0, max: 23, def: 2, - doc: 'Local hour of day (0-23) to do the overnight fine-tuning. Pick a quiet hour such as 2 (02:00).' }, + doc: 'Hour of day (0-23) for overnight training. Pick a quiet hour, e.g. 2.' }, { key: 'ml_training_min_cycles', label: 'Cycles needed first', type: 'number', min: 5, def: 30, - doc: 'Wait until at least this many cycles have been recorded before fine-tuning, so there is enough to learn from.' }, + doc: 'Wait for at least this many recorded cycles before training.' }, { key: 'ml_training_interval_days', label: 'Check at most every', unit: 'days', type: 'number', min: 1, def: 7, - doc: 'Re-check for improvements at most once per this many days.' }, + doc: 'Retrain at most once every this many days.' }, ] }, ]; @@ -382,22 +386,11 @@ for (const sec of _SETTINGS_SECTIONS) { for (const grp of groups) for (const f of (grp.fields || [])) _FIELD_BY_KEY[f.key] = f; } -// Default values for playground-only matcher params (code constants, not stored -// options, so _FIELD_BY_KEY has no entry for them). +// Offline fallback defaults for the two Stage-1 matcher ratios the Playground +// shows (the backend ships the real ones as pg_match_defaults). const _PG_MATCH_DEFAULTS = { profile_match_min_duration_ratio: 0.1, profile_match_max_duration_ratio: 1.8, - corr_weight: 0.45, - keep_min_score: 0.1, - dtw_bandwidth: 0.2, - dtw_blend: 0.5, - dtw_ensemble_w: 0.7, - dtw_ddtw_scale: 30, - dtw_refine_top_n: 5, - duration_weight: 0.22, - energy_weight: 0.22, - duration_scale: 0.175, - energy_scale: 0.25, }; // ─── Setting conflict rules ─────────────────────────────────────────────────── @@ -443,11 +436,11 @@ function _nonPriceReason(id, vals, ctx) { if (!id) return null; if (id === vals.power_sensor) { return { msgKey: 'conflict.price_entity.power_sensor', msgVars: {}, - msgFb: '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.' }; + msgFb: 'This is the device\'s Power Sensor, not a price. Cycles would be costed in watts per kWh; clear this field.' }; } if (id === vals.energy_sensor) { return { msgKey: 'conflict.price_entity.energy_sensor', msgVars: {}, - msgFb: '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.' }; + msgFb: 'This is the device\'s Energy Meter, not a price. Cycles would be costed at the meter reading per kWh; clear this field.' }; } const st = (ctx && ctx.stateOf) ? ctx.stateOf(id) : null; if (!st) return null; @@ -455,12 +448,12 @@ function _nonPriceReason(id, vals, ctx) { const dc = String(attrs.device_class == null ? '' : attrs.device_class).trim().toLowerCase(); if (_NON_PRICE_DEVICE_CLASSES.has(dc)) { return { msgKey: 'conflict.price_entity.device_class', msgVars: {dc}, - msgFb: `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.` }; + msgFb: `This sensor measures ${dc}, not a price per kWh. Clear this field or pick a tariff sensor.` }; } const unit = String(attrs.unit_of_measurement == null ? '' : attrs.unit_of_measurement).trim(); if (_NON_PRICE_UNITS.has(unit.toLowerCase())) { return { msgKey: 'conflict.price_entity.unit', msgVars: {unit}, - msgFb: `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.` }; + msgFb: `This sensor reads in ${unit}, not a price per kWh. Clear this field or pick a tariff sensor.` }; } return null; } @@ -568,15 +561,9 @@ const _SETTING_CONFLICTS = [ profile_match_threshold: { msgKey: 'conflict.unmatch.match', msgVars: {un: v.profile_unmatch_threshold}, msgFb: `Must be above Unmatch Threshold (${v.profile_unmatch_threshold})`, fixVal: +(v.profile_unmatch_threshold + 0.05).toFixed(2) }, }), }, - { - // anti_wrinkle_exit_power < stop_threshold_w — only for devices that support anti-wrinkle - keys: ['anti_wrinkle_exit_power', 'stop_threshold_w'], - check: v => ['washing_machine','dryer','washer_dryer'].includes(v.device_type) && v.anti_wrinkle_exit_power != null && v.stop_threshold_w != null && v.anti_wrinkle_exit_power >= v.stop_threshold_w, - fieldErrors: v => ({ - anti_wrinkle_exit_power: { msgKey: 'conflict.anti_wrinkle_exit.exit', msgVars: {stop: v.stop_threshold_w}, msgFb: `Must be below Stop Threshold (${v.stop_threshold_w} W); otherwise the anti-wrinkle exit power is ignored`, fixVal: +(v.stop_threshold_w * 0.4).toFixed(1) }, - stop_threshold_w: { msgKey: 'conflict.anti_wrinkle_exit.stop', msgVars: {exit: v.anti_wrinkle_exit_power}, msgFb: `Must be above Anti-Wrinkle Exit Power (${v.anti_wrinkle_exit_power} W)`, fixVal: +(v.anti_wrinkle_exit_power * 2.5).toFixed(1) }, - }), - }, + // (No anti_wrinkle_exit_power vs stop_threshold_w rule: the detector counts quiet + // below the higher of the two, so any pair is valid and the old "must be below + // stop" fix moved the exit power to where it does nothing; #285 #296 #325.) { // anti_wrinkle_max_power > start_threshold_w — only for devices that support anti-wrinkle keys: ['anti_wrinkle_max_power', 'start_threshold_w'], @@ -591,8 +578,8 @@ const _SETTING_CONFLICTS = [ keys: ['pump_stuck_duration', 'no_update_active_timeout'], check: v => v.device_type === 'pump' && v.pump_stuck_duration != null && v.no_update_active_timeout != null && v.no_update_active_timeout <= v.pump_stuck_duration, fieldErrors: v => ({ - pump_stuck_duration: { msgKey: 'conflict.pump_stuck.duration', msgVars: {to: v.no_update_active_timeout}, msgFb: `Must be less than No-Update Timeout (${v.no_update_active_timeout} s) so the stuck alarm fires before the watchdog kills the cycle`, fixVal: v.no_update_active_timeout - 60 }, - no_update_active_timeout: { msgKey: 'conflict.pump_stuck.timeout', msgVars: {ps: v.pump_stuck_duration}, msgFb: `Must exceed Pump Stuck Duration (${v.pump_stuck_duration} s) so the stuck alarm fires before the cycle is force-stopped`, fixVal: v.pump_stuck_duration + 60 }, + pump_stuck_duration: { msgKey: 'conflict.pump_stuck.duration', msgVars: {to: v.no_update_active_timeout}, msgFb: `Must be below No-Update Timeout (${v.no_update_active_timeout} s) so the stuck alarm can fire first`, fixVal: v.no_update_active_timeout - 60 }, + no_update_active_timeout: { msgKey: 'conflict.pump_stuck.timeout', msgVars: {ps: v.pump_stuck_duration}, msgFb: `Must exceed Pump Stuck Duration (${v.pump_stuck_duration} s) so the stuck alarm can fire first`, fixVal: v.pump_stuck_duration + 60 }, }), }, { @@ -617,7 +604,7 @@ const _SETTING_CONFLICTS = [ end_energy_threshold: { msgKey: 'conflict.end_energy.energy', msgVars: {w: v.stop_threshold_w, d: v.off_delay}, msgFb: `Too strict for Stop Threshold (${v.stop_threshold_w} W) over Off Delay (${v.off_delay} s); the cycle can only end through a fallback path`, fixVal: Math.ceil(v.stop_threshold_w * v.off_delay / 3600 * 1000) / 1000 }, - stop_threshold_w: { msgKey: 'conflict.end_energy.stop', msgVars: {e: v.end_energy_threshold, d: v.off_delay}, + stop_threshold_w: { msgKey: 'conflict.end_energy.stop', msgVars: {e: v.end_energy_threshold, d: v.off_delay, p: +(v.end_energy_threshold * 3600 / v.off_delay).toFixed(2)}, msgFb: `End Energy Threshold (${v.end_energy_threshold} Wh over ${v.off_delay} s) only permits ${+(v.end_energy_threshold * 3600 / v.off_delay).toFixed(2)} W`, fixVal: Math.floor(v.end_energy_threshold * 3600 / v.off_delay * 10) / 10 }, }), @@ -768,6 +755,18 @@ const _CSS = ` .wd-table tr:last-child td { border-bottom: none; } .wd-table tbody tr:hover td { background: var(--secondary-background-color); } .wd-table-wrap { overflow-x: auto; } +/* Audit UI-20: a horizontal scroller that hides content past an edge gets a shadow on + that edge, so on a phone the tab strip and wide tables visibly continue instead of + looking cut off. A shadow rather than a fade: past a table's last visible column + there is usually only whitespace, where a fade shows nothing. _syncScrollAffordances + sets the classes only while there really is more content on that side; backgrounds + do not affect layout, and nothing overflows at desktop widths, so the desktop view + is unchanged. The background of a scroll container does not scroll with it, so the + shadow stays on the visible edge. Mid-grey reads on light and dark themes alike. */ +.wd-ovf-l, .wd-ovf-r { background-repeat: no-repeat; background-size: 22px 100%; } +.wd-ovf-r { background-image: linear-gradient(to left, rgba(127,127,127,.42), rgba(127,127,127,0)); background-position: right center; } +.wd-ovf-l { background-image: linear-gradient(to right, rgba(127,127,127,.42), rgba(127,127,127,0)); background-position: left center; } +.wd-ovf-l.wd-ovf-r { background-image: linear-gradient(to right, rgba(127,127,127,.42), rgba(127,127,127,0)), linear-gradient(to left, rgba(127,127,127,.42), rgba(127,127,127,0)); background-position: left center, right center; } .wd-th-sort { cursor: pointer; user-select: none; white-space: nowrap; } .wd-th-sort:hover { color: var(--primary-color); } .wd-tc-date { white-space: nowrap; color: var(--secondary-text-color); font-size: .82em; } @@ -876,6 +875,9 @@ th.wd-tc-flags { color: var(--secondary-text-color); font-weight: 500; } .wd-pillbox .wd-pill { display: inline-flex; align-items: center; gap: 4px; max-width: 100%; padding: 2px 4px 2px 9px; font-size: .82em; line-height: 1.4; border-radius: var(--wd-radius-lg); background: var(--primary-color); color: var(--wd-white); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +/* The label shrinks and ellipsizes; the remove button never does (#467: a long + notify service pushed it out of the clipped pill). */ +.wd-pill-txt { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .wd-pill-x { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; padding: 0; border: 0; border-radius: 50%; background: rgba(255,255,255,.25); color: var(--wd-white); font-size: 13px; line-height: 1; cursor: pointer; flex: none; } @@ -915,7 +917,8 @@ th.wd-tc-flags { color: var(--secondary-text-color); font-weight: 500; } box-shadow: 0 4px 18px rgba(0,0,0,.35); z-index: 60; text-align: left; font-weight: 400; text-transform: none; letter-spacing: normal; } -.wd-tip:hover .wd-tip-pop { display: block; } +.wd-tip:hover .wd-tip-pop, .wd-tip:focus .wd-tip-pop { display: block; } +.wd-tip:focus-visible { outline: 2px solid var(--primary-color); outline-offset: 1px; } .wd-tip-txt { font-size: 12px; line-height: 1.5; display: block; } .wd-dg { display: block; width: 100%; height: auto; margin-bottom: 8px; background: var(--secondary-background-color); border-radius: 6px; } .wd-dg .ln { fill: none; stroke: var(--primary-color); stroke-width: 2.5; } @@ -943,9 +946,10 @@ th.wd-tc-flags { color: var(--secondary-text-color); font-weight: 500; } padding: 2px 7px; border-radius: 10px; white-space: nowrap; } .wd-sug-chip-obs { background: rgba(255,152,0,.22); } -.wd-sug-chip-cal { background: rgba(33,150,243,.18); } .wd-sug-val { font-weight: 700; flex-shrink: 0; } .wd-sug-impact-line { flex-basis: 100%; font-size: .86em; opacity: .70; font-style: italic; margin-top: 2px; } +.wd-confirm-list { margin: 0 0 12px; padding-left: 18px; max-height: 50vh; overflow-y: auto; } +.wd-confirm-list li { margin: 4px 0; } .wd-sug-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } .wd-sug-sep { display: none; } .wd-sug-impact { display: none; } @@ -1056,6 +1060,10 @@ button.wd-profile-card { display: block; } .wd-star-btn:hover { transform: scale(1.15); } /* Share-device selection tree (profile -> its reference cycles). */ .wd-sd-tree { display: flex; flex-direction: column; gap: 8px; max-height: 44vh; overflow-y: auto; margin-bottom: 16px; } +/* #460: a group's overflow:hidden drops its automatic min-height to 0, so past the + max-height above the groups shrank and clipped their own rows instead of the tree + scrolling (a phone hits the cap at once). Rows keep their height; the tree scrolls. */ +.wd-sd-tree > * { flex-shrink: 0; } .wd-sd-group { border: 1px solid var(--divider-color); border-radius: var(--wd-radius-md); background: var(--secondary-background-color); overflow: hidden; } .wd-sd-prof { display: flex; align-items: center; gap: 8px; padding: 9px 12px; cursor: pointer; font-weight: 600; } .wd-sd-prof-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } @@ -1082,6 +1090,7 @@ button.wd-profile-card { display: block; } .wd-gear-body { margin-top: 12px; } .wd-empty { text-align: center; padding: 48px 24px; color: var(--secondary-text-color); } .wd-empty .wd-icon { font-size: 3em; margin-bottom: 10px; } +.wd-stale { margin: 6px 0 0; padding: 6px 12px; border-radius: var(--wd-radius-md); background: var(--secondary-background-color); border: 1px solid var(--warning-color, #ffa600); color: var(--primary-text-color); font-size: .85em; } .wd-error-state { display: flex; align-items: center; gap: 10px; padding: 10px 14px; margin-bottom: 10px; border-radius: var(--wd-radius-md); background: var(--secondary-background-color); border: 1px solid var(--divider-color); color: var(--error-color, #b71c1c); font-size: .9em; } .wd-info { font-size: .9em; color: var(--secondary-text-color); line-height: 1.6; margin: 0; } .wd-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.5); z-index: 100; display: flex; align-items: center; justify-content: center; } @@ -1174,7 +1183,6 @@ button.wd-profile-card { display: block; } .wd-devsub { font-size: .72em; color: var(--secondary-text-color); } .wd-dbadge { font-size: .72em; padding: 1px 7px; border-radius: 10px; background: var(--secondary-background-color); } .wd-dbadge.rec { background: var(--error-color, #f44336); color: var(--wd-white); } -.wd-dbadge.sug { background: rgba(255,152,0,.22); } .wd-dbadge.fb { background: rgba(33,150,243,.22); } .wd-dbadge.conf { background: rgba(183,28,28,.18); color: var(--error-color, #b71c1c); } /* Attention cards (status dashboard) */ @@ -1268,6 +1276,7 @@ button.wd-profile-card { display: block; } .wd-tab { padding: 9px 13px; } .wd-modal { padding: 16px; width: calc(100% - 18px); } .wd-modal-lg { max-width: 100%; } + .wd-sd-prof-name { white-space: normal; overflow-wrap: anywhere; } /* #460: wrap, never ellipsise a choice */ .wd-canvas-wrap canvas { height: 200px; } .wd-zoom-reset { width: 36px; height: 36px; top: 8px; right: 8px; font-size: 17px; } .wd-gtip { white-space: normal; max-width: calc(100vw - 24px); } @@ -1339,11 +1348,6 @@ button.wd-profile-card { display: block; } .wd-pg-param-lbl { flex: 1; font-size: .83em; color: var(--secondary-text-color); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .wd-pg-param-inp { width: 76px; flex: 0 0 76px; } .wd-pg-param-drag { font-size: .75em; color: var(--primary-color); cursor: default; flex: 0 0 12px; } -.wd-pg-score-bar-row { display: flex; align-items: center; gap: 6px; font-size: .82em; margin: 2px 0; } -.wd-pg-score-bar-lbl { flex: 0 0 80px; color: var(--secondary-text-color); } -.wd-pg-score-bar-track { flex: 1; height: 6px; background: var(--secondary-background-color); border-radius: 3px; overflow: hidden; } -.wd-pg-score-bar-fill { height: 100%; border-radius: 3px; } -.wd-pg-score-bar-val { flex: 0 0 42px; text-align: right; font-variant-numeric: tabular-nums; color: var(--secondary-text-color); } .wd-pg-cand-row { display: flex; align-items: center; gap: 6px; font-size: .82em; margin: 3px 0; } .wd-pg-cand-name { flex: 0 0 110px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .wd-pg-cand-track { flex: 1; height: 7px; background: var(--secondary-background-color); border-radius: var(--wd-radius-sm); overflow: hidden; } @@ -1416,16 +1420,22 @@ button.wd-profile-card { display: block; } // ─── Helpers ───────────────────────────────────────────────────────────────── +// The _fmt* helpers below feed HTML templates directly, often with values that +// came from outside (a community-store document is untyped: audit STORE-19), so +// they never echo their input: anything that is not a finite number renders '-'. function _fmtDuration(s) { - if (s == null || s < 0) return '-'; + const n = Number(s); + if (s == null || !isFinite(n) || n < 0) return '-'; + s = n; const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), sec = Math.floor(s % 60); if (h > 0) return `${h}h ${m}m`; if (m > 0) return `${m}m ${sec}s`; return `${sec}s`; } function _fmtPower(w) { - if (w == null) return '-'; - return w >= 100 ? `${Math.round(w)} W` : `${w.toFixed(1)} W`; + const n = Number(w); + if (w == null || !isFinite(n)) return '-'; + return n >= 100 ? `${_fmtNum(Math.round(n), 0)} W` : `${_fmtNum(n, 1)} W`; } // Watt label for a chart axis / grid line. #453: a fixed Math.round() prints "0W" // on every tick of a standby trace, so an appliance idling at 0.3 W got three @@ -1448,13 +1458,82 @@ function _niceStep(span, divisions) { return (norm <= 1 ? 1 : norm <= 2 ? 2 : norm <= 2.5 ? 2.5 : norm <= 5 ? 5 : 10) * mag; } function _fmtEnergy(kwh) { - if (kwh == null) return '-'; - return `${kwh.toFixed(2)} kWh`; + if (kwh == null || !isFinite(Number(kwh))) return '-'; + return `${_fmtNum(kwh, 2)} kWh`; } // Current cycle-date display mode ('relative' | 'absolute'), synced from the // user's persisted "Cycle date display" preference by _render() on each paint. let _datePref = 'relative'; +// The HA user's display locale (audit UI-08), synced by _render() like _datePref +// so the module-level formatters follow the user's language and number format +// instead of the browser's. `undefined` means "browser default", which is what +// HA itself does for the "system" choices. +let _uiDateLocale; // panel language (lang_override, else HA language) +let _uiNumLocale; // HA number_format mapped to a locale, as HA's frontend does +let _uiNumGrouping = true; +let _uiHour12; // HA time_format '12' / '24'; undefined = locale default +const _nfCache = new Map(); +function _syncUiLocale(lang, locale) { + const loc = locale || {}; + _uiDateLocale = lang || undefined; + const nf = loc.number_format; + _uiNumLocale = nf === 'comma_decimal' ? ['en-US', 'en'] + : nf === 'decimal_comma' ? ['de', 'es', 'it'] + : nf === 'space_comma' ? ['fr', 'sv', 'cs'] + : nf === 'quote_decimal' ? ['de-CH'] + : nf === 'system' ? undefined + : (lang || undefined); + _uiNumGrouping = nf !== 'none'; + _uiHour12 = loc.time_format === '12' ? true : loc.time_format === '24' ? false : undefined; +} +function _numFmt(opts) { + const key = JSON.stringify([_uiNumLocale, _uiNumGrouping, opts]); + let f = _nfCache.get(key); + if (!f) { + const o = Object.assign({ useGrouping: _uiNumGrouping }, opts); + try { f = new Intl.NumberFormat(_uiNumLocale, o); } catch (_) { f = new Intl.NumberFormat(undefined, o); } + _nfCache.set(key, f); + } + return f; +} +// Display-only number: `dec` fraction digits (fixed, like toFixed). Never use the +// result for a value that is sent back to the backend. A non-number renders '-', +// never the input itself: callers drop the result straight into innerHTML (STORE-19). +function _fmtNum(v, dec = 0) { + const n = Number(v); + if (!isFinite(n)) return '-'; + return _numFmt({ minimumFractionDigits: dec, maximumFractionDigits: dec }).format(n); +} +// A community-store counter (downloads, favourites, ratings) as a non-negative +// number. Store documents are untyped, so a missing, negative or non-numeric +// counter reads as 0 rather than reaching the page as-is (audit STORE-19). +function _storeCount(v) { + const n = Number(v); + return isFinite(n) && n > 0 ? Math.floor(n) : 0; +} +// Money in HA's configured currency, formatted for the user's locale ("€0.21", +// "0,21 €"). A currency that is not an ISO 4217 code keeps the old "0.21 XYZ". +function _fmtCost(v, cur) { + const n = Number(v); + if (v == null || !isFinite(n)) return '-'; + if (cur) { + try { return _numFmt({ style: 'currency', currency: cur }).format(n); } catch (_) { /* not ISO 4217 */ } + } + return `${_fmtNum(n, 2)}${cur ? ' ' + cur : ''}`; +} +function _dateOpts(opts) { + return _uiHour12 === undefined ? opts : Object.assign({ hour12: _uiHour12 }, opts); +} +function _fmtTimeOfDay(d) { + const opts = _dateOpts({ hour: '2-digit', minute: '2-digit', second: '2-digit' }); + try { return d.toLocaleTimeString(_uiDateLocale, opts); } catch (_) { return d.toLocaleTimeString(undefined, opts); } +} +function _fmtDay(ts) { + const d = new Date(ts); + try { return d.toLocaleDateString(_uiDateLocale); } catch (_) { return d.toLocaleDateString(); } +} + // ─── History-import date picker helpers ────────────────────────────────────── // The recorder read is bounded by a start DATE ("import since ..."), which is what a // user actually knows, rather than a day count they have to work out. These produce @@ -1479,7 +1558,7 @@ function _histMinSince() { return _histShiftDays(_HIST_MAX_DAYS - 1); } function _relTime(ms) { const diffSec = Math.round((ms - Date.now()) / 1000); // < 0 = in the past let rtf; - try { rtf = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' }); } + try { rtf = new Intl.RelativeTimeFormat(_uiDateLocale, { numeric: 'auto' }); } catch (_) { return _fmtAbsDate(ms); } const abs = Math.abs(diffSec); const units = [['year', 31536000], ['month', 2592000], ['week', 604800], ['day', 86400], ['hour', 3600], ['minute', 60]]; @@ -1489,7 +1568,9 @@ function _relTime(ms) { return rtf.format(diffSec, 'second'); } function _fmtAbsDate(ms) { - return new Date(ms).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); + const opts = _dateOpts({ month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); + try { return new Date(ms).toLocaleString(_uiDateLocale, opts); } + catch (_) { return new Date(ms).toLocaleString(undefined, opts); } } // Normalize any timestamp (ISO string, unix seconds, unix millis, or a bare // YYYY-MM-DD calendar date) to epoch millis, then format per the date-display @@ -1535,6 +1616,25 @@ function _wsErrText(err) { function _esc(s) { return String(s == null ? '' : s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); } +// A _t() var that is markup on purpose (built from escaped parts). _t() escapes +// every other var, so wrapping one in _html() is the explicit, greppable opt-out +// (audit UI-21: a raw search term reached innerHTML through a translated string). +class _TrustedHtml { + constructor(html) { this.html = String(html == null ? '' : html); } + toString() { return this.html; } +} +function _html(s) { return new _TrustedHtml(s); } +// CLDR plural category ('one', 'few', 'other', ...) of `n` in `lang`; one cached +// Intl.PluralRules per language. An unknown tag degrades to English rules. +const _pluralRules = new Map(); +function _pluralCat(lang, n) { + let pr = _pluralRules.get(lang); + if (pr === undefined) { + try { pr = new Intl.PluralRules(lang || 'en'); } catch (_) { pr = null; } + _pluralRules.set(lang, pr); + } + return pr ? pr.select(n) : (n === 1 ? 'one' : 'other'); +} // Allow only http(s) links. Community-supplied URLs (e.g. a device's manualUrl) // must never render a `javascript:`/`data:` href, which _esc does not neutralise. // Returns '' for anything that is not an absolute http(s) URL. @@ -1573,6 +1673,18 @@ function _withAlpha(col, alpha) { } function _num(v, def) { const n = parseFloat(v); return isNaN(n) ? def : n; } // Visible, keyboard-focusable descendants of `root` (for modal focus trapping). +// A selector that finds "the same control" after a full re-render: its id, else the +// data-* attributes the panel uses to identify buttons, rows and inputs. +const _FOCUS_ATTRS = ['data-action', 'data-cid', 'data-idx', 'data-tab', 'data-sec', 'data-gtab', + 'data-ptab', 'data-hsub', 'data-proftab', 'data-opt', 'data-pgkey', 'data-gname', 'data-sugkey']; +function _focusKey(el) { + if (!el || !el.getAttribute || el.tagName === 'BODY') return null; + const esc = (v) => (window.CSS && CSS.escape) ? CSS.escape(v) : String(v).replace(/["\\]/g, '\\$&'); + if (el.id) return '#' + esc(el.id); + const parts = _FOCUS_ATTRS.filter(a => el.hasAttribute(a)).map(a => `[${a}="${esc(el.getAttribute(a))}"]`); + return parts.length ? el.tagName.toLowerCase() + parts.join('') : null; +} + function _focusableEls(root) { if (!root) return []; const sel = 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])'; @@ -1651,6 +1763,25 @@ function _parseIntList(s) { } // Linear-interpolated y at offset x for a sorted [[x,y],...] series. +// Like _valueAt, but null where the series has no reading: before its first point, +// after its last, and inside a gap (a null-power point breaks the run). The cycle +// chart's series are read this way while recorder context is drawn around them +// (item 513), so the lead-in does not show the trace's first reading as "Power". +function _valueInRun(pts, x) { + if (!pts || !pts.length) return null; + for (let i = 0; i < pts.length; i++) { + const b = pts[i]; + if (b[0] < x) continue; + if (b[0] === x) return b[1] == null ? null : b[1]; + if (i === 0) return null; + const a = pts[i - 1]; + if (a[1] == null || b[1] == null) return null; + const span = (b[0] - a[0]) || 1; + return a[1] + (b[1] - a[1]) * ((x - a[0]) / span); + } + return null; +} + function _valueAt(pts, x) { if (!pts || !pts.length) return null; if (x <= pts[0][0]) return pts[0][1]; @@ -1682,7 +1813,7 @@ function _field(f, value, extra) { const key = f.key; const labelText = f.unit ? `${f.label} (${f.unit})` : f.label; const _u = f.unit ? ` ${f.unit}` : ''; - const tip = f.doc ? _tip(f.doc, f.diagram || _DIAGRAM_BY_KEY[key]) : ''; + const tip = f.doc ? _tip(f.doc, f.diagram || _DIAGRAM_BY_KEY[key], extra.tText) : ''; // D7: "changed" marker (a small dot with a tooltip) when this field has a // recorded change in the settings changelog. const chgDot = extra.changed ? `` : ''; @@ -1729,7 +1860,7 @@ function _field(f, value, extra) { // Chip/pill multi-picker: existing values as removable pills + a combobox // add-input. Managed by DOM (no re-render) and collected on save. const vals = Array.isArray(value) ? value : (value ? [value] : []); - const pills = vals.map(x => `${_esc(x)}`).join(''); + const pills = vals.map(x => `${_esc(x)}`).join(''); input = `
${pills}` + `
` + `` + @@ -1797,66 +1928,32 @@ function _field(f, value, extra) { input = `${extra.datalist || ''}`; } - // Suggestions: drop any recommendation that already equals the current value, - // and when BOTH an Observed (classic) and a Calibrated (ML) recommendation - // remain, render them in a single shared pill (never two stacked pills). - // When they agree within 5%, collapse to one "WashData recommends" label. - // When they diverge, show both with a per-setting one-liner explaining what - // choosing each value will actually do to the appliance's behaviour. + // Suggestion pill: dropped when it already equals the current value. The + // one-line impact says what the change does to the appliance's behaviour. const sug = extra.suggestion; - const mlSug = extra.mlSuggestion; const classicVal = (sug && sug.suggested != null && !_sugSame(sug.suggested, value)) ? sug.suggested : null; - const mlVal = (mlSug && mlSug.value != null && !_sugSame(mlSug.value, value)) ? mlSug.value : null; const t = extra.t; - // Resolve localized reason text (reason_key + reason_params) with the English - // reason as fallback. _tip() escapes, so interpolated values are safe. - const sugReason = sug ? (sug.reason_key ? t(sug.reason_key, sug.reason_params || {}, sug.reason || '') : (sug.reason || '')) : ''; - const mlReason = mlSug ? (mlSug.reason_key ? t(mlSug.reason_key, mlSug.reason_params || {}, mlSug.reason || '') : (mlSug.reason || '')) : ''; - const useBtn = (val) => ``; let sugHtml = ''; - if (classicVal != null && mlVal != null) { - const cN = parseFloat(classicVal), mN = parseFloat(mlVal); - const relDiff = (!isNaN(cN) && !isNaN(mN)) ? Math.abs(cN - mN) / Math.max(Math.abs(cN), Math.abs(mN), 1e-9) : 1; - if (relDiff < 0.05) { - // Both engines agree — collapse to one clear recommendation. - const calLbl = t('suggestion.calibrated_label', {}, 'Calibrated'); - const reason = _tip([sugReason, mlReason ? `${calLbl}: ${mlReason}` : ''].filter(Boolean).join('\n\n')); - sugHtml = `
💡 ${_esc(t('suggestion.both_agree', {}, 'WashData recommends'))}${_esc(classicVal)}${_u}${useBtn(classicVal)}${reason}
`; - } else { - // Engines diverge — show two stacked option rows with per-option context. - const cr = sugReason ? _tip(sugReason) : ''; - const mr = mlReason ? _tip(mlReason) : ''; - const obsLbl = t('suggestion.observed_label', {}, 'Observed'); - const calLbl = t('suggestion.calibrated_label', {}, 'Calibrated'); - let obsImpact = '', calImpact = ''; - if (!isNaN(cN) && !isNaN(mN)) { - const calIsHigher = mN > cN; - calImpact = t(`suggestion.impact.${key}.${calIsHigher ? 'higher' : 'lower'}`, {}, ''); - obsImpact = t(`suggestion.impact.${key}.${calIsHigher ? 'lower' : 'higher'}`, {}, ''); - } - const obsImpactHtml = obsImpact ? `
${_esc(obsImpact)}
` : ''; - const calImpactHtml = calImpact ? `
${_esc(calImpact)}
` : ''; - sugHtml = `
` + - `
💡 ${_esc(obsLbl)}${_esc(classicVal)}${_u}${useBtn(classicVal)}${cr}${obsImpactHtml}
` + - `
🤖 ${_esc(calLbl)}${_esc(mlVal)}${_u}${useBtn(mlVal)}${mr}${calImpactHtml}
` + - `
`; - } - } else if (classicVal != null) { + if (classicVal != null) { + // Resolve localized reason text (reason_key + reason_params) with the English + // reason as fallback. _tip() escapes, so interpolated values are safe. + // Plain text (tText): _tip() escapes it, so _t()'s HTML escaping would double up. + const tt = extra.tText || t; + const sugReason = sug.reason_key ? tt(sug.reason_key, sug.reason_params || {}, sug.reason || '') : (sug.reason || ''); const reason = sugReason ? _tip(sugReason) : ''; - const nowNote = value != null && value !== '' ? ` (now ${_esc(value)}${_u})` : ''; - sugHtml = `
💡 ${_esc(t('suggestion.observed_label', {}, 'Observed'))}${_esc(classicVal)}${_u}${nowNote}${useBtn(classicVal)}${reason}
`; - } else if (mlVal != null) { - const r = mlReason ? _tip(mlReason) : ''; - sugHtml = `
🤖 ${_esc(t('suggestion.calibrated_label', {}, 'Calibrated'))}${_esc(mlVal)}${_u}${useBtn(mlVal)}${r}
`; - } - - // #343: a "mute" button on each suggestion card so the user can tell the - // auto-tuner to stop proposing this setting (e.g. a threshold that breaks an - // anti-crease-tuned device). Injected once just before the card's closing tag. - if (sugHtml && key) { + const nowNote = value != null && value !== '' ? ` ${t('suggestion.now_value', {v: `${value}${_u}`}, '(now {v})')}` : ''; + const impact = _sugImpact(t, key, classicVal, value); + const useBtn = ``; + // #343: a "mute" button on each suggestion card so the user can tell the + // auto-tuner to stop proposing this setting (e.g. a threshold that breaks an + // anti-crease-tuned device). const lockTitle = _esc(t('btn.mute_suggestion', {}, "Stop suggesting this setting")); - const lockBtn = ``; - sugHtml = sugHtml.replace(/<\/div>\s*$/, lockBtn + '
'); + const lockBtn = key ? `` : ''; + sugHtml = `
💡 ${_esc(t('suggestion.observed_label', {}, 'Observed'))}${_esc(classicVal)}${_u}${nowNote}${useBtn}${reason}${lockBtn}${impact ? `
${_esc(impact)}
` : ''}
`; + } + // Accessible name for the control: the
`; } _htmlBody() { + if (!this._devices.length && this._fetchFailed) + return ``; if (!this._devices.length) return `
🧺
${this._t('msg.no_devices', {}, 'No WashData devices configured yet.')}
`; - const mlSugCount = this._mlSugKeys().size; - const sugDot = (this._suggestions.length || mlSugCount) ? ' 💡' : ''; const confIndicator = this._conflictKeysFromOpts().size > 0 ? ' ⚠' : ''; const pgBusy = this._busy.has('pg-sim') || this._busy.has('pg-sweep'); const pgSpinner = pgBusy ? `` : ''; - const labels = { status: this._t('tab.status',{},'Overview'), history: this._t('tab.history',{},'Cycles'), profiles: this._t('tab.profiles',{},'Profiles'), settings: this._t('tab.settings',{},'Settings') + confIndicator + sugDot, playground: this._t('tab.playground',{},'Playground') + pgSpinner, store: this._t('tab.store',{},'Store'), advanced: this._t('tab.advanced',{},'Advanced') }; + const labels = { status: this._t('tab.status',{},'Overview'), history: this._t('tab.history',{},'Cycles'), profiles: this._t('tab.profiles',{},'Profiles'), settings: this._t('tab.settings',{},'Settings') + confIndicator, playground: this._t('tab.playground',{},'Playground') + pgSpinner, store: this._t('tab.store',{},'Store'), advanced: this._t('tab.advanced',{},'Advanced') }; const visible = this._visibleTabIds(); if (!visible.includes(this._tab)) this._tab = 'status'; const tabBtns = visible.map(id => `` ).join(''); - const pane = (id, html) => visible.includes(id) - ? `
${html}
` : ''; + // Only the active pane is built (audit UI-03 / PERF-05): every render used to + // build and parse all seven, 52-235 ms per refresh with 200 cycles loaded, the + // Playground's 35 kB included though it was never opened. Tab clicks re-render. + const pane = (id, build) => visible.includes(id) + ? `
${this._tab === id ? build() : ''}
` : ''; return `
${this._htmlDeviceBar()}
${tabBtns}
+ ${this._htmlStaleChip()}
${this._tabLoading ? `
⏳
${this._t('msg.loading', {}, 'Loading…')}
` : ''} - ${pane('status', this._htmlStatus())} - ${pane('history', this._htmlHistory())} - ${pane('profiles', this._htmlProfiles())} - ${pane('settings', this._htmlSettings())} - ${pane('playground', this._htmlPlayground())} - ${pane('store', this._htmlStore())} - ${pane('advanced', this._htmlPanel())} + ${pane('status', () => this._htmlStatus())} + ${pane('history', () => this._htmlHistoryTab())} + ${pane('profiles', () => this._htmlProfiles())} + ${pane('settings', () => this._htmlSettings())} + ${pane('playground', () => this._htmlPlayground())} + ${pane('store', () => this._htmlStore())} + ${pane('advanced', () => this._htmlPanel())} `; } @@ -4464,8 +4829,6 @@ class HaWashdataPanel extends HTMLElement { const badges = []; const confN = this._conflictCountForOpts(d.options || {}, d.option_defaults || {}); if (confN) badges.push(`⚠ ${confN}`); - const sugN = this._sugCountsForDevice(d).total; - if (sugN) badges.push(`💡 ${sugN}`); if (d.feedback_count) badges.push(`💬 ${d.feedback_count}`); return `
`; } + // MATCH-DECIDE-15: while the live match is undecided (nothing committed yet, or + // a runner-up within the ambiguity margin) say so, with the top two and a + // "~N% sure" read off the margin (backend match_rules.display_sure_pct). Display + // only: the program sensor keeps its raw `detecting...` state for automations. + _htmlMatchUncertainty(dev, isRunning, manual) { + const u = dev && dev.match_uncertainty; + if (!u || !isRunning || manual || !u.top) return ''; + const what = u.runner_up + ? this._t('status.match_uncertain', {top: u.top, runner: u.runner_up}, 'Uncertain: {top} or {runner}') + : this._t('status.match_uncertain_single', {top: u.top}, 'Uncertain: maybe {top}'); + const pct = Number(u.sure_pct); + const sure = (u.sure_pct != null && isFinite(pct)) + ? ` · ${this._t('status.match_sure', {pct}, '~{pct}% sure')}` : ''; + const tip = _tip(this._tText('status.match_uncertain_tip', {top: u.top}, + 'WashData has not settled on a program yet. The figure is how often the leading guess, {top}, was right in past cycles when it led by this much. Information only.')); + return `${what}${sure}${tip}`; + } + _htmlStatus() { const dev = this._devices[this._selIdx]; if (!dev) return `
${this._t('msg.no_device_selected', {}, 'No device selected.')}
`; @@ -4503,13 +4884,13 @@ class HaWashdataPanel extends HTMLElement { : (armed ? this._t('badge.armed', {}, '(applies to the next cycle)') : ''); const tagKind = matched ? (manual ? 'manual' : 'auto') : 'manual'; const tag = suffix ? `${suffix}` : ''; - // Program selection is allowed for any user who can see the device (read+), - // since it only changes live detection, not stored data. - const programCtl = `
${_tip(this._t('lbl.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.'))} + // Program selection is allowed for any user who can see the device (read+): + // a deliberate exception to the edit level, kept by the maintainer. + const programCtl = `
${_tip(this._t('lbl.program_tip', {}, 'Choose which program the current cycle is matched to. Auto-detect picks the best match. Pin a program when auto-detect is wrong; pick one before starting and it applies to the next cycle.'))} ${tag}
`; + ${tag}${this._htmlMatchUncertainty(dev, isRunning, manual)}
`; const attn = []; if (dev.recording && this._canEdit()) attn.push(`
●
${this._t('msg.recording_in_progress', {}, 'Recording in progress')}
${this._t('msg.see_recorder', {}, 'See recorder widget below')}
`); @@ -4519,13 +4900,9 @@ class HaWashdataPanel extends HTMLElement { const n = _confKeys.size, s = n > 1 ? 's' : ''; attn.push(``); } - const _sugC = this._sugCountsForDevice(dev); - if (_sugC.total && this._canEdit()) { - const total = _sugC.total; - const parts = []; - if (_sugC.classic) parts.push(this._t('lbl.n_classic_suggestions', {n: _sugC.classic}, `${_sugC.classic} classic`)); - if (_sugC.ml) parts.push(this._t('lbl.n_ml_suggestions', {n: _sugC.ml}, `${_sugC.ml} ML`)); - attn.push(``); + const _sugN = this._sugCountForDevice(dev); + if (_sugN && this._canEdit()) { + attn.push(``); } // #445 cause 1: the appliance's standby draw sits ABOVE its Stop Threshold, so // the off delay never starts and the cycle cannot finish on its own. Explains @@ -4546,16 +4923,15 @@ class HaWashdataPanel extends HTMLElement { const envPos = dev.envelope_position; const envPct = envPos != null ? Math.round(envPos * 100) : null; const envPosHtml = envPct != null - ? ` ${this._t('lbl.envelope_position', {pct: envPct}, + ? ` ${this._t('lbl.envelope_position', {pct: envPct}, `curve ${envPct}%`)}` : ''; const progressHtml = (isRunning && prog != null) ? `
-
${prog.toFixed(1)}%${envPosHtml}${rem != null ? `${this._t('lbl.time_remaining', {v: _fmtDuration(rem)}, `~${_fmtDuration(rem)} remaining`)}` : ''}
+
${_fmtNum(prog, 1)}%${envPosHtml}${rem != null ? `${this._t('lbl.time_remaining', {v: _fmtDuration(rem)}, `~${_fmtDuration(rem)} remaining`)}` : ''}
` : ''; const pd = this._powerData || {}; const hasCurve = (pd.live || []).length > 1; @@ -4567,20 +4943,23 @@ class HaWashdataPanel extends HTMLElement { ${this._pref('show_raw', false) ? `` : ''}
`; // Setup card: phase-aware guidance replacing the old getting-started card. - // A live cycle (hasCurve) always wins so the user sees their appliance. + // A running cycle always wins so the user sees their appliance. Gated on the + // cycle, not on hasCurve: while idle `live` still holds the last 15 min of + // readings, so the card was hidden on almost every install (audit UI-01). const cycleCount = this._cyclesTotal || 0; const profileCount = (this._profiles || []).length; const setupDismissed = this._pref('setup_card_dismissed', false); const setupStatus = this._setupStatus; // Phase 3 and 4 collapse to a chip when dismissed; earlier phases always show // the full guidance card regardless of the dismissed pref. - const showSetupCard = setupStatus && !hasCurve; + const showSetupCard = setupStatus && !isRunning; const setupCardHtml = showSetupCard ? this._htmlSetupCard(setupStatus, setupDismissed) : ''; - const curveHtml = hasCurve + const chartHtml = hasCurve ? `
${legend}` - : (showSetupCard - ? setupCardHtml - : `

${this._t('msg.live_chart_loading', {}, 'Live power chart appears as readings arrive.')}

`); + : ''; + const curveHtml = showSetupCard + ? setupCardHtml + chartHtml + : (chartHtml || `

${this._t('msg.live_chart_loading', {}, 'Live power chart appears as readings arrive.')}

`); const showDebug = this._pref('show_debug', false); let debugHtml = ''; @@ -4589,23 +4968,15 @@ class HaWashdataPanel extends HTMLElement { const conf = md.confidence != null ? `${(md.confidence * 100).toFixed(1)}%` : '-'; const dRows = (md.candidates || []).map(c => `${_esc(c.profile_name)}${c.confidence_pct}%${c.mae}${c.correlation}${c.duration_ratio >= 0 ? '+' : ''}${c.duration_ratio}%`).join(''); debugHtml = `
-
Live Match Debug ${_tip('Confidence: how closely the current power curve matches the top candidate profile (0-100%). Ambiguous: the two best candidates score within 5% of each other - the label is uncertain until the cycle finishes.')}
+
${this._t('hdr.live_match_debug', {}, 'Live Match Debug')} ${_tip(this._tText('msg.live_match_debug_tip', {}, 'Confidence: how closely the current power curve matches the top candidate profile (0-100%). Ambiguous: the two best candidates score within 5% of each other - the label is uncertain until the cycle finishes.'))}
${conf}
${this._t('lbl.confidence', {}, 'Confidence')}
${md.ambiguous ? this._t('status.ambiguous', {}, 'Ambiguous') : this._t('status.clear', {}, 'Clear')}
${this._t('lbl.label', {}, 'Match')}
- ${dRows ? `${dRows}
ProfileConfMAECorrDuration
` : `

${this._t('msg.no_match_yet', {}, 'No match attempt yet - this populates during a running cycle.')}

`} + ${dRows ? `${dRows}
${this._t('lbl.profile', {}, 'Profile')}${this._t('col.dbg_conf', {}, 'Conf')}${this._t('col.dbg_mae', {}, 'MAE')}${this._t('col.dbg_corr', {}, 'Corr')}${this._t('lbl.duration', {}, 'Duration')}
` : `

${this._t('msg.no_match_yet', {}, 'No match attempt yet - this populates during a running cycle.')}

`}
`; } - // Quick-access cards for features folded out of the tab bar (Diagnostics, - // Logs, and the rest of the Advanced drawer). They open the gear drawer at - // the relevant subtab so the merged 4-tab layout stays discoverable. - const advCards = []; - if (this._canEdit()) advCards.push(``); - advCards.push(``); - const advHtml = `
${this._t('hdr.tools_and_data', {}, 'Tools & Data')}
${advCards.join('')}
`; - const cycleCtrlHtml = (() => { if (!this._canEdit()) return ''; const cycleActive = _ACTIVE_STATES.includes(state); @@ -4615,7 +4986,7 @@ class HaWashdataPanel extends HTMLElement { if (!showPause && !showResume && !showStop) return ''; return `
${showResume ? `` : ''} - ${showPause ? `` : ''} + ${showPause ? `` : ''} ${showStop ? `` : ''}
`; })(); @@ -4629,12 +5000,12 @@ class HaWashdataPanel extends HTMLElement {
${_esc(label)} - ${!rec && dev.sub_state && dev.sub_state.toLowerCase() !== state ? `(${_esc(dev.sub_state)})` : ''} + ${!rec && dev.sub_state && dev.sub_state.toLowerCase() !== state ? `(${_esc(dev.sub_state === 'Stalled' ? this._tText('status.stalled', {}, 'Stalled') : dev.sub_state)})` : ''}
${programCtl}
${_fmtPower(dev.current_power_w)}
${this._t('lbl.power', {}, 'Power')}
-
${prog != null ? prog.toFixed(0) + '%' : '-'}
${this._t('lbl.progress', {}, 'Progress')}
+
${prog != null ? _fmtNum(prog, 0) + '%' : '-'}
${this._t('lbl.progress', {}, 'Progress')}
${_fmtDuration(rem)}
${this._t('lbl.remaining', {}, 'Remaining')}
${progressHtml} @@ -4644,7 +5015,6 @@ class HaWashdataPanel extends HTMLElement {
${this._canEdit() ? this._htmlRecordingWidget() : ''} ${debugHtml} - ${advHtml} `; } @@ -4680,7 +5050,7 @@ class HaWashdataPanel extends HTMLElement {
`; } - const msg = this._t(message_key, message_params || {}, ''); + const msg = this._tText(message_key, message_params || {}, ''); const ctaLabel = this._t(cta_label_key, {}, 'Continue'); const secLabel = secondary_label_key ? this._t(secondary_label_key, {}, '') @@ -4729,14 +5099,14 @@ class HaWashdataPanel extends HTMLElement { return; } if (ctaAction === 'open_cycles' || ctaAction === 'open_cycles_unlabeled') { - this._tab = 'history'; + this._tab = 'history'; this._historySub = 'cycles'; if (ctaAction === 'open_cycles_unlabeled') { this._cycleFilter = { ...(this._cycleFilter || {}), status: 'unlabeled' }; } this._fetchTabData(); return; } - if (ctaAction === 'open_profiles' || ctaAction === 'open_profiles_groups') { + if (ctaAction === 'open_profiles') { this._tab = 'profiles'; this._fetchTabData(); return; @@ -4748,15 +5118,15 @@ class HaWashdataPanel extends HTMLElement { return; } if (ctaAction === 'create_profile_from_cluster') { - // No modal shortcut yet — open the profiles tab where the user can create - // a profile; a future task may pre-populate from cluster cycle IDs. + // The Profiles tab shows the cluster with a "Create profile" that pre-selects + // its cycle (coverage-gap banner). this._tab = 'profiles'; this._fetchTabData(); return; } if (ctaAction && ctaAction.startsWith('open_cycle:')) { // No direct cycle modal shortcut yet — open the history tab. - this._tab = 'history'; + this._tab = 'history'; this._historySub = 'cycles'; this._fetchTabData(); return; } @@ -4781,20 +5151,28 @@ class HaWashdataPanel extends HTMLElement { _htmlPhaseTimeline(dev, prog, isRunning) { const phases = this._statusPhases || []; if (!isRunning || !phases.length || !dev.current_program) return ''; - // Total expected duration for placing phases (fractions of the cycle). - let total = (this._statusEnv && this._statusEnv.target_duration) || 0; - if (!total) { const p = (this._profiles || []).find(x => x.name === dev.current_program); total = (p && p.avg_duration) || 0; } - if (!total) total = Math.max(1, ...phases.map(p => p.end || 0)); + // Same span as the live phase sensor (progress.phase_timeline_span): progress maps + // onto the matched program's expected length (expected_duration_s), or the last + // range end when the ranges run longer, so a range reads at its real minutes + // (audit PROGRESS-10). A gap, or the stretch after the last range, names no + // phase (progress.phase_at: [start, end), the timeline's own end included). + const lastEnd = Math.max(0, ...phases.map(p => p.end || 0)); + const expected = Number(dev.expected_duration_s); + const total = Math.max(lastEnd, Number.isFinite(expected) ? expected : 0); if (total <= 0) return ''; const curFrac = (prog != null) ? Math.min(1, Math.max(0, prog / 100)) : null; let curPhase = ''; + let curFound = false; const segs = phases.map((ph, i) => { const x0 = Math.max(0, Math.min(1, (ph.start || 0) / total)); const x1 = Math.max(0, Math.min(1, (ph.end || 0) / total)); const width = Math.max(0, (x1 - x0) * 100); const col = _PALETTE[i % _PALETTE.length]; const reached = curFrac == null ? true : (x0 <= curFrac); - if (curFrac != null && curFrac >= x0 && curFrac < x1) curPhase = ph.name || ''; + if (!curFound && curFrac != null && x1 > x0 && curFrac >= x0 && (curFrac < x1 || (curFrac >= 1 && x1 >= 1))) { + curFound = true; + curPhase = ph.name || ''; + } const label = (ph.name && width > 12) ? `${_esc(ph.name)}` : ''; return `
${label}
`; }).join(''); @@ -4819,19 +5197,20 @@ class HaWashdataPanel extends HTMLElement { const dotCls = state === 'recording' ? 'wd-rec-active' : state === 'stopped' ? 'wd-rec-ready' : 'wd-rec-idle'; const stateLabel = state === 'recording' ? this._t('status.recording', {}, 'Recording…') : state === 'stopped' ? this._t('status.ready', {}, 'Ready to process') : this._t('status.idle', {}, 'Idle'); let detail = ''; - if (state === 'recording') detail = rs ? `${_fmtDuration(rs.duration_s)} · ${rs.sample_count || 0} samples` : ''; - else if (state === 'stopped') detail = `${rs.sample_count || 0} samples · ${_fmtDuration(rs.duration_s)}`; + const nSamples = (n) => this._t('lbl.n_samples', {n}, `${n} samples`); + if (state === 'recording') detail = rs ? `${_fmtDuration(rs.duration_s)} · ${nSamples(rs.sample_count || 0)}` : ''; + else if (state === 'stopped') detail = `${nSamples(rs.sample_count || 0)} · ${_fmtDuration(rs.duration_s)}`; const buttons = state === 'recording' ? `` : state === 'stopped' ? ` ` - : ``; + : ``; return `
-
${this._t('hdr.manual_recording', {}, 'Manual Recording')}${_tip(this._t('hdr.manual_recording_tip', {}, 'Run a cycle intentionally while WashData records the power trace. Start just before the appliance starts, Stop when it finishes, then Process to save it as a named profile.'))}${detail ? `${detail}` : ''}
+
${this._t('hdr.manual_recording', {}, 'Manual Recording')}${_tip(this._t('hdr.manual_recording_tip', {}, 'Record a cycle on purpose: press Start just before the appliance starts and Stop when it finishes, then Process to save it as a profile.'))}${detail ? `${detail}` : ''}
${buttons}
@@ -4840,6 +5219,15 @@ class HaWashdataPanel extends HTMLElement { // ── History tab ─────────────────────────────────────────────────────────── + // Cycles | Maintenance. Maintenance is a user-facing log and reminder feature, so + // it moved here from Advanced in 0.5.8 (audit UI-25); read users keep it. + _htmlHistoryTab() { + const sub = this._historySub === 'maintenance' ? 'maintenance' : 'cycles'; + const nav = [['cycles', this._t('tab.history', {}, 'Cycles')], ['maintenance', this._t('tab.maintenance', {}, 'Maintenance')]] + .map(([id, lbl]) => ``).join(''); + return `
${nav}
${sub === 'maintenance' ? this._htmlMaintenance() : this._htmlHistory()}`; + } + _htmlHistory() { const realCycles = this._cycles || []; const refCycles = this._refCycles || []; @@ -4860,17 +5248,12 @@ class HaWashdataPanel extends HTMLElement { const mlOf = c => mlById[c.id]; const isReviewed = c => { const m = mlOf(c); return !!(m && m.ml_review && m.ml_review.reviewed_at); }; const isGolden = c => { const m = mlOf(c); return !!(m && m.ml_review && m.ml_review.golden); }; - const needsReview = c => { - // Unresolved pending feedback ALWAYS needs review, even if an ML quality - // review was already saved: the two are separate, and the header counter - // counts pending feedback, so short-circuiting on reviewed_at here made the - // counter and the list disagree (#355). Pending feedback wins. - if (fbIds.has(c.id)) return true; - if (isReviewed(c)) return false; - const m = mlOf(c); - const lbl = m && m.ml_quality_label; - return ['uncertain', 'review'].includes(lbl) || ['force_stopped', 'interrupted'].includes(c.status); - }; + // One review count everywhere (audit UI-26): a cycle needs review when it has + // unresolved detection feedback, which is exactly what the Overview "To + // review" card counts (#355). The ML health label and abnormal ends used to + // queue cycles too; abnormal ends keep their own Interrupted / Force stopped + // filters. + const needsReview = c => fbIds.has(c.id); const needsReviewCount = allCycles.filter(needsReview).length; // Filter @@ -4924,12 +5307,12 @@ class HaWashdataPanel extends HTMLElement { }; const overrunBadge = c => { if (c.anomaly !== 'overrun') return ''; - const r = c.overrun_ratio ? ' ' + this._t('badge.overrun_ratio', {x: Number(c.overrun_ratio).toFixed(1)}, `(${Number(c.overrun_ratio).toFixed(1)}x expected)`) : ''; + const r = c.overrun_ratio ? ' ' + this._tText('badge.overrun_ratio', {x: Number(c.overrun_ratio).toFixed(1)}, `(${Number(c.overrun_ratio).toFixed(1)}x expected)`) : ''; return ` ⏱`; }; const underrunBadge = c => { if (c.anomaly !== 'underrun') return ''; - const r = c.underrun_ratio ? ' ' + this._t('badge.underrun_ratio', {pct: Math.round(c.underrun_ratio * 100)}, `(${Math.round(c.underrun_ratio * 100)}% of expected)`) : ''; + const r = c.underrun_ratio ? ' ' + this._tText('badge.underrun_ratio', {pct: Math.round(c.underrun_ratio * 100)}, `(${Math.round(c.underrun_ratio * 100)}% of expected)`) : ''; return ` ⚡`; }; const energyAnomalyBadge = c => { @@ -4945,23 +5328,23 @@ class HaWashdataPanel extends HTMLElement { const artifactBadge = c => { const n = Array.isArray(c.artifacts) ? c.artifacts.length : 0; if (!n) return ''; - return ` ⚠`; + return ` ⚠`; }; const restartGapBadge = c => { const n = Array.isArray(c.restart_gaps) ? c.restart_gaps.length : 0; if (!n) return ''; - return ` ↻`; + return ` ↻`; }; const cur = (this._hass && this._hass.config && this._hass.config.currency) || ''; const costCell = c => { if (c.cost == null) return '-'; - const txt = `${c.cost.toFixed(2)}${cur ? ' ' + cur : ''}`; + const txt = _fmtCost(c.cost, cur); // A time-weighted cost (#426) is a different claim than a flat one, so say // which price produced it rather than leaving the two indistinguishable. if (c.energy_price_mode === 'dynamic' && c.energy_price != null) { - const tip = this._t('col.cost_dynamic_tip', { price: c.energy_price.toFixed(4), cur }, - `Time-weighted: charged at the price in force during the cycle (effective ${c.energy_price.toFixed(4)} ${cur}/kWh).`); + const tip = this._tText('col.cost_dynamic_tip', { price: _fmtNum(c.energy_price, 4), cur }, + 'Time-weighted: charged at the price in force during the cycle (effective {price} {cur}/kWh).'); return `${txt}`; } return txt; @@ -4975,9 +5358,9 @@ class HaWashdataPanel extends HTMLElement { const check = rowSel ? `` : ``; - const stLabel = { completed: this._t('status.completed',{},'Completed'), interrupted: this._t('status.interrupted',{},'Interrupted'), force_stopped: this._t('status.force_stopped',{},'Force stopped'), active: this._t('status.active',{},'Active') }[st] || st; + const stLabel = this._cycleStatusLabel(st); const flags = `${importedBadge(c)}${reviewBadge(c)}${overrunBadge(c)}${underrunBadge(c)}${energyAnomalyBadge(c)}${artifactBadge(c)}${restartGapBadge(c)}`.trim(); - return ` + return ` ${check} ${prog ? _esc(prog) : `${this._t('lbl.unlabelled', {}, 'Unlabelled')}`} ${flags} @@ -4986,7 +5369,7 @@ class HaWashdataPanel extends HTMLElement { ${_fmtDuration(c.duration)} ${kwh != null ? _fmtEnergy(kwh) : '-'} ${costCell(c)} - ${conf != null ? conf.toFixed(0) + '%' : '-'} + ${conf != null ? _fmtNum(conf, 0) + '%' : '-'} `; }).join(''); @@ -4997,8 +5380,8 @@ class HaWashdataPanel extends HTMLElement { ${_th(this._t('lbl.status', {}, 'Status'), 'status', col === 'status', dir, 'cycsort', '', this._t('col.status_tip', {}, 'Cycle outcome: Completed (natural end), Interrupted (abrupt power drop), Force Stopped (manual), or Needs Review (feedback pending).'))} ${_th(this._t('lbl.date', {}, 'Date'), 'date', col === 'date', dir, 'cycsort', '', this._t('col.date_tip', {}, 'Date and time the cycle started.'))} ${_th(this._t('lbl.duration', {}, 'Duration'), 'duration', col === 'duration', dir, 'cycsort', 'right', this._t('col.duration_tip', {}, 'Total cycle run time from start to end.'))} - ${_th(this._t('lbl.energy', {}, 'Energy'), 'energy', col === 'energy', dir, 'cycsort', 'right', this._t('col.energy_tip', {}, 'Total energy consumed (kWh). Computed by integrating power over time.'))} - ${_th(this._t('lbl.cost', {}, 'Cost'), 'cost', col === 'cost', dir, 'cycsort', 'right', this._t('col.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.'))} + ${_th(this._t('lbl.energy', {}, 'Energy'), 'energy', col === 'energy', dir, 'cycsort', 'right', this._t('col.energy_tip', {}, 'Total energy used (kWh), computed from power over time.'))} + ${_th(this._t('lbl.cost', {}, 'Cost'), 'cost', col === 'cost', dir, 'cycsort', 'right', this._t('col.cost_tip', {}, 'Energy cost of this cycle, fixed when it finished. With Time-Weighted Cost on, each moment is charged at its own price; otherwise at the price at the end. Set a price in Settings to fill this in.'))} ${_th(this._t('lbl.confidence', {}, 'Confidence'), 'confidence', col === 'confidence', dir, 'cycsort', 'right', this._t('col.confidence_tip', {}, 'Profile match confidence (0-100%). How closely the cycle power curve matched the identified program.'))} `; @@ -5022,7 +5405,7 @@ class HaWashdataPanel extends HTMLElement { const title = this._t('lbl.cycles_title', {n: `${realCycles.length}${impNote}${shown}`}, `Cycles (${realCycles.length}${impNote}${shown})`); const toolbar = canEdit ? `
- +
` : ''; @@ -5039,9 +5422,10 @@ class HaWashdataPanel extends HTMLElement {
` : ''; - // D3: "Load more" pagination — only when the backend reports more rows. + // D3: "Load more" pagination - only when the backend reports more rows, real or + // imported (each list has its own cursor). const loadMoreBusy = this._busy.has('cyc-load-more'); - const loadMore = this._cyclesHasMore ? `
+ const loadMore = (this._cyclesHasMore || this._importedHasMore) ? `
` : ''; @@ -5056,12 +5440,23 @@ class HaWashdataPanel extends HTMLElement { ${loadMore}
`; - const cyclesErrorBanner = this._cyclesError ? `
${this._t('msg.fetch_error', {}, 'Failed to load data.')}
` : ''; + const cyclesErrorBanner = this._cyclesError ? `` : ''; return cyclesErrorBanner + cyclesHtml; } // ── Profiles tab ────────────────────────────────────────────────────────── + // The advisory for one profile and code, or undefined. Health, trend and drift + // warnings render from these only (audit UI-12): they carry the maintenance + // suppression, so a logged descale clears the nag the raw stats would keep. + _profileAdvisory(name, code) { + return (this._profileAdvisories || []).find(a => a && a.profile === name && a.code === code); + } + + _advisoryText(a) { + return this._tText(a.message_key, a.message_params || {}, a.message || ''); + } + _trendIcon(trend) { if (trend === 'up') return `↑`; if (trend === 'down') return `↓`; @@ -5070,59 +5465,45 @@ class HaWashdataPanel extends HTMLElement { _profileCardHtml(p) { const dur = p.avg_duration ? this._t('lbl.duration_avg', {v: Math.round(p.avg_duration / 60)}, `~${Math.round(p.avg_duration / 60)}m avg`) : this._t('lbl.no_duration', {}, 'no duration'); - const energy = p.avg_energy != null ? ` · ${_fmtEnergy(p.avg_energy)}/cycle` : ''; + const energy = p.avg_energy != null ? ` · ${this._t('lbl.energy_per_cycle', {v: _fmtEnergy(p.avg_energy)}, '{v}/cycle')}` : ''; const total = (p.avg_energy != null && p.cycle_count) - ? ` · ${_fmtEnergy(p.avg_energy * p.cycle_count)} total` : ''; + ? ` · ${this._t('lbl.energy_total', {v: _html(`${_esc(_fmtEnergy(p.avg_energy * p.cycle_count))}`)}, '{v} total')}` : ''; const cur = (this._hass && this._hass.config && this._hass.config.currency) || ''; - const cost = p.avg_cost != null ? ` · ${this._t('lbl.avg_cost', {}, 'Avg')} ${p.avg_cost.toFixed(2)}${cur ? ' ' + cur : ''}/${this._t('lbl.per_cycle_short', {}, 'cycle')}` : ''; + const cost = p.avg_cost != null ? ` · ${this._t('lbl.avg_cost', {}, 'Avg')} ${_fmtCost(p.avg_cost, cur)}/${this._t('lbl.per_cycle_short', {}, 'cycle')}` : ''; const h = (this._profileHealth || {})[p.name]; const t = (this._profileTrends || {})[p.name]; - let healthBadge = ''; - if (h && h.health_status === 'poor') { - healthBadge = `⚠ ${this._t('badge.poor_fit', {}, 'poor fit')}`; - } else if (h && h.health_status === 'fair') { - healthBadge = `${this._t('badge.fair_fit', {}, 'fair fit')}`; - } - // Trend badge: show if duration is drifting (up = slower/longer, concerning for lime buildup etc.) - let trendBadge = ''; - if (t) { - const durIcon = this._trendIcon(t.duration_trend); - const enIcon = t.energy_trend ? this._trendIcon(t.energy_trend) : ''; - if (t.duration_trend !== 'stable' || t.energy_trend === 'up') { - const tipParts = []; - if (t.duration_trend !== 'stable') { - const dp = `${t.duration_slope_pct > 0 ? '+' : ''}${t.duration_slope_pct}`; - tipParts.push(t.duration_trend === 'up' - ? this._t('msg.duration_trend_up_tip', {pct: dp}, `Duration up (${dp}%/cycle)`) - : this._t('msg.duration_trend_down_tip', {pct: dp}, `Duration down (${dp}%/cycle)`)); - } - if (t.energy_trend && t.energy_trend !== 'stable') { - const ep = `${t.energy_slope_pct > 0 ? '+' : ''}${t.energy_slope_pct}`; - tipParts.push(t.energy_trend === 'up' - ? this._t('msg.energy_trend_up_tip', {pct: ep}, `Energy up (${ep}%/cycle)`) - : this._t('msg.energy_trend_down_tip', {pct: ep}, `Energy down (${ep}%/cycle)`)); - } - const tip = tipParts.join(', ') || this._t('msg.performance_trending', {}, 'Performance trending'); - trendBadge = `${durIcon}${enIcon || ''}`; - } - } + const poorAdv = this._profileAdvisory(p.name, 'poor_health'); + const healthBadge = poorAdv + ? `⚠ ${this._t('badge.poor_fit', {}, 'poor fit')}` + : ''; + const trendAdv = this._profileAdvisory(p.name, 'duration_trend_up') || this._profileAdvisory(p.name, 'energy_trend_up'); + const trendBadge = trendAdv + ? `${this._trendIcon('up')}` + : ''; const warmupThreshold = (this._constants && this._constants.PROFILE_MIN_WARMUP_CYCLES) || 5; const cycleCount = (h && h.cycle_count) || 0; // Imported profiles are trusted downloaded templates: exempt from warm-up (they // match immediately), shown with an "Imported" badge instead of "Still learning". const isWarmup = cycleCount < warmupThreshold && !p.is_imported; const warmupBadge = isWarmup - ? `${this._t('msg.warmup_badge', {done: cycleCount, needed: warmupThreshold}, `Still learning (${cycleCount}/${warmupThreshold} cycles)`)}` + ? `${this._t('msg.warmup_badge', {done: cycleCount, needed: warmupThreshold}, `Still learning (${cycleCount}/${warmupThreshold} cycles)`)}` : ''; const importedBadge = p.is_imported ? `📥 ${this._t('status.imported', {}, 'Imported')}` : ''; + // STORE-21: how often the matcher labelled one of this appliance's own cycles + // with an imported program, so an import that never fits can be pruned. + // Counted locally from the stored cycles; nothing is sent to the store. + const usedN = p.is_imported ? Number((this._profileMatcherCounts || {})[p.name] || 0) : null; + const usedBadge = usedN == null ? '' : `${usedN > 0 + ? this._t('badge.matcher_used', {n: usedN}, `Matched ${usedN} of your cycles`) + : this._t('badge.matcher_unused', {}, 'Not matched to your cycles yet')}`; // A program with no cycle behind it is silently absent from every match: it can // never win, and it cannot veto a shorter look-alike either (#400). That state used // to be a debug log only, so it is called out here, on the program itself. - const unmatchableAdv = (this._profileAdvisories || []).find(a => a && a.profile === p.name && a.code === 'unmatchable'); + const unmatchableAdv = this._profileAdvisory(p.name, 'unmatchable'); const unmatchableBadge = unmatchableAdv - ? `⚠ ${this._t('badge.unmatchable', {}, "can't be matched")}` + ? `⚠ ${this._t('badge.unmatchable', {}, "can't be matched")}` : ''; // How this program ends, measured from its own cycles. A dishwasher that has // gone quiet for its drying phase is the commonest "is it finished?" question, @@ -5135,28 +5516,27 @@ class HaWashdataPanel extends HTMLElement { const mins = Math.max(1, Math.round(term.quiet_before_s / 60)); const watts = Number(term.event_watts || 0).toFixed(0); const secs = Math.round(term.event_seconds || 0); - const tTip = this._t('badge.quiet_tail_tip', + const tTip = this._tText('badge.quiet_tail_tip', {mins, secs, watts, seen: term.seen_in, measured: term.measured}, `Near the end this program goes quiet for about ${mins} min, then draws about ` - + `${watts} W for ${secs} s before finishing. Seen in ${term.seen_in} of ` - + `${term.measured} measured cycles: the appliance does not do it every run.`); + + `${watts} W for ${secs} s. Seen in ${term.seen_in} of ${term.measured} measured cycles.`); terminalBadge = `${this._t('badge.quiet_tail', {mins}, `~${mins}m quiet tail`)}`; } // Register item 304: cycles filed under this program that are too far from its // usual length to ever match it. Not cosmetic - they also drag avg_duration, and // with it every future time estimate for this program. - const durAdv = (this._profileAdvisories || []).find(a => a && a.profile === p.name && a.code === 'duration_outlier'); + const durAdv = this._profileAdvisory(p.name, 'duration_outlier'); const durBadge = durAdv - ? `\u26A0 ${this._t('badge.duration_outlier', {n: (durAdv.message_params || {}).n || 1}, `${(durAdv.message_params || {}).n || 1} odd-length cycle(s)`)}` + ? `\u26A0 ${this._t('badge.duration_outlier', {n: (durAdv.message_params || {}).n || 1}, `${(durAdv.message_params || {}).n || 1} odd-length cycle(s)`)}` : ''; - const badges = [unmatchableBadge, durBadge, healthBadge, trendBadge, terminalBadge, warmupBadge, importedBadge].filter(Boolean).join(' '); + const badges = [unmatchableBadge, durBadge, healthBadge, trendBadge, terminalBadge, warmupBadge, importedBadge, usedBadge].filter(Boolean).join(' '); // Mini power-signature curve: the profile's real average power shape (from its // envelope), so the card thumbnail matches the actual cycle. Painted after // render by _drawProfileSparklines. Needs ≥3 envelope points. const spark = (Array.isArray(p.signature_curve) && p.signature_curve.length >= 3) - ? `` + ? `` : ''; return `
@@ -5166,7 +5546,7 @@ class HaWashdataPanel extends HTMLElement { ${spark}
${badges ? `
${badges}
` : ''} -
${p.cycle_count || 0} cycles · ${dur}${energy}${total}${cost}
+
${this._t('lbl.n_cycles', {n: p.cycle_count || 0}, '{n} cycles')} · ${dur}${energy}${total}${cost}
`; } @@ -5211,7 +5591,7 @@ class HaWashdataPanel extends HTMLElement { const canEdit = this._canEdit(); const byName = {}; this._profiles.forEach(p => { byName[p.name] = p; }); - const pg = this._profileGroups || { groups: [], suggestions: [] }; + const pg = this._profileGroups || { groups: [] }; const groupedNames = new Set(); pg.groups.forEach(g => (g.members || []).forEach(m => groupedNames.add(m))); @@ -5251,14 +5631,27 @@ class HaWashdataPanel extends HTMLElement {
` : ''; - const profilesHtml = onboardBanner + ` + // Coverage gaps: unlabelled recent cycles that look like one program the user + // has not created (92-95% precise on the corpus, silent when nothing is + // missing; register item 432). The setup card's "create from cluster" lands here. + const gapClusters = canEdit ? (((this._coverageGaps || {}).profile_suggestions) || []).slice(0, 2) : []; + const gapBanner = gapClusters.map(c => { + const min = Math.round((c.avg_duration_s || 0) / 60); + const cid = (c.cycle_ids || [])[0] || ''; + return `
+ 🧩 ${this._t('msg.coverage_gap_cluster', {count: c.count, min}, `${c.count} recent unlabelled cycles of about ${min} min look like one program you have not created yet.`)} + +
`; + }).join(''); + + const profilesHtml = onboardBanner + gapBanner + `
${this._t('tab.profiles', {}, 'Profiles')} (${this._profiles.length})

${this._t('msg.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.')}

${canEdit ? `
- - + +
` : ''}
${groupSections} @@ -5271,11 +5664,11 @@ class HaWashdataPanel extends HTMLElement { const subtabBtns = [ ['profiles', this._t('tab.subtab_profiles', {}, 'Profiles')], ['phase-catalog', this._t('tab.subtab_phase_catalog', {}, 'Phase Catalog')], - ].map(([id, lbl]) => ``).join(''); + ].map(([id, lbl]) => ``).join(''); - const profilesErrorBanner = (this._profilesError || this._profileGroupsError) ? `
${this._t('msg.fetch_error', {}, 'Failed to load data.')}
` : ''; + const profilesErrorBanner = (this._profilesError || this._profileGroupsError) ? `` : ''; return ` -
${subtabBtns}
+
${subtabBtns}
${this._profSubtab === 'phase-catalog' ? this._htmlPhases() : (profilesErrorBanner + profilesHtml)} `; } @@ -5311,7 +5704,7 @@ class HaWashdataPanel extends HTMLElement { ${canvas}
${this._t('lbl.members', {}, 'Members')}${members.length ? ` (${members.length})` : ''}
${checks || `${this._t('msg.no_profiles_yet_short', {}, 'No profiles yet.')}`}
-

${this._t('msg.group_modal_help', {}, 'Group programs with the same shape that differ in temperature/spin (durations may vary). Matching scores the group as one candidate, then picks the best-fitting member. Pick at least 2; the overlay shows how alike they are.')}

+

${this._t('msg.group_modal_help', {}, 'Group programs with the same shape that differ in temperature or spin. Matching scores the group as one, then picks the best-fitting member. Pick at least 2; the overlay shows how alike they are.')}

${m.orig ? `` : ''} @@ -5345,9 +5738,32 @@ class HaWashdataPanel extends HTMLElement { // Advanced shows everything; Basic shows only fields flagged `basic: true`. // Purely a visibility filter — hidden fields keep their stored values. _settingFieldVisible(f) { + // Internal tuning (audit UI-02): set by the integration and its suggestions, + // so it is shown only on request, when the device has its own value, or when + // it carries a conflict or a suggestion - never silently out of reach. + if (f.internal && !this._pref('show_internal', false) && !(this._internalReveal || new Set()).has(f.key)) return false; return this._settingsLevel() === 'advanced' || !!f.basic; } + // Internal fields that must stay visible whatever the toggle says: a value of + // the device's own, a conflict, or a suggestion pointing at them. + _computeInternalReveal(o) { + const reveal = new Set(); + const defaults = this._optDefaults || {}; + const conflicts = this._conflictKeysFromOpts(); + const sug = new Set((this._suggestions || []).map(x => x.key)); + for (const sec of _SETTINGS_SECTIONS) { + for (const f of (sec.fields || (sec.groups || []).flatMap(g => g.fields || []))) { + if (!f.internal) continue; + const v = (this._opts || {})[f.key]; + const d = defaults[f.key] !== undefined ? defaults[f.key] : f.def; + const own = v !== undefined && v !== null && v !== '' && String(v) !== String(d); + if (own || conflicts.has(f.key) || sug.has(f.key) || Object.prototype.hasOwnProperty.call(this._pendingSettings || {}, f.key)) reveal.add(f.key); + } + } + return reveal; + } + // F2: does a section expose at least one basic-flagged field? Used to hide // sections that would render empty in Basic mode. _secHasBasicFields(sec) { @@ -5357,18 +5773,16 @@ class HaWashdataPanel extends HTMLElement { _htmlSettings() { const o = this._editedOpts(); + if (!Object.keys(o).length && this._tabError === 'settings') + return ``; if (!Object.keys(o).length) return `
⚙️
${this._t('msg.loading_settings', {}, 'Loading settings…')}
`; - const suggestionsErrorBanner = this._suggestionsError ? `
${this._t('msg.fetch_error', {}, 'Failed to load data.')}
` : ''; + const suggestionsErrorBanner = this._suggestionsError ? `` : ''; const level = this._settingsLevel(); const basicMode = level === 'basic'; + this._internalReveal = this._computeInternalReveal(o); - const classicSugKeys = new Set((this._suggestions || []).map(s => s.key)); - // Calibrated (ML) recommendations that still differ from the effective value - // count as tuning suggestions too, so the section dots + banner surface them, - // mirroring the tab bulb and the "Show only" filter, which already include ML. - const mlSugKeys = this._mlSugKeys(o); - const sugKeys = new Set([...classicSugKeys, ...mlSugKeys]); + const sugKeys = new Set((this._suggestions || []).map(s => s.key)); const secHasSug = (sec) => { const fields = sec.fields || (sec.groups || []).flatMap(g => g.fields || []); return fields.some(f => sugKeys.has(f.key)); @@ -5410,6 +5824,10 @@ class HaWashdataPanel extends HTMLElement { const basicNote = basicMode ? `

${this._t('msg.settings_basic_note', {}, 'Showing essential settings. Switch to Advanced for the full list.')}

` : ''; + // Advanced only: reveal the internal tuning fields (search always finds them). + const internalToggle = basicMode ? '' : ``; const saveBusy = this._busy.has('save-settings'); const confCount = _secConfKeys.size; @@ -5419,16 +5837,10 @@ class HaWashdataPanel extends HTMLElement { ⚠ ${this._t('conflict.settings_banner', {n: confCount}, `Setting conflicts: ${confCount}. Check the highlighted sections and fix them before saving.`)}
` : ''; - // Combined tuning-suggestion count: classic (observed) + Calibrated (ML) keys - // not already covered by a classic suggestion. Apply-all / Dismiss act on the - // classic engine only, so they render solely when a classic suggestion exists; - // Calibrated recommendations are applied individually via their "Use" button. - const classicSugCount = this._suggestions.length; - const mlOnlyCount = [...mlSugKeys].filter(k => !classicSugKeys.has(k)).length; - const sugCount = classicSugCount + mlOnlyCount; + const sugCount = this._suggestions.length; const sugOnly = this._settingsSugOnly && !this._settingsSearch; - const applyAllBtn = classicSugCount ? `` : ''; - const dismissBtn = classicSugCount ? `` : ''; + const applyAllBtn = sugCount ? `` : ''; + const dismissBtn = sugCount ? `` : ''; const banner = sugCount ? (sugOnly ? `
💡 ${this._t('msg.showing_suggestions', {count: sugCount}, `Showing ${sugCount} setting${sugCount > 1 ? 's' : ''} with suggestions.`)} ${this._t('msg.show_all_settings', {}, 'Show all settings')}. @@ -5462,7 +5874,7 @@ class HaWashdataPanel extends HTMLElement { return ` ${suggestionsErrorBanner}
-
${this._t('tab.settings', {}, 'Settings')}${this._mlSettingsLoading ? ` ${this._t('msg.ml_loading', {}, 'loading ML…')}` : ''}
+
${this._t('tab.settings', {}, 'Settings')}
${analyzeBtn}
${confBanner}${banner}${mutedBanner}${basicNote} @@ -5470,6 +5882,7 @@ class HaWashdataPanel extends HTMLElement { ${searchInput}
${nav}
${levelToggle} + ${internalToggle}
${formContent}
@@ -5478,7 +5891,7 @@ class HaWashdataPanel extends HTMLElement {
-

${this._t('msg.saving_triggers_reload', {}, 'Saving triggers an integration reload. HA entities may briefly show as unavailable.')}

+

${this._t('msg.saving_triggers_reload', {}, 'Saving reloads the integration. Entities may briefly show as unavailable.')}

${this._htmlSettingsHistory()} `; @@ -5573,21 +5986,14 @@ class HaWashdataPanel extends HTMLElement { extra.suggestion = { suggested: sug.suggested, current: sug.current, reason: sug.reason, reason_key: sug.reason_key, reason_params: rp }; } - // A muted key (#343) must hide its Calibrated (ML) recommendation too - the - // mute is per-setting, not per-engine, so an ML-only suggestion would - // otherwise stay visible (and counted) after the user muted it. - const mlc = (this._mlSettings || {})[f.key]; - if (mlc && mlc.ml_value != null && !(this._lockedSuggestions || []).includes(f.key)) { - extra.mlSuggestion = { value: mlc.ml_value, reason: mlc.ml_reason, reason_key: mlc.ml_reason_key, reason_params: mlc.ml_reason_params }; - } - extra.useBtnLabel = this._t('btn.use', {}, 'Use'); extra.t = this._t.bind(this); + extra.tText = this._tText.bind(this); // D7: "what changed" marker — a dot with a tooltip when this field appears in // the settings changelog. const chg = (this._settingsChangeByKey || {})[f.key]; if (chg) { - extra.changed = this._t('msg.setting_changed', + extra.changed = this._tText('msg.setting_changed', { old: _chgVal(chg.old), new: _chgVal(chg.new), date: _fmtDate(chg.timestamp) }, `Changed from ${_chgVal(chg.old)} to ${_chgVal(chg.new)} on ${_fmtDate(chg.timestamp)}`); } @@ -5676,7 +6082,7 @@ class HaWashdataPanel extends HTMLElement { if (manualUrl) bits.push(`${this._t('link.manual', {}, 'Manual ↗')}`); // "by " attribution is optional (Online & Community pref). if (((this._constants && this._constants.storePrefs) || {}).show_contributor !== false) { - bits.push(this._t('store.contributed_by', {name: _esc(match.createdByName || this._t('lbl.anonymous', {}, 'Anonymous'))}, `by ${_esc(match.createdByName || 'Anonymous')}`)); + bits.push(this._t('store.contributed_by', {name: match.createdByName || this._tText('lbl.anonymous', {}, 'Anonymous')}, 'by {name}')); } const connected = !!(this._storeStatus && this._storeStatus.connected); let actions = ''; @@ -5692,8 +6098,8 @@ class HaWashdataPanel extends HTMLElement { // The + button already covers "not in the catalog", and the approved-only // toggle is intentionally gone: pending entries are shown with a tag. const storeActions = (this._onlineEnabled() && this._storeDeviceDeclared()) ? `
- - + +
` : ''; return `
@@ -6038,6 +6444,8 @@ class HaWashdataPanel extends HTMLElement { if (this._modal !== m) return; m.profiles = (r && r.items) || []; m.deviceId = (r && r.device_id) || null; + // An outage is not "no profiles yet" (audit STORE-09). + m.storeUnreachable = !!(r && r.error === 'store_unreachable'); } catch (_) { if (this._modal === m) m.profiles = []; } if (this._modal === m) this._render(); } @@ -6052,7 +6460,7 @@ class HaWashdataPanel extends HTMLElement { const list = this._deviceAutomations || []; const pills = list.length ? list.map(a => `` + - `🔗 ${_esc(a.name)}${a.enabled ? '' : ' (off)'}` + + `🔗 ${_esc(a.name)}${a.enabled ? '' : ` ${this._t('lbl.automation_off', {}, '(off)')}`}` + `` + ``).join('') : `${this._autoLoading ? this._t('msg.loading', {}, 'Loading…') : this._t('hdr.no_automations', {}, 'No automations reference this device yet.')}`; @@ -6070,7 +6478,7 @@ class HaWashdataPanel extends HTMLElement {
` : ''; return `
${this._t('hdr.automations', {}, 'Automations')}
-

${this._t('msg.automations_intro', {start: 'ha_washdata_cycle_started', end: 'ha_washdata_cycle_ended'}, '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.')}

+

${this._t('msg.automations_intro', {start: _html('ha_washdata_cycle_started'), end: _html('ha_washdata_cycle_ended')}, '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.')}

${legacyBlock}
${pills}
@@ -6126,10 +6534,15 @@ class HaWashdataPanel extends HTMLElement { if (!dev) return; const hass = this._hass; const eventType = kind === 'started' ? 'ha_washdata_cycle_started' : 'ha_washdata_cycle_ended'; - const label = kind === 'started' ? 'started' : 'finished'; + // Plain text (_tText): these are written into the user's automation config. + const device = dev.title || 'WashData'; const config = { - alias: `${dev.title || 'WashData'}: cycle ${label}`, - description: `Runs when the WashData ${dev.title || ''} cycle ${label}. Add your actions (notify, lights, ...).`, + alias: kind === 'started' + ? this._tText('msg.automation_alias_started', {device}, '{device}: cycle started') + : this._tText('msg.automation_alias_finished', {device}, '{device}: cycle finished'), + description: kind === 'started' + ? this._tText('msg.automation_desc_started', {device}, 'Runs when a {device} cycle starts. Add your actions (notify, lights, ...).') + : this._tText('msg.automation_desc_finished', {device}, 'Runs when a {device} cycle finishes. Add your actions (notify, lights, ...).'), mode: 'single', trigger: [{ platform: 'event', event_type: eventType, event_data: { entry_id: dev.entry_id } }], condition: [], @@ -6144,7 +6557,7 @@ class HaWashdataPanel extends HTMLElement { this._navigate('/config/automation/edit/new'); } } catch (e) { - this._showToast(this._t('msg.toast_automation_failed', {error: e.message || e}, 'Could not create automation: ' + (e.message || e)), 'error'); + this._showToast(this._tText('msg.toast_automation_failed', {error: e.message || e}, 'Could not create automation: ' + (e.message || e)), 'error'); } } @@ -6183,7 +6596,7 @@ class HaWashdataPanel extends HTMLElement { const errTxt = e.message || e; if (!created) { // Automation was never created — a plain retry is safe. - this._showToast(this._t('msg.toast_convert_failed', {error: errTxt}, 'Convert failed: ' + errTxt), 'error'); + this._showToast(this._tText('msg.toast_convert_failed', {error: errTxt}, 'Convert failed: ' + errTxt), 'error'); return; } // The automation WAS created but the follow-up clear threw. The clear may have @@ -6211,7 +6624,7 @@ class HaWashdataPanel extends HTMLElement { // user the automation exists (don't retry). try { await hass.callApi('DELETE', 'config/automation/config/' + id); - this._showToast(this._t('msg.toast_convert_rolled_back', {error: errTxt}, 'Migration failed and was rolled back (no automation left behind): ' + errTxt), 'error'); + this._showToast(this._tText('msg.toast_convert_rolled_back', {error: errTxt}, 'Migration failed and was rolled back (no automation left behind): ' + errTxt), 'error'); } catch (_) { this._showToast(this._t('msg.toast_convert_orphan', {}, 'The automation was created, but clearing the old actions failed. Do not retry: remove the legacy actions manually to avoid a duplicate automation.'), 'error'); } @@ -6242,7 +6655,7 @@ class HaWashdataPanel extends HTMLElement { const trainCard = ''; if (sec.id === 'notifications') { - const varsHint = `

${this._t('msg.notify_services_hint', {entity: 'notify.<name>', vars: '' + _esc(_NOTIFY_VARS) + ''}, 'Use {entity} service IDs (comma-separated for multiple). Template variables: {vars}.')}

`; + const varsHint = `

${this._t('msg.notify_services_hint', {entity: _html('notify.<name>'), vars: _html('' + _esc(_NOTIFY_VARS) + '')}, 'Use {entity} service IDs (comma-separated for multiple). Template variables: {vars}.')}

`; const groups = sec.groups.map(grp => { const fields = (grp.fields || []).filter(f => this._settingFieldVisible(f)).map(f => this._renderField(f, o)).filter(Boolean).join(''); return fields ? `
${_esc(this._t('setting_group.' + _slugSub(grp.sub) + '.label', {}, grp.sub))}
${fields}
` : ''; @@ -6275,7 +6688,14 @@ class HaWashdataPanel extends HTMLElement { if (currentDeviceType && s.onlyDeviceTypes && !s.onlyDeviceTypes.includes(currentDeviceType)) return false; return true; }); - const match = f => (`${f.label || ''} ${f.key || ''} ${f.doc || ''} ${f.hint || ''}`).toLowerCase().includes(q); + // Match what the user actually reads (audit UI-09): the translated label and + // doc, plus the key and the English schema text, so English terms still work. + const match = f => { + const tr = f.key + ? `${this._tText('setting.' + f.key + '.label', {}, '')} ${f.doc != null ? this._tText('setting.' + f.key + '.doc', {}, '') : ''}` + : ''; + return (`${f.label || ''} ${f.key || ''} ${f.doc || ''} ${f.hint || ''} ${tr}`).toLowerCase().includes(q); + }; let out = ''; let count = 0; for (const sec of sections) { @@ -6287,73 +6707,19 @@ class HaWashdataPanel extends HTMLElement { count += hits.length; out += `
${_esc(this._t('section.' + sec.id + '.label', {}, sec.label))}
${rendered}
`; } - return count ? out : `

${this._t('msg.no_settings_match', {q}, `No settings match "${_esc(q)}"`)}

`; + return count ? out : `

${this._t('msg.no_settings_match', {q}, 'No settings match "{q}"')}

`; + } + + // Tuning suggestions waiting on one device-list entry: the backend's key list + // (muted and no-op suggestions already dropped), else its bare count. + _sugCountForDevice(dev) { + if (!dev) return 0; + return Array.isArray(dev.suggestion_keys) ? dev.suggestion_keys.length : (dev.suggestions_count || 0); } // Cross-section view showing only the fields that have active suggestions. - // Setting keys with a Calibrated (ML) recommendation that still differs from the - // effective current value (staged edit if present, else the saved option). Shared - // by the settings banner, section dots, tab bulb, and the "Show only" filter so - // Calibrated suggestions surface everywhere classic (observed) suggestions do. - _mlSugKeys(eff) { - // Fall back to the device-resolved defaults for an unset field (#396) so an ML - // value equal to that default is not counted as differing from "current" - which - // would badge a phantom suggestion whose "Use" value already equals what runs. - const cur = eff || Object.assign({}, this._optDefaults, this._opts, this._pendingSettings || {}); - // Muted keys (#343) are excluded so the banner count, section dots, tab bulb - // and the "Show only" filter all follow the mute state the same way the - // classic suggestions do (which are dropped from this._suggestions on mute). - return this._mlSugKeysFrom(this._mlSettings, cur, this._lockedSuggestions); - } - - // Same rule for an arbitrary (comparison, current values, muted keys) triple, - // so the device-pill badges can score a device that is not the selected one - // from the per-entry caches. - _mlSugKeysFrom(mlSettings, cur, locked) { - const muted = new Set(locked || []); - const vals = cur || {}; - const keys = new Set(); - for (const [key, mlc] of Object.entries(mlSettings || {})) { - if (muted.has(key)) continue; - if (mlc && mlc.ml_value != null && !_sugSame(mlc.ml_value, vals[key])) keys.add(key); - } - return keys; - } - - // Tuning-suggestion counts for one entry of the device list: classic (observed, - // counted by the backend) plus the Calibrated (ML) recommendations that no - // classic suggestion already covers. Same arithmetic as the Settings tab - // banner, so the pill badge, the Overview attention card and the banner agree. - _sugCountsForDevice(dev) { - if (!dev) return { classic: 0, ml: 0, total: 0 }; - const sel = this._devices[this._selIdx]; - const isSel = !!(sel && sel.entry_id === dev.entry_id); - // Keys let us drop an ML recommendation for a key that already has a classic - // suggestion; a payload carrying only the count (older backend, test mocks) - // falls back to adding the two. - const cKeys = Array.isArray(dev.suggestion_keys) ? dev.suggestion_keys : null; - const classic = cKeys ? cKeys.length : (dev.suggestions_count || 0); - // Staged (unsaved) edits count for the selected device only; every other - // device is scored against its saved options, which the poll keeps fresh. - // Device-resolved defaults (#396) sit under the saved options so an unset field - // scores against the value the integration would actually use, not `undefined`. - const cur = isSel - ? Object.assign({}, dev.option_defaults || {}, dev.options || {}, this._opts, this._pendingSettings || {}) - : Object.assign({}, dev.option_defaults || {}, dev.options || {}); - const mlKeys = this._mlSugKeysFrom( - isSel ? this._mlSettings : this._mlSettingsByEntry[dev.entry_id], - cur, - isSel ? this._lockedSuggestions : this._lockedByEntry[dev.entry_id], - ); - const ml = cKeys ? [...mlKeys].filter(k => !cKeys.includes(k)).length : mlKeys.size; - return { classic, ml, total: classic + ml }; - } - _htmlSettingsSugOnly(o) { const sugKeys = new Set((this._suggestions || []).map(s => s.key)); - // Include Calibrated (ML) recommendations so the "Show only" filter surfaces - // ML-only recommendations too (same set the banner + section dots use). - for (const k of this._mlSugKeys(o)) sugKeys.add(k); if (!sugKeys.size) return `

${this._t('msg.no_suggestions', {}, 'No active suggestions.')}

`; const currentDeviceType = (this._opts && this._opts.device_type) || ''; const sections = _SETTINGS_SECTIONS.filter(s => { @@ -6375,9 +6741,10 @@ class HaWashdataPanel extends HTMLElement { } // Dedicated "ML Training" tab: the single home for all ML, laid out as a plain - // sectioned dashboard (Status / Settings / What it's learned / Program-matching - // fine-tuning). Options save through the same path as Settings (_saveSettings - // scans every [data-opt] in the shadow root). + // sectioned dashboard (Status / Settings / What it's learned). Options save + // through the same path as Settings (_saveSettings scans every [data-opt] in + // the shadow root). The program-matching fine-tuning card went with the + // matcher weight tuner in 0.5.8. _htmlMlTab() { const o = this._opts; if (!Object.keys(o).length) @@ -6401,11 +6768,10 @@ class HaWashdataPanel extends HTMLElement {
-

${this._t('msg.saving_triggers_reload', {}, 'Saving triggers an integration reload.')}

+

${this._t('msg.saving_triggers_reload', {}, 'Saving reloads the integration. Entities may briefly show as unavailable.')}

${this._htmlMlLearnedSection(st)} - ${this._htmlMatchingTuningCard()} `; } @@ -6431,7 +6797,7 @@ class HaWashdataPanel extends HTMLElement { ? this._t('msg.enough_data', {current: cyc, min: min}, `Enough data to learn from (${cyc}/${min} cycles).`) : this._t('msg.collecting_data', {need: need, current: cyc, min: min}, `Collecting data. Cycles still needed before fine-tuning can start: ${need} (${cyc}/${min}).`); const bar = `
`; - const last = st.last_trained ? _fmtDate(st.last_trained) : 'never'; + const last = st.last_trained ? _fmtDate(st.last_trained) : this._tText('ml.never', {}, 'never'); const state = running ? ` ${this._t('status.fine_tuning', {}, 'fine-tuning now…')}` : (st.enabled ? this._t('lbl.auto_fine_tune_on', {hour: String(st.hour).padStart(2, '0')}, `auto fine-tune on (around ${String(st.hour).padStart(2, '0')}:00)`) : this._t('lbl.auto_fine_tune_off', {}, 'auto fine-tune off')); @@ -6453,17 +6819,21 @@ class HaWashdataPanel extends HTMLElement { const models = st.on_device_models || {}; const keys = Object.keys(models); const reverting = this._busy.has('ml-revert-models'); + const lastRuns = st.last_run || {}; let body; if (!keys.length) { - body = `

${this._t('msg.no_fine_tuned', {}, 'Nothing fine-tuned yet — WashData is using its built-in models.')}

`; + // Why nothing is fine-tuned, from the last run (audit ML-20). + const why = Object.keys(lastRuns).map(cap => this._mlLastRunLine(lastRuns[cap])).filter(Boolean).join(''); + body = `

${this._t('msg.no_fine_tuned', {}, 'Nothing fine-tuned yet - WashData is using its built-in models.')}

${why}`; } else { const rows = keys.map(cap => { const m = models[cap] || {}; - const when = m.trained_at ? _fmtDate(m.trained_at) : 'unknown'; + const when = m.trained_at ? _fmtDate(m.trained_at) : this._tText('ml.trained_unknown', {}, 'at an unknown time'); return `
${_esc(m.label_key ? this._t(m.label_key, {}, m.label || cap) : (m.label || cap))}${this._mlTrendBadge(m.trend)}
-
${_esc(m.blurb_key ? this._t(m.blurb_key, {}, m.blurb || '') : (m.blurb || ''))} · ${this._t('ml.fine_tuned_at', {when: _esc(when)}, 'fine-tuned ' + _esc(when))}
+
${_esc(m.blurb_key ? this._t(m.blurb_key, {}, m.blurb || '') : (m.blurb || ''))} · ${this._t('ml.fine_tuned_at', {when}, 'fine-tuned {when}')}
+ ${this._mlLastRunLine(lastRuns[cap])}
${this._mlQualityChip(m)}
`; @@ -6481,21 +6851,22 @@ class HaWashdataPanel extends HTMLElement { } // Humanized "fit" indicator for a fine-tuned model: a coloured word + bar, with - // the exact metric on hover. Classifiers use held-out AUC; regressors use how - // much they beat the baseline estimate. + // the exact metric on hover: how much the regressor beats the baseline estimate + // (every fine-tuned model is a regressor since 0.5.8). _mlQualityChip(m) { - let pct = 0, word = '', title = m.metric_key ? this._t(m.metric_key, m.metric_params || {}, m.metric || '') : (m.metric || ''); - if (m.auc != null) { - pct = Math.max(0, Math.min(1, (m.auc - 0.5) / 0.5)) * 100; - word = m.auc >= 0.85 ? this._t('ml.fit_strong',{},'Strong') : m.auc >= 0.75 ? this._t('ml.fit_good',{},'Good') : m.auc >= 0.65 ? this._t('ml.fit_fair',{},'Fair') : this._t('ml.fit_weak',{},'Weak'); - } else if (m.model_mae != null && m.naive_mae != null && m.naive_mae > 0) { + let pct = 0, word = '', title = m.metric_key ? this._tText(m.metric_key, m.metric_params || {}, m.metric || '') : (m.metric || ''); + if (m.model_mae != null && m.naive_mae != null && m.naive_mae > 0) { const impr = Math.max(0, (m.naive_mae - m.model_mae) / m.naive_mae); pct = Math.min(1, impr) * 100; word = impr >= 0.5 ? this._t('ml.fit_strong',{},'Strong') : impr >= 0.2 ? this._t('ml.fit_good',{},'Good') : this._t('ml.fit_slight',{},'Slight'); - title = this._t('ml.better_than_baseline', {pct: (impr * 100).toFixed(0), metric: title}, `${(impr * 100).toFixed(0)}% better than the baseline estimate (${title})`); + title = this._tText('ml.better_than_baseline', {pct: (impr * 100).toFixed(0), metric: title}, `${(impr * 100).toFixed(0)}% better than the baseline estimate (${title})`); } else { return ''; } + // How many cycles the figure rests on (audit ML-20). + if (Number.isInteger(m.held_out_cycles)) { + title += ' · ' + this._tText('ml.held_out_count', {count: m.held_out_cycles}, `measured on ${m.held_out_cycles} held-out cycles`); + } const col = pct >= 70 ? 'var(--success-color,#4caf50)' : pct >= 40 ? 'var(--warning-color,#ff9800)' : 'var(--secondary-text-color)'; return `
${word} ${this._t('ml.fit_word', {}, 'fit')}
@@ -6509,7 +6880,7 @@ class HaWashdataPanel extends HTMLElement { if (!trend) return ''; const map = { improving: [this._t('badge.improving',{},'↗ improving'), 'var(--success-color,#4caf50)', this._t('ml.trend_improving_tip', {}, "This model's fit has improved across recent re-checks.")], - declining: [this._t('badge.declining',{},'↘ declining'), 'var(--warning-color,#ff9800)', this._t('ml.trend_declining_tip', {}, "This model's fit has slipped across recent re-checks — reviewing more cycles may help it re-learn.")], + declining: [this._t('badge.declining',{},'↘ declining'), 'var(--warning-color,#ff9800)', this._t('ml.trend_declining_tip', {}, "This model's fit has slipped across recent re-checks – reviewing more cycles may help it re-learn.")], steady: [this._t('badge.steady',{},'→ steady'), 'var(--secondary-text-color)', this._t('ml.trend_steady_tip', {}, "This model's fit has held roughly steady across recent re-checks.")], }; const e = map[trend]; @@ -6517,58 +6888,30 @@ class HaWashdataPanel extends HTMLElement { return ` ${e[0]}`; } - // Matcher scoring-weight tuning: current defaults vs the on-device tuned - // override, which set is live, and a revert-to-default control. - _htmlMatchingTuningCard() { - const st = this._mlTrainingStatus; - const m = st && st.matching; - if (!m) return ''; - const def = m.defaults || {}; - const rec = m.tuned || null; - const cfg = (rec && rec.config) || null; - const tuned = m.active === 'tuned' && cfg; - const reverting = this._busy.has('ml-revert-match'); - const fmt = (v) => (v == null || isNaN(v)) ? '-' : Number(v).toFixed(2); - const rows = [ - ['corr_weight', 'Shape (correlation)'], - ['duration_weight', 'Duration agreement'], - ['energy_weight', 'Energy agreement'], - ['dtw_ensemble_w', 'DTW derivative blend (DDTW)'], - ].map(([k, lbl]) => { - const dv = def[k], iv = tuned ? cfg[k] : def[k]; - const changed = tuned && dv != null && iv != null && Math.abs(dv - iv) > 1e-9; - return ` - ${lbl} - ${fmt(dv)} - ${fmt(iv)} - `; - }).join(''); - const badge = tuned - ? `${this._t('badge.using_tuned', {}, 'Using tuned weights')}` - : `${this._t('badge.using_defaults', {}, 'Using shipped defaults')}`; - let meta = ''; - if (tuned) { - const when = rec.trained_at ? _fmtDate(rec.trained_at) : 'unknown'; - const b = rec.baseline_test_top1, t = rec.tuned_test_top1; - const gain = (b != null && t != null) - ? ` · held-out top-1 ${(b * 100).toFixed(0)}% → ${(t * 100).toFixed(0)}%` : ''; - meta = `

Tuned ${_esc(when)} from ${rec.cycle_count || 0} cycles${gain}.

`; - } - const revertBtn = tuned - ? `` - : ''; - return `
-
-
${this._t('hdr.ml_matching_tuning', {}, 'Program-matching fine-tuning')}
${revertBtn} -
-

${this._t('msg.matching_tuning_intro', {}, 'When learning, WashData also adjusts how much program matching weighs shape versus duration and energy.')}

-
${badge}
- - - ${rows} -
${this._t('lbl.emphasis', {}, 'Emphasis')}${this._t('lbl.default', {}, 'Default')}${this._t('lbl.in_use', {}, 'In use')}
- ${meta} -
`; + // Why a training run put no new model in use (audit ML-20), as plain text from + // the backend's reason code + params. `[code, text]`; an unknown code reads as + // 'unknown'. + _mlSkipReason(rec) { + const fallbacks = { + insufficient_rows: 'Not enough usable cycles yet: {count} labelled, clean cycles so far.', + holdout_too_small: 'Too few cycles to test a new model fairly: {held_out} could be set aside for testing, {min} are needed.', + not_better_than_naive: 'The new model was not clearly better than the simple estimate (error {model} vs {naive}).', + not_better_than_incumbent: 'The new model was not better than the one in use (error {model} vs {incumbent}), so the current one was kept.', + fit_failed: 'Training failed: {error}', + unknown: 'Nothing new was learnt.', + }; + const code = Object.prototype.hasOwnProperty.call(fallbacks, rec && rec.reason_code) ? rec.reason_code : 'unknown'; + const params = (rec && rec.reason_params) || {}; + const vars = code === 'insufficient_rows' ? {count: Number(params.cycles) || 0} : {...params}; + return [code, this._tText(`ml.skip_reason.${code}`, vars, fallbacks[code])]; + } + + // The last run's reason as one line; '' when it promoted or none is on record. + _mlLastRunLine(run) { + if (!run || run.promoted !== false) return ''; + const [code, reason] = this._mlSkipReason(run); + const when = run.ts ? _fmtDate(run.ts) : this._tText('ml.trained_unknown', {}, 'at an unknown time'); + return `
${this._t('ml.last_run_not_promoted', {when, reason}, 'Last check ({when}): {reason}')}
`; } // ── F3: Playground tab (what-if simulator / A-B / DTW inspector) ───────────── @@ -6583,30 +6926,18 @@ class HaWashdataPanel extends HTMLElement { ['min_off_gap', 'Min Off Gap', 's', 'Gap required to separate two cycles', 'timing'], ['completion_min_seconds', 'Min Cycle Duration', 's', 'Shortest run that counts as a real cycle', 'timing'], ['start_duration_threshold','Start Duration', 's', 'Seconds above threshold to confirm start', 'timing'], - ['end_repeat_count', 'End Repeat Count', '', 'Low readings in a row before ending', 'advanced'], ['interrupted_min_seconds', 'Interrupted Min', 's', 'Short cycles flagged as interrupted', 'advanced'], ['anti_wrinkle_enabled', 'Enable Anti-Wrinkle Detection', '', 'Absorb the tumble pulses after the main phase instead of reading them as new cycles', 'advanced', 'bool'], ['anti_wrinkle_max_power', 'Max Anti-Wrinkle Power','W', 'A pulse above this ends anti-wrinkle and opens a new cycle', 'advanced'], ['anti_wrinkle_max_duration','Max Anti-Wrinkle Duration','s', 'A pulse longer than this ends anti-wrinkle and opens a new cycle', 'advanced'], - ['anti_wrinkle_exit_power', 'Anti-Wrinkle Exit Power','W', 'Power must fall below this between pulses for anti-wrinkle to stay active', 'advanced'], + ['anti_wrinkle_exit_power', 'Anti-Wrinkle Exit Power','W', 'Quiet level between tumble pulses; only takes effect above the Stop Threshold', 'advanced'], ['anti_wrinkle_idle_timeout','Max Pulse Gap', 's', 'Quiet time allowed between two tumble pulses before anti-wrinkle ends', 'advanced'], ['dishwasher_end_spike_quiet_release','Passive-Dry Quiet Release','s', 'Dishwasher: quiet seconds after expected duration before the end-of-cycle drain wait is released', 'advanced'], - ['smart_termination_duration_ratio', 'Smart Termination 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', 'advanced'], - ['anti_crease_finalize_ratio', '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 a dryer whose sensor-dry runtime follows the load, so its tail is recognised instead of sitting until the fallback timeout. Leave washing machines at the default: there this fraction is what keeps a quiet mid-wash phase from being mistaken for the tail. Separate from the Smart Termination Ratio, which gates a different check.', 'advanced'], - ['curve_preroll_seconds', 'Curve Pre-roll', 's', 'Seconds of readings from aborted start attempts that may be carried into the front of a cycle\'s curve. Machines that probe before settling (programme selection, door lock, first fill) can drop the first minutes of real activity from every curve. 0 turns this off. Note that enabling it moves the recorded start earlier, so cycles recorded before and after the change carry different durations for the same program until the older ones age out - expect the learned averages to drift for a while.', 'timing'], - ['profile_match_min_duration_ratio', 'Min Duration Ratio', '', 'Stage 1: shortest run (vs the profile) still allowed to match', 'matching'], - ['profile_match_max_duration_ratio', 'Max Duration Ratio', '', 'Stage 1: longest run (vs the profile) still allowed to match', 'matching'], - ['corr_weight', 'Correlation Weight', '', 'Stage 2: balance between curve shape (correlation) and power level (MAE); default 0.45', 'matching'], - ['keep_min_score', 'Keep Min Score', '', 'Stage 2: floor score to stay in the race; default 0.1 admits weak matches, later stages pick the best', 'matching'], - ['dtw_bandwidth', 'DTW Bandwidth', '', 'Stage 3: Sakoe-Chiba warp band (0 = DTW off; default 0.2 = 20% of cycle length)', 'matching'], - ['dtw_blend', 'DTW Blend', '', 'Stage 3: 0 = core score only, 1 = DTW score only, 0.5 = equal blend (default)', 'matching'], - ['dtw_ensemble_w', 'DTW Ensemble Weight','', 'Stage 3 (ensemble mode): weight on scaled-L1 vs derivative DTW; default 0.7 favours level-aware', 'matching'], - ['dtw_ddtw_scale', 'DDTW Scale', '', 'Stage 3: DDTW half-saturation distance; smaller = more shape-sensitive (default 30)', 'matching'], - ['dtw_refine_top_n', 'DTW Refine Top-N', '', 'Stage 3: candidates DTW re-scores; raise to 7-9 if correct profile ranks 4th-5th (default 5)', 'matching'], - ['duration_weight', 'Duration Weight', '', 'Stage 4: how strongly run-length agreement affects the final score (default 0.22)', 'matching'], - ['energy_weight', 'Energy Weight', '', 'Stage 4: how strongly energy agreement affects the final score (default 0.22)', 'matching'], - ['duration_scale', 'Duration Scale', '', 'Stage 4: log-ratio where duration agreement halves; smaller = stricter penalty (default 0.175)', 'matching'], - ['energy_scale', 'Energy Scale', '', 'Stage 4: log-ratio where energy agreement halves; smaller = stricter penalty (default 0.25)', 'matching'], + ['smart_termination_duration_ratio', 'Smart Termination Ratio', '', 'Fraction of the program\'s average duration a cycle must reach before Smart Termination may end it early; lower it if run times vary', 'advanced'], + ['anti_crease_finalize_ratio', 'Anti-Crease Finalize Ratio', '', 'Fraction of the program\'s average duration a cycle must reach before the anti-crease tail may finish it; lower it on dryers, keep washers at the default', 'advanced'], + ['curve_preroll_seconds', 'Curve Pre-roll', 's', 'Seconds of readings from aborted start attempts carried into the front of a cycle\'s curve; 0 = off', 'timing'], + ['profile_match_min_duration_ratio', 'Min Duration Ratio', '', 'Minimum cycle length as a fraction of the profile\'s duration; default 0.1 (10%)', 'matching'], + ['profile_match_max_duration_ratio', 'Max Duration Ratio', '', 'Maximum cycle length as a fraction of the profile\'s duration; default 1.8 (180%)', 'matching'], ]; } @@ -6664,15 +6995,11 @@ class HaWashdataPanel extends HTMLElement { _pgApplySuggestions(r) { this._pgSuggClassic = (r.classic_suggestions && typeof r.classic_suggestions === 'object') ? r.classic_suggestions : {}; - this._pgSuggMl = (r.ml_suggestions && typeof r.ml_suggestions === 'object') ? r.ml_suggestions : null; - this._pgMlSuggEnabled = !!r.ml_suggestions_enabled; } - // Suggestions label the two "Load suggested" buttons and nothing else, and the ML set - // runs statistics over every clean cycle - real work that used to sit on the critical - // path of opening the tab, ahead of two more round-trips. Fetched in the background - // instead; the buttons render only once their count is non-zero, so they appear when - // the data lands rather than holding up the whole tab. + // Suggestions label the "Load suggested" button and nothing else, so they are + // fetched in the background instead of on the critical path of opening the tab; + // the button renders once its count is non-zero. async _pgFetchSuggestions(entryId) { try { const r = await this._ws({ @@ -6759,27 +7086,27 @@ class HaWashdataPanel extends HTMLElement { const publishable = changed.filter(k => this._pgIsPublishable(k)); const status = !loaded - ? `${this._t('msg.pg_live_unavailable', {}, 'Live settings unavailable — showing defaults.')}` + ? `${this._t('msg.pg_live_unavailable', {}, 'Live settings unavailable, showing defaults.')}` : (changed.length ? `${this._t('msg.pg_n_changed', {n: changed.length}, changed.length + ' changed vs live settings')}` : `✓ ${this._t('msg.pg_matches_live', {}, 'Matches live settings')}`); - const presetOpts = `` - + (this._pgPresets || []).map(p => ``).join(''); + const presets = this._pgPresets || []; + const presetOpts = `` + + presets.map(p => ``).join(''); const hasSel = !!this._pgPresetSel; - const atLimit = this._pgPresetLimit > 0 && (this._pgPresets || []).length >= this._pgPresetLimit - && !(this._pgPresets || []).some(p => p.name === (this._pgPresetName || '').trim()); + const atLimit = this._pgPresetAtLimit((this._pgPresetName || '').trim()); const presetRow = `
- - - ${canEdit ? `` : ''} + + + ${canEdit ? `` : ''}
`; const saveRow = canEdit ? `
- - - ${this._t('msg.pg_preset_limit', {n: this._pgPresetLimit}, 'Preset limit reached (' + this._pgPresetLimit + ')')} + + + ${this._t('msg.pg_preset_limit', {n: this._pgPresetLimit}, 'Preset limit reached ({n})')}
` : ''; const publishBtn = (canEdit && publishable.length) @@ -6787,13 +7114,9 @@ class HaWashdataPanel extends HTMLElement { : ''; const suggClassicCount = Object.keys(this._pgSuggClassic || {}).length; - const suggMlCount = Object.keys(this._pgSuggMl || {}).length; const suggClassicBtn = suggClassicCount > 0 ? `` : ''; - const suggMlBtn = (this._pgMlSuggEnabled && suggMlCount > 0) - ? `` - : ''; return `
@@ -6803,7 +7126,7 @@ class HaWashdataPanel extends HTMLElement {

${this._t('msg.pg_ctrl_intro', {}, 'Values start from this device\'s live integration settings. Edits stay in the Playground until you publish them.')}

- ${suggClassicBtn}${suggMlBtn} + ${suggClassicBtn} ${publishBtn}
${presetRow} @@ -6811,6 +7134,14 @@ class HaWashdataPanel extends HTMLElement {
`; } + // Saving under `name` would need a new slot and the device is at its cap (an + // existing name can always be overwritten). + _pgPresetAtLimit(name) { + const presets = this._pgPresets || []; + return this._pgPresetLimit > 0 && presets.length >= this._pgPresetLimit + && !presets.some(p => p.name === name); + } + // Replace the sandbox values with a preset: stage only what differs from the live // baseline, so the graph's before/after diff keeps meaning "vs the integration". _pgApplyPresetValues(values) { @@ -6824,12 +7155,21 @@ class HaWashdataPanel extends HTMLElement { } } + _pgLoadPreset() { + const preset = (this._pgPresets || []).find(p => p.name === this._pgPresetSel); + if (!preset) return; + this._pgApplyPresetValues(preset.values); + this._showToast(this._tText('toast.pg_preset_loaded', {name: preset.name}, `Preset "${preset.name}" loaded`)); + this._render(); + requestAnimationFrame(() => this._pgDrawCanvas()); + } + async _pgSavePreset() { const dev = this._devices[this._selIdx]; const name = (this._pgPresetName || '').trim(); if (!dev || !this._canEdit() || !name) return; if ((this._pgPresets || []).some(p => p.name === name) - && !confirm(this._t('msg.pg_preset_overwrite', {name}, `Overwrite the preset "${name}"?`))) return; + && !confirm(this._tText('msg.pg_preset_overwrite', {name}, `Overwrite the preset "${name}"?`))) return; await this._busyRun('pg-preset-save', async () => { try { const r = await this._ws({ type: `${_DOMAIN}/save_playground_preset`, entry_id: dev.entry_id, name, values: this._pgCurrentValues() }); @@ -6837,9 +7177,9 @@ class HaWashdataPanel extends HTMLElement { this._pgPresets = Array.isArray(r.presets) ? r.presets : this._pgPresets; this._pgPresetSel = name; this._pgPresetName = ''; - this._showToast(this._t('toast.pg_preset_saved', {name}, `Preset "${name}" saved`)); + this._showToast(this._tText('toast.pg_preset_saved', {name}, `Preset "${name}" saved`)); } catch (e) { - this._showToast(this._t('msg.toast_save_failed', {error: e.message || e}, 'Save failed: ' + (e.message || e)), 'error'); + this._showToast(this._tText('msg.toast_save_failed', {error: e.message || e}, 'Save failed: ' + (e.message || e)), 'error'); } }); this._render(); @@ -6849,7 +7189,7 @@ class HaWashdataPanel extends HTMLElement { const dev = this._devices[this._selIdx]; const name = this._pgPresetSel; if (!dev || !this._canEdit() || !name) return; - if (!confirm(this._t('msg.pg_preset_delete_confirm', {name}, `Delete the preset "${name}"?`))) return; + if (!confirm(this._tText('msg.pg_preset_delete_confirm', {name}, `Delete the preset "${name}"?`))) return; await this._busyRun('pg-preset-delete', async () => { try { const r = await this._ws({ type: `${_DOMAIN}/delete_playground_preset`, entry_id: dev.entry_id, name }); @@ -6857,7 +7197,7 @@ class HaWashdataPanel extends HTMLElement { this._pgPresets = Array.isArray(r.presets) ? r.presets : (this._pgPresets || []).filter(p => p.name !== name); this._pgPresetSel = ''; } catch (e) { - this._showToast(this._t('msg.toast_error', {error: e.message || e}, 'Error: ' + (e.message || e)), 'error'); + this._showToast(this._tText('msg.toast_error', {error: e.message || e}, 'Error: ' + (e.message || e)), 'error'); } }); this._render(); @@ -6868,7 +7208,7 @@ class HaWashdataPanel extends HTMLElement { const dev = this._devices[this._selIdx]; if (!dev) return; const changed = this._pgChangedKeys(); - if (changed.length && !confirm(this._t('msg.pg_load_live_confirm', {n: changed.length}, `Discard ${changed.length} Playground edit(s) and reload the integration's current settings?`))) return; + if (changed.length && !confirm(this._tText('msg.pg_load_live_confirm', {n: changed.length}, `Discard ${changed.length} Playground edit(s) and reload the integration's current settings?`))) return; await this._busyRun('pg-load-live', async () => { this._pgThreshStart = null; this._pgThreshStop = null; this._pgParamOverrides = {}; await this._pgFetchSettings(dev.entry_id); @@ -6878,9 +7218,9 @@ class HaWashdataPanel extends HTMLElement { } // Stage suggested values as Playground overrides (additive merge - does not - // reset existing edits, unlike _pgApplyPresetValues which does a full replace). - _pgLoadSuggested(source) { - const vals = source === 'ml' ? (this._pgSuggMl || {}) : (this._pgSuggClassic || {}); + // reset existing edits). + _pgLoadSuggested() { + const vals = this._pgSuggClassic || {}; const live = this._pgEffective || {}; let staged = 0; for (const [key, val] of Object.entries(vals)) { @@ -6895,11 +7235,7 @@ class HaWashdataPanel extends HTMLElement { if (staged === 0) { this._showToast(this._t('msg.pg_sugg_none', {}, 'All suggestions already match the current settings')); } else { - const msgKey = source === 'ml' ? 'toast.pg_sugg_ml_loaded' : 'toast.pg_sugg_loaded'; - const fallback = source === 'ml' - ? `Staged ${staged} ML-calibrated value(s) - run the Playground to compare` - : `Staged ${staged} suggested value(s) - run the Playground to compare`; - this._showToast(this._t(msgKey, {n: staged}, fallback)); + this._showToast(this._tText('toast.pg_sugg_loaded', {n: staged}, `Staged ${staged} suggested value(s) - run the Playground to compare`)); } this._render(); requestAnimationFrame(() => this._pgDrawCanvas()); @@ -6912,7 +7248,7 @@ class HaWashdataPanel extends HTMLElement { const val = this._pgStagedVal(key); if (val === undefined || val === null) return; const lbl = this._t('setting.' + key + '.label', {}, key); - if (!confirm(this._t('msg.pg_publish_one_confirm', {label: lbl, value: val}, `Save ${lbl} = ${val} to this device's settings?`))) return; + if (!confirm(this._tText('msg.pg_publish_one_confirm', {label: lbl, value: val}, `Save ${lbl} = ${val} to this device's settings?`))) return; await this._busyRun('pg-publish-' + key, async () => { try { await this._ws({ type: `${_DOMAIN}/set_options`, entry_id: dev.entry_id, options: { [key]: val } }); @@ -6925,7 +7261,7 @@ class HaWashdataPanel extends HTMLElement { this._pgClearStaged(key); this._showToast(this._t('toast.settings_saved', {}, 'Settings saved; integration reloading')); } catch (e) { - this._showToast(this._t('msg.toast_save_failed', {error: e.message || e}, 'Save failed: ' + (e.message || e)), 'error'); + this._showToast(this._tText('msg.toast_save_failed', {error: e.message || e}, 'Save failed: ' + (e.message || e)), 'error'); } }); this._render(); @@ -6941,7 +7277,7 @@ class HaWashdataPanel extends HTMLElement { // Compact cycle dropdown const cycleOpts = cycles.map(c => { const prog = c.profile_name || c.matched_profile || this._t('lbl.unlabelled', {}, 'Unlabelled'); - const dur = c.duration ? ` · ${Math.round(c.duration / 60)} min` : ''; + const dur = c.duration ? ` · ${Math.round(c.duration / 60)} ${this._tText('lbl.timer_min', {}, 'min')}` : ''; const dateStr = c.start_time ? ` · ${_fmtDate(c.start_time)}` : ''; return ``; }).join(''); @@ -6955,7 +7291,7 @@ class HaWashdataPanel extends HTMLElement { const busy = this._pgLoading; const topBar = `
-
+
${busy ? `` : ''} @@ -6993,7 +7329,7 @@ class HaWashdataPanel extends HTMLElement { return `
${this._t('hdr.playground', {}, 'Playground')}
-

${this._t('msg.playground_intro', {}, 'Explore how settings affect detection on your real cycle data. Nothing here changes live configuration until you explicitly apply it.')}

+

${this._t('msg.playground_intro', {}, 'Try settings on your real cycle data. Nothing here changes your live settings until you apply it.')}

${restartNote} ${workbench} ${this._htmlPgDrawer()} @@ -7023,8 +7359,9 @@ class HaWashdataPanel extends HTMLElement { `; } - // Just the detection-parameter editor rows (Simulate mode); a change re-runs - // the faithful sim so the state band + estimates update. + // Just the detection-parameter editor rows (Simulate mode). A change redraws the + // thresholds at once; the outcome card then marks the simulation stale and offers + // to re-run it (audit PLAYGROUND-13), rather than re-running on every keystroke. _htmlPgParamRows() { const fields = this._pgOverrideFields(); const threshFields = new Set(['start_threshold_w', 'stop_threshold_w']); @@ -7063,7 +7400,7 @@ class HaWashdataPanel extends HTMLElement { const isChanged = staged !== undefined && staged !== null && !(liveVal !== '' && liveVal !== null && liveVal !== undefined && this._pgSameVal(staged, liveVal)); const pubBtn = (this._canEdit() && isChanged && this._pgIsPublishable(key)) - ? `` + ? `` : ``; return `${header}
@@ -7079,35 +7416,10 @@ class HaWashdataPanel extends HTMLElement {
`; }).join(''); - const stressGc = '#c0392b'; - const stressHeader = `
-
- ${_esc(this._t('lbl.pg_stress_group', {}, 'Idle termination test'))} -
`; - const stressToggle = `
-
-
${_esc(this._t('lbl.pg_stress_toggle', {}, 'Test idle termination'))}
-
${_esc(this._t('lbl.pg_stress_toggle_desc', {}, 'Simulates the appliance staying at its standby draw after recording ends — shows if and when WashData stops the cycle.'))}
-
- -
`; - const stressIdleField = this._pgStressTail ? `
-
-
${_esc(this._t('lbl.pg_stress_idle_w', {}, 'Idle level (W)'))}
-
${_esc(this._t('lbl.pg_stress_idle_w_desc', {}, 'Override the auto-detected standby floor (leave blank for auto).'))}
-
-
- - W -
-
` : ''; - const stressGroup = `${stressHeader}${stressToggle}${stressIdleField}`; return `
${this._t('hdr.pg_detection_params', {}, 'Detection settings')}
${this._htmlPgControlPanel()} - ${paramRows}${stressGroup} + ${paramRows}
${this._pgDetailBusy ? `` : ''} @@ -7128,26 +7440,58 @@ class HaWashdataPanel extends HTMLElement { const alertRows = alerts.length ? alerts.map(a => `
${_esc(this._pgAlertLabel(a.code))} -
${_esc(a.detail_key ? this._t(a.detail_key, a.detail_params || {}, a.detail || '') : (a.detail || ''))}
+
${_esc(a.detail_key ? this._tText(a.detail_key, a.detail_params || {}, a.detail || '') : (a.detail || ''))}
`).join('') : `
✓ ${this._t('msg.pg_no_alerts', {}, 'No issues detected in this run.')}
`; const term = o.termination_reason ? String(o.termination_reason) : '—'; const dur = o.final_duration_s ? Math.round(o.final_duration_s / 60) + ' min' : '—'; const proj = o.projected_energy_wh != null - ? (o.projected_energy_wh >= 1000 ? (o.projected_energy_wh / 1000).toFixed(2) + ' kWh' : Math.round(o.projected_energy_wh) + ' Wh') + ? (o.projected_energy_wh >= 1000 ? _fmtNum(o.projected_energy_wh / 1000, 2) + ' kWh' : _fmtNum(Math.round(o.projected_energy_wh), 0) + ' Wh') : '—'; + const hasNotify = (d.events || []).some(e => String(e.type || '').startsWith('notify_')); const outcomeChip = (label, val) => `
${_esc(val)}
${_esc(label)}
`; + // Edits only redraw (audit PLAYGROUND-13): say this outcome predates them + // rather than let it read as "that setting has no effect". + const stale = this._pgDetailSig != null && this._pgDetailSig !== this._pgOverrideSig(this._pgCurrentOverride()); + const staleBanner = stale + ? `
+ ⚠ ${this._t('msg.pg_outcome_stale', {}, 'Settings changed since this simulation ran. It still shows the old result.')} + +
` + : ''; return `
${this._t('hdr.pg_outcome', {}, 'Simulation outcome')}
+ ${staleBanner}
${outcomeChip(this._t('lbl.pg_ended', {}, 'Ended'), term)} ${outcomeChip(this._t('lbl.duration', {}, 'Duration'), dur)} ${outcomeChip(this._t('lbl.pg_proj_energy', {}, 'Proj. energy'), proj)} + ${o.label_reason ? outcomeChip(this._t('lbl.pg_autolabel', {}, 'Auto-label'), this._pgLabelVerdict(o).short) : ''}
${alertRows}
+ ${hasNotify ? `

🔔 ${this._t('msg.pg_notify_partial', {}, 'Markers show start, almost-done, finish, milestone and quiet-hours notifications only. Live updates, unload reminders and overrun alerts are not simulated.')}

` : ''}
`; } + // Would the manager auto-label this replayed cycle at cycle end? The same + // label_verdict the live cycle end runs (playground outcome, audit F7). + _pgLabelVerdict(o) { + if (o.would_label && o.label_profile) { + return { ok: true, short: String(o.label_profile), + tip: this._tText('msg.pg_label_yes', {name: o.label_profile}, `Would be labelled "${o.label_profile}" at cycle end`) }; + } + const reasons = { + no_winner: this._tText('pg_label_reason.no_winner', {}, 'No program fit'), + below_floor: this._tText('pg_label_reason.below_floor', {}, 'Below the learning confidence'), + ambiguous: this._tText('pg_label_reason.ambiguous', {}, 'Flagged ambiguous'), + margin: this._tText('pg_label_reason.margin', {}, 'Too close to the runner-up'), + unknown_profile: this._tText('pg_label_reason.unknown_profile', {}, 'Program no longer exists'), + }; + const why = reasons[o.label_reason] || this._tText('pg_label_reason.not_evaluated', {}, 'Not evaluated'); + return { ok: false, short: this._tText('lbl.pg_label_review', {}, 'Asks for review'), + tip: this._tText('msg.pg_label_no', {reason: why}, `Would ask for review: ${why}`) }; + } + _pgAlertLabel(code) { const map = { overrun: this._t('lbl.pg_alert_overrun', {}, 'Overrun'), @@ -7159,9 +7503,6 @@ class HaWashdataPanel extends HTMLElement { energy_anomaly: this._t('lbl.pg_alert_energy', {}, 'Energy anomaly'), timeout_end: this._t('lbl.pg_alert_timeout_end', {}, 'Ended by timeout, not prediction'), would_run_indefinitely: this._t('lbl.pg_alert_indefinite', {}, 'Would run indefinitely'), - stress_terminated: this._t('lbl.pg_alert_stress_ok', {}, 'Idle termination: cycle stopped'), - stress_above_threshold: this._t('lbl.pg_alert_stress_warn', {}, 'Idle draw above stop threshold'), - stress_hit_cap: this._t('lbl.pg_alert_stress_cap', {}, 'Hit safety cap'), }; return map[code] || code; } @@ -7174,13 +7515,13 @@ class HaWashdataPanel extends HTMLElement { || this._pgThreshStart != null || this._pgThreshStop != null; const controls = `
${this._t('lbl.last', {}, 'Last')} - + ${this._t('lbl.cycles_lc', {}, 'cycles')} ${overrideActive ? `⚙ ${this._t('msg.pg_override_active', {}, 'Using your edited settings vs. current')}` : ''}
`; const simbar = busy ? this._htmlPgBatchBar() : ''; - const intro = `

${this._t('msg.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.')}

`; + const intro = `

${this._t('msg.pg_history_intro2', {}, 'Replay recent cycles through the real detector and matcher with the settings above. Click a row to load it in the graph; change a setting to compare before and after.')}

`; if (!h || !Array.isArray(h.rows)) { return `${intro}${controls}${this._htmlPgRecentRuns('pg_history')}${simbar}${busy ? '' : `
${this._t('msg.pg_history_empty', {}, 'Press Run to replay your cycles.')}
`}`; } @@ -7201,12 +7542,14 @@ class HaWashdataPanel extends HTMLElement { (h.baseline_rows || []).forEach(r => { baseById[r.cycle_id] = r; }); const rows = h.rows.map(r => { const c = cyc(r.cycle_id); - const when = c && c.start_time ? _fmtDate(c.start_time) : (r.cycle_id || '').slice(0, 8); + const when = c && c.start_time ? _fmtDate(c.start_time) : (r.start_time ? _fmtDate(r.start_time) : (r.cycle_id || '').slice(0, 8)); const ok = r.match_correct === true ? '✓' : (r.match_correct === false ? '✗' : '—'); const okColor = r.match_correct === true ? 'var(--success-color,#4caf50)' : (r.match_correct === false ? 'var(--error-color,#f44336)' : 'var(--secondary-text-color)'); const match = r.matched_profile || this._t('lbl.unlabelled', {}, 'Unlabelled'); const durM = r.duration_s ? Math.round(r.duration_s / 60) + 'm' : '—'; const over = r.overrun_ratio != null ? ` (${Math.round(r.overrun_ratio * 100)}%)` : ''; + const lv = r.label_reason ? this._pgLabelVerdict(r) : null; + const labelGlyph = lv ? ` ${lv.ok ? '🏷' : '?'}` : ''; const alertGlyph = (r.alerts && r.alerts.length) ? ` ⚠` : ''; const b = baseById[r.cycle_id]; let deltaCol = ''; @@ -7218,7 +7561,7 @@ class HaWashdataPanel extends HTMLElement { const sel = r.cycle_id === this._pgCycleId ? ' selected' : ''; return ` ${_esc(when)} - ${ok} ${_esc(match)} + ${ok} ${_esc(match)}${labelGlyph} ${_esc(String(r.termination_reason || '—'))} ${durM}${over}${alertGlyph} ${h.diff ? `${deltaCol}` : ''} @@ -7231,7 +7574,14 @@ class HaWashdataPanel extends HTMLElement { ${this._t('lbl.duration', {}, 'Duration')} ${h.diff ? `${this._t('lbl.pg_vs_current', {}, 'vs current')}` : ''} ${rows}`; - return `${intro}${controls}${this._htmlPgRecentRuns('pg_history')}${diffBanner}${summaryLine}${table}`; + return `${intro}${controls}${this._htmlPgRecentRuns('pg_history')}${diffBanner}${summaryLine}${this._htmlPgInSampleNote()}${table}`; + } + + // PLAYGROUND-23: History and Optimize replay cycles against profiles that were + // built partly from those same cycles (no leave-one-out), so their scores run + // a little high. Measured on 59 cycles: correct matches 46 vs 44 held out. + _htmlPgInSampleNote() { + return `

${this._t('msg.pg_in_sample_note', {}, 'These cycles helped build the profiles they are matched against, so results are optimistic: expect slightly fewer correct matches on new cycles.')}

`; } // Determinate progress bar for chunked history/sweep runs, rendered once while @@ -7265,29 +7615,40 @@ class HaWashdataPanel extends HTMLElement { async _pgRunHistory() { const dev = this._devices[this._selIdx]; if (!dev) return; - const ids = (this._cycles || []).slice(0, Math.max(1, this._pgSimCycles || 20)).map(c => c.id); - if (!ids.length) { this._showToast(this._t('msg.no_cycles_selected', {}, 'No cycles available.'), 'error'); return; } - const override = { ...this._pgParamOverrides }; - if (this._pgThreshStart != null) override.start_threshold_w = this._pgThreshStart; - if (this._pgThreshStop != null) override.stop_threshold_w = this._pgThreshStop; - this._pgBatchProgress = { done: 0, total: ids.length }; + if (!(this._cycles || []).length) { this._showToast(this._t('msg.no_cycles_selected', {}, 'No cycles available.'), 'error'); return; } + // The most recent N by count, not by id: the loaded page holds only 25 cycles, + // so "Last 40" replayed 25 (audit PLAYGROUND-18). The result reports how many ran. + const count = Math.max(1, Math.min(_PG_MAX_BATCH_CYCLES, this._pgSimCycles || 20)); + const override = this._pgCurrentOverride(); + this._pgBatchProgress = { done: 0, total: count }; this._busy.add('pg-history'); this._render(); try { - const r = await this._ws({ type: `${_DOMAIN}/start_playground_history`, entry_id: dev.entry_id, cycle_ids: ids, settings_override: override }); + const r = await this._ws({ type: `${_DOMAIN}/start_playground_history`, entry_id: dev.entry_id, count, settings_override: override }); this._pgHistoryTaskId = r && r.task_id; this._pgNeedsRestart = false; if (!this._pgHistoryTaskId) throw new Error('no task id'); - this._addProvisionalTask(this._pgHistoryTaskId, 'pg_history', dev.entry_id, ids.length); + this._addProvisionalTask(this._pgHistoryTaskId, 'pg_history', dev.entry_id, count); if (!this._tasksSubscribed) this._pgPollTask(this._pgHistoryTaskId); } catch (e) { this._busy.delete('pg-history'); this._pgBatchProgress = null; - if (this._pgIsUnknownCmd(e)) this._pgNeedsRestart = true; - else this._showToast(this._t('msg.toast_error', {error: e.message || e}, 'Error: ' + (e.message || e)), 'error'); + this._pgRunFailed(e); this._render(); } } + // A History / Optimize start that failed: a startup race, the per-device limit of + // one batch run at a time (PLATFORM-04), or anything else. + _pgRunFailed(e) { + if (this._pgIsUnknownCmd(e)) { this._pgNeedsRestart = true; return; } + const code = (e && (e.code || (e.error && e.error.code))) || ''; + if (code === 'task_busy') { + this._showToast(this._t('msg.pg_task_busy', {}, 'A Test-on-history or Optimize run is already in progress for this device. Wait for it or cancel it.'), 'error'); + return; + } + this._showToast(this._tText('msg.toast_error', {error: e.message || e}, 'Error: ' + (e.message || e)), 'error'); + } + // ── Sweep mode (objective 1D curve + 2D heatmap) ────────────────────────── _pgSweepObjectives() { return [ @@ -7296,6 +7657,10 @@ class HaWashdataPanel extends HTMLElement { ['false_end_rate', this._t('lbl.pg_obj_falseend', {}, 'False-end rate'), true], ['median_overrun', this._t('lbl.pg_obj_overrun', {}, 'Duration off-target'), true], ['ambiguity_rate', this._t('lbl.pg_obj_ambiguity', {}, 'Ambiguity rate'), true], + // What the end-gate settings actually move (audit PLAYGROUND-08). + ['end_lag', this._t('lbl.pg_obj_endlag', {}, 'End delay (median)'), true], + ['early_end_rate', this._t('lbl.pg_obj_early', {}, 'Early-end rate'), true], + ['split_rate', this._t('lbl.pg_obj_split', {}, 'Split rate'), true], ]; } @@ -7312,6 +7677,7 @@ class HaWashdataPanel extends HTMLElement {
+
`; const simbar = busy ? this._htmlPgBatchBar() : ''; @@ -7323,28 +7689,49 @@ class HaWashdataPanel extends HTMLElement { if (!r) return ''; if (!Array.isArray(r.points) || !r.points.length) return ''; const obj = this._pgSweepObjectives().find(o => o[0] === r.objective); - const lowerBetter = obj ? obj[2] : false; + const lowerBetter = r.lower_is_better != null ? !!r.lower_is_better : (obj ? obj[2] : false); const metrics = r.points.filter(p => p.metric != null).map(p => p.metric); if (!metrics.length) return `
${this._t('msg.pg_sweep_no_metric', {}, 'Not enough data to score this objective.')}
`; const mn = Math.min(...metrics), mx = Math.max(...metrics); - const best = lowerBetter ? Math.min(...metrics) : Math.max(...metrics); - const bestVal = (r.points.find(p => p.metric === best) || {}).value; - const fmtMetric = m => (r.objective === 'median_overrun') ? Math.round(m * 100) + '% off' : Math.round(m * 100) + '%'; + // The backend picks the winner (audit PLAYGROUND-08): a value that ends more + // cycles early, splits more or detects fewer is never it, and unless one beats + // the current setting by a whole cycle the answer is to keep the current one. + // A run stored before that falls back to the plain extreme. + const hasPick = r.best_value !== undefined; + const extreme = lowerBetter ? mn : mx; + const bestVal = hasPick ? r.best_value : (r.points.find(p => p.metric === extreme) || {}).value; + const bestMetric = hasPick ? r.best_metric : extreme; + const keepCurrent = !!r.keep_current; + const sameVal = (a, b) => a != null && b != null && Math.abs(Number(a) - Number(b)) < 1e-6; + const fmtMetric = m => { + if (m == null) return '-'; + if (r.objective === 'median_overrun') return Math.round(m * 100) + '% off'; + if (r.objective === 'end_lag') return `${_fmtNum(m / 60, 1)} ${this._tText('lbl.timer_min', {}, 'min')}`; + return Math.round(m * 100) + '%'; + }; const bars = r.points.map(p => { const frac = (mx > mn && p.metric != null) ? (p.metric - mn) / (mx - mn) : (p.metric != null ? 1 : 0); - const isBest = p.metric === best; - const isCurrent = r.current_value != null && Math.abs(p.value - r.current_value) < 1e-6; + const isBest = !keepCurrent && sameVal(p.value, bestVal); + const isCurrent = sameVal(p.value, r.current_value); const col = isBest ? 'var(--success-color,#4caf50)' : 'var(--primary-color)'; - return `
- ${_esc(String(p.value))}${isCurrent ? ' ◀' : ''} + const guard = p.guarded + ? ` ⚠` + : ''; + return `
+ ${_esc(String(p.value))}${isCurrent ? ' ◀' : ''}${guard}
- ${p.metric != null ? fmtMetric(p.metric) : '—'} + ${fmtMetric(p.metric)}
`; }).join(''); - const applyBtn = (this._canEdit() && bestVal != null) ? `` : ''; - return `
${this._t('lbl.pg_best_value', {}, 'Best value found')}: ${_esc(String(bestVal))} · ${fmtMetric(best)} · ◀ ${this._t('lbl.pg_current_value', {}, 'Current value:')}
+ const canApply = !keepCurrent && bestVal != null && !sameVal(bestVal, r.current_value); + const applyBtn = (this._canEdit() && canApply) ? `` : ''; + const nLine = r.cycles ? ` · ${this._t('msg.pg_sweep_n_cycles', {count: r.cycles}, `over ${r.cycles} cycles`)}` : ''; + const head = keepCurrent + ? `${this._t('msg.pg_sweep_keep_current', {value: String(r.current_value ?? '-'), metric: fmtMetric(bestMetric)}, `Keep the current value (${r.current_value ?? '-'}, ${fmtMetric(bestMetric)}): no value did better by at least one cycle.`)}` + : `${this._t('lbl.pg_best_value', {}, 'Best value found')}: ${_esc(String(bestVal))} · ${_esc(fmtMetric(bestMetric))}`; + return `
${head}${nLine} · ◀ ${this._t('lbl.pg_current_value', {}, 'Current value:')}
${bars} -
${applyBtn}
`; +
${this._htmlPgInSampleNote()}${applyBtn}
`; } // Kick off a detached, reconnect-safe Optimize sweep on the backend (1D curve), @@ -7359,8 +7746,11 @@ class HaWashdataPanel extends HTMLElement { const values = Array.from({length: steps}, (_, i) => +(fromN + (toN - fromN) * i / (steps - 1)).toFixed(3)); const param = this._pgSweepParam || 'off_delay'; const objective = this._pgSweepObjective || 'match_accuracy'; - const msg = { type: `${_DOMAIN}/start_playground_sweep`, entry_id: dev.entry_id, param, values, objective }; - this._pgBatchProgress = { done: 0, total: values.length }; + // The same "Last N" as Test on history (audit PLAYGROUND-18); the backend adds + // one pass with the current settings, which every value has to beat. + const count = Math.max(1, Math.min(_PG_MAX_BATCH_CYCLES, this._pgSimCycles || 20)); + const msg = { type: `${_DOMAIN}/start_playground_sweep`, entry_id: dev.entry_id, param, values, objective, count }; + this._pgBatchProgress = { done: 0, total: values.length + 1 }; this._busy.add('pg-sweep'); this._render(); try { @@ -7368,12 +7758,11 @@ class HaWashdataPanel extends HTMLElement { this._pgSweepTaskId = r && r.task_id; this._pgNeedsRestart = false; if (!this._pgSweepTaskId) throw new Error('no task id'); - this._addProvisionalTask(this._pgSweepTaskId, 'pg_sweep', dev.entry_id, values.length); + this._addProvisionalTask(this._pgSweepTaskId, 'pg_sweep', dev.entry_id, values.length + 1); if (!this._tasksSubscribed) this._pgPollTask(this._pgSweepTaskId); } catch (e) { this._busy.delete('pg-sweep'); this._pgBatchProgress = null; - if (this._pgIsUnknownCmd(e)) this._pgNeedsRestart = true; - else this._showToast(this._t('msg.toast_error', {error: e.message || e}, 'Error: ' + (e.message || e)), 'error'); + this._pgRunFailed(e); this._render(); } } @@ -7396,7 +7785,7 @@ class HaWashdataPanel extends HTMLElement { const keys = Object.keys(opts); if (!keys.length) return; const labels = keys.map(k => this._t('setting.' + k + '.label', {}, k)).join(', '); - if (!confirm(this._t('msg.pg_apply_settings_confirm', {n: keys.length, list: labels}, `Save these ${keys.length} setting(s) to this device? ${labels}`))) return; + if (!confirm(this._tText('msg.pg_apply_settings_confirm', {n: keys.length, list: labels}, `Save these ${keys.length} setting(s) to this device? ${labels}`))) return; await this._busyRun('pg-apply-settings', async () => { try { await this._ws({ type: `${_DOMAIN}/set_options`, entry_id: dev.entry_id, options: opts }); @@ -7409,7 +7798,7 @@ class HaWashdataPanel extends HTMLElement { for (const key of keys) this._pgClearStaged(key); this._showToast(this._t('toast.settings_saved', {}, 'Settings saved; integration reloading')); } catch (e) { - this._showToast(this._t('msg.toast_save_failed', {error: e.message || e}, 'Save failed: ' + (e.message || e)), 'error'); + this._showToast(this._tText('msg.toast_save_failed', {error: e.message || e}, 'Save failed: ' + (e.message || e)), 'error'); } }); this._render(); @@ -7420,7 +7809,7 @@ class HaWashdataPanel extends HTMLElement { if (!dev || !this._canEdit() || val == null) return; const paramKey = this._pgSweepParam; const lbl = this._t('setting.' + paramKey + '.label', {}, paramKey); - if (!confirm(this._t('msg.pg_apply_confirm', {label: lbl, value: val}, 'Apply best value: ' + lbl + ' = ' + val + '?'))) return; + if (!confirm(this._tText('msg.pg_apply_confirm', {label: lbl, value: val}, 'Apply best value: ' + lbl + ' = ' + val + '?'))) return; await this._busyRun('pg-sweep-apply', async () => { try { await this._ws({ type: `${_DOMAIN}/set_options`, entry_id: dev.entry_id, options: { [paramKey]: +val } }); @@ -7432,7 +7821,7 @@ class HaWashdataPanel extends HTMLElement { else this._pgParamOverrides[paramKey] = +val; this._showToast(this._t('toast.settings_saved', {}, 'Settings saved; integration reloading')); this._render(); - } catch (e) { this._showToast(this._t('msg.toast_save_failed', {error: e.message || e}, 'Save failed: ' + (e.message || e)), 'error'); } + } catch (e) { this._showToast(this._tText('msg.toast_save_failed', {error: e.message || e}, 'Save failed: ' + (e.message || e)), 'error'); } }); } @@ -7444,7 +7833,7 @@ class HaWashdataPanel extends HTMLElement { —% - ${this._t('lbl.pg_time_left_model', {}, 'Time left (model)')} — + ${this._t('lbl.pg_time_left_model', {}, 'Time left (model)')} — ${this._t('lbl.energy', {}, 'Energy')} — ${this._t('lbl.match', {}, 'Match')} — ${this._t('lbl.phase', {}, 'Phase')} — @@ -7453,74 +7842,37 @@ class HaWashdataPanel extends HTMLElement { _htmlPgAnalysis() { - const d = this._pgDtwData; - - const scoreBar = (lbl, val, maxVal, color, dispVal) => { - const frac = (maxVal && val != null) ? Math.max(0, Math.min(1, val / maxVal)) : (val != null ? Math.max(0, Math.min(1, val)) : 0); - return `
- ${_esc(lbl)} -
- ${dispVal != null ? _esc(String(dispVal)) : '—'} -
`; - }; - - let analysisHtml = ''; - - if (d && (d.stage2 || d.stage4)) { - const s2 = d.stage2 || {}, s4 = d.stage4 || {}, dtw = d.dtw || {}; - const finalScore = s4.final_score ?? dtw.blended_score ?? s2.score; - // The Strong/Weak MATCH verdict must reflect the committed match confidence - // (same number as the "Match confidence" bar and the strip), not the envelope - // fit - otherwise it can read "Strong match" while confidence is low. Fall - // back to the envelope/DTW score only when there is no committed match yet. - const ocConf = this._pgDetail && this._pgDetail.outcome && this._pgDetail.outcome.confidence; - const verdictScore = (ocConf != null) ? ocConf : finalScore; - let verdict = '—', vColor = 'var(--secondary-text-color)'; - if (verdictScore != null) { - if (verdictScore >= 0.7) { verdict = '✅ ' + this._t('lbl.pg_strong_match', {}, 'Strong match'); vColor = 'var(--success-color, #4caf50)'; } - else if (verdictScore >= 0.4) { verdict = '⚠ ' + this._t('lbl.pg_weak_match', {}, 'Weak match'); vColor = 'var(--warning-color, #ff9800)'; } - else { verdict = '❌ ' + this._t('lbl.pg_poor_match', {}, 'Poor match'); vColor = 'var(--error-color, #f44336)'; } - } - const profName = d.profile_name || this._pgProfileName || '—'; - analysisHtml += `
${verdict}
`; - if (profName !== '—') analysisHtml += `
${_esc(profName)} · ${_esc(this._t('lbl.score', {}, 'score'))} ${finalScore != null ? finalScore.toFixed(3) : '—'}
`; - analysisHtml += scoreBar(this._t('lbl.correlation', {}, 'Correlation'), s2.correlation, 1, '#42a5f5', s2.correlation != null ? s2.correlation.toFixed(2) : null); - if (dtw.blended_score != null) analysisHtml += scoreBar(this._t('lbl.pg_dtw', {}, 'DTW'), dtw.blended_score, 1, '#ab47bc', dtw.blended_score.toFixed(2)); - if (s4.duration_agreement != null) analysisHtml += scoreBar(this._t('lbl.duration', {}, 'Duration'), s4.duration_agreement, 1, '#66bb6a', s4.duration_agreement.toFixed(2)); - if (s4.energy_agreement != null) analysisHtml += scoreBar(this._t('lbl.energy', {}, 'Energy'), s4.energy_agreement, 1, '#ffa726', s4.energy_agreement.toFixed(2)); - analysisHtml += `
`; - } else if (!d) { - analysisHtml += `

${this._t('msg.pg_analysis_empty2', {}, 'Press Run to see match analysis.')}

`; - } - - // Primary number = the committed MATCH CONFIDENCE from the real sim (identical - // to the strip's "Match"), so there is one authoritative number on screen. + // One number: the committed match confidence from the real replay (identical + // to the strip's "Match"). The separate DTW/envelope scorer that used to sit + // here disagreed with it and was removed in 0.5.8. const oc = this._pgDetail && this._pgDetail.outcome; - const matchedName = (oc && oc.matched_profile) || (d && d.profile_name) || this._pgProfileName || (this._cycles || []).find(c => c.id === this._pgCycleId)?.profile_name || ''; + const matchedName = (oc && oc.matched_profile) || this._pgProfileName || (this._cycles || []).find(c => c.id === this._pgCycleId)?.profile_name || ''; const conf = oc && oc.confidence; + if (!oc) return `

${this._t('msg.pg_analysis_hint2', {}, 'Pick a cycle and press Run to load match analysis.')}

`; + let verdict = '—', vColor = 'var(--secondary-text-color)'; + if (conf != null) { + if (conf >= 0.7) { verdict = '✅ ' + this._t('lbl.pg_strong_match', {}, 'Strong match'); vColor = 'var(--success-color, #4caf50)'; } + else if (conf >= 0.4) { verdict = '⚠ ' + this._t('lbl.pg_weak_match', {}, 'Weak match'); vColor = 'var(--warning-color, #ff9800)'; } + else { verdict = '❌ ' + this._t('lbl.pg_poor_match', {}, 'Poor match'); vColor = 'var(--error-color, #f44336)'; } + } + let html = `
${verdict}
`; if (matchedName && conf != null) { const pctC = Math.round(Math.max(0, Math.min(1, conf)) * 100); - analysisHtml += `
${_esc(this._t('lbl.pg_match_confidence', {}, 'Match confidence'))}
`; - analysisHtml += `
+ html += `
${_esc(this._t('lbl.pg_match_confidence', {}, 'Match confidence'))}
`; + html += `
${_esc(matchedName)}
${pctC}%
`; } - // Envelope fit is a DIFFERENT lens (shape vs the profile's saved envelope, from - // get_dtw_debug), so it is labelled distinctly to avoid being read as the match - // confidence above. - const envScore = d && d.stage4 && d.stage4.final_score; - if (envScore != null) { - const pctE = Math.round(Math.max(0, Math.min(1, envScore)) * 100); - analysisHtml += `
${_esc(this._t('lbl.pg_envelope_fit', {}, 'Envelope fit'))}
`; - analysisHtml += `
-
- ${pctE}% -
`; - } + return `
${html}
`; + } - return `
${analysisHtml || `

${this._t('msg.pg_analysis_hint2', {}, 'Pick a cycle and press Run to load match analysis.')}

`}
`; + async _pgFetchEnvelope(entryId, profileName) { + try { + const envR = await this._ws({ type: `${_DOMAIN}/get_profile_envelope`, entry_id: entryId, profile_name: profileName }); + this._pgEnvData = envR.envelope || null; + } catch (_) { this._pgEnvData = null; } } async _pgLoad() { @@ -7530,8 +7882,9 @@ class HaWashdataPanel extends HTMLElement { if (!cid) return; this._pgCycleId = cid; this._pgLoading = true; + this._pgDetailBusy = false; this._pgView = null; this._pgHoverT = null; - this._pgPowerPts = null; this._pgDtwData = null; this._pgEnvData = null; this._pgDetail = null; + this._pgPowerPts = null; this._pgEnvData = null; this._pgDetail = null; const seq = ++this._pgLoadSeq; this._render(); try { @@ -7549,30 +7902,16 @@ class HaWashdataPanel extends HTMLElement { if (cy) cy._pg_duration = pwResp.full_duration_s; } const profName = this._pgProfileName || (this._cycles || []).find(c => c.id === cid)?.profile_name || ''; - if (pts.length) { - try { - const dtwMsg = { type: `${_DOMAIN}/get_dtw_debug`, entry_id: dev.entry_id, cycle_id: cid }; - if (profName) dtwMsg.profile_name = profName; - this._pgDtwData = await this._ws(dtwMsg); - this._pgNeedsRestart = false; - } catch (e) { - if (this._pgIsUnknownCmd(e)) this._pgNeedsRestart = true; - this._pgDtwData = null; - } - } - const resolvedProf = this._pgDtwData?.profile_name || profName; - if (resolvedProf) { - try { - const envR = await this._ws({ type: `${_DOMAIN}/get_profile_envelope`, entry_id: dev.entry_id, profile_name: resolvedProf }); - this._pgEnvData = envR.envelope || null; - } catch (_) { this._pgEnvData = null; } - } + if (profName) await this._pgFetchEnvelope(dev.entry_id, profName); if (seq !== this._pgLoadSeq) return; // cancelled or device switched mid-flight // Faithful backend simulation (the real detector + matcher + progress + // notification predicates) for this cycle under the current overrides. - await this._pgLoadDetail(dev.entry_id, cid); + await this._pgLoadDetail(dev.entry_id, cid, seq); + // An unlabelled cycle has no profile to overlay until the replay matched one. + const simProf = this._pgDetail && this._pgDetail.outcome && this._pgDetail.outcome.matched_profile; + if (!profName && simProf && seq === this._pgLoadSeq) await this._pgFetchEnvelope(dev.entry_id, simProf); } catch (e) { - this._showToast(this._t('msg.toast_error', {error: e.message || e}, 'Error: ' + (e.message || e)), 'error'); + this._showToast(this._tText('msg.toast_error', {error: e.message || e}, 'Error: ' + (e.message || e)), 'error'); } if (seq !== this._pgLoadSeq) return; // a Cancel (or device switch) supersedes this run this._pgLoading = false; @@ -7603,9 +7942,33 @@ class HaWashdataPanel extends HTMLElement { _pgCancelRun() { this._pgLoadSeq++; this._pgLoading = false; + this._pgDetailBusy = false; + this._pgCancelDetailTask(); this._render(); } + // Stop the backend replay a dropped result belongs to (audit PLAYGROUND-12): bumping + // _pgLoadSeq only discards the answer, and the abandoned Simulate kept replaying to + // the end (2-3 s of CPU per long cycle, x5 on a Pi) with its result retained. + _pgCancelDetailTask() { + const tid = this._pgDetailTaskId; + this._pgDetailTaskId = null; + if (tid) this._ws({ type: `${_DOMAIN}/cancel_task`, task_id: tid }).catch(() => {}); + } + + // The sandbox overrides a Simulate runs with, and a stable signature of them so + // the outcome can say when the settings have moved on since (PLAYGROUND-13). + _pgCurrentOverride() { + const override = { ...this._pgParamOverrides }; + if (this._pgThreshStart != null) override.start_threshold_w = this._pgThreshStart; + if (this._pgThreshStop != null) override.stop_threshold_w = this._pgThreshStop; + return override; + } + + _pgOverrideSig(override) { + return JSON.stringify(Object.keys(override).sort().map(k => [k, override[k]])); + } + // The single "load this cycle into the graph" path — used by the cycle dropdown // AND by drilling into a Test-on-history row. Resets the profile to the target // cycle's own label (so we never resimulate the previous cycle's profile), @@ -7630,20 +7993,22 @@ class HaWashdataPanel extends HTMLElement { // notification predicates) for one cycle under the current threshold/param // overrides. Result drives the Simulate state band, readout, event lane and // alerts rail — there is no client-side detection copy. - async _pgLoadDetail(entryId, cid) { + async _pgLoadDetail(entryId, cid, seq) { // Always attempt (do NOT bail on _pgNeedsRestart): a success here clears the // flag, so a note set by a startup race self-heals once the backend is ready. - const override = { ...this._pgParamOverrides }; - if (this._pgThreshStart != null) override.start_threshold_w = this._pgThreshStart; - if (this._pgThreshStop != null) override.stop_threshold_w = this._pgThreshStop; + const override = this._pgCurrentOverride(); + const sig = this._pgOverrideSig(override); + // A new Simulate supersedes the one still running. + this._pgCancelDetailTask(); // Run the heavy per-5s replay as a detached, chunked background task so a long // cycle no longer stalls Home Assistant (issue #311). We still await its // completion here (the caller drives the busy spinner + redraw), but the // backend yields the event loop between chunks. A header pill shows progress. try { - const r = await this._ws({ type: `${_DOMAIN}/start_playground_cycle_detail`, entry_id: entryId, cycle_id: cid, settings_override: override, stress_tail: this._pgStressTail, stress_idle_w: this._pgStressIdleW != null ? parseFloat(this._pgStressIdleW) : null }); + const r = await this._ws({ type: `${_DOMAIN}/start_playground_cycle_detail`, entry_id: entryId, cycle_id: cid, settings_override: override }); const tid = r && r.task_id; if (!tid) throw new Error('no task id'); + this._pgDetailTaskId = tid; this._addProvisionalTask(tid, 'pg_detail', entryId, 0); const result = await new Promise((resolve) => { this._taskCallbacks[tid] = async (t) => { @@ -7661,31 +8026,40 @@ class HaWashdataPanel extends HTMLElement { if (known && known.state !== 'running') this._settleTaskCallback(known); else if (!this._tasksSubscribed) this._pollTaskGeneric(tid); }); + if (this._pgDetailTaskId === tid) this._pgDetailTaskId = null; if (!this._isActiveEntry(entryId)) return; // device switched mid-flight - drop stale result + if (seq != null && seq !== this._pgLoadSeq) return; // cancelled or superseded this._pgDetail = (result && !result.error) ? result : null; + this._pgDetailSig = this._pgDetail ? sig : null; this._pgNeedsRestart = false; } catch (e) { + if (seq != null && seq !== this._pgLoadSeq) return; if (this._pgIsUnknownCmd(e)) this._pgNeedsRestart = true; this._pgDetail = null; + this._pgDetailSig = null; } } - // Re-run just the detail sim (after a threshold drag / param edit), debounced, - // then redraw. Keeps the current cycle; no full reload. + // Re-run just the detail sim with the current settings (the "settings changed" + // banner's button), debounced, then redraw. Keeps the cycle, the zoom and the + // power trace; no full reload. Supersedes (and cancels) any run in flight. _pgRerunDetail() { const dev = this._devices[this._selIdx]; - if (!dev || !this._pgCycleId) return; + if (!dev || !this._pgCycleId || this._pgLoading) return; clearTimeout(this._pgDetailDebounceTimer); this._pgDetailDebounceTimer = setTimeout(async () => { + const seq = ++this._pgLoadSeq; this._pgDetailBusy = true; this._render(); - await this._pgLoadDetail(dev.entry_id, this._pgCycleId); + await this._pgLoadDetail(dev.entry_id, this._pgCycleId, seq); + if (seq !== this._pgLoadSeq) return; this._pgDetailBusy = false; this._render(); requestAnimationFrame(() => this._pgDrawCanvas()); }, 220); } // Backend detector state -> one of the four state-band categories. - _pgMapState(st) { + _pgMapState(st, stalled) { + if (stalled) return 'stalled'; // #452: shown as Paused / Stalled live if (st === 'running' || st === 'paused') return 'running'; if (st === 'ending') return 'ending'; if (st === 'starting') return 'detecting'; @@ -7709,7 +8083,7 @@ class HaWashdataPanel extends HTMLElement { const segs = []; let cur = null; for (const p of s) { - const st = this._pgMapState(p.state); + const st = this._pgMapState(p.state, p.stalled); if (!cur || cur.state !== st) { cur = { start: p.t, end: p.t, state: st }; segs.push(cur); } else cur.end = p.t; } @@ -7755,19 +8129,8 @@ class HaWashdataPanel extends HTMLElement { return; } - // Stress tail — extract outcome early so totalDur and maxW can include it. - const stressOut = this._pgDetail && this._pgDetail.outcome && this._pgDetail.outcome.stress; - const stressFrom = stressOut && stressOut.enabled ? stressOut.synthetic_from_s : null; - const stressSeries = (stressFrom != null && this._pgDetail && this._pgDetail.series) - ? this._pgDetail.series.filter(pt => pt.t >= stressFrom) - : []; - let totalDur = (this._cycles || []).find(c => c.id === this._pgCycleId)?._pg_duration || pts[pts.length-1].t || 1; - if (stressOut && stressOut.terminated && stressOut.terminated_after_s != null && stressFrom != null) { - totalDur = Math.max(totalDur, stressFrom + stressOut.terminated_after_s); - } else if (stressSeries.length) { - totalDur = Math.max(totalDur, stressSeries[stressSeries.length - 1].t); - } - const maxW = Math.max(...pts.map(p => p.w), ...stressSeries.map(p => p.power || 0), 1); + const totalDur = (this._cycles || []).find(c => c.id === this._pgCycleId)?._pg_duration || pts[pts.length-1].t || 1; + const maxW = Math.max(...pts.map(p => p.w), 1); const threshStart = this._pgThreshStart ?? this._pgFieldVal('start_threshold_w', {}) ?? 50; const threshStop = this._pgThreshStop ?? this._pgFieldVal('stop_threshold_w', {}) ?? 5; @@ -7830,40 +8193,6 @@ class HaWashdataPanel extends HTMLElement { } } - // DTW alignment lines - const d = this._pgDtwData; - const profTrace = d && Array.isArray(d.profile_trace) ? d.profile_trace.filter(p => Array.isArray(p) && p.length >= 2) : []; - const cycTrace = d && Array.isArray(d.cycle_trace) ? d.cycle_trace.filter(p => Array.isArray(p) && p.length >= 2) : []; - const warp = d && Array.isArray(d.warp_path) ? d.warp_path : []; - if (cycTrace.length && profTrace.length && warp.length) { - const profMaxX = Math.max(...profTrace.map(p=>p[0])) || 1; - const cycMaxX = Math.max(...cycTrace.map(p=>p[0])) || 1; - const step = Math.max(1, Math.floor(warp.length / 25)); - ctx.save(); ctx.globalAlpha = 0.13; ctx.strokeStyle = '#fff'; ctx.lineWidth = dpr; - for (let i = 0; i < warp.length; i += step) { - const wp = warp[i]; - if (!Array.isArray(wp) || wp.length < 2) continue; - const ci = Math.min(wp[0], cycTrace.length-1), pi = Math.min(wp[1], profTrace.length-1); - const cx1 = toX(cycTrace[ci][0] / cycMaxX * totalDur); - const cy1 = toY(cycTrace[ci][1]); - const cx2 = toX(profTrace[pi][0] / profMaxX * totalDur); - const cy2 = toY(profTrace[pi][1]); - ctx.beginPath(); ctx.moveTo(cx1, cy1); ctx.lineTo(cx2, cy2); ctx.stroke(); - } - ctx.restore(); - } - - // Profile mean trace from DTW data - if (profTrace.length) { - const profMaxX = Math.max(...profTrace.map(p=>p[0])) || 1; - ctx.beginPath(); ctx.strokeStyle = '#eda100'; ctx.lineWidth = 2*dpr; ctx.setLineDash([6*dpr, 4*dpr]); - profTrace.forEach((p, i) => { - const x = toX(p[0] / profMaxX * totalDur), y = toY(p[1]); - i ? ctx.lineTo(x, y) : ctx.moveTo(x, y); - }); - ctx.stroke(); ctx.setLineDash([]); - } - // Cycle power trace fill ctx.beginPath(); ctx.moveTo(toX(0), toY(0)); @@ -7877,37 +8206,6 @@ class HaWashdataPanel extends HTMLElement { pts.forEach((p, i) => i ? ctx.lineTo(toX(p.t), toY(p.w)) : ctx.moveTo(toX(p.t), toY(p.w))); ctx.stroke(); - // Stress-tail synthetic power trace — dashed continuation of the real curve. - if (stressSeries.length) { - const lastReal = pts[pts.length - 1]; - ctx.beginPath(); - ctx.moveTo(toX(lastReal.t), toY(lastReal.w)); - stressSeries.forEach(p => ctx.lineTo(toX(p.t), toY(p.power))); - ctx.lineTo(toX(stressSeries[stressSeries.length - 1].t), toY(0)); - ctx.lineTo(toX(lastReal.t), toY(0)); - ctx.closePath(); - ctx.fillStyle = _withAlpha(primary, 0.06); ctx.fill(); - ctx.beginPath(); - ctx.strokeStyle = primary; ctx.lineWidth = 1.5 * dpr; - ctx.setLineDash([4 * dpr, 4 * dpr]); - ctx.moveTo(toX(lastReal.t), toY(lastReal.w)); - stressSeries.forEach(p => ctx.lineTo(toX(p.t), toY(p.power))); - ctx.stroke(); ctx.setLineDash([]); - } - - // Stress-tail synthetic region: tinted overlay from synthetic_from_s onward - if (stressFrom != null && stressFrom < vMax) { - const xFrom = toX(stressFrom); - ctx.fillStyle = 'rgba(192,57,43,0.07)'; - ctx.fillRect(xFrom, padT, Math.max(0, padL + plotW - xFrom), powerH); - ctx.save(); - ctx.strokeStyle = 'rgba(192,57,43,0.5)'; - ctx.lineWidth = dpr; - ctx.setLineDash([4*dpr, 4*dpr]); - ctx.beginPath(); ctx.moveTo(xFrom, padT); ctx.lineTo(xFrom, padT + powerH); ctx.stroke(); - ctx.restore(); - } - ctx.restore(); // end plot clip // Threshold lines @@ -7929,8 +8227,8 @@ class HaWashdataPanel extends HTMLElement { drawThrLine(+threshStop, '#e34948', this._t('btn.stop', {}, 'Stop')); // State band - const stateColors = { idle: bgCol, detecting: '#42a5f566', running: '#66bb6a66', ending: '#ef535066', anti_wrinkle: '#ab47bc66' }; - const stateLabels = { idle: this._t('lbl.pg_idle', {}, 'Idle'), detecting: this._t('lbl.pg_detecting', {}, 'Detecting'), running: this._t('lbl.pg_ev_running', {}, 'Running'), ending: this._t('lbl.pg_ev_ending', {}, 'Ending'), anti_wrinkle: this._t('lbl.pg_anti_wrinkle', {}, 'Anti-wrinkle') }; + const stateColors = { idle: bgCol, detecting: '#42a5f566', running: '#66bb6a66', ending: '#ef535066', anti_wrinkle: '#ab47bc66', stalled: '#eda10066' }; + const stateLabels = { idle: this._t('lbl.pg_idle', {}, 'Idle'), detecting: this._t('lbl.pg_detecting', {}, 'Detecting'), running: this._t('lbl.pg_ev_running', {}, 'Running'), ending: this._t('lbl.pg_ev_ending', {}, 'Ending'), anti_wrinkle: this._t('lbl.pg_anti_wrinkle', {}, 'Anti-wrinkle'), stalled: this._t('status.stalled', {}, 'Stalled') }; const stateY = ch - stateBandH - phaseBandH; ctx.fillStyle = bgCol; ctx.fillRect(padL, stateY, cw - padL - padR, stateBandH); // Real detector state band from the backend simulation (no client-side copy). @@ -7947,15 +8245,22 @@ class HaWashdataPanel extends HTMLElement { }); ctx.restore(); - // Phase bar - const cycle = (this._cycles || []).find(c => c.id === this._pgCycleId); - const profN = this._pgDtwData?.profile_name || this._pgProfileName || cycle?.profile_name; - const prof = profN ? (this._profiles || []).find(p => p.name === profN) : null; + // Phase bar: runs of the replay's own live phase readout (series[].phase), the + // same thing the running integration shows. It used to read `phases` off the + // profile rows, which get_profiles never sends, and treated their seconds as + // fractions, so the bar could not draw. const phaseY = ch - phaseBandH; - if (prof && Array.isArray(prof.phases) && prof.phases.length) { + const phaseSegs = []; + for (const p of (this._pgDetail && this._pgDetail.series) || []) { + const last = phaseSegs[phaseSegs.length - 1]; + if (last && last.name === (p.phase || null)) { last.end = p.t; continue; } + if (last) last.end = p.t; + phaseSegs.push({ name: p.phase || null, start: p.t, end: p.t }); + } + if (phaseSegs.some(sg => sg.name)) { ctx.save(); ctx.beginPath(); ctx.rect(padL, phaseY, plotW, phaseBandH); ctx.clip(); - prof.phases.forEach((ph, i) => { - const x1 = toX((ph.start || 0) * totalDur), x2 = toX((ph.end || 1) * totalDur); + phaseSegs.filter(sg => sg.name).forEach((ph, i) => { + const x1 = toX(ph.start), x2 = toX(ph.end); const hue = (i * 47) % 360; ctx.fillStyle = `hsla(${hue},60%,55%,0.55)`; ctx.fillRect(x1, phaseY, Math.max(1, x2 - x1), phaseBandH); @@ -8073,7 +8378,7 @@ class HaWashdataPanel extends HTMLElement { ctx.fillStyle = '#e34948'; ctx.beginPath(); ctx.arc(hx, hy, 3.5*dpr, 0, Math.PI*2); ctx.fill(); // Readout box. const fmtT = t => { const m = Math.floor(t/60); return `${m}:${String(Math.round(t%60)).padStart(2,'0')}`; }; - const fmtW = w => w >= 1000 ? (w/1000).toFixed(2) + ' kW' : _fmtPower(w); + const fmtW = w => w >= 1000 ? _fmtNum(w / 1000, 2) + ' kW' : _fmtPower(w); const lines = [ `${this._t('lbl.from_start', {}, 'From start')} ${fmtT(this._pgHoverT)}`, `${this._t('lbl.to_end', {}, 'To end')} ${fmtT(Math.max(0, totalDur - this._pgHoverT))}`, @@ -8116,12 +8421,15 @@ class HaWashdataPanel extends HTMLElement { match_changed: ['◇', '#eda100', this._t('lbl.pg_ev_match_changed', {}, 'Match changed')], match_ambiguous: ['◈', '#eda100', this._t('lbl.pg_ev_ambiguous', {}, 'Ambiguous')], unmatched: ['○', '#9e9e9e', this._t('lbl.pg_ev_unmatched', {}, 'Unmatched')], + match_reverted: ['↺', '#9e9e9e', this._t('lbl.pg_ev_match_reverted', {}, 'Match reverted')], + verified_pause: ['⏸', '#7e57c2', this._t('lbl.pg_ev_verified_pause', {}, 'Verified pause')], notify_start: ['🔔', '#2a78d6', this._t('lbl.pg_ev_notify_start', {}, 'Start notification')], notify_pre_complete: ['🔔', '#2a78d6', this._t('lbl.pg_ev_notify_pre', {}, 'Almost-done notification')], notify_finish: ['🔔', '#2a78d6', this._t('lbl.pg_ev_notify_finish', {}, 'Finish notification')], notify_milestone: ['🏆', '#2a78d6', this._t('lbl.pg_ev_notify_milestone', {}, 'Milestone notification')], notify_held: ['🌙', '#7e57c2', this._t('lbl.pg_ev_notify_held', {}, 'Notification held (quiet hours)')], finished: ['✓', '#4caf50', this._t('lbl.pg_ev_finished', {}, 'Finished')], + stalled: ['⏸', '#eda100', this._t('status.stalled', {}, 'Stalled')], }; const m = M[type] || ['•', 'var(--secondary-text-color)', type]; return { glyph: m[0], color: m[1], label: m[2] }; @@ -8135,12 +8443,15 @@ class HaWashdataPanel extends HTMLElement { match_changed: this._t('pg_evd.match_changed', {}, 'The leading program changed as more of the cycle was seen.'), match_ambiguous: this._t('pg_evd.match_ambiguous', {}, 'Two programs scored close together, so the match was uncertain.'), unmatched: this._t('pg_evd.unmatched', {}, 'No saved program fit this cycle.'), + match_reverted: this._t('pg_evd.match_reverted', {}, 'The program fell back to detecting: its score dropped well below its own peak.'), + verified_pause: this._t('pg_evd.verified_pause', {}, 'The live alignment confirmed (or released) a pause inside the program. While it holds, the cycle is not ended.'), notify_start: this._t('pg_evd.notify_start', {}, 'A start notification would be sent.'), notify_pre_complete: this._t('pg_evd.notify_pre_complete', {}, 'An almost-done notification would be sent.'), notify_finish: this._t('pg_evd.notify_finish', {}, 'A finish notification would be sent.'), notify_milestone: this._t('pg_evd.notify_milestone', {}, 'A milestone notification would be sent.'), notify_held: this._t('pg_evd.notify_held', {}, 'A notification was held back for quiet hours.'), finished: this._t('pg_evd.finished', {}, 'The cycle reached a terminal state and ended.'), + stalled: this._t('pg_evd.stalled', {}, 'The cycle sat at standby draw longer than this program ever pauses there, with work left. Shown as Paused (Stalled); the cycle stays open.'), }; return D[type] || ''; } @@ -8168,13 +8479,14 @@ class HaWashdataPanel extends HTMLElement { const elapsed = hoverT != null ? hoverT : totalDur; const power = this._pgInterpPower(pts, elapsed); const sp = this._pgSeriesAt(elapsed); - const stateKey = sp ? this._pgMapState(sp.state) : 'idle'; + const stateKey = sp ? this._pgMapState(sp.state, sp.stalled) : 'idle'; const stripStateMap = { idle: [this._t('lbl.pg_idle', {}, 'Idle'), 'var(--secondary-background-color)'], detecting: [this._t('lbl.pg_detecting', {}, 'Detecting'), '#42a5f5'], running: [this._t('lbl.pg_ev_running', {}, 'Running'), '#66bb6a'], ending: [this._t('lbl.pg_ev_ending', {}, 'Ending'), '#ef5350'], anti_wrinkle: [this._t('lbl.pg_anti_wrinkle', {}, 'Anti-wrinkle'), '#ab47bc'], + stalled: [this._t('status.stalled', {}, 'Stalled'), '#eda100'], }; const [stateText, stateColor] = stripStateMap[stateKey] || stripStateMap.idle; const pct = sp && sp.progress != null ? Math.round(sp.progress) : null; @@ -8183,8 +8495,8 @@ class HaWashdataPanel extends HTMLElement { const confDisp = sp && sp.confidence != null ? Math.round(sp.confidence * 100) + '%' : '—'; const phase = sp && sp.phase ? sp.phase : '—'; const fmtTime = s => { const m = Math.floor(s/60); return m + ':' + String(Math.round(s%60)).padStart(2,'0'); }; - const fmtE = wh => wh >= 1000 ? (wh/1000).toFixed(2) + ' kWh' : wh.toFixed(0) + ' Wh'; - const fmtP = w => w >= 1000 ? (w/1000).toFixed(1) + ' kW' : _fmtPower(w); + const fmtE = wh => wh >= 1000 ? _fmtNum(wh / 1000, 2) + ' kWh' : _fmtNum(wh, 0) + ' Wh'; + const fmtP = w => w >= 1000 ? _fmtNum(w / 1000, 1) + ' kW' : _fmtPower(w); const sr = this.shadowRoot; const $id = id => sr && sr.getElementById(id); const set = (id, v) => { const el = $id(id); if (el) el.textContent = v; }; @@ -8252,7 +8564,9 @@ class HaWashdataPanel extends HTMLElement { const actionsCell = canEdit ? ` - ${!isDefault ? `` : ''} + ${p.is_override + ? `` + : !isDefault ? `` : ''} ` : ''; return ` ${_esc(p.name)} ${isDefault ? `${this._t('badge.built_in_tag', {}, 'built-in')}` : ''} @@ -8272,11 +8586,27 @@ class HaWashdataPanel extends HTMLElement {
`; } + // "Undo last import" (register item 195): shown inside the Export / Import card + // only while the backend holds a restore point, and only to admins (undo_import + // is admin-only, like the imports themselves). + _htmlImportUndo() { + const u = this._importUndo; + if (!u || !u.created_at || !this._isAdmin()) return ''; + const c = u.counts || {}; + const cycles = (c.real_cycles || 0) + (c.reference_cycles || 0) + (c.backfill_cycles || 0); + const busy = this._busy.has('import-undo'); + return `
+

${this._t('msg.import_undo_hint', { when: _fmtDate(u.created_at), profiles: c.profiles || 0, cycles }, + 'A restore point from before the last import ({when}) is kept: {profiles} profile(s) and {cycles} cycle(s).')}

+ +
`; + } + _htmlDiagnostics() { const d = this._diag; let statsHtml; if (d && d._error) { - statsHtml = `

${this._t('msg.diagnostics_load_failed', {error: _esc(d._error)}, 'Could not load diagnostics: ' + _esc(d._error))}

`; + statsHtml = `

${this._t('msg.diagnostics_load_failed', {error: d._error}, 'Could not load diagnostics: {error}')}

`; } else if (d) { statsHtml = `
${d.total_cycles ?? '-'}
${this._t('lbl.cycles_count', {}, 'Cycles')}
@@ -8296,7 +8626,7 @@ class HaWashdataPanel extends HTMLElement { ${this._canFull() ? `
${this._t('hdr.maintenance', {}, 'Maintenance Actions')}
-
${this._t('hdr.process_history', {}, 'Process History')}

${this._t('msg.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.')}

+
${this._t('hdr.process_history', {}, 'Process History')}

${this._t('msg.process_history_hint', {}, 'Re-run matching on all stored cycles, refresh suggestions, retrain ML models (if enabled), recost cycles from recorded prices and recompute cycle health. Run it after a batch of reviews.')}

${this._t('hdr.clear_debug', {}, 'Clear Debug Traces')}

${this._t('msg.clear_debug_hint', {}, 'Remove stored debug data to free space.')}

@@ -8311,12 +8641,12 @@ class HaWashdataPanel extends HTMLElement { -
+ ${this._htmlImportUndo()}
${this._t('hdr.import_power_history', {}, 'Import power history')}
-

${this._t('msg.import_history_description', {}, 'Already had a smart plug before WashData? Upload a history export of its power sensor, or read it straight from Home Assistant, and the normal detection runs over it so past cycles turn up in your Cycles list ready to name.')}

+

${this._t('msg.import_history_description', {}, 'Had a smart plug before WashData? Upload its power history or read it from Home Assistant. Detection runs over it, and past cycles appear in your Cycles list ready to name.')}

@@ -8325,23 +8655,47 @@ class HaWashdataPanel extends HTMLElement { // ── Maintenance (Advanced → Maintenance): service log + reminders ──────────── - // Localized label for a maintenance event type (falls back to the raw key). - _maintLabel(type) { + // Label for a maintenance task: a built-in type is translated; a custom task + // (#461) shows the user's own name, never translated. `fallback` is the name a + // log entry recorded, so a renamed or removed task's history still reads. + _maintLabel(type, fallback) { const map = { descale: this._t('maint.descale', {}, 'Descale'), filter_clean: this._t('maint.filter_clean', {}, 'Clean filter'), drum_clean: this._t('maint.drum_clean', {}, 'Clean drum'), bearing_service: this._t('maint.bearing_service', {}, 'Bearing service'), other: this._t('maint.other', {}, 'Other'), + salt: this._t('maint.salt', {}, 'Refill salt'), + rinse_aid: this._t('maint.rinse_aid', {}, 'Refill rinse aid'), + lint_filter: this._t('maint.lint_filter', {}, 'Clean lint filter'), + condenser_clean: this._t('maint.condenser_clean', {}, 'Clean condenser'), }; - return map[type] || type; + if (map[type]) return map[type]; + const tasks = (this._maintenance && this._maintenance.custom_tasks) || []; + const task = tasks.find(t => t && t.id === type); + if (task && task.name) return task.name; + return fallback || type; + } + + // Progress rows for the Service Status card. `status` (#461) covers built-in + // and custom tasks with cycle and day intervals; a payload without it (an older + // backend) falls back to the built-in cycle reminders. + _maintStatusRows(mt, eventTypes) { + if (Array.isArray(mt.status)) return mt.status; + const reminders = mt.reminders || {}; + const cyclesSince = mt.cycles_since || {}; + return eventTypes.map(t => { + const thr = parseInt(reminders[t], 10) || 0; + const since = Math.max(0, parseInt(cyclesSince[t], 10) || 0); + return { id: t, custom: false, cycles_interval: thr, days_interval: 0, cycles_since: since, days_since: null, due: thr > 0 && since >= thr }; + }).filter(r => r.cycles_interval > 0); } _htmlMaintenance() { const canEdit = this._canEdit(); const mt = this._maintenance; if (mt && mt._error) { - return `

${this._t('msg.maintenance_load_error', { error: mt._error }, 'Could not load maintenance data: ' + mt._error)}

`; + return `

${this._t('msg.maintenance_load_error', { error: mt._error }, 'Could not load maintenance data: {error}')}

`; } if (!mt) { return `

${this._t('msg.loading', {}, 'Loading…')}

`; @@ -8350,21 +8704,28 @@ class HaWashdataPanel extends HTMLElement { const due = mt.due || []; const log = mt.log || []; const reminders = mt.reminders || {}; - const cyclesSince = mt.cycles_since || {}; + const customTasks = Array.isArray(mt.custom_tasks) ? mt.custom_tasks : []; + const limits = mt.limits || {}; + const nameMax = parseInt(limits.name_max, 10) || 60; + const tasksMax = parseInt(limits.tasks_max, 10) || 20; const odometer = Math.max(0, parseInt(mt.lifetime_cycle_count, 10) || 0); const canFull = this._canFull(); - // Reminder-due banner (advisory style; never a notification). + // Reminder-due banner (advisory style; never a notification), with a one-click + // "Log done" per due task (#461): it logs that task, which restarts its count. const dueBanner = due.length ? (() => { const items = due.map(t => this._maintLabel(t)).join(', '); - return `
- ${this._t('msg.maintenance_due', { items: _esc(items) }, 'Maintenance due: ' + items)} + const doneBtns = canEdit ? `
${due.map(t => ``).join('')}
` : ''; + return `
+ ${this._t('msg.maintenance_due', { items }, 'Maintenance due: {items}')} + ${doneBtns}
`; })() : ''; - // Add-event form (edit access only). + // Add-event form (edit access only): built-in types, then the custom tasks. const today = new Date().toISOString().slice(0, 10); - const typeOpts = eventTypes.map(t => ``).join(''); + const typeOpts = eventTypes.map(t => ``).join('') + + customTasks.map(t => ``).join(''); const addForm = canEdit ? `
${this._t('hdr.add_maintenance', {}, 'Add Maintenance Event')}
@@ -8382,7 +8743,7 @@ class HaWashdataPanel extends HTMLElement { return `
-
${_esc(this._maintLabel(e.event_type))}
+
${_esc(this._maintLabel(e.event_type, e.task_name))}
${_fmtDate(e.date)}
${notes}
@@ -8391,30 +8752,59 @@ class HaWashdataPanel extends HTMLElement {
`; }).join('') : `

${this._t('msg.no_maintenance', {}, 'No maintenance recorded yet.')}

`; - // Reminder thresholds editor (edit access only). + // Reminder editor (edit access only): the built-in rows for this device type, + // then the user's own tasks (#461), saved together by "Save reminders". + const intervalInput = (attr, id, value, max) => ``; + const taskRows = customTasks.map(t => `
+
+
${intervalInput('data-mtask-cycles', t.id, t.cycles, limits.cycles_max || 100000)}
+
${intervalInput('data-mtask-days', t.id, t.days, limits.days_max || 3650)}
+
+
`).join(''); + const newTaskRow = customTasks.length < tasksMax ? `
+
+
+
+
+
` : `

${this._t('msg.custom_task_limit', { max: tasksMax }, 'You have reached the limit of {max} tasks.')}

`; const remEditor = canEdit ? `
${this._t('hdr.maintenance_reminders', {}, 'Service Reminders')}

${this._t('msg.reminders_intro', {}, 'Show a reminder in the panel this many cycles after the last service. Leave blank or 0 to turn a reminder off.')}

${eventTypes.map(t => `
`).join('')}
+
${this._t('hdr.custom_tasks', {}, 'Your own tasks')}
+

${this._t('msg.custom_tasks_intro', {}, 'Add anything else you want to be reminded of. A task is due when either interval is reached; leave one blank or 0 to use only the other.')}

+ ${taskRows} + ${newTaskRow} +

${this._t('msg.maintenance_due_sensor_hint', {}, 'The Maintenance due binary sensor is on while any task is due, for your own notification automations.')}

` : ''; // Service status: the odometer every reminder is measured against, plus how - // close each task is. The backend computed "cycles since" all along and only - // ever sent the yes/no "due" list, so the panel could not show progress. - const progressRows = eventTypes.map(t => { - const thr = parseInt(reminders[t], 10) || 0; - if (thr <= 0) return ''; - const since = Math.max(0, parseInt(cyclesSince[t], 10) || 0); - const pct = Math.max(0, Math.min(100, Math.round((since / thr) * 100))); - const isDue = since >= thr; - const barColor = isDue ? 'var(--warning-color,#ff9800)' : 'var(--primary-color)'; - return `
+ // close each task is, by cycles and/or by days. + const progressRows = this._maintStatusRows(mt, eventTypes).map(r => { + const thrC = parseInt(r.cycles_interval, 10) || 0; + const thrD = parseInt(r.days_interval, 10) || 0; + const sinceC = Math.max(0, parseInt(r.cycles_since, 10) || 0); + const sinceD = r.days_since == null ? null : Math.max(0, parseInt(r.days_since, 10) || 0); + const parts = []; + let frac = 0; + if (thrC > 0) { + parts.push(this._t('lbl.cycles_since_service', { since: sinceC, total: thrC }, sinceC + ' / ' + thrC + ' cycles')); + frac = Math.max(frac, sinceC / thrC); + } + if (thrD > 0) { + const d = sinceD == null ? 0 : sinceD; + parts.push(this._t('lbl.days_since_service', { since: d, total: thrD }, d + ' / ' + thrD + ' days')); + frac = Math.max(frac, d / thrD); + } + const pct = Math.max(0, Math.min(100, Math.round(frac * 100))); + const barColor = r.due ? 'var(--warning-color,#ff9800)' : 'var(--primary-color)'; + return `
- ${_esc(this._maintLabel(t))} - ${this._t('lbl.cycles_since_service', { since: since, total: thr }, since + ' / ' + thr + ' cycles')} + ${_esc(r.custom ? (r.name || r.id) : this._maintLabel(r.id))} + ${parts.join(' · ')}
@@ -8427,7 +8817,7 @@ class HaWashdataPanel extends HTMLElement {
-

${this._t('msg.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.')}

+

${this._t('msg.odometer_correct_hint', {}, 'Set this if WashData counted a run that never happened, or the appliance ran cycles before WashData was installed.')}

` : ''; @@ -8437,43 +8827,48 @@ class HaWashdataPanel extends HTMLElement { ${odometer} ${this._t('lbl.total_cycles_run', {}, 'cycles run in total')}
-

${this._t('msg.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.')}

+

${this._t('msg.odometer_intro', {}, 'This total only goes up. Deleting a cycle does not lower it, so service reminders stay correct.')}

${progressRows || `

${this._t('msg.no_reminders_set', {}, 'No service reminders are set yet.')}

`} ${odometerEditor}
`; + // Pointer to the external Maintenance Supporter integration (#461): it does + // far more than these reminders and can read WashData's cycle counter. + const supporterCard = `
+
${this._t('hdr.maintenance_supporter', {}, 'Need more?')}
+

${this._t('msg.maintenance_supporter_intro', {}, 'Maintenance Supporter is a separate integration that does much more: any appliance or task, schedules by time, usage or a sensor, notifications and a history. It can use the WashData cycle counter.')}

+

${this._t('link.maintenance_supporter', {}, 'Maintenance Supporter on GitHub ↗')}

+
`; + return `${dueBanner} ${statusCard} ${addForm}
${this._t('hdr.maintenance_log', {}, 'Maintenance Log')}
-

${this._t('msg.maintenance_intro', {}, 'Log servicing you perform on this appliance and get reminded when each task is due again.')}

+

${this._t('msg.maintenance_intro', {}, 'Log servicing on this appliance and get reminded when each task is due again.')}

${rows}
- ${remEditor}`; + ${remEditor} + ${supporterCard}`; } // ── Panel tab (preferences + admin settings + RBAC) ───────────────────────── _htmlPanel() { const canEdit = this._canEdit(); - // Advanced holds only device-scoped tools now: Maintenance, Diagnostics and - // ML Training. The integration-wide sections (My Preferences, Panel Settings, - // Access Control, Online & Community) moved to the header gear (_htmlGearModal). + // Advanced holds the editor tools: Diagnostics and ML Training. Maintenance + // moved to Cycles; the integration-wide sections live in the header gear. const mlAvail = canEdit && this._constants && this._constants.mlTrainingAvailable; - const allowed = new Set(['maintenance']); - if (canEdit) allowed.add('diagnostics'); + const allowed = new Set(['diagnostics']); if (mlAvail) allowed.add('ml'); let sub = this._panelSubtab; - if (!allowed.has(sub)) sub = this._panelSubtab = 'maintenance'; - const subtabs = [['maintenance', this._t('tab.maintenance', {}, 'Maintenance')]]; - if (canEdit) subtabs.push(['diagnostics', this._t('tab.diagnostics', {}, 'Diagnostics')]); + if (!allowed.has(sub)) sub = this._panelSubtab = 'diagnostics'; + const subtabs = [['diagnostics', this._t('tab.diagnostics', {}, 'Diagnostics')]]; if (mlAvail) subtabs.push(['ml', this._t('tab.ml', {}, 'ML Training')]); - const stBtns = subtabs.map(([id, lbl]) => ``).join(''); - const body = sub === 'diagnostics' && canEdit ? this._htmlDiagnostics() - : sub === 'ml' && mlAvail ? this._htmlMlTab() - : this._htmlMaintenance(); - return `
${stBtns}
${body}`; + const stBtns = subtabs.map(([id, lbl]) => ``).join(''); + if (!canEdit) return ''; + const body = sub === 'ml' && mlAvail ? this._htmlMlTab() : this._htmlDiagnostics(); + return `
${stBtns}
${body}`; } _levelSelect(attrs, val, withInherit) { @@ -8498,7 +8893,7 @@ class HaWashdataPanel extends HTMLElement { const dateOptHtml = dateOpts.map(([v, l]) => ``).join(''); const langOverride = cur.lang_override || ''; const langOpts = [ - ['', this._t('pref.lang_auto', {lang: sysLang.toUpperCase()}, 'System default (' + sysLang.toUpperCase() + ')')], + ['', this._tText('pref.lang_auto', {lang: sysLang.toUpperCase()}, 'System default (' + sysLang.toUpperCase() + ')')], ['en', this._t('pref.lang_en', {}, 'English')], ].map(([v, l]) => ``).join(''); return `
@@ -8568,7 +8963,7 @@ class HaWashdataPanel extends HTMLElement { const adminNote = users.filter(u => u.is_admin).map(u => `${_esc(u.name)} - full (admin)`).join(' '); return `
${this._t('hdr.access_control', {}, 'Access Control')}
- ${_switchRow(`id="wd-rbac-enabled" ${rbac.enabled ? 'checked' : ''}`, this._t('lbl.enable_access_control', {}, 'Enable per-user access control'), '', this._t('msg.rbac_hint', {}, 'When off, every Home Assistant user has full access (the default). Administrators always have full access and can manage everyone.'))} + ${_switchRow(`id="wd-rbac-enabled" ${rbac.enabled ? 'checked' : ''}`, this._t('lbl.enable_access_control', {}, 'Enable per-user access control'), '', this._t('msg.rbac_hint', {}, 'Off: every Home Assistant user has full access (default). Administrators always have full access and can manage everyone.'))}
${this._levelSelect('id="wd-rbac-default"', rbac.default_level || 'none', false)}
${adminNote ? `
${adminNote}
` : ''}
@@ -8591,7 +8986,7 @@ class HaWashdataPanel extends HTMLElement { const storeHeader = `
${this._t('hdr.community_store', {}, 'Community Store')}
${storeLink}
`; if (!this._onlineEnabled() || (st && st.enabled === false)) { return `
${storeHeader} -

${this._t('msg.store_enable_hint', {}, 'Enable online features in Settings to browse and import community reference cycles.')}

`; +

${this._t('msg.store_enable_hint', {}, 'Turn on online features in Preferences > Online & Community to browse and import community reference cycles.')}

`; } let body; if (this._storeView === 'device') body = this._htmlStoreDevice(); @@ -8635,14 +9030,15 @@ class HaWashdataPanel extends HTMLElement { const yours = isMine ? `${this._t('store.your_model', {}, 'Yours')}` : ''; // Only ever claim content, never absence: these counters under-report (see // _storeItemHasContent), so a missing chip means "unknown", not "empty". - const nProg = Number(d.profileCount) || 0; + const nProg = _storeCount(d.profileCount); const progChip = nProg > 0 ? `${this._t('store.programs_count', {n: nProg}, `Programs: ${nProg}`)}` : ''; return ``; @@ -8654,7 +9050,7 @@ class HaWashdataPanel extends HTMLElement { if (!this._storeBrandScope()) { return ` ${this._storeSearchHtml()} -

${this._t('msg.store_declare_appliance', {}, 'Tell WashData which appliance you own and this tab shows the setups other people have shared for it. You can also type a brand above to look around.')}

+

${this._t('msg.store_declare_appliance', {}, 'Tell WashData which appliance you own to see setups others shared for it. Or type a brand above to look around.')}

`; } @@ -8664,7 +9060,7 @@ class HaWashdataPanel extends HTMLElement { // The exact model is empty far more often than not, so say plainly that the other // rows are worth a look rather than letting the user conclude the store is empty. const siblingHint = (!this._storeLoading && items.length > 1) - ? `

${this._t('msg.store_sibling_hint', {}, 'Nothing shared for your exact model? A closely-related model from the same brand is usually a good starting point.')}

` + ? `

${this._t('msg.store_sibling_hint', {}, 'Nothing shared for your model? A related model from the same brand is usually a good start.')}

` : ''; return ` ${this._storeSearchHtml()} @@ -8693,6 +9089,11 @@ class HaWashdataPanel extends HTMLElement {
`; } + // The store could not be reached (audit STORE-09): not the same as "nothing shared". + _htmlStoreUnreachable() { + return ``; + } + _htmlStoreDevice() { const items = this._storeProfiles || []; const rows = items.map(p => ``).join(''); const list = this._storeLoading ? this._htmlStoreLoading() - : (items.length ? `
${rows}
` : `

${this._t('store.no_programs', {}, 'No shared programs for this appliance yet.')}

`); + : (items.length ? `
${rows}
` : this._storeBrowseErr ? this._htmlStoreUnreachable() : `

${this._t('store.no_programs', {}, 'No shared programs for this appliance yet.')}

`); // Adopt the whole device: import every program's reference cycles into this // device in one action (merge/upsert; your real cycles are never touched). const dev = this._storeDevice; const dlBusy = this._busy.has('store-download-device'); const dlHeader = (this._canEdit() && dev && items.length) ? `
- ${this._t('msg.store_download_device_intro', {}, 'Adopt every shared program and its reference cycles onto your device. Your own recorded cycles and stats are not affected.')} + ${this._t('msg.store_download_device_intro', {}, 'Add every shared program and its reference cycles to your device. Your own cycles and stats are not affected.')} ${_switchInline(`data-action="store-toggle-dl-settings" ${this._dlSettings ? 'checked' : ''} ${dlBusy ? 'disabled' : ''}`, this._t('lbl.adopt_settings', {}, 'Also adopt settings'), _tip(this._t('msg.adopt_settings_hint', {}, 'Overwrite this device\'s detection & matching thresholds with the shared ones. Your notifications, entities and energy price are never changed.')))}
` : ''; return dlHeader + list; } + // Every value below comes from an untyped community-store document (audit + // STORE-19): numbers go through the _fmt* helpers (never echo their input), + // strings through _esc or a _t() var, which is escaped. _htmlStoreProfile() { const items = this._storeCycles || []; const rows = items.map(c => { - const stats = c.stats || {}; + const stats = (c.stats && typeof c.stats === 'object') ? c.stats : {}; const spark = this._storeSparkline((c.trace && c.trace.points) || []); - const dur = stats.duration != null ? _fmtDuration(stats.duration) : '-'; - const energy = stats.energy_wh != null ? _fmtEnergy(stats.energy_wh / 1000) : '-'; - const peak = stats.peak_w != null ? _fmtPower(stats.peak_w) : '-'; - const uploader = _esc(c.uploaderName || this._t('store.anon', {}, 'anonymous')); + const dur = _fmtDuration(stats.duration); + const energy = _fmtEnergy(stats.energy_wh != null ? Number(stats.energy_wh) / 1000 : null); + const peak = _fmtPower(stats.peak_w); + const uploader = (typeof c.uploaderName === 'string' && c.uploaderName) || this._tText('store.anon', {}, 'anonymous'); const tag = this._statusTag(c); // awaiting-approval / approved pill (cycles carry confirmCount) - const rat = c.rating || {}; - const rating = (rat.avg != null && rat.count) - ? this._t('store.rating_summary', {avg: Number(rat.avg).toFixed(1), n: rat.count}, `★ ${Number(rat.avg).toFixed(1)} (${rat.count})`) + const rat = (c.rating && typeof c.rating === 'object') ? c.rating : {}; + const ratAvg = Number(rat.avg), ratN = _storeCount(rat.count); + const rating = (rat.avg != null && isFinite(ratAvg) && ratN > 0) + ? this._t('store.rating_summary', {avg: _fmtNum(ratAvg, 1), n: ratN}, '★ {avg} ({n})') : this._t('store.no_ratings', {}, 'No ratings yet'); return `
${spark}
${dur} · ${energy} · ${peak} ${tag}
-
${this._t('store.uploaded_by', {name: uploader}, `Shared by ${uploader}`)} · ⬇ ${c.downloads || 0} · ${rating}
+
${this._t('store.uploaded_by', {name: uploader}, 'Shared by {name}')} · ⬇ ${_fmtNum(_storeCount(c.downloads), 0)} · ${rating}
`; }).join(''); const list = this._storeLoading ? this._htmlStoreLoading() - : (items.length ? rows : `

${this._t('store.no_cycles', {}, 'No reference cycles shared for this program yet.')}

`); + : (items.length ? rows : this._storeBrowseErr ? this._htmlStoreUnreachable() : `

${this._t('store.no_cycles', {}, 'No reference cycles shared for this program yet.')}

`); return `
${list}
`; } // Minimal inline SVG sparkline from a [[t, w], ...] trace (no canvas needed for // a static thumbnail). Scales time to width and power to height. _storeSparkline(points) { - const pts = Array.isArray(points) ? points.filter(p => Array.isArray(p) && p.length >= 2) : []; + const pts = Array.isArray(points) + ? points.filter(p => Array.isArray(p) && p.length >= 2).map(p => [Number(p[0]), Number(p[1])]) + .filter(p => isFinite(p[0]) && isFinite(p[1])) + : []; if (pts.length < 2) return ``; const ts = pts.map(p => p[0]); const ws = pts.map(p => p[1]); @@ -8759,7 +9167,9 @@ class HaWashdataPanel extends HTMLElement { return ``; } - // Header gear "Settings" overlay: integration-wide (device-agnostic) sections. + // Header gear "Preferences" overlay: integration-wide (device-agnostic) sections. + // Named apart from the per-device Settings tab (audit UI-25) so the two screens + // are never confused. // My Preferences (per HA user) is always available; Panel Settings, Access // Control and Online & Community are admin-only. Sub-nav uses data-gtab so it // never collides with the main tab router (data-tab) or Advanced (data-ptab). @@ -8770,13 +9180,13 @@ class HaWashdataPanel extends HTMLElement { if (admin && this._constants && this._constants.storeOnlineAvailable) tabs.push(['online', this._t('hdr.online_account_tab', {}, 'Online & Community')]); let tab = m.tab; if (!tabs.some(([id]) => id === tab)) tab = m.tab = 'prefs'; - const nav = tabs.map(([id, lbl]) => ``).join(''); + const nav = tabs.map(([id, lbl]) => ``).join(''); const body = tab === 'panel' && admin ? this._htmlPanelSettings() : tab === 'access' && admin ? this._htmlPanelAccess() : tab === 'online' && admin ? this._htmlOnlineSettings() : this._htmlPanelPrefs(); - return `

${this._t('settings.gear.title', {}, 'Settings')}

-
${nav}
+ return `

${this._t('settings.gear.title', {}, 'Preferences')}

+
${nav}
${body}
`; } @@ -8793,7 +9203,7 @@ class HaWashdataPanel extends HTMLElement { const busy = this._busy.has('store-account'); const connBlock = !on ? '' : (connected ? `
- ${this._t('store.connected_as', {name: _esc(st.name || st.uid || '')}, `Connected as ${_esc(st.name || st.uid || '')}`)} + ${this._t('store.connected_as', {name: st.name || st.uid || ''}, 'Connected as {name}')}
` : `
@@ -8802,7 +9212,7 @@ class HaWashdataPanel extends HTMLElement {
`); return `
${this._t('hdr.online_account', {}, 'Community Store & online features')}
-

${this._t('msg.online_intro_global', {}, 'Browse and share reference recordings with other WashData users, and confirm appliance entries. One connection applies to your whole WashData integration; appliance brand and model are set per device under Basic. All online features are opt-in and off by default.')}

+

${this._t('msg.online_intro_global', {}, 'Browse and share reference recordings with other WashData users. One connection covers all your WashData devices; brand and model are set per device under Basic. Online features are off until you turn them on.')}

${_switchRow(`data-action="store-toggle-online" ${on ? 'checked' : ''} ${busy ? 'disabled' : ''}`, this._t('lbl.enable_online', {}, 'Enable online features'))} ${on ? this._htmlStorePrefs(busy) : ''} ${on ? connBlock : ''} @@ -8814,7 +9224,8 @@ class HaWashdataPanel extends HTMLElement { _htmlStorePrefs(busy) { const prefs = (this._constants && this._constants.storePrefs) || {}; const rows = _STORE_PREFS.map(p => { - const checked = prefs[p.key] !== false; // defaults on; get_constants sends the full set + const v = prefs[p.key]; + const checked = v === undefined ? p.def !== false : v !== false; return _switchRow(`data-action="store-toggle-pref" data-pref="${_esc(p.key)}" ${checked ? 'checked' : ''} ${busy ? 'disabled' : ''}`, this._t(p.labelKey, {}, p.labelFb), _tip(this._t(p.docKey, {}, p.docFb))); }).join(''); // The catalog is cached for an hour because every read is charged against a quota the @@ -8824,7 +9235,7 @@ class HaWashdataPanel extends HTMLElement { return rows + `
- ${this._t('msg.refresh_catalog_hint', {}, 'The community brand and appliance lists are cached to keep the shared store within its daily budget. Refresh to pick up entries added or approved by others.')} + ${this._t('msg.refresh_catalog_hint', {}, 'Community brand and appliance lists are cached to save the store\'s daily budget. Refresh to see entries others added or approved.')}
`; } @@ -8878,10 +9289,10 @@ class HaWashdataPanel extends HTMLElement { if (!dev || !d) return; const eid = dev.entry_id; this._storeDevice = d; this._storeProfile = null; this._storeView = 'device'; - this._storeProfiles = []; this._storeCycles = []; this._storeLoading = true; this._render(); + this._storeProfiles = []; this._storeCycles = []; this._storeLoading = true; this._storeBrowseErr = false; this._render(); this._ws({ type: `${_DOMAIN}/store_get_profiles`, entry_id: eid, device_id: d.id }) - .then(r => { if (!this._isActiveEntry(eid) || this._storeView !== 'device') return; this._storeProfiles = (r && r.items) || []; }) - .catch(() => { if (this._isActiveEntry(eid)) this._storeProfiles = []; }) + .then(r => { if (!this._isActiveEntry(eid) || this._storeView !== 'device') return; this._storeProfiles = (r && r.items) || []; this._storeBrowseErr = !!(r && r.error); }) + .catch(() => { if (this._isActiveEntry(eid)) { this._storeProfiles = []; this._storeBrowseErr = true; } }) .finally(() => { if (this._isActiveEntry(eid)) { this._storeLoading = false; this._render(); } }); } @@ -8919,7 +9330,7 @@ class HaWashdataPanel extends HTMLElement { if (r && r.disabled) { this._storeStatus = { enabled: false }; this._storeDevices = []; } else this._storeDevices = this._sortStoreDevices((r && r.items) || []); } catch (e) { - if (this._isActiveEntry(eid)) { this._storeDevices = []; this._showToast(this._t('toast.store_search_failed', {error: e.message || e}, 'Search failed: ' + (e.message || e)), 'error'); } + if (this._isActiveEntry(eid)) { this._storeDevices = []; this._showToast(this._tText('toast.store_search_failed', {error: e.message || e}, 'Search failed: ' + (e.message || e)), 'error'); } } finally { if (this._isActiveEntry(eid)) { this._storeLoading = false; this._renderKeepingStoreQFocus(); } } @@ -9038,13 +9449,13 @@ class HaWashdataPanel extends HTMLElement { if (d.type !== 'washdata-connect') return; try { const r = await this._ws({ type: `${_DOMAIN}/store_connect`, entry_id: eid, refresh_token: d.refreshToken, uid: d.uid, name: d.displayName }); - if (r && r.error) { this._showToast(this._t('toast.store_connect_failed', {error: r.error}, 'Connect failed: ' + r.error), 'error'); return; } + if (r && r.error) { this._showToast(this._tText('toast.store_connect_failed', {error: r.error}, 'Connect failed: ' + r.error), 'error'); return; } await this._loadStoreStatus(eid); if (!this._isActiveEntry(eid)) return; this._showToast(this._t('toast.store_connected', {}, 'Connected to the community store')); this._render(); } catch (err) { - this._showToast(this._t('toast.store_connect_failed', {error: err.message || err}, 'Connect failed: ' + (err.message || err)), 'error'); + this._showToast(this._tText('toast.store_connect_failed', {error: err.message || err}, 'Connect failed: ' + (err.message || err)), 'error'); } }; window.addEventListener('message', this._storeConnectListener); @@ -9077,7 +9488,7 @@ class HaWashdataPanel extends HTMLElement { if (!silent) this._showToast(this._t('toast.settings_saved', {}, 'Settings saved; integration reloading')); return true; } catch (e) { - this._showToast(this._t('msg.toast_save_failed', {error: e.message || e}, 'Save failed: ' + (e.message || e)), 'error'); + this._showToast(this._tText('msg.toast_save_failed', {error: e.message || e}, 'Save failed: ' + (e.message || e)), 'error'); return false; } } @@ -9127,9 +9538,12 @@ class HaWashdataPanel extends HTMLElement { let xMax = opts.xMax || 0; if (!xMax) { series.forEach(s => (s.points || []).forEach(p => { if (p[0] > xMax) xMax = p[0]; })); if (opts.band) (opts.band.max || []).forEach(p => { if (p[0] > xMax) xMax = p[0]; }); } xMax = xMax || 1; - // Zoom viewport (absent key = full view). + // Zoom viewport (absent key = full view). `opts.xStart` is the data's left edge + // when it is not 0: the cycle chart draws the recorder's history before the + // cycle at negative offsets (item 513). Every other chart starts at 0. + const xStart = Math.min(0, Number(opts.xStart) || 0); const zoom = this._canvasZoom && this._canvasZoom[canvasId]; - const xMin = zoom ? zoom.xMin : 0; + const xMin = zoom ? zoom.xMin : xStart; const xViewMax = zoom ? zoom.xMax : xMax; // Scale y to the VISIBLE window, not the whole trace - this is HA's @@ -9186,7 +9600,14 @@ class HaWashdataPanel extends HTMLElement { ctx.lineTo(X(pts[pts.length - 1][0]), Y(0)); ctx.lineTo(X(pts[0][0]), Y(0)); ctx.closePath(); const g = ctx.createLinearGradient(0, padT, 0, ch - padB); g.addColorStop(0, _withAlpha(col, 0.33)); g.addColorStop(1, _withAlpha(col, 0.03)); ctx.fillStyle = g; ctx.fill(); } - ctx.beginPath(); pts.forEach((p, i) => i ? ctx.lineTo(X(p[0]), Y(p[1])) : ctx.moveTo(X(p[0]), Y(p[1]))); + // A null power breaks the line instead of bridging it (the cycle context's + // unavailable rows, item 513); a series without one draws exactly as before. + ctx.beginPath(); + let pen = false; + pts.forEach(p => { + if (p[1] == null) { pen = false; return; } + if (pen) ctx.lineTo(X(p[0]), Y(p[1])); else { ctx.moveTo(X(p[0]), Y(p[1])); pen = true; } + }); ctx.strokeStyle = col; ctx.lineWidth = (s.width || 1.5) * dpr; ctx.lineJoin = 'round'; if (s.dash) ctx.setLineDash([6 * dpr, 4 * dpr]); ctx.globalAlpha = s.alpha != null ? s.alpha : 1; ctx.stroke(); ctx.globalAlpha = 1; @@ -9203,22 +9624,25 @@ class HaWashdataPanel extends HTMLElement { // Axis time labels. When zoomed: show viewport start on the left edge too. ctx.fillStyle = txt; ctx.font = `${11 * dpr}px sans-serif`; ctx.textBaseline = 'bottom'; - if (zoom) { + if (zoom || xStart < 0) { ctx.textAlign = 'left'; - ctx.fillText((xMin / 60).toFixed(1) + ' min', padL, ch - 2 * dpr); + ctx.fillText((xMin / 60).toFixed(zoom ? 1 : 0) + ' min', padL, ch - 2 * dpr); } ctx.textAlign = 'right'; ctx.fillText((xViewMax / 60).toFixed(0) + ' min', cw - padR, ch - 2 * dpr); canvas._wd = { - xMax, xMin, xViewMax, yMax, dpr, padT, padB, padL, ch, primary, + xMax, xMin, xViewMax, yMax, dpr, padT, padB, padL, ch, primary, xStart, + // [start, end] of the cycle on this axis when context is drawn around it, so + // the readout can say "before start" / "after end" there (item 513). + cycleSpan: opts.cycleSpan || null, // CSS-px width of the plot area; the pinch/pan math needs it to turn a // finger travel distance into seconds at the current scale. plotWcss: plotW / dpr, Xpx: X, Ypx: Y, xToCss: x => X(x) / dpr, cssToX: px => Math.max(xMin, Math.min(xViewMax, xMin + ((px * dpr - padL) / plotW) * (xViewMax - xMin))), - series: (opts.series || []).map(s => ({ points: s.points, stroke: s.stroke, name: s.name, cid: s.cid })), + series: (opts.series || []).map(s => ({ points: s.points, stroke: s.stroke, name: s.name, cid: s.cid, bounded: s.bounded, aside: s.aside, kind: s.kind })), band: opts.band || null, artifacts: opts.artifacts || null, _opts: opts, @@ -9463,7 +9887,7 @@ class HaWashdataPanel extends HTMLElement { // in the wrong direction). const pend = this._gPinchPending && this._gPinchPending.id === id ? this._gPinchPending : null; const cur = pend ? { xMin: pend.lo, xMax: pend.hi } : this._canvasZoom[id]; - const lo = cur ? cur.xMin : 0; + const lo = cur ? cur.xMin : (wd.xStart || 0); const range = (cur ? cur.xMax : (wd.xMax || 1)) - lo; const plotW = Math.max(1, wd.plotWcss); const frac = Math.max(0, Math.min(1, centerCss / plotW)); @@ -9491,11 +9915,14 @@ class HaWashdataPanel extends HTMLElement { const canvas = this.shadowRoot && this.shadowRoot.getElementById(id); const wd = canvas && canvas._wd; if (!wd) return; - const full = wd.xMax || 1; + // The data's left edge is 0 except where a chart draws before it (xStart < 0, + // the cycle chart's recorder lead-in, item 513); `full` is the whole span. + const x0 = wd.xStart || 0; + const full = (wd.xMax || 1) - x0; const minRange = Math.min(full, Math.max(full / _CANVAS_MAX_ZOOM, _CANVAS_MIN_VIEW_S)); const range = Math.min(full, Math.max(minRange, xMax - xMin)); if (range >= full * 0.99) { this._resetCanvasZoom(id); return; } - const lo = Math.max(0, Math.min(full - range, xMin)); + const lo = Math.max(x0, Math.min(x0 + full - range, xMin)); const prev = this._canvasZoom[id]; if (prev && Math.abs(prev.xMin - lo) < 1e-6 && Math.abs(prev.xMax - lo - range) < 1e-6) return; this._canvasZoom[id] = { xMin: lo, xMax: lo + range }; @@ -9510,7 +9937,7 @@ class HaWashdataPanel extends HTMLElement { if (!wd) return; const full = wd.xMax || 1; const cur = this._canvasZoom[id]; - const lo = cur ? cur.xMin : 0; + const lo = cur ? cur.xMin : (wd.xStart || 0); const range = Math.max(1e-6, (cur ? cur.xMax : full) - lo); const frac = Math.max(0, Math.min(1, (focusXt - lo) / range)); const newRange = range * factor; @@ -9587,33 +10014,44 @@ class HaWashdataPanel extends HTMLElement { ctx.setLineDash([]); const colOf = s => (s.stroke === 'primary' ? wd.primary : s.stroke); const dot = (v, col) => { ctx.fillStyle = col; ctx.beginPath(); ctx.arc(xp, wd.Ypx(v), 3.4 * wd.dpr, 0, 6.2832); ctx.fill(); }; - const lines = [`${this._t('lbl.from_start', {}, 'From start')}: ${_fmtClock(x)}`, `${this._t('lbl.to_end', {}, 'To end')}: ${_fmtClock(Math.max(0, wd.xMax - x))}`]; + // Outside the cycle (the recorder context around it, item 513) the readout + // says how far before the start / after the end the cursor is instead. + const span = wd.cycleSpan; + const lines = span && x < span[0] + ? [`${this._t('lbl.before_start', {}, 'Before start')}: ${_fmtClock(span[0] - x)}`] + : span && x > span[1] + ? [`${this._t('lbl.after_end', {}, 'After end')}: ${_fmtClock(x - span[1])}`] + : [`${this._t('lbl.from_start', {}, 'From start')}: ${_fmtClock(x)}`, `${this._t('lbl.to_end', {}, 'To end')}: ${_fmtClock(Math.max(0, (span ? span[1] : wd.xMax) - x))}`]; const series = wd.series || []; + // A bounded series has no value outside its own points (or in a gap), rather + // than the clamped first/last one. + const valOf = s => (s.bounded ? _valueInRun(s.points, x) : _valueAt(s.points, x)); this._hoverNearest = null; - if (series.length > 4) { + // `aside` series (the cycle context) do not count towards the many-curves mode. + if (series.filter(s => !s.aside).length > 4) { // Many curves (cleanup): highlight only the one under the cursor so the // user can identify exactly which cycle to act on. let best = null, bestD = Infinity; - series.forEach(s => { const v = _valueAt(s.points, x); if (v == null) return; const d = Math.abs(wd.Ypx(v) - cursorYdev); if (d < bestD) { bestD = d; best = { s, v }; } }); + series.forEach(s => { const v = valOf(s); if (v == null) return; const d = Math.abs(wd.Ypx(v) - cursorYdev); if (d < bestD) { bestD = d; best = { s, v }; } }); if (best) { const col = colOf(best.s); ctx.strokeStyle = col; ctx.lineWidth = 3 * wd.dpr; ctx.beginPath(); (best.s.points || []).forEach((p, i) => i ? ctx.lineTo(wd.Xpx(p[0]), wd.Ypx(p[1])) : ctx.moveTo(wd.Xpx(p[0]), wd.Ypx(p[1]))); ctx.stroke(); dot(best.v, col); - lines.push(`${_esc(best.s.name || '')}: ${best.v.toFixed(best.v < 100 ? 1 : 0)} W`); + lines.push(`${_esc(best.s.name || '')}: ${_fmtNum(best.v, best.v < 100 ? 1 : 0)} W`); if (best.s.cid) { lines.push(`${this._t('lbl.click_to_select', {}, 'click to select')}`); this._hoverNearest = { id, cid: best.s.cid }; } } } else { - series.forEach(s => { const v = _valueAt(s.points, x); if (v == null) return; dot(v, colOf(s)); lines.push(`${_esc(s.name || this._t('lbl.power', {}, 'Power'))}: ${v.toFixed(v < 100 ? 1 : 0)} W`); }); + series.forEach(s => { const v = valOf(s); if (v == null) return; dot(v, colOf(s)); lines.push(`${_esc(s.name || this._t('lbl.power', {}, 'Power'))}: ${_fmtNum(v, v < 100 ? 1 : 0)} W`); }); } if (wd.band) { const lo = _valueAt(wd.band.min, x), hi = _valueAt(wd.band.max, x); - if (lo != null && hi != null) lines.push(`${this._t('lbl.envelope', {}, 'Envelope')}: ${lo.toFixed(lo < 100 ? 1 : 0)}–${hi.toFixed(hi < 100 ? 1 : 0)} W`); + if (lo != null && hi != null) lines.push(`${this._t('lbl.envelope', {}, 'Envelope')}: ${_fmtNum(lo, lo < 100 ? 1 : 0)}–${_fmtNum(hi, hi < 100 ? 1 : 0)} W`); } // Anomaly detail when hovering inside a detected artifact span. (wd.artifacts || []).forEach(a => { if (x >= a.start_s && x <= a.end_s) { - const detail = a.detail_key ? this._t(a.detail_key, a.detail_params || {}, a.detail || '') : (a.detail || ''); + const detail = a.detail_key ? this._tText(a.detail_key, a.detail_params || {}, a.detail || '') : (a.detail || ''); lines.push(`⚠ ${_esc(_artifactLabel(a.type, (k, v, f) => this._t(k, v, f)))}: ${_esc(detail)}`); } }); @@ -9789,6 +10227,37 @@ class HaWashdataPanel extends HTMLElement { // ── Modals ──────────────────────────────────────────────────────────────── + // The bulk auto-label modal opens on the device's own Auto-Label Confidence, + // clamped to the input's 0.5-0.95 range, not a fixed 0.75 below the 0.9 default + // (audit UI-10). The Cycles tab does not load options, so they are read here; + // `_opts` is the saved-settings baseline and is left alone (#447). + async _openAutoLabelModal(eid) { + let v = this._opts ? this._opts.auto_label_confidence : undefined; + if (v == null && eid) { + try { + const r = await this._ws({ type: `${_DOMAIN}/get_options`, entry_id: eid }); + if (!this._isActiveEntry(eid)) return; + v = (r && r.options || {}).auto_label_confidence; + } catch (_) { /* fall back to the schema default */ } + } + if (v == null) v = (_FIELD_BY_KEY.auto_label_confidence || {}).def; + const n = Number(v); + const threshold = Number.isFinite(n) ? +Math.min(0.95, Math.max(0.5, n)).toFixed(2) : 0.9; + this._modal = { type: 'auto-label', threshold }; + this._render(); + } + + // The finished auto-label task as a toast with its counts (audit UI-10); the + // bare completion line when no result came back. + _autoLabelToast(snap) { + const r = snap && snap.result; + if (!r || typeof r.labeled !== 'number') return this._tText('msg.toast_auto_label_complete', {}, 'Auto-label complete'); + if (snap.state === 'cancelled') { + return this._tText('toast.auto_label_cancelled', {count: r.labeled}, 'Auto-label cancelled after labelling {count} cycles.'); + } + return this._tText('toast.auto_label_done', {count: r.labeled, skipped: r.skipped || 0}, 'Auto-label complete: {count} cycles labelled, {skipped} left unlabelled.'); + } + _profileOptions(selected) { return (this._profiles || []).map(p => `` @@ -9808,9 +10277,11 @@ class HaWashdataPanel extends HTMLElement { let body = ''; if (m.type === 'confirm') { - body = `

${_esc(m.title)}

${_esc(m.message)}

+ // `items`: optional [{text, sub}] rows listed under the message (Apply-all preview). + const items = (m.items || []).map(i => `
  • ${_esc(i.text)}${i.sub ? `
    ${_esc(i.sub)}
    ` : ''}
  • `).join(''); + body = `

    ${_esc(m.title)}

    ${_esc(m.message)}

    ${items ? `
      ${items}
    ` : ''}
    -
    `; +
    `; } else if (m.type === 'label-cycle') { body = `

    ${this._t('modal.label_cycle', {}, 'Label Cycle')}

    @@ -9819,12 +10290,18 @@ class HaWashdataPanel extends HTMLElement {
    `; } else if (m.type === 'create-profile') { - const cycleOpts = (this._cycles || []).slice(0, 40).map(c => - ``).join(''); + const listed = (this._cycles || []).slice(0, 40); + let cycleOpts = listed.map(c => + ``).join(''); + // A coverage-gap cycle may sit outside the loaded list (the Profiles tab does + // not load cycles): offer it anyway so the pre-selection holds. + if (m.prefillCycle && !listed.some(c => c.id === m.prefillCycle)) { + cycleOpts = `` + cycleOpts; + } body = `

    ${this._t('modal.create_profile', {}, 'Create Profile')}

    -
    -
    ${this._t('msg.manual_duration_ref_hint', {}, 'Only used when no reference cycle is selected — a reference cycle sets the duration from its own length.')}
    +
    +
    ${this._t('msg.manual_duration_ref_hint', {}, 'Only used without a reference cycle; a reference cycle sets the duration from its own length.')}
    `; } else if (m.type === 'create-phase') { @@ -9834,7 +10311,7 @@ class HaWashdataPanel extends HTMLElement {
    `; } else if (m.type === 'edit-phase') { - const builtinNote = m.isDefault ? `

    ${this._t('msg.edit_builtin_phase', {}, 'This is a built-in phase. Saving creates a custom override — the original is preserved and can be restored by deleting the override.')}

    ` : ''; + const builtinNote = m.isDefault ? `

    ${this._t('msg.edit_builtin_phase', {}, 'This is a built-in phase. Saving creates a custom copy; delete the copy to restore the original.')}

    ` : ''; body = `

    ${this._t('modal.edit_phase', {}, 'Edit Phase')} ${m.isDefault ? `${this._t('badge.built_in_tag', {}, 'built-in')}` : ''}

    ${builtinNote}
    @@ -9852,22 +10329,15 @@ class HaWashdataPanel extends HTMLElement {
    `; } else if (m.type === 'correct-feedback') { body = `

    ${this._t('modal.correct_feedback', {}, 'Correct Feedback')}

    -

    WashData detected: ${_esc(m.detectedProfile)}

    +

    ${this._t('msg.feedback_detected', {name: _html(`${_esc(m.detectedProfile)}`)}, 'WashData detected: {name}')}

    `; - } else if (m.type === 'import-config') { - body = `

    ${this._t('modal.import_config', {}, 'Import Configuration')}

    -

    ${this._t('msg.import_intro', {}, 'Load an exported file or paste a JSON payload below.')}

    -
    -
    -
    -
    `; } else if (m.type === 'auto-label') { body = `

    ${this._t('modal.auto_label', {}, 'Auto-Label Cycles')}

    ${this._t('msg.auto_label_intro', {}, 'Assign profiles to unlabelled cycles whose match confidence clears the threshold.')}

    -
    +
    `; } else if (m.type === 'merge-cycles') { @@ -9912,9 +10382,11 @@ class HaWashdataPanel extends HTMLElement { const loading = m.profiles == null ? ` ${this._t('msg.loading', {}, 'Loading…')}` : ''; const opts = names.length ? names.map(n => ``).join('') - : ``; + : (m.storeUnreachable ? '' + : ``); body = `

    ${this._t('modal.store_share', {}, 'Share to community store')}

    ${this._t('msg.store_share_intro', {}, 'Upload this reference cycle so others with the same appliance can use it. It is reviewed before appearing publicly.')}

    + ${m.storeUnreachable ? this._htmlStoreUnreachable() : ''}
    @@ -9951,7 +10423,7 @@ class HaWashdataPanel extends HTMLElement { return `
    ${_esc(g.program)} - ${this._t('msg.share_profile_no_cycles', {}, 'No reference cycles — mark a cycle as ⭐ in the Cycles tab to include this profile')} + ${this._t('msg.share_profile_no_cycles', {}, 'No reference cycles – mark a cycle as ⭐ in the Cycles tab to include this profile')}
    `; } @@ -9983,19 +10455,19 @@ class HaWashdataPanel extends HTMLElement {
    `; }).join(''); } - const brand = _esc((this._opts.store_brand || '').trim()); - const model = _esc((this._opts.store_model || '').trim()); + const brand = (this._opts.store_brand || '').trim(); + const model = (this._opts.store_model || '').trim(); // Device-level opt-in: bundle this device's recognition/matching settings. const settingsRow = ``; const guideBlock = `
    ${this._t('msg.share_guidelines_title', {}, 'Before you share')}
    • ${this._t('msg.share_guideline_naming', {}, "Name each profile exactly as shown on the appliance dial or display (e.g. 'Cotton 40', 'Eco 60').")}
    • -
    • ${this._t('msg.share_guideline_quality', {}, 'Only share cycles that completed normally -- no mid-cycle interruptions, door-open events, or power blips.')}
    • +
    • ${this._t('msg.share_guideline_quality', {}, 'Only share cycles that finished normally: no interruptions, door openings or power blips.')}
    • ${this._t('msg.share_guideline_review', {}, 'Your upload starts as pending and appears publicly once enough community members confirm it.')}
    `; @@ -10004,7 +10476,7 @@ class HaWashdataPanel extends HTMLElement { ${this._t('lbl.share_consent', {}, 'I confirm these cycles ran to normal completion without interruption')} ` : ''; return `

    ${this._t('modal.store_share_device', {}, 'Share this device')}

    -

    ${this._t('msg.store_share_device_intro', {brand, model}, `Upload ${brand} ${model} with the reference cycles you select. Others with the same appliance can adopt your programs. Entries are reviewed before appearing publicly.`)}

    +

    ${this._t('msg.store_share_device_intro', {brand, model}, `Upload {brand} {model} with the reference cycles you select. Others with the same appliance can adopt your programs. Entries are reviewed before appearing publicly.`)}

    ${guideBlock}
    ${tree}
    ${hasShareableGroups ? settingsRow : ''} @@ -10019,20 +10491,36 @@ class HaWashdataPanel extends HTMLElement { // Fixed display order for the category tree (mirrors _EXPORT_CATEGORIES on the // backend). Enumerable categories (profiles / cycles) come first. _wizCatOrder() { - return ['profiles', 'real_cycles', 'reference_cycles', 'custom_phases', - 'profile_groups', 'settings', 'matching_config', 'ml_models', 'feedback', + return ['profiles', 'real_cycles', 'reference_cycles', 'backfill_cycles', 'custom_phases', + 'profile_groups', 'settings', 'ml_models', 'feedback', 'suggestions', 'maintenance_log', 'history_logs', 'lifetime_stats']; } + // The three per-cycle categories and the selection set each one fills. Backfill + // (cycles found in imported power history, item 129e) is a peer of the other two. + _wizCycleCats() { return ['real_cycles', 'reference_cycles', 'backfill_cycles']; } + + _wizIdSet(sel, catId) { + return catId === 'real_cycles' ? sel.realIds : (catId === 'backfill_cycles' ? sel.bfIds : sel.refIds); + } + + _wizEmptySel() { + return { cats: new Set(), profiles: new Set(), realIds: new Set(), refIds: new Set(), bfIds: new Set() }; + } + + _wizAnySel(sel) { + return !!(sel && (sel.profiles.size || sel.realIds.size || sel.refIds.size || sel.bfIds.size || sel.cats.size)); + } + _wizCatLabel(catId) { const map = { profiles: this._t('lbl.cat_profiles', {}, 'Profiles (programs)'), real_cycles: this._t('lbl.cat_real_cycles', {}, 'Cycles (run history)'), reference_cycles: this._t('lbl.cat_reference_cycles', {}, 'Reference cycles (imported)'), + backfill_cycles: this._t('lbl.cat_backfill_cycles', {}, 'Cycles found in imported power history'), custom_phases: this._t('lbl.cat_custom_phases', {}, 'Custom phases'), profile_groups: this._t('lbl.cat_profile_groups', {}, 'Profile groups'), settings: this._t('lbl.cat_settings', {}, 'Detection & matching settings'), - matching_config: this._t('lbl.cat_matching_config', {}, 'Matcher tuning'), ml_models: this._t('lbl.cat_ml_models', {}, 'ML models'), feedback: this._t('lbl.cat_feedback', {}, 'Feedback & review labels'), suggestions: this._t('lbl.cat_suggestions', {}, 'Suggestions'), @@ -10046,17 +10534,19 @@ class HaWashdataPanel extends HTMLElement { // Build a fresh selection model from a manifest. When importableOnly is set, // categories/items the backend marked not-importable are left unselected. _wizInitSel(manifest, importableOnly) { - const cats = new Set(), profiles = new Set(), realIds = new Set(), refIds = new Set(); + const sel = this._wizEmptySel(); const c = (manifest && manifest.categories) || {}; const ok = (info) => !!(info && info.present && (!importableOnly || info.importable !== false)); - if (ok(c.profiles)) (c.profiles.items || []).forEach(i => profiles.add(i.name)); - if (ok(c.real_cycles)) (c.real_cycles.groups || []).forEach(g => g.cycles.forEach(cy => { if (cy.id != null) realIds.add(String(cy.id)); })); - if (ok(c.reference_cycles)) (c.reference_cycles.groups || []).forEach(g => g.cycles.forEach(cy => { if (cy.id != null) refIds.add(String(cy.id)); })); - Object.keys(c).forEach(cid => { - if (['profiles', 'real_cycles', 'reference_cycles'].includes(cid)) return; - if (ok(c[cid])) cats.add(cid); + if (ok(c.profiles)) (c.profiles.items || []).forEach(i => sel.profiles.add(i.name)); + this._wizCycleCats().forEach(cid => { + const set = this._wizIdSet(sel, cid); + if (ok(c[cid])) (c[cid].groups || []).forEach(g => g.cycles.forEach(cy => { if (cy.id != null) set.add(String(cy.id)); })); }); - return { cats, profiles, realIds, refIds }; + Object.keys(c).forEach(cid => { + if (cid === 'profiles' || this._wizCycleCats().includes(cid)) return; + if (ok(c[cid])) sel.cats.add(cid); + }); + return sel; } // The selection payload sent to the backend (categories + item subsets). @@ -10066,15 +10556,17 @@ class HaWashdataPanel extends HTMLElement { if (s.profiles.size) categories.push('profiles'); if (s.realIds.size) categories.push('real_cycles'); if (s.refIds.size) categories.push('reference_cycles'); + if (s.bfIds.size) categories.push('backfill_cycles'); s.cats.forEach(cid => categories.push(cid)); const out = { categories }; if (s.profiles.size) out.profiles = Array.from(s.profiles); if (s.realIds.size) out.real_cycle_ids = Array.from(s.realIds); if (s.refIds.size) out.reference_cycle_ids = Array.from(s.refIds); + if (s.bfIds.size) out.backfill_cycle_ids = Array.from(s.bfIds); return out; } - // Ids of the cycles in one manifest group (real_cycles / reference_cycles). + // Ids of the cycles in one manifest group (real / reference / backfill cycles). _wizGroupIds(manifest, catId, prof) { const cat = (manifest.categories || {})[catId] || {}; const g = (cat.groups || []).find(gr => gr.profile === prof); @@ -10091,8 +10583,8 @@ class HaWashdataPanel extends HTMLElement { const sel = items.filter(i => s.profiles.has(i.name)).length; return { sel, total, state: sel === 0 ? 'none' : (sel === total ? 'all' : 'some') }; } - if (catId === 'real_cycles' || catId === 'reference_cycles') { - const set = catId === 'real_cycles' ? s.realIds : s.refIds; + if (this._wizCycleCats().includes(catId)) { + const set = this._wizIdSet(s, catId); const total = cat.count || 0; let sel = 0; (cat.groups || []).forEach(g => g.cycles.forEach(cy => { if (set.has(String(cy.id))) sel++; })); @@ -10125,7 +10617,7 @@ class HaWashdataPanel extends HTMLElement { const info = cats[cid]; const blocked = opts.importableOnly && info.importable === false; const st = this._wizCatState(m, cid, manifest); - const enumerable = ['profiles', 'real_cycles', 'reference_cycles'].includes(cid); + const enumerable = cid === 'profiles' || this._wizCycleCats().includes(cid); const countLbl = enumerable ? `${st.sel}/${info.count}` : ''; const header = `
    -
    ${m.cycleDest === 'real_history' ? this._t('msg.dest_real_history_hint', {}, 'Imported cycles count as this device\'s own history and feed energy/usage stats. Use for moving one appliance to a new install.') : this._t('msg.dest_reference_hint', {}, 'Imported cycles only improve program matching and never affect usage/energy statistics.')}
    +
    ${m.cycleDest === 'real_history' ? this._t('msg.dest_real_history_hint', {}, 'Imported cycles count as this device\'s own history and statistics. Use this to move an appliance to a new install.') : this._t('msg.dest_reference_hint', {}, 'Imported cycles only improve program matching and never affect usage/energy statistics.')}
    `; // Per-conflict resolution (merge mode, selected conflicting profiles). let conflictBar = ''; @@ -10246,7 +10738,7 @@ class HaWashdataPanel extends HTMLElement {
    `).join('')}
    `; } - const anything = m.sel && (m.sel.profiles.size || m.sel.realIds.size || m.sel.refIds.size || m.sel.cats.size); + const anything = this._wizAnySel(m.sel); return `${title} ${warnBanner} ${tree} @@ -10280,6 +10772,7 @@ class HaWashdataPanel extends HTMLElement { const map = { shorter_than_minimum: this._t('lbl.hist_reason_short', {}, 'shorter than this appliance\'s shortest real cycle'), no_clean_end: this._t('lbl.hist_reason_no_end', {}, 'never ended cleanly'), + already_recorded: this._t('lbl.hist_reason_already_recorded', {}, 'overlaps a cycle WashData already recorded'), }; return map[reason] || ''; } @@ -10292,7 +10785,7 @@ class HaWashdataPanel extends HTMLElement { const busy = this._busy.has('hist-import'); const dis = busy ? 'disabled' : ''; return `${title} -

    ${this._t('msg.hist_input_hint', {}, 'Upload a CSV downloaded from the History panel (entity, state, last changed), or let WashData read the sensor\'s history directly. Detection then runs over it exactly as it does live, and you choose which of the cycles it finds to keep.')}

    +

    ${this._t('msg.hist_input_hint', {}, 'Upload a CSV from the History panel (entity, state, last changed), or let WashData read the sensor\'s history directly. Live detection runs over it and you choose which cycles to keep.')}

    @@ -10302,7 +10795,7 @@ class HaWashdataPanel extends HTMLElement {
    -
    ${this._t('msg.hist_recorder_hint', {}, 'Reads from the date you pick up to now. Home Assistant keeps detailed history for 10 days by default and only hourly averages after that, which are too coarse to detect cycles from - pick a date further back only if your recorder is set to keep more.')}
    +
    ${this._t('msg.hist_recorder_hint', {}, 'Reads from the date you pick until now. Home Assistant keeps detailed history for 10 days by default; older hourly averages are too coarse for cycle detection.')}
    ${err}
    @@ -10315,7 +10808,7 @@ class HaWashdataPanel extends HTMLElement { const t = m.scanTaskId ? (this._tasks || {})[m.scanTaskId] : null; const pct = (t && t.total > 0) ? Math.round((t.done / t.total) * 100) : null; return `${title} -

    ${this._t('msg.hist_scanning', {}, 'Replaying your history through the detector. This runs in the background - you can close this dialog and come back to it.')}

    +

    ${this._t('msg.hist_scanning', {}, 'Running your history through the detector in the background. You can close this dialog and come back.')}

    ${pct == null ? this._t('status.preparing', {}, 'Preparing…') : `${pct}%`}

    ${err} @@ -10327,13 +10820,13 @@ class HaWashdataPanel extends HTMLElement { if (m.step === 'done') { const d = m.done || {}; const lines = [ - this._t('msg.hist_imported_count', { n: d.imported || 0 }, `${d.imported || 0} cycles imported.`), - d.duplicates ? this._t('msg.hist_duplicates', { n: d.duplicates }, `${d.duplicates} were already imported and were skipped.`) : '', + this._tText('msg.hist_imported_count', { n: d.imported || 0 }, `${d.imported || 0} cycles imported.`), + d.duplicates ? this._tText('msg.hist_duplicates', { n: d.duplicates }, `${d.duplicates} were already imported and were skipped.`) : '', d.capped ? this._t('msg.hist_capped', {}, 'The per-device limit for imported cycles was reached; the rest were not stored.') : '', ].filter(Boolean); return `${title} ${lines.map(l => `

    ${_esc(l)}

    `).join('')} -

    ${this._t('msg.hist_next_step', {}, 'They are in your Cycles list, tagged as imported history. Open one and use Label to name the program it belongs to.')}

    +

    ${this._t('msg.hist_next_step', {}, 'They are in your Cycles list, tagged as imported history. Open one and use Label to name its program.')}

    @@ -10350,19 +10843,29 @@ class HaWashdataPanel extends HTMLElement { // Account for every row the file contained, so "nothing found" is explained // rather than just reported. const facts = []; - if (parse.rows_total) facts.push(this._t('msg.hist_rows_read', { n: parse.rows_total }, `${parse.rows_total} readings read`)); + if (parse.rows_total) facts.push(this._tText('msg.hist_rows_read', { n: parse.rows_total }, `${parse.rows_total} readings read`)); if (parse.first && parse.last) facts.push(`${_fmtDate(parse.first)} – ${_fmtDate(parse.last)}`); - if (parse.breaks) facts.push(this._t('msg.hist_breaks', { n: parse.breaks }, `${parse.breaks} gaps where the sensor was unavailable`)); - if (parse.rows_other_entity) facts.push(this._t('msg.hist_other_entity', { n: parse.rows_other_entity }, `${parse.rows_other_entity} readings for other entities ignored`)); + if (parse.breaks) facts.push(this._tText('msg.hist_breaks', { n: parse.breaks }, `${parse.breaks} gaps where the sensor was unavailable`)); + if (parse.rows_other_entity) facts.push(this._tText('msg.hist_other_entity', { n: parse.rows_other_entity }, `${parse.rows_other_entity} readings for other entities ignored`)); + // The 500k-row cap cut the read short (audit PLAYGROUND-11): the recorder keeps + // the newest rows, a file its first ones. Either way part of the history is unread. + if (parse.truncated) facts.push(this._tText('msg.hist_truncated', { n: parse.rows_total || 0 }, `stopped at the ${parse.rows_total || 0}-reading limit, so the rest of the history was not scanned`)); // The file held one sensor and it was not this device's: it was read anyway, but say // so plainly - it is the difference between "my export" and "the wrong export". if (parse.entity_substituted_from) { - facts.push(this._t( + facts.push(this._tText( 'msg.hist_entity_substituted', { used: parse.entity_id || '?', wanted: parse.entity_substituted_from }, `read ${parse.entity_id || '?'} (this device is configured for ${parse.entity_substituted_from})`, )); } + // What the parse found suspicious about readable data (audit PLAYGROUND-24). + const warnMsgs = { + looks_like_kw: this._tText('msg.hist_warn_kw', { w: parse.peak_w }, `The highest reading is only ${parse.peak_w}. This sensor seems to report kilowatts; WashData needs watts, so no cycles can be detected.`), + naive_timestamps: this._tText('msg.hist_warn_naive_time', { n: parse.rows_naive_time || 0 }, `${parse.rows_naive_time || 0} timestamps have no time zone and were read as UTC. If the file uses local time, cycles are shifted by your UTC offset and readings near a DST change may be dropped.`), + }; + const warnLines = (parse.warnings || []).map(w => warnMsgs[w]).filter(Boolean) + .map(t => `

    ⚠ ${_esc(t)}

    `).join(''); const skipped = res.skipped || []; const skippedByReason = {}; skipped.forEach(sk => { skippedByReason[sk.reason] = (skippedByReason[sk.reason] || 0) + 1; }); @@ -10371,13 +10874,14 @@ class HaWashdataPanel extends HTMLElement { const settings = res.settings || {}; const settingsLine = settings.min_power != null - ? this._t('msg.hist_settings_used', { w: settings.min_power, s: settings.off_delay }, + ? this._tText('msg.hist_settings_used', { w: settings.min_power, s: settings.off_delay }, `Detected using this device's current settings (minimum power ${settings.min_power} W, off delay ${settings.off_delay} s).`) : ''; if (!segs.length) { return `${title}

    ${this._t('msg.hist_none_found', {}, 'No cycles could be detected in that history.')}

    + ${warnLines} ${facts.length ? `

    ${_esc(facts.join(' · '))}

    ` : ''} ${skipLine ? `

    ${this._t('msg.hist_skipped_spans', {}, 'Skipped stretches')}: ${_esc(skipLine)}

    ` : ''} ${settingsLine ? `

    ${_esc(settingsLine)}

    ` : ''} @@ -10393,8 +10897,8 @@ class HaWashdataPanel extends HTMLElement { return ` ${_esc(_fmtDate(seg.start_time))} - ${_esc(_fmtDuration(seg.duration_s))} - ${seg.energy_wh != null ? _esc((seg.energy_wh / 1000).toFixed(2)) + ' kWh' : '–'} + ${_esc(_fmtDuration(seg.duration_s))}${seg.banked_tail_s ? ' ✂' : ''} + ${seg.energy_wh != null ? _esc(_fmtNum(seg.energy_wh / 1000, 2)) + ' kWh' : '–'} ${_esc(String(Math.round(seg.peak_w)))} W ${reason ? `⚠ ${_esc(reason)}` : `${_esc(this._t('lbl.hist_looks_complete', {}, 'complete'))}`} @@ -10404,6 +10908,7 @@ class HaWashdataPanel extends HTMLElement { const allOn = segs.every(seg => accept.has(seg.index)); return `${title}

    ${this._t('msg.hist_found', { n: segs.length }, `Found ${segs.length} cycles. Untick anything that does not look like a real run - nothing is stored until you import.`)}

    + ${warnLines} ${facts.length ? `

    ${_esc(facts.join(' · '))}

    ` : ''} ${skipLine ? `

    ${this._t('msg.hist_skipped_spans', {}, 'Skipped stretches')}: ${_esc(skipLine)}

    ` : ''} ${settingsLine ? `

    ${_esc(settingsLine)}

    ` : ''} @@ -10457,32 +10962,21 @@ class HaWashdataPanel extends HTMLElement { const fromHistory = cur.cycle_origin === 'backfill'; const full = cur.full_duration_s || cur.duration || 0; const kwh = cur.energy_kwh != null ? cur.energy_kwh : null; - // ML health chip (higher = better) shown when an ML assessment is attached. + // The ML "Cycle health" chip was removed in 0.5.8 (audit ML-03): it flagged + // 16% of real cycles and 8.7% of golden ones for review. const ml = m.ml || null; - let healthCell = ''; - if (ml && ml.ml_quality_score != null) { - const lbl = ml.ml_quality_label; - const col = lbl === 'ok' ? 'var(--success-color,#4caf50)' : lbl === 'uncertain' ? 'var(--warning-color,#ff9800)' : 'var(--error-color,#f44336)'; - const health = Math.round((1 - ml.ml_quality_score) * 100); - healthCell = `
    ${health}%
    ${this._t('lbl.cycle_health', {}, 'Cycle health')}
    `; - } const meta = `
    ${_fmtDuration(cur.duration || full)}
    ${this._t('lbl.duration', {}, 'Duration')}
    ${_fmtEnergy(kwh)}
    ${this._t('lbl.energy', {}, 'Energy')}
    ${_esc(cur.profile_name || this._t('lbl.unlabelled', {}, 'Unlabelled'))}
    ${this._t('lbl.profile', {}, 'Profile')}
    -
    ${_esc(cur.status || '-')}
    ${this._t('lbl.status', {}, 'Status')}
    - ${healthCell} +
    ${_esc(cur.status ? this._cycleStatusLabel(cur.status) : '-')}
    ${this._t('lbl.status', {}, 'Status')}
    `; // Does this cycle still need a review? (mirrors the Cycles-list badge, so a // user who saw the "needs review" dot there knows to click Review here.) const rvw = (ml && ml.ml_review) || {}; const hasPendingFb = (this._feedbacks || []).some(f => f.cycle_id === m.cycleId); - const qLabel = ml && ml.ml_quality_label; - const needsReview = !rvw.reviewed_at && ( - hasPendingFb || - ['uncertain', 'review'].includes(qLabel) || - ['force_stopped', 'interrupted'].includes(cur.status) - ); + // Same rule as the Cycles list: unresolved detection feedback. + const needsReview = hasPendingFb; const reviewDot = (needsReview && m.mode !== 'review') ? ` ●` : ''; @@ -10494,8 +10988,8 @@ class HaWashdataPanel extends HTMLElement {
    ` : (isRef ? `
    📥 ${fromHistory - ? this._t('msg.imported_history_readonly', {}, 'Detected in imported power history. It shapes program matching but is not counted in your statistics, and cannot be trimmed or split. Label it to name the program.') - : this._t('msg.imported_readonly', {}, 'Imported from the community store. Shown for reference and matching. It is not counted in your stats and cannot be edited.')}
    ` : ''); + ? this._t('msg.imported_history_readonly', {}, 'Found in imported power history. It helps matching but is not counted in your statistics and cannot be trimmed or split. Label it to name the program.') + : this._t('msg.imported_readonly', {}, 'Imported from the community store. Used for reference and matching, not counted in your stats, and cannot be edited.')}
    ` : ''); // Pending-detection-feedback banner (Confirm / Correct… / Ignore). Built once // and shown in BOTH Inspect and Review modes, so a cycle in the "needs review" @@ -10568,7 +11062,7 @@ class HaWashdataPanel extends HTMLElement { ${_fmtDuration(s)} – ${_fmtDuration(e)}
    `; }).join(''); - controls = `

    ${this._t('msg.split_intro', {}, 'Click the graph to add or remove a split point, or auto-detect by idle gaps. Each resulting segment can get its own profile.')}

    + controls = `

    ${this._t('msg.split_intro', {}, 'Click the graph to add or remove a split point, or auto-detect by idle gaps. Each part can get its own profile.')}

    @@ -10593,22 +11087,22 @@ class HaWashdataPanel extends HTMLElement { ['sensor_gap', this._t('tag.sensor_gap', {}, 'Sensor gap')], ]; const tagChecks = TAGS.map(([v, l]) => ``).join(''); - const reviewedBadge = rv.reviewed_at ? `${this._t('lbl.reviewed_on', {date: new Date(rv.reviewed_at).toLocaleDateString()}, `reviewed ${new Date(rv.reviewed_at).toLocaleDateString()}`)}` : ''; + const reviewedBadge = rv.reviewed_at ? `${this._t('lbl.reviewed_on', {date: _fmtDay(rv.reviewed_at)}, 'reviewed {date}')}` : ''; // Pending-feedback banner (Confirm/Correct/Ignore) is built in the shared // scope above and rendered here as well as in Inspect mode (#331). - const tProfile = _tip(this._t('msg.review_profile_tip', {}, 'The program this cycle is labelled as. If the auto-detected program was wrong, correct it here - labelling teaches matching for future cycles.')); - const tQuality = _tip(this._t('msg.review_quality_tip', {}, 'How clean this cycle is. Good = a textbook example of this program; Bad = detected but noisy or atypical; Unusable = mis-detected (merged, truncated or spurious). Drives the health score and which cycles are allowed to train the model.')); - const tRecorded = _tip(this._t('msg.review_recorded_tip', {}, 'Mark this as a hand-picked reference cycle for its program - the same role as a manually recorded cycle. Reference cycles are always kept, seed the matching template, and are never dropped by cleanup. (This is the "golden"/recorded flag; both are the same thing.)')); - const tTags = _tip(this._t('msg.review_tags_tip', {}, 'Optional flags describing what went wrong with this cycle, so training and cleanup can account for it.')); + const tProfile = _tip(this._t('msg.review_profile_tip', {}, 'The program this cycle is labelled as. If it is wrong, correct it here; labels teach matching.')); + const tQuality = _tip(this._t('msg.review_quality_tip', {}, 'How clean this cycle is. Good: a textbook run of this program. Bad: detected, but noisy or unusual. Unusable: mis-detected (merged, cut short or spurious). Affects the health score and which cycles train the model.')); + const tRecorded = _tip(this._t('msg.review_recorded_tip', {}, 'Mark this as a hand-picked reference cycle for its program, like a manually recorded cycle. Reference cycles are always kept, seed the matching template, and are never removed by cleanup.')); + const tTags = _tip(this._t('msg.review_tags_tip', {}, 'Optional flags for what went wrong, so training and cleanup can allow for it.')); const tNotes = _tip(this._t('msg.review_notes_tip', {}, 'Free-text notes for your own reference. Not used by matching or training.')); controls = ` ${fbBanner}

    - ${this._t('msg.review_confirm_help', {}, 'Confirm whether this cycle was detected correctly. Your reviews train the model on your machine - the more cycles you confirm, the better matching and health scoring get. A quick Good/Bad is enough.')} + ${this._t('msg.review_confirm_help', {}, 'Was this cycle detected correctly? Confirming cycles improves matching for your machine. A quick Good/Bad is enough.')}

    -
    ${this._t('lbl.compare_profiles', {}, 'Compare with profiles')}${_tip(this._t('msg.compare_profiles_tip', {}, 'Overlay other profile envelopes on the chart above to see which one best fits this cycle.'))}
    +
    ${this._t('lbl.compare_profiles', {}, 'Compare with profiles')}${_tip(this._t('msg.compare_profiles_tip', {}, 'Overlay other profiles on the chart to see which fits this cycle best.'))}
    ${(this._profiles || []).map(p => { const on = (m.overlays || []).includes(p.name); const sw = on ? `` : ''; @@ -10638,7 +11132,7 @@ class HaWashdataPanel extends HTMLElement { const arts = (m.mode === 'view' || m.mode === 'review') ? (cur.artifacts || []) : []; if (arts.length) { const items = arts.map(a => { - const detail = a.detail_key ? this._t(a.detail_key, a.detail_params || {}, a.detail || '') : (a.detail || ''); + const detail = a.detail_key ? this._tText(a.detail_key, a.detail_params || {}, a.detail || '') : (a.detail || ''); return `
  • ${_esc(_artifactLabel(a.type, (k, v, f) => this._t(k, v, f)))} ${this._t('lbl.at', {}, 'at')} ${_fmtClock(a.start_s)}–${_fmtClock(a.end_s)} — ${_esc(detail)}
  • `; }).join(''); artifactBox = `
    @@ -10661,7 +11155,7 @@ class HaWashdataPanel extends HTMLElement { restartGapBox = `
    ↻ ${this._t('msg.restart_gap_header', {n: gaps.length}, `${gaps.length} HA restart gap${gaps.length > 1 ? 's' : ''} during this cycle`)}
      ${items}
    -
    ${this._t('msg.restart_gap_footer', {}, 'Highlighted on the graph. Power data is missing for these intervals — matching used only real readings.')}
    +
    ${this._t('msg.restart_gap_footer', {}, 'Highlighted on the graph. Power data is missing for these intervals; matching used only real readings.')}
    `; } // Thinning caption (#395): the graph draws a decimated copy of the stored @@ -10673,15 +11167,60 @@ class HaWashdataPanel extends HTMLElement { const totalN = cur.sample_count || shownN; decNote = `
    ${this._t('msg.samples_decimated', {shown: shownN, total: totalN}, `Showing ${shownN} of ${totalN} samples (thinned for display; peaks kept). A wide gap here is thinning, not missing data.`)}
    `; } + // Legend for the profile curves behind the trace, so a suspected program + // (#462) is never read as the cycle's label. Same list the chart draws. + const overlays = this._cycleProfileOverlays(m); + const legSw = s => s.dash + ? `background:repeating-linear-gradient(90deg,${s.stroke} 0 5px,transparent 5px 8px)` + : `background:${s.stroke}`; + // The recorder context (item 513) is a legend entry too, so the grey line is + // never read as part of the cycle. + const context = this._cycleContextSeries(m); + const legItems = context ? [...overlays, context.series] : overlays; + const overlayLeg = legItems.length ? `
    + ${this._t('lbl.power', {}, 'Power')} + ${legItems.map(s => ` ${_esc(s.name)}`).join('')} +
    ` : ''; return `

    ${this._t('lbl.cycle', {}, 'Cycle')} · ${_esc(_fmtDate(cur.start_time))}

    ${meta}${modeBar}
    + ${this._htmlCycleContextBar(m, cur)} + ${overlayLeg} ${decNote} ${artifactBox} ${restartGapBox} ${controls}`; } + // The cycle chart's recorder-history control (item 513, discussion #463): the + // length, and one line saying what the grey context is or why there is none. + // Inspect/Review only, and never for an imported cycle (this plug's recorder did + // not measure it). The tip says where the setting that DOES change the cycle is. + _htmlCycleContextBar(m, cur) { + if ((m.mode !== 'view' && m.mode !== 'review') || cur.is_reference || !(cur.samples || []).length) return ''; + const st = m.ctx || { minutes: this._cycleContextMinutes(m.entryId), loading: false, data: null }; + const opts = _CYCLE_CONTEXT_CHOICES.map(v => ``).join(''); + let state = '', note = ''; + if (st.minutes && st.loading) { + state = 'loading'; + note = this._t('msg.cycle_context_loading', {}, 'Loading recorder history…'); + } else if (st.minutes && st.data && this._cycleContextSeries(m)) { + state = 'shown'; + note = this._t('msg.cycle_context_note', {}, 'Grey: the power sensor\'s recorder history around this cycle. It is not part of the cycle and does not affect duration, energy or matching.'); + } else if (st.minutes && st.data) { + state = 'none'; + note = this._t('msg.cycle_context_none', {}, 'The recorder holds no readings around this cycle (Home Assistant keeps 10 days of history by default).'); + } + const tip = _tip(this._tText('msg.cycle_context_tip', {}, 'Display only: shows the power sensor\'s recorder history around this cycle, if still kept. To add failed start attempts to future cycles, use Curve Pre-roll (Settings > Detection > Cycle Start), which changes stored durations.')); + return `
    + ${tip} + ${note ? `${note}` : ''} +
    `; + } + // Per-profile control panel: stats, phases, cleanup, danger. _htmlProfilePanel(m) { const canEdit = this._canEdit(); @@ -10710,7 +11249,7 @@ class HaWashdataPanel extends HTMLElement { return `
    ${ph.health_status === 'poor' ? this._t('health.poor', {}, '⚠ Poor match fit') : ph.health_status === 'fair' ? this._t('health.fair', {}, 'Fair match fit') : this._t('health.good', {}, '✓ Good match fit')} ${this._t('stat.score', {pct: pct}, `score ${pct}%`)}${cvPct}${confPct} - ${ph.health_status === 'poor' ? `${this._t('msg.profile_poor_health_detail', {}, 'Cycles assigned to this profile have inconsistent shapes or low confidence. Consider rebuilding the envelope or reviewing labelled cycles.')}` : ''} + ${this._profileAdvisory(m.name, 'poor_health') ? `${_esc(this._advisoryText(this._profileAdvisory(m.name, 'poor_health')))}` : ''}
    `; })() : ''; // Trend row: shown when at least one metric is drifting @@ -10721,12 +11260,13 @@ class HaWashdataPanel extends HTMLElement { if (pt.energy_trend === 'up') parts.push(this._t('msg.trend_energy_up', {pct: `${pt.energy_slope_pct > 0 ? '+' : ''}${pt.energy_slope_pct}`, avg: _fmtEnergy(pt.energy_recent_mean_wh)}, `Energy trending up (${pt.energy_slope_pct > 0 ? '+' : ''}${pt.energy_slope_pct}%/cycle) — recent avg ${_fmtEnergy(pt.energy_recent_mean_wh)}`)); else if (pt.energy_trend === 'down') parts.push(this._t('msg.trend_energy_down', {pct: `${pt.energy_slope_pct}`}, `Energy trending down (${pt.energy_slope_pct}%/cycle)`)); const isWorrying = pt.duration_trend === 'up' || pt.energy_trend === 'up'; + const trendAdv = this._profileAdvisory(m.name, 'duration_trend_up') || this._profileAdvisory(m.name, 'energy_trend_up'); const col = isWorrying ? 'var(--warning-color,#ff9800)' : 'var(--info-color,#2196f3)'; const bg = isWorrying ? 'rgba(255,152,0,.10)' : 'rgba(33,150,243,.10)'; return `
    ${this._t('msg.performance_trend', {n: pt.cycle_count}, `Performance trend (${pt.cycle_count} cycles)`)}
    ${parts.map(p => `${p}`).join('
    ')} - ${isWorrying ? `
    ${this._t('msg.maintenance_advisory', {}, 'Increasing duration/energy may indicate appliance maintenance needed (e.g. descaling, filter cleaning).')}` : ''} + ${trendAdv ? `
    ${_esc(this._advisoryText(trendAdv))}` : ''}
    `; })() : ''; body = `
    @@ -10742,8 +11282,8 @@ class HaWashdataPanel extends HTMLElement {
    ${st.avg_cost != null ? `
    ${this._t('lbl.avg_cost', {}, 'Avg cost')}
    -
    ${st.avg_cost.toFixed(2)}${cur ? ' ' + cur : ''}${this._t('stat.avg', {}, 'avg')}
    -
    ${this._t('stat.total', {v: st.total_cost != null ? st.total_cost.toFixed(2) + (cur ? ' ' + cur : '') : '-'}, `total ${st.total_cost != null ? st.total_cost.toFixed(2) + (cur ? ' ' + cur : '') : '-'}`)}
    +
    ${_fmtCost(st.avg_cost, cur)}${this._t('stat.avg', {}, 'avg')}
    +
    ${this._t('stat.total', {v: _fmtCost(st.total_cost, cur)}, 'total {v}')}
    ` : ''}
    ${this._t('lbl.activity', {}, 'Activity')}
    @@ -10753,21 +11293,9 @@ class HaWashdataPanel extends HTMLElement {
    ${healthRow} ${trendRow} - ${(ph && ph.shape_drift) ? (() => { - const corr = ph.shape_drift_correlation != null ? ` (r=${Number(ph.shape_drift_correlation).toFixed(2)})` : ''; - return `
    - ${this._t('msg.shape_drift_advisory', {}, '⚠ Shape drifting')}${_esc(corr)} - ${this._t('msg.shape_drift_detail', {}, 'The power pattern for this profile has shifted over time — possible appliance wear or maintenance needed (e.g. descaling, filter cleaning).')} -
    `; - })() : ''} - ${(() => { - const adv = (this._profileAdvisories || []).find(a => a && a.profile === m.name && a.code === 'phase_inconsistent'); - if (!adv) return ''; - return `
    - ${this._t('msg.advisory_phase_inconsistent_title', {}, '⚠ Possibly mixed programs')} - ${_esc(this._t(adv.message_key, adv.message_params, adv.message))} -
    `; - })()} + ${this._profileAdvisory(m.name, 'shape_drift') ? `
    + ⚠ ${_esc(this._advisoryText(this._profileAdvisory(m.name, 'shape_drift')))} +
    ` : ''} ${env.avg && env.avg.length ? `
    ` : `

    ${this._t('msg.no_envelope', {}, 'No envelope yet - rebuild after labelling cycles.')}

    `}`; } else if (m.tab === 'phases') { const cat = m.catalog || []; @@ -10827,19 +11355,25 @@ class HaWashdataPanel extends HTMLElement { // running locks the other out - an unlabel racing a delete would re-label // cycles that are already gone and rebuild the envelope from a stale set. const clBusy = busyDel || busyUnl; - body = `

    ${this._t('msg.cleanup_intro', {}, 'Every labelled cycle overlaid. Tick outliers, then unlabel them (kept in history, but no longer shaping this profile) or delete them outright.')}

    + body = `

    ${this._t('msg.cleanup_intro', {}, 'All labelled cycles overlaid. Tick outliers, then unlabel them (kept in history, no longer shaping this profile) or delete them.')}

    ${allCyc.length ? `
    ` : `

    ${this._t('msg.no_cycles_profile', {}, 'No cycles for this profile.')}

    `} ${allCyc.length ? `
    ${thead}${rows}
    ` : ''} ${canEdit ? `
    - +
    ` : ''}`; } else if (m.tab === 'danger') { const busyR = this._busy.has('pp-rebuild'); const curDurMin = (m.stats && m.stats.avg_duration) ? Math.round(m.stats.avg_duration / 60) : 0; + // #158: with cycles the duration is recomputed on every envelope rebuild, so + // an edit could not stick; show it read-only and say where it comes from. + const durLearned = !!(m.stats && m.stats.duration_learned); + const durHint = durLearned + ? this._t('msg.duration_learned_hint', {}, "Worked out from this program's cycles (outliers left out) and updated after each new one. To change it, trim or remove the cycles that are off.") + : this._t('msg.manual_duration_hint', {}, "The profile's expected cycle length, used for time-remaining estimates. Leave it unchanged to keep the current value."); body = `
    -
    -
    ${this._t('msg.manual_duration_hint', {}, "The profile's average/expected cycle length, used for time-remaining estimates. Edit to set it; leaving it unchanged keeps the current value.")}
    +
    +
    ${durHint}
    @@ -10848,9 +11382,9 @@ class HaWashdataPanel extends HTMLElement { } const shareProfileBtn = (this._onlineEnabled() && this._storeDeviceDeclared()) - ? `` + ? `` : ''; - return `

    Profile · ${_esc(m.name)}

    + return `

    ${this._t('modal.profile_title', {name: m.name}, 'Profile · {name}')}

    ${tabBar}
    ${body}
    @@ -10859,6 +11393,41 @@ class HaWashdataPanel extends HTMLElement {
    `; } + // The profile curves drawn behind a cycle's trace in Inspect/Review (hidden while + // trimming/splitting): the labelled profile's expected curve, or for an unlabelled + // cycle with pending feedback the SUSPECTED profile and its runner-up (#462), each + // dashed and named as such so neither reads as a confirmed label. The chart and + // its legend both read this list, so they agree. Names are plain text. + _cycleProfileOverlays(m) { + const out = []; + if (!m || (m.mode !== 'view' && m.mode !== 'review')) return out; + const cur = m.curve || {}; + const plan = m.overlayPlan || null; + const suspected = !!(plan && plan.suspected); + const name = (plan && plan.name) || cur.profile_name || 'profile'; + const label = suspected + ? this._tText('lbl.suspected_profile', { name }, '{name} (suspected)') + : `${this._tText('lbl.expected', {}, 'Expected')} (${name})`; + const pe = m.profileEnv; + const envEnd = env => env.target_duration || env.avg[env.avg.length - 1][0] || 0; + if (!suspected && (cur.expected || []).length > 1) { + // Server-projected onto this cycle's own time axis via the same alignment + // the artifact shading was computed with, so the overlay, the trace and the + // shading agree. Do NOT extend the x axis - it is already in cycle time. + out.push({ kind: 'expected', points: cur.expected, stroke: '#ff9800', width: 2, alpha: 0.45, name: label }); + } else if (pe && (pe.avg || []).length) { + // The envelope on its own absolute grid, which slides by (duration - + // target_duration): no stored projection, or the profile is only suspected. + out.push({ kind: suspected ? 'suspected' : 'expected', points: pe.avg, stroke: '#ff9800', width: 2, alpha: 0.45, dash: suspected, name: label, xEnd: envEnd(pe) }); + } + const ru = suspected && plan.runnerUp ? m.runnerUpEnv : null; + if (ru && (ru.avg || []).length) { + out.push({ kind: 'runner_up', points: ru.avg, stroke: '#9c27b0', width: 1.6, alpha: 0.55, dash: true, xEnd: envEnd(ru), + name: this._tText('lbl.runner_up_profile', { name: plan.runnerUp }, '{name} (runner-up)') }); + } + return out; + } + _drawCycleEditor() { const m = this._modal; if (!m || m.type !== 'cycle-detail' || !m.loaded) return; @@ -10867,21 +11436,7 @@ class HaWashdataPanel extends HTMLElement { if (!samples.length) return; let full = cur.full_duration_s || samples[samples.length - 1][0] || 1; const series = []; - // Matched-profile expected curve overlaid in Inspect/Review so the user can - // compare the actual trace against what the labelled profile looks like - // (faint orange, behind the live trace). Hidden during Trim/Split editing. - const pe = m.profileEnv; - if ((m.mode === 'view' || m.mode === 'review') && (cur.expected || []).length > 1) { - // Server-projected onto this cycle's own time axis via the same alignment - // the artifact shading was computed with, so the overlay, the trace and the - // shading agree. Do NOT extend `full` - it is already in cycle time. - series.push({ points: cur.expected, stroke: '#ff9800', width: 2, alpha: 0.45, name: `${this._t('lbl.expected', {}, 'Expected')} (${cur.profile_name || 'profile'})` }); - } else if ((m.mode === 'view' || m.mode === 'review') && pe && (pe.avg || []).length) { - // Fallback (no stored envelope / projection failed): the envelope on its - // own absolute grid, which slides by (duration - target_duration). - series.push({ points: pe.avg, stroke: '#ff9800', width: 2, alpha: 0.45, name: `${this._t('lbl.expected', {}, 'Expected')} (${cur.profile_name || 'profile'})` }); - full = Math.max(full, pe.target_duration || pe.avg[pe.avg.length - 1][0] || 0); - } + this._cycleProfileOverlays(m).forEach(s => { series.push(s); full = Math.max(full, s.xEnd || 0); }); // User-selected comparison overlays (Review mode): draw each ticked profile's // envelope so the user can eyeball which profile best fits the cycle. if (m.mode === 'review' && (m.overlays || []).length) { @@ -10895,8 +11450,22 @@ class HaWashdataPanel extends HTMLElement { full = Math.max(full, env.target_duration || (last ? last[0] : 0)); }); } - series.push({ points: samples, stroke: 'primary', fill: true, width: 2, name: this._t('lbl.power', {}, 'Power') }); + // The recorder's history either side of the trace (item 513): drawn grey behind + // the trace on a shaded background, read as "before start" / "after end", and + // every series reads as absent outside its own points while it is shown. + const context = this._cycleContextSeries(m); + if (context) { + series.forEach(s => { s.bounded = true; }); + series.push(context.series); + full = Math.max(full, context.xEnd); + } + series.push({ points: samples, stroke: 'primary', fill: true, width: 2, name: this._t('lbl.power', {}, 'Power'), bounded: !!context }); const bands = [], vlines = []; + if (context) { + const shade = 'rgba(127,127,127,.10)'; + if (context.xStart < context.traceStart) bands.push({ x0: context.xStart, x1: context.traceStart, fill: shade }); + if (context.xEnd > context.traceEnd) bands.push({ x0: context.traceEnd, x1: context.xEnd, fill: shade }); + } let artifacts = []; if (m.mode === 'trim') { const a = m.trim.start, b = m.trim.end; @@ -10922,7 +11491,86 @@ class HaWashdataPanel extends HTMLElement { bands.push({ x0, x1, fill: 'rgba(96,125,139,.20)', label: '↻' }); }); } - this._drawCurves('wd-cyc-canvas', { series, xMax: full, bands, vlines, artifacts }); + this._drawCurves('wd-cyc-canvas', { + series, xMax: full, bands, vlines, artifacts, + ...(context ? { xStart: context.xStart, cycleSpan: [context.traceStart, context.traceEnd] } : {}), + }); + } + + // How many minutes of recorder history the cycle dialog draws around a cycle on + // this device (item 513): a per-user, per-device pref, 10 by default, 0 = off. + _cycleContextMinutes(entryId) { + const map = this._pref('cycle_context_min', null); + const v = map && typeof map === 'object' ? map[entryId] : undefined; + return _CYCLE_CONTEXT_CHOICES.includes(v) ? v : _CYCLE_CONTEXT_DEFAULT_MIN; + } + + // Fetch the recorder's history around the open cycle into `m.ctx` (item 513). + // Display only: get_cycle_context reads the recorder and nothing else. Skipped + // for imported cycles, which this plug never measured, and while switched off. + async _loadCycleContext(entryId, cycleId, curve) { + const m = this._modal; + if (!m || m.type !== 'cycle-detail' || m.cycleId !== cycleId) return; + const minutes = this._cycleContextMinutes(entryId); + if (!minutes || !curve || curve.is_reference || !(curve.samples || []).length) { + m.ctx = { minutes, loading: false, data: null }; + return; + } + m.ctx = { minutes, loading: true, data: null }; + let data = null; + try { + data = await this._ws({ type: `${_DOMAIN}/get_cycle_context`, entry_id: entryId, cycle_id: cycleId, before_s: minutes * 60, after_s: minutes * 60 }); + } catch (_) { data = { available: false, reason: 'no_history' }; } + const cur = this._modal; + // Stale reply: the dialog closed, moved to another cycle, or the length changed. + if (!cur || cur.type !== 'cycle-detail' || cur.cycleId !== cycleId || cur.entryId !== entryId + || !cur.ctx || cur.ctx.minutes !== minutes) return; + cur.ctx = { minutes, loading: false, data }; + this._render(); + } + + // The context as one drawable series plus its extent, or null when none is + // shown (off, loading, nothing recorded, or Trim/Split, which edit the trace and + // must not suggest they can reach outside it). A change-only sensor holds its + // value until the next row, so it is drawn as steps: a straight line from the + // last idle row to the first cycle reading would invent a slow rise that never + // happened. A null power (an unavailable row) breaks the line. + _cycleContextSeries(m) { + if (!m || (m.mode !== 'view' && m.mode !== 'review')) return null; + const d = m.ctx && m.ctx.data; + const samples = (m.curve && m.curve.samples) || []; + if (!d || !d.available || !samples.length) return null; + const first = samples[0], last = samples[samples.length - 1]; + const pts = []; + let hold = null; + const add = (t, w) => { + if (hold != null) pts.push([t, hold]); + if (w == null) { pts.push([t, null]); hold = null; return; } + pts.push([t, w]); + hold = w; + }; + const rows = (list, keep) => (Array.isArray(list) ? list : []).filter(p => Array.isArray(p) && Number.isFinite(p[0]) && keep(p[0])); + const before = rows(d.before, t => t < first[0]); + const after = rows(d.after, t => t >= last[0]); + before.forEach(([t, w]) => add(t, w)); + // Hold the last reading up to the start, then rise into the trace's first one. + if (hold != null) { pts.push([first[0], hold]); pts.push([first[0], first[1]]); } + if (after.length) { + if (pts.length) pts.push([last[0], null]); // no line across the cycle itself + hold = null; + add(last[0], last[1]); + after.forEach(([t, w]) => add(t, w)); + const endX = Number(d.after_end_s) || 0; + if (hold != null && endX > pts[pts.length - 1][0]) pts.push([endX, hold]); + } + if (!pts.some(p => p[1] != null)) return null; + const xStart = before.length ? Math.min(before[0][0], -(Number(d.before_s) || 0)) : first[0]; + const xEnd = after.length ? Math.max(pts[pts.length - 1][0], Number(d.after_end_s) || 0) : last[0]; + return { + series: { kind: 'context', points: pts, stroke: '#9e9e9e', width: 2, alpha: 1, bounded: true, aside: true, + name: this._tText('lbl.cycle_context_series', {}, 'Recorder history') }, + xStart: Math.min(0, xStart), xEnd, traceStart: first[0], traceEnd: last[0], + }; } // Multi-cycle comparison modal (opened from the Cycles select-mode "Compare" @@ -10953,7 +11601,7 @@ class HaWashdataPanel extends HTMLElement { return `

    ${this._t('msg.compare_cycles_title', { count: ids.length }, `Compare ${ids.length} cycles`)}

    ${m.loaded ? '' : `
    ${this._t('msg.loading', {}, 'Loading…')}
    `}
    -
    ${this._t('msg.compare_selected_cycles', {}, 'Selected cycles (solid) — show / hide')}
    +
    ${this._t('msg.compare_selected_cycles', {}, 'Selected cycles (solid) – show / hide')}
    ${cycRows}
    ${this._t('msg.compare_overlay_profiles', {}, 'Overlay profiles (faint)')}${_tip(this._t('msg.compare_overlay_tip', {}, 'Overlay learned profile envelopes to see which program each cycle resembles.'))}
    ${profRows}
    @@ -11057,9 +11705,19 @@ class HaWashdataPanel extends HTMLElement { this.dispatchEvent(new CustomEvent('hass-toggle-menu', { bubbles: true, composed: true })); }); - sr.querySelectorAll('.wd-devcard[data-idx]').forEach(btn => btn.addEventListener('click', () => this._selectDevice(parseInt(btn.dataset.idx, 10)))); + sr.querySelectorAll('.wd-devcard[data-idx]').forEach(btn => btn.addEventListener('click', () => this._guardUnsaved(() => this._selectDevice(parseInt(btn.dataset.idx, 10))))); - sr.querySelectorAll('[data-tab]').forEach(btn => btn.addEventListener('click', () => { if (btn.dataset.tab !== 'settings') { this._pendingSettings = {}; this._dirtyOptKeys = new Set(); } this._tab = btn.dataset.tab; this._fetchTabData(); })); + sr.querySelectorAll('[data-tab]').forEach(btn => btn.addEventListener('click', () => { + const target = btn.dataset.tab; + const go = () => { + if (target !== 'settings') { this._pendingSettings = {}; this._dirtyOptKeys = new Set(); } + this._tab = target; this._fetchTabData(); + }; + // Leaving Settings with unsaved edits used to drop them without a word + // (audit UI-15); the same guard covers device switches and leaving the panel. + if (this._tab === 'settings' && target !== 'settings') { this._guardUnsaved(go); return; } + go(); + })); sr.querySelectorAll('[data-sec]').forEach(btn => btn.addEventListener('click', () => { this._snapshotFormToPending(sr); this._settingsSec = btn.dataset.sec; this._settingsSearch = ''; this._settingsSugOnly = false; this._render(); })); sr.querySelectorAll('[data-ptab]').forEach(btn => btn.addEventListener('click', () => { const sub = this._panelSubtab = btn.dataset.ptab; @@ -11069,10 +11727,18 @@ class HaWashdataPanel extends HTMLElement { if (!dev) return; if (sub === 'diagnostics' && !this._diag) this._fetchToolsData(dev.entry_id).then(() => { if (this._panelSubtab === 'diagnostics') this._render(); }); else if (sub === 'logs') this._fetchLogs().then(() => { if (this._panelSubtab === 'logs') this._render(); }); - else if (sub === 'maintenance') this._fetchMaintenance(dev.entry_id).then(() => { if (this._panelSubtab === 'maintenance') this._render(); }); else if (sub === 'ml') this._fetchTabData(); })); + // Cycles | Maintenance sub-nav (data-hsub, so it never collides with data-tab). + sr.querySelectorAll('[data-hsub]').forEach(btn => btn.addEventListener('click', () => { + this._historySub = btn.dataset.hsub; + this._render(); + const dev = this._devices[this._selIdx]; + if (this._historySub === 'maintenance' && dev) this._fetchMaintenance(dev.entry_id).then(() => { if (this._tab === 'history') this._render(); }); + else this._fetchTabData(); + })); + // Header gear overlay sub-nav (My Preferences / Panel Settings / Access / // Online). Uses data-gtab so it never collides with the main tab router. sr.querySelectorAll('[data-gtab]').forEach(btn => btn.addEventListener('click', () => { @@ -11376,22 +12042,6 @@ class HaWashdataPanel extends HTMLElement { }); }); - // F3: Idle termination test toggle - const pgStressToggle = sr.getElementById('wd-pg-stress-toggle'); - if (pgStressToggle) pgStressToggle.addEventListener('change', () => { - this._pgStressTail = pgStressToggle.checked; - this._pgStressIdleW = null; - this._render(); - }); - - // F3: Idle level override field (only present when toggle is on) - const pgStressIdleW = sr.getElementById('wd-pg-stress-idle-w'); - if (pgStressIdleW) pgStressIdleW.addEventListener('input', () => { - const v = parseFloat(pgStressIdleW.value); - // Number.isFinite rejects Infinity (parseFloat("1e999")) which isNaN misses. - this._pgStressIdleW = (Number.isFinite(v) && v >= 0) ? v : null; - }); - // F3: Cycle selector const pgCycSel = sr.getElementById('wd-pg-cyc-sel'); if (pgCycSel) pgCycSel.addEventListener('change', () => this._pgSelectCycle(pgCycSel.value)); @@ -11400,7 +12050,7 @@ class HaWashdataPanel extends HTMLElement { const pgProfSel = sr.getElementById('wd-pg-prof-sel'); if (pgProfSel) pgProfSel.addEventListener('change', () => { this._pgProfileName = pgProfSel.value; this._pgLoad(); }); - // F3: Settings control panel — preset picker + "save as" name buffer. Both keep + // F3: Settings control panel - preset picker + "save as" name buffer. Both keep // their value in state so a re-render (any param edit) cannot reset them. const pgPresetSel = sr.getElementById('wd-pg-preset-sel'); if (pgPresetSel) pgPresetSel.addEventListener('change', () => { this._pgPresetSel = pgPresetSel.value; this._render(); }); @@ -11408,13 +12058,10 @@ class HaWashdataPanel extends HTMLElement { if (pgPresetName) pgPresetName.addEventListener('input', () => { this._pgPresetName = pgPresetName.value; // Only the save button's disabled state and the preset-limit note depend on - // the typed name, so patch those two in place. A full _render() per keystroke - // rebuilt the entire panel DOM and repainted the Playground canvases, and is - // what forced the caret save/restore workaround this replaces. + // the typed name, so patch those two in place: a full _render() per keystroke + // would rebuild the panel DOM and repaint the Playground canvases. const name = (this._pgPresetName || '').trim(); - const atLimitNow = this._pgPresetLimit > 0 - && (this._pgPresets || []).length >= this._pgPresetLimit - && !(this._pgPresets || []).some(p => p.name === name); + const atLimitNow = this._pgPresetAtLimit(name); const saveBtn = sr.querySelector('[data-action="pg-preset-save"]'); if (saveBtn) saveBtn.disabled = !(name && !atLimitNow); const limitNote = sr.getElementById('wd-pg-preset-limit-note'); @@ -11422,8 +12069,11 @@ class HaWashdataPanel extends HTMLElement { }); // F3: Sim cycle count - const pgSimN = sr.getElementById('wd-pg-simn'); - if (pgSimN) pgSimN.addEventListener('input', () => { this._pgSimCycles = Math.max(1, Math.min(200, parseInt(pgSimN.value, 10) || 20)); }); + // Capped at the backend's per-run limit (audit PLAYGROUND-18: 200 was offered, + // 50 were replayed, silently). Shared by Test on history and Optimize. + sr.querySelectorAll('#wd-pg-simn, #wd-pg-sw-n').forEach(inp => inp.addEventListener('input', () => { + this._pgSimCycles = Math.max(1, Math.min(_PG_MAX_BATCH_CYCLES, parseInt(inp.value, 10) || 20)); + })); // F3: Sweep controls const pgSwParam = sr.getElementById('wd-pg-sw-param'); @@ -11490,19 +12140,22 @@ class HaWashdataPanel extends HTMLElement { // Entity-pill multi-pickers: add/remove chips via direct DOM mutation only // (never _render) so other unsaved settings-form edits are preserved. + // One builder for a pill added by typing and by picking, matching the template. + const mkPill = (v) => { + const pill = document.createElement('span'); + pill.className = 'wd-pill'; pill.dataset.val = v; pill.title = v; + const txt = document.createElement('span'); + txt.className = 'wd-pill-txt'; txt.textContent = v; + pill.appendChild(txt); + const x = document.createElement('button'); + x.type = 'button'; x.className = 'wd-pill-x'; x.setAttribute('aria-label', this._tText('btn.remove', {}, 'Remove')); + x.textContent = '×'; + x.addEventListener('click', () => pill.remove()); + pill.appendChild(x); + return pill; + }; sr.querySelectorAll('.wd-pillbox').forEach(box => { const addInput = box.querySelector('.wd-pill-add'); - const mkPill = (v) => { - const pill = document.createElement('span'); - pill.className = 'wd-pill'; pill.dataset.val = v; - pill.appendChild(document.createTextNode(v)); - const x = document.createElement('button'); - x.type = 'button'; x.className = 'wd-pill-x'; x.setAttribute('aria-label', 'Remove'); - x.textContent = '×'; - x.addEventListener('click', () => pill.remove()); - pill.appendChild(x); - return pill; - }; const addVal = (raw) => { const v = String(raw || '').trim(); if (!v) return; @@ -11543,7 +12196,8 @@ class HaWashdataPanel extends HTMLElement { // brand/model catalog) appear without re-wiring the combobox. const entities = (this._entityListCache || {})[optKey] || []; const lq = (q || '').toLowerCase(); - const hits = lq ? entities.filter(e => e.toLowerCase().includes(lq)).slice(0, 40) + // String(): store-catalog candidates are untyped documents (STORE-19). + const hits = lq ? entities.filter(e => String(e).toLowerCase().includes(lq)).slice(0, 40) : entities.slice(0, 20); if (!hits.length) { drop.hidden = true; return; } drop.innerHTML = hits.map(e => `
    ${_esc(e)}
    `).join(''); @@ -11559,15 +12213,7 @@ class HaWashdataPanel extends HTMLElement { if (isPill) { const box = combo.closest('.wd-pillbox'); if (box && !Array.from(box.querySelectorAll('.wd-pill')).some(p => p.dataset.val === val)) { - const pill = document.createElement('span'); - pill.className = 'wd-pill'; pill.dataset.val = val; - pill.appendChild(document.createTextNode(val)); - const x = document.createElement('button'); - x.type = 'button'; x.className = 'wd-pill-x'; x.setAttribute('aria-label', 'Remove'); - x.textContent = '×'; - x.addEventListener('click', () => pill.remove()); - pill.appendChild(x); - box.insertBefore(pill, combo); + box.insertBefore(mkPill(val), combo); } inp.value = ''; } else { @@ -11733,11 +12379,11 @@ class HaWashdataPanel extends HTMLElement { val === 'auto_detect' ? this._t('msg.toast_auto_detect_enabled', {}, 'Auto-detect enabled') : willArm - ? this._t('msg.toast_program_armed', {program: val}, `Program armed for the next cycle: ${val}`) - : this._t('msg.toast_program_set', {program: val}, `Program set: ${val}`)); + ? this._tText('msg.toast_program_armed', {program: val}, `Program armed for the next cycle: ${val}`) + : this._tText('msg.toast_program_set', {program: val}, `Program set: ${val}`)); return this._fetchAll(); }) - .catch(e => this._showToast(this._t('msg.toast_failed', {error: e.message || e}, 'Failed: ' + (e.message || e)), 'error')); + .catch(e => this._showToast(this._tText('msg.toast_failed', {error: e.message || e}, 'Failed: ' + (e.message || e)), 'error')); }); // Compact cycle rows: toggle selection in select mode, else open the cycle. @@ -11754,6 +12400,13 @@ class HaWashdataPanel extends HTMLElement { this._onAction({ dataset: { action: 'open-cycle', cid } }); } })); + // Keyboard: a focused cycle row opens (or toggles) on Enter / Space, the same as + // a click (audit UI-04: the core "open or label a cycle" flow was mouse-only). + sr.querySelectorAll('tr[data-cid][role="button"]').forEach(row => row.addEventListener('keydown', e => { + if (e.target !== row || (e.key !== 'Enter' && e.key !== ' ')) return; + e.preventDefault(); + row.click(); + })); // Cycle-review comparison overlays: toggle a profile's envelope on the chart. sr.querySelectorAll('.wd-cyc-overlay').forEach(cb => cb.addEventListener('change', () => { const m = this._modal; @@ -12006,7 +12659,7 @@ class HaWashdataPanel extends HTMLElement { this._dirtyOptKeys = new Set(); this._showToast(this._t('toast.settings_reverted', {}, 'Settings reverted; integration reloading')); this._render(); - } catch (e) { this._showToast(this._t('msg.toast_revert_failed', {error: e.message || e}, 'Revert failed: ' + (e.message || e)), 'error'); } + } catch (e) { this._showToast(this._tText('msg.toast_revert_failed', {error: e.message || e}, 'Revert failed: ' + (e.message || e)), 'error'); } }); }); const reloadBtn = sr.getElementById('wd-settings-reload'); @@ -12055,13 +12708,6 @@ class HaWashdataPanel extends HTMLElement { const toastUndo = sr.querySelector('[data-toast-undo]'); if (toastUndo) toastUndo.addEventListener('click', () => this._undoDelete(toastUndo.dataset.toastUndo)); - // Coverage gap cluster suggestion: open create-profile modal with pre-filled name. - sr.querySelectorAll('.wd-create-cluster').forEach(btn => btn.addEventListener('click', () => { - const name = btn.dataset.name || ''; - this._modal = { type: 'create-profile', prefillName: name }; - this._render(); - })); - // Suggestion "mute" (#343) -> tell the backend to stop proposing this setting. sr.querySelectorAll('[data-suglock]').forEach(btn => btn.addEventListener('click', async () => { const k = btn.dataset.suglock; @@ -12094,7 +12740,7 @@ class HaWashdataPanel extends HTMLElement { // "N tuning suggestions" count update immediately. Not persisted - a // refresh without saving re-fetches suggestions and restores it. this._suggestions = this._suggestions.filter(s => s.key !== k); - this._showToast(this._t('msg.sug_staged', {key: k, val: v}, `Set ${k} = ${v}. Save to apply.`), 'info'); + this._showToast(this._tText('msg.sug_staged', {key: k, val: v}, `Set ${k} = ${v}. Save to apply.`), 'info'); this._render(); // Auto-cascade: fix any downstream conflicts the staged value introduced. // _render() is synchronous, so the new form DOM is immediately available. @@ -12394,7 +13040,7 @@ class HaWashdataPanel extends HTMLElement { const b = sr.querySelector(`[data-maction="${act}"]`); if (!b) continue; b.disabled = sel.size === 0; - b.textContent = this._t(key, {n: sel.size}, fb); + b.textContent = this._tText(key, {n: sel.size}, fb); } } this._drawSpaghetti(); @@ -12433,6 +13079,7 @@ class HaWashdataPanel extends HTMLElement { this._render(); return; } + if (a === 'retry-load') { this._loading = true; this._fetchAll(); return; } const dev = this._devices[this._selIdx]; if (!dev) return; const eid = dev.entry_id; @@ -12444,9 +13091,9 @@ class HaWashdataPanel extends HTMLElement { if (a.startsWith('store-')) return this._onActStore(a, btn, dev, eid, sr); if (a.startsWith('auto-')) return this._onActAuto(a, btn, dev, eid, sr); if (a.startsWith('maint-')) return this._onActMaintenance(a, btn, dev, eid, sr); - // 'pg-new' / 'pg-edit' / 'pg-suggest' are profile-GROUP actions handled in the + // 'pg-new' / 'pg-edit' are profile-GROUP actions handled in the // chain below, not Playground ones, so they are excluded from the pg- prefix. - if (a.startsWith('pg-') && a !== 'pg-new' && a !== 'pg-edit' && a !== 'pg-suggest') { + if (a.startsWith('pg-') && a !== 'pg-new' && a !== 'pg-edit') { return this._onActPlayground(a, btn, dev, eid); } @@ -12458,8 +13105,8 @@ class HaWashdataPanel extends HTMLElement { if (!this._profiles.length) this._fetchProfiles(eid); this._render(); this._ws({ type: `${_DOMAIN}/get_cycle_power_data`, entry_id: eid, cycle_id: cid }) - .then(r => { if (this._modal && this._modal.cycleId === cid) { this._modal.curve = r; this._modal.loaded = true; this._modal.trim = { start: 0, end: r.full_duration_s || 0 }; this._render(); if (r.profile_name) this._fetchCycleProfileEnv(eid, r.profile_name); } }) - .catch(e => this._showToast(this._t('toast.could_not_load_cycle', {error: e.message || e}, 'Could not load cycle: ' + (e.message || e)), 'error')); + .then(r => { if (this._modal && this._modal.cycleId === cid) { this._modal.curve = r; this._modal.loaded = true; this._modal.trim = { start: 0, end: r.full_duration_s || 0 }; this._loadCycleOverlays(eid, cid, r); this._loadCycleContext(eid, cid, r); this._render(); } }) + .catch(e => this._showToast(this._tText('toast.could_not_load_cycle', {error: e.message || e}, 'Could not load cycle: ' + (e.message || e)), 'error')); } else if (a === 'cleanup-edit-cycle') { const cid = btn.dataset.cid; @@ -12468,8 +13115,8 @@ class HaWashdataPanel extends HTMLElement { if (!this._profiles.length) this._fetchProfiles(eid); this._render(); this._ws({ type: `${_DOMAIN}/get_cycle_power_data`, entry_id: eid, cycle_id: cid }) - .then(r => { if (this._modal && this._modal.cycleId === cid) { this._modal.curve = r; this._modal.loaded = true; this._modal.trim = { start: 0, end: r.full_duration_s || 0 }; this._render(); if (r.profile_name) this._fetchCycleProfileEnv(eid, r.profile_name); } }) - .catch(e => this._showToast(this._t('toast.could_not_load_cycle', {error: e.message || e}, 'Could not load cycle: ' + (e.message || e)), 'error')); + .then(r => { if (this._modal && this._modal.cycleId === cid) { this._modal.curve = r; this._modal.loaded = true; this._modal.trim = { start: 0, end: r.full_duration_s || 0 }; this._loadCycleOverlays(eid, cid, r); this._loadCycleContext(eid, cid, r); this._render(); } }) + .catch(e => this._showToast(this._tText('toast.could_not_load_cycle', {error: e.message || e}, 'Could not load cycle: ' + (e.message || e)), 'error')); } else if (a === 'open-profile') { const name = btn.dataset.pname; @@ -12493,6 +13140,11 @@ class HaWashdataPanel extends HTMLElement { } else if (a === 'create-profile') { this._modal = { type: 'create-profile' }; this._render(); + } else if (a === 'coverage-create') { + // Pre-select the cluster's newest cycle as the reference; the user names it. + this._modal = { type: 'create-profile', prefillCycle: btn.dataset.cid || '', prefillCycleMin: btn.dataset.min || '' }; + this._render(); + } else if (a === 'setup-cta') { // Setup card primary / secondary CTA — navigate to the relevant panel section. const ctaAction = btn.dataset.ctaAction || ''; @@ -12530,6 +13182,12 @@ class HaWashdataPanel extends HTMLElement { this._setPref('setup_card_dismissed', false); this._render(); + } else if (a === 'set-settings-internal') { + // Audit UI-02: reveal or tuck away the internal tuning fields. + this._snapshotFormToPending(sr); + this._setPref('show_internal', !!btn.checked); + this._render(); + } else if (a === 'set-settings-level') { // F2: switch the Settings tab between Basic and Advanced disclosure. const lvl = (btn.type === 'checkbox' ? btn.checked : btn.dataset.slevel === 'advanced') ? 'advanced' : 'basic'; @@ -12539,17 +13197,13 @@ class HaWashdataPanel extends HTMLElement { this._render(); } - } else if (a === 'pg-new' || a === 'pg-edit' || a === 'pg-suggest') { + } else if (a === 'pg-new' || a === 'pg-edit') { if (a === 'pg-new') { this._modal = { type: 'profile-group', orig: null, name: '', members: [] }; - } else if (a === 'pg-edit') { + } else { const gname = btn.dataset.gname; const g = ((this._profileGroups || {}).groups || []).find(x => x.name === gname); this._modal = { type: 'profile-group', orig: gname, name: gname, members: g ? [...(g.members || [])] : [] }; - } else { - const s = ((this._profileGroups || {}).suggestions || [])[parseInt(btn.dataset.idx, 10)] || null; - if (!s) return; - this._modal = { type: 'profile-group', orig: s.existing_group || null, name: s.existing_group || '', members: [...(s.members || [])] }; } this._render(); // Fetch every profile's envelope so ticked members render on the overlay. @@ -12567,25 +13221,35 @@ class HaWashdataPanel extends HTMLElement { ); } else if (a === 'rec-start') { - this._ws({ type: `${_DOMAIN}/start_recording`, entry_id: eid }).then(() => { this._showToast(this._t('toast.recording_started', {}, 'Recording started')); return this._fetchRecState(eid); }).then(() => this._render()).catch(e => this._showToast(this._t('toast.start_failed', {error: e.message || e}, 'Start failed: ' + (e.message || e)), 'error')); + this._ws({ type: `${_DOMAIN}/start_recording`, entry_id: eid }).then(() => { this._showToast(this._t('toast.recording_started', {}, 'Recording started')); return this._fetchRecState(eid); }).then(() => this._render()).catch(e => this._showToast(this._tText('toast.start_failed', {error: e.message || e}, 'Start failed: ' + (e.message || e)), 'error')); } else if (a === 'rec-stop') { - this._ws({ type: `${_DOMAIN}/stop_recording`, entry_id: eid }).then(() => { this._showToast(this._t('toast.recording_stopped', {}, 'Recording stopped')); return this._fetchRecState(eid); }).then(() => this._render()).catch(e => this._showToast(this._t('toast.stop_failed', {error: e.message || e}, 'Stop failed: ' + (e.message || e)), 'error')); + this._ws({ type: `${_DOMAIN}/stop_recording`, entry_id: eid }).then(() => { this._showToast(this._t('toast.recording_stopped', {}, 'Recording stopped')); return this._fetchRecState(eid); }).then(() => this._render()).catch(e => this._showToast(this._tText('toast.stop_failed', {error: e.message || e}, 'Stop failed: ' + (e.message || e)), 'error')); } else if (a === 'rec-process-open') { this._fetchProfiles(eid).then(() => { this._modal = { type: 'process-recording' }; this._render(); }); } else if (a === 'rec-discard') { this._modal = { type: 'confirm', title: this._t('modal.discard_recording_title', {}, 'Discard Recording'), message: this._t('modal.discard_recording_msg', {}, 'Discard the saved recording? This cannot be undone.'), okLabel: this._t('btn.discard', {}, 'Discard'), - onOk: async () => { try { await this._ws({ type: `${_DOMAIN}/discard_recording`, entry_id: eid }); this._showToast(this._t('toast.recording_discarded', {}, 'Recording discarded')); await this._fetchRecState(eid); } catch (e) { this._showToast(this._t('toast.discard_failed', {error: e.message || e}, 'Discard failed: ' + (e.message || e)), 'error'); } } }; + onOk: async () => { try { await this._ws({ type: `${_DOMAIN}/discard_recording`, entry_id: eid }); this._showToast(this._t('toast.recording_discarded', {}, 'Recording discarded')); await this._fetchRecState(eid); } catch (e) { this._showToast(this._tText('toast.discard_failed', {error: e.message || e}, 'Discard failed: ' + (e.message || e)), 'error'); } } }; this._render(); } else if (a === 'fb-confirm') { - this._ws({ type: `${_DOMAIN}/resolve_feedback`, entry_id: eid, cycle_id: btn.dataset.cid, action: 'confirm' }).then(() => { this._showToast(this._t('toast.feedback_confirmed', {}, 'Feedback confirmed')); return this._fetchFeedbacks(eid); }).then(() => this._render()).catch(e => this._showToast(this._t('msg.toast_error', {error: e.message || e}, 'Error: ' + (e.message || e)), 'error')); + const fbCid = btn.dataset.cid; + this._ws({ type: `${_DOMAIN}/resolve_feedback`, entry_id: eid, cycle_id: fbCid, action: 'confirm' }).then(() => { this._showToast(this._t('toast.feedback_confirmed', {}, 'Feedback confirmed')); return this._fetchFeedbacks(eid); }).then(() => { + // Confirming labels the cycle with the detected profile, so an open dialog + // stops calling its overlay "suspected" (#462). + const m = this._modal; + if (m && m.type === 'cycle-detail' && m.cycleId === fbCid && m.overlayPlan && m.overlayPlan.suspected) { + if (m.curve) m.curve.profile_name = m.overlayPlan.name; + m.overlayPlan = { name: m.overlayPlan.name, suspected: false, runnerUp: null }; + } + this._render(); + }).catch(e => this._showToast(this._tText('msg.toast_error', {error: e.message || e}, 'Error: ' + (e.message || e)), 'error')); } else if (a === 'fb-ignore') { - this._ws({ type: `${_DOMAIN}/resolve_feedback`, entry_id: eid, cycle_id: btn.dataset.cid, action: 'ignore' }).then(() => { this._showToast(this._t('toast.feedback_dismissed', {}, 'Feedback dismissed')); return this._fetchFeedbacks(eid); }).then(() => this._render()).catch(e => this._showToast(this._t('msg.toast_error', {error: e.message || e}, 'Error: ' + (e.message || e)), 'error')); + this._ws({ type: `${_DOMAIN}/resolve_feedback`, entry_id: eid, cycle_id: btn.dataset.cid, action: 'ignore' }).then(() => { this._showToast(this._t('toast.feedback_dismissed', {}, 'Feedback dismissed')); return this._fetchFeedbacks(eid); }).then(() => this._render()).catch(e => this._showToast(this._tText('msg.toast_error', {error: e.message || e}, 'Error: ' + (e.message || e)), 'error')); } else if (a === 'fb-correct') { this._fetchProfiles(eid).then(() => { this._modal = { type: 'correct-feedback', cycleId: btn.dataset.cid, detectedProfile: btn.dataset.prof }; this._render(); }); } else if (a === 'fb-dismiss-all') { - this._modal = { type: 'confirm', title: this._t('modal.dismiss_all_title', {}, 'Dismiss All Feedbacks'), message: this._t('modal.dismiss_all_msg', {count: this._feedbacks.length}, `Dismiss all ${this._feedbacks.length} pending feedback requests?`), okLabel: this._t('modal.dismiss_all_ok', {}, 'Dismiss All'), - onOk: async () => { try { await this._ws({ type: `${_DOMAIN}/dismiss_all_feedbacks`, entry_id: eid }); this._showToast(this._t('toast.feedback_all_dismissed', {}, 'All feedbacks dismissed')); await this._fetchFeedbacks(eid); } catch (e) { this._showToast(this._t('msg.toast_error', {error: e.message || e}, 'Error: ' + (e.message || e)), 'error'); } } }; + this._modal = { type: 'confirm', title: this._t('modal.dismiss_all_title', {}, 'Dismiss All Feedbacks'), message: this._tText('modal.dismiss_all_msg', {count: this._feedbacks.length}, `Dismiss all ${this._feedbacks.length} pending feedback requests?`), okLabel: this._t('modal.dismiss_all_ok', {}, 'Dismiss All'), + onOk: async () => { try { await this._ws({ type: `${_DOMAIN}/dismiss_all_feedbacks`, entry_id: eid }); this._showToast(this._t('toast.feedback_all_dismissed', {}, 'All feedbacks dismissed')); await this._fetchFeedbacks(eid); } catch (e) { this._showToast(this._tText('msg.toast_error', {error: e.message || e}, 'Error: ' + (e.message || e)), 'error'); } } }; this._render(); } else if (a === 'create-phase') { @@ -12594,8 +13258,17 @@ class HaWashdataPanel extends HTMLElement { this._modal = { type: 'edit-phase', phaseId: btn.dataset.pid, phaseName: btn.dataset.pname, phaseDesc: btn.dataset.pdesc, isDefault: btn.dataset.pisdefault === 'true' }; this._render(); } else if (a === 'del-phase') { const pname = btn.dataset.pname, pid = btn.dataset.pid; - this._modal = { type: 'confirm', title: this._t('modal.delete_phase_title', {}, 'Delete Phase'), message: this._t('modal.delete_phase_msg', {name: pname}, `Delete phase "${pname}"?`), okLabel: this._t('btn.delete', {}, 'Delete'), - onOk: async () => { try { await this._ws({ type: `${_DOMAIN}/delete_phase`, entry_id: eid, phase_id: pid }); this._showToast(this._t('toast.phase_deleted', {name: pname}, `Phase "${pname}" deleted`)); await this._fetchPhases(eid); } catch (e) { this._showToast(this._t('msg.toast_delete_failed', {error: e.message || e}, 'Delete failed: ' + (e.message || e)), 'error'); } } }; + // An edited built-in: delete_phase drops the override, which restores the + // built-in and keeps profiles' ranges on it (it used to fail every time). + const reset = btn.dataset.reset === '1'; + const msg = reset + ? this._tText('modal.reset_phase_msg', {name: pname}, `Undo your edit of "${pname}" and restore the built-in phase? Programs keep their ranges.`) + : this._tText('modal.delete_phase_msg', {name: pname}, `Delete phase "${pname}"?`); + const done = reset + ? this._tText('toast.phase_reset', {name: pname}, `"${pname}" restored to the built-in phase`) + : this._tText('toast.phase_deleted', {name: pname}, `Phase "${pname}" deleted`); + this._modal = { type: 'confirm', title: reset ? this._t('modal.reset_phase_title', {}, 'Reset Phase') : this._t('modal.delete_phase_title', {}, 'Delete Phase'), message: msg, okLabel: reset ? this._t('btn.reset', {}, 'Reset') : this._t('btn.delete', {}, 'Delete'), + onOk: async () => { try { await this._ws({ type: `${_DOMAIN}/delete_phase`, entry_id: eid, phase_id: pid }); this._showToast(done); await this._fetchPhases(eid); } catch (e) { this._showToast(this._tText('msg.toast_delete_failed', {error: e.message || e}, 'Delete failed: ' + (e.message || e)), 'error'); } } }; this._render(); } else if (a === 'diag-refresh') { @@ -12605,23 +13278,45 @@ class HaWashdataPanel extends HTMLElement { this._modal = { type: 'confirm', title: this._t('modal.process_history_title', {}, 'Process History'), message: this._t('modal.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.'), okLabel: this._t('modal.process_history_ok', {}, 'Process'), onOk: () => this._kickAndTrack({ type: `${_DOMAIN}/reprocess_history`, entry_id: eid }, 'reprocess', async (r) => { const nc = r.count || 0; - const bits = [this._t('toast.processed_cycles', {n: nc}, nc + ' cycles')]; - if (r.suggestions != null) bits.push(this._t('toast.processed_suggestions', {n: r.suggestions}, r.suggestions + ' suggestion(s)')); + const bits = [this._tText('toast.processed_cycles', {n: nc}, nc + ' cycles')]; + if (r.suggestions != null) bits.push(this._tText('toast.processed_suggestions', {n: r.suggestions}, r.suggestions + ' suggestion(s)')); const np = (r.ml_training && r.ml_training.ok && (r.ml_training.promoted || []).length) || 0; - if (np) bits.push(this._t('toast.processed_models', {n: np}, np + ' model(s) promoted')); - this._showToast(this._t('toast.processed', {bits: bits.join(', ')}, 'Processed ' + bits.join(', '))); + if (np) bits.push(this._tText('toast.processed_models', {n: np}, np + ' model(s) promoted')); + this._showToast(this._tText('toast.processed', {bits: bits.join(', ')}, 'Processed ' + bits.join(', '))); await this._fetchToolsData(eid); }) }; this._render(); } else if (a === 'clear-debug') { this._modal = { type: 'confirm', title: this._t('modal.clear_debug_title', {}, 'Clear Debug Data'), message: this._t('modal.clear_debug_msg', {}, 'Delete all stored debug traces?'), okLabel: this._t('btn.clear', {}, 'Clear'), - onOk: () => this._busyRun('clear-debug', async () => { try { const r = await this._ws({ type: `${_DOMAIN}/clear_debug_data`, entry_id: eid }); this._showToast(this._t('toast.debug_cleared', {count: r.count || 0}, `Cleared ${r.count || 0} debug traces`)); await this._fetchToolsData(eid); } catch (e) { this._showToast(this._t('msg.toast_error', {error: e.message || e}, 'Error: ' + (e.message || e)), 'error'); } }) }; + onOk: () => this._busyRun('clear-debug', async () => { try { const r = await this._ws({ type: `${_DOMAIN}/clear_debug_data`, entry_id: eid }); this._showToast(this._tText('toast.debug_cleared', {count: r.count || 0}, `Cleared ${r.count || 0} debug traces`)); await this._fetchToolsData(eid); } catch (e) { this._showToast(this._tText('msg.toast_error', {error: e.message || e}, 'Error: ' + (e.message || e)), 'error'); } }) }; this._render(); } else if (a === 'wipe-history') { this._modal = { type: 'confirm', title: this._t('modal.wipe_all_title', {}, 'Wipe All Data'), message: this._t('modal.wipe_all_msg', {}, '⚠️ This permanently deletes ALL cycles and profiles. This cannot be undone.'), okLabel: this._t('modal.wipe_all_ok', {}, 'Wipe Everything'), - onOk: () => this._busyRun('wipe', async () => { try { await this._ws({ type: `${_DOMAIN}/wipe_history`, entry_id: eid }); this._showToast(this._t('toast.all_wiped', {}, 'All data wiped')); this._cycles = []; this._profiles = []; await this._fetchToolsData(eid); } catch (e) { this._showToast(this._t('msg.toast_error', {error: e.message || e}, 'Error: ' + (e.message || e)), 'error'); } }) }; + onOk: () => this._busyRun('wipe', async () => { try { await this._ws({ type: `${_DOMAIN}/wipe_history`, entry_id: eid }); this._showToast(this._t('toast.all_wiped', {}, 'All data wiped')); this._cycles = []; this._profiles = []; await this._fetchToolsData(eid); } catch (e) { this._showToast(this._tText('msg.toast_error', {error: e.message || e}, 'Error: ' + (e.message || e)), 'error'); } }) }; this._render(); + } else if (a === 'import-undo') { + // Restoring replaces the whole store again, so it is confirmed like a wipe. + const u = this._importUndo || {}; + this._modal = { type: 'confirm', title: this._tText('modal.undo_import_title', {}, 'Undo last import'), + message: this._tText('modal.undo_import_msg', { when: _fmtDate(u.created_at) }, + `This device's data and settings go back to how they were before the last import (${_fmtDate(u.created_at)}). Cycles recorded and changes made since then are lost.`), + okLabel: this._tText('modal.undo_import_ok', {}, 'Undo import'), + onOk: () => this._busyRun('import-undo', async () => { + try { + const r = await this._ws({ type: `${_DOMAIN}/undo_import`, entry_id: eid }); + const c = ((r && r.summary) || {}).counts || {}; + const cycles = (c.real_cycles || 0) + (c.reference_cycles || 0) + (c.backfill_cycles || 0); + this._showToast(this._tText('toast.import_undone', { profiles: c.profiles || 0, cycles }, + `Import undone: ${c.profiles || 0} profile(s) and ${cycles} cycle(s) restored`)); + } catch (e) { this._showToast(this._tText('toast.import_undo_failed', { error: e.message || e }, 'Could not undo the import: ' + (e.message || e)), 'error'); } + // Refresh either way: a failure can mean the restore point is already gone. + if (!this._isActiveEntry(eid)) return; + await this._fetchToolsData(eid); + await this._fetchCycles(eid); + await this._fetchProfiles(eid); + }) }; + this._render(); } else if (a === 'export-config') { this._ws({ type: `${_DOMAIN}/export_config`, entry_id: eid }).then(r => { const blob = new Blob([r.json_data], { type: 'application/json' }); @@ -12630,15 +13325,15 @@ class HaWashdataPanel extends HTMLElement { a2.href = url; a2.download = `washdata_export_${eid.slice(0, 8)}.json`; document.body.appendChild(a2); a2.click(); document.body.removeChild(a2); URL.revokeObjectURL(url); this._showToast(this._t('toast.export_downloaded', {}, 'Export downloaded')); - }).catch(e => this._showToast(this._t('toast.export_failed', {error: e.message || e}, 'Export failed: ' + (e.message || e)), 'error')); + }).catch(e => this._showToast(this._tText('toast.export_failed', {error: e.message || e}, 'Export failed: ' + (e.message || e)), 'error')); } else if (a === 'export-select-open') { // Open the export wizard: fetch this device's inventory, default everything on. - this._modal = { type: 'export-select', inventory: null, loading: true, sel: { cats: new Set(), profiles: new Set(), realIds: new Set(), refIds: new Set() }, expanded: new Set() }; + this._modal = { type: 'export-select', inventory: null, loading: true, sel: this._wizEmptySel(), expanded: new Set() }; this._render(); (async () => { let inv = null; try { const r = await this._ws({ type: `${_DOMAIN}/get_export_inventory`, entry_id: eid }); inv = (r && r.manifest) || null; } - catch (e) { this._showToast(this._t('toast.export_failed', {error: e.message || e}, 'Export failed: ' + (e.message || e)), 'error'); } + catch (e) { this._showToast(this._tText('toast.export_failed', {error: e.message || e}, 'Export failed: ' + (e.message || e)), 'error'); } if (!this._isActiveEntry(eid) || !this._modal || this._modal.type !== 'export-select') return; if (!inv) { this._modal = null; this._render(); return; } this._modal.inventory = inv; @@ -12651,7 +13346,7 @@ class HaWashdataPanel extends HTMLElement { if (!this._selectMode) this._cycleSel.clear(); this._render(); } else if (a === 'cyc-auto-open') { - this._modal = { type: 'auto-label' }; this._render(); + this._openAutoLabelModal(eid); } else if (a === 'cyc-merge') { const ids = Array.from(this._cycleSel); if (ids.length < 2) return; @@ -12665,7 +13360,7 @@ class HaWashdataPanel extends HTMLElement { // D3: append the next page, preserving current sort/filter. this._busyRun('cyc-load-more', async () => { try { await this._loadMoreCycles(eid); } - catch (e) { this._showToast(this._t('toast.load_more_failed', { error: e.message || e }, 'Could not load more: ' + (e.message || e)), 'error'); } + catch (e) { this._showToast(this._tText('toast.load_more_failed', { error: e.message || e }, 'Could not load more: ' + (e.message || e)), 'error'); } }); } else if (a === 'task-cancel') { const tid = btn.dataset.taskId; @@ -12701,6 +13396,8 @@ class HaWashdataPanel extends HTMLElement { const ids = Array.from(this._cycleSel); if (!ids.length) return; this._deleteCyclesWithUndo(eid, ids); + } else if (a === 'retry-tab') { + this._fetchTabData(); } else if (a === 'retry-cycles') { this._fetchCycles(eid).then(() => this._render()); } else if (a === 'retry-profiles') { @@ -12739,30 +13436,20 @@ class HaWashdataPanel extends HTMLElement { .then(() => this._ws({ type: `${_DOMAIN}/get_options`, entry_id: eid })) .then(r => { this._opts = r.options || {}; this._optDefaults = r.defaults || {}; return this._fetchSettingsChangelog(eid); }) .then(() => { - this._showToast(this._t('msg.toast_reverted', { key: this._t('setting.' + key + '.label', {}, key) }, '{key} reverted'), 'success'); + this._showToast(this._tText('msg.toast_reverted', { key: this._t('setting.' + key + '.label', {}, key) }, '{key} reverted'), 'success'); this._render(); }) - .catch(e => this._showToast(this._t('msg.toast_error', { error: e.message || e }, 'Error: ' + (e.message || e)), 'error')); + .catch(e => this._showToast(this._tText('msg.toast_error', { error: e.message || e }, 'Error: ' + (e.message || e)), 'error')); } else if (a === 'toggle-log-drawer') { this._logOpen = !this._logOpen; try { localStorage.setItem('wd-log-open', this._logOpen ? '1' : '0'); } catch (_) {} this._render(); if (this._logOpen) this._fetchLogs().then(() => { if (this._logOpen) this._render(); }); - } else if (a === 'open-advanced') { - // Overview action cards navigate to the Advanced tab at a given subtab. - const sub = btn.dataset.sub; - if (sub) this._panelSubtab = sub; - this._tab = 'advanced'; - this._render(); - if (this._panelSubtab === 'diagnostics' && !this._diag) this._fetchToolsData(eid).then(() => { if (this._tab === 'advanced') this._render(); }); - else if (this._panelSubtab === 'logs') this._fetchLogs().then(() => { if (this._tab === 'advanced') this._render(); }); - else if (this._panelSubtab === 'maintenance') this._fetchMaintenance(eid).then(() => { if (this._tab === 'advanced') this._render(); }); - else if (this._panelSubtab === 'ml') this._fetchTabData(); } else if (a === 'add-device') { this._navigate(`/config/integrations/integration/${_DOMAIN}`); } else if (a === 'goto-feedbacks') { - this._tab = 'history'; this._cycleFilter = { ...this._cycleFilter, status: 'needs_review' }; this._fetchTabData(); + this._tab = 'history'; this._historySub = 'cycles'; this._cycleFilter = { ...this._cycleFilter, status: 'needs_review' }; this._fetchTabData(); } else if (a === 'goto-recording') { this._tab = 'status'; this._fetchTabData(); } else if (a === 'logs-refresh') { @@ -12776,11 +13463,11 @@ class HaWashdataPanel extends HTMLElement { a2.href = url; a2.download = `washdata_logs_${Date.now()}.txt`; document.body.appendChild(a2); a2.click(); document.body.removeChild(a2); URL.revokeObjectURL(url); this._showToast(this._t('toast.logs_exported', {}, 'Logs exported')); - }).catch(e => this._showToast(this._t('toast.export_failed', {error: e.message || e}, 'Export failed: ' + (e.message || e)), 'error')); + }).catch(e => this._showToast(this._tText('toast.export_failed', {error: e.message || e}, 'Export failed: ' + (e.message || e)), 'error')); } else if (a === 'import-config-open') { // Open the import wizard at the paste/upload step. this._modal = { type: 'import-wizard', step: 'input', jsonText: '', manifest: null, error: null, - sel: { cats: new Set(), profiles: new Set(), realIds: new Set(), refIds: new Set() }, + sel: this._wizEmptySel(), expanded: new Set(), mode: 'merge', cycleDest: 'reference', conflicts: {} }; this._render(); } else if (a === 'hist-import-open') { @@ -12793,9 +13480,6 @@ class HaWashdataPanel extends HTMLElement { done: null, error: null, }; this._render(); - } else if (a === 'import-config-raw') { - // Advanced fallback: the legacy raw-JSON whole-store replace. - this._modal = { type: 'import-config' }; this._render(); } else if (a === 'save-prefs') { const dt = sr.getElementById('wd-pref-tab')?.value || ''; @@ -12816,7 +13500,7 @@ class HaWashdataPanel extends HTMLElement { await this._loadPanelLang(effLang); this._render(); this._showToast(this._t('toast.preferences_saved', {}, 'Preferences saved')); - } catch (e) { this._showToast(this._t('toast.save_failed', {error: e.message || e}, 'Save failed: ' + (e.message || e)), 'error'); } + } catch (e) { this._showToast(this._tText('toast.save_failed', {error: e.message || e}, 'Save failed: ' + (e.message || e)), 'error'); } }); } else if (a === 'save-panel') { @@ -12831,7 +13515,7 @@ class HaWashdataPanel extends HTMLElement { this._tabInitialized = true; // keep the user on the current tab this._applyPanelConfig(); this._showToast(this._t('toast.panel_settings_saved', {}, 'Panel settings saved')); - } catch (e) { this._showToast(this._t('msg.toast_save_failed', {error: e.message || e}, 'Save failed: ' + (e.message || e)), 'error'); } + } catch (e) { this._showToast(this._tText('msg.toast_save_failed', {error: e.message || e}, 'Save failed: ' + (e.message || e)), 'error'); } }); } else if (a === 'pause-cycle') { @@ -12841,7 +13525,7 @@ class HaWashdataPanel extends HTMLElement { this._showToast(this._t('toast.cycle_paused', {}, 'Cycle paused')); return this._fetchAll(); }) - .catch(e => this._showToast(this._t('toast.pause_failed', {error: e.message || e}, 'Pause failed: ' + (e.message || e)), 'error')); + .catch(e => this._showToast(this._tText('toast.pause_failed', {error: e.message || e}, 'Pause failed: ' + (e.message || e)), 'error')); } else if (a === 'resume-cycle') { this._ws({ type: `${_DOMAIN}/resume_cycle`, entry_id: eid }) @@ -12850,7 +13534,7 @@ class HaWashdataPanel extends HTMLElement { this._showToast(this._t('toast.cycle_resumed', {}, 'Cycle resumed')); return this._fetchAll(); }) - .catch(e => this._showToast(this._t('msg.toast_resume_failed', {error: e.message || e}, 'Resume failed: ' + (e.message || e)), 'error')); + .catch(e => this._showToast(this._tText('msg.toast_resume_failed', {error: e.message || e}, 'Resume failed: ' + (e.message || e)), 'error')); } else if (a === 'terminate-cycle') { this._modal = { @@ -12863,7 +13547,7 @@ class HaWashdataPanel extends HTMLElement { await this._ws({ type: `${_DOMAIN}/terminate_cycle`, entry_id: eid }); this._showToast(this._t('toast.cycle_force_stopped', {}, 'Cycle force-stopped')); await this._fetchAll(); - } catch (e) { this._showToast(this._t('msg.toast_force_stop_failed', {error: e.message || e}, 'Force stop failed: ' + (e.message || e)), 'error'); } + } catch (e) { this._showToast(this._tText('msg.toast_force_stop_failed', {error: e.message || e}, 'Force stop failed: ' + (e.message || e)), 'error'); } }, }; this._render(); @@ -12883,27 +13567,48 @@ class HaWashdataPanel extends HTMLElement { await this._ws({ type: `${_DOMAIN}/set_panel_config`, rbac: { enabled, default_level, users: usersMap } }); this._panelCfg = await this._ws({ type: `${_DOMAIN}/get_panel_config` }); this._showToast(this._t('toast.access_saved', {}, 'Access control saved')); - } catch (e) { this._showToast(this._t('msg.toast_save_failed', {error: e.message || e}, 'Save failed: ' + (e.message || e)), 'error'); } + } catch (e) { this._showToast(this._tText('msg.toast_save_failed', {error: e.message || e}, 'Save failed: ' + (e.message || e)), 'error'); } }); } } + _applyAllSuggestions(eid, keys) { + this._busyRun('save-settings', async () => { + try { + await this._ws({ type: `${_DOMAIN}/apply_suggestions`, entry_id: eid, keys }); + this._showToast(this._t('toast.suggestions_applied', {}, 'Suggestions applied; integration reloading')); + await this._fetchSuggestions(eid); + const r = await this._ws({ type: `${_DOMAIN}/get_options`, entry_id: eid }); + this._opts = r.options || {}; + this._optDefaults = r.defaults || {}; // #396 + this._prevOpts = null; + this._cascadePending = {}; + this._preCascadeOpts = null; + } catch (e) { this._showToast(this._tText('toast.apply_failed', {error: e.message || e}, 'Apply failed: ' + (e.message || e)), 'error'); } + }); + } + _onActSuggestions(a, btn, dev, eid, sr) { if (a === 'sug-apply-all') { const keys = this._suggestions.map(s => s.key); - this._busyRun('save-settings', async () => { - try { - await this._ws({ type: `${_DOMAIN}/apply_suggestions`, entry_id: eid, keys }); - this._showToast(this._t('toast.suggestions_applied', {}, 'Suggestions applied; integration reloading')); - await this._fetchSuggestions(eid); - const r = await this._ws({ type: `${_DOMAIN}/get_options`, entry_id: eid }); - this._opts = r.options || {}; - this._optDefaults = r.defaults || {}; // #396 - this._prevOpts = null; - this._cascadePending = {}; - this._preCascadeOpts = null; - } catch (e) { this._showToast(this._t('toast.apply_failed', {error: e.message || e}, 'Apply failed: ' + (e.message || e)), 'error'); } + // Preview first: up to 17 coupled changes used to land on one click. + const items = this._suggestions.map(sg => { + const f = _FIELD_BY_KEY[sg.key] || {}; + const u = f.unit ? ` ${f.unit}` : ''; + const cur = sg.current == null || sg.current === '' ? '-' : `${sg.current}${u}`; + return { + text: `${this._t('setting.' + sg.key + '.label', {}, f.label || sg.key)}: ${cur} → ${sg.suggested}${u}`, + sub: _sugImpact(this._t.bind(this), sg.key, sg.suggested, sg.current), + }; }); + this._modal = { + type: 'confirm', okPrimary: true, items, + title: this._t('modal.apply_suggestions_title', {}, 'Apply suggestions'), + message: this._tText('modal.apply_suggestions_msg', {n: items.length}, `Change these ${items.length} settings? The integration reloads after saving.`), + okLabel: this._t('btn.apply_all', {}, 'Apply all'), + onOk: () => this._applyAllSuggestions(eid, keys), + }; + this._render(); } else if (a === 'sug-show-all') { this._settingsSugOnly = false; this._render(); @@ -12912,7 +13617,7 @@ class HaWashdataPanel extends HTMLElement { this._settingsSugOnly = false; this._busyRun('save-settings', async () => { try { await this._ws({ type: `${_DOMAIN}/clear_suggestions`, entry_id: eid }); this._suggestions = []; this._showToast(this._t('toast.suggestions_dismissed', {}, 'Suggestions dismissed')); } - catch (e) { this._showToast(this._t('toast.error', {error: e.message || e}, 'Error: ' + (e.message || e)), 'error'); } + catch (e) { this._showToast(this._tText('toast.error', {error: e.message || e}, 'Error: ' + (e.message || e)), 'error'); } }); } else if (a === 'sug-unmute-all') { @@ -12929,11 +13634,11 @@ class HaWashdataPanel extends HTMLElement { this._lockedSuggestions = (lastOk && lastOk.value && lastOk.value.locked_suggestions) || []; await this._fetchSuggestions(eid); if (failed) { - this._showToast(this._t('toast.error', {error: `${failed} suggestion(s) failed to unlock`}, `${failed} suggestion(s) failed to unlock`), 'error'); + this._showToast(this._tText('toast.error', {error: `${failed} suggestion(s) failed to unlock`}, `${failed} suggestion(s) failed to unlock`), 'error'); } else { this._showToast(this._t('msg.sug_unmuted_all', {}, 'Muted suggestions reset'), 'success'); } - } catch (e) { this._showToast(this._t('toast.error', {error: e.message || e}, 'Error: ' + (e.message || e)), 'error'); } + } catch (e) { this._showToast(this._tText('toast.error', {error: e.message || e}, 'Error: ' + (e.message || e)), 'error'); } }); } else if (a === 'sug-analyze') { @@ -12941,9 +13646,9 @@ class HaWashdataPanel extends HTMLElement { try { const r = await this._ws({ type: `${_DOMAIN}/run_suggestion_analysis`, entry_id: eid }); const n = (r && r.count) || 0; - this._showToast(n ? this._t('toast.analysis_complete', {count: n}, `Analysis complete: ${n} suggestion(s)`) : this._t('toast.analysis_complete_none', {}, 'Analysis complete: no new suggestions')); + this._showToast(n ? this._tText('toast.analysis_complete', {count: n}, `Analysis complete: ${n} suggestion(s)`) : this._t('toast.analysis_complete_none', {}, 'Analysis complete: no new suggestions')); await this._fetchSuggestions(eid); - } catch (e) { this._showToast(this._t('toast.analysis_failed', {error: e.message || e}, 'Analysis failed: ' + (e.message || e)), 'error'); } + } catch (e) { this._showToast(this._tText('toast.analysis_failed', {error: e.message || e}, 'Analysis failed: ' + (e.message || e)), 'error'); } }); } } @@ -12955,29 +13660,24 @@ class HaWashdataPanel extends HTMLElement { this._kickAndTrack({ type: `${_DOMAIN}/trigger_ml_training`, entry_id: eid }, 'ml-train-now:' + eid, async (r) => { if (r && r.ok) { const promoted = (r.promoted || []).length; - this._showToast(promoted ? this._t('toast.ml_training_promoted', {count: promoted}, `Training complete: promoted ${promoted} model(s)`) : this._t('toast.ml_training_no_improvement', {}, 'Training complete: baseline kept (no improvement)')); + // Not promoted: say why (audit ML-20), not a blanket "no improvement". + const kept = !promoted && (r.results || []).find(x => x && x.promoted === false && x.reason_code); + this._showToast(promoted ? this._tText('toast.ml_training_promoted', {count: promoted}, `Training complete: promoted ${promoted} model(s)`) + : kept ? this._mlSkipReason(kept)[1] + : this._t('toast.ml_training_no_improvement', {}, 'Training complete: baseline kept (no improvement)')); } else { this._showToast(this._t('toast.ml_training_no_improvement', {}, 'Training complete: baseline kept (no improvement)'), 'info'); } await this._loadMlTrainingStatus(eid); }); - } else if (a === 'ml-revert-match') { - this._busyRun('ml-revert-match', async () => { - try { - await this._ws({ type: `${_DOMAIN}/revert_matching_config`, entry_id: eid }); - this._showToast(this._t('toast.matching_reverted', {}, 'Matching weights reverted to defaults')); - await this._loadMlTrainingStatus(eid); - } catch (e) { this._showToast(this._t('toast.revert_failed', {error: e.message || e}, 'Revert failed: ' + (e.message || e)), 'error'); } - }); - } else if (a === 'ml-revert-models') { this._busyRun('ml-revert-models', async () => { try { await this._ws({ type: `${_DOMAIN}/revert_ml_models`, entry_id: eid }); this._showToast(this._t('toast.models_reverted', {}, 'On-device models reverted to baseline')); await this._loadMlTrainingStatus(eid); - } catch (e) { this._showToast(this._t('msg.toast_revert_failed', {error: e.message || e}, 'Revert failed: ' + (e.message || e)), 'error'); } + } catch (e) { this._showToast(this._tText('msg.toast_revert_failed', {error: e.message || e}, 'Revert failed: ' + (e.message || e)), 'error'); } }); } } @@ -12994,7 +13694,7 @@ class HaWashdataPanel extends HTMLElement { if (this._constants.storeOnlineEnabled) { await this._loadStoreStatus(eid); this._ensureStoreConnectListener(); } else { this._storeStatus = { enabled: false }; this._storeConnected = false; } } catch (e) { - this._showToast(this._t('toast.store_error', {error: e.message || e}, 'Error: ' + (e.message || e)), 'error'); + this._showToast(this._tText('toast.store_error', {error: e.message || e}, 'Error: ' + (e.message || e)), 'error'); } }); @@ -13008,7 +13708,7 @@ class HaWashdataPanel extends HTMLElement { const r = await this._ws({ type: `${_DOMAIN}/store_set_prefs`, entry_id: eid, prefs: { [key]: val } }); if (r && r.prefs) this._constants = { ...this._constants, storePrefs: r.prefs }; } catch (e) { - this._showToast(this._t('toast.store_error', {error: e.message || e}, 'Error: ' + (e.message || e)), 'error'); + this._showToast(this._tText('toast.store_error', {error: e.message || e}, 'Error: ' + (e.message || e)), 'error'); } }); @@ -13041,7 +13741,7 @@ class HaWashdataPanel extends HTMLElement { } this._showToast(this._t('toast.catalog_refreshed', {}, 'Community catalog refreshed')); } catch (e) { - this._showToast(this._t('toast.store_error', {error: e.message || e}, 'Error: ' + (e.message || e)), 'error'); + this._showToast(this._tText('toast.store_error', {error: e.message || e}, 'Error: ' + (e.message || e)), 'error'); } }); @@ -13057,7 +13757,7 @@ class HaWashdataPanel extends HTMLElement { await this._ws({ type: `${_DOMAIN}/store_disconnect`, entry_id: eid }); await this._loadStoreStatus(eid); this._showToast(this._t('toast.store_disconnected', {}, 'Disconnected from the community store')); - } catch (e) { this._showToast(this._t('toast.store_error', {error: e.message || e}, 'Error: ' + (e.message || e)), 'error'); } + } catch (e) { this._showToast(this._tText('toast.store_error', {error: e.message || e}, 'Error: ' + (e.message || e)), 'error'); } }); } else if (a === 'store-add-appliance') { @@ -13095,7 +13795,7 @@ class HaWashdataPanel extends HTMLElement { this._busyRun('store-account', async () => { try { const r = await this._ws({ type: `${_DOMAIN}/store_confirm_device`, entry_id: eid, device_id: did }); - if (r && r.error) { this._showToast(this._t('toast.store_error', {error: r.error}, 'Error: ' + r.error), 'error'); return; } + if (r && r.error) { this._showToast(this._tText('toast.store_error', {error: r.error}, 'Error: ' + r.error), 'error'); return; } // Patch every copy of the row the UI may be showing: the picker's badge now // reads the resolved catalog entry, while the Store tab reads the browse list. const rows = [ @@ -13108,7 +13808,7 @@ class HaWashdataPanel extends HTMLElement { } this._showToast(r && r.status === 'approved' ? this._t('toast.device_approved', {}, 'Approved by the community') : this._t('toast.thanks_confirming', {}, 'Thanks for confirming')); this._render(); - } catch (e2) { this._showToast(this._t('toast.store_error', {error: e2.message || e2}, 'Error: ' + (e2.message || e2)), 'error'); } + } catch (e2) { this._showToast(this._tText('toast.store_error', {error: e2.message || e2}, 'Error: ' + (e2.message || e2)), 'error'); } }); } else if (a === 'store-rate-device') { @@ -13118,9 +13818,9 @@ class HaWashdataPanel extends HTMLElement { this._busyRun('store-account', async () => { try { const r = await this._ws({ type: `${_DOMAIN}/store_rate_device`, entry_id: eid, device_id: did, rating }); - if (r && r.error) { this._showToast(this._t('toast.store_error', {error: r.error}, 'Error: ' + r.error), 'error'); return; } + if (r && r.error) { this._showToast(this._tText('toast.store_error', {error: r.error}, 'Error: ' + r.error), 'error'); return; } this._showToast(this._t('toast.rating_saved', {}, 'Quality rating saved')); - } catch (e2) { this._showToast(this._t('toast.store_error', {error: e2.message || e2}, 'Error: ' + (e2.message || e2)), 'error'); } + } catch (e2) { this._showToast(this._tText('toast.store_error', {error: e2.message || e2}, 'Error: ' + (e2.message || e2)), 'error'); } }); } else if (a === 'store-nav') { @@ -13138,10 +13838,10 @@ class HaWashdataPanel extends HTMLElement { const p = (this._storeProfiles || []).find(x => String(x.id) === String(id)); if (!p) return; this._storeProfile = p; this._storeView = 'profile'; - this._storeCycles = []; this._storeLoading = true; this._render(); + this._storeCycles = []; this._storeLoading = true; this._storeBrowseErr = false; this._render(); this._ws({ type: `${_DOMAIN}/store_get_cycles`, entry_id: eid, profile_id: p.id }) - .then(r => { if (!this._isActiveEntry(eid) || this._storeView !== 'profile') return; this._storeCycles = (r && r.items) || []; }) - .catch(() => { if (this._isActiveEntry(eid)) this._storeCycles = []; }) + .then(r => { if (!this._isActiveEntry(eid) || this._storeView !== 'profile') return; this._storeCycles = (r && r.items) || []; this._storeBrowseErr = !!(r && r.error); }) + .catch(() => { if (this._isActiveEntry(eid)) { this._storeCycles = []; this._storeBrowseErr = true; } }) .finally(() => { if (this._isActiveEntry(eid)) { this._storeLoading = false; this._render(); } }); } else if (a === 'store-onboard') { @@ -13160,22 +13860,30 @@ class HaWashdataPanel extends HTMLElement { const withSettings = !!this._dlSettings; this._busyRun('store-download-device', async () => { try { - const r = await this._ws({ type: `${_DOMAIN}/store_download_device`, entry_id: eid, device_id: did, include_settings: withSettings }); - if (r && (r.error || r.disabled)) { const why = r.error || 'unavailable'; this._showToast(this._t('toast.store_download_failed', {error: why}, 'Download failed: ' + why), 'error'); return; } + const started = await this._ws({ type: `${_DOMAIN}/store_download_device`, entry_id: eid, device_id: did, include_settings: withSettings }); + // A registry task now (audit STORE-10); its result is the old reply. + const r = started && started.task_id ? ((await this._awaitTask(started.task_id)) || {}).result : started; + if (r && (r.error || r.disabled)) { const why = r.error || 'unavailable'; this._showToast(this._tText('toast.store_download_failed', {error: why}, 'Download failed: ' + why), 'error'); return; } const p = (r && r.profiles_adopted) || 0, c = (r && r.cycles_imported) || 0; const sa = (r && r.settings_applied) || 0; + const sk = (r && r.cycles_skipped) || 0; + const skipped = sk ? this._tText('toast.store_download_skipped', {n: sk}, `${sk} recording(s) skipped: too short, too gappy or already on your device.`) : ''; if (!p && !c && !sa) { // Nothing adopted: either already imported, or the fetch came back empty. - this._showToast(this._t('toast.store_download_nothing', {}, 'Nothing new to download - this setup is already on your device.'), 'info'); + this._showToast(skipped || this._t('toast.store_download_nothing', {}, 'Nothing new to download - this setup is already on your device.'), 'info'); return; } await this._fetchProfiles(eid); await this._fetchCycles(eid); const ph = (r && r.phases_applied) || 0; - if (sa) this._showToast(this._t('toast.store_device_downloaded_settings', {p, c, ph, s: sa}, `${p} program(s), ${c} recording(s), ${ph} phase map(s), ${sa} setting(s) added`)); - else if (ph) this._showToast(this._t('toast.store_device_downloaded_phases', {p, c, ph}, `${p} program(s), ${c} recording(s), ${ph} phase map(s) added`)); - else this._showToast(this._t('toast.store_device_downloaded', {p, c}, `${p} program(s), ${c} recording(s) added`)); - } catch (e) { this._showToast(this._t('toast.store_download_failed', {error: e.message || e}, 'Download failed: ' + (e.message || e)), 'error'); } + let done; + if (sa) done = this._tText('toast.store_device_downloaded_settings', {p, c, ph, s: sa}, `${p} program(s), ${c} recording(s), ${ph} phase map(s), ${sa} setting(s) added`); + else if (ph) done = this._tText('toast.store_device_downloaded_phases', {p, c, ph}, `${p} program(s), ${c} recording(s), ${ph} phase map(s) added`); + else done = this._tText('toast.store_device_downloaded', {p, c}, `${p} program(s), ${c} recording(s) added`); + const fp = (r && r.partial && r.failed_profiles) || 0; + const partial = fp ? this._tText('toast.store_download_partial', {n: fp}, `${fp} program(s) could not be downloaded; try again later.`) : ''; + this._showToast([done, skipped, partial].filter(Boolean).join('. '), fp ? 'info' : undefined); + } catch (e) { this._showToast(this._tText('toast.store_download_failed', {error: e.message || e}, 'Download failed: ' + (e.message || e)), 'error'); } }); } else if (a === 'store-import') { @@ -13263,13 +13971,13 @@ class HaWashdataPanel extends HTMLElement { } else if (a === 'auto-delete') { const autoId = btn.dataset.autoid, autoName = btn.dataset.autoname || 'this automation'; - this._modal = { type: 'confirm', title: this._t('modal.delete_automation_title', {}, 'Delete Automation'), message: this._t('modal.delete_automation_msg', {name: autoName}, `Delete the automation "${autoName}" from Home Assistant? This cannot be undone.`), okLabel: this._t('btn.delete', {}, 'Delete'), + this._modal = { type: 'confirm', title: this._t('modal.delete_automation_title', {}, 'Delete Automation'), message: this._tText('modal.delete_automation_msg', {name: autoName}, `Delete the automation "${autoName}" from Home Assistant? This cannot be undone.`), okLabel: this._t('btn.delete', {}, 'Delete'), onOk: async () => { try { await this._hass.callApi('DELETE', 'config/automation/config/' + autoId); this._showToast(this._t('toast.automation_deleted', {}, 'Automation deleted')); await this._loadDeviceAutomations(eid); - } catch (e) { this._showToast(this._t('toast.delete_failed', {error: e.message || e}, 'Delete failed: ' + (e.message || e)), 'error'); } + } catch (e) { this._showToast(this._tText('toast.delete_failed', {error: e.message || e}, 'Delete failed: ' + (e.message || e)), 'error'); } } }; this._render(); @@ -13283,16 +13991,9 @@ class HaWashdataPanel extends HTMLElement { await this._ws({ type: `${_DOMAIN}/set_options`, entry_id: eid, options: { notify_actions: [] } }); this._opts = { ...this._opts, notify_actions: [] }; this._showToast(this._t('toast.legacy_removed', {}, 'Legacy actions removed')); - } catch (e) { this._showToast(this._t('toast.remove_failed', {error: e.message || e}, 'Remove failed: ' + (e.message || e)), 'error'); } + } catch (e) { this._showToast(this._tText('toast.remove_failed', {error: e.message || e}, 'Remove failed: ' + (e.message || e)), 'error'); } } }; this._render(); - - } else if (a === 'auto-label') { - const thr = parseFloat(sr.getElementById('wd-auto-label-threshold')?.value || '0.75'); - this._busyRun('auto-label', async () => { - try { await this._ws({ type: `${_DOMAIN}/auto_label_cycles`, entry_id: eid, confidence_threshold: thr }); this._showToast(this._t('toast.auto_label_complete', {}, 'Auto-label complete')); await this._fetchCycles(eid); } - catch (e) { this._showToast(this._t('toast.auto_label_failed', {error: e.message || e}, 'Auto-label failed: ' + (e.message || e)), 'error'); } - }); } } @@ -13301,7 +14002,7 @@ class HaWashdataPanel extends HTMLElement { const eventType = sr.getElementById('wd-maint-type')?.value || ''; const date = sr.getElementById('wd-maint-date')?.value || ''; const notes = (sr.getElementById('wd-maint-notes')?.value || '').trim(); - if (!eventType) { this._showToast(this._t('toast.maint_add_failed', { error: this._t('lbl.event_type', {}, 'Event type') }, 'Could not add event: Event type'), 'error'); return; } + if (!eventType) { this._showToast(this._tText('toast.maint_add_failed', { error: this._t('lbl.event_type', {}, 'Event type') }, 'Could not add event: Event type'), 'error'); return; } this._busyRun('maint-add', async () => { try { const payload = { type: `${_DOMAIN}/add_maintenance_event`, entry_id: eid, event_type: eventType }; @@ -13311,7 +14012,7 @@ class HaWashdataPanel extends HTMLElement { await this._fetchMaintenance(eid); this._showToast(this._t('toast.maint_added', {}, 'Maintenance event added')); this._render(); - } catch (e) { this._showToast(this._t('toast.maint_add_failed', { error: e.message || e }, 'Could not add event: ' + (e.message || e)), 'error'); } + } catch (e) { this._showToast(this._tText('toast.maint_add_failed', { error: e.message || e }, 'Could not add event: ' + (e.message || e)), 'error'); } }); } else if (a === 'maint-delete') { const mid = btn.dataset.mid; @@ -13321,39 +14022,137 @@ class HaWashdataPanel extends HTMLElement { await this._ws({ type: `${_DOMAIN}/delete_maintenance_event`, entry_id: eid, event_id: mid }); await this._fetchMaintenance(eid); this._showToast(this._t('toast.maint_deleted', {}, 'Maintenance event deleted')); - } catch (e) { this._showToast(this._t('toast.maint_delete_failed', { error: e.message || e }, 'Could not delete event: ' + (e.message || e)), 'error'); } + } catch (e) { this._showToast(this._tText('toast.maint_delete_failed', { error: e.message || e }, 'Could not delete event: ' + (e.message || e)), 'error'); } }) }; this._render(); } else if (a === 'maint-save-odometer') { const raw = sr.getElementById('wd-maint-odometer')?.value; const count = parseInt(raw, 10); - if (isNaN(count) || count < 0) { this._showToast(this._t('toast.odometer_save_failed', { error: this._t('lbl.total_cycles_run', {}, 'cycles run in total') }, 'Could not save total: cycles run in total'), 'error'); return; } + if (isNaN(count) || count < 0) { this._showToast(this._tText('toast.odometer_save_failed', { error: this._t('lbl.total_cycles_run', {}, 'cycles run in total') }, 'Could not save total: cycles run in total'), 'error'); return; } this._busyRun('maint-save-odometer', async () => { try { await this._ws({ type: `${_DOMAIN}/set_lifetime_cycle_count`, entry_id: eid, count: count }); await this._fetchMaintenance(eid); this._showToast(this._t('toast.odometer_saved', {}, 'Total cycles saved')); this._render(); - } catch (e) { this._showToast(this._t('toast.odometer_save_failed', { error: e.message || e }, 'Could not save total: ' + (e.message || e)), 'error'); } + } catch (e) { this._showToast(this._tText('toast.odometer_save_failed', { error: e.message || e }, 'Could not save total: ' + (e.message || e)), 'error'); } }); } else if (a === 'maint-save-reminders') { + const mt = this._maintenance || {}; + const saved = mt.reminders || {}; const dict = {}; sr.querySelectorAll('[data-maint-rem]').forEach(el => { const t = el.dataset.maintRem; const n = parseInt(el.value, 10); dict[t] = (!isNaN(n) && n > 0) ? n : 0; }); + // Only a real change goes through set_options (it reloads the entry). + const builtinChanged = Object.keys(dict).some(t => dict[t] !== (parseInt(saved[t], 10) || 0)); + // Custom tasks (#461): rename / interval edits made in place, plus a new task + // typed into the "New task" row and saved without pressing "Add task". + const updates = []; + for (const t of (Array.isArray(mt.custom_tasks) ? mt.custom_tasks : [])) { + const nameEl = sr.querySelector(`[data-mtask-name="${CSS.escape(t.id)}"]`); + if (!nameEl) continue; + const name = nameEl.value.trim(); + if (!name) { this._showToast(this._tText('toast.task_name_required', {}, 'Give the task a name'), 'error'); return; } + const cycles = this._maintInterval(sr.querySelector(`[data-mtask-cycles="${CSS.escape(t.id)}"]`)); + const days = this._maintInterval(sr.querySelector(`[data-mtask-days="${CSS.escape(t.id)}"]`)); + const patch = {}; + if (name !== t.name) patch.name = name; + if (cycles !== (t.cycles || 0)) patch.cycles = cycles; + if (days !== (t.days || 0)) patch.days = days; + if (Object.keys(patch).length) updates.push({ task_id: t.id, ...patch }); + } + // Read before _busyRun: its first render rebuilds the inputs. + const newTask = this._maintNewTaskInput(sr); this._busyRun('maint-save-reminders', async () => { try { - await this._ws({ type: `${_DOMAIN}/set_options`, entry_id: eid, options: { maintenance_reminder_cycles: dict } }); + if (builtinChanged) { + await this._ws({ type: `${_DOMAIN}/set_options`, entry_id: eid, options: { maintenance_reminder_cycles: dict } }); + } + for (const u of updates) { + await this._ws({ type: `${_DOMAIN}/update_maintenance_task`, entry_id: eid, ...u }); + } + if (newTask.name) await this._maintAddTask(eid, newTask); await this._fetchMaintenance(eid); this._showToast(this._t('toast.reminders_saved', {}, 'Service reminders saved')); this._render(); - } catch (e) { this._showToast(this._t('toast.reminders_save_failed', { error: e.message || e }, 'Could not save reminders: ' + (e.message || e)), 'error'); } + } catch (e) { + // A partial save (one task refused) still refreshes what did land. + await this._fetchMaintenance(eid).catch(() => {}); + this._render(); + this._showToast(this._tText('toast.reminders_save_failed', { error: e.message || e }, 'Could not save reminders: ' + (e.message || e)), 'error'); + } + }); + } else if (a === 'maint-task-add') { + const newTask = this._maintNewTaskInput(sr); + if (!newTask.name) { this._showToast(this._tText('toast.task_name_required', {}, 'Give the task a name'), 'error'); return; } + this._busyRun('maint-task-add', async () => { + try { + await this._maintAddTask(eid, newTask); + await this._fetchMaintenance(eid); + this._showToast(this._t('toast.task_added', {}, 'Task added')); + this._render(); + } catch (e) { this._showToast(this._tText('toast.task_save_failed', { error: e.message || e }, 'Could not save task: ' + (e.message || e)), 'error'); } + }); + } else if (a === 'maint-task-remove') { + const tid = btn.dataset.mtask; + const task = ((this._maintenance && this._maintenance.custom_tasks) || []).find(t => t.id === tid); + const name = task ? task.name : tid; + this._modal = { type: 'confirm', title: this._t('modal.remove_task_title', {}, 'Remove Task'), + message: this._tText('modal.remove_task_msg', { task: name }, 'Remove the reminder "{task}"? Its entries in the maintenance log are kept.'), + okLabel: this._t('btn.remove', {}, 'Remove'), + onOk: () => this._busyRun('maint-task-remove', async () => { + try { + await this._ws({ type: `${_DOMAIN}/delete_maintenance_task`, entry_id: eid, task_id: tid }); + await this._fetchMaintenance(eid); + this._showToast(this._t('toast.task_removed', {}, 'Task removed')); + } catch (e) { this._showToast(this._tText('toast.task_save_failed', { error: e.message || e }, 'Could not save task: ' + (e.message || e)), 'error'); } + }) }; + this._render(); + } else if (a === 'maint-log-done') { + // One click per due task (#461): logs it as done now, which restarts its count. + const eventType = btn.dataset.mtype; + if (!eventType) return; + const label = this._maintLabel(eventType); + this._busyRun('maint-log-done', async () => { + try { + await this._ws({ type: `${_DOMAIN}/add_maintenance_event`, entry_id: eid, event_type: eventType }); + await this._fetchMaintenance(eid); + this._showToast(this._tText('toast.maint_logged', { task: label }, 'Logged as done: {task}')); + this._render(); + } catch (e) { this._showToast(this._tText('toast.maint_add_failed', { error: e.message || e }, 'Could not add event: ' + (e.message || e)), 'error'); } }); } } + // A custom-task interval input's value: blank is 0 (off); anything else is sent + // as typed so the backend's single validation rule decides (and names the error). + _maintInterval(el) { + const raw = String((el && el.value) || '').trim(); + if (!raw) return 0; + const n = Number(raw); + return isNaN(n) ? raw : n; + } + + // What the "New task" row holds (#461). + _maintNewTaskInput(sr) { + return { + name: (sr.getElementById('wd-mtask-new-name')?.value || '').trim(), + cycles: this._maintInterval(sr.getElementById('wd-mtask-new-cycles')), + days: this._maintInterval(sr.getElementById('wd-mtask-new-days')), + }; + } + + // Create a custom task from `_maintNewTaskInput` values. + async _maintAddTask(eid, task) { + const payload = { type: `${_DOMAIN}/add_maintenance_task`, entry_id: eid, name: task.name }; + if (task.cycles) payload.cycles = task.cycles; + if (task.days) payload.days = task.days; + return this._ws(payload); + } + _onActPlayground(a, btn, dev, eid) { if (a === 'pg-analysis-tab') { const tab = btn.dataset.subtab || 'history'; @@ -13391,30 +14190,23 @@ class HaWashdataPanel extends HTMLElement { this._pgApplySweepValue(btn.dataset.val); } else if (a === 'pg-run' || a === 'pg-load') { this._pgLoad(); + } else if (a === 'pg-rerun') { + this._pgRerunDetail(); } else if (a === 'pg-cancel-run') { this._pgCancelRun(); } else if (a === 'pg-reset-params') { this._pgThreshStart = null; this._pgThreshStop = null; this._pgParamOverrides = {}; - this._pgStressTail = false; this._pgStressIdleW = null; - this._render(); requestAnimationFrame(() => this._pgDrawCanvas()); + this._render(); requestAnimationFrame(() => this._pgDrawCanvas()); } else if (a === 'pg-apply-settings') { this._pgApplyToSettings(); } else if (a === 'pg-load-live') { this._pgLoadLive(); } else if (a === 'pg-load-suggested') { - this._pgLoadSuggested('classic'); - } else if (a === 'pg-load-calibrated') { - this._pgLoadSuggested('ml'); + this._pgLoadSuggested(); } else if (a === 'pg-preset-save') { this._pgSavePreset(); } else if (a === 'pg-preset-load') { - const preset = (this._pgPresets || []).find(p => p.name === this._pgPresetSel); - if (preset) { - this._pgApplyPresetValues(preset.values); - this._showToast(this._t('toast.pg_preset_loaded', {name: preset.name}, `Preset "${preset.name}" loaded`)); - this._render(); - requestAnimationFrame(() => this._pgDrawCanvas()); - } + this._pgLoadPreset(); } else if (a === 'pg-preset-delete') { this._pgDeletePreset(); } else if (a === 'pg-publish-one') { @@ -13454,7 +14246,7 @@ class HaWashdataPanel extends HTMLElement { await this._ws({ type: `${_DOMAIN}/save_profile_group`, entry_id: eid, name, members }); this._showToast(this._t('toast.group_saved', {}, 'Group saved')); this._modal = null; await this._fetchProfileGroups(eid); - } catch (e) { this._showToast(this._t('msg.toast_save_failed', {error: e.message || e}, 'Save failed: ' + (e.message || e)), 'error'); } + } catch (e) { this._showToast(this._tText('msg.toast_save_failed', {error: e.message || e}, 'Save failed: ' + (e.message || e)), 'error'); } }); return; } @@ -13464,14 +14256,13 @@ class HaWashdataPanel extends HTMLElement { await this._ws({ type: `${_DOMAIN}/delete_profile_group`, entry_id: eid, name: m.orig }); this._showToast(this._t('toast.group_deleted', {}, 'Group deleted')); this._modal = null; await this._fetchProfileGroups(eid); - } catch (e) { this._showToast(this._t('msg.toast_delete_failed', {error: e.message || e}, 'Delete failed: ' + (e.message || e)), 'error'); } + } catch (e) { this._showToast(this._tText('msg.toast_delete_failed', {error: e.message || e}, 'Delete failed: ' + (e.message || e)), 'error'); } }); return; } } - // NB: 'import-ok' (the legacy raw-JSON import modal) is deliberately NOT in this - // list, so the 'import-' names are matched one by one rather than by prefix. + // The 'import-' names are matched one by one rather than by prefix. if (action.startsWith('store-import-') || action === 'store-share-ok' || action.startsWith('wiz-') || action.startsWith('imp-') || action === 'import-back' || action === 'import-analyze' || action === 'import-apply-ok') { @@ -13495,7 +14286,7 @@ class HaWashdataPanel extends HTMLElement { document.body.appendChild(a2); a2.click(); document.body.removeChild(a2); URL.revokeObjectURL(url); this._modal = null; this._showToast(this._t('toast.export_selective_done', {}, 'Export downloaded')); - } catch (e) { this._showToast(this._t('toast.export_failed', {error: e.message || e}, 'Export failed: ' + (e.message || e)), 'error'); } + } catch (e) { this._showToast(this._tText('toast.export_failed', {error: e.message || e}, 'Export failed: ' + (e.message || e)), 'error'); } }); return; } @@ -13531,7 +14322,7 @@ class HaWashdataPanel extends HTMLElement { // Re-fetch feedbacks too: labelling a review cycle now resolves its pending // feedback backend-side (#331), so the "needs review" queue must refresh. try { await this._ws({ type: `${_DOMAIN}/label_cycle`, entry_id: eid, cycle_id: m.cycleId, profile_name: profileName || null, new_profile_name: newName }); this._showToast(this._t('toast.cycle_labelled', {}, 'Cycle labelled')); await this._fetchCycles(eid); await this._fetchProfiles(eid); await this._fetchFeedbacks(eid); } - catch (e) { this._showToast(this._t('toast.label_failed', {error: e.message || e}, 'Label failed: ' + (e.message || e)), 'error'); } + catch (e) { this._showToast(this._tText('toast.label_failed', {error: e.message || e}, 'Label failed: ' + (e.message || e)), 'error'); } this._render(); } else if (action === 'create-profile-ok' && eid) { const name = sr.getElementById('wd-cp-name')?.value?.trim(); @@ -13542,16 +14333,16 @@ class HaWashdataPanel extends HTMLElement { // A reference cycle sets the duration from its own length, so never send a // manual duration alongside one (issue #303 — no silently-ignored field). const manualDur = (!cycle && dur > 0) ? dur : null; - try { await this._ws({ type: `${_DOMAIN}/create_profile`, entry_id: eid, name, reference_cycle: cycle || null, manual_duration_min: manualDur }); this._showToast(this._t('toast.profile_created', {name}, `Profile "${name}" created`)); await this._fetchProfiles(eid); } - catch (e) { this._showToast(this._t('toast.create_failed', {error: e.message || e}, 'Create failed: ' + (e.message || e)), 'error'); } + try { await this._ws({ type: `${_DOMAIN}/create_profile`, entry_id: eid, name, reference_cycle: cycle || null, manual_duration_min: manualDur }); this._showToast(this._tText('toast.profile_created', {name}, `Profile "${name}" created`)); await this._fetchProfiles(eid); } + catch (e) { this._showToast(this._tText('toast.create_failed', {error: e.message || e}, 'Create failed: ' + (e.message || e)), 'error'); } this._render(); } else if (action === 'create-phase-ok' && eid) { const name = sr.getElementById('wd-ph-name')?.value?.trim(); const desc = sr.getElementById('wd-ph-desc')?.value?.trim() || ''; this._modal = null; if (!name) { this._showToast(this._t('toast.phase_name_required', {}, 'Phase name is required'), 'error'); this._render(); return; } - try { await this._ws({ type: `${_DOMAIN}/create_phase`, entry_id: eid, device_type: m.deviceType || '', name, description: desc }); this._showToast(this._t('toast.phase_created', {name}, `Phase "${name}" created`)); await this._fetchPhases(eid); } - catch (e) { this._showToast(this._t('msg.toast_create_failed', {error: e.message || e}, 'Create failed: ' + (e.message || e)), 'error'); } + try { await this._ws({ type: `${_DOMAIN}/create_phase`, entry_id: eid, device_type: m.deviceType || '', name, description: desc }); this._showToast(this._tText('toast.phase_created', {name}, `Phase "${name}" created`)); await this._fetchPhases(eid); } + catch (e) { this._showToast(this._tText('msg.toast_create_failed', {error: e.message || e}, 'Create failed: ' + (e.message || e)), 'error'); } this._render(); } else if (action === 'edit-phase-ok' && eid) { const newName = sr.getElementById('wd-eph-name')?.value?.trim(); @@ -13559,7 +14350,7 @@ class HaWashdataPanel extends HTMLElement { this._modal = null; if (!newName) { this._showToast(this._t('toast.name_required', {}, 'Name required'), 'error'); this._render(); return; } try { await this._ws({ type: `${_DOMAIN}/update_phase`, entry_id: eid, phase_id: m.phaseId, new_name: newName, description: desc }); this._showToast(this._t('toast.phase_updated', {}, 'Phase updated')); await this._fetchPhases(eid); } - catch (e) { this._showToast(this._t('toast.update_failed', {error: e.message || e}, 'Update failed: ' + (e.message || e)), 'error'); } + catch (e) { this._showToast(this._tText('toast.update_failed', {error: e.message || e}, 'Update failed: ' + (e.message || e)), 'error'); } this._render(); } else if (action === 'process-rec-ok' && eid) { const mode = sr.getElementById('wd-pr-mode')?.value; @@ -13570,28 +14361,30 @@ class HaWashdataPanel extends HTMLElement { this._modal = null; if (!profileName) { this._showToast(this._t('msg.toast_profile_name_required', {}, 'Profile name is required'), 'error'); this._render(); return; } try { await this._ws({ type: `${_DOMAIN}/process_recording`, entry_id: eid, profile_name: profileName, save_mode: mode, head_trim: head, tail_trim: tail }); this._showToast(this._t('toast.recording_saved', {}, 'Recording saved to profile')); await this._fetchRecState(eid); await this._fetchProfiles(eid); } - catch (e) { this._showToast(this._t('msg.toast_save_failed', {error: e.message || e}, 'Save failed: ' + (e.message || e)), 'error'); } + catch (e) { this._showToast(this._tText('msg.toast_save_failed', {error: e.message || e}, 'Save failed: ' + (e.message || e)), 'error'); } this._render(); } else if (action === 'correct-fb-ok' && eid) { const corrected = sr.getElementById('wd-fb-profile')?.value; const dur = parseFloat(sr.getElementById('wd-fb-dur')?.value || 0) || null; this._modal = null; try { await this._ws({ type: `${_DOMAIN}/resolve_feedback`, entry_id: eid, cycle_id: m.cycleId, action: 'correct', corrected_profile: corrected, corrected_duration_min: dur }); this._showToast(this._t('toast.correction_submitted', {}, 'Correction submitted')); await this._fetchFeedbacks(eid); } - catch (e) { this._showToast(this._t('msg.toast_error', {error: e.message || e}, 'Error: ' + (e.message || e)), 'error'); } - this._render(); - } else if (action === 'import-ok' && eid) { - const jsonData = sr.getElementById('wd-import-json')?.value; - this._modal = null; - if (!jsonData?.trim()) { this._showToast(this._t('toast.json_required', {}, 'JSON data is required'), 'error'); this._render(); return; } - try { await this._ws({ type: `${_DOMAIN}/import_config`, entry_id: eid, json_data: jsonData }); this._showToast(this._t('toast.import_successful', {}, 'Import successful; integration reloading')); await this._fetchCycles(eid); } - catch (e) { this._showToast(this._t('toast.import_failed', {error: e.message || e}, 'Import failed: ' + (e.message || e)), 'error'); } + catch (e) { this._showToast(this._tText('msg.toast_error', {error: e.message || e}, 'Error: ' + (e.message || e)), 'error'); } this._render(); } else if (action === 'auto-run' && eid) { - const thr = parseFloat(sr.getElementById('wd-al-thr')?.value || '0.75'); + // An empty or unparsable field sends no threshold, and the backend applies + // the device's own Auto-Label Confidence (audit UI-10). + const thr = parseFloat(sr.getElementById('wd-al-thr')?.value || ''); + const req = { type: `${_DOMAIN}/auto_label_cycles`, entry_id: eid }; + if (Number.isFinite(thr)) req.confidence_threshold = Math.min(0.95, Math.max(0.5, thr)); this._modal = null; this._render(); await this._busyRun('auto-label', async () => { - try { await this._ws({ type: `${_DOMAIN}/auto_label_cycles`, entry_id: eid, confidence_threshold: thr }); this._showToast(this._t('msg.toast_auto_label_complete', {}, 'Auto-label complete')); await this._fetchCycles(eid); } - catch (e) { this._showToast(this._t('msg.toast_auto_label_failed', {error: e.message || e}, 'Auto-label failed: ' + (e.message || e)), 'error'); } + try { + const st = await this._ws(req); + const snap = st && st.task_id ? await this._awaitTask(st.task_id) : null; + this._showToast(this._autoLabelToast(snap)); + await this._fetchCycles(eid); + } + catch (e) { this._showToast(this._tText('msg.toast_auto_label_failed', {error: e.message || e}, 'Auto-label failed: ' + (e.message || e)), 'error'); } }); } else if (action === 'merge-ok' && eid) { const target = sr.getElementById('wd-merge-prof')?.value || ''; @@ -13618,11 +14411,11 @@ class HaWashdataPanel extends HTMLElement { await this._busyRun('cyc-relabel', async () => { try { for (const cid of ids) await this._ws({ type: `${_DOMAIN}/label_cycle`, entry_id: eid, cycle_id: cid, profile_name: profileName || null }); - this._showToast(this._t('toast.relabel_done', { count: ids.length }, `Relabelled ${ids.length} cycle(s)`)); + this._showToast(this._tText('toast.relabel_done', { count: ids.length }, `Relabelled ${ids.length} cycle(s)`)); this._cycleSel.clear(); this._selectMode = false; // Bulk relabel resolves any pending feedback on those cycles (#331). await this._fetchCycles(eid); await this._fetchProfiles(eid); await this._fetchFeedbacks(eid); - } catch (e) { this._showToast(this._t('toast.relabel_failed', { error: e.message || e }, 'Relabel failed: ' + (e.message || e)), 'error'); } + } catch (e) { this._showToast(this._tText('toast.relabel_failed', { error: e.message || e }, 'Relabel failed: ' + (e.message || e)), 'error'); } }); } } @@ -13722,7 +14515,7 @@ class HaWashdataPanel extends HTMLElement { if (task.id === m.applyTaskId) { if (task.state === 'error') { m.error = task.error === 'scan_expired' - ? this._t('msg.hist_scan_expired', {}, 'That scan is no longer available. Please scan again.') + ? this._t('msg.hist_scan_expired', {}, 'That scan is no longer available. Scan again.') : (task.error || this._t('msg.hist_import_failed', {}, 'Import failed.')); m.step = 'review'; this._render(); @@ -13834,7 +14627,7 @@ class HaWashdataPanel extends HTMLElement { if (action === 'hist-goto-cycles') { this._modal = null; - this._tab = 'history'; + this._tab = 'history'; this._historySub = 'cycles'; this._cycleFilter = { ...(this._cycleFilter || {}), status: 'imported' }; this._fetchTabData(); return; @@ -13860,11 +14653,13 @@ class HaWashdataPanel extends HTMLElement { await this._busyRun('store-import', async () => { try { const r = await this._ws(msg); - if (r && r.error) { this._showToast(this._t('toast.store_import_failed', {error: r.error}, 'Import failed: ' + r.error), 'error'); return; } + if (r && r.error === 'low_quality') { this._showToast(this._t('toast.store_import_low_quality', {}, 'This recording is too short, too gappy or implausible to import.'), 'error'); return; } + if (r && r.error === 'duplicate') { this._showToast(this._t('toast.store_import_duplicate', {}, 'This recording is already on your device.'), 'info'); return; } + if (r && r.error) { this._showToast(this._tText('toast.store_import_failed', {error: r.error}, 'Import failed: ' + r.error), 'error'); return; } this._modal = null; - this._showToast(this._t('toast.store_imported', {profile: (r && r.profile) || ''}, `Imported into ${(r && r.profile) || 'profile'}`)); + this._showToast(this._tText('toast.store_imported', {profile: (r && r.profile) || ''}, `Imported into ${(r && r.profile) || 'profile'}`)); await this._fetchProfiles(eid); - } catch (e) { this._showToast(this._t('toast.store_import_failed', {error: e.message || e}, 'Import failed: ' + (e.message || e)), 'error'); } + } catch (e) { this._showToast(this._tText('toast.store_import_failed', {error: e.message || e}, 'Import failed: ' + (e.message || e)), 'error'); } }); return; } @@ -13881,12 +14676,12 @@ class HaWashdataPanel extends HTMLElement { const r = await this._ws({ type: `${_DOMAIN}/store_upload_cycle`, entry_id: eid, local_cycle_id: m.cycleId, program, description }); if (r && r.error) { if (r.error === 'no_appliance_declared') this._showToast(this._t('toast.store_no_appliance', {}, 'Set your appliance brand and model in Settings first.'), 'error'); - else { const why = r.detail ? `${r.error} - ${r.detail}` : r.error; this._showToast(this._t('toast.store_share_failed', {error: why}, 'Share failed: ' + why), 'error'); } + else { const why = r.detail ? `${r.error} - ${r.detail}` : r.error; this._showToast(this._tText('toast.store_share_failed', {error: why}, 'Share failed: ' + why), 'error'); } return; } this._modal = null; this._showToast(this._t('toast.store_shared', {}, 'Shared to the community store - pending review.')); - } catch (e) { this._showToast(this._t('toast.store_share_failed', {error: e.message || e}, 'Share failed: ' + (e.message || e)), 'error'); } + } catch (e) { this._showToast(this._tText('toast.store_share_failed', {error: e.message || e}, 'Share failed: ' + (e.message || e)), 'error'); } }); return; } @@ -13905,9 +14700,7 @@ class HaWashdataPanel extends HTMLElement { if (importableOnly && cats[cid].importable === false) return; if (this._wizCatState(m, cid, man).state !== 'all') allSel = false; }); - m.sel = allSel - ? { cats: new Set(), profiles: new Set(), realIds: new Set(), refIds: new Set() } - : this._wizInitSel(man, importableOnly); + m.sel = allSel ? this._wizEmptySel() : this._wizInitSel(man, importableOnly); this._render(); return; } @@ -13918,10 +14711,10 @@ class HaWashdataPanel extends HTMLElement { if (cid === 'profiles') { const items = (man.categories.profiles && man.categories.profiles.items) || []; m.sel.profiles = new Set(turnOn ? items.map(i => i.name) : []); - } else if (cid === 'real_cycles' || cid === 'reference_cycles') { - const set = new Set(); + } else if (this._wizCycleCats().includes(cid)) { + const set = this._wizIdSet(m.sel, cid); + set.clear(); if (turnOn) ((man.categories[cid] && man.categories[cid].groups) || []).forEach(g => g.cycles.forEach(cy => { if (cy.id != null) set.add(String(cy.id)); })); - if (cid === 'real_cycles') m.sel.realIds = set; else m.sel.refIds = set; } else if (turnOn) { m.sel.cats.add(cid); } else { m.sel.cats.delete(cid); } this._render(); return; @@ -13934,7 +14727,7 @@ class HaWashdataPanel extends HTMLElement { } if (action === 'wiz-toggle-cycgroup') { const cid = btn.dataset.cat; const prof = btn.dataset.prof; - const set = cid === 'real_cycles' ? m.sel.realIds : m.sel.refIds; + const set = this._wizIdSet(m.sel, cid); const ids = this._wizGroupIds(man, cid, prof); const all = ids.length > 0 && ids.every(id => set.has(id)); ids.forEach(id => { if (all) set.delete(id); else set.add(id); }); @@ -13943,7 +14736,7 @@ class HaWashdataPanel extends HTMLElement { } if (action === 'wiz-toggle-cyc') { const cid = btn.dataset.cat; const id = btn.dataset.cid; - const set = cid === 'real_cycles' ? m.sel.realIds : m.sel.refIds; + const set = this._wizIdSet(m.sel, cid); if (set.has(id)) set.delete(id); else set.add(id); this._render(); return; @@ -14002,13 +14795,22 @@ class HaWashdataPanel extends HTMLElement { conflict_resolutions: m.conflicts, cycle_destination: m.cycleDest, apply_settings: true }); const s = (r && r.summary) || {}; this._modal = null; - this._showToast(this._t('toast.import_selective_done', { - profiles: s.profiles_imported || 0, - cycles: (s.real_cycles_imported || 0) + (s.reference_cycles_imported || 0), - }, `Imported ${s.profiles_imported || 0} profile(s) and ${(s.real_cycles_imported || 0) + (s.reference_cycles_imported || 0)} cycle(s)`)); + const cycles = (s.real_cycles_imported || 0) + (s.reference_cycles_imported || 0) + (s.backfill_cycles_imported || 0); + // A replace (or an overwrite) saves a restore point first; say so when it + // could not, since the undo the hint promised will not be there. + const destructive = m.mode === 'replace' || Object.values(m.conflicts || {}).includes('overwrite'); + if (destructive && s.restore_point_saved === false) { + this._showToast(this._tText('toast.import_no_restore_point', {}, 'Imported, but no restore point could be saved, so this import cannot be undone'), 'error'); + } else { + this._showToast(this._tText('toast.import_selective_done', { + profiles: s.profiles_imported || 0, cycles, + }, `Imported ${s.profiles_imported || 0} profile(s) and ${cycles} cycle(s)`)); + } await this._fetchCycles(eid); await this._fetchProfiles(eid); - } catch (e) { this._showToast(this._t('toast.import_failed', {error: e.message || e}, 'Import failed: ' + (e.message || e)), 'error'); } + // Show (or refresh) "Undo last import" in the Export / Import card. + if (s.restore_point_saved) await this._fetchToolsData(eid); + } catch (e) { this._showToast(this._tText('toast.import_failed', {error: e.message || e}, 'Import failed: ' + (e.message || e)), 'error'); } }); return; } @@ -14062,7 +14864,7 @@ class HaWashdataPanel extends HTMLElement { // Pre-flight gate error (not connected / no appliance): keep the modal open. if (r && r.error) { if (r.error === 'no_appliance_declared') this._showToast(this._t('toast.store_no_appliance', {}, 'Set your appliance brand and model in Settings first.'), 'error'); - else { const why = r.detail ? `${r.error} - ${r.detail}` : r.error; this._showToast(this._t('toast.store_share_failed', {error: why}, 'Share failed: ' + why), 'error'); } + else { const why = r.detail ? `${r.error} - ${r.detail}` : r.error; this._showToast(this._tText('toast.store_share_failed', {error: why}, 'Share failed: ' + why), 'error'); } return; } const n = (r && r.cycle_ids && r.cycle_ids.length) || 0; @@ -14072,15 +14874,15 @@ class HaWashdataPanel extends HTMLElement { if (!n) { // Nothing uploaded: surface the first error and keep the modal for retry. const why = (r && r.errors && r.errors[0]) || (r && r.detail) || 'upload_failed'; - this._showToast(this._t('toast.store_share_failed', {error: why}, 'Share failed: ' + why), 'error'); + this._showToast(this._tText('toast.store_share_failed', {error: why}, 'Share failed: ' + why), 'error'); return; } this._modal = null; - if (failed) this._showToast(this._t('toast.store_device_shared_partial', {n, failed}, `Shared ${n} cycle(s); ${failed} could not be uploaded.`), 'info'); - else if (dup && !created) this._showToast(this._t('toast.store_device_shared_all_dup', {n: dup}, `All ${dup} cycle(s) were already in the community store.`), 'info'); - else if (dup) this._showToast(this._t('toast.store_device_shared_some_dup', {created, dup}, `Shared ${created} cycle(s); ${dup} were already in the store.`)); - else this._showToast(this._t('toast.store_device_shared', {n: created}, `Shared ${created} cycle(s) to the community store - pending review.`)); - } catch (e) { this._showToast(this._t('toast.store_share_failed', {error: e.message || e}, 'Share failed: ' + (e.message || e)), 'error'); } + if (failed) this._showToast(this._tText('toast.store_device_shared_partial', {n, failed}, `Shared ${n} cycle(s); ${failed} could not be uploaded.`), 'info'); + else if (dup && !created) this._showToast(this._tText('toast.store_device_shared_all_dup', {n: dup}, `All ${dup} cycle(s) were already in the community store.`), 'info'); + else if (dup) this._showToast(this._tText('toast.store_device_shared_some_dup', {created, dup}, `Shared ${created} cycle(s); ${dup} were already in the store.`)); + else this._showToast(this._tText('toast.store_device_shared', {n: created}, `Shared ${created} cycle(s) to the community store - pending review.`)); + } catch (e) { this._showToast(this._tText('toast.store_share_failed', {error: e.message || e}, 'Share failed: ' + (e.message || e)), 'error'); } }); return; } @@ -14094,6 +14896,19 @@ class HaWashdataPanel extends HTMLElement { if (action === 'cyc-trim') { m.mode = 'trim'; if (!m.trim || m.trim.end <= 0) m.trim = { start: 0, end: (m.curve && m.curve.full_duration_s) || 0 }; this._render(); return; } if (action === 'cyc-split') { m.mode = 'split'; this._render(); return; } if (action === 'cyc-review') { m.mode = 'review'; this._render(); return; } + if (action === 'cyc-ctx') { + // Recorder-history length (item 513). The select fires on click too, so an + // unchanged value is a no-op; a new one is remembered for this device. + const v = Number(sr.getElementById('wd-cyc-ctx')?.value); + const ceid = m.entryId || eid; + if (!_CYCLE_CONTEXT_CHOICES.includes(v) || (m.ctx && m.ctx.minutes === v)) return; + const map = this._pref('cycle_context_min', null); + this._setPref('cycle_context_min', { ...(map && typeof map === 'object' ? map : {}), [ceid]: v }); + delete this._canvasZoom['wd-cyc-canvas']; // a viewport inside the old lead-in + this._loadCycleContext(ceid, m.cycleId, m.curve); + this._render(); + return; + } if (action === 'cyc-review-save') { const cid = m.cycleId; const quality = sr.getElementById('wd-cyc-rev-quality')?.value || ''; @@ -14114,8 +14929,20 @@ class HaWashdataPanel extends HTMLElement { // (#331), so refresh the queue rather than leaving a stale entry. if (newLabel !== curLabel) await this._fetchFeedbacks(eid); await this._loadMlIndex(eid); - if (this._modal && this._modal.cycleId === cid) this._modal.ml = (this._mlById || {})[cid] || this._modal.ml; - } catch (e) { this._showToast(this._t('msg.toast_save_failed', {error: e.message || e}, 'Save failed: ' + (e.message || e)), 'error'); } + const open = this._modal; + if (open && open.type === 'cycle-detail' && open.cycleId === cid) { + open.ml = (this._mlById || {})[cid] || open.ml; + // The open dialog showed the label it was opened with, so a relabel + // looked unsaved and saving back to that label was skipped (#469). + delete open.reviewLabel; + if (newLabel !== curLabel && open.curve) { + open.curve.profile_name = newLabel || null; + open.profileEnv = null; + open.runnerUpEnv = null; + this._loadCycleOverlays(eid, cid, open.curve); + } + } + } catch (e) { this._showToast(this._tText('msg.toast_save_failed', {error: e.message || e}, 'Save failed: ' + (e.message || e)), 'error'); } }); return; } @@ -14135,7 +14962,7 @@ class HaWashdataPanel extends HTMLElement { const gap = parseInt(sr.getElementById('wd-split-gap')?.value || '900', 10); await this._busyRun('cyc-auto', async () => { try { const r = await this._ws({ type: `${_DOMAIN}/analyze_split`, entry_id: eid, cycle_id: m.cycleId, gap_seconds: gap }); m.split.offsets = (r.split_offsets || []).slice(); m.split.profiles = []; if (!m.split.offsets.length) this._showToast(this._t('toast.no_split_found', {}, 'No idle gaps found to split on'), 'info'); } - catch (e) { this._showToast(this._t('toast.auto_detect_failed', {error: e.message || e}, 'Auto-detect failed: ' + (e.message || e)), 'error'); } + catch (e) { this._showToast(this._tText('toast.auto_detect_failed', {error: e.message || e}, 'Auto-detect failed: ' + (e.message || e)), 'error'); } }); return; } @@ -14148,7 +14975,7 @@ class HaWashdataPanel extends HTMLElement { // a single click (#373). const full = (m.curve && m.curve.full_duration_s) || 0; const keptPct = full > 0 ? Math.max(0, Math.round(((e2 - s) / full) * 100)) : 100; - if (keptPct < 50 && !confirm(this._t('msg.trim_destructive_confirm', {pct: keptPct}, `This keeps only ${keptPct}% of the cycle and cannot be undone. Continue?`))) return; + if (keptPct < 50 && !confirm(this._tText('msg.trim_destructive_confirm', {pct: keptPct}, `This keeps only ${keptPct}% of the cycle and cannot be undone. Continue?`))) return; this._kickAndTrack( { type: `${_DOMAIN}/trim_cycle`, entry_id: eid, cycle_id: cid, start_s: s, end_s: e2 }, 'cyc-trim-apply', @@ -14168,7 +14995,7 @@ class HaWashdataPanel extends HTMLElement { { type: `${_DOMAIN}/apply_split`, entry_id: eid, cycle_id: cid, split_offsets: offs, segment_profiles: profs }, 'cyc-split-apply', async (result) => { - this._showToast(this._t('toast.split_complete', {count: (result.new_ids || []).length}, `Split into ${(result.new_ids || []).length} cycles`)); + this._showToast(this._tText('toast.split_complete', {count: (result.new_ids || []).length}, `Split into ${(result.new_ids || []).length} cycles`)); await this._closeCycleDetail(eid); await this._fetchCycles(eid); await this._fetchProfiles(eid); @@ -14193,7 +15020,7 @@ class HaWashdataPanel extends HTMLElement { const phases = m.phases.filter(p => p.name).map(p => ({ name: p.name, start: p.start, end: p.end })); await this._busyRun('pp-phase-save', async () => { try { await this._ws({ type: `${_DOMAIN}/set_profile_phases`, entry_id: eid, profile_name: m.name, phases }); this._showToast(this._t('toast.phases_saved', {}, 'Phases saved')); } - catch (e) { this._showToast(this._t('msg.toast_save_failed', {error: e.message || e}, 'Save failed: ' + (e.message || e)), 'error'); } + catch (e) { this._showToast(this._tText('msg.toast_save_failed', {error: e.message || e}, 'Save failed: ' + (e.message || e)), 'error'); } }); return; } @@ -14203,11 +15030,11 @@ class HaWashdataPanel extends HTMLElement { await this._busyRun('pp-cleanup-del', async () => { try { for (const cid of sel) await this._ws({ type: `${_DOMAIN}/delete_cycle`, entry_id: eid, cycle_id: cid }); - this._showToast(this._t('toast.cycles_deleted', {count: sel.length}, `Deleted ${sel.length} cycle(s)`)); + this._showToast(this._tText('toast.cycles_deleted', {count: sel.length}, `Deleted ${sel.length} cycle(s)`)); const r = await this._ws({ type: `${_DOMAIN}/get_profile_cycles`, entry_id: eid, profile_name: m.name }); if (this._modal) this._modal.cleanup = { cycles: r.cycles || [], selected: new Set() }; await this._fetchProfiles(eid); - } catch (e) { this._showToast(this._t('msg.toast_delete_failed', {error: e.message || e}, 'Delete failed: ' + (e.message || e)), 'error'); } + } catch (e) { this._showToast(this._tText('msg.toast_delete_failed', {error: e.message || e}, 'Delete failed: ' + (e.message || e)), 'error'); } }); return; } @@ -14221,7 +15048,7 @@ class HaWashdataPanel extends HTMLElement { await this._busyRun('pp-cleanup-unlabel', async () => { try { for (const cid of sel) await this._ws({ type: `${_DOMAIN}/label_cycle`, entry_id: eid, cycle_id: cid, profile_name: null }); - this._showToast(this._t('toast.unlabel_done', {count: sel.length}, `Unlabelled ${sel.length} cycle(s)`)); + this._showToast(this._tText('toast.unlabel_done', {count: sel.length}, `Unlabelled ${sel.length} cycle(s)`)); const r = await this._ws({ type: `${_DOMAIN}/get_profile_cycles`, entry_id: eid, profile_name: m.name }); // Only adopt the fresh list if this very modal is still the open one; the // user may have closed it or moved to another profile while we ran. @@ -14229,16 +15056,20 @@ class HaWashdataPanel extends HTMLElement { await this._fetchProfiles(eid); await this._fetchCycles(eid); await this._fetchFeedbacks(eid); - } catch (e) { this._showToast(this._t('toast.unlabel_failed', {error: e.message || e}, 'Unlabel failed: ' + (e.message || e)), 'error'); } + } catch (e) { this._showToast(this._tText('toast.unlabel_failed', {error: e.message || e}, 'Unlabel failed: ' + (e.message || e)), 'error'); } }); return; } if (action === 'pp-rename') { const nn = sr.getElementById('wd-pp-rename')?.value?.trim(); - const dur = parseFloat(sr.getElementById('wd-pp-dur')?.value || '0'); + const durEl = sr.getElementById('wd-pp-dur'); + const dur = parseFloat(durEl?.value || '0'); + // Only an edited value is sent (#158): resending the rounded minutes on a + // plain rename overwrote the stored duration, and a computed one is read-only. + const durEdited = !!durEl && !durEl.disabled && dur > 0 && dur !== parseFloat(durEl.dataset.orig || '0'); if (!nn) { this._showToast(this._t('msg.toast_name_required', {}, 'Name required'), 'error'); return; } try { - await this._ws({ type: `${_DOMAIN}/rename_profile`, entry_id: eid, profile_name: m.name, new_name: nn, manual_duration_min: dur > 0 ? dur : null }); + await this._ws({ type: `${_DOMAIN}/rename_profile`, entry_id: eid, profile_name: m.name, new_name: nn, manual_duration_min: durEdited ? dur : null }); this._showToast(this._t('toast.profile_renamed', {}, 'Profile renamed')); m.name = nn; // rename_profile also rewrites the member name inside any profile group // (profile_store.update_profile step 4); re-fetch groups too or the stale @@ -14246,7 +15077,7 @@ class HaWashdataPanel extends HTMLElement { // resaving that stale modal would drop it for real. await Promise.all([this._fetchProfiles(eid), this._fetchProfileGroups(eid)]); m.stats = (this._profiles || []).find(p => p.name === nn) || m.stats; this._render(); - } catch (e) { this._showToast(this._t('toast.rename_failed', {error: e.message || e}, 'Rename failed: ' + (e.message || e)), 'error'); } + } catch (e) { this._showToast(this._tText('toast.rename_failed', {error: e.message || e}, 'Rename failed: ' + (e.message || e)), 'error'); } return; } if (action === 'pp-rebuild') { @@ -14448,7 +15279,7 @@ class HaWashdataPanel extends HTMLElement { if (!notes || !notes.length) { div.hidden = true; div.innerHTML = ''; return; } div.hidden = false; div.innerHTML = notes - .map(n => _esc(this._t(n.msgKey, n.msgVars, n.msgFb))) + .map(n => _esc(this._tText(n.msgKey, n.msgVars, n.msgFb))) .join('
    '); }); @@ -14465,7 +15296,7 @@ class HaWashdataPanel extends HTMLElement { div.hidden = false; if (fieldEl) fieldEl.classList.add('wd-has-conflict'); div.innerHTML = errs.map(e => { - const msg = this._t(e.msgKey, e.msgVars, e.msgFb); + const msg = this._tText(e.msgKey, e.msgVars, e.msgFb); let fixHtml = ''; if (e.suggFix != null) { const displaySug = +e.suggFix.toFixed(2); @@ -14522,7 +15353,7 @@ class HaWashdataPanel extends HTMLElement { this._snapshotFormToPending(sr); if (autoChanged.size > 0) { const n = autoChanged.size, s = n > 1 ? 's' : ''; - this._showToast(this._t('conflict.cascade_toast', {n}, `Other settings adjusted for consistency: ${n}`), 'success'); + this._showToast(this._tText('conflict.cascade_toast', {n}, `Other settings adjusted for consistency: ${n}`), 'success'); } } @@ -14548,6 +15379,13 @@ class HaWashdataPanel extends HTMLElement { // A blank/cleared value for a key that was never stored is a no-op: there is // nothing to clear, and writing it would pin an empty string into options. if (!(k in baseline) && (val === '' || val === null)) continue; + // A never-stored switch rendered at its default (energy_price_dynamic, + // notify_fire_events) is not an edit either: submitting it rewrote options the + // user never touched. Lists keep their own normalisation (evidence sources). + if (!(k in baseline) && typeof val === 'boolean') { + const d = (this._optDefaults && k in this._optDefaults) ? this._optDefaults[k] : (_FIELD_BY_KEY[k] || {}).def; + if (d !== undefined && JSON.stringify(val) === JSON.stringify(d)) continue; + } changed[k] = val; } return changed; @@ -14618,7 +15456,7 @@ class HaWashdataPanel extends HTMLElement { } if (this._invalidJson) { - this._showToast(this._t('toast.invalid_json', {key: this._invalidJson}, `"${this._invalidJson}" is not valid JSON - fix it or clear the field before saving.`), 'error'); + this._showToast(this._tText('toast.invalid_json', {key: this._invalidJson}, `"${this._invalidJson}" is not valid JSON - fix it or clear the field before saving.`), 'error'); return; } const conflicts = this._liveValidateSettings(sr); @@ -14641,7 +15479,7 @@ class HaWashdataPanel extends HTMLElement { await this._ws({ type: `${_DOMAIN}/set_options`, entry_id: dev.entry_id, options: safe }); this._opts = { ...this._opts, ...safe }; this._showToast(this._t('toast.saved_except_conflicts', {}, 'Saved. Fix the highlighted conflicts to save the rest.'), 'info'); - } catch (e) { this._showToast(this._t('msg.toast_save_failed', {error: e.message || e}, 'Save failed: ' + (e.message || e)), 'error'); } + } catch (e) { this._showToast(this._tText('msg.toast_save_failed', {error: e.message || e}, 'Save failed: ' + (e.message || e)), 'error'); } }); } else { this._showToast(this._t('toast.settings_conflicts', {}, 'Fix the highlighted setting conflicts before saving.'), 'error'); @@ -14680,7 +15518,7 @@ class HaWashdataPanel extends HTMLElement { this._stagedSuggestions = false; this._suggestions = []; } this._showToast(this._t('toast.settings_saved', {}, 'Settings saved; integration reloading')); - } catch (e) { this._showToast(this._t('msg.toast_save_failed', {error: e.message || e}, 'Save failed: ' + (e.message || e)), 'error'); } + } catch (e) { this._showToast(this._tText('msg.toast_save_failed', {error: e.message || e}, 'Save failed: ' + (e.message || e)), 'error'); } }); } } diff --git a/custom_components/ha_washdata/www/ha-washdata-panel.min.js b/custom_components/ha_washdata/www/ha-washdata-panel.min.js index c60c1003..5946796e 100644 --- a/custom_components/ha_washdata/www/ha-washdata-panel.min.js +++ b/custom_components/ha_washdata/www/ha-washdata-panel.min.js @@ -8,7 +8,7 @@ * This program comes with ABSOLUTELY NO WARRANTY. See the GNU Affero General * Public License for details: . */ -const y="ha_washdata",Zt=(()=>{try{return new URL(import.meta.url).searchParams.get("v")||""}catch{return""}})(),ge=2e4,ue=6e3,Wt=25,zt=["running","starting","paused","user_paused","ending","anti_wrinkle","rinse"],fe=[{key:"show_contributor",labelKey:"lbl.show_contributor",labelFb:"Show contributor names",docKey:"setting.show_contributor.doc",docFb:'Show the "by " attribution on community appliances and reference cycles.'}],jt=34,Qt=.5,me=200,ye=5,Lt=22,we=320,it=["#e6194B","#3cb44b","#4363d8","#f58231","#911eb4","#42d4f4","#f032e6","#bfef45","#fabed4","#469990","#dcbeff","#9A6324","#800000","#aaffc3","#808000","#ffd8b1","#000075","#a9a9a9"],Ct="{device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}",$t=[{id:"basic",label:"Basic",intro:"Core identity and the essentials most setups need.",groups:[{sub:"Device info",fields:[{key:"name",label:"Device Name",type:"text",doc:"Display name shown in the HA integrations list and device registry."},{key:"device_type",label:"Device Type",type:"devicetype",doc:"Appliance class. Sets sensible detection defaults (thresholds, off-delay, end handling) tuned for that appliance type; change it only if the device was originally set up as the wrong type."},{key:"store_brand",label:"Appliance Brand",type:"storebrand",optional:!0,doc:"Optional. The appliance brand, picked from the community catalog. Used to find and share matching reference recordings. Leave blank if you are not using online features."},{key:"store_model",label:"Appliance Model",type:"storemodel",optional:!0,doc:"Optional. The appliance model, picked from the community catalog once a brand is set. If your model is not listed you can add it to the catalog."}]},{sub:"Basic configuration",fields:[{key:"power_sensor",label:"Power Sensor",type:"entity",domain:"sensor",doc:"The sensor entity reporting live power in watts for this appliance (e.g. sensor.washer_power). All cycle detection is based on this signal."},{key:"min_power",label:"Minimum Power",unit:"W",type:"number",step:.1,min:0,def:2,basic:!0,doc:"Absolute minimum power considered active. Readings below this are treated as 0 W (standby), filtering out the phantom load of smart plugs and standby LEDs."},{key:"off_delay",label:"Off Delay",unit:"s",type:"number",min:0,def:180,basic:!0,doc:"Time to wait after power drops before declaring the cycle finished. If power resumes within this window the cycle continues seamlessly - this bridges pauses between wash stages. Dishwashers have long drying phases (power off for 20-60 min) so the off-delay must exceed that to keep the whole wash+dry as one cycle."},{key:"linked_device",label:"Group Under Device",type:"device",doc:'Optionally nest this WashData device under another device (e.g. the smart plug) in the HA device registry, shown as "Connected via ...".'}]}]},{id:"detection",label:"Detection",intro:"How a cycle is detected as starting, running and finishing.",groups:[{sub:"Thresholds & Gap",fields:[{key:"start_threshold_w",label:"Start Threshold",unit:"W",type:"number",step:1,min:0,basic:!0,doc:"Power must rise above this level to confirm a cycle has started. Setting it too low causes false starts from standby power; too high and slow-starting programs (cold fill) are missed. The suggestion engine sets this just above the machine's observed lowest active power."},{key:"stop_threshold_w",label:"Stop Threshold",unit:"W",type:"number",step:.1,min:0,basic:!0,doc:"Power must fall below this level before the off-delay countdown begins. Set it below the Start Threshold - the gap between them is the hysteresis band that prevents flicker. If set too high, low-power phases (rinse holds, anti-crease) falsely trigger the end sequence."},{key:"min_off_gap",label:"Min Off Gap",unit:"s",type:"number",min:0,basic:!0,doc:"If the machine powers off for less than this time, the on/off/on sequence is treated as one continuous cycle. Prevents soak programs (machine powers off for several minutes mid-wash) from being split into two separate cycles. Set it shorter than the gap between your back-to-back loads if you want those counted as separate cycles. Device-type defaults protect the typical intra-cycle pause for each appliance."}]},{sub:"Cycle Start",fields:[{key:"start_duration_threshold",label:"Start Duration",unit:"s",type:"number",min:0,def:5,doc:"Power must stay above the start threshold this long to confirm a real start, preventing split-second on/off toggles from starting a cycle."},{key:"start_energy_threshold",label:"Start Energy",unit:"Wh",type:"number",step:.01,min:0,def:.2,doc:"Energy (power x time) the appliance must consume before RUNNING. A brief high-power spike has very low energy and is ignored, preventing false starts."},{key:"completion_min_seconds",label:"Min Cycle Duration",unit:"s",type:"number",min:0,def:600,basic:!0,doc:"Cycles shorter than this are discarded as ghost cycles (test runs, opening the door to add a sock)."},{key:"curve_preroll_seconds",label:"Curve Pre-roll",unit:"s",type:"number",step:10,min:0,max:600,def:0,doc:"Seconds of readings from aborted start attempts that may be carried into the front of a cycle's curve. Machines that probe before settling (programme selection, door lock, first fill) can drop the first minutes of real activity from every curve. 0 turns this off. Note that enabling it moves the recorded start earlier, so cycles recorded before and after the change carry different durations for the same program until the older ones age out - expect the learned averages to drift for a while."}]},{sub:"Cycle End",fields:[{key:"end_energy_threshold",label:"End Energy",unit:"Wh",type:"number",step:.001,min:0,def:.05,doc:"During the off-delay countdown, accumulated energy (watts x time) is compared to this threshold. If exceeded, the countdown resets - keeping anti-crease tumbles and dishwasher drying tails attached to the cycle instead of cutting them short. Raise it if cycles end too early during cool-down; lower it if detection is sluggish."},{key:"end_repeat_count",label:"End Repeat Count",type:"number",min:1,def:1,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."},{key:"smart_termination_duration_ratio",label:"Smart Termination Ratio",type:"number",step:.01,min:.5,max:1,doc:"How far into the matched program's expected duration a cycle must be before Smart Termination may end it early once power drops. The expected duration is the program's average, so on appliances whose runtime varies a lot - washers on cold winter vs warm summer inlet water, sensor-dry dryers, load-dependent programs - about half of all runs finish shorter than that average and never get the fast finish, ending only via the fallback timeout minutes late. Lower this (e.g. 0.85) on those machines so the early finish still fires; raise it toward 1.0 to be more conservative. Leave empty for the default (0.98, or 0.99 for dishwashers). It can only ever end a cycle earlier, never later, and never fires on an ambiguous or low-confidence match."}]},{sub:"Power Off",fields:[{key:"power_off_threshold_w",label:"Power Off Threshold",unit:"W",type:"number",step:.1,min:0,def:0,doc:"Optional power-based Off detection. When above 0, once a cycle has finished and power stays below this level for the Power Off Delay, the machine is treated as switched off and the state returns to Off. Leave at 0 to disable (the default). Set it above the true switched-off floor and below the Stop Threshold and your machine's finished-but-on standby draw; if it is not below the Stop Threshold it is ignored. When enabled it replaces the Progress Reset Delay for returning to Off, so a finished machine stays in Finished/Clean until it is actually powered off."},{key:"power_off_delay",label:"Power Off Delay",unit:"s",type:"number",min:0,def:30,doc:"How long power must stay below the Power Off Threshold after a cycle finishes before the state returns to Off. Only used when the Power Off Threshold is above 0. Checked on the background cadence, so the effective delay rounds up to the next state-expiry tick."}]},{sub:"Signal Processing",fields:[{key:"sampling_interval",label:"Sampling Interval",unit:"s",type:"number",min:1,def:30,doc:"Expected time between sensor readings - used to size the smoothing window and start debounce correctly. Every sensor update is captured regardless of this value; it only calibrates the downstream calculations. The suggestion engine measures your sensor's actual cadence from past cycles and sets this automatically."},{key:"smoothing_window",label:"Smoothing Window",type:"number",min:1,def:2,doc:"How much the raw power signal is smoothed. Low (2) is responsive but noisy; high (5) smooths spikes but adds lag."}]}]},{id:"matching",label:"Matching",intro:"How finished cycles are matched to learned profiles and labelled.",notDeviceTypes:["other"],groups:[{sub:"Match Scoring",fields:[{key:"profile_match_threshold",label:"Match Threshold",type:"number",step:.01,min:0,max:1,def:.4,doc:"Minimum similarity score (0-1) required at cycle end to accept a program identification. Raise it to reduce wrong identifications; lower it if your machine's programs are not being matched. Default 0.4 is a conservative starting point."},{key:"profile_unmatch_threshold",label:"Unmatch Threshold",type:"number",step:.01,min:0,max:1,def:.35,doc:"If a live mid-cycle match drops below this score, the tentative identification is cleared. Keep it a little below the Match Threshold so a brief dip in similarity does not flip the display back to unmatched."},{key:"profile_match_interval",label:"Match Interval",unit:"s",type:"number",min:0,doc:"How often to attempt profile matching during a running cycle. Default 300 s (5 minutes) balances detection speed and CPU."}]},{sub:"Duration Gates",fields:[{key:"profile_match_min_duration_ratio",label:"Min Duration Ratio",type:"number",step:.01,min:0,max:1,def:.1,doc:"Minimum cycle length relative to the profile. 0.9 means a cycle must be at least 90% of the profile duration to match."},{key:"profile_match_max_duration_ratio",label:"Max Duration Ratio",type:"number",step:.01,min:0,def:1.8,doc:"Maximum cycle length relative to the profile. 1.3 means a cycle must be under 130% of the profile duration to match."},{key:"profile_duration_tolerance",label:"Profile Duration Tolerance",type:"number",step:.01,min:0,max:1,def:.25,doc:"The +/- band around a profile average duration used during matching. 0.25 means a 60 min profile matches 45-75 min cycles."},{key:"duration_tolerance",label:"Estimate Tolerance",type:"number",step:.01,min:0,max:1,def:.1,doc:"Tolerance for time-remaining estimates (learning feedback, not matching). If the actual duration is within +/-X% of the estimate it counts as a good match."}]},{sub:"Profile Evidence",fields:[{key:"profile_evidence_sources",label:"Cycles that shape a program",type:"checkboxlist",def:["real_cycles","reference_cycles","backfill_cycles"],choices:[["real_cycles","Cycles this machine ran"],["reference_cycles","Downloaded from the community store"],["backfill_cycles","Found in imported power history"]],doc:"Which cycles are used to build each program's power curve, and to match a finished cycle against it. Unticking a kind stops it shaping your programs without deleting anything - the cycles stay in your Cycles list and can still be labelled or removed. Useful if you do not trust imported data. Statistics are unaffected: they always count only the cycles this machine actually ran. Unticking everything is ignored, since a program with no cycles behind it could never match."}]},{sub:"Auto-Labeling",fields:[{key:"auto_label_confidence",label:"Auto-Label Confidence",type:"number",step:.01,min:0,max:1,def:.9,doc:"If the match score at cycle end is at or above this, the program is labeled automatically without any confirmation prompt. Raise it to require higher certainty before auto-labeling; lower it to automate more. Works in conjunction with Learning Confidence below it."},{key:"learning_confidence",label:"Learning Confidence",type:"number",step:.01,min:0,max:1,def:.6,doc:"If the match score falls between this and Auto-Label Confidence, WashData flags the finished cycle for review in the Cycles queue so you can verify the identified program. Below this score the match is too uncertain to surface. Must be kept below Auto-Label Confidence."}]}]},{id:"phase_eta",label:"Time Remaining",intro:"Phase-aware time-remaining, for machines whose cycle length depends on temperature or spin.",onlyDeviceTypes:["washing_machine","washer_dryer"],fields:[{key:"enable_phase_matching",label:"Phase-aware time remaining",type:"checkbox",def:!1,doc:"Break each running cycle into phases (heating, wash, spin) and budget the time remaining per phase, blended with the classic estimate - leaning on the phase budget early in the cycle and the classic estimate near the end. This personalises the countdown to how long your machine actually heats and runs, which is most noticeable in the first half of a cycle. Off = the classic estimate only. Only the time-remaining display is affected; program matching and cycle detection are unchanged."}]},{id:"timing",label:"Timing & Watchdog",intro:"Background cadence, the offline watchdog and housekeeping.",groups:[{sub:"Watchdog",fields:[{key:"watchdog_interval",label:"Watchdog Interval",unit:"s",type:"number",min:1,def:30,doc:"How often the background watchdog checks for stalled sensors and elapsed timeouts. Default 30 s."},{key:"no_update_active_timeout",label:"No-Update Timeout",unit:"s",type:"number",min:0,def:600,doc:"If no power updates arrive for this long while running, assume the plug dropped offline and force-stop to avoid a zombie cycle. Default 600 s allows for cloud or mesh lag."}]},{sub:"Housekeeping",fields:[{key:"progress_reset_delay",label:"Progress Reset Delay",unit:"s",type:"number",min:0,def:1800,doc:"After finishing, hold progress at 100% for this long so Completed is visible on dashboards before resetting to Idle."},{key:"auto_maintenance",label:"Auto Maintenance (nightly cleanup)",type:"checkbox",def:!0,doc:"Run nightly housekeeping: rebuild profile envelopes, recompute cycle health, prune debug traces and retain the most recent cycles."},{key:"power_profile_interval_min",label:"Power Profile Interval",unit:"min",type:"number",min:1,def:15,doc:"Bucket size for the per-profile power_profile sensor attribute (the flat per-slot average-watts array consumed by external planners such as EMHASS and tibber_prices). Smaller buckets keep short power spikes sharp; larger buckets smooth the shape. Default 15 min. Read-time only; does not affect detection."}]},{sub:"Debug",fields:[{key:"expose_debug_entities",label:"Expose Debug Entities",type:"checkbox",doc:"Publish extra diagnostic HA entities (match confidence, ambiguity, state internals). Off keeps the entity list clean for normal use."},{key:"save_debug_traces",label:"Save Debug Traces",type:"checkbox",doc:"Store the full power trace and matching debug data for each cycle. Useful for troubleshooting but increases storage size."}]}]},{id:"anti_wrinkle",label:"Anti-Wrinkle",intro:"Anti-wrinkle / anti-crease mode detects low-power tumble pulses after the main phase and keeps them attached to the finished cycle instead of reading them as new cycles.",onlyDeviceTypes:["washing_machine","dryer","washer_dryer"],fields:[{key:"anti_wrinkle_enabled",label:"Enable Anti-Wrinkle Detection",type:"checkbox",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."},{key:"anti_wrinkle_max_power",label:"Max Anti-Wrinkle Power",unit:"W",type:"number",step:10,min:0,def:400,doc:"A pulse above this power is treated as a real new cycle, not an anti-wrinkle tumble. Set just above the tumble-pulse power."},{key:"anti_wrinkle_max_duration",label:"Max Duration",unit:"s",type:"number",min:0,def:60,doc:"Pulses longer than this are treated as a real cycle rather than an anti-wrinkle tumble."},{key:"anti_wrinkle_exit_power",label:"Exit Power Threshold",unit:"W",type:"number",step:.1,min:0,def:.8,doc:"Power must fall below this between pulses for anti-wrinkle mode to stay active."},{key:"anti_wrinkle_idle_timeout",label:"Max Pulse Gap",unit:"s",type:"number",step:30,min:0,def:120,doc:"How long the machine may stay quiet between two tumble pulses before anti-wrinkle mode ends. Set it above the longest gap your dryer leaves between pulses, otherwise every later pulse is read as a false start."},{key:"anti_crease_finalize_ratio",label:"Anti-Crease Finalize Ratio",type:"number",step:.01,min:.5,max:1,def:.98,doc:"Fraction of the matched program's expected duration a cycle must reach before the anti-crease tumble tail may be finalised. Lower it on a dryer whose sensor-dry runtime follows the load, so its tail is recognised instead of sitting until the fallback timeout. Leave washing machines at the default: there this fraction is what keeps a quiet mid-wash phase from being mistaken for the tail. Separate from the Smart Termination Ratio, which gates a different check."}]},{id:"dishwasher",label:"Dishwasher",intro:"End-of-cycle handling for dishwashers, which typically finish with a long near-silent drying phase before a short final drain.",onlyDeviceTypes:["dishwasher"],fields:[{key:"dishwasher_end_spike_quiet_release",label:"Passive-Dry Quiet Release",unit:"s",type:"number",step:60,min:0,def:600,doc:"Once the cycle passes its expected duration, how long the dishwasher must stay quiet (below the Stop Threshold) before WashData stops waiting for a final drain and ends the cycle. Raise it if your machine has a long silent drying phase before a late final drain that is being missed - a wider window lets the learned duration follow seasonal drift (colder inlet water = longer cycles) instead of locking to the old average. It only ever shortens the wait relative to the internal 30-minute end-spike cap, never extends it."}]},{id:"delay",label:"Delay Start",intro:"Delayed-start detection identifies when an appliance is powered but has not yet begun its cycle.",fields:[{key:"delay_start_detect_enabled",label:"Enable Delay-Start Detection",type:"checkbox",doc:"Detect when the appliance is powered on and waiting (delayed start / standby) but has not begun its cycle, so standby draw is not mistaken for a running cycle."},{key:"delay_confirm_seconds",label:"Confirm Seconds",unit:"s",type:"number",min:0,def:60,doc:"Power must stay in the standby band for this long before the appliance is treated as waiting-to-start rather than running."},{key:"delay_timeout_hours",label:"Timeout Hours",unit:"h",type:"number",step:.5,min:0,def:8,doc:"Stop waiting in delayed-start mode after this many hours and return to idle, so a machine left powered but never started does not wait forever."}]},{id:"triggers",label:"Triggers & Door",intro:"Optional external signals: an end trigger, a door sensor, a pause switch, and the unload reminder.",groups:[{sub:"External End Trigger",fields:[{key:"external_end_trigger_enabled",label:"Enable External End Trigger",type:"checkbox",doc:"Let an external binary sensor signal the end of a cycle, in addition to the built-in power-based detection."},{key:"external_end_trigger",label:"External Trigger Entity",type:"entity",domain:"binary_sensor",doc:'Binary sensor whose state change marks the cycle end (e.g. an appliance "finished" contact or a companion integration).'},{key:"external_end_trigger_inverted",label:"Invert External Trigger (trigger on OFF)",type:"checkbox",doc:"Treat the trigger sensor turning OFF (rather than ON) as the end-of-cycle signal."}]},{sub:"Door & Pause",fields:[{key:"door_sensor_entity",label:"Door Sensor Entity",type:"entity",domain:"binary_sensor",doc:"Optional door binary sensor. Used to detect when the appliance has been opened/unloaded after a cycle."},{key:"door_opens_at_end",label:"Door Opens Automatically At End",type:"checkbox",doc:"For dishwashers that pop the door open at the end of the cycle to dry (AirDry and similar). With this on, a door-open on a running cycle no longer pauses it forever; instead, if the door stays open for the dwell below, WashData treats the cycle as finished. A brief open (adding an item) is ignored. Requires a Door Sensor Entity."},{key:"door_end_dwell_seconds",label:"Door-Open End Dwell",unit:"s",type:"number",min:1,def:60,doc:'How long the door must stay open before WashData ends the cycle, when "Door Opens Automatically At End" is on. Long enough to ignore quickly adding a dish (default 60 s), short enough to end promptly once the machine pops the door.'},{key:"pause_cuts_power",label:"Pause Also Cuts Power (via switch)",type:"checkbox",doc:"When a cycle is paused, also switch off the Switch Entity below. Only for appliances whose plug can safely be cut mid-cycle."},{key:"switch_entity",label:"Switch Entity",type:"entity",domain:"switch",doc:'Optional switch toggled off on pause and back on when resuming, used together with "Pause also cuts power".'}]},{sub:"Unload Reminder",fields:[{key:"notify_unload_delay_minutes",label:"Unload Nag Delay",unit:"min",type:"number",min:0,def:60,basic:!0,doc:'Minutes after a cycle ends before sending the still-waiting "unload the machine" reminder. Set 0 to disable the reminder.'},{key:"notify_unload_repeat",label:"Repeat Until Unloaded",type:"checkbox",doc:'Keep re-sending the unload reminder every "Unload Nag Delay" minutes until the unload is confirmed or you tap "Stop reminding" on the notification. Needs a way to confirm the unload: a Door Sensor Entity, an Unload Confirmation Entity, or the "Confirm Unload Manually" option below. The dismiss button works on Home Assistant companion-app (mobile) notifications.'},{key:"unload_confirm_entity",label:"Unload Confirmation Entity",type:"entity",domains:["event","button","input_button","binary_sensor","sensor","input_boolean","switch","scene","tag"],doc:"Optional entity whose activation means the load has been taken out, for machines that cannot have a door sensor. Any kind works: a Zigbee or NFC button, an input_button helper, a motion sensor in front of the machine, a scene. Every state change counts as a confirmation except switching off and going unavailable. Setting this also enables the unload reminder on a device with no Door Sensor Entity."},{key:"unload_track_without_door",label:"Confirm Unload Manually",type:"checkbox",doc:'Run the unload reminder on a device with no Door Sensor Entity and no Unload Confirmation Entity, and confirm the unload from your own automation instead: press the "Mark Unloaded" button entity, or call the ha_washdata.mark_unloaded service. Ignored when a Door Sensor Entity is set, which already provides the signal.'},{key:"pump_stuck_duration",label:"Pump Stuck Duration",unit:"s",type:"number",min:0,def:1800,onlyDeviceType:"pump",doc:"Seconds a pump may run continuously before it is flagged as possibly stuck (fires the stuck-pump event)."}]}]},{id:"notifications",label:"Notifications",groups:[{sub:"Services",fields:[{key:"notify_start_services",label:"Start Services",type:"entitylist",domain:"notify",placeholder:"add a notify service\u2026",basic:!0,doc:"notify.* services called when a cycle starts. Add one per target (phone, dashboard, etc.); leave empty for no start notification."},{key:"notify_finish_services",label:"Finish Services",type:"entitylist",domain:"notify",placeholder:"add a notify service\u2026",basic:!0,doc:"notify.* services called when a cycle finishes. Add one per target; leave empty for no finish notification."},{key:"notify_live_services",label:"Live Progress Services",type:"entitylist",domain:"notify",placeholder:"add a notify service\u2026",doc:"notify.* services called for live progress updates while a cycle runs. Leave empty to disable live-progress notifications."},{key:"notify_people",label:"People (for Only When Home)",type:"entitylist",domain:"person",placeholder:"add a person\u2026",doc:'person.* entities used by "Notify Only When Home" to decide whether anyone is home.'},{key:"notify_only_when_home",label:"Notify Only When Home",type:"checkbox",doc:"Only send notifications when at least one of the linked people (above) is home."},{key:"notify_fire_events",label:"Fire HA Events for Notifications",type:"checkbox",def:!0,doc:"Also fire ha_washdata_* events on cycle start/finish so you can build your own automations."}]},{sub:"Timing",fields:[{key:"notify_before_end_minutes",label:"Pre-End Alert",unit:"min",type:"number",min:0,def:0,doc:"Send an Almost Done alert when estimated time remaining drops below this. 0 disables it."},{key:"notify_live_interval_seconds",label:"Live Update Interval",unit:"s",type:"number",min:30,def:300,doc:"How often live-progress notifications are refreshed while a cycle runs."},{key:"notify_live_overrun_percent",label:"Live Overrun % Before Alert",unit:"%",type:"number",min:0,def:20,doc:"If a cycle runs past its estimate by more than this percentage, send an overrun alert."},{key:"notify_live_chronometer",label:"Use Live Chronometer",type:"checkbox",doc:"Show a live-updating countdown timer in the notification (on platforms that support it) instead of a static estimate."},{key:"notify_live_silent",label:"Silent Live Updates",type:"checkbox",def:!0,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."},{key:"notify_live_sticky",label:"Keep Live Notification On Tap",type:"checkbox",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."},{key:"notify_live_click_action",label:"Notification Tap Target",type:"text",optional:!0,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.'},{key:"notify_timeout_seconds",label:"Auto-Dismiss After",unit:"s",type:"number",min:0,def:0,doc:"Automatically dismiss the notification after this many seconds (on platforms that support it). 0 keeps it until dismissed manually."}]},{sub:"Messages",fields:[{key:"notify_title",label:"Notification Title",type:"text",def:"WashData: {device}",doc:`Notification title. Template variables: ${Ct}.`},{key:"notify_icon",label:"Notification Icon",type:"text",def:"",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."},{key:"notify_icon_color",label:"Notification Colour",type:"color",def:"",placeholder:"#03A9F4",doc:"Accent colour for this appliance's notifications, so a washer, a dryer and a dishwasher are tellable apart at a glance. iOS tints the icon and the Live Activity progress bar on the Lock Screen and in the Dynamic Island. Android is sent the same colour, but what it does with it is up to the phone: some tint the icon, and many (Samsung One UI especially) ignore it entirely. Hex (e.g. #03A9F4) is understood by both. Leave blank for the platform default."},{key:"notify_start_message",label:"Start Message",type:"textarea",def:"{device} started.",doc:`Body sent when a cycle starts. Template variables: ${Ct}.`},{key:"notify_finish_message",label:"Finish Message",type:"textarea",def:"{device} finished. Duration: {duration}m.",basic:!0,doc:`Body sent when a cycle finishes. Template variables: ${Ct}. {time_finished} and {vs_typical} are most useful here.`},{key:"notify_pre_complete_message",label:"Pre-Complete Message",type:"textarea",def:"{device}: Less than {minutes} minutes remaining.",doc:`Body of the pre-end / almost-done alert. Template variables: ${Ct}.`},{key:"notify_reminder_message",label:"Reminder Message",type:"textarea",def:"",doc:`Body of the still-waiting unload reminder. Blank uses the built-in default. Template variables: ${Ct}.`},{key:"notify_channel",label:"Android Channel (start/live)",type:"text",def:"",placeholder:"e.g. WashData",suggestions:["WashData","WashData Status","Appliance Status"],doc:"Android notification channel name for start/live messages (controls per-channel sound and priority on the mobile app). Blank uses the companion app default."},{key:"notify_finish_channel",label:"Android Channel (finish)",type:"text",def:"",placeholder:"e.g. WashData Finished",suggestions:["WashData Finished","WashData Alerts","Appliance Finished"],doc:"Android notification channel name for the finish message. Blank reuses the start/live channel."}]},{sub:"Energy",fields:[{key:"energy_sensor",label:"Energy Meter Entity",type:"entity",domain:"sensor",optional:!0,doc:"Optional cumulative energy counter (total_increasing kWh/Wh, e.g. the plug's own lifetime meter). When set, each cycle's reported energy is taken from this counter's start-to-end delta, which avoids the under-counting you get from integrating a slow-reporting power sensor. Falls back to the integrated value if the reading is missing, its unit is unknown, or the delta is not positive. Leave blank to keep integrating the power sensor."},{key:"energy_price_entity",label:"Energy Price Entity",type:"entity",domain:"sensor",basic:!0,notPrice:!0,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."},{key:"energy_price_static",label:"Static Energy Price (per kWh)",type:"number",step:.001,min:0,basic:!0,doc:"Fixed price per kWh used for cost figures when no live price entity is set above."},{key:"energy_price_dynamic",label:"Time-Weighted Cost",type:"checkbox",def:!0,basic:!0,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."},{key:"peak_rate_threshold",label:"Peak-Rate Threshold (per kWh)",type:"number",step:.001,min:0,def:0,clearable:!0,doc:"When a cycle starts and the current price per kWh is at or above this value, append a peak-rate tip to the start notification. 0 or blank disables the tip."},{key:"peak_rate_message",label:"Peak-Rate Message",type:"text",def:"",placeholder:"Running at peak rate ({price}/kWh).",doc:"Optional custom text for the peak-rate tip appended to the start notification. Template variables: {device}, {price}. Blank uses the built-in default."}]},{sub:"Cycle Timers",fields:[{key:"notify_cycle_timers",label:"Cycle Timers",type:"timerlist",doc:"Notifications at specific minutes into a cycle (e.g. to add softener). Message supports {device}, {program}, {minutes}. Enable Auto-pause to pause at that point and receive an interactive notification with a Resume button; resume via the panel, the pause/resume service, or the notification action."}]},{sub:"Quiet Hours & Milestones",fields:[{key:"notify_quiet_start_hour",label:"Quiet Hours Start",unit:"h",type:"number",min:0,max:23,clearable:!0,doc:"Start of a do-not-disturb window (0-23). Finish, reminder and clean-laundry notifications that would fire during quiet hours are held and delivered when the window ends. Leave blank to disable. Supports windows that cross midnight (e.g. start 22, end 7)."},{key:"notify_quiet_end_hour",label:"Quiet Hours End",unit:"h",type:"number",min:0,max:23,clearable:!0,doc:"End of the do-not-disturb window (0-23). Held notifications are delivered at this hour. Leave blank to disable."},{key:"notify_milestones",label:"Cycle Milestones",type:"intlist",def:"50, 100, 500, 1000",placeholder:"50, 100, 500, 1000",doc:"Comma-separated cycle counts that trigger a one-off celebration notification when reached (e.g. 50, 100, 500, 1000). Blank disables milestone notifications."},{key:"notify_milestone_message",label:"Milestone Message",type:"textarea",def:"{device} has completed {cycle_count} cycles!",doc:"Message for the milestone notification. Template variables: {device}, {cycle_count}."}]}]},{id:"ml_training",label:"ML Training",fields:[{key:"enable_ml_models",label:"Apply smart models during a cycle",type:"checkbox",def:!1,doc:"While a cycle runs, let the models refine the live results: a steadier time-remaining and energy/cost estimate, and an anti-premature-stop guard on end detection (it can only ever delay a finish, never end one early, and is bounded). Uses your fine-tuned models when available, otherwise the built-in ones. Off = the classic power-based logic only (still reliable)."},{key:"ml_training_enabled",label:"Learn from this machine",type:"checkbox",def:!1,doc:"Periodically study your reviewed cycles overnight and fine-tune the models to this specific machine. A change is only kept when it genuinely scores better on held-out cycles, so this can only help or stay the same \u2014 never regress."},{key:"ml_training_hour",label:"Learn at hour",unit:"h",type:"number",min:0,max:23,def:2,doc:"Local hour of day (0-23) to do the overnight fine-tuning. Pick a quiet hour such as 2 (02:00)."},{key:"ml_training_min_cycles",label:"Cycles needed first",type:"number",min:5,def:30,doc:"Wait until at least this many cycles have been recorded before fine-tuning, so there is enough to learn from."},{key:"ml_training_interval_days",label:"Check at most every",unit:"days",type:"number",min:1,def:7,doc:"Re-check for improvements at most once per this many days."}]}],Pt={};for(const h of $t){const t=h.groups||[{fields:h.fields}];for(const e of t)for(const s of e.fields||[])Pt[s.key]=s}const te={profile_match_min_duration_ratio:.1,profile_match_max_duration_ratio:1.8,corr_weight:.45,keep_min_score:.1,dtw_bandwidth:.2,dtw_blend:.5,dtw_ensemble_w:.7,dtw_ddtw_scale:30,dtw_refine_top_n:5,duration_weight:.22,energy_weight:.22,duration_scale:.175,energy_scale:.25},be=[{keys:["off_delay","min_off_gap"],check:h=>h.off_delay!=null&&h.min_off_gap!=null&&h.min_off_gap>h.off_delay,fieldNotes:h=>({off_delay:{msgKey:"note.off_delay.effective",msgVars:{eff:h.min_off_gap,gap:h.min_off_gap,delay:h.off_delay},msgFb:`A finished cycle actually waits ${h.min_off_gap} s, not ${h.off_delay} s: Min Off Gap (${h.min_off_gap} s) is longer and takes precedence.`}})}],ve=new Set(["energy","energy_storage","power","gas","water","current","voltage"]),xe=new Set(["w","kw","mw","wh","kwh","mwh","va","kva","varh","a","ma","v","mv"]);function qt(h,t,e){if(!h)return null;if(h===t.power_sensor)return{msgKey:"conflict.price_entity.power_sensor",msgVars:{},msgFb:"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."};if(h===t.energy_sensor)return{msgKey:"conflict.price_entity.energy_sensor",msgVars:{},msgFb:"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."};const s=e&&e.stateOf?e.stateOf(h):null;if(!s)return null;const r=s.attributes||{},n=String(r.device_class==null?"":r.device_class).trim().toLowerCase();if(ve.has(n))return{msgKey:"conflict.price_entity.device_class",msgVars:{dc:n},msgFb:`This sensor measures ${n}, not a price per kWh. A price entity overrides the static price below - clear this field or point it at a tariff sensor.`};const a=String(r.unit_of_measurement==null?"":r.unit_of_measurement).trim();return xe.has(a.toLowerCase())?{msgKey:"conflict.price_entity.unit",msgVars:{unit:a},msgFb:`This sensor reads in ${a}, 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.`}:null}const ee=[{keys:["energy_price_entity","energy_sensor","power_sensor"],check:(h,t)=>qt(h.energy_price_entity,h,t)!=null,fieldErrors:(h,t)=>({energy_price_entity:qt(h.energy_price_entity,h,t)})},{keys:["start_threshold_w","stop_threshold_w"],check:h=>h.start_threshold_w!=null&&h.stop_threshold_w!=null&&h.start_threshold_w<=h.stop_threshold_w,fieldErrors:h=>({start_threshold_w:{msgKey:"conflict.hysteresis.start",msgVars:{stop:h.stop_threshold_w},msgFb:`Must be above Stop Threshold (${h.stop_threshold_w} W)`,fixVal:+Math.max(h.stop_threshold_w+.5,h.stop_threshold_w*1.25).toFixed(1)},stop_threshold_w:{msgKey:"conflict.hysteresis.stop",msgVars:{start:h.start_threshold_w},msgFb:`Must be below Start Threshold (${h.start_threshold_w} W)`,fixVal:+Math.min(h.start_threshold_w-.5,h.start_threshold_w*.8).toFixed(1)}})},{keys:["min_power","stop_threshold_w"],check:h=>h.min_power!=null&&h.stop_threshold_w!=null&&h.min_power>h.stop_threshold_w,fieldErrors:h=>({min_power:{msgKey:"conflict.min_power.min_power",msgVars:{stop:h.stop_threshold_w},msgFb:`Must be at or below Stop Threshold (${h.stop_threshold_w} W)`,fixVal:+(h.stop_threshold_w*.8).toFixed(1)},stop_threshold_w:{msgKey:"conflict.min_power.stop",msgVars:{min:h.min_power},msgFb:`Must be at or above Min Power (${h.min_power} W)`,fixVal:+(h.min_power*1.25).toFixed(1)}})},{keys:["power_off_threshold_w","stop_threshold_w"],check:h=>h.power_off_threshold_w!=null&&h.power_off_threshold_w>0&&h.stop_threshold_w!=null&&h.power_off_threshold_w>=h.stop_threshold_w,fieldErrors:h=>({power_off_threshold_w:{msgKey:"conflict.power_off.threshold",msgVars:{stop:h.stop_threshold_w},msgFb:`Must be below Stop Threshold (${h.stop_threshold_w} W) to take effect`,fixVal:+(h.stop_threshold_w*.6).toFixed(1)},stop_threshold_w:{msgKey:"conflict.power_off.stop",msgVars:{pot:h.power_off_threshold_w},msgFb:`Must be above Power Off Threshold (${h.power_off_threshold_w} W)`,fixVal:+(h.power_off_threshold_w*1.67).toFixed(1)}})},{keys:["off_delay","min_off_gap"],check:h=>h.off_delay!=null&&h.min_off_gap!=null&&h.off_delay>h.min_off_gap,fieldErrors:h=>({off_delay:{msgKey:"conflict.off_delay.off_delay",msgVars:{gap:h.min_off_gap},msgFb:`Off Delay (${h.off_delay} s) overrides Min Off Gap (${h.min_off_gap} s); cycles within the gap may merge`,fixVal:h.min_off_gap},min_off_gap:{msgKey:"conflict.off_delay.gap",msgVars:{delay:h.off_delay},msgFb:`Min Off Gap should be at least Off Delay (${h.off_delay} s)`,fixVal:h.off_delay}})},{keys:["watchdog_interval","sampling_interval"],check:h=>h.watchdog_interval!=null&&h.sampling_interval!=null&&h.watchdog_interval<2*h.sampling_interval,fieldErrors:h=>({watchdog_interval:{msgKey:"conflict.watchdog.interval",msgVars:{si:h.sampling_interval},msgFb:`Should be at least 2\xD7 Sampling Interval (${h.sampling_interval} s)`,fixVal:+(2*h.sampling_interval+1)},sampling_interval:{msgKey:"conflict.watchdog.sampling",msgVars:{wi:h.watchdog_interval},msgFb:`Sampling Interval should be at most half of Watchdog Interval (${h.watchdog_interval} s)`,fixVal:+Math.floor(h.watchdog_interval/2)}})},{keys:["no_update_active_timeout","watchdog_interval"],check:h=>h.no_update_active_timeout!=null&&h.watchdog_interval!=null&&h.no_update_active_timeout<=h.watchdog_interval,fieldErrors:h=>({no_update_active_timeout:{msgKey:"conflict.no_update_timeout.timeout",msgVars:{wi:h.watchdog_interval},msgFb:`Must be greater than Watchdog Interval (${h.watchdog_interval} s)`,fixVal:h.watchdog_interval*2},watchdog_interval:{msgKey:"conflict.no_update_timeout.watchdog",msgVars:{to:h.no_update_active_timeout},msgFb:`Must be less than No-Update Timeout (${h.no_update_active_timeout} s)`,fixVal:+Math.floor(h.no_update_active_timeout/2)}})},{keys:["start_duration_threshold","sampling_interval"],check:h=>h.start_duration_threshold!=null&&h.sampling_interval!=null&&h.start_duration_threshold({start_duration_threshold:{msgKey:"conflict.start_dur.threshold",msgVars:{si:h.sampling_interval},msgFb:`Should be at least one Sampling Interval (${h.sampling_interval} s) to prevent single-sample false starts`,fixVal:h.sampling_interval},sampling_interval:{msgKey:"conflict.start_dur.sampling",msgVars:{sdt:h.start_duration_threshold},msgFb:`Sampling Interval exceeds Start Duration (${h.start_duration_threshold} s); single-sample spikes can open a cycle`,fixVal:h.start_duration_threshold}})},{keys:["learning_confidence","profile_match_threshold"],check:h=>h.learning_confidence!=null&&h.profile_match_threshold!=null&&h.learning_confidence({learning_confidence:{msgKey:"conflict.confidence.learning",msgVars:{match:h.profile_match_threshold},msgFb:`Must be at or above Match Threshold (${h.profile_match_threshold})`,fixVal:+h.profile_match_threshold.toFixed(2)},profile_match_threshold:{msgKey:"conflict.confidence.match_for_learning",msgVars:{lc:h.learning_confidence},msgFb:`Must be at or below Learning Confidence (${h.learning_confidence})`,fixVal:+h.learning_confidence.toFixed(2)}})},{keys:["profile_match_threshold","auto_label_confidence"],check:h=>h.profile_match_threshold!=null&&h.auto_label_confidence!=null&&h.profile_match_threshold>h.auto_label_confidence,fieldErrors:h=>({profile_match_threshold:{msgKey:"conflict.confidence.match_for_auto",msgVars:{alc:h.auto_label_confidence},msgFb:`Must be at or below Auto-Label Confidence (${h.auto_label_confidence})`,fixVal:+h.auto_label_confidence.toFixed(2)},auto_label_confidence:{msgKey:"conflict.confidence.auto",msgVars:{match:h.profile_match_threshold},msgFb:`Must be at or above Match Threshold (${h.profile_match_threshold})`,fixVal:+h.profile_match_threshold.toFixed(2)}})},{keys:["profile_unmatch_threshold","profile_match_threshold"],check:h=>h.profile_unmatch_threshold!=null&&h.profile_match_threshold!=null&&h.profile_unmatch_threshold>=h.profile_match_threshold,fieldErrors:h=>({profile_unmatch_threshold:{msgKey:"conflict.unmatch.unmatch",msgVars:{match:h.profile_match_threshold},msgFb:`Must be below Match Threshold (${h.profile_match_threshold}); otherwise a committed match un-matches instantly`,fixVal:+(h.profile_match_threshold-.05).toFixed(2)},profile_match_threshold:{msgKey:"conflict.unmatch.match",msgVars:{un:h.profile_unmatch_threshold},msgFb:`Must be above Unmatch Threshold (${h.profile_unmatch_threshold})`,fixVal:+(h.profile_unmatch_threshold+.05).toFixed(2)}})},{keys:["anti_wrinkle_exit_power","stop_threshold_w"],check:h=>["washing_machine","dryer","washer_dryer"].includes(h.device_type)&&h.anti_wrinkle_exit_power!=null&&h.stop_threshold_w!=null&&h.anti_wrinkle_exit_power>=h.stop_threshold_w,fieldErrors:h=>({anti_wrinkle_exit_power:{msgKey:"conflict.anti_wrinkle_exit.exit",msgVars:{stop:h.stop_threshold_w},msgFb:`Must be below Stop Threshold (${h.stop_threshold_w} W); otherwise the anti-wrinkle exit power is ignored`,fixVal:+(h.stop_threshold_w*.4).toFixed(1)},stop_threshold_w:{msgKey:"conflict.anti_wrinkle_exit.stop",msgVars:{exit:h.anti_wrinkle_exit_power},msgFb:`Must be above Anti-Wrinkle Exit Power (${h.anti_wrinkle_exit_power} W)`,fixVal:+(h.anti_wrinkle_exit_power*2.5).toFixed(1)}})},{keys:["anti_wrinkle_max_power","start_threshold_w"],check:h=>["washing_machine","dryer","washer_dryer"].includes(h.device_type)&&h.anti_wrinkle_max_power!=null&&h.start_threshold_w!=null&&h.anti_wrinkle_max_power<=h.start_threshold_w,fieldErrors:h=>({anti_wrinkle_max_power:{msgKey:"conflict.anti_wrinkle_max.max",msgVars:{start:h.start_threshold_w},msgFb:`Must be above Start Threshold (${h.start_threshold_w} W); otherwise anti-wrinkle duration limit is bypassed`,fixVal:+(h.start_threshold_w*2).toFixed(0)},start_threshold_w:{msgKey:"conflict.anti_wrinkle_max.start",msgVars:{max:h.anti_wrinkle_max_power},msgFb:`Must be below Anti-Wrinkle Max Power (${h.anti_wrinkle_max_power} W)`,fixVal:+(h.anti_wrinkle_max_power*.5).toFixed(1)}})},{keys:["pump_stuck_duration","no_update_active_timeout"],check:h=>h.device_type==="pump"&&h.pump_stuck_duration!=null&&h.no_update_active_timeout!=null&&h.no_update_active_timeout<=h.pump_stuck_duration,fieldErrors:h=>({pump_stuck_duration:{msgKey:"conflict.pump_stuck.duration",msgVars:{to:h.no_update_active_timeout},msgFb:`Must be less than No-Update Timeout (${h.no_update_active_timeout} s) so the stuck alarm fires before the watchdog kills the cycle`,fixVal:h.no_update_active_timeout-60},no_update_active_timeout:{msgKey:"conflict.pump_stuck.timeout",msgVars:{ps:h.pump_stuck_duration},msgFb:`Must exceed Pump Stuck Duration (${h.pump_stuck_duration} s) so the stuck alarm fires before the cycle is force-stopped`,fixVal:h.pump_stuck_duration+60}})},{keys:["profile_match_min_duration_ratio","profile_match_max_duration_ratio"],check:h=>h.profile_match_min_duration_ratio!=null&&h.profile_match_max_duration_ratio!=null&&h.profile_match_min_duration_ratio>=h.profile_match_max_duration_ratio,fieldErrors:h=>({profile_match_min_duration_ratio:{msgKey:"conflict.duration_ratio.min",msgVars:{max:h.profile_match_max_duration_ratio},msgFb:`Must be less than Max Duration Ratio (${h.profile_match_max_duration_ratio})`,fixVal:+(h.profile_match_max_duration_ratio*.5).toFixed(2)},profile_match_max_duration_ratio:{msgKey:"conflict.duration_ratio.max",msgVars:{min:h.profile_match_min_duration_ratio},msgFb:`Must be greater than Min Duration Ratio (${h.profile_match_min_duration_ratio})`,fixVal:+(h.profile_match_min_duration_ratio*2).toFixed(2)}})},{keys:["end_energy_threshold","stop_threshold_w","off_delay"],check:h=>h.end_energy_threshold!=null&&h.stop_threshold_w!=null&&h.off_delay!=null&&h.off_delay>0&&h.end_energy_threshold({end_energy_threshold:{msgKey:"conflict.end_energy.energy",msgVars:{w:h.stop_threshold_w,d:h.off_delay},msgFb:`Too strict for Stop Threshold (${h.stop_threshold_w} W) over Off Delay (${h.off_delay} s); the cycle can only end through a fallback path`,fixVal:Math.ceil(h.stop_threshold_w*h.off_delay/3600*1e3)/1e3},stop_threshold_w:{msgKey:"conflict.end_energy.stop",msgVars:{e:h.end_energy_threshold,d:h.off_delay},msgFb:`End Energy Threshold (${h.end_energy_threshold} Wh over ${h.off_delay} s) only permits ${+(h.end_energy_threshold*3600/h.off_delay).toFixed(2)} W`,fixVal:Math.floor(h.end_energy_threshold*3600/h.off_delay*10)/10}})}],$e=` +const w="ha_washdata",Zt=(()=>{try{return new URL(import.meta.url).searchParams.get("v")||""}catch{return""}})(),ve=2e4,xe=6e3,zt=25,St=50,$e="https://github.com/iluebbe/maintenance_supporter",Mt=["running","starting","paused","user_paused","ending","anti_wrinkle"],ke=[{key:"show_contributor",def:!0,labelKey:"lbl.show_contributor",labelFb:"Show contributor names",docKey:"setting.show_contributor.doc",docFb:'Show the "by " attribution on community appliances and reference cycles.'},{key:"share_name",def:!1,labelKey:"lbl.share_name",labelFb:"Show my name on what I share",docKey:"setting.share_name.doc",docFb:"Show your account name as the uploader of cycles you share. Off: shared without a name."}],Bt=34,Qt=.5,Se=200,Te=5,Tt=22,Ft=[0,5,10,30,60],Ee=10,Ce=320,Q=["#e6194B","#3cb44b","#4363d8","#f58231","#911eb4","#42d4f4","#f032e6","#bfef45","#fabed4","#469990","#dcbeff","#9A6324","#800000","#aaffc3","#808000","#ffd8b1","#000075","#a9a9a9"],Pe="{device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}",yt=[{id:"basic",label:"Basic",intro:"Core identity and the essentials most setups need.",groups:[{sub:"Device info",fields:[{key:"name",label:"Device Name",basic:!0,type:"text",doc:"Name shown in Home Assistant for this device."},{key:"device_type",label:"Device Type",basic:!0,type:"devicetype",doc:"Sets detection defaults (thresholds, off delay, end handling) for this kind of appliance. Change it only if the device was set up as the wrong type."},{key:"store_brand",label:"Appliance Brand",type:"storebrand",optional:!0,basic:!0,doc:"Optional. Your appliance brand from the community catalog, used to find and share reference recordings. Leave blank if you do not use online features."},{key:"store_model",label:"Appliance Model",type:"storemodel",optional:!0,basic:!0,doc:"Optional. Your appliance model from the community catalog (pick a brand first). If it is not listed, you can add it."}]},{sub:"Basic configuration",fields:[{key:"power_sensor",label:"Power Sensor",basic:!0,type:"entity",domain:"sensor",doc:"The sensor that reports this appliance's live power in watts (e.g. sensor.washer_power). All detection is based on it."},{key:"min_power",label:"Minimum Power",unit:"W",type:"number",step:.1,min:0,def:2,basic:!0,doc:"Readings below this count as 0 W (standby). Filters out the idle draw of smart plugs and standby LEDs."},{key:"off_delay",label:"Off Delay",unit:"s",type:"number",min:0,def:180,basic:!0,doc:"How long power must stay low before the cycle is finished. Power coming back within this time continues the cycle. Dishwashers can be idle 20-60 min while drying, so set it longer than that."},{key:"linked_device",label:"Group Under Device",type:"device",doc:'Optional. Show this WashData device under another device (e.g. the smart plug) in Home Assistant, as "Connected via ...".'}]},{sub:"Energy",fields:[{key:"energy_sensor",label:"Energy Meter Entity",type:"entity",domain:"sensor",optional:!0,doc:"Optional kWh or Wh counter (total_increasing, e.g. the plug's own meter). Cycle energy is then read from it, which is more accurate than computing it from a slow power sensor. If its reading is unusable, the computed value is used."},{key:"energy_price_entity",label:"Energy Price Entity",type:"entity",domain:"sensor",basic:!0,notPrice:!0,doc:"Sensor with the current price per kWh (e.g. a dynamic tariff). It overrides the static price below, so it must be a price, not the plug's power or energy sensor."},{key:"energy_price_static",label:"Static Energy Price (per kWh)",type:"number",step:.001,min:0,basic:!0,doc:"Fixed price per kWh, used when no price entity is set."},{key:"energy_price_dynamic",label:"Time-Weighted Cost",type:"checkbox",def:!0,basic:!0,doc:"Charge each cycle at the price at each moment it ran, instead of the price when it finished. Needs a price entity. Process History (Diagnostics) recosts older cycles."}]}]},{id:"detection",label:"Detection",intro:"How a cycle is detected as starting, running and finishing.",groups:[{sub:"Thresholds & Gap",fields:[{key:"start_threshold_w",label:"Start Threshold",unit:"W",type:"number",step:1,min:0,doc:"Power must rise above this to start a cycle. Too low: standby power causes false starts. Too high: slow-starting programs (cold fill) are missed."},{key:"stop_threshold_w",label:"Stop Threshold",unit:"W",type:"number",step:.1,min:0,doc:"Power must fall below this before the off delay starts counting. Keep it below the Start Threshold. Too high, and low-power phases (rinse holds, anti-crease) start the end countdown by mistake."},{key:"min_off_gap",label:"Min Off Gap",unit:"s",type:"number",min:0,doc:"An off period shorter than this does not split a cycle, so soak pauses stay in one wash. Keep it shorter than the gap between back-to-back loads, or those merge into one cycle."}]},{sub:"Cycle Start",fields:[{key:"start_duration_threshold",label:"Start Duration",internal:!0,unit:"s",type:"number",min:0,def:5,doc:"Power must stay above the Start Threshold this long before a cycle starts, so brief on/off blips are ignored."},{key:"start_energy_threshold",label:"Start Energy",internal:!0,unit:"Wh",type:"number",step:.01,min:0,def:.2,doc:"Energy the appliance must use before the cycle counts as running. A short power spike uses little energy, so it does not start a cycle."},{key:"completion_min_seconds",label:"Min Cycle Duration",unit:"s",type:"number",min:0,def:600,doc:"Cycles shorter than this (test runs, opening the door to add a sock) are saved as Interrupted and get no finish notification."},{key:"curve_preroll_seconds",label:"Curve Pre-roll",unit:"s",type:"number",step:10,min:0,max:600,def:0,doc:"Adds readings from failed start attempts to the front of a cycle, for machines that probe before starting (door lock, first fill). 300 s covers most; 0 = off. Recorded starts move earlier, so learned durations drift for a while."}]},{sub:"Cycle End",fields:[{key:"end_energy_threshold",label:"End Energy",unit:"Wh",type:"number",step:.001,min:0,def:.05,doc:"Energy allowed during the off delay. More than this and the cycle keeps going, so tumbles and drying tails stay attached. Lower it if cycles end during a tail; raise it if finished cycles keep waiting."},{key:"smart_termination_duration_ratio",label:"Smart Termination Ratio",type:"number",step:.01,min:.5,max:1,doc:"Fraction of the program's average duration a cycle must reach before Smart Termination may end it early. If run times vary (water temperature, sensor-dry, load size), lower it (e.g. 0.85). Leave empty for 0.98 (0.99 on dishwashers)."}]},{sub:"Power Off",fields:[{key:"power_off_threshold_w",label:"Power Off Threshold",unit:"W",type:"number",step:.1,min:0,def:0,doc:"After a cycle, power below this for the Power Off Delay sets the state to Off, instead of the Progress Reset Delay doing it. Set it above the switched-off draw, below standby and the Stop Threshold. 0 disables it (default)."},{key:"power_off_delay",label:"Power Off Delay",unit:"s",type:"number",min:0,def:30,doc:"How long power must stay below the Power Off Threshold after a cycle before the state returns to Off. Only used when that threshold is above 0. Checked periodically, so the real wait can be slightly longer."}]},{sub:"Signal Processing",fields:[{key:"sampling_interval",label:"Sampling Interval",internal:!0,unit:"s",type:"number",min:1,def:30,doc:"Minimum time between processed readings. Readings that arrive sooner are skipped (low readings during a cycle are always kept). Set it to your plug's reporting interval or lower; a higher value only throws readings away."}]}]},{id:"matching",label:"Matching",intro:"How finished cycles are matched to learned profiles and labelled.",notDeviceTypes:["other"],groups:[{sub:"Match Scoring",fields:[{key:"profile_match_threshold",label:"Match Threshold",type:"number",step:.01,min:0,max:1,def:.4,doc:"Live match score a cycle needs before Smart Termination or the anti-crease finish may end it early. It does not change the shown program. Raise it if weak matches end cycles early; lower it if clear matches wait the off delay."},{key:"profile_unmatch_threshold",label:"Unmatch Threshold",internal:!0,type:"number",step:.01,min:0,max:1,def:.35,doc:"If the live match score drops below this, the current program guess is cleared. Keep it a little below the Match Threshold so a brief dip does not reset it."},{key:"profile_match_interval",label:"Match Interval",internal:!0,unit:"s",type:"number",min:0,doc:"How often to match a running cycle against profiles. Default 300 s (5 min)."}]},{sub:"Duration Gates",fields:[{key:"profile_match_min_duration_ratio",label:"Min Duration Ratio",internal:!0,type:"number",step:.01,min:0,max:1,def:.1,doc:"A program is only considered after the cycle has run this fraction of its typical duration. 0.10: a 2-hour program becomes a candidate after 12 minutes. Not applied in a cycle's first 15 minutes."},{key:"profile_match_max_duration_ratio",label:"Max Duration Ratio",internal:!0,type:"number",step:.01,min:0,def:1.8,doc:"A program is dropped once the cycle has run longer than this multiple of its typical duration. 1.8: a cycle 80% longer than the program no longer matches it."},{key:"duration_tolerance",label:"Estimate Tolerance",internal:!0,type:"number",step:.01,min:0,max:1,def:.1,doc:"Learning feedback only: a cycle within +/-X% of its estimated duration counts as a good estimate. Does not affect matching."}]},{sub:"Profile Evidence",fields:[{key:"profile_evidence_sources",label:"Cycles that shape a program",type:"checkboxlist",def:["real_cycles","reference_cycles","backfill_cycles"],choices:[["real_cycles","Cycles this machine ran"],["reference_cycles","Downloaded from the community store"],["backfill_cycles","Found in imported power history"]],doc:"Which kinds of cycles shape each program's curve and matching. Unticking a kind deletes nothing; useful if you do not trust imported data. Statistics always count only this machine's cycles. Unticking all is ignored."}]},{sub:"Auto-Labeling",fields:[{key:"auto_label_confidence",label:"Auto-Label Confidence",type:"number",step:.01,min:0,max:1,def:.9,doc:"Match score at cycle end at or above which the program is labelled without asking. Raise it for more certainty; lower it to label more cycles automatically. Keep it above Learning Confidence."},{key:"learning_confidence",label:"Learning Confidence",internal:!0,type:"number",step:.01,min:0,max:1,def:.6,doc:"Lowest match score for labelling a finished cycle. A clear winner is labelled; a close call goes to the review queue in Cycles. Below this, nothing is labelled or asked. Keep it below Auto-Label Confidence."}]}]},{id:"timing",label:"Timing & Watchdog",intro:"Background cadence, the offline watchdog and housekeeping.",groups:[{sub:"Watchdog",fields:[{key:"watchdog_interval",label:"Watchdog Interval",internal:!0,unit:"s",type:"number",min:1,def:30,doc:"How often the background check looks for stalled sensors and timeouts. Default 30 s."},{key:"no_update_active_timeout",label:"No-Update Timeout",internal:!0,unit:"s",type:"number",min:0,def:600,doc:"If no power update arrives for this long during a cycle, assume the plug went offline and force-stop the cycle. The default 600 s allows for cloud or mesh delays."}]},{sub:"Housekeeping",fields:[{key:"progress_reset_delay",label:"Progress Reset Delay",unit:"s",type:"number",min:0,def:1800,doc:"After a cycle, keep progress at 100% this long before resetting to Idle."},{key:"auto_maintenance",label:"Auto Maintenance (nightly cleanup)",type:"checkbox",def:!0,doc:"Nightly cleanup: label old cycles that now clearly match a program, rebuild program curves, refresh anomaly marks and cycle health, and remove debug traces if Save Debug Traces is off. Cycles and their power traces are always kept."},{key:"power_profile_interval_min",label:"Power Profile Interval",unit:"min",type:"number",min:1,def:15,doc:"Bucket size of the power_profile attribute (average watts per slot) used by planners such as EMHASS and tibber_prices. Smaller keeps short spikes; larger smooths the curve. Default 15 min. Does not affect detection."}]},{sub:"Debug",fields:[{key:"expose_debug_entities",label:"Expose Debug Entities",internal:!0,type:"checkbox",doc:"Add diagnostic entities (match confidence, ambiguity, internal state). Off keeps the entity list clean."},{key:"save_debug_traces",label:"Save Debug Traces",type:"checkbox",doc:"Also store the matcher's debug data (every candidate's scores) with each cycle, for troubleshooting. Adds about 20 KB per cycle."}]}]},{id:"anti_wrinkle",label:"Anti-Wrinkle",intro:"Keeps a dryer's low-power tumble pulses after the main phase attached to the finished cycle.",onlyDeviceTypes:["washing_machine","dryer","washer_dryer"],fields:[{key:"anti_wrinkle_enabled",label:"Enable Anti-Wrinkle Detection",basic:!0,type:"checkbox",doc:"Keep a dryer's short low-power tumble pulses after the main heat phase with the finished cycle, instead of reading them as new cycles."},{key:"anti_wrinkle_max_power",label:"Max Anti-Wrinkle Power",unit:"W",type:"number",step:10,min:0,def:400,doc:"A pulse above this power counts as a real new cycle, not a tumble. Set it just above your tumble-pulse power."},{key:"anti_wrinkle_max_duration",label:"Max Duration",unit:"s",type:"number",min:0,def:60,doc:"A pulse longer than this counts as a real cycle, not an anti-wrinkle tumble."},{key:"anti_wrinkle_exit_power",label:"Exit Power Threshold",internal:!0,unit:"W",type:"number",step:.1,min:0,def:.8,doc:"In anti-wrinkle mode, power below this counts as quiet between tumble pulses, and quiet for longer than the Max Pulse Gap ends the mode. The Stop Threshold is used when it is higher, so this only changes anything when set above the Stop Threshold."},{key:"anti_wrinkle_idle_timeout",label:"Max Pulse Gap",unit:"s",type:"number",step:30,min:0,def:120,doc:"How long the dryer may stay quiet between tumble pulses before anti-wrinkle mode ends. Set it above your dryer's longest gap, or later pulses are read as false starts."},{key:"anti_crease_finalize_ratio",label:"Anti-Crease Finalize Ratio",type:"number",step:.01,min:.5,max:1,def:.98,doc:"Fraction of the program's average duration a cycle must reach before its low-power tumbling counts as the anti-crease tail. Lower it (e.g. 0.75) on load-sensing dryers. Keep washers at the default (0.98), or a wash can end halfway."}]},{id:"dishwasher",label:"Dishwasher",intro:"End handling for dishwashers, which often dry quietly for a long time before a short final drain.",onlyDeviceTypes:["dishwasher"],fields:[{key:"dishwasher_end_spike_quiet_release",label:"Passive-Dry Quiet Release",unit:"s",type:"number",step:60,min:0,def:600,doc:"After the expected duration, how long the dishwasher must stay below the Stop Threshold before WashData stops waiting for the final drain. Raise it if a late drain after a long dry is missed. The wait never exceeds 30 minutes."}]},{id:"delay",label:"Delay Start",intro:"Detects an appliance that is on and waiting for a delayed start.",fields:[{key:"delay_start_detect_enabled",label:"Enable Delay-Start Detection",type:"checkbox",doc:"Detect an appliance that is on and waiting for a delayed start, so its standby draw is not taken for a running cycle."},{key:"delay_confirm_seconds",label:"Confirm Seconds",unit:"s",type:"number",min:0,def:60,doc:"Power must stay in the standby band this long before the appliance counts as waiting to start."},{key:"delay_timeout_hours",label:"Timeout Hours",unit:"h",type:"number",step:.5,min:0,def:8,doc:"Give up waiting for a delayed start after this many hours and return to idle."}]},{id:"triggers",label:"Triggers & Door",intro:"Optional extras: end trigger, door sensor, pause switch and unload reminder.",groups:[{sub:"External End Trigger",fields:[{key:"external_end_trigger_enabled",label:"Enable External End Trigger",type:"checkbox",doc:"Let a binary sensor signal the end of a cycle, in addition to power-based detection."},{key:"external_end_trigger",label:"External Trigger Entity",type:"entity",domain:"binary_sensor",doc:'Binary sensor whose change marks the cycle end (e.g. a "finished" contact or another integration).'},{key:"external_end_trigger_inverted",label:"Invert External Trigger (trigger on OFF)",type:"checkbox",doc:"Treat the sensor turning OFF (not ON) as the end signal."}]},{sub:"Door & Pause",fields:[{key:"door_sensor_entity",label:"Door Sensor Entity",basic:!0,type:"entity",domain:"binary_sensor",doc:"Optional door sensor. Shows when the appliance was opened to unload after a cycle."},{key:"door_opens_at_end",label:"Door Opens Automatically At End",type:"checkbox",doc:"For dishwashers that open the door at the end to dry (e.g. AirDry). A door left open for the dwell time below ends the cycle instead of pausing it; a brief open is ignored. Needs a Door Sensor Entity."},{key:"door_end_dwell_seconds",label:"Door-Open End Dwell",unit:"s",type:"number",min:1,def:60,doc:'With "Door Opens Automatically At End" on: how long the door must stay open before the cycle ends. Long enough to ignore adding a dish (default 60 s).'},{key:"pause_cuts_power",label:"Pause Also Cuts Power (via switch)",type:"checkbox",doc:"When a cycle is paused, also switch off the Switch Entity below. Only for appliances that can safely lose power mid-cycle."},{key:"switch_entity",label:"Switch Entity",type:"entity",domain:"switch",doc:'Optional switch turned off on pause and back on when resuming, with "Pause Also Cuts Power".'}]},{sub:"Unload Reminder",fields:[{key:"notify_unload_delay_minutes",label:"Unload Nag Delay",unit:"min",type:"number",min:0,def:60,basic:!0,doc:'Minutes after a cycle ends before the "unload the machine" reminder. 0 disables it.'},{key:"notify_unload_repeat",label:"Repeat Until Unloaded",type:"checkbox",doc:'Resend the unload reminder every "Unload Nag Delay" minutes until the unload is confirmed or you tap "Stop reminding" (mobile app). Needs a Door Sensor Entity, an Unload Confirmation Entity, or "Confirm Unload Manually".'},{key:"unload_confirm_entity",label:"Unload Confirmation Entity",type:"entity",domains:["event","button","input_button","binary_sensor","sensor","input_boolean","switch","scene","tag"],doc:"Optional. Any entity that confirms the load was taken out, for machines without a door sensor (a button, input_button, motion sensor, scene). Every state change counts except turning off or unavailable. Also enables the unload reminder."},{key:"unload_track_without_door",label:"Confirm Unload Manually",type:"checkbox",doc:'Run the unload reminder without a door sensor or confirmation entity. Confirm the unload from your own automation with the "Mark Unloaded" button or the ha_washdata.mark_unloaded service. Ignored when a Door Sensor Entity is set.'},{key:"pump_stuck_duration",label:"Pump Stuck Duration",unit:"s",type:"number",min:0,def:1800,onlyDeviceType:"pump",doc:"How long a pump may run nonstop before the stuck-pump event fires."}]}]},{id:"notifications",label:"Notifications",groups:[{sub:"Services",fields:[{key:"notify_start_services",label:"Start Services",type:"entitylist",domain:"notify",placeholder:"add a notify service\u2026",basic:!0,doc:"notify.* services to call when a cycle starts (phone, dashboard, ...). Leave empty for none."},{key:"notify_finish_services",label:"Finish Services",type:"entitylist",domain:"notify",placeholder:"add a notify service\u2026",basic:!0,doc:"notify.* services to call when a cycle finishes. Leave empty for none."},{key:"notify_live_services",label:"Live Progress Services",basic:!0,type:"entitylist",domain:"notify",placeholder:"add a notify service\u2026",doc:"notify.* services for live progress updates while a cycle runs. Leave empty to turn them off."},{key:"notify_people",label:"People (for Only When Home)",type:"entitylist",domain:"person",placeholder:"add a person\u2026",doc:'person.* entities that "Notify Only When Home" checks.'},{key:"notify_only_when_home",label:"Notify Only When Home",type:"checkbox",doc:"Only notify when at least one of the people above is home."},{key:"notify_fire_events",label:"Fire HA Events for Notifications",type:"checkbox",def:!0,doc:"Also fire ha_washdata_* events on cycle start and finish for your own automations."}]},{sub:"Timing",fields:[{key:"notify_before_end_minutes",label:"Pre-End Alert",unit:"min",type:"number",min:0,def:0,doc:"Send an Almost Done alert when estimated time remaining drops below this. 0 disables it."},{key:"notify_live_interval_seconds",label:"Live Update Interval",unit:"s",type:"number",min:30,def:300,doc:"How often live-progress notifications are refreshed while a cycle runs."},{key:"notify_live_overrun_percent",label:"Live Overrun % Before Alert",unit:"%",type:"number",min:0,def:20,doc:"Live updates stop once a cycle runs this percent past its expected duration (20 = up to 1.2x the expected time)."},{key:"notify_live_chronometer",label:"Use Live Chronometer",type:"checkbox",doc:"Show a live countdown timer in the notification (where supported) instead of a fixed estimate."},{key:"notify_live_silent",label:"Silent Live Updates",type:"checkbox",def:!0,doc:"iOS only. Update live progress silently, at a battery-saving priority that iOS may batch. The first update and the start/finish notifications still alert. Off: every update alerts."},{key:"notify_live_sticky",label:"Keep Live Notification On Tap",type:"checkbox",doc:"Android only. Keep the live progress notification when tapped. Off: a tap dismisses it."},{key:"notify_live_click_action",label:"Notification Tap Target",type:"text",optional:!0,doc:`What a tap on this appliance's notification opens. Blank: the WashData panel on this appliance. Enter a dashboard path (e.g. /lovelace/laundry) or URL, or "none" for no tap action.`},{key:"notify_timeout_seconds",label:"Auto-Dismiss After",unit:"s",type:"number",min:0,def:0,doc:"Dismiss the notification after this many seconds (where supported). 0 keeps it until you dismiss it."}]},{sub:"Messages",fields:[{key:"notify_title",label:"Notification Title",type:"text",def:"WashData: {device}",doc:"Notification title. Only {device} is substituted; any other variable falls back to the default title."},{key:"notify_icon",label:"Notification Icon",type:"text",def:"",doc:"Small icon on the notification (e.g. mdi:washing-machine). Sent only to mobile_app targets, not notify groups. On iOS it replaces the app icon and needs companion app 2026.8 or newer. Blank: platform default."},{key:"notify_icon_color",label:"Notification Colour",type:"color",def:"",placeholder:"#03A9F4",doc:"Accent colour for this appliance's notifications, so washer, dryer and dishwasher are easy to tell apart. On iOS it fills the circle behind the icon and colours the Live Activity bar. Many Android phones (Samsung especially) ignore it. Use hex, e.g. #03A9F4. Blank: platform default."},{key:"notify_start_message",label:"Start Message",type:"textarea",def:"{device} started.",doc:"Body sent when a cycle starts. Template variables: {device}, {program}. Any other variable falls back to the default text."},{key:"notify_finish_message",label:"Finish Message",type:"textarea",def:"{device} finished. Duration: {duration} min.",basic:!0,doc:"Body sent when a cycle finishes. Variables: {device}, {program}, {duration} (minutes), {duration_hm} (1 h 05 min), {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}, {status} (completed or force_stopped; interrupted cycles send nothing). Any other variable falls back to the default text."},{key:"notify_pre_complete_message",label:"Pre-Complete Message",type:"textarea",def:"{device}: Less than {minutes} minutes remaining.",doc:"Text of each live progress update. Template variables: {device}, {program}, {minutes} (minutes left). Any other variable falls back to the default text."},{key:"notify_reminder_message",label:"Reminder Message",type:"textarea",def:"",doc:"Text of the one-time Pre-End Alert. Template variables: {device}, {program}, {minutes} (the Pre-End Alert setting). Any other variable falls back to the default text."},{key:"notify_channel",label:"Android Channel (start/live)",type:"text",def:"",placeholder:"e.g. WashData",suggestions:["WashData","WashData Status","Appliance Status"],doc:"Android channel for start and live messages; the channel sets sound and priority. Blank: app default."},{key:"notify_finish_channel",label:"Android Channel (finish)",type:"text",def:"",placeholder:"e.g. WashData Finished",suggestions:["WashData Finished","WashData Alerts","Appliance Finished"],doc:"Android channel for the finish message. Blank reuses the start/live channel."},{key:"peak_rate_threshold",label:"Peak-Rate Threshold (per kWh)",type:"number",step:.001,min:0,def:0,clearable:!0,doc:"If the price per kWh is at or above this when a cycle starts, add a peak-rate tip to the start notification. 0 or blank disables it."},{key:"peak_rate_message",label:"Peak-Rate Message",type:"text",def:"",placeholder:"Running at peak rate ({price}/kWh).",doc:"Custom text for the peak-rate tip. Template variables: {device}, {price}. Blank uses the default."}]},{sub:"Cycle Timers",fields:[{key:"notify_cycle_timers",label:"Cycle Timers",type:"timerlist",doc:"Notifications at set minutes into a cycle (e.g. to add softener). Message supports {device}, {program}, {minutes}. With Auto-pause, the cycle also pauses there and the notification gets a Resume button."}]},{sub:"Quiet Hours & Milestones",fields:[{key:"notify_quiet_start_hour",label:"Quiet Hours Start",basic:!0,unit:"h",type:"number",min:0,max:23,clearable:!0,doc:"Start of a do-not-disturb window (0-23). Finish, reminder and clean-laundry notifications in this window are held until it ends. Can cross midnight (e.g. 22 to 7). Blank disables it."},{key:"notify_quiet_end_hour",label:"Quiet Hours End",basic:!0,unit:"h",type:"number",min:0,max:23,clearable:!0,doc:"End of the do-not-disturb window (0-23); held notifications are sent then. Blank disables it."},{key:"notify_milestones",label:"Cycle Milestones",type:"intlist",def:"50, 100, 500, 1000",placeholder:"50, 100, 500, 1000",doc:"Cycle counts that trigger a one-off celebration notification, comma-separated (e.g. 50, 100, 500, 1000). Blank disables them."},{key:"notify_milestone_message",label:"Milestone Message",type:"textarea",def:"{device} has completed {cycle_count} cycles!",doc:"Message for the milestone notification. Template variables: {device}, {cycle_count}."}]}]},{id:"ml_training",label:"ML Training",fields:[{key:"enable_ml_models",label:"Apply smart models during a cycle",type:"checkbox",def:!1,doc:"Use a model trained on this machine to predict a running cycle's energy and cost, and let a well-known program that stops unusually early finish without the full off delay. Off: power-based logic only."},{key:"ml_training_enabled",label:"Learn from this machine",type:"checkbox",def:!1,doc:"Retrain the energy model on this machine's finished cycles from time to time. A new model is kept only if it beats the simple estimate on test cycles."},{key:"ml_training_hour",label:"Learn at hour",unit:"h",type:"number",min:0,max:23,def:2,doc:"Hour of day (0-23) for overnight training. Pick a quiet hour, e.g. 2."},{key:"ml_training_min_cycles",label:"Cycles needed first",type:"number",min:5,def:30,doc:"Wait for at least this many recorded cycles before training."},{key:"ml_training_interval_days",label:"Check at most every",unit:"days",type:"number",min:1,def:7,doc:"Retrain at most once every this many days."}]}],wt={};for(const h of yt){const t=h.groups||[{fields:h.fields}];for(const e of t)for(const s of e.fields||[])wt[s.key]=s}const te={profile_match_min_duration_ratio:.1,profile_match_max_duration_ratio:1.8},Ae=[{keys:["off_delay","min_off_gap"],check:h=>h.off_delay!=null&&h.min_off_gap!=null&&h.min_off_gap>h.off_delay,fieldNotes:h=>({off_delay:{msgKey:"note.off_delay.effective",msgVars:{eff:h.min_off_gap,gap:h.min_off_gap,delay:h.off_delay},msgFb:`A finished cycle actually waits ${h.min_off_gap} s, not ${h.off_delay} s: Min Off Gap (${h.min_off_gap} s) is longer and takes precedence.`}})}],Me=new Set(["energy","energy_storage","power","gas","water","current","voltage"]),Le=new Set(["w","kw","mw","wh","kwh","mwh","va","kva","varh","a","ma","v","mv"]);function Nt(h,t,e){if(!h)return null;if(h===t.power_sensor)return{msgKey:"conflict.price_entity.power_sensor",msgVars:{},msgFb:"This is the device's Power Sensor, not a price. Cycles would be costed in watts per kWh; clear this field."};if(h===t.energy_sensor)return{msgKey:"conflict.price_entity.energy_sensor",msgVars:{},msgFb:"This is the device's Energy Meter, not a price. Cycles would be costed at the meter reading per kWh; clear this field."};const s=e&&e.stateOf?e.stateOf(h):null;if(!s)return null;const a=s.attributes||{},r=String(a.device_class==null?"":a.device_class).trim().toLowerCase();if(Me.has(r))return{msgKey:"conflict.price_entity.device_class",msgVars:{dc:r},msgFb:`This sensor measures ${r}, not a price per kWh. Clear this field or pick a tariff sensor.`};const i=String(a.unit_of_measurement==null?"":a.unit_of_measurement).trim();return Le.has(i.toLowerCase())?{msgKey:"conflict.price_entity.unit",msgVars:{unit:i},msgFb:`This sensor reads in ${i}, not a price per kWh. Clear this field or pick a tariff sensor.`}:null}const ee=[{keys:["energy_price_entity","energy_sensor","power_sensor"],check:(h,t)=>Nt(h.energy_price_entity,h,t)!=null,fieldErrors:(h,t)=>({energy_price_entity:Nt(h.energy_price_entity,h,t)})},{keys:["start_threshold_w","stop_threshold_w"],check:h=>h.start_threshold_w!=null&&h.stop_threshold_w!=null&&h.start_threshold_w<=h.stop_threshold_w,fieldErrors:h=>({start_threshold_w:{msgKey:"conflict.hysteresis.start",msgVars:{stop:h.stop_threshold_w},msgFb:`Must be above Stop Threshold (${h.stop_threshold_w} W)`,fixVal:+Math.max(h.stop_threshold_w+.5,h.stop_threshold_w*1.25).toFixed(1)},stop_threshold_w:{msgKey:"conflict.hysteresis.stop",msgVars:{start:h.start_threshold_w},msgFb:`Must be below Start Threshold (${h.start_threshold_w} W)`,fixVal:+Math.min(h.start_threshold_w-.5,h.start_threshold_w*.8).toFixed(1)}})},{keys:["min_power","stop_threshold_w"],check:h=>h.min_power!=null&&h.stop_threshold_w!=null&&h.min_power>h.stop_threshold_w,fieldErrors:h=>({min_power:{msgKey:"conflict.min_power.min_power",msgVars:{stop:h.stop_threshold_w},msgFb:`Must be at or below Stop Threshold (${h.stop_threshold_w} W)`,fixVal:+(h.stop_threshold_w*.8).toFixed(1)},stop_threshold_w:{msgKey:"conflict.min_power.stop",msgVars:{min:h.min_power},msgFb:`Must be at or above Min Power (${h.min_power} W)`,fixVal:+(h.min_power*1.25).toFixed(1)}})},{keys:["power_off_threshold_w","stop_threshold_w"],check:h=>h.power_off_threshold_w!=null&&h.power_off_threshold_w>0&&h.stop_threshold_w!=null&&h.power_off_threshold_w>=h.stop_threshold_w,fieldErrors:h=>({power_off_threshold_w:{msgKey:"conflict.power_off.threshold",msgVars:{stop:h.stop_threshold_w},msgFb:`Must be below Stop Threshold (${h.stop_threshold_w} W) to take effect`,fixVal:+(h.stop_threshold_w*.6).toFixed(1)},stop_threshold_w:{msgKey:"conflict.power_off.stop",msgVars:{pot:h.power_off_threshold_w},msgFb:`Must be above Power Off Threshold (${h.power_off_threshold_w} W)`,fixVal:+(h.power_off_threshold_w*1.67).toFixed(1)}})},{keys:["off_delay","min_off_gap"],check:h=>h.off_delay!=null&&h.min_off_gap!=null&&h.off_delay>h.min_off_gap,fieldErrors:h=>({off_delay:{msgKey:"conflict.off_delay.off_delay",msgVars:{gap:h.min_off_gap},msgFb:`Off Delay (${h.off_delay} s) overrides Min Off Gap (${h.min_off_gap} s); cycles within the gap may merge`,fixVal:h.min_off_gap},min_off_gap:{msgKey:"conflict.off_delay.gap",msgVars:{delay:h.off_delay},msgFb:`Min Off Gap should be at least Off Delay (${h.off_delay} s)`,fixVal:h.off_delay}})},{keys:["watchdog_interval","sampling_interval"],check:h=>h.watchdog_interval!=null&&h.sampling_interval!=null&&h.watchdog_interval<2*h.sampling_interval,fieldErrors:h=>({watchdog_interval:{msgKey:"conflict.watchdog.interval",msgVars:{si:h.sampling_interval},msgFb:`Should be at least 2\xD7 Sampling Interval (${h.sampling_interval} s)`,fixVal:+(2*h.sampling_interval+1)},sampling_interval:{msgKey:"conflict.watchdog.sampling",msgVars:{wi:h.watchdog_interval},msgFb:`Sampling Interval should be at most half of Watchdog Interval (${h.watchdog_interval} s)`,fixVal:+Math.floor(h.watchdog_interval/2)}})},{keys:["no_update_active_timeout","watchdog_interval"],check:h=>h.no_update_active_timeout!=null&&h.watchdog_interval!=null&&h.no_update_active_timeout<=h.watchdog_interval,fieldErrors:h=>({no_update_active_timeout:{msgKey:"conflict.no_update_timeout.timeout",msgVars:{wi:h.watchdog_interval},msgFb:`Must be greater than Watchdog Interval (${h.watchdog_interval} s)`,fixVal:h.watchdog_interval*2},watchdog_interval:{msgKey:"conflict.no_update_timeout.watchdog",msgVars:{to:h.no_update_active_timeout},msgFb:`Must be less than No-Update Timeout (${h.no_update_active_timeout} s)`,fixVal:+Math.floor(h.no_update_active_timeout/2)}})},{keys:["start_duration_threshold","sampling_interval"],check:h=>h.start_duration_threshold!=null&&h.sampling_interval!=null&&h.start_duration_threshold({start_duration_threshold:{msgKey:"conflict.start_dur.threshold",msgVars:{si:h.sampling_interval},msgFb:`Should be at least one Sampling Interval (${h.sampling_interval} s) to prevent single-sample false starts`,fixVal:h.sampling_interval},sampling_interval:{msgKey:"conflict.start_dur.sampling",msgVars:{sdt:h.start_duration_threshold},msgFb:`Sampling Interval exceeds Start Duration (${h.start_duration_threshold} s); single-sample spikes can open a cycle`,fixVal:h.start_duration_threshold}})},{keys:["learning_confidence","profile_match_threshold"],check:h=>h.learning_confidence!=null&&h.profile_match_threshold!=null&&h.learning_confidence({learning_confidence:{msgKey:"conflict.confidence.learning",msgVars:{match:h.profile_match_threshold},msgFb:`Must be at or above Match Threshold (${h.profile_match_threshold})`,fixVal:+h.profile_match_threshold.toFixed(2)},profile_match_threshold:{msgKey:"conflict.confidence.match_for_learning",msgVars:{lc:h.learning_confidence},msgFb:`Must be at or below Learning Confidence (${h.learning_confidence})`,fixVal:+h.learning_confidence.toFixed(2)}})},{keys:["profile_match_threshold","auto_label_confidence"],check:h=>h.profile_match_threshold!=null&&h.auto_label_confidence!=null&&h.profile_match_threshold>h.auto_label_confidence,fieldErrors:h=>({profile_match_threshold:{msgKey:"conflict.confidence.match_for_auto",msgVars:{alc:h.auto_label_confidence},msgFb:`Must be at or below Auto-Label Confidence (${h.auto_label_confidence})`,fixVal:+h.auto_label_confidence.toFixed(2)},auto_label_confidence:{msgKey:"conflict.confidence.auto",msgVars:{match:h.profile_match_threshold},msgFb:`Must be at or above Match Threshold (${h.profile_match_threshold})`,fixVal:+h.profile_match_threshold.toFixed(2)}})},{keys:["profile_unmatch_threshold","profile_match_threshold"],check:h=>h.profile_unmatch_threshold!=null&&h.profile_match_threshold!=null&&h.profile_unmatch_threshold>=h.profile_match_threshold,fieldErrors:h=>({profile_unmatch_threshold:{msgKey:"conflict.unmatch.unmatch",msgVars:{match:h.profile_match_threshold},msgFb:`Must be below Match Threshold (${h.profile_match_threshold}); otherwise a committed match un-matches instantly`,fixVal:+(h.profile_match_threshold-.05).toFixed(2)},profile_match_threshold:{msgKey:"conflict.unmatch.match",msgVars:{un:h.profile_unmatch_threshold},msgFb:`Must be above Unmatch Threshold (${h.profile_unmatch_threshold})`,fixVal:+(h.profile_unmatch_threshold+.05).toFixed(2)}})},{keys:["anti_wrinkle_max_power","start_threshold_w"],check:h=>["washing_machine","dryer","washer_dryer"].includes(h.device_type)&&h.anti_wrinkle_max_power!=null&&h.start_threshold_w!=null&&h.anti_wrinkle_max_power<=h.start_threshold_w,fieldErrors:h=>({anti_wrinkle_max_power:{msgKey:"conflict.anti_wrinkle_max.max",msgVars:{start:h.start_threshold_w},msgFb:`Must be above Start Threshold (${h.start_threshold_w} W); otherwise anti-wrinkle duration limit is bypassed`,fixVal:+(h.start_threshold_w*2).toFixed(0)},start_threshold_w:{msgKey:"conflict.anti_wrinkle_max.start",msgVars:{max:h.anti_wrinkle_max_power},msgFb:`Must be below Anti-Wrinkle Max Power (${h.anti_wrinkle_max_power} W)`,fixVal:+(h.anti_wrinkle_max_power*.5).toFixed(1)}})},{keys:["pump_stuck_duration","no_update_active_timeout"],check:h=>h.device_type==="pump"&&h.pump_stuck_duration!=null&&h.no_update_active_timeout!=null&&h.no_update_active_timeout<=h.pump_stuck_duration,fieldErrors:h=>({pump_stuck_duration:{msgKey:"conflict.pump_stuck.duration",msgVars:{to:h.no_update_active_timeout},msgFb:`Must be below No-Update Timeout (${h.no_update_active_timeout} s) so the stuck alarm can fire first`,fixVal:h.no_update_active_timeout-60},no_update_active_timeout:{msgKey:"conflict.pump_stuck.timeout",msgVars:{ps:h.pump_stuck_duration},msgFb:`Must exceed Pump Stuck Duration (${h.pump_stuck_duration} s) so the stuck alarm can fire first`,fixVal:h.pump_stuck_duration+60}})},{keys:["profile_match_min_duration_ratio","profile_match_max_duration_ratio"],check:h=>h.profile_match_min_duration_ratio!=null&&h.profile_match_max_duration_ratio!=null&&h.profile_match_min_duration_ratio>=h.profile_match_max_duration_ratio,fieldErrors:h=>({profile_match_min_duration_ratio:{msgKey:"conflict.duration_ratio.min",msgVars:{max:h.profile_match_max_duration_ratio},msgFb:`Must be less than Max Duration Ratio (${h.profile_match_max_duration_ratio})`,fixVal:+(h.profile_match_max_duration_ratio*.5).toFixed(2)},profile_match_max_duration_ratio:{msgKey:"conflict.duration_ratio.max",msgVars:{min:h.profile_match_min_duration_ratio},msgFb:`Must be greater than Min Duration Ratio (${h.profile_match_min_duration_ratio})`,fixVal:+(h.profile_match_min_duration_ratio*2).toFixed(2)}})},{keys:["end_energy_threshold","stop_threshold_w","off_delay"],check:h=>h.end_energy_threshold!=null&&h.stop_threshold_w!=null&&h.off_delay!=null&&h.off_delay>0&&h.end_energy_threshold({end_energy_threshold:{msgKey:"conflict.end_energy.energy",msgVars:{w:h.stop_threshold_w,d:h.off_delay},msgFb:`Too strict for Stop Threshold (${h.stop_threshold_w} W) over Off Delay (${h.off_delay} s); the cycle can only end through a fallback path`,fixVal:Math.ceil(h.stop_threshold_w*h.off_delay/3600*1e3)/1e3},stop_threshold_w:{msgKey:"conflict.end_energy.stop",msgVars:{e:h.end_energy_threshold,d:h.off_delay,p:+(h.end_energy_threshold*3600/h.off_delay).toFixed(2)},msgFb:`End Energy Threshold (${h.end_energy_threshold} Wh over ${h.off_delay} s) only permits ${+(h.end_energy_threshold*3600/h.off_delay).toFixed(2)} W`,fixVal:Math.floor(h.end_energy_threshold*3600/h.off_delay*10)/10}})}],De=` :host { /* The panel owns the full height it is given and scrolls INTERNALLY (.wd-main). HA renders a non-iframe custom panel into a plain container that sets only @@ -151,6 +151,18 @@ const y="ha_washdata",Zt=(()=>{try{return new URL(import.meta.url).searchParams. .wd-table tr:last-child td { border-bottom: none; } .wd-table tbody tr:hover td { background: var(--secondary-background-color); } .wd-table-wrap { overflow-x: auto; } +/* Audit UI-20: a horizontal scroller that hides content past an edge gets a shadow on + that edge, so on a phone the tab strip and wide tables visibly continue instead of + looking cut off. A shadow rather than a fade: past a table's last visible column + there is usually only whitespace, where a fade shows nothing. _syncScrollAffordances + sets the classes only while there really is more content on that side; backgrounds + do not affect layout, and nothing overflows at desktop widths, so the desktop view + is unchanged. The background of a scroll container does not scroll with it, so the + shadow stays on the visible edge. Mid-grey reads on light and dark themes alike. */ +.wd-ovf-l, .wd-ovf-r { background-repeat: no-repeat; background-size: 22px 100%; } +.wd-ovf-r { background-image: linear-gradient(to left, rgba(127,127,127,.42), rgba(127,127,127,0)); background-position: right center; } +.wd-ovf-l { background-image: linear-gradient(to right, rgba(127,127,127,.42), rgba(127,127,127,0)); background-position: left center; } +.wd-ovf-l.wd-ovf-r { background-image: linear-gradient(to right, rgba(127,127,127,.42), rgba(127,127,127,0)), linear-gradient(to left, rgba(127,127,127,.42), rgba(127,127,127,0)); background-position: left center, right center; } .wd-th-sort { cursor: pointer; user-select: none; white-space: nowrap; } .wd-th-sort:hover { color: var(--primary-color); } .wd-tc-date { white-space: nowrap; color: var(--secondary-text-color); font-size: .82em; } @@ -259,6 +271,9 @@ th.wd-tc-flags { color: var(--secondary-text-color); font-weight: 500; } .wd-pillbox .wd-pill { display: inline-flex; align-items: center; gap: 4px; max-width: 100%; padding: 2px 4px 2px 9px; font-size: .82em; line-height: 1.4; border-radius: var(--wd-radius-lg); background: var(--primary-color); color: var(--wd-white); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +/* The label shrinks and ellipsizes; the remove button never does (#467: a long + notify service pushed it out of the clipped pill). */ +.wd-pill-txt { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .wd-pill-x { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; padding: 0; border: 0; border-radius: 50%; background: rgba(255,255,255,.25); color: var(--wd-white); font-size: 13px; line-height: 1; cursor: pointer; flex: none; } @@ -298,7 +313,8 @@ th.wd-tc-flags { color: var(--secondary-text-color); font-weight: 500; } box-shadow: 0 4px 18px rgba(0,0,0,.35); z-index: 60; text-align: left; font-weight: 400; text-transform: none; letter-spacing: normal; } -.wd-tip:hover .wd-tip-pop { display: block; } +.wd-tip:hover .wd-tip-pop, .wd-tip:focus .wd-tip-pop { display: block; } +.wd-tip:focus-visible { outline: 2px solid var(--primary-color); outline-offset: 1px; } .wd-tip-txt { font-size: 12px; line-height: 1.5; display: block; } .wd-dg { display: block; width: 100%; height: auto; margin-bottom: 8px; background: var(--secondary-background-color); border-radius: 6px; } .wd-dg .ln { fill: none; stroke: var(--primary-color); stroke-width: 2.5; } @@ -326,9 +342,10 @@ th.wd-tc-flags { color: var(--secondary-text-color); font-weight: 500; } padding: 2px 7px; border-radius: 10px; white-space: nowrap; } .wd-sug-chip-obs { background: rgba(255,152,0,.22); } -.wd-sug-chip-cal { background: rgba(33,150,243,.18); } .wd-sug-val { font-weight: 700; flex-shrink: 0; } .wd-sug-impact-line { flex-basis: 100%; font-size: .86em; opacity: .70; font-style: italic; margin-top: 2px; } +.wd-confirm-list { margin: 0 0 12px; padding-left: 18px; max-height: 50vh; overflow-y: auto; } +.wd-confirm-list li { margin: 4px 0; } .wd-sug-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } .wd-sug-sep { display: none; } .wd-sug-impact { display: none; } @@ -439,6 +456,10 @@ button.wd-profile-card { display: block; } .wd-star-btn:hover { transform: scale(1.15); } /* Share-device selection tree (profile -> its reference cycles). */ .wd-sd-tree { display: flex; flex-direction: column; gap: 8px; max-height: 44vh; overflow-y: auto; margin-bottom: 16px; } +/* #460: a group's overflow:hidden drops its automatic min-height to 0, so past the + max-height above the groups shrank and clipped their own rows instead of the tree + scrolling (a phone hits the cap at once). Rows keep their height; the tree scrolls. */ +.wd-sd-tree > * { flex-shrink: 0; } .wd-sd-group { border: 1px solid var(--divider-color); border-radius: var(--wd-radius-md); background: var(--secondary-background-color); overflow: hidden; } .wd-sd-prof { display: flex; align-items: center; gap: 8px; padding: 9px 12px; cursor: pointer; font-weight: 600; } .wd-sd-prof-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } @@ -465,6 +486,7 @@ button.wd-profile-card { display: block; } .wd-gear-body { margin-top: 12px; } .wd-empty { text-align: center; padding: 48px 24px; color: var(--secondary-text-color); } .wd-empty .wd-icon { font-size: 3em; margin-bottom: 10px; } +.wd-stale { margin: 6px 0 0; padding: 6px 12px; border-radius: var(--wd-radius-md); background: var(--secondary-background-color); border: 1px solid var(--warning-color, #ffa600); color: var(--primary-text-color); font-size: .85em; } .wd-error-state { display: flex; align-items: center; gap: 10px; padding: 10px 14px; margin-bottom: 10px; border-radius: var(--wd-radius-md); background: var(--secondary-background-color); border: 1px solid var(--divider-color); color: var(--error-color, #b71c1c); font-size: .9em; } .wd-info { font-size: .9em; color: var(--secondary-text-color); line-height: 1.6; margin: 0; } .wd-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.5); z-index: 100; display: flex; align-items: center; justify-content: center; } @@ -557,7 +579,6 @@ button.wd-profile-card { display: block; } .wd-devsub { font-size: .72em; color: var(--secondary-text-color); } .wd-dbadge { font-size: .72em; padding: 1px 7px; border-radius: 10px; background: var(--secondary-background-color); } .wd-dbadge.rec { background: var(--error-color, #f44336); color: var(--wd-white); } -.wd-dbadge.sug { background: rgba(255,152,0,.22); } .wd-dbadge.fb { background: rgba(33,150,243,.22); } .wd-dbadge.conf { background: rgba(183,28,28,.18); color: var(--error-color, #b71c1c); } /* Attention cards (status dashboard) */ @@ -651,6 +672,7 @@ button.wd-profile-card { display: block; } .wd-tab { padding: 9px 13px; } .wd-modal { padding: 16px; width: calc(100% - 18px); } .wd-modal-lg { max-width: 100%; } + .wd-sd-prof-name { white-space: normal; overflow-wrap: anywhere; } /* #460: wrap, never ellipsise a choice */ .wd-canvas-wrap canvas { height: 200px; } .wd-zoom-reset { width: 36px; height: 36px; top: 8px; right: 8px; font-size: 17px; } .wd-gtip { white-space: normal; max-width: calc(100vw - 24px); } @@ -722,11 +744,6 @@ button.wd-profile-card { display: block; } .wd-pg-param-lbl { flex: 1; font-size: .83em; color: var(--secondary-text-color); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .wd-pg-param-inp { width: 76px; flex: 0 0 76px; } .wd-pg-param-drag { font-size: .75em; color: var(--primary-color); cursor: default; flex: 0 0 12px; } -.wd-pg-score-bar-row { display: flex; align-items: center; gap: 6px; font-size: .82em; margin: 2px 0; } -.wd-pg-score-bar-lbl { flex: 0 0 80px; color: var(--secondary-text-color); } -.wd-pg-score-bar-track { flex: 1; height: 6px; background: var(--secondary-background-color); border-radius: 3px; overflow: hidden; } -.wd-pg-score-bar-fill { height: 100%; border-radius: 3px; } -.wd-pg-score-bar-val { flex: 0 0 42px; text-align: right; font-variant-numeric: tabular-nums; color: var(--secondary-text-color); } .wd-pg-cand-row { display: flex; align-items: center; gap: 6px; font-size: .82em; margin: 3px 0; } .wd-pg-cand-name { flex: 0 0 110px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .wd-pg-cand-track { flex: 1; height: 7px; background: var(--secondary-background-color); border-radius: var(--wd-radius-sm); overflow: hidden; } @@ -795,109 +812,96 @@ button.wd-profile-card { display: block; } @media (max-width: 640px) { .wd-pg-strip { gap: 7px; font-size: .82em; } } -`;function gt(h){if(h==null||h<0)return"-";const t=Math.floor(h/3600),e=Math.floor(h%3600/60),s=Math.floor(h%60);return t>0?`${t}h ${e}m`:e>0?`${e}m ${s}s`:`${s}s`}function Ot(h){return h==null?"-":h>=100?`${Math.round(h)} W`:`${h.toFixed(1)} W`}function Vt(h,t){const e=Math.abs(t||h||0),s=e>0?Math.min(4,Math.max(0,Math.ceil(-Math.log10(e))+1)):0;return`${Number(h.toFixed(s))}W`}function ke(h,t){const e=Math.abs(h)/Math.max(1,t);if(!(e>0)||!isFinite(e))return 1;const s=Math.pow(10,Math.floor(Math.log10(e))),r=e/s;return(r<=1?1:r<=2?2:r<=2.5?2.5:r<=5?5:10)*s}function bt(h){return h==null?"-":`${h.toFixed(2)} kWh`}let se="relative";const Se=3700;function ie(h){const t=e=>String(e).padStart(2,"0");return`${h.getFullYear()}-${t(h.getMonth()+1)}-${t(h.getDate())}`}function ae(h){const t=new Date;return t.setDate(t.getDate()-h),ie(t)}function Te(){return ie(new Date)}function Ut(){return ae(10)}function Ee(){return ae(Se-1)}function Ce(h){const t=Math.round((h-Date.now())/1e3);let e;try{e=new Intl.RelativeTimeFormat(void 0,{numeric:"auto"})}catch{return ne(h)}const s=Math.abs(t),r=[["year",31536e3],["month",2592e3],["week",604800],["day",86400],["hour",3600],["minute",60]];for(const[n,a]of r)if(s>=a)return e.format(Math.round(t/a),n);return e.format(t,"second")}function ne(h){return new Date(h).toLocaleString(void 0,{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"})}function ot(h,t){if(!h)return"-";let e;if(typeof h=="number")e=h>=1e12?h:h*1e3;else{const s=String(h),r=/^(\d{4})-(\d{2})-(\d{2})$/.exec(s);e=r?new Date(+r[1],+r[2]-1,+r[3]).getTime():new Date(s).getTime()}return isNaN(e)?"-":(t||se)==="relative"?Ce(e):ne(e)}function It(h){if(h==null)return"unknown error";if(h instanceof Error)return`${h.name}: ${h.message}`;const t=h.code!=null?String(h.code):"",e=h.message!=null?String(h.message):"";if(!t&&!e)try{return JSON.stringify(h)}catch{return String(h)}const s=t==="unknown_command"?" (the integration is probably still starting up; this should stop on its own)":"";return`${t||"error"}${e?": "+e:""}${s}`}function d(h){return String(h??"").replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function Pe(h){const t=String(h??"").trim();return/^https?:\/\//i.test(t)?t:""}function At(h,t){const e=String(h??"").trim();let s=e.match(/^#([0-9a-fA-F]{3,8})$/);if(s){let r=s[1];if((r.length===3||r.length===4)&&(r=r.split("").map(n=>n+n).join("")),r.length===6||r.length===8){const n=parseInt(r.slice(0,2),16),a=parseInt(r.slice(2,4),16),o=parseInt(r.slice(4,6),16);return`rgba(${n}, ${a}, ${o}, ${t})`}}if(s=e.match(/^rgba?\(([^)]+)\)$/i),s){const r=s[1].split(",").map(n=>n.trim());if(r.length>=3)return`rgba(${r[0]}, ${r[1]}, ${r[2]}, ${t})`}return e}function re(h,t){const e=parseFloat(h);return isNaN(e)?t:e}function Gt(h){return h?Array.from(h.querySelectorAll('a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])')).filter(e=>e.getClientRects().length>0):[]}function Mt(h){return h==null||h===""?"(none)":h===!0?"on":h===!1?"off":Array.isArray(h)?h.join(", ")||"(none)":String(h)}function oe(h,t,e){return h.slice().sort((s,r)=>{const n=t(s),a=t(r);return n==null&&a==null?0:n==null?1:a==null?-1:(na?1:0)*e})}function xt(h,t,e,s,r,n,a){const o=e?s===1?" \u25B2":" \u25BC":' \u2195',i=n==="right"?"text-align:right;":"",l=a?` title="${d(a)}"`:"";return`${h}${o}`}function Bt(h){h=Math.max(0,Math.round(h));const t=Math.floor(h/3600),e=Math.floor(h%3600/60),s=h%60;return t>0?`${t}:${String(e).padStart(2,"0")}:${String(s).padStart(2,"0")}`:`${e}:${String(s).padStart(2,"0")}`}function le(h,t){const e={pause:["lbl.artifact_interruption","Interruption"],dip:["lbl.artifact_low_power","Low power"],spike:["lbl.artifact_high_power","High power"]},[s,r]=e[h]||["lbl.artifact_anomaly","Anomaly"];return t?t(s,{},r):r}function de(h){return h.toLowerCase().replace(/[\s&/\-]+/g,"_").replace(/^_+|_+$/g,"").replace(/_+/g,"_")}function ce(h){let t=String(h??"").trim().toLowerCase();try{t=t.normalize("NFKD").replace(/[\u0300-\u036f]/g,"")}catch{}return t.replace(/[^\p{L}\p{N}]+/gu,"-").replace(/^-+|-+$/g,"")}function he(h){const t=new Set;return String(h??"").split(",").forEach(e=>{const s=parseInt(e.trim(),10);Number.isFinite(s)&&s>0&&t.add(s)}),Array.from(t).sort((e,s)=>e-s)}function Ft(h,t){if(!h||!h.length)return null;if(t<=h[0][0])return h[0][1];if(t>=h[h.length-1][0])return h[h.length-1][1];for(let e=1;e=t){const s=h[e-1],r=h[e],n=r[0]-s[0]||1;return s[1]+(r[1]-s[1])*((t-s[0])/n)}return h[h.length-1][1]}const Kt=/^#[0-9a-fA-F]{6}$/,Ae="#03A9F4";function pe(h){return Kt.test(String(h||""))?String(h):Ae}function Me(h,t,e){e=e||{};const s=h.key,r=h.unit?`${h.label} (${h.unit})`:h.label,n=h.unit?` ${h.unit}`:"",a=h.doc?dt(h.doc,h.diagram||De[s]):"",o=e.changed?``:"";if(h.type==="checkbox")return`
    ${o}${a}
    ${h.hint?`
    ${d(h.hint)}
    `:""}
    `;let i="";const l=t??"";if(h.type==="select"||h.type==="devicetype"||h.type==="device"){const k=(e.opts||[]).map(([D,I])=>``).join("");i=``}else if(h.type==="textarea")i=``;else if(h.type==="json"){const v=l===""||l==null?"":typeof l=="string"?l:JSON.stringify(l,null,2);i=``}else if(h.type==="checkboxlist"){const v=Array.isArray(t)&&t.length?t.map(String):Array.isArray(h.def)?h.def.map(String):[];i=`
    `+(h.choices||[]).map(([k,D])=>``).join("")+"
    "}else if(h.type==="entitylist"){const k=(Array.isArray(t)?t:t?[t]:[]).map(D=>`${d(D)}`).join("");i=`
    ${k}
    `}else if(h.type==="timerlist"){const v=Array.isArray(l)?l:[],k=e.t?e.t("lbl.timer_min",{},"min"):"min",D=e.t?e.t("lbl.timer_msg_placeholder",{},"Message (optional, {device}/{program}/{minutes})"):"Message (optional, {device}/{program}/{minutes})",I=e.t?e.t("lbl.timer_auto_pause",{},"Auto-pause"):"Auto-pause",z=e.t?e.t("btn.remove_timer",{},"Delete"):"Delete",R=e.t?e.t("btn.add_timer",{},"+ Add timer"):"+ Add timer",L=(C,A)=>{const F=C&&C.offset_minutes?String(C.offset_minutes):"",j=C&&C.message?d(C.message):"",st=C&&C.auto_pause?" checked":"";return`
    `},P=v.map((C,A)=>L(C,A)).join("");i=`
    ${P}
    `}else if(h.type==="color"){const v=h.placeholder?` placeholder="${d(h.placeholder)}"`:"",k=Kt.test(String(l))?String(l):pe(h.placeholder),D=e.t?e.t("btn.clear",{},"Clear"):"Clear";i=`
    `}else if(h.type==="entity"){const v=h.placeholder?` placeholder="${d(h.placeholder)}"`:"";i=`
    `}else if(h.type==="list"){const v=Array.isArray(l)?l.join(", "):d(l);i=``}else if(h.type==="intlist"){const v=Array.isArray(l)?l.join(", "):String(l??""),k=h.placeholder?` placeholder="${d(h.placeholder)}"`:"";i=``}else{const v=h.type==="number"?"number":"text",k=e.datalistId?` list="${e.datalistId}"`:"",D=h.step!=null?` step="${h.step}"`:"",I=h.min!=null?` min="${h.min}"`:"",z=h.max!=null?` max="${h.max}"`:"",R=h.placeholder?` placeholder="${d(h.placeholder)}"`:"";i=`${e.datalist||""}`}const c=e.suggestion,g=e.mlSuggestion,p=c&&c.suggested!=null&&!Xt(c.suggested,t)?c.suggested:null,f=g&&g.value!=null&&!Xt(g.value,t)?g.value:null,u=e.t,$=c?c.reason_key?u(c.reason_key,c.reason_params||{},c.reason||""):c.reason||"":"",S=g?g.reason_key?u(g.reason_key,g.reason_params||{},g.reason||""):g.reason||"":"",x=v=>``;let w="";if(p!=null&&f!=null){const v=parseFloat(p),k=parseFloat(f);if((!isNaN(v)&&!isNaN(k)?Math.abs(v-k)/Math.max(Math.abs(v),Math.abs(k),1e-9):1)<.05){const I=u("suggestion.calibrated_label",{},"Calibrated"),z=dt([$,S?`${I}: ${S}`:""].filter(Boolean).join(` - -`));w=`
    \u{1F4A1} ${d(u("suggestion.both_agree",{},"WashData recommends"))}${d(p)}${n}${x(p)}${z}
    `}else{const I=$?dt($):"",z=S?dt(S):"",R=u("suggestion.observed_label",{},"Observed"),L=u("suggestion.calibrated_label",{},"Calibrated");let P="",C="";if(!isNaN(v)&&!isNaN(k)){const j=k>v;C=u(`suggestion.impact.${s}.${j?"higher":"lower"}`,{},""),P=u(`suggestion.impact.${s}.${j?"lower":"higher"}`,{},"")}const A=P?`
    ${d(P)}
    `:"",F=C?`
    ${d(C)}
    `:"";w=`
    \u{1F4A1} ${d(R)}${d(p)}${n}${x(p)}${I}${A}
    \u{1F916} ${d(L)}${d(f)}${n}${x(f)}${z}${F}
    `}}else if(p!=null){const v=$?dt($):"",k=t!=null&&t!==""?` (now ${d(t)}${n})`:"";w=`
    \u{1F4A1} ${d(u("suggestion.observed_label",{},"Observed"))}${d(p)}${n}${k}${x(p)}${v}
    `}else if(f!=null){const v=S?dt(S):"";w=`
    \u{1F916} ${d(u("suggestion.calibrated_label",{},"Calibrated"))}${d(f)}${n}${x(f)}${v}
    `}if(w&&s){const v=d(u("btn.mute_suggestion",{},"Stop suggesting this setting")),k=``;w=w.replace(/<\/div>\s*$/,k+"
    ")}return`
    ${o}${a}
    ${i}${h.hint?`
    ${d(h.hint)}
    `:""}${w}
    `}function Xt(h,t){if(h==null||t==null)return!1;const e=parseFloat(h),s=parseFloat(t);return!isNaN(e)&&!isNaN(s)?Math.abs(e-s)<1e-6:String(h)===String(t)}const De={min_power:"min_power",off_delay:"off_delay",smoothing_window:"smoothing",start_threshold_w:"hysteresis",stop_threshold_w:"hysteresis",start_energy_threshold:"start_energy",running_dead_zone:"dead_zone",profile_duration_tolerance:"duration_tolerance",profile_match_min_duration_ratio:"match_ratios",profile_match_max_duration_ratio:"match_ratios",progress_reset_delay:"progress_reset",completion_min_seconds:"min_duration",min_off_gap:"min_off_gap",start_duration_threshold:"start_duration",end_energy_threshold:"end_energy_thresh",end_repeat_count:"end_repeat",profile_match_threshold:"confidence",profile_unmatch_threshold:"confidence",auto_label_confidence:"confidence",learning_confidence:"confidence",no_update_active_timeout:"watchdog_timeout",anti_wrinkle_enabled:"anti_wrinkle",anti_wrinkle_max_power:"anti_wrinkle",anti_wrinkle_max_duration:"anti_wrinkle",anti_wrinkle_exit_power:"anti_wrinkle",anti_wrinkle_idle_timeout:"anti_wrinkle",anti_crease_finalize_ratio:"anti_wrinkle",sampling_interval:"sampling"};function _e(h){let t=0,e=window.innerWidth,s=0,r=window.innerHeight;for(let n=h.parentElement;n;n=n.parentElement){const a=getComputedStyle(n),o=a.overflowX!=="visible",i=a.overflowY!=="visible";if(!o&&!i)continue;const l=n.getBoundingClientRect();o&&(l.left>t&&(t=l.left),l.rights&&(s=l.top),l.bottomi${t?Le(t):""}${d(h)}`}function Yt(h,t,e=""){return`${e||""}`}function Dt(h,t,e="",s=""){return`
    ${Yt(h,t,e)}
    ${s?`
    ${d(s)}
    `:""}
    `}function Le(h){const t=s=>`${s}`,e='';switch(h){case"smoothing":return t(`${e} - - - raw vs smoothed`);case"min_power":return t(`${e} +`;function ht(h){const t=Number(h);if(h==null||!isFinite(t)||t<0)return"-";h=t;const e=Math.floor(h/3600),s=Math.floor(h%3600/60),a=Math.floor(h%60);return e>0?`${e}h ${s}m`:s>0?`${s}m ${a}s`:`${a}s`}function Lt(h){const t=Number(h);return h==null||!isFinite(t)?"-":t>=100?`${it(Math.round(t),0)} W`:`${it(t,1)} W`}function Ht(h,t){const e=Math.abs(t||h||0),s=e>0?Math.min(4,Math.max(0,Math.ceil(-Math.log10(e))+1)):0;return`${Number(h.toFixed(s))}W`}function Ie(h,t){const e=Math.abs(h)/Math.max(1,t);if(!(e>0)||!isFinite(e))return 1;const s=Math.pow(10,Math.floor(Math.log10(e))),a=e/s;return(a<=1?1:a<=2?2:a<=2.5?2.5:a<=5?5:10)*s}function gt(h){return h==null||!isFinite(Number(h))?"-":`${it(h,2)} kWh`}let se="relative",Et,jt,Wt=!0,qt;const ie=new Map;function Re(h,t){const e=t||{};Et=h||void 0;const s=e.number_format;jt=s==="comma_decimal"?["en-US","en"]:s==="decimal_comma"?["de","es","it"]:s==="space_comma"?["fr","sv","cs"]:s==="quote_decimal"?["de-CH"]:s==="system"?void 0:h||void 0,Wt=s!=="none",qt=e.time_format==="12"?!0:e.time_format==="24"?!1:void 0}function ae(h){const t=JSON.stringify([jt,Wt,h]);let e=ie.get(t);if(!e){const s=Object.assign({useGrouping:Wt},h);try{e=new Intl.NumberFormat(jt,s)}catch{e=new Intl.NumberFormat(void 0,s)}ie.set(t,e)}return e}function it(h,t=0){const e=Number(h);return isFinite(e)?ae({minimumFractionDigits:t,maximumFractionDigits:t}).format(e):"-"}function Dt(h){const t=Number(h);return isFinite(t)&&t>0?Math.floor(t):0}function It(h,t){const e=Number(h);if(h==null||!isFinite(e))return"-";if(t)try{return ae({style:"currency",currency:t}).format(e)}catch{}return`${it(e,2)}${t?" "+t:""}`}function ne(h){return qt===void 0?h:Object.assign({hour12:qt},h)}function re(h){const t=ne({hour:"2-digit",minute:"2-digit",second:"2-digit"});try{return h.toLocaleTimeString(Et,t)}catch{return h.toLocaleTimeString(void 0,t)}}function Oe(h){const t=new Date(h);try{return t.toLocaleDateString(Et)}catch{return t.toLocaleDateString()}}const ze=3700;function oe(h){const t=e=>String(e).padStart(2,"0");return`${h.getFullYear()}-${t(h.getMonth()+1)}-${t(h.getDate())}`}function le(h){const t=new Date;return t.setDate(t.getDate()-h),oe(t)}function Be(){return oe(new Date)}function Ut(){return le(10)}function Fe(){return le(ze-1)}function Ne(h){const t=Math.round((h-Date.now())/1e3);let e;try{e=new Intl.RelativeTimeFormat(Et,{numeric:"auto"})}catch{return de(h)}const s=Math.abs(t),a=[["year",31536e3],["month",2592e3],["week",604800],["day",86400],["hour",3600],["minute",60]];for(const[r,i]of a)if(s>=i)return e.format(Math.round(t/i),r);return e.format(t,"second")}function de(h){const t=ne({month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"});try{return new Date(h).toLocaleString(Et,t)}catch{return new Date(h).toLocaleString(void 0,t)}}function et(h,t){if(!h)return"-";let e;if(typeof h=="number")e=h>=1e12?h:h*1e3;else{const s=String(h),a=/^(\d{4})-(\d{2})-(\d{2})$/.exec(s);e=a?new Date(+a[1],+a[2]-1,+a[3]).getTime():new Date(s).getTime()}return isNaN(e)?"-":(t||se)==="relative"?Ne(e):de(e)}function Ct(h){if(h==null)return"unknown error";if(h instanceof Error)return`${h.name}: ${h.message}`;const t=h.code!=null?String(h.code):"",e=h.message!=null?String(h.message):"";if(!t&&!e)try{return JSON.stringify(h)}catch{return String(h)}const s=t==="unknown_command"?" (the integration is probably still starting up; this should stop on its own)":"";return`${t||"error"}${e?": "+e:""}${s}`}function d(h){return String(h??"").replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}class ce{constructor(t){this.html=String(t??"")}toString(){return this.html}}function vt(h){return new ce(h)}const he=new Map;function He(h,t){let e=he.get(h);if(e===void 0){try{e=new Intl.PluralRules(h||"en")}catch{e=null}he.set(h,e)}return e?e.select(t):t===1?"one":"other"}function je(h){const t=String(h??"").trim();return/^https?:\/\//i.test(t)?t:""}function Pt(h,t){const e=String(h??"").trim();let s=e.match(/^#([0-9a-fA-F]{3,8})$/);if(s){let a=s[1];if((a.length===3||a.length===4)&&(a=a.split("").map(r=>r+r).join("")),a.length===6||a.length===8){const r=parseInt(a.slice(0,2),16),i=parseInt(a.slice(2,4),16),o=parseInt(a.slice(4,6),16);return`rgba(${r}, ${i}, ${o}, ${t})`}}if(s=e.match(/^rgba?\(([^)]+)\)$/i),s){const a=s[1].split(",").map(r=>r.trim());if(a.length>=3)return`rgba(${a[0]}, ${a[1]}, ${a[2]}, ${t})`}return e}function pe(h,t){const e=parseFloat(h);return isNaN(e)?t:e}const We=["data-action","data-cid","data-idx","data-tab","data-sec","data-gtab","data-ptab","data-hsub","data-proftab","data-opt","data-pgkey","data-gname","data-sugkey"];function qe(h){if(!h||!h.getAttribute||h.tagName==="BODY")return null;const t=s=>window.CSS&&CSS.escape?CSS.escape(s):String(s).replace(/["\\]/g,"\\$&");if(h.id)return"#"+t(h.id);const e=We.filter(s=>h.hasAttribute(s)).map(s=>`[${s}="${t(h.getAttribute(s))}"]`);return e.length?h.tagName.toLowerCase()+e.join(""):null}function Vt(h){return h?Array.from(h.querySelectorAll('a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])')).filter(e=>e.getClientRects().length>0):[]}function xt(h){return h==null||h===""?"(none)":h===!0?"on":h===!1?"off":Array.isArray(h)?h.join(", ")||"(none)":String(h)}function _e(h,t,e){return h.slice().sort((s,a)=>{const r=t(s),i=t(a);return r==null&&i==null?0:r==null?1:i==null?-1:(ri?1:0)*e})}function mt(h,t,e,s,a,r,i){const o=e?s===1?" \u25B2":" \u25BC":' \u2195',n=r==="right"?"text-align:right;":"",l=i?` title="${d(i)}"`:"";return`${h}${o}`}function $t(h){h=Math.max(0,Math.round(h));const t=Math.floor(h/3600),e=Math.floor(h%3600/60),s=h%60;return t>0?`${t}:${String(e).padStart(2,"0")}:${String(s).padStart(2,"0")}`:`${e}:${String(s).padStart(2,"0")}`}function ue(h,t){const e={pause:["lbl.artifact_interruption","Interruption"],dip:["lbl.artifact_low_power","Low power"],spike:["lbl.artifact_high_power","High power"]},[s,a]=e[h]||["lbl.artifact_anomaly","Anomaly"];return t?t(s,{},a):a}function ge(h){return h.toLowerCase().replace(/[\s&/\-]+/g,"_").replace(/^_+|_+$/g,"").replace(/_+/g,"_")}function fe(h){let t=String(h??"").trim().toLowerCase();try{t=t.normalize("NFKD").replace(/[\u0300-\u036f]/g,"")}catch{}return t.replace(/[^\p{L}\p{N}]+/gu,"-").replace(/^-+|-+$/g,"")}function me(h){const t=new Set;return String(h??"").split(",").forEach(e=>{const s=parseInt(e.trim(),10);Number.isFinite(s)&&s>0&&t.add(s)}),Array.from(t).sort((e,s)=>e-s)}function Ue(h,t){if(!h||!h.length)return null;for(let e=0;e=h[h.length-1][0])return h[h.length-1][1];for(let e=1;e=t){const s=h[e-1],a=h[e],r=a[0]-s[0]||1;return s[1]+(a[1]-s[1])*((t-s[0])/r)}return h[h.length-1][1]}const Kt=/^#[0-9a-fA-F]{6}$/,Ve="#03A9F4";function ye(h){return Kt.test(String(h||""))?String(h):Ve}function Ge(h,t,e){e=e||{};const s=h.key,a=h.unit?`${h.label} (${h.unit})`:h.label,r=h.unit?` ${h.unit}`:"",i=h.doc?ot(h.doc,h.diagram||Ye[s],e.tText):"",o=e.changed?``:"";if(h.type==="checkbox")return`
    ${o}${i}
    ${h.hint?`
    ${d(h.hint)}
    `:""}
    `;let n="";const l=t??"";if(h.type==="select"||h.type==="devicetype"||h.type==="device"){const b=(e.opts||[]).map(([v,T])=>``).join("");n=``}else if(h.type==="textarea")n=``;else if(h.type==="json"){const u=l===""||l==null?"":typeof l=="string"?l:JSON.stringify(l,null,2);n=``}else if(h.type==="checkboxlist"){const u=Array.isArray(t)&&t.length?t.map(String):Array.isArray(h.def)?h.def.map(String):[];n=`
    `+(h.choices||[]).map(([b,v])=>``).join("")+"
    "}else if(h.type==="entitylist"){const b=(Array.isArray(t)?t:t?[t]:[]).map(v=>`${d(v)}`).join("");n=`
    ${b}
    `}else if(h.type==="timerlist"){const u=Array.isArray(l)?l:[],b=e.t?e.t("lbl.timer_min",{},"min"):"min",v=e.t?e.t("lbl.timer_msg_placeholder",{},"Message (optional, {device}/{program}/{minutes})"):"Message (optional, {device}/{program}/{minutes})",T=e.t?e.t("lbl.timer_auto_pause",{},"Auto-pause"):"Auto-pause",C=e.t?e.t("btn.remove_timer",{},"Delete"):"Delete",x=e.t?e.t("btn.add_timer",{},"+ Add timer"):"+ Add timer",$=(D,B)=>{const U=D&&D.offset_minutes?String(D.offset_minutes):"",z=D&&D.message?d(D.message):"",M=D&&D.auto_pause?" checked":"";return`
    `},L=u.map((D,B)=>$(D,B)).join("");n=`
    ${L}
    `}else if(h.type==="color"){const u=h.placeholder?` placeholder="${d(h.placeholder)}"`:"",b=Kt.test(String(l))?String(l):ye(h.placeholder),v=e.t?e.t("btn.clear",{},"Clear"):"Clear";n=`
    `}else if(h.type==="entity"){const u=h.placeholder?` placeholder="${d(h.placeholder)}"`:"";n=`
    `}else if(h.type==="list"){const u=Array.isArray(l)?l.join(", "):d(l);n=``}else if(h.type==="intlist"){const u=Array.isArray(l)?l.join(", "):String(l??""),b=h.placeholder?` placeholder="${d(h.placeholder)}"`:"";n=``}else{const u=h.type==="number"?"number":"text",b=e.datalistId?` list="${e.datalistId}"`:"",v=h.step!=null?` step="${h.step}"`:"",T=h.min!=null?` min="${h.min}"`:"",C=h.max!=null?` max="${h.max}"`:"",x=h.placeholder?` placeholder="${d(h.placeholder)}"`:"";n=`${e.datalist||""}`}const c=e.suggestion,p=c&&c.suggested!=null&&!Ke(c.suggested,t)?c.suggested:null,_=e.t;let f="";if(p!=null){const u=e.tText||_,b=c.reason_key?u(c.reason_key,c.reason_params||{},c.reason||""):c.reason||"",v=b?ot(b):"",T=t!=null&&t!==""?` ${_("suggestion.now_value",{v:`${t}${r}`},"(now {v})")}`:"",C=we(_,s,p,t),x=``,$=d(_("btn.mute_suggestion",{},"Stop suggesting this setting")),L=s?``:"";f=`
    \u{1F4A1} ${d(_("suggestion.observed_label",{},"Observed"))}${d(p)}${r}${T}${x}${v}${L}${C?`
    ${d(C)}
    `:""}
    `}return a&&n&&(n=n.replace(/<(input|select|textarea)\b(?![^>]*\baria-label=)/,`<$1 aria-label="${d(a)}"`)),`
    ${o}${i}
    ${n}${h.hint?`
    ${d(h.hint)}
    `:""}${f}
    `}function Ke(h,t){if(h==null||t==null)return!1;const e=parseFloat(h),s=parseFloat(t);return!isNaN(e)&&!isNaN(s)?Math.abs(e-s)<1e-6:String(h)===String(t)}function we(h,t,e,s){const a=parseFloat(e),r=parseFloat(s);return isNaN(a)||isNaN(r)||a===r?"":h(`suggestion.impact.${t}.${a>r?"higher":"lower"}`,{},"")}const Ye={min_power:"min_power",off_delay:"off_delay",start_threshold_w:"hysteresis",stop_threshold_w:"hysteresis",start_energy_threshold:"start_energy",running_dead_zone:"dead_zone",profile_match_min_duration_ratio:"match_ratios",profile_match_max_duration_ratio:"match_ratios",progress_reset_delay:"progress_reset",completion_min_seconds:"min_duration",min_off_gap:"min_off_gap",duration_tolerance:"duration_tolerance",start_duration_threshold:"start_duration",end_energy_threshold:"end_energy_thresh",profile_match_threshold:"confidence",profile_unmatch_threshold:"confidence",auto_label_confidence:"confidence",learning_confidence:"confidence",no_update_active_timeout:"watchdog_timeout",anti_wrinkle_enabled:"anti_wrinkle",anti_wrinkle_max_power:"anti_wrinkle",anti_wrinkle_max_duration:"anti_wrinkle",anti_wrinkle_exit_power:"anti_wrinkle",anti_wrinkle_idle_timeout:"anti_wrinkle",anti_crease_finalize_ratio:"anti_wrinkle",sampling_interval:"sampling"};function be(h){let t=0,e=window.innerWidth,s=0,a=window.innerHeight;for(let r=h.parentElement;r;r=r.parentElement){const i=getComputedStyle(r),o=i.overflowX!=="visible",n=i.overflowY!=="visible";if(!o&&!n)continue;const l=r.getBoundingClientRect();o&&(l.left>t&&(t=l.left),l.rights&&(s=l.top),l.bottom=t-1,r=getComputedStyle(h).direction==="rtl";h.classList.toggle(r?"wd-ovf-r":"wd-ovf-l",!s),h.classList.toggle(r?"wd-ovf-l":"wd-ovf-r",!a)}function ot(h,t,e){const s=t?Je(t,e):"";return`i${s}${d(h)}`}function Xt(h,t,e=""){return`${e||""}`}function kt(h,t,e="",s=""){return`
    ${Xt(h,t,e)}
    ${s?`
    ${d(s)}
    `:""}
    `}function Je(h,t){const e=t||((r,i,o)=>o),s=r=>`${r}`,a='';switch(h){case"min_power":return s(`${a} - minbelow = off`);case"hysteresis":return t(`${e} + ${d(e("diagram.min",{},"min"))}${d(e("diagram.below_off",{},"below = off"))}`);case"hysteresis":return s(`${a} - startstop`);case"start_energy":return t(`${e} + ${d(e("diagram.start",{},"start"))}${d(e("diagram.stop",{},"stop"))}`);case"start_energy":return s(`${a} - spike ignored + ${d(e("diagram.spike_ignored",{},"spike ignored"))} - energy counts`);case"off_delay":return t(`${e} + ${d(e("diagram.energy_counts",{},"energy counts"))}`);case"off_delay":return s(`${a} - off-delay wait`);case"duration_tolerance":return t(`${e} + ${d(e("diagram.off_delay_wait",{},"off-delay wait"))}`);case"duration_tolerance":return s(`${a} - -tol+tolprofile`);case"match_ratios":return t(`${e} + ${d(e("diagram.minus_tol",{},"-tol"))}${d(e("diagram.plus_tol",{},"+tol"))}${d(e("diagram.profile",{},"profile"))}`);case"match_ratios":return s(`${a} - minmax`);case"dead_zone":return t(`${e} + ${d(e("diagram.min",{},"min"))}${d(e("diagram.max",{},"max"))}`);case"dead_zone":return s(`${a} - ignored`);case"progress_reset":return t(`${e} + ${d(e("diagram.ignored",{},"ignored"))}`);case"progress_reset":return s(`${a} - held at 100%`);case"min_duration":return t(`${e} + ${d(e("diagram.held_at_100",{},"held at 100%"))}`);case"min_duration":return s(`${a} - too short + ${d(e("diagram.too_short",{},"too short"))} - kept`);case"min_off_gap":return t(`${e} + ${d(e("diagram.kept",{},"kept"))}`);case"min_off_gap":return s(`${a} - off-gap - gap below min = one cycle`);case"start_duration":return t(`${e} + ${d(e("diagram.off_gap",{},"off-gap"))} + ${d(e("diagram.gap_below_min",{},"gap below min = one cycle"))}`);case"start_duration":return s(`${a} - spike: ignored - confirmed`);case"end_energy_thresh":return t(`${e} + ${d(e("diagram.spike_ignored_short",{},"spike: ignored"))} + ${d(e("diagram.confirmed",{},"confirmed"))}`);case"end_energy_thresh":return s(`${a} - tail energy above thresh: timer resets - accum - thr`);case"end_repeat":return t(`${e} - - - - - - - R1 R2 R3 - N reads below stop = end`);case"confidence":return t(` + ${d(e("diagram.tail_energy_resets",{},"tail energy above thresh: timer resets"))} + ${d(e("diagram.accum",{},"accum"))} + ${d(e("diagram.thr",{},"thr"))}`);case"confidence":return s(` - no match - feedback - auto + ${d(e("diagram.no_match",{},"no match"))} + ${d(e("diagram.feedback",{},"feedback"))} + ${d(e("diagram.auto",{},"auto"))} 0.0 - 1.0`);case"watchdog_timeout":return t(`${e} + 1.0`);case"watchdog_timeout":return s(`${a} - no updates - sensor offline: force-stop`);case"anti_wrinkle":return t(`${e} + ${d(e("diagram.no_updates",{},"no updates"))} + ${d(e("diagram.sensor_offline",{},"sensor offline: force-stop"))}`);case"anti_wrinkle":return s(`${a} - heat phase - tumble pulses kept`);case"sampling":return t(`${e} + ${d(e("diagram.heat_phase",{},"heat phase"))} + ${d(e("diagram.tumble_kept",{},"tumble pulses kept"))}`);case"sampling":return s(`${a} - SI - SI - SI - typical reading interval`);default:return""}}class Ie extends HTMLElement{constructor(){super(),this._hass=null,this._initialized=!1,this._pollTimer=null,this._toastTimer=null,this._hassUpdateThrottle=null,this._evtUnsubs=[],this._hoverRafId=null,this._hoverPending=null,this._constants={stateColors:{},deviceTypes:[],mlLabEnabled:!1,mlSuggestionsEnabled:!1,mlTrainingAvailable:!1,storeOnlineAvailable:!1,storeOnlineEnabled:!1,storeWebOrigin:"",storePrefs:{},pgMatchDefaults:{},version:"",iconUrl:""},this._constantsLoaded=!1,this._devices=[],this._cycles=[],this._refCycles=[],this._shareableCycles=[],this._sharePhasePrograms=[],this._shareAllPrograms=[],this._dlSettings=!1,this._selectMode=!1,this._cycleSel=new Set,this._profiles=[],this._profileGroups={groups:[],suggestions:[],min_cohesion:.85},this._profileEnvCache={},this._suggestions=[],this._lockedSuggestions=[],this._feedbacks=[],this._diag=null,this._phases=[],this._recState=null,this._opts={},this._optDefaults={},this._mlComparison=null,this._mlById={},this._mlLoading=!1,this._mlSettings={},this._mlSettingsLoading=!1,this._mlSettingsByEntry={},this._lockedByEntry={},this._mlTrainingStatus=null,this._setupStatus=null,this._selIdx=0,this._deepLinkApplied=null,this._tab="status",this._settingsSec="basic",this._settingsSearch="",this._settingsSugOnly=!1,this._settingsHistoryOpen=!1,this._canvasZoom={},this._gAxisHandle={},this._gtipPinned=!1,this._gtipLines=null,this._gPinchRaf=null,this._gPinchPending=null,this._toolsSubtab="recording",this._loading=!0,this._tabLoading=!1,this._scrollNavKey=null,this._scrollCarry=null,this._lastRefresh=null,this._powerHistory=[],this._powerT0=null,this._statusEnv=null,this._statusEnvName=null,this._powerData={live:[],raw:[],cycle_active:!1,cycle_elapsed_s:0},this._stagedSuggestions=!1,this._pendingSettings={},this._dirtyOptKeys=new Set,this._busy=new Set,this._tasks={},this._cancellingTasks=new Set,this._taskCallbacks={},this._tasksSubscribed=!1,this._pgHistoryTaskId=null,this._pgSweepTaskId=null,this._panelCfg=null,this._panelTrans=null,this._pollMs=ge,this._panelSubtab="maintenance",this._gearTab="prefs",this._catalog={brands:void 0,devices:void 0,forBrand:null,approvedOnly:!1,brandsFull:!1,brandPrefixes:[],typeDevices:void 0,typeDevicesFor:null,modelIndex:new Map},this._catalogEntry=null,this._maintenance=null,this._logs=[],this._logLevel="",this._logDevice="",this._logComponent="",this._logSearch="";try{this._logOpen=localStorage.getItem("wd-log-open")==="1",this._logDrawerWidth=Math.max(280,parseInt(localStorage.getItem("wd-log-width")||"380",10)||380)}catch{this._logOpen=!1,this._logDrawerWidth=380}this._tabInitialized=!1,this._modal=null,this._prevModal=null,this._toast=null,this._cycleSort={col:"date",dir:-1},this._cycleFilter={text:"",status:""},this._cleanupSort={col:"date",dir:-1},this._profSubtab="profiles",this._statusPhases=[],this._statusPhasesName=null,this._cycleOffset=0,this._cyclesTotal=0,this._cyclesHasMore=!1,this._undoBuffer=new Map,this._undoSeq=0,this._kbdHandler=null,this._tipHandler=null,this._settingsChangelog=null,this._settingsChangeByKey={},this._pgCycleId="",this._pgProfileName="",this._pgPowerPts=null,this._pgDtwData=null,this._pgEnvData=null,this._pgAnalysisTab="history",this._pgDetail=null,this._pgDetailBusy=!1,this._pgHistory=null,this._pgSweepObjective="match_accuracy",this._pgSweepNew=null,this._pgDetailDebounceTimer=null,this._pgThreshStart=null,this._pgThreshStop=null,this._pgParamOverrides={},this._pgEffective=null,this._pgPublishable=null,this._pgPresets=[],this._pgPresetLimit=0,this._pgPresetSel="",this._pgPresetName="",this._pgSuggClassic={},this._pgSuggMl=null,this._pgMlSuggEnabled=!1,this._pgView=null,this._pgHoverT=null,this._pgMap=null,this._pgStressTail=!1,this._pgStressIdleW=null,this._pgPanStart=null,this._pgHoverEvent=null,this._pgBatchProgress=null,this._pgBatchCancel=!1,this._pgLoadSeq=0,this._pgDragging=null,this._pgNeedsRestart=!1,this._pgSimCycles=20,this._pgSweepParam="off_delay",this._pgSweepFrom="",this._pgSweepTo="",this._pgSweepSteps=5,this._pgLoading=!1,this._storeView="brands",this._storeQuery="",this._storeDevices=[],this._storeDevice=null,this._storeProfiles=[],this._storeProfile=null,this._storeCycles=[],this._storeStatus=null,this._storeConnected=!1,this._storeLoading=!1,this._storeConnectListener=null}set hass(t){const e=this._hass;if(this._hass=t,!this._initialized&&t){this._initialized=!0,this._boot();return}e!==t&&this._initialized&&!this._loading&&!this._hassUpdateThrottle&&(this._hassUpdateThrottle=setTimeout(()=>{this._hassUpdateThrottle=null,this._fetchAll()},ue))}set panel(t){this._panel=t}set narrow(t){this._narrow=t}connectedCallback(){this._initialized&&(this._startPoll(),this._setupSubscriptions(),this._fetchAll(),this.shadowRoot&&!this._kbdHandler&&(this._kbdHandler=t=>this._onKeydown(t),this.shadowRoot.addEventListener("keydown",this._kbdHandler)),this.shadowRoot&&!this._tipHandler&&(this._tipHandler=t=>{const e=t.target,s=e&&e.closest?e.closest(".wd-tip"):null;s&&this._positionTip(s)},this.shadowRoot.addEventListener("pointerover",this._tipHandler)),this.shadowRoot&&!this._gtipDismissHandler&&(this._gtipDismissHandler=t=>this._maybeDismissGraphTip(t),this.shadowRoot.addEventListener("pointerdown",this._gtipDismissHandler))),this._onResize=()=>{this._syncPanelHeight(),this._resizeLogsPage()},window.addEventListener("resize",this._onResize),this._onLocChanged=()=>this._onLocationChanged(),window.addEventListener("location-changed",this._onLocChanged),window.addEventListener("popstate",this._onLocChanged),window.visualViewport&&window.visualViewport.addEventListener("resize",this._onResize),requestAnimationFrame(()=>this._syncPanelHeight())}_syncPanelHeight(){if(!this.isConnected)return;const e=this.getBoundingClientRect().top+(window.scrollY||document.documentElement.scrollTop||0),s=Math.round((window.innerHeight||0)-e);if(!(s>0))return;const r=`${Math.max(we,s)}px`;this.style.height!==r&&(this.style.height=r)}disconnectedCallback(){this._onResize&&(window.removeEventListener("resize",this._onResize),window.visualViewport&&window.visualViewport.removeEventListener("resize",this._onResize),this._onResize=null),this._onLocChanged&&(window.removeEventListener("location-changed",this._onLocChanged),window.removeEventListener("popstate",this._onLocChanged),this._onLocChanged=null),this._stopPoll(),this._hassUpdateThrottle&&(clearTimeout(this._hassUpdateThrottle),this._hassUpdateThrottle=null),this._pgRestartRetryTimer&&(clearTimeout(this._pgRestartRetryTimer),this._pgRestartRetryTimer=null),this._evtUnsubs.forEach(t=>{try{t()}catch{}}),this._evtUnsubs=[],this._flushPendingDeletes(),this._kbdHandler&&this.shadowRoot&&(this.shadowRoot.removeEventListener("keydown",this._kbdHandler),this._kbdHandler=null),this._tipHandler&&this.shadowRoot&&(this.shadowRoot.removeEventListener("pointerover",this._tipHandler),this._tipHandler=null),this._gtipDismissHandler&&this.shadowRoot&&(this.shadowRoot.removeEventListener("pointerdown",this._gtipDismissHandler),this._gtipDismissHandler=null),this._storeConnectListener&&(window.removeEventListener("message",this._storeConnectListener),this._storeConnectListener=null)}_boot(){const t=this.attachShadow({mode:"open"}),e=document.createElement("style");e.textContent=$e,t.appendChild(e),this._container=document.createElement("div"),this._container.className="wd-root",t.appendChild(this._container),this._gtip=document.createElement("div"),this._gtip.className="wd-gtip",t.appendChild(this._gtip),this._kbdHandler=s=>this._onKeydown(s),t.addEventListener("keydown",this._kbdHandler),this._tipHandler=s=>{const r=s.target,n=r&&r.closest?r.closest(".wd-tip"):null;n&&this._positionTip(n)},t.addEventListener("pointerover",this._tipHandler),this._gtipDismissHandler=s=>this._maybeDismissGraphTip(s),t.addEventListener("pointerdown",this._gtipDismissHandler),this._loadPanelTranslations().catch(()=>{}).finally(()=>{this._fetchAll(),this._startPoll()}),this._setupSubscriptions()}_setupSubscriptions(){this._evtUnsubs.forEach(e=>{try{e()}catch{}}),this._evtUnsubs=[],this._tasksSubscribed=!1;const t=this._hass&&this._hass.connection;if(t&&t.subscribeMessage){const e=s=>{const r=s.data||{};if(r.entry_id&&(this._fetchAll(),s.event_type==="ha_washdata_cycle_ended")){const n=this._devices[this._selIdx];n&&n.entry_id===r.entry_id&&this._fetchCycles(r.entry_id).then(()=>{this._tab==="history"&&this._render()})}};for(const s of["ha_washdata_cycle_started","ha_washdata_cycle_ended"])t.subscribeMessage(e,{type:"subscribe_events",event_type:s}).then(r=>{this._evtUnsubs.push(r)}).catch(()=>{});t.subscribeMessage(s=>this._onTaskEvent(s),{type:`${y}/subscribe_tasks`}).then(s=>{this._tasksSubscribed=!0,this._evtUnsubs.push(s)}).catch(()=>{this._tasksSubscribed=!1})}}_onTaskEvent(t){const e=t&&t.task;if(!e||!e.id)return;const s=this._tasks[e.id];s&&(s.updated_at||0)>(e.updated_at||0)||(this._tasks[e.id]=e,e.state!=="running"&&this._cancellingTasks.delete(e.id),this._updateTaskPills(),this._pgAdoptTask(e),this._onTrackedTaskProgress(e),(e.kind==="history_import"||e.kind==="history_import_apply")&&(e.state==="running"?this._modal&&this._modal.type==="history-import"&&this._render():this._histTaskFinished(e)),this._settleTaskCallback(e))}_pgAdoptTask(t){if(t.kind!=="pg_sweep"&&t.kind!=="pg_history")return;const e=this._devices[this._selIdx];if(!e||t.entry_id!==e.entry_id)return;const s=t.kind==="pg_sweep";if(this._pgSweepTaskId||this._pgHistoryTaskId)return;const r=t.state!=="running"&&t.finished_at&&Date.now()/1e3-t.finished_at<30;t.state!=="running"&&!r||(s?this._pgSweepTaskId=t.id:this._pgHistoryTaskId=t.id,this._pgAnalysisTab=s?"sweep":"history",this._busy.add(s?"pg-sweep":"pg-history"),t.state==="running"&&(this._pgBatchProgress={done:t.done||0,total:t.total||0}),this._render())}_pgAdoptExisting(){Object.values(this._tasks||{}).forEach(t=>{this._pgAdoptTask(t),this._onTrackedTaskProgress(t)})}_htmlPgRecentRuns(t){const e=this._devices[this._selIdx];if(!e)return"";const s=Object.values(this._tasks||{}).filter(n=>n.kind===t&&n.entry_id===e.entry_id&&n.state!=="running"&&n.has_result).sort((n,a)=>(a.finished_at||0)-(n.finished_at||0)).slice(0,8);if(!s.length)return"";const r=s.map(n=>{const a=n.finished_at?ot(n.finished_at*1e3):"",o=n.state==="cancelled"?" \u26A0":"";return``}).join("");return`
    + ${d(e("diagram.si",{},"SI"))} + ${d(e("diagram.si",{},"SI"))} + ${d(e("diagram.si",{},"SI"))} + ${d(e("diagram.reading_interval",{},"typical reading interval"))}`);default:return""}}class Ze extends HTMLElement{constructor(){super(),this._hass=null,this._initialized=!1,this._pollTimer=null,this._toastTimer=null,this._hassUpdateThrottle=null,this._evtUnsubs=[],this._hoverRafId=null,this._hoverPending=null,this._constants={stateColors:{},deviceTypes:[],mlLabEnabled:!1,mlTrainingAvailable:!1,storeOnlineAvailable:!1,storeOnlineEnabled:!1,storeWebOrigin:"",storePrefs:{},pgMatchDefaults:{},version:"",iconUrl:""},this._constantsLoaded=!1,this._devices=[],this._cycles=[],this._refCycles=[],this._shareableCycles=[],this._sharePhasePrograms=[],this._shareAllPrograms=[],this._dlSettings=!1,this._selectMode=!1,this._cycleSel=new Set,this._profiles=[],this._profileGroups={groups:[],min_cohesion:.85},this._profileEnvCache={},this._suggestions=[],this._lockedSuggestions=[],this._feedbacks=[],this._diag=null,this._importUndo=null,this._phases=[],this._recState=null,this._opts={},this._optDefaults={},this._mlComparison=null,this._mlById={},this._mlLoading=!1,this._mlTrainingStatus=null,this._setupStatus=null,this._selIdx=0,this._deepLinkApplied=null,this._tab="status",this._settingsSec="basic",this._settingsSearch="",this._settingsSugOnly=!1,this._settingsHistoryOpen=!1,this._canvasZoom={},this._gAxisHandle={},this._gtipPinned=!1,this._gtipLines=null,this._gPinchRaf=null,this._gPinchPending=null,this._toolsSubtab="recording",this._loading=!0,this._tabLoading=!1,this._scrollNavKey=null,this._scrollCarry=null,this._lastRefresh=null,this._powerHistory=[],this._powerT0=null,this._statusEnv=null,this._statusEnvName=null,this._powerData={live:[],raw:[],cycle_active:!1,cycle_elapsed_s:0},this._stagedSuggestions=!1,this._pendingSettings={},this._dirtyOptKeys=new Set,this._busy=new Set,this._tasks={},this._cancellingTasks=new Set,this._taskCallbacks={},this._tasksSubscribed=!1,this._pgHistoryTaskId=null,this._pgSweepTaskId=null,this._panelCfg=null,this._panelTrans=null,this._pollMs=ve,this._panelSubtab="diagnostics",this._historySub="cycles",this._gearTab="prefs",this._catalog={brands:void 0,devices:void 0,forBrand:null,approvedOnly:!1,brandsFull:!1,brandPrefixes:[],typeDevices:void 0,typeDevicesFor:null,modelIndex:new Map},this._catalogEntry=null,this._maintenance=null,this._logs=[],this._logLevel="",this._logDevice="",this._logComponent="",this._logSearch="";try{this._logOpen=localStorage.getItem("wd-log-open")==="1",this._logDrawerWidth=Math.max(280,parseInt(localStorage.getItem("wd-log-width")||"380",10)||380)}catch{this._logOpen=!1,this._logDrawerWidth=380}this._tabInitialized=!1,this._modal=null,this._prevModal=null,this._toast=null,this._cycleSort={col:"date",dir:-1},this._cycleFilter={text:"",status:""},this._cleanupSort={col:"date",dir:-1},this._profSubtab="profiles",this._statusPhases=[],this._statusPhasesName=null,this._cycleOffset=0,this._cyclesTotal=0,this._cyclesHasMore=!1,this._importedOffset=0,this._importedHasMore=!1,this._undoBuffer=new Map,this._undoSeq=0,this._kbdHandler=null,this._tipHandler=null,this._settingsChangelog=null,this._settingsChangeByKey={},this._pgCycleId="",this._pgProfileName="",this._pgPowerPts=null,this._pgEnvData=null,this._pgAnalysisTab="history",this._pgDetail=null,this._pgDetailBusy=!1,this._pgHistory=null,this._pgSweepObjective="match_accuracy",this._pgSweepNew=null,this._pgDetailDebounceTimer=null,this._pgThreshStart=null,this._pgThreshStop=null,this._pgParamOverrides={},this._pgEffective=null,this._pgPublishable=null,this._pgPresets=[],this._pgPresetLimit=0,this._pgPresetSel="",this._pgPresetName="",this._pgSuggClassic={},this._pgView=null,this._pgHoverT=null,this._pgMap=null,this._pgPanStart=null,this._pgHoverEvent=null,this._pgBatchProgress=null,this._pgBatchCancel=!1,this._pgLoadSeq=0,this._pgDragging=null,this._pgNeedsRestart=!1,this._pgSimCycles=20,this._pgSweepParam="off_delay",this._pgSweepFrom="",this._pgSweepTo="",this._pgSweepSteps=5,this._pgLoading=!1,this._storeView="brands",this._storeQuery="",this._storeDevices=[],this._storeDevice=null,this._storeProfiles=[],this._storeProfile=null,this._storeCycles=[],this._storeStatus=null,this._storeConnected=!1,this._storeLoading=!1,this._storeConnectListener=null}set hass(t){const e=this._hass;if(this._hass=t,!this._initialized&&t){this._initialized=!0,this._boot();return}e!==t&&this._initialized&&!this._loading&&!this._hassUpdateThrottle&&(this._hassUpdateThrottle=setTimeout(()=>{this._hassUpdateThrottle=null,this._fetchAll()},xe))}set panel(t){this._panel=t}set narrow(t){this._narrow=t}connectedCallback(){this._initialized&&(this._startPoll(),this._setupSubscriptions(),this._fetchAll(),this.shadowRoot&&!this._kbdHandler&&(this._kbdHandler=t=>this._onKeydown(t),this.shadowRoot.addEventListener("keydown",this._kbdHandler)),this.shadowRoot&&!this._tipHandler&&(this._tipHandler=t=>{const e=t.target,s=e&&e.closest?e.closest(".wd-tip"):null;s&&this._positionTip(s)},this.shadowRoot.addEventListener("pointerover",this._tipHandler)),this.shadowRoot&&!this._gtipDismissHandler&&(this._gtipDismissHandler=t=>this._maybeDismissGraphTip(t),this.shadowRoot.addEventListener("pointerdown",this._gtipDismissHandler))),this._onResize=()=>{this._syncPanelHeight(),this._resizeLogsPage()},window.addEventListener("resize",this._onResize),this._onLocChanged=()=>this._onLocationChanged(),window.addEventListener("location-changed",this._onLocChanged),window.addEventListener("popstate",this._onLocChanged),this._onOutboundClick=t=>this._maybeGuardOutboundClick(t),window.addEventListener("click",this._onOutboundClick,!0),this._onBeforeUnload=t=>{this._unsavedSettingsCount()&&(t.preventDefault(),t.returnValue="")},window.addEventListener("beforeunload",this._onBeforeUnload),window.visualViewport&&window.visualViewport.addEventListener("resize",this._onResize),requestAnimationFrame(()=>this._syncPanelHeight())}_syncPanelHeight(){if(!this.isConnected)return;const e=this.getBoundingClientRect().top+(window.scrollY||document.documentElement.scrollTop||0),s=Math.round((window.innerHeight||0)-e);if(!(s>0))return;const a=`${Math.max(Ce,s)}px`;this.style.height!==a&&(this.style.height=a)}disconnectedCallback(){this._ovfRO&&(this._ovfRO.disconnect(),this._ovfRO=null),this._onResize&&(window.removeEventListener("resize",this._onResize),window.visualViewport&&window.visualViewport.removeEventListener("resize",this._onResize),this._onResize=null),this._onLocChanged&&(window.removeEventListener("location-changed",this._onLocChanged),window.removeEventListener("popstate",this._onLocChanged),this._onLocChanged=null),this._onOutboundClick&&(window.removeEventListener("click",this._onOutboundClick,!0),this._onOutboundClick=null),this._onBeforeUnload&&(window.removeEventListener("beforeunload",this._onBeforeUnload),this._onBeforeUnload=null),this._stopPoll(),this._hassUpdateThrottle&&(clearTimeout(this._hassUpdateThrottle),this._hassUpdateThrottle=null),this._pgRestartRetryTimer&&(clearTimeout(this._pgRestartRetryTimer),this._pgRestartRetryTimer=null),this._evtUnsubs.forEach(t=>{try{t()}catch{}}),this._evtUnsubs=[],this._flushPendingDeletes(),this._kbdHandler&&this.shadowRoot&&(this.shadowRoot.removeEventListener("keydown",this._kbdHandler),this._kbdHandler=null),this._tipHandler&&this.shadowRoot&&(this.shadowRoot.removeEventListener("pointerover",this._tipHandler),this._tipHandler=null),this._gtipDismissHandler&&this.shadowRoot&&(this.shadowRoot.removeEventListener("pointerdown",this._gtipDismissHandler),this._gtipDismissHandler=null),this._storeConnectListener&&(window.removeEventListener("message",this._storeConnectListener),this._storeConnectListener=null)}_boot(){const t=this.attachShadow({mode:"open"}),e=document.createElement("style");e.textContent=De,t.appendChild(e),this._container=document.createElement("div"),this._container.className="wd-root",t.appendChild(this._container),this._gtip=document.createElement("div"),this._gtip.className="wd-gtip",t.appendChild(this._gtip),this._kbdHandler=s=>this._onKeydown(s),t.addEventListener("keydown",this._kbdHandler),this._tipHandler=s=>{const a=s.target,r=a&&a.closest?a.closest(".wd-tip"):null;r&&this._positionTip(r)},t.addEventListener("pointerover",this._tipHandler),this._gtipDismissHandler=s=>this._maybeDismissGraphTip(s),t.addEventListener("pointerdown",this._gtipDismissHandler),this._loadPanelTranslations().catch(()=>{}).finally(()=>{this._fetchAll(),this._startPoll()}),this._setupSubscriptions()}_setupSubscriptions(){this._evtUnsubs.forEach(e=>{try{e()}catch{}}),this._evtUnsubs=[],this._tasksSubscribed=!1;const t=this._hass&&this._hass.connection;if(t&&t.subscribeMessage){const e=s=>{const a=s.data||{};if(a.entry_id&&(this._fetchAll(),s.event_type==="ha_washdata_cycle_ended")){const r=this._devices[this._selIdx];r&&r.entry_id===a.entry_id&&this._fetchCycles(a.entry_id).then(()=>{this._tab==="history"&&this._render()})}};for(const s of["ha_washdata_cycle_started","ha_washdata_cycle_ended"])t.subscribeMessage(e,{type:"subscribe_events",event_type:s}).then(a=>{this._evtUnsubs.push(a)}).catch(()=>{});t.subscribeMessage(s=>this._onTaskEvent(s),{type:`${w}/subscribe_tasks`}).then(s=>{this._tasksSubscribed=!0,this._evtUnsubs.push(s)}).catch(()=>{this._tasksSubscribed=!1})}}_onTaskEvent(t){const e=t&&t.task;if(!e||!e.id)return;const s=this._tasks[e.id];s&&(s.updated_at||0)>(e.updated_at||0)||(this._tasks[e.id]=e,e.state!=="running"&&this._cancellingTasks.delete(e.id),this._updateTaskPills(),this._pgAdoptTask(e),this._onTrackedTaskProgress(e),(e.kind==="history_import"||e.kind==="history_import_apply")&&(e.state==="running"?this._modal&&this._modal.type==="history-import"&&this._render():this._histTaskFinished(e)),this._settleTaskCallback(e))}_pgAdoptTask(t){if(t.kind!=="pg_sweep"&&t.kind!=="pg_history")return;const e=this._devices[this._selIdx];if(!e||t.entry_id!==e.entry_id)return;const s=t.kind==="pg_sweep";if(this._pgSweepTaskId||this._pgHistoryTaskId)return;const a=t.state!=="running"&&t.finished_at&&Date.now()/1e3-t.finished_at<30;t.state!=="running"&&!a||(s?this._pgSweepTaskId=t.id:this._pgHistoryTaskId=t.id,this._pgAnalysisTab=s?"sweep":"history",this._busy.add(s?"pg-sweep":"pg-history"),t.state==="running"&&(this._pgBatchProgress={done:t.done||0,total:t.total||0}),this._render())}_pgAdoptExisting(){Object.values(this._tasks||{}).forEach(t=>{this._pgAdoptTask(t),this._onTrackedTaskProgress(t)})}_htmlPgRecentRuns(t){const e=this._devices[this._selIdx];if(!e)return"";const s=Object.values(this._tasks||{}).filter(r=>r.kind===t&&r.entry_id===e.entry_id&&r.state!=="running"&&r.has_result).sort((r,i)=>(i.finished_at||0)-(r.finished_at||0)).slice(0,8);if(!s.length)return"";const a=s.map(r=>{const i=r.finished_at?et(r.finished_at*1e3):"",o=r.state==="cancelled"?" \u26A0":"";return``}).join("");return`
    ${this._t("lbl.pg_recent_runs",{},"Recent runs")} -
    ${r}
    -
    `}_settleTaskCallback(t){if(t.state==="running")return;const e=this._taskCallbacks[t.id];if(e){delete this._taskCallbacks[t.id],e(t);return}this._autoSettleAdopted(t)}_autoSettleAdopted(t){if(t.state!=="done"||t.kind!=="ml_training"&&t.kind!=="reprocess"||!t.finished_at||Date.now()/1e3-t.finished_at>60||(this._autoSettled=this._autoSettled||new Set,this._autoSettled.has(t.id)))return;const e=this._devices[this._selIdx];if(!e||t.entry_id!==e.entry_id)return;this._autoSettled.add(t.id);const s=e.entry_id,r=()=>{this._isActiveEntry(s)&&this._render()};t.kind==="ml_training"?this._loadMlTrainingStatus(s).finally(r):this._fetchToolsData(s).finally(r)}async _kickAndTrack(t,e,s){if(this._busy.has(e))return;this._busy.add(e),this._render();let r;try{const a=await this._ws(t);if(r=a&&a.task_id,!r)throw new Error("no task id");const o=String(t.type||"").endsWith("reprocess_history")?"reprocess":String(t.type||"").endsWith("trigger_ml_training")?"ml_training":String(t.type||"").endsWith("apply_split")?"split":String(t.type||"").endsWith("trim_cycle")?"trim":String(t.type||"").endsWith("apply_merge")?"merge":String(t.type||"").endsWith("rebuild_envelopes")?"rebuild":"task";this._addProvisionalTask(r,o,t.entry_id,0)}catch(a){this._busy.delete(e),this._showToast(this._t("msg.toast_error",{error:a.message||a},"Error: "+(a.message||a)),"error"),this._render();return}this._taskCallbacks[r]=async a=>{if(this._busy.delete(e),a.state==="error"){this._showToast(this._t("msg.toast_error",{error:a.error||""},"Error: "+(a.error||"")),"error"),this._render();return}if(a.state==="cancelled"){this._showToast(this._t("toast.task_cancelled",{},"Cancelled."),"info"),this._render();return}let o=null;try{const i=await this._ws({type:`${y}/get_task_result`,task_id:a.id});o=i&&i.result}catch{}if(o==null){this._showToast(this._t("toast.pg_run_gone",{},"That run is no longer available."),"info"),this._render();return}try{await s(o,a.state)}catch{}this._render()};const n=this._tasks[r];if(n&&n.state!=="running"){this._settleTaskCallback(n);return}this._tasksSubscribed||this._pollTaskGeneric(r)}_finalizeTaskError(t,e){const s=Object.assign({},this._tasks[t]||{id:t},{id:t,state:"error",error:e,finished_at:Date.now()/1e3});this._tasks[t]=s,this._updateTaskPills(),this._settleTaskCallback(s)}async _pollTaskGeneric(t){let e=0;for(let s=0;s<3600&&this._taskCallbacks[t];s++){let r;try{r=await this._ws({type:`${y}/get_task_result`,task_id:t}),e=0}catch{if(++e>=5){this._finalizeTaskError(t,"lost connection");return}await new Promise(a=>setTimeout(a,1200));continue}if(!r){this._finalizeTaskError(t,"no result");return}if(this._tasks[t]=r,this._updateTaskPills(),r.state!=="running"){this._settleTaskCallback(r);return}await new Promise(n=>setTimeout(n,1200))}this._taskCallbacks[t]&&this._finalizeTaskError(t,"timed out")}_deviceName(t){const e=(this._devices||[]).find(s=>s.entry_id===t);return e&&(e.name||e.title)||""}_taskActionLabel(t){return{pg_history:this._t("lbl.task_pg_history",{},"Test on history"),pg_sweep:this._t("lbl.task_pg_sweep",{},"Optimize"),pg_detail:this._t("lbl.task_pg_detail",{},"Simulate cycle"),split:this._t("lbl.task_split",{},"Splitting cycle"),trim:this._t("lbl.task_trim",{},"Trimming cycle"),merge:this._t("lbl.task_merge",{},"Merging cycles"),rebuild:this._t("lbl.task_rebuild",{},"Rebuilding envelopes"),reprocess:this._t("lbl.task_reprocess",{},"Reprocessing"),ml_training:this._t("lbl.task_ml_training",{},"Learning"),history_import:this._t("lbl.task_history_import",{},"Scanning power history"),history_import_apply:this._t("lbl.task_history_import_apply",{},"Importing cycles")}[t]||t}_fmtEta(t){return t=Math.round(t),t<60?this._t("lbl.eta_secs",{n:t},`~${t}s left`):this._t("lbl.eta_mins",{n:Math.round(t/60)},`~${Math.round(t/60)}m left`)}_exclNote(t){if(!t||!t.total)return"";const e=(t.items||[]).map(([s,r])=>`${r} ${this._t("suggestion.exclusions.reason."+s,{},String(s).replace(/_/g," "))}`).join(", ");return" "+this._t("suggestion.exclusions.summary",{total:t.total,parts:e},`Excluded ${t.total} mis-detected cycle(s): ${e}.`)}_htmlTaskPills(){const t=Object.values(this._tasks||{}).filter(s=>s.state==="running"),e=new Set(t.map(s=>s.id));return this._cancellingTasks.forEach(s=>{e.has(s)||this._cancellingTasks.delete(s)}),t.length?t.map(s=>{const r=this._cancellingTasks.has(s.id),n=this._deviceName(s.entry_id),a=s.label_key?this._t(s.label_key,s.label_params||{},s.label||this._taskActionLabel(s.kind)):this._taskActionLabel(s.kind),o=(n?n+" \xB7 ":"")+a,i=s.progress!=null?Math.round(s.progress*100)+"%":"",l=s.eta_s!=null&&s.eta_s>0?this._fmtEta(s.eta_s):"",c=this._t("task.cancelling",{},"Cancelling\u2026"),g="wd-task-pill"+(r?" wd-task-pill--cancelling":""),p=r?" \xB7 "+c:i?" "+i:"";return`${d(o)}`+(r?`${d(c)}`:(i?`${i}`:"")+(l?`${d(l)}`:""))+``}).join(""):""}_updateTaskPills(){const t=this.shadowRoot;if(!t)return;const e=t.getElementById("wd-task-pills");e&&(e.innerHTML=this._htmlTaskPills())}_addProvisionalTask(t,e,s,r){!t||this._tasks[t]||(this._tasks[t]={id:t,entry_id:s,kind:e,label:this._taskActionLabel(e),state:"running",done:0,total:r||0,progress:r?0:null,eta_s:null,updated_at:0,has_result:!1},this._updateTaskPills())}_onTrackedTaskProgress(t){if(!(t.id!==this._pgHistoryTaskId&&t.id!==this._pgSweepTaskId)){if(t.state==="running"){this._pgBatchProgress={done:t.done||0,total:t.total||0},this._pgUpdateBatchBar(t.done||0,t.total||0);return}this._pgFinishTask(t,t.id===this._pgHistoryTaskId)}}async _pgFinishTask(t,e){let s=null;try{if(t.state==="done"||t.state==="cancelled"){const r=await this._ws({type:`${y}/get_task_result`,task_id:t.id});s=r&&r.result}}catch{}this._isActiveEntry(t.entry_id)&&(t.state==="error"?this._showToast(this._t("msg.toast_error",{error:t.error||""},"Error: "+(t.error||"")),"error"):s&&(e?this._pgHistory=s:this._pgSweepNew=s&&!s.error?s:null),e?(this._busy.delete("pg-history"),this._pgHistoryTaskId=null):(this._busy.delete("pg-sweep"),this._pgSweepTaskId=null),this._pgBatchProgress=null,this._render())}async _pgPollTask(t){for(let e=0;e<3600&&(t===this._pgHistoryTaskId||t===this._pgSweepTaskId);e++){let s;try{s=await this._ws({type:`${y}/get_task_result`,task_id:t})}catch{break}if(!s)break;if(this._tasks[t]=s,this._updateTaskPills(),s.state!=="running"){this._onTrackedTaskProgress(s);return}this._pgBatchProgress={done:s.done||0,total:s.total||0},this._pgUpdateBatchBar(s.done||0,s.total||0),await new Promise(r=>setTimeout(r,1200))}}_panelTransUrl(t){const e=`/ha_washdata/panel-translations/${encodeURIComponent(t)}.json`;return Zt?`${e}?v=${encodeURIComponent(Zt)}`:e}async _fetchPanelLang(t){if(!t)return null;const e=[t],s=t.indexOf("-");s>0&&e.push(t.slice(0,s));for(const r of e)try{const n=await fetch(this._panelTransUrl(r));if(n.ok){const a=await n.json();if(a&&typeof a=="object")return a}}catch{}return null}async _loadPanelLang(t){if(!t||this._panelTrans&&this._panelTrans[t])return;const e=await this._fetchPanelLang(t);e&&(this._panelTrans||(this._panelTrans={}),this._panelTrans[t]=e)}async _loadPanelTranslations(){const t=this._hass&&this._hass.locale&&this._hass.locale.language;await Promise.all([this._loadPanelLang("en"),t&&t!=="en"?this._loadPanelLang(t):Promise.resolve()])}_startPoll(){this._stopPoll(),this._pollTimer=setInterval(()=>this._fetchAll(),this._pollMs)}_stopPoll(){this._pollTimer&&(clearInterval(this._pollTimer),this._pollTimer=null)}async _ws(t){return this._hass.connection.sendMessagePromise(t)}async _fetchAll(){if(!this._hass)return;const t=this._loading;try{if(!this._constantsLoaded){try{const a=await this._ws({type:`${y}/get_constants`});this._constants={stateColors:a.state_colors||{},deviceTypes:a.device_types||[],mlLabEnabled:!!a.ml_lab_enabled,mlSuggestionsEnabled:!!a.ml_suggestions_enabled,mlTrainingAvailable:!!a.ml_training_available,storeOnlineAvailable:!!a.store_online_available,storeOnlineEnabled:!!a.store_online_enabled,storeWebOrigin:a.store_web_origin||"",storePrefs:a.store_prefs||{},pgMatchDefaults:a.pg_match_defaults||{},PROFILE_MIN_WARMUP_CYCLES:a.PROFILE_MIN_WARMUP_CYCLES,version:a.version||"",iconUrl:a.icon_url||""}}catch{}try{this._panelCfg=await this._ws({type:`${y}/get_panel_config`}),this._applyPanelConfig()}catch{}this._constantsLoaded=!0}const e=await this._ws({type:`${y}/get_devices`});this._lastFetchErr=null,this._devices=e.devices||[],this._lastRefresh=new Date;let s=!1;const r=this._deepLinkToken();if(r&&r!==this._deepLinkApplied&&this._devices.length){this._deepLinkApplied=r;const a=this._deepLinkIdx(r);a>=0?(this._selIdx=a,s=!0,this._rememberDevice(a)):console.warn(`[WashData panel] ?device=${r} matches no WashData device`)}if(!s&&this._selIdx===0&&this._devices.length>1){const a=localStorage.getItem("wd-last-device");if(a){const o=this._devices.findIndex(i=>i.entry_id===a);o>0&&(this._selIdx=o)}}const n=this._devices[this._selIdx];if(n&&this._tab==="status"){try{this._powerData=await this._ws({type:`${y}/get_power_history`,entry_id:n.entry_id,with_raw:this._pref("show_raw_active",!1)})}catch{}if(this._pref("show_debug",!1))try{this._matchDebug=await this._ws({type:`${y}/get_match_debug`,entry_id:n.entry_id})}catch{}if(this._canEdit()&&n.recording)try{this._recState=await this._ws({type:`${y}/get_recording_state`,entry_id:n.entry_id})}catch{}}if(n&&n.current_program){if(this._statusEnvName!==n.current_program){this._statusEnvName=n.current_program;try{const a=await this._ws({type:`${y}/get_profile_envelope`,entry_id:n.entry_id,profile_name:n.current_program});this._statusEnv=a.envelope||null}catch{this._statusEnv=null}}await this._ensureStatusPhases(n.entry_id,n.current_program)}else this._statusEnv=null,this._statusEnvName=null,this._statusPhases=[],this._statusPhasesName=null;if(t&&n){if(await this._fetchCycles(n.entry_id),await this._fetchSuggestions(n.entry_id),await this._fetchProfiles(n.entry_id),this._tab==="status")try{this._setupStatus=await this._ws({type:`${y}/get_setup_status`,entry_id:n.entry_id})}catch{this._setupStatus=null}if(this._constants.storeOnlineAvailable){try{const a=await this._ws({type:`${y}/get_options`,entry_id:n.entry_id});this._isActiveEntry(n.entry_id)&&(this._opts=a.options||{})}catch{}this._onlineEnabled()&&await this._loadStoreStatus(n.entry_id)}}this._logOpen&&this._isAdmin()&&this._fetchLogs().then(()=>this._refreshLogDrawer()).catch(()=>{})}catch(e){const s=It(e);s!==this._lastFetchErr&&(this._lastFetchErr=s,console.warn("[WashData panel] fetch error -",s,e))}finally{this._loading=!1;const e=this.shadowRoot,s=e&&e.activeElement,r=!!(s&&["SELECT","INPUT","TEXTAREA","OPTION"].includes(s.tagName));t?this._render():this._tab==="status"&&!this._modal&&!r?this._render():this._tab==="status"&&!this._modal&&r?(this._drawStatusCurve(),this._refreshDeviceBar()):this._refreshDeviceBar()}}async _fetchCycles(t){this._cyclesError=!1;try{const e=await this._ws({type:`${y}/get_device_cycles`,entry_id:t,limit:Wt,offset:0});this._cycles=e.cycles||[],this._refCycles=[...e.reference_cycles||[],...e.backfill_cycles||[]],this._cycleOffset=this._cycles.length,this._cyclesTotal=e.total!=null?e.total:this._cycles.length,this._cyclesHasMore=e.has_more!=null?!!e.has_more:!1}catch{this._cyclesError=!0,this._cycles=[],this._refCycles=[],this._cycleOffset=0,this._cyclesTotal=0,this._cyclesHasMore=!1}}async _loadMoreCycles(t){const e=await this._ws({type:`${y}/get_device_cycles`,entry_id:t,limit:Wt,offset:this._cycleOffset}),s=e.cycles||[],r=new Set(this._cycles.map(n=>n.id));for(const n of s)r.has(n.id)||this._cycles.push(n);this._cycleOffset+=s.length,this._cyclesTotal=e.total!=null?e.total:this._cyclesTotal,this._cyclesHasMore=e.has_more!=null?!!e.has_more:s.length>=Wt}async _ensureStatusPhases(t,e){if(!e){this._statusPhases=[],this._statusPhasesName=null;return}if(this._statusPhasesName!==e){this._statusPhasesName=e;try{const s=await this._ws({type:`${y}/get_profile_phases`,entry_id:t,profile_name:e});this._statusPhases=(s.phases||[]).map(r=>({name:r.name,start:r.start,end:r.end}))}catch{this._statusPhases=[]}}}async _fetchSettingsChangelog(t){try{const s=await this._ws({type:`${y}/get_settings_changelog`,entry_id:t});this._settingsChangelog=s.changelog||[]}catch{this._settingsChangelog=this._settingsChangelog||[]}const e={};for(const s of this._settingsChangelog||[])s&&s.key!=null&&!(s.key in e)&&(e[s.key]=s);this._settingsChangeByKey=e}_registerUndo(t){const e="u"+ ++this._undoSeq;return t.timer=setTimeout(()=>this._commitDelete(e),1e4),this._undoBuffer.set(e,t),e}_undoDelete(t){const e=this._undoBuffer.get(t);if(e){this._undoBuffer.delete(t),e.timer&&clearTimeout(e.timer);try{e.restore()}catch{}this._toastTimer&&clearTimeout(this._toastTimer),this._toast=null,this._render()}}async _commitDelete(t){const e=this._undoBuffer.get(t);if(!e)return;this._undoBuffer.delete(t),e.timer&&clearTimeout(e.timer);const s=(r,n)=>{const a=this._devices[this._selIdx],o=a&&a.entry_id;if(e.eid===o){try{e.restore(r)}catch{}this._showToast(n,"error"),this._render()}};try{const r=await e.commit();r&&r.length&&s(r,this._t("toast.delete_partial_failed",{},"Some items could not be deleted and were restored"))}catch(r){s(null,this._t("toast.delete_failed",{error:r&&r.message||r},"Delete failed: "+(r&&r.message||r)))}}_isActiveEntry(t){const e=this._devices[this._selIdx];return!!e&&e.entry_id===t}_flushPendingDeletes(){return!this._undoBuffer||!this._undoBuffer.size?Promise.resolve():Promise.all(Array.from(this._undoBuffer.keys()).map(t=>this._commitDelete(t)))}_deleteCyclesWithUndo(t,e){const s=new Set(e),r=[];if(this._cycles=(this._cycles||[]).filter((i,l)=>s.has(i.id)?(r.push({idx:l,rec:i,ref:!1}),!1):!0),this._refCycles=(this._refCycles||[]).filter((i,l)=>s.has(i.id)?(r.push({idx:l,rec:i,ref:!0}),!1):!0),!r.length)return;this._cycleSel.clear(),this._selectMode=!1,this._render();const n=i=>{const l=i&&i.length?i:r,c=this._cycles.slice(),g=this._refCycles.slice();l.slice().sort((p,f)=>p.idx-f.idx).forEach(({idx:p,rec:f,ref:u})=>{const $=u?g:c;$.splice(Math.min(p,$.length),0,f)}),this._cycles=c,this._refCycles=g},a=async()=>{const i=[];for(const l of r)try{await this._ws({type:`${y}/delete_cycle`,entry_id:t,cycle_id:l.rec.id})}catch{i.push(l)}if(i.lengthi.name===e),r=s>=0?this._profiles[s]:{name:e};s>=0&&(this._profiles=this._profiles.filter(i=>i.name!==e)),this._modal=null,this._render();const n=i=>{const l=this._profiles.slice();l.splice(Math.min(s<0?l.length:s,l.length),0,r),this._profiles=l},a=async()=>{try{await this._ws({type:`${y}/delete_profile`,entry_id:t,profile_name:e,unlabel_cycles:!0})}catch{return[{idx:s,rec:r}]}try{await this._fetchProfiles(t),this._tab==="profiles"&&this._render()}catch{}return[]},o=this._registerUndo({eid:t,restore:n,commit:a});this._showToast(this._t("msg.profile_deleted",{name:e},"Profile deleted"),"success",{actionLabel:this._t("btn.undo",{},"Undo"),actionToken:o,duration:1e4})}_onKeydown(t){if(!t.defaultPrevented){if(t.key==="Escape"){this._modal&&(t.preventDefault(),this._onModalAction("cancel",null));return}if(this._modal&&t.key==="Tab"){const e=this.shadowRoot,s=e&&e.querySelector('.wd-modal[role="dialog"]');if(s){const r=Gt(s);if(r.length){const n=e.activeElement,a=r.indexOf(n);t.shiftKey?a<=0&&(t.preventDefault(),r[r.length-1].focus()):(a===-1||a===r.length-1)&&(t.preventDefault(),r[0].focus())}}return}if(["ArrowLeft","ArrowRight","Home","End"].includes(t.key)){const e=this.shadowRoot,s=e&&e.activeElement;if(s&&s.classList.contains("wd-tab")){const r=[...e.querySelectorAll("button.wd-tab")],n=r.indexOf(s);if(n===-1)return;let a=n;t.key==="ArrowRight"?a=(n+1)%r.length:t.key==="ArrowLeft"?a=(n-1+r.length)%r.length:t.key==="Home"?a=0:t.key==="End"&&(a=r.length-1),t.preventDefault(),r[a].click(),r[a].focus()}}}}async _loadMlIndex(t){if(this._mlById=this._mlById||{},!!this._constants.mlLabEnabled)try{const e=await this._ws({type:`${y}/get_ml_comparison`,entry_id:t});if(!this._isActiveEntry(t))return;this._mlComparison=e;const s={};for(const r of e&&e.cycles||[])s[r.id]=r;this._mlById=s,this._mlSettings=e&&e.settings_comparison||this._mlSettings,this._mlSettingsByEntry[t]=this._mlSettings}catch{}}async _loadMlSettings(t){if(this._mlSettings=this._mlSettings||{},!!this._constants.mlSuggestionsEnabled)try{const e=this._mlComparison||await this._ws({type:`${y}/get_ml_comparison`,entry_id:t});if(!this._isActiveEntry(t))return;this._mlComparison=e,this._mlSettings=e&&e.settings_comparison||{},this._mlSettingsByEntry[t]=this._mlSettings}catch{}}async _loadMlTrainingStatus(t){if(this._constants.mlTrainingAvailable)try{const e=await this._ws({type:`${y}/get_ml_training_status`,entry_id:t});if(!this._isActiveEntry(t))return;this._mlTrainingStatus=e}catch{}}async _fetchCycleProfileEnv(t,e){if(e)try{const s=await this._ws({type:`${y}/get_profile_envelope`,entry_id:t,profile_name:e}),r=this._modal;r&&r.type==="cycle-detail"&&r.entryId===t&&r.curve&&(r.curve.profile_name||"")===e&&(r.profileEnv=s.envelope||null,this._render())}catch{}}async _fetchSuggestions(t){this._suggestionsError=!1;try{const e=await this._ws({type:`${y}/get_suggestions`,entry_id:t});if(!this._isActiveEntry(t))return;this._suggestions=e.suggestions||[],this._lockedSuggestions=e.locked_suggestions||[],this._lockedByEntry[t]=this._lockedSuggestions}catch{this._isActiveEntry(t)&&(this._suggestionsError=!0,this._suggestions=[])}}async _fetchProfiles(t){this._profilesError=!1;try{const e=await this._ws({type:`${y}/get_profiles`,entry_id:t});this._profiles=e.profiles||[],this._profileHealth=e.profile_health||{},this._profileTrends=e.profile_trends||{},this._coverageGaps=e.coverage_gaps||{},this._profileAdvisories=e.profile_advisories||[],this._profileTerminal=e.profile_terminal||{}}catch{this._profilesError=!0}return this._profiles}async _ensureProfileEnvs(t,e){this._profileEnvCache=this._profileEnvCache||{};const s=[...new Set(e)].filter(r=>r&&!(r in this._profileEnvCache));return s.length?(await Promise.all(s.map(async r=>{try{const n=await this._ws({type:`${y}/get_profile_envelope`,entry_id:t,profile_name:r});this._profileEnvCache[r]=n&&n.envelope||null}catch{this._profileEnvCache[r]=null}})),this._profileEnvCache):this._profileEnvCache}async _fetchProfileGroups(t){this._profileGroupsError=!1;try{const e=await this._ws({type:`${y}/get_profile_groups`,entry_id:t});this._profileGroups={groups:e.groups||[],suggestions:e.suggestions||[],min_cohesion:e.min_cohesion||.85}}catch{this._profileGroupsError=!0,this._profileGroups={groups:[],suggestions:[],min_cohesion:.85}}return this._profileGroups}_deepLinkToken(){try{const t=new URLSearchParams(window.location.search||"").get("device");return t?t.trim():""}catch{return""}}_deepLinkIdx(t){const e=this._devices||[],s=String(t||"").trim();if(!s||!e.length)return-1;const r=s.toLowerCase();let n=e.findIndex(a=>String(a.entry_id||"").toLowerCase()===r);if(n<0&&(n=e.findIndex(a=>String(a.title||"").trim().toLowerCase()===r)),n<0){const a=ce(s);a&&(n=e.findIndex(o=>ce(o.title)===a))}return n}_rememberDevice(t){const e=(this._devices||[])[t];if(e)try{localStorage.setItem("wd-last-device",e.entry_id)}catch{}}_onOwnPath(){const t=this._panel&&this._panel.url_path;return t?(window.location.pathname||"").split("/").filter(Boolean)[0]===t:!0}_onLocationChanged(){if(!this._initialized||!this._devices.length||!this._onOwnPath())return;const t=this._deepLinkToken();if(!t)return;this._deepLinkApplied=t;const e=this._deepLinkIdx(t);if(e<0){console.warn(`[WashData panel] ?device=${t} matches no WashData device`);return}e!==this._selIdx&&this._selectDevice(e)}async _selectDevice(t){if(t===this._selIdx)return;await this._flushPendingDeletes(),this._selIdx=t,this._rememberDevice(t),this._pendingSettings={},this._dirtyOptKeys=new Set,this._prevOpts=null,this._cascadePending={},this._preCascadeOpts=null,this._stagedSuggestions=!1,this._mlComparison=null,this._mlById={},this._mlSettings={},this._profileEnvCache={},this._powerHistory=[],this._powerT0=null,this._statusEnv=null,this._statusEnvName=null,this._statusPhases=[],this._statusPhasesName=null,this._cycleOffset=0,this._cyclesTotal=0,this._cyclesHasMore=!1,this._settingsChangelog=null,this._settingsChangeByKey={},this._powerData={live:[],raw:[],cycle_active:!1,cycle_elapsed_s:0},this._matchDebug=null,this._profiles=[],this._profileHealth={},this._profileTrends={},this._coverageGaps={},this._profileAdvisories=[],this._profileTerminal={},this._opts={},this._optDefaults={},this._suggestions=[],this._lockedSuggestions=[],this._cycles=[],this._refCycles=[],this._recState=null,this._diag=null,this._maintenance=null,this._phases=[],this._mlTrainingStatus=null,this._setupStatus=null,this._deviceAutomations=[],this._selectMode=!1,this._cycleSel=new Set,this._cycleFilter={text:"",status:""},this._profSubtab="profiles",this._pgCycleId="",this._pgProfileName="",this._pgPowerPts=null,this._pgDtwData=null,this._pgEnvData=null,this._pgThreshStart=null,this._pgThreshStop=null,this._pgParamOverrides={},this._pgEffective=null,this._pgPublishable=null,this._pgPresets=[],this._pgPresetSel="",this._pgPresetName="",this._pgPresetLimit=0,this._pgSuggClassic={},this._pgSuggMl=null,this._pgMlSuggEnabled=!1,this._pgView=null,this._pgHoverT=null,this._pgLoadSeq++,this._pgNeedsRestart=!1,this._pgLoading=!1,this._pgDetail=null,this._pgHistory=null,this._pgSweepNew=null,this._pgHistoryTaskId=null,this._pgSweepTaskId=null,this._busy.delete("pg-history"),this._busy.delete("pg-sweep"),this._pgBatchProgress=null,this._pgDetailDebounceTimer&&(clearTimeout(this._pgDetailDebounceTimer),this._pgDetailDebounceTimer=null),this._pgStressTail=!1,this._pgStressIdleW=null,this._storeView="brands",this._storeDevice=null,this._storeProfile=null,this._storeQuery="",this._storeDevices=[],this._storeProfiles=[],this._storeCycles=[],this._storeStatus=null,this._storeConnected=!1,this._storeLoading=!1,clearTimeout(this._brandSearchTimer),this._brandSearchTimer=null,this._catalog={brands:this._catalog.brands,devices:void 0,forBrand:null,approvedOnly:this._catalog.approvedOnly,brandsFull:this._catalog.brandsFull,brandPrefixes:this._catalog.brandPrefixes},this._entityListCache&&delete this._entityListCache.store_model;const e=this._devices[this._selIdx];e&&await this._fetchSuggestions(e.entry_id),this._fetchTabData()}_refreshDeviceBar(){const t=this.shadowRoot;if(!t)return;const e=t.querySelector(".wd-devbar"),s=this._htmlDeviceBar();if(e&&s){const r=document.createElement("div");r.innerHTML=s;const n=r.firstElementChild;n&&(e.replaceWith(n),n.querySelectorAll(".wd-devcard[data-idx]").forEach(a=>a.addEventListener("click",()=>this._selectDevice(parseInt(a.dataset.idx,10)))))}}_refreshLogDrawer(){this._logOpen&&(this._refreshLogViews(),this._refreshLogFilterOptions())}_refreshLogFilterOptions(){const t=this.shadowRoot;if(!t)return;const e=(n,a,o)=>``+a.map(i=>``).join(""),s=e(this._t("log.all_devices",{},"All devices"),this._logDevices(),this._logDevice),r=e(this._t("log.all_components",{},"All components"),this._logComponents(),this._logComponent);t.querySelectorAll('.wd-log-filter[data-logfilter="device"]').forEach(n=>{n.innerHTML=s,n.value=this._logDevice||""}),t.querySelectorAll('.wd-log-filter[data-logfilter="component"]').forEach(n=>{n.innerHTML=r,n.value=this._logComponent||""})}async _fetchTabData(){const t=this._devices[this._selIdx];if(!t)return;const e=t.entry_id;this._tabLoading=!0,this._render();try{if(this._tab==="status"){if(this._powerData=await this._ws({type:`${y}/get_power_history`,entry_id:e,with_raw:this._pref("show_raw_active",!1)}),this._profiles.length||await this._fetchProfiles(e),!this._profiles.length&&!this._cycles.length&&await this._fetchCycles(e),await this._ensureStatusPhases(e,t.current_program),this._pref("show_debug",!1))try{this._matchDebug=await this._ws({type:`${y}/get_match_debug`,entry_id:e})}catch{}if(this._canEdit())try{this._recState=await this._ws({type:`${y}/get_recording_state`,entry_id:e})}catch{}try{this._setupStatus=await this._ws({type:`${y}/get_setup_status`,entry_id:e})}catch{this._setupStatus=null}}else if(this._tab==="history"){await this._fetchCycles(e),this._profiles.length||await this._fetchProfiles(e);try{const s=await this._ws({type:`${y}/get_feedbacks`,entry_id:e});this._feedbacks=s.feedbacks||[]}catch{}this._constants.storeOnlineAvailable&&this._onlineEnabled()&&this._loadStoreStatus(e).then(()=>{this._tab==="history"&&this._render()}),this._constants.mlLabEnabled&&(this._mlLoading=!0,this._loadMlIndex(e).finally(()=>{this._mlLoading=!1;const s=this._modal;s&&s.type==="cycle-detail"&&!s.ml&&this._mlById[s.cycleId]&&(s.ml=this._mlById[s.cycleId]),(this._tab==="history"||s&&s.type==="cycle-detail")&&this._render()}))}else if(this._tab==="profiles"){if(await this._fetchProfiles(e),this._fetchProfileGroups(e).then(()=>{this._tab==="profiles"&&this._render()}),this._cycles.length||this._fetchCycles(e).then(()=>{this._tab==="profiles"&&this._render()}),this._profSubtab==="phase-catalog")try{const s=await this._ws({type:`${y}/get_phase_catalog`,entry_id:e});this._phases=s.phases||[]}catch{}}else if(this._tab==="settings"){const s=await this._ws({type:`${y}/get_options`,entry_id:e});if(!this._isActiveEntry(e))return;this._opts=s.options||{},this._optDefaults=s.defaults||{},await this._fetchSuggestions(e),await this._fetchSettingsChangelog(e),this._constants.mlSuggestionsEnabled&&(this._mlSettingsLoading=!0,this._loadMlSettings(e).finally(()=>{this._mlSettingsLoading=!1,this._tab==="settings"&&this._renderPreservingFormEdits()})),this._constants.mlTrainingAvailable&&this._loadMlTrainingStatus(e).finally(()=>{this._tab==="settings"&&this._renderPreservingFormEdits()}),this._autoLoading=!0,this._loadDeviceAutomations(e).finally(()=>{this._autoLoading=!1,this._tab==="settings"&&this._renderPreservingFormEdits()}),this._constants.storeOnlineAvailable&&this._loadStoreStatus(e).finally(()=>{this._tab==="settings"&&this._renderPreservingFormEdits()})}else if(this._tab==="store"){try{const s=await this._ws({type:`${y}/get_options`,entry_id:e});if(!this._isActiveEntry(e))return;this._opts=s.options||{}}catch{}if(this._profiles.length||this._fetchProfiles(e),await this._loadStoreStatus(e),!this._isActiveEntry(e))return;this._ensureStoreConnectListener(),this._onlineEnabled()&&this._storeSearch(this._storeBrandScope())}else if(this._tab==="advanced"&&this._panelSubtab==="ml"){const s=await this._ws({type:`${y}/get_options`,entry_id:e});if(!this._isActiveEntry(e))return;this._opts=s.options||{},this._loadMlTrainingStatus(e).finally(()=>{this._tab==="advanced"&&this._panelSubtab==="ml"&&this._renderPreservingFormEdits()})}else if(this._tab==="playground"){if(await Promise.all([(async()=>{try{const s=await this._ws({type:`${y}/get_options`,entry_id:e});this._isActiveEntry(e)&&(this._opts=s.options||{})}catch{}})(),this._pgFetchSettings(e,!1),this._fetchCycles(e),this._profiles.length?Promise.resolve():this._fetchProfiles(e)]),!this._isActiveEntry(e))return;this._pgFetchSuggestions(e),!this._pgCycleId&&this._cycles?.length&&(this._pgCycleId=this._cycles[0].id,this._pgProfileName=""),this._pgNeedsRestart=!1,this._pgRestartRetries=0,this._pgAdoptExisting()}else this._tab==="advanced"&&this._panelSubtab==="maintenance"&&!this._maintenance&&this._fetchMaintenance(e).then(()=>{this._tab==="advanced"&&this._render()})}catch(s){console.warn("[WashData panel] tab data fetch error -",It(s),s)}finally{this._tabLoading=!1,this._render()}}async _fetchToolsData(t){try{const e=await this._ws({type:`${y}/get_diagnostics`,entry_id:t});if(!this._isActiveEntry(t))return;this._diag=e.stats||{}}catch(e){console.warn("[WashData panel] tools fetch error -",It(e),e),this._diag={_error:String(e&&e.message||e)}}}async _fetchMaintenance(t){try{const e=await this._ws({type:`${y}/get_maintenance_log`,entry_id:t});this._maintenance=e||{}}catch(e){console.warn("[WashData panel] maintenance fetch error -",It(e),e),this._maintenance={_error:String(e&&e.message||e)}}}async _fetchLogs(){try{const t=await this._ws({type:`${y}/get_logs`,level:null,limit:500});this._logs=t.logs||[]}catch(t){console.warn("[WashData panel] logs fetch error -",It(t),t)}}_logComponents(){return[...new Set((this._logs||[]).map(t=>t.logger).filter(Boolean))].sort()}_logDevices(){return[...new Set((this._logs||[]).map(t=>t.device).filter(Boolean))].sort()}_filteredLogRecords(){const t={DEBUG:10,INFO:20,WARNING:30,ERROR:40,CRITICAL:50},e=this._logLevel&&t[this._logLevel]||0,s=this._logDevice,r=this._logComponent,n=(this._logSearch||"").trim().toLowerCase();return(this._logs||[]).filter(a=>!(e&&(t[a.level]||0){const s=new Date(e.ts*1e3).toLocaleTimeString(),r=e.device?`${d(e.device)}`:"";return`
    ${s}${d(e.level)}${d(e.logger||"")}${r}${d(e.msg)}
    `}).join(""):(this._logs||[]).length>0?`

    ${this._t("msg.no_logs_match",{},"No log records match these filters.")}

    `:`

    ${this._t("msg.no_logs",{},"No log records buffered yet.")}

    `}_htmlLogFilters(t){const s=["","DEBUG","INFO","WARNING","ERROR"].map(a=>``).join(""),r=``+this._logDevices().map(a=>``).join(""),n=``+this._logComponents().map(a=>``).join("");return` - - - `}_refreshLogViews(){const t=this.shadowRoot;if(!t)return;const e=this._logLinesHtml();["wd-log-lines-drawer","wd-log-lines-page"].forEach(s=>{const r=t.getElementById(s);r&&(r.innerHTML=e)})}_syncLogFilters(t){const e=this.shadowRoot;e&&e.querySelectorAll(`.wd-log-filter[data-logfilter="${t.dataset.logfilter}"]`).forEach(s=>{s!==t&&s.value!==t.value&&(s.value=t.value)})}async _fetchRecState(t){try{this._recState=await this._ws({type:`${y}/get_recording_state`,entry_id:t})}catch{}}async _fetchFeedbacks(t){try{const e=await this._ws({type:`${y}/get_feedbacks`,entry_id:t});this._feedbacks=e.feedbacks||[]}catch{}}async _fetchPhases(t){try{const e=await this._ws({type:`${y}/get_phase_catalog`,entry_id:t});this._phases=e.phases||[]}catch{}}_localize(t,e){try{const s=this._hass&&this._hass.localize?this._hass.localize(t):"";return s&&s!==t?s:e}catch{return e}}_tLookup(t,e){const s=this._panelTrans&&(this._panelTrans[e]||this._panelTrans.en);if(!s)return null;const r=t.split(".").reduce((n,a)=>n&&n[a]!==void 0?n[a]:null,s);return r&&typeof r=="string"?r:null}_t(t,e={},s=""){let r;const a=this._panelCfg&&this._panelCfg.prefs&&this._panelCfg.prefs.lang_override||this._hass&&this._hass.locale&&this._hass.locale.language;this._panelTrans?r=a&&this._tLookup(t,a)||this._tLookup(t,"en")||s:r=this._localize(`component.${y}.panel.${t}`,s);for(const[o,i]of Object.entries(e))r=r.replace(new RegExp(`\\{${o}\\}`,"g"),String(i));return r}_stateColor(t){const e=this._constants.stateColors||{};return e[t]||e.unknown||"var(--disabled-color, #bdbdbd)"}_stateLabel(t){const e=(t||"unknown").replace(/_/g," ").replace(/\b\w/g,s=>s.toUpperCase());return this._localize(`component.${y}.entity.sensor.washer_state.state.${t}`,e)}_deviceTypeLabel(t){const e=(this._constants.deviceTypes||[]).find(r=>r.id===t),s=e?e.label:(t||"").replace(/_/g," ");return this._localize(`component.${y}.selector.device_type.options.${t}`,s)}_deviceTypeOpts(t){const e=(this._constants.deviceTypes||[]).map(s=>[s.id,this._deviceTypeLabel(s.id)]);return t&&!e.some(([s])=>String(s)===String(t))&&e.push([t,this._deviceTypeLabel(t)]),e}_deviceOpts(t){const e=[["","- None -"]],s=this._hass&&this._hass.devices?this._hass.devices:{},r=this._ownDeviceId();return Object.values(s).forEach(n=>{if(r&&String(n.id)===r)return;const a=n.name_by_user||n.name||n.id;e.push([n.id,a])}),t&&String(t)!==r&&!e.some(([n])=>String(n)===String(t))&&e.push([t,this._t("lbl.device_unresolved",{id:t},`Unavailable device (${t})`)]),e}_ownDeviceId(){const t=this._devices&&this._devices[this._selIdx],e=t&&t.entry_id;if(!e)return"";const s=this._hass&&this._hass.devices?this._hass.devices:{};for(const r of Object.values(s)){if(!r||!r.id)continue;if((Array.isArray(r.identifiers)?r.identifiers:[]).some(a=>Array.isArray(a)&&a[0]===y&&String(a[1])===String(e))||Array.isArray(r.config_entries)&&r.config_entries.map(String).includes(String(e)))return String(r.id)}return""}_applyFontScale(t){let e=parseFloat(t);isFinite(e)||(e=1),e=Math.max(.7,Math.min(2,e)),this.style.fontSize=e===1?"":Math.round(e*1e3)/10+"%"}_applyPanelConfig(){const t=this._panelCfg;if(!t)return;this._applyFontScale(t.prefs&&t.prefs.font_scale||1);const e=t.prefs&&t.prefs.lang_override;e&&!(this._panelTrans&&this._panelTrans[e])&&this._loadPanelLang(e).then(()=>this._render()).catch(()=>{});const s=t.panel||{};if(!this._tabInitialized){const r=t.prefs&&t.prefs.default_tab||s.default_tab;r&&["status","history","profiles","settings","playground"].includes(r)&&(this._tab=r),this._tabInitialized=!0}}_isAdmin(){return!!(this._panelCfg&&this._panelCfg.is_admin)}_curPerm(){const t=this._devices[this._selIdx];return t&&t.perm||"full"}_canEdit(){const t=this._curPerm();return this._isAdmin()||t==="edit"||t==="full"}_canFull(){const t=this._curPerm();return this._isAdmin()||t==="full"}_onlineEnabled(){return!!(this._constants&&this._constants.storeOnlineAvailable&&this._constants.storeOnlineEnabled)}_visibleTabIds(){const t=this._isAdmin(),e=!t&&this._panelCfg&&this._panelCfg.panel&&this._panelCfg.panel.hidden_tabs||[],s=["status","history","profiles"];return this._canEdit()&&s.push("settings"),this._canEdit()&&s.push("playground"),this._canEdit()&&this._onlineEnabled()&&s.push("store"),s.push("advanced"),s.filter(r=>t||!e.includes(r))}async _busyRun(t,e){this._busy.add(t),this._render();try{return await e()}finally{this._busy.delete(t),this._render()}}async _closeCycleDetail(t){const e=this._prevModal;if(this._prevModal=null,e&&e.type==="profile-panel"){if(this._modal=e,this._render(),e.tab==="cleanup")try{const s=await this._ws({type:`${y}/get_profile_cycles`,entry_id:t,profile_name:e.name});this._modal&&this._modal.type==="profile-panel"&&this._modal.name===e.name&&(this._modal.cleanup={cycles:s.cycles||[],selected:new Set},this._render(),this._drawSpaghetti())}catch{}}else this._modal=null,this._render()}_SCROLLERS=[".wd-main",".wd-modal",".wd-logs",".wd-log-drawer-body",".wd-table-wrap",".wd-sd-tree"];_SCROLLERS_CHROME=[".wd-tabs"];_MODAL_KEY_PREFIX="modal:";_eachScroller(t,e){const s=this.shadowRoot;if(s)for(const r of t){const n=Array.from(s.querySelectorAll(r)),a=o=>r!==".wd-modal"&&!!o.closest(".wd-modal");for(const o of[!1,!0]){const i=n.filter(c=>a(c)===o),l=o?this._MODAL_KEY_PREFIX:"";i.forEach((c,g)=>e(c,`${l}${r}|${g}/${i.length}`))}}}_captureScroll(){if(!this.shadowRoot)return null;const t={};return this._eachScroller([...this._SCROLLERS,...this._SCROLLERS_CHROME],(e,s)=>{(e.scrollTop||e.scrollLeft)&&(t[s]={top:e.scrollTop,left:e.scrollLeft})}),t}_scrollChromeOnly(t){if(!t)return t;const e={};for(const[s,r]of Object.entries(t))this._SCROLLERS_CHROME.some(n=>s.startsWith(`${n}|`))&&(e[s]=r);return e}_restoreScroll(t){!t||!this.shadowRoot||this._eachScroller([...this._SCROLLERS,...this._SCROLLERS_CHROME],(e,s)=>{const r=t[s];if(!r)return;const n=Math.max(0,e.scrollHeight-e.clientHeight),a=Math.max(0,e.scrollWidth-e.clientWidth);e.scrollTop=Math.min(r.top,n),e.scrollLeft=Math.min(r.left,a)})}_navKey(){return[this._tab,this._selIdx,this._panelSubtab,this._profSubtab,this._toolsSubtab,this._settingsSec,this._storeView].join("|")}_modalNavKey(){const t=this._modal;if(!t)return"";const e=t.type==="profile-group"?t.orig||"":t.name||t.cycleId||"";return[t.type,e,t.tab||t.mode||t.step||""].join("|")}_render(){if(!this._container)return;se=this._pref("date_format","relative");const t=this.shadowRoot,e=t&&(t.activeElement||this.getRootNode()&&this.getRootNode().activeElement)||null;this._hideGraphTip();const s=this._navKey(),r=this._scrollNavKey!=null&&this._scrollNavKey!==s;this._scrollNavKey=s;let n=this._scrollCarry||this._captureScroll();r&&(n=this._scrollChromeOnly(n));const a=this._modalNavKey();this._scrollModalKey!==void 0&&a!==this._scrollModalKey&&n&&(n=Object.fromEntries(Object.entries(n).filter(([o])=>!o.startsWith(".wd-modal|")&&!o.startsWith(this._MODAL_KEY_PREFIX)))),this._scrollModalKey=a,this._container.innerHTML=this._buildHtml(),this._wire(),this._tabLoading?(this._scrollCarry=n,this._restoreScroll(this._scrollChromeOnly(n))):(this._scrollCarry=null,this._restoreScroll(n)),this._drawStatusCurve(),this._drawModalCanvas(),this._drawProfileSparklines(),this._drawHistorySparklines(),this._drawPlaygroundCanvases(),["wd-status-canvas","wd-cyc-canvas","wd-compare-canvas","wd-env-canvas","wd-phase-canvas","wd-spag-canvas","wd-pgroup-canvas"].forEach(o=>this._attachGraphGestures(o)),this._syncModalFocus(e),requestAnimationFrame(()=>this._resizeLogsPage())}_resizeLogsPage(){const t=this.shadowRoot&&this.shadowRoot.getElementById("wd-log-lines-page");if(!t)return;const e=t.getBoundingClientRect();e.top<=0||(t.style.height=window.innerHeight-e.top-24+"px")}_syncModalFocus(t=null){const e=this.shadowRoot;if(!e)return;const s=e.querySelector('.wd-modal[role="dialog"]');if(s){const r=s.querySelector("h2");if(r&&!r.id&&(r.id="wd-modal-title"),this._modalFocusActive){const n=e.activeElement||this.getRootNode()&&this.getRootNode().activeElement||null;if(!n||!s.contains(n)){const a=Gt(s);try{(a[0]||s).focus({preventScroll:!0})}catch{}}}else{this._modalFocusActive=!0;const n=t||null;n&&!s.contains(n)&&(this._modalReturnFocus=n);const a=Gt(s);try{(a[0]||s).focus()}catch{}}}else if(this._modalFocusActive){this._modalFocusActive=!1;const r=this._modalReturnFocus;if(this._modalReturnFocus=null,r&&r.isConnected&&typeof r.focus=="function")try{r.focus()}catch{}}}_renderPreservingFormEdits(){this._snapshotFormToPending(this.shadowRoot),this._render()}_snapshotCycleReviewForm(t){const e=this._modal;if(!t||!e||e.type!=="cycle-detail"||e.mode!=="review")return;const s=t.getElementById("wd-cyc-rev-quality"),r=t.getElementById("wd-cyc-rev-golden"),n=t.getElementById("wd-cyc-rev-notes"),a=t.getElementById("wd-cyc-rev-label");if(!s&&!r&&!n&&!a)return;e.ml||(e.ml={}),e.ml.ml_review||(e.ml.ml_review={});const o=e.ml.ml_review;s&&(o.quality=s.value||""),r&&(o.golden=!!r.checked),n&&(o.notes=n.value||""),o.tags=Array.from(t.querySelectorAll(".wd-cyc-rev-tag")).filter(i=>i.checked).map(i=>i.value),a&&e.curve&&(e.curve.profile_name=a.value||"")}_buildHtml(){const t=this._toast?`
    ${d(this._toast.msg)}${this._toast.actionLabel?``:""}
    `:"";return` +
    ${a}
    +
    `}_settleTaskCallback(t){if(t.state==="running")return;const e=this._taskCallbacks[t.id];if(e){delete this._taskCallbacks[t.id],e(t);return}this._autoSettleAdopted(t)}_autoSettleAdopted(t){if(t.state!=="done"||t.kind!=="ml_training"&&t.kind!=="reprocess"||!t.finished_at||Date.now()/1e3-t.finished_at>60||(this._autoSettled=this._autoSettled||new Set,this._autoSettled.has(t.id)))return;const e=this._devices[this._selIdx];if(!e||t.entry_id!==e.entry_id)return;this._autoSettled.add(t.id);const s=e.entry_id,a=()=>{this._isActiveEntry(s)&&this._render()};t.kind==="ml_training"?this._loadMlTrainingStatus(s).finally(a):this._fetchToolsData(s).finally(a)}async _kickAndTrack(t,e,s){if(this._busy.has(e))return;this._busy.add(e),this._render();let a;try{const i=await this._ws(t);if(a=i&&i.task_id,!a)throw new Error("no task id");const o=String(t.type||"").endsWith("reprocess_history")?"reprocess":String(t.type||"").endsWith("trigger_ml_training")?"ml_training":String(t.type||"").endsWith("apply_split")?"split":String(t.type||"").endsWith("trim_cycle")?"trim":String(t.type||"").endsWith("apply_merge")?"merge":String(t.type||"").endsWith("rebuild_envelopes")?"rebuild":"task";this._addProvisionalTask(a,o,t.entry_id,0)}catch(i){this._busy.delete(e),this._showToast(this._tText("msg.toast_error",{error:i.message||i},"Error: "+(i.message||i)),"error"),this._render();return}this._taskCallbacks[a]=async i=>{if(this._busy.delete(e),i.state==="error"){this._showToast(this._tText("msg.toast_error",{error:i.error||""},"Error: "+(i.error||"")),"error"),this._render();return}if(i.state==="cancelled"){this._showToast(this._t("toast.task_cancelled",{},"Cancelled."),"info"),this._render();return}let o=null;try{const n=await this._ws({type:`${w}/get_task_result`,task_id:i.id});o=n&&n.result}catch{}if(o==null){this._showToast(this._t("toast.pg_run_gone",{},"That run is no longer available."),"info"),this._render();return}try{await s(o,i.state)}catch{}this._render()};const r=this._tasks[a];if(r&&r.state!=="running"){this._settleTaskCallback(r);return}this._tasksSubscribed||this._pollTaskGeneric(a)}_finalizeTaskError(t,e){const s=Object.assign({},this._tasks[t]||{id:t},{id:t,state:"error",error:e,finished_at:Date.now()/1e3});this._tasks[t]=s,this._updateTaskPills(),this._settleTaskCallback(s)}async _pollTaskGeneric(t){let e=0;for(let s=0;s<3600&&this._taskCallbacks[t];s++){let a;try{a=await this._ws({type:`${w}/get_task_result`,task_id:t}),e=0}catch{if(++e>=5){this._finalizeTaskError(t,"lost connection");return}await new Promise(i=>setTimeout(i,1200));continue}if(!a){this._finalizeTaskError(t,"no result");return}if(this._tasks[t]=a,this._updateTaskPills(),a.state!=="running"){this._settleTaskCallback(a);return}await new Promise(r=>setTimeout(r,1200))}this._taskCallbacks[t]&&this._finalizeTaskError(t,"timed out")}_deviceName(t){const e=(this._devices||[]).find(s=>s.entry_id===t);return e&&(e.name||e.title)||""}_taskActionLabel(t){return{pg_history:this._t("lbl.task_pg_history",{},"Test on history"),pg_sweep:this._t("lbl.task_pg_sweep",{},"Optimize"),pg_detail:this._t("lbl.task_pg_detail",{},"Simulate cycle"),split:this._t("lbl.task_split",{},"Splitting cycle"),trim:this._t("lbl.task_trim",{},"Trimming cycle"),merge:this._t("lbl.task_merge",{},"Merging cycles"),rebuild:this._t("lbl.task_rebuild",{},"Rebuilding envelopes"),reprocess:this._t("lbl.task_reprocess",{},"Reprocessing"),ml_training:this._t("lbl.task_ml_training",{},"Learning"),history_import:this._t("lbl.task_history_import",{},"Scanning power history"),history_import_apply:this._t("lbl.task_history_import_apply",{},"Importing cycles")}[t]||t}_fmtEta(t){return t=Math.round(t),t<60?this._t("lbl.eta_secs",{n:t},`~${t}s left`):this._t("lbl.eta_mins",{n:Math.round(t/60)},`~${Math.round(t/60)}m left`)}_exclNote(t){if(!t||!t.total)return"";const e=(t.items||[]).map(([s,a])=>`${a} ${this._t("suggestion.exclusions.reason."+s,{},String(s).replace(/_/g," "))}`).join(", ");return" "+this._tText("suggestion.exclusions.summary",{total:t.total,parts:e},`Excluded ${t.total} mis-detected cycle(s): ${e}.`)}_htmlTaskPills(){const t=Object.values(this._tasks||{}).filter(s=>s.state==="running"),e=new Set(t.map(s=>s.id));return this._cancellingTasks.forEach(s=>{e.has(s)||this._cancellingTasks.delete(s)}),t.length?t.map(s=>{const a=this._cancellingTasks.has(s.id),r=this._deviceName(s.entry_id),i=s.label_key?this._tText(s.label_key,s.label_params||{},s.label||this._taskActionLabel(s.kind)):this._taskActionLabel(s.kind),o=(r?r+" \xB7 ":"")+i,n=s.progress!=null?Math.round(s.progress*100)+"%":"",l=s.eta_s!=null&&s.eta_s>0?this._fmtEta(s.eta_s):"",c=this._t("task.cancelling",{},"Cancelling\u2026"),p="wd-task-pill"+(a?" wd-task-pill--cancelling":""),_=a?" \xB7 "+c:n?" "+n:"";return`${d(o)}`+(a?`${d(c)}`:(n?`${n}`:"")+(l?`${d(l)}`:""))+``}).join(""):""}_updateTaskPills(){const t=this.shadowRoot;if(!t)return;const e=t.getElementById("wd-task-pills");e&&(e.innerHTML=this._htmlTaskPills())}_addProvisionalTask(t,e,s,a){!t||this._tasks[t]||(this._tasks[t]={id:t,entry_id:s,kind:e,label:this._taskActionLabel(e),state:"running",done:0,total:a||0,progress:a?0:null,eta_s:null,updated_at:0,has_result:!1},this._updateTaskPills())}_onTrackedTaskProgress(t){if(!(t.id!==this._pgHistoryTaskId&&t.id!==this._pgSweepTaskId)){if(t.state==="running"){this._pgBatchProgress={done:t.done||0,total:t.total||0},this._pgUpdateBatchBar(t.done||0,t.total||0);return}this._pgFinishTask(t,t.id===this._pgHistoryTaskId)}}async _pgFinishTask(t,e){let s=null;try{if(t.state==="done"||t.state==="cancelled"){const a=await this._ws({type:`${w}/get_task_result`,task_id:t.id});s=a&&a.result}}catch{}this._isActiveEntry(t.entry_id)&&(t.state==="error"?this._showToast(this._tText("msg.toast_error",{error:t.error||""},"Error: "+(t.error||"")),"error"):s&&(e?this._pgHistory=s:this._pgSweepNew=s&&!s.error?s:null),e?(this._busy.delete("pg-history"),this._pgHistoryTaskId=null):(this._busy.delete("pg-sweep"),this._pgSweepTaskId=null),this._pgBatchProgress=null,this._render())}async _awaitTask(t){for(let e=0;e<7200&&t;e++){const s=(this._tasks||{})[t];let a=s&&s.state!=="running"?s:null;if(a||(a=await this._ws({type:`${w}/get_task_result`,task_id:t})),a&&a.state&&a.state!=="running"){if(a.state==="error")throw new Error(a.error||"error");return a}await new Promise(r=>setTimeout(r,1e3))}return null}async _pgPollTask(t){for(let e=0;e<3600&&(t===this._pgHistoryTaskId||t===this._pgSweepTaskId);e++){let s;try{s=await this._ws({type:`${w}/get_task_result`,task_id:t})}catch{break}if(!s)break;if(this._tasks[t]=s,this._updateTaskPills(),s.state!=="running"){this._onTrackedTaskProgress(s);return}this._pgBatchProgress={done:s.done||0,total:s.total||0},this._pgUpdateBatchBar(s.done||0,s.total||0),await new Promise(a=>setTimeout(a,1200))}}_panelTransUrl(t){const e=`/ha_washdata/panel-translations/${encodeURIComponent(t)}.json`;return Zt?`${e}?v=${encodeURIComponent(Zt)}`:e}async _fetchPanelLang(t){if(!t)return null;const e=[t],s=t.indexOf("-");s>0&&e.push(t.slice(0,s));for(const a of e)try{const r=await fetch(this._panelTransUrl(a));if(r.ok){const i=await r.json();if(i&&typeof i=="object")return i}}catch{}return null}async _loadPanelLang(t){if(!t||this._panelTrans&&this._panelTrans[t])return;const e=await this._fetchPanelLang(t);e&&(this._panelTrans||(this._panelTrans={}),this._panelTrans[t]=e)}async _loadPanelTranslations(){const t=this._hass&&this._hass.locale&&this._hass.locale.language;await Promise.all([this._loadPanelLang("en"),t&&t!=="en"?this._loadPanelLang(t):Promise.resolve()])}_startPoll(){this._stopPoll(),this._pollTimer=setInterval(()=>this._fetchAll(),this._pollMs)}_stopPoll(){this._pollTimer&&(clearInterval(this._pollTimer),this._pollTimer=null)}async _ws(t){try{return await this._hass.connection.sendMessagePromise(t)}catch(e){if(e&&e.code==="invalid_threshold_pair"){const s=/\(([-\d.]+) W\)[^(]*\(([-\d.]+) W\)/.exec(String(e.message||""));e.message=this._tText("msg.invalid_threshold_pair",{stop:s?s[1]:"?",start:s?s[2]:"?"},e.message)}throw e}}async _fetchAll(){if(!this._hass)return;const t=this._loading;let e=!1;try{if(!this._constantsLoaded){const[l,c]=await Promise.all([this._ws({type:`${w}/get_constants`}).catch(()=>null),this._ws({type:`${w}/get_panel_config`}).catch(()=>null)]);if(l&&(this._constants={stateColors:l.state_colors||{},deviceTypes:l.device_types||[],mlLabEnabled:!!l.ml_lab_enabled,mlTrainingAvailable:!!l.ml_training_available,storeOnlineAvailable:!!l.store_online_available,storeOnlineEnabled:!!l.store_online_enabled,storeWebOrigin:l.store_web_origin||"",storePrefs:l.store_prefs||{},pgMatchDefaults:l.pg_match_defaults||{},PROFILE_MIN_WARMUP_CYCLES:l.PROFILE_MIN_WARMUP_CYCLES,version:l.version||"",iconUrl:l.icon_url||""}),c)try{this._panelCfg=c,this._applyPanelConfig()}catch{}this._constantsLoaded=!0}const s=await this._ws({type:`${w}/get_devices`});this._lastFetchErr=null,this._fetchFailed=!1,this._devices=s.devices||[],this._lastRefresh=new Date;let a=!1;const r=this._deepLinkToken();if(r&&r!==this._deepLinkApplied&&this._devices.length){this._deepLinkApplied=r;const l=this._deepLinkIdx(r);l>=0?(this._selIdx=l,a=!0,this._rememberDevice(l)):console.warn(`[WashData panel] ?device=${r} matches no WashData device`)}if(!a&&this._selIdx===0&&this._devices.length>1){const l=localStorage.getItem("wd-last-device");if(l){const c=this._devices.findIndex(p=>p.entry_id===l);c>0&&(this._selIdx=c)}}const i=this._devices[this._selIdx],o=async()=>{if(i&&this._tab==="status"){try{this._powerData=await this._ws({type:`${w}/get_power_history`,entry_id:i.entry_id,with_raw:this._pref("show_raw_active",!1)})}catch{}if(this._pref("show_debug",!1))try{this._matchDebug=await this._ws({type:`${w}/get_match_debug`,entry_id:i.entry_id})}catch{}if(this._canEdit()&&i.recording)try{this._recState=await this._ws({type:`${w}/get_recording_state`,entry_id:i.entry_id})}catch{}}},n=async()=>{if(i&&i.current_program){if(this._statusEnvName!==i.current_program){this._statusEnvName=i.current_program;try{const l=await this._ws({type:`${w}/get_profile_envelope`,entry_id:i.entry_id,profile_name:i.current_program});this._statusEnv=l.envelope||null}catch{this._statusEnv=null}}await this._ensureStatusPhases(i.entry_id,i.current_program)}else this._statusEnv=null,this._statusEnvName=null,this._statusPhases=[],this._statusPhasesName=null};if(await Promise.all([o(),n()]),t&&i){const l=i.entry_id,c=Promise.all([this._fetchCycles(l),this._fetchSuggestions(l),this._fetchProfiles(l),this._tab==="status"?this._ws({type:`${w}/get_setup_status`,entry_id:l}).then(p=>{this._isActiveEntry(l)&&(this._setupStatus=p)},()=>{this._isActiveEntry(l)&&(this._setupStatus=null)}):null,this._constants.storeOnlineAvailable?(async()=>{try{const p=await this._ws({type:`${w}/get_options`,entry_id:l});this._isActiveEntry(l)&&(this._opts=p.options||{})}catch{}this._onlineEnabled()&&await this._loadStoreStatus(l)})():null]);this._tab==="status"&&(this._loading=!1,e=!0),this._render(),await c}this._logOpen&&this._isAdmin()&&this._fetchLogs().then(()=>this._refreshLogDrawer()).catch(()=>{})}catch(s){const a=Ct(s);a!==this._lastFetchErr&&(this._lastFetchErr=a,console.warn("[WashData panel] fetch error -",a,s)),this._fetchFailed=!0}finally{this._loading=!1,this._refreshStaleChip();const s=this.shadowRoot,a=s&&s.activeElement,r=!!(a&&["SELECT","INPUT","TEXTAREA","OPTION"].includes(a.tagName));t&&!e?this._render():t&&!this._modal&&!r?this._render():this._tab==="status"&&!this._modal&&!r?this._render():this._tab==="status"&&!this._modal&&r?(this._drawStatusCurve(),this._refreshDeviceBar()):this._refreshDeviceBar()}}async _fetchCycles(t){this._cyclesError=!1;try{const e=await this._ws({type:`${w}/get_device_cycles`,entry_id:t,limit:zt,offset:0,imported_offset:0});if(!this._isActiveEntry(t))return;this._cycles=e.cycles||[],this._refCycles=[...e.reference_cycles||[],...e.backfill_cycles||[]],this._cycleOffset=this._cycles.length,this._cyclesTotal=e.total!=null?e.total:this._cycles.length,this._cyclesHasMore=e.has_more!=null?!!e.has_more:!1,this._importedOffset=this._refCycles.length,this._importedHasMore=!!e.imported_has_more}catch{if(!this._isActiveEntry(t))return;this._cyclesError=!0,this._cycles=[],this._refCycles=[],this._cycleOffset=0,this._cyclesTotal=0,this._cyclesHasMore=!1,this._importedOffset=0,this._importedHasMore=!1}}async _loadMoreCycles(t){const e=await this._ws({type:`${w}/get_device_cycles`,entry_id:t,limit:zt,offset:this._cycleOffset,imported_offset:this._importedOffset});if(!this._isActiveEntry(t))return;const s=e.cycles||[],a=new Set(this._cycles.map(o=>o.id));for(const o of s)a.has(o.id)||this._cycles.push(o);this._cycleOffset+=s.length,this._cyclesTotal=e.total!=null?e.total:this._cyclesTotal,this._cyclesHasMore=e.has_more!=null?!!e.has_more:s.length>=zt;const r=[...e.reference_cycles||[],...e.backfill_cycles||[]],i=new Set((this._refCycles||[]).map(o=>o.id));this._refCycles=(this._refCycles||[]).concat(r.filter(o=>!i.has(o.id))),this._importedOffset+=r.length,this._importedHasMore=!!e.imported_has_more}async _ensureStatusPhases(t,e){if(!e){this._statusPhases=[],this._statusPhasesName=null;return}if(this._statusPhasesName!==e){this._statusPhasesName=e;try{const s=await this._ws({type:`${w}/get_profile_phases`,entry_id:t,profile_name:e});this._statusPhases=(s.phases||[]).map(a=>({name:a.name,start:a.start,end:a.end}))}catch{this._statusPhases=[]}}}async _fetchSettingsChangelog(t){try{const s=await this._ws({type:`${w}/get_settings_changelog`,entry_id:t});this._settingsChangelog=s.changelog||[]}catch{this._settingsChangelog=this._settingsChangelog||[]}const e={};for(const s of this._settingsChangelog||[])s&&s.key!=null&&!(s.key in e)&&(e[s.key]=s);this._settingsChangeByKey=e}_registerUndo(t){const e="u"+ ++this._undoSeq;return t.timer=setTimeout(()=>this._commitDelete(e),1e4),this._undoBuffer.set(e,t),e}_undoDelete(t){const e=this._undoBuffer.get(t);if(e){this._undoBuffer.delete(t),e.timer&&clearTimeout(e.timer);try{e.restore()}catch{}this._toastTimer&&clearTimeout(this._toastTimer),this._toast=null,this._render()}}async _commitDelete(t){const e=this._undoBuffer.get(t);if(!e)return;this._undoBuffer.delete(t),e.timer&&clearTimeout(e.timer);const s=(a,r)=>{const i=this._devices[this._selIdx],o=i&&i.entry_id;if(e.eid===o){try{e.restore(a)}catch{}this._showToast(r,"error"),this._render()}};try{const a=await e.commit();a&&a.length&&s(a,this._t("toast.delete_partial_failed",{},"Some items could not be deleted and were restored"))}catch(a){s(null,this._tText("toast.delete_failed",{error:a&&a.message||a},"Delete failed: "+(a&&a.message||a)))}}_isActiveEntry(t){const e=this._devices[this._selIdx];return!!e&&e.entry_id===t}_flushPendingDeletes(){return!this._undoBuffer||!this._undoBuffer.size?Promise.resolve():Promise.all(Array.from(this._undoBuffer.keys()).map(t=>this._commitDelete(t)))}_deleteCyclesWithUndo(t,e){const s=new Set(e),a=[];if(this._cycles=(this._cycles||[]).filter((n,l)=>s.has(n.id)?(a.push({idx:l,rec:n,ref:!1}),!1):!0),this._refCycles=(this._refCycles||[]).filter((n,l)=>s.has(n.id)?(a.push({idx:l,rec:n,ref:!0}),!1):!0),!a.length)return;this._cycleSel.clear(),this._selectMode=!1,this._render();const r=n=>{const l=n&&n.length?n:a,c=this._cycles.slice(),p=this._refCycles.slice();l.slice().sort((_,f)=>_.idx-f.idx).forEach(({idx:_,rec:f,ref:u})=>{const b=u?p:c;b.splice(Math.min(_,b.length),0,f)}),this._cycles=c,this._refCycles=p},i=async()=>{const n=[];for(const l of a)try{await this._ws({type:`${w}/delete_cycle`,entry_id:t,cycle_id:l.rec.id}),this._isActiveEntry(t)&&(l.ref?this._importedOffset=Math.max(0,this._importedOffset-1):this._cycleOffset=Math.max(0,this._cycleOffset-1))}catch{n.push(l)}if(n.lengthn.name===e),a=s>=0?this._profiles[s]:{name:e};s>=0&&(this._profiles=this._profiles.filter(n=>n.name!==e)),this._modal=null,this._render();const r=n=>{const l=this._profiles.slice();l.splice(Math.min(s<0?l.length:s,l.length),0,a),this._profiles=l},i=async()=>{try{await this._ws({type:`${w}/delete_profile`,entry_id:t,profile_name:e,unlabel_cycles:!0})}catch{return[{idx:s,rec:a}]}try{await this._fetchProfiles(t),this._tab==="profiles"&&this._render()}catch{}return[]},o=this._registerUndo({eid:t,restore:r,commit:i});this._showToast(this._tText("msg.profile_deleted",{name:e},"Profile deleted"),"success",{actionLabel:this._t("btn.undo",{},"Undo"),actionToken:o,duration:1e4})}_onKeydown(t){if(!t.defaultPrevented){if(t.key==="Escape"){this._modal&&(t.preventDefault(),this._onModalAction("cancel",null));return}if(this._modal&&t.key==="Tab"){const e=this.shadowRoot,s=e&&e.querySelector('.wd-modal[role="dialog"]');if(s){const a=Vt(s);if(a.length){const r=e.activeElement,i=a.indexOf(r);t.shiftKey?i<=0&&(t.preventDefault(),a[a.length-1].focus()):(i===-1||i===a.length-1)&&(t.preventDefault(),a[0].focus())}}return}if(["ArrowLeft","ArrowRight","Home","End"].includes(t.key)){const e=this.shadowRoot,s=e&&e.activeElement;if(s&&s.classList.contains("wd-tab")){const a=[...e.querySelectorAll("button.wd-tab")],r=a.indexOf(s);if(r===-1)return;let i=r;t.key==="ArrowRight"?i=(r+1)%a.length:t.key==="ArrowLeft"?i=(r-1+a.length)%a.length:t.key==="Home"?i=0:t.key==="End"&&(i=a.length-1),t.preventDefault(),a[i].click(),a[i].focus()}}}}async _loadMlIndex(t){if(this._mlById=this._mlById||{},!!this._constants.mlLabEnabled)try{const e=await this._ws({type:`${w}/get_ml_comparison`,entry_id:t});if(!this._isActiveEntry(t))return;this._mlComparison=e;const s={};for(const a of e&&e.cycles||[])s[a.id]=a;this._mlById=s}catch{}}async _loadMlTrainingStatus(t){if(this._constants.mlTrainingAvailable)try{const e=await this._ws({type:`${w}/get_ml_training_status`,entry_id:t});if(!this._isActiveEntry(t))return;this._mlTrainingStatus=e}catch{}}_cycleOverlayPlan(t,e){const s=e&&e.profile_name;if(s)return{name:s,suspected:!1,runnerUp:null};const a=(this._feedbacks||[]).find(o=>o.cycle_id===t),r=a&&typeof a.detected_profile=="string"?a.detected_profile:"";if(!r)return null;const i=(Array.isArray(a.ranking)?a.ranking:[]).find(o=>o&&typeof o.name=="string"&&o.name&&o.name!==r);return{name:r,suspected:!0,runnerUp:i?i.name:null}}_loadCycleOverlays(t,e,s){const a=this._modal;!a||a.type!=="cycle-detail"||a.cycleId!==e||(a.overlayPlan=this._cycleOverlayPlan(e,s),a.overlayPlan&&(this._fetchCycleProfileEnv(t,a.overlayPlan.name),a.overlayPlan.runnerUp&&this._fetchCycleProfileEnv(t,a.overlayPlan.runnerUp,"runnerUpEnv")))}async _fetchCycleProfileEnv(t,e,s="profileEnv"){if(!e)return;const a=this._modal;a&&a.type==="cycle-detail"&&((a.envFor=a.envFor||{})[s]=e);try{const r=await this._ws({type:`${w}/get_profile_envelope`,entry_id:t,profile_name:e}),i=this._modal;i&&i.type==="cycle-detail"&&i.entryId===t&&i.envFor&&i.envFor[s]===e&&(i[s]=r.envelope||null,this._render())}catch{}}async _fetchSuggestions(t){this._suggestionsError=!1;try{const e=await this._ws({type:`${w}/get_suggestions`,entry_id:t});if(!this._isActiveEntry(t))return;this._suggestions=e.suggestions||[],this._lockedSuggestions=e.locked_suggestions||[]}catch{this._isActiveEntry(t)&&(this._suggestionsError=!0,this._suggestions=[])}}async _fetchProfiles(t){this._profilesError=!1;try{const e=await this._ws({type:`${w}/get_profiles`,entry_id:t});if(!this._isActiveEntry(t))return this._profiles;this._profiles=e.profiles||[],this._profileHealth=e.profile_health||{},this._profileTrends=e.profile_trends||{},this._coverageGaps=e.coverage_gaps||{},this._profileAdvisories=e.profile_advisories||[],this._profileTerminal=e.profile_terminal||{},this._profileMatcherCounts=e.profile_matcher_counts||{}}catch{this._profilesError=!0}return this._profiles}async _ensureProfileEnvs(t,e){this._profileEnvCache=this._profileEnvCache||{};const s=[...new Set(e)].filter(a=>a&&!(a in this._profileEnvCache));return s.length?(await Promise.all(s.map(async a=>{try{const r=await this._ws({type:`${w}/get_profile_envelope`,entry_id:t,profile_name:a});this._profileEnvCache[a]=r&&r.envelope||null}catch{this._profileEnvCache[a]=null}})),this._profileEnvCache):this._profileEnvCache}async _fetchProfileGroups(t){this._profileGroupsError=!1;try{const e=await this._ws({type:`${w}/get_profile_groups`,entry_id:t});this._profileGroups={groups:e.groups||[],min_cohesion:e.min_cohesion||.85}}catch{this._profileGroupsError=!0,this._profileGroups={groups:[],min_cohesion:.85}}return this._profileGroups}_deepLinkToken(){try{const t=new URLSearchParams(window.location.search||"").get("device");return t?t.trim():""}catch{return""}}_deepLinkIdx(t){const e=this._devices||[],s=String(t||"").trim();if(!s||!e.length)return-1;const a=s.toLowerCase();let r=e.findIndex(i=>String(i.entry_id||"").toLowerCase()===a);if(r<0&&(r=e.findIndex(i=>String(i.title||"").trim().toLowerCase()===a)),r<0){const i=fe(s);i&&(r=e.findIndex(o=>fe(o.title)===i))}return r}_rememberDevice(t){const e=(this._devices||[])[t];if(e)try{localStorage.setItem("wd-last-device",e.entry_id)}catch{}}_unsavedSettingsCount(){return this._tab!=="settings"||!this.shadowRoot?0:(this._snapshotFormToPending(this.shadowRoot),Object.keys(this._changedOptions(this._pendingSettings||{})).length)}_guardUnsaved(t){const e=this._unsavedSettingsCount();if(!e){t();return}this._modal={type:"confirm",title:this._t("modal.discard_settings_title",{},"Discard unsaved changes?"),message:this._tText("modal.discard_settings_msg",{n:e},`${e} setting change(s) are not saved yet. Leave Settings and discard them?`),okLabel:this._t("btn.discard",{},"Discard"),onOk:()=>{this._pendingSettings={},this._dirtyOptKeys=new Set,t()}},this._render()}_maybeGuardOutboundClick(t){if(t.defaultPrevented||t.button!==0||t.metaKey||t.ctrlKey||t.shiftKey||t.altKey)return;const s=(typeof t.composedPath=="function"?t.composedPath():[]).find(i=>i&&i.tagName==="A"&&i.getAttribute&&i.getAttribute("href"));if(!s||s.target&&s.target!=="_self"||s.hasAttribute("download"))return;let a;try{a=new URL(s.getAttribute("href"),window.location.href)}catch{return}if(a.origin!==window.location.origin)return;const r=this._panel&&this._panel.url_path;r&&a.pathname.split("/").filter(Boolean)[0]===r||this._unsavedSettingsCount()&&(t.preventDefault(),t.stopImmediatePropagation(),this._guardUnsaved(()=>this._navigate(a.pathname+a.search+a.hash)))}_onOwnPath(){const t=this._panel&&this._panel.url_path;return t?(window.location.pathname||"").split("/").filter(Boolean)[0]===t:!0}_onLocationChanged(){if(!this._initialized||!this._devices.length||!this._onOwnPath())return;const t=this._deepLinkToken();if(!t)return;this._deepLinkApplied=t;const e=this._deepLinkIdx(t);if(e<0){console.warn(`[WashData panel] ?device=${t} matches no WashData device`);return}e!==this._selIdx&&this._guardUnsaved(()=>this._selectDevice(e))}async _selectDevice(t){if(t===this._selIdx)return;await this._flushPendingDeletes(),this._selIdx=t,this._rememberDevice(t),this._pendingSettings={},this._dirtyOptKeys=new Set,this._prevOpts=null,this._cascadePending={},this._preCascadeOpts=null,this._stagedSuggestions=!1,this._mlComparison=null,this._mlById={},this._profileEnvCache={},this._powerHistory=[],this._powerT0=null,this._statusEnv=null,this._statusEnvName=null,this._statusPhases=[],this._statusPhasesName=null,this._cycleOffset=0,this._cyclesTotal=0,this._cyclesHasMore=!1,this._importedOffset=0,this._importedHasMore=!1,this._settingsChangelog=null,this._settingsChangeByKey={},this._powerData={live:[],raw:[],cycle_active:!1,cycle_elapsed_s:0},this._matchDebug=null,this._profiles=[],this._profileHealth={},this._profileTrends={},this._coverageGaps={},this._profileAdvisories=[],this._profileTerminal={},this._profileMatcherCounts={},this._opts={},this._optDefaults={},this._suggestions=[],this._lockedSuggestions=[],this._cycles=[],this._refCycles=[],this._recState=null,this._diag=null,this._importUndo=null,this._maintenance=null,this._phases=[],this._mlTrainingStatus=null,this._setupStatus=null,this._deviceAutomations=[],this._selectMode=!1,this._cycleSel=new Set,this._cycleFilter={text:"",status:""},this._profSubtab="profiles",this._pgCycleId="",this._pgProfileName="",this._pgPowerPts=null,this._pgEnvData=null,this._pgThreshStart=null,this._pgThreshStop=null,this._pgParamOverrides={},this._pgEffective=null,this._pgPublishable=null,this._pgPresets=[],this._pgPresetSel="",this._pgPresetName="",this._pgPresetLimit=0,this._pgSuggClassic={},this._pgView=null,this._pgHoverT=null,this._pgLoadSeq++,this._pgNeedsRestart=!1,this._pgLoading=!1,this._pgDetail=null,this._pgHistory=null,this._pgSweepNew=null,this._pgHistoryTaskId=null,this._pgSweepTaskId=null,this._busy.delete("pg-history"),this._busy.delete("pg-sweep"),this._pgBatchProgress=null,this._pgDetailDebounceTimer&&(clearTimeout(this._pgDetailDebounceTimer),this._pgDetailDebounceTimer=null),this._storeView="brands",this._storeDevice=null,this._storeProfile=null,this._storeQuery="",this._storeDevices=[],this._storeProfiles=[],this._storeCycles=[],this._storeStatus=null,this._storeConnected=!1,this._storeLoading=!1,clearTimeout(this._brandSearchTimer),this._brandSearchTimer=null,this._catalog={brands:this._catalog.brands,devices:void 0,forBrand:null,approvedOnly:this._catalog.approvedOnly,brandsFull:this._catalog.brandsFull,brandPrefixes:this._catalog.brandPrefixes},this._entityListCache&&delete this._entityListCache.store_model;const e=this._devices[this._selIdx];e&&await this._fetchSuggestions(e.entry_id),this._fetchTabData()}_htmlStaleChip(){const t=this._lastRefresh,e=t&&Date.now()-t.getTime()>2.5*this._pollMs;if(!this._devices.length||!(this._fetchFailed||e))return'';const s=t?re(t):"";return`
    ${d(this._tText("msg.connection_lost",{time:s},`Connection lost - showing data from ${s}.`))}
    `}_refreshStaleChip(){const t=this.shadowRoot&&this.shadowRoot.getElementById("wd-stale");t&&(t.outerHTML=this._htmlStaleChip())}_refreshDeviceBar(){const t=this.shadowRoot;if(!t)return;const e=t.querySelector(".wd-devbar"),s=this._htmlDeviceBar();if(e&&s){const a=document.createElement("div");a.innerHTML=s;const r=a.firstElementChild;r&&(e.replaceWith(r),r.querySelectorAll(".wd-devcard[data-idx]").forEach(i=>i.addEventListener("click",()=>this._guardUnsaved(()=>this._selectDevice(parseInt(i.dataset.idx,10))))))}}_refreshLogDrawer(){this._logOpen&&(this._refreshLogViews(),this._refreshLogFilterOptions())}_refreshLogFilterOptions(){const t=this.shadowRoot;if(!t)return;const e=(r,i,o)=>``+i.map(n=>``).join(""),s=e(this._t("log.all_devices",{},"All devices"),this._logDevices(),this._logDevice),a=e(this._t("log.all_components",{},"All components"),this._logComponents(),this._logComponent);t.querySelectorAll('.wd-log-filter[data-logfilter="device"]').forEach(r=>{r.innerHTML=s,r.value=this._logDevice||""}),t.querySelectorAll('.wd-log-filter[data-logfilter="component"]').forEach(r=>{r.innerHTML=a,r.value=this._logComponent||""})}async _fetchTabData(){const t=this._devices[this._selIdx];if(!t)return;const e=t.entry_id;this._tabLoading=!0,this._render();try{if(this._tab==="status"){if(this._powerData=await this._ws({type:`${w}/get_power_history`,entry_id:e,with_raw:this._pref("show_raw_active",!1)}),this._profiles.length||await this._fetchProfiles(e),!this._profiles.length&&!this._cycles.length&&await this._fetchCycles(e),await this._ensureStatusPhases(e,t.current_program),this._pref("show_debug",!1))try{this._matchDebug=await this._ws({type:`${w}/get_match_debug`,entry_id:e})}catch{}if(this._canEdit())try{this._recState=await this._ws({type:`${w}/get_recording_state`,entry_id:e})}catch{}try{this._setupStatus=await this._ws({type:`${w}/get_setup_status`,entry_id:e})}catch{this._setupStatus=null}}else if(this._tab==="history"&&this._historySub==="maintenance")this._maintenance||await this._fetchMaintenance(e);else if(this._tab==="history"){await this._fetchCycles(e),this._profiles.length||await this._fetchProfiles(e);try{const s=await this._ws({type:`${w}/get_feedbacks`,entry_id:e});this._feedbacks=s.feedbacks||[]}catch{}this._constants.storeOnlineAvailable&&this._onlineEnabled()&&this._loadStoreStatus(e).then(()=>{this._tab==="history"&&this._render()}),this._constants.mlLabEnabled&&(this._mlLoading=!0,this._loadMlIndex(e).finally(()=>{this._mlLoading=!1;const s=this._modal;s&&s.type==="cycle-detail"&&!s.ml&&this._mlById[s.cycleId]&&(s.ml=this._mlById[s.cycleId]),(this._tab==="history"||s&&s.type==="cycle-detail")&&this._render()}))}else if(this._tab==="profiles"){if(await this._fetchProfiles(e),this._fetchProfileGroups(e).then(()=>{this._tab==="profiles"&&this._render()}),this._cycles.length||this._fetchCycles(e).then(()=>{this._tab==="profiles"&&this._render()}),this._profSubtab==="phase-catalog")try{const s=await this._ws({type:`${w}/get_phase_catalog`,entry_id:e});this._phases=s.phases||[]}catch{}}else if(this._tab==="settings"){const s=await this._ws({type:`${w}/get_options`,entry_id:e});if(!this._isActiveEntry(e))return;this._opts=s.options||{},this._optDefaults=s.defaults||{},await this._fetchSuggestions(e),await this._fetchSettingsChangelog(e),this._constants.mlTrainingAvailable&&this._loadMlTrainingStatus(e).finally(()=>{this._tab==="settings"&&this._renderPreservingFormEdits()}),this._autoLoading=!0,this._loadDeviceAutomations(e).finally(()=>{this._autoLoading=!1,this._tab==="settings"&&this._renderPreservingFormEdits()}),this._constants.storeOnlineAvailable&&this._loadStoreStatus(e).finally(()=>{this._tab==="settings"&&this._renderPreservingFormEdits()})}else if(this._tab==="store"){try{const s=await this._ws({type:`${w}/get_options`,entry_id:e});if(!this._isActiveEntry(e))return;this._opts=s.options||{}}catch{}if(this._profiles.length||this._fetchProfiles(e),await this._loadStoreStatus(e),!this._isActiveEntry(e))return;this._ensureStoreConnectListener(),this._onlineEnabled()&&this._storeSearch(this._storeBrandScope())}else if(this._tab==="advanced"&&this._panelSubtab==="ml"){const s=await this._ws({type:`${w}/get_options`,entry_id:e});if(!this._isActiveEntry(e))return;this._opts=s.options||{},this._loadMlTrainingStatus(e).finally(()=>{this._tab==="advanced"&&this._panelSubtab==="ml"&&this._renderPreservingFormEdits()})}else if(this._tab==="playground"){if(await Promise.all([(async()=>{try{const s=await this._ws({type:`${w}/get_options`,entry_id:e});this._isActiveEntry(e)&&(this._opts=s.options||{})}catch{}})(),this._pgFetchSettings(e,!1),this._fetchCycles(e),this._profiles.length?Promise.resolve():this._fetchProfiles(e)]),!this._isActiveEntry(e))return;this._pgFetchSuggestions(e),!this._pgCycleId&&this._cycles?.length&&(this._pgCycleId=this._cycles[0].id,this._pgProfileName=""),this._pgNeedsRestart=!1,this._pgRestartRetries=0,this._pgAdoptExisting()}else this._tab==="advanced"&&this._panelSubtab==="diagnostics"&&!this._diag&&this._fetchToolsData(e).then(()=>{this._tab==="advanced"&&this._render()});this._tabError=null}catch(s){console.warn("[WashData panel] tab data fetch error -",Ct(s),s),this._tabError=this._tab}finally{this._tabLoading=!1,this._render()}}async _fetchToolsData(t){try{const e=await this._ws({type:`${w}/get_diagnostics`,entry_id:t});if(!this._isActiveEntry(t))return;this._diag=e.stats||{},this._importUndo=e.import_undo||null}catch(e){console.warn("[WashData panel] tools fetch error -",Ct(e),e),this._diag={_error:String(e&&e.message||e)}}}async _fetchMaintenance(t){try{const e=await this._ws({type:`${w}/get_maintenance_log`,entry_id:t});this._maintenance=e||{}}catch(e){console.warn("[WashData panel] maintenance fetch error -",Ct(e),e),this._maintenance={_error:String(e&&e.message||e)}}}async _fetchLogs(){try{const t=await this._ws({type:`${w}/get_logs`,level:null,limit:500});this._logs=t.logs||[]}catch(t){console.warn("[WashData panel] logs fetch error -",Ct(t),t)}}_logComponents(){return[...new Set((this._logs||[]).map(t=>t.logger).filter(Boolean))].sort()}_logDevices(){return[...new Set((this._logs||[]).map(t=>t.device).filter(Boolean))].sort()}_filteredLogRecords(){const t={DEBUG:10,INFO:20,WARNING:30,ERROR:40,CRITICAL:50},e=this._logLevel&&t[this._logLevel]||0,s=this._logDevice,a=this._logComponent,r=(this._logSearch||"").trim().toLowerCase();return(this._logs||[]).filter(i=>!(e&&(t[i.level]||0){const s=re(new Date(e.ts*1e3)),a=e.device?`${d(e.device)}`:"";return`
    ${s}${d(e.level)}${d(e.logger||"")}${a}${d(e.msg)}
    `}).join(""):(this._logs||[]).length>0?`

    ${this._t("msg.no_logs_match",{},"No log records match these filters.")}

    `:`

    ${this._t("msg.no_logs",{},"No log records buffered yet.")}

    `}_htmlLogFilters(t){const s=["","DEBUG","INFO","WARNING","ERROR"].map(i=>``).join(""),a=``+this._logDevices().map(i=>``).join(""),r=``+this._logComponents().map(i=>``).join("");return` + + + `}_refreshLogViews(){const t=this.shadowRoot;if(!t)return;const e=this._logLinesHtml();["wd-log-lines-drawer","wd-log-lines-page"].forEach(s=>{const a=t.getElementById(s);a&&(a.innerHTML=e)})}_syncLogFilters(t){const e=this.shadowRoot;e&&e.querySelectorAll(`.wd-log-filter[data-logfilter="${t.dataset.logfilter}"]`).forEach(s=>{s!==t&&s.value!==t.value&&(s.value=t.value)})}async _fetchRecState(t){try{this._recState=await this._ws({type:`${w}/get_recording_state`,entry_id:t})}catch{}}async _fetchFeedbacks(t){try{const e=await this._ws({type:`${w}/get_feedbacks`,entry_id:t});this._feedbacks=e.feedbacks||[]}catch{}}async _fetchPhases(t){try{const e=await this._ws({type:`${w}/get_phase_catalog`,entry_id:t});this._phases=e.phases||[]}catch{}}_localize(t,e){try{const s=this._hass&&this._hass.localize?this._hass.localize(t):"";return s&&s!==t?s:e}catch{return e}}_tLookup(t,e){const s=this._panelTrans&&(this._panelTrans[e]||this._panelTrans.en);if(!s)return null;const a=t.split(".").reduce((r,i)=>r&&r[i]!==void 0?r[i]:null,s);return a&&typeof a=="string"?a:null}_uiLang(){return this._panelCfg&&this._panelCfg.prefs&&this._panelCfg.prefs.lang_override||this._hass&&this._hass.locale&&this._hass.locale.language||""}_tResolve(t,e,s){if(s!=null){const a=He(e,s),r=this._tLookup(`${t}_${a}`,e)||(a!=="other"?this._tLookup(`${t}_other`,e):null);if(r)return r}return this._tLookup(t,e)}_t(t,e={},s=""){return this._tr(t,e,s,!0)}_tText(t,e={},s=""){return this._tr(t,e,s,!1)}_tr(t,e,s,a){let r;e=e||{};const i=this._uiLang(),o=typeof e.count=="number"?e.count:e.n,n=typeof o=="number"&&isFinite(o)?o:null;if(this._panelTrans?r=i&&this._tResolve(t,i,n)||this._tResolve(t,"en",n)||s:r=this._localize(`component.${w}.panel.${t}`,s),typeof r!="string")return r;for(const[l,c]of Object.entries(e)){const p=`{${l}}`;if(!r.includes(p))continue;const _=c instanceof ce?c.html:a?d(String(c)):String(c);r=r.split(p).join(_)}return r}_stateColor(t){const e=this._constants.stateColors||{};return e[t]||e.unknown||"var(--disabled-color, #bdbdbd)"}_cycleStatusLabel(t){return{completed:this._tText("status.completed",{},"Completed"),interrupted:this._tText("status.interrupted",{},"Interrupted"),force_stopped:this._tText("status.force_stopped",{},"Force stopped"),active:this._tText("status.active",{},"Active")}[t]||t}_stateLabel(t){const e=(t||"unknown").replace(/_/g," ").replace(/\b\w/g,s=>s.toUpperCase());return this._localize(`component.${w}.entity.sensor.washer_state.state.${t}`,e)}_deviceTypeLabel(t){const e=(this._constants.deviceTypes||[]).find(a=>a.id===t),s=e?e.label:(t||"").replace(/_/g," ");return this._localize(`component.${w}.selector.device_type.options.${t}`,s)}_deviceTypeOpts(t){const e=(this._constants.deviceTypes||[]).map(s=>[s.id,this._deviceTypeLabel(s.id)]);return t&&!e.some(([s])=>String(s)===String(t))&&e.push([t,this._deviceTypeLabel(t)]),e}_deviceOpts(t){const e=[["","- None -"]],s=this._hass&&this._hass.devices?this._hass.devices:{},a=this._ownDeviceId();return Object.values(s).forEach(r=>{if(a&&String(r.id)===a)return;const i=r.name_by_user||r.name||r.id;e.push([r.id,i])}),t&&String(t)!==a&&!e.some(([r])=>String(r)===String(t))&&e.push([t,this._tText("lbl.device_unresolved",{id:t},`Unavailable device (${t})`)]),e}_ownDeviceId(){const t=this._devices&&this._devices[this._selIdx],e=t&&t.entry_id;if(!e)return"";const s=this._hass&&this._hass.devices?this._hass.devices:{};for(const a of Object.values(s)){if(!a||!a.id)continue;if((Array.isArray(a.identifiers)?a.identifiers:[]).some(i=>Array.isArray(i)&&i[0]===w&&String(i[1])===String(e))||Array.isArray(a.config_entries)&&a.config_entries.map(String).includes(String(e)))return String(a.id)}return""}_applyFontScale(t){let e=parseFloat(t);isFinite(e)||(e=1),e=Math.max(.7,Math.min(2,e)),this.style.fontSize=e===1?"":Math.round(e*1e3)/10+"%"}_applyPanelConfig(){const t=this._panelCfg;if(!t)return;this._applyFontScale(t.prefs&&t.prefs.font_scale||1);const e=t.prefs&&t.prefs.lang_override;e&&!(this._panelTrans&&this._panelTrans[e])&&this._loadPanelLang(e).then(()=>this._render()).catch(()=>{});const s=t.panel||{};if(!this._tabInitialized){const a=t.prefs&&t.prefs.default_tab||s.default_tab;a&&["status","history","profiles","settings","playground"].includes(a)&&(this._tab=a),this._tabInitialized=!0}}_isAdmin(){return!!(this._panelCfg&&this._panelCfg.is_admin)}_curPerm(){const t=this._devices[this._selIdx];return t&&t.perm||"full"}_canEdit(){const t=this._curPerm();return this._isAdmin()||t==="edit"||t==="full"}_canFull(){const t=this._curPerm();return this._isAdmin()||t==="full"}_onlineEnabled(){return!!(this._constants&&this._constants.storeOnlineAvailable&&this._constants.storeOnlineEnabled)}_visibleTabIds(){const t=this._isAdmin(),e=!t&&this._panelCfg&&this._panelCfg.panel&&this._panelCfg.panel.hidden_tabs||[],s=["status","history","profiles"];return this._canEdit()&&s.push("settings"),this._canEdit()&&s.push("playground"),this._canEdit()&&this._onlineEnabled()&&s.push("store"),this._canEdit()&&s.push("advanced"),s.filter(a=>t||!e.includes(a))}async _busyRun(t,e){this._busy.add(t),this._render();try{return await e()}finally{this._busy.delete(t),this._render()}}async _closeCycleDetail(t){const e=this._prevModal;if(this._prevModal=null,e&&e.type==="profile-panel"){if(this._modal=e,this._render(),e.tab==="cleanup")try{const s=await this._ws({type:`${w}/get_profile_cycles`,entry_id:t,profile_name:e.name});this._modal&&this._modal.type==="profile-panel"&&this._modal.name===e.name&&(this._modal.cleanup={cycles:s.cycles||[],selected:new Set},this._render(),this._drawSpaghetti())}catch{}}else this._modal=null,this._render()}_SCROLLERS=[".wd-main",".wd-modal",".wd-logs",".wd-log-drawer-body",".wd-table-wrap",".wd-sd-tree"];_SCROLLERS_CHROME=[".wd-tabs"];_MODAL_KEY_PREFIX="modal:";_eachScroller(t,e){const s=this.shadowRoot;if(s)for(const a of t){const r=Array.from(s.querySelectorAll(a)),i=o=>a!==".wd-modal"&&!!o.closest(".wd-modal");for(const o of[!1,!0]){const n=r.filter(c=>i(c)===o),l=o?this._MODAL_KEY_PREFIX:"";n.forEach((c,p)=>e(c,`${l}${a}|${p}/${n.length}`))}}}_captureScroll(){if(!this.shadowRoot)return null;const t={};return this._eachScroller([...this._SCROLLERS,...this._SCROLLERS_CHROME],(e,s)=>{(e.scrollTop||e.scrollLeft)&&(t[s]={top:e.scrollTop,left:e.scrollLeft})}),t}_scrollChromeOnly(t){if(!t)return t;const e={};for(const[s,a]of Object.entries(t))this._SCROLLERS_CHROME.some(r=>s.startsWith(`${r}|`))&&(e[s]=a);return e}_restoreScroll(t){!t||!this.shadowRoot||this._eachScroller([...this._SCROLLERS,...this._SCROLLERS_CHROME],(e,s)=>{const a=t[s];if(!a)return;const r=Math.max(0,e.scrollHeight-e.clientHeight),i=Math.max(0,e.scrollWidth-e.clientWidth);e.scrollTop=Math.min(a.top,r),e.scrollLeft=Math.min(a.left,i)})}_navKey(){return[this._tab,this._selIdx,this._panelSubtab,this._profSubtab,this._toolsSubtab,this._settingsSec,this._storeView].join("|")}_modalNavKey(){const t=this._modal;if(!t)return"";const e=t.type==="profile-group"?t.orig||"":t.name||t.cycleId||"";return[t.type,e,t.tab||t.mode||t.step||""].join("|")}_render(){if(!this._container)return;se=this._pref("date_format","relative"),Re(this._uiLang(),this._hass&&this._hass.locale);const t=this.shadowRoot,e=t&&(t.activeElement||this.getRootNode()&&this.getRootNode().activeElement)||null,s=!this._modalFocusActive&&t&&t.activeElement?qe(t.activeElement):null;this._hideGraphTip();const a=this._navKey(),r=this._scrollNavKey!=null&&this._scrollNavKey!==a;this._scrollNavKey=a;let i=this._scrollCarry||this._captureScroll();r&&(i=this._scrollChromeOnly(i));const o=this._modalNavKey();if(this._scrollModalKey!==void 0&&o!==this._scrollModalKey&&i&&(i=Object.fromEntries(Object.entries(i).filter(([n])=>!n.startsWith(".wd-modal|")&&!n.startsWith(this._MODAL_KEY_PREFIX)))),this._scrollModalKey=o,this._container.innerHTML=this._buildHtml(),this._wire(),this._tabLoading?(this._scrollCarry=i,this._restoreScroll(this._scrollChromeOnly(i))):(this._scrollCarry=null,this._restoreScroll(i)),this._drawStatusCurve(),this._drawModalCanvas(),this._drawProfileSparklines(),this._drawHistorySparklines(),this._drawPlaygroundCanvases(),["wd-status-canvas","wd-cyc-canvas","wd-compare-canvas","wd-env-canvas","wd-phase-canvas","wd-spag-canvas","wd-pgroup-canvas"].forEach(n=>this._attachGraphGestures(n)),this._syncScrollAffordances(),this._syncModalFocus(e),s&&!this._modalFocusActive){const n=this.shadowRoot.querySelector(s);if(n&&this.shadowRoot.activeElement!==n)try{n.focus({preventScroll:!0})}catch{}}requestAnimationFrame(()=>this._resizeLogsPage())}_syncScrollAffordances(){const t=this.shadowRoot;if(!t)return;const e=t.querySelectorAll(Xe);this._ovfRO?this._ovfRO.disconnect():typeof ResizeObserver=="function"&&(this._ovfRO=new ResizeObserver(s=>s.forEach(a=>Yt(a.target)))),e.forEach(s=>{s.addEventListener("scroll",()=>Yt(s),{passive:!0}),Yt(s),this._ovfRO&&this._ovfRO.observe(s)})}_resizeLogsPage(){const t=this.shadowRoot&&this.shadowRoot.getElementById("wd-log-lines-page");if(!t)return;const e=t.getBoundingClientRect();e.top<=0||(t.style.height=window.innerHeight-e.top-24+"px")}_syncModalFocus(t=null){const e=this.shadowRoot;if(!e)return;const s=e.querySelector('.wd-modal[role="dialog"]');if(s){const a=s.querySelector("h2");if(a&&!a.id&&(a.id="wd-modal-title"),this._modalFocusActive){const r=e.activeElement||this.getRootNode()&&this.getRootNode().activeElement||null;if(!r||!s.contains(r)){const i=Vt(s);try{(i[0]||s).focus({preventScroll:!0})}catch{}}}else{this._modalFocusActive=!0;const r=t||null;r&&!s.contains(r)&&(this._modalReturnFocus=r);const i=Vt(s);try{(i[0]||s).focus()}catch{}}}else if(this._modalFocusActive){this._modalFocusActive=!1;const a=this._modalReturnFocus;if(this._modalReturnFocus=null,a&&a.isConnected&&typeof a.focus=="function")try{a.focus()}catch{}}}_renderPreservingFormEdits(){this._snapshotFormToPending(this.shadowRoot),this._render()}_snapshotCycleReviewForm(t){const e=this._modal;if(!t||!e||e.type!=="cycle-detail"||e.mode!=="review")return;const s=t.getElementById("wd-cyc-rev-quality"),a=t.getElementById("wd-cyc-rev-golden"),r=t.getElementById("wd-cyc-rev-notes"),i=t.getElementById("wd-cyc-rev-label");if(!s&&!a&&!r&&!i)return;e.ml||(e.ml={}),e.ml.ml_review||(e.ml.ml_review={});const o=e.ml.ml_review;s&&(o.quality=s.value||""),a&&(o.golden=!!a.checked),r&&(o.notes=r.value||""),o.tags=Array.from(t.querySelectorAll(".wd-cyc-rev-tag")).filter(n=>n.checked).map(n=>n.value),i&&(e.reviewLabel=i.value||"")}_buildHtml(){const t=this._toast?`
    ${d(this._toast.msg)}${this._toast.actionLabel?``:""}
    `:"";return`
    ${this._htmlHeader()}
    @@ -911,89 +915,89 @@ button.wd-profile-card { display: block; }
    ${this._modal?this._htmlModal():""} ${t} - `}_htmlHeader(){const e=Array.from(this._busy).some(i=>!(i==="pg-sweep"||i==="pg-history"||i==="reprocess"||i.startsWith("ml-train-now")))?`${this._t("status.working",{},"Working\u2026")}`:"",s=this._constants.iconUrl,r=s?``:`${this._t("status.working",{},"Working\u2026")}`:"",s=this._constants.iconUrl,a=s?``:``,n=this._constants.version;return` + `,r=this._constants.version;return`
    ${``} - ${r} -

    WashData

    ${n?`v${d(n)} · `:""}${this._t("msg.appliance_monitor",{},"Appliance monitor")}
    + ${a} +

    WashData

    ${r?`v${d(r)} · `:""}${this._t("msg.appliance_monitor",{},"Appliance monitor")}
    ${e} ${this._htmlTaskPills()} - + ${this._isAdmin()?``:""}
    - `}_htmlBody(){if(!this._devices.length)return`
    \u{1F9FA}
    ${this._t("msg.no_devices",{},"No WashData devices configured yet.")}
    `;const t=this._mlSugKeys().size,e=this._suggestions.length||t?" \u{1F4A1}":"",s=this._conflictKeysFromOpts().size>0?" \u26A0":"",n=this._busy.has("pg-sim")||this._busy.has("pg-sweep")?'':"",a={status:this._t("tab.status",{},"Overview"),history:this._t("tab.history",{},"Cycles"),profiles:this._t("tab.profiles",{},"Profiles"),settings:this._t("tab.settings",{},"Settings")+s+e,playground:this._t("tab.playground",{},"Playground")+n,store:this._t("tab.store",{},"Store"),advanced:this._t("tab.advanced",{},"Advanced")},o=this._visibleTabIds();o.includes(this._tab)||(this._tab="status");const i=o.map(c=>``).join(""),l=(c,g)=>o.includes(c)?`
    ${g}
    `:"";return` + `}_htmlBody(){if(!this._devices.length&&this._fetchFailed)return``;if(!this._devices.length)return`
    \u{1F9FA}
    ${this._t("msg.no_devices",{},"No WashData devices configured yet.")}
    `;const t=this._conflictKeysFromOpts().size>0?" \u26A0":"",s=this._busy.has("pg-sim")||this._busy.has("pg-sweep")?'':"",a={status:this._t("tab.status",{},"Overview"),history:this._t("tab.history",{},"Cycles"),profiles:this._t("tab.profiles",{},"Profiles"),settings:this._t("tab.settings",{},"Settings")+t,playground:this._t("tab.playground",{},"Playground")+s,store:this._t("tab.store",{},"Store"),advanced:this._t("tab.advanced",{},"Advanced")},r=this._visibleTabIds();r.includes(this._tab)||(this._tab="status");const i=r.map(n=>``).join(""),o=(n,l)=>r.includes(n)?`
    ${this._tab===n?l():""}
    `:"";return`
    ${this._htmlDeviceBar()}
    ${i}
    + ${this._htmlStaleChip()}
    ${this._tabLoading?`
    \u23F3
    ${this._t("msg.loading",{},"Loading\u2026")}
    `:""} - ${l("status",this._htmlStatus())} - ${l("history",this._htmlHistory())} - ${l("profiles",this._htmlProfiles())} - ${l("settings",this._htmlSettings())} - ${l("playground",this._htmlPlayground())} - ${l("store",this._htmlStore())} - ${l("advanced",this._htmlPanel())} - `}_htmlDeviceBar(){const t=this._isAdmin()?``:"";return this._devices.length<=1?t?`
    ${t}
    `:"":`
    ${this._devices.map((e,s)=>{const r=e.is_user_paused?"user_paused":e.detector_state||"unknown",n=zt.includes(r),a=!!e.recording,o=a?"var(--error-color, #f44336)":this._stateColor(r),i=a?this._t("status.recording",{},"Recording"):this._stateLabel(r),l=[],c=this._conflictCountForOpts(e.options||{},e.option_defaults||{});c&&l.push(`\u26A0 ${c}`);const g=this._sugCountsForDevice(e).total;return g&&l.push(`\u{1F4A1} ${g}`),e.feedback_count&&l.push(`\u{1F4AC} ${e.feedback_count}`),``:"";return this._devices.length<=1?t?`
    ${t}
    `:"":`
    ${this._devices.map((e,s)=>{const a=e.is_user_paused?"user_paused":e.detector_state||"unknown",r=Mt.includes(a),i=!!e.recording,o=i?"var(--error-color, #f44336)":this._stateColor(a),n=i?this._t("status.recording",{},"Recording"):this._stateLabel(a),l=[],c=this._conflictCountForOpts(e.options||{},e.option_defaults||{});return c&&l.push(`\u26A0 ${c}`),e.feedback_count&&l.push(`\u{1F4AC} ${e.feedback_count}`),``}).join("")}${t}
    `}_htmlStatus(){const t=this._devices[this._selIdx];if(!t)return`
    ${this._t("msg.no_device_selected",{},"No device selected.")}
    `;const e=!!t.is_user_paused,s=e?"user_paused":t.detector_state||"unknown",r=!!t.recording,n=r?"var(--error-color, #f44336)":this._stateColor(s),a=r?this._t("status.recording",{},"Recording"):this._stateLabel(s),o=r||zt.includes(s),i=t.cycle_progress_pct,l=t.time_remaining_s,c=t.current_program,g=!!t.manual_program,p=c?null:t.armed_program||null,f=c||p||"auto_detect",u=(this._profiles||[]).map(E=>E.name);c&&!u.includes(c)&&u.unshift(c),p&&!u.includes(p)&&u.unshift(p);const $=u.map(E=>``).join(""),S=c?g?this._t("badge.manual",{},"(manually selected)"):this._t("badge.auto",{},"(auto-detected)"):p?this._t("badge.armed",{},"(applies to the next cycle)"):"",w=S?`${S}`:"",v=`
    ${dt(this._t("lbl.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."))} + `}).join("")}${t}
    `}_htmlMatchUncertainty(t,e,s){const a=t&&t.match_uncertainty;if(!a||!e||s||!a.top)return"";const r=a.runner_up?this._t("status.match_uncertain",{top:a.top,runner:a.runner_up},"Uncertain: {top} or {runner}"):this._t("status.match_uncertain_single",{top:a.top},"Uncertain: maybe {top}"),i=Number(a.sure_pct),o=a.sure_pct!=null&&isFinite(i)?` \xB7 ${this._t("status.match_sure",{pct:i},"~{pct}% sure")}`:"",n=ot(this._tText("status.match_uncertain_tip",{top:a.top},"WashData has not settled on a program yet. The figure is how often the leading guess, {top}, was right in past cycles when it led by this much. Information only."));return`${r}${o}${n}`}_htmlStatus(){const t=this._devices[this._selIdx];if(!t)return`
    ${this._t("msg.no_device_selected",{},"No device selected.")}
    `;const e=!!t.is_user_paused,s=e?"user_paused":t.detector_state||"unknown",a=!!t.recording,r=a?"var(--error-color, #f44336)":this._stateColor(s),i=a?this._t("status.recording",{},"Recording"):this._stateLabel(s),o=a||Mt.includes(s),n=t.cycle_progress_pct,l=t.time_remaining_s,c=t.current_program,p=!!t.manual_program,_=c?null:t.armed_program||null,f=c||_||"auto_detect",u=(this._profiles||[]).map(P=>P.name);c&&!u.includes(c)&&u.unshift(c),_&&!u.includes(_)&&u.unshift(_);const b=u.map(P=>``).join(""),v=c?p?this._t("badge.manual",{},"(manually selected)"):this._t("badge.auto",{},"(auto-detected)"):_?this._t("badge.armed",{},"(applies to the next cycle)"):"",C=v?`${v}`:"",x=`
    ${ot(this._t("lbl.program_tip",{},"Choose which program the current cycle is matched to. Auto-detect picks the best match. Pin a program when auto-detect is wrong; pick one before starting and it applies to the next cycle."))} ${w}
    `,k=[];t.recording&&this._canEdit()&&k.push(`
    \u25CF
    ${this._t("msg.recording_in_progress",{},"Recording in progress")}
    ${this._t("msg.see_recorder",{},"See recorder widget below")}
    `),t.feedback_count&&this._canEdit()&&k.push(``);const D=this._conflictKeysFromOpts();if(D.size&&this._canEdit()){const E=D.size,O=E>1?"s":"";k.push(``)}const I=this._sugCountsForDevice(t);if(I.total&&this._canEdit()){const E=I.total,O=[];I.classic&&O.push(this._t("lbl.n_classic_suggestions",{n:I.classic},`${I.classic} classic`)),I.ml&&O.push(this._t("lbl.n_ml_suggestions",{n:I.ml},`${I.ml} ML`)),k.push(``)}const z=t.standby_above_stop;z&&this._canEdit()&&k.push(``);const R=k.length?`
    ${k.join("")}
    `:"",L=t.envelope_position,P=L!=null?Math.round(L*100):null,C=P!=null?` ${this._t("lbl.envelope_position",{pct:P},`curve ${P}%`)}`:"",A=o&&i!=null?` -
    -
    ${i.toFixed(1)}%${C}${l!=null?`${this._t("lbl.time_remaining",{v:gt(l)},`~${gt(l)} remaining`)}`:""}
    - `:"",j=((this._powerData||{}).live||[]).length>1,st=this._pref("show_expected",!0),J=this._pref("show_raw_active",!1),X=`
    + ${b} + ${C}${this._htmlMatchUncertainty(t,o,p)}
    `,$=[];t.recording&&this._canEdit()&&$.push(`
    \u25CF
    ${this._t("msg.recording_in_progress",{},"Recording in progress")}
    ${this._t("msg.see_recorder",{},"See recorder widget below")}
    `),t.feedback_count&&this._canEdit()&&$.push(``);const L=this._conflictKeysFromOpts();if(L.size&&this._canEdit()){const P=L.size,X=P>1?"s":"";$.push(``)}const D=this._sugCountForDevice(t);D&&this._canEdit()&&$.push(``);const B=t.standby_above_stop;B&&this._canEdit()&&$.push(``);const U=$.length?`
    ${$.join("")}
    `:"",z=t.envelope_position,M=z!=null?Math.round(z*100):null,A=M!=null?` ${this._t("lbl.envelope_position",{pct:M},`curve ${M}%`)}`:"",O=o&&n!=null?` +
    +
    ${it(n,1)}%${A}${l!=null?`${this._t("lbl.time_remaining",{v:ht(l)},`~${ht(l)} remaining`)}`:""}
    + `:"",I=((this._powerData||{}).live||[]).length>1,R=this._pref("show_expected",!0),G=this._pref("show_raw_active",!1),W=`
    ${this._t("lbl.power",{},"Power")} - ${this._statusEnv?``:""} - ${this._pref("show_raw",!1)?``:""} -
    `,tt=this._cyclesTotal||0,_t=(this._profiles||[]).length,Z=this._pref("setup_card_dismissed",!1),lt=this._setupStatus,Q=lt&&!j,nt=Q?this._htmlSetupCard(lt,Z):"",ht=j?`
    ${X}`:Q?nt:`

    ${this._t("msg.live_chart_loading",{},"Live power chart appears as readings arrive.")}

    `,pt=this._pref("show_debug",!1);let ut="";if(pt){const E=this._matchDebug||{},O=E.confidence!=null?`${(E.confidence*100).toFixed(1)}%`:"-",K=(E.candidates||[]).map(N=>`${d(N.profile_name)}${N.confidence_pct}%${N.mae}${N.correlation}${N.duration_ratio>=0?"+":""}${N.duration_ratio}%`).join("");ut=`
    -
    Live Match Debug ${dt("Confidence: how closely the current power curve matches the top candidate profile (0-100%). Ambiguous: the two best candidates score within 5% of each other - the label is uncertain until the cycle finishes.")}
    + ${this._statusEnv?``:""} + ${this._pref("show_raw",!1)?``:""} +
    `,J=this._cyclesTotal||0,Y=(this._profiles||[]).length,nt=this._pref("setup_card_dismissed",!1),_t=this._setupStatus,ct=_t&&!o,dt=ct?this._htmlSetupCard(_t,nt):"",ut=I?`
    ${W}`:"",m=ct?dt+ut:ut||`

    ${this._t("msg.live_chart_loading",{},"Live power chart appears as readings arrive.")}

    `,g=this._pref("show_debug",!1);let k="";if(g){const P=this._matchDebug||{},X=P.confidence!=null?`${(P.confidence*100).toFixed(1)}%`:"-",q=(P.candidates||[]).map(j=>`${d(j.profile_name)}${j.confidence_pct}%${j.mae}${j.correlation}${j.duration_ratio>=0?"+":""}${j.duration_ratio}%`).join("");k=`
    +
    ${this._t("hdr.live_match_debug",{},"Live Match Debug")} ${ot(this._tText("msg.live_match_debug_tip",{},"Confidence: how closely the current power curve matches the top candidate profile (0-100%). Ambiguous: the two best candidates score within 5% of each other - the label is uncertain until the cycle finishes."))}
    -
    ${O}
    ${this._t("lbl.confidence",{},"Confidence")}
    -
    ${E.ambiguous?this._t("status.ambiguous",{},"Ambiguous"):this._t("status.clear",{},"Clear")}
    ${this._t("lbl.label",{},"Match")}
    +
    ${X}
    ${this._t("lbl.confidence",{},"Confidence")}
    +
    ${P.ambiguous?this._t("status.ambiguous",{},"Ambiguous"):this._t("status.clear",{},"Clear")}
    ${this._t("lbl.label",{},"Match")}
    - ${K?`${K}
    ProfileConfMAECorrDuration
    `:`

    ${this._t("msg.no_match_yet",{},"No match attempt yet - this populates during a running cycle.")}

    `} -
    `}const m=[];this._canEdit()&&m.push(``),m.push(``);const _=`
    ${this._t("hdr.tools_and_data",{},"Tools & Data")}
    ${m.join("")}
    `,b=(()=>{if(!this._canEdit())return"";const E=zt.includes(s),O=E&&!e,K=e,N=E||e;return!O&&!K&&!N?"":`
    - ${K?``:""} - ${O?``:""} - ${N?``:""} + ${q?`${q}
    ${this._t("lbl.profile",{},"Profile")}${this._t("col.dbg_conf",{},"Conf")}${this._t("col.dbg_mae",{},"MAE")}${this._t("col.dbg_corr",{},"Corr")}${this._t("lbl.duration",{},"Duration")}
    `:`

    ${this._t("msg.no_match_yet",{},"No match attempt yet - this populates during a running cycle.")}

    `} +
    `}const y=(()=>{if(!this._canEdit())return"";const P=Mt.includes(s),X=P&&!e,q=e,j=P||e;return!X&&!q&&!j?"":`
    + ${q?``:""} + ${X?``:""} + ${j?``:""}
    `})();return` - ${R} + ${U}
    ${d(t.title)}
    - ${b} + ${y}
    -
    - ${d(a)} - ${!r&&t.sub_state&&t.sub_state.toLowerCase()!==s?`(${d(t.sub_state)})`:""} +
    + ${d(i)} + ${!a&&t.sub_state&&t.sub_state.toLowerCase()!==s?`(${d(t.sub_state==="Stalled"?this._tText("status.stalled",{},"Stalled"):t.sub_state)})`:""}
    - ${v} + ${x}
    -
    ${Ot(t.current_power_w)}
    ${this._t("lbl.power",{},"Power")}
    -
    ${i!=null?i.toFixed(0)+"%":"-"}
    ${this._t("lbl.progress",{},"Progress")}
    -
    ${gt(l)}
    ${this._t("lbl.remaining",{},"Remaining")}
    +
    ${Lt(t.current_power_w)}
    ${this._t("lbl.power",{},"Power")}
    +
    ${n!=null?it(n,0)+"%":"-"}
    ${this._t("lbl.progress",{},"Progress")}
    +
    ${ht(l)}
    ${this._t("lbl.remaining",{},"Remaining")}
    - ${A} - ${this._htmlPhaseTimeline(t,i,o)} - ${Q?"":`
    ${this._t("hdr.live_power",{},"Live Power")}
    `} - ${ht} + ${O} + ${this._htmlPhaseTimeline(t,n,o)} + ${ct?"":`
    ${this._t("hdr.live_power",{},"Live Power")}
    `} + ${m}
    ${this._canEdit()?this._htmlRecordingWidget():""} - ${ut} - ${_} - `}_htmlSetupCard(t,e=!1){if(!t)return"";const{phase:s,message_key:r,message_params:n,cta_label_key:a,cta_action:o,secondary_label_key:i,secondary_action:l,skippable:c,dismissible:g,step_key:p}=t;if(s==="phase4")return` + ${k} + `}_htmlSetupCard(t,e=!1){if(!t)return"";const{phase:s,message_key:a,message_params:r,cta_label_key:i,cta_action:o,secondary_label_key:n,secondary_action:l,skippable:c,dismissible:p,step_key:_}=t;if(s==="phase4")return`
    @@ -1003,13 +1007,13 @@ button.wd-profile-card { display: block; } title="${d(this._t("setup.cta.show_guidance",{},"Show guidance"))}"> ${this._t("setup.hdr.card",{},"Device Setup")} -
    `;const f=this._t(r,n||{},""),u=this._t(a,{},"Continue"),$=i?this._t(i,{},""):null,S=c?` +
    `;const f=this._tText(a,r||{},""),u=this._t(i,{},"Continue"),b=n?this._t(n,{},""):null,v=c?` - ${this._t("setup.cta.skip_step",{},"Skip this step")} - ${this._t("setup.cta.skip_forever",{},"Don't show again")} - `:"",x=g?` + `:"",T=p?` ${this._t("setup.cta.hide_guidance",{},"Hide guidance")} `:"";return` @@ -1019,176 +1023,182 @@ button.wd-profile-card { display: block; }
    - ${$?` - ${d($)} + data-cta-params="${d(JSON.stringify(r||{}))}"> + ${d(b)} `:""}
    - ${S} - ${x} -
    `}_dispatchSetupCta(t,e){if(t){if(t==="open_recorder"){this.shadowRoot.querySelector(".wd-rec-dot")?.closest(".wd-card")?.scrollIntoView({behavior:"smooth"});return}if(t==="open_cycles"||t==="open_cycles_unlabeled"){this._tab="history",t==="open_cycles_unlabeled"&&(this._cycleFilter={...this._cycleFilter||{},status:"unlabeled"}),this._fetchTabData();return}if(t==="open_profiles"||t==="open_profiles_groups"){this._tab="profiles",this._fetchTabData();return}if(t==="open_suggestions"){this._tab="settings",this._settingsSugOnly=!0,this._fetchTabData();return}if(t==="create_profile_from_cluster"){this._tab="profiles",this._fetchTabData();return}if(t&&t.startsWith("open_cycle:")){this._tab="history",this._fetchTabData();return}}}async _reloadSetupStatus(){const t=this._devices[this._selIdx];if(t){try{this._setupStatus=await this._ws({type:`${y}/get_setup_status`,entry_id:t.entry_id})}catch{this._setupStatus=null}this._render()}}_htmlPhaseTimeline(t,e,s){const r=this._statusPhases||[];if(!s||!r.length||!t.current_program)return"";let n=this._statusEnv&&this._statusEnv.target_duration||0;if(!n){const g=(this._profiles||[]).find(p=>p.name===t.current_program);n=g&&g.avg_duration||0}if(n||(n=Math.max(1,...r.map(g=>g.end||0))),n<=0)return"";const a=e!=null?Math.min(1,Math.max(0,e/100)):null;let o="";const i=r.map((g,p)=>{const f=Math.max(0,Math.min(1,(g.start||0)/n)),u=Math.max(0,Math.min(1,(g.end||0)/n)),$=Math.max(0,(u-f)*100),S=it[p%it.length],x=a==null?!0:f<=a;a!=null&&a>=f&&a12?`${d(g.name)}`:"";return`
    ${w}
    `}).join(""),l=a!=null?`
    `:"",c=o?`
    ${this._t("lbl.current_phase",{},"Current phase")}: ${d(o)}
    `:"";return`
    - - ${c} -
    `}_htmlRecordingWidget(){const t=this._recState,e=this._devices[this._selIdx],r=!!(e&&e.recording)?"recording":t?t.state:"idle",n=r==="recording"?"wd-rec-active":r==="stopped"?"wd-rec-ready":"wd-rec-idle",a=r==="recording"?this._t("status.recording",{},"Recording\u2026"):r==="stopped"?this._t("status.ready",{},"Ready to process"):this._t("status.idle",{},"Idle");let o="";r==="recording"?o=t?`${gt(t.duration_s)} \xB7 ${t.sample_count||0} samples`:"":r==="stopped"&&(o=`${t.sample_count||0} samples \xB7 ${gt(t.duration_s)}`);const i=r==="recording"?``:r==="stopped"?` - `:``;return`
    + ${v} + ${T} +
    `}_dispatchSetupCta(t,e){if(t){if(t==="open_recorder"){this.shadowRoot.querySelector(".wd-rec-dot")?.closest(".wd-card")?.scrollIntoView({behavior:"smooth"});return}if(t==="open_cycles"||t==="open_cycles_unlabeled"){this._tab="history",this._historySub="cycles",t==="open_cycles_unlabeled"&&(this._cycleFilter={...this._cycleFilter||{},status:"unlabeled"}),this._fetchTabData();return}if(t==="open_profiles"){this._tab="profiles",this._fetchTabData();return}if(t==="open_suggestions"){this._tab="settings",this._settingsSugOnly=!0,this._fetchTabData();return}if(t==="create_profile_from_cluster"){this._tab="profiles",this._fetchTabData();return}if(t&&t.startsWith("open_cycle:")){this._tab="history",this._historySub="cycles",this._fetchTabData();return}}}async _reloadSetupStatus(){const t=this._devices[this._selIdx];if(t){try{this._setupStatus=await this._ws({type:`${w}/get_setup_status`,entry_id:t.entry_id})}catch{this._setupStatus=null}this._render()}}_htmlPhaseTimeline(t,e,s){const a=this._statusPhases||[];if(!s||!a.length||!t.current_program)return"";const r=Math.max(0,...a.map(u=>u.end||0)),i=Number(t.expected_duration_s),o=Math.max(r,Number.isFinite(i)?i:0);if(o<=0)return"";const n=e!=null?Math.min(1,Math.max(0,e/100)):null;let l="",c=!1;const p=a.map((u,b)=>{const v=Math.max(0,Math.min(1,(u.start||0)/o)),T=Math.max(0,Math.min(1,(u.end||0)/o)),C=Math.max(0,(T-v)*100),x=Q[b%Q.length],$=n==null?!0:v<=n;!c&&n!=null&&T>v&&n>=v&&(n=1&&T>=1)&&(c=!0,l=u.name||"");const L=u.name&&C>12?`${d(u.name)}`:"";return`
    ${L}
    `}).join(""),_=n!=null?`
    `:"",f=l?`
    ${this._t("lbl.current_phase",{},"Current phase")}: ${d(l)}
    `:"";return`
    + + ${f} +
    `}_htmlRecordingWidget(){const t=this._recState,e=this._devices[this._selIdx],a=!!(e&&e.recording)?"recording":t?t.state:"idle",r=a==="recording"?"wd-rec-active":a==="stopped"?"wd-rec-ready":"wd-rec-idle",i=a==="recording"?this._t("status.recording",{},"Recording\u2026"):a==="stopped"?this._t("status.ready",{},"Ready to process"):this._t("status.idle",{},"Idle");let o="";const n=c=>this._t("lbl.n_samples",{n:c},`${c} samples`);a==="recording"?o=t?`${ht(t.duration_s)} \xB7 ${n(t.sample_count||0)}`:"":a==="stopped"&&(o=`${n(t.sample_count||0)} \xB7 ${ht(t.duration_s)}`);const l=a==="recording"?``:a==="stopped"?` + `:``;return`
    -
    -
    ${this._t("hdr.manual_recording",{},"Manual Recording")}${dt(this._t("hdr.manual_recording_tip",{},"Run a cycle intentionally while WashData records the power trace. Start just before the appliance starts, Stop when it finishes, then Process to save it as a named profile."))}${o?`${o}`:""}
    +
    +
    ${this._t("hdr.manual_recording",{},"Manual Recording")}${ot(this._t("hdr.manual_recording_tip",{},"Record a cycle on purpose: press Start just before the appliance starts and Stop when it finishes, then Process to save it as a profile."))}${o?`${o}`:""}
    -
    ${i}
    +
    ${l}
    -
    `}_htmlHistory(){const t=this._cycles||[],e=this._refCycles||[],s=e.concat(t),r=this._canEdit(),n=this._selectMode&&r,a=this._cycleSel,{col:o,dir:i}=this._cycleSort,{text:l,status:c}=this._cycleFilter,g=this._mlById||{},p=new Set((this._feedbacks||[]).map(_=>_.cycle_id)),f=_=>g[_.id],u=_=>{const b=f(_);return!!(b&&b.ml_review&&b.ml_review.reviewed_at)},$=_=>{const b=f(_);return!!(b&&b.ml_review&&b.ml_review.golden)},S=_=>{if(p.has(_.id))return!0;if(u(_))return!1;const b=f(_),E=b&&b.ml_quality_label;return["uncertain","review"].includes(E)||["force_stopped","interrupted"].includes(_.status)},x=s.filter(S).length;let w=s;if(l){const _=l.toLowerCase();w=w.filter(b=>(b.profile_name||b.matched_profile||"").toLowerCase().includes(_))}c==="unlabelled"?w=w.filter(_=>!_.profile_name&&!_.matched_profile):c==="needs_review"?w=w.filter(S):c==="imported"?w=w.filter(_=>_.is_reference):c&&(w=w.filter(_=>(_.status||"completed")===c));const v={date:_=>_.start_time?new Date(_.start_time).getTime():0,confidence:_=>_.match_confidence,duration:_=>_.duration,energy:_=>_.energy_kwh!=null?_.energy_kwh:_.energy_wh!=null?_.energy_wh/1e3:null,cost:_=>_.cost!=null?_.cost:-1,status:_=>_.status||"completed",profile:_=>(_.profile_name||_.matched_profile||"\uFFFF").toLowerCase()};w=oe(w,v[o]||v.date,i);const k=_=>_==="completed"?"var(--success-color, #4caf50)":_==="interrupted"?"var(--error-color, #f44336)":_==="force_stopped"?"var(--warning-color, #ff9800)":"var(--secondary-text-color)",D=_=>{if(!_.is_reference)return"";const b=_.cycle_origin==="backfill",E=b?this._t("badge.backfilled_tip",{},"Detected in imported power history. Shapes program matching only, not counted in stats."):this._t("badge.imported_tip",{},"Imported from the community store. Used for matching only, not counted in stats.");return` ${b?"\u{1F557}":"\u{1F4E5}"}`},I=_=>$(_)?' \u2B50':p.has(_.id)?' \u{1F4AC}':u(_)?' \u2713':S(_)?' \u25CF':"",z=_=>{if(_.anomaly!=="overrun")return"";const b=_.overrun_ratio?" "+this._t("badge.overrun_ratio",{x:Number(_.overrun_ratio).toFixed(1)},`(${Number(_.overrun_ratio).toFixed(1)}x expected)`):"";return` \u23F1`},R=_=>{if(_.anomaly!=="underrun")return"";const b=_.underrun_ratio?" "+this._t("badge.underrun_ratio",{pct:Math.round(_.underrun_ratio*100)},`(${Math.round(_.underrun_ratio*100)}% of expected)`):"";return` \u26A1`},L=_=>{if(!_.energy_anomaly||_.energy_anomaly==="none")return"";const b=_.energy_anomaly==="energy_spike",E=_.energy_z_score!=null?` (${_.energy_z_score>0?"+":""}${Number(_.energy_z_score).toFixed(1)}\u03C3)`:"",O=b?"badge.energy_spike":"badge.energy_low",K=b?"Higher energy than usual":"Lower energy than usual",N=b?"\u{1F53A}":"\u{1F53B}",q=b?"var(--error-color,#f44336)":"var(--info-color,#2196f3)";return` ${N}`},P=_=>{const b=Array.isArray(_.artifacts)?_.artifacts.length:0;return b?` 1?"ies":"y"} detected (e.g. door opened mid-cycle) \u2014 open to see them on the graph`))}" style="color:var(--warning-color,#ff9800)">\u26A0`:""},C=_=>{const b=Array.isArray(_.restart_gaps)?_.restart_gaps.length:0;return b?` 1?"s":""} during this cycle \u2014 power trace has a hole`))}" style="color:var(--info-color,#2196f3)">\u21BB`:""},A=this._hass&&this._hass.config&&this._hass.config.currency||"",F=_=>{if(_.cost==null)return"-";const b=`${_.cost.toFixed(2)}${A?" "+A:""}`;if(_.energy_price_mode==="dynamic"&&_.energy_price!=null){const E=this._t("col.cost_dynamic_tip",{price:_.energy_price.toFixed(4),cur:A},`Time-weighted: charged at the price in force during the cycle (effective ${_.energy_price.toFixed(4)} ${A}/kWh).`);return`${b}`}return b},j=w.map(_=>{const b=_.profile_name||_.matched_profile,E=_.match_confidence!=null?_.match_confidence*100:null,O=_.status||"completed",K=_.energy_kwh!=null?_.energy_kwh:_.energy_wh!=null?_.energy_wh/1e3:null,N=n,q=N?``:``,V={completed:this._t("status.completed",{},"Completed"),interrupted:this._t("status.interrupted",{},"Interrupted"),force_stopped:this._t("status.force_stopped",{},"Force stopped"),active:this._t("status.active",{},"Active")}[O]||O,U=`${D(_)}${I(_)}${z(_)}${R(_)}${L(_)}${P(_)}${C(_)}`.trim();return` - ${q} - ${b?d(b):`${this._t("lbl.unlabelled",{},"Unlabelled")}`} - ${U} - ${d(V)} - ${ot(_.start_time)} - ${gt(_.duration)} - ${K!=null?bt(K):"-"} - ${F(_)} - ${E!=null?E.toFixed(0)+"%":"-"} - `}).join(""),st=` +
    `}_htmlHistoryTab(){const t=this._historySub==="maintenance"?"maintenance":"cycles";return`
    ${[["cycles",this._t("tab.history",{},"Cycles")],["maintenance",this._t("tab.maintenance",{},"Maintenance")]].map(([s,a])=>``).join("")}
    ${t==="maintenance"?this._htmlMaintenance():this._htmlHistory()}`}_htmlHistory(){const t=this._cycles||[],e=this._refCycles||[],s=e.concat(t),a=this._canEdit(),r=this._selectMode&&a,i=this._cycleSel,{col:o,dir:n}=this._cycleSort,{text:l,status:c}=this._cycleFilter,p=this._mlById||{},_=new Set((this._feedbacks||[]).map(y=>y.cycle_id)),f=y=>p[y.id],u=y=>{const P=f(y);return!!(P&&P.ml_review&&P.ml_review.reviewed_at)},b=y=>{const P=f(y);return!!(P&&P.ml_review&&P.ml_review.golden)},v=y=>_.has(y.id),T=s.filter(v).length;let C=s;if(l){const y=l.toLowerCase();C=C.filter(P=>(P.profile_name||P.matched_profile||"").toLowerCase().includes(y))}c==="unlabelled"?C=C.filter(y=>!y.profile_name&&!y.matched_profile):c==="needs_review"?C=C.filter(v):c==="imported"?C=C.filter(y=>y.is_reference):c&&(C=C.filter(y=>(y.status||"completed")===c));const x={date:y=>y.start_time?new Date(y.start_time).getTime():0,confidence:y=>y.match_confidence,duration:y=>y.duration,energy:y=>y.energy_kwh!=null?y.energy_kwh:y.energy_wh!=null?y.energy_wh/1e3:null,cost:y=>y.cost!=null?y.cost:-1,status:y=>y.status||"completed",profile:y=>(y.profile_name||y.matched_profile||"\uFFFF").toLowerCase()};C=_e(C,x[o]||x.date,n);const $=y=>y==="completed"?"var(--success-color, #4caf50)":y==="interrupted"?"var(--error-color, #f44336)":y==="force_stopped"?"var(--warning-color, #ff9800)":"var(--secondary-text-color)",L=y=>{if(!y.is_reference)return"";const P=y.cycle_origin==="backfill",X=P?this._t("badge.backfilled_tip",{},"Detected in imported power history. Shapes program matching only, not counted in stats."):this._t("badge.imported_tip",{},"Imported from the community store. Used for matching only, not counted in stats.");return` ${P?"\u{1F557}":"\u{1F4E5}"}`},D=y=>b(y)?' \u2B50':_.has(y.id)?' \u{1F4AC}':u(y)?' \u2713':v(y)?' \u25CF':"",B=y=>{if(y.anomaly!=="overrun")return"";const P=y.overrun_ratio?" "+this._tText("badge.overrun_ratio",{x:Number(y.overrun_ratio).toFixed(1)},`(${Number(y.overrun_ratio).toFixed(1)}x expected)`):"";return` \u23F1`},U=y=>{if(y.anomaly!=="underrun")return"";const P=y.underrun_ratio?" "+this._tText("badge.underrun_ratio",{pct:Math.round(y.underrun_ratio*100)},`(${Math.round(y.underrun_ratio*100)}% of expected)`):"";return` \u26A1`},z=y=>{if(!y.energy_anomaly||y.energy_anomaly==="none")return"";const P=y.energy_anomaly==="energy_spike",X=y.energy_z_score!=null?` (${y.energy_z_score>0?"+":""}${Number(y.energy_z_score).toFixed(1)}\u03C3)`:"",q=P?"badge.energy_spike":"badge.energy_low",j=P?"Higher energy than usual":"Lower energy than usual",V=P?"\u{1F53A}":"\u{1F53B}",E=P?"var(--error-color,#f44336)":"var(--info-color,#2196f3)";return` ${V}`},M=y=>{const P=Array.isArray(y.artifacts)?y.artifacts.length:0;return P?` 1?"ies":"y"} detected (e.g. door opened mid-cycle) - open to see them on the graph`))}" style="color:var(--warning-color,#ff9800)">\u26A0`:""},A=y=>{const P=Array.isArray(y.restart_gaps)?y.restart_gaps.length:0;return P?` 1?"s":""} during this cycle - power trace has a hole`))}" style="color:var(--info-color,#2196f3)">\u21BB`:""},O=this._hass&&this._hass.config&&this._hass.config.currency||"",K=y=>{if(y.cost==null)return"-";const P=It(y.cost,O);if(y.energy_price_mode==="dynamic"&&y.energy_price!=null){const X=this._tText("col.cost_dynamic_tip",{price:it(y.energy_price,4),cur:O},"Time-weighted: charged at the price in force during the cycle (effective {price} {cur}/kWh).");return`${P}`}return P},I=C.map(y=>{const P=y.profile_name||y.matched_profile,X=y.match_confidence!=null?y.match_confidence*100:null,q=y.status||"completed",j=y.energy_kwh!=null?y.energy_kwh:y.energy_wh!=null?y.energy_wh/1e3:null,V=r,E=V?``:``,S=this._cycleStatusLabel(q),F=`${L(y)}${D(y)}${B(y)}${U(y)}${z(y)}${M(y)}${A(y)}`.trim();return` + ${E} + ${P?d(P):`${this._t("lbl.unlabelled",{},"Unlabelled")}`} + ${F} + ${d(S)} + ${et(y.start_time)} + ${ht(y.duration)} + ${j!=null?gt(j):"-"} + ${K(y)} + ${X!=null?it(X,0)+"%":"-"} + `}).join(""),R=` - ${xt(this._t("lbl.profile",{},"Profile"),"profile",o==="profile",i,"cycsort","",this._t("col.profile_tip",{},"Matched program name. Unlabelled means no profile matched at end of cycle."))} + ${mt(this._t("lbl.profile",{},"Profile"),"profile",o==="profile",n,"cycsort","",this._t("col.profile_tip",{},"Matched program name. Unlabelled means no profile matched at end of cycle."))} ${this._t("lbl.flags",{},"Flags")} - ${xt(this._t("lbl.status",{},"Status"),"status",o==="status",i,"cycsort","",this._t("col.status_tip",{},"Cycle outcome: Completed (natural end), Interrupted (abrupt power drop), Force Stopped (manual), or Needs Review (feedback pending)."))} - ${xt(this._t("lbl.date",{},"Date"),"date",o==="date",i,"cycsort","",this._t("col.date_tip",{},"Date and time the cycle started."))} - ${xt(this._t("lbl.duration",{},"Duration"),"duration",o==="duration",i,"cycsort","right",this._t("col.duration_tip",{},"Total cycle run time from start to end."))} - ${xt(this._t("lbl.energy",{},"Energy"),"energy",o==="energy",i,"cycsort","right",this._t("col.energy_tip",{},"Total energy consumed (kWh). Computed by integrating power over time."))} - ${xt(this._t("lbl.cost",{},"Cost"),"cost",o==="cost",i,"cycsort","right",this._t("col.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."))} - ${xt(this._t("lbl.confidence",{},"Confidence"),"confidence",o==="confidence",i,"cycsort","right",this._t("col.confidence_tip",{},"Profile match confidence (0-100%). How closely the cycle power curve matched the identified program."))} - `,J=`
    + ${mt(this._t("lbl.status",{},"Status"),"status",o==="status",n,"cycsort","",this._t("col.status_tip",{},"Cycle outcome: Completed (natural end), Interrupted (abrupt power drop), Force Stopped (manual), or Needs Review (feedback pending)."))} + ${mt(this._t("lbl.date",{},"Date"),"date",o==="date",n,"cycsort","",this._t("col.date_tip",{},"Date and time the cycle started."))} + ${mt(this._t("lbl.duration",{},"Duration"),"duration",o==="duration",n,"cycsort","right",this._t("col.duration_tip",{},"Total cycle run time from start to end."))} + ${mt(this._t("lbl.energy",{},"Energy"),"energy",o==="energy",n,"cycsort","right",this._t("col.energy_tip",{},"Total energy used (kWh), computed from power over time."))} + ${mt(this._t("lbl.cost",{},"Cost"),"cost",o==="cost",n,"cycsort","right",this._t("col.cost_tip",{},"Energy cost of this cycle, fixed when it finished. With Time-Weighted Cost on, each moment is charged at its own price; otherwise at the price at the end. Set a price in Settings to fill this in."))} + ${mt(this._t("lbl.confidence",{},"Confidence"),"confidence",o==="confidence",n,"cycsort","right",this._t("col.confidence_tip",{},"Profile match confidence (0-100%). How closely the cycle power curve matched the identified program."))} + `,G=`
    -
    `,X=w.length!==s.length?this._t("lbl.n_shown",{n:w.length},`, ${w.length} shown`):"",tt=e.length?this._t("lbl.n_imported_note",{n:e.length},`, ${e.length} imported`):"",_t=this._t("lbl.cycles_title",{n:`${t.length}${tt}${X}`},`Cycles (${t.length}${tt}${X})`),Z=r?`
    - - -
    `:"",lt=new Set(e.map(_=>_.id)),Q=[...a].some(_=>lt.has(_)),nt=n?`
    - ${this._t("lbl.n_selected",{n:a.size},`${a.size} selected`)} - - - - -
    `:"",ht=this._busy.has("cyc-load-more"),pt=this._cyclesHasMore?`
    - -
    `:"",ut=` +
    `,W=C.length!==s.length?this._t("lbl.n_shown",{n:C.length},`, ${C.length} shown`):"",J=e.length?this._t("lbl.n_imported_note",{n:e.length},`, ${e.length} imported`):"",Y=this._t("lbl.cycles_title",{n:`${t.length}${J}${W}`},`Cycles (${t.length}${J}${W})`),nt=a?`
    + + +
    `:"",_t=new Set(e.map(y=>y.id)),ct=[...i].some(y=>_t.has(y)),dt=r?`
    + ${this._t("lbl.n_selected",{n:i.size},`${i.size} selected`)} + + + + +
    `:"",ut=this._busy.has("cyc-load-more"),m=this._cyclesHasMore||this._importedHasMore?`
    + +
    `:"",g=`
    -
    ${_t}
    - ${J} - ${Z}${nt} - ${w.length===0?`
    \u{1F4CB}
    ${s.length?this._t("msg.no_cycles_match",{},"No cycles match the current filter."):this._t("msg.no_cycles_yet",{},"No cycles recorded yet.")}
    `:`
    ${st}${j}
    `} - ${pt} -
    `;return(this._cyclesError?`
    ${this._t("msg.fetch_error",{},"Failed to load data.")}
    `:"")+ut}_trendIcon(t){return t==="up"?`\u2191`:t==="down"?`\u2193`:""}_profileCardHtml(t){const e=t.avg_duration?this._t("lbl.duration_avg",{v:Math.round(t.avg_duration/60)},`~${Math.round(t.avg_duration/60)}m avg`):this._t("lbl.no_duration",{},"no duration"),s=t.avg_energy!=null?` \xB7 ${bt(t.avg_energy)}/cycle`:"",r=t.avg_energy!=null&&t.cycle_count?` \xB7 ${bt(t.avg_energy*t.cycle_count)} total`:"",n=this._hass&&this._hass.config&&this._hass.config.currency||"",a=t.avg_cost!=null?` \xB7 ${this._t("lbl.avg_cost",{},"Avg")} ${t.avg_cost.toFixed(2)}${n?" "+n:""}/${this._t("lbl.per_cycle_short",{},"cycle")}`:"",o=(this._profileHealth||{})[t.name],i=(this._profileTrends||{})[t.name];let l="";o&&o.health_status==="poor"?l=`\u26A0 ${this._t("badge.poor_fit",{},"poor fit")}`:o&&o.health_status==="fair"&&(l=`${this._t("badge.fair_fit",{},"fair fit")}`);let c="";if(i){const R=this._trendIcon(i.duration_trend),L=i.energy_trend?this._trendIcon(i.energy_trend):"";if(i.duration_trend!=="stable"||i.energy_trend==="up"){const P=[];if(i.duration_trend!=="stable"){const A=`${i.duration_slope_pct>0?"+":""}${i.duration_slope_pct}`;P.push(i.duration_trend==="up"?this._t("msg.duration_trend_up_tip",{pct:A},`Duration up (${A}%/cycle)`):this._t("msg.duration_trend_down_tip",{pct:A},`Duration down (${A}%/cycle)`))}if(i.energy_trend&&i.energy_trend!=="stable"){const A=`${i.energy_slope_pct>0?"+":""}${i.energy_slope_pct}`;P.push(i.energy_trend==="up"?this._t("msg.energy_trend_up_tip",{pct:A},`Energy up (${A}%/cycle)`):this._t("msg.energy_trend_down_tip",{pct:A},`Energy down (${A}%/cycle)`))}const C=P.join(", ")||this._t("msg.performance_trending",{},"Performance trending");c=`${R}${L||""}`}}const g=this._constants&&this._constants.PROFILE_MIN_WARMUP_CYCLES||5,p=o&&o.cycle_count||0,u=p${this._t("msg.warmup_badge",{done:p,needed:g},`Still learning (${p}/${g} cycles)`)}`:"",$=t.is_imported?`\u{1F4E5} ${this._t("status.imported",{},"Imported")}`:"",S=(this._profileAdvisories||[]).find(R=>R&&R.profile===t.name&&R.code==="unmatchable"),x=S?`\u26A0 ${this._t("badge.unmatchable",{},"can't be matched")}`:"",w=(this._profileTerminal||{})[t.name];let v="";if(w&&w.quiet_before_s>0&&w.seen_in>=2){const R=Math.max(1,Math.round(w.quiet_before_s/60)),L=Number(w.event_watts||0).toFixed(0),P=Math.round(w.event_seconds||0),C=this._t("badge.quiet_tail_tip",{mins:R,secs:P,watts:L,seen:w.seen_in,measured:w.measured},`Near the end this program goes quiet for about ${R} min, then draws about ${L} W for ${P} s before finishing. Seen in ${w.seen_in} of ${w.measured} measured cycles: the appliance does not do it every run.`);v=`${this._t("badge.quiet_tail",{mins:R},`~${R}m quiet tail`)}`}const k=(this._profileAdvisories||[]).find(R=>R&&R.profile===t.name&&R.code==="duration_outlier"),D=k?`\u26A0 ${this._t("badge.duration_outlier",{n:(k.message_params||{}).n||1},`${(k.message_params||{}).n||1} odd-length cycle(s)`)}`:"",I=[x,D,l,c,v,u,$].filter(Boolean).join(" "),z=Array.isArray(t.signature_curve)&&t.signature_curve.length>=3?``:"";return` +
    ${Y}
    + ${G} + ${nt}${dt} + ${C.length===0?`
    \u{1F4CB}
    ${s.length?this._t("msg.no_cycles_match",{},"No cycles match the current filter."):this._t("msg.no_cycles_yet",{},"No cycles recorded yet.")}
    `:`
    ${R}${I}
    `} + ${m} +
    `;return(this._cyclesError?``:"")+g}_profileAdvisory(t,e){return(this._profileAdvisories||[]).find(s=>s&&s.profile===t&&s.code===e)}_advisoryText(t){return this._tText(t.message_key,t.message_params||{},t.message||"")}_trendIcon(t){return t==="up"?`\u2191`:t==="down"?`\u2193`:""}_profileCardHtml(t){const e=t.avg_duration?this._t("lbl.duration_avg",{v:Math.round(t.avg_duration/60)},`~${Math.round(t.avg_duration/60)}m avg`):this._t("lbl.no_duration",{},"no duration"),s=t.avg_energy!=null?` \xB7 ${this._t("lbl.energy_per_cycle",{v:gt(t.avg_energy)},"{v}/cycle")}`:"",a=t.avg_energy!=null&&t.cycle_count?` \xB7 ${this._t("lbl.energy_total",{v:vt(`${d(gt(t.avg_energy*t.cycle_count))}`)},"{v} total")}`:"",r=this._hass&&this._hass.config&&this._hass.config.currency||"",i=t.avg_cost!=null?` \xB7 ${this._t("lbl.avg_cost",{},"Avg")} ${It(t.avg_cost,r)}/${this._t("lbl.per_cycle_short",{},"cycle")}`:"",o=(this._profileHealth||{})[t.name],n=(this._profileTrends||{})[t.name],l=this._profileAdvisory(t.name,"poor_health"),c=l?`\u26A0 ${this._t("badge.poor_fit",{},"poor fit")}`:"",p=this._profileAdvisory(t.name,"duration_trend_up")||this._profileAdvisory(t.name,"energy_trend_up"),_=p?`${this._trendIcon("up")}`:"",f=this._constants&&this._constants.PROFILE_MIN_WARMUP_CYCLES||5,u=o&&o.cycle_count||0,v=u${this._t("msg.warmup_badge",{done:u,needed:f},`Still learning (${u}/${f} cycles)`)}`:"",T=t.is_imported?`\u{1F4E5} ${this._t("status.imported",{},"Imported")}`:"",C=t.is_imported?Number((this._profileMatcherCounts||{})[t.name]||0):null,x=C==null?"":`${C>0?this._t("badge.matcher_used",{n:C},`Matched ${C} of your cycles`):this._t("badge.matcher_unused",{},"Not matched to your cycles yet")}`,$=this._profileAdvisory(t.name,"unmatchable"),L=$?`\u26A0 ${this._t("badge.unmatchable",{},"can't be matched")}`:"",D=(this._profileTerminal||{})[t.name];let B="";if(D&&D.quiet_before_s>0&&D.seen_in>=2){const O=Math.max(1,Math.round(D.quiet_before_s/60)),K=Number(D.event_watts||0).toFixed(0),I=Math.round(D.event_seconds||0),R=this._tText("badge.quiet_tail_tip",{mins:O,secs:I,watts:K,seen:D.seen_in,measured:D.measured},`Near the end this program goes quiet for about ${O} min, then draws about ${K} W for ${I} s. Seen in ${D.seen_in} of ${D.measured} measured cycles.`);B=`${this._t("badge.quiet_tail",{mins:O},`~${O}m quiet tail`)}`}const U=this._profileAdvisory(t.name,"duration_outlier"),z=U?`\u26A0 ${this._t("badge.duration_outlier",{n:(U.message_params||{}).n||1},`${(U.message_params||{}).n||1} odd-length cycle(s)`)}`:"",M=[L,z,c,_,B,v,T,x].filter(Boolean).join(" "),A=Array.isArray(t.signature_curve)&&t.signature_curve.length>=3?``:"";return`
    -
    `}_paintSparkline(t,e){if(!t||!Array.isArray(e)||e.length<3)return;const s=(getComputedStyle(this).getPropertyValue("--primary-color")||"#03a9f4").trim()||"#03a9f4",r=window.devicePixelRatio||1,n=t.getBoundingClientRect(),a=t.width=Math.max(1,Math.round((n.width||64)*r)),o=t.height=Math.max(1,Math.round((n.height||20)*r)),i=t.getContext("2d");i.clearRect(0,0,a,o);const l=Math.max(...e,1),c=2*r,g=f=>c+(e.length===1?0:f/(e.length-1)*(a-2*c)),p=f=>o-c-Math.max(0,f)/l*(o-2*c);i.beginPath(),e.forEach((f,u)=>{const $=g(u),S=p(f);u===0?i.moveTo($,S):i.lineTo($,S)}),i.strokeStyle=s,i.lineWidth=1.5*r,i.lineJoin="round",i.lineCap="round",i.stroke()}_drawProfileSparklines(){const t=this.shadowRoot;if(!t)return;const e=t.querySelectorAll("canvas[data-spark-prof]");if(!e.length)return;const s={};for(const r of this._profiles||[])s[r.name]=r;e.forEach(r=>{const n=s[r.dataset.sparkProf];this._paintSparkline(r,n&&n.signature_curve||[])})}_htmlProfiles(){const t=this._busy.has("rebuild-envelopes"),e=this._canEdit(),s={};this._profiles.forEach(u=>{s[u.name]=u});const r=this._profileGroups||{groups:[],suggestions:[]},n=new Set;r.groups.forEach(u=>(u.members||[]).forEach($=>n.add($)));const a=r.groups.map(u=>{const $=(u.members||[]).map(k=>s[k]?this._profileCardHtml(s[k]):"").join(""),S=Math.round((u.cohesion!=null?u.cohesion:1)*100),x=u.cohesive?`${this._t("lbl.cohesion_good",{pct:S},"cohesion "+S+"%")}`:`${this._t("lbl.cohesion_low",{pct:S},"\u26A0 low cohesion "+S+"%")}`,w=u.cohesive?"":`

    ${this._t("msg.group_not_cohesive",{},"These profiles aren't similar enough to group reliably, so matching treats them individually until you remove the outlier or split the group.")}

    `;return`
    +
    `}_paintSparkline(t,e){if(!t||!Array.isArray(e)||e.length<3)return;const s=(getComputedStyle(this).getPropertyValue("--primary-color")||"#03a9f4").trim()||"#03a9f4",a=window.devicePixelRatio||1,r=t.getBoundingClientRect(),i=t.width=Math.max(1,Math.round((r.width||64)*a)),o=t.height=Math.max(1,Math.round((r.height||20)*a)),n=t.getContext("2d");n.clearRect(0,0,i,o);const l=Math.max(...e,1),c=2*a,p=f=>c+(e.length===1?0:f/(e.length-1)*(i-2*c)),_=f=>o-c-Math.max(0,f)/l*(o-2*c);n.beginPath(),e.forEach((f,u)=>{const b=p(u),v=_(f);u===0?n.moveTo(b,v):n.lineTo(b,v)}),n.strokeStyle=s,n.lineWidth=1.5*a,n.lineJoin="round",n.lineCap="round",n.stroke()}_drawProfileSparklines(){const t=this.shadowRoot;if(!t)return;const e=t.querySelectorAll("canvas[data-spark-prof]");if(!e.length)return;const s={};for(const a of this._profiles||[])s[a.name]=a;e.forEach(a=>{const r=s[a.dataset.sparkProf];this._paintSparkline(a,r&&r.signature_curve||[])})}_htmlProfiles(){const t=this._busy.has("rebuild-envelopes"),e=this._canEdit(),s={};this._profiles.forEach(v=>{s[v.name]=v});const a=this._profileGroups||{groups:[]},r=new Set;a.groups.forEach(v=>(v.members||[]).forEach(T=>r.add(T)));const i=a.groups.map(v=>{const T=(v.members||[]).map(D=>s[D]?this._profileCardHtml(s[D]):"").join(""),C=Math.round((v.cohesion!=null?v.cohesion:1)*100),x=v.cohesive?`${this._t("lbl.cohesion_good",{pct:C},"cohesion "+C+"%")}`:`${this._t("lbl.cohesion_low",{pct:C},"\u26A0 low cohesion "+C+"%")}`,$=v.cohesive?"":`

    ${this._t("msg.group_not_cohesive",{},"These profiles aren't similar enough to group reliably, so matching treats them individually until you remove the outlier or split the group.")}

    `;return`
    - ${e?``:`\u{1F517} ${d(u.name)}`} + ${e?``:`\u{1F517} ${d(v.name)}`} ${x} - ${e?``:""} + ${e?``:""}
    - ${w} -
    ${$}
    -
    `}).join(""),o=this._profiles.filter(u=>!n.has(u.name)),i=o.map(u=>this._profileCardHtml(u)).join(""),l=this._profiles.length===0&&!(this._cycles||[]).length&&!(this._refCycles||[]).length,g=(e&&this._onlineEnabled()&&l?` + ${$} +
    ${T}
    +
    `}).join(""),o=this._profiles.filter(v=>!r.has(v.name)),n=o.map(v=>this._profileCardHtml(v)).join(""),l=this._profiles.length===0&&!(this._cycles||[]).length&&!(this._refCycles||[]).length,c=e&&this._onlineEnabled()&&l?`
    \u{1F4E5} ${this._t("msg.onboard_download",{},"New device? Adopt a ready-made setup (programs, reference cycles and phases) from another WashData user with the same appliance.")} -
    `:"")+` +
    `:"",_=(e?((this._coverageGaps||{}).profile_suggestions||[]).slice(0,2):[]).map(v=>{const T=Math.round((v.avg_duration_s||0)/60),C=(v.cycle_ids||[])[0]||"";return`
    + \u{1F9E9} ${this._t("msg.coverage_gap_cluster",{count:v.count,min:T},`${v.count} recent unlabelled cycles of about ${T} min look like one program you have not created yet.`)} + +
    `}).join(""),f=c+_+`
    ${this._t("tab.profiles",{},"Profiles")} (${this._profiles.length})

    ${this._t("msg.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.")}

    ${e?`
    - - + +
    `:""}
    - ${a} - ${this._profiles.length===0?`
    \u{1F4CA}
    ${this._t("msg.no_profiles_yet",{},"No profiles yet. Create one from a labelled cycle.")}
    `:o.length?`${a?`
    ${this._t("lbl.ungrouped",{},"Ungrouped")}
    `:""}
    ${i}
    `:""}`,p=[["profiles",this._t("tab.subtab_profiles",{},"Profiles")],["phase-catalog",this._t("tab.subtab_phase_catalog",{},"Phase Catalog")]].map(([u,$])=>``).join(""),f=this._profilesError||this._profileGroupsError?`
    ${this._t("msg.fetch_error",{},"Failed to load data.")}
    `:"";return` -
    ${p}
    - ${this._profSubtab==="phase-catalog"?this._htmlPhases():f+g} - `}_htmlProfileGroupModal(t){const e=this._busy.has("pg-save"),s=this._profileEnvCache||{},r=p=>it[Math.max(0,this._profiles.findIndex(f=>f.name===p))%it.length],n=t.members||[],a=this._profiles.map(p=>{const f=n.includes(p.name),u=p.avg_duration?`~${Math.round(p.avg_duration/60)}m`:"",$=p.avg_energy!=null?` \xB7 ${bt(p.avg_energy)}`:"",S=f?``:"";return``}).join(""),o=n.filter(p=>s[p]&&(s[p].avg||[]).length),i=o.length?`
    ${o.map(p=>` ${d(p)}`).join("")}
    `:"",l=o.length?`
    ${i}`:`

    ${this._t("msg.group_preview_hint",{},"Tick 2+ members to preview and compare their power curves.")}

    `,c=((this._profileGroups||{}).groups||[]).find(p=>p.name===t.orig),g=c&&c.cohesion!=null?`${c.cohesive?this._t("lbl.cohesion_good",{pct:Math.round(c.cohesion*100)},"cohesion "+Math.round(c.cohesion*100)+"%"):this._t("lbl.cohesion_low",{pct:Math.round(c.cohesion*100)},"\u26A0 low cohesion "+Math.round(c.cohesion*100)+"%")}`:"";return`

    ${t.orig?this._t("modal.edit_group",{},"Edit profile group"):this._t("modal.new_group",{},"New profile group")}

    -
    ${g}
    + ${i} + ${this._profiles.length===0?`
    \u{1F4CA}
    ${this._t("msg.no_profiles_yet",{},"No profiles yet. Create one from a labelled cycle.")}
    `:o.length?`${i?`
    ${this._t("lbl.ungrouped",{},"Ungrouped")}
    `:""}
    ${n}
    `:""}`,u=[["profiles",this._t("tab.subtab_profiles",{},"Profiles")],["phase-catalog",this._t("tab.subtab_phase_catalog",{},"Phase Catalog")]].map(([v,T])=>``).join(""),b=this._profilesError||this._profileGroupsError?``:"";return` +
    ${u}
    + ${this._profSubtab==="phase-catalog"?this._htmlPhases():b+f} + `}_htmlProfileGroupModal(t){const e=this._busy.has("pg-save"),s=this._profileEnvCache||{},a=_=>Q[Math.max(0,this._profiles.findIndex(f=>f.name===_))%Q.length],r=t.members||[],i=this._profiles.map(_=>{const f=r.includes(_.name),u=_.avg_duration?`~${Math.round(_.avg_duration/60)}m`:"",b=_.avg_energy!=null?` \xB7 ${gt(_.avg_energy)}`:"",v=f?``:"";return``}).join(""),o=r.filter(_=>s[_]&&(s[_].avg||[]).length),n=o.length?`
    ${o.map(_=>` ${d(_)}`).join("")}
    `:"",l=o.length?`
    ${n}`:`

    ${this._t("msg.group_preview_hint",{},"Tick 2+ members to preview and compare their power curves.")}

    `,c=((this._profileGroups||{}).groups||[]).find(_=>_.name===t.orig),p=c&&c.cohesion!=null?`${c.cohesive?this._t("lbl.cohesion_good",{pct:Math.round(c.cohesion*100)},"cohesion "+Math.round(c.cohesion*100)+"%"):this._t("lbl.cohesion_low",{pct:Math.round(c.cohesion*100)},"\u26A0 low cohesion "+Math.round(c.cohesion*100)+"%")}`:"";return`

    ${t.orig?this._t("modal.edit_group",{},"Edit profile group"):this._t("modal.new_group",{},"New profile group")}

    +
    ${p}
    ${l} -
    ${this._t("lbl.members",{},"Members")}${n.length?` (${n.length})`:""}
    -
    ${a||`${this._t("msg.no_profiles_yet_short",{},"No profiles yet.")}`}
    -

    ${this._t("msg.group_modal_help",{},"Group programs with the same shape that differ in temperature/spin (durations may vary). Matching scores the group as one candidate, then picks the best-fitting member. Pick at least 2; the overlay shows how alike they are.")}

    +
    ${this._t("lbl.members",{},"Members")}${r.length?` (${r.length})`:""}
    +
    ${i||`${this._t("msg.no_profiles_yet_short",{},"No profiles yet.")}`}
    +

    ${this._t("msg.group_modal_help",{},"Group programs with the same shape that differ in temperature or spin. Matching scores the group as one, then picks the best-fitting member. Pick at least 2; the overlay shows how alike they are.")}

    ${t.orig?``:""} -
    `}_drawGroupCanvas(){const t=this._modal;if(!t||t.type!=="profile-group")return;const e=this._profileEnvCache||{},s=a=>it[Math.max(0,this._profiles.findIndex(o=>o.name===a))%it.length];let r=0;const n=(t.members||[]).filter(a=>e[a]&&(e[a].avg||[]).length).map(a=>{const o=e[a],i=o.avg[o.avg.length-1];return r=Math.max(r,o.target_duration||(i?i[0]:0)),{points:o.avg,stroke:s(a),width:2,alpha:.9,name:a}});n.length&&this._drawCurves("wd-pgroup-canvas",{series:n,xMax:r})}_settingsLevel(){return this._pref("settings_level","basic")==="advanced"?"advanced":"basic"}_settingFieldVisible(t){return this._settingsLevel()==="advanced"||!!t.basic}_secHasBasicFields(t){return(t.fields||(t.groups||[]).flatMap(s=>s.fields||[])).some(s=>s.basic)}_htmlSettings(){const t=this._editedOpts();if(!Object.keys(t).length)return`
    \u2699\uFE0F
    ${this._t("msg.loading_settings",{},"Loading settings\u2026")}
    `;const e=this._suggestionsError?`
    ${this._t("msg.fetch_error",{},"Failed to load data.")}
    `:"",r=this._settingsLevel()==="basic",n=new Set((this._suggestions||[]).map(Z=>Z.key)),a=this._mlSugKeys(t),o=new Set([...n,...a]),i=Z=>(Z.fields||(Z.groups||[]).flatMap(Q=>Q.fields||[])).some(Q=>o.has(Q.key)),l=this._conflictKeysFromOpts(),c=Z=>(Z.fields||(Z.groups||[]).flatMap(Q=>Q.fields||[])).some(Q=>l.has(Q.key)),g=this._opts&&this._opts.device_type||"",p=$t.filter(Z=>!(Z.id==="ml_training"||g&&Z.notDeviceTypes&&Z.notDeviceTypes.includes(g)||g&&Z.onlyDeviceTypes&&!Z.onlyDeviceTypes.includes(g)||r&&!this._secHasBasicFields(Z))),f=(p.find(Z=>Z.id===this._settingsSec)||p[0]||{}).id,u=p.map(Z=>{const lt=i(Z),Q=c(Z);return``}).join(""),$=`
    `}_drawGroupCanvas(){const t=this._modal;if(!t||t.type!=="profile-group")return;const e=this._profileEnvCache||{},s=i=>Q[Math.max(0,this._profiles.findIndex(o=>o.name===i))%Q.length];let a=0;const r=(t.members||[]).filter(i=>e[i]&&(e[i].avg||[]).length).map(i=>{const o=e[i],n=o.avg[o.avg.length-1];return a=Math.max(a,o.target_duration||(n?n[0]:0)),{points:o.avg,stroke:s(i),width:2,alpha:.9,name:i}});r.length&&this._drawCurves("wd-pgroup-canvas",{series:r,xMax:a})}_settingsLevel(){return this._pref("settings_level","basic")==="advanced"?"advanced":"basic"}_settingFieldVisible(t){return t.internal&&!this._pref("show_internal",!1)&&!(this._internalReveal||new Set).has(t.key)?!1:this._settingsLevel()==="advanced"||!!t.basic}_computeInternalReveal(t){const e=new Set,s=this._optDefaults||{},a=this._conflictKeysFromOpts(),r=new Set((this._suggestions||[]).map(i=>i.key));for(const i of yt)for(const o of i.fields||(i.groups||[]).flatMap(n=>n.fields||[])){if(!o.internal)continue;const n=(this._opts||{})[o.key],l=s[o.key]!==void 0?s[o.key]:o.def;(n!=null&&n!==""&&String(n)!==String(l)||a.has(o.key)||r.has(o.key)||Object.prototype.hasOwnProperty.call(this._pendingSettings||{},o.key))&&e.add(o.key)}return e}_secHasBasicFields(t){return(t.fields||(t.groups||[]).flatMap(s=>s.fields||[])).some(s=>s.basic)}_htmlSettings(){const t=this._editedOpts();if(!Object.keys(t).length&&this._tabError==="settings")return``;if(!Object.keys(t).length)return`
    \u2699\uFE0F
    ${this._t("msg.loading_settings",{},"Loading settings\u2026")}
    `;const e=this._suggestionsError?``:"",a=this._settingsLevel()==="basic";this._internalReveal=this._computeInternalReveal(t);const r=new Set((this._suggestions||[]).map(W=>W.key)),i=W=>(W.fields||(W.groups||[]).flatMap(Y=>Y.fields||[])).some(Y=>r.has(Y.key)),o=this._conflictKeysFromOpts(),n=W=>(W.fields||(W.groups||[]).flatMap(Y=>Y.fields||[])).some(Y=>o.has(Y.key)),l=this._opts&&this._opts.device_type||"",c=yt.filter(W=>!(W.id==="ml_training"||l&&W.notDeviceTypes&&W.notDeviceTypes.includes(l)||l&&W.onlyDeviceTypes&&!W.onlyDeviceTypes.includes(l)||a&&!this._secHasBasicFields(W))),p=(c.find(W=>W.id===this._settingsSec)||c[0]||{}).id,_=c.map(W=>{const J=i(W),Y=n(W);return``}).join(""),f=``,S=r?`

    ${this._t("msg.settings_basic_note",{},"Showing essential settings. Switch to Advanced for the full list.")}

    `:"",x=this._busy.has("save-settings"),w=l.size,v=w!==1?"s":"",k=w?` + ${this._t("lbl.settings_advanced",{},"Advanced")} + `,u=a?`

    ${this._t("msg.settings_basic_note",{},"Showing essential settings. Switch to Advanced for the full list.")}

    `:"",b=a?"":``,v=this._busy.has("save-settings"),T=o.size,C=T!==1?"s":"",x=T?`
    - \u26A0 ${this._t("conflict.settings_banner",{n:w},`Setting conflicts: ${w}. Check the highlighted sections and fix them before saving.`)} + \u26A0 ${this._t("conflict.settings_banner",{n:T},`Setting conflicts: ${T}. Check the highlighted sections and fix them before saving.`)} -
    `:"",D=this._suggestions.length,I=[...a].filter(Z=>!n.has(Z)).length,z=D+I,R=this._settingsSugOnly&&!this._settingsSearch,L=D?``:"",P=D?``:"",C=z?R?` +
    `:"",$=this._suggestions.length,L=this._settingsSugOnly&&!this._settingsSearch,D=$?``:"",B=$?``:"",U=$?L?`
    - \u{1F4A1} ${this._t("msg.showing_suggestions",{count:z},`Showing ${z} setting${z>1?"s":""} with suggestions.`)} ${this._t("msg.show_all_settings",{},"Show all settings")}. - ${L} + \u{1F4A1} ${this._t("msg.showing_suggestions",{count:$},`Showing ${$} setting${$>1?"s":""} with suggestions.`)} ${this._t("msg.show_all_settings",{},"Show all settings")}. + ${D}
    `:`
    - \u{1F4A1} ${this._t("msg.tuning_suggestions_available",{count:z},`${z} tuning suggestion${z>1?"s":""} available from observed cycles. They appear beside the relevant fields.`)} + \u{1F4A1} ${this._t("msg.tuning_suggestions_available",{count:$},`${$} tuning suggestion${$>1?"s":""} available from observed cycles. They appear beside the relevant fields.`)} - ${L} - ${P} -
    `:"",A=this._busy.has("sug-analyze"),F=``,j=this._settingsSearch||"",st=j.trim().toLowerCase(),J=``,X=st?this._htmlSettingsSearch(t,st):R?this._htmlSettingsSugOnly(t):this._htmlSettingsSection(t),tt=(this._lockedSuggestions||[]).length,_t=tt?` + ${D} + ${B} +
    `:"",z=this._busy.has("sug-analyze"),M=``,A=this._settingsSearch||"",O=A.trim().toLowerCase(),K=``,I=O?this._htmlSettingsSearch(t,O):L?this._htmlSettingsSugOnly(t):this._htmlSettingsSection(t),R=(this._lockedSuggestions||[]).length,G=R?`
    - \u{1F515} ${this._t("msg.n_suggestions_muted",{count:tt},`${tt} suggestion${tt>1?"s":""} muted; the auto-tuner will not propose these.`)} + \u{1F515} ${this._t("msg.n_suggestions_muted",{count:R},`${R} suggestion${R>1?"s":""} muted; the auto-tuner will not propose these.`)}
    `:"";return` ${e}
    -
    ${this._t("tab.settings",{},"Settings")}${this._mlSettingsLoading?` ${this._t("msg.ml_loading",{},"loading ML\u2026")}`:""}
    - ${F} +
    ${this._t("tab.settings",{},"Settings")}
    + ${M}
    - ${k}${C}${_t}${S} + ${x}${U}${G}${u}
    - ${J} -
    ${u}
    - ${$} + ${K} +
    ${_}
    + ${f} + ${b}
    -
    ${X}
    +
    ${I}
    - +
    -

    ${this._t("msg.saving_triggers_reload",{},"Saving triggers an integration reload. HA entities may briefly show as unavailable.")}

    +

    ${this._t("msg.saving_triggers_reload",{},"Saving reloads the integration. Entities may briefly show as unavailable.")}

    ${this._htmlSettingsHistory()} - `}_htmlSettingsHistory(){const t=this._settingsChangelog||[];if(!t.length)return"";const e=this._settingsHistoryOpen,s=this._canEdit(),r=t.slice(0,100).map(n=>{const a=s?``:"";return` - ${d(this._t("setting."+n.key+".label",{},n.key))} - ${d(Mt(n.old))} \u2192 ${d(Mt(n.new))} - ${ot(n.timestamp)} - ${a} + `}_htmlSettingsHistory(){const t=this._settingsChangelog||[];if(!t.length)return"";const e=this._settingsHistoryOpen,s=this._canEdit(),a=t.slice(0,100).map(r=>{const i=s?``:"";return` + ${d(this._t("setting."+r.key+".label",{},r.key))} + ${d(xt(r.old))} \u2192 ${d(xt(r.new))} + ${et(r.timestamp)} + ${i} `}).join("");return`
    ${this._t("hdr.settings_history",{},"Settings history")} (${t.length})
    @@ -1199,31 +1209,31 @@ button.wd-profile-card { display: block; } ${this._t("lbl.change",{},"Change")} ${this._t("lbl.date",{},"Date")} - ${r}
    `:""} -
    `}_renderField(t,e){if(t.onlyDeviceType&&(e.device_type||"washing_machine")!==t.onlyDeviceType)return"";if(t.type==="storebrand"||t.type==="storemodel")return this._renderStorePicker(t,e);let s=e[t.key];if(s===void 0){const l=this._optDefaults;s=l&&l[t.key]!=null?l[t.key]:t.def}const r={};if(t.type==="devicetype")r.opts=this._deviceTypeOpts(s||e.device_type);else if(t.type==="device")r.opts=this._deviceOpts(s);else if(t.type==="select")r.opts=t.opts||[];else if(t.type==="entity"){const l=this._hass&&this._hass.states?this._hass.states:{},c=Array.isArray(t.domains)?t.domains:t.domain==="binary_sensor"?["binary_sensor","sensor"]:t.domain?[t.domain]:null,g=Object.keys(l).filter(p=>!c||c.some(f=>p.startsWith(f+"."))).filter(p=>!t.notPrice||qt(p,e,this._conflictCtx())==null).sort().slice(0,500);this._entityListCache||(this._entityListCache={}),this._entityListCache[t.key]=g}else if(t.type==="entitylist"){const l=this._hass&&this._hass.states?this._hass.states:{},c=Object.keys(l).filter(g=>!t.domain||g.startsWith(t.domain+".")).sort();if(t.domain==="notify"&&this._hass&&this._hass.services&&this._hass.services.notify){const g=Object.keys(this._hass.services.notify).map(p=>`notify.${p}`);r.entities=[...new Set([...c,...g])].sort().slice(0,500)}else r.entities=c.slice(0,500);this._entityListCache||(this._entityListCache={}),this._entityListCache[t.key]=r.entities}else if(Array.isArray(t.suggestions)&&t.suggestions.length){const l=`wd-dl-${t.key}`;r.datalistId=l,r.datalist=`${t.suggestions.map(c=>``}const n=this._suggestions.find(l=>l.key===t.key);if(n){const l=Object.assign({},n.reason_params||{});n.exclusions&&n.exclusions.total&&(l.excl=this._exclNote(n.exclusions)),r.suggestion={suggested:n.suggested,current:n.current,reason:n.reason,reason_key:n.reason_key,reason_params:l}}const a=(this._mlSettings||{})[t.key];a&&a.ml_value!=null&&!(this._lockedSuggestions||[]).includes(t.key)&&(r.mlSuggestion={value:a.ml_value,reason:a.ml_reason,reason_key:a.ml_reason_key,reason_params:a.ml_reason_params}),r.useBtnLabel=this._t("btn.use",{},"Use"),r.t=this._t.bind(this);const o=(this._settingsChangeByKey||{})[t.key];o&&(r.changed=this._t("msg.setting_changed",{old:Mt(o.old),new:Mt(o.new),date:ot(o.timestamp)},`Changed from ${Mt(o.old)} to ${Mt(o.new)} on ${ot(o.timestamp)}`));const i=Object.assign({},t,{label:this._t("setting."+t.key+".label",{},t.label||""),doc:t.doc!=null?this._t("setting."+t.key+".doc",{},t.doc):t.doc});return Me(i,s,r)}_renderStorePicker(t,e){const s=t.type==="storebrand",r=t.key,n=String(e[r]==null?"":e[r]),a=this._t("setting."+r+".label",{},t.label||""),o=this._t("setting."+r+".doc",{},t.doc||""),i=d(this._t("placeholder."+r,{},s?"e.g. Bosch":"e.g. WAT28660"));return this._onlineEnabled()?s?this._renderBrandPicker(r,n,a,o,i):this._renderModelPicker(r,e,n,a,o,i):`
    - -
    ${d(this._t("msg.store_picker_offline",{},"Enable online features in the settings gear to pick from the community catalog."))}
    `}_statusTag(t){if(t&&t.status==="pending"){const e=d(this._t("badge.awaiting_tip",{},"Awaiting community approval")),s=typeof t.confirmCount=="number"?this._t("badge.awaiting_n",{n:t.confirmCount},`Awaiting approval \xB7 ${t.confirmCount} confirmed`):this._t("badge.awaiting",{},"Awaiting approval");return`${s}`}return t&&t.status==="approved"?`${this._t("badge.approved",{},"Approved")}`:""}_renderBrandPicker(t,e,s,r,n){this._ensureCatalogEntry();const a=Array.isArray(this._catalog.brands)?this._catalog.brands:[];this._entityListCache=this._entityListCache||{},this._entityListCache[t]=a.map(l=>l.brand).filter(Boolean);const o=this._statusTag(this._catalogEntryFor(e,"brand")),i=this._catalog.brands===null?` ${this._t("msg.loading",{},"Loading\u2026")}`:"";return`
    + ${a}
    `:""} +
    `}_renderField(t,e){if(t.onlyDeviceType&&(e.device_type||"washing_machine")!==t.onlyDeviceType)return"";if(t.type==="storebrand"||t.type==="storemodel")return this._renderStorePicker(t,e);let s=e[t.key];if(s===void 0){const n=this._optDefaults;s=n&&n[t.key]!=null?n[t.key]:t.def}const a={};if(t.type==="devicetype")a.opts=this._deviceTypeOpts(s||e.device_type);else if(t.type==="device")a.opts=this._deviceOpts(s);else if(t.type==="select")a.opts=t.opts||[];else if(t.type==="entity"){const n=this._hass&&this._hass.states?this._hass.states:{},l=Array.isArray(t.domains)?t.domains:t.domain==="binary_sensor"?["binary_sensor","sensor"]:t.domain?[t.domain]:null,c=Object.keys(n).filter(p=>!l||l.some(_=>p.startsWith(_+"."))).filter(p=>!t.notPrice||Nt(p,e,this._conflictCtx())==null).sort().slice(0,500);this._entityListCache||(this._entityListCache={}),this._entityListCache[t.key]=c}else if(t.type==="entitylist"){const n=this._hass&&this._hass.states?this._hass.states:{},l=Object.keys(n).filter(c=>!t.domain||c.startsWith(t.domain+".")).sort();if(t.domain==="notify"&&this._hass&&this._hass.services&&this._hass.services.notify){const c=Object.keys(this._hass.services.notify).map(p=>`notify.${p}`);a.entities=[...new Set([...l,...c])].sort().slice(0,500)}else a.entities=l.slice(0,500);this._entityListCache||(this._entityListCache={}),this._entityListCache[t.key]=a.entities}else if(Array.isArray(t.suggestions)&&t.suggestions.length){const n=`wd-dl-${t.key}`;a.datalistId=n,a.datalist=`${t.suggestions.map(l=>``}const r=this._suggestions.find(n=>n.key===t.key);if(r){const n=Object.assign({},r.reason_params||{});r.exclusions&&r.exclusions.total&&(n.excl=this._exclNote(r.exclusions)),a.suggestion={suggested:r.suggested,current:r.current,reason:r.reason,reason_key:r.reason_key,reason_params:n}}a.useBtnLabel=this._t("btn.use",{},"Use"),a.t=this._t.bind(this),a.tText=this._tText.bind(this);const i=(this._settingsChangeByKey||{})[t.key];i&&(a.changed=this._tText("msg.setting_changed",{old:xt(i.old),new:xt(i.new),date:et(i.timestamp)},`Changed from ${xt(i.old)} to ${xt(i.new)} on ${et(i.timestamp)}`));const o=Object.assign({},t,{label:this._t("setting."+t.key+".label",{},t.label||""),doc:t.doc!=null?this._t("setting."+t.key+".doc",{},t.doc):t.doc});return Ge(o,s,a)}_renderStorePicker(t,e){const s=t.type==="storebrand",a=t.key,r=String(e[a]==null?"":e[a]),i=this._t("setting."+a+".label",{},t.label||""),o=this._t("setting."+a+".doc",{},t.doc||""),n=d(this._t("placeholder."+a,{},s?"e.g. Bosch":"e.g. WAT28660"));return this._onlineEnabled()?s?this._renderBrandPicker(a,r,i,o,n):this._renderModelPicker(a,e,r,i,o,n):`
    + +
    ${d(this._t("msg.store_picker_offline",{},"Enable online features in the settings gear to pick from the community catalog."))}
    `}_statusTag(t){if(t&&t.status==="pending"){const e=d(this._t("badge.awaiting_tip",{},"Awaiting community approval")),s=typeof t.confirmCount=="number"?this._t("badge.awaiting_n",{n:t.confirmCount},`Awaiting approval \xB7 ${t.confirmCount} confirmed`):this._t("badge.awaiting",{},"Awaiting approval");return`${s}`}return t&&t.status==="approved"?`${this._t("badge.approved",{},"Approved")}`:""}_renderBrandPicker(t,e,s,a,r){this._ensureCatalogEntry();const i=Array.isArray(this._catalog.brands)?this._catalog.brands:[];this._entityListCache=this._entityListCache||{},this._entityListCache[t]=i.map(l=>l.brand).filter(Boolean);const o=this._statusTag(this._catalogEntryFor(e,"brand")),n=this._catalog.brands===null?` ${this._t("msg.loading",{},"Loading\u2026")}`:"";return`
    - +
    -
    `}_renderModelPicker(t,e,s,r,n,a){const o=String(e.store_brand||"");if(!o)return`
    - -
    ${d(this._t("msg.pick_brand_first",{},"Pick an appliance brand first."))}
    `;this._ensureCatalogEntry(),this._catalog.forBrand!==o&&(this._catalog.forBrand=o,this._catalog.devices=void 0);const i=Array.isArray(this._catalog.devices)?this._catalog.devices:[];this._entityListCache=this._entityListCache||{},this._entityListCache[t]=i.map(u=>u.model).filter(Boolean);const l=this._catalogEntryFor(s,"device"),c=this._statusTag(l),g=this._catalog.devices===null?` ${this._t("msg.loading",{},"Loading\u2026")}`:"";let p="";if(l){const u=[],$=Pe(l.manualUrl);$&&u.push(`${this._t("link.manual",{},"Manual \u2197")}`),(this._constants&&this._constants.storePrefs||{}).show_contributor!==!1&&u.push(this._t("store.contributed_by",{name:d(l.createdByName||this._t("lbl.anonymous",{},"Anonymous"))},`by ${d(l.createdByName||"Anonymous")}`));const S=!!(this._storeStatus&&this._storeStatus.connected);let x="";if(S){const w=[1,2,3,4,5].map(v=>``).join("");x=` - ${w}`}else x=`${this._t("msg.connect_to_confirm",{},"Connect in the settings gear to confirm or rate.")}`;p=`
    ${u.join(" \xB7 ")}
    ${x}
    `}const f=this._onlineEnabled()&&this._storeDeviceDeclared()?`
    - - -
    `:"";return`
    +
    `}_renderModelPicker(t,e,s,a,r,i){const o=String(e.store_brand||"");if(!o)return`
    + +
    ${d(this._t("msg.pick_brand_first",{},"Pick an appliance brand first."))}
    `;this._ensureCatalogEntry(),this._catalog.forBrand!==o&&(this._catalog.forBrand=o,this._catalog.devices=void 0);const n=Array.isArray(this._catalog.devices)?this._catalog.devices:[];this._entityListCache=this._entityListCache||{},this._entityListCache[t]=n.map(u=>u.model).filter(Boolean);const l=this._catalogEntryFor(s,"device"),c=this._statusTag(l),p=this._catalog.devices===null?` ${this._t("msg.loading",{},"Loading\u2026")}`:"";let _="";if(l){const u=[],b=je(l.manualUrl);b&&u.push(`${this._t("link.manual",{},"Manual \u2197")}`),(this._constants&&this._constants.storePrefs||{}).show_contributor!==!1&&u.push(this._t("store.contributed_by",{name:l.createdByName||this._tText("lbl.anonymous",{},"Anonymous")},"by {name}"));const v=!!(this._storeStatus&&this._storeStatus.connected);let T="";if(v){const C=[1,2,3,4,5].map(x=>``).join("");T=` + ${C}`}else T=`${this._t("msg.connect_to_confirm",{},"Connect in the settings gear to confirm or rate.")}`;_=`
    ${u.join(" \xB7 ")}
    ${T}
    `}const f=this._onlineEnabled()&&this._storeDeviceDeclared()?`
    + + +
    `:"";return`
    - +
    - ${p}${f}
    `}_storeApplianceType(){return{washing_machine:"washer",washer:"washer",dryer:"dryer",dishwasher:"dishwasher",washer_dryer:"washer_dryer"}[this._opts.device_type]||""}_storeDeviceDeclared(){return!!((this._opts.store_brand||"").trim()&&(this._opts.store_model||"").trim())}_shareableByProgram(){const t=new Map;for(const s of this._shareableCycles||[]){const r=(s.profile_name||"").trim();r&&(t.has(r)||t.set(r,[]),t.get(r).push(s))}const e=Array.from(t.entries()).map(([s,r])=>({program:s,cycles:r})).sort((s,r)=>s.program.localeCompare(r.program));for(const s of this._shareAllPrograms||[])t.has(s)||e.push({program:s,cycles:[],noCycles:!0});return e}_editedOpts(){return Object.assign({},this._opts,this._pendingSettings)}_catalogEntryKey(){const t=this._editedOpts();return[(t.store_brand||"").trim().toLowerCase(),(t.store_model||"").trim().toLowerCase(),t.device_type||""].join("|")}_ensureCatalogEntry(){const t=this._catalogEntryKey(),e=this._catalogEntry;e&&e.key===t||(this._catalogEntry={key:t,brand:null,device:null,deviceId:null,loading:!0},this._loadCatalogEntry(t))}_catalogEntryFor(t,e){const s=this._catalogEntry;if(!s||s.key!==this._catalogEntryKey())return null;const r=e==="brand"?s.brand:s.device;if(!r)return null;const n=e==="brand"?r.brand:r.model;return String(n||"").toLowerCase()===String(t||"").trim().toLowerCase()?r:null}async _loadCatalogEntry(t){const e=this._devices[this._selIdx],s=this._editedOpts(),r=(s.store_brand||"").trim(),n=(s.store_model||"").trim();if(!e||!this._onlineEnabled()||!r||!n){this._catalogEntry&&this._catalogEntry.key===t&&(this._catalogEntry.loading=!1);return}let a=null;try{a=await this._ws({type:`${y}/store_get_catalog_entry`,entry_id:e.entry_id,brand:r,model:n,appliance_type:s.device_type||""})}catch{}!this._catalogEntry||this._catalogEntry.key!==t||(this._catalogEntry={key:t,loading:!1,brand:a&&a.brand||null,device:a&&a.device||null,deviceId:a&&a.device_id||null},this._isActiveEntry(e.entry_id)&&this._renderPreservingFormEdits())}_refreshComboAfterLoad(t,e,s=!0){const r=this.shadowRoot;for(const n of Array.isArray(t)?t:[t]){const a=r&&r.getElementById(n);if(a&&r.activeElement===a){if(s){a._wdSyntheticInput=!0;try{a.dispatchEvent(new Event("input",{bubbles:!0}))}finally{a._wdSyntheticInput=!1}}return}}this._isActiveEntry(e)&&this._render()}_ensureCatalogList(t,e,s=!1){if(this._onlineEnabled()){if(t==="store_brand"){this._ensureBrandCandidates(e);return}if(t==="store_search"){this._ensureBrandCandidates(e),s&&String(e||"").trim().length>=2&&this._ensureStoreTypeDevices();return}if(t==="store_model"){const r=String(this._editedOpts().store_brand||"").trim();if(!r)return;(this._catalog.forBrand!==r||this._catalog.devices===void 0)&&(this._catalog.forBrand=r,this._catalog.devices=null,this._loadCatalogDevices(r))}}}_ensureBrandCandidates(t){const e=String(t||"").trim().toLowerCase();if(this._catalog.brandsFull)return;if(!e){clearTimeout(this._brandSearchTimer),this._brandSearchTimer=null,this._catalog.brands===void 0&&(this._catalog.brands=null,this._loadCatalogBrands(""));return}(this._catalog.brandPrefixes||(this._catalog.brandPrefixes=[])).some(r=>e.startsWith(r))||(clearTimeout(this._brandSearchTimer),this._brandSearchTimer=setTimeout(()=>this._loadCatalogBrands(e),250))}_mergeBrandCandidates(t){const e=new Map((Array.isArray(this._catalog.brands)?this._catalog.brands:[]).map(s=>[String(s.id!=null?s.id:s.brand),s]));for(const s of t||[])e.set(String(s.id!=null?s.id:s.brand),s);this._catalog.brands=Array.from(e.values()).sort((s,r)=>String(s.brand||"").localeCompare(String(r.brand||"")))}async _loadCatalogBrands(t=""){this._entityListCache=this._entityListCache||{};let e=!1;const s=this._devices[this._selIdx];if(!s||!this._onlineEnabled()){this._catalog.brands=[],this._entityListCache.store_brand=[];return}if(!(t&&this._catalog.brandsFull)){this._catalog.brands===void 0&&(this._catalog.brands=null);try{const r=await this._ws({type:`${y}/store_list_brands`,entry_id:s.entry_id,query:t||null,include_pending:!this._catalog.approvedOnly}),n=r&&r.items||[];t?(this._mergeBrandCandidates(n),(this._catalog.brandPrefixes||(this._catalog.brandPrefixes=[])).push(t)):(this._catalog.brands=n,this._catalog.brandsFull=!0)}catch{Array.isArray(this._catalog.brands)||(this._catalog.brands=[]),e=!0}this._entityListCache.store_brand=(this._catalog.brands||[]).map(r=>r.brand).filter(Boolean),this._syncStoreSearchCandidates(),this._refreshComboAfterLoad(["wd-store-brand","wd-store-q"],s.entry_id,!e)}}_syncStoreSearchCandidates(){this._entityListCache=this._entityListCache||{};const t=new Map,e=[];for(const r of this._catalog.typeDevices||[]){const n=String(r&&r.model||"").trim();if(!n)continue;const a=r.brand?`${n} \xB7 ${r.brand}`:n;t.has(a)||(t.set(a,r),e.push(a))}this._catalog.modelIndex=t;const s=(this._catalog.brands||[]).map(r=>r.brand).filter(Boolean);this._entityListCache.store_search=s.concat(e)}_dropModelCandidates(){this._catalog.typeDevices=void 0,this._catalog.typeDevicesFor=null,this._catalog.modelIndex=new Map,this._entityListCache&&delete this._entityListCache.store_search}_ensureStoreTypeDevices(){const t=this._devices[this._selIdx];if(!t||!this._onlineEnabled())return;const e=this._storeApplianceType();this._catalog.typeDevicesFor===e&&this._catalog.typeDevices!==void 0||(this._catalog.typeDevicesFor=e,this._catalog.typeDevices=null,this._ws({type:`${y}/store_search_devices`,entry_id:t.entry_id,query:null,appliance_type:e,include_pending:!0}).then(s=>{this._catalog.typeDevicesFor===e&&(this._catalog.typeDevices=s&&s.items||[])}).catch(()=>{this._catalog.typeDevicesFor===e&&(this._catalog.typeDevices=[])}).finally(()=>{this._syncStoreSearchCandidates(),this._refreshComboAfterLoad(["wd-store-q"],t.entry_id,!!(this._catalog.typeDevices||[]).length)}))}async _loadCatalogDevices(t){this._entityListCache=this._entityListCache||{};const e=this._devices[this._selIdx];if(!e||!this._onlineEnabled()||!t){this._catalog.devices=[],this._entityListCache.store_model=[];return}try{const s=await this._ws({type:`${y}/store_search_devices`,entry_id:e.entry_id,query:t,appliance_type:this._storeApplianceType(),include_pending:!this._catalog.approvedOnly});this._catalog.forBrand===t&&(this._catalog.devices=s&&s.items||[])}catch{this._catalog.forBrand===t&&(this._catalog.devices=[])}this._entityListCache.store_model=(this._catalog.devices||[]).map(s=>s.model).filter(Boolean),this._refreshComboAfterLoad("wd-store-model",e.entry_id)}async _loadShareProfiles(){const t=this._devices[this._selIdx],e=this._modal;if(!t||!e||e.type!=="store-share")return;const s=(this._opts.store_brand||"").trim(),r=(this._opts.store_model||"").trim();if(!s||!r){e.profiles=[],this._modal===e&&this._render();return}try{const n=await this._ws({type:`${y}/store_get_device_profiles`,entry_id:t.entry_id,brand:s,model:r,appliance_type:this._opts.device_type||""});if(this._modal!==e)return;e.profiles=n&&n.items||[],e.deviceId=n&&n.device_id||null}catch{this._modal===e&&(e.profiles=[])}this._modal===e&&this._render()}_htmlAutomations(){const t=this._deviceAutomations||[],e=t.length?t.map(n=>`\u{1F517} ${d(n.name)}${n.enabled?"":' (off)'}`).join(""):`${this._autoLoading?this._t("msg.loading",{},"Loading\u2026"):this._t("hdr.no_automations",{},"No automations reference this device yet.")}`,s=Array.isArray(this._opts.notify_actions)?this._opts.notify_actions:[],r=s.length?` + ${_}${f}
    `}_storeApplianceType(){return{washing_machine:"washer",washer:"washer",dryer:"dryer",dishwasher:"dishwasher",washer_dryer:"washer_dryer"}[this._opts.device_type]||""}_storeDeviceDeclared(){return!!((this._opts.store_brand||"").trim()&&(this._opts.store_model||"").trim())}_shareableByProgram(){const t=new Map;for(const s of this._shareableCycles||[]){const a=(s.profile_name||"").trim();a&&(t.has(a)||t.set(a,[]),t.get(a).push(s))}const e=Array.from(t.entries()).map(([s,a])=>({program:s,cycles:a})).sort((s,a)=>s.program.localeCompare(a.program));for(const s of this._shareAllPrograms||[])t.has(s)||e.push({program:s,cycles:[],noCycles:!0});return e}_editedOpts(){return Object.assign({},this._opts,this._pendingSettings)}_catalogEntryKey(){const t=this._editedOpts();return[(t.store_brand||"").trim().toLowerCase(),(t.store_model||"").trim().toLowerCase(),t.device_type||""].join("|")}_ensureCatalogEntry(){const t=this._catalogEntryKey(),e=this._catalogEntry;e&&e.key===t||(this._catalogEntry={key:t,brand:null,device:null,deviceId:null,loading:!0},this._loadCatalogEntry(t))}_catalogEntryFor(t,e){const s=this._catalogEntry;if(!s||s.key!==this._catalogEntryKey())return null;const a=e==="brand"?s.brand:s.device;if(!a)return null;const r=e==="brand"?a.brand:a.model;return String(r||"").toLowerCase()===String(t||"").trim().toLowerCase()?a:null}async _loadCatalogEntry(t){const e=this._devices[this._selIdx],s=this._editedOpts(),a=(s.store_brand||"").trim(),r=(s.store_model||"").trim();if(!e||!this._onlineEnabled()||!a||!r){this._catalogEntry&&this._catalogEntry.key===t&&(this._catalogEntry.loading=!1);return}let i=null;try{i=await this._ws({type:`${w}/store_get_catalog_entry`,entry_id:e.entry_id,brand:a,model:r,appliance_type:s.device_type||""})}catch{}!this._catalogEntry||this._catalogEntry.key!==t||(this._catalogEntry={key:t,loading:!1,brand:i&&i.brand||null,device:i&&i.device||null,deviceId:i&&i.device_id||null},this._isActiveEntry(e.entry_id)&&this._renderPreservingFormEdits())}_refreshComboAfterLoad(t,e,s=!0){const a=this.shadowRoot;for(const r of Array.isArray(t)?t:[t]){const i=a&&a.getElementById(r);if(i&&a.activeElement===i){if(s){i._wdSyntheticInput=!0;try{i.dispatchEvent(new Event("input",{bubbles:!0}))}finally{i._wdSyntheticInput=!1}}return}}this._isActiveEntry(e)&&this._render()}_ensureCatalogList(t,e,s=!1){if(this._onlineEnabled()){if(t==="store_brand"){this._ensureBrandCandidates(e);return}if(t==="store_search"){this._ensureBrandCandidates(e),s&&String(e||"").trim().length>=2&&this._ensureStoreTypeDevices();return}if(t==="store_model"){const a=String(this._editedOpts().store_brand||"").trim();if(!a)return;(this._catalog.forBrand!==a||this._catalog.devices===void 0)&&(this._catalog.forBrand=a,this._catalog.devices=null,this._loadCatalogDevices(a))}}}_ensureBrandCandidates(t){const e=String(t||"").trim().toLowerCase();if(this._catalog.brandsFull)return;if(!e){clearTimeout(this._brandSearchTimer),this._brandSearchTimer=null,this._catalog.brands===void 0&&(this._catalog.brands=null,this._loadCatalogBrands(""));return}(this._catalog.brandPrefixes||(this._catalog.brandPrefixes=[])).some(a=>e.startsWith(a))||(clearTimeout(this._brandSearchTimer),this._brandSearchTimer=setTimeout(()=>this._loadCatalogBrands(e),250))}_mergeBrandCandidates(t){const e=new Map((Array.isArray(this._catalog.brands)?this._catalog.brands:[]).map(s=>[String(s.id!=null?s.id:s.brand),s]));for(const s of t||[])e.set(String(s.id!=null?s.id:s.brand),s);this._catalog.brands=Array.from(e.values()).sort((s,a)=>String(s.brand||"").localeCompare(String(a.brand||"")))}async _loadCatalogBrands(t=""){this._entityListCache=this._entityListCache||{};let e=!1;const s=this._devices[this._selIdx];if(!s||!this._onlineEnabled()){this._catalog.brands=[],this._entityListCache.store_brand=[];return}if(!(t&&this._catalog.brandsFull)){this._catalog.brands===void 0&&(this._catalog.brands=null);try{const a=await this._ws({type:`${w}/store_list_brands`,entry_id:s.entry_id,query:t||null,include_pending:!this._catalog.approvedOnly}),r=a&&a.items||[];t?(this._mergeBrandCandidates(r),(this._catalog.brandPrefixes||(this._catalog.brandPrefixes=[])).push(t)):(this._catalog.brands=r,this._catalog.brandsFull=!0)}catch{Array.isArray(this._catalog.brands)||(this._catalog.brands=[]),e=!0}this._entityListCache.store_brand=(this._catalog.brands||[]).map(a=>a.brand).filter(Boolean),this._syncStoreSearchCandidates(),this._refreshComboAfterLoad(["wd-store-brand","wd-store-q"],s.entry_id,!e)}}_syncStoreSearchCandidates(){this._entityListCache=this._entityListCache||{};const t=new Map,e=[];for(const a of this._catalog.typeDevices||[]){const r=String(a&&a.model||"").trim();if(!r)continue;const i=a.brand?`${r} \xB7 ${a.brand}`:r;t.has(i)||(t.set(i,a),e.push(i))}this._catalog.modelIndex=t;const s=(this._catalog.brands||[]).map(a=>a.brand).filter(Boolean);this._entityListCache.store_search=s.concat(e)}_dropModelCandidates(){this._catalog.typeDevices=void 0,this._catalog.typeDevicesFor=null,this._catalog.modelIndex=new Map,this._entityListCache&&delete this._entityListCache.store_search}_ensureStoreTypeDevices(){const t=this._devices[this._selIdx];if(!t||!this._onlineEnabled())return;const e=this._storeApplianceType();this._catalog.typeDevicesFor===e&&this._catalog.typeDevices!==void 0||(this._catalog.typeDevicesFor=e,this._catalog.typeDevices=null,this._ws({type:`${w}/store_search_devices`,entry_id:t.entry_id,query:null,appliance_type:e,include_pending:!0}).then(s=>{this._catalog.typeDevicesFor===e&&(this._catalog.typeDevices=s&&s.items||[])}).catch(()=>{this._catalog.typeDevicesFor===e&&(this._catalog.typeDevices=[])}).finally(()=>{this._syncStoreSearchCandidates(),this._refreshComboAfterLoad(["wd-store-q"],t.entry_id,!!(this._catalog.typeDevices||[]).length)}))}async _loadCatalogDevices(t){this._entityListCache=this._entityListCache||{};const e=this._devices[this._selIdx];if(!e||!this._onlineEnabled()||!t){this._catalog.devices=[],this._entityListCache.store_model=[];return}try{const s=await this._ws({type:`${w}/store_search_devices`,entry_id:e.entry_id,query:t,appliance_type:this._storeApplianceType(),include_pending:!this._catalog.approvedOnly});this._catalog.forBrand===t&&(this._catalog.devices=s&&s.items||[])}catch{this._catalog.forBrand===t&&(this._catalog.devices=[])}this._entityListCache.store_model=(this._catalog.devices||[]).map(s=>s.model).filter(Boolean),this._refreshComboAfterLoad("wd-store-model",e.entry_id)}async _loadShareProfiles(){const t=this._devices[this._selIdx],e=this._modal;if(!t||!e||e.type!=="store-share")return;const s=(this._opts.store_brand||"").trim(),a=(this._opts.store_model||"").trim();if(!s||!a){e.profiles=[],this._modal===e&&this._render();return}try{const r=await this._ws({type:`${w}/store_get_device_profiles`,entry_id:t.entry_id,brand:s,model:a,appliance_type:this._opts.device_type||""});if(this._modal!==e)return;e.profiles=r&&r.items||[],e.deviceId=r&&r.device_id||null,e.storeUnreachable=!!(r&&r.error==="store_unreachable")}catch{this._modal===e&&(e.profiles=[])}this._modal===e&&this._render()}_htmlAutomations(){const t=this._deviceAutomations||[],e=t.length?t.map(r=>`\u{1F517} ${d(r.name)}${r.enabled?"":` ${this._t("lbl.automation_off",{},"(off)")}`}`).join(""):`${this._autoLoading?this._t("msg.loading",{},"Loading\u2026"):this._t("hdr.no_automations",{},"No automations reference this device yet.")}`,s=Array.isArray(this._opts.notify_actions)?this._opts.notify_actions:[],a=s.length?`
    ${this._t("msg.legacy_actions_title",{count:s.length},`${s.length} legacy custom action${s.length>1?"s":""} still running`)}

    ${this._t("msg.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.")}

    @@ -1233,8 +1243,8 @@ button.wd-profile-card { display: block; }
    `:"";return`
    ${this._t("hdr.automations",{},"Automations")}
    -

    ${this._t("msg.automations_intro",{start:"ha_washdata_cycle_started",end:"ha_washdata_cycle_ended"},"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.")}

    - ${r} +

    ${this._t("msg.automations_intro",{start:vt("ha_washdata_cycle_started"),end:vt("ha_washdata_cycle_ended")},"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.")}

    + ${a}
    ${e}
    @@ -1245,238 +1255,206 @@ button.wd-profile-card { display: block; }
    -
    `}async _loadDeviceAutomations(t){this._deviceAutomations=[];const e=this._hass;if(!(!e||!e.callWS))try{let s=null;const r=e.devices||{};for(const i of Object.values(r))if((i.config_entries||[]).includes(t)){s=i.id;break}const n=s?await e.callWS({type:"search/related",item_type:"device",item_id:s}):await e.callWS({type:"search/related",item_type:"config_entry",item_id:t});if(!this._isActiveEntry(t))return;const a=n&&n.automation||[],o=e.states||{};this._deviceAutomations=a.map(i=>{const l=o[i]&&o[i].attributes||{};return{entity_id:i,id:l.id,name:l.friendly_name||i,enabled:o[i]?o[i].state==="on":!0}}).filter(i=>i.id)}catch{this._isActiveEntry(t)&&(this._deviceAutomations=[])}}_navigate(t){try{history.pushState(null,"",t),this.dispatchEvent(new CustomEvent("location-changed",{bubbles:!0,composed:!0,detail:{replace:!1}}))}catch{try{window.location.assign(t)}catch{}}}async _newAutomationFromEvent(t){const e=this._devices[this._selIdx];if(!e)return;const s=this._hass,r=t==="started"?"ha_washdata_cycle_started":"ha_washdata_cycle_ended",n=t==="started"?"started":"finished",a={alias:`${e.title||"WashData"}: cycle ${n}`,description:`Runs when the WashData ${e.title||""} cycle ${n}. Add your actions (notify, lights, ...).`,mode:"single",trigger:[{platform:"event",event_type:r,event_data:{entry_id:e.entry_id}}],condition:[],action:[]},o="washdata_"+Date.now().toString(36);try{s&&s.callApi?(await s.callApi("POST","config/automation/config/"+o,a),this._navigate("/config/automation/edit/"+o)):this._navigate("/config/automation/edit/new")}catch(i){this._showToast(this._t("msg.toast_automation_failed",{error:i.message||i},"Could not create automation: "+(i.message||i)),"error")}}async _convertLegacyActions(){const t=this._devices[this._selIdx],e=this._hass,s=Array.isArray(this._opts.notify_actions)?this._opts.notify_actions:[];if(!t||!s.length)return;const r={alias:`${t.title||"WashData"}: migrated custom actions`,description:"Migrated from WashData legacy custom actions (which ran on cycle start, finish and live). Trim the triggers as needed. Note: old {device}/{duration}-style placeholders are NOT templated here - replace them with Jinja templates such as {{ trigger.event.data.device_name }}.",mode:"single",trigger:[{platform:"event",event_type:"ha_washdata_cycle_started",event_data:{entry_id:t.entry_id}},{platform:"event",event_type:"ha_washdata_cycle_ended",event_data:{entry_id:t.entry_id}}],condition:[],action:s},n="washdata_"+Date.now().toString(36);if(!e||!e.callApi){this._showToast(this._t("msg.toast_no_automation",{},"Cannot create automation here"),"error");return}let a=!1;try{await e.callApi("POST","config/automation/config/"+n,r),a=!0,await this._ws({type:`${y}/set_options`,entry_id:t.entry_id,options:{notify_actions:[]}}),this._isActiveEntry(t.entry_id)&&(this._opts={...this._opts,notify_actions:[]}),this._showToast(this._t("msg.toast_automation_migrated",{},"Actions migrated to an automation; opening editor")),this._navigate("/config/automation/edit/"+n)}catch(o){const i=o.message||o;if(!a){this._showToast(this._t("msg.toast_convert_failed",{error:i},"Convert failed: "+i),"error");return}let l=null;try{const c=await this._ws({type:`${y}/get_options`,entry_id:t.entry_id}),g=c&&c.options||{},p=Array.isArray(g.notify_actions)?g.notify_actions:[];l=p.length>0,this._isActiveEntry(t.entry_id)&&(this._opts={...this._opts,notify_actions:p})}catch{l=null}if(l===!1){this._showToast(this._t("msg.toast_automation_migrated",{},"Actions migrated to an automation; opening editor")),this._navigate("/config/automation/edit/"+n);return}if(l===!0){try{await e.callApi("DELETE","config/automation/config/"+n),this._showToast(this._t("msg.toast_convert_rolled_back",{error:i},"Migration failed and was rolled back (no automation left behind): "+i),"error")}catch{this._showToast(this._t("msg.toast_convert_orphan",{},"The automation was created, but clearing the old actions failed. Do not retry: remove the legacy actions manually to avoid a duplicate automation."),"error")}return}this._showToast(this._t("msg.toast_convert_orphan",{},"The automation was created, but clearing the old actions failed. Do not retry: remove the legacy actions manually to avoid a duplicate automation."),"error")}}_htmlSettingsSection(t){const e=this._opts&&this._opts.device_type||"",s=this._settingsLevel()==="basic",r=l=>!(l.id==="ml_training"||e&&l.notDeviceTypes&&l.notDeviceTypes.includes(e)||e&&l.onlyDeviceTypes&&!l.onlyDeviceTypes.includes(e)||s&&!this._secHasBasicFields(l)),n=$t.find(l=>l.id===this._settingsSec&&r(l))||$t.find(l=>r(l))||$t[0],a=n.intro||this._t("section."+n.id+".intro",{},"")?`

    ${d(this._t("section."+n.id+".intro",{},n.intro||""))}

    `:"",o="";if(n.id==="notifications"){const l=`

    ${this._t("msg.notify_services_hint",{entity:"notify.<name>",vars:""+d(Ct)+""},"Use {entity} service IDs (comma-separated for multiple). Template variables: {vars}.")}

    `,c=n.groups.map(p=>{const f=(p.fields||[]).filter(u=>this._settingFieldVisible(u)).map(u=>this._renderField(u,t)).filter(Boolean).join("");return f?`
    ${d(this._t("setting_group."+de(p.sub)+".label",{},p.sub))}
    ${f}
    `:""}).join("");return`${s?"":this._htmlAutomations()}${l}${c}`}if(n.groups){const l=n.groups.map(c=>{const g=c.sub?`
    ${d(this._t("setting_group."+de(c.sub)+".label",{},c.sub))}
    `:"",p=(c.fields||[]).filter(f=>this._settingFieldVisible(f)).map(f=>this._renderField(f,t)).filter(Boolean).join("");return p?`${g}
    ${p}
    `:""}).join("");return`${a}${o}${l}`}const i=(n.fields||[]).filter(l=>this._settingFieldVisible(l)).map(l=>this._renderField(l,t)).filter(Boolean).join("");return`${a}${o}
    ${i}
    `}_htmlSettingsSearch(t,e){const s=this._opts&&this._opts.device_type||"",r=$t.filter(i=>!(i.id==="ml_training"||s&&i.notDeviceTypes&&i.notDeviceTypes.includes(s)||s&&i.onlyDeviceTypes&&!i.onlyDeviceTypes.includes(s))),n=i=>`${i.label||""} ${i.key||""} ${i.doc||""} ${i.hint||""}`.toLowerCase().includes(e);let a="",o=0;for(const i of r){const c=(i.fields||(i.groups||[]).flatMap(p=>p.fields||[])).filter(n);if(!c.length)continue;const g=c.map(p=>this._renderField(p,t)).filter(Boolean).join("");g&&(o+=c.length,a+=`
    ${d(this._t("section."+i.id+".label",{},i.label))}
    ${g}
    `)}return o?a:`

    ${this._t("msg.no_settings_match",{q:e},`No settings match "${d(e)}"`)}

    `}_mlSugKeys(t){const e=t||Object.assign({},this._optDefaults,this._opts,this._pendingSettings||{});return this._mlSugKeysFrom(this._mlSettings,e,this._lockedSuggestions)}_mlSugKeysFrom(t,e,s){const r=new Set(s||[]),n=e||{},a=new Set;for(const[o,i]of Object.entries(t||{}))r.has(o)||i&&i.ml_value!=null&&!Xt(i.ml_value,n[o])&&a.add(o);return a}_sugCountsForDevice(t){if(!t)return{classic:0,ml:0,total:0};const e=this._devices[this._selIdx],s=!!(e&&e.entry_id===t.entry_id),r=Array.isArray(t.suggestion_keys)?t.suggestion_keys:null,n=r?r.length:t.suggestions_count||0,a=s?Object.assign({},t.option_defaults||{},t.options||{},this._opts,this._pendingSettings||{}):Object.assign({},t.option_defaults||{},t.options||{}),o=this._mlSugKeysFrom(s?this._mlSettings:this._mlSettingsByEntry[t.entry_id],a,s?this._lockedSuggestions:this._lockedByEntry[t.entry_id]),i=r?[...o].filter(l=>!r.includes(l)).length:o.size;return{classic:n,ml:i,total:n+i}}_htmlSettingsSugOnly(t){const e=new Set((this._suggestions||[]).map(a=>a.key));for(const a of this._mlSugKeys(t))e.add(a);if(!e.size)return`

    ${this._t("msg.no_suggestions",{},"No active suggestions.")}

    `;const s=this._opts&&this._opts.device_type||"",r=$t.filter(a=>!(a.id==="ml_training"||s&&a.notDeviceTypes&&a.notDeviceTypes.includes(s)||s&&a.onlyDeviceTypes&&!a.onlyDeviceTypes.includes(s)));let n="";for(const a of r){const i=(a.fields||(a.groups||[]).flatMap(c=>c.fields||[])).filter(c=>e.has(c.key));if(!i.length)continue;const l=i.map(c=>this._renderField(c,t)).filter(Boolean).join("");l&&(n+=`
    ${d(this._t("section."+a.id+".label",{},a.label))}
    ${l}
    `)}return n||`

    ${this._t("msg.no_suggestions",{},"No active suggestions.")}

    `}_htmlMlTab(){const t=this._opts;if(!Object.keys(t).length)return`
    \u{1F916}
    ${this._t("msg.loading",{},"Loading\u2026")}
    `;const e=this._mlTrainingStatus,s=this._devices[this._selIdx],r=s&&s.entry_id,n=$t.find(i=>i.id==="ml_training"),a=n?(n.fields||[]).map(i=>this._renderField(i,t)).filter(Boolean).join(""):"",o=this._busy.has("save-settings");return` +
    `}async _loadDeviceAutomations(t){this._deviceAutomations=[];const e=this._hass;if(!(!e||!e.callWS))try{let s=null;const a=e.devices||{};for(const n of Object.values(a))if((n.config_entries||[]).includes(t)){s=n.id;break}const r=s?await e.callWS({type:"search/related",item_type:"device",item_id:s}):await e.callWS({type:"search/related",item_type:"config_entry",item_id:t});if(!this._isActiveEntry(t))return;const i=r&&r.automation||[],o=e.states||{};this._deviceAutomations=i.map(n=>{const l=o[n]&&o[n].attributes||{};return{entity_id:n,id:l.id,name:l.friendly_name||n,enabled:o[n]?o[n].state==="on":!0}}).filter(n=>n.id)}catch{this._isActiveEntry(t)&&(this._deviceAutomations=[])}}_navigate(t){try{history.pushState(null,"",t),this.dispatchEvent(new CustomEvent("location-changed",{bubbles:!0,composed:!0,detail:{replace:!1}}))}catch{try{window.location.assign(t)}catch{}}}async _newAutomationFromEvent(t){const e=this._devices[this._selIdx];if(!e)return;const s=this._hass,a=t==="started"?"ha_washdata_cycle_started":"ha_washdata_cycle_ended",r=e.title||"WashData",i={alias:t==="started"?this._tText("msg.automation_alias_started",{device:r},"{device}: cycle started"):this._tText("msg.automation_alias_finished",{device:r},"{device}: cycle finished"),description:t==="started"?this._tText("msg.automation_desc_started",{device:r},"Runs when a {device} cycle starts. Add your actions (notify, lights, ...)."):this._tText("msg.automation_desc_finished",{device:r},"Runs when a {device} cycle finishes. Add your actions (notify, lights, ...)."),mode:"single",trigger:[{platform:"event",event_type:a,event_data:{entry_id:e.entry_id}}],condition:[],action:[]},o="washdata_"+Date.now().toString(36);try{s&&s.callApi?(await s.callApi("POST","config/automation/config/"+o,i),this._navigate("/config/automation/edit/"+o)):this._navigate("/config/automation/edit/new")}catch(n){this._showToast(this._tText("msg.toast_automation_failed",{error:n.message||n},"Could not create automation: "+(n.message||n)),"error")}}async _convertLegacyActions(){const t=this._devices[this._selIdx],e=this._hass,s=Array.isArray(this._opts.notify_actions)?this._opts.notify_actions:[];if(!t||!s.length)return;const a={alias:`${t.title||"WashData"}: migrated custom actions`,description:"Migrated from WashData legacy custom actions (which ran on cycle start, finish and live). Trim the triggers as needed. Note: old {device}/{duration}-style placeholders are NOT templated here - replace them with Jinja templates such as {{ trigger.event.data.device_name }}.",mode:"single",trigger:[{platform:"event",event_type:"ha_washdata_cycle_started",event_data:{entry_id:t.entry_id}},{platform:"event",event_type:"ha_washdata_cycle_ended",event_data:{entry_id:t.entry_id}}],condition:[],action:s},r="washdata_"+Date.now().toString(36);if(!e||!e.callApi){this._showToast(this._t("msg.toast_no_automation",{},"Cannot create automation here"),"error");return}let i=!1;try{await e.callApi("POST","config/automation/config/"+r,a),i=!0,await this._ws({type:`${w}/set_options`,entry_id:t.entry_id,options:{notify_actions:[]}}),this._isActiveEntry(t.entry_id)&&(this._opts={...this._opts,notify_actions:[]}),this._showToast(this._t("msg.toast_automation_migrated",{},"Actions migrated to an automation; opening editor")),this._navigate("/config/automation/edit/"+r)}catch(o){const n=o.message||o;if(!i){this._showToast(this._tText("msg.toast_convert_failed",{error:n},"Convert failed: "+n),"error");return}let l=null;try{const c=await this._ws({type:`${w}/get_options`,entry_id:t.entry_id}),p=c&&c.options||{},_=Array.isArray(p.notify_actions)?p.notify_actions:[];l=_.length>0,this._isActiveEntry(t.entry_id)&&(this._opts={...this._opts,notify_actions:_})}catch{l=null}if(l===!1){this._showToast(this._t("msg.toast_automation_migrated",{},"Actions migrated to an automation; opening editor")),this._navigate("/config/automation/edit/"+r);return}if(l===!0){try{await e.callApi("DELETE","config/automation/config/"+r),this._showToast(this._tText("msg.toast_convert_rolled_back",{error:n},"Migration failed and was rolled back (no automation left behind): "+n),"error")}catch{this._showToast(this._t("msg.toast_convert_orphan",{},"The automation was created, but clearing the old actions failed. Do not retry: remove the legacy actions manually to avoid a duplicate automation."),"error")}return}this._showToast(this._t("msg.toast_convert_orphan",{},"The automation was created, but clearing the old actions failed. Do not retry: remove the legacy actions manually to avoid a duplicate automation."),"error")}}_htmlSettingsSection(t){const e=this._opts&&this._opts.device_type||"",s=this._settingsLevel()==="basic",a=l=>!(l.id==="ml_training"||e&&l.notDeviceTypes&&l.notDeviceTypes.includes(e)||e&&l.onlyDeviceTypes&&!l.onlyDeviceTypes.includes(e)||s&&!this._secHasBasicFields(l)),r=yt.find(l=>l.id===this._settingsSec&&a(l))||yt.find(l=>a(l))||yt[0],i=r.intro||this._t("section."+r.id+".intro",{},"")?`

    ${d(this._t("section."+r.id+".intro",{},r.intro||""))}

    `:"",o="";if(r.id==="notifications"){const l=`

    ${this._t("msg.notify_services_hint",{entity:vt("notify.<name>"),vars:vt(""+d(Pe)+"")},"Use {entity} service IDs (comma-separated for multiple). Template variables: {vars}.")}

    `,c=r.groups.map(_=>{const f=(_.fields||[]).filter(u=>this._settingFieldVisible(u)).map(u=>this._renderField(u,t)).filter(Boolean).join("");return f?`
    ${d(this._t("setting_group."+ge(_.sub)+".label",{},_.sub))}
    ${f}
    `:""}).join("");return`${s?"":this._htmlAutomations()}${l}${c}`}if(r.groups){const l=r.groups.map(c=>{const p=c.sub?`
    ${d(this._t("setting_group."+ge(c.sub)+".label",{},c.sub))}
    `:"",_=(c.fields||[]).filter(f=>this._settingFieldVisible(f)).map(f=>this._renderField(f,t)).filter(Boolean).join("");return _?`${p}
    ${_}
    `:""}).join("");return`${i}${o}${l}`}const n=(r.fields||[]).filter(l=>this._settingFieldVisible(l)).map(l=>this._renderField(l,t)).filter(Boolean).join("");return`${i}${o}
    ${n}
    `}_htmlSettingsSearch(t,e){const s=this._opts&&this._opts.device_type||"",a=yt.filter(n=>!(n.id==="ml_training"||s&&n.notDeviceTypes&&n.notDeviceTypes.includes(s)||s&&n.onlyDeviceTypes&&!n.onlyDeviceTypes.includes(s))),r=n=>{const l=n.key?`${this._tText("setting."+n.key+".label",{},"")} ${n.doc!=null?this._tText("setting."+n.key+".doc",{},""):""}`:"";return`${n.label||""} ${n.key||""} ${n.doc||""} ${n.hint||""} ${l}`.toLowerCase().includes(e)};let i="",o=0;for(const n of a){const c=(n.fields||(n.groups||[]).flatMap(_=>_.fields||[])).filter(r);if(!c.length)continue;const p=c.map(_=>this._renderField(_,t)).filter(Boolean).join("");p&&(o+=c.length,i+=`
    ${d(this._t("section."+n.id+".label",{},n.label))}
    ${p}
    `)}return o?i:`

    ${this._t("msg.no_settings_match",{q:e},'No settings match "{q}"')}

    `}_sugCountForDevice(t){return t?Array.isArray(t.suggestion_keys)?t.suggestion_keys.length:t.suggestions_count||0:0}_htmlSettingsSugOnly(t){const e=new Set((this._suggestions||[]).map(i=>i.key));if(!e.size)return`

    ${this._t("msg.no_suggestions",{},"No active suggestions.")}

    `;const s=this._opts&&this._opts.device_type||"",a=yt.filter(i=>!(i.id==="ml_training"||s&&i.notDeviceTypes&&i.notDeviceTypes.includes(s)||s&&i.onlyDeviceTypes&&!i.onlyDeviceTypes.includes(s)));let r="";for(const i of a){const n=(i.fields||(i.groups||[]).flatMap(c=>c.fields||[])).filter(c=>e.has(c.key));if(!n.length)continue;const l=n.map(c=>this._renderField(c,t)).filter(Boolean).join("");l&&(r+=`
    ${d(this._t("section."+i.id+".label",{},i.label))}
    ${l}
    `)}return r||`

    ${this._t("msg.no_suggestions",{},"No active suggestions.")}

    `}_htmlMlTab(){const t=this._opts;if(!Object.keys(t).length)return`
    \u{1F916}
    ${this._t("msg.loading",{},"Loading\u2026")}
    `;const e=this._mlTrainingStatus,s=this._devices[this._selIdx],a=s&&s.entry_id,r=yt.find(n=>n.id==="ml_training"),i=r?(r.fields||[]).map(n=>this._renderField(n,t)).filter(Boolean).join(""):"",o=this._busy.has("save-settings");return`
    ${this._t("hdr.ml_smart_learning",{},"Smart Learning")}

    ${this._t("msg.ml_intro",{},"WashData ships with smart models that work out of the box.")}

    - ${this._htmlMlStatusSection(e,r)} + ${this._htmlMlStatusSection(e,a)}
    ${this._t("hdr.ml_settings_card",{},"Settings")}

    ${this._t("msg.ml_settings_intro",{},"Two independent switches: one applies the models while a cycle runs, the other lets WashData fine-tune them to your machine over time.")}

    -
    ${a}
    +
    ${i}
    -

    ${this._t("msg.saving_triggers_reload",{},"Saving triggers an integration reload.")}

    +

    ${this._t("msg.saving_triggers_reload",{},"Saving reloads the integration. Entities may briefly show as unavailable.")}

    ${this._htmlMlLearnedSection(e)} - ${this._htmlMatchingTuningCard()} - `}_htmlMlStatusSection(t,e){const s=e&&this._busy.has("ml-train-now:"+e)||t&&t.running,r=this._canEdit()?``:"";if(!t)return`
    ${this._t("hdr.status",{},"Status")}

    ${this._t("msg.loading",{},"Loading\u2026")}

    `;const n=Object.keys(t.on_device_models||{}).length,a=n?`${this._t("ml.personalized",{},"\u25CF Personalized to this machine")} ${this._t("lbl.models_fine_tuned",{count:n},"(fine-tuned models: "+n+")")}`:`${this._t("ml.builtin_models",{},"\u25CF Using built-in models")}`,o=t.cycle_count||0,i=t.min_cycles||0,l=o>=i,c=i>0?Math.min(100,Math.round(o/i*100)):100,g=l?"var(--success-color,#4caf50)":"var(--warning-color,#ff9800)",p=Math.max(0,i-o),f=l?this._t("msg.enough_data",{current:o,min:i},`Enough data to learn from (${o}/${i} cycles).`):this._t("msg.collecting_data",{need:p,current:o,min:i},`Collecting data. Cycles still needed before fine-tuning can start: ${p} (${o}/${i}).`),u=`
    `,$=t.last_trained?ot(t.last_trained):"never",S=s?` ${this._t("status.fine_tuning",{},"fine-tuning now\u2026")}`:t.enabled?this._t("lbl.auto_fine_tune_on",{hour:String(t.hour).padStart(2,"0")},`auto fine-tune on (around ${String(t.hour).padStart(2,"0")}:00)`):this._t("lbl.auto_fine_tune_off",{},"auto fine-tune off");return`
    + `}_htmlMlStatusSection(t,e){const s=e&&this._busy.has("ml-train-now:"+e)||t&&t.running,a=this._canEdit()?``:"";if(!t)return`
    ${this._t("hdr.status",{},"Status")}

    ${this._t("msg.loading",{},"Loading\u2026")}

    `;const r=Object.keys(t.on_device_models||{}).length,i=r?`${this._t("ml.personalized",{},"\u25CF Personalized to this machine")} ${this._t("lbl.models_fine_tuned",{count:r},"(fine-tuned models: "+r+")")}`:`${this._t("ml.builtin_models",{},"\u25CF Using built-in models")}`,o=t.cycle_count||0,n=t.min_cycles||0,l=o>=n,c=n>0?Math.min(100,Math.round(o/n*100)):100,p=l?"var(--success-color,#4caf50)":"var(--warning-color,#ff9800)",_=Math.max(0,n-o),f=l?this._t("msg.enough_data",{current:o,min:n},`Enough data to learn from (${o}/${n} cycles).`):this._t("msg.collecting_data",{need:_,current:o,min:n},`Collecting data. Cycles still needed before fine-tuning can start: ${_} (${o}/${n}).`),u=`
    `,b=t.last_trained?et(t.last_trained):this._tText("ml.never",{},"never"),v=s?` ${this._t("status.fine_tuning",{},"fine-tuning now\u2026")}`:t.enabled?this._t("lbl.auto_fine_tune_on",{hour:String(t.hour).padStart(2,"0")},`auto fine-tune on (around ${String(t.hour).padStart(2,"0")}:00)`):this._t("lbl.auto_fine_tune_off",{},"auto fine-tune off");return`
    -
    ${this._t("hdr.status",{},"Status")}
    ${r} +
    ${this._t("hdr.status",{},"Status")}
    ${a}
    -
    ${a}
    +
    ${i}

    ${f}

    ${u} -

    ${this._t("lbl.last_checked",{},"Last checked:")} ${d($)} \xB7 ${S}

    -
    `}_htmlMlLearnedSection(t){if(!t)return"";const e=t.on_device_models||{},s=Object.keys(e),r=this._busy.has("ml-revert-models");let n;if(!s.length)n=`

    ${this._t("msg.no_fine_tuned",{},"Nothing fine-tuned yet \u2014 WashData is using its built-in models.")}

    `;else{const a=s.map(i=>{const l=e[i]||{},c=l.trained_at?ot(l.trained_at):"unknown";return`
    +

    ${this._t("lbl.last_checked",{},"Last checked:")} ${d(b)} \xB7 ${v}

    +
    `}_htmlMlLearnedSection(t){if(!t)return"";const e=t.on_device_models||{},s=Object.keys(e),a=this._busy.has("ml-revert-models"),r=t.last_run||{};let i;if(s.length){const o=s.map(l=>{const c=e[l]||{},p=c.trained_at?et(c.trained_at):this._tText("ml.trained_unknown",{},"at an unknown time");return`
    -
    ${d(l.label_key?this._t(l.label_key,{},l.label||i):l.label||i)}${this._mlTrendBadge(l.trend)}
    -
    ${d(l.blurb_key?this._t(l.blurb_key,{},l.blurb||""):l.blurb||"")} \xB7 ${this._t("ml.fine_tuned_at",{when:d(c)},"fine-tuned "+d(c))}
    +
    ${d(c.label_key?this._t(c.label_key,{},c.label||l):c.label||l)}${this._mlTrendBadge(c.trend)}
    +
    ${d(c.blurb_key?this._t(c.blurb_key,{},c.blurb||""):c.blurb||"")} \xB7 ${this._t("ml.fine_tuned_at",{when:p},"fine-tuned {when}")}
    + ${this._mlLastRunLine(r[l])}
    - ${this._mlQualityChip(l)} -
    `}).join(""),o=this._canEdit()?``:"";n=`
    ${a}
    ${o}`}return`
    + ${this._mlQualityChip(c)} +
    `}).join(""),n=this._canEdit()?``:"";i=`
    ${o}
    ${n}`}else{const o=Object.keys(r).map(n=>this._mlLastRunLine(r[n])).filter(Boolean).join("");i=`

    ${this._t("msg.no_fine_tuned",{},"Nothing fine-tuned yet - WashData is using its built-in models.")}

    ${o}`}return`
    ${this._t("hdr.ml_learned",{},"What WashData has learned")}

    ${this._t("msg.ml_learned_intro",{},"Models fine-tuned to this machine.")}

    - ${n} -
    `}_mlQualityChip(t){let e=0,s="",r=t.metric_key?this._t(t.metric_key,t.metric_params||{},t.metric||""):t.metric||"";if(t.auc!=null)e=Math.max(0,Math.min(1,(t.auc-.5)/.5))*100,s=t.auc>=.85?this._t("ml.fit_strong",{},"Strong"):t.auc>=.75?this._t("ml.fit_good",{},"Good"):t.auc>=.65?this._t("ml.fit_fair",{},"Fair"):this._t("ml.fit_weak",{},"Weak");else if(t.model_mae!=null&&t.naive_mae!=null&&t.naive_mae>0){const a=Math.max(0,(t.naive_mae-t.model_mae)/t.naive_mae);e=Math.min(1,a)*100,s=a>=.5?this._t("ml.fit_strong",{},"Strong"):a>=.2?this._t("ml.fit_good",{},"Good"):this._t("ml.fit_slight",{},"Slight"),r=this._t("ml.better_than_baseline",{pct:(a*100).toFixed(0),metric:r},`${(a*100).toFixed(0)}% better than the baseline estimate (${r})`)}else return"";const n=e>=70?"var(--success-color,#4caf50)":e>=40?"var(--warning-color,#ff9800)":"var(--secondary-text-color)";return`
    -
    ${s} ${this._t("ml.fit_word",{},"fit")}
    -
    -
    `}_mlTrendBadge(t){if(!t)return"";const s={improving:[this._t("badge.improving",{},"\u2197 improving"),"var(--success-color,#4caf50)",this._t("ml.trend_improving_tip",{},"This model's fit has improved across recent re-checks.")],declining:[this._t("badge.declining",{},"\u2198 declining"),"var(--warning-color,#ff9800)",this._t("ml.trend_declining_tip",{},"This model's fit has slipped across recent re-checks \u2014 reviewing more cycles may help it re-learn.")],steady:[this._t("badge.steady",{},"\u2192 steady"),"var(--secondary-text-color)",this._t("ml.trend_steady_tip",{},"This model's fit has held roughly steady across recent re-checks.")]}[t];return s?` ${s[0]}`:""}_htmlMatchingTuningCard(){const t=this._mlTrainingStatus,e=t&&t.matching;if(!e)return"";const s=e.defaults||{},r=e.tuned||null,n=r&&r.config||null,a=e.active==="tuned"&&n,o=this._busy.has("ml-revert-match"),i=f=>f==null||isNaN(f)?"-":Number(f).toFixed(2),l=[["corr_weight","Shape (correlation)"],["duration_weight","Duration agreement"],["energy_weight","Energy agreement"],["dtw_ensemble_w","DTW derivative blend (DDTW)"]].map(([f,u])=>{const $=s[f],S=a?n[f]:s[f],x=a&&$!=null&&S!=null&&Math.abs($-S)>1e-9;return` - ${u} - ${i($)} - ${i(S)} - `}).join(""),c=a?`${this._t("badge.using_tuned",{},"Using tuned weights")}`:`${this._t("badge.using_defaults",{},"Using shipped defaults")}`;let g="";if(a){const f=r.trained_at?ot(r.trained_at):"unknown",u=r.baseline_test_top1,$=r.tuned_test_top1,S=u!=null&&$!=null?` \xB7 held-out top-1 ${(u*100).toFixed(0)}% \u2192 ${($*100).toFixed(0)}%`:"";g=`

    Tuned ${d(f)} from ${r.cycle_count||0} cycles${S}.

    `}const p=a?``:"";return`
    -
    -
    ${this._t("hdr.ml_matching_tuning",{},"Program-matching fine-tuning")}
    ${p} -
    -

    ${this._t("msg.matching_tuning_intro",{},"When learning, WashData also adjusts how much program matching weighs shape versus duration and energy.")}

    -
    ${c}
    - - - ${l} -
    ${this._t("lbl.emphasis",{},"Emphasis")}${this._t("lbl.default",{},"Default")}${this._t("lbl.in_use",{},"In use")}
    - ${g} -
    `}_pgOverrideFields(){return[["start_threshold_w","Start Threshold","W","Minimum watts to count as started","detection"],["stop_threshold_w","Stop Threshold","W","Below this, machine counts as off","detection"],["off_delay","Off Delay","s","Seconds of low power before cycle ends","timing"],["min_off_gap","Min Off Gap","s","Gap required to separate two cycles","timing"],["completion_min_seconds","Min Cycle Duration","s","Shortest run that counts as a real cycle","timing"],["start_duration_threshold","Start Duration","s","Seconds above threshold to confirm start","timing"],["end_repeat_count","End Repeat Count","","Low readings in a row before ending","advanced"],["interrupted_min_seconds","Interrupted Min","s","Short cycles flagged as interrupted","advanced"],["anti_wrinkle_enabled","Enable Anti-Wrinkle Detection","","Absorb the tumble pulses after the main phase instead of reading them as new cycles","advanced","bool"],["anti_wrinkle_max_power","Max Anti-Wrinkle Power","W","A pulse above this ends anti-wrinkle and opens a new cycle","advanced"],["anti_wrinkle_max_duration","Max Anti-Wrinkle Duration","s","A pulse longer than this ends anti-wrinkle and opens a new cycle","advanced"],["anti_wrinkle_exit_power","Anti-Wrinkle Exit Power","W","Power must fall below this between pulses for anti-wrinkle to stay active","advanced"],["anti_wrinkle_idle_timeout","Max Pulse Gap","s","Quiet time allowed between two tumble pulses before anti-wrinkle ends","advanced"],["dishwasher_end_spike_quiet_release","Passive-Dry Quiet Release","s","Dishwasher: quiet seconds after expected duration before the end-of-cycle drain wait is released","advanced"],["smart_termination_duration_ratio","Smart Termination 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","advanced"],["anti_crease_finalize_ratio","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 a dryer whose sensor-dry runtime follows the load, so its tail is recognised instead of sitting until the fallback timeout. Leave washing machines at the default: there this fraction is what keeps a quiet mid-wash phase from being mistaken for the tail. Separate from the Smart Termination Ratio, which gates a different check.","advanced"],["curve_preroll_seconds","Curve Pre-roll","s","Seconds of readings from aborted start attempts that may be carried into the front of a cycle's curve. Machines that probe before settling (programme selection, door lock, first fill) can drop the first minutes of real activity from every curve. 0 turns this off. Note that enabling it moves the recorded start earlier, so cycles recorded before and after the change carry different durations for the same program until the older ones age out - expect the learned averages to drift for a while.","timing"],["profile_match_min_duration_ratio","Min Duration Ratio","","Stage 1: shortest run (vs the profile) still allowed to match","matching"],["profile_match_max_duration_ratio","Max Duration Ratio","","Stage 1: longest run (vs the profile) still allowed to match","matching"],["corr_weight","Correlation Weight","","Stage 2: balance between curve shape (correlation) and power level (MAE); default 0.45","matching"],["keep_min_score","Keep Min Score","","Stage 2: floor score to stay in the race; default 0.1 admits weak matches, later stages pick the best","matching"],["dtw_bandwidth","DTW Bandwidth","","Stage 3: Sakoe-Chiba warp band (0 = DTW off; default 0.2 = 20% of cycle length)","matching"],["dtw_blend","DTW Blend","","Stage 3: 0 = core score only, 1 = DTW score only, 0.5 = equal blend (default)","matching"],["dtw_ensemble_w","DTW Ensemble Weight","","Stage 3 (ensemble mode): weight on scaled-L1 vs derivative DTW; default 0.7 favours level-aware","matching"],["dtw_ddtw_scale","DDTW Scale","","Stage 3: DDTW half-saturation distance; smaller = more shape-sensitive (default 30)","matching"],["dtw_refine_top_n","DTW Refine Top-N","","Stage 3: candidates DTW re-scores; raise to 7-9 if correct profile ranks 4th-5th (default 5)","matching"],["duration_weight","Duration Weight","","Stage 4: how strongly run-length agreement affects the final score (default 0.22)","matching"],["energy_weight","Energy Weight","","Stage 4: how strongly energy agreement affects the final score (default 0.22)","matching"],["duration_scale","Duration Scale","","Stage 4: log-ratio where duration agreement halves; smaller = stricter penalty (default 0.175)","matching"],["energy_scale","Energy Scale","","Stage 4: log-ratio where energy agreement halves; smaller = stricter penalty (default 0.25)","matching"]]}_pgFieldVal(t,e){const s=e||{};if(s[t]!==void 0)return s[t];const r=this._pgEffective||{};if(r[t]!==void 0&&r[t]!==null)return r[t];const n=this._opts||{};if(n[t]!==void 0&&n[t]!==null)return n[t];const a=Pt[t]||{};if(a.def!==void 0)return a.def;const o=this._constants&&this._constants.pgMatchDefaults||{};return o[t]!==void 0?o[t]:te[t]!==void 0?te[t]:""}async _pgFetchSettings(t,e=!0){try{const s=await this._ws({type:`${y}/get_playground_settings`,entry_id:t,include_suggestions:e});if(!this._isActiveEntry(t))return;this._pgEffective=s.effective||{},this._pgPublishable=Array.isArray(s.publishable)?s.publishable:null,this._pgPresets=Array.isArray(s.presets)?s.presets:[],this._pgPresetLimit=s.preset_limit||0,this._pgPresetSel&&!this._pgPresets.some(r=>r.name===this._pgPresetSel)&&(this._pgPresetSel=""),e&&this._pgApplySuggestions(s)}catch(s){this._pgIsUnknownCmd(s)&&(this._pgNeedsRestart=!0)}}_pgApplySuggestions(t){this._pgSuggClassic=t.classic_suggestions&&typeof t.classic_suggestions=="object"?t.classic_suggestions:{},this._pgSuggMl=t.ml_suggestions&&typeof t.ml_suggestions=="object"?t.ml_suggestions:null,this._pgMlSuggEnabled=!!t.ml_suggestions_enabled}async _pgFetchSuggestions(t){try{const e=await this._ws({type:`${y}/get_playground_settings`,entry_id:t,include_suggestions:!0});if(!this._isActiveEntry(t))return;this._pgApplySuggestions(e),this._tab==="playground"&&this._render()}catch{}}_pgCurrentValues(){const t={};for(const[e]of this._pgOverrideFields()){const s=this._pgFieldVal(e,{});s!==""&&s!==null&&s!==void 0&&(t[e]=s)}this._pgThreshStart!=null&&(t.start_threshold_w=this._pgThreshStart),this._pgThreshStop!=null&&(t.stop_threshold_w=this._pgThreshStop);for(const[e,s]of Object.entries(this._pgParamOverrides||{}))t[e]=s;return t}_pgStagedVal(t){return t==="start_threshold_w"?this._pgThreshStart??void 0:t==="stop_threshold_w"?this._pgThreshStop??void 0:this._pgParamOverrides[t]}_pgSetStaged(t,e){t==="start_threshold_w"?this._pgThreshStart=e:t==="stop_threshold_w"?this._pgThreshStop=e:this._pgParamOverrides[t]=e}_pgClearStaged(t){t==="start_threshold_w"?this._pgThreshStart=null:t==="stop_threshold_w"?this._pgThreshStop=null:delete this._pgParamOverrides[t]}_pgChangedKeys(){const t=this._pgEffective||{},e=[];for(const[s]of this._pgOverrideFields()){const r=this._pgStagedVal(s);if(r==null)continue;const n=t[s];n!=null&&this._pgSameVal(r,n)||e.push(s)}return e}_pgSameVal(t,e){if(typeof t=="boolean"||typeof e=="boolean")return!!t==!!e;const s=parseFloat(t),r=parseFloat(e);return!isNaN(s)&&!isNaN(r)?Math.abs(s-r)<1e-9:String(t)===String(e)}_pgIsPublishable(t){return Array.isArray(this._pgPublishable)?this._pgPublishable.includes(t):!!Pt[t]}_htmlPgControlPanel(){const t=this._pgChangedKeys(),e=this._canEdit(),s=!!this._pgEffective,r=t.filter(S=>this._pgIsPublishable(S)),n=s?t.length?`${this._t("msg.pg_n_changed",{n:t.length},t.length+" changed vs live settings")}`:`\u2713 ${this._t("msg.pg_matches_live",{},"Matches live settings")}`:`${this._t("msg.pg_live_unavailable",{},"Live settings unavailable \u2014 showing defaults.")}`,a=``+(this._pgPresets||[]).map(S=>``).join(""),o=!!this._pgPresetSel,i=this._pgPresetLimit>0&&(this._pgPresets||[]).length>=this._pgPresetLimit&&!(this._pgPresets||[]).some(S=>S.name===(this._pgPresetName||"").trim()),l=`
    - - - ${e?``:""} -
    `,c=e?`
    - - - ${this._t("msg.pg_preset_limit",{n:this._pgPresetLimit},"Preset limit reached ("+this._pgPresetLimit+")")} -
    `:"",g=e&&r.length?``:"",p=Object.keys(this._pgSuggClassic||{}).length,f=Object.keys(this._pgSuggMl||{}).length,u=p>0?``:"",$=this._pgMlSuggEnabled&&f>0?``:"";return`
    + ${i} +
    `}_mlQualityChip(t){let e=0,s="",a=t.metric_key?this._tText(t.metric_key,t.metric_params||{},t.metric||""):t.metric||"";if(t.model_mae!=null&&t.naive_mae!=null&&t.naive_mae>0){const i=Math.max(0,(t.naive_mae-t.model_mae)/t.naive_mae);e=Math.min(1,i)*100,s=i>=.5?this._t("ml.fit_strong",{},"Strong"):i>=.2?this._t("ml.fit_good",{},"Good"):this._t("ml.fit_slight",{},"Slight"),a=this._tText("ml.better_than_baseline",{pct:(i*100).toFixed(0),metric:a},`${(i*100).toFixed(0)}% better than the baseline estimate (${a})`)}else return"";Number.isInteger(t.held_out_cycles)&&(a+=" \xB7 "+this._tText("ml.held_out_count",{count:t.held_out_cycles},`measured on ${t.held_out_cycles} held-out cycles`));const r=e>=70?"var(--success-color,#4caf50)":e>=40?"var(--warning-color,#ff9800)":"var(--secondary-text-color)";return`
    +
    ${s} ${this._t("ml.fit_word",{},"fit")}
    +
    +
    `}_mlTrendBadge(t){if(!t)return"";const s={improving:[this._t("badge.improving",{},"\u2197 improving"),"var(--success-color,#4caf50)",this._t("ml.trend_improving_tip",{},"This model's fit has improved across recent re-checks.")],declining:[this._t("badge.declining",{},"\u2198 declining"),"var(--warning-color,#ff9800)",this._t("ml.trend_declining_tip",{},"This model's fit has slipped across recent re-checks \u2013 reviewing more cycles may help it re-learn.")],steady:[this._t("badge.steady",{},"\u2192 steady"),"var(--secondary-text-color)",this._t("ml.trend_steady_tip",{},"This model's fit has held roughly steady across recent re-checks.")]}[t];return s?` ${s[0]}`:""}_mlSkipReason(t){const e={insufficient_rows:"Not enough usable cycles yet: {count} labelled, clean cycles so far.",holdout_too_small:"Too few cycles to test a new model fairly: {held_out} could be set aside for testing, {min} are needed.",not_better_than_naive:"The new model was not clearly better than the simple estimate (error {model} vs {naive}).",not_better_than_incumbent:"The new model was not better than the one in use (error {model} vs {incumbent}), so the current one was kept.",fit_failed:"Training failed: {error}",unknown:"Nothing new was learnt."},s=Object.prototype.hasOwnProperty.call(e,t&&t.reason_code)?t.reason_code:"unknown",a=t&&t.reason_params||{},r=s==="insufficient_rows"?{count:Number(a.cycles)||0}:{...a};return[s,this._tText(`ml.skip_reason.${s}`,r,e[s])]}_mlLastRunLine(t){if(!t||t.promoted!==!1)return"";const[e,s]=this._mlSkipReason(t),a=t.ts?et(t.ts):this._tText("ml.trained_unknown",{},"at an unknown time");return`
    ${this._t("ml.last_run_not_promoted",{when:a,reason:s},"Last check ({when}): {reason}")}
    `}_pgOverrideFields(){return[["start_threshold_w","Start Threshold","W","Minimum watts to count as started","detection"],["stop_threshold_w","Stop Threshold","W","Below this, machine counts as off","detection"],["off_delay","Off Delay","s","Seconds of low power before cycle ends","timing"],["min_off_gap","Min Off Gap","s","Gap required to separate two cycles","timing"],["completion_min_seconds","Min Cycle Duration","s","Shortest run that counts as a real cycle","timing"],["start_duration_threshold","Start Duration","s","Seconds above threshold to confirm start","timing"],["interrupted_min_seconds","Interrupted Min","s","Short cycles flagged as interrupted","advanced"],["anti_wrinkle_enabled","Enable Anti-Wrinkle Detection","","Absorb the tumble pulses after the main phase instead of reading them as new cycles","advanced","bool"],["anti_wrinkle_max_power","Max Anti-Wrinkle Power","W","A pulse above this ends anti-wrinkle and opens a new cycle","advanced"],["anti_wrinkle_max_duration","Max Anti-Wrinkle Duration","s","A pulse longer than this ends anti-wrinkle and opens a new cycle","advanced"],["anti_wrinkle_exit_power","Anti-Wrinkle Exit Power","W","Quiet level between tumble pulses; only takes effect above the Stop Threshold","advanced"],["anti_wrinkle_idle_timeout","Max Pulse Gap","s","Quiet time allowed between two tumble pulses before anti-wrinkle ends","advanced"],["dishwasher_end_spike_quiet_release","Passive-Dry Quiet Release","s","Dishwasher: quiet seconds after expected duration before the end-of-cycle drain wait is released","advanced"],["smart_termination_duration_ratio","Smart Termination Ratio","","Fraction of the program's average duration a cycle must reach before Smart Termination may end it early; lower it if run times vary","advanced"],["anti_crease_finalize_ratio","Anti-Crease Finalize Ratio","","Fraction of the program's average duration a cycle must reach before the anti-crease tail may finish it; lower it on dryers, keep washers at the default","advanced"],["curve_preroll_seconds","Curve Pre-roll","s","Seconds of readings from aborted start attempts carried into the front of a cycle's curve; 0 = off","timing"],["profile_match_min_duration_ratio","Min Duration Ratio","","Minimum cycle length as a fraction of the profile's duration; default 0.1 (10%)","matching"],["profile_match_max_duration_ratio","Max Duration Ratio","","Maximum cycle length as a fraction of the profile's duration; default 1.8 (180%)","matching"]]}_pgFieldVal(t,e){const s=e||{};if(s[t]!==void 0)return s[t];const a=this._pgEffective||{};if(a[t]!==void 0&&a[t]!==null)return a[t];const r=this._opts||{};if(r[t]!==void 0&&r[t]!==null)return r[t];const i=wt[t]||{};if(i.def!==void 0)return i.def;const o=this._constants&&this._constants.pgMatchDefaults||{};return o[t]!==void 0?o[t]:te[t]!==void 0?te[t]:""}async _pgFetchSettings(t,e=!0){try{const s=await this._ws({type:`${w}/get_playground_settings`,entry_id:t,include_suggestions:e});if(!this._isActiveEntry(t))return;this._pgEffective=s.effective||{},this._pgPublishable=Array.isArray(s.publishable)?s.publishable:null,this._pgPresets=Array.isArray(s.presets)?s.presets:[],this._pgPresetLimit=s.preset_limit||0,this._pgPresetSel&&!this._pgPresets.some(a=>a.name===this._pgPresetSel)&&(this._pgPresetSel=""),e&&this._pgApplySuggestions(s)}catch(s){this._pgIsUnknownCmd(s)&&(this._pgNeedsRestart=!0)}}_pgApplySuggestions(t){this._pgSuggClassic=t.classic_suggestions&&typeof t.classic_suggestions=="object"?t.classic_suggestions:{}}async _pgFetchSuggestions(t){try{const e=await this._ws({type:`${w}/get_playground_settings`,entry_id:t,include_suggestions:!0});if(!this._isActiveEntry(t))return;this._pgApplySuggestions(e),this._tab==="playground"&&this._render()}catch{}}_pgCurrentValues(){const t={};for(const[e]of this._pgOverrideFields()){const s=this._pgFieldVal(e,{});s!==""&&s!==null&&s!==void 0&&(t[e]=s)}this._pgThreshStart!=null&&(t.start_threshold_w=this._pgThreshStart),this._pgThreshStop!=null&&(t.stop_threshold_w=this._pgThreshStop);for(const[e,s]of Object.entries(this._pgParamOverrides||{}))t[e]=s;return t}_pgStagedVal(t){return t==="start_threshold_w"?this._pgThreshStart??void 0:t==="stop_threshold_w"?this._pgThreshStop??void 0:this._pgParamOverrides[t]}_pgSetStaged(t,e){t==="start_threshold_w"?this._pgThreshStart=e:t==="stop_threshold_w"?this._pgThreshStop=e:this._pgParamOverrides[t]=e}_pgClearStaged(t){t==="start_threshold_w"?this._pgThreshStart=null:t==="stop_threshold_w"?this._pgThreshStop=null:delete this._pgParamOverrides[t]}_pgChangedKeys(){const t=this._pgEffective||{},e=[];for(const[s]of this._pgOverrideFields()){const a=this._pgStagedVal(s);if(a==null)continue;const r=t[s];r!=null&&this._pgSameVal(a,r)||e.push(s)}return e}_pgSameVal(t,e){if(typeof t=="boolean"||typeof e=="boolean")return!!t==!!e;const s=parseFloat(t),a=parseFloat(e);return!isNaN(s)&&!isNaN(a)?Math.abs(s-a)<1e-9:String(t)===String(e)}_pgIsPublishable(t){return Array.isArray(this._pgPublishable)?this._pgPublishable.includes(t):!!wt[t]}_htmlPgControlPanel(){const t=this._pgChangedKeys(),e=this._canEdit(),s=!!this._pgEffective,a=t.filter(b=>this._pgIsPublishable(b)),r=s?t.length?`${this._t("msg.pg_n_changed",{n:t.length},t.length+" changed vs live settings")}`:`\u2713 ${this._t("msg.pg_matches_live",{},"Matches live settings")}`:`${this._t("msg.pg_live_unavailable",{},"Live settings unavailable, showing defaults.")}`,i=this._pgPresets||[],o=``+i.map(b=>``).join(""),n=!!this._pgPresetSel,l=this._pgPresetAtLimit((this._pgPresetName||"").trim()),c=`
    + + + ${e?``:""} +
    `,p=e?`
    + + + ${this._t("msg.pg_preset_limit",{n:this._pgPresetLimit},"Preset limit reached ({n})")} +
    `:"",_=e&&a.length?``:"",f=Object.keys(this._pgSuggClassic||{}).length,u=f>0?``:"";return`
    ${this._t("hdr.pg_settings_source",{},"Settings source")} - ${n} + ${r}

    ${this._t("msg.pg_ctrl_intro",{},"Values start from this device's live integration settings. Edits stay in the Playground until you publish them.")}

    - ${u}${$} - ${g} + ${u} + ${_}
    - ${l} ${c} -
    `}_pgApplyPresetValues(t){this._pgThreshStart=null,this._pgThreshStop=null,this._pgParamOverrides={};const e=this._pgEffective||{};for(const[s,r]of Object.entries(t||{})){if(r==null)continue;const n=e[s];n!=null&&this._pgSameVal(r,n)||this._pgSetStaged(s,r)}}async _pgSavePreset(){const t=this._devices[this._selIdx],e=(this._pgPresetName||"").trim();!t||!this._canEdit()||!e||(this._pgPresets||[]).some(s=>s.name===e)&&!confirm(this._t("msg.pg_preset_overwrite",{name:e},`Overwrite the preset "${e}"?`))||(await this._busyRun("pg-preset-save",async()=>{try{const s=await this._ws({type:`${y}/save_playground_preset`,entry_id:t.entry_id,name:e,values:this._pgCurrentValues()});if(!this._isActiveEntry(t.entry_id))return;this._pgPresets=Array.isArray(s.presets)?s.presets:this._pgPresets,this._pgPresetSel=e,this._pgPresetName="",this._showToast(this._t("toast.pg_preset_saved",{name:e},`Preset "${e}" saved`))}catch(s){this._showToast(this._t("msg.toast_save_failed",{error:s.message||s},"Save failed: "+(s.message||s)),"error")}}),this._render())}async _pgDeletePreset(){const t=this._devices[this._selIdx],e=this._pgPresetSel;!t||!this._canEdit()||!e||confirm(this._t("msg.pg_preset_delete_confirm",{name:e},`Delete the preset "${e}"?`))&&(await this._busyRun("pg-preset-delete",async()=>{try{const s=await this._ws({type:`${y}/delete_playground_preset`,entry_id:t.entry_id,name:e});if(!this._isActiveEntry(t.entry_id))return;this._pgPresets=Array.isArray(s.presets)?s.presets:(this._pgPresets||[]).filter(r=>r.name!==e),this._pgPresetSel=""}catch(s){this._showToast(this._t("msg.toast_error",{error:s.message||s},"Error: "+(s.message||s)),"error")}}),this._render())}async _pgLoadLive(){const t=this._devices[this._selIdx];if(!t)return;const e=this._pgChangedKeys();e.length&&!confirm(this._t("msg.pg_load_live_confirm",{n:e.length},`Discard ${e.length} Playground edit(s) and reload the integration's current settings?`))||(await this._busyRun("pg-load-live",async()=>{this._pgThreshStart=null,this._pgThreshStop=null,this._pgParamOverrides={},await this._pgFetchSettings(t.entry_id)}),this._render(),requestAnimationFrame(()=>this._pgDrawCanvas()))}_pgLoadSuggested(t){const e=t==="ml"?this._pgSuggMl||{}:this._pgSuggClassic||{},s=this._pgEffective||{};let r=0;for(const[n,a]of Object.entries(e)){if(a==null)continue;const o=s[n];o!=null&&this._pgSameVal(a,o)||(this._pgSetStaged(n,a),r++)}if(r===0)this._showToast(this._t("msg.pg_sugg_none",{},"All suggestions already match the current settings"));else{const n=t==="ml"?"toast.pg_sugg_ml_loaded":"toast.pg_sugg_loaded",a=t==="ml"?`Staged ${r} ML-calibrated value(s) - run the Playground to compare`:`Staged ${r} suggested value(s) - run the Playground to compare`;this._showToast(this._t(n,{n:r},a))}this._render(),requestAnimationFrame(()=>this._pgDrawCanvas())}async _pgPublishOne(t){const e=this._devices[this._selIdx];if(!e||!this._canEdit()||!t||!this._pgIsPublishable(t))return;const s=this._pgStagedVal(t);if(s==null)return;const r=this._t("setting."+t+".label",{},t);confirm(this._t("msg.pg_publish_one_confirm",{label:r,value:s},`Save ${r} = ${s} to this device's settings?`))&&(await this._busyRun("pg-publish-"+t,async()=>{try{if(await this._ws({type:`${y}/set_options`,entry_id:e.entry_id,options:{[t]:s}}),!this._isActiveEntry(e.entry_id))return;this._opts={...this._opts,[t]:s},this._pgEffective&&(this._pgEffective={...this._pgEffective,[t]:s}),this._pgClearStaged(t),this._showToast(this._t("toast.settings_saved",{},"Settings saved; integration reloading"))}catch(n){this._showToast(this._t("msg.toast_save_failed",{error:n.message||n},"Save failed: "+(n.message||n)),"error")}}),this._render())}_htmlPlayground(){if(!this._devices[this._selIdx])return`
    ${this._t("msg.no_device_selected",{},"No device selected.")}
    `;const e=this._cycles||[],s=this._profiles||[],r=e.map(u=>{const $=u.profile_name||u.matched_profile||this._t("lbl.unlabelled",{},"Unlabelled"),S=u.duration?` \xB7 ${Math.round(u.duration/60)} min`:"",x=u.start_time?` \xB7 ${ot(u.start_time)}`:"";return``}).join(""),n=``+s.map(u=>``).join(""),a=this._pgLoading,o=`
    -
    -
    + ${p} +
    `}_pgPresetAtLimit(t){const e=this._pgPresets||[];return this._pgPresetLimit>0&&e.length>=this._pgPresetLimit&&!e.some(s=>s.name===t)}_pgApplyPresetValues(t){this._pgThreshStart=null,this._pgThreshStop=null,this._pgParamOverrides={};const e=this._pgEffective||{};for(const[s,a]of Object.entries(t||{})){if(a==null)continue;const r=e[s];r!=null&&this._pgSameVal(a,r)||this._pgSetStaged(s,a)}}_pgLoadPreset(){const t=(this._pgPresets||[]).find(e=>e.name===this._pgPresetSel);t&&(this._pgApplyPresetValues(t.values),this._showToast(this._tText("toast.pg_preset_loaded",{name:t.name},`Preset "${t.name}" loaded`)),this._render(),requestAnimationFrame(()=>this._pgDrawCanvas()))}async _pgSavePreset(){const t=this._devices[this._selIdx],e=(this._pgPresetName||"").trim();!t||!this._canEdit()||!e||(this._pgPresets||[]).some(s=>s.name===e)&&!confirm(this._tText("msg.pg_preset_overwrite",{name:e},`Overwrite the preset "${e}"?`))||(await this._busyRun("pg-preset-save",async()=>{try{const s=await this._ws({type:`${w}/save_playground_preset`,entry_id:t.entry_id,name:e,values:this._pgCurrentValues()});if(!this._isActiveEntry(t.entry_id))return;this._pgPresets=Array.isArray(s.presets)?s.presets:this._pgPresets,this._pgPresetSel=e,this._pgPresetName="",this._showToast(this._tText("toast.pg_preset_saved",{name:e},`Preset "${e}" saved`))}catch(s){this._showToast(this._tText("msg.toast_save_failed",{error:s.message||s},"Save failed: "+(s.message||s)),"error")}}),this._render())}async _pgDeletePreset(){const t=this._devices[this._selIdx],e=this._pgPresetSel;!t||!this._canEdit()||!e||confirm(this._tText("msg.pg_preset_delete_confirm",{name:e},`Delete the preset "${e}"?`))&&(await this._busyRun("pg-preset-delete",async()=>{try{const s=await this._ws({type:`${w}/delete_playground_preset`,entry_id:t.entry_id,name:e});if(!this._isActiveEntry(t.entry_id))return;this._pgPresets=Array.isArray(s.presets)?s.presets:(this._pgPresets||[]).filter(a=>a.name!==e),this._pgPresetSel=""}catch(s){this._showToast(this._tText("msg.toast_error",{error:s.message||s},"Error: "+(s.message||s)),"error")}}),this._render())}async _pgLoadLive(){const t=this._devices[this._selIdx];if(!t)return;const e=this._pgChangedKeys();e.length&&!confirm(this._tText("msg.pg_load_live_confirm",{n:e.length},`Discard ${e.length} Playground edit(s) and reload the integration's current settings?`))||(await this._busyRun("pg-load-live",async()=>{this._pgThreshStart=null,this._pgThreshStop=null,this._pgParamOverrides={},await this._pgFetchSettings(t.entry_id)}),this._render(),requestAnimationFrame(()=>this._pgDrawCanvas()))}_pgLoadSuggested(){const t=this._pgSuggClassic||{},e=this._pgEffective||{};let s=0;for(const[a,r]of Object.entries(t)){if(r==null)continue;const i=e[a];i!=null&&this._pgSameVal(r,i)||(this._pgSetStaged(a,r),s++)}s===0?this._showToast(this._t("msg.pg_sugg_none",{},"All suggestions already match the current settings")):this._showToast(this._tText("toast.pg_sugg_loaded",{n:s},`Staged ${s} suggested value(s) - run the Playground to compare`)),this._render(),requestAnimationFrame(()=>this._pgDrawCanvas())}async _pgPublishOne(t){const e=this._devices[this._selIdx];if(!e||!this._canEdit()||!t||!this._pgIsPublishable(t))return;const s=this._pgStagedVal(t);if(s==null)return;const a=this._t("setting."+t+".label",{},t);confirm(this._tText("msg.pg_publish_one_confirm",{label:a,value:s},`Save ${a} = ${s} to this device's settings?`))&&(await this._busyRun("pg-publish-"+t,async()=>{try{if(await this._ws({type:`${w}/set_options`,entry_id:e.entry_id,options:{[t]:s}}),!this._isActiveEntry(e.entry_id))return;this._opts={...this._opts,[t]:s},this._pgEffective&&(this._pgEffective={...this._pgEffective,[t]:s}),this._pgClearStaged(t),this._showToast(this._t("toast.settings_saved",{},"Settings saved; integration reloading"))}catch(r){this._showToast(this._tText("msg.toast_save_failed",{error:r.message||r},"Save failed: "+(r.message||r)),"error")}}),this._render())}_htmlPlayground(){if(!this._devices[this._selIdx])return`
    ${this._t("msg.no_device_selected",{},"No device selected.")}
    `;const e=this._cycles||[],s=this._profiles||[],a=e.map(u=>{const b=u.profile_name||u.matched_profile||this._t("lbl.unlabelled",{},"Unlabelled"),v=u.duration?` \xB7 ${Math.round(u.duration/60)} ${this._tText("lbl.timer_min",{},"min")}`:"",T=u.start_time?` \xB7 ${et(u.start_time)}`:"";return``}).join(""),r=``+s.map(u=>``).join(""),i=this._pgLoading,o=`
    +
    +
    - - ${a?``:""} + + ${i?``:""}
    -
    `,i=a?`
    -
    ${this._t("msg.pg_simulating",{},"Simulating cycle\u2026")}
    `:"",l=!this._pgPowerPts&&!a?`
    +
    `,n=i?`
    +
    ${this._t("msg.pg_simulating",{},"Simulating cycle\u2026")}
    `:"",l=!this._pgPowerPts&&!i?`
    〜
    ${this._t("msg.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.")}
    -
    `:"",c=`${i}
    ${l}
    `,g=this._htmlPgStrip(),p=this._pgNeedsRestart?`

    \u26A0 ${this._t("msg.pg_restart_note",{},"Restart Home Assistant to enable simulation tools.")}

    `:"",f=`${o}${c}${g} +
    `:"",c=`${n}
    ${l}
    `,p=this._htmlPgStrip(),_=this._pgNeedsRestart?`

    \u26A0 ${this._t("msg.pg_restart_note",{},"Restart Home Assistant to enable simulation tools.")}

    `:"",f=`${o}${c}${p}
    ${this._htmlPgParamRows()}
    ${this._htmlPgAlerts()}${this._htmlPgAnalysis()}
    `;return`
    ${this._t("hdr.playground",{},"Playground")}
    -

    ${this._t("msg.playground_intro",{},"Explore how settings affect detection on your real cycle data. Nothing here changes live configuration until you explicitly apply it.")}

    - ${p} +

    ${this._t("msg.playground_intro",{},"Try settings on your real cycle data. Nothing here changes your live settings until you apply it.")}

    + ${_} ${f} ${this._htmlPgDrawer()}
    `}_htmlPgDrawer(){const t=this._pgAnalysisTab||"history",s=`
    - ${[["history",this._t("lbl.pg_mode_history",{},"Test on history")],["sweep",this._t("lbl.pg_mode_optimize",{},"Optimize")]].map(([n,a])=>``).join("")} -
    `,r=t==="sweep"?this._htmlPgSweepMode():this._htmlPgHistoryMode();return`
    + ${[["history",this._t("lbl.pg_mode_history",{},"Test on history")],["sweep",this._t("lbl.pg_mode_optimize",{},"Optimize")]].map(([r,i])=>``).join("")} +
    `,a=t==="sweep"?this._htmlPgSweepMode():this._htmlPgHistoryMode();return`
    ${this._t("hdr.pg_across_cycles",{},"Across your cycles")} ${s}
    - ${r} -
    `}_htmlPgParamRows(){const t=this._pgOverrideFields(),e=new Set(["start_threshold_w","stop_threshold_w"]),s={detection:"#2a78d6",timing:"#1baf7a",advanced:"#eda100",matching:"#a05cd6"},r={detection:this._t("lbl.pg_group_detection",{},"Detection triggers"),timing:this._t("lbl.pg_group_timing",{},"Timing rules"),advanced:this._t("lbl.pg_group_advanced",{},"Edge cases"),matching:this._t("lbl.pg_group_matching",{},"Program matching")};let n="";const a=t.map(([p,f,u,$,S,x])=>{const w=this._t("setting."+p+".label",{},f),v=this._pgFieldVal(p,{});let k;p==="start_threshold_w"?k=this._pgThreshStart??v:p==="stop_threshold_w"?k=this._pgThreshStop??v:k=this._pgParamOverrides[p]??v;const D=x==="bool",I=e.has(p),z=u||"",R=s[S]||"#2a78d6",L=r[S]||"";let P="";S&&S!==n&&(P=`
    -
    - ${d(L)} -
    `,n=S);const C=this._pgStagedVal(p),A=C!=null&&!(v!==""&&v!==null&&v!==void 0&&this._pgSameVal(C,v)),F=this._canEdit()&&A&&this._pgIsPublishable(p)?``:'';return`${P}
    + ${a} + `}_htmlPgParamRows(){const t=this._pgOverrideFields(),e=new Set(["start_threshold_w","stop_threshold_w"]),s={detection:"#2a78d6",timing:"#1baf7a",advanced:"#eda100",matching:"#a05cd6"},a={detection:this._t("lbl.pg_group_detection",{},"Detection triggers"),timing:this._t("lbl.pg_group_timing",{},"Timing rules"),advanced:this._t("lbl.pg_group_advanced",{},"Edge cases"),matching:this._t("lbl.pg_group_matching",{},"Program matching")};let r="";const i=t.map(([o,n,l,c,p,_])=>{const f=this._t("setting."+o+".label",{},n),u=this._pgFieldVal(o,{});let b;o==="start_threshold_w"?b=this._pgThreshStart??u:o==="stop_threshold_w"?b=this._pgThreshStop??u:b=this._pgParamOverrides[o]??u;const v=_==="bool",T=e.has(o),C=l||"",x=s[p]||"#2a78d6",$=a[p]||"";let L="";p&&p!==r&&(L=`
    +
    + ${d($)} +
    `,r=p);const D=this._pgStagedVal(o),B=D!=null&&!(u!==""&&u!==null&&u!==void 0&&this._pgSameVal(D,u)),U=this._canEdit()&&B&&this._pgIsPublishable(o)?``:'';return`${L}
    -
    ${d(w)}${I?` \u2195`:""}
    - ${$?`
    ${d(this._t("pg_desc."+p,{},$))}
    `:""} +
    ${d(f)}${T?` \u2195`:""}
    + ${c?`
    ${d(this._t("pg_desc."+o,{},c))}
    `:""}
    - ${D?``:``} - ${z?`${d(z)}`:""} - ${F} + ${v?``:``} + ${C?`${d(C)}`:""} + ${U}
    -
    `}).join(""),i=`
    -
    - ${d(this._t("lbl.pg_stress_group",{},"Idle termination test"))} -
    `,l=`
    -
    -
    ${d(this._t("lbl.pg_stress_toggle",{},"Test idle termination"))}
    -
    ${d(this._t("lbl.pg_stress_toggle_desc",{},"Simulates the appliance staying at its standby draw after recording ends \u2014 shows if and when WashData stops the cycle."))}
    -
    - -
    `,c=this._pgStressTail?`
    -
    -
    ${d(this._t("lbl.pg_stress_idle_w",{},"Idle level (W)"))}
    -
    ${d(this._t("lbl.pg_stress_idle_w_desc",{},"Override the auto-detected standby floor (leave blank for auto)."))}
    -
    -
    - - W -
    -
    `:"",g=`${i}${l}${c}`;return`
    +
    `}).join("");return`
    ${this._t("hdr.pg_detection_params",{},"Detection settings")}
    ${this._htmlPgControlPanel()} - ${a}${g} + ${i}
    ${this._pgDetailBusy?'':""}
    -
    `}_htmlPgAlerts(){const t=this._pgDetail;if(!t)return"";const e=t.outcome||{},s={error:"var(--error-color,#f44336)",warn:"var(--warning-color,#ff9800)",info:"var(--info-color,#2196f3)"},r=Array.isArray(t.alerts)?t.alerts:[],n=r.length?r.map(c=>`
    - ${d(this._pgAlertLabel(c.code))} -
    ${d(c.detail_key?this._t(c.detail_key,c.detail_params||{},c.detail||""):c.detail||"")}
    -
    `).join(""):`
    \u2713 ${this._t("msg.pg_no_alerts",{},"No issues detected in this run.")}
    `,a=e.termination_reason?String(e.termination_reason):"\u2014",o=e.final_duration_s?Math.round(e.final_duration_s/60)+" min":"\u2014",i=e.projected_energy_wh!=null?e.projected_energy_wh>=1e3?(e.projected_energy_wh/1e3).toFixed(2)+" kWh":Math.round(e.projected_energy_wh)+" Wh":"\u2014",l=(c,g)=>`
    ${d(g)}
    ${d(c)}
    `;return`
    +
    `}_htmlPgAlerts(){const t=this._pgDetail;if(!t)return"";const e=t.outcome||{},s={error:"var(--error-color,#f44336)",warn:"var(--warning-color,#ff9800)",info:"var(--info-color,#2196f3)"},a=Array.isArray(t.alerts)?t.alerts:[],r=a.length?a.map(f=>`
    + ${d(this._pgAlertLabel(f.code))} +
    ${d(f.detail_key?this._tText(f.detail_key,f.detail_params||{},f.detail||""):f.detail||"")}
    +
    `).join(""):`
    \u2713 ${this._t("msg.pg_no_alerts",{},"No issues detected in this run.")}
    `,i=e.termination_reason?String(e.termination_reason):"\u2014",o=e.final_duration_s?Math.round(e.final_duration_s/60)+" min":"\u2014",n=e.projected_energy_wh!=null?e.projected_energy_wh>=1e3?it(e.projected_energy_wh/1e3,2)+" kWh":it(Math.round(e.projected_energy_wh),0)+" Wh":"\u2014",l=(t.events||[]).some(f=>String(f.type||"").startsWith("notify_")),c=(f,u)=>`
    ${d(u)}
    ${d(f)}
    `,_=this._pgDetailSig!=null&&this._pgDetailSig!==this._pgOverrideSig(this._pgCurrentOverride())?`
    + \u26A0 ${this._t("msg.pg_outcome_stale",{},"Settings changed since this simulation ran. It still shows the old result.")} + +
    `:"";return`
    ${this._t("hdr.pg_outcome",{},"Simulation outcome")}
    + ${_}
    - ${l(this._t("lbl.pg_ended",{},"Ended"),a)} - ${l(this._t("lbl.duration",{},"Duration"),o)} - ${l(this._t("lbl.pg_proj_energy",{},"Proj. energy"),i)} + ${c(this._t("lbl.pg_ended",{},"Ended"),i)} + ${c(this._t("lbl.duration",{},"Duration"),o)} + ${c(this._t("lbl.pg_proj_energy",{},"Proj. energy"),n)} + ${e.label_reason?c(this._t("lbl.pg_autolabel",{},"Auto-label"),this._pgLabelVerdict(e).short):""}
    -
    ${n}
    -
    `}_pgAlertLabel(t){return{overrun:this._t("lbl.pg_alert_overrun",{},"Overrun"),underrun:this._t("lbl.pg_alert_underrun",{},"Underrun"),did_not_finish:this._t("lbl.pg_alert_did_not_finish",{},"Did not finish"),false_end:this._t("lbl.pg_alert_false_end",{},"Split into multiple cycles"),unmatched:this._t("lbl.pg_alert_unmatched",{},"Unmatched"),ambiguous:this._t("lbl.pg_alert_ambiguous",{},"Ambiguous match"),energy_anomaly:this._t("lbl.pg_alert_energy",{},"Energy anomaly"),timeout_end:this._t("lbl.pg_alert_timeout_end",{},"Ended by timeout, not prediction"),would_run_indefinitely:this._t("lbl.pg_alert_indefinite",{},"Would run indefinitely"),stress_terminated:this._t("lbl.pg_alert_stress_ok",{},"Idle termination: cycle stopped"),stress_above_threshold:this._t("lbl.pg_alert_stress_warn",{},"Idle draw above stop threshold"),stress_hit_cap:this._t("lbl.pg_alert_stress_cap",{},"Hit safety cap")}[t]||t}_htmlPgHistoryMode(){const t=this._busy.has("pg-history"),e=this._pgHistory,s=Object.keys(this._pgParamOverrides||{}).length>0||this._pgThreshStart!=null||this._pgThreshStop!=null,r=`
    +
    ${r}
    + ${l?`

    \u{1F514} ${this._t("msg.pg_notify_partial",{},"Markers show start, almost-done, finish, milestone and quiet-hours notifications only. Live updates, unload reminders and overrun alerts are not simulated.")}

    `:""} +
    `}_pgLabelVerdict(t){if(t.would_label&&t.label_profile)return{ok:!0,short:String(t.label_profile),tip:this._tText("msg.pg_label_yes",{name:t.label_profile},`Would be labelled "${t.label_profile}" at cycle end`)};const s={no_winner:this._tText("pg_label_reason.no_winner",{},"No program fit"),below_floor:this._tText("pg_label_reason.below_floor",{},"Below the learning confidence"),ambiguous:this._tText("pg_label_reason.ambiguous",{},"Flagged ambiguous"),margin:this._tText("pg_label_reason.margin",{},"Too close to the runner-up"),unknown_profile:this._tText("pg_label_reason.unknown_profile",{},"Program no longer exists")}[t.label_reason]||this._tText("pg_label_reason.not_evaluated",{},"Not evaluated");return{ok:!1,short:this._tText("lbl.pg_label_review",{},"Asks for review"),tip:this._tText("msg.pg_label_no",{reason:s},`Would ask for review: ${s}`)}}_pgAlertLabel(t){return{overrun:this._t("lbl.pg_alert_overrun",{},"Overrun"),underrun:this._t("lbl.pg_alert_underrun",{},"Underrun"),did_not_finish:this._t("lbl.pg_alert_did_not_finish",{},"Did not finish"),false_end:this._t("lbl.pg_alert_false_end",{},"Split into multiple cycles"),unmatched:this._t("lbl.pg_alert_unmatched",{},"Unmatched"),ambiguous:this._t("lbl.pg_alert_ambiguous",{},"Ambiguous match"),energy_anomaly:this._t("lbl.pg_alert_energy",{},"Energy anomaly"),timeout_end:this._t("lbl.pg_alert_timeout_end",{},"Ended by timeout, not prediction"),would_run_indefinitely:this._t("lbl.pg_alert_indefinite",{},"Would run indefinitely")}[t]||t}_htmlPgHistoryMode(){const t=this._busy.has("pg-history"),e=this._pgHistory,s=Object.keys(this._pgParamOverrides||{}).length>0||this._pgThreshStart!=null||this._pgThreshStop!=null,a=`
    ${this._t("lbl.last",{},"Last")} - + ${this._t("lbl.cycles_lc",{},"cycles")} ${s?`\u2699 ${this._t("msg.pg_override_active",{},"Using your edited settings vs. current")}`:""} -
    `,n=t?this._htmlPgBatchBar():"",a=`

    ${this._t("msg.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.")}

    `;if(!e||!Array.isArray(e.rows))return`${a}${r}${this._htmlPgRecentRuns("pg_history")}${n}${t?"":`
    ${this._t("msg.pg_history_empty",{},"Press Run to replay your cycles.")}
    `}`;let o="";if(e.diff){const u=($,S,x)=>`${$} ${d(x)}`;o=`
    +
    `,r=t?this._htmlPgBatchBar():"",i=`

    ${this._t("msg.pg_history_intro2",{},"Replay recent cycles through the real detector and matcher with the settings above. Click a row to load it in the graph; change a setting to compare before and after.")}

    `;if(!e||!Array.isArray(e.rows))return`${i}${a}${this._htmlPgRecentRuns("pg_history")}${r}${t?"":`
    ${this._t("msg.pg_history_empty",{},"Press Run to replay your cycles.")}
    `}`;let o="";if(e.diff){const u=(b,v,T)=>`${b} ${d(T)}`;o=`
    ${u((e.diff.newly_correct||[]).length,"var(--success-color,#4caf50)",this._t("lbl.pg_newly_correct",{},"newly correct"))} ${u((e.diff.regressed||[]).length,"var(--error-color,#f44336)",this._t("lbl.pg_regressed",{},"regressed"))} ${u((e.diff.end_timing_changed||[]).length,"var(--warning-color,#ff9800)",this._t("lbl.pg_end_timing_changed",{},"end-timing changed"))} -
    `}const i=e.summary||{},l=`
    ${this._t("msg.pg_history_summary",{detected:i.detected,correct:i.match_correct,total:i.cycles},`${i.detected}/${i.cycles} detected \xB7 ${i.match_correct} matched correctly`)}
    `,c=u=>(this._cycles||[]).find($=>$.id===u),g={};(e.baseline_rows||[]).forEach(u=>{g[u.cycle_id]=u});const p=e.rows.map(u=>{const $=c(u.cycle_id),S=$&&$.start_time?ot($.start_time):(u.cycle_id||"").slice(0,8),x=u.match_correct===!0?"\u2713":u.match_correct===!1?"\u2717":"\u2014",w=u.match_correct===!0?"var(--success-color,#4caf50)":u.match_correct===!1?"var(--error-color,#f44336)":"var(--secondary-text-color)",v=u.matched_profile||this._t("lbl.unlabelled",{},"Unlabelled"),k=u.duration_s?Math.round(u.duration_s/60)+"m":"\u2014",D=u.overrun_ratio!=null?` (${Math.round(u.overrun_ratio*100)}%)`:"",I=u.alerts&&u.alerts.length?` \u26A0`:"",z=g[u.cycle_id];let R="";return z&&(z.match_correct!==!0&&u.match_correct===!0?R=`\u25B2 ${this._t("lbl.pg_fixed",{},"fixed")}`:z.match_correct===!0&&u.match_correct!==!0?R=`\u25BC ${this._t("lbl.pg_broke",{},"broke")}`:String(z.termination_reason)!==String(u.termination_reason)&&(R=`${d(String(z.termination_reason||"\u2014"))}\u2192${d(String(u.termination_reason||"\u2014"))}`)),` - ${d(S)} - ${x} ${d(v)} +
    `}const n=e.summary||{},l=`
    ${this._t("msg.pg_history_summary",{detected:n.detected,correct:n.match_correct,total:n.cycles},`${n.detected}/${n.cycles} detected \xB7 ${n.match_correct} matched correctly`)}
    `,c=u=>(this._cycles||[]).find(b=>b.id===u),p={};(e.baseline_rows||[]).forEach(u=>{p[u.cycle_id]=u});const _=e.rows.map(u=>{const b=c(u.cycle_id),v=b&&b.start_time?et(b.start_time):u.start_time?et(u.start_time):(u.cycle_id||"").slice(0,8),T=u.match_correct===!0?"\u2713":u.match_correct===!1?"\u2717":"\u2014",C=u.match_correct===!0?"var(--success-color,#4caf50)":u.match_correct===!1?"var(--error-color,#f44336)":"var(--secondary-text-color)",x=u.matched_profile||this._t("lbl.unlabelled",{},"Unlabelled"),$=u.duration_s?Math.round(u.duration_s/60)+"m":"\u2014",L=u.overrun_ratio!=null?` (${Math.round(u.overrun_ratio*100)}%)`:"",D=u.label_reason?this._pgLabelVerdict(u):null,B=D?` ${D.ok?"\u{1F3F7}":"?"}`:"",U=u.alerts&&u.alerts.length?` \u26A0`:"",z=p[u.cycle_id];let M="";return z&&(z.match_correct!==!0&&u.match_correct===!0?M=`\u25B2 ${this._t("lbl.pg_fixed",{},"fixed")}`:z.match_correct===!0&&u.match_correct!==!0?M=`\u25BC ${this._t("lbl.pg_broke",{},"broke")}`:String(z.termination_reason)!==String(u.termination_reason)&&(M=`${d(String(z.termination_reason||"\u2014"))}\u2192${d(String(u.termination_reason||"\u2014"))}`)),` + ${d(v)} + ${T} ${d(x)}${B} ${d(String(u.termination_reason||"\u2014"))} - ${k}${D}${I} - ${e.diff?`${R}`:""} + ${$}${L}${U} + ${e.diff?`${M}`:""} `}).join(""),f=` ${e.diff?``:""} - ${p}
    ${this._t("lbl.cycle",{},"Cycle")} ${this._t("lbl.match",{},"Match")} ${this._t("lbl.pg_ended",{},"Ended")} ${this._t("lbl.duration",{},"Duration")}${this._t("lbl.pg_vs_current",{},"vs current")}
    `;return`${a}${r}${this._htmlPgRecentRuns("pg_history")}${o}${l}${f}`}_htmlPgBatchBar(){const t=this._pgBatchProgress;return t?`
    + ${_}`;return`${i}${a}${this._htmlPgRecentRuns("pg_history")}${o}${l}${this._htmlPgInSampleNote()}${f}`}_htmlPgInSampleNote(){return`

    ${this._t("msg.pg_in_sample_note",{},"These cycles helped build the profiles they are matched against, so results are optimistic: expect slightly fewer correct matches on new cycles.")}

    `}_htmlPgBatchBar(){const t=this._pgBatchProgress;return t?`
    ${t.done}/${t.total} -
    `:""}_pgUpdateBatchBar(t,e){const s=this.shadowRoot;if(!s)return;const r=s.getElementById("wd-pg-batch-fill"),n=s.getElementById("wd-pg-batch-count"),a=e?Math.round(100*t/e):0;r&&(r.style.width=a+"%"),n&&(n.textContent=String(t))}async _pgRunHistory(){const t=this._devices[this._selIdx];if(!t)return;const e=(this._cycles||[]).slice(0,Math.max(1,this._pgSimCycles||20)).map(r=>r.id);if(!e.length){this._showToast(this._t("msg.no_cycles_selected",{},"No cycles available."),"error");return}const s={...this._pgParamOverrides};this._pgThreshStart!=null&&(s.start_threshold_w=this._pgThreshStart),this._pgThreshStop!=null&&(s.stop_threshold_w=this._pgThreshStop),this._pgBatchProgress={done:0,total:e.length},this._busy.add("pg-history"),this._render();try{const r=await this._ws({type:`${y}/start_playground_history`,entry_id:t.entry_id,cycle_ids:e,settings_override:s});if(this._pgHistoryTaskId=r&&r.task_id,this._pgNeedsRestart=!1,!this._pgHistoryTaskId)throw new Error("no task id");this._addProvisionalTask(this._pgHistoryTaskId,"pg_history",t.entry_id,e.length),this._tasksSubscribed||this._pgPollTask(this._pgHistoryTaskId)}catch(r){this._busy.delete("pg-history"),this._pgBatchProgress=null,this._pgIsUnknownCmd(r)?this._pgNeedsRestart=!0:this._showToast(this._t("msg.toast_error",{error:r.message||r},"Error: "+(r.message||r)),"error"),this._render()}}_pgSweepObjectives(){return[["match_accuracy",this._t("lbl.pg_obj_match",{},"Match accuracy"),!1],["end_timing_accuracy",this._t("lbl.pg_obj_endtiming",{},"End-timing accuracy"),!1],["false_end_rate",this._t("lbl.pg_obj_falseend",{},"False-end rate"),!0],["median_overrun",this._t("lbl.pg_obj_overrun",{},"Duration off-target"),!0],["ambiguity_rate",this._t("lbl.pg_obj_ambiguity",{},"Ambiguity rate"),!0]]}_htmlPgSweepMode(){const t=this._busy.has("pg-sweep"),s=this._pgOverrideFields().filter(([,,,,,i])=>i!=="bool").map(([i,l])=>``).join(""),r=this._pgSweepObjectives().map(([i,l])=>``).join(""),n=`

    ${this._t("msg.pg_sweep_intro2",{},"Find the setting that best meets an objective across your recent cycles. Nothing changes until you apply it.")}

    `,a=`
    +
    `:""}_pgUpdateBatchBar(t,e){const s=this.shadowRoot;if(!s)return;const a=s.getElementById("wd-pg-batch-fill"),r=s.getElementById("wd-pg-batch-count"),i=e?Math.round(100*t/e):0;a&&(a.style.width=i+"%"),r&&(r.textContent=String(t))}async _pgRunHistory(){const t=this._devices[this._selIdx];if(!t)return;if(!(this._cycles||[]).length){this._showToast(this._t("msg.no_cycles_selected",{},"No cycles available."),"error");return}const e=Math.max(1,Math.min(St,this._pgSimCycles||20)),s=this._pgCurrentOverride();this._pgBatchProgress={done:0,total:e},this._busy.add("pg-history"),this._render();try{const a=await this._ws({type:`${w}/start_playground_history`,entry_id:t.entry_id,count:e,settings_override:s});if(this._pgHistoryTaskId=a&&a.task_id,this._pgNeedsRestart=!1,!this._pgHistoryTaskId)throw new Error("no task id");this._addProvisionalTask(this._pgHistoryTaskId,"pg_history",t.entry_id,e),this._tasksSubscribed||this._pgPollTask(this._pgHistoryTaskId)}catch(a){this._busy.delete("pg-history"),this._pgBatchProgress=null,this._pgRunFailed(a),this._render()}}_pgRunFailed(t){if(this._pgIsUnknownCmd(t)){this._pgNeedsRestart=!0;return}if((t&&(t.code||t.error&&t.error.code)||"")==="task_busy"){this._showToast(this._t("msg.pg_task_busy",{},"A Test-on-history or Optimize run is already in progress for this device. Wait for it or cancel it."),"error");return}this._showToast(this._tText("msg.toast_error",{error:t.message||t},"Error: "+(t.message||t)),"error")}_pgSweepObjectives(){return[["match_accuracy",this._t("lbl.pg_obj_match",{},"Match accuracy"),!1],["end_timing_accuracy",this._t("lbl.pg_obj_endtiming",{},"End-timing accuracy"),!1],["false_end_rate",this._t("lbl.pg_obj_falseend",{},"False-end rate"),!0],["median_overrun",this._t("lbl.pg_obj_overrun",{},"Duration off-target"),!0],["ambiguity_rate",this._t("lbl.pg_obj_ambiguity",{},"Ambiguity rate"),!0],["end_lag",this._t("lbl.pg_obj_endlag",{},"End delay (median)"),!0],["early_end_rate",this._t("lbl.pg_obj_early",{},"Early-end rate"),!0],["split_rate",this._t("lbl.pg_obj_split",{},"Split rate"),!0]]}_htmlPgSweepMode(){const t=this._busy.has("pg-sweep"),s=this._pgOverrideFields().filter(([,,,,,n])=>n!=="bool").map(([n,l])=>``).join(""),a=this._pgSweepObjectives().map(([n,l])=>``).join(""),r=`

    ${this._t("msg.pg_sweep_intro2",{},"Find the setting that best meets an objective across your recent cycles. Nothing changes until you apply it.")}

    `,i=`
    -
    +
    +
    -
    `,o=t?this._htmlPgBatchBar():"";return`${n}${a}${this._htmlPgRecentRuns("pg_sweep")}${o}${this._htmlPgSweepResult()}`}_htmlPgSweepResult(){const t=this._pgSweepNew;if(!t||!Array.isArray(t.points)||!t.points.length)return"";const e=this._pgSweepObjectives().find(p=>p[0]===t.objective),s=e?e[2]:!1,r=t.points.filter(p=>p.metric!=null).map(p=>p.metric);if(!r.length)return`
    ${this._t("msg.pg_sweep_no_metric",{},"Not enough data to score this objective.")}
    `;const n=Math.min(...r),a=Math.max(...r),o=s?Math.min(...r):Math.max(...r),i=(t.points.find(p=>p.metric===o)||{}).value,l=p=>t.objective==="median_overrun"?Math.round(p*100)+"% off":Math.round(p*100)+"%",c=t.points.map(p=>{const f=a>n&&p.metric!=null?(p.metric-n)/(a-n):p.metric!=null?1:0,u=p.metric===o,$=t.current_value!=null&&Math.abs(p.value-t.current_value)<1e-6,S=u?"var(--success-color,#4caf50)":"var(--primary-color)";return`
    - ${d(String(p.value))}${$?" \u25C0":""} -
    - ${p.metric!=null?l(p.metric):"\u2014"} -
    `}).join(""),g=this._canEdit()&&i!=null?``:"";return`
    ${this._t("lbl.pg_best_value",{},"Best value found")}: ${d(String(i))} \xB7 ${l(o)} \xB7 \u25C0 ${this._t("lbl.pg_current_value",{},"Current value:")}
    - ${c} -
    ${g}
    `}async _pgRunSweep2(){const t=this._devices[this._selIdx];if(!t)return;const e=parseFloat(this._pgSweepFrom),s=parseFloat(this._pgSweepTo);if(isNaN(e)||isNaN(s)||e===s){this._showToast(this._t("msg.toast_range_required",{},"Set a valid From/To range."),"error");return}const r=Math.max(2,Math.min(12,this._pgSweepSteps||5)),n=Array.from({length:r},(l,c)=>+(e+(s-e)*c/(r-1)).toFixed(3)),a=this._pgSweepParam||"off_delay",o=this._pgSweepObjective||"match_accuracy",i={type:`${y}/start_playground_sweep`,entry_id:t.entry_id,param:a,values:n,objective:o};this._pgBatchProgress={done:0,total:n.length},this._busy.add("pg-sweep"),this._render();try{const l=await this._ws(i);if(this._pgSweepTaskId=l&&l.task_id,this._pgNeedsRestart=!1,!this._pgSweepTaskId)throw new Error("no task id");this._addProvisionalTask(this._pgSweepTaskId,"pg_sweep",t.entry_id,n.length),this._tasksSubscribed||this._pgPollTask(this._pgSweepTaskId)}catch(l){this._busy.delete("pg-sweep"),this._pgBatchProgress=null,this._pgIsUnknownCmd(l)?this._pgNeedsRestart=!0:this._showToast(this._t("msg.toast_error",{error:l.message||l},"Error: "+(l.message||l)),"error"),this._render()}}async _pgApplyToSettings(){const t=this._devices[this._selIdx];if(!t||!this._canEdit())return;const e={};for(const n of this._pgChangedKeys())this._pgIsPublishable(n)&&(e[n]=this._pgStagedVal(n));const s=Object.keys(e);if(!s.length)return;const r=s.map(n=>this._t("setting."+n+".label",{},n)).join(", ");confirm(this._t("msg.pg_apply_settings_confirm",{n:s.length,list:r},`Save these ${s.length} setting(s) to this device? ${r}`))&&(await this._busyRun("pg-apply-settings",async()=>{try{if(await this._ws({type:`${y}/set_options`,entry_id:t.entry_id,options:e}),!this._isActiveEntry(t.entry_id))return;this._opts={...this._opts,...e},this._pgEffective&&(this._pgEffective={...this._pgEffective,...e});for(const n of s)this._pgClearStaged(n);this._showToast(this._t("toast.settings_saved",{},"Settings saved; integration reloading"))}catch(n){this._showToast(this._t("msg.toast_save_failed",{error:n.message||n},"Save failed: "+(n.message||n)),"error")}}),this._render())}async _pgApplySweepValue(t){const e=this._devices[this._selIdx];if(!e||!this._canEdit()||t==null)return;const s=this._pgSweepParam,r=this._t("setting."+s+".label",{},s);confirm(this._t("msg.pg_apply_confirm",{label:r,value:t},"Apply best value: "+r+" = "+t+"?"))&&await this._busyRun("pg-sweep-apply",async()=>{try{await this._ws({type:`${y}/set_options`,entry_id:e.entry_id,options:{[s]:+t}}),this._opts={...this._opts,[s]:+t},s==="start_threshold_w"?this._pgThreshStart=+t:s==="stop_threshold_w"?this._pgThreshStop=+t:this._pgParamOverrides[s]=+t,this._showToast(this._t("toast.settings_saved",{},"Settings saved; integration reloading")),this._render()}catch(n){this._showToast(this._t("msg.toast_save_failed",{error:n.message||n},"Save failed: "+(n.message||n)),"error")}})}_htmlPgStrip(){return`
    +
    `,o=t?this._htmlPgBatchBar():"";return`${r}${i}${this._htmlPgRecentRuns("pg_sweep")}${o}${this._htmlPgSweepResult()}`}_htmlPgSweepResult(){const t=this._pgSweepNew;if(!t||!Array.isArray(t.points)||!t.points.length)return"";const e=this._pgSweepObjectives().find(x=>x[0]===t.objective),s=t.lower_is_better!=null?!!t.lower_is_better:e?e[2]:!1,a=t.points.filter(x=>x.metric!=null).map(x=>x.metric);if(!a.length)return`
    ${this._t("msg.pg_sweep_no_metric",{},"Not enough data to score this objective.")}
    `;const r=Math.min(...a),i=Math.max(...a),o=t.best_value!==void 0,n=s?r:i,l=o?t.best_value:(t.points.find(x=>x.metric===n)||{}).value,c=o?t.best_metric:n,p=!!t.keep_current,_=(x,$)=>x!=null&&$!=null&&Math.abs(Number(x)-Number($))<1e-6,f=x=>x==null?"-":t.objective==="median_overrun"?Math.round(x*100)+"% off":t.objective==="end_lag"?`${it(x/60,1)} ${this._tText("lbl.timer_min",{},"min")}`:Math.round(x*100)+"%",u=t.points.map(x=>{const $=i>r&&x.metric!=null?(x.metric-r)/(i-r):x.metric!=null?1:0,L=!p&&_(x.value,l),D=_(x.value,t.current_value),B=L?"var(--success-color,#4caf50)":"var(--primary-color)",U=x.guarded?` \u26A0`:"";return`
    + ${d(String(x.value))}${D?" \u25C0":""}${U} +
    + ${f(x.metric)} +
    `}).join(""),b=!p&&l!=null&&!_(l,t.current_value),v=this._canEdit()&&b?``:"",T=t.cycles?` \xB7 ${this._t("msg.pg_sweep_n_cycles",{count:t.cycles},`over ${t.cycles} cycles`)}`:"";return`
    ${p?`${this._t("msg.pg_sweep_keep_current",{value:String(t.current_value??"-"),metric:f(c)},`Keep the current value (${t.current_value??"-"}, ${f(c)}): no value did better by at least one cycle.`)}`:`${this._t("lbl.pg_best_value",{},"Best value found")}: ${d(String(l))} \xB7 ${d(f(c))}`}${T} \xB7 \u25C0 ${this._t("lbl.pg_current_value",{},"Current value:")}
    + ${u} +
    ${this._htmlPgInSampleNote()}${v}
    `}async _pgRunSweep2(){const t=this._devices[this._selIdx];if(!t)return;const e=parseFloat(this._pgSweepFrom),s=parseFloat(this._pgSweepTo);if(isNaN(e)||isNaN(s)||e===s){this._showToast(this._t("msg.toast_range_required",{},"Set a valid From/To range."),"error");return}const a=Math.max(2,Math.min(12,this._pgSweepSteps||5)),r=Array.from({length:a},(c,p)=>+(e+(s-e)*p/(a-1)).toFixed(3)),i=this._pgSweepParam||"off_delay",o=this._pgSweepObjective||"match_accuracy",n=Math.max(1,Math.min(St,this._pgSimCycles||20)),l={type:`${w}/start_playground_sweep`,entry_id:t.entry_id,param:i,values:r,objective:o,count:n};this._pgBatchProgress={done:0,total:r.length+1},this._busy.add("pg-sweep"),this._render();try{const c=await this._ws(l);if(this._pgSweepTaskId=c&&c.task_id,this._pgNeedsRestart=!1,!this._pgSweepTaskId)throw new Error("no task id");this._addProvisionalTask(this._pgSweepTaskId,"pg_sweep",t.entry_id,r.length+1),this._tasksSubscribed||this._pgPollTask(this._pgSweepTaskId)}catch(c){this._busy.delete("pg-sweep"),this._pgBatchProgress=null,this._pgRunFailed(c),this._render()}}async _pgApplyToSettings(){const t=this._devices[this._selIdx];if(!t||!this._canEdit())return;const e={};for(const r of this._pgChangedKeys())this._pgIsPublishable(r)&&(e[r]=this._pgStagedVal(r));const s=Object.keys(e);if(!s.length)return;const a=s.map(r=>this._t("setting."+r+".label",{},r)).join(", ");confirm(this._tText("msg.pg_apply_settings_confirm",{n:s.length,list:a},`Save these ${s.length} setting(s) to this device? ${a}`))&&(await this._busyRun("pg-apply-settings",async()=>{try{if(await this._ws({type:`${w}/set_options`,entry_id:t.entry_id,options:e}),!this._isActiveEntry(t.entry_id))return;this._opts={...this._opts,...e},this._pgEffective&&(this._pgEffective={...this._pgEffective,...e});for(const r of s)this._pgClearStaged(r);this._showToast(this._t("toast.settings_saved",{},"Settings saved; integration reloading"))}catch(r){this._showToast(this._tText("msg.toast_save_failed",{error:r.message||r},"Save failed: "+(r.message||r)),"error")}}),this._render())}async _pgApplySweepValue(t){const e=this._devices[this._selIdx];if(!e||!this._canEdit()||t==null)return;const s=this._pgSweepParam,a=this._t("setting."+s+".label",{},s);confirm(this._tText("msg.pg_apply_confirm",{label:a,value:t},"Apply best value: "+a+" = "+t+"?"))&&await this._busyRun("pg-sweep-apply",async()=>{try{await this._ws({type:`${w}/set_options`,entry_id:e.entry_id,options:{[s]:+t}}),this._opts={...this._opts,[s]:+t},s==="start_threshold_w"?this._pgThreshStart=+t:s==="stop_threshold_w"?this._pgThreshStop=+t:this._pgParamOverrides[s]=+t,this._showToast(this._t("toast.settings_saved",{},"Settings saved; integration reloading")),this._render()}catch(r){this._showToast(this._tText("msg.toast_save_failed",{error:r.message||r},"Save failed: "+(r.message||r)),"error")}})}_htmlPgStrip(){return`
    ${this._t("lbl.pg_idle",{},"Idle")} ${this._t("lbl.power",{},"Power")} \u2014 \u2014% - ${this._t("lbl.pg_time_left_model",{},"Time left (model)")} \u2014 + ${this._t("lbl.pg_time_left_model",{},"Time left (model)")} \u2014 ${this._t("lbl.energy",{},"Energy")} \u2014 ${this._t("lbl.match",{},"Match")} \u2014 ${this._t("lbl.phase",{},"Phase")} \u2014 -
    `}_htmlPgAnalysis(){const t=this._pgDtwData,e=(i,l,c,g,p)=>{const f=c&&l!=null?Math.max(0,Math.min(1,l/c)):l!=null?Math.max(0,Math.min(1,l)):0;return`
    - ${d(i)} -
    - ${p!=null?d(String(p)):"\u2014"} -
    `};let s="";if(t&&(t.stage2||t.stage4)){const i=t.stage2||{},l=t.stage4||{},c=t.dtw||{},g=l.final_score??c.blended_score??i.score,p=this._pgDetail&&this._pgDetail.outcome&&this._pgDetail.outcome.confidence,f=p??g;let u="\u2014",$="var(--secondary-text-color)";f!=null&&(f>=.7?(u="\u2705 "+this._t("lbl.pg_strong_match",{},"Strong match"),$="var(--success-color, #4caf50)"):f>=.4?(u="\u26A0 "+this._t("lbl.pg_weak_match",{},"Weak match"),$="var(--warning-color, #ff9800)"):(u="\u274C "+this._t("lbl.pg_poor_match",{},"Poor match"),$="var(--error-color, #f44336)"));const S=t.profile_name||this._pgProfileName||"\u2014";s+=`
    ${u}
    `,S!=="\u2014"&&(s+=`
    ${d(S)} \xB7 ${d(this._t("lbl.score",{},"score"))} ${g!=null?g.toFixed(3):"\u2014"}
    `),s+=e(this._t("lbl.correlation",{},"Correlation"),i.correlation,1,"#42a5f5",i.correlation!=null?i.correlation.toFixed(2):null),c.blended_score!=null&&(s+=e(this._t("lbl.pg_dtw",{},"DTW"),c.blended_score,1,"#ab47bc",c.blended_score.toFixed(2))),l.duration_agreement!=null&&(s+=e(this._t("lbl.duration",{},"Duration"),l.duration_agreement,1,"#66bb6a",l.duration_agreement.toFixed(2))),l.energy_agreement!=null&&(s+=e(this._t("lbl.energy",{},"Energy"),l.energy_agreement,1,"#ffa726",l.energy_agreement.toFixed(2))),s+='
    '}else t||(s+=`

    ${this._t("msg.pg_analysis_empty2",{},"Press Run to see match analysis.")}

    `);const r=this._pgDetail&&this._pgDetail.outcome,n=r&&r.matched_profile||t&&t.profile_name||this._pgProfileName||(this._cycles||[]).find(i=>i.id===this._pgCycleId)?.profile_name||"",a=r&&r.confidence;if(n&&a!=null){const i=Math.round(Math.max(0,Math.min(1,a))*100);s+=`
    ${d(this._t("lbl.pg_match_confidence",{},"Match confidence"))}
    `,s+=`
    - ${d(n)} -
    - ${i}% -
    `}const o=t&&t.stage4&&t.stage4.final_score;if(o!=null){const i=Math.round(Math.max(0,Math.min(1,o))*100);s+=`
    ${d(this._t("lbl.pg_envelope_fit",{},"Envelope fit"))}
    `,s+=`
    -
    - ${i}% -
    `}return`
    ${s||`

    ${this._t("msg.pg_analysis_hint2",{},"Pick a cycle and press Run to load match analysis.")}

    `}
    `}async _pgLoad(){const t=this._devices[this._selIdx];if(!t||this._pgLoading)return;const e=this._pgCycleId||this._cycles?.[0]?.id||"";if(!e)return;this._pgCycleId=e,this._pgLoading=!0,this._pgView=null,this._pgHoverT=null,this._pgPowerPts=null,this._pgDtwData=null,this._pgEnvData=null,this._pgDetail=null;const s=++this._pgLoadSeq;this._render();try{const r=await this._ws({type:`${y}/get_cycle_power_data`,entry_id:t.entry_id,cycle_id:e}),n=r.samples||[],a=[];for(const l of n){if(!Array.isArray(l)||l.length<2)continue;const c=+l[0],g=+l[1];!isNaN(c)&&!isNaN(g)&&a.push({t:c,w:g})}if(this._pgPowerPts=a.length?a:null,typeof r.full_duration_s=="number"&&r.full_duration_s>0){const l=(this._cycles||[]).find(c=>c.id===e);l&&(l._pg_duration=r.full_duration_s)}const o=this._pgProfileName||(this._cycles||[]).find(l=>l.id===e)?.profile_name||"";if(a.length)try{const l={type:`${y}/get_dtw_debug`,entry_id:t.entry_id,cycle_id:e};o&&(l.profile_name=o),this._pgDtwData=await this._ws(l),this._pgNeedsRestart=!1}catch(l){this._pgIsUnknownCmd(l)&&(this._pgNeedsRestart=!0),this._pgDtwData=null}const i=this._pgDtwData?.profile_name||o;if(i)try{const l=await this._ws({type:`${y}/get_profile_envelope`,entry_id:t.entry_id,profile_name:i});this._pgEnvData=l.envelope||null}catch{this._pgEnvData=null}if(s!==this._pgLoadSeq)return;await this._pgLoadDetail(t.entry_id,e)}catch(r){this._showToast(this._t("msg.toast_error",{error:r.message||r},"Error: "+(r.message||r)),"error")}s===this._pgLoadSeq&&(this._pgLoading=!1,this._render(),requestAnimationFrame(()=>{this._pgDrawCanvas(),this._pgUpdateStripAt(null)}),this._pgNeedsRestart&&this._tab==="playground"?(this._pgRestartRetries||0)<5&&!this._pgRestartRetryTimer&&(this._pgRestartRetries=(this._pgRestartRetries||0)+1,this._pgRestartRetryTimer=setTimeout(()=>{this._pgRestartRetryTimer=null,this._pgNeedsRestart&&this._tab==="playground"&&this._pgLoad()},3e3)):this._pgRestartRetries=0)}_pgCancelRun(){this._pgLoadSeq++,this._pgLoading=!1,this._render()}_pgSelectCycle(t){t&&(this._pgCycleId=t,this._pgProfileName="",this._pgView=null,this._pgHoverT=null,this._pgLoading=!1,this._pgLoad(),requestAnimationFrame(()=>{const e=this.shadowRoot&&this.shadowRoot.getElementById("wd-pg-canvas");e&&typeof e.scrollIntoView=="function"&&e.scrollIntoView({behavior:"smooth",block:"center"})}))}async _pgLoadDetail(t,e){const s={...this._pgParamOverrides};this._pgThreshStart!=null&&(s.start_threshold_w=this._pgThreshStart),this._pgThreshStop!=null&&(s.stop_threshold_w=this._pgThreshStop);try{const r=await this._ws({type:`${y}/start_playground_cycle_detail`,entry_id:t,cycle_id:e,settings_override:s,stress_tail:this._pgStressTail,stress_idle_w:this._pgStressIdleW!=null?parseFloat(this._pgStressIdleW):null}),n=r&&r.task_id;if(!n)throw new Error("no task id");this._addProvisionalTask(n,"pg_detail",t,0);const a=await new Promise(o=>{this._taskCallbacks[n]=async l=>{if(l.state==="done"||l.state==="cancelled")try{const c=await this._ws({type:`${y}/get_task_result`,task_id:l.id});o(c&&c.result)}catch{o(null)}else o(null)};const i=this._tasks[n];i&&i.state!=="running"?this._settleTaskCallback(i):this._tasksSubscribed||this._pollTaskGeneric(n)});if(!this._isActiveEntry(t))return;this._pgDetail=a&&!a.error?a:null,this._pgNeedsRestart=!1}catch(r){this._pgIsUnknownCmd(r)&&(this._pgNeedsRestart=!0),this._pgDetail=null}}_pgRerunDetail(){const t=this._devices[this._selIdx];!t||!this._pgCycleId||(clearTimeout(this._pgDetailDebounceTimer),this._pgDetailDebounceTimer=setTimeout(async()=>{this._pgDetailBusy=!0,this._render(),await this._pgLoadDetail(t.entry_id,this._pgCycleId),this._pgDetailBusy=!1,this._render(),requestAnimationFrame(()=>this._pgDrawCanvas())},220))}_pgMapState(t){return t==="running"||t==="paused"?"running":t==="ending"?"ending":t==="starting"?"detecting":t==="anti_wrinkle"?"anti_wrinkle":"idle"}_pgSeriesAt(t){const e=this._pgDetail&&this._pgDetail.series;if(!e||!e.length)return null;let s=e[0];for(const r of e)if(r.t<=t)s=r;else break;return s}_pgStateSegsFromSeries(t){const e=this._pgDetail&&this._pgDetail.series;if(!e||!e.length)return[];const s=[];let r=null;for(const n of e){const a=this._pgMapState(n.state);!r||r.state!==a?(r={start:n.t,end:n.t,state:a},s.push(r)):r.end=n.t}return s.length&&(s[s.length-1].end=t),s}_pgDrawCanvas(){if(this._tab!=="playground")return;const t=this.shadowRoot,e=t&&t.getElementById("wd-pg-canvas");if(!e)return;const s=this._pgPowerPts,r=e.getBoundingClientRect(),n=window.devicePixelRatio||1,a=Math.max(1,Math.round(r.width*n)),o=Math.max(1,Math.round((r.height||280)*n));(e.width!==a||e.height!==o)&&(e.width=a,e.height=o);const i=e.getContext("2d"),l=getComputedStyle(this),c=(l.getPropertyValue("--primary-color")||"#03a9f4").trim(),g=(l.getPropertyValue("--divider-color")||"rgba(127,127,127,.2)").trim(),p=(l.getPropertyValue("--secondary-text-color")||"#888").trim(),f=(l.getPropertyValue("--secondary-background-color")||"#1a1a1a").trim();i.clearRect(0,0,a,o);const u=34*n,$=14*n,S=jt*n,x=44*n,w=8*n,v=S+8*n,k=u+$+4*n,D=o-v-k;if(!s||!s.length){this._pgLoading&&(i.fillStyle=p,i.font=`${12*n}px sans-serif`,i.textAlign="center",i.textBaseline="middle",i.fillText(this._t("msg.loading",{},"Loading\u2026"),a/2,o/2));return}const I=this._pgDetail&&this._pgDetail.outcome&&this._pgDetail.outcome.stress,z=I&&I.enabled?I.synthetic_from_s:null,R=z!=null&&this._pgDetail&&this._pgDetail.series?this._pgDetail.series.filter(T=>T.t>=z):[];let L=(this._cycles||[]).find(T=>T.id===this._pgCycleId)?._pg_duration||s[s.length-1].t||1;I&&I.terminated&&I.terminated_after_s!=null&&z!=null?L=Math.max(L,z+I.terminated_after_s):R.length&&(L=Math.max(L,R[R.length-1].t));const P=Math.max(...s.map(T=>T.w),...R.map(T=>T.power||0),1),C=this._pgThreshStart??this._pgFieldVal("start_threshold_w",{})??50,A=this._pgThreshStop??this._pgFieldVal("stop_threshold_w",{})??5;let F=0,j=L;this._pgView&&this._pgView.max-this._pgView.min>1&&(F=Math.max(0,this._pgView.min),j=Math.min(L,this._pgView.max),j-F<=1&&(F=0,j=L));const st=Math.max(1e-6,j-F),J=a-x-w,X=T=>x+(T-F)/st*J,tt=T=>v+(1-Math.max(0,T)/P)*D;this._pgMap={vMin:F,vMax:j,totalDur:L,padLpx:x/n,plotWpx:J/n},i.strokeStyle="rgba(127,127,127,0.12)",i.lineWidth=n,i.setLineDash([]);const _t=ke(P,5),Z=[];for(let T=1;T*_t<=P*1.0001;T++)Z.push(T*_t);Z.forEach(T=>{const B=tt(T);Bv+D||(i.beginPath(),i.moveTo(x,B),i.lineTo(a-w,B),i.stroke(),i.fillStyle=p,i.font=`${9*n}px sans-serif`,i.textAlign="right",i.textBaseline="middle",i.fillText(Vt(T,_t),x-4*n,B))}),i.save(),i.beginPath(),i.rect(x,v,J,D),i.clip();const lt=this._pgEnvData,Q=lt&&Array.isArray(lt.avg)?lt.avg.filter(T=>Array.isArray(T)&&T.length>=2):[];if(Q.length){const T=Math.max(...Q.map(H=>H[0]))||1,B=H=>X(H/T*L),Y=Array.isArray(lt.min)?lt.min.filter(H=>Array.isArray(H)&&H.length>=2):[],W=Array.isArray(lt.max)?lt.max.filter(H=>Array.isArray(H)&&H.length>=2):[];if(Y.length&&W.length){i.beginPath(),W.forEach((H,ft)=>ft?i.lineTo(B(H[0]),tt(H[1])):i.moveTo(B(H[0]),tt(H[1])));for(let H=Y.length-1;H>=0;H--)i.lineTo(B(Y[H][0]),tt(Y[H][1]));i.closePath(),i.fillStyle="#eda10012",i.fill()}else i.beginPath(),i.strokeStyle="#eda100",i.lineWidth=2*n,i.setLineDash([5*n,4*n]),Q.forEach((H,ft)=>ft?i.lineTo(B(H[0]),tt(H[1])):i.moveTo(B(H[0]),tt(H[1]))),i.stroke(),i.setLineDash([])}const nt=this._pgDtwData,ht=nt&&Array.isArray(nt.profile_trace)?nt.profile_trace.filter(T=>Array.isArray(T)&&T.length>=2):[],pt=nt&&Array.isArray(nt.cycle_trace)?nt.cycle_trace.filter(T=>Array.isArray(T)&&T.length>=2):[],ut=nt&&Array.isArray(nt.warp_path)?nt.warp_path:[];if(pt.length&&ht.length&&ut.length){const T=Math.max(...ht.map(W=>W[0]))||1,B=Math.max(...pt.map(W=>W[0]))||1,Y=Math.max(1,Math.floor(ut.length/25));i.save(),i.globalAlpha=.13,i.strokeStyle="#fff",i.lineWidth=n;for(let W=0;WB[0]))||1;i.beginPath(),i.strokeStyle="#eda100",i.lineWidth=2*n,i.setLineDash([6*n,4*n]),ht.forEach((B,Y)=>{const W=X(B[0]/T*L),H=tt(B[1]);Y?i.lineTo(W,H):i.moveTo(W,H)}),i.stroke(),i.setLineDash([])}if(i.beginPath(),i.moveTo(X(0),tt(0)),s.forEach(T=>i.lineTo(X(T.t),tt(T.w))),i.lineTo(X(s[s.length-1].t),tt(0)),i.closePath(),i.fillStyle=At(c,.1),i.fill(),i.beginPath(),i.strokeStyle=c,i.lineWidth=2*n,s.forEach((T,B)=>B?i.lineTo(X(T.t),tt(T.w)):i.moveTo(X(T.t),tt(T.w))),i.stroke(),R.length){const T=s[s.length-1];i.beginPath(),i.moveTo(X(T.t),tt(T.w)),R.forEach(B=>i.lineTo(X(B.t),tt(B.power))),i.lineTo(X(R[R.length-1].t),tt(0)),i.lineTo(X(T.t),tt(0)),i.closePath(),i.fillStyle=At(c,.06),i.fill(),i.beginPath(),i.strokeStyle=c,i.lineWidth=1.5*n,i.setLineDash([4*n,4*n]),i.moveTo(X(T.t),tt(T.w)),R.forEach(B=>i.lineTo(X(B.t),tt(B.power))),i.stroke(),i.setLineDash([])}if(z!=null&&z{const W=tt(T);Wv+D+2||(i.save(),i.strokeStyle=B,i.lineWidth=2*n,i.setLineDash([8*n,4*n]),i.beginPath(),i.moveTo(x,W),i.lineTo(a-w,W),i.stroke(),i.setLineDash([]),i.fillStyle=B,i.beginPath(),i.arc(x+(a-x-w)*.06,W,5*n,0,Math.PI*2),i.fill(),i.fillStyle=B,i.font=`bold ${9*n}px sans-serif`,i.textAlign="left",i.textBaseline="bottom",i.fillText(Y+" "+Vt(T),x+14*n,W-2*n),i.restore())};m(+C,"#2a78d6",this._t("lbl.start",{},"Start")),m(+A,"#e34948",this._t("btn.stop",{},"Stop"));const _={idle:f,detecting:"#42a5f566",running:"#66bb6a66",ending:"#ef535066",anti_wrinkle:"#ab47bc66"},b={idle:this._t("lbl.pg_idle",{},"Idle"),detecting:this._t("lbl.pg_detecting",{},"Detecting"),running:this._t("lbl.pg_ev_running",{},"Running"),ending:this._t("lbl.pg_ev_ending",{},"Ending"),anti_wrinkle:this._t("lbl.pg_anti_wrinkle",{},"Anti-wrinkle")},E=o-u-$;i.fillStyle=f,i.fillRect(x,E,a-x-w,u),i.save(),i.beginPath(),i.rect(x,E,J,u),i.clip(),this._pgStateSegsFromSeries(L).forEach(T=>{const B=X(T.start),Y=X(T.end);i.fillStyle=_[T.state]||g,i.fillRect(B,E,Math.max(1,Y-B),u),Y-B>50*n&&(i.fillStyle=p,i.font=`${8*n}px sans-serif`,i.textAlign="center",i.textBaseline="middle",i.fillText((b[T.state]||T.state).toUpperCase(),(B+Y)/2,E+u/2))}),i.restore();const K=(this._cycles||[]).find(T=>T.id===this._pgCycleId),N=this._pgDtwData?.profile_name||this._pgProfileName||K?.profile_name,q=N?(this._profiles||[]).find(T=>T.name===N):null,V=o-$;q&&Array.isArray(q.phases)&&q.phases.length&&(i.save(),i.beginPath(),i.rect(x,V,J,$),i.clip(),q.phases.forEach((T,B)=>{const Y=X((T.start||0)*L),W=X((T.end||1)*L),H=B*47%360;i.fillStyle=`hsla(${H},60%,55%,0.55)`,i.fillRect(Y,V,Math.max(1,W-Y),$),W-Y>40*n&&(i.fillStyle="#fff",i.font=`${7*n}px sans-serif`,i.textAlign="center",i.textBaseline="middle",i.fillText(T.name,(Y+W)/2,V+$/2))}),i.restore()),this._pgEventHits=[];const U=this._pgDetail&&Array.isArray(this._pgDetail.events)?this._pgDetail.events.filter(T=>T.type!=="state"&&T.t>=F&&T.t<=j).slice().sort((T,B)=>T.t-B.t):[],M=11*n,G=v-S/2-2*n,et=M*2+3*n;let rt=-1/0;const at=this._pgHoverEvent;if(U.forEach(T=>{const B=this._pgEventMeta(T.type),Y=X(T.t);let W=Math.max(Y,rt+et);W=Math.max(x+M,Math.min(a-w-M,W)),rt=W;const H=at&&at.t===T.t&&at.type===T.type;i.save(),i.strokeStyle=B.color,i.globalAlpha=H?.9:.5,i.lineWidth=(H?2:1)*n,i.beginPath(),i.moveTo(W,G+M),i.lineTo(Y,v),i.stroke(),H&&(i.globalAlpha=.3,i.setLineDash([2*n,3*n]),i.beginPath(),i.moveTo(Y,v),i.lineTo(Y,o-$),i.stroke(),i.setLineDash([])),i.restore(),i.save(),i.beginPath(),i.arc(W,G,M,0,Math.PI*2),i.fillStyle=H?B.color:f||"#1a1a1a",i.fill(),i.lineWidth=(H?2:1.5)*n,i.strokeStyle=B.color,i.stroke(),i.fillStyle=H?"#fff":B.color,i.font=`${14*n}px sans-serif`,i.textAlign="center",i.textBaseline="middle",i.fillText(B.glyph,W,G+n),i.restore(),this._pgEventHits.push({cx:W/n,cy:G/n,r:M/n,type:T.type,label:B.label,detail:T.detail,t:T.t})}),at){const T=this._pgEventHits.find(B=>B.t===at.t&&B.type===at.type);if(T){const B=this._pgEventMeta(T.type),Y=vt=>`${Math.floor(vt/60)}:${String(Math.round(vt%60)).padStart(2,"0")}`,W=this._pgEventDescription(T.type),H=[{t:T.label,bold:!0,col:B.color},...W?[{t:W}]:[],...T.detail?[{t:T.detail,dim:!0}]:[],{t:`${this._t("lbl.from_start",{},"From start")} ${Y(T.t)}`,dim:!0}];i.save(),i.font=`${10*n}px sans-serif`;const ft=Math.min(220*n,a-2*x),yt=[];H.forEach(vt=>{const Rt=String(vt.t).split(" ");let Et="";Rt.forEach(Ht=>{const Jt=Et?Et+" "+Ht:Ht;i.measureText(Jt).width>ft&&Et?(yt.push({...vt,t:Et}),Et=Ht):Et=Jt}),yt.push({...vt,t:Et})});const Tt=14*n,wt=Math.min(ft+14*n,a-8*n),kt=yt.length*Tt+10*n;let St=T.cx*n-wt/2;St=Math.max(4*n,Math.min(a-wt-4*n,St));const Nt=G+M+6*n;i.fillStyle=f,i.globalAlpha=.97,i.fillRect(St,Nt,wt,kt),i.globalAlpha=1,i.strokeStyle=B.color,i.lineWidth=n,i.strokeRect(St,Nt,wt,kt),i.textAlign="left",i.textBaseline="top",yt.forEach((vt,Rt)=>{i.font=`${vt.bold?"600 ":""}${10*n}px sans-serif`,i.fillStyle=vt.col||(vt.dim?p:(l.getPropertyValue("--primary-text-color")||"#ddd").trim()),i.fillText(vt.t,St+7*n,Nt+5*n+Rt*Tt)}),i.restore()}}if(this._pgHoverT!=null&&this._pgHoverT>=F&&this._pgHoverT<=j){const T=X(this._pgHoverT);i.save(),i.strokeStyle="#e34948",i.lineWidth=1.5*n,i.setLineDash([4*n,3*n]),i.beginPath(),i.moveTo(T,v),i.lineTo(T,o-$),i.stroke(),i.setLineDash([]);const B=this._pgInterpPower(s,this._pgHoverT),Y=tt(B);i.fillStyle="#e34948",i.beginPath(),i.arc(T,Y,3.5*n,0,Math.PI*2),i.fill();const W=mt=>`${Math.floor(mt/60)}:${String(Math.round(mt%60)).padStart(2,"0")}`,H=mt=>mt>=1e3?(mt/1e3).toFixed(2)+" kW":Ot(mt),ft=[`${this._t("lbl.from_start",{},"From start")} ${W(this._pgHoverT)}`,`${this._t("lbl.to_end",{},"To end")} ${W(Math.max(0,L-this._pgHoverT))}`,`${this._t("lbl.power",{},"Power")} ${H(B)}`];i.font=`${9.5*n}px sans-serif`;const yt=Math.max(...ft.map(mt=>i.measureText(mt).width))+12*n,Tt=ft.length*13*n+8*n;let wt=T+8*n;wt+yt>a-w&&(wt=T-yt-8*n);const kt=v+4*n;i.fillStyle=f,i.globalAlpha=.95,i.fillRect(wt,kt,yt,Tt),i.globalAlpha=1,i.strokeStyle=g,i.lineWidth=n,i.strokeRect(wt,kt,yt,Tt),i.fillStyle=p,i.textAlign="left",i.textBaseline="top",ft.forEach((mt,St)=>i.fillText(mt,wt+6*n,kt+5*n+St*13*n)),i.restore()}i.fillStyle=p,i.font=`${9*n}px sans-serif`,i.textAlign="center",i.textBaseline="top";const ct=T=>`${Math.floor(T/60)}:${String(Math.round(T%60)).padStart(2,"0")}`;for(let T=0;T<=4;T++){const B=F+st*T/4,Y=X(B);i.textAlign=T===0?"left":T===4?"right":"center",i.fillText(ct(B),Math.max(x,Math.min(a-w,Y)),E+u+1*n)}stL.id===this._pgCycleId)?._pg_duration||e[e.length-1].t||1,n=t??r,a=this._pgInterpPower(e,n),o=this._pgSeriesAt(n),i=o?this._pgMapState(o.state):"idle",l={idle:[this._t("lbl.pg_idle",{},"Idle"),"var(--secondary-background-color)"],detecting:[this._t("lbl.pg_detecting",{},"Detecting"),"#42a5f5"],running:[this._t("lbl.pg_ev_running",{},"Running"),"#66bb6a"],ending:[this._t("lbl.pg_ev_ending",{},"Ending"),"#ef5350"],anti_wrinkle:[this._t("lbl.pg_anti_wrinkle",{},"Anti-wrinkle"),"#ab47bc"]},[c,g]=l[i]||l.idle,p=o&&o.progress!=null?Math.round(o.progress):null,f=o&&o.remaining_s!=null?o.remaining_s:null,u=o&&o.energy_wh!=null?o.energy_wh:this._pgTrapEnergy(e,n),$=o&&o.confidence!=null?Math.round(o.confidence*100)+"%":"\u2014",S=o&&o.phase?o.phase:"\u2014",x=L=>Math.floor(L/60)+":"+String(Math.round(L%60)).padStart(2,"0"),w=L=>L>=1e3?(L/1e3).toFixed(2)+" kWh":L.toFixed(0)+" Wh",v=L=>L>=1e3?(L/1e3).toFixed(1)+" kW":Ot(L),k=this.shadowRoot,D=L=>k&&k.getElementById(L),I=(L,P)=>{const C=D(L);C&&(C.textContent=P)},z=(L,P,C)=>{const A=D(L);A&&(A.style[P]=C)},R=D("wd-pg-state-badge");R&&(R.textContent=c,R.style.background=g,R.style.color=g.includes("var")?"":"#fff"),I("wd-pg-power",v(a)),I("wd-pg-pct",p!=null?p+"%":"\u2014%"),z("wd-pg-pbar","width",(p??0)+"%"),I("wd-pg-rem",f!=null?x(f):"\u2014"),I("wd-pg-energy",w(u)),I("wd-pg-conf",$),I("wd-pg-phase",S)}_pgIsUnknownCmd(t){const e=t&&(t.code||t.error&&t.error.code)||"",s=(t&&(t.message||t.error)||"").toString().toLowerCase();return e==="unknown_command"||s.includes("unknown command")||s.includes("unknown_command")}_pgInterpPower(t,e){if(!t.length)return 0;if(e<=t[0].t)return t[0].w;for(let s=1;s=e))break}return s}_drawPlaygroundCanvases(){this._tab==="playground"&&this._pgDrawCanvas()}_htmlPhases(){const t=this._devices[this._selIdx],e=t&&t.options.device_type||"washing_machine",s=this._canEdit(),r=this._phases.map(o=>{const i=o.is_default,l=o.translation_key?this._t(o.translation_key,{},o.description||""):o.description||"",c=s?` +
    `}_htmlPgAnalysis(){const t=this._pgDetail&&this._pgDetail.outcome,e=t&&t.matched_profile||this._pgProfileName||(this._cycles||[]).find(o=>o.id===this._pgCycleId)?.profile_name||"",s=t&&t.confidence;if(!t)return`

    ${this._t("msg.pg_analysis_hint2",{},"Pick a cycle and press Run to load match analysis.")}

    `;let a="\u2014",r="var(--secondary-text-color)";s!=null&&(s>=.7?(a="\u2705 "+this._t("lbl.pg_strong_match",{},"Strong match"),r="var(--success-color, #4caf50)"):s>=.4?(a="\u26A0 "+this._t("lbl.pg_weak_match",{},"Weak match"),r="var(--warning-color, #ff9800)"):(a="\u274C "+this._t("lbl.pg_poor_match",{},"Poor match"),r="var(--error-color, #f44336)"));let i=`
    ${a}
    `;if(e&&s!=null){const o=Math.round(Math.max(0,Math.min(1,s))*100);i+=`
    ${d(this._t("lbl.pg_match_confidence",{},"Match confidence"))}
    `,i+=`
    + ${d(e)} +
    + ${o}% +
    `}return`
    ${i}
    `}async _pgFetchEnvelope(t,e){try{const s=await this._ws({type:`${w}/get_profile_envelope`,entry_id:t,profile_name:e});this._pgEnvData=s.envelope||null}catch{this._pgEnvData=null}}async _pgLoad(){const t=this._devices[this._selIdx];if(!t||this._pgLoading)return;const e=this._pgCycleId||this._cycles?.[0]?.id||"";if(!e)return;this._pgCycleId=e,this._pgLoading=!0,this._pgDetailBusy=!1,this._pgView=null,this._pgHoverT=null,this._pgPowerPts=null,this._pgEnvData=null,this._pgDetail=null;const s=++this._pgLoadSeq;this._render();try{const a=await this._ws({type:`${w}/get_cycle_power_data`,entry_id:t.entry_id,cycle_id:e}),r=a.samples||[],i=[];for(const l of r){if(!Array.isArray(l)||l.length<2)continue;const c=+l[0],p=+l[1];!isNaN(c)&&!isNaN(p)&&i.push({t:c,w:p})}if(this._pgPowerPts=i.length?i:null,typeof a.full_duration_s=="number"&&a.full_duration_s>0){const l=(this._cycles||[]).find(c=>c.id===e);l&&(l._pg_duration=a.full_duration_s)}const o=this._pgProfileName||(this._cycles||[]).find(l=>l.id===e)?.profile_name||"";if(o&&await this._pgFetchEnvelope(t.entry_id,o),s!==this._pgLoadSeq)return;await this._pgLoadDetail(t.entry_id,e,s);const n=this._pgDetail&&this._pgDetail.outcome&&this._pgDetail.outcome.matched_profile;!o&&n&&s===this._pgLoadSeq&&await this._pgFetchEnvelope(t.entry_id,n)}catch(a){this._showToast(this._tText("msg.toast_error",{error:a.message||a},"Error: "+(a.message||a)),"error")}s===this._pgLoadSeq&&(this._pgLoading=!1,this._render(),requestAnimationFrame(()=>{this._pgDrawCanvas(),this._pgUpdateStripAt(null)}),this._pgNeedsRestart&&this._tab==="playground"?(this._pgRestartRetries||0)<5&&!this._pgRestartRetryTimer&&(this._pgRestartRetries=(this._pgRestartRetries||0)+1,this._pgRestartRetryTimer=setTimeout(()=>{this._pgRestartRetryTimer=null,this._pgNeedsRestart&&this._tab==="playground"&&this._pgLoad()},3e3)):this._pgRestartRetries=0)}_pgCancelRun(){this._pgLoadSeq++,this._pgLoading=!1,this._pgDetailBusy=!1,this._pgCancelDetailTask(),this._render()}_pgCancelDetailTask(){const t=this._pgDetailTaskId;this._pgDetailTaskId=null,t&&this._ws({type:`${w}/cancel_task`,task_id:t}).catch(()=>{})}_pgCurrentOverride(){const t={...this._pgParamOverrides};return this._pgThreshStart!=null&&(t.start_threshold_w=this._pgThreshStart),this._pgThreshStop!=null&&(t.stop_threshold_w=this._pgThreshStop),t}_pgOverrideSig(t){return JSON.stringify(Object.keys(t).sort().map(e=>[e,t[e]]))}_pgSelectCycle(t){t&&(this._pgCycleId=t,this._pgProfileName="",this._pgView=null,this._pgHoverT=null,this._pgLoading=!1,this._pgLoad(),requestAnimationFrame(()=>{const e=this.shadowRoot&&this.shadowRoot.getElementById("wd-pg-canvas");e&&typeof e.scrollIntoView=="function"&&e.scrollIntoView({behavior:"smooth",block:"center"})}))}async _pgLoadDetail(t,e,s){const a=this._pgCurrentOverride(),r=this._pgOverrideSig(a);this._pgCancelDetailTask();try{const i=await this._ws({type:`${w}/start_playground_cycle_detail`,entry_id:t,cycle_id:e,settings_override:a}),o=i&&i.task_id;if(!o)throw new Error("no task id");this._pgDetailTaskId=o,this._addProvisionalTask(o,"pg_detail",t,0);const n=await new Promise(l=>{this._taskCallbacks[o]=async p=>{if(p.state==="done"||p.state==="cancelled")try{const _=await this._ws({type:`${w}/get_task_result`,task_id:p.id});l(_&&_.result)}catch{l(null)}else l(null)};const c=this._tasks[o];c&&c.state!=="running"?this._settleTaskCallback(c):this._tasksSubscribed||this._pollTaskGeneric(o)});if(this._pgDetailTaskId===o&&(this._pgDetailTaskId=null),!this._isActiveEntry(t)||s!=null&&s!==this._pgLoadSeq)return;this._pgDetail=n&&!n.error?n:null,this._pgDetailSig=this._pgDetail?r:null,this._pgNeedsRestart=!1}catch(i){if(s!=null&&s!==this._pgLoadSeq)return;this._pgIsUnknownCmd(i)&&(this._pgNeedsRestart=!0),this._pgDetail=null,this._pgDetailSig=null}}_pgRerunDetail(){const t=this._devices[this._selIdx];!t||!this._pgCycleId||this._pgLoading||(clearTimeout(this._pgDetailDebounceTimer),this._pgDetailDebounceTimer=setTimeout(async()=>{const e=++this._pgLoadSeq;this._pgDetailBusy=!0,this._render(),await this._pgLoadDetail(t.entry_id,this._pgCycleId,e),e===this._pgLoadSeq&&(this._pgDetailBusy=!1,this._render(),requestAnimationFrame(()=>this._pgDrawCanvas()))},220))}_pgMapState(t,e){return e?"stalled":t==="running"||t==="paused"?"running":t==="ending"?"ending":t==="starting"?"detecting":t==="anti_wrinkle"?"anti_wrinkle":"idle"}_pgSeriesAt(t){const e=this._pgDetail&&this._pgDetail.series;if(!e||!e.length)return null;let s=e[0];for(const a of e)if(a.t<=t)s=a;else break;return s}_pgStateSegsFromSeries(t){const e=this._pgDetail&&this._pgDetail.series;if(!e||!e.length)return[];const s=[];let a=null;for(const r of e){const i=this._pgMapState(r.state,r.stalled);!a||a.state!==i?(a={start:r.t,end:r.t,state:i},s.push(a)):a.end=r.t}return s.length&&(s[s.length-1].end=t),s}_pgDrawCanvas(){if(this._tab!=="playground")return;const t=this.shadowRoot,e=t&&t.getElementById("wd-pg-canvas");if(!e)return;const s=this._pgPowerPts,a=e.getBoundingClientRect(),r=window.devicePixelRatio||1,i=Math.max(1,Math.round(a.width*r)),o=Math.max(1,Math.round((a.height||280)*r));(e.width!==i||e.height!==o)&&(e.width=i,e.height=o);const n=e.getContext("2d"),l=getComputedStyle(this),c=(l.getPropertyValue("--primary-color")||"#03a9f4").trim(),p=(l.getPropertyValue("--divider-color")||"rgba(127,127,127,.2)").trim(),_=(l.getPropertyValue("--secondary-text-color")||"#888").trim(),f=(l.getPropertyValue("--secondary-background-color")||"#1a1a1a").trim();n.clearRect(0,0,i,o);const u=34*r,b=14*r,v=Bt*r,T=44*r,C=8*r,x=v+8*r,$=u+b+4*r,L=o-x-$;if(!s||!s.length){this._pgLoading&&(n.fillStyle=_,n.font=`${12*r}px sans-serif`,n.textAlign="center",n.textBaseline="middle",n.fillText(this._t("msg.loading",{},"Loading\u2026"),i/2,o/2));return}const D=(this._cycles||[]).find(E=>E.id===this._pgCycleId)?._pg_duration||s[s.length-1].t||1,B=Math.max(...s.map(E=>E.w),1),U=this._pgThreshStart??this._pgFieldVal("start_threshold_w",{})??50,z=this._pgThreshStop??this._pgFieldVal("stop_threshold_w",{})??5;let M=0,A=D;this._pgView&&this._pgView.max-this._pgView.min>1&&(M=Math.max(0,this._pgView.min),A=Math.min(D,this._pgView.max),A-M<=1&&(M=0,A=D));const O=Math.max(1e-6,A-M),K=i-T-C,I=E=>T+(E-M)/O*K,R=E=>x+(1-Math.max(0,E)/B)*L;this._pgMap={vMin:M,vMax:A,totalDur:D,padLpx:T/r,plotWpx:K/r},n.strokeStyle="rgba(127,127,127,0.12)",n.lineWidth=r,n.setLineDash([]);const G=Ie(B,5),W=[];for(let E=1;E*G<=B*1.0001;E++)W.push(E*G);W.forEach(E=>{const S=R(E);Sx+L||(n.beginPath(),n.moveTo(T,S),n.lineTo(i-C,S),n.stroke(),n.fillStyle=_,n.font=`${9*r}px sans-serif`,n.textAlign="right",n.textBaseline="middle",n.fillText(Ht(E,G),T-4*r,S))}),n.save(),n.beginPath(),n.rect(T,x,K,L),n.clip();const J=this._pgEnvData,Y=J&&Array.isArray(J.avg)?J.avg.filter(E=>Array.isArray(E)&&E.length>=2):[];if(Y.length){const E=Math.max(...Y.map(H=>H[0]))||1,S=H=>I(H/E*D),F=Array.isArray(J.min)?J.min.filter(H=>Array.isArray(H)&&H.length>=2):[],N=Array.isArray(J.max)?J.max.filter(H=>Array.isArray(H)&&H.length>=2):[];if(F.length&&N.length){n.beginPath(),N.forEach((H,Z)=>Z?n.lineTo(S(H[0]),R(H[1])):n.moveTo(S(H[0]),R(H[1])));for(let H=F.length-1;H>=0;H--)n.lineTo(S(F[H][0]),R(F[H][1]));n.closePath(),n.fillStyle="#eda10012",n.fill()}else n.beginPath(),n.strokeStyle="#eda100",n.lineWidth=2*r,n.setLineDash([5*r,4*r]),Y.forEach((H,Z)=>Z?n.lineTo(S(H[0]),R(H[1])):n.moveTo(S(H[0]),R(H[1]))),n.stroke(),n.setLineDash([])}n.beginPath(),n.moveTo(I(0),R(0)),s.forEach(E=>n.lineTo(I(E.t),R(E.w))),n.lineTo(I(s[s.length-1].t),R(0)),n.closePath(),n.fillStyle=Pt(c,.1),n.fill(),n.beginPath(),n.strokeStyle=c,n.lineWidth=2*r,s.forEach((E,S)=>S?n.lineTo(I(E.t),R(E.w)):n.moveTo(I(E.t),R(E.w))),n.stroke(),n.restore();const nt=(E,S,F)=>{const N=R(E);Nx+L+2||(n.save(),n.strokeStyle=S,n.lineWidth=2*r,n.setLineDash([8*r,4*r]),n.beginPath(),n.moveTo(T,N),n.lineTo(i-C,N),n.stroke(),n.setLineDash([]),n.fillStyle=S,n.beginPath(),n.arc(T+(i-T-C)*.06,N,5*r,0,Math.PI*2),n.fill(),n.fillStyle=S,n.font=`bold ${9*r}px sans-serif`,n.textAlign="left",n.textBaseline="bottom",n.fillText(F+" "+Ht(E),T+14*r,N-2*r),n.restore())};nt(+U,"#2a78d6",this._t("lbl.start",{},"Start")),nt(+z,"#e34948",this._t("btn.stop",{},"Stop"));const _t={idle:f,detecting:"#42a5f566",running:"#66bb6a66",ending:"#ef535066",anti_wrinkle:"#ab47bc66",stalled:"#eda10066"},ct={idle:this._t("lbl.pg_idle",{},"Idle"),detecting:this._t("lbl.pg_detecting",{},"Detecting"),running:this._t("lbl.pg_ev_running",{},"Running"),ending:this._t("lbl.pg_ev_ending",{},"Ending"),anti_wrinkle:this._t("lbl.pg_anti_wrinkle",{},"Anti-wrinkle"),stalled:this._t("status.stalled",{},"Stalled")},dt=o-u-b;n.fillStyle=f,n.fillRect(T,dt,i-T-C,u),n.save(),n.beginPath(),n.rect(T,dt,K,u),n.clip(),this._pgStateSegsFromSeries(D).forEach(E=>{const S=I(E.start),F=I(E.end);n.fillStyle=_t[E.state]||p,n.fillRect(S,dt,Math.max(1,F-S),u),F-S>50*r&&(n.fillStyle=_,n.font=`${8*r}px sans-serif`,n.textAlign="center",n.textBaseline="middle",n.fillText((ct[E.state]||E.state).toUpperCase(),(S+F)/2,dt+u/2))}),n.restore();const m=o-b,g=[];for(const E of this._pgDetail&&this._pgDetail.series||[]){const S=g[g.length-1];if(S&&S.name===(E.phase||null)){S.end=E.t;continue}S&&(S.end=E.t),g.push({name:E.phase||null,start:E.t,end:E.t})}g.some(E=>E.name)&&(n.save(),n.beginPath(),n.rect(T,m,K,b),n.clip(),g.filter(E=>E.name).forEach((E,S)=>{const F=I(E.start),N=I(E.end),H=S*47%360;n.fillStyle=`hsla(${H},60%,55%,0.55)`,n.fillRect(F,m,Math.max(1,N-F),b),N-F>40*r&&(n.fillStyle="#fff",n.font=`${7*r}px sans-serif`,n.textAlign="center",n.textBaseline="middle",n.fillText(E.name,(F+N)/2,m+b/2))}),n.restore()),this._pgEventHits=[];const k=this._pgDetail&&Array.isArray(this._pgDetail.events)?this._pgDetail.events.filter(E=>E.type!=="state"&&E.t>=M&&E.t<=A).slice().sort((E,S)=>E.t-S.t):[],y=11*r,P=x-v/2-2*r,X=y*2+3*r;let q=-1/0;const j=this._pgHoverEvent;if(k.forEach(E=>{const S=this._pgEventMeta(E.type),F=I(E.t);let N=Math.max(F,q+X);N=Math.max(T+y,Math.min(i-C-y,N)),q=N;const H=j&&j.t===E.t&&j.type===E.type;n.save(),n.strokeStyle=S.color,n.globalAlpha=H?.9:.5,n.lineWidth=(H?2:1)*r,n.beginPath(),n.moveTo(N,P+y),n.lineTo(F,x),n.stroke(),H&&(n.globalAlpha=.3,n.setLineDash([2*r,3*r]),n.beginPath(),n.moveTo(F,x),n.lineTo(F,o-b),n.stroke(),n.setLineDash([])),n.restore(),n.save(),n.beginPath(),n.arc(N,P,y,0,Math.PI*2),n.fillStyle=H?S.color:f||"#1a1a1a",n.fill(),n.lineWidth=(H?2:1.5)*r,n.strokeStyle=S.color,n.stroke(),n.fillStyle=H?"#fff":S.color,n.font=`${14*r}px sans-serif`,n.textAlign="center",n.textBaseline="middle",n.fillText(S.glyph,N,P+r),n.restore(),this._pgEventHits.push({cx:N/r,cy:P/r,r:y/r,type:E.type,label:S.label,detail:E.detail,t:E.t})}),j){const E=this._pgEventHits.find(S=>S.t===j.t&&S.type===j.type);if(E){const S=this._pgEventMeta(E.type),F=ft=>`${Math.floor(ft/60)}:${String(Math.round(ft%60)).padStart(2,"0")}`,N=this._pgEventDescription(E.type),H=[{t:E.label,bold:!0,col:S.color},...N?[{t:N}]:[],...E.detail?[{t:E.detail,dim:!0}]:[],{t:`${this._t("lbl.from_start",{},"From start")} ${F(E.t)}`,dim:!0}];n.save(),n.font=`${10*r}px sans-serif`;const Z=Math.min(220*r,i-2*T),tt=[];H.forEach(ft=>{const At=String(ft.t).split(" ");let bt="";At.forEach(Ot=>{const Jt=bt?bt+" "+Ot:Ot;n.measureText(Jt).width>Z&&bt?(tt.push({...ft,t:bt}),bt=Ot):bt=Jt}),tt.push({...ft,t:bt})});const st=14*r,rt=Math.min(Z+14*r,i-8*r),lt=tt.length*st+10*r;let pt=E.cx*r-rt/2;pt=Math.max(4*r,Math.min(i-rt-4*r,pt));const Rt=P+y+6*r;n.fillStyle=f,n.globalAlpha=.97,n.fillRect(pt,Rt,rt,lt),n.globalAlpha=1,n.strokeStyle=S.color,n.lineWidth=r,n.strokeRect(pt,Rt,rt,lt),n.textAlign="left",n.textBaseline="top",tt.forEach((ft,At)=>{n.font=`${ft.bold?"600 ":""}${10*r}px sans-serif`,n.fillStyle=ft.col||(ft.dim?_:(l.getPropertyValue("--primary-text-color")||"#ddd").trim()),n.fillText(ft.t,pt+7*r,Rt+5*r+At*st)}),n.restore()}}if(this._pgHoverT!=null&&this._pgHoverT>=M&&this._pgHoverT<=A){const E=I(this._pgHoverT);n.save(),n.strokeStyle="#e34948",n.lineWidth=1.5*r,n.setLineDash([4*r,3*r]),n.beginPath(),n.moveTo(E,x),n.lineTo(E,o-b),n.stroke(),n.setLineDash([]);const S=this._pgInterpPower(s,this._pgHoverT),F=R(S);n.fillStyle="#e34948",n.beginPath(),n.arc(E,F,3.5*r,0,Math.PI*2),n.fill();const N=at=>`${Math.floor(at/60)}:${String(Math.round(at%60)).padStart(2,"0")}`,H=at=>at>=1e3?it(at/1e3,2)+" kW":Lt(at),Z=[`${this._t("lbl.from_start",{},"From start")} ${N(this._pgHoverT)}`,`${this._t("lbl.to_end",{},"To end")} ${N(Math.max(0,D-this._pgHoverT))}`,`${this._t("lbl.power",{},"Power")} ${H(S)}`];n.font=`${9.5*r}px sans-serif`;const tt=Math.max(...Z.map(at=>n.measureText(at).width))+12*r,st=Z.length*13*r+8*r;let rt=E+8*r;rt+tt>i-C&&(rt=E-tt-8*r);const lt=x+4*r;n.fillStyle=f,n.globalAlpha=.95,n.fillRect(rt,lt,tt,st),n.globalAlpha=1,n.strokeStyle=p,n.lineWidth=r,n.strokeRect(rt,lt,tt,st),n.fillStyle=_,n.textAlign="left",n.textBaseline="top",Z.forEach((at,pt)=>n.fillText(at,rt+6*r,lt+5*r+pt*13*r)),n.restore()}n.fillStyle=_,n.font=`${9*r}px sans-serif`,n.textAlign="center",n.textBaseline="top";const V=E=>`${Math.floor(E/60)}:${String(Math.round(E%60)).padStart(2,"0")}`;for(let E=0;E<=4;E++){const S=M+O*E/4,F=I(S);n.textAlign=E===0?"left":E===4?"right":"center",n.fillText(V(S),Math.max(T,Math.min(i-C,F)),dt+u+1*r)}Oz.id===this._pgCycleId)?._pg_duration||e[e.length-1].t||1,r=t??a,i=this._pgInterpPower(e,r),o=this._pgSeriesAt(r),n=o?this._pgMapState(o.state,o.stalled):"idle",l={idle:[this._t("lbl.pg_idle",{},"Idle"),"var(--secondary-background-color)"],detecting:[this._t("lbl.pg_detecting",{},"Detecting"),"#42a5f5"],running:[this._t("lbl.pg_ev_running",{},"Running"),"#66bb6a"],ending:[this._t("lbl.pg_ev_ending",{},"Ending"),"#ef5350"],anti_wrinkle:[this._t("lbl.pg_anti_wrinkle",{},"Anti-wrinkle"),"#ab47bc"],stalled:[this._t("status.stalled",{},"Stalled"),"#eda100"]},[c,p]=l[n]||l.idle,_=o&&o.progress!=null?Math.round(o.progress):null,f=o&&o.remaining_s!=null?o.remaining_s:null,u=o&&o.energy_wh!=null?o.energy_wh:this._pgTrapEnergy(e,r),b=o&&o.confidence!=null?Math.round(o.confidence*100)+"%":"\u2014",v=o&&o.phase?o.phase:"\u2014",T=z=>Math.floor(z/60)+":"+String(Math.round(z%60)).padStart(2,"0"),C=z=>z>=1e3?it(z/1e3,2)+" kWh":it(z,0)+" Wh",x=z=>z>=1e3?it(z/1e3,1)+" kW":Lt(z),$=this.shadowRoot,L=z=>$&&$.getElementById(z),D=(z,M)=>{const A=L(z);A&&(A.textContent=M)},B=(z,M,A)=>{const O=L(z);O&&(O.style[M]=A)},U=L("wd-pg-state-badge");U&&(U.textContent=c,U.style.background=p,U.style.color=p.includes("var")?"":"#fff"),D("wd-pg-power",x(i)),D("wd-pg-pct",_!=null?_+"%":"\u2014%"),B("wd-pg-pbar","width",(_??0)+"%"),D("wd-pg-rem",f!=null?T(f):"\u2014"),D("wd-pg-energy",C(u)),D("wd-pg-conf",b),D("wd-pg-phase",v)}_pgIsUnknownCmd(t){const e=t&&(t.code||t.error&&t.error.code)||"",s=(t&&(t.message||t.error)||"").toString().toLowerCase();return e==="unknown_command"||s.includes("unknown command")||s.includes("unknown_command")}_pgInterpPower(t,e){if(!t.length)return 0;if(e<=t[0].t)return t[0].w;for(let s=1;s=e))break}return s}_drawPlaygroundCanvases(){this._tab==="playground"&&this._pgDrawCanvas()}_htmlPhases(){const t=this._devices[this._selIdx],e=t&&t.options.device_type||"washing_machine",s=this._canEdit(),a=this._phases.map(o=>{const n=o.is_default,l=o.translation_key?this._t(o.translation_key,{},o.description||""):o.description||"",c=s?` - - ${i?"":``} + + ${o.is_override?``:n?"":``} `:"";return` - ${d(o.name)} ${i?`${this._t("badge.built_in_tag",{},"built-in")}`:""} + ${d(o.name)} ${n?`${this._t("badge.built_in_tag",{},"built-in")}`:""} ${d(l.length>60?l.slice(0,57)+"\u2026":l)} ${c} - `}).join(""),n=s?`${this._t("lbl.actions",{},"Actions")}`:"",a=s?`
    `:"";return` + `}).join(""),r=s?`${this._t("lbl.actions",{},"Actions")}`:"",i=s?`
    `:"";return`
    ${this._t("hdr.phase_catalog",{},"Phase Catalog")}

    ${this._t("msg.phase_catalog_intro",{},"Named segments of a cycle (Pre-wash, Heating, Spin\u2026). Assign them to a profile from its control panel.")}

    - ${a} - ${this._phases.length===0?`

    ${this._t("msg.no_phases",{},"No phases defined.")}

    `:`${n}${r}
    ${this._t("lbl.name",{},"Name")}${this._t("lbl.description",{},"Description")}
    `} -
    `}_htmlDiagnostics(){const t=this._diag;let e;return t&&t._error?e=`

    ${this._t("msg.diagnostics_load_failed",{error:d(t._error)},"Could not load diagnostics: "+d(t._error))}

    `:t?e=`
    + ${i} + ${this._phases.length===0?`

    ${this._t("msg.no_phases",{},"No phases defined.")}

    `:`${r}${a}
    ${this._t("lbl.name",{},"Name")}${this._t("lbl.description",{},"Description")}
    `} +
    `}_htmlImportUndo(){const t=this._importUndo;if(!t||!t.created_at||!this._isAdmin())return"";const e=t.counts||{},s=(e.real_cycles||0)+(e.reference_cycles||0)+(e.backfill_cycles||0),a=this._busy.has("import-undo");return`
    +

    ${this._t("msg.import_undo_hint",{when:et(t.created_at),profiles:e.profiles||0,cycles:s},"A restore point from before the last import ({when}) is kept: {profiles} profile(s) and {cycles} cycle(s).")}

    + +
    `}_htmlDiagnostics(){const t=this._diag;let e;return t&&t._error?e=`

    ${this._t("msg.diagnostics_load_failed",{error:t._error},"Could not load diagnostics: {error}")}

    `:t?e=`
    ${t.total_cycles??"-"}
    ${this._t("lbl.cycles_count",{},"Cycles")}
    ${t.total_profiles??"-"}
    ${this._t("tab.profiles",{},"Profiles")}
    ${t.debug_traces_count??"-"}
    ${this._t("lbl.debug_traces",{},"Debug Traces")}
    @@ -1490,7 +1468,7 @@ button.wd-profile-card { display: block; } ${this._canFull()?`
    ${this._t("hdr.maintenance",{},"Maintenance Actions")}
    -
    ${this._t("hdr.process_history",{},"Process History")}

    ${this._t("msg.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.")}

    +
    ${this._t("hdr.process_history",{},"Process History")}

    ${this._t("msg.process_history_hint",{},"Re-run matching on all stored cycles, refresh suggestions, retrain ML models (if enabled), recost cycles from recorded prices and recompute cycle health. Run it after a batch of reviews.")}

    ${this._t("hdr.clear_debug",{},"Clear Debug Traces")}

    ${this._t("msg.clear_debug_hint",{},"Remove stored debug data to free space.")}

    @@ -1505,81 +1483,102 @@ button.wd-profile-card { display: block; } -
    + ${this._htmlImportUndo()}
    ${this._t("hdr.import_power_history",{},"Import power history")}
    -

    ${this._t("msg.import_history_description",{},"Already had a smart plug before WashData? Upload a history export of its power sensor, or read it straight from Home Assistant, and the normal detection runs over it so past cycles turn up in your Cycles list ready to name.")}

    +

    ${this._t("msg.import_history_description",{},"Had a smart plug before WashData? Upload its power history or read it from Home Assistant. Detection runs over it, and past cycles appear in your Cycles list ready to name.")}

    -
    `:`

    ${this._t("msg.maintenance_requires_access",{},"Maintenance and export/import require full access.")}

    `}`}_maintLabel(t){return{descale:this._t("maint.descale",{},"Descale"),filter_clean:this._t("maint.filter_clean",{},"Clean filter"),drum_clean:this._t("maint.drum_clean",{},"Clean drum"),bearing_service:this._t("maint.bearing_service",{},"Bearing service"),other:this._t("maint.other",{},"Other")}[t]||t}_htmlMaintenance(){const t=this._canEdit(),e=this._maintenance;if(e&&e._error)return`

    ${this._t("msg.maintenance_load_error",{error:e._error},"Could not load maintenance data: "+e._error)}

    `;if(!e)return`

    ${this._t("msg.loading",{},"Loading\u2026")}

    `;const s=e.event_types&&e.event_types.length?e.event_types:["descale","filter_clean","drum_clean","bearing_service","other"],r=e.due||[],n=e.log||[],a=e.reminders||{},o=e.cycles_since||{},i=Math.max(0,parseInt(e.lifetime_cycle_count,10)||0),l=this._canFull(),c=r.length?(()=>{const v=r.map(k=>this._maintLabel(k)).join(", ");return`
    - ${this._t("msg.maintenance_due",{items:d(v)},"Maintenance due: "+v)} -
    `})():"",g=new Date().toISOString().slice(0,10),p=s.map(v=>``).join(""),f=t?`
    +
    `:`

    ${this._t("msg.maintenance_requires_access",{},"Maintenance and export/import require full access.")}

    `}`}_maintLabel(t,e){const s={descale:this._t("maint.descale",{},"Descale"),filter_clean:this._t("maint.filter_clean",{},"Clean filter"),drum_clean:this._t("maint.drum_clean",{},"Clean drum"),bearing_service:this._t("maint.bearing_service",{},"Bearing service"),other:this._t("maint.other",{},"Other"),salt:this._t("maint.salt",{},"Refill salt"),rinse_aid:this._t("maint.rinse_aid",{},"Refill rinse aid"),lint_filter:this._t("maint.lint_filter",{},"Clean lint filter"),condenser_clean:this._t("maint.condenser_clean",{},"Clean condenser")};if(s[t])return s[t];const r=(this._maintenance&&this._maintenance.custom_tasks||[]).find(i=>i&&i.id===t);return r&&r.name?r.name:e||t}_maintStatusRows(t,e){if(Array.isArray(t.status))return t.status;const s=t.reminders||{},a=t.cycles_since||{};return e.map(r=>{const i=parseInt(s[r],10)||0,o=Math.max(0,parseInt(a[r],10)||0);return{id:r,custom:!1,cycles_interval:i,days_interval:0,cycles_since:o,days_since:null,due:i>0&&o>=i}}).filter(r=>r.cycles_interval>0)}_htmlMaintenance(){const t=this._canEdit(),e=this._maintenance;if(e&&e._error)return`

    ${this._t("msg.maintenance_load_error",{error:e._error},"Could not load maintenance data: {error}")}

    `;if(!e)return`

    ${this._t("msg.loading",{},"Loading\u2026")}

    `;const s=e.event_types&&e.event_types.length?e.event_types:["descale","filter_clean","drum_clean","bearing_service","other"],a=e.due||[],r=e.log||[],i=e.reminders||{},o=Array.isArray(e.custom_tasks)?e.custom_tasks:[],n=e.limits||{},l=parseInt(n.name_max,10)||60,c=parseInt(n.tasks_max,10)||20,p=Math.max(0,parseInt(e.lifetime_cycle_count,10)||0),_=this._canFull(),f=a.length?(()=>{const M=a.map(O=>this._maintLabel(O)).join(", "),A=t?`
    ${a.map(O=>``).join("")}
    `:"";return`
    + ${this._t("msg.maintenance_due",{items:M},"Maintenance due: {items}")} + ${A} +
    `})():"",u=new Date().toISOString().slice(0,10),b=s.map(M=>``).join("")+o.map(M=>``).join(""),v=t?`
    ${this._t("hdr.add_maintenance",{},"Add Maintenance Event")}
    -
    -
    +
    +
    -
    `:"",u=n.length?n.map(v=>{const k=t?``:"",D=v.notes?`
    ${d(v.notes)}
    `:"";return`
    +
    `:"",T=r.length?r.map(M=>{const A=t?``:"",O=M.notes?`
    ${d(M.notes)}
    `:"";return`
    -
    ${d(this._maintLabel(v.event_type))}
    -
    ${ot(v.date)}
    - ${D} +
    ${d(this._maintLabel(M.event_type,M.task_name))}
    +
    ${et(M.date)}
    + ${O}
    - ${k} + ${A}
    -
    `}).join(""):`

    ${this._t("msg.no_maintenance",{},"No maintenance recorded yet.")}

    `,$=t?`
    +
    `}).join(""):`

    ${this._t("msg.no_maintenance",{},"No maintenance recorded yet.")}

    `,C=(M,A,O,K)=>``,x=o.map(M=>`
    +
    +
    ${C("data-mtask-cycles",M.id,M.cycles,n.cycles_max||1e5)}
    +
    ${C("data-mtask-days",M.id,M.days,n.days_max||3650)}
    +
    +
    `).join(""),$=o.length +
    +
    +
    +
    +
    `:`

    ${this._t("msg.custom_task_limit",{max:c},"You have reached the limit of {max} tasks.")}

    `,L=t?`
    ${this._t("hdr.maintenance_reminders",{},"Service Reminders")}

    ${this._t("msg.reminders_intro",{},"Show a reminder in the panel this many cycles after the last service. Leave blank or 0 to turn a reminder off.")}

    - ${s.map(v=>`
    `).join("")} + ${s.map(M=>`
    `).join("")}
    +
    ${this._t("hdr.custom_tasks",{},"Your own tasks")}
    +

    ${this._t("msg.custom_tasks_intro",{},"Add anything else you want to be reminded of. A task is due when either interval is reached; leave one blank or 0 to use only the other.")}

    + ${x} + ${$} +

    ${this._t("msg.maintenance_due_sensor_hint",{},"The Maintenance due binary sensor is on while any task is due, for your own notification automations.")}

    -
    `:"",S=s.map(v=>{const k=parseInt(a[v],10)||0;if(k<=0)return"";const D=Math.max(0,parseInt(o[v],10)||0),I=Math.max(0,Math.min(100,Math.round(D/k*100))),R=D>=k?"var(--warning-color,#ff9800)":"var(--primary-color)";return`
    +
    `:"",D=this._maintStatusRows(e,s).map(M=>{const A=parseInt(M.cycles_interval,10)||0,O=parseInt(M.days_interval,10)||0,K=Math.max(0,parseInt(M.cycles_since,10)||0),I=M.days_since==null?null:Math.max(0,parseInt(M.days_since,10)||0),R=[];let G=0;if(A>0&&(R.push(this._t("lbl.cycles_since_service",{since:K,total:A},K+" / "+A+" cycles")),G=Math.max(G,K/A)),O>0){const Y=I??0;R.push(this._t("lbl.days_since_service",{since:Y,total:O},Y+" / "+O+" days")),G=Math.max(G,Y/O)}const W=Math.max(0,Math.min(100,Math.round(G*100))),J=M.due?"var(--warning-color,#ff9800)":"var(--primary-color)";return`
    - ${d(this._maintLabel(v))} - ${this._t("lbl.cycles_since_service",{since:D,total:k},D+" / "+k+" cycles")} + ${d(M.custom?M.name||M.id:this._maintLabel(M.id))} + ${R.join(" \xB7 ")}
    -
    +
    -
    `}).join(""),x=l?`
    +
    `}).join(""),B=_?`
    - +
    -

    ${this._t("msg.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.")}

    +

    ${this._t("msg.odometer_correct_hint",{},"Set this if WashData counted a run that never happened, or the appliance ran cycles before WashData was installed.")}

    -
    `:"",w=`
    +
    `:"",U=`
    ${this._t("hdr.service_status",{},"Service Status")}
    - ${i} + ${p} ${this._t("lbl.total_cycles_run",{},"cycles run in total")}
    -

    ${this._t("msg.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.")}

    - ${S||`

    ${this._t("msg.no_reminders_set",{},"No service reminders are set yet.")}

    `} - ${x} -
    `;return`${c} - ${w} - ${f} +

    ${this._t("msg.odometer_intro",{},"This total only goes up. Deleting a cycle does not lower it, so service reminders stay correct.")}

    + ${D||`

    ${this._t("msg.no_reminders_set",{},"No service reminders are set yet.")}

    `} + ${B} +
    `,z=`
    +
    ${this._t("hdr.maintenance_supporter",{},"Need more?")}
    +

    ${this._t("msg.maintenance_supporter_intro",{},"Maintenance Supporter is a separate integration that does much more: any appliance or task, schedules by time, usage or a sensor, notifications and a history. It can use the WashData cycle counter.")}

    +

    ${this._t("link.maintenance_supporter",{},"Maintenance Supporter on GitHub \u2197")}

    +
    `;return`${f} + ${U} + ${v}
    ${this._t("hdr.maintenance_log",{},"Maintenance Log")}
    -

    ${this._t("msg.maintenance_intro",{},"Log servicing you perform on this appliance and get reminded when each task is due again.")}

    -
    ${u}
    +

    ${this._t("msg.maintenance_intro",{},"Log servicing on this appliance and get reminded when each task is due again.")}

    +
    ${T}
    - ${$}`}_htmlPanel(){const t=this._canEdit(),e=t&&this._constants&&this._constants.mlTrainingAvailable,s=new Set(["maintenance"]);t&&s.add("diagnostics"),e&&s.add("ml");let r=this._panelSubtab;s.has(r)||(r=this._panelSubtab="maintenance");const n=[["maintenance",this._t("tab.maintenance",{},"Maintenance")]];t&&n.push(["diagnostics",this._t("tab.diagnostics",{},"Diagnostics")]),e&&n.push(["ml",this._t("tab.ml",{},"ML Training")]);const a=n.map(([i,l])=>``).join(""),o=r==="diagnostics"&&t?this._htmlDiagnostics():r==="ml"&&e?this._htmlMlTab():this._htmlMaintenance();return`
    ${a}
    ${o}`}_levelSelect(t,e,s){const r=(s?[["inherit",this._t("access.inherit",{},"Inherit")]]:[]).concat([["none",this._t("access.none",{},"None (hidden)")],["read",this._t("access.read",{},"Read")],["edit",this._t("access.edit",{},"Edit")],["full",this._t("access.full",{},"Full")]]);return``}_htmlPanelPrefs(){const t=this._panelCfg&&this._panelCfg.prefs||{},e=this._hass&&this._hass.locale&&this._hass.locale.language||"en",r=[["",this._t("pref.use_panel_default",{},"(panel default)")],["status",this._t("tab.status",{},"Overview")],["history",this._t("tab.history",{},"Cycles")],["profiles",this._t("tab.profiles",{},"Profiles")],["settings",this._t("tab.settings",{},"Settings")],["playground",this._t("tab.playground",{},"Playground")]].map(([l,c])=>``).join(""),a=[["relative",this._t("pref.date_relative",{},"Relative (e.g. 2 hours ago)")],["absolute",this._t("pref.date_absolute",{},"Absolute (e.g. 14:32 on 2 Jul)")]].map(([l,c])=>``).join(""),o=t.lang_override||"",i=[["",this._t("pref.lang_auto",{lang:e.toUpperCase()},"System default ("+e.toUpperCase()+")")],["en",this._t("pref.lang_en",{},"English")]].map(([l,c])=>``).join("");return`
    + ${L} + ${z}`}_htmlPanel(){const t=this._canEdit(),e=t&&this._constants&&this._constants.mlTrainingAvailable,s=new Set(["diagnostics"]);e&&s.add("ml");let a=this._panelSubtab;s.has(a)||(a=this._panelSubtab="diagnostics");const r=[["diagnostics",this._t("tab.diagnostics",{},"Diagnostics")]];e&&r.push(["ml",this._t("tab.ml",{},"ML Training")]);const i=r.map(([n,l])=>``).join("");if(!t)return"";const o=a==="ml"&&e?this._htmlMlTab():this._htmlDiagnostics();return`
    ${i}
    ${o}`}_levelSelect(t,e,s){const a=(s?[["inherit",this._t("access.inherit",{},"Inherit")]]:[]).concat([["none",this._t("access.none",{},"None (hidden)")],["read",this._t("access.read",{},"Read")],["edit",this._t("access.edit",{},"Edit")],["full",this._t("access.full",{},"Full")]]);return``}_htmlPanelPrefs(){const t=this._panelCfg&&this._panelCfg.prefs||{},e=this._hass&&this._hass.locale&&this._hass.locale.language||"en",a=[["",this._t("pref.use_panel_default",{},"(panel default)")],["status",this._t("tab.status",{},"Overview")],["history",this._t("tab.history",{},"Cycles")],["profiles",this._t("tab.profiles",{},"Profiles")],["settings",this._t("tab.settings",{},"Settings")],["playground",this._t("tab.playground",{},"Playground")]].map(([l,c])=>``).join(""),i=[["relative",this._t("pref.date_relative",{},"Relative (e.g. 2 hours ago)")],["absolute",this._t("pref.date_absolute",{},"Absolute (e.g. 14:32 on 2 Jul)")]].map(([l,c])=>``).join(""),o=t.lang_override||"",n=[["",this._tText("pref.lang_auto",{lang:e.toUpperCase()},"System default ("+e.toUpperCase()+")")],["en",this._t("pref.lang_en",{},"English")]].map(([l,c])=>``).join("");return`
    ${this._t("hdr.my_preferences",{},"My Preferences")}

    ${this._t("msg.prefs_personal",{},"These apply to your Home Assistant account only.")}

    ${this._t("hdr.display",{},"Display")}
    -
    -
    -
    +
    +
    +
    @@ -1590,47 +1589,48 @@ button.wd-profile-card { display: block; }
    ${this._t("msg.font_size_hint",{},"Make everything in this panel larger or smaller. Applies to your account on this device.")}
    ${this._t("hdr.status_graph",{},"Status Graph")}
    - ${Dt(`id="wd-pref-expected" ${t.show_expected!==!1?"checked":""}`,this._t("lbl.show_expected",{},"Show expected curve overlay (matched profile, orange)"))} - ${Dt(`id="wd-pref-raw" ${t.show_raw?"checked":""}`,this._t("lbl.show_raw",{},"Show raw socket toggle in live power graph"))} + ${kt(`id="wd-pref-expected" ${t.show_expected!==!1?"checked":""}`,this._t("lbl.show_expected",{},"Show expected curve overlay (matched profile, orange)"))} + ${kt(`id="wd-pref-raw" ${t.show_raw?"checked":""}`,this._t("lbl.show_raw",{},"Show raw socket toggle in live power graph"))}
    ${this._t("hdr.diagnostics_pref",{},"Diagnostics")}
    - ${Dt(`id="wd-pref-debug" ${t.show_debug?"checked":""}`,this._t("lbl.show_debug",{},"Show live match debug card on the Status page (confidence, ambiguity, top candidates)"))} + ${kt(`id="wd-pref-debug" ${t.show_debug?"checked":""}`,this._t("lbl.show_debug",{},"Show live match debug card on the Status page (confidence, ambiguity, top candidates)"))}
    -
    `}_htmlPanelSettings(){const t=this._panelCfg&&this._panelCfg.panel||{},e=[["status",this._t("tab.status",{},"Overview")],["history",this._t("tab.history",{},"Cycles")],["profiles",this._t("tab.profiles",{},"Profiles")],["settings",this._t("tab.settings",{},"Settings")],["playground",this._t("tab.playground",{},"Playground")]],s=e.map(([a,o])=>``).join(""),r=t.hidden_tabs||[],n=e.filter(([a])=>a!=="status").map(([a,o])=>Yt(`data-hidetab="${a}" ${r.includes(a)?"checked":""}`,o)).join("");return`
    +
    `}_htmlPanelSettings(){const t=this._panelCfg&&this._panelCfg.panel||{},e=[["status",this._t("tab.status",{},"Overview")],["history",this._t("tab.history",{},"Cycles")],["profiles",this._t("tab.profiles",{},"Profiles")],["settings",this._t("tab.settings",{},"Settings")],["playground",this._t("tab.playground",{},"Playground")]],s=e.map(([i,o])=>``).join(""),a=t.hidden_tabs||[],r=e.filter(([i])=>i!=="status").map(([i,o])=>Xt(`data-hidetab="${i}" ${a.includes(i)?"checked":""}`,o)).join("");return`
    ${this._t("hdr.panel_settings",{},"Panel Settings (all users)")}
    -
    ${n}
    +
    ${r}
    -
    `}_htmlPanelAccess(){const t=this._panelCfg&&this._panelCfg.rbac||{enabled:!1,default_level:"none",users:{}},e=this._panelCfg&&this._panelCfg.users||[],s=this._devices||[],r=e.filter(a=>!a.is_admin).map(a=>{const o=(t.users||{})[a.id]||{default:"none",devices:{}},i=s.map(l=>`
    ${d(l.title)}${this._levelSelect(`data-rbacuser="${d(a.id)}" data-rbacdev="${d(l.entry_id)}"`,(o.devices||{})[l.entry_id]||"inherit",!0)}
    `).join("");return`
    -
    ${d(a.name)}
    -
    ${this._t("lbl.default_other",{},"Default (other devices)")}${this._levelSelect(`data-rbacuser="${d(a.id)}" data-rbacdev="__default__"`,o.default||"none",!1)}
    - ${i} -
    `}).join(""),n=e.filter(a=>a.is_admin).map(a=>`${d(a.name)} - full (admin)`).join(" ");return`
    +
    `}_htmlPanelAccess(){const t=this._panelCfg&&this._panelCfg.rbac||{enabled:!1,default_level:"none",users:{}},e=this._panelCfg&&this._panelCfg.users||[],s=this._devices||[],a=e.filter(i=>!i.is_admin).map(i=>{const o=(t.users||{})[i.id]||{default:"none",devices:{}},n=s.map(l=>`
    ${d(l.title)}${this._levelSelect(`data-rbacuser="${d(i.id)}" data-rbacdev="${d(l.entry_id)}"`,(o.devices||{})[l.entry_id]||"inherit",!0)}
    `).join("");return`
    +
    ${d(i.name)}
    +
    ${this._t("lbl.default_other",{},"Default (other devices)")}${this._levelSelect(`data-rbacuser="${d(i.id)}" data-rbacdev="__default__"`,o.default||"none",!1)}
    + ${n} +
    `}).join(""),r=e.filter(i=>i.is_admin).map(i=>`${d(i.name)} - full (admin)`).join(" ");return`
    ${this._t("hdr.access_control",{},"Access Control")}
    - ${Dt(`id="wd-rbac-enabled" ${t.enabled?"checked":""}`,this._t("lbl.enable_access_control",{},"Enable per-user access control"),"",this._t("msg.rbac_hint",{},"When off, every Home Assistant user has full access (the default). Administrators always have full access and can manage everyone."))} + ${kt(`id="wd-rbac-enabled" ${t.enabled?"checked":""}`,this._t("lbl.enable_access_control",{},"Enable per-user access control"),"",this._t("msg.rbac_hint",{},"Off: every Home Assistant user has full access (default). Administrators always have full access and can manage everyone."))}
    ${this._levelSelect('id="wd-rbac-default"',t.default_level||"none",!1)}
    - ${n?`
    ${n}
    `:""} + ${r?`
    ${r}
    `:""}
    - ${r||`

    ${this._t("msg.no_other_users",{},"No other Home Assistant users found.")}

    `}`}_htmlStore(){if(!this._canEdit())return`
    ${this._t("msg.no_device_selected",{},"No device selected.")}
    `;const t=this._storeStatus,e=`${this._t("store.website",{},"Store website \u2197")}`,s=`
    ${this._t("hdr.community_store",{},"Community Store")}
    ${e}
    `;if(!this._onlineEnabled()||t&&t.enabled===!1)return`
    ${s} -

    ${this._t("msg.store_enable_hint",{},"Enable online features in Settings to browse and import community reference cycles.")}

    `;let r;return this._storeView==="device"?r=this._htmlStoreDevice():this._storeView==="profile"?r=this._htmlStoreProfile():r=this._htmlStoreBrands(),`
    + ${a||`

    ${this._t("msg.no_other_users",{},"No other Home Assistant users found.")}

    `}`}_htmlStore(){if(!this._canEdit())return`
    ${this._t("msg.no_device_selected",{},"No device selected.")}
    `;const t=this._storeStatus,e=`${this._t("store.website",{},"Store website \u2197")}`,s=`
    ${this._t("hdr.community_store",{},"Community Store")}
    ${e}
    `;if(!this._onlineEnabled()||t&&t.enabled===!1)return`
    ${s} +

    ${this._t("msg.store_enable_hint",{},"Turn on online features in Preferences > Online & Community to browse and import community reference cycles.")}

    `;let a;return this._storeView==="device"?a=this._htmlStoreDevice():this._storeView==="profile"?a=this._htmlStoreProfile():a=this._htmlStoreBrands(),`
    ${s} ${this._htmlStoreCrumbs()} - ${r} -
    `}_htmlStoreCrumbs(){const t=[``];if(this._storeDevice){const e=this._storeDevice,s=`${e.brand||""} ${e.model||""}`.trim()||this._t("store.device",{},"Device");t.push('\u203A'),t.push(this._storeView==="device"?`${d(s)}`:``)}return this._storeProfile&&this._storeView==="profile"&&(t.push('\u203A'),t.push(`${d(this._storeProfile.program||"")}`)),`
    ${t.join("")}
    `}_htmlStoreLoading(){return`
    \u23F3
    ${this._t("msg.loading",{},"Loading\u2026")}
    `}_htmlStoreBrands(){const t=this._storeDevices||[],e=String((this._opts||{}).store_model||"").trim().toLowerCase(),s=t.map(o=>{const i=`${d(o.brand||"")} ${d(o.model||"")}`.trim()||this._t("store.device",{},"Device"),l=o.applianceType?`${d(this._deviceTypeLabel(o.applianceType))}`:"",g=e&&String(o.model||"").toLowerCase()===e?`${this._t("store.your_model",{},"Yours")}`:"",p=Number(o.profileCount)||0,f=p>0?`${this._t("store.programs_count",{n:p},`Programs: ${p}`)}`:"";return`
    `}_htmlStoreCrumbs(){const t=[``];if(this._storeDevice){const e=this._storeDevice,s=`${e.brand||""} ${e.model||""}`.trim()||this._t("store.device",{},"Device");t.push('\u203A'),t.push(this._storeView==="device"?`${d(s)}`:``)}return this._storeProfile&&this._storeView==="profile"&&(t.push('\u203A'),t.push(`${d(this._storeProfile.program||"")}`)),`
    ${t.join("")}
    `}_htmlStoreLoading(){return`
    \u23F3
    ${this._t("msg.loading",{},"Loading\u2026")}
    `}_htmlStoreBrands(){const t=this._storeDevices||[],e=String((this._opts||{}).store_model||"").trim().toLowerCase(),s=t.map(o=>{const n=`${d(o.brand||"")} ${d(o.model||"")}`.trim()||this._t("store.device",{},"Device"),l=o.applianceType?`${d(this._deviceTypeLabel(o.applianceType))}`:"",p=e&&String(o.model||"").toLowerCase()===e?`${this._t("store.your_model",{},"Yours")}`:"",_=Dt(o.profileCount),f=_>0?`${this._t("store.programs_count",{n:_},`Programs: ${_}`)}`:"";return``}).join("");if(!this._storeBrandScope())return` ${this._storeSearchHtml()} -

    ${this._t("msg.store_declare_appliance",{},"Tell WashData which appliance you own and this tab shows the setups other people have shared for it. You can also type a brand above to look around.")}

    - `;const r=`

    ${this._t("store.no_results",{},"No matching appliances found. Try a different search.")}

    `,n=this._storeLoading?this._htmlStoreLoading():t.length?`
    ${s}
    `:r,a=!this._storeLoading&&t.length>1?`

    ${this._t("msg.store_sibling_hint",{},"Nothing shared for your exact model? A closely-related model from the same brand is usually a good starting point.")}

    `:"";return` +

    ${this._t("msg.store_declare_appliance",{},"Tell WashData which appliance you own to see setups others shared for it. Or type a brand above to look around.")}

    + `;const a=`

    ${this._t("store.no_results",{},"No matching appliances found. Try a different search.")}

    `,r=this._storeLoading?this._htmlStoreLoading():t.length?`
    ${s}
    `:a,i=!this._storeLoading&&t.length>1?`

    ${this._t("msg.store_sibling_hint",{},"Nothing shared for your model? A related model from the same brand is usually a good start.")}

    `:"";return` ${this._storeSearchHtml()} - ${a} - ${n}`}_storeSearchHtml(){return` + ${i} + ${r}`}_storeSearchHtml(){return` `}_htmlStoreDevice(){const t=this._storeProfiles||[],e=t.map(o=>`
    `}_htmlStoreUnreachable(){return``}_htmlStoreDevice(){const t=this._storeProfiles||[],e=t.map(o=>``).join(""),s=this._storeLoading?this._htmlStoreLoading():t.length?`
    ${e}
    `:`

    ${this._t("store.no_programs",{},"No shared programs for this appliance yet.")}

    `,r=this._storeDevice,n=this._busy.has("store-download-device");return(this._canEdit()&&r&&t.length?`
    - ${this._t("msg.store_download_device_intro",{},"Adopt every shared program and its reference cycles onto your device. Your own recorded cycles and stats are not affected.")} - ${Yt(`data-action="store-toggle-dl-settings" ${this._dlSettings?"checked":""} ${n?"disabled":""}`,this._t("lbl.adopt_settings",{},"Also adopt settings"),dt(this._t("msg.adopt_settings_hint",{},"Overwrite this device's detection & matching thresholds with the shared ones. Your notifications, entities and energy price are never changed.")))} - -
    `:"")+s}_htmlStoreProfile(){const t=this._storeCycles||[],e=t.map(r=>{const n=r.stats||{},a=this._storeSparkline(r.trace&&r.trace.points||[]),o=n.duration!=null?gt(n.duration):"-",i=n.energy_wh!=null?bt(n.energy_wh/1e3):"-",l=n.peak_w!=null?Ot(n.peak_w):"-",c=d(r.uploaderName||this._t("store.anon",{},"anonymous")),g=this._statusTag(r),p=r.rating||{},f=p.avg!=null&&p.count?this._t("store.rating_summary",{avg:Number(p.avg).toFixed(1),n:p.count},`\u2605 ${Number(p.avg).toFixed(1)} (${p.count})`):this._t("store.no_ratings",{},"No ratings yet");return`
    + `).join(""),s=this._storeLoading?this._htmlStoreLoading():t.length?`
    ${e}
    `:this._storeBrowseErr?this._htmlStoreUnreachable():`

    ${this._t("store.no_programs",{},"No shared programs for this appliance yet.")}

    `,a=this._storeDevice,r=this._busy.has("store-download-device");return(this._canEdit()&&a&&t.length?`
    + ${this._t("msg.store_download_device_intro",{},"Add every shared program and its reference cycles to your device. Your own cycles and stats are not affected.")} + ${Xt(`data-action="store-toggle-dl-settings" ${this._dlSettings?"checked":""} ${r?"disabled":""}`,this._t("lbl.adopt_settings",{},"Also adopt settings"),ot(this._t("msg.adopt_settings_hint",{},"Overwrite this device's detection & matching thresholds with the shared ones. Your notifications, entities and energy price are never changed.")))} + +
    `:"")+s}_htmlStoreProfile(){const t=this._storeCycles||[],e=t.map(a=>{const r=a.stats&&typeof a.stats=="object"?a.stats:{},i=this._storeSparkline(a.trace&&a.trace.points||[]),o=ht(r.duration),n=gt(r.energy_wh!=null?Number(r.energy_wh)/1e3:null),l=Lt(r.peak_w),c=typeof a.uploaderName=="string"&&a.uploaderName||this._tText("store.anon",{},"anonymous"),p=this._statusTag(a),_=a.rating&&typeof a.rating=="object"?a.rating:{},f=Number(_.avg),u=Dt(_.count),b=_.avg!=null&&isFinite(f)&&u>0?this._t("store.rating_summary",{avg:it(f,1),n:u},"\u2605 {avg} ({n})"):this._t("store.no_ratings",{},"No ratings yet");return`
    - ${a} + ${i}
    -
    ${o} \xB7 ${i} \xB7 ${l} ${g}
    -
    ${this._t("store.uploaded_by",{name:c},`Shared by ${c}`)} \xB7 \u2B07 ${r.downloads||0} \xB7 ${f}
    +
    ${o} \xB7 ${n} \xB7 ${l} ${p}
    +
    ${this._t("store.uploaded_by",{name:c},"Shared by {name}")} \xB7 \u2B07 ${it(Dt(a.downloads),0)} \xB7 ${b}
    - +
    -
    `}).join("");return`
    ${this._storeLoading?this._htmlStoreLoading():t.length?e:`

    ${this._t("store.no_cycles",{},"No reference cycles shared for this program yet.")}

    `}
    `}_storeSparkline(t){const e=Array.isArray(t)?t.filter(u=>Array.isArray(u)&&u.length>=2):[];if(e.length<2)return'';const s=e.map(u=>u[0]),r=e.map(u=>u[1]),n=Math.min(...s),a=Math.max(...s),o=Math.max(1,...r),i=120,l=36,c=2,g=u=>c+(a>n?(u-n)/(a-n):0)*(i-2*c),p=u=>l-c-Math.max(0,u)/o*(l-2*c),f=e.map(u=>`${g(u[0]).toFixed(1)},${p(u[1]).toFixed(1)}`).join(" ");return``}_htmlGearModal(t){const e=this._isAdmin(),s=[["prefs",this._t("hdr.my_preferences",{},"My Preferences")]];e&&s.push(["panel",this._t("hdr.panel_settings",{},"Panel Settings")],["access",this._t("hdr.access_control",{},"Access Control")]),e&&this._constants&&this._constants.storeOnlineAvailable&&s.push(["online",this._t("hdr.online_account_tab",{},"Online & Community")]);let r=t.tab;s.some(([o])=>o===r)||(r=t.tab="prefs");const n=s.map(([o,i])=>``).join(""),a=r==="panel"&&e?this._htmlPanelSettings():r==="access"&&e?this._htmlPanelAccess():r==="online"&&e?this._htmlOnlineSettings():this._htmlPanelPrefs();return`

    ${this._t("settings.gear.title",{},"Settings")}

    -
    ${n}
    -
    ${a}
    `}_htmlOnlineSettings(){if(!(this._constants&&this._constants.storeOnlineAvailable))return`

    ${this._t("msg.online_unavailable",{},"Online features are not available on this server.")}

    `;const t=this._onlineEnabled(),e=this._storeStatus||{},s=!!(t&&e.connected),r=this._busy.has("store-account"),n=t?s?`
    - ${this._t("store.connected_as",{name:d(e.name||e.uid||"")},`Connected as ${d(e.name||e.uid||"")}`)} - +
    `}).join("");return`
    ${this._storeLoading?this._htmlStoreLoading():t.length?e:this._storeBrowseErr?this._htmlStoreUnreachable():`

    ${this._t("store.no_cycles",{},"No reference cycles shared for this program yet.")}

    `}
    `}_storeSparkline(t){const e=Array.isArray(t)?t.filter(u=>Array.isArray(u)&&u.length>=2).map(u=>[Number(u[0]),Number(u[1])]).filter(u=>isFinite(u[0])&&isFinite(u[1])):[];if(e.length<2)return'';const s=e.map(u=>u[0]),a=e.map(u=>u[1]),r=Math.min(...s),i=Math.max(...s),o=Math.max(1,...a),n=120,l=36,c=2,p=u=>c+(i>r?(u-r)/(i-r):0)*(n-2*c),_=u=>l-c-Math.max(0,u)/o*(l-2*c),f=e.map(u=>`${p(u[0]).toFixed(1)},${_(u[1]).toFixed(1)}`).join(" ");return``}_htmlGearModal(t){const e=this._isAdmin(),s=[["prefs",this._t("hdr.my_preferences",{},"My Preferences")]];e&&s.push(["panel",this._t("hdr.panel_settings",{},"Panel Settings")],["access",this._t("hdr.access_control",{},"Access Control")]),e&&this._constants&&this._constants.storeOnlineAvailable&&s.push(["online",this._t("hdr.online_account_tab",{},"Online & Community")]);let a=t.tab;s.some(([o])=>o===a)||(a=t.tab="prefs");const r=s.map(([o,n])=>``).join(""),i=a==="panel"&&e?this._htmlPanelSettings():a==="access"&&e?this._htmlPanelAccess():a==="online"&&e?this._htmlOnlineSettings():this._htmlPanelPrefs();return`

    ${this._t("settings.gear.title",{},"Preferences")}

    +
    ${r}
    +
    ${i}
    `}_htmlOnlineSettings(){if(!(this._constants&&this._constants.storeOnlineAvailable))return`

    ${this._t("msg.online_unavailable",{},"Online features are not available on this server.")}

    `;const t=this._onlineEnabled(),e=this._storeStatus||{},s=!!(t&&e.connected),a=this._busy.has("store-account"),r=t?s?`
    + ${this._t("store.connected_as",{name:e.name||e.uid||""},"Connected as {name}")} +
    `:`
    ${this._t("store.not_connected",{},"Not connected. Connect a GitHub account to confirm appliances and share your own cycles.")}
    `:"";return`
    ${this._t("hdr.online_account",{},"Community Store & online features")}
    -

    ${this._t("msg.online_intro_global",{},"Browse and share reference recordings with other WashData users, and confirm appliance entries. One connection applies to your whole WashData integration; appliance brand and model are set per device under Basic. All online features are opt-in and off by default.")}

    - ${Dt(`data-action="store-toggle-online" ${t?"checked":""} ${r?"disabled":""}`,this._t("lbl.enable_online",{},"Enable online features"))} - ${t?this._htmlStorePrefs(r):""} - ${t?n:""} -
    `}_htmlStorePrefs(t){const e=this._constants&&this._constants.storePrefs||{},s=fe.map(n=>{const a=e[n.key]!==!1;return Dt(`data-action="store-toggle-pref" data-pref="${d(n.key)}" ${a?"checked":""} ${t?"disabled":""}`,this._t(n.labelKey,{},n.labelFb),dt(this._t(n.docKey,{},n.docFb)))}).join(""),r=this._busy.has("store-refresh-catalog");return s+` +

    ${this._t("msg.online_intro_global",{},"Browse and share reference recordings with other WashData users. One connection covers all your WashData devices; brand and model are set per device under Basic. Online features are off until you turn them on.")}

    + ${kt(`data-action="store-toggle-online" ${t?"checked":""} ${a?"disabled":""}`,this._t("lbl.enable_online",{},"Enable online features"))} + ${t?this._htmlStorePrefs(a):""} + ${t?r:""} +
    `}_htmlStorePrefs(t){const e=this._constants&&this._constants.storePrefs||{},s=ke.map(r=>{const i=e[r.key],o=i===void 0?r.def!==!1:i!==!1;return kt(`data-action="store-toggle-pref" data-pref="${d(r.key)}" ${o?"checked":""} ${t?"disabled":""}`,this._t(r.labelKey,{},r.labelFb),ot(this._t(r.docKey,{},r.docFb)))}).join(""),a=this._busy.has("store-refresh-catalog");return s+`
    - - ${this._t("msg.refresh_catalog_hint",{},"The community brand and appliance lists are cached to keep the shared store within its daily budget. Refresh to pick up entries added or approved by others.")} -
    `}async _loadStoreStatus(t){if(!this._onlineEnabled()){this._storeStatus={enabled:!1},this._storeConnected=!1;return}try{const e=await this._ws({type:`${y}/store_status`,entry_id:t});if(!this._isActiveEntry(t))return;this._storeStatus=e||null,this._storeConnected=!!(e&&e.connected)}catch{}}_storeBrandScope(){return String(this._storeQuery||(this._opts||{}).store_brand||"").trim()}_renderKeepingStoreQFocus(){const t=this.shadowRoot,e=t&&t.getElementById("wd-store-q"),s=!!e&&t.activeElement===e,r=s?e.selectionStart:0;if(this._render(),!s)return;const n=t.getElementById("wd-store-q");if(!n)return;n.focus();try{n.setSelectionRange(r,r)}catch{}const a=n.parentElement&&n.parentElement.querySelector(".wd-combo-drop");a&&(a.hidden=!0)}_storeOpenDevice(t){const e=this._devices[this._selIdx];if(!e||!t)return;const s=e.entry_id;this._storeDevice=t,this._storeProfile=null,this._storeView="device",this._storeProfiles=[],this._storeCycles=[],this._storeLoading=!0,this._render(),this._ws({type:`${y}/store_get_profiles`,entry_id:s,device_id:t.id}).then(r=>{!this._isActiveEntry(s)||this._storeView!=="device"||(this._storeProfiles=r&&r.items||[])}).catch(()=>{this._isActiveEntry(s)&&(this._storeProfiles=[])}).finally(()=>{this._isActiveEntry(s)&&(this._storeLoading=!1,this._render())})}async _storeOpenModel(t){if(!t)return;await this._storeSearch(String(t.brand||""));const e=(this._storeDevices||[]).find(s=>String(s.id)===String(t.id))||t;this._storeOpenDevice(e)}async _storeSearch(t){const e=this._devices[this._selIdx];if(!e)return;const s=e.entry_id;this._storeQuery=t||"",this._storeView="brands",this._storeDevice=null,this._storeProfile=null,this._storeProfiles=[],this._storeCycles=[];const r=this._storeBrandScope();if(!r){this._storeDevices=[],this._storeLoading=!1,this._renderKeepingStoreQFocus();return}this._storeLoading=!0,this._renderKeepingStoreQFocus();try{const n=await this._ws({type:`${y}/store_search_devices`,entry_id:s,query:r,appliance_type:this._storeApplianceType(),include_pending:!0});if(!this._isActiveEntry(s))return;n&&n.disabled?(this._storeStatus={enabled:!1},this._storeDevices=[]):this._storeDevices=this._sortStoreDevices(n&&n.items||[])}catch(n){this._isActiveEntry(s)&&(this._storeDevices=[],this._showToast(this._t("toast.store_search_failed",{error:n.message||n},"Search failed: "+(n.message||n)),"error"))}finally{this._isActiveEntry(s)&&(this._storeLoading=!1,this._renderKeepingStoreQFocus())}}_sortStoreDevices(t){const e=String((this._opts||{}).store_model||"").trim().toLowerCase(),s=r=>e&&String(r.model||"").toLowerCase()===e?0:this._storeItemHasContent(r)?1:2;return t.map((r,n)=>({d:r,i:n})).sort((r,n)=>s(r.d)-s(n.d)||r.i-n.i).map(r=>r.d)}_storeItemHasContent(t){return(Number(t&&t.profileCount)||0)>0||(Number(t&&t.cycleCount)||0)>0}_ensureStoreConnectListener(){if(this._storeConnectListener)return;const t=this._constants.storeWebOrigin;if(!t)return;let e;try{e=new URL(t).origin}catch{return}this._storeConnectListener=async s=>{if(s.origin!==e)return;const r=s.data;if(!r)return;const n=this._devices[this._selIdx];if(!n)return;const a=n.entry_id;if(r.type==="washdata-device-created"){const o={};r.brand&&(o.store_brand=r.brand),r.model&&(o.store_model=r.model),this._catalog.brands=void 0,this._catalog.devices=void 0,this._catalog.forBrand=null,this._catalog.brandsFull=!1,this._catalog.brandPrefixes=[],this._dropModelCandidates(),this._catalogEntry=null;const i=this._storeContribEid||a,l=Object.keys(o).length>0,c=this._isActiveEntry(i);this._showToast(l&&!c?this._t("toast.contrib_device_changed",{},"Added to the community store, but the device changed while the form was open. Select it in Settings."):this._t("toast.appliance_added",{},"Appliance added - awaiting approval")),l&&c&&await this._saveStoreOptions(o,{silent:!0}),this._render();return}if(r.type==="washdata-brand-created"){this._catalog.brands=void 0,this._catalog.brandsFull=!1,this._catalog.brandPrefixes=[],this._dropModelCandidates(),this._catalogEntry=null;const o=this._isActiveEntry(this._storeContribEid||a);this._showToast(r.brand&&!o?this._t("toast.contrib_device_changed",{},"Added to the community store, but the device changed while the form was open. Select it in Settings."):this._t("toast.brand_added",{},"Brand added - awaiting approval")),r.brand&&o&&await this._saveStoreOptions({store_brand:r.brand,store_model:""},{silent:!0}),this._render();return}if(r.type==="washdata-profile-created"){const o=this._modal;o&&o.type==="store-share"&&(o.program=r.program||o.program,this._loadShareProfiles()),this._showToast(this._t("toast.profile_added",{},"Profile added - awaiting approval"));return}if(r.type==="washdata-connect")try{const o=await this._ws({type:`${y}/store_connect`,entry_id:a,refresh_token:r.refreshToken,uid:r.uid,name:r.displayName});if(o&&o.error){this._showToast(this._t("toast.store_connect_failed",{error:o.error},"Connect failed: "+o.error),"error");return}if(await this._loadStoreStatus(a),!this._isActiveEntry(a))return;this._showToast(this._t("toast.store_connected",{},"Connected to the community store")),this._render()}catch(o){this._showToast(this._t("toast.store_connect_failed",{error:o.message||o},"Connect failed: "+(o.message||o)),"error")}},window.addEventListener("message",this._storeConnectListener)}async _saveStoreOptions(t,{silent:e=!1}={}){const s=this._devices[this._selIdx];if(!s)return!1;const r=s.entry_id;try{if(await this._ws({type:`${y}/set_options`,entry_id:r,options:t}),!this._isActiveEntry(r))return!0;this._opts={...this._opts,...t};for(const n of Object.keys(t))delete this._pendingSettings[n],this._dirtyOptKeys.delete(n);return e||this._showToast(this._t("toast.settings_saved",{},"Settings saved; integration reloading")),!0}catch(n){return this._showToast(this._t("msg.toast_save_failed",{error:n.message||n},"Save failed: "+(n.message||n)),"error"),!1}}_htmlLogDrawer(){return`
    + + ${this._t("msg.refresh_catalog_hint",{},"Community brand and appliance lists are cached to save the store's daily budget. Refresh to see entries others added or approved.")} +
    `}async _loadStoreStatus(t){if(!this._onlineEnabled()){this._storeStatus={enabled:!1},this._storeConnected=!1;return}try{const e=await this._ws({type:`${w}/store_status`,entry_id:t});if(!this._isActiveEntry(t))return;this._storeStatus=e||null,this._storeConnected=!!(e&&e.connected)}catch{}}_storeBrandScope(){return String(this._storeQuery||(this._opts||{}).store_brand||"").trim()}_renderKeepingStoreQFocus(){const t=this.shadowRoot,e=t&&t.getElementById("wd-store-q"),s=!!e&&t.activeElement===e,a=s?e.selectionStart:0;if(this._render(),!s)return;const r=t.getElementById("wd-store-q");if(!r)return;r.focus();try{r.setSelectionRange(a,a)}catch{}const i=r.parentElement&&r.parentElement.querySelector(".wd-combo-drop");i&&(i.hidden=!0)}_storeOpenDevice(t){const e=this._devices[this._selIdx];if(!e||!t)return;const s=e.entry_id;this._storeDevice=t,this._storeProfile=null,this._storeView="device",this._storeProfiles=[],this._storeCycles=[],this._storeLoading=!0,this._storeBrowseErr=!1,this._render(),this._ws({type:`${w}/store_get_profiles`,entry_id:s,device_id:t.id}).then(a=>{!this._isActiveEntry(s)||this._storeView!=="device"||(this._storeProfiles=a&&a.items||[],this._storeBrowseErr=!!(a&&a.error))}).catch(()=>{this._isActiveEntry(s)&&(this._storeProfiles=[],this._storeBrowseErr=!0)}).finally(()=>{this._isActiveEntry(s)&&(this._storeLoading=!1,this._render())})}async _storeOpenModel(t){if(!t)return;await this._storeSearch(String(t.brand||""));const e=(this._storeDevices||[]).find(s=>String(s.id)===String(t.id))||t;this._storeOpenDevice(e)}async _storeSearch(t){const e=this._devices[this._selIdx];if(!e)return;const s=e.entry_id;this._storeQuery=t||"",this._storeView="brands",this._storeDevice=null,this._storeProfile=null,this._storeProfiles=[],this._storeCycles=[];const a=this._storeBrandScope();if(!a){this._storeDevices=[],this._storeLoading=!1,this._renderKeepingStoreQFocus();return}this._storeLoading=!0,this._renderKeepingStoreQFocus();try{const r=await this._ws({type:`${w}/store_search_devices`,entry_id:s,query:a,appliance_type:this._storeApplianceType(),include_pending:!0});if(!this._isActiveEntry(s))return;r&&r.disabled?(this._storeStatus={enabled:!1},this._storeDevices=[]):this._storeDevices=this._sortStoreDevices(r&&r.items||[])}catch(r){this._isActiveEntry(s)&&(this._storeDevices=[],this._showToast(this._tText("toast.store_search_failed",{error:r.message||r},"Search failed: "+(r.message||r)),"error"))}finally{this._isActiveEntry(s)&&(this._storeLoading=!1,this._renderKeepingStoreQFocus())}}_sortStoreDevices(t){const e=String((this._opts||{}).store_model||"").trim().toLowerCase(),s=a=>e&&String(a.model||"").toLowerCase()===e?0:this._storeItemHasContent(a)?1:2;return t.map((a,r)=>({d:a,i:r})).sort((a,r)=>s(a.d)-s(r.d)||a.i-r.i).map(a=>a.d)}_storeItemHasContent(t){return(Number(t&&t.profileCount)||0)>0||(Number(t&&t.cycleCount)||0)>0}_ensureStoreConnectListener(){if(this._storeConnectListener)return;const t=this._constants.storeWebOrigin;if(!t)return;let e;try{e=new URL(t).origin}catch{return}this._storeConnectListener=async s=>{if(s.origin!==e)return;const a=s.data;if(!a)return;const r=this._devices[this._selIdx];if(!r)return;const i=r.entry_id;if(a.type==="washdata-device-created"){const o={};a.brand&&(o.store_brand=a.brand),a.model&&(o.store_model=a.model),this._catalog.brands=void 0,this._catalog.devices=void 0,this._catalog.forBrand=null,this._catalog.brandsFull=!1,this._catalog.brandPrefixes=[],this._dropModelCandidates(),this._catalogEntry=null;const n=this._storeContribEid||i,l=Object.keys(o).length>0,c=this._isActiveEntry(n);this._showToast(l&&!c?this._t("toast.contrib_device_changed",{},"Added to the community store, but the device changed while the form was open. Select it in Settings."):this._t("toast.appliance_added",{},"Appliance added - awaiting approval")),l&&c&&await this._saveStoreOptions(o,{silent:!0}),this._render();return}if(a.type==="washdata-brand-created"){this._catalog.brands=void 0,this._catalog.brandsFull=!1,this._catalog.brandPrefixes=[],this._dropModelCandidates(),this._catalogEntry=null;const o=this._isActiveEntry(this._storeContribEid||i);this._showToast(a.brand&&!o?this._t("toast.contrib_device_changed",{},"Added to the community store, but the device changed while the form was open. Select it in Settings."):this._t("toast.brand_added",{},"Brand added - awaiting approval")),a.brand&&o&&await this._saveStoreOptions({store_brand:a.brand,store_model:""},{silent:!0}),this._render();return}if(a.type==="washdata-profile-created"){const o=this._modal;o&&o.type==="store-share"&&(o.program=a.program||o.program,this._loadShareProfiles()),this._showToast(this._t("toast.profile_added",{},"Profile added - awaiting approval"));return}if(a.type==="washdata-connect")try{const o=await this._ws({type:`${w}/store_connect`,entry_id:i,refresh_token:a.refreshToken,uid:a.uid,name:a.displayName});if(o&&o.error){this._showToast(this._tText("toast.store_connect_failed",{error:o.error},"Connect failed: "+o.error),"error");return}if(await this._loadStoreStatus(i),!this._isActiveEntry(i))return;this._showToast(this._t("toast.store_connected",{},"Connected to the community store")),this._render()}catch(o){this._showToast(this._tText("toast.store_connect_failed",{error:o.message||o},"Connect failed: "+(o.message||o)),"error")}},window.addEventListener("message",this._storeConnectListener)}async _saveStoreOptions(t,{silent:e=!1}={}){const s=this._devices[this._selIdx];if(!s)return!1;const a=s.entry_id;try{if(await this._ws({type:`${w}/set_options`,entry_id:a,options:t}),!this._isActiveEntry(a))return!0;this._opts={...this._opts,...t};for(const r of Object.keys(t))delete this._pendingSettings[r],this._dirtyOptKeys.delete(r);return e||this._showToast(this._t("toast.settings_saved",{},"Settings saved; integration reloading")),!0}catch(r){return this._showToast(this._tText("msg.toast_save_failed",{error:r.message||r},"Save failed: "+(r.message||r)),"error"),!1}}_htmlLogDrawer(){return`
    ${this._t("hdr.logs",{},"Logs")} @@ -1686,23 +1686,23 @@ button.wd-profile-card { display: block; }

    ${this._t("msg.log_buffer_hint",{},"Newest first \xB7 buffers the last 500 ha_washdata records since restart \xB7 drag the left edge to resize.")}

    ${this._logLinesHtml()}
    -
    `}_drawCurves(t,e){const s=this.shadowRoot&&this.shadowRoot.getElementById(t);if(!s)return null;const r=s.getBoundingClientRect(),n=window.devicePixelRatio||1,a=Math.max(1,Math.round(r.width*n)),o=Math.max(1,Math.round((r.height||240)*n));(s.width!==a||s.height!==o)&&(s.width=a,s.height=o);const i=s.getContext("2d"),l=getComputedStyle(this),c=(l.getPropertyValue("--primary-color")||"#03a9f4").trim()||"#03a9f4",g=(l.getPropertyValue("--divider-color")||"rgba(127,127,127,.3)").trim()||"rgba(127,127,127,.3)",p=(l.getPropertyValue("--secondary-text-color")||"#888").trim()||"#888",f=44*n,u=12*n,$=12*n,S=22*n,x=e.series||[];let w=e.xMax||0;w||(x.forEach(P=>(P.points||[]).forEach(C=>{C[0]>w&&(w=C[0])})),e.band&&(e.band.max||[]).forEach(P=>{P[0]>w&&(w=P[0])})),w=w||1;const v=this._canvasZoom&&this._canvasZoom[t],k=v?v.xMin:0,D=v?v.xMax:w;let I=e.yMax||0;if(!I){const P=C=>{const A=C||[];for(let F=0;F=k)||j>D&&!(F>0&&A[F-1][0]<=D)||A[F][1]>I&&(I=A[F][1])}};x.forEach(C=>{C.noScale||P(C.points)}),e.band&&P(e.band.max),I=(I||10)*1.08}const z=a-f-u,R=P=>f+(P-k)/(D-k)*z,L=P=>o-S-P/I*(o-$-S);i.clearRect(0,0,a,o),i.strokeStyle=g,i.lineWidth=n,i.fillStyle=p,i.font=`${11*n}px sans-serif`,i.textAlign="right",i.textBaseline="middle";for(let P=0;P<=2;P++){const C=$+P/2*(o-$-S);i.beginPath(),i.moveTo(f,C),i.lineTo(a-u,C),i.stroke(),i.fillText(Vt(I*(1-P/2),I/2),f-4*n,C)}if(i.save(),i.beginPath(),i.rect(f,$,z,o-$),i.clip(),(e.bands||[]).forEach(P=>{i.fillStyle=P.fill;const C=R(P.x0),A=R(P.x1);i.fillRect(Math.min(C,A),$,Math.abs(A-C),o-$-S)}),e.band&&(e.band.min||[]).length&&(e.band.max||[]).length){i.beginPath(),e.band.max.forEach((P,C)=>C?i.lineTo(R(P[0]),L(P[1])):i.moveTo(R(P[0]),L(P[1])));for(let P=e.band.min.length-1;P>=0;P--)i.lineTo(R(e.band.min[P][0]),L(e.band.min[P][1]));i.closePath(),i.fillStyle=e.band.fill||At(c,.13),i.fill()}return x.forEach(P=>{const C=P.points||[];if(!C.length)return;const A=P.stroke==="primary"?c:P.stroke;if(P.fill){i.beginPath(),C.forEach((j,st)=>st?i.lineTo(R(j[0]),L(j[1])):i.moveTo(R(j[0]),L(j[1]))),i.lineTo(R(C[C.length-1][0]),L(0)),i.lineTo(R(C[0][0]),L(0)),i.closePath();const F=i.createLinearGradient(0,$,0,o-S);F.addColorStop(0,At(A,.33)),F.addColorStop(1,At(A,.03)),i.fillStyle=F,i.fill()}i.beginPath(),C.forEach((F,j)=>j?i.lineTo(R(F[0]),L(F[1])):i.moveTo(R(F[0]),L(F[1]))),i.strokeStyle=A,i.lineWidth=(P.width||1.5)*n,i.lineJoin="round",P.dash&&i.setLineDash([6*n,4*n]),i.globalAlpha=P.alpha!=null?P.alpha:1,i.stroke(),i.globalAlpha=1,P.dash&&i.setLineDash([])}),i.textAlign="center",i.textBaseline="top",(e.vlines||[]).forEach(P=>{const C=R(P.x);i.beginPath(),i.moveTo(C,$),i.lineTo(C,o-S),i.strokeStyle=P.color,i.lineWidth=2*n,i.setLineDash([4*n,3*n]),i.stroke(),i.setLineDash([]),P.handle&&(i.fillStyle=P.color,i.beginPath(),i.arc(C,$+4*n,4.5*n,0,Math.PI*2),i.fill()),P.label&&(i.fillStyle=P.color,i.fillText(P.label,C,$+(P.handle?12*n:2*n)))}),i.restore(),i.fillStyle=p,i.font=`${11*n}px sans-serif`,i.textBaseline="bottom",v&&(i.textAlign="left",i.fillText((k/60).toFixed(1)+" min",f,o-2*n)),i.textAlign="right",i.fillText((D/60).toFixed(0)+" min",a-u,o-2*n),s._wd={xMax:w,xMin:k,xViewMax:D,yMax:I,dpr:n,padT:$,padB:S,padL:f,ch:o,primary:c,plotWcss:z/n,Xpx:R,Ypx:L,xToCss:P=>R(P)/n,cssToX:P=>Math.max(k,Math.min(D,k+(P*n-f)/z*(D-k))),series:(e.series||[]).map(P=>({points:P.points,stroke:P.stroke,name:P.name,cid:P.cid})),band:e.band||null,artifacts:e.artifacts||null,_opts:e},s._wd}_drawModalCanvas(){const t=this._modal;t&&(t.type==="cycle-detail"?this._drawCycleEditor():t.type==="compare-cycles"?this._drawCompareCanvas():t.type==="profile-group"?this._drawGroupCanvas():t.type==="profile-panel"&&(t.tab==="stats"?this._drawProfileEnvelope():t.tab==="phases"?this._drawPhaseEditor():t.tab==="cleanup"&&this._drawSpaghetti()))}_redrawCanvas(t){if(t==="wd-status-canvas")this._drawStatusCurve();else if(t==="wd-cyc-canvas")this._drawCycleEditor();else if(t==="wd-compare-canvas")this._drawCompareCanvas();else if(t==="wd-env-canvas")this._drawProfileEnvelope();else if(t==="wd-phase-canvas")this._drawPhaseEditor();else if(t==="wd-spag-canvas")this._drawSpaghetti();else if(t==="wd-pgroup-canvas")this._drawGroupCanvas();else{const e=this.shadowRoot&&this.shadowRoot.getElementById(t);e&&e._wd&&e._wd._opts&&this._drawCurves(t,e._wd._opts)}}_pref(t,e){const s=this._panelCfg&&this._panelCfg.prefs||{};return s[t]===void 0?e:s[t]}_setPref(t,e){this._panelCfg||(this._panelCfg={}),this._panelCfg.prefs={...this._panelCfg.prefs||{},[t]:e},this._ws({type:`${y}/set_user_prefs`,prefs:{[t]:e}}).catch(()=>{})}_drawStatusCurve(){const t=this._powerData||{},e=t.live||[];if(e.length<2)return;const s=this._statusEnv,r=this._pref("show_expected",!0),n=this._pref("show_raw_active",!1),a=[];let o=e[e.length-1][0];if(t.cycle_active&&s&&(s.avg||[]).length&&r){const l=s.target_duration||s.avg[s.avg.length-1][0];a.push({points:s.avg,stroke:"#ff9800",width:2,alpha:.4,name:this._t("lbl.expected",{},"Expected")}),o=Math.max(o,l)}a.push({points:e,stroke:"primary",fill:!0,width:2,name:this._t("lbl.power",{},"Power")}),n&&(t.raw||[]).length>1&&a.push({points:t.raw,stroke:"#9e9e9e",width:1,alpha:.65,name:this._t("lbl.raw_socket",{},"Raw socket"),noScale:!0});const i=[];(t.restart_gaps||[]).forEach(l=>{if(!e.length)return;const c=t.cycle_start_iso;if(!c)return;const g=new Date(c).getTime(),p=Math.max(0,(new Date(l.start_ts).getTime()-g)/1e3),f=Math.max(p+1,(new Date(l.end_ts).getTime()-g)/1e3);i.push({x0:p,x1:f,fill:"rgba(96,125,139,.20)"})}),this._drawCurves("wd-status-canvas",{series:a,xMax:o,bands:i})}_attachGraphGestures(t){const e=this.shadowRoot,s=e&&e.getElementById(t);if(!s)return;this._syncZoomReset(t),this._attachTouchOwnershipGuard(s);const r=new Map;let n=null;const a=i=>i-s.getBoundingClientRect().left;s.addEventListener("pointerdown",i=>{if(r.set(i.pointerId,{x:i.clientX,y:i.clientY}),r.size===1){if(i.pointerType==="touch"&&this._unpinGraphTip(),i.pointerType==="touch"&&this._onAxisHandle(s,t,i)){s._wdOwnGesture=!0;try{s.setPointerCapture(i.pointerId)}catch{}}return}if(r.size!==2)return;s._wdPinching=!0,s._wdOwnGesture=!1,s._wdAbortEdit&&s._wdAbortEdit(),this._hideGraphTip();const[l,c]=[...r.values()];n={dist:Math.max(1,Math.abs(l.x-c.x)),centerCss:a((l.x+c.x)/2)}}),s.addEventListener("pointermove",i=>{if(r.has(i.pointerId)&&r.set(i.pointerId,{x:i.clientX,y:i.clientY}),s._wdPinching){n&&r.size>=2&&this._onGraphPinch(s,t,r,n,a);return}this._onGraphHover(i,t)});const o=i=>{r.delete(i.pointerId),r.size<2&&(n=null,s._wdPinching=!1),r.size===0&&(s._wdOwnGesture=!1)};s.addEventListener("pointerup",i=>{const l=s._wdPinching;o(i),i.pointerType==="touch"&&!l&&this._pinGraphTip()}),s.addEventListener("pointercancel",i=>{o(i),this._hideGraphTip()}),s.addEventListener("pointerleave",i=>{i.pointerType!=="touch"&&this._hideGraphTip()}),s.addEventListener("wheel",i=>{const l=s._wd;if(!l)return;i.preventDefault();const c=i.ctrlKey?Math.min(4,Math.max(.25,Math.exp(i.deltaY*.01))):i.deltaY>0?1.3:.75;this._zoomCanvasAbout(t,l.cssToX(a(i.clientX)),c)},{passive:!1}),s.addEventListener("dblclick",i=>{const l=s._wd;if(l){if(this._canvasZoom[t]){this._resetCanvasZoom(t),this._hideGraphTip();return}this._zoomCanvasAbout(t,l.cssToX(a(i.clientX)),Qt)}})}_attachTouchOwnershipGuard(t){t._wdTouchGuard||(t._wdTouchGuard=!0,t.addEventListener("touchmove",e=>{(e.touches.length>=2||t._wdOwnGesture)&&e.preventDefault()},{passive:!1}))}_onAxisHandle(t,e,s){const r=this._gAxisHandle?this._gAxisHandle[e]:null,n=t._wd;if(r==null||!n)return!1;const a=t.getBoundingClientRect(),o=(n.ch-n.padB)/n.dpr;return Math.abs(s.clientX-a.left-r)<=Lt&&Math.abs(s.clientY-a.top-o)<=Lt}_onGraphPinch(t,e,s,r,n){const a=t._wd;if(!a)return;const[o,i]=[...s.values()],l=Math.max(1,Math.abs(o.x-i.x)),c=n((o.x+i.x)/2),g=this._gPinchPending&&this._gPinchPending.id===e?this._gPinchPending:null,p=g?{xMin:g.lo,xMax:g.hi}:this._canvasZoom[e],f=p?p.xMin:0,u=(p?p.xMax:a.xMax||1)-f,$=Math.max(1,a.plotWcss),S=Math.max(0,Math.min(1,c/$)),x=f+S*u,w=u*(r.dist/l),v=x-S*w-(c-r.centerCss)/$*w;r.dist=l,r.centerCss=c,this._gPinchPending={id:e,lo:v,hi:v+w},!this._gPinchRaf&&(this._gPinchRaf=requestAnimationFrame(()=>{this._gPinchRaf=null;const k=this._gPinchPending;this._gPinchPending=null,k&&this._setCanvasViewport(k.id,k.lo,k.hi)}))}_setCanvasViewport(t,e,s){const r=this.shadowRoot&&this.shadowRoot.getElementById(t),n=r&&r._wd;if(!n)return;const a=n.xMax||1,o=Math.min(a,Math.max(a/me,ye)),i=Math.min(a,Math.max(o,s-e));if(i>=a*.99){this._resetCanvasZoom(t);return}const l=Math.max(0,Math.min(a-i,e)),c=this._canvasZoom[t];c&&Math.abs(c.xMin-l)<1e-6&&Math.abs(c.xMax-l-i)<1e-6||(this._canvasZoom[t]={xMin:l,xMax:l+i},this._redrawCanvas(t),this._syncZoomReset(t))}_zoomCanvasAbout(t,e,s){const r=this.shadowRoot&&this.shadowRoot.getElementById(t),n=r&&r._wd;if(!n)return;const a=n.xMax||1,o=this._canvasZoom[t],i=o?o.xMin:0,l=Math.max(1e-6,(o?o.xMax:a)-i),c=Math.max(0,Math.min(1,(e-i)/l)),g=l*s;this._setCanvasViewport(t,e-c*g,e+(1-c)*g)}_resetCanvasZoom(t){if(!this._canvasZoom[t]){this._syncZoomReset(t);return}delete this._canvasZoom[t],this._redrawCanvas(t),this._syncZoomReset(t)}_ensureZoomReset(t,e){const s=e.parentElement;if(!s)return null;let r=s.querySelector(".wd-zoom-reset");r||(r=document.createElement("button"),r.type="button",r.className="wd-zoom-reset",r.textContent="\u21BB",r.addEventListener("click",a=>{a.stopPropagation(),this._resetCanvasZoom(t)}),s.appendChild(r));const n=this._t("btn.reset_zoom",{},"Reset zoom");return r.title!==n&&(r.title=n,r.setAttribute("aria-label",n)),r}_syncZoomReset(t){const e=this.shadowRoot&&this.shadowRoot.getElementById(t);if(!e)return;const s=this._ensureZoomReset(t,e);s&&s.classList.toggle("wd-zoom-reset--on",!!this._canvasZoom[t])}_onGraphHover(t,e){const s=t.clientX,r=t.clientY,n=t.pointerType==="touch";if(this._hoverRafId){this._hoverPending={px:s,py:r,id:e,touch:n};return}this._hoverPending={px:s,py:r,id:e,touch:n},this._hoverRafId=requestAnimationFrame(()=>{this._hoverRafId=null;const{px:a,py:o,id:i,touch:l}=this._hoverPending||{};i&&this._onGraphHoverInner(a,o,i,{touch:l})})}_onGraphHoverInner(t,e,s,r={}){const n=this.shadowRoot&&this.shadowRoot.getElementById(s),a=n&&n._wd;if(!a)return;const o=n.getBoundingClientRect(),i=a.cssToX(t-o.left),l=(e-o.top)*a.dpr;this._redrawCanvas(s);const c=n.getContext("2d"),g=a.Xpx(i);c.save(),c.strokeStyle="rgba(140,140,140,.75)",c.lineWidth=a.dpr,c.setLineDash([3*a.dpr,3*a.dpr]),c.beginPath(),c.moveTo(g,a.padT),c.lineTo(g,a.ch-a.padB),c.stroke(),c.setLineDash([]);const p=w=>w.stroke==="primary"?a.primary:w.stroke,f=(w,v)=>{c.fillStyle=v,c.beginPath(),c.arc(g,a.Ypx(w),3.4*a.dpr,0,6.2832),c.fill()},u=[`${this._t("lbl.from_start",{},"From start")}: ${Bt(i)}`,`${this._t("lbl.to_end",{},"To end")}: ${Bt(Math.max(0,a.xMax-i))}`],$=a.series||[];if(this._hoverNearest=null,$.length>4){let w=null,v=1/0;if($.forEach(k=>{const D=Ft(k.points,i);if(D==null)return;const I=Math.abs(a.Ypx(D)-l);II?c.lineTo(a.Xpx(D[0]),a.Ypx(D[1])):c.moveTo(a.Xpx(D[0]),a.Ypx(D[1]))),c.stroke(),f(w.v,k),u.push(`${d(w.s.name||"")}: ${w.v.toFixed(w.v<100?1:0)} W`),w.s.cid&&(u.push(`${this._t("lbl.click_to_select",{},"click to select")}`),this._hoverNearest={id:s,cid:w.s.cid})}}else $.forEach(w=>{const v=Ft(w.points,i);v!=null&&(f(v,p(w)),u.push(`${d(w.name||this._t("lbl.power",{},"Power"))}: ${v.toFixed(v<100?1:0)} W`))});if(a.band){const w=Ft(a.band.min,i),v=Ft(a.band.max,i);w!=null&&v!=null&&u.push(`${this._t("lbl.envelope",{},"Envelope")}: ${w.toFixed(w<100?1:0)}\u2013${v.toFixed(v<100?1:0)} W`)}(a.artifacts||[]).forEach(w=>{if(i>=w.start_s&&i<=w.end_s){const v=w.detail_key?this._t(w.detail_key,w.detail_params||{},w.detail||""):w.detail||"";u.push(`\u26A0 ${d(le(w.type,(k,D,I)=>this._t(k,D,I)))}: ${d(v)}`)}});const S=!!this._canvasZoom[s];let x=null;if(r.touch?x=S?this._t("lbl.zoom_hint_touch_zoomed",{},"pinch to zoom \xB7 double-tap to reset"):this._t("lbl.zoom_hint_touch",{},"pinch to zoom"):S&&(x=this._t("lbl.zoom_hint",{},"scroll to zoom \xB7 dblclick to reset")),x&&u.push(`${d(x)}`),r.touch){const w=a.ch-a.padB;c.beginPath(),c.arc(g,w,Lt*.41*a.dpr,0,6.2832),c.fillStyle=a.primary,c.fill(),c.strokeStyle="rgba(255,255,255,.92)",c.lineWidth=1.6*a.dpr,c.stroke(),this._gAxisHandle||(this._gAxisHandle={}),this._gAxisHandle[s]=a.xToCss(i)}else this._gAxisHandle&&delete this._gAxisHandle[s];c.restore(),this._showGraphTip(t,e,u,{touch:r.touch,rect:o}),this._syncSpagRowHighlight(this._hoverNearest?this._hoverNearest.cid:null)}_showGraphTip(t,e,s,r={}){const n=this._gtip;if(!n)return;this._gtipLines=s,n.innerHTML=s.join("
    "),n.classList.remove("wd-gtip--pinned"),this._gtipPinned=!1,n.style.display="block";const a=n.offsetWidth,o=n.offsetHeight,i=16;let l,c;if(r.touch&&r.rect){const g=r.rect,p=8;g.top-o-p>=6?c=g.top-o-p:g.bottom+p+o<=window.innerHeight-6?c=g.bottom+p:c=e>g.top+g.height*.5?g.top+6:g.bottom-o-6,l=t-a/2}else l=t+i,c=e+i,c+o>window.innerHeight-6&&(c=e-o-i),l+a>window.innerWidth-6&&(l=t-a-i);n.style.left=Math.max(6,Math.min(l,window.innerWidth-a-6))+"px",n.style.top=Math.max(6,Math.min(c,window.innerHeight-o-6))+"px"}_pinGraphTip(){const t=this._gtip;if(!t||t.style.display==="none"||this._gtipPinned)return;this._gtipPinned=!0,t.classList.add("wd-gtip--pinned");const e=this._t("lbl.tap_to_dismiss",{},"tap the chart to dismiss");t.innerHTML=(this._gtipLines||[]).join("
    ")+`${d(e)}`}_maybeDismissGraphTip(t){if(!this._gtipPinned)return;const e=t.target;e&&e.closest&&e.closest(".wd-canvas-wrap")||this._unpinGraphTip()}_unpinGraphTip(){this._gtipPinned&&(this._gtipPinned=!1,this._gtip&&(this._gtip.classList.remove("wd-gtip--pinned"),this._gtip.style.display="none"))}_hideGraphTip(){this._hoverRafId&&(cancelAnimationFrame(this._hoverRafId),this._hoverRafId=null,this._hoverPending=null),this._gtipPinned=!1,this._gtip&&(this._gtip.classList.remove("wd-gtip--pinned"),this._gtip.style.display="none"),this._syncSpagRowHighlight(null)}_positionTip(t){const e=t.querySelector(".wd-tip-pop");if(!e)return;e.style.transform="";const s=!e.offsetWidth,r=s?e.style.cssText:"";s&&(e.style.visibility="hidden",e.style.display="block");const n=e.getBoundingClientRect();if(s&&(e.style.cssText=r),!n.width)return;const a=_e(t),o=6,i=a.left+o,l=a.right-o-n.width,c=la,l=Math.max(64,Math.floor(i?o:a));e.classList.toggle("wd-drop-up",i),e.style.maxHeight=`${Math.min(220,l)}px`}_syncSpagRowHighlight(t){if(t===this._spagHoverCid)return;this._spagHoverCid=t;const e=this.shadowRoot;e&&e.querySelectorAll("tr[data-cid]").forEach(s=>{s.style.backgroundColor=t&&s.dataset.cid===t?"var(--secondary-background-color,rgba(0,0,0,.06))":""})}_showToast(t,e="success",s={}){this._toastTimer&&clearTimeout(this._toastTimer);const r=s.duration||3500;this._toast={msg:t,cls:`wd-toast-${e}`,actionLabel:s.actionLabel||null,actionToken:s.actionToken||null},this._render(),this._toastTimer=setTimeout(()=>{this._toast=null,this._render()},r)}_profileOptions(t){return(this._profiles||[]).map(e=>``).join("")}_htmlModal(){const t=this._modal;if(t.type==="cycle-detail")return`
    `;if(t.type==="profile-panel")return`
    `;if(t.type==="profile-group")return`
    `;if(t.type==="compare-cycles")return`
    `;if(t.type==="gear-settings")return`
    `;if(t.type==="export-select")return`
    `;if(t.type==="import-wizard")return`
    `;if(t.type==="history-import")return`
    `;let e="";if(t.type==="confirm")e=`

    ${d(t.title)}

    ${d(t.message)}

    +
    `}_drawCurves(t,e){const s=this.shadowRoot&&this.shadowRoot.getElementById(t);if(!s)return null;const a=s.getBoundingClientRect(),r=window.devicePixelRatio||1,i=Math.max(1,Math.round(a.width*r)),o=Math.max(1,Math.round((a.height||240)*r));(s.width!==i||s.height!==o)&&(s.width=i,s.height=o);const n=s.getContext("2d"),l=getComputedStyle(this),c=(l.getPropertyValue("--primary-color")||"#03a9f4").trim()||"#03a9f4",p=(l.getPropertyValue("--divider-color")||"rgba(127,127,127,.3)").trim()||"rgba(127,127,127,.3)",_=(l.getPropertyValue("--secondary-text-color")||"#888").trim()||"#888",f=44*r,u=12*r,b=12*r,v=22*r,T=e.series||[];let C=e.xMax||0;C||(T.forEach(A=>(A.points||[]).forEach(O=>{O[0]>C&&(C=O[0])})),e.band&&(e.band.max||[]).forEach(A=>{A[0]>C&&(C=A[0])})),C=C||1;const x=Math.min(0,Number(e.xStart)||0),$=this._canvasZoom&&this._canvasZoom[t],L=$?$.xMin:x,D=$?$.xMax:C;let B=e.yMax||0;if(!B){const A=O=>{const K=O||[];for(let I=0;I=L)||R>D&&!(I>0&&K[I-1][0]<=D)||K[I][1]>B&&(B=K[I][1])}};T.forEach(O=>{O.noScale||A(O.points)}),e.band&&A(e.band.max),B=(B||10)*1.08}const U=i-f-u,z=A=>f+(A-L)/(D-L)*U,M=A=>o-v-A/B*(o-b-v);n.clearRect(0,0,i,o),n.strokeStyle=p,n.lineWidth=r,n.fillStyle=_,n.font=`${11*r}px sans-serif`,n.textAlign="right",n.textBaseline="middle";for(let A=0;A<=2;A++){const O=b+A/2*(o-b-v);n.beginPath(),n.moveTo(f,O),n.lineTo(i-u,O),n.stroke(),n.fillText(Ht(B*(1-A/2),B/2),f-4*r,O)}if(n.save(),n.beginPath(),n.rect(f,b,U,o-b),n.clip(),(e.bands||[]).forEach(A=>{n.fillStyle=A.fill;const O=z(A.x0),K=z(A.x1);n.fillRect(Math.min(O,K),b,Math.abs(K-O),o-b-v)}),e.band&&(e.band.min||[]).length&&(e.band.max||[]).length){n.beginPath(),e.band.max.forEach((A,O)=>O?n.lineTo(z(A[0]),M(A[1])):n.moveTo(z(A[0]),M(A[1])));for(let A=e.band.min.length-1;A>=0;A--)n.lineTo(z(e.band.min[A][0]),M(e.band.min[A][1]));n.closePath(),n.fillStyle=e.band.fill||Pt(c,.13),n.fill()}return T.forEach(A=>{const O=A.points||[];if(!O.length)return;const K=A.stroke==="primary"?c:A.stroke;if(A.fill){n.beginPath(),O.forEach((G,W)=>W?n.lineTo(z(G[0]),M(G[1])):n.moveTo(z(G[0]),M(G[1]))),n.lineTo(z(O[O.length-1][0]),M(0)),n.lineTo(z(O[0][0]),M(0)),n.closePath();const R=n.createLinearGradient(0,b,0,o-v);R.addColorStop(0,Pt(K,.33)),R.addColorStop(1,Pt(K,.03)),n.fillStyle=R,n.fill()}n.beginPath();let I=!1;O.forEach(R=>{if(R[1]==null){I=!1;return}I?n.lineTo(z(R[0]),M(R[1])):(n.moveTo(z(R[0]),M(R[1])),I=!0)}),n.strokeStyle=K,n.lineWidth=(A.width||1.5)*r,n.lineJoin="round",A.dash&&n.setLineDash([6*r,4*r]),n.globalAlpha=A.alpha!=null?A.alpha:1,n.stroke(),n.globalAlpha=1,A.dash&&n.setLineDash([])}),n.textAlign="center",n.textBaseline="top",(e.vlines||[]).forEach(A=>{const O=z(A.x);n.beginPath(),n.moveTo(O,b),n.lineTo(O,o-v),n.strokeStyle=A.color,n.lineWidth=2*r,n.setLineDash([4*r,3*r]),n.stroke(),n.setLineDash([]),A.handle&&(n.fillStyle=A.color,n.beginPath(),n.arc(O,b+4*r,4.5*r,0,Math.PI*2),n.fill()),A.label&&(n.fillStyle=A.color,n.fillText(A.label,O,b+(A.handle?12*r:2*r)))}),n.restore(),n.fillStyle=_,n.font=`${11*r}px sans-serif`,n.textBaseline="bottom",($||x<0)&&(n.textAlign="left",n.fillText((L/60).toFixed($?1:0)+" min",f,o-2*r)),n.textAlign="right",n.fillText((D/60).toFixed(0)+" min",i-u,o-2*r),s._wd={xMax:C,xMin:L,xViewMax:D,yMax:B,dpr:r,padT:b,padB:v,padL:f,ch:o,primary:c,xStart:x,cycleSpan:e.cycleSpan||null,plotWcss:U/r,Xpx:z,Ypx:M,xToCss:A=>z(A)/r,cssToX:A=>Math.max(L,Math.min(D,L+(A*r-f)/U*(D-L))),series:(e.series||[]).map(A=>({points:A.points,stroke:A.stroke,name:A.name,cid:A.cid,bounded:A.bounded,aside:A.aside,kind:A.kind})),band:e.band||null,artifacts:e.artifacts||null,_opts:e},s._wd}_drawModalCanvas(){const t=this._modal;t&&(t.type==="cycle-detail"?this._drawCycleEditor():t.type==="compare-cycles"?this._drawCompareCanvas():t.type==="profile-group"?this._drawGroupCanvas():t.type==="profile-panel"&&(t.tab==="stats"?this._drawProfileEnvelope():t.tab==="phases"?this._drawPhaseEditor():t.tab==="cleanup"&&this._drawSpaghetti()))}_redrawCanvas(t){if(t==="wd-status-canvas")this._drawStatusCurve();else if(t==="wd-cyc-canvas")this._drawCycleEditor();else if(t==="wd-compare-canvas")this._drawCompareCanvas();else if(t==="wd-env-canvas")this._drawProfileEnvelope();else if(t==="wd-phase-canvas")this._drawPhaseEditor();else if(t==="wd-spag-canvas")this._drawSpaghetti();else if(t==="wd-pgroup-canvas")this._drawGroupCanvas();else{const e=this.shadowRoot&&this.shadowRoot.getElementById(t);e&&e._wd&&e._wd._opts&&this._drawCurves(t,e._wd._opts)}}_pref(t,e){const s=this._panelCfg&&this._panelCfg.prefs||{};return s[t]===void 0?e:s[t]}_setPref(t,e){this._panelCfg||(this._panelCfg={}),this._panelCfg.prefs={...this._panelCfg.prefs||{},[t]:e},this._ws({type:`${w}/set_user_prefs`,prefs:{[t]:e}}).catch(()=>{})}_drawStatusCurve(){const t=this._powerData||{},e=t.live||[];if(e.length<2)return;const s=this._statusEnv,a=this._pref("show_expected",!0),r=this._pref("show_raw_active",!1),i=[];let o=e[e.length-1][0];if(t.cycle_active&&s&&(s.avg||[]).length&&a){const l=s.target_duration||s.avg[s.avg.length-1][0];i.push({points:s.avg,stroke:"#ff9800",width:2,alpha:.4,name:this._t("lbl.expected",{},"Expected")}),o=Math.max(o,l)}i.push({points:e,stroke:"primary",fill:!0,width:2,name:this._t("lbl.power",{},"Power")}),r&&(t.raw||[]).length>1&&i.push({points:t.raw,stroke:"#9e9e9e",width:1,alpha:.65,name:this._t("lbl.raw_socket",{},"Raw socket"),noScale:!0});const n=[];(t.restart_gaps||[]).forEach(l=>{if(!e.length)return;const c=t.cycle_start_iso;if(!c)return;const p=new Date(c).getTime(),_=Math.max(0,(new Date(l.start_ts).getTime()-p)/1e3),f=Math.max(_+1,(new Date(l.end_ts).getTime()-p)/1e3);n.push({x0:_,x1:f,fill:"rgba(96,125,139,.20)"})}),this._drawCurves("wd-status-canvas",{series:i,xMax:o,bands:n})}_attachGraphGestures(t){const e=this.shadowRoot,s=e&&e.getElementById(t);if(!s)return;this._syncZoomReset(t),this._attachTouchOwnershipGuard(s);const a=new Map;let r=null;const i=n=>n-s.getBoundingClientRect().left;s.addEventListener("pointerdown",n=>{if(a.set(n.pointerId,{x:n.clientX,y:n.clientY}),a.size===1){if(n.pointerType==="touch"&&this._unpinGraphTip(),n.pointerType==="touch"&&this._onAxisHandle(s,t,n)){s._wdOwnGesture=!0;try{s.setPointerCapture(n.pointerId)}catch{}}return}if(a.size!==2)return;s._wdPinching=!0,s._wdOwnGesture=!1,s._wdAbortEdit&&s._wdAbortEdit(),this._hideGraphTip();const[l,c]=[...a.values()];r={dist:Math.max(1,Math.abs(l.x-c.x)),centerCss:i((l.x+c.x)/2)}}),s.addEventListener("pointermove",n=>{if(a.has(n.pointerId)&&a.set(n.pointerId,{x:n.clientX,y:n.clientY}),s._wdPinching){r&&a.size>=2&&this._onGraphPinch(s,t,a,r,i);return}this._onGraphHover(n,t)});const o=n=>{a.delete(n.pointerId),a.size<2&&(r=null,s._wdPinching=!1),a.size===0&&(s._wdOwnGesture=!1)};s.addEventListener("pointerup",n=>{const l=s._wdPinching;o(n),n.pointerType==="touch"&&!l&&this._pinGraphTip()}),s.addEventListener("pointercancel",n=>{o(n),this._hideGraphTip()}),s.addEventListener("pointerleave",n=>{n.pointerType!=="touch"&&this._hideGraphTip()}),s.addEventListener("wheel",n=>{const l=s._wd;if(!l)return;n.preventDefault();const c=n.ctrlKey?Math.min(4,Math.max(.25,Math.exp(n.deltaY*.01))):n.deltaY>0?1.3:.75;this._zoomCanvasAbout(t,l.cssToX(i(n.clientX)),c)},{passive:!1}),s.addEventListener("dblclick",n=>{const l=s._wd;if(l){if(this._canvasZoom[t]){this._resetCanvasZoom(t),this._hideGraphTip();return}this._zoomCanvasAbout(t,l.cssToX(i(n.clientX)),Qt)}})}_attachTouchOwnershipGuard(t){t._wdTouchGuard||(t._wdTouchGuard=!0,t.addEventListener("touchmove",e=>{(e.touches.length>=2||t._wdOwnGesture)&&e.preventDefault()},{passive:!1}))}_onAxisHandle(t,e,s){const a=this._gAxisHandle?this._gAxisHandle[e]:null,r=t._wd;if(a==null||!r)return!1;const i=t.getBoundingClientRect(),o=(r.ch-r.padB)/r.dpr;return Math.abs(s.clientX-i.left-a)<=Tt&&Math.abs(s.clientY-i.top-o)<=Tt}_onGraphPinch(t,e,s,a,r){const i=t._wd;if(!i)return;const[o,n]=[...s.values()],l=Math.max(1,Math.abs(o.x-n.x)),c=r((o.x+n.x)/2),p=this._gPinchPending&&this._gPinchPending.id===e?this._gPinchPending:null,_=p?{xMin:p.lo,xMax:p.hi}:this._canvasZoom[e],f=_?_.xMin:i.xStart||0,u=(_?_.xMax:i.xMax||1)-f,b=Math.max(1,i.plotWcss),v=Math.max(0,Math.min(1,c/b)),T=f+v*u,C=u*(a.dist/l),x=T-v*C-(c-a.centerCss)/b*C;a.dist=l,a.centerCss=c,this._gPinchPending={id:e,lo:x,hi:x+C},!this._gPinchRaf&&(this._gPinchRaf=requestAnimationFrame(()=>{this._gPinchRaf=null;const $=this._gPinchPending;this._gPinchPending=null,$&&this._setCanvasViewport($.id,$.lo,$.hi)}))}_setCanvasViewport(t,e,s){const a=this.shadowRoot&&this.shadowRoot.getElementById(t),r=a&&a._wd;if(!r)return;const i=r.xStart||0,o=(r.xMax||1)-i,n=Math.min(o,Math.max(o/Se,Te)),l=Math.min(o,Math.max(n,s-e));if(l>=o*.99){this._resetCanvasZoom(t);return}const c=Math.max(i,Math.min(i+o-l,e)),p=this._canvasZoom[t];p&&Math.abs(p.xMin-c)<1e-6&&Math.abs(p.xMax-c-l)<1e-6||(this._canvasZoom[t]={xMin:c,xMax:c+l},this._redrawCanvas(t),this._syncZoomReset(t))}_zoomCanvasAbout(t,e,s){const a=this.shadowRoot&&this.shadowRoot.getElementById(t),r=a&&a._wd;if(!r)return;const i=r.xMax||1,o=this._canvasZoom[t],n=o?o.xMin:r.xStart||0,l=Math.max(1e-6,(o?o.xMax:i)-n),c=Math.max(0,Math.min(1,(e-n)/l)),p=l*s;this._setCanvasViewport(t,e-c*p,e+(1-c)*p)}_resetCanvasZoom(t){if(!this._canvasZoom[t]){this._syncZoomReset(t);return}delete this._canvasZoom[t],this._redrawCanvas(t),this._syncZoomReset(t)}_ensureZoomReset(t,e){const s=e.parentElement;if(!s)return null;let a=s.querySelector(".wd-zoom-reset");a||(a=document.createElement("button"),a.type="button",a.className="wd-zoom-reset",a.textContent="\u21BB",a.addEventListener("click",i=>{i.stopPropagation(),this._resetCanvasZoom(t)}),s.appendChild(a));const r=this._t("btn.reset_zoom",{},"Reset zoom");return a.title!==r&&(a.title=r,a.setAttribute("aria-label",r)),a}_syncZoomReset(t){const e=this.shadowRoot&&this.shadowRoot.getElementById(t);if(!e)return;const s=this._ensureZoomReset(t,e);s&&s.classList.toggle("wd-zoom-reset--on",!!this._canvasZoom[t])}_onGraphHover(t,e){const s=t.clientX,a=t.clientY,r=t.pointerType==="touch";if(this._hoverRafId){this._hoverPending={px:s,py:a,id:e,touch:r};return}this._hoverPending={px:s,py:a,id:e,touch:r},this._hoverRafId=requestAnimationFrame(()=>{this._hoverRafId=null;const{px:i,py:o,id:n,touch:l}=this._hoverPending||{};n&&this._onGraphHoverInner(i,o,n,{touch:l})})}_onGraphHoverInner(t,e,s,a={}){const r=this.shadowRoot&&this.shadowRoot.getElementById(s),i=r&&r._wd;if(!i)return;const o=r.getBoundingClientRect(),n=i.cssToX(t-o.left),l=(e-o.top)*i.dpr;this._redrawCanvas(s);const c=r.getContext("2d"),p=i.Xpx(n);c.save(),c.strokeStyle="rgba(140,140,140,.75)",c.lineWidth=i.dpr,c.setLineDash([3*i.dpr,3*i.dpr]),c.beginPath(),c.moveTo(p,i.padT),c.lineTo(p,i.ch-i.padB),c.stroke(),c.setLineDash([]);const _=$=>$.stroke==="primary"?i.primary:$.stroke,f=($,L)=>{c.fillStyle=L,c.beginPath(),c.arc(p,i.Ypx($),3.4*i.dpr,0,6.2832),c.fill()},u=i.cycleSpan,b=u&&n${$t(u[0]-n)}`]:u&&n>u[1]?[`${this._t("lbl.after_end",{},"After end")}: ${$t(n-u[1])}`]:[`${this._t("lbl.from_start",{},"From start")}: ${$t(n)}`,`${this._t("lbl.to_end",{},"To end")}: ${$t(Math.max(0,(u?u[1]:i.xMax)-n))}`],v=i.series||[],T=$=>$.bounded?Ue($.points,n):Gt($.points,n);if(this._hoverNearest=null,v.filter($=>!$.aside).length>4){let $=null,L=1/0;if(v.forEach(D=>{const B=T(D);if(B==null)return;const U=Math.abs(i.Ypx(B)-l);UU?c.lineTo(i.Xpx(B[0]),i.Ypx(B[1])):c.moveTo(i.Xpx(B[0]),i.Ypx(B[1]))),c.stroke(),f($.v,D),b.push(`${d($.s.name||"")}: ${it($.v,$.v<100?1:0)} W`),$.s.cid&&(b.push(`${this._t("lbl.click_to_select",{},"click to select")}`),this._hoverNearest={id:s,cid:$.s.cid})}}else v.forEach($=>{const L=T($);L!=null&&(f(L,_($)),b.push(`${d($.name||this._t("lbl.power",{},"Power"))}: ${it(L,L<100?1:0)} W`))});if(i.band){const $=Gt(i.band.min,n),L=Gt(i.band.max,n);$!=null&&L!=null&&b.push(`${this._t("lbl.envelope",{},"Envelope")}: ${it($,$<100?1:0)}\u2013${it(L,L<100?1:0)} W`)}(i.artifacts||[]).forEach($=>{if(n>=$.start_s&&n<=$.end_s){const L=$.detail_key?this._tText($.detail_key,$.detail_params||{},$.detail||""):$.detail||"";b.push(`\u26A0 ${d(ue($.type,(D,B,U)=>this._t(D,B,U)))}: ${d(L)}`)}});const C=!!this._canvasZoom[s];let x=null;if(a.touch?x=C?this._t("lbl.zoom_hint_touch_zoomed",{},"pinch to zoom \xB7 double-tap to reset"):this._t("lbl.zoom_hint_touch",{},"pinch to zoom"):C&&(x=this._t("lbl.zoom_hint",{},"scroll to zoom \xB7 dblclick to reset")),x&&b.push(`${d(x)}`),a.touch){const $=i.ch-i.padB;c.beginPath(),c.arc(p,$,Tt*.41*i.dpr,0,6.2832),c.fillStyle=i.primary,c.fill(),c.strokeStyle="rgba(255,255,255,.92)",c.lineWidth=1.6*i.dpr,c.stroke(),this._gAxisHandle||(this._gAxisHandle={}),this._gAxisHandle[s]=i.xToCss(n)}else this._gAxisHandle&&delete this._gAxisHandle[s];c.restore(),this._showGraphTip(t,e,b,{touch:a.touch,rect:o}),this._syncSpagRowHighlight(this._hoverNearest?this._hoverNearest.cid:null)}_showGraphTip(t,e,s,a={}){const r=this._gtip;if(!r)return;this._gtipLines=s,r.innerHTML=s.join("
    "),r.classList.remove("wd-gtip--pinned"),this._gtipPinned=!1,r.style.display="block";const i=r.offsetWidth,o=r.offsetHeight,n=16;let l,c;if(a.touch&&a.rect){const p=a.rect,_=8;p.top-o-_>=6?c=p.top-o-_:p.bottom+_+o<=window.innerHeight-6?c=p.bottom+_:c=e>p.top+p.height*.5?p.top+6:p.bottom-o-6,l=t-i/2}else l=t+n,c=e+n,c+o>window.innerHeight-6&&(c=e-o-n),l+i>window.innerWidth-6&&(l=t-i-n);r.style.left=Math.max(6,Math.min(l,window.innerWidth-i-6))+"px",r.style.top=Math.max(6,Math.min(c,window.innerHeight-o-6))+"px"}_pinGraphTip(){const t=this._gtip;if(!t||t.style.display==="none"||this._gtipPinned)return;this._gtipPinned=!0,t.classList.add("wd-gtip--pinned");const e=this._t("lbl.tap_to_dismiss",{},"tap the chart to dismiss");t.innerHTML=(this._gtipLines||[]).join("
    ")+`${d(e)}`}_maybeDismissGraphTip(t){if(!this._gtipPinned)return;const e=t.target;e&&e.closest&&e.closest(".wd-canvas-wrap")||this._unpinGraphTip()}_unpinGraphTip(){this._gtipPinned&&(this._gtipPinned=!1,this._gtip&&(this._gtip.classList.remove("wd-gtip--pinned"),this._gtip.style.display="none"))}_hideGraphTip(){this._hoverRafId&&(cancelAnimationFrame(this._hoverRafId),this._hoverRafId=null,this._hoverPending=null),this._gtipPinned=!1,this._gtip&&(this._gtip.classList.remove("wd-gtip--pinned"),this._gtip.style.display="none"),this._syncSpagRowHighlight(null)}_positionTip(t){const e=t.querySelector(".wd-tip-pop");if(!e)return;e.style.transform="";const s=!e.offsetWidth,a=s?e.style.cssText:"";s&&(e.style.visibility="hidden",e.style.display="block");const r=e.getBoundingClientRect();if(s&&(e.style.cssText=a),!r.width)return;const i=be(t),o=6,n=i.left+o,l=i.right-o-r.width,c=li,l=Math.max(64,Math.floor(n?o:i));e.classList.toggle("wd-drop-up",n),e.style.maxHeight=`${Math.min(220,l)}px`}_syncSpagRowHighlight(t){if(t===this._spagHoverCid)return;this._spagHoverCid=t;const e=this.shadowRoot;e&&e.querySelectorAll("tr[data-cid]").forEach(s=>{s.style.backgroundColor=t&&s.dataset.cid===t?"var(--secondary-background-color,rgba(0,0,0,.06))":""})}_showToast(t,e="success",s={}){this._toastTimer&&clearTimeout(this._toastTimer);const a=s.duration||3500;this._toast={msg:t,cls:`wd-toast-${e}`,actionLabel:s.actionLabel||null,actionToken:s.actionToken||null},this._render(),this._toastTimer=setTimeout(()=>{this._toast=null,this._render()},a)}async _openAutoLabelModal(t){let e=this._opts?this._opts.auto_label_confidence:void 0;if(e==null&&t)try{const r=await this._ws({type:`${w}/get_options`,entry_id:t});if(!this._isActiveEntry(t))return;e=(r&&r.options||{}).auto_label_confidence}catch{}e==null&&(e=(wt.auto_label_confidence||{}).def);const s=Number(e),a=Number.isFinite(s)?+Math.min(.95,Math.max(.5,s)).toFixed(2):.9;this._modal={type:"auto-label",threshold:a},this._render()}_autoLabelToast(t){const e=t&&t.result;return!e||typeof e.labeled!="number"?this._tText("msg.toast_auto_label_complete",{},"Auto-label complete"):t.state==="cancelled"?this._tText("toast.auto_label_cancelled",{count:e.labeled},"Auto-label cancelled after labelling {count} cycles."):this._tText("toast.auto_label_done",{count:e.labeled,skipped:e.skipped||0},"Auto-label complete: {count} cycles labelled, {skipped} left unlabelled.")}_profileOptions(t){return(this._profiles||[]).map(e=>``).join("")}_htmlModal(){const t=this._modal;if(t.type==="cycle-detail")return`
    `;if(t.type==="profile-panel")return`
    `;if(t.type==="profile-group")return`
    `;if(t.type==="compare-cycles")return`
    `;if(t.type==="gear-settings")return`
    `;if(t.type==="export-select")return`
    `;if(t.type==="import-wizard")return`
    `;if(t.type==="history-import")return`
    `;let e="";if(t.type==="confirm"){const s=(t.items||[]).map(a=>`
  • ${d(a.text)}${a.sub?`
    ${d(a.sub)}
    `:""}
  • `).join("");e=`

    ${d(t.title)}

    ${d(t.message)}

    ${s?`
      ${s}
    `:""}
    -
    `;else if(t.type==="label-cycle")e=`

    ${this._t("modal.label_cycle",{},"Label Cycle")}

    + `}else if(t.type==="label-cycle")e=`

    ${this._t("modal.label_cycle",{},"Label Cycle")}

    -
    `;else if(t.type==="create-profile"){const s=(this._cycles||[]).slice(0,40).map(r=>``).join("");e=`

    ${this._t("modal.create_profile",{},"Create Profile")}

    + `;else if(t.type==="create-profile"){const s=(this._cycles||[]).slice(0,40);let a=s.map(r=>``).join("");t.prefillCycle&&!s.some(r=>r.id===t.prefillCycle)&&(a=``+a),e=`

    ${this._t("modal.create_profile",{},"Create Profile")}

    -
    -
    ${this._t("msg.manual_duration_ref_hint",{},"Only used when no reference cycle is selected \u2014 a reference cycle sets the duration from its own length.")}
    +
    +
    ${this._t("msg.manual_duration_ref_hint",{},"Only used without a reference cycle; a reference cycle sets the duration from its own length.")}
    `}else if(t.type==="create-phase")e=`

    ${this._t("modal.new_phase",{},"New Phase")}

    -
    `;else if(t.type==="edit-phase"){const s=t.isDefault?`

    ${this._t("msg.edit_builtin_phase",{},"This is a built-in phase. Saving creates a custom override \u2014 the original is preserved and can be restored by deleting the override.")}

    `:"";e=`

    ${this._t("modal.edit_phase",{},"Edit Phase")} ${t.isDefault?`${this._t("badge.built_in_tag",{},"built-in")}`:""}

    + `;else if(t.type==="edit-phase"){const s=t.isDefault?`

    ${this._t("msg.edit_builtin_phase",{},"This is a built-in phase. Saving creates a custom copy; delete the copy to restore the original.")}

    `:"";e=`

    ${this._t("modal.edit_phase",{},"Edit Phase")} ${t.isDefault?`${this._t("badge.built_in_tag",{},"built-in")}`:""}

    ${s}
    @@ -1715,18 +1715,13 @@ button.wd-profile-card { display: block; }
    ${d(this._t("msg.tail_trim_hint",{},"Remove this many seconds from the end"))}
    `;else if(t.type==="correct-feedback")e=`

    ${this._t("modal.correct_feedback",{},"Correct Feedback")}

    -

    WashData detected: ${d(t.detectedProfile)}

    +

    ${this._t("msg.feedback_detected",{name:vt(`${d(t.detectedProfile)}`)},"WashData detected: {name}")}

    -
    `;else if(t.type==="import-config")e=`

    ${this._t("modal.import_config",{},"Import Configuration")}

    -

    ${this._t("msg.import_intro",{},"Load an exported file or paste a JSON payload below.")}

    -
    -
    -
    -
    `;else if(t.type==="auto-label")e=`

    ${this._t("modal.auto_label",{},"Auto-Label Cycles")}

    + `;else if(t.type==="auto-label")e=`

    ${this._t("modal.auto_label",{},"Auto-Label Cycles")}

    ${this._t("msg.auto_label_intro",{},"Assign profiles to unlabelled cycles whose match confidence clears the threshold.")}

    -
    +
    `;else if(t.type==="merge-cycles")e=`

    ${this._t("modal.merge_cycles",{n:t.ids.length},`Merge ${t.ids.length} Cycles`)}

    ${this._t("msg.merge_intro",{},"The selected cycles are combined into one (chronological order; gaps filled with 0 W). Pick the resulting profile.")}

    @@ -1738,184 +1733,187 @@ button.wd-profile-card { display: block; }
    -
    `;else if(t.type==="store-import"){const s=t.mode!=="merge",r=``;e=`

    ${this._t("modal.store_import",{},"Import reference cycle")}

    + `;else if(t.type==="store-import"){const s=t.mode!=="merge",a=``;e=`

    ${this._t("modal.store_import",{},"Import reference cycle")}

    - ${s?`
    `:`
    ${r}
    `} + ${s?`
    `:`
    ${a}
    `}
    -
    `}else if(t.type==="store-share"){const s=Array.isArray(t.profiles)?t.profiles:[],r=[],n=new Set,a=l=>{const c=(l||"").toLowerCase();l&&!n.has(c)&&(n.add(c),r.push(l))};a(t.program),s.forEach(l=>a(l.program));const o=t.profiles==null?` ${this._t("msg.loading",{},"Loading\u2026")}`:"",i=r.length?r.map(l=>``).join(""):``;e=`

    ${this._t("modal.store_share",{},"Share to community store")}

    + `}else if(t.type==="store-share"){const s=Array.isArray(t.profiles)?t.profiles:[],a=[],r=new Set,i=l=>{const c=(l||"").toLowerCase();l&&!r.has(c)&&(r.add(c),a.push(l))};i(t.program),s.forEach(l=>i(l.program));const o=t.profiles==null?` ${this._t("msg.loading",{},"Loading\u2026")}`:"",n=a.length?a.map(l=>``).join(""):t.storeUnreachable?'':``;e=`

    ${this._t("modal.store_share",{},"Share to community store")}

    ${this._t("msg.store_share_intro",{},"Upload this reference cycle so others with the same appliance can use it. It is reviewed before appearing publicly.")}

    + ${t.storeUnreachable?this._htmlStoreUnreachable():""}
    - +
    -
    `}else t.type==="store-share-device"&&(e=this._htmlShareDeviceModal(t));return`
    `}_htmlShareDeviceModal(t){const e=this._shareableByProgram(),s=this._busy.has("store-share-device"),r=t.selected||new Set,n=!!t.consented,a=e.reduce((u,$)=>u+$.cycles.filter(S=>r.has(S.id)).length,0),o=e.some(u=>!u.noCycles);let i;t.loading?i=`
    \u23F3
    ${this._t("msg.loading",{},"Loading\u2026")}
    `:e.length?i=e.map(u=>{if(u.noCycles)return`
    +
    `}else t.type==="store-share-device"&&(e=this._htmlShareDeviceModal(t));return`
    `}_htmlShareDeviceModal(t){const e=this._shareableByProgram(),s=this._busy.has("store-share-device"),a=t.selected||new Set,r=!!t.consented,i=e.reduce((u,b)=>u+b.cycles.filter(v=>a.has(v.id)).length,0),o=e.some(u=>!u.noCycles);let n;t.loading?n=`
    \u23F3
    ${this._t("msg.loading",{},"Loading\u2026")}
    `:e.length?n=e.map(u=>{if(u.noCycles)return`
    ${d(u.program)} - ${this._t("msg.share_profile_no_cycles",{},"No reference cycles \u2014 mark a cycle as \u2B50 in the Cycles tab to include this profile")} + ${this._t("msg.share_profile_no_cycles",{},"No reference cycles \u2013 mark a cycle as \u2B50 in the Cycles tab to include this profile")}
    -
    `;const $=u.cycles.every(k=>r.has(k.id)),S=!$&&u.cycles.some(k=>r.has(k.id)),x=u.cycles.map(k=>{const D=k.start_time?ot(k.start_time):"",I=k.duration!=null?gt(k.duration):"";return``}).join(""),v=(this._sharePhasePrograms||[]).includes(u.program)?`